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>
This commit is contained in:
2026-03-01 16:39:39 +03:00
parent 8f28cfeac2
commit 40d1c303bb
7 changed files with 451 additions and 6 deletions

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.
- Export format should be explicit (`csv`, `json`, etc.).
- Download response should set:
- `Content-Type`
- `Content-Disposition`
- If CSV targets spreadsheet users, document delimiter and BOM policy.
- Download response must set:
- `Content-Type: text/csv; charset=utf-8`
- `Content-Disposition: attachment; filename="..."`
## 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

View File

@@ -75,6 +75,30 @@ Canonical mapping:
- right-aligned action 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
If a pattern needs table selection + bulk actions, it must: