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:
@@ -5,7 +5,53 @@ Read and follow the project Bible before making any changes:
|
||||
**[`bible/README.md`](bible/README.md)**
|
||||
|
||||
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: "51–100 из 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 = ';'`
|
||||
|
||||
73
kit/patterns/go-background-tasks/contract.md
Normal file
73
kit/patterns/go-background-tasks/contract.md
Normal 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 (1–3 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 // 0–100
|
||||
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: 1–3 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
|
||||
82
kit/patterns/go-code-style/contract.md
Normal file
82
kit/patterns/go-code-style/contract.md
Normal 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.
|
||||
110
kit/patterns/go-database/contract.md
Normal file
110
kit/patterns/go-database/contract.md
Normal 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.
|
||||
64
kit/patterns/go-logging/contract.md
Normal file
64
kit/patterns/go-logging/contract.md
Normal 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)
|
||||
```
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 (300–500 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 51–100 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:
|
||||
|
||||
Reference in New Issue
Block a user