Compare commits

...

1 Commits

Author SHA1 Message Date
40d1c303bb Add shared engineering rule contracts
- go-logging: slog, server-side only, structured attributes
- go-database: MySQL cursor safety, soft delete, GORM tags, fail-fast, N+1 prevention
- go-background-tasks: Task Manager pattern, polling, no SSE
- go-code-style: layering, error wrapping, startup sequence, config, templating
- import-export: CSV Excel-compatible rules (BOM, semicolon, decimal comma, DD.MM.YYYY)
- table-management: filtering and pagination rules added
- CLAUDE.template.md: updated to reference all shared contracts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-01 16:39:39 +03:00
7 changed files with 451 additions and 6 deletions

View File

@@ -5,7 +5,53 @@ Read and follow the project Bible before making any changes:
**[`bible/README.md`](bible/README.md)** **[`bible/README.md`](bible/README.md)**
The Bible is the single source of truth for architecture, data models, API contracts, and UI The Bible is the single source of truth for architecture, data models, API contracts, and UI
pattern conventions. pattern conventions. Every significant architectural decision must be recorded in the Bible
decision log before or alongside the code change.
Every significant architectural decision must be recorded in the appropriate Bible decision log. ---
## Shared Engineering Rules
The following rules apply to ALL changes in this project.
They are maintained centrally in `tools/ui-design-code/kit/patterns/`.
### Go Code Style
See [`tools/ui-design-code/kit/patterns/go-code-style/contract.md`](tools/ui-design-code/kit/patterns/go-code-style/contract.md)
- Handler → Service → Repository layering
- Error wrapping with `fmt.Errorf("...: %w", err)`
- Startup sequence: connect DB → migrate → start server
- Business logic and status thresholds live on the server, not in JS
### Logging
See [`tools/ui-design-code/kit/patterns/go-logging/contract.md`](tools/ui-design-code/kit/patterns/go-logging/contract.md)
- Use `slog`, log to binary stdout/stderr only
- Never use `console.log` as a substitute for server-side logging
- Always log: startup, background task start/finish/error, export row counts, ingest results
### Database
See [`tools/ui-design-code/kit/patterns/go-database/contract.md`](tools/ui-design-code/kit/patterns/go-database/contract.md)
- **CRITICAL**: never execute SQL on the same tx while iterating a cursor — two-phase only
- Soft delete via `is_active = false`, not physical deletes
- Fail-fast DB check before starting the HTTP server
- No N+1 queries: use JOINs or batch `IN` queries
### Background Tasks
See [`tools/ui-design-code/kit/patterns/go-background-tasks/contract.md`](tools/ui-design-code/kit/patterns/go-background-tasks/contract.md)
- All slow operations: POST → task_id → client polls `/api/tasks/:id`
- No SSE — polling only
- Return `202 Accepted` when task is created
### Tables, Filtering, Pagination
See [`tools/ui-design-code/kit/patterns/table-management/contract.md`](tools/ui-design-code/kit/patterns/table-management/contract.md)
- Server-side filtering and pagination only
- Filter state in URL query params; applying a filter resets to page 1
- Response must include `total_count`, `page`, `per_page`, `total_pages`
- Display format: "51100 из 342"
### CSV Export
See [`tools/ui-design-code/kit/patterns/import-export/contract.md`](tools/ui-design-code/kit/patterns/import-export/contract.md)
- UTF-8 BOM required (`\xEF\xBB\xBF`)
- Semicolon delimiter (`;`), not comma
- Decimal separator: comma (`1 234,56`), not period
- Dates as `DD.MM.YYYY`
- `csv.Writer` with `Comma = ';'`

View File

