feat(repolint): line-cap CI gate with ratchet baseline #193

Open
opened 2026-07-22 00:01:41 +02:00 by dominik.polakovics · 0 comments

Adopt a per-file line-cap policy, enforced as a hard CI gate with a ratcheting baseline for existing violators. Line count is a gate, not a diagnosis: the underlying pains are merge conflicts between concurrent lab agents, degraded AI edits on huge files, and review load. Churn×size data picked the thresholds; line count is the only PR-local, deterministic metric CI can enforce.

Caps

File class Classification rule Cap
Generated linguist-generated=true via git check-attr, or // Code generated .* DO NOT EDIT\. within the first 10 lines (Go convention) exempt
Reference/docs path under docs/** exempt
Tests *_test.go, *.test.ts, *.test.tsx 2000
CSS *.css 2000
Source every other tracked *.go, *.ts, *.tsx 1000

Numbers are grounded in the repo distribution (2026-07-21): source p95 ≈ 700 (Go 740 / TS 629 — near-identical, hence one shared number), tests p95 ≈ 1037. Caps sit ~1.4–2× above p95 so they only bite genuine outliers.

Deliverables

  1. Checker — a Go test (suggested: internal/repolint) that:

    • enumerates files via git ls-files (git is on PATH in CI per the D17 bar),
    • classifies per the table above,
    • fails if lines(f) > max(cap(f), baseline(f)), with a message that names the file, its count, its limit, and the two legal remedies (split the file, or — for a grandfathered file within headroom — nothing; growing past headroom has no remedy but splitting),
    • fails on stale baselines (the ratchet): a baseline entry is illegal if baseline(f) > max(cap(f), ceil(lines(f) * 1.05)). So shrinking a file forces the baseline down in the same PR, and once a file is at/under its cap the entry must be deleted — files leave the table forever, never re-enter.
    • supports regeneration: go test ./internal/repolint -update (or a small cmd) rewrites the baseline file tight (ceil(lines*1.05), entries only for over-cap files) so fixing a stale-baseline failure is one command.
  2. Baseline file — checked in (suggested: internal/repolint/baseline.json), seeded with ceil(size*1.05) at implementation time. Reference values from 2026-07-21 (recompute at branch time — these files churn daily):

    File Lines (07-21) Baseline
    web/src/routes/RunChat.test.tsx 4325 4542
    web/src/base.css 3945 4143
    web/src/routes/RunChat.tsx 2956 3104
    internal/afk/cycle_integration_test.go 2747 2885
    internal/tracker/forgejo/forgejo_test.go 2024 2126
    web/src/api.ts 1563 1642
    internal/agentapi/handlers.go 1430 1502
    internal/tracker/github/github.go 1235 1297
    web/src/routes/RepoSettings.tsx 1215 1276
    internal/tracker/forgejo/forgejo.go 1158 1216

    (web/package-lock.json is exempt as generated; docs/reference/lab-v0/handlers.go is exempt under docs/**.)

  3. .gitattributes — add web/package-lock.json linguist-generated=true. Future codegen must carry either the attribute or the Go generated-header.

  4. ADR 0051 — document the policy: the caps table, the ratchet semantics, and the split-quality bar: an over-cap file gets a decomposition issue naming one responsibility per proposed file before split code is written; splits named foo2/part2/single-caller helpers, or with heavy import fan-out between new siblings, are rejected in review. The gate is a tripwire; cohesion is judged by review.

Acceptance

  • go test ./... is red if any non-exempt tracked file exceeds max(cap, baseline), and red if any baseline entry is stale per the ratchet rule; green otherwise.
  • No changes to .forgejo/workflows/ needed — the checker rides the existing go test in both the native and nix gates.
  • -update regeneration produces a byte-stable file (sorted paths).
Adopt a per-file line-cap policy, enforced as a hard CI gate with a ratcheting baseline for existing violators. Line count is a *gate*, not a diagnosis: the underlying pains are merge conflicts between concurrent lab agents, degraded AI edits on huge files, and review load. Churn×size data picked the thresholds; line count is the only PR-local, deterministic metric CI can enforce. ## Caps | File class | Classification rule | Cap | |---|---|---| | Generated | `linguist-generated=true` via `git check-attr`, or `// Code generated .* DO NOT EDIT\.` within the first 10 lines (Go convention) | exempt | | Reference/docs | path under `docs/**` | exempt | | Tests | `*_test.go`, `*.test.ts`, `*.test.tsx` | 2000 | | CSS | `*.css` | 2000 | | Source | every other tracked `*.go`, `*.ts`, `*.tsx` | 1000 | Numbers are grounded in the repo distribution (2026-07-21): source p95 ≈ 700 (Go 740 / TS 629 — near-identical, hence one shared number), tests p95 ≈ 1037. Caps sit ~1.4–2× above p95 so they only bite genuine outliers. ## Deliverables 1. **Checker** — a Go test (suggested: `internal/repolint`) that: - enumerates files via `git ls-files` (git is on PATH in CI per the D17 bar), - classifies per the table above, - **fails** if `lines(f) > max(cap(f), baseline(f))`, with a message that names the file, its count, its limit, and the two legal remedies (split the file, or — for a grandfathered file within headroom — nothing; growing past headroom has no remedy but splitting), - **fails on stale baselines** (the ratchet): a baseline entry is illegal if `baseline(f) > max(cap(f), ceil(lines(f) * 1.05))`. So shrinking a file forces the baseline down in the same PR, and once a file is at/under its cap the entry must be deleted — files leave the table forever, never re-enter. - supports regeneration: `go test ./internal/repolint -update` (or a small cmd) rewrites the baseline file tight (`ceil(lines*1.05)`, entries only for over-cap files) so fixing a stale-baseline failure is one command. 2. **Baseline file** — checked in (suggested: `internal/repolint/baseline.json`), seeded with `ceil(size*1.05)` at implementation time. Reference values from 2026-07-21 (recompute at branch time — these files churn daily): | File | Lines (07-21) | Baseline | |---|---|---| | web/src/routes/RunChat.test.tsx | 4325 | 4542 | | web/src/base.css | 3945 | 4143 | | web/src/routes/RunChat.tsx | 2956 | 3104 | | internal/afk/cycle_integration_test.go | 2747 | 2885 | | internal/tracker/forgejo/forgejo_test.go | 2024 | 2126 | | web/src/api.ts | 1563 | 1642 | | internal/agentapi/handlers.go | 1430 | 1502 | | internal/tracker/github/github.go | 1235 | 1297 | | web/src/routes/RepoSettings.tsx | 1215 | 1276 | | internal/tracker/forgejo/forgejo.go | 1158 | 1216 | (`web/package-lock.json` is exempt as generated; `docs/reference/lab-v0/handlers.go` is exempt under `docs/**`.) 3. **`.gitattributes`** — add `web/package-lock.json linguist-generated=true`. Future codegen must carry either the attribute or the Go generated-header. 4. **ADR 0051** — document the policy: the caps table, the ratchet semantics, and the **split-quality bar**: an over-cap file gets a decomposition issue naming one responsibility per proposed file *before* split code is written; splits named `foo2`/`part2`/single-caller `helpers`, or with heavy import fan-out between new siblings, are rejected in review. The gate is a tripwire; cohesion is judged by review. ## Acceptance - `go test ./...` is red if any non-exempt tracked file exceeds `max(cap, baseline)`, and red if any baseline entry is stale per the ratchet rule; green otherwise. - No changes to `.forgejo/workflows/` needed — the checker rides the existing `go test` in both the native and nix gates. - `-update` regeneration produces a byte-stable file (sorted paths).
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#193
No description provided.