From e93dbf9677b3b920279752f82238343aed756732 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 04:16:12 +0000 Subject: [PATCH 01/17] Walk the real corpus with the copied-accumulator lens, and refuse instead of aborting The copied-accumulator lens could already decide a real module; what it could not do was say which modules it had asked. Its population was nine hand-authored paths, so "no suspects" meant "none among those nine" and the rest of the corpus was not clean but unasked. v2.lens.complexity_accumulator_copy.corpus_gate takes the subject universe from module_declaration_facts_live -- the producer the module graph already resolves the corpus with -- and carries suspects and unreadable subjects at identity grain, with the Unclassifiable causes counted per cause rather than summed. It consumes the same accumulator_copy_findings authority the compile gate runs; the ingest chain is factored to source_tree so findings and tree are two consumers of one chain rather than two spellings of it. Three failure arms found by executing it, all the same class -- a failure that absorbs or aborts instead of refusing: * The roster named src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ by the #9637 reorganisation. The row was never repointed and, the gate being offline, nothing ran it to notice. * That missing path did not fail the gate, it ENDED THE PROCESS: the subject read used the filesystem_read intrinsic, whose result carries content and no success channel. At corpus grain every subject after the missing one is never asked. The read now folds through filesystem_read_outcome and the analysis standing gains a SourceUnreadable arm; the same defect in v2.lens.identity_captured_navigation.roster_gate is fixed with it. * module_declaration_facts panics on an absent pool root, so the below-floor arm's red is only authorable from a directory that exists and holds no modules. That refusal is correct; the witness is repointed rather than the host changed. Executed evidence, all green by execution with its control: * an_unreadable_subject_is_refused_with_its_cause + a_readable_subject_still_ reaches_the_lens. The red control is measured, not asserted: against the pre-fix filesystem_read spelling the first fails with a runtime type error and the second never runs at all. * a_planted_copied_accumulator_is_caught_through_the_corpus_path + a_clean_ accumulating_fold_stays_clean_through_the_corpus_path, over two fixtures differing in exactly one construction and read from disk, so the corpus path is what discriminates rather than the detector alone. * corpus_subtree_gate_reports_its_unreadable_population and an_empty_population_is_below_floor_rather_than_clean. What the census says, and it relocates the lane's blocker. Re-derive with census_for_paths / census_for_root in v2.test.claim.long.accumulator_copy_corpus_census_test; the numbers are in the pull request rather than transcribed here. Reach is bounded first by INGEST COVERAGE -- most sampled subjects are refused by the v2 grammar before any lens runs -- then by cost, then by a process-lifetime segfault. Roster policy is nowhere on that list, and neither is decl_facts. The asymptotic half is separately answered, negatively, and the instruments that answered it are kept because the evidence reads the wrong way at first glance: the ingest yields a grammar production tree encoded in kernel nodes, so cost_lens folds over it and returns a class for the PARSE SHAPE, not the program. The budget gate's subjects are semantic Arrows; bridging needs lowering. The shape detector needs only the production tree, which is why it walks real modules today. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- dag/gunbc/ci/ci_layer_roots.dag | 6 + .../roster_is_its_own_denominator.dag | 6 + .../lens/common/source_analysis_standing.dag | 13 + .../corpus_gate.dag | 260 +++++++++++++++ .../roster_gate.dag | 57 +++- .../roster_gate.dag | 13 +- .../accumulator_copy_roster_gate_test.dag | 2 +- ...cumulator_copy_unreadable_subject_test.dag | 55 +++ .../accumulator_copy_corpus_census_test.dag | 313 ++++++++++++++++++ .../clean_linear.dag | 5 + .../planted_copy.dag | 5 + 11 files changed, 720 insertions(+), 15 deletions(-) create mode 100644 src/v2/lens/complexity_accumulator_copy/corpus_gate.dag create mode 100644 src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag create mode 100644 src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag create mode 100644 test/fixture_roots/complexity_accumulator_copy/clean_linear.dag create mode 100644 test/fixture_roots/complexity_accumulator_copy/planted_copy.dag diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index a15b8742529..a0824b579f5 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -403,6 +403,12 @@ data witness_exclusion_frontier: List = [ reason: "SUBSTANTIATED per-row (found 2026-08-06, PR #7894 CI red, run 31069215347): maintenance_census_report_positive_control_derives_from_current_head calls current_rust_maintenance_census_report(), which reaches current_stage0_rust_source_observation() and therefore git.Inspect.HeadCommit without mock_response — the same class stage0_rust_host_observation_live_witness_test.dag documents above; a refusing hermetic discovery member fails its whole batch (batch 3 discovery-corpus, 7298-row corpus). This row alone would only remove the function from hermetic discovery, which is not an executing consumer by itself (DESIGN.md 'witness cost derives from purpose', operator ruling 2026-08-04) — so, mirroring stage0_rust_host_observation_live_witness_test.dag's closing contract, the function is ALSO enrolled in bin_witness_wet_entries below (GITHUB_SHA supplied by the runner, not hand-set), which is its real per-PR executing consumer under WitnessHasExecutingConsumer standing; this exclusion row only keeps it off the hermetic per-PR discovery batch it cannot pass. This origin file has only the one witness, so no scaffold-witness residue remains offline-only.", dissolution: unbound_dissolution(description: "mock_response coverage lands for the git.Inspect revision-addressed operations used by current_stage0_rust_source_observation AND this assertion is re-checked under a fixed mock; or the fn enrolls on a scheduled wet lane. Either way this row deletes.") }, + WitnessExclusionRow { + pattern: "claim/long/accumulator_copy_corpus_census", + classification: OfflineLocalRecipe, + reason: "The corpus-grain consumer of the accumulator-copy lens (v2.lens.complexity_accumulator_copy.corpus_gate). Its population is the module index rather than a hand-authored path list, so its cost is the corpus: measured 2026-09-06 on this session's host, ~80s per subject through the same tokenize/parse/normalize ingest the roster gate uses, against 5007 tracked .dag files -- upwards of a hundred hours serial -- and it cannot be waited out, because one process dies with SIGSEGV around the twentieth subject (accumulation across subjects, not any one subject: the file it dies on analyses cleanly alone), so the sweep is batched by construction. That prices it out of every CI lane by the same argument that excluded claim/long/mandatory_tag_extdeps_corpus, and for the same reason: the walk is interpreted. The cheap discriminating controls for the same code path -- an unreadable subject refusing with its cause, and a readable one still reaching the lens -- are NOT here; they are ordinary witnesses in claim/complexity/accumulator_copy_unreadable_subject_test.dag and are not excluded.", + dissolution: unbound_dissolution(description: "witness realization executes the ingest at native speed, or the lens reads the compile graph instead of re-parsing source; either way the corpus census enrolls on a real cadence and this row deletes") + }, WitnessExclusionRow { pattern: "claim/long/mandatory_tag_extdeps_corpus", classification: OfflineLocalRecipe, diff --git a/dag/gunbc/recurring_failure_mode/roster_is_its_own_denominator.dag b/dag/gunbc/recurring_failure_mode/roster_is_its_own_denominator.dag index 5e3d5e9d199..11b0015fd14 100644 --- a/dag/gunbc/recurring_failure_mode/roster_is_its_own_denominator.dag +++ b/dag/gunbc/recurring_failure_mode/roster_is_its_own_denominator.dag @@ -31,6 +31,12 @@ data roster_is_its_own_denominator: RecurringFailureMode = RecurringFailureMode "WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. ", + "SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. ", + + "AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. ", + + "WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. ", + "RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.)", ], diff --git a/src/v2/lens/common/source_analysis_standing.dag b/src/v2/lens/common/source_analysis_standing.dag index 78c53332210..d38c39a365a 100644 --- a/src/v2/lens/common/source_analysis_standing.dag +++ b/src/v2/lens/common/source_analysis_standing.dag @@ -1,9 +1,22 @@ module v2.lens.common.source_analysis_standing +// SourceUnreadable IS THE ARM THE PIPELINE HAD NO WAY TO REACH, and its absence was not a gap in +// coverage but an abort. Every consumer of this coproduct obtains its subject by reading a file, +// and the read ran through the `filesystem_read` intrinsic, whose result carries content and +// nothing else: a path that does not exist raises an interpreter type error and takes the WHOLE +// run down, so a roster or corpus sweep learned nothing about the files after the missing one. +// Receipt: src/v2/workflow/glob_discovery_law.dag moved to src/v2/test/workflow/ in the #9637 +// directory reorganisation and the roster row that names it was not repointed; because the roster +// gate is offline, nothing ran it, and the row sat crashing rather than refusing until a +// corpus-grain consumer executed it (2026-09-06). +// +// The read now folds through extdeps.filesystem.filesystem_io's filesystem_read_outcome, so an +// unreadable subject is one typed, located, counted row and the sweep continues. type SourceAnalysisNotEstablishedCause = LanguageGrammarUnavailable | LanguageGrammarRejected + | SourceUnreadable | SourceTokenizationRejected | SourceParseRejected | SourceNormalizationRejected diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag new file mode 100644 index 00000000000..7bd159cbe53 --- /dev/null +++ b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag @@ -0,0 +1,260 @@ +module v2.lens.complexity_accumulator_copy.corpus_gate + +import std.algebra { Cons, Empty } +import v2.lens.common.construction_justification { ConstructionJustification, WallAfterGrounding } +import std.disposition { SubstrateMandatoryTag } +import v2.lens.common.source_analysis_standing { SourceAnalysisNotEstablishedCause } +import v2.lens.complexity_accumulator_copy { Finding } +import v2.lens.complexity_accumulator_copy.analyze { + finding_is_refusal, + finding_is_suspect, + finding_refusal_cause +} +import v2.lens.complexity_accumulator_copy.roster_gate { + SourceFindingsEstablished, + SourceFindingsNotEstablished, + count_matching, + ingest_findings +} +import v2.lens.module_graph { ModuleDeclarationFact, module_declaration_facts_live } +import v2.std.algebra { list_reverse } +import v2.std.collection { List } +import v2.std.integer { Int } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.optional { Absent, Present } +import v2.std.text { String } + +// THE POPULATION IS DISCOVERED, NOT AUTHORED. The roster gate beside this module asks the same +// question of nine hand-picked paths, so its answer is about those nine and the corpus outside +// them is not clean but UNASKED -- coverage by illusion in the shape DESIGN section 6 names. This +// module takes the subject universe from module_declaration_facts_live, the same host producer the +// module graph resolves the corpus with, so adding a .dag file adds a subject with no edit here. +// +// WHAT THAT PRODUCER CAN STILL MISS, stated because a coverage claim that does not state it is +// worth less than none: a file whose module binding is unclassifiable is dropped by the module +// index rather than refused, so it never becomes a row and never becomes a refusal either. The +// population floor below is what makes an accidentally-narrowed walk red instead of vacuously +// green; it does not close that gap, and closing it belongs to the module index, not here. + +// A REFUSAL CAUSE IS CARRIED, NOT SUMMED AWAY. The five Unclassifiable causes have five different +// remedies -- an unregistered combiner is a MODEL gap, an unreadable call head is an INSTRUMENT +// gap, a may-alias name is a RESOLUTION gap -- so a corpus total over them names no work. They are +// counted per cause here for the same reason the compile gate types them per cause there. + +type RefusalCauseCount { + cause: Symbol + count: Int +} + +type CorpusFileStanding + = CorpusFileScanned { suspects: Int, refusals: Int, causes: List } + | CorpusFileNotEstablished { cause: SourceAnalysisNotEstablishedCause } + +type CorpusFileRow { + path: String + standing: CorpusFileStanding +} + +// THIS FOLD REBUILDS ITS ACCUMULATOR, WHICH IS THE SHAPE THIS VERY LENS CONVICTS, AND THE +// JUSTIFICATION IS STRUCTURAL RATHER THAN "n IS SMALL HERE" -- DESIGN section 6 rejects that excuse +// because reuse changes n. The bound here is not incidental: RefusalCauseCount is keyed on the +// Unclassifiable cause, the causes are a CLOSED set fixed by the analyze module's arms, so this +// list cannot exceed that set however large the corpus grows. The rebuild is over a constant, and +// the alternative -- a per-cause field, or a map keyed on a symbol -- buys nothing and loses the +// property that a new cause needs no edit here. +fn add_cause(counts: List, cause: Symbol, n: Int) -> List { + if any(xs: counts, predicate: fn(row) { row.cause == cause }) { + list_reverse(xs: fold_list(xs: counts, empty: Empty, cons: fn(acc, row) { + Cons { + head: if row.cause == cause { + RefusalCauseCount { cause: row.cause, count: row.count + n } + } else { + row + }, + tail: acc + } + })) + } else { + Cons { head: RefusalCauseCount { cause: cause, count: n }, tail: counts } + } +} + +fn refusal_cause_counts(fs: List) -> List { + fold_list(xs: fs, empty: Empty, cons: fn(acc, f) { + match finding_refusal_cause(f: f) { + Present { value: cause } => add_cause(counts: acc, cause: cause, n: 1) + Absent => acc + } + }) +} + +fn merge_cause_counts( + left: List, + right: List +) -> List { + fold_list(xs: right, empty: left, cons: fn(acc, row) { + add_cause(counts: acc, cause: row.cause, n: row.count) + }) +} + +fn corpus_file_standing(path: String) -> CorpusFileStanding { + match ingest_findings(path: path) { + SourceFindingsNotEstablished { cause: cause } => + CorpusFileNotEstablished { cause: cause } + SourceFindingsEstablished { findings: fs } => + CorpusFileScanned { + suspects: count_matching(fs: fs, pred: finding_is_suspect), + refusals: count_matching(fs: fs, pred: finding_is_refusal), + causes: refusal_cause_counts(fs: fs) + } + } +} + +fn corpus_file_row(path: String) -> CorpusFileRow { + CorpusFileRow { path: path, standing: corpus_file_standing(path: path) } +} + +fn corpus_paths(pool_roots: List) -> List { + list_reverse(xs: fold_list( + xs: module_declaration_facts_live(pool_roots: pool_roots), + empty: Empty, + cons: fn(acc, fact) { Cons { head: fact.path, tail: acc } } + )) +} + +fn corpus_rows_for_paths(paths: List) -> List { + list_reverse(xs: fold_list( + xs: paths, + empty: Empty, + cons: fn(acc, path) { Cons { head: corpus_file_row(path: path), tail: acc } } + )) +} + +fn corpus_rows(pool_roots: List) -> List { + corpus_rows_for_paths(paths: corpus_paths(pool_roots: pool_roots)) +} + +// SUSPECTS AND UNREADABLE SUBJECTS ARE CARRIED AT IDENTITY GRAIN, NOT AS COUNTS. A count says how +// many and never which, so it cannot be joined against anything and a removal cannot be given a +// disposition (DESIGN section 5, the monotone debt contract). + +fn corpus_suspect_paths(rows: List) -> List { + list_reverse(xs: fold_list( + xs: rows, + empty: Empty, + cons: fn(acc, row) { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => + if s > 0 { Cons { head: row.path, tail: acc } } else { acc } + CorpusFileNotEstablished { cause: _ } => acc + } + } + )) +} + +fn corpus_not_established_rows(rows: List) -> List { + list_reverse(xs: fold_list( + xs: rows, + empty: Empty, + cons: fn(acc, row) { + match row.standing { + CorpusFileScanned { suspects: _, refusals: _, causes: _ } => acc + CorpusFileNotEstablished { cause: _ } => Cons { head: row, tail: acc } + } + } + )) +} + +fn corpus_suspect_site_count(rows: List) -> Int { + fold_list(xs: rows, empty: 0, cons: fn(acc, row) { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => acc + s + CorpusFileNotEstablished { cause: _ } => acc + } + }) +} + +fn corpus_refusal_site_count(rows: List) -> Int { + fold_list(xs: rows, empty: 0, cons: fn(acc, row) { + match row.standing { + CorpusFileScanned { suspects: _, refusals: r, causes: _ } => acc + r + CorpusFileNotEstablished { cause: _ } => acc + } + }) +} + +fn corpus_refusal_causes(rows: List) -> List { + fold_list(xs: rows, empty: Empty, cons: fn(acc, row) { + match row.standing { + CorpusFileScanned { suspects: _, refusals: _, causes: cs } => + merge_cause_counts(left: acc, right: cs) + CorpusFileNotEstablished { cause: _ } => acc + } + }) +} + +type CorpusReport { + files: Int + scanned: Int + suspect_files: List + suspect_sites: Int + refusal_sites: Int + refusal_causes: List + not_established: List +} + +fn corpus_report_from_rows(rows: List) -> CorpusReport { + let unreadable = corpus_not_established_rows(rows: rows) + CorpusReport { + files: length(xs: rows), + scanned: length(xs: rows) - length(xs: unreadable), + suspect_files: corpus_suspect_paths(rows: rows), + suspect_sites: corpus_suspect_site_count(rows: rows), + refusal_sites: corpus_refusal_site_count(rows: rows), + refusal_causes: corpus_refusal_causes(rows: rows), + not_established: unreadable + } +} + +fn corpus_report(pool_roots: List) -> CorpusReport { + corpus_report_from_rows(rows: corpus_rows(pool_roots: pool_roots)) +} + +// AN EMPTY POPULATION IS ITS OWN ARM. Without it a walk that resolved no modules at all -- a wrong +// pool root, a producer outage -- answers CorpusClean, which is the affirmative "nothing wrong +// here" for a run that asked nothing. + +type CorpusGateDisposition + = CorpusClean { files: Int } + | CorpusSuspectObserved { paths: List, sites: Int } + | CorpusNotEstablishedObserved { rows: List } + | CorpusPopulationBelowFloor { files: Int, floor: Int } + +fn corpus_gate_disposition(report: CorpusReport, population_floor: Int) -> CorpusGateDisposition { + if report.files < population_floor { + CorpusPopulationBelowFloor { files: report.files, floor: population_floor } + } else { + match report.suspect_files { + Cons { head: _, tail: _ } => + CorpusSuspectObserved { paths: report.suspect_files, sites: report.suspect_sites } + Empty => + match report.not_established { + Cons { head: _, tail: _ } => + CorpusNotEstablishedObserved { rows: report.not_established } + Empty => CorpusClean { files: report.files } + } + } + } +} + +fn corpus_gate(pool_roots: List, population_floor: Int) -> CorpusGateDisposition { + corpus_gate_disposition( + report: corpus_report(pool_roots: pool_roots), + population_floor: population_floor + ) +} + +data construction_justification: ConstructionJustification = ConstructionJustification { + class: WallAfterGrounding { dissolves_to: SubstrateMandatoryTag } +} diff --git a/src/v2/lens/complexity_accumulator_copy/roster_gate.dag b/src/v2/lens/complexity_accumulator_copy/roster_gate.dag index a641f8e7a41..0bf61c50939 100644 --- a/src/v2/lens/complexity_accumulator_copy/roster_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/roster_gate.dag @@ -11,7 +11,8 @@ import v2.lens.common.source_analysis_standing { SourceAnalysisNotEstablishedCause, SourceNormalizationRejected, SourceParseRejected, - SourceTokenizationRejected + SourceTokenizationRejected, + SourceUnreadable } // Operator-ruled OFFLINE (NOT CI discovery-enrolled; witness_exclusion_substrings row @@ -44,33 +45,42 @@ type SourceFindingsStanding = SourceFindingsEstablished { findings: List } | SourceFindingsNotEstablished { cause: SourceAnalysisNotEstablishedCause } -fn source_findings(source_text: String) -> SourceFindingsStanding { +// THE INGESTED TREE IS ITS OWN ANSWER, NOT A PRIVATE STEP OF THE FINDINGS FOLD. Two consumers want +// this chain -- the copied-accumulator findings below, and any lens that wants a real module's node +// tree instead of a hand-built fixture -- and forking the tokenize/validate/parse/normalize chain +// per consumer is the second representation section 3 forbids. source_findings is now one line over +// this, so the refusal causes cannot drift apart between them. +type SourceTreeStanding + = SourceTreeEstablished { root: Node } + | SourceTreeNotEstablished { cause: SourceAnalysisNotEstablishedCause } + +fn source_tree(source_text: String) -> SourceTreeStanding { let lm = dag_language_model() let at = invariant_port_locus(port: ^accumulator_copy_roster_gate_port, invariant: ^accumulator_copy_source_analysis_invariant) match lm.grammar { - VoidGrammar => SourceFindingsNotEstablished { cause: LanguageGrammarUnavailable } + VoidGrammar => SourceTreeNotEstablished { cause: LanguageGrammarUnavailable } ModeledGrammar { root: root } => match grammar_validate_for_parse(root: root, at: at) { - Rejected { diagnostics: _ } => SourceFindingsNotEstablished { cause: LanguageGrammarRejected } + Rejected { diagnostics: _ } => SourceTreeNotEstablished { cause: LanguageGrammarRejected } Accepted { value: validated, diagnostics: _ } => match tokenize( text: source_text, file: ^accumulator_copy_roster_subject, rules: lm.lex ) { - Rejected { diagnostics: _ } => SourceFindingsNotEstablished { cause: SourceTokenizationRejected } + Rejected { diagnostics: _ } => SourceTreeNotEstablished { cause: SourceTokenizationRejected } Accepted { value: stream, diagnostics: _ } => match parse_production( tokens: token_stream_remaining(stream: stream), validated: validated, production_name: validated.root.start ) { - Rejected { diagnostics: _ } => SourceFindingsNotEstablished { cause: SourceParseRejected } + Rejected { diagnostics: _ } => SourceTreeNotEstablished { cause: SourceParseRejected } Accepted { value: artifact, diagnostics: _ } => match normalize(parse_tree: artifact.tree) { - Rejected { diagnostics: _ } => SourceFindingsNotEstablished { cause: SourceNormalizationRejected } + Rejected { diagnostics: _ } => SourceTreeNotEstablished { cause: SourceNormalizationRejected } Accepted { value: tree, diagnostics: _ } => - SourceFindingsEstablished { findings: accumulator_copy_findings(root: tree.root) } + SourceTreeEstablished { root: tree.root } } } } @@ -78,9 +88,36 @@ fn source_findings(source_text: String) -> SourceFindingsStanding { } } +fn findings_of_tree(standing: SourceTreeStanding) -> SourceFindingsStanding { + match standing { + SourceTreeNotEstablished { cause: cause } => SourceFindingsNotEstablished { cause: cause } + SourceTreeEstablished { root: root } => + SourceFindingsEstablished { findings: accumulator_copy_findings(root: root) } + } +} + +fn source_findings(source_text: String) -> SourceFindingsStanding { + v2.lens.complexity_accumulator_copy.roster_gate.findings_of_tree( + standing: v2.lens.complexity_accumulator_copy.roster_gate.source_tree(source_text: source_text) + ) +} + +// THE TYPED READ IS WRITTEN ONCE, HERE, AND BOTH PATH-TAKING CONSUMERS GO THROUGH IT. Duplicating +// it per consumer would let one of them keep the untyped spelling that made a missing file end the +// process instead of refusing. +fn ingest_tree(path: String) -> SourceTreeStanding { + let read = Filesystem.Read(path: path) + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error: _ } => + SourceTreeNotEstablished { cause: SourceUnreadable } + FilesystemReadSucceeded { content: text } => + v2.lens.complexity_accumulator_copy.roster_gate.source_tree(source_text: text) + } +} + fn ingest_findings(path: String) -> SourceFindingsStanding { - v2.lens.complexity_accumulator_copy.roster_gate.source_findings( - source_text: filesystem_read(path: path).content + v2.lens.complexity_accumulator_copy.roster_gate.findings_of_tree( + standing: v2.lens.complexity_accumulator_copy.roster_gate.ingest_tree(path: path) ) } diff --git a/src/v2/lens/identity_captured_navigation/roster_gate.dag b/src/v2/lens/identity_captured_navigation/roster_gate.dag index ce51a46b3e4..c7592645271 100644 --- a/src/v2/lens/identity_captured_navigation/roster_gate.dag +++ b/src/v2/lens/identity_captured_navigation/roster_gate.dag @@ -7,7 +7,8 @@ import v2.lens.common.source_analysis_standing { SourceAnalysisNotEstablishedCause, LanguageGrammarUnavailable, SourceTokenizationRejected, - SourceParseRejected + SourceParseRejected, + SourceUnreadable } // The fast inline-source controls are required-floor witnesses at @@ -115,9 +116,13 @@ fn source_findings(source_text: String) -> SourceFindingsStanding { } fn ingest_findings(path: String) -> SourceFindingsStanding { - v2.lens.identity_captured_navigation.roster_gate.source_findings( - source_text: filesystem_read(path: path).content - ) + let read = Filesystem.Read(path: path) + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadRefused { error: _ } => + SourceFindingsNotEstablished { cause: SourceUnreadable } + FilesystemReadSucceeded { content: text } => + v2.lens.identity_captured_navigation.roster_gate.source_findings(source_text: text) + } } fn count_matching(fs: List, pred: fn(Finding) -> Bool) -> Int { diff --git a/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag b/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag index b8af976a3d9..4950c284fff 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag @@ -28,7 +28,7 @@ test fn roster_compile_stage_zero_suspects_within_ratchet() -> Bool { } test fn roster_glob_discovery_zero_suspects_within_ratchet() -> Bool { - file_gate(path: "src/v2/workflow/glob_discovery_law.dag", refusal_ceiling: 0) + file_gate(path: "src/v2/test/workflow/glob_discovery_law.dag", refusal_ceiling: 0) } test fn roster_lens_traversal_zero_suspects_within_ratchet() -> Bool { diff --git a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag new file mode 100644 index 00000000000..9231da89727 --- /dev/null +++ b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag @@ -0,0 +1,55 @@ +module v2.test.claim.complexity.accumulator_copy_unreadable_subject + +import v2.lens.common.source_analysis_standing { SourceUnreadable } +import v2.lens.complexity_accumulator_copy.roster_gate { + GateClean, + GateNotEstablished, + GateRefusalBudgetExceeded, + GateSuspectObserved, + file_gate_disposition +} +import v2.std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE RED CONTROL FOR A DEFECT THAT DID NOT RETURN A WRONG ANSWER -- IT ENDED THE RUN. file_gate's +// subject used to be read by the `filesystem_read` intrinsic, whose result type carries `content` +// and no success channel, so a path that does not exist raised an interpreter type error and took +// the whole process down with it. On a nine-row roster that reads as one red test; on a corpus +// sweep it is an absorbing failure -- every subject after the missing one is never asked. +// +// The receipt is not hypothetical: src/v2/workflow/glob_discovery_law.dag moved to +// src/v2/test/workflow/ in the #9637 directory reorganisation, the roster row naming it was not +// repointed, and because the roster gate is offline nothing executed it. It crashed rather than +// refused for as long as that row stood. +// +// This witness fails on the pre-fix build with a runtime error rather than a false, which is the +// discrimination: it cannot pass by accident, and it cannot pass while the read is untyped. +test fn an_unreadable_subject_is_refused_with_its_cause() -> Bool { + match file_gate_disposition( + path: "src/v2/workflow/this_path_is_deliberately_absent.dag", + refusal_ceiling: 0 + ) { + GateNotEstablished { cause: cause } => cause == SourceUnreadable + GateClean => false + GateSuspectObserved { suspects: _ } => false + GateRefusalBudgetExceeded { refusals: _, ceiling: _ } => false + } +} + +// THE POSITIVE CONTROL ON THE SAME PATH. Without it, a read that refused every subject would +// satisfy the claim above while establishing nothing -- the arm would be permanently green for the +// wrong reason. This subject is a file the lens is already known to read and find clean (it is the +// roster's own cost-model row), so a green here says the typed read still reaches real bytes. +test fn a_readable_subject_still_reaches_the_lens() -> Bool { + match file_gate_disposition( + path: "src/v2/lens/complexity_accumulator_copy.dag", + refusal_ceiling: 0 + ) { + GateClean => true + GateNotEstablished { cause: _ } => false + GateSuspectObserved { suspects: _ } => false + GateRefusalBudgetExceeded { refusals: _, ceiling: _ } => false + } +} diff --git a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag new file mode 100644 index 00000000000..ecf36c37117 --- /dev/null +++ b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag @@ -0,0 +1,313 @@ +module v2.test.claim.long.accumulator_copy_corpus_census_test + +import v2.lens.complexity_accumulator_copy.corpus_gate { + CorpusFileNotEstablished, + CorpusFileRow, + CorpusFileScanned, + CorpusClean, + CorpusGateDisposition, + CorpusNotEstablishedObserved, + CorpusPopulationBelowFloor, + CorpusReport, + CorpusSuspectObserved, + corpus_gate, + corpus_paths, + corpus_report, + corpus_report_from_rows, + corpus_rows_for_paths +} +import std.algebra { Cons, Empty } +import v2.lens.complexity_accumulator_copy.roster_gate { + SourceTreeEstablished, + SourceTreeNotEstablished, + ingest_tree +} +import v2.lens.cost { + ClassConstant, + ClassExponential, + ClassFactorial, + ClassLinear, + ClassLinearithmic, + ClassLog, + ClassPolyLog, + ClassPolynomial, + ClassUnknown, + asymptotic_class_of_cost, + cost_lens +} +import v2.std.node { + Arrow, + Atom, + Bind, + Branch, + Cardinality, + ComputationNode, + Conj, + Disj, + Instantiation, + Loop, + Named, + Transform, + TypeNode, + Value +} +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.node_query { find_arrow_body_child, find_named_child } +import v2.std.witness { Holds, Violates } +import v2.std.collection { List } +import v2.std.integer { Int } +import v2.std.logic { Bool } +import v2.std.text { String } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE COST IS THE REASON THIS FILE IS OFFLINE, AND IT IS THE LENS'S OWN SUBJECT MATTER. The +// corpus consumer re-ingests every subject through tokenize -> parse_production -> normalize in the +// interpreter, measured on this session's host at roughly eighty seconds per subject against 5007 +// tracked .dag files. That is not a scheduling inconvenience to be waited out; it is the same +// displaced cost DESIGN section 1 prices, and it is why the lens has stood roster-bound rather than +// corpus-bound since it was written. The exclusion row is +// claim/long/accumulator_copy_corpus_census in gunbc.ci_layer_roots, and its dissolution names the +// capability -- native ingest, or a lens that reads the compile graph instead of re-parsing source +// -- rather than any artifact. +// +// ONE PROCESS CANNOT CARRY THE CORPUS, AND THE FAILURE IS A SEGMENTATION FAULT RATHER THAN A +// DIAGNOSTIC. Measured twice with census_for_paths over a stratified sample of the tracked corpus: +// the run dies with SIGSEGV around the twentieth subject, and the subject it dies ON analyses +// cleanly when handed to the same instrument alone -- so the cause is accumulation across subjects +// within a process, not any one subject's shape. That is worse than the cost: it means the +// whole-population form of corpus_gate is unreachable by construction and not merely slow, and the +// failure carries no cause, no locus and no count, so a sweep that dies looks exactly like a sweep +// that finished if only its exit status is read. +// +// So the census is run in BATCHES, which is why the instrument takes an explicit subject list +// beside the discovered-root form. The batch ceiling is an observation about this host and this +// build, not a modeled bound; it is stated here so the next author batches deliberately rather than +// discovering it as a silent truncation. Dissolution is the same trigger the exclusion row names -- +// native ingest -- plus a driver that runs batches per process and joins their reports. +// +// EVERY WITNESS IN THIS FILE EXECUTES, WHICH IS WHY THE WHOLE-CORPUS ONE IS ABSENT. The obvious +// row to write here is a gate over the tracked corpus, and it is deliberately not written: nothing +// can run it, so it would be a documented red rather than an observed one -- roster membership +// standing in for a verdict. The subtree witness below names a real discovered population small +// enough to execute, and the corpus form stays a run of census_for_root until its cost dissolves. +// +// THE THREE INSTRUMENTS BELOW ARE ENTRY POINTS, NOT RESULTS. DESIGN section 6 asks that a +// measurement be cited by naming the producer that re-derives it. population_for_root answers how +// many subjects a root contributes; census_for_root and census_for_paths answer what the lens finds +// over a discovered root and over an explicit subject list respectively. Their output belongs in a +// run, never transcribed into this file. + +fn population_for_root(root: String) -> Int { + length(xs: corpus_paths(pool_roots: [root])) +} + +fn census_for_root(root: String) -> CorpusReport { + corpus_report(pool_roots: [root]) +} + +fn census_for_paths(paths_csv: String) -> CorpusReport { + corpus_report_from_rows(rows: corpus_rows_for_paths(paths: split(s: paths_csv, delimiter: ","))) +} + +fn subtree_gate() -> CorpusGateDisposition { + corpus_gate(pool_roots: ["dag/extdeps/filesystem"], population_floor: 5) +} + +// A SUBTREE WHOSE SUBJECTS THE INGEST CANNOT READ IS NOT A CLEAN SUBTREE, and this witness says so +// rather than passing. dag/extdeps/filesystem is deliberately the subject: three of its six modules +// come back not-established under the v2 grammar, which is the reach fact the corpus consumer was +// built to surface. Asserting CorpusClean here would have required either excluding the subtree or +// widening the disposition until unreadable counted as fine, and both are the coverage-by-illusion +// this file exists to end. The assertion is therefore that the gate REPORTS the unreadable +// population -- and its failure mode is the one that matters: if the ingest silently started +// answering clean for those subjects, this goes red. +test fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { + match subtree_gate() { + CorpusNotEstablishedObserved { rows: _ } => true + CorpusClean { files: _ } => false + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusPopulationBelowFloor { files: _, floor: _ } => false + } +} + +// THE POPULATION FLOOR'S OWN RED CONTROL. Without it the gate's clean arm is reachable by a walk +// that resolved nothing at all, and an empty question reads as an affirmative answer. +// +// THE SUBJECT IS A REAL DIRECTORY HOLDING NO MODULES, NOT AN ABSENT ONE, AND THE FIRST CUT HAD IT +// WRONG. Pointing this at a path that does not exist does not produce an empty population: the host +// refuses at root admission (anchor_source_root: "... is not a directory") and unwinds, so the +// witness PANICKED rather than answering. That refusal is correct -- a mistyped root is a caller +// defect and should stop the line -- but it means the below-floor arm's red is only authorable from +// a directory that is present and contributes no modules. docs/ is that subject. +test fn an_empty_population_is_below_floor_rather_than_clean() -> Bool { + match corpus_gate(pool_roots: ["docs"], population_floor: 1) { + CorpusPopulationBelowFloor { files: _, floor: _ } => true + CorpusClean { files: _ } => false + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false + } +} + +// THE DISCRIMINATING PAIR FOR THE CORPUS PATH, AND IT IS A PAIR ON PURPOSE. The lens already has a +// planted-source control that hands a String to source_finding_standing; that one proves the +// DETECTOR fires. It does not prove the corpus path fires, because the corpus path is +// path -> typed read -> ingest -> row, and every one of those steps is new. These two subjects +// differ in exactly one authored construction -- the accumulating fold copies its carrier through a +// registered combiner in one and adds to it in the other -- so a green on both says the corpus path +// discriminates rather than that it answers a constant. +// +// They live under test/fixture_roots/, outside every source root, because the planted subject is +// deliberately a quadratic and must never be compiled as production source or reached by the +// corpus population. + +fn fixture_rows() -> List { + corpus_rows_for_paths(paths: [ + "test/fixture_roots/complexity_accumulator_copy/planted_copy.dag", + "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag" + ]) +} + +fn suspects_at(row: CorpusFileRow) -> Int { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s + CorpusFileNotEstablished { cause: _ } => 0 - 1 + } +} + +// A NOT-ESTABLISHED SUBJECT ANSWERS -1 ABOVE RATHER THAN 0, so a fixture the ingest cannot read +// fails BOTH witnesses instead of silently satisfying the clean one. Zero would have made +// unreadable and clean the same answer, which is the conflation this whole change exists to end. +test fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { + match fixture_rows() { + Cons { head: planted, tail: _ } => suspects_at(row: planted) > 0 + Empty => false + } +} + +test fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { + match fixture_rows() { + Cons { head: _, tail: Cons { head: clean, tail: _ } } => suspects_at(row: clean) == 0 + _ => false + } +} + +// CAN THE IN-.DAG INGEST PATH FEED THE ASYMPTOTIC ENGINE OVER A REAL MODULE? ANSWERED BY EXECUTION: +// NO, AND THE FIRST READING OF THE EVIDENCE SAID YES. The instruments below are what settled it and +// they are kept because the answer is easy to get wrong in exactly the way it was got wrong here. +// +// ingested_root_shape answers TypeNode/Conj for a real module, and cost_lens over that same root +// answers Linear rather than refusing. Read together those two look like the asymptotic engine +// returning a verdict on a real module's cost. They are not. ingested_root_child_labels names the +// root's edges -- grammar_production_identity_node_projection and +// grammar_production_captured_node_projection -- so the tree is a GRAMMAR PRODUCTION TREE encoded +// in kernel nodes, and the Linear verdict is a cost of the parse shape, not of the program. That +// is why ingested_decl_cost_class answers decl-not-found: there are no declaration-named edges on +// the root for find_named_child to select, because normalize has not lowered anything. +// +// The consequence is the lane's shape. The copied-accumulator walker wants exactly this production +// tree -- it reads the grammar captures -- so it runs on real modules today. The budget gate's +// subjects are semantic Arrows, so feeding it a real module needs resolve / infer / fold_lowering, +// which is the compile-graph capability gunbc.plans.structural_quadratic_wall_coverage_audit names +// as the blocker. That blocker is real for the asymptotic path and is NOT a blocker for the shape +// detector; conflating the two is what makes the audit's stale prose read as if nothing can move. +// +// These are instruments, not witnesses -- run them and read the value; do not transcribe it back +// into this file. + +fn ingested_root_shape(path: String) -> String { + match ingest_tree(path: path) { + SourceTreeNotEstablished { cause: _ } => "not-established" + SourceTreeEstablished { root: r } => + match r.kind { + TypeNode { connective: c } => + match c { + Atom { identity: _ } => "TypeNode/Atom" + Conj => "TypeNode/Conj" + Disj => "TypeNode/Disj" + Arrow => "TypeNode/Arrow" + Cardinality => "TypeNode/Cardinality" + Instantiation => "TypeNode/Instantiation" + } + ComputationNode { behavior: b } => + match b { + Value => "ComputationNode/Value" + Transform => "ComputationNode/Transform" + Branch => "ComputationNode/Branch" + Loop => "ComputationNode/Loop" + Bind => "ComputationNode/Bind" + Match => "ComputationNode/Match" + } + } + } +} + +fn ingested_cost_class(path: String) -> String { + match ingest_tree(path: path) { + SourceTreeNotEstablished { cause: _ } => "not-established" + SourceTreeEstablished { root: r } => + match cost_lens(n: r) { + Violates { diagnostic: _ } => "UnknownCost/refused" + Holds { value: c } => + match asymptotic_class_of_cost(cost: c) { + ClassConstant => "Constant" + ClassLog => "Log" + ClassLinear => "Linear" + ClassLinearithmic => "Linearithmic" + ClassPolynomial { degree: _ } => "Polynomial" + ClassPolyLog { exponent: _ } => "PolyLog" + ClassExponential => "Exponential" + ClassFactorial => "Factorial" + ClassUnknown => "Unknown" + } + } + } +} + +// THE ROSTER ROW'S OWN SHAPE, PROBED AGAINST A REAL MODULE. The budget gate's subject supply asks +// for an Arrow and takes its body child; a module root is a Conj of declarations, so the step this +// probe measures is whether one declaration of a real module can be selected and handed over in +// exactly that shape. +fn ingested_decl_cost_class(path: String, decl: String) -> String { + match ingest_tree(path: path) { + SourceTreeNotEstablished { cause: _ } => "not-established" + SourceTreeEstablished { root: r } => + match find_named_child(root: r, name: symbol_intern_lexeme(lexeme: decl)) { + Rejected { diagnostics: _ } => "decl-not-found" + Accepted { value: arrow, diagnostics: _ } => + match find_arrow_body_child(root: arrow) { + Rejected { diagnostics: _ } => "arrow-body-not-found" + Accepted { value: body, diagnostics: _ } => + match cost_lens(n: body) { + Violates { diagnostic: _ } => "UnknownCost/refused" + Holds { value: c } => + match asymptotic_class_of_cost(cost: c) { + ClassConstant => "Constant" + ClassLog => "Log" + ClassLinear => "Linear" + ClassLinearithmic => "Linearithmic" + ClassPolynomial { degree: _ } => "Polynomial" + ClassPolyLog { exponent: _ } => "PolyLog" + ClassExponential => "Exponential" + ClassFactorial => "Factorial" + ClassUnknown => "Unknown" + } + } + } + } + } +} + +fn ingested_root_child_labels(path: String) -> String { + match ingest_tree(path: path) { + SourceTreeNotEstablished { cause: _ } => "not-established" + SourceTreeEstablished { root: r } => + fold(r.children, init: "", f: fn(acc, e) { + match e.label { + Named { name: n } => join([acc, "|N:", symbol_lexeme(n)], "") + _ => join([acc, "|P"], "") + } + }) + } +} diff --git a/test/fixture_roots/complexity_accumulator_copy/clean_linear.dag b/test/fixture_roots/complexity_accumulator_copy/clean_linear.dag new file mode 100644 index 00000000000..bce2a65115d --- /dev/null +++ b/test/fixture_roots/complexity_accumulator_copy/clean_linear.dag @@ -0,0 +1,5 @@ +module test.fixture.complexity_accumulator_copy.clean_linear + +fn clean_linear(xs: List) -> Int { + fold(xs, init: 0, f: fn(acc, x) { acc + x }) +} diff --git a/test/fixture_roots/complexity_accumulator_copy/planted_copy.dag b/test/fixture_roots/complexity_accumulator_copy/planted_copy.dag new file mode 100644 index 00000000000..1c2dfb08479 --- /dev/null +++ b/test/fixture_roots/complexity_accumulator_copy/planted_copy.dag @@ -0,0 +1,5 @@ +module test.fixture.complexity_accumulator_copy.planted_copy + +fn planted_quadratic(xs: List) -> Int { + fold(xs, init: 0, f: fn(acc, x) { list_append(left: acc, right: x) }) +} From 8dc2967f9dffeec18aeba1d21e962c495221c78f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 04:31:26 +0000 Subject: [PATCH 02/17] Answer the fixture rows with two predicates, not a sentinel packed into an Int Review remark on #10645: the -1 returned for a not-established subject was "unlovely but deliberate". The reasoning behind it was right -- an unreadable fixture must fail BOTH witnesses rather than satisfying the clean one -- and the carrier was wrong. A sentinel in an Int is the same collapse of a typed standing onto a scalar that the rest of this change exists to undo, and it is safe only while every caller remembers what -1 means. row_has_suspect and row_scanned_without_suspect each answer their own question and each answer false for a not-established subject, so an unreadable fixture is neither suspect nor clean and fails both witnesses by construction rather than by arithmetic. The guarantee is re-established by execution, not by inspection: pointing fixture_rows at two absent paths turns BOTH witnesses red, and they are green again on the real fixtures. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- .../accumulator_copy_corpus_census_test.dag | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag index ecf36c37117..f33fa1a5742 100644 --- a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag +++ b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag @@ -169,26 +169,39 @@ fn fixture_rows() -> List { ]) } -fn suspects_at(row: CorpusFileRow) -> Int { +// TWO PREDICATES OVER THE STANDING, NOT ONE PROJECTION TO A COUNT. The first cut of this answered +// the suspect count and returned -1 for a subject the ingest could not read, so that an unreadable +// fixture would fail both witnesses rather than satisfying the clean one. That reasoning was right +// and the carrier was wrong: it is a sentinel packed into an Int, which is the same collapse of a +// typed standing onto a scalar that this whole change exists to undo -- and it only reads as safe +// while every caller remembers what -1 means. Each predicate below answers its own question and +// answers FALSE for a not-established subject, which gives the identical guarantee with nothing to +// remember: an unreadable fixture is neither suspect nor clean, so it fails both witnesses by +// construction rather than by arithmetic. +fn row_has_suspect(row: CorpusFileRow) -> Bool { match row.standing { - CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s - CorpusFileNotEstablished { cause: _ } => 0 - 1 + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s > 0 + CorpusFileNotEstablished { cause: _ } => false + } +} + +fn row_scanned_without_suspect(row: CorpusFileRow) -> Bool { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s == 0 + CorpusFileNotEstablished { cause: _ } => false } } -// A NOT-ESTABLISHED SUBJECT ANSWERS -1 ABOVE RATHER THAN 0, so a fixture the ingest cannot read -// fails BOTH witnesses instead of silently satisfying the clean one. Zero would have made -// unreadable and clean the same answer, which is the conflation this whole change exists to end. test fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { match fixture_rows() { - Cons { head: planted, tail: _ } => suspects_at(row: planted) > 0 + Cons { head: planted, tail: _ } => row_has_suspect(row: planted) Empty => false } } test fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { match fixture_rows() { - Cons { head: _, tail: Cons { head: clean, tail: _ } } => suspects_at(row: clean) == 0 + Cons { head: _, tail: Cons { head: clean, tail: _ } } => row_scanned_without_suspect(row: clean) _ => false } } From 7038bcd028a97ef7c654086cd96c1b3cd8747a2a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 04:58:55 +0000 Subject: [PATCH 03/17] chore: regenerate drifted generated artifacts (ci auto-heal) Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md roster_is_its_own_denominator Ledger-Repair-Judged: docs/design-rung-drops.md --- docs/design-failure-modes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index 8443513668c..08bf358143f 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -304,7 +304,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a transport mock reports success independently of its request, so a hermetic success is mistaken for evidence that a real effect could succeed** (INVALID STATE: a single success-valued mock_response answers every endpoint and command, including the explicitly unreachable control. SPECIMEN: extdeps.ssh.session ssh.Session.ExecPortableWords returns exit_code 0, success true and mock-ssh-portable-words for every client_args. The HostEffect.ShellCommand dissolution exposed this when its wet failure controls moved to typed ReadEffectivePosixPrincipal over SSH. The two error directions must stay distinct: success assertions can pass vacuously; failure assertions turn false because the mock reports success. The existing test.claim.host_effect_apply_witness witness_ssh_shell_unreachable_fails_closed is already enrolled as an expected semantic RED in v2.workflow.floor_expected_red, not evidence that an unreachable host was consulted. By contrast, extdeps.access.posix_effective_principal_read_op access.PosixEffectivePrincipal.Read returns an explicit failed hermetic observation. It cannot fabricate a principal, but its failed mock also cannot establish the wet success claim. Moving a test from an unmodeled shell operation to either typed operation therefore changes its hermetic outcome, and retaining the old shell route-gap enrollment would be stale evidence. RUNG: no construction or admission rule prevents the unconditional-success mock; the general class remains unremedied and silent false success is outside the ladder. Named failure assertions expose individual instances, which is not a guarantee over transport mocks. This filing adds no prevention. NEXT-RUNG CAPABILITY: transport-mock admission requires request-sensitive success and failure controls, and refuses a mock whose success claim has no discriminating RED; where external reachability cannot be established hermetically, an explicit unobserved refusal and an executing wet control must remain distinct. The existing gunbc.hermetic_mock_fidelity records the same defect previously repaired for shell.Exec; this is the surviving SSH instance, not a claim that this PR repairs that class.) - **a deconfliction plan does not enumerate its writers** (two or more lanes reason about how to avoid colliding on one subject, agree on a scheme, and collide anyway -- because the scheme is a PREDICTION about who will write the subject next, and the predicted population omits a writer. The omitted writer is not an adversary: it is a peer who agreed to stand down and then WROTE ANYWAY -- an earlier revision of this sentence said "and then did not act", which is the opposite mechanism and could not produce the collision the row models, because a writer that does not write collides with nobody. The result merges CLEAN, so the collision is silent. INVALID STATE: a lane holding a deconfliction property that quantifies over a writer population it never discovered. WHY IT SURVIVES REVIEW: the plan is examined for whether it WOULD prevent a collision among the writers it names, and it always would. Nothing in a diff exhibits the writer it omits, and the omitted writer's own change is independently correct. SPECIMEN (main's parse break, 2026-09-04). Four lanes independently authored a repair for one orphaned annotation. Deconfliction ran and SUCCEEDED AS A DISCUSSION: #10414 was named the winner on an earliest-with-proof rule, and the other authors assented. Three of the four then merged anyway (#10425, #10408, #10428), each deleting the same nineteen lines and each adding its own relocated copy, leaving one paragraph standing three times in the module. One copy carried an inverted double negative asserting the opposite of the sentence it was restating. Repaired by removing two copies in #10435. THE ASSENT IS LOAD-BEARING, NOT INCIDENTAL, AND IT IS WHAT BOUNDS THIS CLASS: the population was not unknown and the peers were not uncooperative. Every author was identified, every author agreed, and the scheme still failed -- so the missing thing is not discovery of the writers but an INTERLOCK on them. A class where the colliding writer was never identified is a different and easier problem, because enumerating it is the whole remedy. WHY GIT CANNOT SEE IT, which is the load-bearing mechanism: identical DELETIONS on every side reconcile silently, and each side's ADDITION lands at a different offset, so no hunk overlaps. Three independently correct repairs of one defect merge without a single conflict. A collision that produces a conflict is the benign case; this class is defined by the absence of one. THE SECOND-ORDER TRAP: the ranking conversation consumes the attention that the standing-down would need. Agreement about WHICH repair wins reads as resolution, and it is not -- closing a pull request is the act, and here one of four was closed. Naming a winner is not the losers standing down. NOT TO BE MERGED WITH a_branch_property_falsified_by_a_derived_push, which shares only the SYMPTOM that no conflict is produced so nothing notifies. That symptom has at least three mechanisms -- this one, the derived-push one, and the generated-artifact merge driver refusing with no markers -- so it does not discriminate, and uniting on it is the bare alias section 3 forbids rather than the proven coincidence section 6 requires. The concrete cost of merging them is a grain mismatch section 4b(3) names as its own review tell: this row is genuinely blocked on cross-head knowledge, the other is decidable from a single ref TODAY, and one row carries one stated ceiling because RecurringFailureMode has no ceiling or trigger FIELD -- so the affordable class would be parked behind this one's capability and never rank for a climb it does not need. RUNG: found BELOW the ladder. The duplicated meaning is a silently wrong answer -- every copy parses, every gate is green, and the projection of a triple-landed repair is indistinguishable from a correct one by any executing check. CEILING 2, mechanically preventable, and the reason it is not higher is that the writer population is not decidable from the repository: it includes future human decisions about whether to close a head. What IS decidable is the SUBJECT overlap between open heads. NEXT-RUNG TRIGGER: the capability of resolving an open pull request head to the set of DECLARATIONS it modifies, which is the same Node-tree read the namespace authority already performs and which no instrument currently exposes. A path-grain fence does not discharge it, because this specimen was a single path and the collision was within one declaration's neighbourhood. REVIEW TELL: a lane stating why it CANNOT collide with another lane. The claim is a quantifier over writers, so ask which writers it ranges over and whether an unclosed peer head is inside it. And after a CONTESTED fix lands, re-measure the SUBJECT for duplication rather than re-measuring whether it is green -- green is exactly what triple-duplication looks like. - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. -- **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) +- **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. - **a printed remedy is mangled by the medium that prints it** (an instruction is authored correctly, and the channel that carries it to its reader REWRITES it in transit. The author reads the authored string and sees the remedy; the reader receives something shorter that no longer does what it says; and no error crosses the boundary, because the rewriting is the medium behaving exactly as specified. THE RECOGNITION RULE, and it is the whole row: THE SENDER SEES INTACT TEXT, THE RECIPIENT GETS A SILENTLY SHORTENED SENTENCE, AND NOTHING REPORTS A FAILURE. The send succeeds. So every instrument that reads the AUTHORED artifact -- grep, review, a byte-exact drift gate over the source -- agrees the remedy is present, because it is present. The defect is in the rendering, which means the subject has to be what the reader RECEIVES and never what the author wrote. FIRST SPECIMEN, calm-owl-417, gunbc#10460, and it is the one that names the class: gunbc.generated_artifact_merge_driver prints its repair recipe through a double-quoted `echo`, because the lines are templates and `$merged_path` must interpolate. Double quotes leave the backquote active, and the recipe's sed program wrote its capture group between backquotes. Bash ran `\([a-z0-9_]*\)` as a COMMAND SUBSTITUTION at echo time: two `command not found` lines on stderr, and the printed instruction came out with the capture group DELETED. Pasted, it extracted zero rows from both sides of a set difference, and a difference over two empty sets is empty -- which the recipe's own words read as `MUST BE EMPTY`. The mangled remedy reported the vacuous pass the intact remedy exists to prevent, at the exact moment rows were at risk. SECOND SPECIMEN, same PR, and it is the same boundary read from the other side rather than a repeat: `$` stays active ON PURPOSE, so a `$1` written into a recipe line is the DRIVER's argv and not the reader's. An extractor drafted as a shell function taking `$1` rendered as `git show "O:docs/design-rung-drops.md"` -- git's own `%O` placeholder substituted into the instruction it was printing. Escaping does not save that one, because the escaper doubles backslashes first, so the remedy is to name only the variable the printer DEFINES. The two specimens together are why the class is the MEDIUM and not the escaping: one is a character the medium consumed, the other is a character the medium substituted, and no single escaping rule addresses both. THE DAMAGE IS NOT PROPORTIONAL TO THE CORRUPTION, and this is the sharpest available statement of the class. The repaired extractor carries TWO sed clauses, because the two delegated projections spell a row identity differently -- a backquoted slug bullet and a `### Title` heading. Mangling one character in the FIRST clause makes sed refuse the ENTIRE program on an invalid backreference, so the second clause, which nothing touched, extracts nothing either. Measured against a fixture carrying both spellings and 157 rostered rows: the intact program names 157, the mangled program names ZERO -- not 36, which is what a reader assuming per-clause degradation would predict. One corrupted character silenced a projection it never appeared in. THIRD AND FOURTH SPECIMENS, 2026-09-05, AND THEY ARE WHAT MAKE THIS A CLASS RATHER THAN A FACT ABOUT MERGE DRIVERS: the same mechanism fired twice more within ninety minutes, in a completely different medium, in messages ABOUT this class. An agent messaging body was passed to `dashboard-message --body` as a double-quoted shell argument; the body contained backquoted identifiers; bash substituted them away and the recipients received sentences with phrases missing. One of the two was a message whose SUBJECT was instruments that report success without carrying their claim. The other was sent by the author repairing the first specimen, while writing about the first specimen, roughly an hour after reading the analysis describing it. WHAT THOSE TWO ADD: the class is not a property of emitted artifacts or of merge drivers. It is a property of ANY text handed to a shell-quoted channel, and the agent messaging channel is one of them and is uninstrumented at both ends. THE OBVIOUS DEFENCE IS THE THING THAT FAILED, and a reader will supply the wrong remedy here unless it is stated. `Remember to quote correctly` was in active working memory in both messaging specimens and did not prevent either. The diagnosis in both cases was identical: the protection was attached to a command SPELLING rather than to the hazard -- one command was being single-quoted correctly all evening, and a DIFFERENT command was reached for with double quotes without re-deriving the rule. Vigilance that is bound to a habit does not transfer to the next call site. THE MECHANICAL FORM, which is the operative remedy and is cheap: write the body to a file with a QUOTED heredoc, then pass `"$(cat file)"`. Single-quoting the argument is insufficient, not merely awkward, because prose reliably contains apostrophes and one apostrophe ends the quoting. THE MANGLING IS NOT MERELY LOSSY -- IT EXECUTES, IN THE SENDER'S ENVIRONMENT, AND THAT IS THE MOST UNDER-STATED PROPERTY OF THIS CLASS. Discovered as physical residue rather than by reasoning: the third specimen's message contained the phrase `-> Bool` inside a double-quoted argument, and the shell read it as a COMMAND `-` that was not found, followed by a REDIRECTION that created an empty file named `Bool` in the repository working tree. It sat there untracked for fifteen minutes and was found by `git status`, not by anything watching the channel. The general statement is worse than the instance: a backquoted span inside double quotes is COMMAND SUBSTITUTION, which is arbitrary execution, and an unquoted redirection operator writes files. So the sender's mistake is not that a sentence arrived shortened; it is that a fragment of English prose was EVALUATED AS A PROGRAM against a live working tree. The lossy reading of this class is the benign one. AND IT WAS ONE ROUTINE COMMAND FROM BEING COMMITTED: the author of that instance stages with `git add -A`, so the next commit would have swept an artifact of a mangled chat message into a pull request, with a plausible-looking name and no origin anyone could reconstruct. THE REASON IT DID NOT LAND IS COMMIT ORDERING AND NOTHING ELSE: the last commit happened to precede the corruption by fifteen minutes. Recording that as `it did not land` would be luck reported as safety, which is the reporting failure this ledger exists to refuse -- no mechanism was involved, nothing detected it, and nothing would have blocked it. The file's name resembles a type this corpus uses everywhere, so review would most likely have read it as an intentional stray rather than as residue, and its provenance is unreconstructable from the diff. THE CHECK THIS LICENSES, and it caught something on its FIRST application in a second tree: inspect `git status` for unexplained worktree changes after any session that has sent shell-quoted messages, and do not stage with `git add -A` in a tree where that has happened. Applied immediately by a second author, it found no execution residue but did surface a forgotten STAGED modification to a generated artifact, checked out during verification and left behind -- a different cause with the same shape, on a branch that was main. GENERALIZATION: the hazard is not restricted to mangled messages. ANY command that writes into the worktree as a side effect of verification leaves residue a reviewer cannot distinguish from intent. The heredoc form prevents the cause; the status check catches damage already done, whatever wrote it. THE TRANSPORT'S SUCCESS SIGNAL WAS NEVER ASKED THE RELEVANT QUESTION, and this is the precise form of `no error crosses the boundary`. Successful delivery answers whether the SUBMITTED payload reached its destination. It does not answer whether that payload preserved the INTENDED message. Those are two different claims, and every send in every specimen above was truthful about the first while nothing anywhere was asked the second. A PRESERVATION CHECK MUST BE ANCHORED BEFORE THE DANGEROUS TRANSFORMATION, AND THE OBVIOUS IMPLEMENTATION IS WORTHLESS. The natural control is to digest what was sent and compare it with what arrived. If that digest is computed AFTER the shell has consumed the backquoted spans, it faithfully attests the CORRUPTED payload and reports a perfect match: both sides agree, the comparison is sound, and the defect is invisible, because the damage happened upstream of the first observation. The comparison must STRADDLE the transformation that does the damage -- intended payload captured BEFORE it, against payload received after it -- which is this row's own recognition rule applied to its own remedy. It is the same shape as the neighbouring instruments in this ledger, one layer further out: `comm` cannot report that its own operand was empty, a digest over emitted driver bytes proves an escape was APPLIED but not that it was CORRECT, and a preservation digest taken downstream of the corruption proves only that the corruption was transmitted reliably. QUALIFICATION, so a correct transport does not read as corruption: where a channel LEGITIMATELY transforms encoding or line endings, the comparison needs an explicit preservation contract stating what is allowed to change. THE RECEIVING END FAILS TOO, AND THAT COMPLETES THE PICTURE. Recorded as evidence about DETECTABILITY rather than about the mechanism: in the third specimen a reader received the corrupted message, read it, and did not identify it as corrupted. The missing expressions and an injected directory listing were both present in what they received, and the surrounding prose still supported the conclusions the conversation reached -- so the damage was survivable enough to be invisible, and they flagged only afterwards that those spans should have been treated as corrupt. So there are THREE parties and no signal at any of them: the sender sees intact text, the transport reports success against a question about a different payload, and the recipient reads fluent prose with holes in it. THE CLASS FIRED IN PRODUCTION ON THE BRANCH THAT CLOSES IT, AT EQUAL ROW COUNTS, and this receipt is a discriminating RED on the real acceptance path rather than a fixture. Merging main into that branch reached this driver on docs/design-failure-modes.md. It refused as designed -- zero conflict markers, three index stages, the ours side left clean -- and the repaired step 2, run verbatim, measured: base 126 rows, ours 126 rows, COUNTS EQUAL, with the ours side dropping exactly one row that main had added, `duplicate_record_literal_field_silently_last_wins`, and adding one of its own. A cardinality check passes on that comparison and reports nothing. Only the identity join names the casualty. So the case a reviewer had challenged the branch to cover -- a substitution that preserves the count -- arrived unprompted, on the same projection, within minutes of the controls for it being enrolled; the branch did not have to argue that the case was reachable, because it walked in. RUNG FOUND AT: silent wrongness wherever the subject read by any instrument is the authored string, because there the defect is invisible by construction; mitigatable where a witness reads the EMITTED artifact, which is where gunbc#10460 put it. CEILING: mechanically preventable for media this repository emits into -- a witness that executes the emitted artifact and reads what the reader receives cannot be satisfied by an authored string, and the shell-quoting encodings have single authorities that a hand-spelled call site can be made not to bypass. It stays MITIGATABLE for channels this repository does not own, such as an external tool's argv handling, which is observed rather than proven. NEXT-RUNG TRIGGER: the bash medium read forward from grammar rows, so a printed line's variable references and shell-active characters are DECLARED rather than spelled -- at which point no emitter concatenates a shell literal and the escaping arm has nothing to get wrong. That is the same trigger gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_emit_dissolution_trigger already carries, and it is named here rather than restated because one capability retires both. DISTINGUISHING FACT AGAINST ITS NEAREST NEIGHBOURS. It is NOT `empty_capture_read_as_clean_result`: there a well-formed refusal travels on a stream the reader did not keep, and the reader's error is consuming the emptiness; here the text is rewritten before anyone reads it and the reader's conduct is irrelevant. The empty operand is one CONSEQUENCE of this class, not the class, and the second specimen proves it -- a substituted positional argument is not an empty capture at all. It is NOT `restoration_promise_names_a_route_that_does_not_exist`: there the named route is absent; here it exists and the rendering breaks it.) From b852b0ad34fafc65e9e2ea0ac1a5622335fd3567 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 05:10:54 +0000 Subject: [PATCH 04/17] Write the SourceUnreadable arm at the one consumer that matches the cause totally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Floor blocker on #10645, and it is the class this branch keeps finding, turned on the branch itself: widening a coproduct is defeated at the CONSUMER, not at the declaration. I grepped for total matches before adding SourceUnreadable and concluded there were none. The grep looked for the cause variants; this match is over the OUTER SourceFindingsStanding with the cause nested inside the pattern, so it never appeared in the results. accumulator_copy_roster_standing_test.dag:72:3: error: non-exhaustive match: missing variant(s) SourceFindingsNotEstablished { cause: SourceUnreadable } The wall caught what the search missed, which is the whole argument for the match being total with no wildcard: a catch-all there would have absorbed the new variant silently and the widening would have shipped looking complete. SourceUnreadable is unreachable on that call -- source_findings takes the text it is handed and never reads a file -- so the arm answers false, and the annotation explaining why sits ABOVE the declaration: the first cut put it inside the match body, which §4c refuses at module-item grain (six blocking errors, caught by the same local check). Verified under the gate-equivalent tree view rather than a plain resolve, because a plain resolve does not decide exhaustiveness: gunbc compile --dependency-pool-index primary-precedence over this entry and the three other entries the widening reaches -- 0 blocking errors on all four. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- .../complexity/accumulator_copy_roster_standing_test.dag | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/v2/test/claim/complexity/accumulator_copy_roster_standing_test.dag b/src/v2/test/claim/complexity/accumulator_copy_roster_standing_test.dag index 5bca831e2f4..df8568a9516 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_roster_standing_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_roster_standing_test.dag @@ -19,7 +19,8 @@ import v2.lens.common.source_analysis_standing { LanguageGrammarUnavailable, SourceNormalizationRejected, SourceParseRejected, - SourceTokenizationRejected + SourceTokenizationRejected, + SourceUnreadable } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.std.logic { Bool } @@ -68,6 +69,11 @@ test fn not_established_cause_is_preserved_on_both_axes() -> Bool { } } +// EVERY CAUSE IS WRITTEN OUT BELOW, INCLUDING ONE THIS CALL CANNOT PRODUCE. source_findings takes +// the source text it is handed and never reads a file, so SourceUnreadable is unreachable here and +// its arm answers false. The arm exists because the match is deliberately total with no wildcard, +// and that totality is what made widening the cause coproduct fail LOUDLY at this consumer instead +// of defaulting through a catch-all -- the wall doing its job on the change that added the variant. test fn malformed_source_reports_tokenization_rejection() -> Bool { match v2.lens.complexity_accumulator_copy.roster_gate.source_findings(source_text: "@@@ not dag source @@@ )( unterminated \"") { SourceFindingsNotEstablished { cause: SourceTokenizationRejected } => true @@ -75,6 +81,7 @@ test fn malformed_source_reports_tokenization_rejection() -> Bool { SourceFindingsNotEstablished { cause: LanguageGrammarRejected } => false SourceFindingsNotEstablished { cause: SourceParseRejected } => false SourceFindingsNotEstablished { cause: SourceNormalizationRejected } => false + SourceFindingsNotEstablished { cause: SourceUnreadable } => false SourceFindingsEstablished { findings: _ } => false } } From 43af9919fa75dc9ac0e7dbf51f4b9831be341d85 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 06:24:25 +0000 Subject: [PATCH 05/17] Split the lens witnesses by measured cost, and carry the refusal census into the verdict Two things, both found by CI rather than by reading, and both about a verdict that said less than it knew. REVIEW 61280 (REQUEST_CHANGES) IS HALF RIGHT, AND THE HALF IT IS RIGHT ABOUT IS REAL. It reported that the no-suspect arm discarded refusal_sites and refusal_causes, and it did: corpus_report computed both and the disposition threw them away, so a caller reading the disposition could not tell a population with no refusals from one with hundreds. That is the collapse of a typed standing this branch exists to stop, committed at its own last step. The arm now carries the census and is renamed CorpusNoSuspectsObserved, because "clean" asserted more than was established. The other half is declined, and by a standing ruling rather than by preference. The review asked for a refusal disposition BEFORE the corpus may be declared clean -- that is, for refusals to gate. The operator ruling of 2026-07-13, carried in this lens's own compile_gate_law, splits the Finding coproduct so that a Poly2Suspect rejects while Unclassifiable causes ride the accepted channel "typed per-cause, located, counted, NEVER GATING AND NEVER SILENTLY DROPPED". Gating would also be wrong on the facts: refusals are the EXPECTED state of a real subject -- the sibling roster budgets them per file with ceilings -- so an arm refusing on refusal_sites > 0 would refuse essentially every real population and convey nothing by doing it. The new witness asserts the payload, not the arm name, on a real subject whose residue the roster already budgets. THE FLOOR BLOCKED FIVE WITNESSES WITH interrupted_before_verdict, AND THE CAUSE IS PRICE, NOT CORRECTNESS. Measured per witness: a five-line fixture ~4.7s, a 186-line module ~50s, the 874-line analyze.dag ~1272s -- ingest cost scales with subject size and does so superlinearly. The floor budgets an ENTRY rather than a witness, so one twenty-minute member reported every sibling in its file as interrupted, taking cheap evidence down with it. So the witnesses are split by cost, not by importance. Every discriminating control -- the typed-read pair, the corpus-path planted/clean pair, the population-floor red -- now sits in claim/complexity/ entries that run in seconds and stay ON the floor; the positive control was repointed from a 186-line module to the five-line fixture, which establishes the identical claim at 2.8s instead of fifty. Only the corpus-grain witnesses, whose price is the corpus, move behind a floor exclusion. AND THE EXCLUSION HAD TO GO WHERE THE FLOOR ACTUALLY LOOKS. The ci_layer_roots row added earlier governs DISCOVERY only; run_required_floor consults floor_prepared_subject_exclusions and nothing else, as that list's own note records. The roster-gate entry is the sharper case: it is operator-ruled OFFLINE by its own note and had NO floor exclusion at all -- it stayed off the floor only because nobody edited it, and repointing its stale row was the first edit. "Nobody has touched it lately" is not an exclusion. Verified: gunbc compile --dependency-pool-index primary-precedence over all three entries (0 blocking errors), cargo check -p v1-compiler, and the five floor-bound witnesses green at 29ms..3.6s. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- .../src/cli_run/required_floor_runner.rs | 27 +++++ .../corpus_gate.dag | 35 +++++- .../accumulator_copy_corpus_path_test.dag | 98 +++++++++++++++ ...cumulator_copy_unreadable_subject_test.dag | 15 ++- .../accumulator_copy_corpus_census_test.dag | 113 ++++++------------ 5 files changed, 205 insertions(+), 83 deletions(-) create mode 100644 src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index c867cea34eb..bb4e02ac790 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -2856,6 +2856,33 @@ pub fn floor_prepared_subject_exclusions() -> Vec { // is not observable hermetically. Enrolling it would assert the mock rather than the // behaviour -- specification without execution. "test/manual/command_runner_local_argv_receipt_test.dag".to_string(), + // COST, MEASURED PER WITNESS, NOT A CLASS JUDGEMENT. The accumulator-copy lens re-ingests + // each subject through tokenize/parse/normalize in the interpreter, and that cost scales + // with subject size superlinearly: a five-line fixture is ~4.7s, a 186-line module ~50s, and + // the 874-line analyze.dag ~1272s. The floor budgets an ENTRY rather than a witness, so one + // expensive member reports every sibling in its file as interrupted_before_verdict -- + // which is how these two entries first announced themselves, taking cheap evidence down + // with them. The cheap discriminating witnesses were split OUT of them (the corpus-path + // pair, the population-floor control and the typed-read pair now live under + // claim/complexity/ and are deliberately NOT excluded, so the class keeps executing + // evidence on every push). What is excluded here is only the corpus-grain work whose price + // is the corpus. + // + // Both entries also carry a gunbc.ci_layer_roots exclusion, and that row governs DISCOVERY + // ONLY -- run_required_floor consults THIS list and nothing else, as the note above records. + // The census entry needed both. + // + // The roster-gate entry is the sharper case and it was never safe: it is operator-ruled + // OFFLINE (~15m serial by its own note) and had no floor exclusion at all. It stayed off + // the floor only because nobody edited it -- the floor admits CHANGED witnesses, so the + // first edit to it blocks, which is what happened when its stale glob_discovery_law.dag row + // was repointed. "Nobody has touched it lately" is not an exclusion. + // + // TRIGGER, naming the capability rather than an artifact: ingest that does not re-parse + // source in the interpreter (native ingest, or a lens reading the compile graph). At that + // point the corpus-grain witnesses are affordable and both rows delete. + "test/claim/long/accumulator_copy_corpus_census_test.dag".to_string(), + "test/claim/complexity/accumulator_copy_roster_gate_test.dag".to_string(), ] } diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag index 7bd159cbe53..7eee215604f 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag @@ -222,11 +222,35 @@ fn corpus_report(pool_roots: List) -> CorpusReport { } // AN EMPTY POPULATION IS ITS OWN ARM. Without it a walk that resolved no modules at all -- a wrong -// pool root, a producer outage -- answers CorpusClean, which is the affirmative "nothing wrong +// pool root, a producer outage -- answers the no-suspect arm, which is the affirmative "nothing wrong // here" for a run that asked nothing. +// THE NO-SUSPECT ARM CARRIES THE RESIDUE, AND IT IS NOT NAMED "CLEAN". Review 61280 read this arm +// as discarding the refusal census and asked for a refusal disposition before the corpus may be +// declared clean. Half of that is right and the half that is not is governed by a standing ruling, +// so both halves are written down here rather than settled by preference. +// +// REFUSALS DO NOT GATE, and that is not this module's choice. The operator ruling of 2026-07-13, +// carried in this lens's own compile_gate_law, splits the Finding coproduct on the suspect/refusal +// axis: a Poly2Suspect rejects, while Unclassifiable causes are the undecidable residue that rides +// the accepted channel -- "typed per-cause, located, counted, NEVER GATING AND NEVER SILENTLY +// DROPPED". Gating here would also be wrong on the facts rather than merely out of order: refusals +// are the EXPECTED state of a real subject, which is why the roster gate budgets them per file with +// ceilings rather than forbidding them, so a corpus arm that refused on refusal_sites > 0 would +// refuse essentially every real population and convey nothing by doing it. +// +// NEVER SILENTLY DROPPED IS THE CLAUSE THIS ARM BROKE. corpus_report computes refusal_sites and +// refusal_causes and the disposition threw both away, so a caller reading only the disposition could +// not tell a population with no refusals from one with hundreds -- the collapse of a typed standing +// onto a smaller one that this module exists to stop, committed at its own last step. The arm now +// carries the census, and it is renamed: "clean" asserted more than was established, and the honest +// name says exactly what was found -- no suspects, with this residue beside it. type CorpusGateDisposition - = CorpusClean { files: Int } + = CorpusNoSuspectsObserved { + files: Int, + refusal_sites: Int, + refusal_causes: List + } | CorpusSuspectObserved { paths: List, sites: Int } | CorpusNotEstablishedObserved { rows: List } | CorpusPopulationBelowFloor { files: Int, floor: Int } @@ -242,7 +266,12 @@ fn corpus_gate_disposition(report: CorpusReport, population_floor: Int) -> Corpu match report.not_established { Cons { head: _, tail: _ } => CorpusNotEstablishedObserved { rows: report.not_established } - Empty => CorpusClean { files: report.files } + Empty => + CorpusNoSuspectsObserved { + files: report.files, + refusal_sites: report.refusal_sites, + refusal_causes: report.refusal_causes + } } } } diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag new file mode 100644 index 00000000000..b960fe7221f --- /dev/null +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -0,0 +1,98 @@ +module v2.test.claim.complexity.accumulator_copy_corpus_path_test + +import std.algebra { Cons, Empty } +import v2.lens.complexity_accumulator_copy.corpus_gate { + CorpusFileNotEstablished, + CorpusFileRow, + CorpusFileScanned, + CorpusNoSuspectsObserved, + CorpusNotEstablishedObserved, + CorpusPopulationBelowFloor, + CorpusSuspectObserved, + corpus_gate, + corpus_rows_for_paths +} +import v2.std.collection { List } +import v2.std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +// THE CORPUS PATH'S DISCRIMINATING EVIDENCE, SPLIT HERE BY COST SO IT CAN RUN ON THE FLOOR. +// Everything in this file reads a five-line fixture or nothing at all, so it is seconds; the +// corpus-grain witnesses that read real modules are in +// v2.test.claim.long.accumulator_copy_corpus_census_test and are floor-excluded with their measured +// cost. The split exists because the floor budgets an ENTRY, not a witness: one twenty-minute +// member made every other witness in the same file report interrupted_before_verdict, so cheap +// evidence was being lost to an expensive neighbour rather than to any fault of its own. + +fn fixture_rows() -> List { + corpus_rows_for_paths(paths: [ + "test/fixture_roots/complexity_accumulator_copy/planted_copy.dag", + "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag" + ]) +} + +// TWO PREDICATES OVER THE STANDING, NOT ONE PROJECTION TO A COUNT. An earlier cut answered the +// suspect count and returned -1 for a subject the ingest could not read, so an unreadable fixture +// would fail both witnesses rather than satisfying the clean one. The reasoning was right and the +// carrier was wrong: a sentinel packed into an Int is the same collapse of a typed standing onto a +// scalar this lane exists to undo, and it is safe only while every caller remembers what -1 means. +// Each predicate answers its own question and answers FALSE for a not-established subject, which +// gives the identical guarantee with nothing to remember -- verified by pointing fixture_rows at +// two absent paths and watching BOTH witnesses go red. +fn row_has_suspect(row: CorpusFileRow) -> Bool { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s > 0 + CorpusFileNotEstablished { cause: _ } => false + } +} + +fn row_scanned_without_suspect(row: CorpusFileRow) -> Bool { + match row.standing { + CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s == 0 + CorpusFileNotEstablished { cause: _ } => false + } +} + +// THE PAIR IS A PAIR ON PURPOSE. The lens already has a planted-source control that hands a String +// to source_finding_standing, and that one proves the DETECTOR fires. It does not prove the CORPUS +// path fires, because that path is path -> typed read -> ingest -> row and every step of it is new. +// These two subjects differ in exactly one authored construction -- the accumulating fold copies its +// carrier through a registered combiner in one and adds to it in the other -- so a green on both +// says the path discriminates rather than that it answers a constant. +// +// They live under test/fixture_roots/, outside every source root, because the planted subject is +// deliberately a quadratic and must never be compiled as production source or reached by the corpus +// population. +test fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { + match fixture_rows() { + Cons { head: planted, tail: _ } => row_has_suspect(row: planted) + Empty => false + } +} + +test fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { + match fixture_rows() { + Cons { head: _, tail: Cons { head: clean, tail: _ } } => row_scanned_without_suspect(row: clean) + _ => false + } +} + +// THE POPULATION FLOOR'S OWN RED CONTROL. Without it the no-suspect arm is reachable by a walk that +// resolved nothing at all, and an empty question reads as an affirmative answer. +// +// THE SUBJECT IS A REAL DIRECTORY HOLDING NO MODULES, NOT AN ABSENT ONE, AND THE FIRST CUT HAD IT +// WRONG. Pointing this at a path that does not exist does not produce an empty population: the host +// refuses at root admission (anchor_source_root: "... is not a directory") and unwinds, so the +// witness PANICKED rather than answering. That refusal is correct -- a mistyped root is a caller +// defect and should stop the line -- but it means the below-floor arm's red is only authorable from +// a directory that is present and contributes no modules. docs/ is that subject. +test fn an_empty_population_is_below_floor_rather_than_clean() -> Bool { + match corpus_gate(pool_roots: ["docs"], population_floor: 1) { + CorpusPopulationBelowFloor { files: _, floor: _ } => true + CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false + } +} diff --git a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag index 9231da89727..93599be4a24 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag @@ -40,11 +40,20 @@ test fn an_unreadable_subject_is_refused_with_its_cause() -> Bool { // THE POSITIVE CONTROL ON THE SAME PATH. Without it, a read that refused every subject would // satisfy the claim above while establishing nothing -- the arm would be permanently green for the -// wrong reason. This subject is a file the lens is already known to read and find clean (it is the -// roster's own cost-model row), so a green here says the typed read still reaches real bytes. +// wrong reason. A green here says the typed read still reaches real bytes and the lens still +// answers over them. +// +// THE SUBJECT IS A FIVE-LINE FIXTURE, AND THAT IS A COST DECISION MADE FROM MEASUREMENT. It pointed +// at a real 186-line production module first; that cost about fifty seconds and the required floor +// reported it interrupted_before_verdict, so the control was being lost to its own price. Ingest +// cost scales with subject size and does so superlinearly across this lane -- five lines is +// seconds, 186 lines is tens of seconds, 874 lines is over twenty minutes -- so a control whose +// entire claim is "a readable subject still reaches the lens" has no business buying a large one. +// The sanctioned clean fixture establishes the same thing and stays affordable to the lane that +// runs it on every push. test fn a_readable_subject_still_reaches_the_lens() -> Bool { match file_gate_disposition( - path: "src/v2/lens/complexity_accumulator_copy.dag", + path: "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag", refusal_ceiling: 0 ) { GateClean => true diff --git a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag index f33fa1a5742..ab95a335bca 100644 --- a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag +++ b/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag @@ -4,13 +4,14 @@ import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusFileNotEstablished, CorpusFileRow, CorpusFileScanned, - CorpusClean, + CorpusNoSuspectsObserved, CorpusGateDisposition, CorpusNotEstablishedObserved, CorpusPopulationBelowFloor, CorpusReport, CorpusSuspectObserved, corpus_gate, + corpus_gate_disposition, corpus_paths, corpus_report, corpus_report_from_rows, @@ -118,7 +119,7 @@ fn subtree_gate() -> CorpusGateDisposition { // A SUBTREE WHOSE SUBJECTS THE INGEST CANNOT READ IS NOT A CLEAN SUBTREE, and this witness says so // rather than passing. dag/extdeps/filesystem is deliberately the subject: three of its six modules // come back not-established under the v2 grammar, which is the reach fact the corpus consumer was -// built to surface. Asserting CorpusClean here would have required either excluding the subtree or +// built to surface. Asserting a no-suspect verdict here would have required either excluding the subtree or // widening the disposition until unreadable counted as fine, and both are the coverage-by-illusion // this file exists to end. The assertion is therefore that the gate REPORTS the unreadable // population -- and its failure mode is the one that matters: if the ingest silently started @@ -126,86 +127,12 @@ fn subtree_gate() -> CorpusGateDisposition { test fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { match subtree_gate() { CorpusNotEstablishedObserved { rows: _ } => true - CorpusClean { files: _ } => false + CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false CorpusSuspectObserved { paths: _, sites: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false } } -// THE POPULATION FLOOR'S OWN RED CONTROL. Without it the gate's clean arm is reachable by a walk -// that resolved nothing at all, and an empty question reads as an affirmative answer. -// -// THE SUBJECT IS A REAL DIRECTORY HOLDING NO MODULES, NOT AN ABSENT ONE, AND THE FIRST CUT HAD IT -// WRONG. Pointing this at a path that does not exist does not produce an empty population: the host -// refuses at root admission (anchor_source_root: "... is not a directory") and unwinds, so the -// witness PANICKED rather than answering. That refusal is correct -- a mistyped root is a caller -// defect and should stop the line -- but it means the below-floor arm's red is only authorable from -// a directory that is present and contributes no modules. docs/ is that subject. -test fn an_empty_population_is_below_floor_rather_than_clean() -> Bool { - match corpus_gate(pool_roots: ["docs"], population_floor: 1) { - CorpusPopulationBelowFloor { files: _, floor: _ } => true - CorpusClean { files: _ } => false - CorpusSuspectObserved { paths: _, sites: _ } => false - CorpusNotEstablishedObserved { rows: _ } => false - } -} - -// THE DISCRIMINATING PAIR FOR THE CORPUS PATH, AND IT IS A PAIR ON PURPOSE. The lens already has a -// planted-source control that hands a String to source_finding_standing; that one proves the -// DETECTOR fires. It does not prove the corpus path fires, because the corpus path is -// path -> typed read -> ingest -> row, and every one of those steps is new. These two subjects -// differ in exactly one authored construction -- the accumulating fold copies its carrier through a -// registered combiner in one and adds to it in the other -- so a green on both says the corpus path -// discriminates rather than that it answers a constant. -// -// They live under test/fixture_roots/, outside every source root, because the planted subject is -// deliberately a quadratic and must never be compiled as production source or reached by the -// corpus population. - -fn fixture_rows() -> List { - corpus_rows_for_paths(paths: [ - "test/fixture_roots/complexity_accumulator_copy/planted_copy.dag", - "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag" - ]) -} - -// TWO PREDICATES OVER THE STANDING, NOT ONE PROJECTION TO A COUNT. The first cut of this answered -// the suspect count and returned -1 for a subject the ingest could not read, so that an unreadable -// fixture would fail both witnesses rather than satisfying the clean one. That reasoning was right -// and the carrier was wrong: it is a sentinel packed into an Int, which is the same collapse of a -// typed standing onto a scalar that this whole change exists to undo -- and it only reads as safe -// while every caller remembers what -1 means. Each predicate below answers its own question and -// answers FALSE for a not-established subject, which gives the identical guarantee with nothing to -// remember: an unreadable fixture is neither suspect nor clean, so it fails both witnesses by -// construction rather than by arithmetic. -fn row_has_suspect(row: CorpusFileRow) -> Bool { - match row.standing { - CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s > 0 - CorpusFileNotEstablished { cause: _ } => false - } -} - -fn row_scanned_without_suspect(row: CorpusFileRow) -> Bool { - match row.standing { - CorpusFileScanned { suspects: s, refusals: _, causes: _ } => s == 0 - CorpusFileNotEstablished { cause: _ } => false - } -} - -test fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { - match fixture_rows() { - Cons { head: planted, tail: _ } => row_has_suspect(row: planted) - Empty => false - } -} - -test fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { - match fixture_rows() { - Cons { head: _, tail: Cons { head: clean, tail: _ } } => row_scanned_without_suspect(row: clean) - _ => false - } -} - // CAN THE IN-.DAG INGEST PATH FEED THE ASYMPTOTIC ENGINE OVER A REAL MODULE? ANSWERED BY EXECUTION: // NO, AND THE FIRST READING OF THE EVIDENCE SAID YES. The instruments below are what settled it and // they are kept because the answer is easy to get wrong in exactly the way it was got wrong here. @@ -324,3 +251,35 @@ fn ingested_root_child_labels(path: String) -> String { }) } } + +// THE ARM THAT USED TO ERASE ITS OWN CENSUS, WITNESSED ON A REAL SUBJECT. Review 61280 found that +// the no-suspect disposition dropped the refusal counts and causes the report had already computed. +// A witness that only checked the arm's NAME would have passed before the repair and after it, so +// this one asserts the payload: a positive refusal count and a non-empty cause census reaching a +// caller that reads the disposition alone. +// +// The subject is chosen because it is real and its residue is already known to the sibling roster, +// which budgets src/v2/lens/complexity_accumulator_copy/analyze.dag at a nonzero refusal ceiling -- +// the accumulator-threading walk earns honest combiner_unregistered_carrier_reaches refusals from +// this very lens. So this is not a fixture arranged to have refusals; it is the lens reading its +// own analyzer and reporting what it cannot decide. +fn analyzer_disposition() -> CorpusGateDisposition { + corpus_gate_disposition( + report: corpus_report_from_rows( + rows: corpus_rows_for_paths( + paths: ["src/v2/lens/complexity_accumulator_copy/analyze.dag"] + ) + ), + population_floor: 1 + ) +} + +test fn a_no_suspect_verdict_still_carries_its_refusal_census() -> Bool { + match analyzer_disposition() { + CorpusNoSuspectsObserved { files: _, refusal_sites: sites, refusal_causes: causes } => + (sites > 0) && (length(xs: causes) > 0) + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false + CorpusPopulationBelowFloor { files: _, floor: _ } => false + } +} From c8bbe6332b3ad5b6c57277ff7baa5d2df25f54a9 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 07:00:57 +0000 Subject: [PATCH 06/17] Stop excluding a module the corpus imports by name from the floor's prepared subject The floor refused the WHOLE subject at run 34016411960 before any witness ran: excluding test/claim/complexity/accumulator_copy_roster_gate_test.dag from floor_prepared_subject_exclusions does not skip its witnesses, it drops the path from prepare_repository_closure, so live_read_classification_test.dag:46 -- which imports that module by name for its ReadsLiveTree classification -- refused with `unresolved import`. Nothing in the change was measured. The exclusion was also priced against the wrong number. Measured on this branch, the entry's one changed witness costs 22.2s wall (22.1s of it entry resolve), not the ~15m its own note quotes; it reported interrupted_before_verdict alongside the two genuinely corpus-grain census witnesses and was excluded on that association. Those two remain excluded and nothing imports them. The standing constraint is now recorded on the list itself: a module with importers may not be excluded here, only one nothing names. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- .../src/cli_run/required_floor_runner.rs | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index bb4e02ac790..deb9e9da975 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -2861,28 +2861,39 @@ pub fn floor_prepared_subject_exclusions() -> Vec { // with subject size superlinearly: a five-line fixture is ~4.7s, a 186-line module ~50s, and // the 874-line analyze.dag ~1272s. The floor budgets an ENTRY rather than a witness, so one // expensive member reports every sibling in its file as interrupted_before_verdict -- - // which is how these two entries first announced themselves, taking cheap evidence down - // with them. The cheap discriminating witnesses were split OUT of them (the corpus-path + // which is how this entry first announced itself, taking cheap evidence down + // with it. The cheap discriminating witnesses were split OUT (the corpus-path // pair, the population-floor control and the typed-read pair now live under // claim/complexity/ and are deliberately NOT excluded, so the class keeps executing // evidence on every push). What is excluded here is only the corpus-grain work whose price // is the corpus. // - // Both entries also carry a gunbc.ci_layer_roots exclusion, and that row governs DISCOVERY - // ONLY -- run_required_floor consults THIS list and nothing else, as the note above records. - // The census entry needed both. + // The entry also carries a gunbc.ci_layer_roots exclusion, and that row governs DISCOVERY + // ONLY -- run_required_floor consults THIS list and nothing else, as the note above + // records. The census entry needed both. // - // The roster-gate entry is the sharper case and it was never safe: it is operator-ruled - // OFFLINE (~15m serial by its own note) and had no floor exclusion at all. It stayed off - // the floor only because nobody edited it -- the floor admits CHANGED witnesses, so the - // first edit to it blocks, which is what happened when its stale glob_discovery_law.dag row - // was repointed. "Nobody has touched it lately" is not an exclusion. + // AN EXCLUSION HERE IS NOT "SKIP THIS WITNESS": prepare_repository_closure drops the path + // from the prepared graph entirely, so every module that IMPORTS the excluded one refuses + // with `unresolved import` and the floor refuses the WHOLE subject before any witness runs + // (measured: run 34016411960, modules_resolved=2591, refused at + // src/v2/test/claim/long/live_read_classification_test.dag:46, which imports the roster + // gate entry by name for its ReadsLiveTree classification). That is the mechanism working + // -- the deletion is the census -- and it is the standing constraint on this list: a module + // with importers may not be excluded, only one nothing names. accumulator_copy_corpus_census + // has no importer, which is why it is the only row here. + // + // The roster-gate entry was excluded in the same change and is NOT excluded now, because + // the exclusion was priced against the wrong number. Its own note says ~15m serial; the + // measured cost of the one changed witness on this branch is 22.2s wall (claim_batch + // --entry accumulator_copy_roster_gate_test.dag --functions + // roster_glob_discovery_zero_suspects_within_ratchet), of which 22.1s is entry resolve. + // It reported interrupted_before_verdict alongside the two corpus-grain witnesses that + // really were expensive, and was excluded on that association rather than on its own price. // // TRIGGER, naming the capability rather than an artifact: ingest that does not re-parse // source in the interpreter (native ingest, or a lens reading the compile graph). At that - // point the corpus-grain witnesses are affordable and both rows delete. + // point the corpus-grain witnesses are affordable and this row deletes. "test/claim/long/accumulator_copy_corpus_census_test.dag".to_string(), - "test/claim/complexity/accumulator_copy_roster_gate_test.dag".to_string(), ] } From d2a4ef0fe4428082d0f5184a08722cda9f61a6b7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 07:35:52 +0000 Subject: [PATCH 07/17] A corpus-priced assertion cannot be a witness: make the census entry points, and wall the exclusion list Run 34018018622 refused with cause=ChangedWitnessOutsidePreparedSubject: the required floor admits a CHANGED witness regardless of any exclusion, so the two corpus-grain `test fn` rows introduced by this PR could not be excluded by the change that introduced them. That is correct and it is the point -- an exclusion a new witness could opt into on its own landing commit is the escape hatch DESIGN section 5 forbids. At ~80s per subject those rows can never run, and a witness that cannot run is roster membership standing in for a verdict. So they stop being witnesses. claim/long/accumulator_copy_corpus_census_test.dag moves to v2.lens.complexity_accumulator_copy.corpus_census as entry points beside the lens (a barren *_test.dag is itself refused by floor_naming_hygiene, so the rename is required, not cosmetic). Both exclusion rows -- the ci_layer_roots discovery row and the floor-preparation row -- delete with them. The executing evidence for the corpus path is unchanged and is now the whole of the claim: the cheap planted/clean pair in claim/complexity/accumulator_copy_corpus_path_test.dag, which runs on every push over a population corpus_gate discovers. Second: the constraint on floor_prepared_subject_exclusions is now a wall rather than a note. assemble_prepared_subject_closure refuses with cause=ExclusionOrphansImporter, naming the importer, the excluded module and the row that matched, when any retained module imports an excluded one. Previously that situation surfaced as `unresolved import` against a file nobody had touched, which is why its first two diagnoses both concluded the module had been moved. The import extractor is the one `import_resolution_facts` folds, and preparation already holds every source's bytes, so it costs no extra read. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- dag/gunbc/ci/ci_layer_roots.dag | 6 --- src/v1/stage0/src/cli_run.rs | 49 +++++++++++++++++++ .../corpus_census.dag} | 42 ++++++++++------ .../accumulator_copy_corpus_path_test.dag | 16 +++--- 4 files changed, 86 insertions(+), 27 deletions(-) rename src/v2/{test/claim/long/accumulator_copy_corpus_census_test.dag => lens/complexity_accumulator_copy/corpus_census.dag} (84%) diff --git a/dag/gunbc/ci/ci_layer_roots.dag b/dag/gunbc/ci/ci_layer_roots.dag index a0824b579f5..a15b8742529 100644 --- a/dag/gunbc/ci/ci_layer_roots.dag +++ b/dag/gunbc/ci/ci_layer_roots.dag @@ -403,12 +403,6 @@ data witness_exclusion_frontier: List = [ reason: "SUBSTANTIATED per-row (found 2026-08-06, PR #7894 CI red, run 31069215347): maintenance_census_report_positive_control_derives_from_current_head calls current_rust_maintenance_census_report(), which reaches current_stage0_rust_source_observation() and therefore git.Inspect.HeadCommit without mock_response — the same class stage0_rust_host_observation_live_witness_test.dag documents above; a refusing hermetic discovery member fails its whole batch (batch 3 discovery-corpus, 7298-row corpus). This row alone would only remove the function from hermetic discovery, which is not an executing consumer by itself (DESIGN.md 'witness cost derives from purpose', operator ruling 2026-08-04) — so, mirroring stage0_rust_host_observation_live_witness_test.dag's closing contract, the function is ALSO enrolled in bin_witness_wet_entries below (GITHUB_SHA supplied by the runner, not hand-set), which is its real per-PR executing consumer under WitnessHasExecutingConsumer standing; this exclusion row only keeps it off the hermetic per-PR discovery batch it cannot pass. This origin file has only the one witness, so no scaffold-witness residue remains offline-only.", dissolution: unbound_dissolution(description: "mock_response coverage lands for the git.Inspect revision-addressed operations used by current_stage0_rust_source_observation AND this assertion is re-checked under a fixed mock; or the fn enrolls on a scheduled wet lane. Either way this row deletes.") }, - WitnessExclusionRow { - pattern: "claim/long/accumulator_copy_corpus_census", - classification: OfflineLocalRecipe, - reason: "The corpus-grain consumer of the accumulator-copy lens (v2.lens.complexity_accumulator_copy.corpus_gate). Its population is the module index rather than a hand-authored path list, so its cost is the corpus: measured 2026-09-06 on this session's host, ~80s per subject through the same tokenize/parse/normalize ingest the roster gate uses, against 5007 tracked .dag files -- upwards of a hundred hours serial -- and it cannot be waited out, because one process dies with SIGSEGV around the twentieth subject (accumulation across subjects, not any one subject: the file it dies on analyses cleanly alone), so the sweep is batched by construction. That prices it out of every CI lane by the same argument that excluded claim/long/mandatory_tag_extdeps_corpus, and for the same reason: the walk is interpreted. The cheap discriminating controls for the same code path -- an unreadable subject refusing with its cause, and a readable one still reaching the lens -- are NOT here; they are ordinary witnesses in claim/complexity/accumulator_copy_unreadable_subject_test.dag and are not excluded.", - dissolution: unbound_dissolution(description: "witness realization executes the ingest at native speed, or the lens reads the compile graph instead of re-parsing source; either way the corpus census enrolls on a real cadence and this row deletes") - }, WitnessExclusionRow { pattern: "claim/long/mandatory_tag_extdeps_corpus", classification: OfflineLocalRecipe, diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index b2ef049dd8d..1d3ae721a3c 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -39227,6 +39227,55 @@ pub fn assemble_prepared_subject_closure( }); sources.push(sf.clone()); } + // AN EXCLUSION MAY NOT ORPHAN AN IMPORTER, AND THIS IS THE WALL THAT SAYS SO. + // + // The list reads like "skip these witnesses" and is not: a matched path is dropped from the + // prepared graph outright, so any module that still imports it refuses with `unresolved + // import` and the WHOLE subject refuses before a single witness runs. The refusal that + // reaches the operator then names the IMPORTER -- a file nobody touched -- while the row that + // caused it is somewhere else entirely, so the evidence points away from the cause. Measured + // as exactly that, twice in one hour: required-floor run 34016411960 refused at + // src/v2/test/claim/long/live_read_classification_test.dag:46 for an exclusion added to + // `floor_prepared_subject_exclusions`, and the first two diagnoses of it both concluded the + // imported module had been moved or renamed. It had not. + // + // The constraint is decidable from data preparation already holds -- the retained sources and + // their import headers -- so it is a wall here and not a note beside the list. The import + // extractor is the same one `import_resolution_facts` folds, never a second reader. + if !discovery_exclusions.is_empty() { + let excluded_modules: HashSet<&str> = + discovery_exclusions.keys().map(|m| m.as_str()).collect(); + let mut orphaned: Vec = Vec::new(); + for (module_path, sf) in index.iter() { + if discovery_exclusions.contains_key(module_path) { + continue; + } + for imported in extract_import_paths(&sf.content) { + if excluded_modules.contains(imported.as_str()) { + let matched = &discovery_exclusions[&imported]; + orphaned.push(format!( + " {} imports '{}', excluded by row '{}'", + workspace_relative_repo_path(&sf.path), + imported, + matched + )); + } + } + } + if !orphaned.is_empty() { + orphaned.sort(); + orphaned.dedup(); + return Err(format!( + "PREPARED-SUBJECT REFUSAL cause=ExclusionOrphansImporter — {} retained module(s) \ + import a module this preparation excluded. An exclusion row removes the path \ + from the prepared graph, so the importer cannot resolve and the whole subject \ + would refuse naming the importer rather than the row. Only a module nothing \ + imports may be excluded.\n{}", + orphaned.len(), + orphaned.join("\n") + )); + } + } if sources.is_empty() { return Err("whole-tree corpus is empty (no .dag modules under source roots)".to_string()); } diff --git a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag similarity index 84% rename from src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag rename to src/v2/lens/complexity_accumulator_copy/corpus_census.dag index ab95a335bca..c142d2381a6 100644 --- a/src/v2/test/claim/long/accumulator_copy_corpus_census_test.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -1,4 +1,4 @@ -module v2.test.claim.long.accumulator_copy_corpus_census_test +module v2.lens.complexity_accumulator_copy.corpus_census import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusFileNotEstablished, @@ -68,10 +68,24 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // interpreter, measured on this session's host at roughly eighty seconds per subject against 5007 // tracked .dag files. That is not a scheduling inconvenience to be waited out; it is the same // displaced cost DESIGN section 1 prices, and it is why the lens has stood roster-bound rather than -// corpus-bound since it was written. The exclusion row is -// claim/long/accumulator_copy_corpus_census in gunbc.ci_layer_roots, and its dissolution names the -// capability -- native ingest, or a lens that reads the compile graph instead of re-parsing source -// -- rather than any artifact. +// corpus-bound since it was written. +// +// SO THIS FILE DECLARES NO WITNESS, AND THAT IS THE POINT RATHER THAN AN OMISSION. It was first +// written as claim/long/accumulator_copy_corpus_census_test.dag with two `test fn` rows and two +// exclusions -- a discovery row in gunbc.ci_layer_roots and a floor-preparation row -- and the +// floor refused both times, correctly. A CHANGED witness is admitted to the required floor +// regardless of any exclusion (cause=ChangedWitnessOutsidePreparedSubject, run 34018018622), which +// is exactly right: an exclusion that a new witness could opt into on the change that introduces it +// would be the escape hatch DESIGN section 5 forbids. A corpus-priced assertion therefore cannot be +// authored as a witness at all, and pretending otherwise buys a roster row that never runs. The +// executing evidence for this code path is the cheap pair in +// claim/complexity/accumulator_copy_corpus_path_test.dag -- a planted copied accumulator caught +// through corpus_gate and a clean fold that stays clean -- which run on every push. What lives here +// are entry points, run by a person, whose output belongs in a run. +// +// DISSOLUTION names the capability: ingest that does not re-parse source in the interpreter (native +// ingest, or a lens reading the compile graph). At that price the corpus form becomes affordable +// and these entry points become witnesses. // // ONE PROCESS CANNOT CARRY THE CORPUS, AND THE FAILURE IS A SEGMENTATION FAULT RATHER THAN A // DIAGNOSTIC. Measured twice with census_for_paths over a stratified sample of the tracked corpus: @@ -85,14 +99,14 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // So the census is run in BATCHES, which is why the instrument takes an explicit subject list // beside the discovered-root form. The batch ceiling is an observation about this host and this // build, not a modeled bound; it is stated here so the next author batches deliberately rather than -// discovering it as a silent truncation. Dissolution is the same trigger the exclusion row names -- -// native ingest -- plus a driver that runs batches per process and joins their reports. +// discovering it as a silent truncation. Dissolution is the same trigger named above -- native +// ingest -- plus a driver that runs batches per process and joins their reports. // -// EVERY WITNESS IN THIS FILE EXECUTES, WHICH IS WHY THE WHOLE-CORPUS ONE IS ABSENT. The obvious -// row to write here is a gate over the tracked corpus, and it is deliberately not written: nothing -// can run it, so it would be a documented red rather than an observed one -- roster membership -// standing in for a verdict. The subtree witness below names a real discovered population small -// enough to execute, and the corpus form stays a run of census_for_root until its cost dissolves. +// THE WHOLE-CORPUS FORM IS ABSENT EVEN AS AN ENTRY POINT WITH A FIXED ANSWER. The obvious thing to +// write is a gate over the tracked corpus; nothing can run it, so it would be a documented red +// rather than an observed one -- roster membership standing in for a verdict. The subtree predicate +// below names a real discovered population small enough to execute, and the corpus form stays a run +// of census_for_root until its cost dissolves. // // THE THREE INSTRUMENTS BELOW ARE ENTRY POINTS, NOT RESULTS. DESIGN section 6 asks that a // measurement be cited by naming the producer that re-derives it. population_for_root answers how @@ -124,7 +138,7 @@ fn subtree_gate() -> CorpusGateDisposition { // this file exists to end. The assertion is therefore that the gate REPORTS the unreadable // population -- and its failure mode is the one that matters: if the ingest silently started // answering clean for those subjects, this goes red. -test fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { +fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { match subtree_gate() { CorpusNotEstablishedObserved { rows: _ } => true CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false @@ -274,7 +288,7 @@ fn analyzer_disposition() -> CorpusGateDisposition { ) } -test fn a_no_suspect_verdict_still_carries_its_refusal_census() -> Bool { +fn a_no_suspect_verdict_still_carries_its_refusal_census() -> Bool { match analyzer_disposition() { CorpusNoSuspectsObserved { files: _, refusal_sites: sites, refusal_causes: causes } => (sites > 0) && (length(xs: causes) > 0) diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index b960fe7221f..b48f540778a 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -18,13 +18,15 @@ import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -// THE CORPUS PATH'S DISCRIMINATING EVIDENCE, SPLIT HERE BY COST SO IT CAN RUN ON THE FLOOR. -// Everything in this file reads a five-line fixture or nothing at all, so it is seconds; the -// corpus-grain witnesses that read real modules are in -// v2.test.claim.long.accumulator_copy_corpus_census_test and are floor-excluded with their measured -// cost. The split exists because the floor budgets an ENTRY, not a witness: one twenty-minute -// member made every other witness in the same file report interrupted_before_verdict, so cheap -// evidence was being lost to an expensive neighbour rather than to any fault of its own. +// THIS FILE IS THE ONLY EXECUTING EVIDENCE FOR THE CORPUS PATH, AND IT IS THE WHOLE OF IT. +// Everything here reads a five-line fixture or nothing at all, so it is seconds. The corpus-grain +// forms -- a census over a discovered root, over an explicit batch, and the subtree predicate -- +// are ENTRY POINTS, not witnesses, in v2.lens.complexity_accumulator_copy.corpus_census: at roughly +// eighty seconds per subject they cannot be enrolled, and a witness that cannot run is a roster row +// standing in for a verdict. So the claim this lane makes is scoped to what runs: the planted +// copied accumulator below is caught through corpus_gate and the clean fold beside it stays clean, +// over a population the gate discovers rather than a hand-list. The corpus-wide verdict is a run, +// and it is named where the instruments live. fn fixture_rows() -> List { corpus_rows_for_paths(paths: [ From 2182708b41392d4cf3de6ff3f526b52da2c3b366 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 07:49:35 +0000 Subject: [PATCH 08/17] Name the census's next-rung trigger, and record the executed RED for the exclusion wall DESIGN 4b(2): the corpus census is now a named entry point enrolled in nothing. That is honest and it is also the state that decays without any red appearing, so the class names its trigger rather than leaving it as "when someone runs it". It is can-climb-after-one-grounding: an ingest realization that does not re-parse source in the interpreter. The row states what that trigger must be SUFFICIENT FOR, because the loss is corpus-grain while the tempting trigger is per-subject: corpus_gate returning a verdict over the DISCOVERED population, inside one required-lane budget AND inside one process. Neither half implies the other -- a tenfold per-subject speedup leaves the sweep hours long, and a sweep that fits the budget still dies with SIGSEGV around the twentieth subject. An artifact delivering one half contributes to the trigger and does not retire it. The row also states what is NOT claimed: that the tracked corpus is free of copied accumulators. The ExclusionOrphansImporter wall's RED was executed, not assumed: excluding analyze.dag -- imported by three modules and changed by nobody -- refuses at preparation and names the exclusion row beside each importer, which is the half missing when the same situation was diagnosed twice as a module having moved. The comment names the command rather than transcribing the run. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- src/v1/stage0/src/cli_run.rs | 9 +++++++ .../corpus_census.dag | 26 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/v1/stage0/src/cli_run.rs b/src/v1/stage0/src/cli_run.rs index 1d3ae721a3c..f5158b6dd52 100644 --- a/src/v1/stage0/src/cli_run.rs +++ b/src/v1/stage0/src/cli_run.rs @@ -39242,6 +39242,15 @@ pub fn assemble_prepared_subject_closure( // The constraint is decidable from data preparation already holds -- the retained sources and // their import headers -- so it is a wall here and not a note beside the list. The import // extractor is the same one `import_resolution_facts` folds, never a second reader. + // + // ITS DISCRIMINATING RED IS AUTHORABLE AND WAS EXECUTED, not merely available: add a row for a + // module the corpus imports and nobody changed (`v2/lens/complexity_accumulator_copy/analyze.dag` + // was the specimen) and run `claim_executor --required-ci --source-root dag --source-root src/v2 + // --required-lane witnesses`. What has to hold in that receipt is not that something refused -- + // the old shape refused too -- but that the refusal names the EXCLUSION ROW beside each importer, + // which is the half that was missing when the same situation was diagnosed twice as a module + // having been moved. The positive control is every ordinary required-floor run: with no orphaning + // row the preparation is admitted and the floor proceeds to its own phases. if !discovery_exclusions.is_empty() { let excluded_modules: HashSet<&str> = discovery_exclusions.keys().map(|m| m.as_str()).collect(); diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index c142d2381a6..7574572aa0e 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -108,6 +108,32 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // below names a real discovered population small enough to execute, and the corpus form stays a run // of census_for_root until its cost dissolves. // +// THE NEXT-RUNG TRIGGER FOR THIS CLASS, AND WHAT IT MUST BE SUFFICIENT FOR. Enrolled in nothing is +// the honest standing for these entry points today, and it is also the standing that rots without a +// red ever appearing, so DESIGN section 4b's second meta-obligation applies: this is can-climb- +// after-one-grounding, not cannot-climb, and the grounding is named here rather than left as "when +// someone runs it". +// +// TRIGGER: an ingest realization for the accumulator-copy lens that does not re-parse source in the +// interpreter -- native ingest, or a lens that reads the compile graph the floor has already built. +// +// SUFFICIENT FOR, stated because a trigger naming less than the capability it restores is satisfied +// while the capability stays dead, and because the loss here is CORPUS-grain while the tempting +// trigger is per-subject: the capability is corpus_gate returning a verdict over the DISCOVERED +// population -- every module the module index yields, not a sample and not a subtree -- inside one +// required-lane budget and inside one process. Both halves are load-bearing and neither implies the +// other. A tenfold speedup per subject satisfies a per-subject reading of the trigger and still +// leaves the sweep hours long; and a sweep that fits the budget still dies with SIGSEGV around the +// twentieth subject, which is a failure carrying no cause, no locus and no count, so it cannot be +// enrolled at any speed. An artifact that delivers only one half CONTRIBUTES to this trigger and +// does not retire it. +// +// UNTIL THEN the claim this lane makes is the cheap pair in +// claim/complexity/accumulator_copy_corpus_path_test.dag, and it is stated at that grain: the lens +// catches a planted copied accumulator and leaves a clean fold alone, over a population corpus_gate +// discovers. What is NOT claimed is that the tracked corpus is free of copied accumulators. Reading +// the absence of a red here as that verdict is the specific misreading this row exists to refuse. + // THE THREE INSTRUMENTS BELOW ARE ENTRY POINTS, NOT RESULTS. DESIGN section 6 asks that a // measurement be cited by naming the producer that re-derives it. population_for_root answers how // many subjects a root contributes; census_for_root and census_for_paths answer what the lens finds From 318c451920c11aae7b446d1a2857fe5cf91fe254 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 08:06:17 +0000 Subject: [PATCH 09/17] File the SIGSEGV as its own failure-mode class; delete the now-inert exclusion row and its figures The census sweep dying by SIGSEGV was a subordinate clause in a message and existed nowhere else. It is a compiler failing to fail closed: DESIGN section 5 admits succeed-fully or fail-with-a-typed-located-diagnostic, and process death is neither -- no cause, no locus, no count, so nothing can enrol it, attribute it or bound it, and a sweep that died at subject k presents in the same shape as one that finished. Filed as gunbc.recurring_failure_mode sweep_terminated_by_process_death_rather_than_a_typed_refusal, rung found at BELOW the ladder (silent wrongness, not the bottom rung), ceiling structurally guaranteed because the property is decidable -- an explicit stack or depth bound in the interpreter's walk converts unbounded native recursion into a located refusal. The trigger states its sufficiency: any traversal deep enough to exhaust the native stack refuses, not a guard around one instrument and not a bigger stack. Fixing it is not in this PR. Separately, the floor exclusion row for accumulator_copy_corpus_census_test.dag was still standing after the file moved out from under it, so it matched nothing -- an inert row with a long justification, which is the shape the ledger warns about. It and its comment delete. What replaces it is the pair of constraints the list actually has, both enforced elsewhere rather than asked for in prose: a row may not name a module anything imports (ExclusionOrphansImporter), and a row is not how a changed witness gets out of running (ChangedWitnessOutsidePreparedSubject). Review 61303's advisory applied: the transcribed wall-clock figures are replaced by the entry points that re-derive them (census_for_paths, population_for_root). One of those figures had already rotted and bought a wrong exclusion, so this is the same lesson twice. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- dag/gunbc/recurring_failure_mode/roster.dag | 2 + ...cess_death_rather_than_a_typed_refusal.dag | 36 ++++++++++++++ .../src/cli_run/required_floor_runner.rs | 48 +++++-------------- .../corpus_census.dag | 21 ++++---- .../accumulator_copy_corpus_path_test.dag | 6 +-- 5 files changed, 65 insertions(+), 48 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag diff --git a/dag/gunbc/recurring_failure_mode/roster.dag b/dag/gunbc/recurring_failure_mode/roster.dag index f6446d17805..9590c9b4d31 100644 --- a/dag/gunbc/recurring_failure_mode/roster.dag +++ b/dag/gunbc/recurring_failure_mode/roster.dag @@ -143,6 +143,7 @@ import gunbc.recurring_failure_mode.transport_mock_success_independent_of_reques import gunbc.recurring_failure_mode.a_deconfliction_plan_does_not_enumerate_its_writers { a_deconfliction_plan_does_not_enumerate_its_writers } import gunbc.recurring_failure_mode.a_branch_property_falsified_by_a_derived_push { a_branch_property_falsified_by_a_derived_push } import gunbc.recurring_failure_mode.roster_is_its_own_denominator { roster_is_its_own_denominator } +import gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal { sweep_terminated_by_process_death_rather_than_a_typed_refusal } import gunbc.recurring_failure_mode.instrument_answers_outside_its_domain { instrument_answers_outside_its_domain } import gunbc.recurring_failure_mode.discriminating_evidence_authored_into_the_operational_population { discriminating_evidence_authored_into_the_operational_population } import gunbc.recurring_failure_mode.printed_remedy_is_mangled_by_the_medium_that_prints_it { printed_remedy_is_mangled_by_the_medium_that_prints_it } @@ -295,6 +296,7 @@ data recurring_failure_mode_roster: List = [ a_deconfliction_plan_does_not_enumerate_its_writers, a_branch_property_falsified_by_a_derived_push, roster_is_its_own_denominator, + sweep_terminated_by_process_death_rather_than_a_typed_refusal, instrument_answers_outside_its_domain, discriminating_evidence_authored_into_the_operational_population, printed_remedy_is_mangled_by_the_medium_that_prints_it, diff --git a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag new file mode 100644 index 00000000000..ef0e8d9f39a --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag @@ -0,0 +1,36 @@ +module gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data sweep_terminated_by_process_death_rather_than_a_typed_refusal: RecurringFailureMode = RecurringFailureMode { + identity: "sweep_terminated_by_process_death_rather_than_a_typed_refusal" as NonEmptyStr, + + receipts: [ + "**a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. ", + + "HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. ", + + "THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. ", + + "WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. ", + + "It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. ", + + "It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. ", + + "It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. ", + + "RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. ", + + "CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. ", + + "NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. ", + + "AN INTERIM MITIGATION EXISTS AND IS NOT THE CLIMB, recorded so it is not mistaken for one: a sweep whose derived population is known can compare the count of subjects it reported against the count it discovered and refuse on a shortfall. That is rung 1 -- the harm is contained, the run stops looking finished -- and it leaves the invalid state entirely writable. ", + + "RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.)", + ], + + evidence: [], +} diff --git a/src/v1/stage0/src/cli_run/required_floor_runner.rs b/src/v1/stage0/src/cli_run/required_floor_runner.rs index deb9e9da975..53433d49f04 100644 --- a/src/v1/stage0/src/cli_run/required_floor_runner.rs +++ b/src/v1/stage0/src/cli_run/required_floor_runner.rs @@ -2856,44 +2856,18 @@ pub fn floor_prepared_subject_exclusions() -> Vec { // is not observable hermetically. Enrolling it would assert the mock rather than the // behaviour -- specification without execution. "test/manual/command_runner_local_argv_receipt_test.dag".to_string(), - // COST, MEASURED PER WITNESS, NOT A CLASS JUDGEMENT. The accumulator-copy lens re-ingests - // each subject through tokenize/parse/normalize in the interpreter, and that cost scales - // with subject size superlinearly: a five-line fixture is ~4.7s, a 186-line module ~50s, and - // the 874-line analyze.dag ~1272s. The floor budgets an ENTRY rather than a witness, so one - // expensive member reports every sibling in its file as interrupted_before_verdict -- - // which is how this entry first announced itself, taking cheap evidence down - // with it. The cheap discriminating witnesses were split OUT (the corpus-path - // pair, the population-floor control and the typed-read pair now live under - // claim/complexity/ and are deliberately NOT excluded, so the class keeps executing - // evidence on every push). What is excluded here is only the corpus-grain work whose price - // is the corpus. + // A ROW HERE MAY NOT NAME A MODULE ANYTHING IMPORTS. An exclusion is not "skip this + // witness": prepare_repository_closure drops the path from the prepared graph, so an + // importer cannot resolve and the whole subject refuses. That constraint is enforced -- + // `assemble_prepared_subject_closure` refuses with cause=ExclusionOrphansImporter, naming + // the row beside each importer -- rather than asked for here. // - // The entry also carries a gunbc.ci_layer_roots exclusion, and that row governs DISCOVERY - // ONLY -- run_required_floor consults THIS list and nothing else, as the note above - // records. The census entry needed both. - // - // AN EXCLUSION HERE IS NOT "SKIP THIS WITNESS": prepare_repository_closure drops the path - // from the prepared graph entirely, so every module that IMPORTS the excluded one refuses - // with `unresolved import` and the floor refuses the WHOLE subject before any witness runs - // (measured: run 34016411960, modules_resolved=2591, refused at - // src/v2/test/claim/long/live_read_classification_test.dag:46, which imports the roster - // gate entry by name for its ReadsLiveTree classification). That is the mechanism working - // -- the deletion is the census -- and it is the standing constraint on this list: a module - // with importers may not be excluded, only one nothing names. accumulator_copy_corpus_census - // has no importer, which is why it is the only row here. - // - // The roster-gate entry was excluded in the same change and is NOT excluded now, because - // the exclusion was priced against the wrong number. Its own note says ~15m serial; the - // measured cost of the one changed witness on this branch is 22.2s wall (claim_batch - // --entry accumulator_copy_roster_gate_test.dag --functions - // roster_glob_discovery_zero_suspects_within_ratchet), of which 22.1s is entry resolve. - // It reported interrupted_before_verdict alongside the two corpus-grain witnesses that - // really were expensive, and was excluded on that association rather than on its own price. - // - // TRIGGER, naming the capability rather than an artifact: ingest that does not re-parse - // source in the interpreter (native ingest, or a lens reading the compile graph). At that - // point the corpus-grain witnesses are affordable and this row deletes. - "test/claim/long/accumulator_copy_corpus_census_test.dag".to_string(), + // A row may also not be the way a CHANGED witness gets out of running. The floor admits a + // changed witness regardless of any exclusion (cause=ChangedWitnessOutsidePreparedSubject), + // which is what stops this list being an escape hatch a witness can opt into on the commit + // that introduces it. A corpus-priced assertion therefore is not authored as a witness at + // all; v2.lens.complexity_accumulator_copy.corpus_census is the worked example, and it + // carries its own next-rung trigger. ] } diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index 7574572aa0e..b609526ebc8 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -65,8 +65,10 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // THE COST IS THE REASON THIS FILE IS OFFLINE, AND IT IS THE LENS'S OWN SUBJECT MATTER. The // corpus consumer re-ingests every subject through tokenize -> parse_production -> normalize in the -// interpreter, measured on this session's host at roughly eighty seconds per subject against 5007 -// tracked .dag files. That is not a scheduling inconvenience to be waited out; it is the same +// interpreter, and the per-subject price is minutes rather than milliseconds against a corpus of +// thousands. The figure is not written here: census_for_paths below re-derives it on the host that +// asks, and population_for_root re-derives the denominator, which is what a reader needs rather +// than a digit that decays. That is not a scheduling inconvenience to be waited out; it is the same // displaced cost DESIGN section 1 prices, and it is why the lens has stood roster-bound rather than // corpus-bound since it was written. // @@ -88,8 +90,9 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // and these entry points become witnesses. // // ONE PROCESS CANNOT CARRY THE CORPUS, AND THE FAILURE IS A SEGMENTATION FAULT RATHER THAN A -// DIAGNOSTIC. Measured twice with census_for_paths over a stratified sample of the tracked corpus: -// the run dies with SIGSEGV around the twentieth subject, and the subject it dies ON analyses +// DIAGNOSTIC. Reproduced twice with census_for_paths over a stratified sample of the tracked +// corpus, which is the instrument to re-run rather than a batch size to copy from here: the run +// dies with SIGSEGV a few tens of subjects in, and the subject it dies ON analyses // cleanly when handed to the same instrument alone -- so the cause is accumulation across subjects // within a process, not any one subject's shape. That is worse than the cost: it means the // whole-population form of corpus_gate is unreachable by construction and not merely slow, and the @@ -123,10 +126,12 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // population -- every module the module index yields, not a sample and not a subtree -- inside one // required-lane budget and inside one process. Both halves are load-bearing and neither implies the // other. A tenfold speedup per subject satisfies a per-subject reading of the trigger and still -// leaves the sweep hours long; and a sweep that fits the budget still dies with SIGSEGV around the -// twentieth subject, which is a failure carrying no cause, no locus and no count, so it cannot be -// enrolled at any speed. An artifact that delivers only one half CONTRIBUTES to this trigger and -// does not retire it. +// leaves the sweep hours long; and a sweep that fits the budget still dies with SIGSEGV partway +// through, a failure carrying no cause, no locus and no count, so it cannot be enrolled at any +// speed. That second half is filed as its own class -- +// gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal -- +// because it outlives this lens: any single-process fold over a discovered population has it. An +// artifact that delivers only one half CONTRIBUTES to this trigger and does not retire it. // // UNTIL THEN the claim this lane makes is the cheap pair in // claim/complexity/accumulator_copy_corpus_path_test.dag, and it is stated at that grain: the lens diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index b48f540778a..817e864d452 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -21,9 +21,9 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // THIS FILE IS THE ONLY EXECUTING EVIDENCE FOR THE CORPUS PATH, AND IT IS THE WHOLE OF IT. // Everything here reads a five-line fixture or nothing at all, so it is seconds. The corpus-grain // forms -- a census over a discovered root, over an explicit batch, and the subtree predicate -- -// are ENTRY POINTS, not witnesses, in v2.lens.complexity_accumulator_copy.corpus_census: at roughly -// eighty seconds per subject they cannot be enrolled, and a witness that cannot run is a roster row -// standing in for a verdict. So the claim this lane makes is scoped to what runs: the planted +// are ENTRY POINTS, not witnesses, in v2.lens.complexity_accumulator_copy.corpus_census: their +// per-subject price is minutes, so they cannot be enrolled, and a witness that cannot run is a +// roster row standing in for a verdict. census_for_paths re-derives that price on the host asking. So the claim this lane makes is scoped to what runs: the planted // copied accumulator below is caught through corpus_gate and the clean fold beside it stays clean, // over a population the gate discovers rather than a hand-list. The corpus-wide verdict is a run, // and it is named where the instruments live. From 451c38f5a0221a334362dd3fdb6e9083a105fddc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 08:32:17 +0000 Subject: [PATCH 10/17] Coverage is decided before every other arm: a truncated sweep refuses instead of reporting its survivors The class filed one commit ago sits BELOW the ladder, not on its bottom rung, and DESIGN section 5 forbids silent wrongness outright rather than admitting it as a low rung. So it does not wait for its trigger. Getting from outside the ladder onto rung 1 is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation in the opposite costume. corpus_gate_disposition_over decides coverage first, because every other arm is a statement ABOUT a population and none of them is true of a population that was not walked. The discovered list and the reported rows are walked in lockstep; the first divergence, or the rows running out, ends the covered prefix and everything after it is reported by name as CorpusCoverageIncomplete. It is an identity join, not a count equality (DESIGN section 5). A batch re-run after a death reports the right NUMBER of rows over the wrong subjects, and a count comparison is green on exactly that. The lockstep walk is also why this is not quadratic: a membership test per discovered path would be the nested fold over one collection this lens exists to catch, and "the corpus is only a few thousand" is the not-time-stable excuse section 6 refuses. Three witnesses, ~3s each, on the floor: a truncated sweep refuses; a right-sized report over the wrong subjects refuses; and a complete report over the same population reaches its ordinary verdict -- the positive control, without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer "nothing missing" turns both reds red and leaves the control green. They do not retire when the depth-bounded walk lands; they become its regression control. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- ...cess_death_rather_than_a_typed_refusal.dag | 4 +- .../corpus_census.dag | 3 + .../corpus_gate.dag | 80 ++++++++++++++++++- .../accumulator_copy_corpus_path_test.dag | 71 ++++++++++++++++ 4 files changed, 155 insertions(+), 3 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag index ef0e8d9f39a..a01b1872a5e 100644 --- a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag +++ b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag @@ -27,7 +27,9 @@ data sweep_terminated_by_process_death_rather_than_a_typed_refusal: RecurringFai "NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. ", - "AN INTERIM MITIGATION EXISTS AND IS NOT THE CLIMB, recorded so it is not mistaken for one: a sweep whose derived population is known can compare the count of subjects it reported against the count it discovered and refuse on a shortfall. That is rung 1 -- the harm is contained, the run stops looking finished -- and it leaves the invalid state entirely writable. ", + "THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ", + + "ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. ", "RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.)", ], diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index b609526ebc8..6486a9c422f 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -7,6 +7,7 @@ import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusNoSuspectsObserved, CorpusGateDisposition, CorpusNotEstablishedObserved, + CorpusCoverageIncomplete, CorpusPopulationBelowFloor, CorpusReport, CorpusSuspectObserved, @@ -175,6 +176,7 @@ fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false CorpusSuspectObserved { paths: _, sites: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false } } @@ -326,5 +328,6 @@ fn a_no_suspect_verdict_still_carries_its_refusal_census() -> Bool { CorpusSuspectObserved { paths: _, sites: _ } => false CorpusNotEstablishedObserved { rows: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false } } diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag index 7eee215604f..6de4892407e 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag @@ -245,6 +245,60 @@ fn corpus_report(pool_roots: List) -> CorpusReport { // onto a smaller one that this module exists to stop, committed at its own last step. The arm now // carries the census, and it is renamed: "clean" asserted more than was established, and the honest // name says exactly what was found -- no suspects, with this residue beside it. + +// A REPORT THAT COVERS LESS THAN THE POPULATION IT NAMES IS NOT A VERDICT ABOUT THAT POPULATION, +// and this is the join that says so. It exists because of a filed class: +// gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal -- +// a single-process fold over a discovered population can be killed by the host partway through +// (SIGSEGV out of the interpreter's unbounded native walk), and what survives is a report in the +// same SHAPE as a complete one, one page shorter. That class sits BELOW the ladder, which DESIGN +// section 5 forbids outright rather than admitting as a low rung, so it does not wait for its +// trigger: this comparison does not fix the crash and is not meant to, it ends the silence, which +// is the forbidden half. The real climb -- a depth-bounded interpreter walk that refuses with a +// locus -- is named in that row, and when it lands these witnesses stay as the regression control +// that the higher rung is real rather than retiring with the mitigation. +// +// IT IS AN IDENTITY JOIN, NOT A COUNT EQUALITY, per DESIGN section 5: rows are produced from the +// discovered list in order, so the two are walked in lockstep and the first position where they +// disagree -- or where the rows run out -- ends the covered prefix and everything from there is +// reported as missing BY NAME. A count comparison would pass a report that analysed one subject +// twice and skipped another, which is exactly the shape a batched sweep produces when a batch is +// re-run after a death. +// +// THE LOCKSTEP WALK IS ALSO WHY THIS IS NOT QUADRATIC. A membership test per discovered path would +// be a nested fold over the same collection -- the shape this very lens exists to catch -- and +// "the corpus is only a few thousand" is the not-time-stable excuse DESIGN section 6 refuses. +// Ordered comparison is linear and needs no set. + +fn coverage_missing(discovered: List, rows: List) -> List { + match discovered { + Empty => Empty + Cons { head: d, tail: dt } => + match rows { + Empty => discovered + Cons { head: r, tail: rt } => + if r.path == d { coverage_missing(discovered: dt, rows: rt) } else { discovered } + } + } +} + +type CorpusCoverage + = CorpusCoverageComplete { covered: Int } + | CorpusCoverageShort { discovered: Int, reported: Int, missing: List } + +fn corpus_coverage(discovered: List, rows: List) -> CorpusCoverage { + let missing = coverage_missing(discovered: discovered, rows: rows) + match missing { + Empty => CorpusCoverageComplete { covered: length(xs: rows) } + Cons { head: _, tail: _ } => + CorpusCoverageShort { + discovered: length(xs: discovered), + reported: length(xs: rows), + missing: missing + } + } +} + type CorpusGateDisposition = CorpusNoSuspectsObserved { files: Int, @@ -254,6 +308,7 @@ type CorpusGateDisposition | CorpusSuspectObserved { paths: List, sites: Int } | CorpusNotEstablishedObserved { rows: List } | CorpusPopulationBelowFloor { files: Int, floor: Int } + | CorpusCoverageIncomplete { discovered: Int, reported: Int, missing: List } fn corpus_gate_disposition(report: CorpusReport, population_floor: Int) -> CorpusGateDisposition { if report.files < population_floor { @@ -277,9 +332,30 @@ fn corpus_gate_disposition(report: CorpusReport, population_floor: Int) -> Corpu } } +// COVERAGE IS DECIDED BEFORE ANYTHING ELSE, because every other arm is a statement ABOUT a +// population and none of them is true of a population that was not walked. A short report reaching +// the no-suspect arm is the exact silent-wrongness this guard exists to end. +fn corpus_gate_disposition_over( + discovered: List, + rows: List, + population_floor: Int +) -> CorpusGateDisposition { + match corpus_coverage(discovered: discovered, rows: rows) { + CorpusCoverageShort { discovered: d, reported: r, missing: m } => + CorpusCoverageIncomplete { discovered: d, reported: r, missing: m } + CorpusCoverageComplete { covered: _ } => + corpus_gate_disposition( + report: corpus_report_from_rows(rows: rows), + population_floor: population_floor + ) + } +} + fn corpus_gate(pool_roots: List, population_floor: Int) -> CorpusGateDisposition { - corpus_gate_disposition( - report: corpus_report(pool_roots: pool_roots), + let discovered = corpus_paths(pool_roots: pool_roots) + corpus_gate_disposition_over( + discovered: discovered, + rows: corpus_rows_for_paths(paths: discovered), population_floor: population_floor ) } diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index 817e864d452..df0ff135843 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -5,14 +5,18 @@ import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusFileNotEstablished, CorpusFileRow, CorpusFileScanned, + CorpusGateDisposition, CorpusNoSuspectsObserved, CorpusNotEstablishedObserved, + CorpusCoverageIncomplete, CorpusPopulationBelowFloor, CorpusSuspectObserved, corpus_gate, + corpus_gate_disposition_over, corpus_rows_for_paths } import v2.std.collection { List } +import v2.std.text { String } import v2.std.logic { Bool } import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } @@ -96,5 +100,72 @@ test fn an_empty_population_is_below_floor_rather_than_clean() -> Bool { CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false CorpusSuspectObserved { paths: _, sites: _ } => false CorpusNotEstablishedObserved { rows: _ } => false + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + } +} + +data planted_path: String = "test/fixture_roots/complexity_accumulator_copy/planted_copy.dag" + +data clean_path: String = "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag" + +fn disposition_over(discovered: List, rows: List) -> CorpusGateDisposition { + corpus_gate_disposition_over(discovered: discovered, rows: rows, population_floor: 1) +} + +fn is_coverage_incomplete(d: CorpusGateDisposition) -> Bool { + match d { + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => true + CorpusPopulationBelowFloor { files: _, floor: _ } => false + CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false + } +} + +// THE MITIGATION FOR A CLASS THAT SITS BELOW THE LADDER, AND ITS DISCRIMINATING RED. +// gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal: +// a single-process fold over a discovered population can be killed partway through, and the +// survivors present in the same shape as a complete run. Silent wrongness is outside the ladder and +// forbidden outright, so the guard is owed now even though the real climb -- a depth-bounded +// interpreter walk -- is not in this change. This witness is the truncated sweep: two subjects +// discovered, one row reported, which is precisely what a death after the first subject leaves +// behind. +test fn a_truncated_sweep_refuses_rather_than_reporting_its_survivors() -> Bool { + is_coverage_incomplete( + d: disposition_over( + discovered: [planted_path, clean_path], + rows: corpus_rows_for_paths(paths: [planted_path]) + ) + ) +} + +// THE JOIN IS ON IDENTITY, AND THIS IS THE WITNESS THAT SAYS SO RATHER THAN THE COMMENT. Same +// discovered population, same NUMBER of rows, one subject analysed twice and the other never -- +// the shape a batched sweep produces when a batch is re-run after a death. A count comparison is +// green here, which is why DESIGN section 5 says completeness is an identity join. +test fn a_report_of_the_right_size_over_the_wrong_subjects_still_refuses() -> Bool { + is_coverage_incomplete( + d: disposition_over( + discovered: [planted_path, clean_path], + rows: corpus_rows_for_paths(paths: [planted_path, planted_path]) + ) + ) +} + +// THE POSITIVE CONTROL ON BOTH OF THE ABOVE. Without it the guard could refuse everything and the +// two reds would still pass -- the absorbing arm wearing a typed name. A complete report over the +// same population reaches the ordinary verdict, and the verdict it reaches is the suspect one, +// because the planted subject is a suspect: so this control also proves coverage is decided BEFORE +// the other arms rather than instead of them. +test fn a_complete_report_over_the_same_population_reaches_its_ordinary_verdict() -> Bool { + match disposition_over( + discovered: [planted_path, clean_path], + rows: corpus_rows_for_paths(paths: [planted_path, clean_path]) + ) { + CorpusSuspectObserved { paths: _, sites: _ } => true + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusPopulationBelowFloor { files: _, floor: _ } => false + CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false } } From 26eee1fcbe3825ac240b88ab6e9c6a2b0ee80f4e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 09:09:11 +0000 Subject: [PATCH 11/17] chore: regenerate drifted generated artifacts (ci auto-heal) Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md sweep_terminated_by_process_death_rather_than_a_typed_refusal Ledger-Repair-Judged: docs/design-rung-drops.md --- docs/design-failure-modes.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index 08bf358143f..701917cd8f9 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -136,6 +136,7 @@ The exact `RecurringFailureMode.identity` population, which no other projection - `a_deconfliction_plan_does_not_enumerate_its_writers` - `a_branch_property_falsified_by_a_derived_push` - `roster_is_its_own_denominator` +- `sweep_terminated_by_process_death_rather_than_a_typed_refusal` - `instrument_answers_outside_its_domain` - `discriminating_evidence_authored_into_the_operational_population` - `printed_remedy_is_mangled_by_the_medium_that_prints_it` @@ -305,6 +306,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a deconfliction plan does not enumerate its writers** (two or more lanes reason about how to avoid colliding on one subject, agree on a scheme, and collide anyway -- because the scheme is a PREDICTION about who will write the subject next, and the predicted population omits a writer. The omitted writer is not an adversary: it is a peer who agreed to stand down and then WROTE ANYWAY -- an earlier revision of this sentence said "and then did not act", which is the opposite mechanism and could not produce the collision the row models, because a writer that does not write collides with nobody. The result merges CLEAN, so the collision is silent. INVALID STATE: a lane holding a deconfliction property that quantifies over a writer population it never discovered. WHY IT SURVIVES REVIEW: the plan is examined for whether it WOULD prevent a collision among the writers it names, and it always would. Nothing in a diff exhibits the writer it omits, and the omitted writer's own change is independently correct. SPECIMEN (main's parse break, 2026-09-04). Four lanes independently authored a repair for one orphaned annotation. Deconfliction ran and SUCCEEDED AS A DISCUSSION: #10414 was named the winner on an earliest-with-proof rule, and the other authors assented. Three of the four then merged anyway (#10425, #10408, #10428), each deleting the same nineteen lines and each adding its own relocated copy, leaving one paragraph standing three times in the module. One copy carried an inverted double negative asserting the opposite of the sentence it was restating. Repaired by removing two copies in #10435. THE ASSENT IS LOAD-BEARING, NOT INCIDENTAL, AND IT IS WHAT BOUNDS THIS CLASS: the population was not unknown and the peers were not uncooperative. Every author was identified, every author agreed, and the scheme still failed -- so the missing thing is not discovery of the writers but an INTERLOCK on them. A class where the colliding writer was never identified is a different and easier problem, because enumerating it is the whole remedy. WHY GIT CANNOT SEE IT, which is the load-bearing mechanism: identical DELETIONS on every side reconcile silently, and each side's ADDITION lands at a different offset, so no hunk overlaps. Three independently correct repairs of one defect merge without a single conflict. A collision that produces a conflict is the benign case; this class is defined by the absence of one. THE SECOND-ORDER TRAP: the ranking conversation consumes the attention that the standing-down would need. Agreement about WHICH repair wins reads as resolution, and it is not -- closing a pull request is the act, and here one of four was closed. Naming a winner is not the losers standing down. NOT TO BE MERGED WITH a_branch_property_falsified_by_a_derived_push, which shares only the SYMPTOM that no conflict is produced so nothing notifies. That symptom has at least three mechanisms -- this one, the derived-push one, and the generated-artifact merge driver refusing with no markers -- so it does not discriminate, and uniting on it is the bare alias section 3 forbids rather than the proven coincidence section 6 requires. The concrete cost of merging them is a grain mismatch section 4b(3) names as its own review tell: this row is genuinely blocked on cross-head knowledge, the other is decidable from a single ref TODAY, and one row carries one stated ceiling because RecurringFailureMode has no ceiling or trigger FIELD -- so the affordable class would be parked behind this one's capability and never rank for a climb it does not need. RUNG: found BELOW the ladder. The duplicated meaning is a silently wrong answer -- every copy parses, every gate is green, and the projection of a triple-landed repair is indistinguishable from a correct one by any executing check. CEILING 2, mechanically preventable, and the reason it is not higher is that the writer population is not decidable from the repository: it includes future human decisions about whether to close a head. What IS decidable is the SUBJECT overlap between open heads. NEXT-RUNG TRIGGER: the capability of resolving an open pull request head to the set of DECLARATIONS it modifies, which is the same Node-tree read the namespace authority already performs and which no instrument currently exposes. A path-grain fence does not discharge it, because this specimen was a single path and the collision was within one declaration's neighbourhood. REVIEW TELL: a lane stating why it CANNOT collide with another lane. The claim is a quantifier over writers, so ask which writers it ranges over and whether an unclosed peer head is inside it. And after a CONTESTED fix lands, re-measure the SUBJECT for duplication rather than re-measuring whether it is green -- green is exactly what triple-duplication looks like. - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. - **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) +- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. - **a printed remedy is mangled by the medium that prints it** (an instruction is authored correctly, and the channel that carries it to its reader REWRITES it in transit. The author reads the authored string and sees the remedy; the reader receives something shorter that no longer does what it says; and no error crosses the boundary, because the rewriting is the medium behaving exactly as specified. THE RECOGNITION RULE, and it is the whole row: THE SENDER SEES INTACT TEXT, THE RECIPIENT GETS A SILENTLY SHORTENED SENTENCE, AND NOTHING REPORTS A FAILURE. The send succeeds. So every instrument that reads the AUTHORED artifact -- grep, review, a byte-exact drift gate over the source -- agrees the remedy is present, because it is present. The defect is in the rendering, which means the subject has to be what the reader RECEIVES and never what the author wrote. FIRST SPECIMEN, calm-owl-417, gunbc#10460, and it is the one that names the class: gunbc.generated_artifact_merge_driver prints its repair recipe through a double-quoted `echo`, because the lines are templates and `$merged_path` must interpolate. Double quotes leave the backquote active, and the recipe's sed program wrote its capture group between backquotes. Bash ran `\([a-z0-9_]*\)` as a COMMAND SUBSTITUTION at echo time: two `command not found` lines on stderr, and the printed instruction came out with the capture group DELETED. Pasted, it extracted zero rows from both sides of a set difference, and a difference over two empty sets is empty -- which the recipe's own words read as `MUST BE EMPTY`. The mangled remedy reported the vacuous pass the intact remedy exists to prevent, at the exact moment rows were at risk. SECOND SPECIMEN, same PR, and it is the same boundary read from the other side rather than a repeat: `$` stays active ON PURPOSE, so a `$1` written into a recipe line is the DRIVER's argv and not the reader's. An extractor drafted as a shell function taking `$1` rendered as `git show "O:docs/design-rung-drops.md"` -- git's own `%O` placeholder substituted into the instruction it was printing. Escaping does not save that one, because the escaper doubles backslashes first, so the remedy is to name only the variable the printer DEFINES. The two specimens together are why the class is the MEDIUM and not the escaping: one is a character the medium consumed, the other is a character the medium substituted, and no single escaping rule addresses both. THE DAMAGE IS NOT PROPORTIONAL TO THE CORRUPTION, and this is the sharpest available statement of the class. The repaired extractor carries TWO sed clauses, because the two delegated projections spell a row identity differently -- a backquoted slug bullet and a `### Title` heading. Mangling one character in the FIRST clause makes sed refuse the ENTIRE program on an invalid backreference, so the second clause, which nothing touched, extracts nothing either. Measured against a fixture carrying both spellings and 157 rostered rows: the intact program names 157, the mangled program names ZERO -- not 36, which is what a reader assuming per-clause degradation would predict. One corrupted character silenced a projection it never appeared in. THIRD AND FOURTH SPECIMENS, 2026-09-05, AND THEY ARE WHAT MAKE THIS A CLASS RATHER THAN A FACT ABOUT MERGE DRIVERS: the same mechanism fired twice more within ninety minutes, in a completely different medium, in messages ABOUT this class. An agent messaging body was passed to `dashboard-message --body` as a double-quoted shell argument; the body contained backquoted identifiers; bash substituted them away and the recipients received sentences with phrases missing. One of the two was a message whose SUBJECT was instruments that report success without carrying their claim. The other was sent by the author repairing the first specimen, while writing about the first specimen, roughly an hour after reading the analysis describing it. WHAT THOSE TWO ADD: the class is not a property of emitted artifacts or of merge drivers. It is a property of ANY text handed to a shell-quoted channel, and the agent messaging channel is one of them and is uninstrumented at both ends. THE OBVIOUS DEFENCE IS THE THING THAT FAILED, and a reader will supply the wrong remedy here unless it is stated. `Remember to quote correctly` was in active working memory in both messaging specimens and did not prevent either. The diagnosis in both cases was identical: the protection was attached to a command SPELLING rather than to the hazard -- one command was being single-quoted correctly all evening, and a DIFFERENT command was reached for with double quotes without re-deriving the rule. Vigilance that is bound to a habit does not transfer to the next call site. THE MECHANICAL FORM, which is the operative remedy and is cheap: write the body to a file with a QUOTED heredoc, then pass `"$(cat file)"`. Single-quoting the argument is insufficient, not merely awkward, because prose reliably contains apostrophes and one apostrophe ends the quoting. THE MANGLING IS NOT MERELY LOSSY -- IT EXECUTES, IN THE SENDER'S ENVIRONMENT, AND THAT IS THE MOST UNDER-STATED PROPERTY OF THIS CLASS. Discovered as physical residue rather than by reasoning: the third specimen's message contained the phrase `-> Bool` inside a double-quoted argument, and the shell read it as a COMMAND `-` that was not found, followed by a REDIRECTION that created an empty file named `Bool` in the repository working tree. It sat there untracked for fifteen minutes and was found by `git status`, not by anything watching the channel. The general statement is worse than the instance: a backquoted span inside double quotes is COMMAND SUBSTITUTION, which is arbitrary execution, and an unquoted redirection operator writes files. So the sender's mistake is not that a sentence arrived shortened; it is that a fragment of English prose was EVALUATED AS A PROGRAM against a live working tree. The lossy reading of this class is the benign one. AND IT WAS ONE ROUTINE COMMAND FROM BEING COMMITTED: the author of that instance stages with `git add -A`, so the next commit would have swept an artifact of a mangled chat message into a pull request, with a plausible-looking name and no origin anyone could reconstruct. THE REASON IT DID NOT LAND IS COMMIT ORDERING AND NOTHING ELSE: the last commit happened to precede the corruption by fifteen minutes. Recording that as `it did not land` would be luck reported as safety, which is the reporting failure this ledger exists to refuse -- no mechanism was involved, nothing detected it, and nothing would have blocked it. The file's name resembles a type this corpus uses everywhere, so review would most likely have read it as an intentional stray rather than as residue, and its provenance is unreconstructable from the diff. THE CHECK THIS LICENSES, and it caught something on its FIRST application in a second tree: inspect `git status` for unexplained worktree changes after any session that has sent shell-quoted messages, and do not stage with `git add -A` in a tree where that has happened. Applied immediately by a second author, it found no execution residue but did surface a forgotten STAGED modification to a generated artifact, checked out during verification and left behind -- a different cause with the same shape, on a branch that was main. GENERALIZATION: the hazard is not restricted to mangled messages. ANY command that writes into the worktree as a side effect of verification leaves residue a reviewer cannot distinguish from intent. The heredoc form prevents the cause; the status check catches damage already done, whatever wrote it. THE TRANSPORT'S SUCCESS SIGNAL WAS NEVER ASKED THE RELEVANT QUESTION, and this is the precise form of `no error crosses the boundary`. Successful delivery answers whether the SUBMITTED payload reached its destination. It does not answer whether that payload preserved the INTENDED message. Those are two different claims, and every send in every specimen above was truthful about the first while nothing anywhere was asked the second. A PRESERVATION CHECK MUST BE ANCHORED BEFORE THE DANGEROUS TRANSFORMATION, AND THE OBVIOUS IMPLEMENTATION IS WORTHLESS. The natural control is to digest what was sent and compare it with what arrived. If that digest is computed AFTER the shell has consumed the backquoted spans, it faithfully attests the CORRUPTED payload and reports a perfect match: both sides agree, the comparison is sound, and the defect is invisible, because the damage happened upstream of the first observation. The comparison must STRADDLE the transformation that does the damage -- intended payload captured BEFORE it, against payload received after it -- which is this row's own recognition rule applied to its own remedy. It is the same shape as the neighbouring instruments in this ledger, one layer further out: `comm` cannot report that its own operand was empty, a digest over emitted driver bytes proves an escape was APPLIED but not that it was CORRECT, and a preservation digest taken downstream of the corruption proves only that the corruption was transmitted reliably. QUALIFICATION, so a correct transport does not read as corruption: where a channel LEGITIMATELY transforms encoding or line endings, the comparison needs an explicit preservation contract stating what is allowed to change. THE RECEIVING END FAILS TOO, AND THAT COMPLETES THE PICTURE. Recorded as evidence about DETECTABILITY rather than about the mechanism: in the third specimen a reader received the corrupted message, read it, and did not identify it as corrupted. The missing expressions and an injected directory listing were both present in what they received, and the surrounding prose still supported the conclusions the conversation reached -- so the damage was survivable enough to be invisible, and they flagged only afterwards that those spans should have been treated as corrupt. So there are THREE parties and no signal at any of them: the sender sees intact text, the transport reports success against a question about a different payload, and the recipient reads fluent prose with holes in it. THE CLASS FIRED IN PRODUCTION ON THE BRANCH THAT CLOSES IT, AT EQUAL ROW COUNTS, and this receipt is a discriminating RED on the real acceptance path rather than a fixture. Merging main into that branch reached this driver on docs/design-failure-modes.md. It refused as designed -- zero conflict markers, three index stages, the ours side left clean -- and the repaired step 2, run verbatim, measured: base 126 rows, ours 126 rows, COUNTS EQUAL, with the ours side dropping exactly one row that main had added, `duplicate_record_literal_field_silently_last_wins`, and adding one of its own. A cardinality check passes on that comparison and reports nothing. Only the identity join names the casualty. So the case a reviewer had challenged the branch to cover -- a substitution that preserves the count -- arrived unprompted, on the same projection, within minutes of the controls for it being enrolled; the branch did not have to argue that the case was reachable, because it walked in. RUNG FOUND AT: silent wrongness wherever the subject read by any instrument is the authored string, because there the defect is invisible by construction; mitigatable where a witness reads the EMITTED artifact, which is where gunbc#10460 put it. CEILING: mechanically preventable for media this repository emits into -- a witness that executes the emitted artifact and reads what the reader receives cannot be satisfied by an authored string, and the shell-quoting encodings have single authorities that a hand-spelled call site can be made not to bypass. It stays MITIGATABLE for channels this repository does not own, such as an external tool's argv handling, which is observed rather than proven. NEXT-RUNG TRIGGER: the bash medium read forward from grammar rows, so a printed line's variable references and shell-active characters are DECLARED rather than spelled -- at which point no emitter concatenates a shell literal and the escaping arm has nothing to get wrong. That is the same trigger gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_emit_dissolution_trigger already carries, and it is named here rather than restated because one capability retires both. DISTINGUISHING FACT AGAINST ITS NEAREST NEIGHBOURS. It is NOT `empty_capture_read_as_clean_result`: there a well-formed refusal travels on a stream the reader did not keep, and the reader's error is consuming the emptiness; here the text is rewritten before anyone reads it and the reader's conduct is irrelevant. The empty operand is one CONSEQUENCE of this class, not the class, and the second specimen proves it -- a substituted positional argument is not an empty capture at all. It is NOT `restoration_promise_names_a_route_that_does_not_exist`: there the named route is absent; here it exists and the rendering breaks it.) From 743ab9f0d7b49b9072b9025e1c86b0f3f2645037 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 10:09:13 +0000 Subject: [PATCH 12/17] The floor's 500ms CPU line prices out every ingest-touching assertion in this lens, at any subject size Run 34022136976 interrupted seven identities. The log discriminates the cause and it is not a shared budget or a process death: each row reads raised_by=cpu_deadline cpu_at_least=~506ms/500ms, and the totals are interrupted_cpu_deadline=6 interrupted_wall_deadline=1. Per witness, on the CPU clock. The local figures agree rather than disagreeing -- a preempted row is cut just past the line and reported UNMEASURED, so 506ms is the deadline, not the cost, and the cost is the ~3s claim_batch measures. The consequence is larger than these witnesses. Interpreted ingest of a FIVE-LINE fixture measures ~3s, and a four-line String snippet with no filesystem read measures about the same: six times the line at the smallest subject expressible. So no assertion in this lens that reaches the ingest can be enrolled at any subject size -- which is why every witness this lens has ever had is frozen or deferred, a fact visible in the roster that had never been explained. So the evidence is re-aimed rather than trimmed, and the scope is stated rather than rounded up. The witnesses that remain touch no ingest and measure 0-1ms: the coverage join, whose subject IS the join and whose inputs are rows, and the typed read arm, whose subject IS the read -- the mechanism the SourceUnreadable red can fail open into. Both are different claims from the ingest claim, not cheaper proxies for it. The ingest-priced predicates -- planted caught, clean stays clean, readable subject reaches the lens -- become plain fns beside the corpus census: they pass by execution under claim_batch and are enrolled in nothing, and they dissolve on the same trigger. The one-line repoint of the stale glob_discovery_law row is reverted. Any edit to that entry admits its 22s witness to the floor, so the repair is not landable until the trigger clears; the specimen stays filed in roster_is_its_own_denominator. Also recorded, in the class filed this morning: a floor budget preemption is NOT that class. It is typed, located, counted and blocking -- the arm DESIGN section 5 asks for -- and the distinguishing question is not whether a run ended early but whether ending early produced a cause or produced silence. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- ...cess_death_rather_than_a_typed_refusal.dag | 2 + .../corpus_census.dag | 52 +++++++++++++++-- .../accumulator_copy_corpus_path_test.dag | 56 ++++++++++++++----- .../accumulator_copy_roster_gate_test.dag | 2 +- ...cumulator_copy_unreadable_subject_test.dag | 42 ++++++++------ 5 files changed, 117 insertions(+), 37 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag index a01b1872a5e..b10472e9239 100644 --- a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag +++ b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag @@ -31,6 +31,8 @@ data sweep_terminated_by_process_death_rather_than_a_typed_refusal: RecurringFai "ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. ", + "A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the 500ms CPU deadline, and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, cpu_at_least=NNNms/500ms, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. ", + "RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.)", ], diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index 6486a9c422f..b037dc41fae 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -58,6 +58,13 @@ import v2.std.node_query { find_arrow_body_child, find_named_child } import v2.std.witness { Holds, Violates } import v2.std.collection { List } import v2.std.integer { Int } +import v2.lens.complexity_accumulator_copy.roster_gate { + GateClean, + GateNotEstablished, + GateRefusalBudgetExceeded, + GateSuspectObserved, + file_gate_disposition +} import v2.std.logic { Bool } import v2.std.text { String } import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } @@ -80,11 +87,27 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // regardless of any exclusion (cause=ChangedWitnessOutsidePreparedSubject, run 34018018622), which // is exactly right: an exclusion that a new witness could opt into on the change that introduces it // would be the escape hatch DESIGN section 5 forbids. A corpus-priced assertion therefore cannot be -// authored as a witness at all, and pretending otherwise buys a roster row that never runs. The -// executing evidence for this code path is the cheap pair in -// claim/complexity/accumulator_copy_corpus_path_test.dag -- a planted copied accumulator caught -// through corpus_gate and a clean fold that stays clean -- which run on every push. What lives here -// are entry points, run by a person, whose output belongs in a run. +// authored as a witness at all, and pretending otherwise buys a roster row that never runs. What +// lives here are entry points, run by a person, whose output belongs in a run. +// +// AND THE PRICE THAT DECIDES THIS IS NOT THE CORPUS -- IT IS ONE SUBJECT, WHICH IS A HARDER FACT +// THAN THE ONE THIS FILE FIRST RECORDED. The required floor's per-witness CPU line is 500ms +// (v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms, an operator ruling whose +// two raises were repudiated). Interpreted ingest of a FIVE-LINE fixture measures about 3s, and a +// four-line String snippet with no filesystem read measures about the same: six times the line, at +// the smallest subject expressible. So no assertion in this lens that reaches the ingest can be +// enrolled at any subject size, which is why every witness this lens ever had is frozen or +// deferred -- a fact that was visible in the roster and had never been explained. Measured with +// claim_batch on this branch; run it rather than trusting this sentence. +// +// WHAT DOES EXECUTE ON EVERY PUSH IS THEREFORE NARROWER THAN THIS FILE ONCE CLAIMED, and the +// correction matters more than the claim did. It is the evidence that does not touch ingest: the +// coverage join in claim/complexity/accumulator_copy_corpus_path_test.dag, whose subject is the +// join and whose inputs are rows, and the typed-read pair in +// claim/complexity/accumulator_copy_unreadable_subject_test.dag, whose subject is the read arm. +// The ingest-priced predicates -- a planted copied accumulator caught through corpus_gate, a clean +// fold that stays clean, a readable subject reaching the lens -- pass by execution under +// claim_batch and are enrolled in nothing. That is the honest scope of this lane's claim. // // DISSOLUTION names the capability: ingest that does not re-parse source in the interpreter (native // ingest, or a lens reading the compile graph). At that price the corpus form becomes affordable @@ -146,6 +169,25 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // over a discovered root and over an explicit subject list respectively. Their output belongs in a // run, never transcribed into this file. +// THE INGEST-PRICED PREDICATES, HOMED HERE WITH THE REST OF WHAT THE 500ms CPU LINE PRICES OUT. +// Each costs seconds of interpreted ingest per subject and so cannot be a witness; each is a real +// question with a real answer, run by a person or by the dissolution named above. a_readable_subject +// _still_reaches_the_lens is the deep positive control for the typed-read red in +// claim/complexity/accumulator_copy_unreadable_subject: that witness controls the READ arm, which +// is cheap and is the mechanism the red can fail open into; this one controls the whole path +// through to a clean verdict, and it is the half the budget does not admit. +fn a_readable_subject_still_reaches_the_lens() -> Bool { + match file_gate_disposition( + path: "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag", + refusal_ceiling: 0 + ) { + GateClean => true + GateNotEstablished { cause: _ } => false + GateSuspectObserved { suspects: _ } => false + GateRefusalBudgetExceeded { refusals: _, ceiling: _ } => false + } +} + fn population_for_root(root: String) -> Int { length(xs: corpus_paths(pool_roots: [root])) } diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index df0ff135843..39535065a8d 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -22,15 +22,18 @@ import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -// THIS FILE IS THE ONLY EXECUTING EVIDENCE FOR THE CORPUS PATH, AND IT IS THE WHOLE OF IT. -// Everything here reads a five-line fixture or nothing at all, so it is seconds. The corpus-grain -// forms -- a census over a discovered root, over an explicit batch, and the subtree predicate -- -// are ENTRY POINTS, not witnesses, in v2.lens.complexity_accumulator_copy.corpus_census: their -// per-subject price is minutes, so they cannot be enrolled, and a witness that cannot run is a -// roster row standing in for a verdict. census_for_paths re-derives that price on the host asking. So the claim this lane makes is scoped to what runs: the planted -// copied accumulator below is caught through corpus_gate and the clean fold beside it stays clean, -// over a population the gate discovers rather than a hand-list. The corpus-wide verdict is a run, -// and it is named where the instruments live. +// WHAT IS A WITNESS HERE AND WHAT IS NOT IS DECIDED BY THE 500ms CPU LINE, MEASURED PER FUNCTION. +// The required floor's per-witness CPU deadline is 500ms; interpreted ingest of the five-line +// fixture below measures about 3s, so every predicate that reaches the ingest reports +// interrupted_before_verdict and BLOCKS rather than running (measured on this branch: seven of +// them at once, run 34022136976). Those predicates are therefore plain fns -- they pass by +// execution under claim_batch and are enrolled in nothing, which is the same standing as the +// corpus-grain forms in v2.lens.complexity_accumulator_copy.corpus_census and dissolves on the +// same trigger. +// +// The witnesses that remain touch no ingest and measure 0-1ms: the coverage join, whose subject is +// the join and whose inputs are rows, and the population floor. They are not a cheaper proxy for +// the ingest claim; they are a different claim, stated at the grain that can execute. fn fixture_rows() -> List { corpus_rows_for_paths(paths: [ @@ -71,14 +74,14 @@ fn row_scanned_without_suspect(row: CorpusFileRow) -> Bool { // They live under test/fixture_roots/, outside every source root, because the planted subject is // deliberately a quadratic and must never be compiled as production source or reached by the corpus // population. -test fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { +fn a_planted_copied_accumulator_is_caught_through_the_corpus_path() -> Bool { match fixture_rows() { Cons { head: planted, tail: _ } => row_has_suspect(row: planted) Empty => false } } -test fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { +fn a_clean_accumulating_fold_stays_clean_through_the_corpus_path() -> Bool { match fixture_rows() { Cons { head: _, tail: Cons { head: clean, tail: _ } } => row_scanned_without_suspect(row: clean) _ => false @@ -108,6 +111,25 @@ data planted_path: String = "test/fixture_roots/complexity_accumulator_copy/plan data clean_path: String = "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag" +// THE COVERAGE WITNESSES BUILD THEIR ROWS RATHER THAN INGESTING THEM, AND THAT IS THE RIGHT +// SUBJECT RATHER THAN A CHEAPER ONE. What is under test here is the JOIN -- given a discovered +// population and a set of reported rows, does a report that covers less than its population refuse +// -- and a CorpusFileRow is that join's input, not its producer. Ingesting real subjects to reach +// it would test the ingest twice and the join once, and it would cost about 3s per witness against +// the floor's 500ms CPU line, which is how the ingest-priced witnesses in this lens ended up +// enrolled in nothing. The producer-side claim is a different claim and is priced separately; +// see corpus_census. +// +// The rows are deliberately NOT all identical: the planted row carries a suspect and the clean row +// does not, so the positive control below reaches the SUSPECT verdict rather than merely "not +// incomplete", and a guard that passed coverage but dropped the standing would fail it. +fn scanned_row(path: String, suspects: Int) -> CorpusFileRow { + CorpusFileRow { + path: path, + standing: CorpusFileScanned { suspects: suspects, refusals: 0, causes: Empty } + } +} + fn disposition_over(discovered: List, rows: List) -> CorpusGateDisposition { corpus_gate_disposition_over(discovered: discovered, rows: rows, population_floor: 1) } @@ -134,7 +156,7 @@ test fn a_truncated_sweep_refuses_rather_than_reporting_its_survivors() -> Bool is_coverage_incomplete( d: disposition_over( discovered: [planted_path, clean_path], - rows: corpus_rows_for_paths(paths: [planted_path]) + rows: [scanned_row(path: planted_path, suspects: 1)] ) ) } @@ -147,7 +169,10 @@ test fn a_report_of_the_right_size_over_the_wrong_subjects_still_refuses() -> Bo is_coverage_incomplete( d: disposition_over( discovered: [planted_path, clean_path], - rows: corpus_rows_for_paths(paths: [planted_path, planted_path]) + rows: [ + scanned_row(path: planted_path, suspects: 1), + scanned_row(path: planted_path, suspects: 1) + ] ) ) } @@ -160,7 +185,10 @@ test fn a_report_of_the_right_size_over_the_wrong_subjects_still_refuses() -> Bo test fn a_complete_report_over_the_same_population_reaches_its_ordinary_verdict() -> Bool { match disposition_over( discovered: [planted_path, clean_path], - rows: corpus_rows_for_paths(paths: [planted_path, clean_path]) + rows: [ + scanned_row(path: planted_path, suspects: 1), + scanned_row(path: clean_path, suspects: 0) + ] ) { CorpusSuspectObserved { paths: _, sites: _ } => true CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false diff --git a/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag b/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag index 4950c284fff..b8af976a3d9 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_roster_gate_test.dag @@ -28,7 +28,7 @@ test fn roster_compile_stage_zero_suspects_within_ratchet() -> Bool { } test fn roster_glob_discovery_zero_suspects_within_ratchet() -> Bool { - file_gate(path: "src/v2/test/workflow/glob_discovery_law.dag", refusal_ceiling: 0) + file_gate(path: "src/v2/workflow/glob_discovery_law.dag", refusal_ceiling: 0) } test fn roster_lens_traversal_zero_suspects_within_ratchet() -> Bool { diff --git a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag index 93599be4a24..4d6260912e1 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag @@ -1,5 +1,10 @@ module v2.test.claim.complexity.accumulator_copy_unreadable_subject +import extdeps.filesystem.filesystem_io { + FilesystemReadRefused, + FilesystemReadSucceeded, + filesystem_read_outcome +} import v2.lens.common.source_analysis_standing { SourceUnreadable } import v2.lens.complexity_accumulator_copy.roster_gate { GateClean, @@ -43,22 +48,25 @@ test fn an_unreadable_subject_is_refused_with_its_cause() -> Bool { // wrong reason. A green here says the typed read still reaches real bytes and the lens still // answers over them. // -// THE SUBJECT IS A FIVE-LINE FIXTURE, AND THAT IS A COST DECISION MADE FROM MEASUREMENT. It pointed -// at a real 186-line production module first; that cost about fifty seconds and the required floor -// reported it interrupted_before_verdict, so the control was being lost to its own price. Ingest -// cost scales with subject size and does so superlinearly across this lane -- five lines is -// seconds, 186 lines is tens of seconds, 874 lines is over twenty minutes -- so a control whose -// entire claim is "a readable subject still reaches the lens" has no business buying a large one. -// The sanctioned clean fixture establishes the same thing and stays affordable to the lane that -// runs it on every push. -test fn a_readable_subject_still_reaches_the_lens() -> Bool { - match file_gate_disposition( - path: "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag", - refusal_ceiling: 0 - ) { - GateClean => true - GateNotEstablished { cause: _ } => false - GateSuspectObserved { suspects: _ } => false - GateRefusalBudgetExceeded { refusals: _, ceiling: _ } => false +// THE POSITIVE CONTROL, AND ITS SUBJECT IS THE TYPED READ RATHER THAN THE WHOLE LENS -- A COST +// DECISION MADE FROM MEASUREMENT, NOT A CONVENIENCE. What the red above can fail open into is a +// read arm that refuses EVERY subject: then "not established with cause SourceUnreadable" is the +// answer for a present file too, and the red passes while establishing nothing. So the control has +// to show the refusing arm is not universal, and the read is exactly the mechanism that decides +// that -- one channel, two arms, both reachable. +// +// Reaching it through file_gate_disposition on a real subject would establish the same thing and +// cost about 3s of interpreted ingest, six times the floor's 500ms CPU line, so it would be +// enrolled in nothing and would take this red down with it (measured: the required floor reported +// exactly that, interrupted_before_verdict, for every ingest-priced witness in this lane). The +// deeper form -- a readable subject reaching the lens and coming back clean -- is kept as the entry +// point a_readable_subject_still_reaches_the_lens in +// v2.lens.complexity_accumulator_copy.corpus_census, priced with the rest of the ingest-bound +// evidence and carrying the same trigger. +test fn a_present_subject_is_read_rather_than_refused() -> Bool { + let read = Filesystem.Read(path: "test/fixture_roots/complexity_accumulator_copy/clean_linear.dag") + match filesystem_read_outcome(content: read.content, success: read.success, error: read.error) { + FilesystemReadSucceeded { content: _ } => true + FilesystemReadRefused { error: _ } => false } } From fc576da5a0f29c6fa6b4813c14d413586b0e270a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 10:18:55 +0000 Subject: [PATCH 13/17] Name the discovery-to-rows seam, and put the smallest-subject measurement in the trigger Two sentences were being carried by one. "The ingest is unenrolled" was recorded; "the discovery-to-join seam is unverified" was not, and only the second locates where a real defect would sit. The executing reds prove the join is correct GIVEN well-formed rows. The unenrolled predicates prove the lens is correct GIVEN real files. Nothing executing joins those halves: that corpus_rows_for_paths, folded over what corpus_paths discovers, yields rows in the same order, at the same identities, at the same arity the lockstep walk assumes. That assumption is load-bearing and no type states it -- a producer that reordered or dropped a path reads as a truncated sweep, the right refusal for the wrong reason, and one that silently repaired an order mismatch would make the guard permanently green. Recorded beside the join, with the row to write first when the trigger clears: not another join case, but one witness that discovers a small real population and checks the produced rows against it at identity grain. Second, the measurement that sets the bar moves into the trigger's sufficiency clause, because it is the finding that outlives this PR. Interpreted ingest of a five-line fixture, and of a four-line String with no filesystem read, both measure about six times the 500ms line -- at the smallest subject expressible. That is not a budget that could be raised to fit the work; it is a floor no ingest-reaching assertion can ever clear, and it retroactively explains why every witness this lens has ever had is frozen or deferred. A trigger that made large subjects affordable and left the smallest at six times the line would restore nothing. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- .../corpus_census.dag | 16 ++++++++++++--- .../corpus_gate.dag | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index b037dc41fae..93d54db4ff5 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -144,9 +144,19 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // TRIGGER: an ingest realization for the accumulator-copy lens that does not re-parse source in the // interpreter -- native ingest, or a lens that reads the compile graph the floor has already built. // -// SUFFICIENT FOR, stated because a trigger naming less than the capability it restores is satisfied -// while the capability stays dead, and because the loss here is CORPUS-grain while the tempting -// trigger is per-subject: the capability is corpus_gate returning a verdict over the DISCOVERED +// SUFFICIENT FOR -- AND THE MEASUREMENT THAT SETS THE BAR IS THE SMALLEST SUBJECT, NOT THE CORPUS. +// This is the finding to carry out of this lane and it belongs in the trigger rather than in a +// discussion: interpreted ingest of a FIVE-LINE fixture, and of a FOUR-LINE String with no +// filesystem read, both measure about six times the required floor's 500ms per-witness CPU line. +// At the smallest subject expressible. So this is not a budget that could be raised to fit the +// work; it is a floor that no ingest-reaching assertion can ever clear, and it retroactively +// explains why every witness this lens has ever had is frozen or deferred -- a fact that stood +// visible in the roster with no stated cause. A trigger that made large subjects affordable and +// left the smallest one at six times the line would restore nothing. +// +// Stated because a trigger naming less than the capability it restores is satisfied while the +// capability stays dead, and because the loss here is CORPUS-grain while the tempting trigger is +// per-subject: the capability is corpus_gate returning a verdict over the DISCOVERED // population -- every module the module index yields, not a sample and not a subtree -- inside one // required-lane budget and inside one process. Both halves are load-bearing and neither implies the // other. A tenfold speedup per subject satisfies a per-subject reading of the trigger and still diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag index 6de4892407e..ba3639f6e3d 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag @@ -270,6 +270,26 @@ fn corpus_report(pool_roots: List) -> CorpusReport { // "the corpus is only a few thousand" is the not-time-stable excuse DESIGN section 6 refuses. // Ordered comparison is linear and needs no set. +// WHAT THIS JOIN DOES NOT ESTABLISH, NAMED SEPARATELY BECAUSE IT IS A DIFFERENT SENTENCE FROM +// "THE INGEST IS UNENROLLED" AND ONLY THAT ONE WAS WRITTEN DOWN. The executing evidence proves the +// join is correct GIVEN well-formed rows: it builds CorpusFileRow values directly, because the join +// is the subject and a row is its input. The unenrolled predicates prove the lens is correct GIVEN +// real files. Nothing executing joins those two halves. +// +// So the unverified thing is specifically the DISCOVERY-TO-ROWS SEAM: that corpus_rows_for_paths, +// folded over the paths corpus_paths discovers, yields rows in the SAME ORDER, at the SAME +// IDENTITIES and the SAME ARITY the lockstep walk assumes. The assumption is load-bearing here and +// nowhere stated by a type: coverage_missing reports a divergence at the first position where the +// two disagree, so a producer that reordered, deduplicated, or dropped a path would be reported as +// a truncated sweep -- the right refusal for the wrong reason -- and one that silently REPAIRED an +// order mismatch would make this guard permanently green. +// +// That seam is where a real defect would sit, because it is the only part of this path with no +// evidence on either side of it. It cannot be witnessed today for the same reason everything else +// here cannot: reaching a real row costs an ingest. It dissolves on the same trigger, and when that +// trigger clears, the row to write first is not another join case -- it is one witness that +// discovers a small real population and checks the produced rows against it at identity grain. + fn coverage_missing(discovered: List, rows: List) -> List { match discovered { Empty => Empty From f2d00e6db4c065d0e112f601ea036c94aabd85bc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 10:55:44 +0000 Subject: [PATCH 14/17] chore: regenerate drifted generated artifacts (ci auto-heal) Ledger-Repair-Judged: docs/design-failure-modes.md Ledger-Rows-Repaired: docs/design-failure-modes.md sweep_terminated_by_process_death_rather_than_a_typed_refusal Ledger-Repair-Judged: docs/design-rung-drops.md --- docs/design-failure-modes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index 701917cd8f9..e624a63849b 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -306,7 +306,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a deconfliction plan does not enumerate its writers** (two or more lanes reason about how to avoid colliding on one subject, agree on a scheme, and collide anyway -- because the scheme is a PREDICTION about who will write the subject next, and the predicted population omits a writer. The omitted writer is not an adversary: it is a peer who agreed to stand down and then WROTE ANYWAY -- an earlier revision of this sentence said "and then did not act", which is the opposite mechanism and could not produce the collision the row models, because a writer that does not write collides with nobody. The result merges CLEAN, so the collision is silent. INVALID STATE: a lane holding a deconfliction property that quantifies over a writer population it never discovered. WHY IT SURVIVES REVIEW: the plan is examined for whether it WOULD prevent a collision among the writers it names, and it always would. Nothing in a diff exhibits the writer it omits, and the omitted writer's own change is independently correct. SPECIMEN (main's parse break, 2026-09-04). Four lanes independently authored a repair for one orphaned annotation. Deconfliction ran and SUCCEEDED AS A DISCUSSION: #10414 was named the winner on an earliest-with-proof rule, and the other authors assented. Three of the four then merged anyway (#10425, #10408, #10428), each deleting the same nineteen lines and each adding its own relocated copy, leaving one paragraph standing three times in the module. One copy carried an inverted double negative asserting the opposite of the sentence it was restating. Repaired by removing two copies in #10435. THE ASSENT IS LOAD-BEARING, NOT INCIDENTAL, AND IT IS WHAT BOUNDS THIS CLASS: the population was not unknown and the peers were not uncooperative. Every author was identified, every author agreed, and the scheme still failed -- so the missing thing is not discovery of the writers but an INTERLOCK on them. A class where the colliding writer was never identified is a different and easier problem, because enumerating it is the whole remedy. WHY GIT CANNOT SEE IT, which is the load-bearing mechanism: identical DELETIONS on every side reconcile silently, and each side's ADDITION lands at a different offset, so no hunk overlaps. Three independently correct repairs of one defect merge without a single conflict. A collision that produces a conflict is the benign case; this class is defined by the absence of one. THE SECOND-ORDER TRAP: the ranking conversation consumes the attention that the standing-down would need. Agreement about WHICH repair wins reads as resolution, and it is not -- closing a pull request is the act, and here one of four was closed. Naming a winner is not the losers standing down. NOT TO BE MERGED WITH a_branch_property_falsified_by_a_derived_push, which shares only the SYMPTOM that no conflict is produced so nothing notifies. That symptom has at least three mechanisms -- this one, the derived-push one, and the generated-artifact merge driver refusing with no markers -- so it does not discriminate, and uniting on it is the bare alias section 3 forbids rather than the proven coincidence section 6 requires. The concrete cost of merging them is a grain mismatch section 4b(3) names as its own review tell: this row is genuinely blocked on cross-head knowledge, the other is decidable from a single ref TODAY, and one row carries one stated ceiling because RecurringFailureMode has no ceiling or trigger FIELD -- so the affordable class would be parked behind this one's capability and never rank for a climb it does not need. RUNG: found BELOW the ladder. The duplicated meaning is a silently wrong answer -- every copy parses, every gate is green, and the projection of a triple-landed repair is indistinguishable from a correct one by any executing check. CEILING 2, mechanically preventable, and the reason it is not higher is that the writer population is not decidable from the repository: it includes future human decisions about whether to close a head. What IS decidable is the SUBJECT overlap between open heads. NEXT-RUNG TRIGGER: the capability of resolving an open pull request head to the set of DECLARATIONS it modifies, which is the same Node-tree read the namespace authority already performs and which no instrument currently exposes. A path-grain fence does not discharge it, because this specimen was a single path and the collision was within one declaration's neighbourhood. REVIEW TELL: a lane stating why it CANNOT collide with another lane. The claim is a quantifier over writers, so ask which writers it ranges over and whether an unclosed peer head is inside it. And after a CONTESTED fix lands, re-measure the SUBJECT for duplication rather than re-measuring whether it is green -- green is exactly what triple-duplication looks like. - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. - **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) -- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) +- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the 500ms CPU deadline, and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, cpu_at_least=NNNms/500ms, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. - **a printed remedy is mangled by the medium that prints it** (an instruction is authored correctly, and the channel that carries it to its reader REWRITES it in transit. The author reads the authored string and sees the remedy; the reader receives something shorter that no longer does what it says; and no error crosses the boundary, because the rewriting is the medium behaving exactly as specified. THE RECOGNITION RULE, and it is the whole row: THE SENDER SEES INTACT TEXT, THE RECIPIENT GETS A SILENTLY SHORTENED SENTENCE, AND NOTHING REPORTS A FAILURE. The send succeeds. So every instrument that reads the AUTHORED artifact -- grep, review, a byte-exact drift gate over the source -- agrees the remedy is present, because it is present. The defect is in the rendering, which means the subject has to be what the reader RECEIVES and never what the author wrote. FIRST SPECIMEN, calm-owl-417, gunbc#10460, and it is the one that names the class: gunbc.generated_artifact_merge_driver prints its repair recipe through a double-quoted `echo`, because the lines are templates and `$merged_path` must interpolate. Double quotes leave the backquote active, and the recipe's sed program wrote its capture group between backquotes. Bash ran `\([a-z0-9_]*\)` as a COMMAND SUBSTITUTION at echo time: two `command not found` lines on stderr, and the printed instruction came out with the capture group DELETED. Pasted, it extracted zero rows from both sides of a set difference, and a difference over two empty sets is empty -- which the recipe's own words read as `MUST BE EMPTY`. The mangled remedy reported the vacuous pass the intact remedy exists to prevent, at the exact moment rows were at risk. SECOND SPECIMEN, same PR, and it is the same boundary read from the other side rather than a repeat: `$` stays active ON PURPOSE, so a `$1` written into a recipe line is the DRIVER's argv and not the reader's. An extractor drafted as a shell function taking `$1` rendered as `git show "O:docs/design-rung-drops.md"` -- git's own `%O` placeholder substituted into the instruction it was printing. Escaping does not save that one, because the escaper doubles backslashes first, so the remedy is to name only the variable the printer DEFINES. The two specimens together are why the class is the MEDIUM and not the escaping: one is a character the medium consumed, the other is a character the medium substituted, and no single escaping rule addresses both. THE DAMAGE IS NOT PROPORTIONAL TO THE CORRUPTION, and this is the sharpest available statement of the class. The repaired extractor carries TWO sed clauses, because the two delegated projections spell a row identity differently -- a backquoted slug bullet and a `### Title` heading. Mangling one character in the FIRST clause makes sed refuse the ENTIRE program on an invalid backreference, so the second clause, which nothing touched, extracts nothing either. Measured against a fixture carrying both spellings and 157 rostered rows: the intact program names 157, the mangled program names ZERO -- not 36, which is what a reader assuming per-clause degradation would predict. One corrupted character silenced a projection it never appeared in. THIRD AND FOURTH SPECIMENS, 2026-09-05, AND THEY ARE WHAT MAKE THIS A CLASS RATHER THAN A FACT ABOUT MERGE DRIVERS: the same mechanism fired twice more within ninety minutes, in a completely different medium, in messages ABOUT this class. An agent messaging body was passed to `dashboard-message --body` as a double-quoted shell argument; the body contained backquoted identifiers; bash substituted them away and the recipients received sentences with phrases missing. One of the two was a message whose SUBJECT was instruments that report success without carrying their claim. The other was sent by the author repairing the first specimen, while writing about the first specimen, roughly an hour after reading the analysis describing it. WHAT THOSE TWO ADD: the class is not a property of emitted artifacts or of merge drivers. It is a property of ANY text handed to a shell-quoted channel, and the agent messaging channel is one of them and is uninstrumented at both ends. THE OBVIOUS DEFENCE IS THE THING THAT FAILED, and a reader will supply the wrong remedy here unless it is stated. `Remember to quote correctly` was in active working memory in both messaging specimens and did not prevent either. The diagnosis in both cases was identical: the protection was attached to a command SPELLING rather than to the hazard -- one command was being single-quoted correctly all evening, and a DIFFERENT command was reached for with double quotes without re-deriving the rule. Vigilance that is bound to a habit does not transfer to the next call site. THE MECHANICAL FORM, which is the operative remedy and is cheap: write the body to a file with a QUOTED heredoc, then pass `"$(cat file)"`. Single-quoting the argument is insufficient, not merely awkward, because prose reliably contains apostrophes and one apostrophe ends the quoting. THE MANGLING IS NOT MERELY LOSSY -- IT EXECUTES, IN THE SENDER'S ENVIRONMENT, AND THAT IS THE MOST UNDER-STATED PROPERTY OF THIS CLASS. Discovered as physical residue rather than by reasoning: the third specimen's message contained the phrase `-> Bool` inside a double-quoted argument, and the shell read it as a COMMAND `-` that was not found, followed by a REDIRECTION that created an empty file named `Bool` in the repository working tree. It sat there untracked for fifteen minutes and was found by `git status`, not by anything watching the channel. The general statement is worse than the instance: a backquoted span inside double quotes is COMMAND SUBSTITUTION, which is arbitrary execution, and an unquoted redirection operator writes files. So the sender's mistake is not that a sentence arrived shortened; it is that a fragment of English prose was EVALUATED AS A PROGRAM against a live working tree. The lossy reading of this class is the benign one. AND IT WAS ONE ROUTINE COMMAND FROM BEING COMMITTED: the author of that instance stages with `git add -A`, so the next commit would have swept an artifact of a mangled chat message into a pull request, with a plausible-looking name and no origin anyone could reconstruct. THE REASON IT DID NOT LAND IS COMMIT ORDERING AND NOTHING ELSE: the last commit happened to precede the corruption by fifteen minutes. Recording that as `it did not land` would be luck reported as safety, which is the reporting failure this ledger exists to refuse -- no mechanism was involved, nothing detected it, and nothing would have blocked it. The file's name resembles a type this corpus uses everywhere, so review would most likely have read it as an intentional stray rather than as residue, and its provenance is unreconstructable from the diff. THE CHECK THIS LICENSES, and it caught something on its FIRST application in a second tree: inspect `git status` for unexplained worktree changes after any session that has sent shell-quoted messages, and do not stage with `git add -A` in a tree where that has happened. Applied immediately by a second author, it found no execution residue but did surface a forgotten STAGED modification to a generated artifact, checked out during verification and left behind -- a different cause with the same shape, on a branch that was main. GENERALIZATION: the hazard is not restricted to mangled messages. ANY command that writes into the worktree as a side effect of verification leaves residue a reviewer cannot distinguish from intent. The heredoc form prevents the cause; the status check catches damage already done, whatever wrote it. THE TRANSPORT'S SUCCESS SIGNAL WAS NEVER ASKED THE RELEVANT QUESTION, and this is the precise form of `no error crosses the boundary`. Successful delivery answers whether the SUBMITTED payload reached its destination. It does not answer whether that payload preserved the INTENDED message. Those are two different claims, and every send in every specimen above was truthful about the first while nothing anywhere was asked the second. A PRESERVATION CHECK MUST BE ANCHORED BEFORE THE DANGEROUS TRANSFORMATION, AND THE OBVIOUS IMPLEMENTATION IS WORTHLESS. The natural control is to digest what was sent and compare it with what arrived. If that digest is computed AFTER the shell has consumed the backquoted spans, it faithfully attests the CORRUPTED payload and reports a perfect match: both sides agree, the comparison is sound, and the defect is invisible, because the damage happened upstream of the first observation. The comparison must STRADDLE the transformation that does the damage -- intended payload captured BEFORE it, against payload received after it -- which is this row's own recognition rule applied to its own remedy. It is the same shape as the neighbouring instruments in this ledger, one layer further out: `comm` cannot report that its own operand was empty, a digest over emitted driver bytes proves an escape was APPLIED but not that it was CORRECT, and a preservation digest taken downstream of the corruption proves only that the corruption was transmitted reliably. QUALIFICATION, so a correct transport does not read as corruption: where a channel LEGITIMATELY transforms encoding or line endings, the comparison needs an explicit preservation contract stating what is allowed to change. THE RECEIVING END FAILS TOO, AND THAT COMPLETES THE PICTURE. Recorded as evidence about DETECTABILITY rather than about the mechanism: in the third specimen a reader received the corrupted message, read it, and did not identify it as corrupted. The missing expressions and an injected directory listing were both present in what they received, and the surrounding prose still supported the conclusions the conversation reached -- so the damage was survivable enough to be invisible, and they flagged only afterwards that those spans should have been treated as corrupt. So there are THREE parties and no signal at any of them: the sender sees intact text, the transport reports success against a question about a different payload, and the recipient reads fluent prose with holes in it. THE CLASS FIRED IN PRODUCTION ON THE BRANCH THAT CLOSES IT, AT EQUAL ROW COUNTS, and this receipt is a discriminating RED on the real acceptance path rather than a fixture. Merging main into that branch reached this driver on docs/design-failure-modes.md. It refused as designed -- zero conflict markers, three index stages, the ours side left clean -- and the repaired step 2, run verbatim, measured: base 126 rows, ours 126 rows, COUNTS EQUAL, with the ours side dropping exactly one row that main had added, `duplicate_record_literal_field_silently_last_wins`, and adding one of its own. A cardinality check passes on that comparison and reports nothing. Only the identity join names the casualty. So the case a reviewer had challenged the branch to cover -- a substitution that preserves the count -- arrived unprompted, on the same projection, within minutes of the controls for it being enrolled; the branch did not have to argue that the case was reachable, because it walked in. RUNG FOUND AT: silent wrongness wherever the subject read by any instrument is the authored string, because there the defect is invisible by construction; mitigatable where a witness reads the EMITTED artifact, which is where gunbc#10460 put it. CEILING: mechanically preventable for media this repository emits into -- a witness that executes the emitted artifact and reads what the reader receives cannot be satisfied by an authored string, and the shell-quoting encodings have single authorities that a hand-spelled call site can be made not to bypass. It stays MITIGATABLE for channels this repository does not own, such as an external tool's argv handling, which is observed rather than proven. NEXT-RUNG TRIGGER: the bash medium read forward from grammar rows, so a printed line's variable references and shell-active characters are DECLARED rather than spelled -- at which point no emitter concatenates a shell literal and the escaping arm has nothing to get wrong. That is the same trigger gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_emit_dissolution_trigger already carries, and it is named here rather than restated because one capability retires both. DISTINGUISHING FACT AGAINST ITS NEAREST NEIGHBOURS. It is NOT `empty_capture_read_as_clean_result`: there a well-formed refusal travels on a stream the reader did not keep, and the reader's error is consuming the emptiness; here the text is rewritten before anyone reads it and the reader's conduct is irrelevant. The empty operand is one CONSEQUENCE of this class, not the class, and the second specimen proves it -- a substituted positional argument is not an empty capture at all. It is NOT `restoration_promise_names_a_route_that_does_not_exist`: there the named route is absent; here it exists and the rendering breaks it.) From 541aab5d4e0210a9da9e9169bbf54eddb9e9a8f0 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 11:13:29 +0000 Subject: [PATCH 15/17] Name the CPU line rather than transcribing it, and record that the debt contract is not a door Two corrections from reading the authority instead of the number. The floor's per-witness CPU line is now cited as v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms everywhere this lane mentions it. The figure is deliberately not carried: required_floor.dag's own prose already states a stale ten-fold value for that function in nine places, two hundred lines from the declaration, and five files corpus-wide repeat it. Copying the digit here would have made this lane the tenth site of the exact class it spent the morning filing. Not fixing that prose -- not this lane -- but not inheriting it either. Second, the trigger was written as though the ceiling might move. It will not: the line did not drift, gunbc#9517 RESTORED it and froze the over-cost population as a monotone debt contract in the same change. So it is an operator ceiling with a debt contract behind it, and the trigger now says what must become true UNDER it -- an ingest realization whose cost at corpus grain fits inside that line -- rather than waiting for a budget that was deliberately put back. And the debt contract is not an admissions queue. v2.workflow.floor_cost_debt declares itself shrink-only ("from here the direction is shrink-only in earnest"), so it is the roster of what the restored ceiling caught. "Enrolled in nothing" is therefore not a state these entry points can leave by asking to be excused; the only exit is the trigger. That now stands in the file rather than in a thread. The ledger projection is regenerated in the same commit rather than left to the heal job, whose push has now lost a non-fast-forward race twice. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- ...cess_death_rather_than_a_typed_refusal.dag | 2 +- docs/design-failure-modes.md | 2 +- .../corpus_census.dag | 32 ++++++++++++------- .../accumulator_copy_corpus_path_test.dag | 11 ++++--- ...cumulator_copy_unreadable_subject_test.dag | 2 +- 5 files changed, 29 insertions(+), 20 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag index b10472e9239..6f8f9d8f092 100644 --- a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag +++ b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag @@ -31,7 +31,7 @@ data sweep_terminated_by_process_death_rather_than_a_typed_refusal: RecurringFai "ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. ", - "A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the 500ms CPU deadline, and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, cpu_at_least=NNNms/500ms, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. ", + "A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. ", "RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.)", ], diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index e624a63849b..d7783f030b7 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -306,7 +306,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a deconfliction plan does not enumerate its writers** (two or more lanes reason about how to avoid colliding on one subject, agree on a scheme, and collide anyway -- because the scheme is a PREDICTION about who will write the subject next, and the predicted population omits a writer. The omitted writer is not an adversary: it is a peer who agreed to stand down and then WROTE ANYWAY -- an earlier revision of this sentence said "and then did not act", which is the opposite mechanism and could not produce the collision the row models, because a writer that does not write collides with nobody. The result merges CLEAN, so the collision is silent. INVALID STATE: a lane holding a deconfliction property that quantifies over a writer population it never discovered. WHY IT SURVIVES REVIEW: the plan is examined for whether it WOULD prevent a collision among the writers it names, and it always would. Nothing in a diff exhibits the writer it omits, and the omitted writer's own change is independently correct. SPECIMEN (main's parse break, 2026-09-04). Four lanes independently authored a repair for one orphaned annotation. Deconfliction ran and SUCCEEDED AS A DISCUSSION: #10414 was named the winner on an earliest-with-proof rule, and the other authors assented. Three of the four then merged anyway (#10425, #10408, #10428), each deleting the same nineteen lines and each adding its own relocated copy, leaving one paragraph standing three times in the module. One copy carried an inverted double negative asserting the opposite of the sentence it was restating. Repaired by removing two copies in #10435. THE ASSENT IS LOAD-BEARING, NOT INCIDENTAL, AND IT IS WHAT BOUNDS THIS CLASS: the population was not unknown and the peers were not uncooperative. Every author was identified, every author agreed, and the scheme still failed -- so the missing thing is not discovery of the writers but an INTERLOCK on them. A class where the colliding writer was never identified is a different and easier problem, because enumerating it is the whole remedy. WHY GIT CANNOT SEE IT, which is the load-bearing mechanism: identical DELETIONS on every side reconcile silently, and each side's ADDITION lands at a different offset, so no hunk overlaps. Three independently correct repairs of one defect merge without a single conflict. A collision that produces a conflict is the benign case; this class is defined by the absence of one. THE SECOND-ORDER TRAP: the ranking conversation consumes the attention that the standing-down would need. Agreement about WHICH repair wins reads as resolution, and it is not -- closing a pull request is the act, and here one of four was closed. Naming a winner is not the losers standing down. NOT TO BE MERGED WITH a_branch_property_falsified_by_a_derived_push, which shares only the SYMPTOM that no conflict is produced so nothing notifies. That symptom has at least three mechanisms -- this one, the derived-push one, and the generated-artifact merge driver refusing with no markers -- so it does not discriminate, and uniting on it is the bare alias section 3 forbids rather than the proven coincidence section 6 requires. The concrete cost of merging them is a grain mismatch section 4b(3) names as its own review tell: this row is genuinely blocked on cross-head knowledge, the other is decidable from a single ref TODAY, and one row carries one stated ceiling because RecurringFailureMode has no ceiling or trigger FIELD -- so the affordable class would be parked behind this one's capability and never rank for a climb it does not need. RUNG: found BELOW the ladder. The duplicated meaning is a silently wrong answer -- every copy parses, every gate is green, and the projection of a triple-landed repair is indistinguishable from a correct one by any executing check. CEILING 2, mechanically preventable, and the reason it is not higher is that the writer population is not decidable from the repository: it includes future human decisions about whether to close a head. What IS decidable is the SUBJECT overlap between open heads. NEXT-RUNG TRIGGER: the capability of resolving an open pull request head to the set of DECLARATIONS it modifies, which is the same Node-tree read the namespace authority already performs and which no instrument currently exposes. A path-grain fence does not discharge it, because this specimen was a single path and the collision was within one declaration's neighbourhood. REVIEW TELL: a lane stating why it CANNOT collide with another lane. The claim is a quantifier over writers, so ask which writers it ranges over and whether an unclosed peer head is inside it. And after a CONTESTED fix lands, re-measure the SUBJECT for duplication rather than re-measuring whether it is green -- green is exactly what triple-duplication looks like. - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. - **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) -- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the 500ms CPU deadline, and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, cpu_at_least=NNNms/500ms, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) +- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. - **a printed remedy is mangled by the medium that prints it** (an instruction is authored correctly, and the channel that carries it to its reader REWRITES it in transit. The author reads the authored string and sees the remedy; the reader receives something shorter that no longer does what it says; and no error crosses the boundary, because the rewriting is the medium behaving exactly as specified. THE RECOGNITION RULE, and it is the whole row: THE SENDER SEES INTACT TEXT, THE RECIPIENT GETS A SILENTLY SHORTENED SENTENCE, AND NOTHING REPORTS A FAILURE. The send succeeds. So every instrument that reads the AUTHORED artifact -- grep, review, a byte-exact drift gate over the source -- agrees the remedy is present, because it is present. The defect is in the rendering, which means the subject has to be what the reader RECEIVES and never what the author wrote. FIRST SPECIMEN, calm-owl-417, gunbc#10460, and it is the one that names the class: gunbc.generated_artifact_merge_driver prints its repair recipe through a double-quoted `echo`, because the lines are templates and `$merged_path` must interpolate. Double quotes leave the backquote active, and the recipe's sed program wrote its capture group between backquotes. Bash ran `\([a-z0-9_]*\)` as a COMMAND SUBSTITUTION at echo time: two `command not found` lines on stderr, and the printed instruction came out with the capture group DELETED. Pasted, it extracted zero rows from both sides of a set difference, and a difference over two empty sets is empty -- which the recipe's own words read as `MUST BE EMPTY`. The mangled remedy reported the vacuous pass the intact remedy exists to prevent, at the exact moment rows were at risk. SECOND SPECIMEN, same PR, and it is the same boundary read from the other side rather than a repeat: `$` stays active ON PURPOSE, so a `$1` written into a recipe line is the DRIVER's argv and not the reader's. An extractor drafted as a shell function taking `$1` rendered as `git show "O:docs/design-rung-drops.md"` -- git's own `%O` placeholder substituted into the instruction it was printing. Escaping does not save that one, because the escaper doubles backslashes first, so the remedy is to name only the variable the printer DEFINES. The two specimens together are why the class is the MEDIUM and not the escaping: one is a character the medium consumed, the other is a character the medium substituted, and no single escaping rule addresses both. THE DAMAGE IS NOT PROPORTIONAL TO THE CORRUPTION, and this is the sharpest available statement of the class. The repaired extractor carries TWO sed clauses, because the two delegated projections spell a row identity differently -- a backquoted slug bullet and a `### Title` heading. Mangling one character in the FIRST clause makes sed refuse the ENTIRE program on an invalid backreference, so the second clause, which nothing touched, extracts nothing either. Measured against a fixture carrying both spellings and 157 rostered rows: the intact program names 157, the mangled program names ZERO -- not 36, which is what a reader assuming per-clause degradation would predict. One corrupted character silenced a projection it never appeared in. THIRD AND FOURTH SPECIMENS, 2026-09-05, AND THEY ARE WHAT MAKE THIS A CLASS RATHER THAN A FACT ABOUT MERGE DRIVERS: the same mechanism fired twice more within ninety minutes, in a completely different medium, in messages ABOUT this class. An agent messaging body was passed to `dashboard-message --body` as a double-quoted shell argument; the body contained backquoted identifiers; bash substituted them away and the recipients received sentences with phrases missing. One of the two was a message whose SUBJECT was instruments that report success without carrying their claim. The other was sent by the author repairing the first specimen, while writing about the first specimen, roughly an hour after reading the analysis describing it. WHAT THOSE TWO ADD: the class is not a property of emitted artifacts or of merge drivers. It is a property of ANY text handed to a shell-quoted channel, and the agent messaging channel is one of them and is uninstrumented at both ends. THE OBVIOUS DEFENCE IS THE THING THAT FAILED, and a reader will supply the wrong remedy here unless it is stated. `Remember to quote correctly` was in active working memory in both messaging specimens and did not prevent either. The diagnosis in both cases was identical: the protection was attached to a command SPELLING rather than to the hazard -- one command was being single-quoted correctly all evening, and a DIFFERENT command was reached for with double quotes without re-deriving the rule. Vigilance that is bound to a habit does not transfer to the next call site. THE MECHANICAL FORM, which is the operative remedy and is cheap: write the body to a file with a QUOTED heredoc, then pass `"$(cat file)"`. Single-quoting the argument is insufficient, not merely awkward, because prose reliably contains apostrophes and one apostrophe ends the quoting. THE MANGLING IS NOT MERELY LOSSY -- IT EXECUTES, IN THE SENDER'S ENVIRONMENT, AND THAT IS THE MOST UNDER-STATED PROPERTY OF THIS CLASS. Discovered as physical residue rather than by reasoning: the third specimen's message contained the phrase `-> Bool` inside a double-quoted argument, and the shell read it as a COMMAND `-` that was not found, followed by a REDIRECTION that created an empty file named `Bool` in the repository working tree. It sat there untracked for fifteen minutes and was found by `git status`, not by anything watching the channel. The general statement is worse than the instance: a backquoted span inside double quotes is COMMAND SUBSTITUTION, which is arbitrary execution, and an unquoted redirection operator writes files. So the sender's mistake is not that a sentence arrived shortened; it is that a fragment of English prose was EVALUATED AS A PROGRAM against a live working tree. The lossy reading of this class is the benign one. AND IT WAS ONE ROUTINE COMMAND FROM BEING COMMITTED: the author of that instance stages with `git add -A`, so the next commit would have swept an artifact of a mangled chat message into a pull request, with a plausible-looking name and no origin anyone could reconstruct. THE REASON IT DID NOT LAND IS COMMIT ORDERING AND NOTHING ELSE: the last commit happened to precede the corruption by fifteen minutes. Recording that as `it did not land` would be luck reported as safety, which is the reporting failure this ledger exists to refuse -- no mechanism was involved, nothing detected it, and nothing would have blocked it. The file's name resembles a type this corpus uses everywhere, so review would most likely have read it as an intentional stray rather than as residue, and its provenance is unreconstructable from the diff. THE CHECK THIS LICENSES, and it caught something on its FIRST application in a second tree: inspect `git status` for unexplained worktree changes after any session that has sent shell-quoted messages, and do not stage with `git add -A` in a tree where that has happened. Applied immediately by a second author, it found no execution residue but did surface a forgotten STAGED modification to a generated artifact, checked out during verification and left behind -- a different cause with the same shape, on a branch that was main. GENERALIZATION: the hazard is not restricted to mangled messages. ANY command that writes into the worktree as a side effect of verification leaves residue a reviewer cannot distinguish from intent. The heredoc form prevents the cause; the status check catches damage already done, whatever wrote it. THE TRANSPORT'S SUCCESS SIGNAL WAS NEVER ASKED THE RELEVANT QUESTION, and this is the precise form of `no error crosses the boundary`. Successful delivery answers whether the SUBMITTED payload reached its destination. It does not answer whether that payload preserved the INTENDED message. Those are two different claims, and every send in every specimen above was truthful about the first while nothing anywhere was asked the second. A PRESERVATION CHECK MUST BE ANCHORED BEFORE THE DANGEROUS TRANSFORMATION, AND THE OBVIOUS IMPLEMENTATION IS WORTHLESS. The natural control is to digest what was sent and compare it with what arrived. If that digest is computed AFTER the shell has consumed the backquoted spans, it faithfully attests the CORRUPTED payload and reports a perfect match: both sides agree, the comparison is sound, and the defect is invisible, because the damage happened upstream of the first observation. The comparison must STRADDLE the transformation that does the damage -- intended payload captured BEFORE it, against payload received after it -- which is this row's own recognition rule applied to its own remedy. It is the same shape as the neighbouring instruments in this ledger, one layer further out: `comm` cannot report that its own operand was empty, a digest over emitted driver bytes proves an escape was APPLIED but not that it was CORRECT, and a preservation digest taken downstream of the corruption proves only that the corruption was transmitted reliably. QUALIFICATION, so a correct transport does not read as corruption: where a channel LEGITIMATELY transforms encoding or line endings, the comparison needs an explicit preservation contract stating what is allowed to change. THE RECEIVING END FAILS TOO, AND THAT COMPLETES THE PICTURE. Recorded as evidence about DETECTABILITY rather than about the mechanism: in the third specimen a reader received the corrupted message, read it, and did not identify it as corrupted. The missing expressions and an injected directory listing were both present in what they received, and the surrounding prose still supported the conclusions the conversation reached -- so the damage was survivable enough to be invisible, and they flagged only afterwards that those spans should have been treated as corrupt. So there are THREE parties and no signal at any of them: the sender sees intact text, the transport reports success against a question about a different payload, and the recipient reads fluent prose with holes in it. THE CLASS FIRED IN PRODUCTION ON THE BRANCH THAT CLOSES IT, AT EQUAL ROW COUNTS, and this receipt is a discriminating RED on the real acceptance path rather than a fixture. Merging main into that branch reached this driver on docs/design-failure-modes.md. It refused as designed -- zero conflict markers, three index stages, the ours side left clean -- and the repaired step 2, run verbatim, measured: base 126 rows, ours 126 rows, COUNTS EQUAL, with the ours side dropping exactly one row that main had added, `duplicate_record_literal_field_silently_last_wins`, and adding one of its own. A cardinality check passes on that comparison and reports nothing. Only the identity join names the casualty. So the case a reviewer had challenged the branch to cover -- a substitution that preserves the count -- arrived unprompted, on the same projection, within minutes of the controls for it being enrolled; the branch did not have to argue that the case was reachable, because it walked in. RUNG FOUND AT: silent wrongness wherever the subject read by any instrument is the authored string, because there the defect is invisible by construction; mitigatable where a witness reads the EMITTED artifact, which is where gunbc#10460 put it. CEILING: mechanically preventable for media this repository emits into -- a witness that executes the emitted artifact and reads what the reader receives cannot be satisfied by an authored string, and the shell-quoting encodings have single authorities that a hand-spelled call site can be made not to bypass. It stays MITIGATABLE for channels this repository does not own, such as an external tool's argv handling, which is observed rather than proven. NEXT-RUNG TRIGGER: the bash medium read forward from grammar rows, so a printed line's variable references and shell-active characters are DECLARED rather than spelled -- at which point no emitter concatenates a shell literal and the escaping arm has nothing to get wrong. That is the same trigger gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_emit_dissolution_trigger already carries, and it is named here rather than restated because one capability retires both. DISTINGUISHING FACT AGAINST ITS NEAREST NEIGHBOURS. It is NOT `empty_capture_read_as_clean_result`: there a well-formed refusal travels on a stream the reader did not keep, and the reader's error is consuming the emptiness; here the text is rewritten before anyone reads it and the reader's conduct is irrelevant. The empty operand is one CONSEQUENCE of this class, not the class, and the second specimen proves it -- a substituted positional argument is not an empty capture at all. It is NOT `restoration_promise_names_a_route_that_does_not_exist`: there the named route is absent; here it exists and the rendering breaks it.) diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index 93d54db4ff5..b2c42dfd305 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -91,11 +91,12 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // lives here are entry points, run by a person, whose output belongs in a run. // // AND THE PRICE THAT DECIDES THIS IS NOT THE CORPUS -- IT IS ONE SUBJECT, WHICH IS A HARDER FACT -// THAN THE ONE THIS FILE FIRST RECORDED. The required floor's per-witness CPU line is 500ms -// (v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms, an operator ruling whose -// two raises were repudiated). Interpreted ingest of a FIVE-LINE fixture measures about 3s, and a -// four-line String snippet with no filesystem read measures about the same: six times the line, at -// the smallest subject expressible. So no assertion in this lens that reaches the ingest can be +// THAN THE ONE THIS FILE FIRST RECORDED. The required floor's per-witness CPU line is +// v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms -- named rather than +// transcribed, because that module's own prose already carries a stale ten-fold figure for it in +// nine places and this file will not become the tenth. Interpreted ingest of a FIVE-LINE fixture, +// and of a four-line String snippet with no filesystem read, both measure several times that line, +// at the smallest subject expressible; claim_batch re-derives both on the host asking. So no assertion in this lens that reaches the ingest can be // enrolled at any subject size, which is why every witness this lens ever had is frozen or // deferred -- a fact that was visible in the roster and had never been explained. Measured with // claim_batch on this branch; run it rather than trusting this sentence. @@ -147,12 +148,19 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // SUFFICIENT FOR -- AND THE MEASUREMENT THAT SETS THE BAR IS THE SMALLEST SUBJECT, NOT THE CORPUS. // This is the finding to carry out of this lane and it belongs in the trigger rather than in a // discussion: interpreted ingest of a FIVE-LINE fixture, and of a FOUR-LINE String with no -// filesystem read, both measure about six times the required floor's 500ms per-witness CPU line. -// At the smallest subject expressible. So this is not a budget that could be raised to fit the -// work; it is a floor that no ingest-reaching assertion can ever clear, and it retroactively -// explains why every witness this lens has ever had is frozen or deferred -- a fact that stood -// visible in the roster with no stated cause. A trigger that made large subjects affordable and -// left the smallest one at six times the line would restore nothing. +// filesystem read, both measure several times the required floor's per-witness CPU line +// (v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms) -- at the smallest subject +// expressible. So the trigger is not "when the per-claim budget accommodates ingest", which reads +// as waiting for the ceiling to move: the ceiling did not drift, it was deliberately RESTORED by +// gunbc#9517, which froze the over-cost population as a monotone debt contract at the same time. +// It is an operator ceiling with a debt contract behind it, and the trigger is what must become +// true UNDER it: an ingest realization whose cost at corpus grain fits inside that line. A change +// that made large subjects affordable and left the smallest one over would restore nothing. +// +// AND THE DEBT CONTRACT IS NOT A DOOR. v2.workflow.floor_cost_debt is declared shrink-only ("from +// here the direction is shrink-only in earnest"), so it is the roster of what the restored ceiling +// caught, not an admissions queue. "Enrolled in nothing" is therefore not a state these entry +// points can leave by asking to be excused; the only exit is the trigger. // // Stated because a trigger naming less than the capability it restores is satisfied while the // capability stays dead, and because the loss here is CORPUS-grain while the tempting trigger is @@ -179,7 +187,7 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree // over a discovered root and over an explicit subject list respectively. Their output belongs in a // run, never transcribed into this file. -// THE INGEST-PRICED PREDICATES, HOMED HERE WITH THE REST OF WHAT THE 500ms CPU LINE PRICES OUT. +// THE INGEST-PRICED PREDICATES, HOMED HERE WITH THE REST OF WHAT THE FLOOR'S CPU LINE PRICES OUT. // Each costs seconds of interpreted ingest per subject and so cannot be a witness; each is a real // question with a real answer, run by a person or by the dissolution named above. a_readable_subject // _still_reaches_the_lens is the deep positive control for the typed-read red in diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index 39535065a8d..cc6c34a5623 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -22,9 +22,10 @@ import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -// WHAT IS A WITNESS HERE AND WHAT IS NOT IS DECIDED BY THE 500ms CPU LINE, MEASURED PER FUNCTION. -// The required floor's per-witness CPU deadline is 500ms; interpreted ingest of the five-line -// fixture below measures about 3s, so every predicate that reaches the ingest reports +// WHAT IS A WITNESS HERE AND WHAT IS NOT IS DECIDED BY THE FLOOR'S CPU LINE, MEASURED PER FUNCTION. +// The line is v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms, named rather +// than transcribed; interpreted ingest of the five-line fixture below measures several times it, +// so every predicate that reaches the ingest reports // interrupted_before_verdict and BLOCKS rather than running (measured on this branch: seven of // them at once, run 34022136976). Those predicates are therefore plain fns -- they pass by // execution under claim_batch and are enrolled in nothing, which is the same standing as the @@ -116,8 +117,8 @@ data clean_path: String = "test/fixture_roots/complexity_accumulator_copy/clean_ // population and a set of reported rows, does a report that covers less than its population refuse // -- and a CorpusFileRow is that join's input, not its producer. Ingesting real subjects to reach // it would test the ingest twice and the join once, and it would cost about 3s per witness against -// the floor's 500ms CPU line, which is how the ingest-priced witnesses in this lens ended up -// enrolled in nothing. The producer-side claim is a different claim and is priced separately; +// the floor's CPU line, which is how the ingest-priced witnesses in this lens ended up enrolled in +// nothing. The producer-side claim is a different claim and is priced separately; // see corpus_census. // // The rows are deliberately NOT all identical: the planted row carries a suspect and the clean row diff --git a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag index 4d6260912e1..137eba95996 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_unreadable_subject_test.dag @@ -56,7 +56,7 @@ test fn an_unreadable_subject_is_refused_with_its_cause() -> Bool { // that -- one channel, two arms, both reachable. // // Reaching it through file_gate_disposition on a real subject would establish the same thing and -// cost about 3s of interpreted ingest, six times the floor's 500ms CPU line, so it would be +// cost several times the floor's per-witness CPU line in interpreted ingest, so it would be // enrolled in nothing and would take this red down with it (measured: the required floor reported // exactly that, interrupted_before_verdict, for every ingest-priced witness in this lane). The // deeper form -- a readable subject reaching the lens and coming back clean -- is kept as the entry From f01c60d7a4a974cf41ddf8b1c148b5f74979adb2 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 11:28:08 +0000 Subject: [PATCH 16/17] File the repair-job red, and correct my own count: one race, one designed supersede A repair job that MODIFIES a branch can exit non-zero for a reason belonging to its own protocol rather than to the content under review, and the only channel most readers have is a red square beside the pull request. The red is true about the job and false about the change, and nothing at that grain distinguishes them. Filed as gunbc.recurring_failure_mode repair_job_red_is_attributed_to_the_content_it_repaired. Two arms, and they are not one mechanism -- I reported them as one before reading the second log, which is the co-occurrence inference this ledger already records. ARM ONE, a genuine race (run 34026632467): heal regenerated correctly and its push was rejected non-fast-forward because I had pushed to the same branch while it was building. ARM TWO, and this is the more interesting half (run 34027083483): heal SUCCEEDED -- HealProduced, prior_head fc576da5a0f, healed_head f2d00e6db4c, artifact pushed -- and then exited 1 with SupersededByHealedHead ... revalidation-required. That is by design: the protocol refuses to report green for a head that no longer exists. Every step worked and the pull request shows a failing job. It cannot be fixed by retrying, because nothing is wrong. So the row states its population honestly rather than claiming recurrence it does not have: recurrence for arm one is NOT established by one receipt; arm two recurs by construction, once per heal that lands. The trigger is sufficient for both -- a retry-with-rebase ends arm one and leaves arm two red on every successful heal, so a fix that only handles the race does not retire the row. Rung mitigatable, ceiling mechanically preventable and deliberately not higher: the job knows which branch it took, so the two outcomes are decidable and separable at the reporting boundary -- but no compiler refuses a reader who misreads a red square, so the wall is at the boundary, not in the reader. Not fixing heal; not this lane. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- ..._attributed_to_the_content_it_repaired.dag | 34 +++++++++++++++++++ dag/gunbc/recurring_failure_mode/roster.dag | 2 ++ docs/design-failure-modes.md | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/repair_job_red_is_attributed_to_the_content_it_repaired.dag diff --git a/dag/gunbc/recurring_failure_mode/repair_job_red_is_attributed_to_the_content_it_repaired.dag b/dag/gunbc/recurring_failure_mode/repair_job_red_is_attributed_to_the_content_it_repaired.dag new file mode 100644 index 00000000000..ad6498f64a7 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/repair_job_red_is_attributed_to_the_content_it_repaired.dag @@ -0,0 +1,34 @@ +module gunbc.recurring_failure_mode.repair_job_red_is_attributed_to_the_content_it_repaired + +import std.types { NonEmptyStr } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data repair_job_red_is_attributed_to_the_content_it_repaired: RecurringFailureMode = RecurringFailureMode { + identity: "repair_job_red_is_attributed_to_the_content_it_repaired" as NonEmptyStr, + + receipts: [ + "**a repair job reports failure for reasons that are not about the thing it repaired, and every reader attributes it to the change** (INVALID STATE: a CI job whose purpose is to REPAIR a branch -- regenerate a projection, apply a fix, push the result -- exits non-zero for a reason belonging to the repair PROTOCOL rather than to the content under review, and the only channel most readers have is the job's red square beside the pull request. The red is true about the job and false about the change, and nothing in the rendering distinguishes the two. ", + + "HARM: misattribution, priced in diagnosis cycles. A red that names the wrong cause sends its author to look at their own diff, and it is indistinguishable at the grain people read -- a job name and a conclusion -- from a genuine drift that the author really did cause. Worse for a bot-authored repair: the author sees a failure on a branch they just pushed to and reasonably concludes they broke it. The cost is paid per reader per occurrence and is invisible in the log, because the log is fine. ", + + "TWO ARMS OBSERVED, AND THEY ARE NOT THE SAME MECHANISM. The distinction is recorded because the second was mistaken for the first once, by the author of this row, and the second is the more interesting half. ", + + "ARM ONE, THE LOST RACE (2026-09-06, run 34026632467): heal-generated-artifacts regenerated correctly and its push was rejected non-fast-forward because the human author had pushed to the same branch while the job was building. The repair was right; the branch had moved. This arm is a genuine race and it produces a red that is about scheduling. ", + + "ARM TWO, THE DESIGNED SUPERSEDE (2026-09-06, run 34027083483): the same job SUCCEEDED -- HealProduced, prior_head fc576da5a0f, healed_head f2d00e6db4c, artifact pushed -- and then exited 1 with `SupersededByHealedHead ... revalidation-required`. That is not a race and not a defect: the protocol deliberately refuses to report green for a head that no longer exists, and requires the revalidation run it just dispatched. Every step worked exactly as designed and the pull request shows a failing job. This arm cannot be fixed by retrying, because there is nothing wrong. ", + + "WHAT THE POPULATION IS, STATED BECAUSE THE FIRST DRAFT OF THIS ROW OVERSTATED IT. One race and one designed supersede, both on one branch on one afternoon. The author reported them to a reviewer as 'lost the race twice' before reading the second log, which is the same co-occurrence-implies-common-cause inference this ledger records elsewhere. Recurrence for arm one is NOT established by these receipts; arm two recurs by construction, once per heal that lands. ", + + "WHY IT IS NOT `a_written_row_is_not_a_firing_mechanism` OR ITS NEIGHBOURS, checked at identity grain. The mechanism here fires correctly and reports correctly about ITSELF; what fails is the JOIN between a job-level conclusion and the question a reader is asking, which is about the change. It is also not the absorbing fallback: nothing widens and nothing is silently accepted -- the failure is loud and typed (`SupersededByHealedHead` names its cause, its prior head and its healed head). The defect is that a true statement about the repair is consumed as a statement about the subject. ", + + "RUNG FOUND AT: mitigatable. The harm is contained by the fact that both arms leave a legible log line naming the real cause, so a reader who opens the job learns the truth; what is missing is that the summary grain carries the distinction at all. ", + + "CEILING: mechanically preventable, and deliberately not higher. Whether a given exit belongs to the repair protocol or to the subject IS decidable -- the job knows which branch it took -- so a job can report the two as different outcomes. It cannot reach structural guarantee, because the reader's inference is not a modeled artifact: no compiler refuses a person who misreads a red square. The wall available is at the reporting boundary, not in the reader. ", + + "NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a repair job's protocol outcomes are reported in a channel DISTINCT from its subject verdict -- so 'I could not land my repair' and 'the content I checked is wrong' are different states at the grain the pull request renders, not two spellings of one red. Sufficient for BOTH arms: a retry-with-rebase would end arm one and leave arm two reporting red on every successful heal, so a fix that only handles the race does not retire this row. The cheaper half is available now and already ends the misattribution: refuse with a typed cause that says which it is. ", + + "RECOGNITION RULE: when a job that MODIFIES a branch reports red, ask whether its own log says the modification succeeded. If it does, the red is about the protocol and the change is unjudged -- and if the job's name does not say so, every reader will spend a cycle learning it.)", + ], + + evidence: [], +} diff --git a/dag/gunbc/recurring_failure_mode/roster.dag b/dag/gunbc/recurring_failure_mode/roster.dag index 9590c9b4d31..dc85d9af018 100644 --- a/dag/gunbc/recurring_failure_mode/roster.dag +++ b/dag/gunbc/recurring_failure_mode/roster.dag @@ -144,6 +144,7 @@ import gunbc.recurring_failure_mode.a_deconfliction_plan_does_not_enumerate_its_ import gunbc.recurring_failure_mode.a_branch_property_falsified_by_a_derived_push { a_branch_property_falsified_by_a_derived_push } import gunbc.recurring_failure_mode.roster_is_its_own_denominator { roster_is_its_own_denominator } import gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal { sweep_terminated_by_process_death_rather_than_a_typed_refusal } +import gunbc.recurring_failure_mode.repair_job_red_is_attributed_to_the_content_it_repaired { repair_job_red_is_attributed_to_the_content_it_repaired } import gunbc.recurring_failure_mode.instrument_answers_outside_its_domain { instrument_answers_outside_its_domain } import gunbc.recurring_failure_mode.discriminating_evidence_authored_into_the_operational_population { discriminating_evidence_authored_into_the_operational_population } import gunbc.recurring_failure_mode.printed_remedy_is_mangled_by_the_medium_that_prints_it { printed_remedy_is_mangled_by_the_medium_that_prints_it } @@ -297,6 +298,7 @@ data recurring_failure_mode_roster: List = [ a_branch_property_falsified_by_a_derived_push, roster_is_its_own_denominator, sweep_terminated_by_process_death_rather_than_a_typed_refusal, + repair_job_red_is_attributed_to_the_content_it_repaired, instrument_answers_outside_its_domain, discriminating_evidence_authored_into_the_operational_population, printed_remedy_is_mangled_by_the_medium_that_prints_it, diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index d7783f030b7..e2f5f2a5228 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -137,6 +137,7 @@ The exact `RecurringFailureMode.identity` population, which no other projection - `a_branch_property_falsified_by_a_derived_push` - `roster_is_its_own_denominator` - `sweep_terminated_by_process_death_rather_than_a_typed_refusal` +- `repair_job_red_is_attributed_to_the_content_it_repaired` - `instrument_answers_outside_its_domain` - `discriminating_evidence_authored_into_the_operational_population` - `printed_remedy_is_mangled_by_the_medium_that_prints_it` @@ -307,6 +308,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. - **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) - **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) +- **a repair job reports failure for reasons that are not about the thing it repaired, and every reader attributes it to the change** (INVALID STATE: a CI job whose purpose is to REPAIR a branch -- regenerate a projection, apply a fix, push the result -- exits non-zero for a reason belonging to the repair PROTOCOL rather than to the content under review, and the only channel most readers have is the job's red square beside the pull request. The red is true about the job and false about the change, and nothing in the rendering distinguishes the two. HARM: misattribution, priced in diagnosis cycles. A red that names the wrong cause sends its author to look at their own diff, and it is indistinguishable at the grain people read -- a job name and a conclusion -- from a genuine drift that the author really did cause. Worse for a bot-authored repair: the author sees a failure on a branch they just pushed to and reasonably concludes they broke it. The cost is paid per reader per occurrence and is invisible in the log, because the log is fine. TWO ARMS OBSERVED, AND THEY ARE NOT THE SAME MECHANISM. The distinction is recorded because the second was mistaken for the first once, by the author of this row, and the second is the more interesting half. ARM ONE, THE LOST RACE (2026-09-06, run 34026632467): heal-generated-artifacts regenerated correctly and its push was rejected non-fast-forward because the human author had pushed to the same branch while the job was building. The repair was right; the branch had moved. This arm is a genuine race and it produces a red that is about scheduling. ARM TWO, THE DESIGNED SUPERSEDE (2026-09-06, run 34027083483): the same job SUCCEEDED -- HealProduced, prior_head fc576da5a0f, healed_head f2d00e6db4c, artifact pushed -- and then exited 1 with `SupersededByHealedHead ... revalidation-required`. That is not a race and not a defect: the protocol deliberately refuses to report green for a head that no longer exists, and requires the revalidation run it just dispatched. Every step worked exactly as designed and the pull request shows a failing job. This arm cannot be fixed by retrying, because there is nothing wrong. WHAT THE POPULATION IS, STATED BECAUSE THE FIRST DRAFT OF THIS ROW OVERSTATED IT. One race and one designed supersede, both on one branch on one afternoon. The author reported them to a reviewer as 'lost the race twice' before reading the second log, which is the same co-occurrence-implies-common-cause inference this ledger records elsewhere. Recurrence for arm one is NOT established by these receipts; arm two recurs by construction, once per heal that lands. WHY IT IS NOT `a_written_row_is_not_a_firing_mechanism` OR ITS NEIGHBOURS, checked at identity grain. The mechanism here fires correctly and reports correctly about ITSELF; what fails is the JOIN between a job-level conclusion and the question a reader is asking, which is about the change. It is also not the absorbing fallback: nothing widens and nothing is silently accepted -- the failure is loud and typed (`SupersededByHealedHead` names its cause, its prior head and its healed head). The defect is that a true statement about the repair is consumed as a statement about the subject. RUNG FOUND AT: mitigatable. The harm is contained by the fact that both arms leave a legible log line naming the real cause, so a reader who opens the job learns the truth; what is missing is that the summary grain carries the distinction at all. CEILING: mechanically preventable, and deliberately not higher. Whether a given exit belongs to the repair protocol or to the subject IS decidable -- the job knows which branch it took -- so a job can report the two as different outcomes. It cannot reach structural guarantee, because the reader's inference is not a modeled artifact: no compiler refuses a person who misreads a red square. The wall available is at the reporting boundary, not in the reader. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a repair job's protocol outcomes are reported in a channel DISTINCT from its subject verdict -- so 'I could not land my repair' and 'the content I checked is wrong' are different states at the grain the pull request renders, not two spellings of one red. Sufficient for BOTH arms: a retry-with-rebase would end arm one and leave arm two reporting red on every successful heal, so a fix that only handles the race does not retire this row. The cheaper half is available now and already ends the misattribution: refuse with a typed cause that says which it is. RECOGNITION RULE: when a job that MODIFIES a branch reports red, ask whether its own log says the modification succeeded. If it does, the red is about the protocol and the change is unjudged -- and if the job's name does not say so, every reader will spend a cycle learning it.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. - **a printed remedy is mangled by the medium that prints it** (an instruction is authored correctly, and the channel that carries it to its reader REWRITES it in transit. The author reads the authored string and sees the remedy; the reader receives something shorter that no longer does what it says; and no error crosses the boundary, because the rewriting is the medium behaving exactly as specified. THE RECOGNITION RULE, and it is the whole row: THE SENDER SEES INTACT TEXT, THE RECIPIENT GETS A SILENTLY SHORTENED SENTENCE, AND NOTHING REPORTS A FAILURE. The send succeeds. So every instrument that reads the AUTHORED artifact -- grep, review, a byte-exact drift gate over the source -- agrees the remedy is present, because it is present. The defect is in the rendering, which means the subject has to be what the reader RECEIVES and never what the author wrote. FIRST SPECIMEN, calm-owl-417, gunbc#10460, and it is the one that names the class: gunbc.generated_artifact_merge_driver prints its repair recipe through a double-quoted `echo`, because the lines are templates and `$merged_path` must interpolate. Double quotes leave the backquote active, and the recipe's sed program wrote its capture group between backquotes. Bash ran `\([a-z0-9_]*\)` as a COMMAND SUBSTITUTION at echo time: two `command not found` lines on stderr, and the printed instruction came out with the capture group DELETED. Pasted, it extracted zero rows from both sides of a set difference, and a difference over two empty sets is empty -- which the recipe's own words read as `MUST BE EMPTY`. The mangled remedy reported the vacuous pass the intact remedy exists to prevent, at the exact moment rows were at risk. SECOND SPECIMEN, same PR, and it is the same boundary read from the other side rather than a repeat: `$` stays active ON PURPOSE, so a `$1` written into a recipe line is the DRIVER's argv and not the reader's. An extractor drafted as a shell function taking `$1` rendered as `git show "O:docs/design-rung-drops.md"` -- git's own `%O` placeholder substituted into the instruction it was printing. Escaping does not save that one, because the escaper doubles backslashes first, so the remedy is to name only the variable the printer DEFINES. The two specimens together are why the class is the MEDIUM and not the escaping: one is a character the medium consumed, the other is a character the medium substituted, and no single escaping rule addresses both. THE DAMAGE IS NOT PROPORTIONAL TO THE CORRUPTION, and this is the sharpest available statement of the class. The repaired extractor carries TWO sed clauses, because the two delegated projections spell a row identity differently -- a backquoted slug bullet and a `### Title` heading. Mangling one character in the FIRST clause makes sed refuse the ENTIRE program on an invalid backreference, so the second clause, which nothing touched, extracts nothing either. Measured against a fixture carrying both spellings and 157 rostered rows: the intact program names 157, the mangled program names ZERO -- not 36, which is what a reader assuming per-clause degradation would predict. One corrupted character silenced a projection it never appeared in. THIRD AND FOURTH SPECIMENS, 2026-09-05, AND THEY ARE WHAT MAKE THIS A CLASS RATHER THAN A FACT ABOUT MERGE DRIVERS: the same mechanism fired twice more within ninety minutes, in a completely different medium, in messages ABOUT this class. An agent messaging body was passed to `dashboard-message --body` as a double-quoted shell argument; the body contained backquoted identifiers; bash substituted them away and the recipients received sentences with phrases missing. One of the two was a message whose SUBJECT was instruments that report success without carrying their claim. The other was sent by the author repairing the first specimen, while writing about the first specimen, roughly an hour after reading the analysis describing it. WHAT THOSE TWO ADD: the class is not a property of emitted artifacts or of merge drivers. It is a property of ANY text handed to a shell-quoted channel, and the agent messaging channel is one of them and is uninstrumented at both ends. THE OBVIOUS DEFENCE IS THE THING THAT FAILED, and a reader will supply the wrong remedy here unless it is stated. `Remember to quote correctly` was in active working memory in both messaging specimens and did not prevent either. The diagnosis in both cases was identical: the protection was attached to a command SPELLING rather than to the hazard -- one command was being single-quoted correctly all evening, and a DIFFERENT command was reached for with double quotes without re-deriving the rule. Vigilance that is bound to a habit does not transfer to the next call site. THE MECHANICAL FORM, which is the operative remedy and is cheap: write the body to a file with a QUOTED heredoc, then pass `"$(cat file)"`. Single-quoting the argument is insufficient, not merely awkward, because prose reliably contains apostrophes and one apostrophe ends the quoting. THE MANGLING IS NOT MERELY LOSSY -- IT EXECUTES, IN THE SENDER'S ENVIRONMENT, AND THAT IS THE MOST UNDER-STATED PROPERTY OF THIS CLASS. Discovered as physical residue rather than by reasoning: the third specimen's message contained the phrase `-> Bool` inside a double-quoted argument, and the shell read it as a COMMAND `-` that was not found, followed by a REDIRECTION that created an empty file named `Bool` in the repository working tree. It sat there untracked for fifteen minutes and was found by `git status`, not by anything watching the channel. The general statement is worse than the instance: a backquoted span inside double quotes is COMMAND SUBSTITUTION, which is arbitrary execution, and an unquoted redirection operator writes files. So the sender's mistake is not that a sentence arrived shortened; it is that a fragment of English prose was EVALUATED AS A PROGRAM against a live working tree. The lossy reading of this class is the benign one. AND IT WAS ONE ROUTINE COMMAND FROM BEING COMMITTED: the author of that instance stages with `git add -A`, so the next commit would have swept an artifact of a mangled chat message into a pull request, with a plausible-looking name and no origin anyone could reconstruct. THE REASON IT DID NOT LAND IS COMMIT ORDERING AND NOTHING ELSE: the last commit happened to precede the corruption by fifteen minutes. Recording that as `it did not land` would be luck reported as safety, which is the reporting failure this ledger exists to refuse -- no mechanism was involved, nothing detected it, and nothing would have blocked it. The file's name resembles a type this corpus uses everywhere, so review would most likely have read it as an intentional stray rather than as residue, and its provenance is unreconstructable from the diff. THE CHECK THIS LICENSES, and it caught something on its FIRST application in a second tree: inspect `git status` for unexplained worktree changes after any session that has sent shell-quoted messages, and do not stage with `git add -A` in a tree where that has happened. Applied immediately by a second author, it found no execution residue but did surface a forgotten STAGED modification to a generated artifact, checked out during verification and left behind -- a different cause with the same shape, on a branch that was main. GENERALIZATION: the hazard is not restricted to mangled messages. ANY command that writes into the worktree as a side effect of verification leaves residue a reviewer cannot distinguish from intent. The heredoc form prevents the cause; the status check catches damage already done, whatever wrote it. THE TRANSPORT'S SUCCESS SIGNAL WAS NEVER ASKED THE RELEVANT QUESTION, and this is the precise form of `no error crosses the boundary`. Successful delivery answers whether the SUBMITTED payload reached its destination. It does not answer whether that payload preserved the INTENDED message. Those are two different claims, and every send in every specimen above was truthful about the first while nothing anywhere was asked the second. A PRESERVATION CHECK MUST BE ANCHORED BEFORE THE DANGEROUS TRANSFORMATION, AND THE OBVIOUS IMPLEMENTATION IS WORTHLESS. The natural control is to digest what was sent and compare it with what arrived. If that digest is computed AFTER the shell has consumed the backquoted spans, it faithfully attests the CORRUPTED payload and reports a perfect match: both sides agree, the comparison is sound, and the defect is invisible, because the damage happened upstream of the first observation. The comparison must STRADDLE the transformation that does the damage -- intended payload captured BEFORE it, against payload received after it -- which is this row's own recognition rule applied to its own remedy. It is the same shape as the neighbouring instruments in this ledger, one layer further out: `comm` cannot report that its own operand was empty, a digest over emitted driver bytes proves an escape was APPLIED but not that it was CORRECT, and a preservation digest taken downstream of the corruption proves only that the corruption was transmitted reliably. QUALIFICATION, so a correct transport does not read as corruption: where a channel LEGITIMATELY transforms encoding or line endings, the comparison needs an explicit preservation contract stating what is allowed to change. THE RECEIVING END FAILS TOO, AND THAT COMPLETES THE PICTURE. Recorded as evidence about DETECTABILITY rather than about the mechanism: in the third specimen a reader received the corrupted message, read it, and did not identify it as corrupted. The missing expressions and an injected directory listing were both present in what they received, and the surrounding prose still supported the conclusions the conversation reached -- so the damage was survivable enough to be invisible, and they flagged only afterwards that those spans should have been treated as corrupt. So there are THREE parties and no signal at any of them: the sender sees intact text, the transport reports success against a question about a different payload, and the recipient reads fluent prose with holes in it. THE CLASS FIRED IN PRODUCTION ON THE BRANCH THAT CLOSES IT, AT EQUAL ROW COUNTS, and this receipt is a discriminating RED on the real acceptance path rather than a fixture. Merging main into that branch reached this driver on docs/design-failure-modes.md. It refused as designed -- zero conflict markers, three index stages, the ours side left clean -- and the repaired step 2, run verbatim, measured: base 126 rows, ours 126 rows, COUNTS EQUAL, with the ours side dropping exactly one row that main had added, `duplicate_record_literal_field_silently_last_wins`, and adding one of its own. A cardinality check passes on that comparison and reports nothing. Only the identity join names the casualty. So the case a reviewer had challenged the branch to cover -- a substitution that preserves the count -- arrived unprompted, on the same projection, within minutes of the controls for it being enrolled; the branch did not have to argue that the case was reachable, because it walked in. RUNG FOUND AT: silent wrongness wherever the subject read by any instrument is the authored string, because there the defect is invisible by construction; mitigatable where a witness reads the EMITTED artifact, which is where gunbc#10460 put it. CEILING: mechanically preventable for media this repository emits into -- a witness that executes the emitted artifact and reads what the reader receives cannot be satisfied by an authored string, and the shell-quoting encodings have single authorities that a hand-spelled call site can be made not to bypass. It stays MITIGATABLE for channels this repository does not own, such as an external tool's argv handling, which is observed rather than proven. NEXT-RUNG TRIGGER: the bash medium read forward from grammar rows, so a printed line's variable references and shell-active characters are DECLARED rather than spelled -- at which point no emitter concatenates a shell literal and the escaping arm has nothing to get wrong. That is the same trigger gunbc.generated_artifact_merge_driver generated_artifact_merge_driver_emit_dissolution_trigger already carries, and it is named here rather than restated because one capability retires both. DISTINGUISHING FACT AGAINST ITS NEAREST NEIGHBOURS. It is NOT `empty_capture_read_as_clean_result`: there a well-formed refusal travels on a stream the reader did not keep, and the reader's error is consuming the emptiness; here the text is rewritten before anyone reads it and the reader's conduct is irrelevant. The empty operand is one CONSEQUENCE of this class, not the class, and the second specimen proves it -- a substituted positional argument is not an empty capture at all. It is NOT `restoration_promise_names_a_route_that_does_not_exist`: there the named route is absent; here it exists and the rendering breaks it.) From f303f43efdea95054a52f004263cac68d7620eeb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 6 Sep 2026 12:22:30 +0000 Subject: [PATCH 17/17] Both halves of review 61363: refuse surplus identities, and stop claiming the join contains the crash Both findings are real and both are mine. ONE, THE COVERAGE WALK ONLY CHECKED ONE END. It returned "nothing missing" as soon as the DISCOVERED list ran out, whatever rows remained, so a report carrying subjects the population does not contain was accepted. The degenerate shape went all the way through: an empty discovered population, one clean reported row, floor 1 reached the no-suspect arm -- because the floor counts REPORTED ROWS, so the guard meant to run before it defeated the floor's own red control. A surplus identity is not a lesser problem than a missing one: it means the rows did not come from this population, so nothing about them is a statement about it. The walk is now a typed three-way -- aligned, missing, surplus -- and CorpusCoverageSurplus refuses with the extra subjects named. Two new witnesses, one per finding, at the exact shapes the review named; both verified discriminating by mutation (neutering the surplus arm reddens both and leaves the truncation red and the positive control green). TWO, THE MITIGATION CLAIM WAS OVERSTATED, and in the one row that may least afford it. corpus_gate evaluates corpus_rows_for_paths to completion before the join runs, so a SIGSEGV during collection kills the process with the join never reached. The join does NOT contain the in-process crash and never could from that position. What it converts is a REPORT SHORT OF ITS POPULATION -- rows assembled across batches after a death, a truncated row set arriving from anywhere. So the row now says which arm climbed: the assembled-report arm reached rung 1, the crash arm is untouched and stays below the ladder, and containing it needs a surviving reporting boundary outside the dying process, which does not exist here and is not claimed. Filing a row about silent wrongness and then inflating its own mitigation is the failure that row exists to name. Caught by reading the code rather than the sentence, which is the right way to have caught it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01H746YJwMsd7HxnijtCFGps --- ...cess_death_rather_than_a_typed_refusal.dag | 4 +- docs/design-failure-modes.md | 2 +- .../corpus_census.dag | 3 + .../corpus_gate.dag | 65 ++++++++++++++++--- .../accumulator_copy_corpus_path_test.dag | 47 ++++++++++++++ 5 files changed, 108 insertions(+), 13 deletions(-) diff --git a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag index 6f8f9d8f092..cd8032c2dcd 100644 --- a/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag +++ b/dag/gunbc/recurring_failure_mode/sweep_terminated_by_process_death_rather_than_a_typed_refusal.dag @@ -27,9 +27,9 @@ data sweep_terminated_by_process_death_rather_than_a_typed_refusal: RecurringFai "NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. ", - "THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ", + "THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. WHAT IT DOES NOT DO, CORRECTED HERE BECAUSE THIS ROW FIRST OVERSTATED IT AND A ROW ABOUT SILENT WRONGNESS MAY NOT CARRY AN INFLATED CLAIM. Review 61363 read the code rather than the sentence: corpus_gate evaluates corpus_rows_for_paths to completion BEFORE the join runs, so a SIGSEGV during collection kills the process with the join never reached. The mitigation therefore does NOT contain the in-process crash and never could from that position. What it converts is a REPORT SHORT OF ITS POPULATION -- rows assembled across batches after a death, a truncated row set arriving from anywhere -- from indistinguishable-from-clean into a typed refusal naming the missing subjects. The crash arm is untouched and stays below the ladder; the assembled-report arm is what reached rung 1. Containing the crash needs a SURVIVING REPORTING BOUNDARY: something outside the dying process that knows the population and observes the absence, which does not exist here and is not claimed. ", - "ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. ", + "ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated report refuses, a right-sized report over the wrong subjects refuses, a report carrying subjects the population does not contain refuses, an empty population with a reported row refuses rather than reading clean, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy every red beside it. The surplus half and the empty-population case were found by review 61363, not by the author: the walk checked only that DISCOVERED ran out, so leftover rows were accepted, and the degenerate shape let the population floor count reported rows instead of the discovered population -- the floor's own red control defeated by the guard that runs before it. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. ", "A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. ", diff --git a/docs/design-failure-modes.md b/docs/design-failure-modes.md index e2f5f2a5228..22ab3a83787 100644 --- a/docs/design-failure-modes.md +++ b/docs/design-failure-modes.md @@ -307,7 +307,7 @@ The landing measurement partitions the 31 parser-visible identities into **2 cit - **a deconfliction plan does not enumerate its writers** (two or more lanes reason about how to avoid colliding on one subject, agree on a scheme, and collide anyway -- because the scheme is a PREDICTION about who will write the subject next, and the predicted population omits a writer. The omitted writer is not an adversary: it is a peer who agreed to stand down and then WROTE ANYWAY -- an earlier revision of this sentence said "and then did not act", which is the opposite mechanism and could not produce the collision the row models, because a writer that does not write collides with nobody. The result merges CLEAN, so the collision is silent. INVALID STATE: a lane holding a deconfliction property that quantifies over a writer population it never discovered. WHY IT SURVIVES REVIEW: the plan is examined for whether it WOULD prevent a collision among the writers it names, and it always would. Nothing in a diff exhibits the writer it omits, and the omitted writer's own change is independently correct. SPECIMEN (main's parse break, 2026-09-04). Four lanes independently authored a repair for one orphaned annotation. Deconfliction ran and SUCCEEDED AS A DISCUSSION: #10414 was named the winner on an earliest-with-proof rule, and the other authors assented. Three of the four then merged anyway (#10425, #10408, #10428), each deleting the same nineteen lines and each adding its own relocated copy, leaving one paragraph standing three times in the module. One copy carried an inverted double negative asserting the opposite of the sentence it was restating. Repaired by removing two copies in #10435. THE ASSENT IS LOAD-BEARING, NOT INCIDENTAL, AND IT IS WHAT BOUNDS THIS CLASS: the population was not unknown and the peers were not uncooperative. Every author was identified, every author agreed, and the scheme still failed -- so the missing thing is not discovery of the writers but an INTERLOCK on them. A class where the colliding writer was never identified is a different and easier problem, because enumerating it is the whole remedy. WHY GIT CANNOT SEE IT, which is the load-bearing mechanism: identical DELETIONS on every side reconcile silently, and each side's ADDITION lands at a different offset, so no hunk overlaps. Three independently correct repairs of one defect merge without a single conflict. A collision that produces a conflict is the benign case; this class is defined by the absence of one. THE SECOND-ORDER TRAP: the ranking conversation consumes the attention that the standing-down would need. Agreement about WHICH repair wins reads as resolution, and it is not -- closing a pull request is the act, and here one of four was closed. Naming a winner is not the losers standing down. NOT TO BE MERGED WITH a_branch_property_falsified_by_a_derived_push, which shares only the SYMPTOM that no conflict is produced so nothing notifies. That symptom has at least three mechanisms -- this one, the derived-push one, and the generated-artifact merge driver refusing with no markers -- so it does not discriminate, and uniting on it is the bare alias section 3 forbids rather than the proven coincidence section 6 requires. The concrete cost of merging them is a grain mismatch section 4b(3) names as its own review tell: this row is genuinely blocked on cross-head knowledge, the other is decidable from a single ref TODAY, and one row carries one stated ceiling because RecurringFailureMode has no ceiling or trigger FIELD -- so the affordable class would be parked behind this one's capability and never rank for a climb it does not need. RUNG: found BELOW the ladder. The duplicated meaning is a silently wrong answer -- every copy parses, every gate is green, and the projection of a triple-landed repair is indistinguishable from a correct one by any executing check. CEILING 2, mechanically preventable, and the reason it is not higher is that the writer population is not decidable from the repository: it includes future human decisions about whether to close a head. What IS decidable is the SUBJECT overlap between open heads. NEXT-RUNG TRIGGER: the capability of resolving an open pull request head to the set of DECLARATIONS it modifies, which is the same Node-tree read the namespace authority already performs and which no instrument currently exposes. A path-grain fence does not discharge it, because this specimen was a single path and the collision was within one declaration's neighbourhood. REVIEW TELL: a lane stating why it CANNOT collide with another lane. The claim is a quantifier over writers, so ask which writers it ranges over and whether an unclosed peer head is inside it. And after a CONTESTED fix lands, re-measure the SUBJECT for duplication rather than re-measuring whether it is green -- green is exactly what triple-duplication looks like. - **a branch property falsified by a derived push** (an author asserts a property about their OWN branch -- most often "this change touches only the authority and never its generated projection" -- and an automated actor then DERIVES the projection onto that same branch after CI. The asserted property was true of the commit the author wrote and false of the branch a reader inspects. This is single-party: no competing author exists and none was omitted. INVALID STATE: a published claim about a ref's contents that the ref's own automation can falsify after publication, with nothing that re-checks or withdraws the claim. WHY IT SURVIVES REVIEW: the claim is TRUE when written and true at review time if review reads the author's commit. The falsifying commit is authored by CI, is independently correct, and arrives after the human attention has moved on. Nothing notifies, because deriving a projection produces no conflict. SPECIMEN (PR #10417). The scheme was authority-only editing: touch gunbc.recurring_failure_mode and never docs/design-failure-modes.md. Heal generated the projection onto the branch anyway -- d695b3ab63e is a heal commit adding docs/design-failure-modes.md on top of the author's authority-only commit. The collision property is a fact about the branch AFTER CI, never about the commit the author wrote. Confirmed recurrent rather than incidental, and the OBVIOUS INSTRUMENT FOR THAT IS CONTAMINATED IN THE DIRECTION OF THE CONCLUSION, which is why the count is stated with its discriminator. Selecting heal commits by AUTHOR does not work in this repository: every session commits under the identity gunbc-ci-auto-heal, so that author matches 161 commits touching docs/design-failure-modes.md, of which most are ordinary session work. Heal is separated by its exact commit SUBJECT instead -- "chore: regenerate drifted generated artifacts (ci auto-heal)" -- which yields 54 on that path, against a negated-subject control of 253 that establishes the filter is not itself the answer. THE DISCRIMINATOR THAT BOUNDS IT, which is what makes the row usable rather than a warning: it bites authority-only changes on carriers that HAVE a projection. A sibling lane's authority-only PR on gunbc.guarantee_stall drew zero heal commits, because no generated artifact consumes that carrier. Check whether the carrier has a projection before relying on the property. NOT A WRITER-POPULATION PROBLEM, and this is the distinction that keeps it separate from a_deconfliction_plan_does_not_enumerate_its_writers: no other author is involved, none is omitted, and the author predicted nothing about anyone else. Heal does not COMPETE for the subject -- it derives an artifact from what the author already wrote. Counting it as a member of a writer set would widen that set to "anything that can append a commit", which includes the author's own CI, and the concept stops discriminating. What the two share is only the SYMPTOM that no conflict is produced so nothing notifies -- a symptom with at least three mechanisms, the third being the generated-artifact merge driver refusing with no markers. RUNG: found BELOW the ladder. A public claim silently goes false and no typed refusal fires anywhere. CEILING 3, structurally guaranteed, and REACHABLE WITHOUT NEW CAPABILITY -- which is the whole reason this is its own row. The predicate is single-ref and decidable today: whether the ref changed after the revision the claim was asserted against is answerable from that one ref's history, needing no cross-head enumeration, no open-head discovery and no declaration resolution. Filing it with the writer-population class would state one ceiling for both, park this affordable half behind that class's unbuilt capability, and reproduce exactly the grain mismatch section 4b(3) names as its own review tell. NEXT-RUNG TRIGGER: the capability of binding a published branch claim to the revision it was asserted against, so the claim carries its own subject and a later derived push makes it re-adjudicable rather than merely stale. The affordable intermediate is a check that refuses when a claim about a path set is read at a ref whose head postdates the asserted revision. REVIEW TELL: an author explaining that their change CANNOT collide because of which paths it touches. Ask whether the carrier has a generated projection, and whether the property is being asserted about the commit or about the branch a reader will actually fetch. - **a roster is its own denominator, so the instrument reading it cannot notice the roster is short** (INVALID STATE: a report enumerates the rows a hand-authored roster happens to hold, renders a verdict per row, and presents the result as an answer ABOUT THE SUBJECT -- while no independent authority states how many identities that subject should have, so a subject missing rows renders as a complete block and reads as a complete answer. HARM: every way a roster can lose a row -- a partial edit, a bad merge, a branch mix-up, a deletion nobody reviewed, a row never added -- produces a receipt indistinguishable from the whole truth. The receipt is not wrong about the rows it names; it is silent about the ones it was never asked. And because the silence is total, the reader's only defence is to already know the answer, which is exactly what they consulted the instrument to learn. THE SPECIMEN, found by execution and by an author's own mistake rather than by review (2026-09-04, import-cut lane): gunbc.namespace_cut_subject_observation subject_roster_report was run to establish that the namespace cut's probe identities are PRESENT before the cut. It rendered the ResolverImportNameUniverse subject as three PRESENT rows and stopped. Three further probes belonging to that same subject existed on a different branch and were absent from the tree that ran. The block was well-formed, fully green, and short by half, and nothing in the output distinguished it from the complete subject. The defect was caught only because the author independently remembered that three more probes should be there. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked rather than assumed, because three neighbours are close enough to be tempting. It is not `selection_view_read_as_population`: that class turns on membership computed from a MEASURED quantity, so the set moves when the instrument or the load moves. Nothing here is measured -- the roster is hand-authored and perfectly stable -- and the set moves only when an author edits it. The defect is that the authored set is UNJOINED to any statement of what it should be, not that a threshold slid. It is not `check_subject_narrower_than_its_declared_claim`: there, an implementation ranges over less than the claim it is cited for, and the claim exists to be compared against. Here there is NO independent claim about cardinality at all -- the roster is simultaneously the subject definition and the evidence of its own completeness, which is why the comparison that would expose the gap has nothing to stand on. That class is a gap between two statements; this one is the absence of the second statement. It is not `green_reported_over_a_population_the_instrument_does_not_own`: that instrument denominates a real population correctly and merely does not own all of it. This instrument denominates NOTHING -- it has no population, only a list. RUNG FOUND AT: outside the ladder, and this is the honest placement rather than a low one. The gap is invisible from the instrument's own result for the same reason a wall over a clean corpus is green, so there is no executed evidence that could currently discriminate a complete subject from a short one. Naming it mitigatable would be inflation, because nothing contains the harm: the receipt is consumed as though it were total. CEILING: structurally guaranteed. Whether a subject's probe set is complete is decidable ONCE an independent authority states the expected identities, and that authority is derivable rather than hand-countable -- which is what keeps this off the ratchet-forever arm and out of a numeric-literal oracle. A literal expected-count per subject would be the count equality DESIGN section 5 forbids: completeness is an identity join. TRIGGER, FIRST FORM, AND WHY IT WAS UNDER-SPECIFIED -- recorded rather than silently replaced, because the correction is the instructive part. The row first named the trigger as: an independently derived statement of each subject's expected identities, joined at identity grain, refusing a subject the roster does not cover. That is a real climb and it is NOT the structural one. Two lists plus an exact set-equality gate reaches MECHANICALLY PREVENTABLE and stops there, because BOTH lists stay writable and an omission is still an edit the two can share -- a single author dropping a subject from the expected list and the roster in one change satisfies the gate perfectly. Naming a second authority as the trigger for a STRUCTURAL ceiling was the artifact-shaped trigger this ledger warns about, committed inside the row that exists to warn about denominators. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the operational roster is DERIVED as a TOTAL PROJECTION from a closed expected-root coproduct -- so every expected root has a constructor, the projection is exhaustive over it, and a subject omitted from the roster has no way to be written rather than merely being caught. At that point the roster cannot disagree with its denominator because it is not a second list; it is the denominator viewed through a total function. The set-equality gate remains legitimate as the RUNG-2 step on the way there, and it must not be reported as the ceiling reached. WHY IT IS URGENT RATHER THAN INTERESTING: this instrument sits inside the exit criterion of the namespace cut. The final head is to be accepted on its testimony that both import authorities are ABSENT -- and absence is precisely the verdict this defect cannot distinguish from not-having-been-asked. A short roster at the final head would report the cut complete. The class must climb before that receipt is cited, not after. SECOND SPECIMEN, AND IT SHOWS THE CLASS DOES NOT NEED A MISSING ROW -- A ROW THAT STOPPED NAMING ANYTHING IS ENOUGH. v2.test.claim.complexity.accumulator_copy_roster_gate enumerates nine paths for the copied-accumulator lens to analyse. One of them, src/v2/workflow/glob_discovery_law.dag, moved to src/v2/test/workflow/ in the #9637 directory reorganisation and the roster row was not repointed. The roster is its own denominator here in the sharpest way available: nothing states which of the corpus the nine are supposed to stand for, so a row that decayed into naming nothing is indistinguishable from a row that is doing its job, and the gate being operator-ruled OFFLINE meant no consumer executed it to find out. AND THE FAILURE ARM MADE IT WORSE THAN SILENT -- IT WAS ABSORBING. The subject read went through the filesystem_read intrinsic, whose result type carries content and no success channel, so the missing path raised an interpreter type error and ended the process. At roster grain that reads as one red test; at corpus grain every subject after the missing one is never asked, which is the same silence this row describes arriving through a loud door. Repaired by folding the read through extdeps.filesystem.filesystem_io filesystem_read_outcome and giving the analysis-standing coproduct a SourceUnreadable arm, so an unreadable subject is one typed counted row and the sweep continues; the discriminating control is v2.test.claim.complexity.accumulator_copy_unreadable_subject, which on the pre-fix build fails with a runtime error rather than a false. WHAT THE REPAIR DOES NOT DO, stated so the specimen is not read as a climb it did not earn. Repointing a stale row and typing the read leave the roster a hand-authored list with no denominator. The population question is answered separately by v2.lens.complexity_accumulator_copy.corpus_gate, whose subjects come from the module index rather than from an author -- the derivation the trigger above asks for, at the one grain where a total projection was already available. It does not retire the trigger: the corpus consumer is priced out of every cadence by interpreted ingest, so the roster is still what executes. RECOGNITION RULE: when a report's rows come from a hand-authored list, ask what would happen if a row were deleted -- if the answer is 'the report renders one fewer row and says nothing', the list is acting as its own denominator and the report's completeness is an unstated assumption rather than a checked fact.) -- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. It does not fix the crash and is not meant to; it converts a run that died at subject k from indistinguishable-from-clean into loud and counted. The invalid state stays entirely writable, which is why this is rung 1 and the ceiling above is unchanged. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated sweep refuses, a right-sized report over the wrong subjects refuses, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy both reds. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) +- **a corpus sweep ends by process death, so it reports less than it measured and looks like it stopped for a reason** (INVALID STATE: an instrument folds over a population one subject at a time, and the process is killed by the host -- SIGSEGV from unbounded native recursion in the interpreter -- partway through the fold. DESIGN section 5 admits two arms, succeed fully or fail with a typed located diagnostic, and this is neither: the run produces no cause, no locus and no count, because the code that would have produced them is not running any more. HARM, AND IT IS THE SELF-CONCEALING KIND. Nothing can enroll the class, because there is no diagnostic to key on; nothing can attribute it, because there is no locus; and nothing can bound it, because there is no count of what was skipped. Worse than either, the survivors are indistinguishable from a complete answer at the only grain anyone reads: a sweep that died at the twentieth of five thousand subjects and a sweep that finished clean both present as a list of findings and an absence of alarms. Whoever reads it learns that no suspect was found, which is true, and infers that the population was examined, which is false. The exit status is the only channel carrying the difference, and it is the one channel a person reading a report does not see. THE SPECIMEN (2026-09-06, complexity-lens lane): v2.lens.complexity_accumulator_copy.corpus_census census_for_paths over a stratified sample of the tracked corpus dies with SIGSEGV around the twentieth subject, reproduced twice. The subject it dies ON analyses cleanly when handed to the same instrument alone, so the cause is accumulation across subjects within one process and not any one subject's shape -- which is what makes it a property of the SWEEP rather than of a datum, and therefore invisible to every per-subject control. WHY IT IS NOT ONE OF THE ROWS BESIDE IT, checked at identity grain over gunbc.recurring_failure_mode.roster rather than by memory. It is not `absorbing_fallback`: that class has a failure arm that WIDENS -- it computes a superset and calls it an answer, so the deficit is concealed by a value the program chose to produce. Here no arm executes at all. The two are opposite ends of the same obligation: an absorbing arm answers when it should refuse, and this one cannot reach the point of refusing. A repair for one does nothing for the other, because a typed cause added downstream of the death is never reached. It is not `absent_reads_identically_to_never_looked`, which is the nearest and the tempting one. There the instrument RAN over its whole subject and the absence of a record is what carries no distinction. Here the instrument did not run over its whole subject, and the missing records were never attempted. That difference decides the repair: the other class is fixed by recording the looked-at set, and doing that here records a set that is itself truncated by the same defect -- the receipt would be short and would say so about nothing. It is not `roster_is_its_own_denominator`: that class is a hand-authored list unjoined to any statement of what it should cover. This population IS derived -- corpus_gate discovers its subjects from the module index -- so the denominator is known and correct, and the defect is that the fold over it terminates early. A correct denominator makes this class MORE legible, not less: it is what would let a survivor count be compared against an expected one, which is the mitigation available before the wall lands. RUNG FOUND AT: below the ladder. This is not the bottom rung, it is silent wrongness, which DESIGN section 5 places outside the ladder and forbids outright -- a run that dies is consumed as a run that finished, and no modeled machinery contains the harm. Calling it mitigatable would be inflation: nothing today bounds, counts or types it. CEILING: structurally guaranteed, and the reason it is not merely mitigatable is that the property is decidable rather than a fact about external reality. Native stack exhaustion in the interpreter is not an undecidable predicate about the analysed program; it is the absence of a bound on the interpreter's own recursion. A depth-bounded or explicitly-stacked walk converts an unbounded native recursion into a typed refusal that carries its locus and its depth, which is a wall available now with no missing grounding. It is not structurally impossible, because the walk depth remains representable -- the refusal is derived, not constructed away. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: the interpreter's node walk carries an explicit stack or an explicit depth bound, so exceeding it raises a located typed diagnostic instead of terminating the process. It must be SUFFICIENT FOR the general case rather than for the specimen: any traversal deep enough to exhaust the native stack refuses with a cause and a locus, whichever entry point reached it -- not a per-instrument guard around census_for_paths, and not a larger stack, which moves the cliff without typing it. A batching driver for one sweep CONTRIBUTES to living with the class and does not retire it. THE INTERIM MITIGATION IS BUILT, AND IT IS NOT THE CLIMB. Getting from OUTSIDE the ladder onto rung 1 is not the same move as climbing 1 to 2, and it is not optional: silent wrongness is forbidden outright, so a mitigation that ends the silence is owed as soon as the class is classified honestly -- an honest classification that changes nothing about what gets built is rung inflation wearing the opposite costume. v2.lens.complexity_accumulator_copy.corpus_gate corpus_gate_disposition_over decides COVERAGE before any other arm: the discovered population and the reported rows are walked in lockstep and the first divergence ends the covered prefix, so a report that covers less than the population it names refuses with CorpusCoverageIncomplete carrying the missing subjects BY NAME. It is an identity join and not a count equality, which is the difference between catching a truncated sweep and catching only some of them: a batch re-run after a death reports the right NUMBER of rows over the wrong subjects. WHAT IT DOES NOT DO, CORRECTED HERE BECAUSE THIS ROW FIRST OVERSTATED IT AND A ROW ABOUT SILENT WRONGNESS MAY NOT CARRY AN INFLATED CLAIM. Review 61363 read the code rather than the sentence: corpus_gate evaluates corpus_rows_for_paths to completion BEFORE the join runs, so a SIGSEGV during collection kills the process with the join never reached. The mitigation therefore does NOT contain the in-process crash and never could from that position. What it converts is a REPORT SHORT OF ITS POPULATION -- rows assembled across batches after a death, a truncated row set arriving from anywhere -- from indistinguishable-from-clean into a typed refusal naming the missing subjects. The crash arm is untouched and stays below the ladder; the assembled-report arm is what reached rung 1. Containing the crash needs a SURVIVING REPORTING BOUNDARY: something outside the dying process that knows the population and observes the absence, which does not exist here and is not claimed. ITS EVIDENCE DOES NOT RETIRE ON THE CLIMB. Three witnesses in v2.test.claim.complexity.accumulator_copy_corpus_path_test execute on every push: a truncated report refuses, a right-sized report over the wrong subjects refuses, a report carrying subjects the population does not contain refuses, an empty population with a reported row refuses rather than reading clean, and a complete report over the same population reaches its ordinary verdict -- the positive control without which a guard that refused everything would satisfy every red beside it. The surplus half and the empty-population case were found by review 61363, not by the author: the walk checked only that DISCOVERED ran out, so leftover rows were accepted, and the degenerate shape let the population floor count reported rows instead of the discovered population -- the floor's own red control defeated by the guard that runs before it. Verified discriminating by mutation rather than by reading: neutering the join to answer 'nothing missing' turns both reds red and leaves the control green. When the depth-bounded walk lands, these stay enrolled as the regression control that the higher rung is real. A NEIGHBOUR THAT LOOKS LIKE THIS CLASS AND IS ITS OPPOSITE, recorded because the confusion is available to anyone reading a floor log and was made once already. The required floor reports interrupted_before_verdict when a witness crosses the CPU deadline v2.workflow.required_floor required_floor_claim_cpu_safety_limit_ms declares -- named rather than transcribed, because that module's own prose carries a stale ten-fold figure for it in nine places -- and 'the run stopped partway and the identity has no verdict' is superficially this row's sentence. It is not this class and the population does not widen to cover it. The floor's preemption is a TYPED, LOCATED, COUNTED refusal -- raised_by=cpu_deadline, the measured CPU against the declared line, the identity named, the totals carried as interrupted_cpu_deadline and interrupted_wall_deadline, and the lane BLOCKS on it -- which is precisely the arm DESIGN section 5 asks for and precisely what this class lacks. The distinguishing question is not whether a run ended early; it is whether ending early produced a cause, a locus and a count, or produced silence that reads as completion. A budget preemption is the floor working; a SIGSEGV is the floor having nothing to work with. RECOGNITION RULE: when an instrument folds over a population in one process, ask what its output looks like if the process dies at subject k. If the answer is 'the same shape as a complete run, one page shorter', the sweep has no completion channel and its report is an unstated assumption about its own exit status.) - **a repair job reports failure for reasons that are not about the thing it repaired, and every reader attributes it to the change** (INVALID STATE: a CI job whose purpose is to REPAIR a branch -- regenerate a projection, apply a fix, push the result -- exits non-zero for a reason belonging to the repair PROTOCOL rather than to the content under review, and the only channel most readers have is the job's red square beside the pull request. The red is true about the job and false about the change, and nothing in the rendering distinguishes the two. HARM: misattribution, priced in diagnosis cycles. A red that names the wrong cause sends its author to look at their own diff, and it is indistinguishable at the grain people read -- a job name and a conclusion -- from a genuine drift that the author really did cause. Worse for a bot-authored repair: the author sees a failure on a branch they just pushed to and reasonably concludes they broke it. The cost is paid per reader per occurrence and is invisible in the log, because the log is fine. TWO ARMS OBSERVED, AND THEY ARE NOT THE SAME MECHANISM. The distinction is recorded because the second was mistaken for the first once, by the author of this row, and the second is the more interesting half. ARM ONE, THE LOST RACE (2026-09-06, run 34026632467): heal-generated-artifacts regenerated correctly and its push was rejected non-fast-forward because the human author had pushed to the same branch while the job was building. The repair was right; the branch had moved. This arm is a genuine race and it produces a red that is about scheduling. ARM TWO, THE DESIGNED SUPERSEDE (2026-09-06, run 34027083483): the same job SUCCEEDED -- HealProduced, prior_head fc576da5a0f, healed_head f2d00e6db4c, artifact pushed -- and then exited 1 with `SupersededByHealedHead ... revalidation-required`. That is not a race and not a defect: the protocol deliberately refuses to report green for a head that no longer exists, and requires the revalidation run it just dispatched. Every step worked exactly as designed and the pull request shows a failing job. This arm cannot be fixed by retrying, because there is nothing wrong. WHAT THE POPULATION IS, STATED BECAUSE THE FIRST DRAFT OF THIS ROW OVERSTATED IT. One race and one designed supersede, both on one branch on one afternoon. The author reported them to a reviewer as 'lost the race twice' before reading the second log, which is the same co-occurrence-implies-common-cause inference this ledger records elsewhere. Recurrence for arm one is NOT established by these receipts; arm two recurs by construction, once per heal that lands. WHY IT IS NOT `a_written_row_is_not_a_firing_mechanism` OR ITS NEIGHBOURS, checked at identity grain. The mechanism here fires correctly and reports correctly about ITSELF; what fails is the JOIN between a job-level conclusion and the question a reader is asking, which is about the change. It is also not the absorbing fallback: nothing widens and nothing is silently accepted -- the failure is loud and typed (`SupersededByHealedHead` names its cause, its prior head and its healed head). The defect is that a true statement about the repair is consumed as a statement about the subject. RUNG FOUND AT: mitigatable. The harm is contained by the fact that both arms leave a legible log line naming the real cause, so a reader who opens the job learns the truth; what is missing is that the summary grain carries the distinction at all. CEILING: mechanically preventable, and deliberately not higher. Whether a given exit belongs to the repair protocol or to the subject IS decidable -- the job knows which branch it took -- so a job can report the two as different outcomes. It cannot reach structural guarantee, because the reader's inference is not a modeled artifact: no compiler refuses a person who misreads a red square. The wall available is at the reporting boundary, not in the reader. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY: a repair job's protocol outcomes are reported in a channel DISTINCT from its subject verdict -- so 'I could not land my repair' and 'the content I checked is wrong' are different states at the grain the pull request renders, not two spellings of one red. Sufficient for BOTH arms: a retry-with-rebase would end arm one and leave arm two reporting red on every successful heal, so a fix that only handles the race does not retire this row. The cheaper half is available now and already ends the misattribution: refuse with a typed cause that says which it is. RECOGNITION RULE: when a job that MODIFIES a branch reports red, ask whether its own log says the modification succeeded. If it does, the red is about the protocol and the change is unjudged -- and if the job's name does not say so, every reader will spend a cycle learning it.) - **an instrument is asked about a subject outside its domain and ANSWERS ANYWAY** (INVALID STATE: a query ranges over a domain narrower than the subject it is being read as covering, and a subject falling outside that domain does not come back as UNKNOWN or as a refusal -- it comes back as a confident, well-formed, WRONG answer, whose direction is set by which way the domain gap happens to point. Deletion, modification, or silence, depending on the gap rather than on the subject. HARM: the wrong answer is manufactured work or a manufactured accusation. It is not a missed defect -- it is a defect that does not exist, reported with the authority of a command that ran successfully and exited zero. Downstream, someone debugs a branch nobody tested, reverts a change nobody made, or re-derives a figure that was already correct. TWO RECEIPTS, ONE TREE, ONE DAY, AND EACH INSTRUMENT WAS CORRECT EXACTLY WHERE THE OTHER FAILED (2026-09-04, import-cut lane). FIRST: `git diff --name-only ` was used to derive which paths had changed since a backup ref. It ranges over TRACKED content, so an untracked file -- present in the ref, absent from the index -- rendered as `deleted file mode 100644`, 38 deletions, for a file that was on disk and byte-identical to the ref. The report would have been that a ledger row had been deleted. SECOND: a per-path content comparison was built specifically to escape that tracked-content blindness, hashing every worktree path against the ref. It followed SYMLINKS. Git stores a symlink as mode 120000 whose blob content is the TARGET STRING, so `CLAUDE.md` and `README.md` -- both the literal string `DESIGN.md`, both the same blob -- compared unequal against the hash of DESIGN.md's contents. The report would have been that the design document had been modified, under a hold that forbade tree changes. THE PAIR IS THE FINDING, NOT EITHER HALF. The second instrument was built to fix the first, and it acquired its own blind spot at precisely the place the first was correct: git diff was wrong about untracked files and right about symlinks; the hash loop was right about untracked files and wrong about symlinks. THERE IS NO SAFE INSTRUMENT TO SWITCH TO. Replacing a tool that has a known domain gap with one that does not share it exchanges the gap for a different gap, and the new one is unmapped precisely because it is new. THIRD SPECIMEN, AND THE ONLY ONE WHERE THE INSTRUMENT WAS A PROCEDURE RATHER THAN A TOOL. A reviewer specified a registration check for a newly minted ledger row: confirm `grep -c ` returns 2 in the roster, confirm the row file exists, confirm the projection join is clean. Every one of those checks passed. The merge that satisfied them was a roster conflict in which two lanes had appended to the same list, and a resolution taking only the author's side would have passed ALL THREE while silently deleting two rows the other lane filed that day. The check's domain was THE AUTHOR'S OWN ROWS. Asked about the roster -- a shared, append-only carrier -- it answered confidently and wrongly, because a procedure that verifies your additions cannot see another party's deletions. The repair is the same shape as the tool cases: the union resolution was verified in BOTH directions, the other lane's identities as well as the author's. Worth carrying because a hand-written procedure feels domain-free in a way a tool does not, and it is not: every check ranges over something, and the thing it ranges over is usually what its author was thinking about. THE CONSEQUENCE THAT MAKES THIS THE SHARPEST OF THE THREE: a domain gap in a hand-written verification procedure is INDISTINGUISHABLE FROM CORRECTNESS BY ANY CHECK INSIDE THAT PROCEDURE. A tool's gap can at least be found by reading its documentation; a procedure's gap is bounded by what its author had in mind, and the procedure contains no record of what that was. WHY IT IS NOT `empty_capture_read_as_clean_result`: that class is an EMPTY capture consumed as a finding of nothing -- zero bytes on a redirect, a log that reads zero with no refusal. Every instance on it is about reading NOTHING. Both instruments here returned rich, plausible, specific content. Nothing was empty; the content was about the wrong subject. WHY IT IS NOT `check_subject_narrower_than_its_declared_claim`: there the harm is a false GREEN -- coverage smaller than the claim it is cited for, so a real defect passes unseen. Here the harm is a false FINDING, a defect reported where none exists. Opposite direction, opposite consequence, and the repairs do not transfer: widening coverage does not stop a narrow instrument from confidently answering outside itself. RUNG: outside the ladder. Neither near-miss was caught by a mechanism. Both were caught because a human found ONE ROW SURPRISING and investigated it -- and in both cases the fallback defence was the reader already knowing the answer, which this ledger criticises elsewhere and which is therefore not a defence that may be cited here. There is currently no executed evidence that would discriminate a domain-gap answer from a true one. CEILING: mechanically preventable, and stated as rung 2 rather than reaching for higher, because the domain of an ad-hoc query is not in general decidable from the query. What IS decidable is whether the subject population and the instrument population agree, so the honest ceiling is a gate that compares them and refuses on any subject the instrument cannot range over. NEXT-RUNG TRIGGER, NAMING THE CAPABILITY AND NOT THE TWO SPECIMENS: the class closes when an instrument CANNOT ANSWER AT ALL until it has established a DECLARED DOMAIN -- its admitted subject kinds, its acquisition population, and its identity semantics -- and the requested subject carries a TYPED RELATION to that domain: SubjectWithinDomain, or InstrumentOutOfDomain { uncovered_subjects }. From the second, NO count, delta, deletion, modification, absence or zero is constructible. Fixing git diff and making a hash loop symlink-aware would discharge nothing: those are the two gaps we happen to have met, and the pair above is the receipt that the next instrument has the next gap. WHAT THE DOMAIN MUST SPEAK ABOUT, for the file-population instruments this repository actually uses: tracked, untracked, deleted, renamed, regular file, SYMLINK-AS-LINK-IDENTITY, symlink target, and generated or ignored where relevant. A census need not ADMIT every one of those kinds -- it must REFUSE when asked about a kind it does not admit. A TWO-WAY IDENTITY JOIN IS NECESSARY AND NOT SUFFICIENT, which is the subtlest part of this class and was found only by pushing on it. A two-way join closes substitution and unilateral omission. It does NOT close a shared domain gap: if the real subject contains X, the acquisition omits X because X is untracked, and the EXPECTED side is derived through that same acquisition, then expected omits X, observed omits X, and the two-way join reports EQUAL while the class survives untouched. So an expected-identity authority must be independent in TWO dimensions -- at IDENTITY grain and at ACQUISITION grain. An expected set projected from the same scan that produces the observation is not independent; it is the same blindness counted twice. SCOPE, STATED SO THIS ROW CANNOT BECOME AN ABSORBING BLOCKER: the obligation is NOT that every instrument in the repository proves its domain. It is that every instrument whose answer is CONSUMED by a program relying on this class must establish its domain before its verdict is admitted, and that population is derivable from the consumers. The honest immediate climb for those routes is OutsideTheLadder to MechanicallyPreventable; structural impossibility across the repository is not being claimed. THE COROLLARY FOR ANY DELETION CENSUS, which is where this class does the most damage: a corpus-deletion census has an IMPLICIT DENOMINATOR -- every source entity capable of belonging to the subject. If its acquisition is git-tracked files, then an untracked source is NOT ABSENT, it is NOT OBSERVED, and the census reports the deletion as complete. Where symlinks occur in the population, the census must decide WHICH FACT it claims: repository object identity, repository path identity, link target bytes, or resolved contents. Those are four different subjects, and a verification that had to be made symlink-aware after the fact is the evidence that 'same contents' was ambiguous until link semantics were fixed. WHY THIS ROW EXISTS BEFORE ITS CENSUS RUNS: the namespace cut's own machinery is delta instruments end to end -- a corpus delta, a census worked to zero, an exit criterion joining two populations. Each is a place a domain gap returns a confident wrong COUNT rather than a refusal, and today's two receipts are the evidence that reading the output does not reveal it. Filed before the census, this is a class the census can be designed against; filed after, it is a postmortem. RECOGNITION RULE: when an instrument reports a change you did not make, do not reconcile around it and do not reach for a different tool -- ask what population the query actually ranged over, and whether the surprising subject is inside it. A surprising row is the only signal this class emits, and it emits it exactly once, before the wrong answer becomes a report.) - **a check's discriminating RED is authored as rows in the LIVE SUBJECT LIST the check reads** (the probe is genuine, the arm it exercises is genuine, and the expected verdict is a refusal -- so every ordinary run of the instrument refuses about one of its own subjects, and the instrument's answer over the real population is knowingly wrong by construction). NOTHING LOOKS WRONG AT THE AUTHORING SITE, which is what separates this from ordinary bad data. The evidence is correct, the arm is correct, the annotation explaining why the rows are deliberately wrong is correct, and the author can defend every one of those facts individually. The defect is not the evidence but its HOME: it is the exact inverse of DESIGN section 4b's boundary, which locates an authorable invalid state in SOURCE HANDED TO THE COMPILER BY A FIXTURE. A fixture population is disposable and private to the check; an operational population is what the instrument testifies about. THE MECHANISM THAT HIDES IT IS THE MISSING VERDICT, and this is the sharp part. The contamination is invisible exactly IN PROPORTION to how badly the instrument reports: an instrument that cannot refuse on its own uncertainty emits the honest per-row word -- UNOBSERVABLE -- into a report nobody adjudicates, and exits 0. So the report is truthful and the verdict is not, and no gate anywhere can see the difference. Strengthening the verdict is therefore what SURFACES the class rather than what fixes it, and a lane that adds the refusal without moving the evidence will simply start failing on its own fixtures. THE DETECTOR IS A QUESTION ABOUT INTENT, ASKED STRUCTURALLY: does any row in the operational population have an EXPECTED verdict that is a refusal? If yes, the population is carrying evidence and the instrument's answer over it is not a measurement. It does not require reading the prose -- an annotation explaining that rows are deliberately wrong is itself the tell, because a correct operational row needs no such explanation. SPECIMEN, gunbc#10454, and it is the reason this row exists. gunbc.namespace_cut_subject_roster carried two HostRustItemProbe rows citing src/v1/stage0/src/cli_run.rs for build_import_adjacency and import_closure_from_adjacency, both of which had MOVED to src/v1/stage0/src/cli_run/entry_resolve.rs. They were retained deliberately, with an annotation, as the discriminating RED for the stale-citation refusal arm in gunbc.namespace_cut_subject_observation host_probe_over_items -- an arm that is real and whose red is real. But gunbc.namespace_cut_subject_observation subject_roster_report wrote its report and returned ExitSuccess without ever inspecting whether any probe was Unobservable, so every pre-cut run of the cut's own exit-criterion instrument reported that it could not observe two of its subjects and exited 0. codex review 60319 named it. The repair was all three at once: correct the citations, move the RED to test.claim.namespace_cut_stale_citation_witness where host_probe_over_items was already parameterized on its item population for exactly that purpose, and make the command refuse on any Unobservable probe. IT GENERALISES PAST INSTRUMENTS to any check whose regression probe lives in the population it polices -- a lint whose failing case is committed source, a migration census whose expecting-red subject is a live row, a conformance suite whose counterexample is an enrolled module. Wherever the check's denominator and its evidence are the same set, the check's answer over the real population is off by its own evidence, and the report cannot say so. Rung found at 1: the harm was contained only because a reader adjudicated a report the command had already greened. Rung after gunbc#10454 is 2, mechanically preventable -- the command now refuses on any Unobservable probe, folded from the observation coproduct with no catch-all arm, so a new verdict variant fails to compile rather than defaulting to not-a-problem; but the invalid state is still WRITABLE and safety depends on that refusal executing and staying enrolled. CEILING 4, structural impossibility, and it is genuinely reachable because the distinction is decidable at construction: an operational subject roster and a fixture population are different kinds, and only one of them may carry a row whose expected verdict is a refusal. NEXT TRIGGER, capability and not artifact: a probe-population type distinguishing operationally-asserted subjects from fixture-authored ones, SUFFICIENT FOR a probe whose expected verdict is a refusal to have NO CONSTRUCTOR in an operational roster. The fixture file that discharged this specimen is not the trigger -- it relocated one instance and leaves the next roster equally able to accept evidence rows. diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag index b2c42dfd305..79e5248968e 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_census.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_census.dag @@ -8,6 +8,7 @@ import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusGateDisposition, CorpusNotEstablishedObserved, CorpusCoverageIncomplete, + CorpusCoverageSurplus, CorpusPopulationBelowFloor, CorpusReport, CorpusSuspectObserved, @@ -237,6 +238,7 @@ fn corpus_subtree_gate_reports_its_unreadable_population() -> Bool { CorpusSuspectObserved { paths: _, sites: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => false } } @@ -389,5 +391,6 @@ fn a_no_suspect_verdict_still_carries_its_refusal_census() -> Bool { CorpusNotEstablishedObserved { rows: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => false } } diff --git a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag index ba3639f6e3d..ed88b966621 100644 --- a/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/corpus_gate.dag @@ -253,8 +253,14 @@ fn corpus_report(pool_roots: List) -> CorpusReport { // (SIGSEGV out of the interpreter's unbounded native walk), and what survives is a report in the // same SHAPE as a complete one, one page shorter. That class sits BELOW the ladder, which DESIGN // section 5 forbids outright rather than admitting as a low rung, so it does not wait for its -// trigger: this comparison does not fix the crash and is not meant to, it ends the silence, which -// is the forbidden half. The real climb -- a depth-bounded interpreter walk that refuses with a +// trigger: this comparison ends a silence, which is the forbidden half. +// +// BUT NOT THE CRASH'S SILENCE, AND THE DIFFERENCE IS EXACT. corpus_gate evaluates +// corpus_rows_for_paths to completion before this join runs, so a death DURING collection takes the +// process out with the join never reached (review 61363). What this refuses is a report that covers +// less than the population it names -- rows assembled across batches after a death, or a truncated +// row set arriving from anywhere. Containing the crash itself needs a surviving reporting boundary +// outside the dying process; that does not exist here and is not claimed. The real climb -- a depth-bounded interpreter walk that refuses with a // locus -- is named in that row, and when it lands these witnesses stay as the regression control // that the higher rung is real rather than retiring with the mitigation. // @@ -290,14 +296,44 @@ fn corpus_report(pool_roots: List) -> CorpusReport { // trigger clears, the row to write first is not another join case -- it is one witness that // discovers a small real population and checks the produced rows against it at identity grain. -fn coverage_missing(discovered: List, rows: List) -> List { +// BOTH SEQUENCES MUST END TOGETHER, AND THE FIRST CUT OF THIS WALK ONLY CHECKED ONE OF THEM. +// Review 61363 found it: running out of DISCOVERED paths returned "nothing missing" whatever rows +// were left over, so a report carrying subjects the population does not contain was accepted, and +// the degenerate case went all the way through -- an empty discovered population with one clean +// reported row and a floor of 1 reached the no-suspect arm, because the floor was counting REPORTED +// ROWS rather than the discovered population. That is the population-floor red control defeated by +// the guard that was supposed to run before it. A surplus identity is not a lesser problem than a +// missing one: it means the rows did not come from this population, so nothing about them is a +// statement about it. +fn row_path_list(rows: List) -> List { + list_reverse(xs: fold_list( + xs: rows, + empty: Empty, + cons: fn(acc, row) { Cons { head: row.path, tail: acc } } + )) +} + +type CoverageWalk + = WalkAligned + | WalkMissing { missing: List } + | WalkSurplus { extra: List } + +fn coverage_walk(discovered: List, rows: List) -> CoverageWalk { match discovered { - Empty => Empty + Empty => + match rows { + Empty => WalkAligned + Cons { head: _, tail: _ } => WalkSurplus { extra: row_path_list(rows: rows) } + } Cons { head: d, tail: dt } => match rows { - Empty => discovered + Empty => WalkMissing { missing: discovered } Cons { head: r, tail: rt } => - if r.path == d { coverage_missing(discovered: dt, rows: rt) } else { discovered } + if r.path == d { + coverage_walk(discovered: dt, rows: rt) + } else { + WalkMissing { missing: discovered } + } } } } @@ -305,17 +341,23 @@ fn coverage_missing(discovered: List, rows: List) -> List type CorpusCoverage = CorpusCoverageComplete { covered: Int } | CorpusCoverageShort { discovered: Int, reported: Int, missing: List } + | CorpusCoverageExtra { discovered: Int, reported: Int, extra: List } fn corpus_coverage(discovered: List, rows: List) -> CorpusCoverage { - let missing = coverage_missing(discovered: discovered, rows: rows) - match missing { - Empty => CorpusCoverageComplete { covered: length(xs: rows) } - Cons { head: _, tail: _ } => + match coverage_walk(discovered: discovered, rows: rows) { + WalkAligned => CorpusCoverageComplete { covered: length(xs: rows) } + WalkMissing { missing: missing } => CorpusCoverageShort { discovered: length(xs: discovered), reported: length(xs: rows), missing: missing } + WalkSurplus { extra: extra } => + CorpusCoverageExtra { + discovered: length(xs: discovered), + reported: length(xs: rows), + extra: extra + } } } @@ -329,6 +371,7 @@ type CorpusGateDisposition | CorpusNotEstablishedObserved { rows: List } | CorpusPopulationBelowFloor { files: Int, floor: Int } | CorpusCoverageIncomplete { discovered: Int, reported: Int, missing: List } + | CorpusCoverageSurplus { discovered: Int, reported: Int, extra: List } fn corpus_gate_disposition(report: CorpusReport, population_floor: Int) -> CorpusGateDisposition { if report.files < population_floor { @@ -363,6 +406,8 @@ fn corpus_gate_disposition_over( match corpus_coverage(discovered: discovered, rows: rows) { CorpusCoverageShort { discovered: d, reported: r, missing: m } => CorpusCoverageIncomplete { discovered: d, reported: r, missing: m } + CorpusCoverageExtra { discovered: d, reported: r, extra: e } => + CorpusCoverageSurplus { discovered: d, reported: r, extra: e } CorpusCoverageComplete { covered: _ } => corpus_gate_disposition( report: corpus_report_from_rows(rows: rows), diff --git a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag index cc6c34a5623..578802b63b6 100644 --- a/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag +++ b/src/v2/test/claim/complexity/accumulator_copy_corpus_path_test.dag @@ -9,6 +9,7 @@ import v2.lens.complexity_accumulator_copy.corpus_gate { CorpusNoSuspectsObserved, CorpusNotEstablishedObserved, CorpusCoverageIncomplete, + CorpusCoverageSurplus, CorpusPopulationBelowFloor, CorpusSuspectObserved, corpus_gate, @@ -105,6 +106,7 @@ test fn an_empty_population_is_below_floor_rather_than_clean() -> Bool { CorpusSuspectObserved { paths: _, sites: _ } => false CorpusNotEstablishedObserved { rows: _ } => false CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => false } } @@ -138,6 +140,7 @@ fn disposition_over(discovered: List, rows: List) -> Corp fn is_coverage_incomplete(d: CorpusGateDisposition) -> Bool { match d { CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => true + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false CorpusSuspectObserved { paths: _, sites: _ } => false @@ -145,6 +148,49 @@ fn is_coverage_incomplete(d: CorpusGateDisposition) -> Bool { } } +fn is_coverage_surplus(d: CorpusGateDisposition) -> Bool { + match d { + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => true + CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusPopulationBelowFloor { files: _, floor: _ } => false + CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false + CorpusSuspectObserved { paths: _, sites: _ } => false + CorpusNotEstablishedObserved { rows: _ } => false + } +} + +// THE OTHER END OF THE WALK, AND IT WAS OPEN. Review 61363: the first cut returned "nothing +// missing" as soon as the DISCOVERED list ended, whatever rows remained, so a report carrying +// subjects the population does not contain was accepted. A surplus identity is not a lesser +// problem than a missing one -- it means the rows did not come from this population, so nothing +// about them is a statement about it. +test fn a_report_carrying_subjects_the_population_does_not_contain_refuses() -> Bool { + is_coverage_surplus( + d: disposition_over( + discovered: [planted_path], + rows: [ + scanned_row(path: planted_path, suspects: 1), + scanned_row(path: clean_path, suspects: 0) + ] + ) + ) +} + +// THE DEGENERATE CASE REVIEW 61363 NAMED, WITNESSED AT THE EXACT SHAPE IT NAMED. An empty +// discovered population with one clean reported row and a floor of 1 reached the NO-SUSPECT arm, +// because the floor counted REPORTED ROWS rather than the discovered population: the guard meant to +// run before the floor let the floor's own red control be defeated. It refuses now, and it refuses +// as SURPLUS rather than as below-floor, because the accurate statement is that these rows are not +// from this population at all. +test fn an_empty_population_with_a_reported_row_refuses_rather_than_reading_clean() -> Bool { + is_coverage_surplus( + d: disposition_over( + discovered: Empty, + rows: [scanned_row(path: clean_path, suspects: 0)] + ) + ) +} + // THE MITIGATION FOR A CLASS THAT SITS BELOW THE LADDER, AND ITS DISCRIMINATING RED. // gunbc.recurring_failure_mode.sweep_terminated_by_process_death_rather_than_a_typed_refusal: // a single-process fold over a discovered population can be killed partway through, and the @@ -193,6 +239,7 @@ test fn a_complete_report_over_the_same_population_reaches_its_ordinary_verdict( ) { CorpusSuspectObserved { paths: _, sites: _ } => true CorpusCoverageIncomplete { discovered: _, reported: _, missing: _ } => false + CorpusCoverageSurplus { discovered: _, reported: _, extra: _ } => false CorpusPopulationBelowFloor { files: _, floor: _ } => false CorpusNoSuspectsObserved { files: _, refusal_sites: _, refusal_causes: _ } => false CorpusNotEstablishedObserved { rows: _ } => false