@@ -0,0 +1,73 @@
# Contract: Background Tasks (Go Web Applications)
## Core Rule
All long-running operations (> ~300ms or uncertain duration) must run as background tasks.
Never block an HTTP handler waiting for a slow operation to complete.
## Standard Flow
```
POST /api/something → creates task → returns {task_id}
GET /api/tasks/:id → client polls → returns status + progress
UI shows spinner → on success: toast + refresh
```
The client polls `/api/tasks/:id` on a fixed interval (13 s) until status is terminal.
## Task Status Values
| Status | Meaning |
|-------------|---------|
| `queued` | Created, not yet started |
| `running` | In progress |
| `success` | Completed successfully |
| `failed` | Completed with error |
| `canceled` | Explicitly canceled by user |
## Task Struct (canonical fields)
```go
type Task struct {
ID string
Type string // e.g. "recalculate", "import", "export"
Status string
Progress int // 0100
Message string // human-readable current step
Result any // populated on success (row count, file URL, etc.)
Error string // populated on failure
CreatedAt time.Time
UpdatedAt time.Time
}
```
## Rules
- Do NOT use SSE (Server-Sent Events) for task progress. Use polling.
SSE requires persistent connections and complicates load balancers and proxies.
- One task per operation. Do not batch unrelated operations into a single task.
- Tasks must be stored in memory (or DB for persistence) with a TTL — clean up after completion.
- Handler that creates the task returns `202 Accepted` with `{"task_id": "..."}`.
- Log task start, finish, and error server-side (see go-logging contract).
## UI Contract
- Show a spinner/progress bar while `status == "running"`.
- On `success`: show toast notification, refresh affected data.
- On `failed`: show error message from `task.Error`.
- Poll interval: 13 seconds. Stop polling on terminal status.
- Do not disable the whole page during task execution — allow user to navigate away.
## What Qualifies as a Background Task
- CSV/file export with > ~500 rows
- Bulk recalculation or recompute operations
- External API calls (sync, collect, import from remote)
- Any import/ingest that touches multiple DB tables
- Database repair or cleanup operations
## What Does NOT Need a Background Task
- Simple CRUD operations (create/update/delete single record)
- Filtered list queries
- Single-record exports

View File

