Compare commits
4 Commits
d2e11b8bdd
...
v0.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
688b87e98d | ||
|
|
52444350c1 | ||
|
|
747c42499d | ||
|
|
5a69e0bba8 |
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.
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contract: Database Patterns (Go / MySQL / MariaDB)
|
||||
|
||||
Version: 1.6
|
||||
Version: 1.9
|
||||
|
||||
## MySQL Transaction Cursor Safety (CRITICAL)
|
||||
|
||||
@@ -104,31 +104,123 @@ items, _ := repo.GetItemsByPricelistIDs(ids) // 1 query with WHERE id IN (...)
|
||||
// then group in Go
|
||||
```
|
||||
|
||||
## Backup Before Any DB Change
|
||||
## Automatic Backup During Migration
|
||||
|
||||
Any operation that changes persisted database state must have a fresh backup taken immediately before execution.
|
||||
|
||||
This applies to:
|
||||
- Go migrations
|
||||
- Manual SQL runbooks
|
||||
- Data backfills and repair scripts
|
||||
- Imports, bulk updates, and bulk deletes
|
||||
- Admin tools or one-off operator commands
|
||||
The migration engine is responsible for all backup steps. The operator must never be required to take a backup manually.
|
||||
|
||||
Backup naming, storage, archive format, retention, and restore-readiness must follow the `backup-management` contract.
|
||||
|
||||
### Full DB Backup on New Migrations
|
||||
|
||||
When the migration engine detects that new (unapplied) migrations exist, it must take a full database backup before applying any of them.
|
||||
|
||||
Rules:
|
||||
- No schema change or data mutation is allowed on a non-ephemeral database without a current backup.
|
||||
- "Small" or "safe" changes are not exceptions.
|
||||
- The operator must know how to restore from that backup before applying the change.
|
||||
- If a migration or script is intended for production/staging, the rollout instructions must state the backup step explicitly.
|
||||
- The backup taken before a migration must be triggered by the application's own backup mechanism, not by assuming `mysql`, `mysqldump`, or other DB client tools exist on the user's machine.
|
||||
- Before a migration starts, double-check that backup output resolves outside the git worktree and is not tracked or staged in git.
|
||||
- The full backup must complete and be verified before the first migration step runs.
|
||||
- The backup must be triggered by the application's own backup mechanism; do not assume `mysql`, `mysqldump`, `pg_dump`, or any other external DB client tool is present on the operator's machine.
|
||||
- Before creating the backup, verify that the backup output path resolves outside the git worktree and is not tracked or staged in git.
|
||||
|
||||
### Per-Table Backup Before Each Table Migration
|
||||
|
||||
Before applying a migration step that affects a specific table, take a targeted backup of that table.
|
||||
|
||||
Rules:
|
||||
- A per-table backup must be created immediately before the migration step that modifies that table.
|
||||
- If a single migration step touches multiple tables, back up each affected table before the step runs.
|
||||
- Per-table backups are in addition to the full DB backup; they are not a substitute for it.
|
||||
|
||||
### Session Rollback on Failure
|
||||
|
||||
If any migration step fails during a session, the engine must roll back all migrations applied in that session.
|
||||
|
||||
Rules:
|
||||
- "Session" means all migration steps started in a single run of the migration engine.
|
||||
- On failure, roll back every step applied in the current session in reverse order before surfacing the error.
|
||||
- If rollback of a step is not possible (e.g., the operation is not reversible in MySQL without the per-table backup), restore from the per-table backup taken before that step.
|
||||
- After rollback or restore, the database must be in the same state as before the session started.
|
||||
- The engine must emit structured diagnostics that identify which step failed, which steps were rolled back, and the final database state.
|
||||
|
||||
## Migration Policy
|
||||
|
||||
- For local-first desktop applications, startup and migration recovery must follow the `local-first-recovery` contract.
|
||||
- Migrations are numbered sequentially and never modified after merge.
|
||||
- Trigger, take, and verify a fresh backup through the application-owned backup mechanism before applying migrations to any non-ephemeral database.
|
||||
- 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.
|
||||
|
||||
## SQL Migration File Format
|
||||
|
||||
Every `.sql` migration file must begin with a structured header block:
|
||||
|
||||
```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 LIMIT 1
|
||||
-- verify: No empty supplier_code | SELECT supplier_name FROM supplier WHERE supplier_code = '' LIMIT 1
|
||||
```
|
||||
|
||||
**`-- Tables affected:`** — comma-separated list of tables the migration touches. Used by the backup engine to take a targeted pre-migration backup. Omit only if no table can be identified; the engine falls back to full DB backup.
|
||||
|
||||
**`-- recovery.*:`** — human-readable rollback SQL for each migration state (`not-started`, `partial`, `completed`). Executed manually by an operator if automatic restore fails. Must be correct, copy-pasteable SQL.
|
||||
|
||||
**`-- verify:`** — post-migration assertion query. Format: `-- verify: <description> | <SQL>`. The engine runs the query after all statements in the file succeed. If the query returns **any row**, the migration is considered failed and is rolled back. Write the query so it returns a row only when something is **wrong**:
|
||||
|
||||
```sql
|
||||
-- verify: Orphaned FK refs | SELECT id FROM child c LEFT JOIN parent p ON p.id = c.parent_id WHERE p.id IS NULL LIMIT 1
|
||||
-- ^ returns a row = bad ^ returns nothing = good
|
||||
```
|
||||
|
||||
- Verify queries must filter out NULL/empty values that would cause false positives: add `AND col IS NOT NULL AND col != ''`.
|
||||
- A migration is only recorded as applied after all verify checks pass.
|
||||
- Verify checks are not a substitute for testing; they are a last-resort safety net on production.
|
||||
|
||||
## Pre-Production Migration Testing in Docker
|
||||
|
||||
Before applying a set of new migrations to production, always validate them against a copy of the production database in a local MariaDB Docker container that matches the production version and collation.
|
||||
|
||||
```bash
|
||||
# Start container matching production (MariaDB 11.8, utf8mb4_uca1400_ai_ci)
|
||||
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
|
||||
|
||||
# Load production dump
|
||||
docker exec -i pf_test mariadb -uroot -ptest RFQ_LOG < prod_dump.sql
|
||||
|
||||
# Run migrations via pfs (uses real migration engine + verify checks, no backup)
|
||||
./pfs -migrate-dsn "root:test@tcp(127.0.0.1:3306)/RFQ_LOG?parseTime=true&charset=utf8mb4&multiStatements=true" \
|
||||
-no-backup -verbose
|
||||
```
|
||||
|
||||
The `-migrate-dsn` flag connects to the given DSN, runs all pending migrations, runs verify checks, and exits. No config file, no server, no browser.
|
||||
|
||||
**Rules:**
|
||||
- Always test on a dump of the **current production database**, not a fixture — schema drift and real data distributions expose bugs that fixtures miss.
|
||||
- The Docker container must use the same MariaDB version and `--collation-server` as production.
|
||||
- Each migration file is executed as a **single session** so `SET FOREIGN_KEY_CHECKS = 0` applies to all its statements. Never test by running statements from a migration file individually across separate sessions — the session variable will reset between them.
|
||||
- If any migration fails in Docker, fix the SQL before touching production. Do not rely on "it will be different in production."
|
||||
|
||||
## SQL Migration Authoring — Common Pitfalls
|
||||
|
||||
**Semicolons inside string literals break naive splitters.**
|
||||
The migration engine uses a quote-aware statement splitter. Do not rely on external tools that split on bare `;`. When writing supplier/product names with punctuation, use commas — not semicolons — as separators in string literals. A semicolon inside `'COMPANY; LTD'` will break any naive `split(";")` approach.
|
||||
|
||||
**`SET FOREIGN_KEY_CHECKS = 0` only applies to the current session.**
|
||||
This is a session variable. If statements run in separate connections (e.g. via individual subprocess calls), FK checks are re-enabled for each new connection. Always run an entire migration file as one session. The pfs migration engine runs all statements in a file on the same GORM db handle, which reuses the same connection.
|
||||
|
||||
**Verify queries must exclude NULL values.**
|
||||
A query like `SELECT c.col FROM child c LEFT JOIN parent p ON p.id = c.id WHERE p.id IS NULL` will return rows with `c.col = NULL` if the child table has rows with a NULL FK value. Add `AND c.col IS NOT NULL AND c.col != ''` to avoid false failures.
|
||||
|
||||
**Catch-all INSERT for referential integrity before adding FK constraints.**
|
||||
When adding a FK constraint to a table that previously had no FK (legacy data may have orphaned references), add a catch-all step before the constraint:
|
||||
|
||||
```sql
|
||||
-- Ensure every value referenced in child table exists in parent before adding FK.
|
||||
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 != '';
|
||||
```
|
||||
|
||||
This is not a hack — it repairs data that was valid before the constraint existed. Never delete orphaned child rows unless data loss is acceptable.
|
||||
|
||||
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
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
## Когда тест не нужен
|
||||
|
||||
Reference in New Issue
Block a user