Story: Harden the gates the component clean standard rests on
Table of Contents
This page documents a story in Sprint 26. It captures the goal, current status, acceptance criteria, and the tasks that compose it.
1. Goal
A component clean-up passes or fails on what a gate can see. The ores.service clean-up took three verification rounds, and every round failed on something no gate could see: a generated header the drift gate does not cover, a test case the compiler never received, and a record whose numbers went stale when trunk moved. Each round cost a full review cycle, and each found a defect the previous round had declared clean.
This story turns each of those manual checks into a gate. A gate is the only
form of the check that survives the reviewer: it runs on every pull request, it
runs on a tree nobody measured by hand, and it fails in the same way for the
next component as it did for ores.service.
Three classes, one task each:
- Generated output no gate sees. The drift gate regenerates the components the registry lists, and 4,892 of the 5,829 files carrying the generated marker are outside that list. A rename lands in one component's model and writes into another component's tree, where no gate looks.
- A test case that never reaches the compiler. A
TEST_CASEinside a false conditional does not exist. The file compiles, the link succeeds, ctest reports green, and only a case count nothing compares against betrays it. - A record whose numbers go stale on rebase. Every number in a clean-up record is measured at a head that trunk then moves under.
2. Status
| Field | Value |
|---|---|
| State | DONE |
| Parent sprint | Sprint 26 |
| Now | Nothing. |
| Waiting on | Nothing. |
| Next | Nothing. |
| Last touched | 2026-09-27 |
3. Acceptance
- Each of the three classes is caught by a check that runs on a pull request, with no step a human performs by hand.
- Each new check has a test that fails when the check stops finding anything, so a gate cannot pass every tree by finding nothing.
- Every check the standard names as the proof of an item is in the same CI job the item's story relies on.
4. Tasks
| Task | State | Start | End | Description |
|---|---|---|---|---|
| A test file whose cases are compiled away reports green | DONE | 2026-09-27 | 2026-09-27 | A TEST_CASE that never reaches the compiler is invisible: the file builds, ctest passes, and only the case count betrays it. The ores.service readiness tests sat inside a BOOST_ASIO_HAS_LOCAL_SOCKETS guard placed above every boost::asio include, so the macro was undefined when the guard was read and the file compiled to nothing. ctest reported 39 cases where the sources held 42. Nothing compares the two. |
| Sweep every catalogue component for drift the registry cannot see | DONE | 2026-09-27 | 2026-09-27 | The drift gate's coverage is the registry, which lists ten components; the catalogue holds twenty-four. Output that an archetype routes into another component's tree, and any generated file under a component the registry does not list, has no gate. Add a dry-run sweep over every catalogue component that fails only on files the branch touched, so a cross-component rename cannot pass review unseen. |
| Mark generated recipes, and give them a gate | DONE | 2026-09-27 | 2026-09-27 | The recipe templates carry a codegen marker as a Mustache comment, which Mustache strips, so no generated recipe reaches the tree with one: 247 files sit at the generated recipe path shape and 172 carry the tangle block a generated recipe exports, none carries a marker, against 6,478 tracked C++ files that do. The sweep renders per technical space, so it never produces a recipe and cannot cover them either. Emit the marker at the render seam and render the whole ores address. |
| Make a measured number name its command and its commit | DONE | 2026-09-27 | 2026-09-27 | A clean-up record is a claim about a tree, and its numbers are the part a reviewer checks least and a verifier re-runs first. When trunk moves under a branch a count, a gate's component count and a suite's size all change with no code changing, and the record goes quietly wrong: the ores.service record was failed twice by verifiers for exactly this. The task template has nowhere to put a measured number and the standard's Evidence column says 'kept on the task', which is not the same claim as naming the command and the commit. |
| Check that a model's pasted block names symbols that exist | DONE | 2026-09-27 | 2026-09-27 | A model can paste hand-written C++ into generated output through an :implements block, and the block is compiled only when its component is regenerated. So a cross-component rename can leave a block naming a symbol that no longer exists, with no compile error, until somebody regenerates that component and the build breaks. That is the measured ores.service defect. The sweep catches it only when the generated file disagrees with its model; this catches the model itself. |
5. Decisions
- Ban the construct rather than detect the instance. The capture this work
came from proposed preprocessing each test source with its own flags from
compile_commands.json. That is exact, and it cannot be a pull-request gate: the codegen job has no C++ build, and the job that has one runs on a schedule and on tags. A rule that forbids aTEST_CASEinside a conditional block needs no flags, no build and no formatter, and it makes the class impossible instead of reporting it. The cost is six cases in three files, and the change makes those cases more visible than they were, not less. - A static rule reads the tree the way the compiler does, or it is a rule about
the files it happens to open. The first cut of the gate read only
*_tests.cppand treated a backslash-newline as ordinary text. An independent verifier defeated both:#\followed byif 0hid a case, and a case in an included fragment was never read at all. The gate now resolves the splices of translation phase 2 for the whole file before it looks for anything, reads every C++ file under atests/directory, and fails on a file whose conditionals do not balance, because a guard left open deletes a case in whichever file includes it. - A recorded number names its command and its head, and the verb that would
re-run them is not built. A clean-up record's counts go stale on their own
when trunk moves, so the task template carries a Measurements table with a
command and a commit per row and the standard says a count alone is not
evidence. The capture also proposed a
compass task verifythat re-runs a record's declared commands. It is not built: records legitimately carrycheck_component_drift.py --dry-run, whose non-dry-run form regenerates a component in place, and a verb that runs whatever a document declares would eventually run a destructive one where nobody expects to be asked. The route that would earn its place is a whitelist the gate scripts publish themselves. - Where the rule cannot read a file, it fails instead of guessing. A compiler takes a raw string literal's extent from the spliced text and its content from the source, while the gate removes every splice. A splice at a literal's opening, or inside its content, can therefore move where the literal ends, and the gate then reads string content as code or code as string. Rather than pick one reading, it reads the raw strings of both texts and reports the file where they disagree. A gate whose silence means clean has to be loud about the inputs it cannot model, or its silence stops meaning anything. The first attempt at this claimed the source reading failed safe, and it did not: the claim was false in the direction that matters, which is how a later verification round found a silent green pass over a real deleted case.
6. Out of scope
- The registry tier for model-free components, which is what would let
ores.serviceand its five siblings intoCOMPONENTS_UNDER_TEST. Recorded in the drift-gate capture. has_permission()returning true for a token that carries no permissions, a fail-open on an authorization check kept as a recorded pre-RBAC compatibility rule.