Skip to content

Scaffold the app under the full quality leash, with structural ratchets - #21

Merged
cjimti merged 8 commits into
mainfrom
feat/1-quality-leash-scaffold
Aug 3, 2026
Merged

cjimti merged 8 commits into
mainfrom
feat/1-quality-leash-scaffold

Conversation

@cjimti

@cjimti cjimti commented Aug 3, 2026 •

Copy link
Copy Markdown
Member

Closes #1. Closes #19 (merged in via #22).

The first line of Go in this repo is born under the full quality leash. make verify is the single CI-parity gate: every check it runs has an equivalent CI job at the same threshold.


1. The app scaffold (#1)

Go module github.com/txn2/m6t, Wails v2.13.0 with React + TypeScript + Vite. Single window, app id com.txn2.m6t, placeholder UI.

main.go              composition root: embeds frontend/dist, hands options to Wails
internal/app/        the Wails binding layer — the bound object and window options
internal/buildinfo/  link-time build identity; a dependency root
frontend/            React + TS (Vite), eslint complexity gates, vitest
frontend/wailsjs/    generated bindings, committed, staleness-gated

The UI reads the build identity across the bridge and says so when it is detached from the runtime — the placeholder proves the bridge, the embed and the ldflags stamp all work end to end.

The bound surface is one method. Wails exports every exported method of a bound object to TypeScript, so App keeps exactly what the UI calls (Version()). An earlier draft exported Options and Context and dragged the entire Wails options tree into models.ts.

2. The leash (#1)

make verify runs: tools-check fmt test coverage-report patch-coverage lint security semgrep licenses frontend-lint frontend-test bindings-check build-check dead-code. verify-release adds gremlins at 60% efficacy.

Gate Floor
Total coverage 80%
Patch coverage (changed lines) 85%
Cyclomatic / cognitive complexity 10 / 15, both languages
Mutation efficacy 60% (verify-release)
Licenses MIT / BSD / Apache-2.0 / ISC / 0BSD
ESLint suppressions 0

Tool versions are pinned (golangci-lint v2.11.4, gosec v2.28.0, gremlins v0.6.0, wails v2.13.0) and tools-check refuses to run when a local version drifts from CI's.

Drift-proofing: pins_test.go fails the build when a gate figure is stated two different ways across the Makefile, ci.yml, codecov.yml and CONTRIBUTING.md — including the cross-language complexity budgets and the claim that verify runs every gate CI runs.

3. Structural ratchets (#19)

Ten plain Go tests in the repo root. The per-function linters all evaluate code inside one function; a god-package of a hundred tidy functions passes every one of them. These bound what those linters cannot see.

Gate Bounds File
Package size Lines and files per package package_budget_test.go
Package pin Every package has a ratchet entry package_budget_test.go
Exported surface Package-scope exported identifiers surface_budget_test.go
God-object Fields/methods on the App coordinator godobject_budget_test.go
Dead package Everything reachable from main package_graph_test.go
Import graph What may depend on what package_graph_test.go
No-op interface Interfaces implemented only by stubs noop_interface_test.go
Integration guard Tagged tests actually execute integration_guard_test.go
Frontend ratchet ESLint suppressions only shrink frontend_ratchet_test.go
Wiring guard The gates above still run structural_gates_test.go

Pinned actuals: App 1 field / 1 method · internal/app 67 LOC, 2 exported · internal/buildinfo 74 LOC, 2 exported · root 22 LOC, 0 exported · suppressions 0.

4. CI fixes found by CI

Three failures surfaced on this PR that no macOS-local gate could catch. Each is fixed here, and two of them are now impossible to reintroduce.

Linux webview linkage. Wails defaults to webkit2gtk-4.0, EOL and unpackaged on Ubuntu 24.04. Fixed with the webkit2_41 build tag, set in both the Makefile (uname -s) and the CI matrix so they cannot drift.

CodeQL on a private repo. First failure was Resource not accessible by integration — a job-level permissions: block replaces the top-level read-all, so the job ran without actions: read. Fixing that exposed the real blocker: Advanced Security must be enabled. Code scanning upload needs GHAS, which private repos do not have. The analysis still runs and scripts/codeql-gate.py still fails the build on any blocking alert; only the Security-tab UI is lost. Flip upload: never back to always when this repo goes public or GHAS is enabled.

The untracked-file blind spot — the important one. git diff cannot see untracked files, so any gate built on it skips every NEW file silently, and a silent skip is indistinguishable from a pass. make lint had this hole and reported green on ten unlinted files that CI then rejected. bindings-check had it too: a newly generated binding file is untracked, so it would report the bindings current while a whole module was missing from them — live ammunition for #2.

Fixed with one shared guard (scripts/require-tracked.sh) behind lint, bindings-check and patch-coverage, replacing the single copy that had been written for one gate and not its neighbors. TestGitDiffGatesRequireTrackedFiles now fails the build if any Makefile target runs git diff without calling it first.


Verified by exercise, not by assertion

Every gate below was run against a deliberate violation and reverted.

Leash:

Violation Result
Delete a test 21.4% coverage, coverage-report fails
COVERAGE_MIN 80→81 in one place 3 disagreement failures
Empty PATH tools-check prints all 9 install commands
MPL-2.0 dependency Not allowed license MPL-2.0
Complexity-12 function eslint fails; --suppress-rule baselines it
Add a bound method bindings-check reports `App.d.ts

Ratchets:

Violation Result
Extra field on App 2 fields, exceeding the ceiling of 1
Extra method on App 2 methods, exceeding the ceiling of 1
New exported identifier exports 3 identifiers (App, Options, Probe)
Unimported internal/orphan not reachable from main + no entry in structuralPins
3 ESLint suppressions exceeding the ceiling of 0
Tagged test, no runner execute nowhere and rot silently
200 filler lines 269 LOC (ceiling 200)
main importing buildinfo imports [...], pinned as [internal/app]
Gate file given a build tag excluded from the default test run

Guards:

Violation Result
Untracked .go file make lint refuses, names the file
Untracked binding file make bindings-check refuses, names App.js
Unguarded git diff target new gate reports it by name

Each gate also has a unit test on its own metric — a detector that never fires and a working gate look identical from the outside.

A git archive of the tracked files builds, tests and packages the .app, so the scaffold stands up from a clean clone.


Judgment calls worth challenging

  • ignore frontend/node_modules in go.mod. npm packages ship Go source (flatted/golang) which ./... pulled into the build — total coverage read 6.9% before this was caught.
  • LOC ceilings carry headroom while every count gate is pinned exactly. A line ceiling at the current count is a freeze, not a ratchet. Seeded as policy, documented at locCeilingNote, re-pin after PTY service #2/Project registry and tabs #5.
  • App's 1/1 ceilings will rise as services land — one composed handle per PR, on that line, with the reason. What the gate stops is the accumulation nobody decided on.
  • The import graph is pinned here and in depguard. Deliberate: the test survives a lint-config edit.
  • No-op detection matches by method-name set, not full type checking, to avoid pulling in x/tools. Conservative direction, documented at the gate.
  • CodeQL is not in verify — a database build takes minutes. It runs in CI and via make codeql.
  • Dropped the template's Nunito font. OFL is not on the allowlist; shipping it while claiming a permissive-only inventory would make the gate a lie.

Known follow-up

scorecard.yml will fail once this merges to main: publish_results: true requires a public repository. Not fixed here — it needs the same public/private decision as the CodeQL upload.

Out of scope

Packaging (#16). Re-pinning the LOC ceilings against real service packages (#2, #5).

cjimti added 4 commits August 2, 2026 22:14
Wails v2 + React/TS scaffold, plus make verify as the CI-parity gate:
pinned tools, 80% total / 85% patch coverage, golangci-lint with
depguard layering and revive guardrails, gosec/govulncheck/semgrep,
license allowlist, frontend complexity gates, and pin-drift tests that
fail when a gate figure is stated two different ways.

Closes #1
Wails defaults to webkit2gtk-4.0, which is EOL and not packaged on
Ubuntu 24.04, so the Linux smoke build failed to link. DESIGN.md §9
names 4.1 as the Linux dependency; the webkit2_41 tag is what makes the
build honour that. Set in the Makefile for local Linux builds and in the
CI matrix so both sides use the same flag.
The analysis succeeded but the SARIF upload failed with 'Resource not
accessible by integration': on a private repository that step calls the
workflow-runs API, which needs actions: read. A job-level permissions
block replaces the top-level read-all rather than adding to it, so the
job was running without it.
Ten plain Go tests in the repo root that make architectural decay a build
failure rather than a review opinion: package size, the pinned package
list, exported surface, the App coordinator's field/method ceilings,
dead-package detection, the pinned import graph, no-op-only interfaces,
an integration-tag guard, the ESLint suppressions ratchet, and a guard
that the gates themselves still run.

Ceilings are pinned at measured actuals and only move down. Two
exceptions are documented at the constant: LOC ceilings carry headroom
(pinning a line count is a freeze, not a ratchet) and re-pin once #2/#5
land, and the coordinator's ceilings rise one composed handle at a time
as services arrive.

Each gate has a unit test on its own metric, because a detector that
never fires and a working gate look identical from the outside.

Closes #19
cjimti added 4 commits August 3, 2026 00:04
`git diff` cannot see untracked files, so a gate built on it skips every
NEW file silently — and a silent skip is indistinguishable from a pass.
`make lint` had this hole and reported green on ten unlinted files that
CI then rejected. bindings-check had it too: a newly generated binding
file is untracked, so the gate would report the bindings current while a
whole module was missing from them.

One shared guard (scripts/require-tracked.sh) now backs lint,
bindings-check and patch-coverage, replacing the single copy that had
been written for one gate and not its neighbors. A new structural gate
fails the build when any Makefile target runs `git diff` without calling
it first, so the class cannot be reintroduced.

Also fixes the 11 lint issues CI reported (8 misspell, modernize,
wrapcheck, gocritic) plus the British spellings CI had not reached yet.
Code scanning upload requires GitHub Advanced Security; on a private
repository the analysis succeeds and the upload fails with 'Advanced
Security must be enabled'. The analysis is what finds bugs, so it keeps
running and scripts/codeql-gate.py still fails the build on any blocking
alert — only the Security-tab UI is lost. Flip upload back to always
when the repo goes public or GHAS is enabled.
@cjimti cjimti changed the title Scaffold the app under the full quality leash Scaffold the app under the full quality leash, with structural ratchets Aug 3, 2026
@cjimti
cjimti merged commit d4d8944 into main Aug 3, 2026
9 checks passed
@cjimti
cjimti deleted the feat/1-quality-leash-scaffold branch August 3, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Structural ratchet gates: budgets, god-object ceilings, dead-code detection Scaffold with the quality leash: make verify, CI parity, license gate

1 participant