Repository navigation
Phase 9: cross-cutting invariants — the checklist, the audit's verdict and the documentation - #101
Conversation
Phase 9's instrument. Four suites beside phase 8a's transport one, over one shared Runner so the five statuses are decided in one place: InvariantSuite (28 assertions over all twenty-four XCUT ids, ten private_constant groups), PackagingSuite (8 over NFR-1, 2, 3, 10, 11, 13, 14 and 15, read from published gem metadata rather than a source gemspec), CodecSuite (2 portable seam properties, re-derived from the requirement text rather than lifted out of dexpace-serde-json, whose files are untouched) and ExecutorSuite (7, SEAM-25's harness half among them, with ASYNC-3 written so it genuinely fails). Report gains .merge, #to_h and the MUST-level vacuity blocker: Levels carries every requirement id's normative level, generated from appendix C, and a vacuity on a MUST fails the run until an acceptance names that id with a citation. SharedInstance is XCUT-11's structural predicate, R8's disjunction, with the declaration supplied by the driver and never read off the subject. Aggregate gives a whole run one verdict and a preamble that prints what green does not prove; APPENDIX_B.md is the 61-row coverage map. Three repository-wide invariant scans become blocking gates over one RubyVM::AbstractSyntaxTree walker -- gates:cause_walk, gates:bounded_map and gates:seam_names -- taking the set to twenty-one, all in DEFAULT_GATES and all in ci.yml's gates job. gates:serde_boundary now asserts its PENDING list empty, which is the clause SSE-37 handed forward.
Review round 0, R0-1 and R0-2. R0-1. Three waits in the phase-9 suites carried no bound, so a subject that never answers parked the whole run instead of failing an assertion. ExecutorSuite::Shutdown#check_release opened with an unbounded `transport.entered.pop`: an executor that REFUSES the post -- ASYNC-2's saturated queue, or a closed pool -- pushes nothing there and the poster rescues the refusal, so the run hung and the ensure that frees the gate and closes the pool was never reached. The entry pop now carries the assertion's own bound and an expired one is :vacuous with its reason: with no unit started there is no "blocking task on a worker thread", which is the antecedent ASYNC-3's sentence opens with, and a MUST-level vacuity blocks the report anyway. The two `.each(&:join)` sites take the same treatment against a subject whose submission or whose shared call never returns -- ExecutorSuite's concurrent_post and InvariantSuite's drive_threads -- each as a Failure naming the clause, with the whole thread set sharing one budget so sixteen stuck threads cost one bound and not sixteen. That is 8a's rule for this gem, which await_closed_connection's `timeout:` fixed: an adapter that never releases fails the assertion instead of hanging. R0-2. PackagingSuite's NFR-14 assertion documented "with no source supplied it is :vacuous with that reason, never a pass, because 'nobody told us' is not evidence of a single source" and did not implement it -- with no `versions:` every want was nil, every unit was skipped and the check passed having proved nothing. It now raises the vacuity the comment names, and the comment says what a PARTIAL source means too.
Found by writing review round 0's R0-4 test, which drives the shipped DEFAULT_RESOLVE for the first time: every case in packaging_suite_test.rb supplies its own `resolve:`, so the lambda a real driver gets had never run. RubyGems raises Gem::MissingSpecError for a name it cannot resolve, and that descends from Gem::LoadError < LoadError < ScriptError -- it is not a StandardError. So `rescue ::StandardError` let it past, and it escaped Runner's own bare rescue too, aborting the whole run where the suite's contract is one :vacuous result naming the absent unit. Measured on 4.0.6: a Suite.run naming an uninstalled gem errored out of the first assertion instead of reporting eight vacuities. The rescue now names Gem::LoadError beside StandardError, and the comment says why the explicit class is load-bearing.
…oute InvariantSuite::Models#only_closes_what_it_created makes two Checks against one caller-supplied resource: it was not closed, and it still works. `Borrowed#usable?` was `!@closed`, the exact negation of the `closed?` the first Check already reads, so no subject the suite can build separated the two and replacing the second Check's condition with a literal `true` left the whole gem's suite green. The code comment and the phase checklist both claimed the opposite -- that a component tearing the resource down another way would pass the first alone. `Borrowed` now carries that other way. `#finish` is the adapter-side teardown reached without going through `#close` -- `Net::HTTP` spells it exactly that, a pooled client spells it "retire" -- and `usable?` reads both flags, so a holder that shuts a borrowed resource down without calling `#close` passes the first Check and fails the second. The comment is rewritten to say what the double now does and how the gap was measured.
Every assertion is driven against a deliberately non-conforming double before a conforming one makes it pass, which is what proves it can fail at all; the four suites' own suites are that, plus the runner's status loop, the report's blocker, the predicate's disjunction and the aggregate's declaration-keyed map. Three first-party drivers, one per suite with a subject in this repository: core's for InvariantSuite (fifteen declared shared instances and the six factories the suite cannot build), dexpace-serde-json's for CodecSuite, and dexpace-async-thread's for ExecutorSuite, which waives ASYNC-3 by id and then runs it unwaived in a second test that requires the failure, so the waiver cannot outlive the limitation it records. The three new gates get a fixture workspace under DEXPACE_GATE_ROOT and gates:serde_boundary's abort branch a RUBYOPT prelude, because neither had a failing-fixture route at all -- and building the first found that the three gates opened every file relative to the process's CWD.
Review round 0, R0-1 through R0-5, plus the defect R0-4's test found. R0-1. Three tests for the three waits that carried no bound, each driving ONE assertion: a subject that blocks or refuses everywhere is non-conforming in several ways and these are about the bound. An executor that refuses the post makes ASYNC-3 :vacuous; one whose #post blocks fails SEAM-12; a shared instance whose #call never returns fails XCUT-11's first clause. Each double reports its own parked threads, so the release joins every one and the thread count is where teardown expects it. R0-2. The NFR-14 case now asserts the :vacuous its own name always claimed, with the report's reason, and a second case covers the partial source the assertion's comment describes. R0-3. The interpreter-facts test asserted a warning COUNT of one and was red on the declared 3.2 floor, whose parser also reports `unused literal ignored` for the same source. The fact is which $VERBOSE value silences the class, so the assertion is presence and the comment records the measurement. R0-4. PackagingCase::DEFAULT_RESOLVE is public locked surface and ran in no test, because every case supplied its own `resolve:`. It does now -- and the first run of it found the defect the accompanying code commit fixes, which is the whole reason a shipped default needs a test. R0-5. RequirementLevels::ROW's nineteen-prefix restriction was asserted by nothing: loosening it to `[A-Z]+-\d+` left the map at 645 rows over this repository's own appendix C. A synthetic table holding an RFC number and an ISO date in the id column is what discriminates, matching the case the sibling scanner in tools/appendix_b.rb already carries.
Two shapes from the brief's own minimum mutation list survived round 1, and both survived because every double in the file emitted exactly one kind of payload or derived one observation from the other. ChattyPool logs a non-shutdown event payload and a bare String message at construction, beside its one shutdown on close. It is conforming, so the whole report stays green -- and replacing ExecutorCase#shutdowns' Keys::EVENT match with a bare `entries.count` now reddens SEAM-25, XCUT-13's second half and XCUT-22's no-shutdown half together, which is the difference between "one shutdown event" and "one sink write". TearingSeam tears its borrowed resource down through the teardown beside `#close` rather than through `#close`, so `closed?` stays false and only XCUT-22's second Check can report it. Replacing that Check's condition with a literal `true` now fails this test, where before it left the whole gem's suite green. The existing overreaching-holder test gains the message assertion its twin carries.
Review round 2 measured five whole InvariantSuite assertions that could be neutralised with `rake test:gems` still green: XCUT-1, XCUT-2, XCUT-6, XCUT-7 and XCUT-10. Nothing in the repository drove a subject that made any of their clauses fail, so each reported :passed because it could not report anything else -- the failure mode this phase's own R3 exists to prevent. The file's header made it worse by naming XCUT-6, XCUT-7 and XCUT-10 among the IDs it drove. Thirteen defective stand-in cores, in the shape the file already uses: a Policy whose cancellation? reads only the outermost error and one that calls a cancellation retryable (XCUT-1); one matching the cancellation type exactly, so a subtype escapes, and one telling a timeout apart by its message (XCUT-2); a capability query reading only the outermost error and one matching a concrete type (XCUT-6); a configured set ANDed with the baked classifier and a default set that is not the six RETRY-13 fixes (XCUT-7); a safety gate that re-sends a bare POST and one taking a failure parameter (XCUT-10). Two more close the same gap where the reviewer's sweep found only warning-artefact coverage: a redactor forwarding a URL's userinfo and a default-ALLOW header list (XCUT-19), and an error-body snapshot that multiplies its cap (XCUT-24). Measured: neutralising each of the seven assertions whole and warning-free -- `return nil if subject` as the body's first line, so no local is orphaned and NFR-6's fatal-warning hook cannot mask the result -- reddens exactly the new tests for that assertion, thirteen across the seven. Unmutated the file is 32 runs and 66 assertions, and the gem's own suite is 275 and 1,089. The header now names only the IDs the file really drives, and says why XCUT-3 and XCUT-12 are not among them: a double for either would park a thread past the assertion's own bound and leak it into the base case's thread count, so their guards stay the two subject mutations.
Review round 3 swept every phase-9 assertion body one at a time and found one that no document accounted for: XCUT-18's model-layer `header_syntax_validation` could be neutralised whole with the entire `rake test:gems` green, because both of its committed tests pin the model assertion `:passed` and drive only the call-site one. An audit phase ships the claim that its assertions discriminate, and an assertion nothing proves can fail makes that claim false while the report counts it as evidence. Two defective stand-in cores close it, in the shape this file already uses. A permissive validator that refuses nothing reddens the NAME clauses and every VALUE clause but one. A symmetric validator that applies the NAME rule to outbound values as well reddens exactly the HTAB clause, so the asymmetry that IS the requirement -- a name must reject horizontal tab, an outbound value must accept it -- has a control of its own rather than resting on the real subject. Proven by mutation: neutralising `header_syntax_validation` whole reddens both new tests, and deleting the HTAB-accepted check alone reddens only the second. The file is 34 runs / 74 assertions unmutated.
The checklist carries forty-one own rows and eleven cross-reference rows, the forty-five mutations run red with the message each produced, and the aggregate run's verdict: 43 passed, 1 failed, 0 vacuous, 1 waived. The one failure is NFR-13 -- 307 of 307 shipped .rbs files carry no SPDX header -- which phase 10 already owns by name and which is verified still owned rather than re-filed. The design gains an As-built addendum, P9-21 to P9-33. One docs/first-release.md blocker closes: gates:bounded_map is green, and async_http's Clients is a verified false positive of the scan rather than a silenced defect. Six findings go to phase 10's inbound list, none repaired. One corpus note corrects an earlier phase-9 entry: gates:drain_loop was not built, so XCUT-14's drain clause ships with the behavioural assertion alone. CLAUDE.md, the two as-built pages, both READMEs and the conformance gem's README are re-derived from the tree; the knowledge-lookup skill gains this phase's audit group, which did not exist.
Review round 0, R0-6 through R0-12, plus the two code fixes' half that lives in the documents. R0-8. The status note gave 91.09 % as the code tip's 4.0.6 coverage and quoted the same figure correctly for 3.2.11 one clause earlier. It was the 3.2.11 number twice. Every proof figure in that paragraph is now re-measured at this round's tips rather than edited: the code tip is 4,020 runs and 94.67 % (11,688 of 12,346) on 4.0.6 and 90.97 % (11,105 of 12,207) on 3.2.11, the tests tip 4,189 runs and 99.82 %, and the sentence says outright which row each percentage belongs to, because that is the mistake it exists to stop repeating. The aggregate run was reproduced at these tips and is unchanged at 43 / 1 / 0 / 1, so the audit's verdict stands on this round's code and not only on last round's. R0-6. The ledger said forty-five mutations went red where the checklist said forty-four with one killed uncounted; the checklist was right. The count is now forty-four at implementation plus seven in this round, and the killed shape is named in the row rather than folded into a total. R0-7. CLAUDE.md's exclusion list stopped at phase 8c, so it asserted that phase 9's checklist -- added by this very stack -- was still to be written. R0-11: the gem README still called TransportSuite twenty-eight assertions; it has been thirty-four over twenty-eight TRANSPORT ids since 8c, measured from the loaded suite. R0-9. The audit-group row claimed no sleep in any new test. There is one, deliberate, in a non-conforming double whose whole job is to block past XCUT-13's half-second bound; the row now names it with its reason instead of denying it. R0-10: a paragraph sitting between rows 26 and 27 ended the mutation table as GFM reads it, leaving nineteen rows to render as run-on pipes. It moves below. R0-12. XCUT-19's row claimed clauses (a)-(d) and appendix C has five; XCUT-13's named two of three. Both scope-outs were already argued in the code and unstated in the row a reader checks. Each row now cites where the missing clause is proven -- 5b's HTTPLogging::DEFAULT for body logging, and 8.3's ban on arranging an interrupt at all. Rows 46-52 record this round's seven mutations with the message each produced. Rows 47 and 48 name what was mutated -- the Check inside the bounded join, not the call to it, which does not compile under -w -- and drop a failure count that did not reproduce: one, not thirteen.
Plan Task 15 Step 3 asks, in as many words, that a mark which disagrees with the design's prediction say so and name which prediction was wrong. The checklist recorded the observations and never the correction, so NFR-7, NFR-10 and NFR-17 read as transcriptions of a table that predicted the opposite. A paragraph above the NFR block and a clause in each of the three rows now name them, with NFR-4's narrower overturning and NFR-13's and NFR-16's predictions that held; P9-36 carries it in the As-built addendum, which had no row for it either. P9-37 gives the bounded-map allowlist re-adjudication the ledger row its six written reasons belonged in. Deviation 4 cited P9-27, which is the DEXPACE_GATE_ROOT repair and is what deviation 6 cites. P9-38 and P9-39 record the two guards review round 2 repaired, with their mutations as rows 53 and 54 and a departures subsection of their own; the mutation count moves from fifty-one to fifty-three here, in the design's P9-32 and in the roadmap's Guards paragraph, and says outright that the pair SURVIVED round 1 rather than being newly thought of. The As-built band is P9-21-P9-39 in both places that state it.
Round 2 added two tests and three lines of lib/, so every run count and both coverage denominators moved: the code tip is 11,689 of 12,349 on 4.0.6 and 11,106 of 12,210 on 3.2.11, the tests and docs tips 4,191 runs and 74,584 assertions at 12,327 of 12,349, and the 3.2 row 4,003 and 73,808 at 11,733 of 12,210. The paragraph is re-measured rather than patched, and it now records the seed and repeat runs it always implied. The aggregate run over all four suites with the real subjects reproduces unchanged at 43 / 1 / 0 / 1.
Three documents said something the tree does not. R6's boundary. The audit's own driver is one ADDED file under gems/dexpace-core/test/, and no document named it: the design's Module layout still says "no file in gems/dexpace-core/ at all" while its own P9-9 says "the driver is dexpace-core's own suite", and the checklist's and the roadmap's boundary sentences enumerated the trees phase 9 changed without it. All three now name the file and the narrow bend it is -- one added test file, no existing core file modified, renamed or deleted, git diff --name-status one path at status A -- and P9-40 records the contradiction by pointing at both sentences rather than editing either away. The (b) evidence claim. The checklist asserted over every XCUT row that the assertion had been driven against a deliberately non-conforming double, "which is what proves the assertion can fail at all". It was false for seven rows and, until this round's doubles, for five of them at whole-assertion scope. The paragraph now names the two rows where (b) is still a hand-run subject mutation -- XCUT-3 and XCUT-12, whose doubles would leak a parked thread -- and says what review round 3 added for the other seven. P9-41. The mutation set. "One mutation per assertion, per gate and per piece of shared machinery" was untrue as written: XCUT-2, XCUT-6, XCUT-7 and XCUT-10 had no row at all and XCUT-1 only subject rows. The preamble says so, rows 55-61 are the seven whole-assertion neutralisations that close it, and the count moves from fifty-three to sixty. Proofs re-measured at these tips: tests and docs 4,204 runs and 74,610 assertions at 99.82 % on 4.0.6 and 3.4.10, the same runs at 12,181 of 12,203 on 3.3.12, and 4,016 / 73,834 / 5 skips at 96.09 % on 3.2.11. The coverage denominators did not move, because the round added no lib/.
Review round 3's sweep neutralised all forty-five assertion bodies in the four suites one at a time and left four green. Three were accounted for -- XCUT-3's and XCUT-12's, which this checklist names, and a private clause helper whose parent assertion is caught. The fourth, XCUT-18's model-layer assertion, was named nowhere. The claim that covered it could not see it because it was counted in ROWS, and XCUT-18 carries two assertions, so the row was covered on one of them. The evidence paragraph now counts assertions, names the sweep as what makes it checkable, and says which of XCUT-18's two assertions each piece of evidence covers. Ledger row P9-42, deviation 16, and the mutation table gains rows 62 and 63 -- the total moves to sixty-two in all four places that state it. Two counts were wrong in the same direction, coverage being wider than claimed. The core driver's own factory returns TWENTY shared instances, not fifteen: eighteen frozen, which conform on frozen? alone, and two unfrozen that declare exactly what they hold. The checklist's XCUT-11 row and the as-built conformance page both said fifteen; both now read off the factory. Design R8's nine is a different list -- the subjects it names across five phase documents -- and stands. The stale "not counted among the fifty-three" is sixty-two.
Two false sentences in shipped documents, found by review round 4. R4-1: the phase-9 checklist stated NFR-14's normative level as MUST. Appendix C and the phase's own design table both say SHOULD. Only the Level cell moves; the row's verdict, task numbers and evidence sentence were already correct. Re-checked mechanically rather than by eye: every XCUT/NFR level cell in the checklist against tools/requirement_levels.rb's parse of appendix C -- 41 cells, zero differences, where the same check named NFR-14 and nothing else before the edit. R4-2: the pre-existing phase-10 inbound bullet on the async-http client cache still said Clients#@by_origin is an uncapped per-origin map and that gates:bounded_map "stays red until it lands". Both halves are false against the tree: 8c shipped the cap and the gate is green. Appended one dated bracketed correction in place, deleting and renumbering nothing, on 7a's precedent for a bullet the tree has falsified -- the as-built ivar is @by_key over a (reactor, origin) key, #fetch calls #drain inside the same @mutex.synchronize as the insert, #drain loops back to MAX_ORIGINS evicting closed reactors first, the gate is green over all 307 files with clients.rb allowlisted as a verified false positive of the scan, and the docs/first-release.md blocker is closed with no phase-10 repair owed.
Review round 4's R4-2 corrected the phase-10 inbound bullet itself. The same premise survived in two places the finding did not name, and both are in material this phase may write, so they are fixed rather than reported. The roadmap's narration of the 2026-09-13 routing pass cited the uncapped Clients#@by_origin as its example of a bullet whose repair is expected to be moot, and said such bullets stay unchanged until a green gates:bounded_map run closes them. That run happened on 2026-09-23, so the paragraph gains a dated bracketed note in place rather than a rewrite: the record of what that pass said is itself the point. docs/first-release.md's opening summary still called the blocker red and said phase 10's repair clears it. It is ticked at its own entry, closed by this phase's run against a cap 8c had already shipped, and no repair is owed.
Review record for the phase 9 stack (#99 → #100 → #101)5 independent reviews, each by a fresh agent with no memory of the previous one, each re-running every gate itself on every tip (4.0.6 every gate individually at the code tip and the full
Nothing was skipped. Round 0 → fixed in round 1
Round 1 → fixed in round 2
Round 2 → fixed in round 3
Round 3 → fixed in round 4
Round 4 (final) — open, carried into the PR bodies
What the final reviewer verified by experiment, both interpreters
Tips reviewed at the final round: code After the fifth reviewThe table's last row says round 4's two findings were "carried into the PR bodies". They were not — both were fixed, and the record should say by whom. Round 3 was the fourth review, which is where this lane's rule sends the stack up whatever the verdict. It did not, because Round 4 then surfaced two more, both documentation and both false sentences in shipped documents: the checklist stating Two more commits close them:
Nothing was skipped, and the leftovers in the PR bodies are leftovers by judgement, not by exhaustion: the |
Closes #33. Third PR of phase 9's stack — the phase record, the audit's verdict and the documentation — on top of #100. Phase 9 is unsegmented, so this PR closes the phase.
What lands
XCUT-1–24and all seventeenNFRs.P9-21–P9-33, beside the design's ownP9-1–P9-10; nothing renumbered. It also records four prediction-versus-observation corrections made before the disposition run, because a checklist written against a stale prior is the failure this phase exists to prevent:NFR-7,NFR-10andNFR-17's cited finding were already green onmain, and the "one true positive" the wholegates:bounded_mapnarrative rested on had been repaired by 8c.docs/first-release.md— thegates:bounded_mapblocker ticked and closed by this phase's own run, with no phase-10 repair owed.dexpace-conformanceis routed with its evidence rather than fixed:Registry#resolve's unbounded discovery loop (SEAM-5,XCUT-23);XCUT-12's fiber-scheduler form, unasserted because no first-party suite composes a credential path under a reactor;XCUT-9's two residuesgates:cause_walkproves neither absent; 7a'sSEAM-21evidence living inside an assertion body and declared in no ID-keyed map;tools/surface.rbbeing blind to aprivate_constantmodule (NFR-4); andPackagingCase's gem-name-to-constant rule covering none of this port's four exceptions (NFR-15).NFR-13's verdict, which is a real gap and is reported rather than repaired: 307 of 307 shipped.rbsfiles carry no SPDX header, in all six gems, inside the bundle and inside the built.gemfiles alike. A RuboCop cop parses Ruby and cannot reach.rbs, so nothing checked them. Already owned by phase 10's design addendumA8and its inbound row 8 — verified still owned, not re-recorded.docs/sdk-documentation/conformance.md(the four suites with run examples),quality-gates.md(three new rows, renumbered to 21),architecture.md, the gem README,README.md,docs/README.md,CLAUDE.md(the built-phases sentence, the twenty-one gates in every place that states the count, the conformance gem's clause), one new entry indocs/knowledge/notes/cross-cutting-invariants.md(nothing edited in place), and this phase's audit group added to the knowledge-lookup skill's table.Two late corrections, and who verified them
The last two commits close review 4's findings.
5e24008corrects the checklist'sNFR-14level cell (MUST→SHOULD; appendix C and the phase's own design table both say SHOULD, re-checked by diffing all 41 level cells against the generated map rather than by eye) and appends the dated correction the phase owed to the pre-existing phase-10 inbound bullet on the async-http client cache, whose two claims — an uncappedClients#@by_origin, and a gate that "stays red until it lands" — are both false against the tree.ab9a03dis the maintainer's own commit and was verified by the maintainer rather than by a fresh reviewer — stated here because that is a departure from this lane's discipline. It discharges the same premise in the two other places the fix agent flagged but was scoped out of: the roadmap's narration of the 2026-09-13 routing pass, anddocs/first-release.md's opening summary, which still called the blocker red and said phase 10's repair clears it. Both are in material this phase may write, so they are fixes rather than findings. The roadmap's paragraph gets a dated bracketed note rather than a rewrite, because the record of what that pass said is itself the point.Verification
Docs tip:
bundle exec rake(all twenty-one gates) green on 4.0.6 — 4,206 runs / 74,618 assertions / 0 failures / 0 errors / 9 skips / 99.82%,test:gates129/412,cops:test193/877, YARD 306 files 100% documented,bundler-auditclean;ruby .claude/skills/housekeeping/probe.rbexit 0, "no drift found";ruby scripts/verify_knowledge_structure.rbOK; the frozen trees,docs/deviations.mdand every other phase's documents untouched. The consolidation of the ledger rows into design §10 is a human's —docs/sdk-design-ruby/is frozen — anddocs/deviations.mdwaits for phase 10.Known follow-ups (not blocking a gate)
gates:bounded_map's allowlist reason forclients.rbis a dated reading, not a mechanism: the integrity test asserts each allowlisted path still reports, not that its reason still holds, so a refactor removing the drain would keep the gate green. The threeXCUT-14cases inclients_test.rbare what actually pin the cap.XCUT-3andXCUT-12still have no committed non-conforming double; the checklist names both as deliberate, andXCUT-12's fiber form is on phase 10's inbound list.[warn]lines onData.definesuperclasses and one unknown-tag warning innet_http/request_mapper.rb. Pre-existing, non-fatal.