Repository navigation
Add the structural ratchet gates - #22
Merged
Merged
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #19. Stacked on #21 (
feat/1-quality-leash-scaffold) — #19 needs the Makefile,make verifyandinternal/, none of which exist onmainuntil #21 merges. Merge #21 first, then this retargets tomaincleanly.What this adds
Ten structural gates, as plain Go tests in the repository root. No external tooling, no new dependencies —
make testruns them, somake verifygates on them.The per-function linters (gocyclo, gocognit, revive) all evaluate code inside one function. A god-package assembled from a hundred small, tidy functions passes every one of them. These gates bound what those linters cannot see: how big a package is, how much it exports, how many packages exist, what depends on what, and how much state the coordinator holds.
package_budget_test.gopackage_budget_test.gosurface_budget_test.goAppcoordinatorgodobject_budget_test.gomainpackage_graph_test.gopackage_graph_test.gonoop_interface_test.gointegration_guard_test.gofrontend_ratchet_test.gostructural_gates_test.goCeilings, pinned at measured actuals
Reproduce:
go test -count=1 -run 'TestPackageSizeBudget|TestPackageExportedSurfaceBudget|TestAppGodObjectBudget' -v .Ceilings only move down. There is no suppression comment and no escape hatch — raising one belongs in the PR that needs it, on that line, with the reason. The justification in review is the mechanism.
Negative-case proof
Every gate was run against a scratch violation and reverted. None of these are hypothetical:
AppApp has 2 fields, exceeding the ceiling of 1AppApp has 2 methods, exceeding the ceiling of 1internal/app exports 3 identifiers (App, Options, Probe)internal/orphannot reachable from main through non-test importsno entry in structuralPins; add one in this PRsuppresses 3 violations (complexity x3), exceeding the ceiling of 0execute nowhere and rot silently269 LOC (ceiling 200)mainimporting buildinfoimports [internal/app internal/buildinfo], pinned as [internal/app]excluded from the default test runEach gate also has a unit test on its own metric —
TestCountGoFileDetectsGeneratedCode,TestExportedNamesCountsPackageScopeOnly,TestGodObjectMetricCountsGroupedAndEmbeddedFields,TestReachabilityFollowsTransitiveImports,TestNoopDetectionDistinguishesStubsFromBehaviour,TestIntegrationTagDetection,TestSuppressionCountingSumsEveryEntry,TestWiringDetectorsFire. A detector that never fires and a working gate look identical from the outside; these pin the difference.Two deliberate deviations from a literal reading of the issue
1. LOC ceilings carry headroom. The issue says every ceiling equals today's actual with zero slack. For line counts that is a freeze, not a ratchet — one more line of doc comment in
buildinfo.gowould fail the build. They are seeded as policy (200 / 150 / 60, roughly 2–3x current) and documented atlocCeilingNotewith the reasoning, to re-pin against real measurements once #2 and #5 land. Every count gate — fields, methods, packages, exported names, suppressions — is pinned exactly, because those do not grow through ordinary editing.2. The
Appceilings at 1/1 will rise as services land. That is the mechanism working: each backend service arrives as one composed handle, in a PR that says so on that line. What the gate stops is the accumulation nobody decided on — six loose fields where one owner struct belonged. It does mean #2 and #5 will each touch these numbers.Scope notes
verify.x/tools. That is the conservative direction: a false positive needs a type that shares every method name with an interface and has nothing but empty bodies — itself the smell being hunted. Documented at the gate..golangci.yml. Deliberate duplication: the test survives a lint-config edit.Also in this PR
CONTRIBUTING.mddocuments all ten gates, the zero-slack policy, and both deviations.pins_test.gogains an agreement check so the ESLint suppressions ceiling stated in CONTRIBUTING cannot drift from the gate.skipDirno longer prunesbuild/(a pruned directory is a place a package could live outside every ceiling), andhasBuildConstraintnow stops at the package clause so a gate file holding a//go:buildstring as a fixture does not read as constrained itself.Verification
make verifygreen. Coverage unchanged — every file here is_test.go, so nothing enters the coverage denominator.