diff --git a/dag/gunbc/compiler_frontend_program_interlock.dag b/dag/gunbc/compiler_frontend_program_interlock.dag index 1f731f16714..9ae228628dd 100644 --- a/dag/gunbc/compiler_frontend_program_interlock.dag +++ b/dag/gunbc/compiler_frontend_program_interlock.dag @@ -301,6 +301,7 @@ type NamespaceDeltaDisposition | NewUnresolvedness | NewPoolCoincidenceResolution | AuthoredReferenceResolution + | DerivedGeneratorInputResolution | UnexplainedSubjectMotion | NotEvaluated @@ -316,10 +317,17 @@ type NamespaceDeltaDisposition // The partition the operator ruled is therefore recorded HERE, where it cannot // be mistaken for an executing mechanism: the wall AUTO-ADMITS a same-declaration // -identity rebind, a removal of genuinely unused subject membership, an -// explicitly evaluated zero delta, and an authored-reference resolution. Every -// other disposition -- a changed target, new ambiguity, new unresolvedness, new -// pool-coincidence resolution, unexplained subject motion, and NotEvaluated -- -// REFUSES unless an exact operator-authored transition admission names it. +// explicitly evaluated zero delta, an authored-reference resolution, and a +// derived-generator-input resolution. Every other disposition -- a changed +// target, new ambiguity, new unresolvedness, new pool-coincidence resolution, +// unexplained subject motion, and NotEvaluated -- REFUSES unless an exact +// operator-authored transition admission names it. +// +// THE FIFTH MEMBER ARRIVED WITH THE 2026-09-08 SPLIT BELOW, and it is recorded +// in this sentence rather than only beside the arm that added it: this paragraph +// is where the partition is stated, so a new auto-admitted disposition recorded +// only at its own declaration would leave one authority answering the same +// question twice. // // AuthoredReferenceResolution WAS SPLIT OUT OF NewPoolCoincidenceResolution on // 2026-08-27, by operator ruling relayed through swift-badger-524, after the @@ -338,4 +346,67 @@ type NamespaceDeltaDisposition // shared projection is wanted at that point, it belongs beside the wall that // computes it, with the consumer that makes it load-bearing. +// DerivedGeneratorInputResolution IS THE THIRD STATE OF THAT SAME `0 -> 1`, SPLIT OUT +// on 2026-09-08 by operator ruling, and it is split for the reason the 2026-08-27 +// split was: one symbol was carrying two states with opposite owners and opposite +// repairs. The discriminator the paragraph above names -- THE MODULE'S OWN SOURCE -- +// presumes a module that HAS an author. A GENERATED module does not. gunbc#10822 made +// gunbc.recurring_failure_mode.roster derived: its content is produced by reading the +// row directory, so it has no author to consult, `authored_here` is false for every +// binding it will ever acquire, and every new row file was classified as a coincidence +// in the pool. The wall then refused the append that the generator exists to perform -- +// measured across five lanes and seven changes, where every change adding a row MODULE +// refused and every change editing an existing row passed. +// +// THE ADMISSIBLE FACT IS NOT "THE MODULE IS GENERATED", AND THAT DISTINCTION IS THE +// WHOLE ARM. Admitting a category would auto-admit every binding any generated module +// ever acquires, on the one surface where a wrong binding has no human reader -- the +// state-space conflation this file warns about, committed in the opposite direction. +// What is admissible is a binding that is THE MECHANICAL IMAGE OF THE GENERATOR'S +// DECLARED INPUT RELATION: the derived roster's input is the row directory, so a +// binding to a module that is a member of that directory is the deterministic +// consequence of a file the change adds, and is decidable from the generator's own +// declared inputs rather than from a reader's judgement. A generated module acquiring a +// binding its generator's inputs do NOT explain remains NewPoolCoincidenceResolution +// and remains refused; that refusal is the arm's positive control, not a leftover. +// +// IT ANSWERS FOR ONE GENERATOR, AND THE PROSE ABOVE WOULD OTHERWISE READ AS THOUGH +// IT GENERALIZED. The realization opens by refusing every module that is not the +// failure-mode roster, so a SECOND derived module with declared inputs is refused by +// this wall exactly as the first one was, and the repair would be someone editing +// the predicate to name it. That is a hand-maintained membership list where each new +// member costs an edit -- the shape gunbc#10822 has just finished deleting one layer +// up -- and it is disclosed here rather than discovered by the next author, who would +// otherwise meet a refusal and read an authority claiming to be about generators' +// declared inputs in general. +// +// THE NARROWNESS IS NOT LAZINESS, AND THE REASON IS THE TRIGGER. A general arm needs +// a carrier where a generator DECLARES ITS INPUT RELATION, and none exists: +// gunbc.generated_artifact's GeneratedArtifact is a closed coproduct of artifact +// IDENTITIES and carries no inputs at all, so there is nothing for a general +// predicate to consult. Inventing that carrier inside a required-gate repair would be +// modelling a substrate concept to unblock a wall, which is the wrong order. So the +// next rung is that carrier: when a generator's declared input relation is modeled, +// this predicate reads it and stops naming a module. Until then the honest reading is +// ONE generator, decided from ITS OWN constants rather than from a literal spelled +// here -- which is why it is a narrow arm and not a special case. +// +// THE RESIDUAL RISK, STATED RATHER THAN LEFT FOR A READER TO FIND. This arm trusts +// the generator's DECLARED inputs, and what it actually checks is the declaration, +// not the generator's behaviour. If a generator's implementation ever drifts from +// what it declares -- reading a second directory, or emitting a binding no input +// explains -- the wall auto-admits against a stale premise and says nothing, because +// the premise is exactly what it is not in a position to re-derive. That is an +// honest boundary rather than a rung: it is the same class of fact as any other +// declared-and-not-enforced input relation, and the ceiling is a generator whose +// declared inputs ARE its inputs by construction. Until that exists, the mitigation +// is that the declaration and the implementation sit in one module, so drift is a +// same-file edit rather than an action at a distance. +// +// SO THE PARTITION THE OPERATOR RULED GAINS ONE AUTO-ADMITTED MEMBER and loses none. +// The membership arm already reached this answer for the same files -- an added row +// edge auto-admits as ExplicitlyEvaluatedZeroDelta, "reached by a name this module +// authors" -- so before this arm the two arms disagreed about ONE file, which is the +// tell that the disposition and not the population was wrong. + data interlock_ruling_date: NonEmptyStr = "2026-08-26" as NonEmptyStr diff --git a/dag/gunbc/derived_row_roster_seed_growth.dag b/dag/gunbc/derived_row_roster_seed_growth.dag index 1a2dac1a99d..bc634c1f2b3 100644 --- a/dag/gunbc/derived_row_roster_seed_growth.dag +++ b/dag/gunbc/derived_row_roster_seed_growth.dag @@ -49,7 +49,7 @@ data derived_row_roster_seed_growth_justification: SeedGrowthJustification = See DeclarationRef { module_path: "v1_compiler.cli_run.rostered_row_join.file_roster_join_tests", decl_name: "roster_member_without_a_file_refuses", field: WholeDeclaration }, DeclarationRef { module_path: "v1_compiler.cli_run.rostered_row_join.file_roster_join_tests", decl_name: "equal_file_and_roster_sets_are_silent", field: WholeDeclaration } ], - reason: "WHY RUST RATHER THAN .dag: the substrate has no directory enumeration sufficient to BIND each sibling `.dag` as a typed RecurringFailureMode value. Membership discovery exists (`v2.std.decl_index` `data_decl_type_facts`); value binding does not. Until that grounding lands, a checkout with no committed roster has no module for the three importers (`gunbc.design_ledgers`, `gunbc.ledger_row_coherence`, `test.claim.generated_artifact_merge_driver_real_execution_witness`) and the required parse join reports RosterModuleAbsent on the intended steady state. The host fold lists sibling files, writes gitignored roster.dag atomically, and refuses a shortened list. That is a §7 seed realization of a missing capability, not a scaffold: dissolution is grounded in gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall (declaration-value binding), not in a sentence in the Rust module.\n\nThe file-versus-roster join arms are the other half of the same host: a skip in the writer must still refuse. They execute inside run_rostered_row_join on the required parse path (witnesses lane). file_roster_join_tests are the off-path REDs for those arms; they are enumerated because test items are still hand Rust.\n\nWHY ADMITTED AGAINST THE v1 FREEZE: gunbc.v1_maintenance_standing v1_seed_standing admits by PURPOSE. Removing the silent-drop constructor from the failure-mode ledger is an instrument tax every self-host lane was paying (merge hotspot). No new language behavior, no escape hatch, no emitted public surface.\n\nTHE BASE SIDE OF THE DERIVED ROSTER (gunbc#10856, three production items and three test items). is_derived_roster_path, roster_root_prefix and roster_from_path_listing exist because a DERIVED artifact has no base side in a diff and must not be inherited from the head. roster.dag is gitignored and written on the read path, so it can never appear in `git diff --name-status`, and namespace_wave_admission`s baseline -- reconstructed by carrying forward every head record the diff did not touch -- was therefore standing the HEAD`s generated roster up as the BASE`s. That made an ordinary row append read as a name resolving into a pool rather than an authored reference, because the roster`s two sides were the same bytes. They live HERE rather than beside their caller for the reason this module exists: roster_from_path_listing renders through render_roster, the same function the writer uses, so the roster`s bytes keep ONE authority whether the question is asked of a directory or of a base tree listing. They carry the same trigger as the rest of this population -- with declaration-value binding the derivation has no name-list at all, on either side, and these delete with the writer.\n\nRe-derive this list with tools.seed_growth_change_population rather than maintaining a count.", + reason: "WHY RUST RATHER THAN .dag: the substrate has no directory enumeration sufficient to BIND each sibling `.dag` as a typed RecurringFailureMode value. Membership discovery exists (`v2.std.decl_index` `data_decl_type_facts`); value binding does not. Until that grounding lands, a checkout with no committed roster has no module for the three importers (`gunbc.design_ledgers`, `gunbc.ledger_row_coherence`, `test.claim.generated_artifact_merge_driver_real_execution_witness`) and the required parse join reports RosterModuleAbsent on the intended steady state. The host fold lists sibling files, writes gitignored roster.dag atomically, and refuses a shortened list. That is a §7 seed realization of a missing capability, not a scaffold: dissolution is grounded in gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall (declaration-value binding), not in a sentence in the Rust module.\n\nThe file-versus-roster join arms are the other half of the same host: a skip in the writer must still refuse. They execute inside run_rostered_row_join on the required parse path (witnesses lane). file_roster_join_tests are the off-path REDs for those arms; they are enumerated because test items are still hand Rust.\n\nWHY ADMITTED AGAINST THE v1 FREEZE: gunbc.v1_maintenance_standing v1_seed_standing admits by PURPOSE. Removing the silent-drop constructor from the failure-mode ledger is an instrument tax every self-host lane was paying (merge hotspot). No new language behavior, no escape hatch, no emitted public surface.\n\nTHE BASE SIDE OF THE DERIVED ROSTER (gunbc#10856, three production items and three test items). is_derived_roster_path, roster_root_prefix and roster_from_path_listing exist because a DERIVED artifact has no base side in a diff and must not be inherited from the head. roster.dag is gitignored and written on the read path, so it can never appear in `git diff --name-status`, and namespace_wave_admission`s baseline -- reconstructed by carrying forward every head record the diff did not touch -- was therefore standing the HEAD`s generated roster up as the BASE`s. That made an ordinary row append read as a name resolving into a pool rather than an authored reference, because the roster`s two sides were the same bytes. They live HERE rather than beside their caller for the reason this module exists: roster_from_path_listing renders through render_roster, the same function the writer uses, so the roster`s bytes keep ONE authority whether the question is asked of a directory or of a base tree listing. They carry the same trigger as the rest of this population -- with declaration-value binding the derivation has no name-list at all, on either side, and these delete with the writer.\n\nRe-derive this list with tools.seed_growth_change_population rather than maintaining a count.\n\nROSTER_MODULE WAS ADDED 2026-09-09 AND IT REMOVES A CONSUMER'S GUESS RATHER THAN ADDING CAPABILITY HERE. The wave-admission wall recovered this module's own roster module name by stripping `.dag` off ROSTER_BASENAME and substituting a default when the suffix was absent -- a fabricated plausible name on an AUTO-ADMISSION path, where the consequence of the guess is a comparison that silently succeeds against a name nobody declares rather than a refusal. The name belongs to the generator that emits the module, so it is declared here and consumed there: one constant added, one recovery and one default deleted, and no new host capability. It dissolves with this row's existing trigger, unchanged.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, trigger: "THE SUBJECT IS THESE DECLARATIONS, and they retire with declaration introspection sufficient to BIND a discovered data declaration as a typed value of its declared type — the same capability named by gunbc.guarantee_stall.roster_re_enumerates_its_own_rows_stall — at which point the host fold and the file-join arms DELETE because there is no authored or derived name-list to omit a row from. A differently named hand writer is not dissolution: it is this obligation under a new key. Registering roster.dag on gunbc.generated_artifact does not discharge this: that would restore heal/drift rewrite of a gitignored file. data_decl_type_facts already exists and does not discharge a trigger that names BINDING.", current_boundary: "v1_compiler.cli_run.derived_row_roster is reached from run_required_wave_admission (is_derived_roster_path, roster_root_prefix and roster_from_path_listing, which is the phase`s base side and the only consumer of those three), collect_dag_files_result, collect_dag_files_tolerant (write errors ignored), main.rs collect_dag_files, compiler_tests collect_dag_recursive, and run_dag_parse_sweep (the loud required-CI writer). join_row_files_to_roster_members is reached only from join_recurring_failure_mode_files_to_roster inside run_rostered_row_join. The derived file is gitignored and is not a gunbc.generated_artifact. Docs gate main agrees only DesignRungDropsArtifact." diff --git a/dag/gunbc/namespace/namespace_wave_admission.dag b/dag/gunbc/namespace/namespace_wave_admission.dag index b18432d4869..9d8646f1bf0 100644 --- a/dag/gunbc/namespace/namespace_wave_admission.dag +++ b/dag/gunbc/namespace/namespace_wave_admission.dag @@ -341,6 +341,11 @@ data namespace_wave_admission_seed_growth_justification: SeedGrowthJustification decl_name: "binding_disposition", field: WholeDeclaration }, + DeclarationRef { + module_path: "v1_compiler.namespace_wave_admission", + decl_name: "derived_generator_input_binding", + field: WholeDeclaration + }, DeclarationRef { module_path: "v1_compiler.namespace_wave_admission", decl_name: "blast_radius", @@ -422,11 +427,13 @@ data namespace_wave_admission_seed_growth_justification: SeedGrowthJustification field: WholeDeclaration }, ], - reason: "THE OPERATOR RULED THAT A REQUEST TO ADD HAND RUST IS THE OCCASION TO MIGRATE OR DELETE HAND RUST (2026-08-26, relayed by warm-hawk-909: `yes we need to stop adding hand rust asap` and `use it as an opportunity to migrate/delete hand rust`). So this row answers at DECLARATION grain rather than as a count, and it records what the growth request bought back.\n\nWHAT THIS CHANGE DELETES OR AVOIDS, MEASURED, NOT ARGUED. (i) leaf_of was DELETED: it was a nickname for v1.00_core qualified_last_segment, which v1.05_emit_rust rust_fn_sig_leaf_name_dotted_note names as THE single authority for taking an authored spelling to its last segment. The wall now calls that mirror. That is a section 3 fork this change had introduced and removed before landing, and it is also the stronger construction, because the reduction the wall keys on is now the corpus authority rather than a local respelling. (ii) render_set was DELETED and inlined at its two call sites. (iii) claim_executor carried a BYTE-IDENTICAL PRIVATE COPY of git_stdout; the wall needed the same helper, and rather than land a third spelling the lib now owns the one copy, the bin`s copy is DELETED and its call sites read it. That is a NET REDUCTION of one pre-existing hand declaration and one fork.\n\nWHAT WAS NOT DELETED AND WHY, PER CLASS, BECAUSE `THESE ARE IRREDUCIBLE` IS A CLAIM AND A REASON PER DECLARATION IS AN ARGUMENT.\n\nCLASS A -- THE PURE FOLD, the declarations named here (a count is deliberately not carried: this row answers at declaration grain, and a transcribed number decays independently of the list it summarizes): adjudicate, binding_rows, binding_disposition, declaring_candidates, declarer_of, closure_of, membership_map, direct_membership, module_prefix_of, membership_declared, membership_bound_through, blast_radius, disposition_label, disposition_auto_admitted, delta_subject_render, report_unadjudicated, render_delta, vocabulary_findings, and the types and constants they fold over. Every one is a pure function of two indexes and needs NO host effect. They are host Rust for exactly ONE reason and it is not effort: their INPUT TYPE is host-only. ModuleDeclarationRecord and DeclarationIndex are Rust types produced by a host walk, and there is no .dag carrier for a per-module declaration record. Authoring one now would be a SECOND REPRESENTATION of a type gunbc.declaration_index_seed_growth already owns -- the section 3 violation this wall exists to refuse elsewhere -- so the correct move is to FOLLOW that carrier`s migration rather than fork it. Its trigger already names this class: when the record derivation is modeled, `the record builder and every finding function follow it`. This roster is one of those consumers.\n\nCLASS B -- THE ACQUISITION, four declarations: git_stdout, in_sweep_scope, base_records, run_required_wave_admission. These are blocked on a DIFFERENT grounding, which is why they are named separately rather than swept into class A: there is no typed repository-read transport a .dag fold can call to obtain the text of a file AT A REF. That is the scm lane`s subject, not a missing surface this change could have authored. base_records additionally needs the frontend`s own per-source parse, which is the same acquisition boundary.\n\nPR #9436 CHECKABLE HAND-RUST RECEIPT (review 56812). The 53 exact transition admissions are DATA consumed by the already-rostered NAMESPACE_TRANSITION_ADMISSIONS declaration; they add no compiler function, type, or admission mechanism. Measured against origin/main after rebasing: `git diff --numstat origin/main -- src/v1/stage0/src/namespace_wave_admission.rs` reports 559 added / 7 removed. `git show origin/main:src/v1/stage0/src/namespace_wave_admission.rs | grep -cE '^(pub )?(fn|struct|enum|const|static) '` reports 34 declarations, and the same grep on the worktree file reports 37. The anchored added/removed forms over `git diff` report 4 and 1 respectively: the unchanged roster declaration moves from the old `DeltaSubject` element type to `AdmissionSubject`, while `AdmissionSubject`, `admission_subject_matches`, and `admission_subject_render` are the three net additions. All three are enumerated in this justification. The initializer is a literal authored roster, not a scan, file read, environment read, or computed predicate. Each row is exact on subject and disposition; stale rows refuse, so the roster has its own deletion trigger: any absorbed or vanished delta makes required CI red until that row is removed. This is an expansion in maintained lines, not in declarations or host capability, and it dissolves row-by-row with the transition it names.\n\nONE THING THIS ROW WILL NOT DO: shrink the wall`s coverage to lower the count. A smaller wall that adds less Rust would spend correctness to buy a metric, which is the ratchet failure DESIGN names in the other direction.\n\nAND THE COUNT WENT UP RATHER THAN DOWN, WHICH IS THE HONEST DIRECTION. This roster was authored at 25 rows and is now 36, because it was JOINED against the module`s actual declaration population instead of listed from memory: leaf_of, binding_rows, vocabulary_findings and three constants were missing from the first draft. gunbc.seed_growth already warns that hand_authored_declarations is an authored obligation roster rather than a derived denominator; this is that warning firing on its own first use. Four methods were also converted from inherent impl blocks to free functions, because std.decl_ref names WholeDeclaration or NamedField and neither names a method on an impl block -- so as methods they would have been UNCITABLE items the roster structurally cannot enumerate. The module now carries no impl block, and every declaration in it is enumerated here.\n\nNO LABEL CONSTANT IS ENUMERATED HERE ANY LONGER, AND FOUR HAVE NOW BEEN DELETED BY THEIR OWN TRIGGERS. THE STANDING GAP THIS ROW ALREADY WARNED OF HAS NOW FIRED IN THE OTHER DIRECTION, AND THAT IS WHY THE COUNT MOVES. CALL_SEMANTICS_TARGET_REHOME_LABEL was gunbc#10688`s own; gunbc#10813 deleted the constant when its rows reported CONSUMED but left this enumeration`s DeclarationRef for it standing, so the roster cited a declaration the module no longer carries -- the exact decay this row predicts, now visible from the surviving side rather than the missing one. gunbc#10856 removes that stale reference. RECURRING_FAILURE_MODE_CLASS_ADDED_LABEL, which gunbc#10813 added and never enumerated here, is deleted with its row on the same change. Those two are the LABEL half of the decay only; the same change also repairs the nine non-label defects recorded below, after which no reference here is dangling and no declaration is unenumerated. NAMESPACE_TRANSITION_ADMISSIONS therefore stands as an EMPTY enumeration, which is the honest reading that no transition is currently admitted -- the declaration is not deleted, because the roster`s population is an enumeration and never a predicate. AND IT IS EXPECTED TO STAY EMPTY FOR THE FAILURE-MODE LEDGER SPECIFICALLY: the per-class admission row gunbc#10813 authored was admitting a BASELINE defect rather than the ledger`s growth shape, and gunbc#10856 repairs that baseline, so appending a class no longer produces a delta needing a row. EXIT_OK_REHOME_LABEL and SCM_REPOSITORY_BUILDER_REHOME_LABEL are likewise NOT listed because they no longer exist: both sets of rows reported CONSUMED and came due on the next roster-touching change, which this one is, so the rows and their labels went together, each adjudicated by the per-spelling declaration join those rows demanded rather than by their own trigger sentence. A label constant is text shared by the rows that cite it -- no compiler function, type, or host capability -- and it dissolves with the last row carrying it, which is why the enumeration loses every one of them and gains none. + reason: "THE OPERATOR RULED THAT A REQUEST TO ADD HAND RUST IS THE OCCASION TO MIGRATE OR DELETE HAND RUST (2026-08-26, relayed by warm-hawk-909: `yes we need to stop adding hand rust asap` and `use it as an opportunity to migrate/delete hand rust`). So this row answers at DECLARATION grain rather than as a count, and it records what the growth request bought back.\n\nWHAT THIS CHANGE DELETES OR AVOIDS, MEASURED, NOT ARGUED. (i) leaf_of was DELETED: it was a nickname for v1.00_core qualified_last_segment, which v1.05_emit_rust rust_fn_sig_leaf_name_dotted_note names as THE single authority for taking an authored spelling to its last segment. The wall now calls that mirror. That is a section 3 fork this change had introduced and removed before landing, and it is also the stronger construction, because the reduction the wall keys on is now the corpus authority rather than a local respelling. (ii) render_set was DELETED and inlined at its two call sites. (iii) claim_executor carried a BYTE-IDENTICAL PRIVATE COPY of git_stdout; the wall needed the same helper, and rather than land a third spelling the lib now owns the one copy, the bin`s copy is DELETED and its call sites read it. That is a NET REDUCTION of one pre-existing hand declaration and one fork.\n\nWHAT WAS NOT DELETED AND WHY, PER CLASS, BECAUSE `THESE ARE IRREDUCIBLE` IS A CLAIM AND A REASON PER DECLARATION IS AN ARGUMENT.\n\nCLASS A -- THE PURE FOLD, the declarations named here (a count is deliberately not carried: this row answers at declaration grain, and a transcribed number decays independently of the list it summarizes): adjudicate, binding_rows, binding_disposition, derived_generator_input_binding, declaring_candidates, declarer_of, closure_of, membership_map, direct_membership, module_prefix_of, membership_declared, membership_bound_through, blast_radius, disposition_label, disposition_auto_admitted, delta_subject_render, report_unadjudicated, render_delta, vocabulary_findings, and the types and constants they fold over. Every one is a pure function of two indexes and needs NO host effect. They are host Rust for exactly ONE reason and it is not effort: their INPUT TYPE is host-only. ModuleDeclarationRecord and DeclarationIndex are Rust types produced by a host walk, and there is no .dag carrier for a per-module declaration record. Authoring one now would be a SECOND REPRESENTATION of a type gunbc.declaration_index_seed_growth already owns -- the section 3 violation this wall exists to refuse elsewhere -- so the correct move is to FOLLOW that carrier`s migration rather than fork it. Its trigger already names this class: when the record derivation is modeled, `the record builder and every finding function follow it`. This roster is one of those consumers.\n\nCLASS B -- THE ACQUISITION, four declarations: git_stdout, in_sweep_scope, base_records, run_required_wave_admission. These are blocked on a DIFFERENT grounding, which is why they are named separately rather than swept into class A: there is no typed repository-read transport a .dag fold can call to obtain the text of a file AT A REF. That is the scm lane`s subject, not a missing surface this change could have authored. base_records additionally needs the frontend`s own per-source parse, which is the same acquisition boundary.\n\nPR #9436 CHECKABLE HAND-RUST RECEIPT (review 56812). The 53 exact transition admissions are DATA consumed by the already-rostered NAMESPACE_TRANSITION_ADMISSIONS declaration; they add no compiler function, type, or admission mechanism. Measured against origin/main after rebasing: `git diff --numstat origin/main -- src/v1/stage0/src/namespace_wave_admission.rs` reports 559 added / 7 removed. `git show origin/main:src/v1/stage0/src/namespace_wave_admission.rs | grep -cE '^(pub )?(fn|struct|enum|const|static) '` reports 34 declarations, and the same grep on the worktree file reports 37. The anchored added/removed forms over `git diff` report 4 and 1 respectively: the unchanged roster declaration moves from the old `DeltaSubject` element type to `AdmissionSubject`, while `AdmissionSubject`, `admission_subject_matches`, and `admission_subject_render` are the three net additions. All three are enumerated in this justification. The initializer is a literal authored roster, not a scan, file read, environment read, or computed predicate. Each row is exact on subject and disposition; stale rows refuse, so the roster has its own deletion trigger: any absorbed or vanished delta makes required CI red until that row is removed. This is an expansion in maintained lines, not in declarations or host capability, and it dissolves row-by-row with the transition it names.\n\nONE THING THIS ROW WILL NOT DO: shrink the wall`s coverage to lower the count. A smaller wall that adds less Rust would spend correctness to buy a metric, which is the ratchet failure DESIGN names in the other direction.\n\nAND THE COUNT WENT UP RATHER THAN DOWN, WHICH IS THE HONEST DIRECTION. This roster was authored at 25 rows and is now 36, because it was JOINED against the module`s actual declaration population instead of listed from memory: leaf_of, binding_rows, vocabulary_findings and three constants were missing from the first draft. gunbc.seed_growth already warns that hand_authored_declarations is an authored obligation roster rather than a derived denominator; this is that warning firing on its own first use. Four methods were also converted from inherent impl blocks to free functions, because std.decl_ref names WholeDeclaration or NamedField and neither names a method on an impl block -- so as methods they would have been UNCITABLE items the roster structurally cannot enumerate. The module now carries no impl block, and every declaration in it is enumerated here.\n\nNO LABEL CONSTANT IS ENUMERATED HERE ANY LONGER, AND FOUR HAVE NOW BEEN DELETED BY THEIR OWN TRIGGERS. THE STANDING GAP THIS ROW ALREADY WARNED OF HAS NOW FIRED IN THE OTHER DIRECTION, AND THAT IS WHY THE COUNT MOVES. CALL_SEMANTICS_TARGET_REHOME_LABEL was gunbc#10688`s own; gunbc#10813 deleted the constant when its rows reported CONSUMED but left this enumeration`s DeclarationRef for it standing, so the roster cited a declaration the module no longer carries -- the exact decay this row predicts, now visible from the surviving side rather than the missing one. gunbc#10856 removes that stale reference. RECURRING_FAILURE_MODE_CLASS_ADDED_LABEL, which gunbc#10813 added and never enumerated here, is deleted with its row on the same change. Those two are the LABEL half of the decay only; the same change also repairs the nine non-label defects recorded below, after which no reference here is dangling and no declaration is unenumerated. NAMESPACE_TRANSITION_ADMISSIONS therefore stands as an EMPTY enumeration, which is the honest reading that no transition is currently admitted -- the declaration is not deleted, because the roster`s population is an enumeration and never a predicate. AND IT IS EXPECTED TO STAY EMPTY FOR THE FAILURE-MODE LEDGER SPECIFICALLY: the per-class admission row gunbc#10813 authored was admitting a BASELINE defect rather than the ledger`s growth shape, and gunbc#10856 repairs that baseline, so appending a class no longer produces a delta needing a row. EXIT_OK_REHOME_LABEL and SCM_REPOSITORY_BUILDER_REHOME_LABEL are likewise NOT listed because they no longer exist: both sets of rows reported CONSUMED and came due on the next roster-touching change, which this one is, so the rows and their labels went together, each adjudicated by the per-spelling declaration join those rows demanded rather than by their own trigger sentence. A label constant is text shared by the rows that cite it -- no compiler function, type, or host capability -- and it dissolves with the last row carrying it, which is why the enumeration loses every one of them and gains none. + +THE 2026-09-09 GROWTH REQUEST, RECORDED AT DECLARATION GRAIN AS THIS ROW DEMANDS. derived_generator_input_binding is ONE added declaration in this module (review 62792 also moved the roster's own module name into derived_row_roster as ROSTER_MODULE, replacing a strip-and-guess recovery in this wall: that is a constant in ANOTHER module's roster, and it REMOVES a fabricated default from an auto-admission path rather than adding capability here), and it is CLASS A by the same test as its neighbours: a pure predicate over a module path and a candidate set, needing no host effect, host Rust only because binding_disposition's inputs are. It buys back the wall`s ability to admit an append the generator itself produces -- before it, every change adding a failure-mode row MODULE refused, measured across five lanes and seven changes. IT DELETES NOTHING, AND THAT IS STATED RATHER THAN DRESSED: no scaffold was removed and no fork was consolidated by this change, so the honest receipt is that the count went 42 to 43 and bought a classification the wall could not otherwise make. The alternative that would have added no declaration -- widening the authored_here branch to treat every generated module as authored -- was REFUSED as the fail-open direction of the same conflation the split repairs, so the declaration is the price of not weakening the wall. It dissolves with CLASS A, unchanged. THE STANDING GAP THIS RECORDED HAD ALSO DECAYED IN THE OTHER DIRECTION, AND gunbc#10856 RAN THE JOIN BY HAND RATHER THAN REPEATING THE CLAIM. EXIT_OK_REHOME_LABEL was never enumerated while it lived, and neither was SCM_REPOSITORY_BUILDER_REHOME_LABEL before it, so the sentence above -- that every declaration in the module is enumerated here -- had been false for two lanes running without anyone being at fault. It was false again at this change`s own head, in BOTH directions at once: membership_supported was enumerated here after the declaration had been renamed away (the module`s membership arm is membership_declared over membership_bound_through), and eight declarations were enumerated nowhere -- KERNEL_DECLARATION_IDENTITY, ADMISSION_ROSTER_REL_PATH, diff_sides, admission_consumed_at_base, membership_declared, membership_bound_through, locally_authored_claim_added and wave_admission_refusal. All nine are repaired here, and the enumeration now stands in EXACT BIJECTION with the module`s declarations, joined at NAME rather than at count. That bijection is a MEASUREMENT OF ONE HEAD, not a mechanism: nothing in the repository performs this join, so it will decay again the moment a lane adds a declaration without adding a row. The mechanical repair is unchanged and is precisely the per-module declaration record gunbc.declaration_index_seed_growth owes, which this roster`s CLASS A already waits on.", owning_dissolution_lane: "v1-hand-queue-drain" as RoadmapNodeId, - trigger: "Delete these in TWO steps, one per class above, and a PARTIAL migration is the expected shape rather than a failure. Class A follows gunbc.declaration_index_seed_growth`s first step: the moment a per-module declaration record is a .dag carrier, every one of the CLASS A declarations moves with it, because they are pure folds over it and nothing else. Class B follows the scm lane: when a typed repository-read transport can supply a file`s text at a ref. adjudicate, binding_disposition, declaring_candidates, declarer_of, closure_of, membership_map, direct_membership, module_prefix_of, membership_declared, membership_bound_through and blast_radius are pure functions of two indexes and move to .dag the moment the per-module record derivation does -- they need no host effect at all, so they follow gunbc.declaration_index_seed_growth's first step rather than waiting for its second. git_stdout, in_sweep_scope, base_records and run_required_wave_admission stay behind until a typed repository-read transport can supply a file's text at a ref, which is the scm lane's subject. A PARTIAL migration is the expected shape. What does NOT dissolve them is the phase moving to another host entry point, and what does NOT dissolve them is a hermetic witness over a fabricated pair of indexes -- the fixture suite already does that, and it is the evidence rather than the dissolution.", + trigger: "Delete these in TWO steps, one per class above, and a PARTIAL migration is the expected shape rather than a failure. Class A follows gunbc.declaration_index_seed_growth`s first step: the moment a per-module declaration record is a .dag carrier, every one of the CLASS A declarations moves with it, because they are pure folds over it and nothing else. Class B follows the scm lane: when a typed repository-read transport can supply a file`s text at a ref. adjudicate, binding_disposition, derived_generator_input_binding, declaring_candidates, declarer_of, closure_of, membership_map, direct_membership, module_prefix_of, membership_declared, membership_bound_through and blast_radius are pure functions of two indexes and move to .dag the moment the per-module record derivation does -- they need no host effect at all, so they follow gunbc.declaration_index_seed_growth's first step rather than waiting for its second. git_stdout, in_sweep_scope, base_records and run_required_wave_admission stay behind until a typed repository-read transport can supply a file's text at a ref, which is the scm lane's subject. A PARTIAL migration is the expected shape. What does NOT dissolve them is the phase moving to another host entry point, and what does NOT dissolve them is a hermetic witness over a fabricated pair of indexes -- the fixture suite already does that, and it is the evidence rather than the dissolution.", current_boundary: "src/v1/stage0/src/namespace_wave_admission.rs; src/v1/stage0/src/declaration_index.rs dotted_chain and ModuleDeclarationRecord.referenced; src/v1/stage0/src/bin/claim_executor.rs phase namespace-wave-admission; src/v1/stage0/tests/namespace_wave_admission.rs; dag/gunbc/namespace/namespace_wave_admission.dag" } diff --git a/src/v1/stage0/src/cli_run/derived_row_roster.rs b/src/v1/stage0/src/cli_run/derived_row_roster.rs index 275f8245b4c..09858905979 100644 --- a/src/v1/stage0/src/cli_run/derived_row_roster.rs +++ b/src/v1/stage0/src/cli_run/derived_row_roster.rs @@ -43,11 +43,21 @@ pub const ROW_MODULE: &str = "gunbc.recurring_failure_mode"; /// `RosterModuleAbsent` finding, and the run goes red naming the module. The cost of a second /// spelling is a confusing refusal about a module that appears to exist, not a silent one. /// -/// NOT THE NAMESPACE WAVE. `run_required_wave_admission` finds this file by PATH -- -/// `is_derived_roster_path` and `roster_root_prefix`, both composed from `ROW_DIR_REL` and -/// `ROSTER_BASENAME` -- and never reads this constant. An earlier version of this comment said -/// the const was consumed by "the namespace wave's base reconstruction"; that is false, and the -/// note stays so the next reader does not re-derive it from the diff. +/// NOT THE NAMESPACE WAVE'S DISCOVERY, BUT NOW ONE OF ITS CLASSIFIERS. +/// `run_required_wave_admission` still finds this file by PATH -- `is_derived_roster_path` and +/// `roster_root_prefix`, both composed from `ROW_DIR_REL` and `ROSTER_BASENAME` -- and no +/// discovery path reads this constant. An earlier version of this comment said the const was +/// consumed by "the namespace wave's base reconstruction"; that was false and the correction +/// stays, because the true consumer is narrower and later: `derived_generator_input_binding` +/// reads it to decide whether the module being classified IS this generator, and reads +/// `ROW_MODULE` to decide whether what it binds to is one of this generator's declared inputs. +/// +/// THAT CONSUMER IS WHY THIS IS A CONSTANT AND NOT A STRIP OF `ROSTER_BASENAME`. Recovering the +/// name from the filename means stripping `.dag` and choosing what to do when the suffix is +/// absent, and every such choice is a guess about a name this module owns. The guess would sit +/// on an AUTO-ADMISSION path, where a fabricated fallback keeps comparing against a plausible +/// name instead of refusing -- the fabricated-plausible-output DESIGN section 5 forbids. +/// Declaring it once leaves no default to guess. /// /// It cannot be `concat!` of its parts because `ROW_MODULE` is a `const` and not a literal token, /// so the composition is ASSERTED by `the_roster_module_is_the_row_module_plus_the_roster_stem` diff --git a/src/v1/stage0/src/namespace_wave_admission.rs b/src/v1/stage0/src/namespace_wave_admission.rs index a17e9cd935c..62ae0046345 100644 --- a/src/v1/stage0/src/namespace_wave_admission.rs +++ b/src/v1/stage0/src/namespace_wave_admission.rs @@ -101,6 +101,7 @@ pub enum NamespaceDeltaDisposition { NewUnresolvedness, NewPoolCoincidenceResolution, AuthoredReferenceResolution, + DerivedGeneratorInputResolution, UnexplainedSubjectMotion, NotEvaluated, } @@ -121,6 +122,9 @@ pub fn disposition_label(d: NamespaceDeltaDisposition) -> &'static str { NamespaceDeltaDisposition::NewUnresolvedness => "NewUnresolvedness", NamespaceDeltaDisposition::NewPoolCoincidenceResolution => "NewPoolCoincidenceResolution", NamespaceDeltaDisposition::AuthoredReferenceResolution => "AuthoredReferenceResolution", + NamespaceDeltaDisposition::DerivedGeneratorInputResolution => { + "DerivedGeneratorInputResolution" + } NamespaceDeltaDisposition::UnexplainedSubjectMotion => "UnexplainedSubjectMotion", NamespaceDeltaDisposition::NotEvaluated => "NotEvaluated", } @@ -138,7 +142,8 @@ pub fn disposition_auto_admitted(d: NamespaceDeltaDisposition) -> bool { NamespaceDeltaDisposition::SameDeclarationIdentityRebind | NamespaceDeltaDisposition::UnusedSubjectMembershipRemoved | NamespaceDeltaDisposition::ExplicitlyEvaluatedZeroDelta - | NamespaceDeltaDisposition::AuthoredReferenceResolution => true, + | NamespaceDeltaDisposition::AuthoredReferenceResolution + | NamespaceDeltaDisposition::DerivedGeneratorInputResolution => true, NamespaceDeltaDisposition::TargetChanged | NamespaceDeltaDisposition::NewAmbiguity | NamespaceDeltaDisposition::NewUnresolvedness @@ -153,7 +158,7 @@ pub const DISPOSITION_AUTHORITY_MODULE: &str = "gunbc.compiler_frontend_program_ pub const DISPOSITION_AUTHORITY_DECL: &str = "NamespaceDeltaDisposition"; /// Every label this host enum carries, in the authority's own spelling. -pub const DISPOSITION_LABELS: [&str; 10] = [ +pub const DISPOSITION_LABELS: [&str; 11] = [ "SameDeclarationIdentityRebind", "UnusedSubjectMembershipRemoved", "ExplicitlyEvaluatedZeroDelta", @@ -162,6 +167,7 @@ pub const DISPOSITION_LABELS: [&str; 10] = [ "NewUnresolvedness", "NewPoolCoincidenceResolution", "AuthoredReferenceResolution", + "DerivedGeneratorInputResolution", "UnexplainedSubjectMotion", "NotEvaluated", ]; @@ -2020,6 +2026,7 @@ pub fn adjudicate( base_set, head_set, locally_authored_claim_added(head, base_record, head_record, &key.1), + derived_generator_input_binding(module, head_set), ); deltas.push(NamespaceDelta { subject: DeltaSubject::Binding { @@ -2196,6 +2203,36 @@ fn admission_consumed_at_base( } } +/// Whether a binding this module gained is the mechanical image of its GENERATOR'S DECLARED +/// INPUT RELATION. +/// +/// THE QUESTION IS NOT "IS THIS MODULE GENERATED". A generated module is exactly where a wrong +/// binding has no human reader, so exempting the category would be the fail-open direction of the +/// same conflation the `0 -> 1` split repaired. The question is whether the generator's OWN +/// DECLARED INPUTS explain the binding: `derived_row_roster` declares that the roster is produced +/// by reading the `.dag` files of `ROW_MODULE`, so a roster binding whose every candidate is a +/// module of that directory is the deterministic consequence of a file the change adds, and is +/// decidable here from the generator's constants rather than from a reader's judgement. +/// +/// EVERY OTHER BINDING A DERIVED MODULE ACQUIRES STILL REFUSES, and that refusal is this arm's +/// positive control rather than an unhandled case: a roster binding to a module OUTSIDE its +/// generator's input directory is not explained by the generator, so it stays +/// `NewPoolCoincidenceResolution`. +fn derived_generator_input_binding(module: &str, head: &BTreeSet) -> bool { + let roster_module = crate::cli_run::derived_row_roster::ROSTER_MODULE; + if module != roster_module { + return false; + } + let input_prefix = format!("{}.", crate::cli_run::derived_row_roster::ROW_MODULE); + !head.is_empty() + && head.iter().all(|candidate| { + candidate != roster_module + && candidate + .strip_prefix(&input_prefix) + .is_some_and(|stem| !stem.contains('.')) + }) +} + /// Which disposition a changed candidate SET carries. /// /// EVERY ARM IS OVER SETS, NOT WINNERS. `1 -> 0` stopped denoting anything; `1 -> 2` now admits @@ -2209,6 +2246,17 @@ fn admission_consumed_at_base( /// the repair the wall wants, and the state it refused. Opposite owners, opposite repairs, one /// symbol: DESIGN's state-space conflation. /// +/// `0 -> 1` HAS A THIRD STATE, AND IT IS THE ONE A GENERATED MODULE INHABITS. The `authored_here` +/// discriminator presumes a module that HAS an author to consult. A DERIVED module does not: its content +/// is produced by a generator, `authored_here` is false for every binding it will ever acquire, +/// and so every binding it gains reads as a coincidence in the pool. `derived_generator_input` +/// answers the question authorship cannot — is this binding THE MECHANICAL IMAGE OF THE +/// GENERATOR'S DECLARED INPUT RELATION, the deterministic consequence of a source file the change +/// adds. It is deliberately NOT "the module is generated": admitting that category would +/// auto-admit every binding any generated module ever acquires, on the one surface where a wrong +/// binding has no human reader. A generated module acquiring a binding its generator's inputs do +/// not explain stays `NewPoolCoincidenceResolution` and stays refused. +/// /// THE DISCRIMINATOR IS THE MODULE'S OWN SOURCE, available for free — the membership arm already /// consults authorship (`membership_declared`, over `membership_bound_through`), which admitted /// the membership edge of the very change this arm refused. So `authored_here` is passed in, not @@ -2217,6 +2265,7 @@ fn binding_disposition( base: &BTreeSet, head: &BTreeSet, authored_here: bool, + derived_generator_input: bool, ) -> NamespaceDeltaDisposition { if head.is_empty() { return NamespaceDeltaDisposition::NewUnresolvedness; @@ -2224,6 +2273,8 @@ fn binding_disposition( if base.is_empty() { return if authored_here { NamespaceDeltaDisposition::AuthoredReferenceResolution + } else if derived_generator_input { + NamespaceDeltaDisposition::DerivedGeneratorInputResolution } else { NamespaceDeltaDisposition::NewPoolCoincidenceResolution }; diff --git a/src/v1/stage0/tests/namespace_wave_admission.rs b/src/v1/stage0/tests/namespace_wave_admission.rs index f4bb4258019..3eec2de6a2a 100644 --- a/src/v1/stage0/tests/namespace_wave_admission.rs +++ b/src/v1/stage0/tests/namespace_wave_admission.rs @@ -1967,3 +1967,97 @@ fn an_ambiguous_base_binding_is_not_consumption() { report.stale_admissions ); } + +// ── `0 -> 1` HAS A THIRD STATE: THE ONE A GENERATED MODULE INHABITS ── +// +// gunbc#10822 made the failure-mode roster DERIVED — its content is produced by reading the row +// directory — so it has no author for `authored_here` to consult, and every row file added after +// that classified as a coincidence in the pool. The wall then refused the append the generator +// exists to perform: measured across five lanes and seven changes, every change adding a row +// MODULE refused while every change editing an existing row passed. +// +// THE PAIR BELOW IS THE DISCRIMINATING ONE, and the second arm is why this is a classifier and +// not an exemption: both fixtures are a derived roster gaining a binding it did not have. They +// differ in ONE fact — whether the newly bound module is a member of the generator's DECLARED +// INPUT directory. Admitting on "the module is generated" would pass both. + +// THE ROSTER SPELLS THE ROW NAME ON BOTH SIDES, which is what the observed delta requires: the +// wall compares only rows present on both sides, and the real report reads `base {} -> head {..}` +// — the spelling denoting NOTHING at base and the row module at head. A fixture where the +// spelling appears only at head produces no delta at all, and asserting over an empty delta list +// is a pass that measured nothing. +const ROSTER: &str = "module gunbc.recurring_failure_mode.roster\n\nimport gunbc.recurring_failure_mode.beta_row { beta_row }\n\ndata recurring_failure_mode_roster: String = beta_row\n"; +const ALPHA_ROW: &str = + "module gunbc.recurring_failure_mode.alpha_row\n\ndata alpha_row: String = \"a\"\n"; +const BETA_ROW_ABSENT: &str = + "module gunbc.recurring_failure_mode.beta_row\n\ndata unrelated_name: String = \"b\"\n"; +const BETA_ROW: &str = + "module gunbc.recurring_failure_mode.beta_row\n\ndata beta_row: String = \"b\"\n"; +const OUTSIDE_ROSTER: &str = "module gunbc.recurring_failure_mode.roster\n\nimport gunbc.elsewhere { beta_row }\n\ndata recurring_failure_mode_roster: String = beta_row\n"; +const OUTSIDE_ABSENT: &str = "module gunbc.elsewhere\n\ndata unrelated_name: String = \"x\"\n"; +const OUTSIDE_SUPPLIES: &str = "module gunbc.elsewhere\n\ndata beta_row: String = \"x\"\n"; + +#[test] +fn a_derived_roster_binding_to_a_new_row_module_is_the_generators_declared_input() { + let report = compare( + "derived_input", + &[ + ("roster.dag", ROSTER), + ("alpha.dag", ALPHA_ROW), + ("beta.dag", BETA_ROW_ABSENT), + ], + &[ + ("roster.dag", ROSTER), + ("alpha.dag", ALPHA_ROW), + ("beta.dag", BETA_ROW), + ], + ); + assert!( + dispositions_for(&report, "beta_row") + .contains(&NamespaceDeltaDisposition::DerivedGeneratorInputResolution), + "a derived roster binding a module of its generator's declared input directory must be \ + DerivedGeneratorInputResolution, got: {:?}", + report.deltas + ); + // AND IT MUST ADJUDICATE. The classification is only the finding if the wall then admits it: + // this is the arm that unblocks every change adding a row file. + assert!( + report_unadjudicated(&report).is_empty(), + "the generator's own declared input must not need an authored admission, got: {:?}", + report_unadjudicated(&report) + ); +} + +#[test] +fn a_derived_roster_binding_outside_its_generators_inputs_still_refuses() { + // THE POSITIVE CONTROL, AND THE REASON THIS IS NOT A CATEGORY EXEMPTION. Same derived module, + // same `0 -> 1`, same absence of an author — but the module that began supplying the name is + // `gunbc.elsewhere`, which the roster's generator never reads. Nothing explains this binding, + // so it stays a coincidence and stays refused. If this arm ever greens, the wall has been + // widened rather than sharpened. + let report = compare( + "derived_outside", + &[ + ("roster.dag", OUTSIDE_ROSTER), + ("alpha.dag", ALPHA_ROW), + ("outside.dag", OUTSIDE_ABSENT), + ], + &[ + ("roster.dag", OUTSIDE_ROSTER), + ("alpha.dag", ALPHA_ROW), + ("outside.dag", OUTSIDE_SUPPLIES), + ], + ); + assert!( + dispositions_for(&report, "beta_row") + .contains(&NamespaceDeltaDisposition::NewPoolCoincidenceResolution), + "a derived module gaining a binding its generator's inputs do NOT explain must stay \ + NewPoolCoincidenceResolution, got: {:?}", + report.deltas + ); + assert!( + !report_unadjudicated(&report).is_empty(), + "an unexplained binding on a derived module must remain unadjudicated, got: {:?}", + report.deltas + ); +}