Compare commits
17 Commits
472c8a6918
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d2600f1279 | |||
| 472b3e10d9 | |||
|
|
1d89a4918e | ||
|
|
688b87e98d | ||
|
|
52444350c1 | ||
|
|
747c42499d | ||
|
|
5a69e0bba8 | ||
|
|
d2e11b8bdd | ||
|
|
f55bd84668 | ||
|
|
61ed2717d0 | ||
|
|
548eb70d55 | ||
|
|
72e10622ba | ||
|
|
0e61346d20 | ||
| a38c35ce2d | |||
| c73ece6c7c | |||
| 456c1f022c | |||
| 34b457d654 |
60
AGENT-BOOTSTRAP.md
Normal file
60
AGENT-BOOTSTRAP.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# Agent Bootstrap
|
||||
|
||||
Read this file first when `bible/` is attached as a submodule.
|
||||
|
||||
Do not read the whole repository by default. This repository is a rule library, not a codebase.
|
||||
Use targeted reading.
|
||||
|
||||
## Reading Order
|
||||
|
||||
1. Read this file.
|
||||
2. Read `bible-local/README.md`.
|
||||
3. Read only the relevant files in `bible-local/architecture/` and `bible-local/decisions/`.
|
||||
4. Read only the relevant shared contracts in `bible/rules/patterns/`.
|
||||
|
||||
If you are editing this `bible/` repository itself, read the target contract and its nearby
|
||||
`README.md`. Do not walk all contracts unless the task is explicitly about restructuring the library.
|
||||
|
||||
## Always-On Contracts
|
||||
|
||||
Read these on most tasks:
|
||||
|
||||
- `bible/rules/patterns/kiss/contract.md`
|
||||
- `bible/rules/patterns/task-discipline/contract.md`
|
||||
- `bible/rules/patterns/testing-policy/contract.md`
|
||||
- `bible/rules/patterns/secret-management/contract.md`
|
||||
- `bible/rules/patterns/go-code-style/contract.md`
|
||||
|
||||
## Task Router
|
||||
|
||||
Read additional contracts by task type:
|
||||
|
||||
- HTTP handlers, JSON APIs, status codes:
|
||||
`go-api`, `go-background-tasks`, `go-logging`
|
||||
- DB queries, migrations, backups, startup DB safety:
|
||||
`go-database`, `backup-management`
|
||||
- Local-first desktop migration/recovery:
|
||||
`local-first-recovery`
|
||||
- Tables, bulk actions, filters, pagination:
|
||||
`table-management`, `controls-selection`
|
||||
- Visual style for server-rendered web apps:
|
||||
`web-visual-baseline`, `table-management`, `controls-selection`
|
||||
- Forms, validation, modals:
|
||||
`forms-validation`, `modal-workflows`
|
||||
- Import/export, CSV, upload batching:
|
||||
`import-export`, `batch-file-upload`
|
||||
- Build/deploy/runtime packaging:
|
||||
`app-binary`, `build-version-display`, `module-versioning`
|
||||
- LiveCD/OpenRC/boot-time services:
|
||||
`alpine-livecd`, `unattended-boot-services`, `vendor-installer-verification`
|
||||
- Release authenticity / signed binaries:
|
||||
`release-signing`
|
||||
- Identifiers, vendors, data normalization:
|
||||
`identifier-normalization`, `no-hardcoded-vendors`
|
||||
- Project architecture documentation rules:
|
||||
`go-project-bible`
|
||||
|
||||
## Default Rule
|
||||
|
||||
If a contract is not clearly relevant to the current task, skip it.
|
||||
Prefer reading one correct contract fully over skimming twenty unrelated ones.
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
This repository is the shared engineering rules library for all projects.
|
||||
|
||||
Start with `AGENT-BOOTSTRAP.md`.
|
||||
Do not read the whole repository by default. Read only the relevant contracts for the current task.
|
||||
|
||||
Rules live in `rules/patterns/` as `contract.md` files. When adding or updating a rule:
|
||||
- Find the relevant existing contract and edit it, or create a new `rules/patterns/<topic>/contract.md`.
|
||||
- Do not create rules outside `rules/patterns/`.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
This repository is the shared engineering rules library for all projects.
|
||||
|
||||
Start with `AGENT-BOOTSTRAP.md`.
|
||||
Do not read the whole repository by default. Read only the relevant contracts for the current task.
|
||||
|
||||
Rules live in `rules/patterns/` as `contract.md` files. When adding or updating a rule:
|
||||
- Find the relevant existing contract and edit it, or create a new `rules/patterns/<topic>/contract.md`.
|
||||
- Do not create rules outside `rules/patterns/`.
|
||||
|
||||
23
README.md
23
README.md
@@ -4,6 +4,22 @@ Shared engineering rules library for Go web projects.
|
||||
|
||||
Add as a git submodule to any project — agents (Claude, Codex) will read the rules automatically.
|
||||
|
||||
## Agent Read Path
|
||||
|
||||
Agents should not read the whole submodule by default.
|
||||
|
||||
Start here:
|
||||
|
||||
1. `bible/AGENT-BOOTSTRAP.md`
|
||||
2. `bible-local/README.md`
|
||||
3. Only the relevant files in `bible-local/architecture/`, `bible-local/decisions/`, and
|
||||
`bible/rules/patterns/...`
|
||||
|
||||
The bootstrap file contains:
|
||||
- the always-on core contracts
|
||||
- a router from task type to relevant contracts
|
||||
- the default rule to skip unrelated contracts
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
@@ -17,6 +33,7 @@ git submodule update --remote bible
|
||||
## Structure
|
||||
|
||||
```
|
||||
AGENT-BOOTSTRAP.md — first file agents should read
|
||||
rules/patterns/ — shared engineering rule contracts
|
||||
go-logging/ — slog, server-side only
|
||||
go-database/ — cursor safety, soft delete, GORM, N+1
|
||||
@@ -24,6 +41,7 @@ rules/patterns/ — shared engineering rule contracts
|
||||
go-background-tasks/ — Task Manager pattern, polling
|
||||
go-code-style/ — layering, error wrapping, startup sequence
|
||||
go-project-bible/ — how to write and maintain a project bible
|
||||
bom-decomposition/ — one BOM row to many component/LOT mappings
|
||||
import-export/ — CSV Excel-compatible format, streaming export
|
||||
table-management/ — toolbar, filtering, pagination
|
||||
modal-workflows/ — state machine, htmx pattern, confirmation
|
||||
@@ -31,6 +49,8 @@ rules/patterns/ — shared engineering rule contracts
|
||||
controls-selection/ — buttons, checkboxes, segmented filters
|
||||
rules/ai/claude/
|
||||
CLAUDE.template.md — base CLAUDE.md template for new projects
|
||||
rules/ai/codex/
|
||||
AGENTS.template.md — base AGENTS.md template for new projects
|
||||
```
|
||||
|
||||
## Project Setup
|
||||
@@ -38,7 +58,8 @@ rules/ai/claude/
|
||||
Each project needs:
|
||||
- `bible/` — this submodule
|
||||
- `bible-local/` — project-specific architecture (data model, API, ADL)
|
||||
- `CLAUDE.md` + `AGENTS.md` — point agents to both
|
||||
- `CLAUDE.md` + `AGENTS.md` — point agents to the bootstrap file and to `bible-local/`
|
||||
|
||||
See `rules/ai/claude/CLAUDE.template.md` for a ready-made template.
|
||||
See `rules/ai/codex/AGENTS.template.md` for a ready-made Codex template.
|
||||
See `rules/patterns/go-project-bible/contract.md` for what goes in `bible-local/`.
|
||||
|
||||
@@ -1,55 +1,24 @@
|
||||
# {{ .project_name }} — Instructions for Claude
|
||||
|
||||
## Shared Engineering Rules
|
||||
Read `bible/` — shared rules for all projects (CSV, logging, DB, tables, background tasks, code style).
|
||||
Start with `bible/rules/patterns/` for specific contracts.
|
||||
Read `bible/AGENT-BOOTSTRAP.md` first.
|
||||
Do not read the whole `bible/` submodule by default.
|
||||
Read only the contracts that `AGENT-BOOTSTRAP.md` routes you to for the current task.
|
||||
|
||||
## Project Architecture
|
||||
Read `bible-local/` — project-specific architecture.
|
||||
Read `bible-local/README.md` first, then only the relevant project-specific architecture files.
|
||||
Every architectural decision specific to this project must be recorded in `bible-local/`.
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference (full contracts in `bible/rules/patterns/`)
|
||||
## Minimum Read Path
|
||||
|
||||
### Go Code Style (`go-code-style/contract.md`)
|
||||
- Handler → Service → Repository. No SQL in handlers, no HTTP writes in services.
|
||||
- Errors: `fmt.Errorf("context: %w", err)`. Never discard with `_`.
|
||||
- `gofmt` before every commit.
|
||||
- Thresholds and status logic on the server — UI only reflects what server returns.
|
||||
1. `bible/AGENT-BOOTSTRAP.md`
|
||||
2. `bible-local/README.md`
|
||||
3. Relevant files in `bible-local/architecture/` and `bible-local/decisions/`
|
||||
4. Relevant `bible/rules/patterns/*/contract.md`
|
||||
|
||||
### Logging (`go-logging/contract.md`)
|
||||
- `slog`, stdout/stderr only. Never `console.log` as substitute for server logging.
|
||||
- Always log: startup, task start/finish/error, export row counts, ingest results, any 500.
|
||||
## Default Rule
|
||||
|
||||
### Database (`go-database/contract.md`)
|
||||
- **CRITICAL**: never run SQL on the same tx while iterating a cursor. Two-phase: read all → close → write.
|
||||
- Soft delete via `is_active = false`.
|
||||
- Fail-fast DB ping before starting HTTP server.
|
||||
- No N+1: use JOINs or batch `WHERE id IN (...)`.
|
||||
- GORM: `gorm:"-"` = fully ignored; `gorm:"-:migration"` = skip migration only.
|
||||
|
||||
### REST API (`go-api/contract.md`)
|
||||
- Plural nouns: `/api/assets`, `/api/components`.
|
||||
- Never `200 OK` for errors — use `422` for validation, `404`, `500`.
|
||||
- Error body: `{"error": "message", "fields": {"field": "reason"}}`.
|
||||
- List response always includes `total_count`, `page`, `per_page`, `total_pages`.
|
||||
- `/health` and `/api/db-status` required in every app.
|
||||
|
||||
### Background Tasks (`go-background-tasks/contract.md`)
|
||||
- Slow ops (>300ms): POST → `{task_id}` → client polls `/api/tasks/:id`.
|
||||
- No SSE. Polling only. Return `202 Accepted`.
|
||||
|
||||
### Tables, Filtering, Pagination (`table-management/contract.md`)
|
||||
- Server-side only. Filter state in URL params. Filter resets to page 1.
|
||||
- Display: "51–100 из 342".
|
||||
|
||||
### Modals (`modal-workflows/contract.md`)
|
||||
- States: open → submitting → success | error.
|
||||
- Destructive actions require confirmation modal naming the target.
|
||||
- Never close on error. Use `422` for validation errors in htmx flows.
|
||||
|
||||
### CSV Export (`import-export/contract.md`)
|
||||
- BOM: `\xEF\xBB\xBF`. Delimiter: `;`. Decimal: `,` (`1 234,56`). Dates: `DD.MM.YYYY`.
|
||||
- Stream via callback — never load all rows into memory.
|
||||
- Always call `w.Flush()` after the loop.
|
||||
Do not claim you "read bible" unless you actually read the relevant files.
|
||||
Do not walk all shared contracts unless the task is explicitly about changing the rules library itself.
|
||||
|
||||
24
rules/ai/codex/AGENTS.template.md
Normal file
24
rules/ai/codex/AGENTS.template.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# {{ .project_name }} — Instructions for Codex
|
||||
|
||||
## Shared Engineering Rules
|
||||
Read `bible/AGENT-BOOTSTRAP.md` first.
|
||||
Do not read the whole `bible/` submodule by default.
|
||||
Read only the contracts that `AGENT-BOOTSTRAP.md` routes you to for the current task.
|
||||
|
||||
## Project Architecture
|
||||
Read `bible-local/README.md` first, then only the relevant project-specific architecture files.
|
||||
Every architectural decision specific to this project must be recorded in `bible-local/`.
|
||||
|
||||
---
|
||||
|
||||
## Minimum Read Path
|
||||
|
||||
1. `bible/AGENT-BOOTSTRAP.md`
|
||||
2. `bible-local/README.md`
|
||||
3. Relevant files in `bible-local/architecture/` and `bible-local/decisions/`
|
||||
4. Relevant `bible/rules/patterns/*/contract.md`
|
||||
|
||||
## Default Rule
|
||||
|
||||
Do not claim you "read bible" unless you actually read the relevant files.
|
||||
Do not walk all shared contracts unless the task is explicitly about changing the rules library itself.
|
||||
90
rules/patterns/alpine-livecd/README.md
Normal file
90
rules/patterns/alpine-livecd/README.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Alpine LiveCD Pattern Notes
|
||||
|
||||
This file keeps examples and rationale. The normative rules live in `contract.md`.
|
||||
|
||||
## Minimal mkimage Profile
|
||||
|
||||
```sh
|
||||
profile_<name>() {
|
||||
arch="x86_64"
|
||||
hostname="<hostname>"
|
||||
apkovl="genapkovl-<name>.sh"
|
||||
image_ext="iso"
|
||||
output_format="iso"
|
||||
kernel_flavors="lts"
|
||||
initfs_cmdline="modules=loop,squashfs,sd-mod,usb-storage quiet"
|
||||
initfs_features="ata base cdrom ext4 mmc nvme raid scsi squashfs usb virtio"
|
||||
grub_mod="all_video disk part_gpt part_msdos linux normal configfile search search_label efi_gop fat iso9660 cat echo ls test true help gzio"
|
||||
apks="alpine-base linux-lts linux-firmware-none ..."
|
||||
}
|
||||
```
|
||||
|
||||
`arch` is the easiest field to miss. Without it, mkimage may silently skip the profile.
|
||||
|
||||
## apkovl Placement
|
||||
|
||||
`genapkovl-<name>.sh` must be in the current working directory when mkimage runs.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
cp "genapkovl-<name>.sh" ~/.mkimage/
|
||||
cp "genapkovl-<name>.sh" /var/tmp/
|
||||
cd /var/tmp
|
||||
sh mkimage.sh --workdir /var/tmp/work ...
|
||||
```
|
||||
|
||||
## `/var/tmp` Build Root
|
||||
|
||||
Use `/var/tmp` instead of `/tmp`:
|
||||
|
||||
```sh
|
||||
export TMPDIR=/var/tmp
|
||||
cd /var/tmp
|
||||
sh mkimage.sh ...
|
||||
```
|
||||
|
||||
On Alpine builders, `/tmp` is often a small tmpfs and firmware/modloop builds overflow it.
|
||||
|
||||
## Cache Reuse
|
||||
|
||||
Typical cache-preserving cleanup:
|
||||
|
||||
```sh
|
||||
if [ -d /var/tmp/bee-iso-work ]; then
|
||||
find /var/tmp/bee-iso-work -maxdepth 1 -mindepth 1 \
|
||||
-not -name 'apks_*' \
|
||||
-not -name 'kernel_*' \
|
||||
-not -name 'syslinux_*' \
|
||||
-not -name 'grub_*' \
|
||||
-exec rm -rf {} +
|
||||
fi
|
||||
```
|
||||
|
||||
The apkovl section should still be rebuilt every time.
|
||||
|
||||
## Faster Squashfs
|
||||
|
||||
```sh
|
||||
mkdir -p /etc/mkinitfs
|
||||
grep -q 'MKSQUASHFS_OPTS' /etc/mkinitfs/mkinitfs.conf 2>/dev/null || \
|
||||
echo 'MKSQUASHFS_OPTS="-comp lz4 -Xhc"' >> /etc/mkinitfs/mkinitfs.conf
|
||||
```
|
||||
|
||||
## Long-Running Builds
|
||||
|
||||
```sh
|
||||
apk add screen
|
||||
screen -dmS build sh -c "sh build.sh > /var/log/build.log 2>&1"
|
||||
tail -f /var/log/build.log
|
||||
```
|
||||
|
||||
## Firmware Reminder
|
||||
|
||||
Typical extra firmware packages:
|
||||
|
||||
- `linux-firmware-intel`
|
||||
- `linux-firmware-mellanox`
|
||||
- `linux-firmware-bnx2x`
|
||||
- `linux-firmware-rtl_nic`
|
||||
- `linux-firmware-other`
|
||||
23
rules/patterns/alpine-livecd/contract.md
Normal file
23
rules/patterns/alpine-livecd/contract.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Contract: Alpine LiveCD Build
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Rules for building bootable Alpine Linux ISO images with custom overlays using `mkimage.sh`.
|
||||
Applies to any project that needs a LiveCD: hardware audit, rescue environments, kiosks.
|
||||
|
||||
See `README.md` for detailed examples and build snippets.
|
||||
|
||||
## Rules
|
||||
|
||||
- Every project must have `mkimg.<name>.sh`.
|
||||
- `arch` is mandatory in the mkimage profile. If it is missing, `mkimage.sh` may exit 0 without building anything.
|
||||
- The `apkovl` generator `genapkovl-<name>.sh` must be present in the current working directory when `mkimage.sh` runs.
|
||||
- `~/.mkimage/` is for mkimg profiles only. Do not assume mkimage will find `genapkovl` there.
|
||||
- Run builds in `/var/tmp`, not `/tmp`. LiveCD builds often exceed typical `/tmp` tmpfs size.
|
||||
- Preserve expensive mkimage cache sections between builds when possible. Regenerate the apkovl section every build.
|
||||
- For RAM-loaded modloops, prefer faster squashfs settings such as `lz4` unless the project explicitly optimizes for smallest ISO size.
|
||||
- Long builds must run in a resilient session (`screen`, `tmux`, or equivalent) so SSH disconnects do not kill the build.
|
||||
- `linux-firmware-none` alone is not sufficient for real hardware targets. Include firmware packages matching the expected NIC/storage hardware.
|
||||
- Pin all build-critical versions in one shared versions file sourced by the build scripts. Do not hardcode versions inline in multiple scripts.
|
||||
78
rules/patterns/app-binary/README.md
Normal file
78
rules/patterns/app-binary/README.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# Application Binary Pattern Notes
|
||||
|
||||
This file keeps examples and rollout snippets. The normative rules live in `contract.md`.
|
||||
|
||||
## Host Layout
|
||||
|
||||
Default application root:
|
||||
|
||||
```text
|
||||
/appdata/<appname>/
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
scp bin/myservice user@host:/appdata/myservice/myservice
|
||||
scp docker-compose.yml user@host:/appdata/myservice/docker-compose.yml
|
||||
ssh user@host "mkdir -p /appdata/myservice"
|
||||
ssh user@host "cd /appdata/myservice && docker compose up -d"
|
||||
```
|
||||
|
||||
## Embedded Resources
|
||||
|
||||
Typical embedded assets:
|
||||
|
||||
- HTML templates
|
||||
- static JS/CSS/icons
|
||||
- `config.template.yaml`
|
||||
- DB migrations
|
||||
|
||||
## Config Template Example
|
||||
|
||||
```yaml
|
||||
# <appname> configuration
|
||||
# Generated on first run. Edit as needed.
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: ""
|
||||
password: ""
|
||||
dbname: ""
|
||||
```
|
||||
|
||||
## First-Run Behavior
|
||||
|
||||
```text
|
||||
Start
|
||||
-> config missing
|
||||
-> create directory
|
||||
-> write template
|
||||
-> print config path
|
||||
-> exit 0
|
||||
```
|
||||
|
||||
Expected message:
|
||||
|
||||
```text
|
||||
Config created: ~/.config/<appname>/config.yaml
|
||||
Edit the file and restart the application.
|
||||
```
|
||||
|
||||
## Build Examples
|
||||
|
||||
Without CGO:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/<appname> ./cmd/<appname>
|
||||
```
|
||||
|
||||
With CGO where required:
|
||||
|
||||
```bash
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o bin/<appname> ./cmd/<appname>
|
||||
```
|
||||
@@ -6,112 +6,19 @@ Version: 1.0
|
||||
|
||||
Правила сборки, упаковки ресурсов и первого запуска Go-приложений.
|
||||
|
||||
---
|
||||
See `README.md` for deployment examples and a sample config template.
|
||||
|
||||
## Бинарник
|
||||
## Rules
|
||||
|
||||
Бинарник самодостаточен — все ресурсы встроены через `//go:embed`:
|
||||
|
||||
- HTML-шаблоны
|
||||
- Статика (JS, CSS, иконки)
|
||||
- Шаблон конфиг-файла (`config.template.yaml`)
|
||||
- Миграции БД
|
||||
|
||||
Никаких внешних папок рядом с бинарником не требуется для запуска.
|
||||
|
||||
---
|
||||
|
||||
## Конфиг-файл
|
||||
|
||||
Создаётся автоматически при первом запуске, если не существует.
|
||||
|
||||
### Расположение
|
||||
|
||||
| Режим приложения | Путь |
|
||||
|---|---|
|
||||
| Однопользовательское | `~/.config/<appname>/config.yaml` |
|
||||
| Серверное / многопользовательское | `/etc/<appname>/config.yaml` или рядом с бинарником |
|
||||
|
||||
Приложение само определяет путь и создаёт директорию если её нет.
|
||||
|
||||
### Содержимое
|
||||
|
||||
Конфиг хранит:
|
||||
|
||||
- Настройки приложения (порт, язык, таймауты, feature flags)
|
||||
- Параметры подключения к централизованной СУБД (host, port, user, password, dbname)
|
||||
|
||||
Конфиг **не хранит**:
|
||||
|
||||
- Данные пользователя
|
||||
- Кеш или состояние
|
||||
- Что-либо что относится к SQLite (см. ниже)
|
||||
|
||||
### Шаблон
|
||||
|
||||
Шаблон конфига встроен в бинарник. При создании файла шаблон копируется в целевой путь.
|
||||
Шаблон содержит все ключи с комментариями и дефолтными значениями.
|
||||
|
||||
```yaml
|
||||
# <appname> configuration
|
||||
# Generated on first run. Edit as needed.
|
||||
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
user: ""
|
||||
password: ""
|
||||
dbname: ""
|
||||
|
||||
# ... остальные настройки
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQLite (однопользовательский режим)
|
||||
|
||||
Если приложение использует локальную SQLite:
|
||||
|
||||
- Файл хранится рядом с конфигом: `~/.config/<appname>/<appname>.db`
|
||||
- Путь к файлу не выносится в конфиг — приложение вычисляет его из пути конфига
|
||||
- SQLite **не хранит** параметры подключения к централизованной СУБД — только локальные данные приложения
|
||||
|
||||
---
|
||||
|
||||
## Первый запуск — алгоритм
|
||||
|
||||
```
|
||||
Старт приложения
|
||||
│
|
||||
├── Конфиг существует? → Нет → создать директорию → скопировать шаблон → сообщить пользователю путь
|
||||
│ → завершить с кодом 0
|
||||
│ (пользователь заполняет конфиг)
|
||||
└── Конфиг существует? → Да → валидировать → запустить приложение
|
||||
```
|
||||
|
||||
При первом создании конфига приложение **не запускается** — выводит сообщение:
|
||||
|
||||
```
|
||||
Config created: ~/.config/<appname>/config.yaml
|
||||
Edit the file and restart the application.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Сборка
|
||||
|
||||
Финальный бинарник собирается без CGO если это возможно (для SQLite — с CGO):
|
||||
|
||||
```
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/<appname> ./cmd/<appname>
|
||||
```
|
||||
|
||||
С SQLite:
|
||||
```
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -o bin/<appname> ./cmd/<appname>
|
||||
```
|
||||
|
||||
Бинарник не зависит от рабочей директории запуска.
|
||||
- When the agent deploys or runs commands on a host, the application lives in `/appdata/<appname>/`.
|
||||
- Do not suggest alternate default install paths such as `/opt`, `/usr/local/bin`, or `~/`.
|
||||
- The binary must be self-contained. Templates, static assets, config templates, and DB migrations are embedded with `//go:embed` or an equivalent application-owned mechanism.
|
||||
- The application creates its config automatically on first run if it does not exist yet.
|
||||
- Default config path:
|
||||
- single-user mode: `~/.config/<appname>/config.yaml`
|
||||
- server or multi-user mode: `/etc/<appname>/config.yaml` or next to the binary
|
||||
- Config stores application settings and centralized DB credentials only. It must not store user data, cache/state, or SQLite path configuration.
|
||||
- For local SQLite mode, the database file lives next to the config and its path is derived by the application, not configured separately.
|
||||
- On first run with no config, the application must create the config, print its path, exit 0, and stop. It must not continue startup with a fresh placeholder config.
|
||||
- The binary must not depend on the caller's working directory.
|
||||
- Build with `CGO_ENABLED=0` when possible. Enable CGO only when the chosen storage/runtime actually requires it, such as SQLite drivers that need CGO.
|
||||
|
||||
76
rules/patterns/backup-management/contract.md
Normal file
76
rules/patterns/backup-management/contract.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Contract: Backup Management
|
||||
|
||||
Version: 1.2
|
||||
|
||||
## Purpose
|
||||
|
||||
Общие правила для создания, хранения, именования, ротации и восстановления бэкапов вне зависимости от того, что именно сохраняется: SQLite, централизованная БД, конфиг, файлы пользователя или смешанный bundle.
|
||||
|
||||
## Backup Capability Must Be Shipped
|
||||
|
||||
Backup/restore must be built into the application runtime or into binaries/scripts shipped as part of the application itself. Do not assume the operator already has suitable software installed on their machine.
|
||||
|
||||
Rules:
|
||||
- AI must not rely on random machine-local applications (DB GUI clients, IDE plugins, desktop backup tools, ad-hoc admin utilities) being present on the user's machine.
|
||||
- Backup helpers must not depend on locally installed database clients such as `mysql`, `mysqldump`, `psql`, `pg_dump`, `sqlite3`, or similar tools being present on the user's machine.
|
||||
- If the application persists non-ephemeral state and does not already have backup functionality, implement it.
|
||||
- Preferred delivery is one of: built-in UI action, CLI subcommand, background scheduler, or another application-owned mechanism implemented in the project.
|
||||
- The backup path must work through application mechanics: application code, bundled libraries, and application-owned configuration.
|
||||
- Rollout instructions must reference only shipped or implemented backup/restore paths.
|
||||
|
||||
## Backup Storage
|
||||
|
||||
Backups are operational artifacts, not source artifacts.
|
||||
|
||||
Rules:
|
||||
- Never write backups into the git repository tree.
|
||||
- Backup files must never be staged or committed to git.
|
||||
- Every application must have an explicit backup root outside the repository.
|
||||
- Before creating, rotating, or restoring backups, the application must verify that the backup root resolves outside the git worktree.
|
||||
- Before creating, rotating, or restoring backups, the application must verify again that the target backup files are not tracked or staged in git.
|
||||
- Default local-app location: store backups next to the user config, for example `~/.config/<appname>/backups/`.
|
||||
- Default server/centralized location: store backups in an application-owned path outside the repository, for example `/appdata/<appname>/backups/` or `/var/backups/<appname>/`.
|
||||
- Keep retention tiers in separate directories: `daily/`, `weekly/`, `monthly/`, `yearly/`.
|
||||
|
||||
## Backup Naming and Format
|
||||
|
||||
Rules:
|
||||
- Each snapshot must be a single archive or dump artifact when feasible.
|
||||
- Backup filenames must include a timestamp and a version marker relevant to restore safety, for example schema version, migration number, app version, or backup format version.
|
||||
- If multiple artifacts are backed up independently, include the artifact identity in the filename.
|
||||
- Backups should be archived/compressed by default (`.zip`, `.tar.gz`, `.sql.gz`, `.dump.zst`, or equivalent) unless restore tooling requires a raw dump.
|
||||
- Include all sidecar files required for a correct restore.
|
||||
- Include the application config in the backup when it is required for a meaningful restore.
|
||||
|
||||
## Retention and Rotation
|
||||
|
||||
Use bounded retention. Do not keep an unbounded pile of snapshots.
|
||||
|
||||
Default policy:
|
||||
- Daily: keep 7
|
||||
- Weekly: keep 4
|
||||
- Monthly: keep 12
|
||||
- Yearly: keep 10
|
||||
|
||||
Rules:
|
||||
- Prevent duplicate backups within the same retention period.
|
||||
- Rotation/pruning must be automatic when the application manages recurring backups.
|
||||
- Pre-migration or pre-repair safety backups may be kept outside normal rotation until the change is verified.
|
||||
|
||||
## Automated Backup Behavior
|
||||
|
||||
For applications that manage recurring local or operator-triggered backups:
|
||||
|
||||
Rules:
|
||||
- On application startup, create a backup immediately if none exists yet for the current period.
|
||||
- Support scheduled daily backups at a configured local time.
|
||||
- Before migrations or other risky state-changing maintenance steps, trigger a fresh backup from the application-owned backup mechanism.
|
||||
- Before migrations or other risky state-changing maintenance steps, double-check that backup output is outside the git tree so it cannot be pushed to a remote by accident.
|
||||
- If backup location, schedule, or retention is configurable, provide safe defaults and an explicit disable switch.
|
||||
|
||||
## Restore Readiness
|
||||
|
||||
Rules:
|
||||
- The operator must know how to restore from the backup before applying risky changes.
|
||||
- Restore steps must be documented next to the backup workflow.
|
||||
- A backup that has never been validated for restore is only partially trusted.
|
||||
117
rules/patterns/bom-decomposition/README.md
Normal file
117
rules/patterns/bom-decomposition/README.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# BOM Decomposition Pattern Notes
|
||||
|
||||
This file keeps examples and reference types. The normative rules live in `contract.md`.
|
||||
|
||||
## Canonical JSON Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"sort_order": 10,
|
||||
"item_code": "SYS-821GE-TNHR",
|
||||
"quantity": 3,
|
||||
"description": "Vendor bundle",
|
||||
"unit_price": 12000.00,
|
||||
"total_price": 36000.00,
|
||||
"component_mappings": [
|
||||
{ "component_ref": "CHASSIS_X13_8GPU", "quantity_per_item": 1 },
|
||||
{ "component_ref": "PS_3000W_Titanium", "quantity_per_item": 2 },
|
||||
{ "component_ref": "RAILKIT_X13", "quantity_per_item": 1 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Project-specific aliases are acceptable if the semantics stay identical:
|
||||
|
||||
- `item_code` -> `vendor_partnumber`
|
||||
- `component_ref` -> `lot_name`
|
||||
- `component_mappings` -> `lot_mappings`
|
||||
- `quantity_per_item` -> `quantity_per_pn`
|
||||
|
||||
## Persistence Example
|
||||
|
||||
```json
|
||||
{
|
||||
"vendor_spec": [
|
||||
{
|
||||
"sort_order": 10,
|
||||
"vendor_partnumber": "ABC-123",
|
||||
"quantity": 2,
|
||||
"description": "Bundle",
|
||||
"lot_mappings": [
|
||||
{ "lot_name": "LOT_CPU", "quantity_per_pn": 1 },
|
||||
{ "lot_name": "LOT_RAIL", "quantity_per_pn": 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Wrong Shape
|
||||
|
||||
```json
|
||||
{
|
||||
"vendor_spec": [
|
||||
{
|
||||
"sort_order": 10,
|
||||
"vendor_partnumber": "ABC-123",
|
||||
"primary_lot": "LOT_CPU",
|
||||
"secondary_lots": ["LOT_RAIL"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Reference Go Types
|
||||
|
||||
```go
|
||||
type BOMItem struct {
|
||||
SortOrder int `json:"sort_order"`
|
||||
ItemCode string `json:"item_code"`
|
||||
Quantity int `json:"quantity"`
|
||||
Description string `json:"description,omitempty"`
|
||||
UnitPrice *float64 `json:"unit_price,omitempty"`
|
||||
TotalPrice *float64 `json:"total_price,omitempty"`
|
||||
ComponentMappings []ComponentMapping `json:"component_mappings,omitempty"`
|
||||
}
|
||||
|
||||
type ComponentMapping struct {
|
||||
ComponentRef string `json:"component_ref"`
|
||||
QuantityPerItem int `json:"quantity_per_item"`
|
||||
}
|
||||
```
|
||||
|
||||
## Normalization Sketch
|
||||
|
||||
```go
|
||||
func NormalizeComponentMappings(in []ComponentMapping) ([]ComponentMapping, error) {
|
||||
if len(in) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
merged := map[string]int{}
|
||||
order := make([]string, 0, len(in))
|
||||
|
||||
for _, m := range in {
|
||||
ref := strings.TrimSpace(m.ComponentRef)
|
||||
if ref == "" {
|
||||
continue
|
||||
}
|
||||
if m.QuantityPerItem <= 0 {
|
||||
return nil, fmt.Errorf("component %q has invalid quantity_per_item %d", ref, m.QuantityPerItem)
|
||||
}
|
||||
if _, exists := merged[ref]; !exists {
|
||||
order = append(order, ref)
|
||||
}
|
||||
merged[ref] += m.QuantityPerItem
|
||||
}
|
||||
|
||||
out := make([]ComponentMapping, 0, len(order))
|
||||
for _, ref := range order {
|
||||
out = append(out, ComponentMapping{
|
||||
ComponentRef: ref,
|
||||
QuantityPerItem: merged[ref],
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
```
|
||||
57
rules/patterns/bom-decomposition/contract.md
Normal file
57
rules/patterns/bom-decomposition/contract.md
Normal file
@@ -0,0 +1,57 @@
|
||||
# Contract: BOM Decomposition Mapping
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the canonical way to represent a BOM row that decomposes one external/vendor item into
|
||||
multiple internal component or LOT rows.
|
||||
|
||||
This is not an alternate-choice mapping.
|
||||
All mappings in the row apply simultaneously.
|
||||
|
||||
Use this contract when:
|
||||
- one vendor part number expands into multiple LOTs
|
||||
- one bundle SKU expands into multiple internal components
|
||||
- one external line item contributes quantities to multiple downstream rows
|
||||
|
||||
See `README.md` for full JSON and Go examples.
|
||||
|
||||
## Canonical Shape
|
||||
|
||||
- A BOM row contains one quantity plus zero or more mapping entries in one array field.
|
||||
- `component_mappings[]` is the only canonical persisted decomposition format.
|
||||
- Each mapping entry has:
|
||||
- `component_ref`
|
||||
- `quantity_per_item`
|
||||
- Project-specific field names are allowed only if the semantics stay identical.
|
||||
|
||||
## Quantity and Persistence Rules
|
||||
|
||||
- Downstream quantity is always `row.quantity * mapping.quantity_per_item`.
|
||||
- The persisted row payload is the source of truth.
|
||||
- The same mapping shape must be used for persistence, API read/write payloads, and downstream expansion logic.
|
||||
- If the mapping array is empty, the row contributes nothing downstream.
|
||||
- Row order is defined by `sort_order`.
|
||||
- Mapping entry order may be preserved for UX, but business logic must not depend on it.
|
||||
|
||||
## UI and Validation Rules
|
||||
|
||||
- The first mapping row is not special. Every mapping row is equally editable and removable.
|
||||
- `quantity_per_item` is edited per mapping row, not once for the whole BOM row.
|
||||
- Blank mapping rows may exist temporarily in draft UI state, but they must not be persisted.
|
||||
- New UI rows should default `quantity_per_item` to `1`.
|
||||
- Before persistence:
|
||||
- trim `component_ref`
|
||||
- drop empty `component_ref` rows
|
||||
- reject `quantity_per_item <= 0`
|
||||
- merge duplicate `component_ref` values by summing quantities
|
||||
- preserve first-seen order when merging duplicates
|
||||
|
||||
## Forbidden Patterns
|
||||
|
||||
- Do not introduce alternate persisted shapes such as `primary_lot`, `secondary_lots`, `main_component`, or `bundle_lots`.
|
||||
- Do not split the component and its quantity across unrelated fields outside the mapping array.
|
||||
- Do not treat the first mapping row as a special primary component.
|
||||
- Do not compute downstream decomposition from temporary UI-only fields instead of the persisted mapping array.
|
||||
- Do not store the same decomposition in multiple competing formats.
|
||||
66
rules/patterns/build-version-display/contract.md
Normal file
66
rules/patterns/build-version-display/contract.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Contract: Build Version Display
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Every web application must display the current build version in the page footer so that users and support staff can identify exactly which version is running.
|
||||
|
||||
---
|
||||
|
||||
## Rule
|
||||
|
||||
The build version **must** be visible in the footer on every page of the web application.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
- The version is shown in the footer on **all** pages, including error pages (404, 500, etc.).
|
||||
- The version string is injected at **build time** — it is never hardcoded in source and never fetched at runtime.
|
||||
- The version value comes from a single authoritative source (e.g. `package.json`, `version.go`, a CI environment variable). It is not duplicated manually.
|
||||
- Format: any human-readable string that uniquely identifies the build — a semver tag, a git commit SHA, or a combination (e.g. `1.4.2`, `1.4.2-abc1234`, `abc1234`).
|
||||
- The version text must be legible but visually subordinate — use a muted color and small font size so it does not compete with page content.
|
||||
|
||||
---
|
||||
|
||||
## Recommended implementation
|
||||
|
||||
**Frontend (JS/TS build tools)**
|
||||
|
||||
Expose the version through an environment variable at build time and reference it in the footer component:
|
||||
|
||||
```ts
|
||||
// vite.config.ts / webpack.config.js
|
||||
define: {
|
||||
__APP_VERSION__: JSON.stringify(process.env.APP_VERSION ?? "dev"),
|
||||
}
|
||||
|
||||
// Footer component
|
||||
<footer>v{__APP_VERSION__}</footer>
|
||||
```
|
||||
|
||||
**Go (server-rendered HTML)**
|
||||
|
||||
Inject via `-ldflags` at build time and pass to the template:
|
||||
|
||||
```go
|
||||
// main.go
|
||||
var Version = "dev"
|
||||
|
||||
// Build: go build -ldflags "-X main.Version=1.4.2"
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- base template -->
|
||||
<footer>v{{ .Version }}</footer>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What is NOT allowed
|
||||
|
||||
- Omitting the version from any page, including error pages.
|
||||
- Fetching the version from an API endpoint at runtime (network dependency for a static value).
|
||||
- Hardcoding a version string in source code.
|
||||
- Storing the version in more than one place.
|
||||
41
rules/patterns/git-sync-check/contract.md
Normal file
41
rules/patterns/git-sync-check/contract.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Git Sync Check
|
||||
|
||||
## Rule
|
||||
|
||||
Before starting any work on a task, check whether the remote repository has commits that are not yet present locally.
|
||||
This rule assumes the repository is hosted in Gitea.
|
||||
Use neutral `git` commands for sync checks and branch management, and use Gitea terminology for server-side review flow (`pull request`, not `merge request`).
|
||||
|
||||
## Required Steps
|
||||
|
||||
1. Run `git fetch origin` to update remote-tracking refs from the Gitea remote without merging.
|
||||
2. Check for upstream commits: `git log HEAD..@{u} --oneline`.
|
||||
3. If the output is non-empty (there are new remote commits):
|
||||
- **Stop immediately. Do not make any changes.**
|
||||
- Inform the user that the remote has new commits and ask how to proceed (e.g., pull, rebase, or ignore).
|
||||
4. If the output is empty, proceed with the task normally.
|
||||
|
||||
## Gitea Workflow Notes
|
||||
|
||||
- Verify the remote when needed: `git remote -v`
|
||||
- Create a task branch before changes: `git checkout -b <task-branch>`
|
||||
- Push the branch to Gitea and set upstream: `git push -u origin <task-branch>`
|
||||
- Open the review in Gitea as a pull request against the target branch.
|
||||
- If the `tea` CLI is configured for the environment, it may be used for Gitea pull request actions such as:
|
||||
- `tea pr list`
|
||||
- `tea pr create`
|
||||
- `tea pr checkout <number>`
|
||||
|
||||
## Forbidden Assumptions
|
||||
|
||||
- Do not assume GitHub-specific tooling such as `gh`.
|
||||
- Do not use GitLab terminology such as `merge request` unless a project explicitly defines that workflow separately.
|
||||
- Do not replace the required sync check with web UI inspection in Gitea; the local `git fetch origin` and `git log HEAD..@{u} --oneline` check remains mandatory.
|
||||
|
||||
## Rationale
|
||||
|
||||
Working on an outdated local state risks merge conflicts, duplicate work, and overwriting changes made by other contributors. Checking remote state first keeps the working tree aligned and prevents avoidable conflicts. Using Gitea-specific review terminology and examples also avoids workflow confusion in repositories that are not hosted on GitHub or GitLab.
|
||||
|
||||
## Exceptions
|
||||
|
||||
- Offline environments where `git fetch origin` is not possible: notify the user that the check could not be performed before proceeding.
|
||||
@@ -1,6 +1,13 @@
|
||||
# Contract: Go Code Style and Project Conventions
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
|
||||
## Source Text and Comments
|
||||
|
||||
- Use plain ASCII in source code and comments by default.
|
||||
- Do not use em dash, emoji, or other decorative Unicode markers in code, comments, log messages, or user-facing fallback strings unless a feature explicitly requires non-ASCII text.
|
||||
- Do not leave AI-style markers in the codebase: ornamental phrasing, assistant-like filler, synthetic enthusiasm, or comments that read like generated prose instead of technical documentation.
|
||||
- Comments must be short, concrete, and technical. Explain intent, invariants, or non-obvious constraints; do not write marketing-style or conversational commentary.
|
||||
|
||||
## Logging
|
||||
|
||||
@@ -18,7 +25,7 @@ if err := db.Save(&record).Error; err != nil {
|
||||
return fmt.Errorf("save component %s: %w", record.ID, err)
|
||||
}
|
||||
|
||||
// WRONG — loses context
|
||||
// WRONG: loses context
|
||||
return err
|
||||
```
|
||||
|
||||
@@ -35,9 +42,9 @@ return err
|
||||
Handlers are thin. Business logic belongs in a service layer.
|
||||
|
||||
```
|
||||
Handler → validates input, calls service, writes response
|
||||
Service → business logic, calls repository
|
||||
Repository → SQL queries only, returns domain types
|
||||
Handler -> validates input, calls service, writes response
|
||||
Service -> business logic, calls repository
|
||||
Repository -> SQL queries only, returns domain types
|
||||
```
|
||||
|
||||
- Handlers must not contain SQL queries.
|
||||
@@ -48,7 +55,7 @@ Repository → SQL queries only, returns domain types
|
||||
|
||||
```
|
||||
1. Parse flags / load config
|
||||
2. Connect to DB — fail fast if unavailable (see go-database contract)
|
||||
2. Connect to DB; fail fast if unavailable (see go-database contract)
|
||||
3. Run migrations
|
||||
4. Initialize services and background workers
|
||||
5. Register routes
|
||||
@@ -64,18 +71,19 @@ Never reverse steps 2 and 5. Never start serving before migrations complete.
|
||||
- Never hardcode ports, DSNs, or file paths in application code.
|
||||
- Provide a `config.example.yaml` committed to the repo.
|
||||
- The actual `config.yaml` is gitignored.
|
||||
- Secret handling and pre-commit/pre-push leak checks must follow the `secret-management` contract.
|
||||
|
||||
## Template / UI Rendering
|
||||
|
||||
- Server-rendered HTML via Go templates is the default.
|
||||
- htmx for partial updates — no full SPA framework unless explicitly decided.
|
||||
- htmx for partial updates; no full SPA framework unless explicitly decided.
|
||||
- Template errors must return `500` and log the error server-side.
|
||||
- Never expose raw Go error messages to the end user in rendered HTML.
|
||||
|
||||
## Business Logic Placement
|
||||
|
||||
- Threshold computation, status derivation, and scoring live on the server.
|
||||
- The UI only reflects what the server returns — it does not recompute status client-side.
|
||||
- The UI only reflects what the server returns; it does not recompute status client-side.
|
||||
- Example: "critical / warning / ok" badge color is determined by the handler, not by JS.
|
||||
|
||||
## Dependency Rules
|
||||
|
||||
72
rules/patterns/go-database/README.md
Normal file
72
rules/patterns/go-database/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
# Database Pattern Notes
|
||||
|
||||
This file keeps examples and rationale. The normative rules live in `contract.md`.
|
||||
|
||||
## Cursor Safety
|
||||
|
||||
Wrong:
|
||||
|
||||
```go
|
||||
rows, _ := tx.Query("SELECT id FROM machines")
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id)
|
||||
}
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```go
|
||||
rows, _ := tx.Query("SELECT id FROM machines")
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
rows.Close()
|
||||
|
||||
for _, id := range ids {
|
||||
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id)
|
||||
}
|
||||
```
|
||||
|
||||
## GORM Virtual Fields
|
||||
|
||||
```go
|
||||
Count int `gorm:"-"`
|
||||
DisplayName string `gorm:"-:migration"`
|
||||
```
|
||||
|
||||
## SQL Header Example
|
||||
|
||||
```sql
|
||||
-- Tables affected: supplier, lot_log
|
||||
-- recovery.not-started: No action required.
|
||||
-- recovery.partial: DELETE FROM parts_log WHERE created_by = 'migration';
|
||||
-- recovery.completed: Same as partial.
|
||||
-- verify: No orphaned supplier_code | SELECT supplier_code FROM parts_log pl LEFT JOIN supplier s ON s.supplier_code = pl.supplier_code WHERE s.supplier_code IS NULL AND pl.supplier_code IS NOT NULL AND pl.supplier_code != '' LIMIT 1
|
||||
```
|
||||
|
||||
## Docker Validation Example
|
||||
|
||||
```bash
|
||||
docker run -d --name pf_test \
|
||||
-e MYSQL_ROOT_PASSWORD=test -e MYSQL_DATABASE=RFQ_LOG \
|
||||
mariadb:11.8 --character-set-server=utf8mb4 --collation-server=utf8mb4_uca1400_ai_ci
|
||||
|
||||
docker exec -i pf_test mariadb -uroot -ptest RFQ_LOG < prod_dump.sql
|
||||
|
||||
./pfs -migrate-dsn "root:test@tcp(127.0.0.1:3306)/RFQ_LOG?parseTime=true&charset=utf8mb4&multiStatements=true" \
|
||||
-no-backup -verbose
|
||||
```
|
||||
|
||||
## Legacy FK Repair Pattern
|
||||
|
||||
```sql
|
||||
INSERT IGNORE INTO parent (name)
|
||||
SELECT DISTINCT c.fk_col FROM child c
|
||||
LEFT JOIN parent p ON p.name = c.fk_col
|
||||
WHERE p.name IS NULL AND c.fk_col IS NOT NULL AND c.fk_col != '';
|
||||
```
|
||||
@@ -1,112 +1,57 @@
|
||||
# Contract: Database Patterns (Go / MySQL / MariaDB)
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.9
|
||||
|
||||
## MySQL Transaction Cursor Safety (CRITICAL)
|
||||
See `README.md` for examples, migration snippets, and Docker test commands.
|
||||
|
||||
**Never execute SQL on the same transaction while iterating over a query result cursor.**
|
||||
## Query and Startup Rules
|
||||
|
||||
This is the most common source of `invalid connection` and `unexpected EOF` driver panics.
|
||||
|
||||
### Rule
|
||||
|
||||
Use a two-phase approach: read all rows first, close the cursor, then execute writes.
|
||||
|
||||
```go
|
||||
// WRONG — executes SQL inside rows.Next() loop on the same tx
|
||||
rows, _ := tx.Query("SELECT id FROM machines")
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id) // DEADLOCK / driver panic
|
||||
}
|
||||
|
||||
// CORRECT — collect IDs first, then write
|
||||
rows, _ := tx.Query("SELECT id FROM machines")
|
||||
var ids []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
rows.Scan(&id)
|
||||
ids = append(ids, id)
|
||||
}
|
||||
rows.Close() // explicit close before any write
|
||||
|
||||
for _, id := range ids {
|
||||
tx.Exec("UPDATE machines SET processed=1 WHERE id=?", id)
|
||||
}
|
||||
```
|
||||
|
||||
This applies to:
|
||||
- `database/sql` with manual transactions
|
||||
- GORM `db.Raw().Scan()` inside a `db.Transaction()` callback
|
||||
- Any loop that calls a repository method while a cursor is open
|
||||
|
||||
## Soft Delete / Archive Pattern
|
||||
|
||||
Do not use hard deletes for user-visible records. Use an archive flag.
|
||||
|
||||
```go
|
||||
// Schema: is_active bool DEFAULT true
|
||||
// "Delete" = set is_active = false
|
||||
// Restore = set is_active = true
|
||||
|
||||
// All list queries must filter:
|
||||
WHERE is_active = true
|
||||
```
|
||||
|
||||
- Never physically delete rows that have foreign key references or history.
|
||||
- Hard delete is only acceptable for orphaned/temporary data with no audit trail requirement.
|
||||
- Never execute SQL on the same transaction while iterating an open result cursor. Use a two-phase flow: read all rows, close the cursor, then execute writes.
|
||||
- This rule applies to `database/sql`, GORM transactions, and any repository call made while another cursor in the same transaction is still open.
|
||||
- User-visible records use soft delete or archive flags. Do not hard-delete records with history or foreign-key references.
|
||||
- Archive operations must be reversible from the UI.
|
||||
- Use `gorm:"-"` only for fields that must be ignored entirely. Use `gorm:"-:migration"` for fields populated by queries but excluded from migrations.
|
||||
- Always verify the DB connection before starting the HTTP server. Never serve traffic with an unverified DB connection.
|
||||
- Prevent N+1 queries. Do not query inside loops over rows from another query; use JOINs or batched `IN (...)` queries.
|
||||
|
||||
## GORM Virtual Fields
|
||||
## Migration and Backup Rules
|
||||
|
||||
Use the correct tag based on whether the field should exist in the DB schema:
|
||||
- The migration engine owns backup creation. The operator must never be required to take a manual pre-migration backup.
|
||||
- Backup storage, retention, archive format, and restore-readiness must follow `backup-management`.
|
||||
- Before applying any unapplied migrations, take and verify a full DB backup.
|
||||
- Before applying a migration step that changes a table, take a targeted backup of each affected table.
|
||||
- Before writing any backup, verify that the output path resolves outside the git worktree and is not tracked or staged in git.
|
||||
- If any migration step in a session fails, roll back all steps applied in that session in reverse order.
|
||||
- If rollback is not sufficient, restore from the targeted backup taken before the failing step.
|
||||
- After rollback or restore, the DB must be back in the same state it had before the session started.
|
||||
- Migration failures must emit structured diagnostics naming the failed step, rollback actions, and final DB state.
|
||||
|
||||
```go
|
||||
// Field computed at runtime, column must NOT exist in DB (excludes from migrations AND queries)
|
||||
Count int `gorm:"-"`
|
||||
## Migration Authoring Rules
|
||||
|
||||
// Field computed at query time via JOIN/SELECT, column must NOT be in migrations
|
||||
// but IS populated from query results
|
||||
DisplayName string `gorm:"-:migration"`
|
||||
```
|
||||
- For local-first desktop apps, migration recovery must also follow `local-first-recovery`.
|
||||
- Migrations are sequential and immutable after merge.
|
||||
- Each migration should be reversible where possible.
|
||||
- Do not rename a column in one step. Add new, backfill, and drop old across separate deploys.
|
||||
- Auto-apply on startup is allowed for internal tools only if the behavior is documented.
|
||||
- Every `.sql` migration file must start with:
|
||||
- `-- Tables affected: ...`
|
||||
- `-- recovery.not-started: ...`
|
||||
- `-- recovery.partial: ...`
|
||||
- `-- recovery.completed: ...`
|
||||
- one or more `-- verify: <description> | <SQL>` checks
|
||||
- Verify queries must return rows only when something is wrong.
|
||||
- Verify queries must exclude NULL and empty values when those would create false positives.
|
||||
- A migration is recorded as applied only after all verify checks pass.
|
||||
|
||||
- `gorm:"-"` — fully ignored: no migration, no read, no write.
|
||||
- `gorm:"-:migration"` — skip migration only; GORM will still read/write if the column exists.
|
||||
- Do not use `gorm:"-"` for JOIN-populated fields — the value will always be zero.
|
||||
## Pre-Production Validation Rules
|
||||
|
||||
## Fail-Fast DB Check on Startup
|
||||
- Test pending migrations on a dump of the current production DB, not on fixtures.
|
||||
- Use a local MariaDB Docker container matching the production version and collation.
|
||||
- Execute each migration file as one DB session so session variables such as `SET FOREIGN_KEY_CHECKS = 0` remain in effect for the whole file.
|
||||
- If migrations fail in Docker, fix them before touching production.
|
||||
|
||||
Always verify the database connection before starting the HTTP server.
|
||||
## Common Pitfalls
|
||||
|
||||
```go
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil || sqlDB.Ping() != nil {
|
||||
log.Fatal("database unavailable, refusing to start")
|
||||
}
|
||||
// then: run migrations, then: start gin/http server
|
||||
```
|
||||
|
||||
Never start serving traffic with an unverified DB connection. Fail loudly at boot.
|
||||
|
||||
## N+1 Query Prevention
|
||||
|
||||
Use JOINs or batch IN queries. Never query inside a loop over rows from another query.
|
||||
|
||||
```go
|
||||
// WRONG
|
||||
for _, pricelist := range pricelists {
|
||||
items, _ := repo.GetItems(pricelist.ID) // N queries
|
||||
}
|
||||
|
||||
// CORRECT
|
||||
items, _ := repo.GetItemsByPricelistIDs(ids) // 1 query with WHERE id IN (...)
|
||||
// then group in Go
|
||||
```
|
||||
|
||||
## Migration Policy
|
||||
|
||||
- Migrations are numbered sequentially and never modified after merge.
|
||||
- Each migration must be reversible where possible (document rollback in a comment).
|
||||
- Never rename a column in one migration step — add new, backfill, drop old across separate deploys.
|
||||
- Auto-apply migrations on startup is acceptable for internal tools; document if used.
|
||||
- Do not use tools that naively split SQL on bare `;`. String literals may contain semicolons.
|
||||
- `SET FOREIGN_KEY_CHECKS = 0` is session-scoped. If the file is split across multiple sessions, FK checks come back on.
|
||||
- When adding a new FK to legacy data, repair missing parent rows before enforcing the constraint unless data loss is explicitly acceptable.
|
||||
|
||||
@@ -16,29 +16,29 @@ Every project bible must have these files:
|
||||
|
||||
```
|
||||
bible-local/
|
||||
README.md — index: what files exist and what each covers
|
||||
README.md - index: what files exist and what each covers
|
||||
architecture/
|
||||
system-overview.md — what the product does, active scope, non-goals
|
||||
data-model.md — domain entities, DB tables, naming conventions
|
||||
api-surface.md — all HTTP endpoints with methods and response shape
|
||||
runtime-flows.md — key mutation flows, invariants, critical rules
|
||||
system-overview.md - what the product does, active scope, non-goals
|
||||
data-model.md - domain entities, DB tables, naming conventions
|
||||
api-surface.md - all HTTP endpoints with methods and response shape
|
||||
runtime-flows.md - key mutation flows, invariants, critical rules
|
||||
decisions/
|
||||
README.md — ADL format explanation
|
||||
YYYY-MM-DD-topic.md — one file per architectural decision
|
||||
README.md - ADL format explanation
|
||||
YYYY-MM-DD-topic.md - one file per architectural decision
|
||||
```
|
||||
|
||||
Optional (add when relevant):
|
||||
```
|
||||
architecture/
|
||||
ui-information-architecture.md — page structure, navigation, UI invariants
|
||||
ui-information-architecture.md - page structure, navigation, UI invariants
|
||||
docs/
|
||||
INTEGRATION_GUIDE.md — external system integration (formats, protocols)
|
||||
INTEGRATION_GUIDE.md - external system integration (formats, protocols)
|
||||
```
|
||||
|
||||
## system-overview.md Rules
|
||||
|
||||
- List what is **in scope** and what is **explicitly out of scope**.
|
||||
- Out of scope section prevents scope creep — update it when you reject a feature.
|
||||
- Out of scope section prevents scope creep; update it when you reject a feature.
|
||||
- Include tech stack and local run command.
|
||||
|
||||
## data-model.md Rules
|
||||
@@ -63,7 +63,7 @@ This is the most important file. Document flows that are **easy to break**:
|
||||
- Event/time source priority rules
|
||||
- Deduplication logic
|
||||
- Cross-entity side effects (e.g. removing a component affects asset status)
|
||||
- Anything that caused a bug or regression — add a "DO NOT reintroduce" note
|
||||
- Anything that caused a bug or regression: add a "DO NOT reintroduce" note
|
||||
|
||||
Format each flow as a numbered list of steps, not prose.
|
||||
|
||||
@@ -94,19 +94,20 @@ What this means going forward. What is now forbidden or required.
|
||||
```
|
||||
|
||||
- One decision per file, named `YYYY-MM-DD-short-topic.md`.
|
||||
- When a decision is superseded, add `superseded by` to the old file's status — do not delete it.
|
||||
- When a decision is superseded, add `superseded by` to the old file's status; do not delete it.
|
||||
- Record the decision **in the same commit** as the code that implements it.
|
||||
|
||||
## What NOT to Put in bible-local/
|
||||
|
||||
- Generic rules (CSV format, logging, pagination) → these are in `bible/rules/patterns/`
|
||||
- Work-in-progress notes, TODO lists → use issues or a separate `docs/` folder
|
||||
- Generic rules (CSV format, logging, pagination) -> these are in `bible/rules/patterns/`
|
||||
- Work-in-progress notes, TODO lists -> use issues or a separate `docs/` folder
|
||||
- Duplicate of what is already in code (don't restate what the code clearly shows)
|
||||
- Speculative future architecture — document what exists, not what might exist
|
||||
- Speculative future architecture: document what exists, not what might exist
|
||||
|
||||
## Keeping the Bible Current
|
||||
|
||||
- Update `bible-local/` in the **same commit** as the code change it describes.
|
||||
- If you change a flow, update `runtime-flows.md` in the same PR.
|
||||
- If a section becomes outdated, fix or delete it — stale docs are worse than no docs.
|
||||
- If a section becomes outdated, fix or delete it; stale docs are worse than no docs.
|
||||
- Bible files are in **English only**.
|
||||
- Do not use em dash in Bible files; prefer ASCII punctuation such as `-`, `:`, or `;` depending on the sentence.
|
||||
|
||||
@@ -11,3 +11,31 @@ Canonical file transfer UX patterns for Go web applications:
|
||||
This pattern covers UI and UX contracts. Business-specific validation and file schemas remain in
|
||||
the host project's own architecture docs.
|
||||
|
||||
## Export Handler Sketch
|
||||
|
||||
```go
|
||||
func ExportCSV(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
|
||||
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||
|
||||
w := csv.NewWriter(c.Writer)
|
||||
w.Comma = ';'
|
||||
w.Write([]string{"ID", "Name", "Price"})
|
||||
|
||||
err := svc.StreamRows(ctx, filters, func(row Row) error {
|
||||
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
|
||||
})
|
||||
w.Flush()
|
||||
if err != nil {
|
||||
slog.Error("csv export failed mid-stream", "err", err)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Locale Notes
|
||||
|
||||
- BOM avoids broken UTF-8 in Excel on Windows.
|
||||
- Semicolon avoids single-column imports in RU/EU locales.
|
||||
- Decimal comma keeps numbers numeric in Excel.
|
||||
- `DD.MM.YYYY` is preferred over ISO dates for user-facing spreadsheet exports.
|
||||
|
||||
@@ -2,124 +2,38 @@
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Import Workflow
|
||||
See `README.md` for the reference export handler and locale examples.
|
||||
|
||||
Recommended stages:
|
||||
## Import Rules
|
||||
|
||||
1. `Upload`
|
||||
2. `Preview / Validate`
|
||||
3. `Confirm`
|
||||
4. `Execute`
|
||||
5. `Result summary`
|
||||
- Recommended flow: `Upload -> Preview / Validate -> Confirm -> Execute -> Result summary`.
|
||||
- Validation preview must be human-readable.
|
||||
- Warnings and errors should be visible per row and in aggregate.
|
||||
- The confirm step must communicate scope and side effects clearly.
|
||||
|
||||
Rules:
|
||||
## Export Rules
|
||||
|
||||
- Validation preview must be human-readable (table/list), not raw JSON only.
|
||||
- Warnings and errors should be shown per row and in aggregate summary.
|
||||
- Confirm step should clearly communicate scope and side effects.
|
||||
- The user must explicitly choose export scope when ambiguity exists, such as `selected`, `filtered`, or `all`.
|
||||
- Export format must be explicit.
|
||||
- Download responses must set `Content-Type` and `Content-Disposition` correctly.
|
||||
|
||||
## Export Workflow
|
||||
## CSV Rules
|
||||
|
||||
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
|
||||
- Export format should be explicit (`csv`, `json`, etc.).
|
||||
- Download response must set:
|
||||
- `Content-Type: text/csv; charset=utf-8`
|
||||
- `Content-Disposition: attachment; filename="..."`
|
||||
- For spreadsheet-facing CSV, write UTF-8 BOM as the first bytes.
|
||||
- Use semicolon `;` as the CSV delimiter.
|
||||
- Use comma as the decimal separator for user-facing numeric values.
|
||||
- Use `DD.MM.YYYY` for user-facing dates.
|
||||
- Use `encoding/csv` with `csv.Writer.Comma = ';'` so quoting and escaping stay correct.
|
||||
|
||||
## CSV Format Rules (Excel-compatible)
|
||||
## Streaming Rules
|
||||
|
||||
These rules are **mandatory** whenever CSV is exported for spreadsheet users.
|
||||
|
||||
### Encoding and BOM
|
||||
|
||||
- Write UTF-8 BOM (`\xEF\xBB\xBF`) as the very first bytes of the response.
|
||||
- Without BOM, Excel on Windows opens UTF-8 CSV as ANSI and garbles Cyrillic/special characters.
|
||||
|
||||
```go
|
||||
w.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||
```
|
||||
|
||||
### Delimiter
|
||||
|
||||
- Use **semicolon** (`;`) as the field delimiter, not comma.
|
||||
- Excel in Russian/European locale uses semicolon as the list separator.
|
||||
- Comma-delimited files open as a single column in these locales.
|
||||
|
||||
### Numbers
|
||||
|
||||
- Write decimal numbers with a **comma** as the decimal separator: `1 234,56` — not `1234.56`.
|
||||
- Excel in Russian locale does not recognize period as a decimal separator in numeric cells.
|
||||
- Format integers and floats explicitly; do not rely on Go's default `%v` or `strconv.FormatFloat`.
|
||||
- Use a thin non-breaking space (`\u202F`) or regular space as a thousands separator when the value
|
||||
benefits from readability (e.g. prices, quantities > 9999).
|
||||
|
||||
```go
|
||||
// correct
|
||||
fmt.Sprintf("%.2f", price) // then replace "." -> ","
|
||||
strings.ReplaceAll(fmt.Sprintf("%.2f", price), ".", ",")
|
||||
|
||||
// wrong — produces "1234.56", Excel treats it as text in RU locale
|
||||
fmt.Sprintf("%.2f", price)
|
||||
```
|
||||
|
||||
### Dates
|
||||
|
||||
- Write dates as `DD.MM.YYYY` — the format Excel in Russian locale parses as a date cell automatically.
|
||||
- Do not use ISO 8601 (`2006-01-02`) for user-facing CSV; it is not auto-recognized as a date in RU locale.
|
||||
|
||||
### Text quoting
|
||||
|
||||
- Wrap any field that contains the delimiter (`;`), a newline, or a double-quote in double quotes.
|
||||
- Escape embedded double-quotes by doubling them: `""`.
|
||||
- Use `encoding/csv` with `csv.Writer` and set `csv.Writer.Comma = ';'`; it handles quoting automatically.
|
||||
|
||||
## Streaming Export Architecture (Go)
|
||||
|
||||
For exports with potentially large row counts use a 3-layer streaming pattern.
|
||||
Never load all rows into memory before writing — stream directly to the response writer.
|
||||
|
||||
```
|
||||
Handler → sets HTTP headers + writes BOM → calls Service
|
||||
Service → delegates to Repository with a row callback
|
||||
Repository → queries in batches → calls callback per row
|
||||
Handler/Service → csv.Writer.Flush() after all rows
|
||||
```
|
||||
|
||||
```go
|
||||
// Handler
|
||||
func ExportCSV(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", `attachment; filename="export.csv"`)
|
||||
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF}) // BOM
|
||||
|
||||
w := csv.NewWriter(c.Writer)
|
||||
w.Comma = ';'
|
||||
w.Write([]string{"ID", "Name", "Price"}) // header row
|
||||
|
||||
err := svc.StreamRows(ctx, filters, func(row Row) error {
|
||||
return w.Write([]string{row.ID, row.Name, formatPrice(row.Price)})
|
||||
})
|
||||
w.Flush()
|
||||
if err != nil {
|
||||
// headers already sent — log only, cannot change status
|
||||
slog.Error("csv export failed mid-stream", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Repository — batch fetch with callback
|
||||
func (r *Repo) StreamRows(ctx, filters, fn func(Row) error) error {
|
||||
rows, err := r.db.QueryContext(ctx, query, args...)
|
||||
// ... scan and call fn(row) for each row
|
||||
}
|
||||
```
|
||||
|
||||
- Use `JOIN` in the repository query to avoid N+1 per row.
|
||||
- Batch size is optional; streaming row-by-row is fine for most datasets.
|
||||
- Always call `w.Flush()` after the loop — `csv.Writer` buffers internally.
|
||||
- Large exports must stream rows directly to the response. Do not load the full dataset into memory first.
|
||||
- Use the canonical flow:
|
||||
`Handler -> Service -> Repository callback -> csv.Writer`
|
||||
- Repository queries should avoid N+1 by using JOINs or another batched shape.
|
||||
- Always call `csv.Writer.Flush()` after writing rows.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Import errors should map to clear user-facing messages.
|
||||
- Export errors after streaming starts must be logged server-side only — HTTP headers are already
|
||||
sent and the status code cannot be changed mid-stream.
|
||||
|
||||
- Import errors must map to clear user-facing messages.
|
||||
- Once streaming has started, export failures are logged server-side only. Do not try to change the HTTP status after headers/body bytes were already sent.
|
||||
|
||||
56
rules/patterns/kiss/contract.md
Normal file
56
rules/patterns/kiss/contract.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# Contract: Keep It Simple
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Principle
|
||||
|
||||
Working solutions do not need to be interesting.
|
||||
|
||||
Prefer the simplest solution that correctly solves the problem. Complexity must be justified by a real, present requirement — not by anticipation of future needs or desire to use a particular technique.
|
||||
|
||||
## Rules
|
||||
|
||||
- Choose boring technology. A well-understood, dull solution beats a clever one.
|
||||
- Do not introduce abstractions, patterns, or frameworks before they are needed by at least two concrete use cases.
|
||||
- Do not design for hypothetical future requirements. Build for what exists now.
|
||||
- Prefer sequential, readable code over clever one-liners.
|
||||
- If you can delete code and the system still works, delete it.
|
||||
- Extra configurability, generalization, and extensibility are costs, not features. Add them only when explicitly required.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Adding helpers or utilities for one-time operations.
|
||||
- Wrapping simple logic in interfaces "for testability" when a direct call works.
|
||||
- Using a framework or library to solve a problem the standard library already handles.
|
||||
- Writing error handling, fallbacks, or validation for scenarios that cannot happen.
|
||||
- Refactoring working code because it "could be cleaner."
|
||||
|
||||
## Bulletproof features
|
||||
|
||||
A feature must be correct by construction, not by circumstance.
|
||||
|
||||
Do not write mechanisms that silently rely on:
|
||||
- another feature being in a specific state,
|
||||
- input data having a particular shape that "usually" holds,
|
||||
- a certain call order or timing,
|
||||
- a global flag, ambient variable, or external condition being set upstream.
|
||||
|
||||
Such mechanisms are thin: they work only when the world cooperates. When any surrounding assumption shifts, they break in ways that are hard to trace. This is the primary source of bugs.
|
||||
|
||||
**Design rules:**
|
||||
|
||||
- A feature owns its preconditions. If it requires data in a certain state, it must enforce or produce that state itself — not inherit it silently from a caller.
|
||||
- Never write logic that only works if a sibling feature runs first and succeeds. If coordination is needed, make it explicit (a parameter, a return value, a clear contract).
|
||||
- Avoid implicit state machines — sequences where operations must happen in the right order with no enforcement. Either enforce the order structurally or eliminate the dependency.
|
||||
- Prefer thick, unconditional logic over thin conditional chains that assume stable context. A mechanism that always does the right thing is more reliable than one that does the right thing only when conditions are favorable.
|
||||
|
||||
A feature is done when it is correct on its own, not when it is correct given that everything else is also correct.
|
||||
|
||||
## Checklist before committing
|
||||
|
||||
1. Could this be done with fewer lines without losing clarity?
|
||||
2. Does every abstraction here have more than one caller?
|
||||
3. Is any of this code handling a case that cannot actually occur?
|
||||
4. Did I add anything beyond what was asked?
|
||||
|
||||
If the answer to any of 1–4 is "yes," simplify before committing.
|
||||
95
rules/patterns/local-first-recovery/contract.md
Normal file
95
rules/patterns/local-first-recovery/contract.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Contract: Local-First Recovery
|
||||
|
||||
Version: 1.2
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared recovery and migration rules for local-first desktop applications that keep local state and may rebuild part of that state from sync, reload, import, or other deterministic upstream sources.
|
||||
|
||||
## Core Rule
|
||||
|
||||
A migration or startup strategy is not considered sufficient merely because it succeeded once on the current developer database.
|
||||
|
||||
Priority order:
|
||||
- Priority 1: protect user data. Do not do anything that can damage, discard, or silently rewrite non-recoverable user data.
|
||||
- Priority 2: preserve availability. Do not do anything that unnecessarily prevents the application from starting or operating in a reduced mode.
|
||||
|
||||
If user data is safe, prefer degraded startup over startup failure. If minimum useful functionality can be started safely, start it.
|
||||
|
||||
Startup and schema-migration behavior must be designed for degraded real-world states, including:
|
||||
- legacy schema versions
|
||||
- interrupted migrations
|
||||
- stale temp tables
|
||||
- invalid payloads
|
||||
- duplicates
|
||||
- `NULL` in required columns
|
||||
- partially migrated tables
|
||||
|
||||
## Required Data Classification
|
||||
|
||||
The architecture must explicitly separate:
|
||||
- disposable cache tables
|
||||
- protected user data tables
|
||||
|
||||
Definitions:
|
||||
- Disposable cache tables are read-only, sync-derived, imported, or otherwise rebuildable from a trusted source.
|
||||
- Protected user data tables contain user-authored or otherwise non-rebuildable data.
|
||||
|
||||
Do not mix both classes in one table if recovery semantics differ.
|
||||
|
||||
## Availability Policy For Disposable Data
|
||||
|
||||
For disposable cache tables, availability has priority.
|
||||
|
||||
Rules:
|
||||
- If a table cannot be migrated safely, it may be quarantined, dropped, or recreated empty.
|
||||
- The application must continue startup after such recovery.
|
||||
- The application must restore disposable data through the normal sync, reload, import, or rebuild path.
|
||||
- Recovery must not require manual SQL intervention for routine degraded states.
|
||||
|
||||
## Protection Policy For User Data
|
||||
|
||||
For protected user data, destructive reset is forbidden.
|
||||
|
||||
Rules:
|
||||
- Do not drop, truncate, or recreate protected tables as a recovery shortcut.
|
||||
- Backup-before-change is mandatory, must be performed automatically by the migration engine (never by the operator), and must follow the `backup-management` and `go-database` contracts.
|
||||
- Validate-before-migrate is mandatory.
|
||||
- Migration logic must use fail-safe semantics: stop before applying a risky destructive step when invariants are broken or input is invalid.
|
||||
- The application must emit explicit diagnostics that identify the blocked table, migration step, and reason.
|
||||
|
||||
## Recovery Logic Requirements
|
||||
|
||||
Rules:
|
||||
- Recovery logic must be deterministic.
|
||||
- Recovery logic must be idempotent.
|
||||
- Recovery logic must be retry-safe on every startup.
|
||||
- Recovery logic must be observable through structured logs.
|
||||
- Re-running startup after a partial failure must move the system toward a valid state, not deeper into corruption.
|
||||
|
||||
## Quality Bar
|
||||
|
||||
The application must either:
|
||||
- self-recover and continue startup
|
||||
|
||||
or:
|
||||
|
||||
- stop only when continuing would risk loss or corruption of non-recoverable user data
|
||||
|
||||
Stopping for disposable cache corruption alone is not acceptable when the data can be rebuilt safely.
|
||||
|
||||
If the full feature set cannot be restored safely during startup, the application should start with the minimum safe functionality instead of failing startup, as long as protected user data remains safe.
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
Degraded and legacy states must be tested explicitly, not only happy-path fresh installs.
|
||||
|
||||
Required test coverage includes:
|
||||
- legacy schema upgrades
|
||||
- interrupted migration recovery
|
||||
- partially migrated tables
|
||||
- duplicate rows where uniqueness is expected
|
||||
- `NULL` in required columns
|
||||
- invalid payloads in persisted rows
|
||||
- disposable-table reset and rebuild flow
|
||||
- protected-data migration refusal with explicit diagnostics
|
||||
61
rules/patterns/module-versioning/README.md
Normal file
61
rules/patterns/module-versioning/README.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Module Versioning Pattern Notes
|
||||
|
||||
This file keeps examples and the decision tree. The normative rules live in `contract.md`.
|
||||
|
||||
## Version Format
|
||||
|
||||
```text
|
||||
N.M
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- `1.0`
|
||||
- `2.0`
|
||||
- `2.1`
|
||||
- `2.3`
|
||||
|
||||
## Canonical Storage Options
|
||||
|
||||
Go constant:
|
||||
|
||||
```go
|
||||
const Version = "2.1"
|
||||
```
|
||||
|
||||
Document header:
|
||||
|
||||
```text
|
||||
Version: 2.1
|
||||
```
|
||||
|
||||
Config field:
|
||||
|
||||
```json
|
||||
{ "version": "2.1" }
|
||||
```
|
||||
|
||||
## Tag Format
|
||||
|
||||
```text
|
||||
<module-name>/v<N.M>
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
- `parser/v2.0`
|
||||
- `api-client/v1.3`
|
||||
|
||||
## Decision Tree
|
||||
|
||||
```text
|
||||
Module changed?
|
||||
-> no: version unchanged
|
||||
-> yes: behavior or interface changed?
|
||||
-> yes: N+1, reset minor to 0
|
||||
-> no: narrow bugfix only -> N+0.1
|
||||
```
|
||||
|
||||
## Commit Reminder
|
||||
|
||||
If a commit changes a module, the same commit should update the module version.
|
||||
@@ -9,114 +9,15 @@ Version: 1.0
|
||||
|
||||
Модули — это логические слои внутри одного репозитория, не отдельные пакеты.
|
||||
|
||||
---
|
||||
See `README.md` for examples and the decision tree.
|
||||
|
||||
## Формат версии
|
||||
## Rules
|
||||
|
||||
```
|
||||
N.M
|
||||
```
|
||||
|
||||
- `N` — мажорная версия (целое число, начинается с 1)
|
||||
- `M` — минорная версия (кратна 0.1, начинается с 0)
|
||||
|
||||
Примеры: `1.0`, `2.0`, `2.1`, `2.3`
|
||||
|
||||
---
|
||||
|
||||
## Правила инкремента
|
||||
|
||||
### N+1 — любая функциональная правка
|
||||
|
||||
Поднимаем мажор при **любом изменении функциональности**:
|
||||
|
||||
- добавление новой функции, метода, поля
|
||||
- изменение существующего поведения
|
||||
- удаление функциональности
|
||||
- рефакторинг, меняющий структуру модуля
|
||||
- изменение интерфейса взаимодействия с другими слоями
|
||||
|
||||
При инкременте мажора минор **сбрасывается в 0**: `2.3 → 3.0`
|
||||
|
||||
### N+0.1 — исправление бага
|
||||
|
||||
Поднимаем минор при **коротком точечном багфиксе**:
|
||||
|
||||
- исправление некорректного поведения без изменения интерфейса
|
||||
- правка крайнего случая (edge case)
|
||||
- исправление опечатки в логике
|
||||
|
||||
Функциональность при этом **не меняется**.
|
||||
|
||||
---
|
||||
|
||||
## Где хранить версию
|
||||
|
||||
Версия фиксируется в одном месте внутри модуля. Выбрать один из вариантов:
|
||||
|
||||
**Go** — константа в пакете:
|
||||
```go
|
||||
const Version = "2.1"
|
||||
```
|
||||
|
||||
**Файл** — заголовок contract.md или README модуля:
|
||||
```
|
||||
Version: 2.1
|
||||
```
|
||||
|
||||
**JSON/YAML конфиг** — поле `version`:
|
||||
```json
|
||||
{ "version": "2.1" }
|
||||
```
|
||||
|
||||
Не дублировать версию в нескольких местах одного модуля.
|
||||
|
||||
---
|
||||
|
||||
## Git-тег (опционально)
|
||||
|
||||
Если модуль выпускается как отдельная поставка, тег ставится в формате:
|
||||
|
||||
```
|
||||
<module-name>/v<N.M>
|
||||
```
|
||||
|
||||
Примеры: `parser/v2.0`, `api-client/v1.3`
|
||||
|
||||
Тег ставится только на коммит, в котором обновлена версия внутри модуля.
|
||||
Тег без обновления версии в коде — ошибка.
|
||||
|
||||
---
|
||||
|
||||
## Стартовая версия
|
||||
|
||||
Новый модуль начинается с `1.0`.
|
||||
Версия `0.x` не используется.
|
||||
|
||||
---
|
||||
|
||||
## Инструкция для агентов (Codex, Claude)
|
||||
|
||||
**Обязательно при каждом коммите:**
|
||||
|
||||
1. Определи, к какому модулю относятся изменения.
|
||||
2. Прочитай текущую версию модуля из канонического места (константа, заголовок, конфиг).
|
||||
3. Выбери инкремент по правилу:
|
||||
- Изменяется поведение, добавляется или удаляется функциональность → **N+1**, минор сбросить в 0
|
||||
- Только исправление бага, поведение не меняется → **N+0.1**
|
||||
4. Обнови версию в коде до коммита.
|
||||
5. Включи новую версию в сообщение коммита: `feat(parser): add csv dialect — v2.0`
|
||||
|
||||
**Агент не должен делать коммит без обновления версии затронутого модуля.**
|
||||
|
||||
### Дерево решений
|
||||
|
||||
```
|
||||
Изменения в модуле?
|
||||
│
|
||||
├── Да — это багфикс (логика была неверной, интерфейс не менялся)?
|
||||
│ ├── Да → N+0.1
|
||||
│ └── Нет → N+1, сброс минора в 0
|
||||
│
|
||||
└── Нет изменений в модуле → версия не меняется
|
||||
```
|
||||
- Module version format is `N.M`.
|
||||
- New modules start at `1.0`. `0.x` is not used.
|
||||
- Any functional change bumps the major version and resets minor to `0`.
|
||||
- A narrow bugfix that does not change behavior or interface bumps minor by `0.1`.
|
||||
- Store the version in one canonical place only: code constant, module document header, or config field.
|
||||
- If the module is tagged separately, use `<module-name>/v<N.M>`.
|
||||
- Do not create a tag without updating the module's canonical version first.
|
||||
- When a commit changes a module, update that module's version in the same commit.
|
||||
|
||||
84
rules/patterns/release-signing/README.md
Normal file
84
rules/patterns/release-signing/README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Release Signing Pattern Notes
|
||||
|
||||
This file keeps examples and rationale. The normative rules live in `contract.md`.
|
||||
|
||||
## Keys Repository Shape
|
||||
|
||||
```text
|
||||
keys/
|
||||
developers/
|
||||
<name>.pub
|
||||
scripts/
|
||||
keygen.sh
|
||||
sign-release.sh
|
||||
verify-signature.sh
|
||||
```
|
||||
|
||||
## Runtime Trust Loader
|
||||
|
||||
```go
|
||||
// trustedKeysRaw is injected via -ldflags.
|
||||
// Format: base64(key1):base64(key2):...
|
||||
var trustedKeysRaw string
|
||||
```
|
||||
|
||||
Typical parsing pattern:
|
||||
|
||||
```go
|
||||
func trustedKeys() ([]ed25519.PublicKey, error) {
|
||||
if trustedKeysRaw == "" {
|
||||
return nil, fmt.Errorf("dev build: trusted keys not embedded, updates disabled")
|
||||
}
|
||||
var keys []ed25519.PublicKey
|
||||
for _, enc := range strings.Split(trustedKeysRaw, ":") {
|
||||
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(enc))
|
||||
if err != nil || len(b) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("invalid trusted key: %w", err)
|
||||
}
|
||||
keys = append(keys, ed25519.PublicKey(b))
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
```
|
||||
|
||||
## Build Example
|
||||
|
||||
```sh
|
||||
KEYS=$(paste -sd: /path/to/keys/developers/*.pub)
|
||||
go build \
|
||||
-ldflags "-s -w -X <module>/internal/updater.trustedKeysRaw=${KEYS}" \
|
||||
-o dist/<binary>-linux-amd64 \
|
||||
./cmd/<binary>
|
||||
```
|
||||
|
||||
## Verification Sketch
|
||||
|
||||
```go
|
||||
func verifySignature(binaryPath, sigPath string) error {
|
||||
keys, err := trustedKeys()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := os.ReadFile(binaryPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read binary: %w", err)
|
||||
}
|
||||
sig, err := os.ReadFile(sigPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read signature: %w", err)
|
||||
}
|
||||
for _, key := range keys {
|
||||
if ed25519.Verify(key, data, sig) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("signature verification failed: no trusted key matched")
|
||||
}
|
||||
```
|
||||
|
||||
## Release Assets
|
||||
|
||||
```text
|
||||
<binary>-linux-amd64
|
||||
<binary>-linux-amd64.sig
|
||||
```
|
||||
23
rules/patterns/release-signing/contract.md
Normal file
23
rules/patterns/release-signing/contract.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Contract: Release Signing
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Ed25519 asymmetric signing for Go release binaries.
|
||||
Guarantees that a binary accepted by a running application was produced by a trusted developer.
|
||||
Applies to any Go binary that is distributed or supports self-update.
|
||||
|
||||
See `README.md` for reference code and build snippets.
|
||||
|
||||
## Rules
|
||||
|
||||
- Public keys are stored in the centralized keys repository. Public keys may be committed; private keys must stay on each developer machine and must never be committed or shared.
|
||||
- Adding or removing a trusted developer means changing the committed `.pub` set and rebuilding affected releases.
|
||||
- A release is trusted if its signature verifies against any embedded trusted public key.
|
||||
- The `.sig` asset is a raw 64-byte Ed25519 signature, not PEM and not base64.
|
||||
- Trusted public keys must be injected at build time via `-ldflags`. Do not hardcode them in source.
|
||||
- A build without injected keys is a valid dev build. It must continue working normally, but verified updates are disabled.
|
||||
- Signature verification uses Go stdlib `crypto/ed25519` only.
|
||||
- Signature verification failure must log a warning and keep the current binary. It must not crash the app and must not block unrelated operation.
|
||||
- Every signed release must ship the binary and its matching `.sig` asset.
|
||||
80
rules/patterns/secret-management/contract.md
Normal file
80
rules/patterns/secret-management/contract.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Contract: Secret Management
|
||||
|
||||
Version: 1.1
|
||||
|
||||
## Purpose
|
||||
|
||||
Общие правила, которые предотвращают утечку секретов в git, логи, конфиги, шаблоны и release-артефакты.
|
||||
|
||||
## No Secrets In Git
|
||||
|
||||
Secrets must never be committed to the repository, even temporarily.
|
||||
|
||||
This includes:
|
||||
- API keys
|
||||
- access tokens
|
||||
- passwords
|
||||
- DSNs with credentials
|
||||
- private keys
|
||||
- session secrets
|
||||
- OAuth client secrets
|
||||
- `.env` files with real values
|
||||
- production or staging config files with real credentials
|
||||
|
||||
Rules:
|
||||
- Real secrets must never appear in tracked files, commit history, tags, release assets, examples, fixtures, tests, or docs.
|
||||
- `.gitignore` is required for runtime config and local secret files, but `.gitignore` alone is not considered sufficient protection.
|
||||
- Commit only templates and examples with obvious placeholders, for example `CHANGEME`, `example`, or empty strings.
|
||||
- Never place secrets in screenshots, pasted logs, SQL dumps, backups, or exported archives that could later be committed.
|
||||
|
||||
## Where Secrets Live
|
||||
|
||||
Rules:
|
||||
- Store real secrets only in local runtime config, secret stores, environment injection, or deployment-specific configuration outside git.
|
||||
- Keep committed config files secret-free: `config.example.yaml`, `.env.example`, and similar files must contain placeholders only.
|
||||
- If a feature requires a new secret, document the config key name and format, not the real value.
|
||||
|
||||
## Required Git Checks
|
||||
|
||||
Before every commit:
|
||||
- Verify that files with real secrets are gitignored.
|
||||
- Inspect staged changes for secrets, not just working tree files.
|
||||
- Run an automated secret scan against staged content using project tooling or a repository-approved scanner.
|
||||
- If the scan cannot be run, stop and do not commit until an equivalent staged-content check is performed.
|
||||
|
||||
Before every push:
|
||||
- Scan the commits being pushed for secrets again.
|
||||
- Refuse the push if any potential secret is detected until it is reviewed and removed.
|
||||
|
||||
High-risk patterns that must be checked explicitly:
|
||||
- PEM blocks (`BEGIN PRIVATE KEY`, `BEGIN OPENSSH PRIVATE KEY`, `BEGIN RSA PRIVATE KEY`)
|
||||
- tokens in URLs or DSNs
|
||||
- `password=`, `token=`, `secret=`, `apikey=`, `api_key=`
|
||||
- cloud credentials
|
||||
- webhook secrets
|
||||
- JWT signing keys
|
||||
|
||||
## Scheduled Security Audit
|
||||
|
||||
Rules:
|
||||
- Perform a security audit at least once per week.
|
||||
- At least once per week, scan the git repository for leaked secrets, including current files, staged changes, commit history, and reachable tags.
|
||||
- Treat weekly secret scanning as mandatory even if pre-commit and pre-push checks already exist.
|
||||
- If the weekly audit finds a leaked secret, follow the Incident Response rules immediately.
|
||||
|
||||
## Logging and Generated Artifacts
|
||||
|
||||
Rules:
|
||||
- Do not print secrets into terminal output, structured logs, panic messages, or debug dumps.
|
||||
- Do not embed secrets into generated backups, exports, support bundles, or crash reports unless the artifact is explicitly treated as secret operational data and guaranteed to stay outside git.
|
||||
- If secrets must appear in an operational artifact, that artifact inherits the same "never in git" rule as backups.
|
||||
|
||||
## Incident Response
|
||||
|
||||
If a secret is committed or pushed:
|
||||
- Treat it as compromised immediately.
|
||||
- Rotate or revoke the secret.
|
||||
- Remove it from the current tree and from any generated artifacts.
|
||||
- Remove it from all affected commits and from repository history, not just from the latest revision.
|
||||
- Inform the user that history cleanup may be required.
|
||||
- Do not claim safety merely because the repo is private.
|
||||
21
rules/patterns/task-discipline/contract.md
Normal file
21
rules/patterns/task-discipline/contract.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contract: Task Discipline
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Principle
|
||||
|
||||
Finish before switching. A task is not done until it reaches a logical end.
|
||||
|
||||
## Rules
|
||||
|
||||
- Do not start a new task while the current one is unfinished. Switching mid-task leaves half-done work that is harder to recover than if it had never been started.
|
||||
- If a new idea or requirement surfaces during work, note it and address it after the current task is complete.
|
||||
- "Logical end" means: the change works, is committed, and leaves the codebase in a coherent state — not just "the immediate code compiles."
|
||||
- Do not open new files, refactor adjacent code, or fix unrelated issues while implementing a specific task. Stay focused on the defined scope.
|
||||
- If the current task is blocked, resolve the blocker or explicitly hand off — do not silently pivot to something else.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Starting a refactor while in the middle of a bug fix.
|
||||
- Leaving a feature half-implemented because something more interesting came up.
|
||||
- Responding to a new requirement by abandoning the current one without documenting what was left unfinished.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contract: Testing Policy
|
||||
|
||||
Version: 1.0
|
||||
Version: 1.1
|
||||
|
||||
## Purpose
|
||||
|
||||
@@ -17,10 +17,13 @@ Version: 1.0
|
||||
- **Трансформации** — конвертация единиц, нормализация, маппинг полей
|
||||
- **Бизнес-правила** — расчёты, фильтрация, агрегация, приоритизация
|
||||
- **Граничные случаи** — пустой ввод, нулевые значения, переполнение, отсутствующие поля
|
||||
- **Degraded / legacy states** — legacy schema, interrupted migrations, duplicates, invalid persisted rows, partially migrated tables
|
||||
- **Регрессии** — если баг был найден, тест фиксирует его до исправления
|
||||
|
||||
Тест пишется в том же коммите, что и функциональность. Функциональность без теста (там где он обязателен) — неполный коммит.
|
||||
|
||||
Для local-first desktop приложений правила деградированных состояний и recovery-тестов определяются также `local-first-recovery` contract.
|
||||
|
||||
---
|
||||
|
||||
## Когда тест не нужен
|
||||
|
||||
80
rules/patterns/unattended-boot-services/README.md
Normal file
80
rules/patterns/unattended-boot-services/README.md
Normal file
@@ -0,0 +1,80 @@
|
||||
# Unattended Boot Services Pattern Notes
|
||||
|
||||
This file keeps examples and rationale. The normative rules live in `contract.md`.
|
||||
|
||||
## Dependency Skeleton
|
||||
|
||||
```sh
|
||||
depend() {
|
||||
need localmount
|
||||
after some-service
|
||||
use logger
|
||||
}
|
||||
```
|
||||
|
||||
Avoid `need net` for best-effort services.
|
||||
|
||||
## Network-Independent SSH
|
||||
|
||||
```sh
|
||||
#!/sbin/openrc-run
|
||||
description="SSH server"
|
||||
|
||||
depend() {
|
||||
need localmount
|
||||
after bee-sshsetup
|
||||
use logger
|
||||
}
|
||||
|
||||
start() {
|
||||
check_config || return 1
|
||||
ebegin "Starting dropbear"
|
||||
/usr/sbin/dropbear ${DROPBEAR_OPTS}
|
||||
eend $?
|
||||
}
|
||||
```
|
||||
|
||||
Place this in `etc/init.d/dropbear` in the overlay to override package defaults that require network.
|
||||
|
||||
## Persistent DHCP
|
||||
|
||||
Wrong:
|
||||
|
||||
```sh
|
||||
udhcpc -i "$iface" -t 3 -T 5 -n -q
|
||||
```
|
||||
|
||||
Correct:
|
||||
|
||||
```sh
|
||||
udhcpc -i "$iface" -b -t 0 -T 3 -q
|
||||
```
|
||||
|
||||
## Typical Start Order
|
||||
|
||||
```text
|
||||
localmount
|
||||
-> sshsetup
|
||||
-> dropbear
|
||||
-> network
|
||||
-> nvidia
|
||||
-> audit
|
||||
```
|
||||
|
||||
Use `after` for ordering without turning soft dependencies into hard boot blockers.
|
||||
|
||||
## Error Handling Skeleton
|
||||
|
||||
```sh
|
||||
start() {
|
||||
ebegin "Running audit"
|
||||
/usr/local/bin/audit --output /var/log/audit.json >> /var/log/audit.log 2>&1
|
||||
local rc=$?
|
||||
if [ $rc -eq 0 ]; then
|
||||
einfo "Audit complete"
|
||||
else
|
||||
ewarn "Audit finished with errors — check /var/log/audit.log"
|
||||
fi
|
||||
eend 0
|
||||
}
|
||||
```
|
||||
22
rules/patterns/unattended-boot-services/contract.md
Normal file
22
rules/patterns/unattended-boot-services/contract.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# Contract: Unattended Boot Services (OpenRC)
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Rules for OpenRC services that run in unattended environments: LiveCDs, kiosks, embedded systems.
|
||||
No user is present. No TTY prompts. Every failure path must have a silent fallback.
|
||||
|
||||
See `README.md` for sample init scripts and ordering sketches.
|
||||
|
||||
## Rules
|
||||
|
||||
- Never block boot. A service failure must not stop the rest of the runlevel.
|
||||
- Never prompt. Do not use `read`, pause logic, or any interactive fallback.
|
||||
- Every `start()` must end with `eend 0` unless failure makes the environment fundamentally unusable, such as breaking SSH setup.
|
||||
- Write service diagnostics to `/var/log/`. TTY output is secondary.
|
||||
- Missing tools, absent network, or driver load failures must degrade gracefully: log and continue.
|
||||
- Use the minimum dependency set. Prefer `after` and `use`; do not add `need net`, `need networking`, or `need network-online` unless the service is truly useless without network and failure should be loud.
|
||||
- SSH services must start without requiring network availability.
|
||||
- DHCP must be non-blocking and persistent. Run the client in background retry mode rather than failing the boot sequence when no lease is immediately available.
|
||||
- External commands must be timeout-bounded so a bad device or tool cannot hang boot indefinitely.
|
||||
82
rules/patterns/vendor-installer-verification/contract.md
Normal file
82
rules/patterns/vendor-installer-verification/contract.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Contract: Vendor Installer Verification
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Purpose
|
||||
|
||||
Rules for downloading and verifying proprietary vendor installers (`.run`, `.exe`, `.tar.gz`)
|
||||
where the vendor publishes a checksum alongside the binary.
|
||||
Applies to: NVIDIA drivers, vendor CLI tools, firmware packages.
|
||||
|
||||
---
|
||||
|
||||
## Download Order
|
||||
|
||||
Always download the checksum file **before** the installer:
|
||||
|
||||
```sh
|
||||
BASE_URL="https://vendor.example.com/downloads/${VERSION}"
|
||||
BIN_FILE="/var/cache/vendor-${VERSION}.run"
|
||||
SHA_FILE="/var/cache/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 1. Download checksum first
|
||||
wget -q -O "$SHA_FILE" "${BASE_URL}/vendor-${VERSION}.run.sha256sum"
|
||||
|
||||
# 2. Download installer
|
||||
wget --show-progress -O "$BIN_FILE" "${BASE_URL}/vendor-${VERSION}.run"
|
||||
|
||||
# 3. Verify
|
||||
cd /var/cache
|
||||
sha256sum -c "$SHA_FILE" || { echo "ERROR: sha256 mismatch"; rm -f "$BIN_FILE"; exit 1; }
|
||||
```
|
||||
|
||||
Reason: if the download is interrupted, you have the expected checksum to verify against on retry.
|
||||
|
||||
---
|
||||
|
||||
## Cache with Verification
|
||||
|
||||
Never assume a cached file is valid — a previous download may have been interrupted (0-byte file):
|
||||
|
||||
```sh
|
||||
verify_cached() {
|
||||
[ -s "$SHA_FILE" ] || return 1 # sha256 file missing or empty
|
||||
[ -s "$BIN_FILE" ] || return 1 # binary missing or empty
|
||||
cd "$(dirname "$BIN_FILE")"
|
||||
sha256sum -c "$SHA_FILE" --status 2>/dev/null
|
||||
}
|
||||
|
||||
if ! verify_cached; then
|
||||
rm -f "$BIN_FILE" "$SHA_FILE"
|
||||
# ... download and verify
|
||||
else
|
||||
echo "verified from cache"
|
||||
fi
|
||||
```
|
||||
|
||||
**Never check only for file existence.** Check that the file is non-empty (`-s`) AND passes checksum.
|
||||
|
||||
---
|
||||
|
||||
## Version Validation
|
||||
|
||||
Before writing build scripts, verify the version URL actually exists:
|
||||
|
||||
```sh
|
||||
curl -sIL "https://vendor.example.com/downloads/${VERSION}/installer.run" \
|
||||
| grep -i 'http/\|content-length'
|
||||
```
|
||||
|
||||
A `404` or `content-length: 0` means the version does not exist on that CDN.
|
||||
Vendor version numbering may have gaps (e.g. NVIDIA skips minor versions on some CDNs).
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Download checksum before installer — never after.
|
||||
- Verify checksum before extracting or executing.
|
||||
- On mismatch: delete the file, exit with error. Never proceed with a bad installer.
|
||||
- Cache by `version` + any secondary key (e.g. kernel version for compiled modules).
|
||||
- Never commit installer files to git — always download at build time.
|
||||
- Log the expected hash when downloading so failures are diagnosable.
|
||||
302
rules/patterns/web-visual-baseline/contract.md
Normal file
302
rules/patterns/web-visual-baseline/contract.md
Normal file
@@ -0,0 +1,302 @@
|
||||
# Contract: Web Visual Baseline
|
||||
|
||||
Version: 1.0
|
||||
|
||||
## Scope
|
||||
|
||||
Defines the default visual baseline for future web applications in this ecosystem.
|
||||
|
||||
The canonical reference is the UI style from:
|
||||
|
||||
- `/Users/mchusavitin/Documents/git/chart/web/static/view.css`
|
||||
- `/Users/mchusavitin/Documents/git/chart/web/templates/view.html`
|
||||
- `/Users/mchusavitin/Documents/git/chart/web/templates/upload.html`
|
||||
|
||||
When a project does not already have an established design system, use this baseline by default.
|
||||
|
||||
## Core Direction
|
||||
|
||||
- Prefer a clean, data-first interface over decorative marketing UI.
|
||||
- Default to server-rendered HTML with simple CSS.
|
||||
- Optimize for scanability, density, and operational clarity.
|
||||
- Use restrained visual hierarchy, not novelty effects.
|
||||
- Reuse the baseline directly when possible; copying the canonical CSS and adapting tokens is allowed.
|
||||
|
||||
## Canonical Visual Language
|
||||
|
||||
- Dark application header on top.
|
||||
- White page background and white content surfaces.
|
||||
- Light secondary surfaces for headers and table heads.
|
||||
- Thin gray borders with a subtle shadow.
|
||||
- Small radii (`4px`).
|
||||
- Dense but readable typography (`14px/1.5` baseline).
|
||||
- Blue accent in the `#2185d0` family for primary actions and active accents.
|
||||
- Tables and key-value layouts as the primary presentation pattern.
|
||||
- Status communicated with both text and color.
|
||||
|
||||
## Typography
|
||||
|
||||
- Use `Lato, "Helvetica Neue", Arial, Helvetica, sans-serif` unless a project has an approved alternative.
|
||||
- Page titles are compact and strong, not oversized hero typography.
|
||||
- Section titles should be clear and structural.
|
||||
- Avoid display fonts, novelty fonts, and oversized marketing headings in application UI.
|
||||
|
||||
## Layout Primitives
|
||||
|
||||
- `page-header`: dark global header with page title and compact actions.
|
||||
- `page-main`: centered content area with generous outer margin and bounded max width.
|
||||
- `panel`: white surface with border, light shadow, and simple heading strip.
|
||||
- `section-card`: heading followed by table/content block.
|
||||
- `table-wrap`: horizontal overflow container for dense data tables.
|
||||
|
||||
## Preferred Components
|
||||
|
||||
- Key-value tables for singleton object/detail views.
|
||||
- Dense data tables for repeated records.
|
||||
- Compact upload/open panels when local file input is needed.
|
||||
- Quiet header actions for secondary navigation.
|
||||
- Clear primary buttons for the main action on a screen.
|
||||
- Simple alert/error boxes with border + tinted background.
|
||||
|
||||
## Status Rules
|
||||
|
||||
- `OK`: green
|
||||
- `Warning`: amber
|
||||
- `Critical`: red
|
||||
- `Unknown`: gray
|
||||
- `Empty`: light gray
|
||||
|
||||
Status must not rely on color alone.
|
||||
Show text or another explicit indicator together with the color treatment.
|
||||
|
||||
## Responsive Rules
|
||||
|
||||
- Keep desktop density high.
|
||||
- Collapse grids to one column on small screens.
|
||||
- Preserve table readability with horizontal scrolling instead of destructive cardification by default.
|
||||
- Header actions may wrap or stack on mobile, but should remain compact.
|
||||
|
||||
## Forbidden Drift
|
||||
|
||||
- Do not default to glassmorphism, blurred shells, floating neon gradients, or soft-dribbble styling.
|
||||
- Do not replace dense tables with oversized card grids when the data is inherently tabular.
|
||||
- Do not introduce arbitrary color coding for non-status fields.
|
||||
- Do not use oversized border radii, heavy shadows, or large empty spacing as the default application style.
|
||||
- Do not import a SPA/dashboard aesthetic unless the product explicitly requires it.
|
||||
|
||||
## Relationship To Other UI Contracts
|
||||
|
||||
- Use this contract as the visual baseline.
|
||||
- Use `table-management` for shared table geometry and interaction seams.
|
||||
- Use `controls-selection` for button hierarchy, filters, and bulk selection semantics.
|
||||
- Pattern-specific contracts may override details only when they document the reason.
|
||||
|
||||
## Copyable Starter CSS
|
||||
|
||||
Use this as the default starting point for new web apps:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--surface: #ffffff;
|
||||
--surface-2: #f9fafb;
|
||||
--border: rgba(34, 36, 38, 0.15);
|
||||
--border-lite: rgba(34, 36, 38, 0.1);
|
||||
--ink: rgba(0, 0, 0, 0.87);
|
||||
--muted: rgba(0, 0, 0, 0.6);
|
||||
--accent: #2185d0;
|
||||
--accent-dark: #1678c2;
|
||||
--accent-bg: #dff0ff;
|
||||
--ok: #16ab39;
|
||||
--warn: #f2711c;
|
||||
--crit: #db2828;
|
||||
--header-bg: #1b1c1d;
|
||||
--radius: 4px;
|
||||
--shadow: 0 1px 2px 0 rgba(34, 36, 38, 0.15);
|
||||
--content-width: 1500px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: 14px/1.5 Lato, "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 24px;
|
||||
background: var(--header-bg);
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.page-main {
|
||||
width: min(var(--content-width), calc(100vw - 48px));
|
||||
margin: 28px auto 56px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
margin-bottom: 28px;
|
||||
overflow: hidden;
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.panel > h2 {
|
||||
margin: 0;
|
||||
padding: 13px 16px;
|
||||
background: var(--surface-2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.kv-table,
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.kv-table th,
|
||||
.kv-table td,
|
||||
.data-table th,
|
||||
.data-table td {
|
||||
padding: 11px 14px;
|
||||
text-align: left;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid var(--border-lite);
|
||||
}
|
||||
|
||||
.kv-table th,
|
||||
.data-table th {
|
||||
background: var(--surface-2);
|
||||
border-top: 0;
|
||||
border-bottom: 1px solid var(--border-lite);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.data-table tbody tr:hover {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.button-primary {
|
||||
display: inline-block;
|
||||
padding: 8px 18px;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button-primary:hover {
|
||||
background: var(--accent-dark);
|
||||
}
|
||||
|
||||
.header-action {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
border-radius: var(--radius);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
text-decoration: none;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.header-action:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.status-ok { color: var(--ok); }
|
||||
.status-warning { color: var(--warn); }
|
||||
.status-critical { color: var(--crit); }
|
||||
.status-unknown { color: rgba(0, 0, 0, 0.45); }
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
width: calc(100vw - 24px);
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Copyable Starter HTML
|
||||
|
||||
```html
|
||||
<header class="page-header">
|
||||
<h1>Application Title</h1>
|
||||
<a class="header-action" href="/back">Back</a>
|
||||
</header>
|
||||
|
||||
<main class="page-main">
|
||||
<section class="panel">
|
||||
<h2>Overview</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="kv-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Host</th>
|
||||
<td>server-01</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td><span class="status-ok">OK</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Devices</h2>
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Vendor</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>NIC 1</td>
|
||||
<td>Intel</td>
|
||||
<td><span class="status-warning">Warning</span></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
```
|
||||
Reference in New Issue
Block a user