@@ -0,0 +1,82 @@
# Contract: Go Code Style and Project Conventions
## Logging
See `kit/patterns/go-logging/contract.md` for full rules.
Summary: use `slog`, log to stdout/stderr (binary console), never to browser console.
## Error Handling
Always wrap errors with context. Use `fmt.Errorf("...: %w", err)`.
```go
// CORRECT
if err := db.Save(&record).Error; err != nil {
return fmt.Errorf("save component %s: %w", record.ID, err)
}
// WRONG — loses context
return err
```
- Never silently discard errors with `_` in production paths.
- Return errors up the call stack; log at the handler/task boundary, not deep in service code.
## Code Formatting
- Always run `gofmt` before committing. No exceptions.
- No manual alignment of struct fields or variable assignments.
## HTTP Handler Structure
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
```
- Handlers must not contain SQL queries.
- Services must not write HTTP responses.
- Repositories must not contain business rules.
## Startup Sequence (Go web app)
```
1. Parse flags / load config
2. Connect to DB — fail fast if unavailable (see go-database contract)
3. Run migrations
4. Initialize services and background workers
5. Register routes
6. Start HTTP server
```
Never reverse steps 2 and 5. Never start serving before migrations complete.
## Configuration
- Config lives in a single `config.yaml` file, not scattered env vars.
- Env vars may override config values but must be documented.
- 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.
## Template / UI Rendering
- Server-rendered HTML via Go templates is the default.
- 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.
- Example: "critical / warning / ok" badge color is determined by the handler, not by JS.
## Dependency Rules
- Prefer standard library. Add a dependency only when the stdlib alternative is significantly worse.
- Document the reason for each non-stdlib dependency in a comment or ADL entry.

View File

@@ -0,0 +1,110 @@
# Contract: Database Patterns (Go / MySQL / MariaDB)
## MySQL Transaction Cursor Safety (CRITICAL)
**Never execute SQL on the same transaction while iterating over a query result cursor.**
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.
- Archive operations must be reversible from the UI.
## GORM Virtual Fields
Use the correct tag based on whether the field should exist in the DB schema:
```go
// Field computed at runtime, column must NOT exist in DB (excludes from migrations AND queries)
Count int `gorm:"-"`
// Field computed at query time via JOIN/SELECT, column must NOT be in migrations
// but IS populated from query results
DisplayName string `gorm:"-:migration"`
```
- `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.
## Fail-Fast DB Check on Startup
Always verify the database connection before starting the HTTP server.
```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.

View File

@@ -0,0 +1,64 @@
# Contract: Logging (Go Web Applications)
## Core Rule
**All logging goes to the server binary's stdout/stderr — never to the browser console.**
- `console.log`, `console.error`, `console.warn` in JavaScript are for debugging only.
Remove them before committing. They are not a substitute for server-side logging.
- Diagnostic information (errors, slow queries, ingest results, background task status) must be
logged server-side so it is visible in the terminal where the binary runs.
## Go Logging Standard
Use the standard library `log/slog` (Go 1.21+) or `log` for simpler projects.
```go
// correct — structured, visible in binary console
slog.Info("export started", "user", userID, "rows", count)
slog.Error("db query failed", "err", err)
// wrong — goes to browser devtools, invisible in production
// (JS template rendered in HTML)
// console.log("export started")
```
## Log Levels
| Level | When to use |
|---------|-------------|
| `Debug` | Detailed flow tracing; disabled in production builds |
| `Info` | Normal operation milestones: server start, request handled, export done |
| `Warn` | Recoverable anomaly: retry, fallback used, deprecated path |
| `Error` | Operation failed; always include `"err", err` as a structured attribute |
## What to Always Log
- Server startup: address, port, config source.
- Background task start/finish/error with task name and duration.
- CSV/file export: scope, row count, duration.
- Ingest/import: file name, rows accepted, rows rejected, error summary.
- Any `500` response: handler name + error.
## What Not to Log
- Do not log full request bodies or CSV content — can contain PII or large payloads.
- Do not log passwords, tokens, or secrets even partially.
- Do not add `fmt.Println` for debugging and leave it in committed code.
## htmx / Browser-rendered Responses
- Handler errors that result in an htmx partial re-render must still log the error server-side.
- Do not rely on the browser network tab as the only visibility into server errors.
## Format
- Use structured key-value attributes, not concatenated strings.
```go
// correct
slog.Error("sync failed", "component", comp.Serial, "err", err)
// wrong — hard to grep, hard to parse
log.Printf("sync failed for component %s: %v", comp.Serial, err)
```

View File

@@ -20,10 +20,56 @@ Rules:
- User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists. - User must explicitly choose export scope (`selected`, `filtered`, `all`) when ambiguity exists.
- Export format should be explicit (`csv`, `json`, etc.). - Export format should be explicit (`csv`, `json`, etc.).
- Download response should set: - Download response must set:
- `Content-Type` - `Content-Type: text/csv; charset=utf-8`
- `Content-Disposition` - `Content-Disposition: attachment; filename="..."`
- If CSV targets spreadsheet users, document delimiter and BOM policy.
## CSV Format Rules (Excel-compatible)
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.
## Error Handling ## Error Handling

View File

@@ -75,6 +75,30 @@ Canonical mapping:
- right-aligned action icons. - right-aligned action icons.
- Row action icons in `actions` column are intentionally smaller than toolbar icons. - Row action icons in `actions` column are intentionally smaller than toolbar icons.
## Filtering Rules
- Filters live above the table, in a dedicated filter bar or inline in the toolbar.
- Every active filter must be visually indicated (highlighted field, chip, or badge count).
- Applying a filter always resets pagination to page 1.
- Filter state must be reflected in the URL as query parameters so the page is bookmarkable/shareable.
- "Reset filters" clears all filter fields and reloads with no filter params.
- Server-side filtering only — do not filter an already-loaded JS array client-side unless the full
dataset is guaranteed small (< 500 rows) and never grows.
- Filter inputs debounce text input (300500 ms) before triggering a server request.
## Pagination Rules
- Pagination is server-side. Never load all rows and paginate client-side.
- URL query parameters carry page state: `?page=2&per_page=50`.
- `page` is 1-based.
- `per_page` defaults to a fixed project constant (e.g. 50); user may change it from a fixed set
(25 / 50 / 100).
- The server response includes: `total_count`, `page`, `per_page`, `total_pages`.
- Display: "Showing 51100 of 342" — always show the range and total.
- Prev/Next buttons are disabled (not hidden) at the boundary pages.
- Direct page-number input is optional; if present it clamps to `[1, total_pages]` on blur.
- Changing `per_page` resets to page 1.
## Reuse Rule ## Reuse Rule
If a pattern needs table selection + bulk actions, it must: If a pattern needs table selection + bulk actions, it must: