From 395ae126c6095b3cca1c84e8c33dd20456782d3d Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 00:03:46 +0000 Subject: [PATCH 01/30] Carry the refusal location on the catch-all assemble cause MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OtherAssembleRefusal is the catch-all arm of AssembleRejectionCause, and it carried only a reason symbol. A reason symbol does not say WHICH file refused, so recovering that meant re-lexing an entire import closure by hand — measured on use_site_verdict, whose 15-file closure had to be folded through tokenize one source at a time to learn that dag/std/algebra.dag was the refusing file. The location is carried as a probe-owned projection rather than a bare Locus. Locus admits NodeLocus and PortLocus, which have no manifest serialization; a host emitter that errored on them would abort a 27-module survey over one unrepresentable position. FrontierProbeCauseLocation is serializable by construction and total, and its unavailable arm carries a typed reason so the deficit is counted rather than fabricated or fatal. Location is kept structurally separate from the cause key: two modules refusing for the same reason at different positions are the same detailed cause, so a clustering key reads the cause and not this field. The survey binary is the third consumer and is updated with it — the DAG carrier change alone would have emitted a manifest that no longer typechecks against the new field, which is worse than dropping it. Co-Authored-By: Claude Opus 5 (1M context) --- .../stage0/src/bin/frontier_probe_survey.rs | 79 ++++++++++++- .../frontier_probe_assemble_detail.dag | 11 +- .../self_host/frontier_probe_types.dag | 19 ++- .../frontier_probe_cause_location_test.dag | 111 ++++++++++++++++++ 4 files changed, 215 insertions(+), 5 deletions(-) create mode 100644 src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag diff --git a/src/v1/stage0/src/bin/frontier_probe_survey.rs b/src/v1/stage0/src/bin/frontier_probe_survey.rs index 704d135a50b..4aa8d8f84af 100644 --- a/src/v1/stage0/src/bin/frontier_probe_survey.rs +++ b/src/v1/stage0/src/bin/frontier_probe_survey.rs @@ -415,6 +415,77 @@ fn non_empty_symbol_chain_emit(value: &Value, ctx: &InterpContext) -> Result Result { + match value { + Value::Variant { + variant_name, + fields, + .. + } => { + let name = ctx.resolve(*variant_name); + if name == "CauseLocationTextual" { + let file = ctx + .field(fields, "file") + .ok_or_else(|| format!("{name} missing file"))?; + let extent = ctx + .field(fields, "extent") + .ok_or_else(|| format!("{name} missing extent"))?; + Ok(format!( + "CauseLocationTextual {{ file: {}, extent: {} }}", + value_symbol_name(file)?, + extent_emit(extent, ctx)? + )) + } else if name == "CauseLocationUnavailable" { + let reason = ctx + .field(fields, "reason") + .ok_or_else(|| format!("{name} missing reason"))?; + Ok(format!( + "CauseLocationUnavailable {{ reason: {} }}", + value_symbol_name(reason)? + )) + } else { + Err(format!("unknown FrontierProbeCauseLocation variant {name}")) + } + } + _ => Err("cause location not a variant".to_string()), + } +} + +fn extent_emit(value: &Value, ctx: &InterpContext) -> Result { + match value { + Value::Variant { + variant_name, + fields, + .. + } => { + let name = ctx.resolve(*variant_name); + if name == "WholeFile" { + Ok("WholeFile".to_string()) + } else if name == "ByteRange" { + let start = ctx + .field(fields, "start") + .ok_or_else(|| format!("{name} missing start"))?; + let end = ctx + .field(fields, "end") + .ok_or_else(|| format!("{name} missing end"))?; + Ok(format!( + "ByteRange {{ start: {}, end: {} }}", + int_from_value(start)?, + int_from_value(end)? + )) + } else { + Err(format!("unknown Extent variant {name}")) + } + } + _ => Err("extent not a variant".to_string()), + } +} + fn assemble_rejection_cause_emit(value: &Value, ctx: &InterpContext) -> Result { match value { Value::Variant { @@ -465,9 +536,13 @@ fn assemble_rejection_cause_emit(value: &Value, ctx: &InterpContext) -> Result GrammarChoiceOverlap { rows: rows } - Absent => OtherAssembleRefusal { reason: d.head.diagnostic.reason } + Absent => OtherAssembleRefusal { + reason: d.head.diagnostic.reason, + at: frontier_probe_cause_location_from_locus(at: d.head.diagnostic.at) + } } } } @@ -278,7 +282,10 @@ fn assemble_rejection_cause_from_diagnostics( Absent => MissingModuleIdentityUnavailable } } else { - OtherAssembleRefusal { reason: d.head.reason } + OtherAssembleRefusal { + reason: d.head.reason, + at: frontier_probe_cause_location_from_locus(at: d.head.at) + } } } diff --git a/src/v2/compiler/self_host/frontier_probe_types.dag b/src/v2/compiler/self_host/frontier_probe_types.dag index 619271c1f1e..b4d2772e7d8 100644 --- a/src/v2/compiler/self_host/frontier_probe_types.dag +++ b/src/v2/compiler/self_host/frontier_probe_types.dag @@ -39,6 +39,7 @@ import v2.std.grammar_choice_ambiguity { } import v2.std.integer { Int } import v2.std.logic { Bool } +import v2.std.diagnostic { Extent, Locus, NodeLocus, PortLocus, Textual } import v2.std.node { Symbol } import v2.std.optional { Absent, Optional, Present } import v2.std.qualified_name { QualifiedName } @@ -104,7 +105,23 @@ type AssembleRejectionCause } | AssembleMissingModule { requested: QualifiedName } | MissingModuleIdentityUnavailable - | OtherAssembleRefusal { reason: Symbol } + | OtherAssembleRefusal { reason: Symbol, at: FrontierProbeCauseLocation } + +data frontier_probe_cause_location_note: String = "OtherAssembleRefusal is the catch-all arm, so its reason symbol alone does not say WHICH file refused — recovering that previously meant inferring across a whole closure. The location is carried here, but NOT as a bare Locus: Locus admits NodeLocus and PortLocus, which have no manifest serialization, and a host emitter that errored on them would abort an entire 27-module survey over one unrepresentable position. This probe-owned projection is serializable by construction and total, with the unavailable arm carrying a typed reason so the deficit is counted rather than fabricated or fatal. Location is kept structurally SEPARATE from the cause key: two modules refusing for the same reason at different positions are the same detailed cause, so a clustering key must read the cause and not this field." + +type FrontierProbeCauseLocation + = CauseLocationTextual { file: Symbol, extent: Extent } + | CauseLocationUnavailable { reason: Symbol } + +fn frontier_probe_cause_location_from_locus(at: Locus) -> FrontierProbeCauseLocation { + match at { + Textual { file: f, extent: e } => CauseLocationTextual { file: f, extent: e } + NodeLocus { anchor: _ } => + CauseLocationUnavailable { reason: ^cause_location_node_locus_not_serializable } + PortLocus { anchor: _ } => + CauseLocationUnavailable { reason: ^cause_location_port_locus_not_serializable } + } +} type FrontierProbeAssembleDetail = NoFrontierProbeDetail diff --git a/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag new file mode 100644 index 00000000000..54e4a4b0f81 --- /dev/null +++ b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag @@ -0,0 +1,111 @@ +module v2.test.claim.self_host.frontier_probe_cause_location + +import v2.compiler.parse_diagnostic { + NonEmptyParseStageDiagnostics, + parse_stage_diagnostic_from_diagnostic_only +} +import v2.compiler.self_host.frontier_probe_assemble_detail { + assemble_rejection_cause_from_parse_stage +} +import v2.compiler.self_host.frontier_probe_types { + AssembleMissingModule, + CauseLocationTextual, + CauseLocationUnavailable, + GrammarChoiceOverlap, + MaskedAssembleRefusal, + MaskedAssembleRefusalWithoutEvidence, + MissingModuleIdentityUnavailable, + OtherAssembleRefusal +} +import std.algebra { Empty } +import v2.std.diagnostic { + ByteRange, + Diagnostic, + Textual, + Unavailable, + UserInputBoundary, + WholeFile, + node_locus +} +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } + +data frontier_probe_cause_location_test_note: String = "Discriminating controls for the cause-location field on OtherAssembleRefusal. The field exists because that arm is the catch-all: its reason symbol alone does not say WHICH file refused, and recovering the file previously meant lexing an entire closure by hand. These are FIXTURES, not a pipeline walk, so they cost milliseconds and stay per-PR affordable. The byte range is asserted exactly, not merely for presence: an implementation that preserved the file but substituted WholeFile would satisfy a presence check while destroying the position that makes the field worth carrying. The Unavailable arm is exercised too, because Locus admits NodeLocus and PortLocus, which have no manifest serialization — the projection must classify them rather than error, so one unrepresentable position cannot abort a whole survey." + +data cause_location_probe_file: Symbol = ^cause_location_probe_file_symbol + +fn textual_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^cause_location_probe_unclassified_reason, + at: Textual { + file: cause_location_probe_file, + extent: ByteRange { start: 7, end: 9 } + }, + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn node_locus_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^cause_location_probe_unclassified_reason, + at: node_locus(node: Empty), + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn cause_from(d: Diagnostic) -> Symbol { + match assemble_rejection_cause_from_parse_stage( + d: NonEmptyParseStageDiagnostics { + head: parse_stage_diagnostic_from_diagnostic_only(d: d), + tail: Empty + } + ) { + GrammarChoiceOverlap { rows: _ } => ^not_other_refusal + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^not_other_refusal + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^not_other_refusal + AssembleMissingModule { requested: _ } => ^not_other_refusal + MissingModuleIdentityUnavailable => ^not_other_refusal + OtherAssembleRefusal { reason: _, at: a } => + match a { + CauseLocationTextual { file: f, extent: _ } => f + CauseLocationUnavailable { reason: u } => u + } + } +} + +fn textual_extent_start() -> Int { + match assemble_rejection_cause_from_parse_stage( + d: NonEmptyParseStageDiagnostics { + head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), + tail: Empty + } + ) { + GrammarChoiceOverlap { rows: _ } => 0 - 1 + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => 0 - 1 + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => 0 - 1 + AssembleMissingModule { requested: _ } => 0 - 1 + MissingModuleIdentityUnavailable => 0 - 1 + OtherAssembleRefusal { reason: _, at: a } => + match a { + CauseLocationTextual { file: _, extent: e } => + match e { + WholeFile => 0 - 2 + ByteRange { start: s, end: _ } => s + } + CauseLocationUnavailable { reason: _ } => 0 - 3 + } + } +} + +test fn textual_locus_preserves_the_file_holds() -> Bool { + cause_from(d: textual_diagnostic()) == cause_location_probe_file +} + +test fn textual_locus_preserves_the_exact_byte_range_holds() -> Bool { + textual_extent_start() == 7 +} + +test fn node_locus_projects_to_unavailable_rather_than_erroring_holds() -> Bool { + cause_from(d: node_locus_diagnostic()) == ^cause_location_node_locus_not_serializable +} From 2880549bab4f6baeef0df09739f3bdc198efe0ed Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 03:37:01 +0000 Subject: [PATCH 02/30] SH-D D0: move the refusal location onto AssembleRejectionDetail, close its variants The location was a field of OtherAssembleRefusal, which made a position structurally part of what the refusal IS: two modules refusing for the same reason at different positions read as different detailed causes at a clustering key. It now sits beside the cause on AssembleRejectionDetail. CauseLocationUnavailable { reason: Symbol } is replaced by the closed pair CauseLocationPort { port } and CauseLocationNodeNotSerializable. A PortLocus carries real serializable data and no longer degrades to a reason string; only a Node, which has no manifest rendering, reaches an unavailable arm. The survey emitter follows the field and the arms, and stays total. Controls (all executed): each of the three Locus arms reaches its own arm; the byte range is asserted exactly, so substituting WholeFile reds; and a two-diagnostic control pins reason and location to the SAME diagnostic (RED verified by asserting the tail's offset). Co-Authored-By: Claude Opus 5 (1M context) --- .../stage0/src/bin/frontier_probe_survey.rs | 45 +++--- .../frontier_probe_assemble_detail.dag | 19 ++- .../self_host/frontier_probe_types.dag | 22 +-- ...irect_rust_door_real_module_probe_test.dag | 84 +++++++++++ .../long/frontier_probe_lex_localizer.dag | 34 +++++ .../frontier_probe_real_module_cause_test.dag | 117 +++++++++++++++ ...frontier_probe_real_module_reason_test.dag | 63 ++++++++ .../long/frontier_probe_reason_readout.dag | 99 +++++++++++++ ...er_probe_cause_location_roundtrip_test.dag | 37 +++++ .../frontier_probe_cause_location_test.dag | 136 ++++++++++++------ .../frontier_probe_overlap_detail_test.dag | 6 +- .../v2_tokenize_annotation_probe_test.dag | 23 +++ ...v2_tokenize_comment_payload_probe_test.dag | 30 ++++ .../v2_tokenize_non_ascii_probe_test.dag | 64 +++++++++ ..._probe_cause_location_manifest_fixture.dag | 36 +++++ 15 files changed, 730 insertions(+), 85 deletions(-) create mode 100644 src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag create mode 100644 src/v2/test/claim/long/frontier_probe_lex_localizer.dag create mode 100644 src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag create mode 100644 src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag create mode 100644 src/v2/test/claim/long/frontier_probe_reason_readout.dag create mode 100644 src/v2/test/claim/self_host/frontier_probe_cause_location_roundtrip_test.dag create mode 100644 src/v2/test/claim/v2_tokenize_annotation_probe_test.dag create mode 100644 src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag create mode 100644 src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag create mode 100644 src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag diff --git a/src/v1/stage0/src/bin/frontier_probe_survey.rs b/src/v1/stage0/src/bin/frontier_probe_survey.rs index 4aa8d8f84af..139997d9b97 100644 --- a/src/v1/stage0/src/bin/frontier_probe_survey.rs +++ b/src/v1/stage0/src/bin/frontier_probe_survey.rs @@ -415,11 +415,12 @@ fn non_empty_symbol_chain_emit(value: &Value, ctx: &InterpContext) -> Result Result { match value { Value::Variant { @@ -436,18 +437,20 @@ fn cause_location_emit(value: &Value, ctx: &InterpContext) -> Result Result Result GrammarChoiceOverlap { rows: rows } - Absent => OtherAssembleRefusal { - reason: d.head.diagnostic.reason, - at: frontier_probe_cause_location_from_locus(at: d.head.diagnostic.at) - } + Absent => OtherAssembleRefusal { reason: d.head.diagnostic.reason } } } } @@ -282,10 +279,7 @@ fn assemble_rejection_cause_from_diagnostics( Absent => MissingModuleIdentityUnavailable } } else { - OtherAssembleRefusal { - reason: d.head.reason, - at: frontier_probe_cause_location_from_locus(at: d.head.at) - } + OtherAssembleRefusal { reason: d.head.reason } } } @@ -322,7 +316,8 @@ fn frontier_probe_assemble_detail_from_parse_stage( ) -> FrontierProbeAssembleDetail { AssembleRejectionDetail { rejection_chain: non_empty_symbol_chain_from_non_empty_parse_stage(d: d), - cause: assemble_rejection_cause_from_parse_stage(d: d) + cause: assemble_rejection_cause_from_parse_stage(d: d), + at: frontier_probe_cause_location_from_locus(at: d.head.diagnostic.at) } } @@ -331,7 +326,8 @@ fn frontier_probe_assemble_detail_from_rejection( ) -> FrontierProbeAssembleDetail { AssembleRejectionDetail { rejection_chain: non_empty_symbol_chain_from_non_empty_diagnostics(d: d), - cause: assemble_rejection_cause_from_diagnostics(d: d) + cause: assemble_rejection_cause_from_diagnostics(d: d), + at: frontier_probe_cause_location_from_locus(at: d.head.at) } } @@ -346,7 +342,8 @@ fn frontier_probe_assemble_detail_from_rejection_with_overlap_stage( rejection_chain: non_empty_symbol_chain_from_non_empty_diagnostics(d: d), cause: assemble_rejection_cause_from_parse_stage( d: non_empty_parse_stage_diagnostics_merged(d: d, overlap_stage: stage) - ) + ), + at: frontier_probe_cause_location_from_locus(at: d.head.at) } } } diff --git a/src/v2/compiler/self_host/frontier_probe_types.dag b/src/v2/compiler/self_host/frontier_probe_types.dag index b4d2772e7d8..0c4ce5c5b9d 100644 --- a/src/v2/compiler/self_host/frontier_probe_types.dag +++ b/src/v2/compiler/self_host/frontier_probe_types.dag @@ -39,6 +39,7 @@ import v2.std.grammar_choice_ambiguity { } import v2.std.integer { Int } import v2.std.logic { Bool } +import v2.std.compilers.lexing { symbol_lexeme } import v2.std.diagnostic { Extent, Locus, NodeLocus, PortLocus, Textual } import v2.std.node { Symbol } import v2.std.optional { Absent, Optional, Present } @@ -105,21 +106,21 @@ type AssembleRejectionCause } | AssembleMissingModule { requested: QualifiedName } | MissingModuleIdentityUnavailable - | OtherAssembleRefusal { reason: Symbol, at: FrontierProbeCauseLocation } + | OtherAssembleRefusal { reason: Symbol } -data frontier_probe_cause_location_note: String = "OtherAssembleRefusal is the catch-all arm, so its reason symbol alone does not say WHICH file refused — recovering that previously meant inferring across a whole closure. The location is carried here, but NOT as a bare Locus: Locus admits NodeLocus and PortLocus, which have no manifest serialization, and a host emitter that errored on them would abort an entire 27-module survey over one unrepresentable position. This probe-owned projection is serializable by construction and total, with the unavailable arm carrying a typed reason so the deficit is counted rather than fabricated or fatal. Location is kept structurally SEPARATE from the cause key: two modules refusing for the same reason at different positions are the same detailed cause, so a clustering key must read the cause and not this field." +data frontier_probe_cause_location_note: String = "OtherAssembleRefusal is the catch-all arm, so its reason symbol alone does not say WHICH file refused — recovering that previously meant inferring across a whole closure. The location is carried here, but NOT as a bare Locus: Locus admits NodeLocus and PortLocus, which have no manifest serialization, and a host emitter that errored on them would abort an entire 27-module survey over one unrepresentable position. The file is a String, not a Symbol: a path contains slashes and dots, and an emitted bare ^dag/std/algebra.dag does not re-parse — the front end reads it as division and projection, so the manifest would emit but never reload. Every other emitted manifest carries paths as Strings for the same reason. This probe-owned projection is serializable by construction and total, with the unavailable arm carrying a typed reason so the deficit is counted rather than fabricated or fatal. Location sits on AssembleRejectionDetail, NOT inside AssembleRejectionCause. Two modules refusing for the same reason at different positions are the SAME detailed cause, so a clustering key reads the cause — and the only way to guarantee a key cannot accidentally include position is for position not to be part of the cause value at all. A note asserting the separation while the field lived inside the cause would have been a claim the structure contradicted. The port arm is kept as real data rather than folded into an unavailable arm, because a port IS serializable; only a Node has no manifest rendering." type FrontierProbeCauseLocation - = CauseLocationTextual { file: Symbol, extent: Extent } - | CauseLocationUnavailable { reason: Symbol } + = CauseLocationTextual { file: String, extent: Extent } + | CauseLocationPort { port: Symbol } + | CauseLocationNodeNotSerializable fn frontier_probe_cause_location_from_locus(at: Locus) -> FrontierProbeCauseLocation { match at { - Textual { file: f, extent: e } => CauseLocationTextual { file: f, extent: e } - NodeLocus { anchor: _ } => - CauseLocationUnavailable { reason: ^cause_location_node_locus_not_serializable } - PortLocus { anchor: _ } => - CauseLocationUnavailable { reason: ^cause_location_port_locus_not_serializable } + Textual { file: f, extent: e } => + CauseLocationTextual { file: symbol_lexeme(sym: f), extent: e } + NodeLocus { anchor: _ } => CauseLocationNodeNotSerializable + PortLocus { anchor: p } => CauseLocationPort { port: p.at } } } @@ -127,7 +128,8 @@ type FrontierProbeAssembleDetail = NoFrontierProbeDetail | AssembleRejectionDetail { rejection_chain: NonEmptySymbolChain, - cause: AssembleRejectionCause + cause: AssembleRejectionCause, + at: FrontierProbeCauseLocation } data frontier_probe_overlap_detail_note: String = "AssembleRejectionDetail binds only parse-stage-carried evidence — never a global grammar roster. GrammarChoiceOverlap rows are extracted from ParseDiagnosticEvidence on the parse-stage diagnostic carrier. When overlap residue heads a chain with a nonempty tail and carries evidence, cause is MaskedAssembleRefusal; when the tail is nonempty but evidence is absent, MaskedAssembleRefusalWithoutEvidence preserves remaining_chain without absorbing into OtherAssembleRefusal. AssembleMissingModule in assemble_detail awaits parse-stage ModuleRootMissingEvidence; FrontierProbeDetail.MissingModule (#7762) is the structural resolve_module_not_found authority on detail. MissingModuleIdentityUnavailable remains when assemble cause cannot bind parse-stage evidence. Emit-stage receipts use NoFrontierProbeDetail." diff --git a/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag b/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag new file mode 100644 index 00000000000..6ec5d5f6965 --- /dev/null +++ b/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag @@ -0,0 +1,84 @@ +module v2.test.long.direct_rust_door_real_module_probe + +import v2.compiler.self_host.candidate_generation { + ArtifactProduced, + AssemblyRejected, + EmissionRejected, + InferenceRejected, + ProducerReceiptRejected, + ProvenancedRustEmissionObservation, + observe_provenanced_rust_emission_from_ingest +} +import v2.extdeps.languages.rust { rust_target_model } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.logic { Bool } +import v2.std.text { String } +import v2.test.workflow.host_source_root_ingest_manifest { + host_compiler_closure_admission, + host_source_root_ingest +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data direct_rust_door_real_module_probe_note: String = "SH-D D1-zero instrument: drives the v2 direct Rust door (observe_provenanced_rust_emission_from_ingest) against a REAL compiler module's import closure, supplied by the host_source_root_ingest_manifest overlay that discover_source_root_ingest --entry writes. This is the first real-module use of the door: direct_rust_door_production_group is hardwired to direct_rust_door_fixture (direct_rust_door_module_path names the fixture file), so no existing consumer observes a production module through it. Five mutually exclusive one-arm probes, not one pass/fail: a Bool witness cannot report WHICH stage stopped, so the arm that greens IS the measurement and the other four must red. Run per module via the local recipe below; the module under test is whichever entry the overlay manifest was generated for, which is why no module path is named here. LOCAL RECIPE: (1) discover_source_root_ingest --source-root src/v2 --source-root dag --entry --emit-manifest target/door-probe/host_source_root_ingest_manifest.dag; (2) claim_batch --source-root src/v2 --source-root dag --source-root target/door-probe --entry src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag --functions . NOT ADMITTED for CI: walks the real pipeline, so it needs an explicit gunbc.explicit_witness_admission row with a declared cost before any cadence carries it — a long/ path is not admission." + +fn direct_rust_door_real_module_observation() -> ProvenancedRustEmissionObservation { + observe_provenanced_rust_emission_from_ingest( + ingest: host_source_root_ingest, + admission: host_compiler_closure_admission, + target: rust_target_model(), + module_path: direct_rust_door_real_module_path + ) +} + +data direct_rust_door_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" + +test fn real_module_door_stops_at_assembly() -> Bool { + match direct_rust_door_real_module_observation() { + AssemblyRejected { diagnostics: _ } => true + InferenceRejected { diagnostics: _ } => false + EmissionRejected { diagnostics: _ } => false + ProducerReceiptRejected { diagnostics: _ } => false + ArtifactProduced { artifact: _ } => false + } +} + +test fn real_module_door_stops_at_inference() -> Bool { + match direct_rust_door_real_module_observation() { + AssemblyRejected { diagnostics: _ } => false + InferenceRejected { diagnostics: _ } => true + EmissionRejected { diagnostics: _ } => false + ProducerReceiptRejected { diagnostics: _ } => false + ArtifactProduced { artifact: _ } => false + } +} + +test fn real_module_door_stops_at_emission() -> Bool { + match direct_rust_door_real_module_observation() { + AssemblyRejected { diagnostics: _ } => false + InferenceRejected { diagnostics: _ } => false + EmissionRejected { diagnostics: _ } => true + ProducerReceiptRejected { diagnostics: _ } => false + ArtifactProduced { artifact: _ } => false + } +} + +test fn real_module_door_stops_at_producer_receipt() -> Bool { + match direct_rust_door_real_module_observation() { + AssemblyRejected { diagnostics: _ } => false + InferenceRejected { diagnostics: _ } => false + EmissionRejected { diagnostics: _ } => false + ProducerReceiptRejected { diagnostics: _ } => true + ArtifactProduced { artifact: _ } => false + } +} + +test fn real_module_door_produces_artifact() -> Bool { + match direct_rust_door_real_module_observation() { + AssemblyRejected { diagnostics: _ } => false + InferenceRejected { diagnostics: _ } => false + EmissionRejected { diagnostics: _ } => false + ProducerReceiptRejected { diagnostics: _ } => false + ArtifactProduced { artifact: _ } => true + } +} diff --git a/src/v2/test/claim/long/frontier_probe_lex_localizer.dag b/src/v2/test/claim/long/frontier_probe_lex_localizer.dag new file mode 100644 index 00000000000..467d9992e96 --- /dev/null +++ b/src/v2/test/claim/long/frontier_probe_lex_localizer.dag @@ -0,0 +1,34 @@ +module v2.test.long.frontier_probe_lex_localizer + +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.algebra { fold_list } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.text { String } +import v2.test.workflow.host_source_root_ingest_manifest { host_source_root_ingest } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data frontier_probe_lex_localizer_note: String = "SH-D localizer, NOT a witness. The use_site_verdict closure measures ^tokenize_lex_e1_unrecognized_char and two guesses have already been refuted by execution: non-ASCII inside string literals tokenizes fine (emoji and arrow, ASCII control accepting), and a standalone leading // annotation block tokenizes fine. Guessing is the wrong instrument. This folds the SAME source text the ingest carries through v2 tokenize per file and returns the first file_path that refuses, so the search is one run over the real bytes rather than a sequence of hypotheses. Returns the empty string if every file tokenizes, which would relocate the refusal to assembly rather than lexing and is itself a decisive answer." + +fn first_failing_lex_file() -> String { + fold_list( + xs: host_source_root_ingest, + empty: "", + cons: fn(acc, r) { + if acc != "" { + acc + } else { + match tokenize( + text: r.source.carried, + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => "" + Rejected { diagnostics: _ } => r.artifact.file_path + } + } + } + ) +} diff --git a/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag b/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag new file mode 100644 index 00000000000..73671cb4f9f --- /dev/null +++ b/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag @@ -0,0 +1,117 @@ +module v2.test.long.frontier_probe_real_module_cause + +import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } +import v2.compiler.self_host.frontier_probe_types { + AssembleMissingModule, + AssembleRejectionDetail, + FrontierProbeReceipt, + GrammarChoiceOverlap, + MaskedAssembleRefusal, + MaskedAssembleRefusalWithoutEvidence, + MissingModuleIdentityUnavailable, + NoFrontierProbeDetail, + OtherAssembleRefusal +} +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.logic { Bool } +import v2.std.text { String } +import v2.test.workflow.host_source_root_ingest_manifest { + host_compiler_closure_admission, + host_source_root_ingest +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data frontier_probe_real_module_cause_note: String = "SH-D D1 detailed-cause instrument: runs frontier_probe_emit_from_ingest against a REAL module closure supplied by the host_source_root_ingest_manifest overlay, and discriminates the AssembleRejectionCause variant the receipt actually carries. This is the DETAILED grain the operator ruling requires and the survey does not currently produce: frontier_gap_clustering_from_receipts counts blocker CLASSES and then supplies a hardcoded located_reason literal per branch, so it cannot distinguish two modules whose ambiguity rows differ. One arm per variant because a Bool witness cannot report a variant; the arm that greens IS the measurement. Same overlay recipe as direct_rust_door_real_module_probe. NOT ADMITTED for CI: real-pipeline walk, needs an explicit admission row with declared cost." + +fn frontier_probe_real_module_receipt() -> FrontierProbeReceipt { + frontier_probe_emit_from_ingest( + module_path: frontier_probe_real_module_path, + ingest: host_source_root_ingest, + admission: host_compiler_closure_admission + ) +} + +data frontier_probe_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" + +test fn real_module_cause_is_grammar_choice_overlap() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => false + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => true + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false + AssembleMissingModule { requested: _ } => false + MissingModuleIdentityUnavailable => false + OtherAssembleRefusal { reason: _, at: _ } => false + } + } +} + +test fn real_module_cause_is_masked_with_evidence() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => false + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => false + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => true + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false + AssembleMissingModule { requested: _ } => false + MissingModuleIdentityUnavailable => false + OtherAssembleRefusal { reason: _, at: _ } => false + } + } +} + +test fn real_module_cause_is_masked_without_evidence() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => false + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => false + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => true + AssembleMissingModule { requested: _ } => false + MissingModuleIdentityUnavailable => false + OtherAssembleRefusal { reason: _, at: _ } => false + } + } +} + +test fn real_module_cause_is_missing_module() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => false + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => false + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false + AssembleMissingModule { requested: _ } => true + MissingModuleIdentityUnavailable => true + OtherAssembleRefusal { reason: _, at: _ } => false + } + } +} + +test fn real_module_cause_is_other_assemble_refusal() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => false + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => false + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false + AssembleMissingModule { requested: _ } => false + MissingModuleIdentityUnavailable => false + OtherAssembleRefusal { reason: _, at: _ } => true + } + } +} + +test fn real_module_assemble_detail_absent() -> Bool { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => true + AssembleRejectionDetail { rejection_chain: _, cause: _, at: _ } => false + } +} diff --git a/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag b/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag new file mode 100644 index 00000000000..000d864f5d4 --- /dev/null +++ b/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag @@ -0,0 +1,63 @@ +module v2.test.long.frontier_probe_real_module_reason + +import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } +import v2.compiler.self_host.frontier_probe_types { + AssembleMissingModule, + AssembleRejectionDetail, + FrontierProbeReceipt, + GrammarChoiceOverlap, + MaskedAssembleRefusal, + MaskedAssembleRefusalWithoutEvidence, + MissingModuleIdentityUnavailable, + NoFrontierProbeDetail, + OtherAssembleRefusal +} +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } +import v2.test.workflow.host_source_root_ingest_manifest { + host_compiler_closure_admission, + host_source_root_ingest +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data frontier_probe_real_module_reason_note: String = "SH-D D1 follow-up: the cause probe established that use_site_verdict's assemble rejection binds OtherAssembleRefusal (the catch-all), NOT GrammarChoiceOverlap, while its frontier row names located_reason ^parse_grammar_choice_overlap_residue. Two readings remain open and this instrument separates them: either the underlying reason symbol genuinely differs from the row (the row is misattributed), or the reason symbol matches and only the OVERLAP EVIDENCE failed to bind, which routes to OtherAssembleRefusal per frontier_probe_overlap_detail_note. Those have different owners and different repairs, so the symbol itself is read here rather than inferred. Also reads receipt.located_reason directly, since that is the field the survey clusters on." + +fn frontier_probe_real_module_receipt() -> FrontierProbeReceipt { + frontier_probe_emit_from_ingest( + module_path: frontier_probe_real_module_path, + ingest: host_source_root_ingest, + admission: host_compiler_closure_admission + ) +} + +data frontier_probe_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" + +fn real_module_other_refusal_reason() -> Symbol { + match frontier_probe_real_module_receipt().assemble_detail { + NoFrontierProbeDetail => ^no_assemble_detail + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => ^bound_grammar_choice_overlap + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^bound_masked_with_evidence + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^bound_masked_without_evidence + AssembleMissingModule { requested: _ } => ^bound_missing_module + MissingModuleIdentityUnavailable => ^bound_missing_module_identity + OtherAssembleRefusal { reason: r, at: _ } => r + } + } +} + +test fn other_refusal_reason_is_parse_grammar_choice_overlap_residue() -> Bool { + real_module_other_refusal_reason() == ^parse_grammar_choice_overlap_residue +} + +test fn located_reason_is_parse_grammar_choice_overlap_residue() -> Bool { + frontier_probe_real_module_receipt().located_reason == ^parse_grammar_choice_overlap_residue +} + +test fn other_refusal_reason_is_resolve_module_not_found() -> Bool { + real_module_other_refusal_reason() == ^resolve_module_not_found +} diff --git a/src/v2/test/claim/long/frontier_probe_reason_readout.dag b/src/v2/test/claim/long/frontier_probe_reason_readout.dag new file mode 100644 index 00000000000..2a0e5107ed6 --- /dev/null +++ b/src/v2/test/claim/long/frontier_probe_reason_readout.dag @@ -0,0 +1,99 @@ +module v2.test.long.frontier_probe_reason_readout + +import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } +import v2.compiler.self_host.frontier_probe_types { + AssembleMissingModule, + AssembleRejectionDetail, + FrontierProbeReceipt, + GrammarChoiceOverlap, + MaskedAssembleRefusal, + MaskedAssembleRefusalWithoutEvidence, + MissingModuleIdentityUnavailable, + NoFrontierProbeDetail, + OtherAssembleRefusal, + CauseLocationTextual, + CauseLocationUnavailable +} +import v2.std.diagnostic { ByteRange, WholeFile } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.node { Symbol } +import v2.std.text { String } +import v2.test.workflow.host_source_root_ingest_manifest { + host_compiler_closure_admission, + host_source_root_ingest +} + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data frontier_probe_reason_readout_note: String = "SH-D readout entry, NOT a witness: three equality probes established that use_site_verdict's measured located_reason is neither ^parse_grammar_choice_overlap_residue (the symbol its frontier row records) nor ^resolve_module_not_found, and that its assemble cause is OtherAssembleRefusal. Guessing further symbols at ~80s per equality test is the wrong instrument, so these fns RETURN the symbol for gunbc run to print. No test fn here on purpose: this reads a value, it does not assert one." + +fn readout_receipt() -> FrontierProbeReceipt { + frontier_probe_emit_from_ingest( + module_path: readout_module_path, + ingest: host_source_root_ingest, + admission: host_compiler_closure_admission + ) +} + +data readout_module_path: String = "src/v2/compiler/use_site_verdict.dag" + +fn readout_located_reason() -> Symbol { + readout_receipt().located_reason +} + +fn readout_assemble_cause_reason() -> Symbol { + match readout_receipt().assemble_detail { + NoFrontierProbeDetail => ^no_assemble_detail + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => ^bound_grammar_choice_overlap + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^bound_masked_with_evidence + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^bound_masked_without_evidence + AssembleMissingModule { requested: _ } => ^bound_missing_module + MissingModuleIdentityUnavailable => ^bound_missing_module_identity + OtherAssembleRefusal { reason: r, at: _ } => r + } + } +} + +fn readout_assemble_cause_locus_file() -> Symbol { + match readout_receipt().assemble_detail { + NoFrontierProbeDetail => ^no_assemble_detail + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => ^locus_not_carried_overlap + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^locus_not_carried_masked + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^locus_not_carried_masked_bare + AssembleMissingModule { requested: _ } => ^locus_not_carried_missing_module + MissingModuleIdentityUnavailable => ^locus_not_carried_missing_identity + OtherAssembleRefusal { reason: _, at: a } => + match a { + CauseLocationTextual { file: f, extent: _ } => f + CauseLocationUnavailable { reason: u } => u + } + } + } +} + +fn readout_assemble_cause_locus_extent() -> Symbol { + match readout_receipt().assemble_detail { + NoFrontierProbeDetail => ^no_assemble_detail + AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => + match c { + GrammarChoiceOverlap { rows: _ } => ^not_other_refusal + MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^not_other_refusal + MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^not_other_refusal + AssembleMissingModule { requested: _ } => ^not_other_refusal + MissingModuleIdentityUnavailable => ^not_other_refusal + OtherAssembleRefusal { reason: _, at: a } => + match a { + CauseLocationTextual { file: _, extent: e } => + match e { + WholeFile => ^extent_whole_file + ByteRange { start: _, end: _ } => ^extent_byte_range + } + CauseLocationUnavailable { reason: u } => u + } + } + } +} diff --git a/src/v2/test/claim/self_host/frontier_probe_cause_location_roundtrip_test.dag b/src/v2/test/claim/self_host/frontier_probe_cause_location_roundtrip_test.dag new file mode 100644 index 00000000000..2ab799f0386 --- /dev/null +++ b/src/v2/test/claim/self_host/frontier_probe_cause_location_roundtrip_test.dag @@ -0,0 +1,37 @@ +module v2.test.claim.self_host.frontier_probe_cause_location_roundtrip + +import v2.compiler.self_host.frontier_probe_types { + AssembleRejectionDetail, + CauseLocationNodeNotSerializable, + CauseLocationPort, + CauseLocationTextual, + NoFrontierProbeDetail +} +import v2.test.fixture.frontier_probe_cause_location_manifest_fixture { + frontier_probe_cause_location_emitted_receipt +} +import v2.std.diagnostic { ByteRange, WholeFile } +import v2.std.logic { Bool } +import v2.std.text { String } + +data frontier_probe_cause_location_roundtrip_note: String = "RELOAD half of the emit-reload round trip. The fixture beside this file is verbatim survey-bin output; these claims prove that output parses, typechecks against the live carrier, and still carries the location after a front-end round trip. Emitting a location the manifest cannot reload is worse than dropping it, because it fails at reload rather than at the edit — that is the exact defect this pair exists to catch." + +fn roundtrip_extent_start() -> Int { + match frontier_probe_cause_location_emitted_receipt.assemble_detail { + NoFrontierProbeDetail => 0 - 1 + AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => + match a { + CauseLocationTextual { file: _, extent: e } => + match e { + WholeFile => 0 - 3 + ByteRange { start: s, end: _ } => s + } + CauseLocationPort { port: _ } => 0 - 4 + CauseLocationNodeNotSerializable => 0 - 5 + } + } +} + +test fn emitted_manifest_reloads_with_its_location_intact_holds() -> Bool { + roundtrip_extent_start() == 28138 +} diff --git a/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag index 54e4a4b0f81..c7a14cf87e5 100644 --- a/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag +++ b/src/v2/test/claim/self_host/frontier_probe_cause_location_test.dag @@ -5,19 +5,17 @@ import v2.compiler.parse_diagnostic { parse_stage_diagnostic_from_diagnostic_only } import v2.compiler.self_host.frontier_probe_assemble_detail { - assemble_rejection_cause_from_parse_stage + frontier_probe_assemble_detail_from_parse_stage } import v2.compiler.self_host.frontier_probe_types { - AssembleMissingModule, + AssembleRejectionDetail, + NonEmptySymbolChain, + CauseLocationNodeNotSerializable, + CauseLocationPort, CauseLocationTextual, - CauseLocationUnavailable, - GrammarChoiceOverlap, - MaskedAssembleRefusal, - MaskedAssembleRefusalWithoutEvidence, - MissingModuleIdentityUnavailable, - OtherAssembleRefusal -} -import std.algebra { Empty } + NoFrontierProbeDetail +} +import std.algebra { Cons, Empty } import v2.std.diagnostic { ByteRange, Diagnostic, @@ -25,21 +23,37 @@ import v2.std.diagnostic { Unavailable, UserInputBoundary, WholeFile, - node_locus + node_locus, + port_locus } import v2.std.logic { Bool } import v2.std.node { Symbol } import v2.std.text { String } -data frontier_probe_cause_location_test_note: String = "Discriminating controls for the cause-location field on OtherAssembleRefusal. The field exists because that arm is the catch-all: its reason symbol alone does not say WHICH file refused, and recovering the file previously meant lexing an entire closure by hand. These are FIXTURES, not a pipeline walk, so they cost milliseconds and stay per-PR affordable. The byte range is asserted exactly, not merely for presence: an implementation that preserved the file but substituted WholeFile would satisfy a presence check while destroying the position that makes the field worth carrying. The Unavailable arm is exercised too, because Locus admits NodeLocus and PortLocus, which have no manifest serialization — the projection must classify them rather than error, so one unrepresentable position cannot abort a whole survey." +data frontier_probe_cause_location_test_note: String = "Discriminating controls for the refusal location on AssembleRejectionDetail. The location exists because a reason symbol alone does not say WHICH file refused: recovering that previously meant folding tokenize over an entire import closure by hand. These are FIXTURES, not a pipeline walk, so they cost milliseconds. The byte range is asserted EXACTLY rather than for presence — an implementation that kept the file but substituted WholeFile would pass a presence check while destroying the position that makes the field worth carrying, and that substitution is the executed RED for these claims. All three Locus arms are exercised: a port is real serializable data and keeps its symbol, and only a Node, which has no manifest rendering, reaches the not-serializable arm. A two-diagnostic control pins the PAIRING: with a second diagnostic carrying a different reason at a different position, the reason and the location must both come from the head, so neither field can be drawn from a diagnostic the other did not come from. The location is asserted on the DETAIL and never inside the cause, because a clustering key reads the cause and two modules refusing for the same reason at different positions are the same detailed cause." -data cause_location_probe_file: Symbol = ^cause_location_probe_file_symbol +fn detail_location_of(d: Diagnostic) -> Symbol { + match frontier_probe_assemble_detail_from_parse_stage( + d: NonEmptyParseStageDiagnostics { + head: parse_stage_diagnostic_from_diagnostic_only(d: d), + tail: Empty + } + ) { + NoFrontierProbeDetail => ^no_detail + AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => + match a { + CauseLocationTextual { file: _, extent: _ } => ^location_is_textual + CauseLocationPort { port: p } => p + CauseLocationNodeNotSerializable => ^location_is_node_not_serializable + } + } +} fn textual_diagnostic() -> Diagnostic { Diagnostic { reason: ^cause_location_probe_unclassified_reason, at: Textual { - file: cause_location_probe_file, + file: ^cause_location_probe_file_symbol, extent: ByteRange { start: 7, end: 9 } }, correction: Unavailable { reason: UserInputBoundary } @@ -54,58 +68,98 @@ fn node_locus_diagnostic() -> Diagnostic { } } -fn cause_from(d: Diagnostic) -> Symbol { - match assemble_rejection_cause_from_parse_stage( +fn port_locus_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^cause_location_probe_unclassified_reason, + at: port_locus(port: ^cause_location_probe_port), + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn textual_extent_start() -> Int { + match frontier_probe_assemble_detail_from_parse_stage( d: NonEmptyParseStageDiagnostics { - head: parse_stage_diagnostic_from_diagnostic_only(d: d), + head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), tail: Empty } ) { - GrammarChoiceOverlap { rows: _ } => ^not_other_refusal - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^not_other_refusal - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^not_other_refusal - AssembleMissingModule { requested: _ } => ^not_other_refusal - MissingModuleIdentityUnavailable => ^not_other_refusal - OtherAssembleRefusal { reason: _, at: a } => + NoFrontierProbeDetail => 0 - 1 + AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => match a { - CauseLocationTextual { file: f, extent: _ } => f - CauseLocationUnavailable { reason: u } => u + CauseLocationTextual { file: _, extent: e } => + match e { + WholeFile => 0 - 2 + ByteRange { start: s, end: _ } => s + } + CauseLocationPort { port: _ } => 0 - 3 + CauseLocationNodeNotSerializable => 0 - 4 } } } -fn textual_extent_start() -> Int { - match assemble_rejection_cause_from_parse_stage( - d: NonEmptyParseStageDiagnostics { - head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), +fn two_diagnostic_stage() -> NonEmptyParseStageDiagnostics { + NonEmptyParseStageDiagnostics { + head: parse_stage_diagnostic_from_diagnostic_only(d: textual_diagnostic()), + tail: Cons { + head: parse_stage_diagnostic_from_diagnostic_only(d: later_diagnostic()), tail: Empty } - ) { - GrammarChoiceOverlap { rows: _ } => 0 - 1 - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => 0 - 1 - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => 0 - 1 - AssembleMissingModule { requested: _ } => 0 - 1 - MissingModuleIdentityUnavailable => 0 - 1 - OtherAssembleRefusal { reason: _, at: a } => + } +} + +fn later_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^cause_location_probe_second_reason, + at: Textual { + file: ^cause_location_probe_other_file_symbol, + extent: ByteRange { start: 99, end: 101 } + }, + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn paired_start_of_two() -> Int { + match frontier_probe_assemble_detail_from_parse_stage(d: two_diagnostic_stage()) { + NoFrontierProbeDetail => 0 - 1 + AssembleRejectionDetail { rejection_chain: _, cause: _, at: a } => match a { CauseLocationTextual { file: _, extent: e } => match e { WholeFile => 0 - 2 ByteRange { start: s, end: _ } => s } - CauseLocationUnavailable { reason: _ } => 0 - 3 + CauseLocationPort { port: _ } => 0 - 3 + CauseLocationNodeNotSerializable => 0 - 4 } } } -test fn textual_locus_preserves_the_file_holds() -> Bool { - cause_from(d: textual_diagnostic()) == cause_location_probe_file +fn paired_reason_of_two() -> Symbol { + match frontier_probe_assemble_detail_from_parse_stage(d: two_diagnostic_stage()) { + NoFrontierProbeDetail => ^no_detail + AssembleRejectionDetail { rejection_chain: c, cause: _, at: _ } => + match c { + NonEmptySymbolChain { head: h, tail: _ } => h + } + } +} + +test fn location_and_reason_are_drawn_from_the_same_diagnostic_holds() -> Bool { + paired_start_of_two() == 7 && paired_reason_of_two() == ^cause_location_probe_unclassified_reason +} + +test fn textual_locus_reaches_the_textual_arm_holds() -> Bool { + detail_location_of(d: textual_diagnostic()) == ^location_is_textual } test fn textual_locus_preserves_the_exact_byte_range_holds() -> Bool { textual_extent_start() == 7 } -test fn node_locus_projects_to_unavailable_rather_than_erroring_holds() -> Bool { - cause_from(d: node_locus_diagnostic()) == ^cause_location_node_locus_not_serializable +test fn port_locus_keeps_its_port_rather_than_degrading_holds() -> Bool { + detail_location_of(d: port_locus_diagnostic()) == ^cause_location_probe_port +} + +test fn node_locus_reaches_the_not_serializable_arm_holds() -> Bool { + detail_location_of(d: node_locus_diagnostic()) == ^location_is_node_not_serializable } diff --git a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag b/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag index bdb4b04a9c3..9469b9bf594 100644 --- a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag +++ b/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag @@ -65,7 +65,8 @@ test fn overlap_evidence_binds_exact_row_not_global_roster() -> Bool { match detail { AssembleRejectionDetail { cause: GrammarChoiceOverlap { rows: rows }, - rejection_chain: _ + rejection_chain: _, + at: _ } => match rows { NonEmptyGrammarChoiceAmbiguityRows { @@ -164,7 +165,8 @@ test fn overlap_stage_attaches_evidence_when_diagnostics_stripped() -> Bool { match detail { AssembleRejectionDetail { cause: GrammarChoiceOverlap { rows: rows }, - rejection_chain: _ + rejection_chain: _, + at: _ } => match rows { NonEmptyGrammarChoiceAmbiguityRows { diff --git a/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag b/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag new file mode 100644 index 00000000000..155f3a3c0af --- /dev/null +++ b/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag @@ -0,0 +1,23 @@ +module v2.test.claim.v2_tokenize_annotation_probe + +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.logic { Bool } +import v2.std.text { String } + +data v2_tokenize_annotation_probe_note: String = "SH-D: does the v2 lexer admit the standalone leading // annotation block DESIGN 4c describes? Two files in the use_site_verdict closure carry them (dag/std/algebra.dag, dag/std/dissolution.dag), and the closure measures ^tokenize_lex_e1_unrecognized_char." + +test fn leading_annotation_block_tokenizes() -> Bool { + match tokenize(text: "// note\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn leading_annotation_block_rejects_as_unrecognized_char() -> Bool { + match tokenize(text: "// note\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char + } +} diff --git a/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag b/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag new file mode 100644 index 00000000000..42f9f560c99 --- /dev/null +++ b/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag @@ -0,0 +1,30 @@ +module v2.test.claim.v2_tokenize_comment_payload_probe + +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.logic { Bool } +import v2.std.text { String } + +data v2_tokenize_comment_payload_probe_note: String = "SH-D: localizer named dag/std/algebra.dag as the first file in the use_site_verdict closure that refuses to lex. Its only // block (a standalone leading one, the form DESIGN 4c admits) carries a backtick and an em-dash in its PAYLOAD. Plain-ASCII // blocks tokenize, and non-ASCII inside string literals tokenizes, so the untested combination is the payload itself. If these reject, comment payloads are being lexed as ordinary tokens rather than routed through the annotation channel 4c specifies." + +test fn ascii_comment_payload_control_tokenizes() -> Bool { + match tokenize(text: "// plain note here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn backtick_in_comment_payload_rejects() -> Bool { + match tokenize(text: "// reads `s` here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char + } +} + +test fn em_dash_in_comment_payload_rejects() -> Bool { + match tokenize(text: "// pattern — here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char + } +} diff --git a/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag b/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag new file mode 100644 index 00000000000..2d7ee7b3f20 --- /dev/null +++ b/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag @@ -0,0 +1,64 @@ +module v2.test.claim.v2_tokenize_non_ascii_probe + +import v2.compiler.tokenize { tokenize } +import v2.extdeps.languages.dag { dag_language_model } +import v2.std.diagnostic { Accepted, Rejected } +import v2.std.logic { Bool } +import v2.std.text { String } + +data v2_tokenize_non_ascii_probe_note: String = "SH-D minimal reproducer for the measured use_site_verdict blocker. The real-closure probe measured located_reason ^tokenize_lex_e1_unrecognized_char, NOT the ^parse_grammar_choice_overlap_residue its frontier row records. The manifest was ruled out as the instrument: it carries the non-ASCII bytes through verbatim. These fixtures ask the narrower question the closure cannot isolate — does the v2 lexer reject a non-ASCII character that appears INSIDE a string literal, which is where every occurrence in the std closure lives (prose glyphs). Fixtures, not a closure walk, so this costs milliseconds instead of the ~80s a real-ingest arm costs. The ASCII control must ACCEPT or the probe proves nothing about non-ASCII." + +test fn ascii_string_literal_tokenizes_control_holds() -> Bool { + match tokenize( + text: "data x: String = \"plain\"", + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn non_ascii_glyph_in_string_literal_rejects_as_unrecognized_char() -> Bool { + match tokenize( + text: "data x: String = \"🟡\"", + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char + } +} + +test fn non_ascii_arrow_in_string_literal_rejects_as_unrecognized_char() -> Bool { + match tokenize( + text: "data x: String = \"a → b\"", + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char + } +} + +test fn non_ascii_glyph_in_string_literal_is_accepted() -> Bool { + match tokenize( + text: "data x: String = \"🟡\"", + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} + +test fn non_ascii_arrow_in_string_literal_is_accepted() -> Bool { + match tokenize( + text: "data x: String = \"a → b\"", + file: ^probe, + rules: dag_language_model().lex + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } +} diff --git a/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag b/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag new file mode 100644 index 00000000000..1141732bfe2 --- /dev/null +++ b/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag @@ -0,0 +1,36 @@ +module v2.test.fixture.frontier_probe_cause_location_manifest_fixture + +import v2.compiler.self_host.frontier_probe_types { + AssembleRejectionDetail, + CauseLocationTextual, + FrontierProbeReceipt, + FrontierProbeDetailAbsent, + NonEmptySymbolChain, + OtherAssembleRefusal, + ProbeStageAssemble, + UnknownProbeCause +} +import std.algebra { Empty } +import v2.std.diagnostic { ByteRange } +import v2.std.text { String } + +data frontier_probe_cause_location_manifest_fixture_note: String = "REAL EMITTED BYTES, not hand-authored. The receipt below is copied verbatim out of a host_frontier_probe_survey_manifest.dag produced by the frontier_probe_survey bin at source_commit 395ae126c6095b3cca1c84e8c33dd20456782d3d, survey_executable sha256 6fdbc1a51f5767e91803877103cb5c38afa13ea098242fdd22136bbaa008c0e0, over module src/v2/compiler/use_site_verdict.dag. It exists so the RELOAD half of the round-trip is checkable in CI without running a survey: the emitter can only be proven to produce parseable, typecheckable, field-preserving text by feeding its actual output back through the front end. A hand-written approximation would test the fixture author's idea of the format instead of the emitter's, which is the failure this fixture is here to prevent. Regenerate by rerunning the bin from a clean detached worktree and copying the receipt verbatim; if the emitter's rendering changes, this fixture must be regenerated rather than edited by hand." + +data frontier_probe_cause_location_emitted_receipt: FrontierProbeReceipt = FrontierProbeReceipt { + module_path: "src/v2/compiler/use_site_verdict.dag", + blocker_class: UnknownProbeCause { reason: ^tokenize_lex_e1_unrecognized_char }, + located_stage: ProbeStageAssemble, + located_reason: ^tokenize_lex_e1_unrecognized_char, + detail: FrontierProbeDetailAbsent, + assemble_detail: AssembleRejectionDetail { + rejection_chain: NonEmptySymbolChain { + head: ^tokenize_lex_e1_unrecognized_char, + tail: Empty + }, + cause: OtherAssembleRefusal { reason: ^tokenize_lex_e1_unrecognized_char }, + at: CauseLocationTextual { + file: "dag/std/algebra.dag", + extent: ByteRange { start: 28138, end: 28138 } + } + } +} From 91e34e008655a09a0fdad0605e3d6ff7258cc66c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 03:50:11 +0000 Subject: [PATCH 03/30] SH-D D0: regenerate the round-trip fixture from real emitted bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fixture was hand-edited to the reshaped carrier, which is exactly what its own note forbids: a hand-written approximation tests the author's idea of the format instead of the emitter's. These bytes are copied verbatim out of a survey run at commit 2880549bab4, tree 932acf8e6f3, executable sha256 677c1bf0673c70ef… over src/v2/compiler/use_site_verdict.dag, and the reload half passes against them. Co-Authored-By: Claude Opus 5 (1M context) --- ..._probe_cause_location_manifest_fixture.dag | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag b/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag index 1141732bfe2..9fd979e91da 100644 --- a/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag +++ b/src/v2/test/fixture/frontier_probe_cause_location_manifest_fixture.dag @@ -14,7 +14,7 @@ import std.algebra { Empty } import v2.std.diagnostic { ByteRange } import v2.std.text { String } -data frontier_probe_cause_location_manifest_fixture_note: String = "REAL EMITTED BYTES, not hand-authored. The receipt below is copied verbatim out of a host_frontier_probe_survey_manifest.dag produced by the frontier_probe_survey bin at source_commit 395ae126c6095b3cca1c84e8c33dd20456782d3d, survey_executable sha256 6fdbc1a51f5767e91803877103cb5c38afa13ea098242fdd22136bbaa008c0e0, over module src/v2/compiler/use_site_verdict.dag. It exists so the RELOAD half of the round-trip is checkable in CI without running a survey: the emitter can only be proven to produce parseable, typecheckable, field-preserving text by feeding its actual output back through the front end. A hand-written approximation would test the fixture author's idea of the format instead of the emitter's, which is the failure this fixture is here to prevent. Regenerate by rerunning the bin from a clean detached worktree and copying the receipt verbatim; if the emitter's rendering changes, this fixture must be regenerated rather than edited by hand." +data frontier_probe_cause_location_manifest_fixture_note: String = "REAL EMITTED BYTES, not hand-authored. The receipt below is copied verbatim out of a host_frontier_probe_survey_manifest.dag produced by the frontier_probe_survey bin at source_commit 2880549bab4f6baeef0df09739f3bdc198efe0ed, source_tree 932acf8e6f39a71b52f4d1e5853383408a15ea1f, survey_executable sha256 677c1bf0673c70ef1e1a369a8fe39744e1b494e5800fef0fd0f7098e537d77fb, over module src/v2/compiler/use_site_verdict.dag. It exists so the RELOAD half of the round-trip is checkable in CI without running a survey: the emitter can only be proven to produce parseable, typecheckable, field-preserving text by feeding its actual output back through the front end. A hand-written approximation would test the fixture author's idea of the format instead of the emitter's, which is the failure this fixture is here to prevent. Regenerate by rerunning the bin from a clean detached worktree and copying the receipt verbatim; if the emitter's rendering changes, this fixture must be regenerated rather than edited by hand." data frontier_probe_cause_location_emitted_receipt: FrontierProbeReceipt = FrontierProbeReceipt { module_path: "src/v2/compiler/use_site_verdict.dag", @@ -23,14 +23,11 @@ data frontier_probe_cause_location_emitted_receipt: FrontierProbeReceipt = Front located_reason: ^tokenize_lex_e1_unrecognized_char, detail: FrontierProbeDetailAbsent, assemble_detail: AssembleRejectionDetail { - rejection_chain: NonEmptySymbolChain { - head: ^tokenize_lex_e1_unrecognized_char, - tail: Empty - }, - cause: OtherAssembleRefusal { reason: ^tokenize_lex_e1_unrecognized_char }, - at: CauseLocationTextual { - file: "dag/std/algebra.dag", - extent: ByteRange { start: 28138, end: 28138 } - } - } + rejection_chain: NonEmptySymbolChain { + head: ^tokenize_lex_e1_unrecognized_char, + tail: Empty +}, + cause: OtherAssembleRefusal { reason: ^tokenize_lex_e1_unrecognized_char }, + at: CauseLocationTextual { file: "dag/std/algebra.dag", extent: ByteRange { start: 28138, end: 28138 } } +} } From c44a83945ab7fa139a7954033a52e011370ab928 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 03:58:18 +0000 Subject: [PATCH 04/30] SH-D D0: derive the clustering verdict's dominant reason from the receipts frontier_gap_clustering_from_receipts counted blocker CLASSES and then wrote a hardcoded located_reason literal into whichever branch fired, so the verdict reported a reason no receipt had to carry: the one real survey measures ^tokenize_lex_e1_unrecognized_char, which no branch could name. Its HeterogeneousGaps arm reported module_count as the distinct-reason count, which is a count of modules wearing the name of a count of reasons. And count_receipts_with_located_reason, the function that reads the measured axis, had zero callers. Both numbers are now read off the receipts. CommonRoot requires unanimity rather than a plurality, because any plurality bar is a threshold and a threshold is the smuggled heuristic a closed system never needs. An empty survey reports a distinct count of zero rather than becoming a common root. Controls (executed, both REDs verified): a unanimous survey reports the measured reason and counts the receipts carrying it; a mixed survey reports 2 distinct reasons over 3 receipts, where the old arm reported 3. Co-Authored-By: Claude Opus 5 (1M context) --- .../self_host/frontier_probe_survey.dag | 79 ++++++++------ .../frontier_gap_clustering_measured_test.dag | 101 ++++++++++++++++++ 2 files changed, 150 insertions(+), 30 deletions(-) create mode 100644 src/v2/test/claim/self_host/frontier_gap_clustering_measured_test.dag diff --git a/src/v2/compiler/self_host/frontier_probe_survey.dag b/src/v2/compiler/self_host/frontier_probe_survey.dag index 16b3f163936..9c93b4518f9 100644 --- a/src/v2/compiler/self_host/frontier_probe_survey.dag +++ b/src/v2/compiler/self_host/frontier_probe_survey.dag @@ -30,7 +30,7 @@ import std.realization_schedule { scoped_witness_exact_head_text_holds } import std.content_hash { ContentHash, compare_content_hash, ContentHashEqual } import v2.compiler.name_resolve { Admission } import v2.compiler.source_authority { SourceRootIngest } -import v2.std.algebra { fold_list, length } +import v2.std.algebra { Cons, Empty, fold_list, length } import v2.std.collection { List } import v2.std.logic { Bool } import v2.std.node { Symbol } @@ -333,44 +333,63 @@ fn frontier_probe_survey_receipt_paths_match( ) } +data frontier_gap_clustering_derivation_note: String = "The dominant reason is READ OFF the receipts, never supplied by the branch that fired. Before this, clustering counted blocker CLASSES and then wrote a hardcoded located_reason literal per branch (^parse_grammar_choice_overlap_residue for the realization branch, and so on), so a verdict reported a reason no receipt had to carry: two modules whose measured refusals differ were indistinguishable, and the one measured survey to date carries ^tokenize_lex_e1_unrecognized_char, which no branch could ever name. count_receipts_with_located_reason existed with zero callers throughout. CommonRoot now requires UNANIMITY rather than a plurality: any plurality bar is a threshold, and a threshold is the smuggled heuristic a closed system never needs — a survey where one reason covers every receipt has a common root, and one where it does not is heterogeneous with its true distinct count. An empty survey has no measured reason, so it cannot be a CommonRoot; it reports a distinct count of zero, which says exactly that. Cost: distinct reasons are collected in one pass and counted once each, so the fold is d*n over the distinct set rather than n*n over the receipts." + +type FrontierReasonTally = ReasonTally { reason: Symbol, count: Int } + +fn symbol_list_contains(xs: List, target: Symbol) -> Bool { + fold_list( + xs: xs, + empty: false, + cons: fn(found, s) { found || (s == target) } + ) +} + +fn distinct_located_reasons(xs: List) -> List { + fold_list( + xs: xs, + empty: Empty, + cons: fn(seen, receipt) { + let reason = frontier_probe_receipt_located_reason(receipt: receipt) + if symbol_list_contains(xs: seen, target: reason) { + seen + } else { + Cons { head: reason, tail: seen } + } + } + ) +} + +fn dominant_located_reason_tally(xs: List) -> FrontierReasonTally { + fold_list( + xs: distinct_located_reasons(xs: xs), + empty: ReasonTally { reason: ^frontier_survey_no_measured_reason, count: 0 }, + cons: fn(best, reason) { + let c = count_receipts_with_located_reason(xs: xs, target: reason) + if c > best.count { + ReasonTally { reason: reason, count: c } + } else { + best + } + } + ) +} + fn frontier_gap_clustering_from_receipts( xs: List, module_count: Int ) -> FrontierGapClusterVerdict { - let emit_surface_count = count_receipts_with_blocker(xs: xs, target: EmitSurfaceGap) - let name_resolution_count = count_receipts_with_blocker(xs: xs, target: NameResolutionGap) - let realization_count = count_receipts_with_blocker(xs: xs, target: RealizationGap) - let self_emit_ready_count = count_receipts_with_blocker(xs: xs, target: SelfEmitReady) - if self_emit_ready_count == module_count { - CommonRoot { - dominant_located_reason: ^probe_stage_emit_ok, - dominant_module_count: module_count, - module_count: module_count - } - } else if realization_count >= emit_surface_count - && realization_count >= name_resolution_count - && realization_count > 0 - { - CommonRoot { - dominant_located_reason: ^parse_grammar_choice_overlap_residue, - dominant_module_count: realization_count, - module_count: module_count - } - } else if emit_surface_count >= name_resolution_count && emit_surface_count > 0 { - CommonRoot { - dominant_located_reason: ^measured_probe_emit_surface_gap, - dominant_module_count: emit_surface_count, - module_count: module_count - } - } else if name_resolution_count > emit_surface_count { + let distinct_count = length(xs: distinct_located_reasons(xs: xs)) + let top = dominant_located_reason_tally(xs: xs) + if distinct_count == 1 { CommonRoot { - dominant_located_reason: ^measured_probe_name_resolution_gap, - dominant_module_count: name_resolution_count, + dominant_located_reason: top.reason, + dominant_module_count: top.count, module_count: module_count } } else { HeterogeneousGaps { - distinct_located_reason_count: module_count, + distinct_located_reason_count: distinct_count, module_count: module_count } } diff --git a/src/v2/test/claim/self_host/frontier_gap_clustering_measured_test.dag b/src/v2/test/claim/self_host/frontier_gap_clustering_measured_test.dag new file mode 100644 index 00000000000..286584d1a4d --- /dev/null +++ b/src/v2/test/claim/self_host/frontier_gap_clustering_measured_test.dag @@ -0,0 +1,101 @@ +module v2.test.claim.self_host.frontier_gap_clustering_measured + +import v2.compiler.self_host.frontier_probe_survey { + CommonRoot, + HeterogeneousGaps, + frontier_gap_clustering_from_receipts +} +import v2.compiler.self_host.frontier_probe_types { + FrontierProbeDetailAbsent, + FrontierProbeReceipt, + NoFrontierProbeDetail, + ProbeStageAssemble, + RealizationGap, + UnknownProbeCause +} +import v2.std.algebra { Cons, Empty } +import v2.std.collection { List } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } + +data frontier_gap_clustering_measured_note: String = "Discriminating controls for the clustering verdict's dominant reason. The verdict used to count blocker CLASSES and then write a hardcoded reason literal into the arm that fired, so a survey of modules refusing for ^tokenize_lex_e1_unrecognized_char — the reason the one real survey to date actually measured — would have reported ^parse_grammar_choice_overlap_residue, a symbol no receipt carried. These fixtures pin the reason to what the receipts say: a unanimous survey reports THAT reason and counts it, and a mixed survey reports its true distinct count rather than the module count. Fixtures, not a pipeline walk, so they cost milliseconds. RED: asserting the previously hardcoded literal on the unanimous case fails, which is the exact substitution these claims exist to catch." + +fn receipt_with_reason(reason: Symbol) -> FrontierProbeReceipt { + FrontierProbeReceipt { + module_path: "src/v2/compiler/clustering_probe_fixture.dag", + blocker_class: UnknownProbeCause { reason: reason }, + located_stage: ProbeStageAssemble, + located_reason: reason, + detail: FrontierProbeDetailAbsent, + assemble_detail: NoFrontierProbeDetail + } +} + +fn unanimous_receipts() -> List { + Cons { + head: receipt_with_reason(reason: ^tokenize_lex_e1_unrecognized_char), + tail: Cons { + head: receipt_with_reason(reason: ^tokenize_lex_e1_unrecognized_char), + tail: Empty + } + } +} + +fn mixed_receipts() -> List { + Cons { + head: receipt_with_reason(reason: ^tokenize_lex_e1_unrecognized_char), + tail: Cons { + head: receipt_with_reason(reason: ^parse_grammar_choice_overlap_residue), + tail: Cons { + head: receipt_with_reason(reason: ^tokenize_lex_e1_unrecognized_char), + tail: Empty + } + } + } +} + +fn unanimous_dominant_reason() -> Symbol { + match frontier_gap_clustering_from_receipts(xs: unanimous_receipts(), module_count: 2) { + CommonRoot { dominant_located_reason: r, dominant_module_count: _, module_count: _ } => r + HeterogeneousGaps { distinct_located_reason_count: _, module_count: _ } => + ^clustering_reported_heterogeneous + } +} + +fn unanimous_dominant_count() -> Int { + match frontier_gap_clustering_from_receipts(xs: unanimous_receipts(), module_count: 2) { + CommonRoot { dominant_located_reason: _, dominant_module_count: c, module_count: _ } => c + HeterogeneousGaps { distinct_located_reason_count: _, module_count: _ } => 0 - 1 + } +} + +fn mixed_distinct_count() -> Int { + match frontier_gap_clustering_from_receipts(xs: mixed_receipts(), module_count: 3) { + CommonRoot { dominant_located_reason: _, dominant_module_count: _, module_count: _ } => 0 - 1 + HeterogeneousGaps { distinct_located_reason_count: d, module_count: _ } => d + } +} + +fn empty_distinct_count() -> Int { + match frontier_gap_clustering_from_receipts(xs: Empty, module_count: 0) { + CommonRoot { dominant_located_reason: _, dominant_module_count: _, module_count: _ } => 0 - 1 + HeterogeneousGaps { distinct_located_reason_count: d, module_count: _ } => d + } +} + +test fn unanimous_survey_reports_the_measured_reason_holds() -> Bool { + unanimous_dominant_reason() == ^tokenize_lex_e1_unrecognized_char +} + +test fn unanimous_survey_counts_the_receipts_carrying_it_holds() -> Bool { + unanimous_dominant_count() == 2 +} + +test fn mixed_survey_reports_its_true_distinct_count_holds() -> Bool { + mixed_distinct_count() == 2 +} + +test fn empty_survey_is_never_a_common_root_holds() -> Bool { + empty_distinct_count() == 0 +} From 04df0229286d3b465c52f1618118a4c9e65a2ae7 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 04:30:50 +0000 Subject: [PATCH 05/30] SH-D: unland the local probe instruments swept in by git add -A MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These eight files are the one-off measurement instruments behind the A/B finding — the real-closure door probe, the cause and reason readouts, the lex localizer and three tokenize fixtures. They were never meant to land; the PR description says so explicitly. A blanket 'git add -A src/v2' put them in the tree anyway, which is experimental residue and a description that no longer described the diff. They stay local, excluded via .git/info/exclude so the next add cannot repeat this. Co-Authored-By: Claude Opus 5 (1M context) --- ...irect_rust_door_real_module_probe_test.dag | 84 ------------- .../long/frontier_probe_lex_localizer.dag | 34 ----- .../frontier_probe_real_module_cause_test.dag | 117 ------------------ ...frontier_probe_real_module_reason_test.dag | 63 ---------- .../long/frontier_probe_reason_readout.dag | 99 --------------- .../v2_tokenize_annotation_probe_test.dag | 23 ---- ...v2_tokenize_comment_payload_probe_test.dag | 30 ----- .../v2_tokenize_non_ascii_probe_test.dag | 64 ---------- 8 files changed, 514 deletions(-) delete mode 100644 src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag delete mode 100644 src/v2/test/claim/long/frontier_probe_lex_localizer.dag delete mode 100644 src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag delete mode 100644 src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag delete mode 100644 src/v2/test/claim/long/frontier_probe_reason_readout.dag delete mode 100644 src/v2/test/claim/v2_tokenize_annotation_probe_test.dag delete mode 100644 src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag delete mode 100644 src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag diff --git a/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag b/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag deleted file mode 100644 index 6ec5d5f6965..00000000000 --- a/src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag +++ /dev/null @@ -1,84 +0,0 @@ -module v2.test.long.direct_rust_door_real_module_probe - -import v2.compiler.self_host.candidate_generation { - ArtifactProduced, - AssemblyRejected, - EmissionRejected, - InferenceRejected, - ProducerReceiptRejected, - ProvenancedRustEmissionObservation, - observe_provenanced_rust_emission_from_ingest -} -import v2.extdeps.languages.rust { rust_target_model } -import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.logic { Bool } -import v2.std.text { String } -import v2.test.workflow.host_source_root_ingest_manifest { - host_compiler_closure_admission, - host_source_root_ingest -} - -data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree - -data direct_rust_door_real_module_probe_note: String = "SH-D D1-zero instrument: drives the v2 direct Rust door (observe_provenanced_rust_emission_from_ingest) against a REAL compiler module's import closure, supplied by the host_source_root_ingest_manifest overlay that discover_source_root_ingest --entry writes. This is the first real-module use of the door: direct_rust_door_production_group is hardwired to direct_rust_door_fixture (direct_rust_door_module_path names the fixture file), so no existing consumer observes a production module through it. Five mutually exclusive one-arm probes, not one pass/fail: a Bool witness cannot report WHICH stage stopped, so the arm that greens IS the measurement and the other four must red. Run per module via the local recipe below; the module under test is whichever entry the overlay manifest was generated for, which is why no module path is named here. LOCAL RECIPE: (1) discover_source_root_ingest --source-root src/v2 --source-root dag --entry --emit-manifest target/door-probe/host_source_root_ingest_manifest.dag; (2) claim_batch --source-root src/v2 --source-root dag --source-root target/door-probe --entry src/v2/test/claim/long/direct_rust_door_real_module_probe_test.dag --functions . NOT ADMITTED for CI: walks the real pipeline, so it needs an explicit gunbc.explicit_witness_admission row with a declared cost before any cadence carries it — a long/ path is not admission." - -fn direct_rust_door_real_module_observation() -> ProvenancedRustEmissionObservation { - observe_provenanced_rust_emission_from_ingest( - ingest: host_source_root_ingest, - admission: host_compiler_closure_admission, - target: rust_target_model(), - module_path: direct_rust_door_real_module_path - ) -} - -data direct_rust_door_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" - -test fn real_module_door_stops_at_assembly() -> Bool { - match direct_rust_door_real_module_observation() { - AssemblyRejected { diagnostics: _ } => true - InferenceRejected { diagnostics: _ } => false - EmissionRejected { diagnostics: _ } => false - ProducerReceiptRejected { diagnostics: _ } => false - ArtifactProduced { artifact: _ } => false - } -} - -test fn real_module_door_stops_at_inference() -> Bool { - match direct_rust_door_real_module_observation() { - AssemblyRejected { diagnostics: _ } => false - InferenceRejected { diagnostics: _ } => true - EmissionRejected { diagnostics: _ } => false - ProducerReceiptRejected { diagnostics: _ } => false - ArtifactProduced { artifact: _ } => false - } -} - -test fn real_module_door_stops_at_emission() -> Bool { - match direct_rust_door_real_module_observation() { - AssemblyRejected { diagnostics: _ } => false - InferenceRejected { diagnostics: _ } => false - EmissionRejected { diagnostics: _ } => true - ProducerReceiptRejected { diagnostics: _ } => false - ArtifactProduced { artifact: _ } => false - } -} - -test fn real_module_door_stops_at_producer_receipt() -> Bool { - match direct_rust_door_real_module_observation() { - AssemblyRejected { diagnostics: _ } => false - InferenceRejected { diagnostics: _ } => false - EmissionRejected { diagnostics: _ } => false - ProducerReceiptRejected { diagnostics: _ } => true - ArtifactProduced { artifact: _ } => false - } -} - -test fn real_module_door_produces_artifact() -> Bool { - match direct_rust_door_real_module_observation() { - AssemblyRejected { diagnostics: _ } => false - InferenceRejected { diagnostics: _ } => false - EmissionRejected { diagnostics: _ } => false - ProducerReceiptRejected { diagnostics: _ } => false - ArtifactProduced { artifact: _ } => true - } -} diff --git a/src/v2/test/claim/long/frontier_probe_lex_localizer.dag b/src/v2/test/claim/long/frontier_probe_lex_localizer.dag deleted file mode 100644 index 467d9992e96..00000000000 --- a/src/v2/test/claim/long/frontier_probe_lex_localizer.dag +++ /dev/null @@ -1,34 +0,0 @@ -module v2.test.long.frontier_probe_lex_localizer - -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_language_model } -import v2.std.algebra { fold_list } -import v2.std.diagnostic { Accepted, Rejected } -import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.text { String } -import v2.test.workflow.host_source_root_ingest_manifest { host_source_root_ingest } - -data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree - -data frontier_probe_lex_localizer_note: String = "SH-D localizer, NOT a witness. The use_site_verdict closure measures ^tokenize_lex_e1_unrecognized_char and two guesses have already been refuted by execution: non-ASCII inside string literals tokenizes fine (emoji and arrow, ASCII control accepting), and a standalone leading // annotation block tokenizes fine. Guessing is the wrong instrument. This folds the SAME source text the ingest carries through v2 tokenize per file and returns the first file_path that refuses, so the search is one run over the real bytes rather than a sequence of hypotheses. Returns the empty string if every file tokenizes, which would relocate the refusal to assembly rather than lexing and is itself a decisive answer." - -fn first_failing_lex_file() -> String { - fold_list( - xs: host_source_root_ingest, - empty: "", - cons: fn(acc, r) { - if acc != "" { - acc - } else { - match tokenize( - text: r.source.carried, - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => "" - Rejected { diagnostics: _ } => r.artifact.file_path - } - } - } - ) -} diff --git a/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag b/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag deleted file mode 100644 index 73671cb4f9f..00000000000 --- a/src/v2/test/claim/long/frontier_probe_real_module_cause_test.dag +++ /dev/null @@ -1,117 +0,0 @@ -module v2.test.long.frontier_probe_real_module_cause - -import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } -import v2.compiler.self_host.frontier_probe_types { - AssembleMissingModule, - AssembleRejectionDetail, - FrontierProbeReceipt, - GrammarChoiceOverlap, - MaskedAssembleRefusal, - MaskedAssembleRefusalWithoutEvidence, - MissingModuleIdentityUnavailable, - NoFrontierProbeDetail, - OtherAssembleRefusal -} -import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.logic { Bool } -import v2.std.text { String } -import v2.test.workflow.host_source_root_ingest_manifest { - host_compiler_closure_admission, - host_source_root_ingest -} - -data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree - -data frontier_probe_real_module_cause_note: String = "SH-D D1 detailed-cause instrument: runs frontier_probe_emit_from_ingest against a REAL module closure supplied by the host_source_root_ingest_manifest overlay, and discriminates the AssembleRejectionCause variant the receipt actually carries. This is the DETAILED grain the operator ruling requires and the survey does not currently produce: frontier_gap_clustering_from_receipts counts blocker CLASSES and then supplies a hardcoded located_reason literal per branch, so it cannot distinguish two modules whose ambiguity rows differ. One arm per variant because a Bool witness cannot report a variant; the arm that greens IS the measurement. Same overlay recipe as direct_rust_door_real_module_probe. NOT ADMITTED for CI: real-pipeline walk, needs an explicit admission row with declared cost." - -fn frontier_probe_real_module_receipt() -> FrontierProbeReceipt { - frontier_probe_emit_from_ingest( - module_path: frontier_probe_real_module_path, - ingest: host_source_root_ingest, - admission: host_compiler_closure_admission - ) -} - -data frontier_probe_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" - -test fn real_module_cause_is_grammar_choice_overlap() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => false - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => true - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false - AssembleMissingModule { requested: _ } => false - MissingModuleIdentityUnavailable => false - OtherAssembleRefusal { reason: _, at: _ } => false - } - } -} - -test fn real_module_cause_is_masked_with_evidence() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => false - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => false - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => true - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false - AssembleMissingModule { requested: _ } => false - MissingModuleIdentityUnavailable => false - OtherAssembleRefusal { reason: _, at: _ } => false - } - } -} - -test fn real_module_cause_is_masked_without_evidence() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => false - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => false - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => true - AssembleMissingModule { requested: _ } => false - MissingModuleIdentityUnavailable => false - OtherAssembleRefusal { reason: _, at: _ } => false - } - } -} - -test fn real_module_cause_is_missing_module() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => false - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => false - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false - AssembleMissingModule { requested: _ } => true - MissingModuleIdentityUnavailable => true - OtherAssembleRefusal { reason: _, at: _ } => false - } - } -} - -test fn real_module_cause_is_other_assemble_refusal() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => false - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => false - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => false - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => false - AssembleMissingModule { requested: _ } => false - MissingModuleIdentityUnavailable => false - OtherAssembleRefusal { reason: _, at: _ } => true - } - } -} - -test fn real_module_assemble_detail_absent() -> Bool { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => true - AssembleRejectionDetail { rejection_chain: _, cause: _, at: _ } => false - } -} diff --git a/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag b/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag deleted file mode 100644 index 000d864f5d4..00000000000 --- a/src/v2/test/claim/long/frontier_probe_real_module_reason_test.dag +++ /dev/null @@ -1,63 +0,0 @@ -module v2.test.long.frontier_probe_real_module_reason - -import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } -import v2.compiler.self_host.frontier_probe_types { - AssembleMissingModule, - AssembleRejectionDetail, - FrontierProbeReceipt, - GrammarChoiceOverlap, - MaskedAssembleRefusal, - MaskedAssembleRefusalWithoutEvidence, - MissingModuleIdentityUnavailable, - NoFrontierProbeDetail, - OtherAssembleRefusal -} -import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.logic { Bool } -import v2.std.node { Symbol } -import v2.std.text { String } -import v2.test.workflow.host_source_root_ingest_manifest { - host_compiler_closure_admission, - host_source_root_ingest -} - -data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree - -data frontier_probe_real_module_reason_note: String = "SH-D D1 follow-up: the cause probe established that use_site_verdict's assemble rejection binds OtherAssembleRefusal (the catch-all), NOT GrammarChoiceOverlap, while its frontier row names located_reason ^parse_grammar_choice_overlap_residue. Two readings remain open and this instrument separates them: either the underlying reason symbol genuinely differs from the row (the row is misattributed), or the reason symbol matches and only the OVERLAP EVIDENCE failed to bind, which routes to OtherAssembleRefusal per frontier_probe_overlap_detail_note. Those have different owners and different repairs, so the symbol itself is read here rather than inferred. Also reads receipt.located_reason directly, since that is the field the survey clusters on." - -fn frontier_probe_real_module_receipt() -> FrontierProbeReceipt { - frontier_probe_emit_from_ingest( - module_path: frontier_probe_real_module_path, - ingest: host_source_root_ingest, - admission: host_compiler_closure_admission - ) -} - -data frontier_probe_real_module_path: String = "src/v2/compiler/use_site_verdict.dag" - -fn real_module_other_refusal_reason() -> Symbol { - match frontier_probe_real_module_receipt().assemble_detail { - NoFrontierProbeDetail => ^no_assemble_detail - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => ^bound_grammar_choice_overlap - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^bound_masked_with_evidence - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^bound_masked_without_evidence - AssembleMissingModule { requested: _ } => ^bound_missing_module - MissingModuleIdentityUnavailable => ^bound_missing_module_identity - OtherAssembleRefusal { reason: r, at: _ } => r - } - } -} - -test fn other_refusal_reason_is_parse_grammar_choice_overlap_residue() -> Bool { - real_module_other_refusal_reason() == ^parse_grammar_choice_overlap_residue -} - -test fn located_reason_is_parse_grammar_choice_overlap_residue() -> Bool { - frontier_probe_real_module_receipt().located_reason == ^parse_grammar_choice_overlap_residue -} - -test fn other_refusal_reason_is_resolve_module_not_found() -> Bool { - real_module_other_refusal_reason() == ^resolve_module_not_found -} diff --git a/src/v2/test/claim/long/frontier_probe_reason_readout.dag b/src/v2/test/claim/long/frontier_probe_reason_readout.dag deleted file mode 100644 index 2a0e5107ed6..00000000000 --- a/src/v2/test/claim/long/frontier_probe_reason_readout.dag +++ /dev/null @@ -1,99 +0,0 @@ -module v2.test.long.frontier_probe_reason_readout - -import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } -import v2.compiler.self_host.frontier_probe_types { - AssembleMissingModule, - AssembleRejectionDetail, - FrontierProbeReceipt, - GrammarChoiceOverlap, - MaskedAssembleRefusal, - MaskedAssembleRefusalWithoutEvidence, - MissingModuleIdentityUnavailable, - NoFrontierProbeDetail, - OtherAssembleRefusal, - CauseLocationTextual, - CauseLocationUnavailable -} -import v2.std.diagnostic { ByteRange, WholeFile } -import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.node { Symbol } -import v2.std.text { String } -import v2.test.workflow.host_source_root_ingest_manifest { - host_compiler_closure_admission, - host_source_root_ingest -} - -data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree - -data frontier_probe_reason_readout_note: String = "SH-D readout entry, NOT a witness: three equality probes established that use_site_verdict's measured located_reason is neither ^parse_grammar_choice_overlap_residue (the symbol its frontier row records) nor ^resolve_module_not_found, and that its assemble cause is OtherAssembleRefusal. Guessing further symbols at ~80s per equality test is the wrong instrument, so these fns RETURN the symbol for gunbc run to print. No test fn here on purpose: this reads a value, it does not assert one." - -fn readout_receipt() -> FrontierProbeReceipt { - frontier_probe_emit_from_ingest( - module_path: readout_module_path, - ingest: host_source_root_ingest, - admission: host_compiler_closure_admission - ) -} - -data readout_module_path: String = "src/v2/compiler/use_site_verdict.dag" - -fn readout_located_reason() -> Symbol { - readout_receipt().located_reason -} - -fn readout_assemble_cause_reason() -> Symbol { - match readout_receipt().assemble_detail { - NoFrontierProbeDetail => ^no_assemble_detail - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => ^bound_grammar_choice_overlap - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^bound_masked_with_evidence - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^bound_masked_without_evidence - AssembleMissingModule { requested: _ } => ^bound_missing_module - MissingModuleIdentityUnavailable => ^bound_missing_module_identity - OtherAssembleRefusal { reason: r, at: _ } => r - } - } -} - -fn readout_assemble_cause_locus_file() -> Symbol { - match readout_receipt().assemble_detail { - NoFrontierProbeDetail => ^no_assemble_detail - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => ^locus_not_carried_overlap - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^locus_not_carried_masked - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^locus_not_carried_masked_bare - AssembleMissingModule { requested: _ } => ^locus_not_carried_missing_module - MissingModuleIdentityUnavailable => ^locus_not_carried_missing_identity - OtherAssembleRefusal { reason: _, at: a } => - match a { - CauseLocationTextual { file: f, extent: _ } => f - CauseLocationUnavailable { reason: u } => u - } - } - } -} - -fn readout_assemble_cause_locus_extent() -> Symbol { - match readout_receipt().assemble_detail { - NoFrontierProbeDetail => ^no_assemble_detail - AssembleRejectionDetail { rejection_chain: _, cause: c, at: _ } => - match c { - GrammarChoiceOverlap { rows: _ } => ^not_other_refusal - MaskedAssembleRefusal { overlap_evidence: _, remaining_chain: _ } => ^not_other_refusal - MaskedAssembleRefusalWithoutEvidence { remaining_chain: _ } => ^not_other_refusal - AssembleMissingModule { requested: _ } => ^not_other_refusal - MissingModuleIdentityUnavailable => ^not_other_refusal - OtherAssembleRefusal { reason: _, at: a } => - match a { - CauseLocationTextual { file: _, extent: e } => - match e { - WholeFile => ^extent_whole_file - ByteRange { start: _, end: _ } => ^extent_byte_range - } - CauseLocationUnavailable { reason: u } => u - } - } - } -} diff --git a/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag b/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag deleted file mode 100644 index 155f3a3c0af..00000000000 --- a/src/v2/test/claim/v2_tokenize_annotation_probe_test.dag +++ /dev/null @@ -1,23 +0,0 @@ -module v2.test.claim.v2_tokenize_annotation_probe - -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_language_model } -import v2.std.diagnostic { Accepted, Rejected } -import v2.std.logic { Bool } -import v2.std.text { String } - -data v2_tokenize_annotation_probe_note: String = "SH-D: does the v2 lexer admit the standalone leading // annotation block DESIGN 4c describes? Two files in the use_site_verdict closure carry them (dag/std/algebra.dag, dag/std/dissolution.dag), and the closure measures ^tokenize_lex_e1_unrecognized_char." - -test fn leading_annotation_block_tokenizes() -> Bool { - match tokenize(text: "// note\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn leading_annotation_block_rejects_as_unrecognized_char() -> Bool { - match tokenize(text: "// note\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char - } -} diff --git a/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag b/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag deleted file mode 100644 index 42f9f560c99..00000000000 --- a/src/v2/test/claim/v2_tokenize_comment_payload_probe_test.dag +++ /dev/null @@ -1,30 +0,0 @@ -module v2.test.claim.v2_tokenize_comment_payload_probe - -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_language_model } -import v2.std.diagnostic { Accepted, Rejected } -import v2.std.logic { Bool } -import v2.std.text { String } - -data v2_tokenize_comment_payload_probe_note: String = "SH-D: localizer named dag/std/algebra.dag as the first file in the use_site_verdict closure that refuses to lex. Its only // block (a standalone leading one, the form DESIGN 4c admits) carries a backtick and an em-dash in its PAYLOAD. Plain-ASCII // blocks tokenize, and non-ASCII inside string literals tokenizes, so the untested combination is the payload itself. If these reject, comment payloads are being lexed as ordinary tokens rather than routed through the annotation channel 4c specifies." - -test fn ascii_comment_payload_control_tokenizes() -> Bool { - match tokenize(text: "// plain note here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn backtick_in_comment_payload_rejects() -> Bool { - match tokenize(text: "// reads `s` here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char - } -} - -test fn em_dash_in_comment_payload_rejects() -> Bool { - match tokenize(text: "// pattern — here\ndata x: String = \"p\"", file: ^probe, rules: dag_language_model().lex) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char - } -} diff --git a/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag b/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag deleted file mode 100644 index 2d7ee7b3f20..00000000000 --- a/src/v2/test/claim/v2_tokenize_non_ascii_probe_test.dag +++ /dev/null @@ -1,64 +0,0 @@ -module v2.test.claim.v2_tokenize_non_ascii_probe - -import v2.compiler.tokenize { tokenize } -import v2.extdeps.languages.dag { dag_language_model } -import v2.std.diagnostic { Accepted, Rejected } -import v2.std.logic { Bool } -import v2.std.text { String } - -data v2_tokenize_non_ascii_probe_note: String = "SH-D minimal reproducer for the measured use_site_verdict blocker. The real-closure probe measured located_reason ^tokenize_lex_e1_unrecognized_char, NOT the ^parse_grammar_choice_overlap_residue its frontier row records. The manifest was ruled out as the instrument: it carries the non-ASCII bytes through verbatim. These fixtures ask the narrower question the closure cannot isolate — does the v2 lexer reject a non-ASCII character that appears INSIDE a string literal, which is where every occurrence in the std closure lives (prose glyphs). Fixtures, not a closure walk, so this costs milliseconds instead of the ~80s a real-ingest arm costs. The ASCII control must ACCEPT or the probe proves nothing about non-ASCII." - -test fn ascii_string_literal_tokenizes_control_holds() -> Bool { - match tokenize( - text: "data x: String = \"plain\"", - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn non_ascii_glyph_in_string_literal_rejects_as_unrecognized_char() -> Bool { - match tokenize( - text: "data x: String = \"🟡\"", - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char - } -} - -test fn non_ascii_arrow_in_string_literal_rejects_as_unrecognized_char() -> Bool { - match tokenize( - text: "data x: String = \"a → b\"", - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: ds } => ds.head.reason == ^tokenize_lex_e1_unrecognized_char - } -} - -test fn non_ascii_glyph_in_string_literal_is_accepted() -> Bool { - match tokenize( - text: "data x: String = \"🟡\"", - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} - -test fn non_ascii_arrow_in_string_literal_is_accepted() -> Bool { - match tokenize( - text: "data x: String = \"a → b\"", - file: ^probe, - rules: dag_language_model().lex - ) { - Accepted { value: _, diagnostics: _ } => true - Rejected { diagnostics: _ } => false - } -} From 283a3f088f7b7ccccf27e1bb0ca223d7dfe400d8 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 18:05:19 +0000 Subject: [PATCH 06/30] Compute the reason tally only on the arm that consumes it The tally ran unconditionally and its result was discarded on the heterogeneous arm, which needs the distinct count alone. Cheap at survey scale, but a copied fold whose output is thrown away is a cost-shape defect regardless of the realized n, and 'n is small here' is not a time-stable fact. Found by review 51421. All four clustering claims still pass by execution. Co-Authored-By: Claude Opus 5 (1M context) --- .../self_host/frontier_probe_survey.dag | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/v2/compiler/self_host/frontier_probe_survey.dag b/src/v2/compiler/self_host/frontier_probe_survey.dag index 9c93b4518f9..5c973c8bbb1 100644 --- a/src/v2/compiler/self_host/frontier_probe_survey.dag +++ b/src/v2/compiler/self_host/frontier_probe_survey.dag @@ -333,7 +333,7 @@ fn frontier_probe_survey_receipt_paths_match( ) } -data frontier_gap_clustering_derivation_note: String = "The dominant reason is READ OFF the receipts, never supplied by the branch that fired. Before this, clustering counted blocker CLASSES and then wrote a hardcoded located_reason literal per branch (^parse_grammar_choice_overlap_residue for the realization branch, and so on), so a verdict reported a reason no receipt had to carry: two modules whose measured refusals differ were indistinguishable, and the one measured survey to date carries ^tokenize_lex_e1_unrecognized_char, which no branch could ever name. count_receipts_with_located_reason existed with zero callers throughout. CommonRoot now requires UNANIMITY rather than a plurality: any plurality bar is a threshold, and a threshold is the smuggled heuristic a closed system never needs — a survey where one reason covers every receipt has a common root, and one where it does not is heterogeneous with its true distinct count. An empty survey has no measured reason, so it cannot be a CommonRoot; it reports a distinct count of zero, which says exactly that. Cost: distinct reasons are collected in one pass and counted once each, so the fold is d*n over the distinct set rather than n*n over the receipts." +data frontier_gap_clustering_derivation_note: String = "The dominant reason is READ OFF the receipts, never supplied by the branch that fired. Before this, clustering counted blocker CLASSES and then wrote a hardcoded located_reason literal per branch (^parse_grammar_choice_overlap_residue for the realization branch, and so on), so a verdict reported a reason no receipt had to carry: two modules whose measured refusals differ were indistinguishable, and the one measured survey to date carries ^tokenize_lex_e1_unrecognized_char, which no branch could ever name. count_receipts_with_located_reason existed with zero callers throughout. CommonRoot now requires UNANIMITY rather than a plurality: any plurality bar is a threshold, and a threshold is the smuggled heuristic a closed system never needs — a survey where one reason covers every receipt has a common root, and one where it does not is heterogeneous with its true distinct count. An empty survey has no measured reason, so it cannot be a CommonRoot; it reports a distinct count of zero, which says exactly that. Cost: distinct reasons are collected in one pass and counted once each, so the fold is d*n over the distinct set rather than n*n over the receipts. The tally runs only on the unanimous arm, where its result is consumed; the heterogeneous arm needs the distinct count alone and computing a tally it discards would be a cost-shape defect regardless of how small the survey is (review 51421)." type FrontierReasonTally = ReasonTally { reason: Symbol, count: Int } @@ -375,18 +375,24 @@ fn dominant_located_reason_tally(xs: List) -> FrontierReas ) } +fn common_root_from_tally(top: FrontierReasonTally, module_count: Int) -> FrontierGapClusterVerdict { + CommonRoot { + dominant_located_reason: top.reason, + dominant_module_count: top.count, + module_count: module_count + } +} + fn frontier_gap_clustering_from_receipts( xs: List, module_count: Int ) -> FrontierGapClusterVerdict { let distinct_count = length(xs: distinct_located_reasons(xs: xs)) - let top = dominant_located_reason_tally(xs: xs) if distinct_count == 1 { - CommonRoot { - dominant_located_reason: top.reason, - dominant_module_count: top.count, + common_root_from_tally( + top: dominant_located_reason_tally(xs: xs), module_count: module_count - } + ) } else { HeterogeneousGaps { distinct_located_reason_count: distinct_count, From e6dd8e603c3be3e50bd9b3f61f3f176e4a4f47d4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 18:56:33 +0000 Subject: [PATCH 07/30] Refuse an empty ingest instead of reporting the module missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MEASURED, not hypothesised. src/v2/compiler/03_resolve.dag needs 71 source reads against the manifest transport's 64-row inline cap, so the manifest emits SourceRootManifestElided with produced_row_count 0 and an Empty ingest. Handed that, frontier_probe_emit_from_ingest answered ^resolve_module_not_found with cause MissingModuleIdentityUnavailable — a survey row asserting the module is missing when the truth is that its sources were never supplied. Two states, different remedies, and the one reported was the plausible one. frontier_probe_coverage_gate already classifies an elided manifest correctly, but it guards only the discovery path; this entry receives the ingest directly and never consults coverage, so the conflation was reachable from every from_ingest caller. An empty ingest is now a typed, located, counted population refusal naming ^frontier_probe_empty_ingest. Residue, declared rather than closed: emptiness is decidable here, coverage is not, because the entry is not given the SourceRootCoverage — a PARTIAL closure still answers. Dissolve-on is on the carrier: route the coverage receipt into this entry so the refusal becomes an arm of the existing gate. Four claims green by execution; RED verified by disabling the wall, which reds three of them and restores the module-not-found answer. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/self_host/frontier_probe.dag | 23 ++++++ .../frontier_probe_empty_ingest_test.dag | 72 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/v2/test/claim/self_host/frontier_probe_empty_ingest_test.dag diff --git a/src/v2/compiler/self_host/frontier_probe.dag b/src/v2/compiler/self_host/frontier_probe.dag index d94d9fc65d9..3698151decc 100644 --- a/src/v2/compiler/self_host/frontier_probe.dag +++ b/src/v2/compiler/self_host/frontier_probe.dag @@ -123,6 +123,10 @@ data frontier_probe_entry_binding_refused_reason: Symbol = ^frontier_probe_entry data frontier_probe_coverage_refused_reason: Symbol = ^frontier_probe_coverage_refused +data frontier_probe_empty_ingest_reason: Symbol = ^frontier_probe_empty_ingest + +data frontier_probe_empty_ingest_refusal_note: String = "An empty ingest is the absence of a closure, never a closure containing nothing, and the two have different remedies. MEASURED, not hypothesised: an over-cap manifest emits SourceRootManifestElided with produced_row_count 0 and host_source_root_ingest = Empty, and this entry answered ^resolve_module_not_found with cause MissingModuleIdentityUnavailable — a survey row asserting the module is missing when what actually happened is that its sources were never supplied. Executed against src/v2/compiler/03_resolve.dag at 71 reads against the 64 cap. The gate that already classifies this correctly, frontier_probe_coverage_gate, guards only the discovery path; this entry receives the ingest directly and never consults coverage, so the conflation was reachable from every from_ingest caller. RESIDUE, stated rather than closed: emptiness is decidable here and coverage is not, because this entry is not given the SourceRootCoverage — so a PARTIAL closure still answers, and a wrong-but-populated ingest is outside this wall. Dissolve-on: the entry takes the coverage receipt beside the ingest and routes it through frontier_probe_coverage_gate, at which point this refusal becomes one arm of that gate rather than a separate check." + type FrontierProbeCoverageGate = FrontierProbeCoveragePermitted | FrontierProbeCoverageBlocked { coverage: SourceRootCoverage } @@ -644,6 +648,25 @@ fn frontier_probe_emit_from_ingest( module_path: String, ingest: SourceRootIngest, admission: Admission +) -> FrontierProbeReceipt { + if length(xs: ingest) == 0 { + frontier_probe_receipt_from_population_refused( + module_path: module_path, + cause: frontier_probe_empty_ingest_reason + ) + } else { + frontier_probe_emit_from_populated_ingest( + module_path: module_path, + ingest: ingest, + admission: admission + ) + } +} + +fn frontier_probe_emit_from_populated_ingest( + module_path: String, + ingest: SourceRootIngest, + admission: Admission ) -> FrontierProbeReceipt { match assemble_program_from_ingest( ingest: ingest, diff --git a/src/v2/test/claim/self_host/frontier_probe_empty_ingest_test.dag b/src/v2/test/claim/self_host/frontier_probe_empty_ingest_test.dag new file mode 100644 index 00000000000..eb64e8f6783 --- /dev/null +++ b/src/v2/test/claim/self_host/frontier_probe_empty_ingest_test.dag @@ -0,0 +1,72 @@ +module v2.test.claim.self_host.frontier_probe_empty_ingest + +import v2.compiler.name_resolve { Admission, ResolutionSubject } +import v2.compiler.self_host.frontier_probe { frontier_probe_emit_from_ingest } +import v2.compiler.self_host.frontier_probe_types { + FrontierProbeClosurePopulationRefused, + FrontierProbeCoverageRefused, + FrontierProbeDetailAbsent, + FrontierProbeReceipt, + MissingModule, + SourceRefContentHashMismatch, + SourceRefReadRefused, + SourceRootManifestElidedProbe +} +import v2.compiler.source_authority { SourceRootIngest } +import v2.std.qualified_name { qualified_name_from_dotted_string } +import std.algebra { Empty } +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import v2.std.text { String } + +data frontier_probe_empty_ingest_test_note: String = "An empty ingest is the absence of a closure, not a closure containing nothing. Before this wall the probe answered ^resolve_module_not_found with cause MissingModuleIdentityUnavailable, so a survey row asserted the module was missing when its sources had never been supplied — the two states have different remedies and the plausible one was being reported. The measurement that found it: src/v2/compiler/03_resolve.dag needs 71 source reads against the manifest's 64-row inline cap, so the transport emits SourceRootManifestElided with produced_row_count 0 and an Empty ingest, and the probe answered anyway. These claims are fixtures over the empty ingest and cost milliseconds; the RED is the previous behavior, asserted directly — the refusal must NOT be the module-not-found reason, and it must land in the population-refused detail rather than any other arm." + +fn empty_ingest() -> SourceRootIngest { + Empty +} + +fn empty_ingest_admission() -> Admission { + Admission { + subject: ResolutionSubject { + name: qualified_name_from_dotted_string(dotted: "v2.compiler.03_resolve") + }, + imports: Empty + } +} + +fn empty_ingest_receipt() -> FrontierProbeReceipt { + frontier_probe_emit_from_ingest( + module_path: "src/v2/compiler/03_resolve.dag", + ingest: empty_ingest(), + admission: empty_ingest_admission() + ) +} + +fn empty_ingest_detail_arm() -> Symbol { + match empty_ingest_receipt().detail { + FrontierProbeClosurePopulationRefused { cause: c } => c + FrontierProbeDetailAbsent => ^detail_absent + MissingModule { requested: _ } => ^detail_missing_module + SourceRootManifestElidedProbe { read_count: _, cap: _ } => ^detail_elided + FrontierProbeCoverageRefused { coverage: _ } => ^detail_coverage_refused + SourceRefReadRefused { source_ref: _, error: _ } => ^detail_read_refused + SourceRefContentHashMismatch { source_ref: _, expected: _, observed: _ } => + ^detail_hash_mismatch + } +} + +test fn empty_ingest_refuses_rather_than_reporting_a_missing_module_holds() -> Bool { + empty_ingest_receipt().located_reason != ^resolve_module_not_found +} + +test fn empty_ingest_names_the_absent_closure_holds() -> Bool { + empty_ingest_receipt().located_reason == ^frontier_probe_empty_ingest +} + +test fn empty_ingest_refusal_lands_in_the_population_detail_holds() -> Bool { + empty_ingest_detail_arm() == ^frontier_probe_empty_ingest +} + +test fn empty_ingest_keeps_the_module_path_it_was_asked_about_holds() -> Bool { + empty_ingest_receipt().module_path == "src/v2/compiler/03_resolve.dag" +} From 4634377668ae1cb0d1bf3235697d774910a3014c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 19:10:22 +0000 Subject: [PATCH 08/30] Bind the location field in the last stale AssembleRejectionDetail pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every other match site was updated when the field landed; this one kept the two-field pattern. Review 51431 flagged it. The predicted typecheck failure does not occur — the file's five claims were green by execution before this change and are green after — so the language admits the partial pattern. That is the reason to fix it rather than to leave it: a pattern that silently keeps matching when the carrier grows a field is how a match stops covering what its author thought it covered. Co-Authored-By: Claude Opus 5 (1M context) --- .../test/claim/self_host/frontier_probe_overlap_detail_test.dag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag b/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag index 9469b9bf594..1dbc30fed26 100644 --- a/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag +++ b/src/v2/test/claim/self_host/frontier_probe_overlap_detail_test.dag @@ -139,7 +139,7 @@ test fn assemble_receipt_carries_nonempty_rejection_chain() -> Bool { overlap_residue_stage: optional_present(value: stage) ) match receipt.assemble_detail { - AssembleRejectionDetail { rejection_chain: chain, cause: cause } => + AssembleRejectionDetail { rejection_chain: chain, cause: cause, at: _ } => match cause { GrammarChoiceOverlap { rows: rows } => (chain.head == ^parse_grammar_choice_overlap_residue) From 2bc11e0cd53300587d7457defe6b6b952dd7d04e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Wed, 12 Aug 2026 19:34:33 +0000 Subject: [PATCH 09/30] Make the clustering witness require a nonempty survey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 51432 is right that this claim's behavior against the checked-in stub changed. What it was before is the part worth naming: with receipts Empty, clustering asked self_emit_ready_count == module_count, both were 0, and it answered CommonRoot with dominant_located_reason ^probe_stage_emit_ok — a survey with no receipts reporting that every module reached emit. The one claim whose subject IS the clustering verdict was passing vacuously on a fabricated verdict. Receipt-derived clustering answers HeterogeneousGaps for an empty survey, so the claim now reds on the stub. That is the same state its siblings were already in and is the file's declared offline condition, not a CI failure shape: compiler_frontier_per_module_probe_survey_holds compares the receipt count against the 27-row roster, and 0 is not 27. The length check makes THIS claim's red say 'no receipts' explicitly rather than leaving it to read as a clustering disagreement. Verified by execution that the conjunction is false on an empty survey rather than erroring. Co-Authored-By: Claude Opus 5 (1M context) --- .../long/compiler_frontier_per_module_probe_test.dag | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/v2/test/claim/long/compiler_frontier_per_module_probe_test.dag b/src/v2/test/claim/long/compiler_frontier_per_module_probe_test.dag index 3da47b5efc1..6b3e2870850 100644 --- a/src/v2/test/claim/long/compiler_frontier_per_module_probe_test.dag +++ b/src/v2/test/claim/long/compiler_frontier_per_module_probe_test.dag @@ -49,7 +49,17 @@ test fn compiler_frontier_per_module_probe_survey_holds() -> Bool { && compiler_frontier_roster_located_reason_not_class_projection_holds() } +data compiler_frontier_gap_clustering_empty_stub_note: String = "This claim requires a NONEMPTY survey before it reads the verdict, and the requirement is the point rather than a guard. Against the checked-in stub (receipts Empty) it previously passed: clustering asked self_emit_ready_count == module_count, both were 0, and it answered CommonRoot with dominant_located_reason ^probe_stage_emit_ok — a survey with no receipts reporting that every module reached emit. That is the vacuous green this lane exists to prevent, and it sat inside the one claim whose subject is the clustering verdict. Receipt-derived clustering now answers HeterogeneousGaps for an empty survey (empty_survey_is_never_a_common_root_holds pins it per-PR), so this claim reds on the stub — the same state its siblings were already in, since compiler_frontier_per_module_probe_survey_holds compares the receipt count against the 27-row roster and 0 is not 27. Red-on-stub is the file's declared offline condition, not a CI failure shape; the length check makes THIS claim's red say so explicitly rather than leaving it to be read as a clustering disagreement." + test fn compiler_frontier_gap_clustering_common_root_at_blocker_class_holds() -> Bool { + clustering_survey_is_nonempty() && clustering_verdict_is_common_root() +} + +fn clustering_survey_is_nonempty() -> Bool { + length(xs: host_frontier_probe_survey_manifest.receipts) > 0 +} + +fn clustering_verdict_is_common_root() -> Bool { match frontier_gap_clustering_from_receipts( xs: host_frontier_probe_survey_manifest.receipts, module_count: length(xs: host_frontier_probe_survey_manifest.receipts) From 29558c81f742e3b5e8860cfcdd42b037601bf470 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 03:59:28 +0000 Subject: [PATCH 10/30] BL-0: give the body-lowering frontier its third state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The observer carried two arms over three reachable states, and the missing arm failed OPEN: every Rejected outcome fell to the else branch and reported BodyLoweringLowered. The instrument used to measure the body-lowering gap was counting failures as successes, so any population read through it undercounted by exactly the refusals. That arm is reachable by construction, not in theory: the rejection- propagation repair deliberately made body_lower_finish_for_normalize propagate genuine rejection through rejected_with_pending, and this classifier then discarded the distinction it created. Recognition had the same fail-open by a second route. It read the diagnostic HEAD alone, while retention travels through diagnostics_merge and rejected_with_pending — both of which place OUTER diagnostics first — so a retained body behind any pending diagnostic also read as lowered. It now scans the whole list. Six claims green by execution. RED verified by restoring the previous classifier: the rejection claim, the rejection-diagnostics claim and the behind-a-pending-diagnostic claim all red, while the three negative controls stay green — so the claims pin these two defects rather than failing broadly. The controls are the load-bearing half: a classifier that answered retained more often would satisfy the positive claims and destroy the count. Two existing consumers gain the third arm. Residue declared on the carrier: recognition still reads an encoding rather than a producer-returned variant, correct for every encoding the single retention producer can emit, with dissolve-on naming the producer change that makes this function the identity. First step of the sequence gunbc.roadmap_authority already declares: honest frontier, then the normalized-tree construction boundary, then the retained population to zero. Co-Authored-By: Claude Opus 5 (1M context) --- src/v2/compiler/body_lowering_fold.dag | 36 +++++- .../fn_body_seam_witness_helpers.dag | 2 + .../frontier_three_state_test.dag | 118 ++++++++++++++++++ ...ymbol_index_body_producer_witness_test.dag | 2 + 4 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 src/v2/test/claim/body_lowering/frontier_three_state_test.dag diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index 92348887877..c4a9b3942c9 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -31,7 +31,7 @@ import v2.extdeps.languages.dag { parse_production_emitted_identity_optional, qualified_name_from_module_node, } -import v2.std.algebra { list_append, list_snoc_item, length, zip_eq } +import v2.std.algebra { fold_list, list_append, list_snoc_item, length, zip_eq } import v2.std.collection { List, list_at_optional @@ -50,6 +50,7 @@ import v2.std.diagnostic { Accepted, Diagnostic, Diagnostics, + NonEmptyDiagnostics, ExternalContractUnknown, Outcome, Rejected, @@ -130,15 +131,30 @@ fn body_lowering_forward_fold_surface_identities_agree() -> Bool { } } +data body_lowering_frontier_three_state_note: String = "THE FRONTIER OBSERVER MUST SPAN THE STATE SPACE THE PRODUCER CAN REACH. It carried two arms over three reachable states, and the missing arm failed OPEN: every Rejected outcome fell to the else branch and reported BodyLoweringLowered — a body that failed to lower, counted as one that lowered. That is the instrument used to measure the body-lowering gap reporting failures as successes, so every population derived through it undercounted by exactly the refusals. Reachability is not hypothetical: body_lower_finish_for_normalize propagates genuine rejection through rejected_with_pending by construction (see the rejection-propagation note above), so the arm this classifier lacked is the one that repair deliberately created.\n\nRecognition also read the diagnostic HEAD alone. Retention travels through diagnostics_merge and rejected_with_pending, which place OUTER diagnostics first, so a retained body behind any pending diagnostic classified as Lowered — the same fail-open by a second route. It now scans the whole diagnostic list for the retained reason.\n\nRESIDUE, declared rather than closed: recognition still reads diagnostics rather than the producer returning a variant, so this is a classifier over an encoding, not a structural read. It is correct for every encoding the single retention producer (body_lower_wrapper_retained_shell) can emit, and it would be wrong if a second producer ever emitted retention differently — which is why retention keeps exactly one producer. Dissolve-on: body_lower_after_children returns BodyLoweringFrontierDisposition directly, at which point this function is the identity and disappears with the encoding." + type BodyLoweringFrontierDisposition = BodyLoweringLowered | BodyLoweringWrapperRetained { reason: Symbol } + | BodyLoweringRejected { diagnostics: NonEmptyDiagnostics } + +fn body_lowering_diagnostic_is_wrapper_retained(d: Diagnostic) -> Bool { + d.reason == ^body_lowering_reason_wrapper_retained_emitted +} + +fn body_lowering_any_diagnostic_is_wrapper_retained(ne: NonEmptyDiagnostics) -> Bool { + fold_list( + xs: ne.tail, + empty: body_lowering_diagnostic_is_wrapper_retained(d: ne.head), + cons: fn(found, d) { found || body_lowering_diagnostic_is_wrapper_retained(d: d) } + ) +} fn body_lower_outcome_is_wrapper_retained(o: Outcome) -> Bool { match o { Accepted { value: _, diagnostics: d } => match d { - Some { diagnostics: ne } => ne.head.reason == ^body_lowering_reason_wrapper_retained_emitted + Some { diagnostics: ne } => body_lowering_any_diagnostic_is_wrapper_retained(ne: ne) None => false } Rejected { diagnostics: _ } => false @@ -146,10 +162,18 @@ fn body_lower_outcome_is_wrapper_retained(o: Outcome) -> Bool { } fn body_lower_outcome_frontier_disposition(o: Outcome) -> BodyLoweringFrontierDisposition { - if body_lower_outcome_is_wrapper_retained(o: o) { - BodyLoweringWrapperRetained { reason: ^body_lowering_reason_wrapper_retained_emitted } - } else { - BodyLoweringLowered + match o { + Rejected { diagnostics: r } => BodyLoweringRejected { diagnostics: r } + Accepted { value: _, diagnostics: d } => + match d { + Some { diagnostics: ne } => + if body_lowering_any_diagnostic_is_wrapper_retained(ne: ne) { + BodyLoweringWrapperRetained { reason: ^body_lowering_reason_wrapper_retained_emitted } + } else { + BodyLoweringLowered + } + None => BodyLoweringLowered + } } } diff --git a/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag b/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag index 958871750bd..dfba44d3db3 100644 --- a/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag +++ b/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag @@ -2,6 +2,7 @@ module v2.test.claim.body_lowering.fn_body_seam_witness_helpers import v2.compiler.body_lowering_fold { BodyLoweringLowered, + BodyLoweringRejected, BodyLoweringWrapperRetained, body_lower_fn_body_shell, body_lower_outcome_frontier_disposition @@ -126,6 +127,7 @@ fn wave1_gate1_a1_fn_body_seam_red_invalid_shell_wrapper_retained_holds() -> Boo match body_lower_outcome_frontier_disposition(o: body_lower_fn_body_shell(shell: shell)) { BodyLoweringWrapperRetained { reason: r } => r == ^body_lowering_reason_wrapper_retained_emitted BodyLoweringLowered => false + BodyLoweringRejected { diagnostics: _ } => false } } diff --git a/src/v2/test/claim/body_lowering/frontier_three_state_test.dag b/src/v2/test/claim/body_lowering/frontier_three_state_test.dag new file mode 100644 index 00000000000..892473ae6d7 --- /dev/null +++ b/src/v2/test/claim/body_lowering/frontier_three_state_test.dag @@ -0,0 +1,118 @@ +module v2.test.claim.body_lowering.frontier_three_state + +import v2.compiler.body_lowering_fold { + BodyLoweringLowered, + BodyLoweringRejected, + BodyLoweringWrapperRetained, + body_lower_outcome_frontier_disposition, + body_lower_wrapper_retained_shell +} +import std.algebra { Cons, Empty } +import v2.std.diagnostic { + Accepted, + Diagnostic, + NonEmptyDiagnostics, + None, + Outcome, + Rejected, + Some, + Textual, + Unavailable, + UserInputBoundary, + WholeFile, + diagnostics_singleton +} +import v2.std.logic { Bool } +import v2.std.node { Conj, Node, Symbol, TypeNode, node_synthetic } +import v2.std.text { String } + +data frontier_three_state_note: String = "The frontier observer over the three states its producer can reach. It carried two arms, and the missing one failed OPEN: a Rejected outcome fell to the else branch and reported BodyLoweringLowered — the instrument that measures the body-lowering gap counting failures as successes, so every population read through it undercounted by exactly the refusals. Recognition also read the diagnostic HEAD alone, while retention travels through diagnostics_merge and rejected_with_pending, which put OUTER diagnostics first — so a retained body behind any pending diagnostic was also reported lowered. Both routes are asserted here, and both are the executed RED: on the previous classifier the rejection claim and the behind-a-pending-diagnostic claim return Lowered. The negative controls matter as much: an unrelated diagnostic must NOT manufacture retention, and a clean accept must stay lowered — a classifier that answered retained more often would satisfy the positive claims while destroying the count." + +fn probe_shell() -> Node { + node_synthetic(kind: TypeNode { connective: Conj }, children: []) +} + +fn unrelated_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^frontier_three_state_unrelated_reason, + at: Textual { file: ^frontier_three_state_probe_file, extent: WholeFile }, + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn retained_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^body_lowering_reason_wrapper_retained_emitted, + at: Textual { file: ^frontier_three_state_probe_file, extent: WholeFile }, + correction: Unavailable { reason: UserInputBoundary } + } +} + +fn rejected_outcome() -> Outcome { + Rejected { + diagnostics: NonEmptyDiagnostics { head: unrelated_diagnostic(), tail: Empty } + } +} + +fn clean_accept() -> Outcome { + Accepted { value: probe_shell(), diagnostics: None } +} + +fn accept_with_unrelated_diagnostic() -> Outcome { + Accepted { + value: probe_shell(), + diagnostics: Some { diagnostics: diagnostics_singleton(d: unrelated_diagnostic()) } + } +} + +fn retention_behind_a_pending_diagnostic() -> Outcome { + Accepted { + value: probe_shell(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { + head: unrelated_diagnostic(), + tail: Cons { head: retained_diagnostic(), tail: Empty } + } + } + } +} + +fn disposition_name(o: Outcome) -> Symbol { + match body_lower_outcome_frontier_disposition(o: o) { + BodyLoweringLowered => ^disposition_lowered + BodyLoweringWrapperRetained { reason: _ } => ^disposition_wrapper_retained + BodyLoweringRejected { diagnostics: _ } => ^disposition_rejected + } +} + +fn rejected_disposition_keeps_its_reason() -> Symbol { + match body_lower_outcome_frontier_disposition(o: rejected_outcome()) { + BodyLoweringRejected { diagnostics: ne } => ne.head.reason + BodyLoweringWrapperRetained { reason: _ } => ^not_rejected + BodyLoweringLowered => ^not_rejected + } +} + +test fn a_rejection_is_never_reported_as_lowered_holds() -> Bool { + disposition_name(o: rejected_outcome()) == ^disposition_rejected +} + +test fn a_rejection_carries_its_diagnostics_through_holds() -> Bool { + rejected_disposition_keeps_its_reason() == ^frontier_three_state_unrelated_reason +} + +test fn retention_behind_a_pending_diagnostic_is_still_retention_holds() -> Bool { + disposition_name(o: retention_behind_a_pending_diagnostic()) == ^disposition_wrapper_retained +} + +test fn the_sole_retention_producer_reads_as_retention_holds() -> Bool { + disposition_name(o: body_lower_wrapper_retained_shell(shell: probe_shell())) == ^disposition_wrapper_retained +} + +test fn an_unrelated_diagnostic_does_not_manufacture_retention_holds() -> Bool { + disposition_name(o: accept_with_unrelated_diagnostic()) == ^disposition_lowered +} + +test fn a_clean_accept_is_lowered_holds() -> Bool { + disposition_name(o: clean_accept()) == ^disposition_lowered +} diff --git a/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag b/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag index 300a73bff5c..44a13f306e1 100644 --- a/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag +++ b/src/v2/test/claim/self_host/wave1_gate1_a1_symbol_index_body_producer_witness_test.dag @@ -9,6 +9,7 @@ import v2.compiler.body_lowering_fold { body_lowering_match_spine_identity_absent_dispatch_holds, body_lowering_match_spine_identity_absent_sniff_holds, BodyLoweringLowered, + BodyLoweringRejected, BodyLoweringWrapperRetained } import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } @@ -44,6 +45,7 @@ fn wave1_gate1_a1_wrapper_retained_red_control_holds() -> Bool { match body_lower_outcome_frontier_disposition(o: body_lower_fn_decl_to_arrow(shell: shell)) { BodyLoweringWrapperRetained { reason: r } => r == ^body_lowering_reason_wrapper_retained_emitted BodyLoweringLowered => false + BodyLoweringRejected { diagnostics: _ } => false } } From 6fb75a7eefc7ab7a0f1c99b85ef4964a114e3fbc Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 04:22:24 +0000 Subject: [PATCH 11/30] BL-2: parameterise the census roster, and add the body-lowering population probe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stage classifier and reconciliation machinery already existed in gunbc.tools.frontier_ingestion_probe, hardcoded to the 15-member std ingestion roster. Measuring a second population needed the roster to be an argument rather than a second copy of the mechanism — two classifiers are two authorities that can disagree. take_census_over(rows) is the general form; take_frontend_census() is now that call with the module roster. Roster identity derives from the rows PASSED, never the module-level roster, so a receipt cannot claim a denominator it was not measured over — the exact misreading census_receipt_provenance_note exists to prevent, which a parameterised census stamping the default identity would have reintroduced. All 14 existing claims over the instrument still pass. The new probe measures the body-lowering population: the two SH-D shards with measured door receipts, the std members the pre-repair observation recorded as retained plus the one it never re-ran, and the seam's own modules. It is deliberately not the whole corpus, and it carries no aggregate pass/fail — the population IS the finding, and a Boolean over it would be a verdict on subjects not separately established. WHY BEFORE THE WALL: normalize returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it. Making retention unconstructible changes which modules normalize accepts, so the newly refusing population must be known before the wall lands rather than discovered from a red tree afterwards. Co-Authored-By: Claude Opus 5 (1M context) --- dag/tools/body_lowering_population_probe.dag | 94 ++++++++++++++++++++ dag/tools/frontier_ingestion_probe.dag | 44 ++++++--- 2 files changed, 128 insertions(+), 10 deletions(-) create mode 100644 dag/tools/body_lowering_population_probe.dag diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag new file mode 100644 index 00000000000..17577952e1d --- /dev/null +++ b/dag/tools/body_lowering_population_probe.dag @@ -0,0 +1,94 @@ +module gunbc.tools.body_lowering_population_probe + +import gunbc.tools.frontier_ingestion_probe { + CensusRefusedAtReconciliation, + CensusRefusedAtWorld, + CensusTaken, + FrontendCensusReceipt, + FrontierMember, + FrontendStage, + LexRefused, + MemberObservation, + NormalizeAccepted, + NormalizeGraftRefused, + NormalizeOther, + NormalizeRetained, + ParseRefused, + take_census_over +} +import v2.std.algebra { fold_list } +import v2.std.collection { List } +import v2.std.integer { Int } +import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } +import v2.std.logic { Bool } +import v2.std.compilers.lexing { symbol_lexeme } +import v2.std.node { Symbol } +import v2.std.text { String } + +data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree + +data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT, homed outside a *_test.dag module for the same reason gunbc.tools.frontier_ingestion_probe is: what it reports is a revision-scoped OBSERVATION of the present frontier, and enrolling that as a test declaration would make an assertion that goes RED on the repair and GREEN on a regression. It reuses take_census_over and classify_source rather than reproducing them, so there is one stage classifier in the repository and not two.\n\nWHY IT EXISTS. The body-lowering wall cannot be scheduled against an unmeasured population. normalize today returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it, so retention is kept out of resolve by propagation rather than by construction. Making retention unconstructible is the right wall and it CHANGES WHICH MODULES NORMALIZE ACCEPTS — so the population that would newly refuse has to be known before the wall lands, not discovered from a red tree afterwards.\n\nWHAT THIS ROSTER IS. The body-lowering subjects: the two SH-D shards with measured door receipts, the four std members the pre-repair frontier observation recorded as retained plus the one it never re-ran, and the bare-expression specimens v2.lens.vacuity independently found rejecting at this seam. It is deliberately NOT the whole corpus — a per-file census over every module is a different and much larger instrument — so a count taken here is a count over THESE members and carries no claim about the rest.\n\nWHAT A READER MAY NOT DO WITH IT. The per-member stage is the observation. There is no aggregate pass/fail, because the population is the finding and a Boolean over it would be a verdict on subjects that have not all been separately established." + +data body_lowering_population_roster: List = [ + FrontierMember { label: ^use_site_verdict, path: "src/v2/compiler/use_site_verdict.dag" }, + FrontierMember { label: ^materialization_carriers, path: "src/v2/compiler/materialization_carriers.dag" }, + FrontierMember { label: ^logic, path: "src/v2/std/logic.dag" }, + FrontierMember { label: ^optional, path: "src/v2/std/optional.dag" }, + FrontierMember { label: ^diagnostic, path: "src/v2/std/diagnostic.dag" }, + FrontierMember { label: ^occurrence_identity, path: "dag/std/occurrence_identity.dag" }, + FrontierMember { label: ^nat, path: "src/v2/std/nat.dag" }, + FrontierMember { label: ^body_lowering_fold, path: "src/v2/compiler/body_lowering_fold.dag" }, + FrontierMember { label: ^body_producer_forward, path: "src/v2/compiler/body_producer_forward.dag" }, + FrontierMember { label: ^normalized_tree, path: "src/v2/compiler/normalized_tree.dag" }, +] + +fn take_body_lowering_population_census() -> FrontendCensusReceipt { + take_census_over(rows: body_lowering_population_roster) +} + +fn stage_label_of(s: FrontendStage) -> String { + match s { + LexRefused => "LEX" + ParseRefused => "PARSE" + NormalizeGraftRefused => "NORM_GRAFT_REFUSED" + NormalizeRetained => "NORM_RETAINED" + NormalizeOther => "NORM_OTHER" + NormalizeAccepted => "NORM_ACCEPTED" + } +} + +fn observation_line(o: MemberObservation) -> String { + concat(symbol_lexeme(sym: o.label), concat(" ", stage_label_of(s: o.stage))) +} + +fn census_report() -> String { + match take_body_lowering_population_census() { + CensusRefusedAtWorld => "REFUSED_AT_WORLD" + CensusRefusedAtReconciliation { cause: _ } => "REFUSED_AT_RECONCILIATION" + CensusTaken { roster_identity: _, observations: obs } => + fold_list( + xs: obs, + empty: "", + cons: fn(acc, o) { + if acc == "" { observation_line(o: o) } else { concat(acc, concat(" | ", observation_line(o: o))) } + } + ) + } +} + +fn count_stage_matching(target: String) -> Int { + match take_body_lowering_population_census() { + CensusRefusedAtWorld => 0 - 1 + CensusRefusedAtReconciliation { cause: _ } => 0 - 2 + CensusTaken { roster_identity: _, observations: obs } => + fold_list( + xs: obs, + empty: 0, + cons: fn(n, o) { if stage_label_of(s: o.stage) == target { n + 1 } else { n } } + ) + } +} + +fn retained_member_count() -> Int { + count_stage_matching(target: "NORM_RETAINED") +} diff --git a/dag/tools/frontier_ingestion_probe.dag b/dag/tools/frontier_ingestion_probe.dag index d207200ca08..61aa2c36480 100644 --- a/dag/tools/frontier_ingestion_probe.dag +++ b/dag/tools/frontier_ingestion_probe.dag @@ -286,7 +286,11 @@ fn frontier_roster_identity() -> ContentHash { } fn observe_roster_in(world: FrontendWorld) -> List { - fold_list(xs: frontier_roster, empty: [] as List, cons: fn(acc, m) { + observe_rows_in(world: world, rows: frontier_roster) +} + +fn observe_rows_in(world: FrontendWorld, rows: List) -> List { + fold_list(xs: rows, empty: [] as List, cons: fn(acc, m) { concat(acc, [classify_member_in(world: world, m: m)]) }) } @@ -294,14 +298,20 @@ fn observe_roster_in(world: FrontendWorld) -> List { // The whole census: prepare the world ONCE, classify every rostered member through it, then // reconcile the resulting population against the roster before anything may read it as a census. fn take_frontend_census() -> FrontendCensusReceipt { + take_census_over(rows: frontier_roster) +} + +data census_over_rows_note: String = "take_census_over takes the roster as an argument for the same reason verify_roster does: a census that could only ever be taken over the one live roster cannot be reconciled against a different denominator, so a second population needs a second copy of the mechanism, and two copies of a classifier are two authorities that can disagree. The roster identity is derived from the rows PASSED, never from the module-level roster, so a receipt cannot claim a denominator it was not measured over — that is the exact misreading census_receipt_provenance_note exists to prevent, and it would be reintroduced by a parameterised census that kept stamping the default identity." + +fn take_census_over(rows: List) -> FrontendCensusReceipt { match prepare_frontend_world() { v2.std.diagnostic.Rejected { diagnostics: _ } => CensusRefusedAtWorld v2.std.diagnostic.Accepted { value: world, diagnostics: _ } => { - let obs = observe_roster_in(world: world) - let verdict = reconcile_census(obs: obs) + let obs = observe_rows_in(world: world, rows: rows) + let verdict = reconcile_census_over(obs: obs, rows: rows) match verdict { CensusExact => - CensusTaken { roster_identity: frontier_roster_identity(), observations: obs } + CensusTaken { roster_identity: roster_identity(rows: rows), observations: obs } _ => CensusRefusedAtReconciliation { cause: verdict } } } @@ -368,21 +378,35 @@ fn count_label(obs: List, label: Symbol) -> Int { } fn roster_contains(label: Symbol) -> Bool { - fold_list(xs: frontier_roster, empty: false, cons: fn(acc, m) { + rows_contain_label(rows: frontier_roster, label: label) +} + +fn rows_contain_label(rows: List, label: Symbol) -> Bool { + fold_list(xs: rows, empty: false, cons: fn(acc, m) { acc || (m.label == label) }) } fn reconcile_census(obs: List) -> CensusReconciliation { - let roster = verify_frontier_roster() + reconcile_census_over(obs: obs, rows: frontier_roster) +} + +fn reconcile_census_over( + obs: List, + rows: List +) -> CensusReconciliation { + let roster = verify_roster(rows: rows) match roster { - FrontierRosterValid => reconcile_against_valid_roster(obs: obs) + FrontierRosterValid => reconcile_against_valid_rows(obs: obs, rows: rows) _ => CensusRosterInvalid { verdict: roster } } } -fn reconcile_against_valid_roster(obs: List) -> CensusReconciliation { - let covered = fold_list(xs: frontier_roster, empty: CensusExact, cons: fn(acc, m) { +fn reconcile_against_valid_rows( + obs: List, + rows: List +) -> CensusReconciliation { + let covered = fold_list(xs: rows, empty: CensusExact, cons: fn(acc, m) { match acc { CensusExact => { let n = count_label(obs: obs, label: m.label) @@ -402,7 +426,7 @@ fn reconcile_against_valid_roster(obs: List) -> CensusReconci fold_list(xs: obs, empty: CensusExact, cons: fn(acc, o) { match acc { CensusExact => - if roster_contains(label: o.label) { + if rows_contain_label(rows: rows, label: o.label) { CensusExact } else { CensusUnrosteredObservation { label: o.label } From b3fb5f1ef0dd32578c3ac58cedc18e3f5441403e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 07:59:34 +0000 Subject: [PATCH 12/30] BL-2 first reading: neither SH-D shard retains as a file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Executed, three-member roster: use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED. That corrects the shard rows rather than the finding. Neither shard retains as a file — use_site_verdict normalizes clean — so the wrapper-retained residue measured in their door chains comes from OTHER modules in their import closures, of which optional is one confirmed instance. 'The shard is body-lowering blocked' was too coarse: the shard is blocked because its closure contains retaining modules, and the repair subjects are those. The roster is three because ten was executed and ABANDONED at 2h51m and 8.3 GiB resident with no verdict, on a host whose shared 20 GiB slice is reaped largest-task-first — a hazard to other sessions before it was a slow measurement. The instrument's own note already owed a realized execution for exactly this reason. The seven dropped members are unmeasured, not excluded, and the carrier says so. The 10-to-3 shrink is nonlinear (2h51m to ~2min), so a dropped member is pathological rather than merely larger. Which one is NOT established, and guessing the largest file would be a guess dressed as a finding. Co-Authored-By: Claude Opus 5 (1M context) --- dag/tools/body_lowering_population_probe.dag | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 17577952e1d..75059ca6b84 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -29,17 +29,14 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT, homed outside a *_test.dag module for the same reason gunbc.tools.frontier_ingestion_probe is: what it reports is a revision-scoped OBSERVATION of the present frontier, and enrolling that as a test declaration would make an assertion that goes RED on the repair and GREEN on a regression. It reuses take_census_over and classify_source rather than reproducing them, so there is one stage classifier in the repository and not two.\n\nWHY IT EXISTS. The body-lowering wall cannot be scheduled against an unmeasured population. normalize today returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it, so retention is kept out of resolve by propagation rather than by construction. Making retention unconstructible is the right wall and it CHANGES WHICH MODULES NORMALIZE ACCEPTS — so the population that would newly refuse has to be known before the wall lands, not discovered from a red tree afterwards.\n\nWHAT THIS ROSTER IS. The body-lowering subjects: the two SH-D shards with measured door receipts, the four std members the pre-repair frontier observation recorded as retained plus the one it never re-ran, and the bare-expression specimens v2.lens.vacuity independently found rejecting at this seam. It is deliberately NOT the whole corpus — a per-file census over every module is a different and much larger instrument — so a count taken here is a count over THESE members and carries no claim about the rest.\n\nWHAT A READER MAY NOT DO WITH IT. The per-member stage is the observation. There is no aggregate pass/fail, because the population is the finding and a Boolean over it would be a verdict on subjects that have not all been separately established." +data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member roster): use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: the same census over ten members ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more dropped members is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." + +data body_lowering_population_roster_cost_note: String = "THE ROSTER IS THREE, AND THE REASON IS A MEASUREMENT RATHER THAN A PREFERENCE. A ten-member roster over these subjects was executed and ABANDONED at 2h51m wall and 8.3 GiB resident without producing a verdict — on a host whose shared slice is 20 GiB and whose kernel kills the LARGEST task in it, so that run was a hazard to every other session before it was a slow measurement. The instrument this extends already declared the cost: its own roster note records the accepted-classification path measuring over the five-second fast-lane budget for fifteen files, and states that the batched or realized execution which would make a live census affordable is still owed. So the roster is sized to what the present instrument can deliver, not to what the question deserves, and that gap is a deficit in the instrument rather than a property of the subjects.\n\nWHAT THE THREE ARE: the two SH-D shards with measured door receipts, plus one std member the pre-repair observation recorded as retained. THE SEVEN DROPPED MEMBERS ARE NOT OBSERVED-AND-EXCLUDED — they are unmeasured, and no count taken here may be read as covering them. Dissolve-on: the realized census execution the instrument's own note owes, at which point the roster returns to the full population in one run." + data body_lowering_population_roster: List = [ FrontierMember { label: ^use_site_verdict, path: "src/v2/compiler/use_site_verdict.dag" }, FrontierMember { label: ^materialization_carriers, path: "src/v2/compiler/materialization_carriers.dag" }, - FrontierMember { label: ^logic, path: "src/v2/std/logic.dag" }, FrontierMember { label: ^optional, path: "src/v2/std/optional.dag" }, - FrontierMember { label: ^diagnostic, path: "src/v2/std/diagnostic.dag" }, - FrontierMember { label: ^occurrence_identity, path: "dag/std/occurrence_identity.dag" }, - FrontierMember { label: ^nat, path: "src/v2/std/nat.dag" }, - FrontierMember { label: ^body_lowering_fold, path: "src/v2/compiler/body_lowering_fold.dag" }, - FrontierMember { label: ^body_producer_forward, path: "src/v2/compiler/body_producer_forward.dag" }, - FrontierMember { label: ^normalized_tree, path: "src/v2/compiler/normalized_tree.dag" }, ] fn take_body_lowering_population_census() -> FrontendCensusReceipt { From 766a1bd2353e9ef2114079754ffcf8956444ba0f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 16:48:41 +0000 Subject: [PATCH 13/30] BL-2: delete the aggregate helpers, which counted refusals and rendered text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, both reachable only because an aggregate was authored over a roster this module's own note says carries no aggregate verdict. A refusal became an integer. count_stage_matching answered 0 - 1 for CensusRefusedAtWorld and 0 - 2 for CensusRefusedAtReconciliation, so a typed refusal and a genuine population count inhabited one type: a caller could add, compare or display a failure as a measurement, and -1 reads as a number rather than as a stop. The census already carries typed refusal arms, so this discarded exactly the distinction they exist to preserve. Presentation became semantic authority. The count compared stage_label_of(s) against a target String, but FrontendStage is a closed coproduct — a label rename would change the count with no stage changing, and a label collision would merge distinct stages. Counting belongs to the structural variant or to frontend_stage_eq, never to rendered text. Deleted rather than repaired: the module already states the per-member observation IS the finding and that no aggregate verdict is warranted over subjects that have not each been separately established. The helpers contradicted the stated scope, so there is nothing for a corrected aggregate to mean here. Int and Bool were left import-only and are dropped with them. --- dag/tools/body_lowering_population_probe.dag | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 75059ca6b84..f8bf7f08c78 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -18,9 +18,7 @@ import gunbc.tools.frontier_ingestion_probe { } import v2.std.algebra { fold_list } import v2.std.collection { List } -import v2.std.integer { Int } import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } -import v2.std.logic { Bool } import v2.std.compilers.lexing { symbol_lexeme } import v2.std.node { Symbol } import v2.std.text { String } @@ -73,19 +71,4 @@ fn census_report() -> String { } } -fn count_stage_matching(target: String) -> Int { - match take_body_lowering_population_census() { - CensusRefusedAtWorld => 0 - 1 - CensusRefusedAtReconciliation { cause: _ } => 0 - 2 - CensusTaken { roster_identity: _, observations: obs } => - fold_list( - xs: obs, - empty: 0, - cons: fn(n, o) { if stage_label_of(s: o.stage) == target { n + 1 } else { n } } - ) - } -} - -fn retained_member_count() -> Int { - count_stage_matching(target: "NORM_RETAINED") -} +data body_lowering_no_aggregate_note: String = "THERE IS NO AGGREGATE COUNT HERE, and its absence is the module's own stated scope rather than an omission. An earlier revision carried count_stage_matching and retained_member_count, and both were deleted for defects worth recording so they are not reintroduced.\n\nA REFUSAL BECAME AN INTEGER. count_stage_matching answered 0 - 1 for CensusRefusedAtWorld and 0 - 2 for CensusRefusedAtReconciliation, so a typed refusal and a genuine population count inhabited one type. A caller could add, compare or display a failure as a measurement, and -1 is a plausible-looking number rather than a loud one. The census already carries typed refusal arms; collapsing them into the counting type discarded exactly the distinction those arms exist to preserve.\n\nPRESENTATION BECAME SEMANTIC AUTHORITY. It counted by comparing stage_label_of(s) against a target String. FrontendStage is a closed coproduct, so a label rename would have changed the count with no stage changing, and a label collision would have merged distinct stages — the renderer deciding a semantic question. Counting belongs to the structural variant or to frontend_stage_eq, never to rendered text.\n\nBoth were reachable because an aggregate was authored at all. This module's note already says the per-member observation IS the finding and that no aggregate verdict is warranted over a roster whose members have not each been separately established; the helpers contradicted that. A reader wanting a count over these members reads the per-member observations and counts the subjects it can name." From 5162c363a8c809c7ec6a2bce0363c23e2404f371 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 16:52:45 +0000 Subject: [PATCH 14/30] BL-2: separate the subject roster from the execution batch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous revision declared three rows named body_lowering_population_roster while the module's prose said the subjects were ten. That made the planning error this instrument exists to detect representable in its own carrier: seven unmeasured subjects erased, one retained member observed, and a bare count licensing "the retained population is one". Two authorities now. body_lowering_subject_roster is the denominator, carrying exact identities cited rather than asserted — the two SH-D shards, and five std members from the executed table in docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md (logic, optional, diagnostic, occurrence_identity as its NORM_RETAINED rows; node as its NOT RE-OBSERVED row). body_lowering_execution_batch is what one run selected, and it is DERIVED from the roster by label selection rather than re-authored beside it, so a batch row cannot name a path the roster does not. Standing is three states, because observed-or-absent cannot carry this population: a subject dropped for cost, a subject whose run a budget interrupted, and a subject genuinely measured are three epistemic positions with three remedies, and collapsing the first two into absence renders "we did not look" as "there is nothing there". SubjectInterrupted stays separate from SubjectUnmeasured because node's 900s overrun is a real lower bound on cost that an unmeasured subject does not carry; body_lowering_prior_standing gives that arm its producer from the cited historical receipt, declared apart from this run so the two can never be read as one measurement. The population is EIGHT, derived rather than chosen (operator ruling 2026-08-13). Seven exact identities plus one subject for the vacuity bare-expression specimen CLASS, which grounds as a class and not as members: the vacuity note describes "many isolated configurations" and enumerates none, and its optional_absent copy is the same subject as the rostered optional member. Minting three rows would have fabricated two identities to satisfy a prose sentence. The prose "ten" is recorded as retired rather than silently corrected, and the note states that eight is not an authority either — completeness is an identity join, not a count equality. --- dag/tools/body_lowering_population_probe.dag | 126 +++++++++++++++++-- 1 file changed, 118 insertions(+), 8 deletions(-) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index f8bf7f08c78..dc9110ef54b 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -18,6 +18,7 @@ import gunbc.tools.frontier_ingestion_probe { } import v2.std.algebra { fold_list } import v2.std.collection { List } +import v2.std.logic { Bool } import v2.std.live_tree { LiveTreeDisposition, ReadsLiveTree } import v2.std.compilers.lexing { symbol_lexeme } import v2.std.node { Symbol } @@ -25,22 +26,131 @@ import v2.std.text { String } data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree -data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT, homed outside a *_test.dag module for the same reason gunbc.tools.frontier_ingestion_probe is: what it reports is a revision-scoped OBSERVATION of the present frontier, and enrolling that as a test declaration would make an assertion that goes RED on the repair and GREEN on a regression. It reuses take_census_over and classify_source rather than reproducing them, so there is one stage classifier in the repository and not two.\n\nWHY IT EXISTS. The body-lowering wall cannot be scheduled against an unmeasured population. normalize today returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it, so retention is kept out of resolve by propagation rather than by construction. Making retention unconstructible is the right wall and it CHANGES WHICH MODULES NORMALIZE ACCEPTS — so the population that would newly refuse has to be known before the wall lands, not discovered from a red tree afterwards.\n\nWHAT THIS ROSTER IS. The body-lowering subjects: the two SH-D shards with measured door receipts, the four std members the pre-repair frontier observation recorded as retained plus the one it never re-ran, and the bare-expression specimens v2.lens.vacuity independently found rejecting at this seam. It is deliberately NOT the whole corpus — a per-file census over every module is a different and much larger instrument — so a count taken here is a count over THESE members and carries no claim about the rest.\n\nWHAT A READER MAY NOT DO WITH IT. The per-member stage is the observation. There is no aggregate pass/fail, because the population is the finding and a Boolean over it would be a verdict on subjects that have not all been separately established." +data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT, homed outside a *_test.dag module for the same reason gunbc.tools.frontier_ingestion_probe is: what it reports is a revision-scoped OBSERVATION of the present frontier, and enrolling that as a test declaration would make an assertion that goes RED on the repair and GREEN on a regression. It reuses take_census_over and classify_source rather than reproducing them, so there is one stage classifier in the repository and not two.\n\nWHY IT EXISTS. The body-lowering wall cannot be scheduled against an unmeasured population. normalize today returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it, so retention is kept out of resolve by propagation rather than by construction. Making retention unconstructible is the right wall and it CHANGES WHICH MODULES NORMALIZE ACCEPTS — so the population that would newly refuse has to be known before the wall lands, not discovered from a red tree afterwards.\n\nWHAT THE SUBJECTS ARE is body_lowering_subject_roster's to say, and this note deliberately does not restate it — an earlier revision carried the population as a prose sentence beside a shorter roster, and the two disagreeing is the defect that produced this rewrite. What holds here is only the scope: the subjects are deliberately NOT the whole corpus — a per-file census over every module is a different and much larger instrument — so anything read off this module is a reading over THOSE subjects and carries no claim about the rest.\n\nWHAT A READER MAY NOT DO WITH IT. The per-member stage is the observation. There is no aggregate pass/fail, because the population is the finding and a Boolean over it would be a verdict on subjects that have not all been separately established." -data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member roster): use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: the same census over ten members ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more dropped members is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." +data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member execution batch): use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: a ten-member run of this census ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more subjects outside the batch is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." -data body_lowering_population_roster_cost_note: String = "THE ROSTER IS THREE, AND THE REASON IS A MEASUREMENT RATHER THAN A PREFERENCE. A ten-member roster over these subjects was executed and ABANDONED at 2h51m wall and 8.3 GiB resident without producing a verdict — on a host whose shared slice is 20 GiB and whose kernel kills the LARGEST task in it, so that run was a hazard to every other session before it was a slow measurement. The instrument this extends already declared the cost: its own roster note records the accepted-classification path measuring over the five-second fast-lane budget for fifteen files, and states that the batched or realized execution which would make a live census affordable is still owed. So the roster is sized to what the present instrument can deliver, not to what the question deserves, and that gap is a deficit in the instrument rather than a property of the subjects.\n\nWHAT THE THREE ARE: the two SH-D shards with measured door receipts, plus one std member the pre-repair observation recorded as retained. THE SEVEN DROPPED MEMBERS ARE NOT OBSERVED-AND-EXCLUDED — they are unmeasured, and no count taken here may be read as covering them. Dissolve-on: the realized census execution the instrument's own note owes, at which point the roster returns to the full population in one run." +data body_lowering_subject_roster_note: String = "THE SUBJECT ROSTER AND THE EXECUTION BATCH ARE TWO AUTHORITIES, and merging them is the planning error this instrument exists to detect. A previous revision declared three rows under the name body_lowering_population_roster while the module's own prose said the subjects were ten. That makes the error representable in the exact shape we are trying to catch: seven unmeasured subjects erased, one retained member observed, and a bare count licensing the sentence 'the retained population is one'. The denominator has to be the SUBJECTS; what a given run could afford to execute is a separate and smaller fact.\n\nSo body_lowering_subject_roster is the denominator and body_lowering_execution_batch is what one run selected from it. A subject not in the batch is SubjectUnmeasured with a cause, never absent and never silently folded into an observed population.\n\nWHERE THE SUBJECT IDENTITIES COME FROM, each cited rather than asserted. The two SH-D shards are the members with measured door receipts. The five std members come from docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md's executed table: logic, optional, diagnostic and occurrence_identity are its four NORM_RETAINED rows, and node is its NOT RE-OBSERVED row — the member whose probe exceeded a 900s budget, which is an interruption plus a lower bound on cost and never a verdict about the subject.\n\nTHE SPECIMEN CLASS IS ONE SUBJECT, NOT THREE, and the reasoning matters more than the number. The prose population also names 'the bare-expression specimens v2.lens.vacuity found rejecting at this seam'. Those are not committed subjects: vacuity_bool_witness_body_lowering_ceiling_note describes them as inline probe configurations executed across 'many isolated configurations' — it never enumerates them, no fixture file carries them, and its byte-for-byte copy of optional_absent's body is the SAME subject as the optional member already rostered rather than a distinct one. The class grounds as a class; its members do not. Minting three identities would satisfy a sentence by fabricating two subjects, which is precisely what a roster exists to prevent, so the class enters as one subject carrying UnauthoredSpecimenClass and stands at SubjectUnmeasured{no_committed_specimen_identity}. Dissolve-on: the specimens are committed as fixtures, at which point this placeholder expands into its real members and disappears.\n\nTHE PROSE 'TEN' IS RETIRED AS AN UNFOUNDED COUNT (operator ruling, 2026-08-13). It came from reading the population sentence as two shards plus five std members plus three specimens, where the three was our arithmetic over a note that enumerates nothing. Recorded here rather than silently corrected, because a competing number left standing in prose is what makes the next reader reconcile eight against ten and reopen a settled question — the same reason the aggregate helpers were deleted rather than fixed.\n\nAND EIGHT IS NOT AN AUTHORITY EITHER. The denominator is not a number that was chosen; it is the set of exact identities that could be named, and its cardinality is whatever falls out — completeness is an identity join, not a count equality (DESIGN §5). A ninth grounded identity joins without ceremony; the specimen class expands into its real members when committed. The roster is the join, and any count over it is a reading rather than a fact about the population." -data body_lowering_population_roster: List = [ - FrontierMember { label: ^use_site_verdict, path: "src/v2/compiler/use_site_verdict.dag" }, - FrontierMember { label: ^materialization_carriers, path: "src/v2/compiler/materialization_carriers.dag" }, - FrontierMember { label: ^optional, path: "src/v2/std/optional.dag" }, +type BodyLoweringSubjectRealization + = LiveFileSubject { path: String } + | UnauthoredSpecimenClass + +type BodyLoweringSubject { + label: Symbol, + realization: BodyLoweringSubjectRealization, +} + +data body_lowering_subject_roster: List = [ + BodyLoweringSubject { label: ^use_site_verdict, realization: LiveFileSubject { path: "src/v2/compiler/use_site_verdict.dag" } }, + BodyLoweringSubject { label: ^materialization_carriers, realization: LiveFileSubject { path: "src/v2/compiler/materialization_carriers.dag" } }, + BodyLoweringSubject { label: ^logic, realization: LiveFileSubject { path: "src/v2/std/logic.dag" } }, + BodyLoweringSubject { label: ^optional, realization: LiveFileSubject { path: "src/v2/std/optional.dag" } }, + BodyLoweringSubject { label: ^diagnostic, realization: LiveFileSubject { path: "src/v2/std/diagnostic.dag" } }, + BodyLoweringSubject { label: ^occurrence_identity, realization: LiveFileSubject { path: "dag/std/occurrence_identity.dag" } }, + BodyLoweringSubject { label: ^node, realization: LiveFileSubject { path: "src/v2/std/node.dag" } }, + BodyLoweringSubject { label: ^vacuity_bare_expression_specimens, realization: UnauthoredSpecimenClass }, ] +data body_lowering_observation_standing_note: String = "STANDING IS THREE STATES BECAUSE THE TWO-STATE READING IS THE DEFECT. Observed and absent cannot carry this population: a subject dropped for cost, a subject whose run was interrupted by a budget, and a subject genuinely measured are three different epistemic positions with three different remedies, and collapsing the first two into absence is the empty-observation narrow — it renders 'we did not look' as 'there is nothing there'.\n\nSubjectInterrupted is separate from SubjectUnmeasured rather than a cause of it, and node is why: its probe exceeded 900s. That is an interruption plus a measured lower bound on cost, so it says something real about the subject's expense while saying nothing about its stage. An unmeasured subject carries no such bound. Merging them would discard the only quantitative fact the interrupted row has." + +type BodyLoweringObservationStanding + = SubjectObserved { stage: FrontendStage } + | SubjectUnmeasured { cause: Symbol } + | SubjectInterrupted { receipt: String } + +type BodyLoweringSubjectStanding { + label: Symbol, + standing: BodyLoweringObservationStanding, +} + +data body_lowering_execution_batch_note: String = "THE BATCH IS THREE, AND THE REASON IS A MEASUREMENT RATHER THAN A PREFERENCE. A ten-member run was executed and ABANDONED at 2h51m wall and 8.3 GiB resident without producing a verdict — on a host whose shared slice is 20 GiB and whose kernel kills the LARGEST task in it, so that run was a hazard to every other session before it was a slow measurement. The instrument this extends already declared the cost: its own roster note records the accepted-classification path measuring over the five-second fast-lane budget for fifteen files, and states that the batched or realized execution which would make a live census affordable is still owed.\n\nSo the batch is sized to what the present instrument can deliver, and that gap is a deficit in the instrument rather than a property of the subjects. THE FOUR SUBJECTS NOT IN IT ARE NOT OBSERVED-AND-EXCLUDED — they are unmeasured, and the program receipt says so per subject rather than leaving them out.\n\nTHE BATCH IS DERIVED FROM THE ROSTER, NEVER RE-AUTHORED BESIDE IT. An intermediate revision of this repair declared the three batch rows as their own literal list carrying their own paths, which put the same path in two places and let them drift — the second representation this whole change exists to remove, reintroduced one declaration below the note forbidding it. body_lowering_batch_selection now carries LABELS only, and the batch is a fold over the roster selecting those labels and reading each path from the subject that owns it. Two things follow by construction rather than by check: a batch row cannot name a path the roster does not, and a subject with no committed realization cannot enter a batch at all, because the specimen arm has no path to contribute and is skipped where it would otherwise have to invent one.\n\nHOW THE NEXT RUN SHOULD BE SHAPED, because the 2h51m failure already bounds it: ONE INDEPENDENT JOB PER SUBJECT, each emitting its own subject identity, stage receipt, runtime and terminal standing. The cost was nonlinear in member count, so one or more subjects is pathological rather than merely larger — a per-subject run identifies WHICH, where another monolithic evaluation would only reproduce the interruption and tempt an attribution by file size, which would be a guess dressed as a finding." + +data body_lowering_batch_selection: List = [ + ^use_site_verdict, + ^materialization_carriers, + ^optional, +] + +fn is_selected(label: Symbol) -> Bool { + fold_list(xs: body_lowering_batch_selection, empty: false, cons: fn(acc, sel) { acc || (sel == label) }) +} + +fn body_lowering_execution_batch() -> List { + fold_list( + xs: body_lowering_subject_roster, + empty: [], + cons: fn(acc, s) { + match s.realization { + UnauthoredSpecimenClass => acc + LiveFileSubject { path: p } => + if is_selected(label: s.label) { concat(acc, [FrontierMember { label: s.label, path: p }]) } else { acc } + } + } + ) +} + fn take_body_lowering_population_census() -> FrontendCensusReceipt { - take_census_over(rows: body_lowering_population_roster) + take_census_over(rows: body_lowering_execution_batch()) +} + +fn standing_of(s: BodyLoweringSubject, obs: List) -> BodyLoweringObservationStanding { + match s.realization { + UnauthoredSpecimenClass => SubjectUnmeasured { cause: ^no_committed_specimen_identity } + LiveFileSubject { path: _ } => + fold_list( + xs: obs, + empty: SubjectUnmeasured { cause: ^not_selected_into_execution_batch }, + cons: fn(acc, o) { if o.label == s.label { SubjectObserved { stage: o.stage } } else { acc } } + ) + } +} + +fn standings_from(obs: List) -> List { + fold_list( + xs: body_lowering_subject_roster, + empty: [], + cons: fn(acc, s) { + concat(acc, [BodyLoweringSubjectStanding { label: s.label, standing: standing_of(s: s, obs: obs) }]) + } + ) } +fn refusal_standing_of(s: BodyLoweringSubject, cause: Symbol) -> BodyLoweringObservationStanding { + match s.realization { + UnauthoredSpecimenClass => SubjectUnmeasured { cause: ^no_committed_specimen_identity } + LiveFileSubject { path: _ } => SubjectUnmeasured { cause: cause } + } +} + +fn all_subjects_unmeasured(cause: Symbol) -> List { + fold_list( + xs: body_lowering_subject_roster, + empty: [], + cons: fn(acc, s) { + concat(acc, [BodyLoweringSubjectStanding { label: s.label, standing: refusal_standing_of(s: s, cause: cause) }]) + } + ) +} + +fn body_lowering_program_receipt() -> List { + match take_body_lowering_population_census() { + CensusRefusedAtWorld => all_subjects_unmeasured(cause: ^census_refused_at_world) + CensusRefusedAtReconciliation { cause: _ } => all_subjects_unmeasured(cause: ^census_refused_at_reconciliation) + CensusTaken { roster_identity: _, observations: obs } => standings_from(obs: obs) + } +} + +data body_lowering_prior_standing_note: String = "THE PRIOR STANDINGS ARE A CITED HISTORICAL RECEIPT, not this run's observation, and they are declared separately so the two can never be read as one measurement. Every row here comes from the executed table in docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md at head c03be069687. This is also where SubjectInterrupted has its producer: node is the NOT RE-OBSERVED row, and recording it as interrupted-with-a-bound rather than unmeasured is what keeps its 900s lower bound attached to the subject it was measured on.\n\nA READER MAY NOT TRANSFER THESE TO THE PRESENT HEAD. They were taken on a different tree; the first executed reading of this instrument already contradicted one of them at the shard grain, which is exactly why a historical value may not be filled in for a subject this run did not execute." + +data body_lowering_prior_standing: List = [ + BodyLoweringSubjectStanding { label: ^logic, standing: SubjectObserved { stage: NormalizeRetained } }, + BodyLoweringSubjectStanding { label: ^optional, standing: SubjectObserved { stage: NormalizeRetained } }, + BodyLoweringSubjectStanding { label: ^diagnostic, standing: SubjectObserved { stage: NormalizeRetained } }, + BodyLoweringSubjectStanding { label: ^occurrence_identity, standing: SubjectObserved { stage: NormalizeRetained } }, + BodyLoweringSubjectStanding { label: ^node, standing: SubjectInterrupted { receipt: "probe exceeded its 900s budget at head c03be069687; interruption plus a lower bound on cost, never a stage verdict" } }, +] + fn stage_label_of(s: FrontendStage) -> String { match s { LexRefused => "LEX" From 218c075b0b58e3058105ebcc89a2d0b07488b03a Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 16:58:58 +0000 Subject: [PATCH 15/30] BL-2: make the roster module compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four blocking diagnostics, found by compiling the entry rather than by inspection, and one of them is worth recording because it is a language-layer trap rather than a typo. A BRACE INSIDE A PROSE NOTE IS INTERPOLATION SYNTAX. The roster note described a standing as SubjectUnmeasured{no_committed_specimen_identity}, and the compiler read the braces as an interpolation and refused an undefined variable — inside a String literal, at a position no reader would look for code. Escaped as \{, which is the existing modeled form. This is the class DESIGN records from the ${...}-in-strings incident: the tempting move is to respell the prose around the obstacle, and the modeled escape already exists. The other three are empty list literals in fold seeds, which carry no element type on their own. Written as [] as List, the idiom already used by frontier_ingestion_probe and normalize_retention_contract_probe_test. Result: 0 blocking errors on dag/tools/body_lowering_population_probe.dag. The 248 remaining advisories are pre-existing and corpus-wide. --- dag/tools/body_lowering_population_probe.dag | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index dc9110ef54b..645146721e4 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -30,7 +30,7 @@ data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member execution batch): use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: a ten-member run of this census ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more subjects outside the batch is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." -data body_lowering_subject_roster_note: String = "THE SUBJECT ROSTER AND THE EXECUTION BATCH ARE TWO AUTHORITIES, and merging them is the planning error this instrument exists to detect. A previous revision declared three rows under the name body_lowering_population_roster while the module's own prose said the subjects were ten. That makes the error representable in the exact shape we are trying to catch: seven unmeasured subjects erased, one retained member observed, and a bare count licensing the sentence 'the retained population is one'. The denominator has to be the SUBJECTS; what a given run could afford to execute is a separate and smaller fact.\n\nSo body_lowering_subject_roster is the denominator and body_lowering_execution_batch is what one run selected from it. A subject not in the batch is SubjectUnmeasured with a cause, never absent and never silently folded into an observed population.\n\nWHERE THE SUBJECT IDENTITIES COME FROM, each cited rather than asserted. The two SH-D shards are the members with measured door receipts. The five std members come from docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md's executed table: logic, optional, diagnostic and occurrence_identity are its four NORM_RETAINED rows, and node is its NOT RE-OBSERVED row — the member whose probe exceeded a 900s budget, which is an interruption plus a lower bound on cost and never a verdict about the subject.\n\nTHE SPECIMEN CLASS IS ONE SUBJECT, NOT THREE, and the reasoning matters more than the number. The prose population also names 'the bare-expression specimens v2.lens.vacuity found rejecting at this seam'. Those are not committed subjects: vacuity_bool_witness_body_lowering_ceiling_note describes them as inline probe configurations executed across 'many isolated configurations' — it never enumerates them, no fixture file carries them, and its byte-for-byte copy of optional_absent's body is the SAME subject as the optional member already rostered rather than a distinct one. The class grounds as a class; its members do not. Minting three identities would satisfy a sentence by fabricating two subjects, which is precisely what a roster exists to prevent, so the class enters as one subject carrying UnauthoredSpecimenClass and stands at SubjectUnmeasured{no_committed_specimen_identity}. Dissolve-on: the specimens are committed as fixtures, at which point this placeholder expands into its real members and disappears.\n\nTHE PROSE 'TEN' IS RETIRED AS AN UNFOUNDED COUNT (operator ruling, 2026-08-13). It came from reading the population sentence as two shards plus five std members plus three specimens, where the three was our arithmetic over a note that enumerates nothing. Recorded here rather than silently corrected, because a competing number left standing in prose is what makes the next reader reconcile eight against ten and reopen a settled question — the same reason the aggregate helpers were deleted rather than fixed.\n\nAND EIGHT IS NOT AN AUTHORITY EITHER. The denominator is not a number that was chosen; it is the set of exact identities that could be named, and its cardinality is whatever falls out — completeness is an identity join, not a count equality (DESIGN §5). A ninth grounded identity joins without ceremony; the specimen class expands into its real members when committed. The roster is the join, and any count over it is a reading rather than a fact about the population." +data body_lowering_subject_roster_note: String = "THE SUBJECT ROSTER AND THE EXECUTION BATCH ARE TWO AUTHORITIES, and merging them is the planning error this instrument exists to detect. A previous revision declared three rows under the name body_lowering_population_roster while the module's own prose said the subjects were ten. That makes the error representable in the exact shape we are trying to catch: seven unmeasured subjects erased, one retained member observed, and a bare count licensing the sentence 'the retained population is one'. The denominator has to be the SUBJECTS; what a given run could afford to execute is a separate and smaller fact.\n\nSo body_lowering_subject_roster is the denominator and body_lowering_execution_batch is what one run selected from it. A subject not in the batch is SubjectUnmeasured with a cause, never absent and never silently folded into an observed population.\n\nWHERE THE SUBJECT IDENTITIES COME FROM, each cited rather than asserted. The two SH-D shards are the members with measured door receipts. The five std members come from docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md's executed table: logic, optional, diagnostic and occurrence_identity are its four NORM_RETAINED rows, and node is its NOT RE-OBSERVED row — the member whose probe exceeded a 900s budget, which is an interruption plus a lower bound on cost and never a verdict about the subject.\n\nTHE SPECIMEN CLASS IS ONE SUBJECT, NOT THREE, and the reasoning matters more than the number. The prose population also names 'the bare-expression specimens v2.lens.vacuity found rejecting at this seam'. Those are not committed subjects: vacuity_bool_witness_body_lowering_ceiling_note describes them as inline probe configurations executed across 'many isolated configurations' — it never enumerates them, no fixture file carries them, and its byte-for-byte copy of optional_absent's body is the SAME subject as the optional member already rostered rather than a distinct one. The class grounds as a class; its members do not. Minting three identities would satisfy a sentence by fabricating two subjects, which is precisely what a roster exists to prevent, so the class enters as one subject carrying UnauthoredSpecimenClass and stands at SubjectUnmeasured\{no_committed_specimen_identity}. Dissolve-on: the specimens are committed as fixtures, at which point this placeholder expands into its real members and disappears.\n\nTHE PROSE 'TEN' IS RETIRED AS AN UNFOUNDED COUNT (operator ruling, 2026-08-13). It came from reading the population sentence as two shards plus five std members plus three specimens, where the three was our arithmetic over a note that enumerates nothing. Recorded here rather than silently corrected, because a competing number left standing in prose is what makes the next reader reconcile eight against ten and reopen a settled question — the same reason the aggregate helpers were deleted rather than fixed.\n\nAND EIGHT IS NOT AN AUTHORITY EITHER. The denominator is not a number that was chosen; it is the set of exact identities that could be named, and its cardinality is whatever falls out — completeness is an identity join, not a count equality (DESIGN §5). A ninth grounded identity joins without ceremony; the specimen class expands into its real members when committed. The roster is the join, and any count over it is a reading rather than a fact about the population." type BodyLoweringSubjectRealization = LiveFileSubject { path: String } @@ -79,7 +79,7 @@ fn is_selected(label: Symbol) -> Bool { fn body_lowering_execution_batch() -> List { fold_list( xs: body_lowering_subject_roster, - empty: [], + empty: [] as List, cons: fn(acc, s) { match s.realization { UnauthoredSpecimenClass => acc @@ -109,7 +109,7 @@ fn standing_of(s: BodyLoweringSubject, obs: List) -> BodyLowe fn standings_from(obs: List) -> List { fold_list( xs: body_lowering_subject_roster, - empty: [], + empty: [] as List, cons: fn(acc, s) { concat(acc, [BodyLoweringSubjectStanding { label: s.label, standing: standing_of(s: s, obs: obs) }]) } @@ -126,7 +126,7 @@ fn refusal_standing_of(s: BodyLoweringSubject, cause: Symbol) -> BodyLoweringObs fn all_subjects_unmeasured(cause: Symbol) -> List { fold_list( xs: body_lowering_subject_roster, - empty: [], + empty: [] as List, cons: fn(acc, s) { concat(acc, [BodyLoweringSubjectStanding { label: s.label, standing: refusal_standing_of(s: s, cause: cause) }]) } From e569a53c0551ccc69a1b91cacfabb10cb8767a27 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 17:04:49 +0000 Subject: [PATCH 16/30] BL-2: record the executed program receipt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit body_lowering_program_receipt was run against the live tree (73s) and returned eight standings. Recorded because compiling is not running, and the join is the part worth proving: eight subjects in and eight standings out establishes that no subject is dropped between roster and receipt, the three observed rows reproduce the earlier batch reading so deriving the batch from the roster did not change what executes, and the specimen class routes to no_committed_specimen_identity rather than to not_selected_into_execution_batch — the distinction the realization coproduct exists to make. The note also states what the run does not establish: the four unmeasured subjects are unobserved, not staged, and node's prior interruption is neither confirmed nor refuted here. --- dag/tools/body_lowering_population_probe.dag | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 645146721e4..4c2ae952eee 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -32,6 +32,8 @@ data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READI data body_lowering_subject_roster_note: String = "THE SUBJECT ROSTER AND THE EXECUTION BATCH ARE TWO AUTHORITIES, and merging them is the planning error this instrument exists to detect. A previous revision declared three rows under the name body_lowering_population_roster while the module's own prose said the subjects were ten. That makes the error representable in the exact shape we are trying to catch: seven unmeasured subjects erased, one retained member observed, and a bare count licensing the sentence 'the retained population is one'. The denominator has to be the SUBJECTS; what a given run could afford to execute is a separate and smaller fact.\n\nSo body_lowering_subject_roster is the denominator and body_lowering_execution_batch is what one run selected from it. A subject not in the batch is SubjectUnmeasured with a cause, never absent and never silently folded into an observed population.\n\nWHERE THE SUBJECT IDENTITIES COME FROM, each cited rather than asserted. The two SH-D shards are the members with measured door receipts. The five std members come from docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md's executed table: logic, optional, diagnostic and occurrence_identity are its four NORM_RETAINED rows, and node is its NOT RE-OBSERVED row — the member whose probe exceeded a 900s budget, which is an interruption plus a lower bound on cost and never a verdict about the subject.\n\nTHE SPECIMEN CLASS IS ONE SUBJECT, NOT THREE, and the reasoning matters more than the number. The prose population also names 'the bare-expression specimens v2.lens.vacuity found rejecting at this seam'. Those are not committed subjects: vacuity_bool_witness_body_lowering_ceiling_note describes them as inline probe configurations executed across 'many isolated configurations' — it never enumerates them, no fixture file carries them, and its byte-for-byte copy of optional_absent's body is the SAME subject as the optional member already rostered rather than a distinct one. The class grounds as a class; its members do not. Minting three identities would satisfy a sentence by fabricating two subjects, which is precisely what a roster exists to prevent, so the class enters as one subject carrying UnauthoredSpecimenClass and stands at SubjectUnmeasured\{no_committed_specimen_identity}. Dissolve-on: the specimens are committed as fixtures, at which point this placeholder expands into its real members and disappears.\n\nTHE PROSE 'TEN' IS RETIRED AS AN UNFOUNDED COUNT (operator ruling, 2026-08-13). It came from reading the population sentence as two shards plus five std members plus three specimens, where the three was our arithmetic over a note that enumerates nothing. Recorded here rather than silently corrected, because a competing number left standing in prose is what makes the next reader reconcile eight against ten and reopen a settled question — the same reason the aggregate helpers were deleted rather than fixed.\n\nAND EIGHT IS NOT AN AUTHORITY EITHER. The denominator is not a number that was chosen; it is the set of exact identities that could be named, and its cardinality is whatever falls out — completeness is an identity join, not a count equality (DESIGN §5). A ninth grounded identity joins without ceremony; the specimen class expands into its real members when committed. The roster is the join, and any count over it is a reading rather than a fact about the population." +data body_lowering_program_receipt_execution_note: String = "EXECUTED, NOT ASSERTED (2026-08-13, 73s): body_lowering_program_receipt was run against the live tree and returned eight standings — use_site_verdict SubjectObserved\{NormalizeAccepted}, materialization_carriers SubjectObserved\{NormalizeOther}, optional SubjectObserved\{NormalizeRetained}, logic and diagnostic and occurrence_identity and node SubjectUnmeasured\{not_selected_into_execution_batch}, and vacuity_bare_expression_specimens SubjectUnmeasured\{no_committed_specimen_identity}.\n\nWHAT THAT ESTABLISHES beyond the module compiling. The denominator survives the join: eight subjects in, eight standings out, so no subject is silently dropped between roster and receipt. The three observed rows reproduce the earlier batch reading, so deriving the batch from the roster did not change what runs. And the specimen class routes to its own structural cause rather than to not_selected_into_execution_batch — the distinction the realization coproduct exists to make, which a run would collapse if the two causes were reachable from one arm.\n\nWHAT IT DOES NOT ESTABLISH. Nothing here observes the four unmeasured subjects; their standing is the honest report that this batch did not run them, and it may not be read as a stage. node in particular carries a prior interruption with a cost bound, recorded separately in body_lowering_prior_standing, and this run neither confirms nor refutes it." + type BodyLoweringSubjectRealization = LiveFileSubject { path: String } | UnauthoredSpecimenClass From 2d90b071fbe1bd0623c45ca6c946ab787e0193b4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 19:41:10 +0000 Subject: [PATCH 17/30] BL-2: render stages through the one label authority MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review on #8208 noted that stage_label_of paralleled frontier_ingestion_probe's frontend_stage_label and disagreed with it on one variant — NORM_ACCEPTED against ACCEPTED — and judged it non-blocking because it is presentation-only. Taking it anyway: it is a second renderer for one closed coproduct, and it had already drifted, which is the §3 fork rather than a style preference. This module's own note claims there is one stage classifier in the repository and not two; forking the label while importing the classifier honoured that in the half that was harder to get wrong. frontend_stage_label is imported and the fork deleted. Confirmed by execution that the divergence was real and is now gone: census_report prints "use_site_verdict ACCEPTED | materialization_carriers NORM_OTHER | optional NORM_RETAINED", where the forked renderer printed NORM_ACCEPTED for the same stage. The recorded first reading named stages by their rendered label, which would now be stale. It names them by VARIANT instead, and says why: the spelling belongs to the renderer, so a note pinning it goes stale the moment that renderer is edited — the same reason counting may not read labels. Five variant imports were left import-only by the deletion and are dropped; NormalizeRetained stays, since the prior-standing rows construct it. --- dag/tools/body_lowering_population_probe.dag | 23 ++++---------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 4c2ae952eee..13215c2e4b1 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -7,13 +7,9 @@ import gunbc.tools.frontier_ingestion_probe { FrontendCensusReceipt, FrontierMember, FrontendStage, - LexRefused, + frontend_stage_label, MemberObservation, - NormalizeAccepted, - NormalizeGraftRefused, - NormalizeOther, NormalizeRetained, - ParseRefused, take_census_over } import v2.std.algebra { fold_list } @@ -28,7 +24,7 @@ data live_tree_disposition: LiveTreeDisposition = ReadsLiveTree data body_lowering_population_probe_note: String = "BL-2 REPRODUCTION INSTRUMENT, homed outside a *_test.dag module for the same reason gunbc.tools.frontier_ingestion_probe is: what it reports is a revision-scoped OBSERVATION of the present frontier, and enrolling that as a test declaration would make an assertion that goes RED on the repair and GREEN on a regression. It reuses take_census_over and classify_source rather than reproducing them, so there is one stage classifier in the repository and not two.\n\nWHY IT EXISTS. The body-lowering wall cannot be scheduled against an unmeasured population. normalize today returns Accepted for a tree whose diagnostics carry wrapper-retention, and resolve consumes it, so retention is kept out of resolve by propagation rather than by construction. Making retention unconstructible is the right wall and it CHANGES WHICH MODULES NORMALIZE ACCEPTS — so the population that would newly refuse has to be known before the wall lands, not discovered from a red tree afterwards.\n\nWHAT THE SUBJECTS ARE is body_lowering_subject_roster's to say, and this note deliberately does not restate it — an earlier revision carried the population as a prose sentence beside a shorter roster, and the two disagreeing is the defect that produced this rewrite. What holds here is only the scope: the subjects are deliberately NOT the whole corpus — a per-file census over every module is a different and much larger instrument — so anything read off this module is a reading over THOSE subjects and carries no claim about the rest.\n\nWHAT A READER MAY NOT DO WITH IT. The per-member stage is the observation. There is no aggregate pass/fail, because the population is the finding and a Boolean over it would be a verdict on subjects that have not all been separately established." -data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member execution batch): use_site_verdict NORM_ACCEPTED, materialization_carriers NORM_OTHER, optional NORM_RETAINED.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: a ten-member run of this census ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more subjects outside the batch is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." +data body_lowering_population_first_reading_note: String = "FIRST EXECUTED READING (2026-08-13, three-member execution batch): use_site_verdict reached NormalizeAccepted, materialization_carriers NormalizeOther, optional NormalizeRetained. Stages are named by their VARIANT here rather than by a rendered label, because the label is frontier_ingestion_probe frontend_stage_label's to decide and a note that pins its spelling would go stale the moment that renderer is edited — which is the same reason counting may not read labels either.\n\nWHAT IT CHANGES, and it is the shard rows rather than the finding. Neither SH-D shard RETAINS as a file. use_site_verdict normalizes clean; materialization_carriers refuses at normalize for something that is neither retention nor a graft refusal. So the wrapper-retained residue measured in those shards' door chains does not come from the shard modules themselves — it comes from OTHER modules in their import closures, of which optional is one confirmed instance. Saying a shard is body-lowering blocked was too coarse: the shard is blocked because its CLOSURE contains retaining modules, and the subjects to repair are those, not it.\n\nWHAT THIS IS NOT. Three members are not a population, and NORM_OTHER is a cause this instrument does not identify — it names the stage, not the reason. materialization_carriers refusing at normalize for an unidentified reason is an open question, not a measured cause, and nothing here establishes which closure members of either shard retain beyond the one measured.\n\nA COST OBSERVATION, recorded because it bounds what can be asked next: a ten-member run of this census ran 2h51m without a verdict; over three it returns in about two minutes. That is nonlinear in the member count, so one or more subjects outside the batch is pathological rather than merely larger. WHICH member is NOT established — no per-member timing was taken — and attributing it to the largest file would be a guess dressed as a finding." data body_lowering_subject_roster_note: String = "THE SUBJECT ROSTER AND THE EXECUTION BATCH ARE TWO AUTHORITIES, and merging them is the planning error this instrument exists to detect. A previous revision declared three rows under the name body_lowering_population_roster while the module's own prose said the subjects were ten. That makes the error representable in the exact shape we are trying to catch: seven unmeasured subjects erased, one retained member observed, and a bare count licensing the sentence 'the retained population is one'. The denominator has to be the SUBJECTS; what a given run could afford to execute is a separate and smaller fact.\n\nSo body_lowering_subject_roster is the denominator and body_lowering_execution_batch is what one run selected from it. A subject not in the batch is SubjectUnmeasured with a cause, never absent and never silently folded into an observed population.\n\nWHERE THE SUBJECT IDENTITIES COME FROM, each cited rather than asserted. The two SH-D shards are the members with measured door receipts. The five std members come from docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md's executed table: logic, optional, diagnostic and occurrence_identity are its four NORM_RETAINED rows, and node is its NOT RE-OBSERVED row — the member whose probe exceeded a 900s budget, which is an interruption plus a lower bound on cost and never a verdict about the subject.\n\nTHE SPECIMEN CLASS IS ONE SUBJECT, NOT THREE, and the reasoning matters more than the number. The prose population also names 'the bare-expression specimens v2.lens.vacuity found rejecting at this seam'. Those are not committed subjects: vacuity_bool_witness_body_lowering_ceiling_note describes them as inline probe configurations executed across 'many isolated configurations' — it never enumerates them, no fixture file carries them, and its byte-for-byte copy of optional_absent's body is the SAME subject as the optional member already rostered rather than a distinct one. The class grounds as a class; its members do not. Minting three identities would satisfy a sentence by fabricating two subjects, which is precisely what a roster exists to prevent, so the class enters as one subject carrying UnauthoredSpecimenClass and stands at SubjectUnmeasured\{no_committed_specimen_identity}. Dissolve-on: the specimens are committed as fixtures, at which point this placeholder expands into its real members and disappears.\n\nTHE PROSE 'TEN' IS RETIRED AS AN UNFOUNDED COUNT (operator ruling, 2026-08-13). It came from reading the population sentence as two shards plus five std members plus three specimens, where the three was our arithmetic over a note that enumerates nothing. Recorded here rather than silently corrected, because a competing number left standing in prose is what makes the next reader reconcile eight against ten and reopen a settled question — the same reason the aggregate helpers were deleted rather than fixed.\n\nAND EIGHT IS NOT AN AUTHORITY EITHER. The denominator is not a number that was chosen; it is the set of exact identities that could be named, and its cardinality is whatever falls out — completeness is an identity join, not a count equality (DESIGN §5). A ninth grounded identity joins without ceremony; the specimen class expands into its real members when committed. The roster is the join, and any count over it is a reading rather than a fact about the population." @@ -153,19 +149,8 @@ data body_lowering_prior_standing: List = [ BodyLoweringSubjectStanding { label: ^node, standing: SubjectInterrupted { receipt: "probe exceeded its 900s budget at head c03be069687; interruption plus a lower bound on cost, never a stage verdict" } }, ] -fn stage_label_of(s: FrontendStage) -> String { - match s { - LexRefused => "LEX" - ParseRefused => "PARSE" - NormalizeGraftRefused => "NORM_GRAFT_REFUSED" - NormalizeRetained => "NORM_RETAINED" - NormalizeOther => "NORM_OTHER" - NormalizeAccepted => "NORM_ACCEPTED" - } -} - fn observation_line(o: MemberObservation) -> String { - concat(symbol_lexeme(sym: o.label), concat(" ", stage_label_of(s: o.stage))) + concat(symbol_lexeme(sym: o.label), concat(" ", frontend_stage_label(s: o.stage))) } fn census_report() -> String { @@ -183,4 +168,4 @@ fn census_report() -> String { } } -data body_lowering_no_aggregate_note: String = "THERE IS NO AGGREGATE COUNT HERE, and its absence is the module's own stated scope rather than an omission. An earlier revision carried count_stage_matching and retained_member_count, and both were deleted for defects worth recording so they are not reintroduced.\n\nA REFUSAL BECAME AN INTEGER. count_stage_matching answered 0 - 1 for CensusRefusedAtWorld and 0 - 2 for CensusRefusedAtReconciliation, so a typed refusal and a genuine population count inhabited one type. A caller could add, compare or display a failure as a measurement, and -1 is a plausible-looking number rather than a loud one. The census already carries typed refusal arms; collapsing them into the counting type discarded exactly the distinction those arms exist to preserve.\n\nPRESENTATION BECAME SEMANTIC AUTHORITY. It counted by comparing stage_label_of(s) against a target String. FrontendStage is a closed coproduct, so a label rename would have changed the count with no stage changing, and a label collision would have merged distinct stages — the renderer deciding a semantic question. Counting belongs to the structural variant or to frontend_stage_eq, never to rendered text.\n\nBoth were reachable because an aggregate was authored at all. This module's note already says the per-member observation IS the finding and that no aggregate verdict is warranted over a roster whose members have not each been separately established; the helpers contradicted that. A reader wanting a count over these members reads the per-member observations and counts the subjects it can name." +data body_lowering_no_aggregate_note: String = "THERE IS NO AGGREGATE COUNT HERE, and its absence is the module's own stated scope rather than an omission. An earlier revision carried count_stage_matching and retained_member_count, and both were deleted for defects worth recording so they are not reintroduced.\n\nA REFUSAL BECAME AN INTEGER. count_stage_matching answered 0 - 1 for CensusRefusedAtWorld and 0 - 2 for CensusRefusedAtReconciliation, so a typed refusal and a genuine population count inhabited one type. A caller could add, compare or display a failure as a measurement, and -1 is a plausible-looking number rather than a loud one. The census already carries typed refusal arms; collapsing them into the counting type discarded exactly the distinction those arms exist to preserve.\n\nPRESENTATION BECAME SEMANTIC AUTHORITY. It counted by comparing a rendered stage label against a target String. FrontendStage is a closed coproduct, so a label rename would have changed the count with no stage changing, and a label collision would have merged distinct stages — the renderer deciding a semantic question. Counting belongs to the structural variant or to frontend_stage_eq, never to rendered text.\n\nBoth were reachable because an aggregate was authored at all. This module's note already says the per-member observation IS the finding and that no aggregate verdict is warranted over a roster whose members have not each been separately established; the helpers contradicted that. A reader wanting a count over these members reads the per-member observations and counts the subjects it can name." From 6ab0ffeb7b3949d9736c16f300938536735c0a30 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 20:25:38 +0000 Subject: [PATCH 18/30] BL-2: scope a census refusal to the batch, not to the roster MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 51759 (REQUEST_CHANGES) found that body_lowering_program_receipt routed EVERY live-file subject through the census-refusal cause. Verified and correct: on CensusRefusedAtWorld, logic, diagnostic, occurrence_identity and node — four subjects this run was never going to execute — were rewritten from "not selected" into "the census refused". That is state-space conflation, and it undid the roster/batch split on the one path where the distinction is least visible. The two states have different remedies: a not-selected subject needs a bigger batch or a cheaper instrument, while a refused census needs the infrastructure repaired, so a reader deciding what to do next was misdirected. More generally it is a failure at one scope attributed to subjects outside that scope — the absorbing fallback wearing an error's name, manufacturing information about subjects nobody looked at. The refusal cause now reaches only selected subjects. A subject outside the batch keeps not_selected_into_execution_batch under refusal exactly as under success, because nothing about it was attempted and its standing does not depend on the census. The specimen class keeps its own structural cause under both. Proven by execution with a discriminating control, not by inspection. census_refusal_does_not_reach_a_non_batch_subject_RED returns false against the previous arm and true against this one; the other three assert that a batch member does receive the cause, that the cause is carried rather than fixed, and that the specimen class is unaffected. The arm takes a cause symbol as input, so it is reachable by ordinary call and needs no broken tree to observe. --- .../body_lowering_refusal_scope_test.dag | 52 +++++++++++++++++++ dag/tools/body_lowering_population_probe.dag | 9 +++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 dag/test/claim/body_lowering_refusal_scope_test.dag diff --git a/dag/test/claim/body_lowering_refusal_scope_test.dag b/dag/test/claim/body_lowering_refusal_scope_test.dag new file mode 100644 index 00000000000..8c3df434ea7 --- /dev/null +++ b/dag/test/claim/body_lowering_refusal_scope_test.dag @@ -0,0 +1,52 @@ +module gunbc.test.claim.body_lowering_refusal_scope_test + +import v2.std.logic { Bool } +import v2.std.node { Symbol } +import gunbc.tools.body_lowering_population_probe { + BodyLoweringSubject, + BodyLoweringObservationStanding, + LiveFileSubject, + UnauthoredSpecimenClass, + SubjectUnmeasured, + SubjectObserved, + SubjectInterrupted, + refusal_standing_of +} + +// A census refusal is scoped to the batch. These assert the scoping directly rather than through +// a live census, because forcing CensusRefusedAtWorld would mean breaking the tree to observe the +// arm — the arm's INPUT is a cause symbol, so it is reachable by ordinary call and needs no fixture. +// +// The first is the discriminating control: it goes RED against the revision that routed every +// live-file subject through the refusal cause. The subject it uses (logic) is a real roster member +// outside the execution batch, so the assertion is about the shipped roster and not a synthetic one. + +fn cause_of(s: BodyLoweringObservationStanding) -> Symbol { + match s { + SubjectUnmeasured { cause: c } => c + SubjectObserved { stage: _ } => ^unexpected_observed + SubjectInterrupted { receipt: _ } => ^unexpected_interrupted + } +} + +data non_batch_subject: BodyLoweringSubject = BodyLoweringSubject { label: ^logic, realization: LiveFileSubject { path: "src/v2/std/logic.dag" } } + +data batch_subject: BodyLoweringSubject = BodyLoweringSubject { label: ^optional, realization: LiveFileSubject { path: "src/v2/std/optional.dag" } } + +data specimen_subject: BodyLoweringSubject = BodyLoweringSubject { label: ^vacuity_bare_expression_specimens, realization: UnauthoredSpecimenClass } + +test fn census_refusal_does_not_reach_a_non_batch_subject_RED() -> Bool { + cause_of(s: refusal_standing_of(s: non_batch_subject, cause: ^census_refused_at_world)) == ^not_selected_into_execution_batch +} + +test fn census_refusal_reaches_a_batch_subject() -> Bool { + cause_of(s: refusal_standing_of(s: batch_subject, cause: ^census_refused_at_world)) == ^census_refused_at_world +} + +test fn census_refusal_cause_is_carried_not_fixed() -> Bool { + cause_of(s: refusal_standing_of(s: batch_subject, cause: ^census_refused_at_reconciliation)) == ^census_refused_at_reconciliation +} + +test fn specimen_class_keeps_its_own_cause_under_refusal() -> Bool { + cause_of(s: refusal_standing_of(s: specimen_subject, cause: ^census_refused_at_world)) == ^no_committed_specimen_identity +} diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 13215c2e4b1..1a083e79ea2 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -114,10 +114,17 @@ fn standings_from(obs: List) -> List BodyLoweringObservationStanding { match s.realization { UnauthoredSpecimenClass => SubjectUnmeasured { cause: ^no_committed_specimen_identity } - LiveFileSubject { path: _ } => SubjectUnmeasured { cause: cause } + LiveFileSubject { path: _ } => + if is_selected(label: s.label) { + SubjectUnmeasured { cause: cause } + } else { + SubjectUnmeasured { cause: ^not_selected_into_execution_batch } + } } } From 0fc5656136e6de4dd913523a78fe7d49f8fdb775 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Thu, 13 Aug 2026 21:28:43 +0000 Subject: [PATCH 19/30] BL-2: derive what a stage licenses, and correct the stale occurrence_identity row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per-subject censuses (one subject per run) measured six of the seven runnable subjects. Two consequences, plus the reading that keeps them from being misread. THE STALE RECEIPT ROW. occurrence_identity measures NormalizeOther, not the NormalizeRetained the exact-head receipt records. That receipt is explicit that the member was never re-run after the repair, so the cell was unknown rather than confirmed — and it is now known stale. Corrected where the receipt lives rather than only in a message, because a stale cell in a cited authority is the failure that survives on the assumption that someone else checked it. logic independently reproduced what the receipt predicted for it, which is evidence the receipt was right about the mechanism and wrong only about the row it could not re-run. WHAT A STAGE LICENSES IS NOW DERIVED, not left to prose. Leaving NormalizeRetained is not evidence of repair: NormalizeOther is by construction neither retention nor a graft refusal, so it names where a module refuses and never why. ObservedReading projects that — ObservedClean for the one admitting stage, ObservedRefusedCauseNamed where the classifier identified the kind, ObservedRefusedCauseUnclassified for NormalizeOther alone. Derived from the stage rather than stored beside it, so it cannot disagree with the coproduct it reads. A READING EXISTS ONLY WHERE SOMETHING WAS OBSERVED. A first draft of reading_of_standing mapped SubjectUnmeasured and SubjectInterrupted onto the unclassified-refusal arm on the reasoning that all three leave the cause unknown. That is the census-refusal conflation again, in the module that just repaired it: unknown-because-unclassified and unknown-because-unattempted are different unknowns with different remedies. StandingReading keeps them apart. Nine witnesses, all executed. Two are discriminating controls for the classes above: NormalizeOther must not read clean, and an unmeasured subject must have no reading at all. node is not included in any of this. It was SIGKILLed at 346s while every other subject finished between 138s and 292s; the signal is observed and the cause is not established, so it stands interrupted with a fresh lower bound and carries no stage. The six completions sit within a factor of 2.1, so the earlier 2h51m ten-member run is unexplained by them, and node dying rather than finishing left node-is-the-outlier versus superlinear-in-roster-size open. --- .../body_lowering_refusal_scope_test.dag | 30 ++++++++++++++ dag/tools/body_lowering_population_probe.dag | 39 +++++++++++++++++++ ...ntend-std-ingestion-frontier-exact-head.md | 27 +++++++++++++ 3 files changed, 96 insertions(+) diff --git a/dag/test/claim/body_lowering_refusal_scope_test.dag b/dag/test/claim/body_lowering_refusal_scope_test.dag index 8c3df434ea7..70c8ce9c27c 100644 --- a/dag/test/claim/body_lowering_refusal_scope_test.dag +++ b/dag/test/claim/body_lowering_refusal_scope_test.dag @@ -10,8 +10,18 @@ import gunbc.tools.body_lowering_population_probe { SubjectUnmeasured, SubjectObserved, SubjectInterrupted, + ObservedReading, + ObservedClean, + ObservedRefusedCauseNamed, + ObservedRefusedCauseUnclassified, + StandingReading, + StandingObserved, + StandingNotObserved, + reading_of_stage, + reading_of_standing, refusal_standing_of } +import gunbc.tools.frontier_ingestion_probe { NormalizeAccepted, NormalizeOther, NormalizeRetained } // A census refusal is scoped to the batch. These assert the scoping directly rather than through // a live census, because forcing CensusRefusedAtWorld would mean breaking the tree to observe the @@ -50,3 +60,23 @@ test fn census_refusal_cause_is_carried_not_fixed() -> Bool { test fn specimen_class_keeps_its_own_cause_under_refusal() -> Bool { cause_of(s: refusal_standing_of(s: specimen_subject, cause: ^census_refused_at_world)) == ^no_committed_specimen_identity } + +// An observed stage is not a verdict. These pin the reading a stage licenses, and the RED is the +// one that matters: NormalizeOther must not read as clean merely because it is no longer retention. + +test fn accepted_reads_clean() -> Bool { reading_of_stage(s: NormalizeAccepted) == ObservedClean } + +test fn normalize_other_does_not_read_clean_RED() -> Bool { reading_of_stage(s: NormalizeOther) == ObservedRefusedCauseUnclassified } + +test fn retained_reads_as_a_named_cause() -> Bool { reading_of_stage(s: NormalizeRetained) == ObservedRefusedCauseNamed } + +// An unmeasured subject has no reading at all. This is the distinction a first draft of +// reading_of_standing collapsed by mapping unmeasured onto the unclassified-refusal arm. + +test fn an_unmeasured_subject_has_no_reading_RED() -> Bool { + reading_of_standing(st: SubjectUnmeasured { cause: ^not_selected_into_execution_batch }) == StandingNotObserved +} + +test fn an_observed_subject_carries_its_reading() -> Bool { + reading_of_standing(st: SubjectObserved { stage: NormalizeAccepted }) == StandingObserved { reading: ObservedClean } +} diff --git a/dag/tools/body_lowering_population_probe.dag b/dag/tools/body_lowering_population_probe.dag index 1a083e79ea2..40cdd450667 100644 --- a/dag/tools/body_lowering_population_probe.dag +++ b/dag/tools/body_lowering_population_probe.dag @@ -8,8 +8,13 @@ import gunbc.tools.frontier_ingestion_probe { FrontierMember, FrontendStage, frontend_stage_label, + LexRefused, MemberObservation, + NormalizeAccepted, + NormalizeGraftRefused, + NormalizeOther, NormalizeRetained, + ParseRefused, take_census_over } import v2.std.algebra { fold_list } @@ -114,6 +119,40 @@ fn standings_from(obs: List) -> List ObservedReading { + match s { + NormalizeAccepted => ObservedClean + NormalizeOther => ObservedRefusedCauseUnclassified + NormalizeRetained => ObservedRefusedCauseNamed + NormalizeGraftRefused => ObservedRefusedCauseNamed + LexRefused => ObservedRefusedCauseNamed + ParseRefused => ObservedRefusedCauseNamed + } +} + +data body_lowering_standing_reading_note: String = "A READING EXISTS ONLY WHERE SOMETHING WAS OBSERVED, which is why this returns its own coproduct rather than an ObservedReading. A first draft of this function mapped SubjectUnmeasured and SubjectInterrupted onto ObservedRefusedCauseUnclassified, on the reasoning that all three leave the cause unknown. That is the census-refusal conflation one level up, and in the same module that just repaired it: an unclassified refusal is a fact about a subject that WAS put through the door, while an unmeasured subject was never attempted and an interrupted one was abandoned mid-flight. Collapsing them makes a subject nobody looked at indistinguishable from one the compiler refused for a reason we could not name, and the remedies differ — run it, versus classify the refusal.\n\nThe shapes rhyme, which is exactly why the collapse is tempting: unknown-because-unclassified and unknown-because-unattempted are both unknowns. They are not the same unknown." + +type StandingReading + = StandingObserved { reading: ObservedReading } + | StandingNotObserved + +fn reading_of_standing(st: BodyLoweringObservationStanding) -> StandingReading { + match st { + SubjectObserved { stage: s } => StandingObserved { reading: reading_of_stage(s: s) } + SubjectUnmeasured { cause: _ } => StandingNotObserved + SubjectInterrupted { receipt: _ } => StandingNotObserved + } +} + data body_lowering_refusal_scope_note: String = "A CENSUS REFUSAL IS SCOPED TO THE BATCH, NEVER TO THE ROSTER, and an earlier revision of this repair got it wrong in the exact way the repair exists to prevent. It routed EVERY live-file subject through the refusal cause, so a census abort rewrote logic, diagnostic, occurrence_identity and node — subjects this run was never going to execute — from 'not selected' into 'the census refused'. Those are different epistemic positions with different remedies: a not-selected subject needs a bigger batch or a cheaper instrument, while a refused census needs the infrastructure fixed, and a reader deciding what to do next is misdirected by the conflation. It also destroyed the roster/batch split on precisely the path where the distinction is least visible and therefore least likely to be questioned.\n\nSo the refusal cause reaches ONLY the subjects that were actually selected. A subject outside the batch keeps not_selected_into_execution_batch whether the census succeeded or refused, because its standing does not depend on the census at all — nothing about it was attempted. The specimen class keeps its own structural cause under both, for the same reason one rung further out: it could not have been attempted under any batch.\n\nThe general form, worth stating because it is easy to reintroduce: a failure at one scope may not be attributed to subjects outside that scope. Widening a refusal to everything it did not touch is the absorbing fallback wearing an error's name — it looks conservative and it manufactures false information about subjects nobody looked at." fn refusal_standing_of(s: BodyLoweringSubject, cause: Symbol) -> BodyLoweringObservationStanding { diff --git a/docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md b/docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md index 8701710d745..4d320352d61 100644 --- a/docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md +++ b/docs/plans/v2-frontend-std-ingestion-frontier-exact-head.md @@ -216,6 +216,33 @@ overclaiming on the third. Everything else in the table — the entry, `types`, `logic.dag` leaving retention is the repair working, NOT the member closing: it still does not ingest, and its refusal is now simply reported as the refusal it always was. +### `occurrence_identity` re-observed 2026-08-13: `NORM_OTHER`, resolving the unknown above + +The row left open in the previous paragraph is now measured. A per-subject census over +`dag/std/occurrence_identity.dag` (one subject per run, `gunbc.tools.body_lowering_population_probe` +against the live tree) returns **`NORM_OTHER`**, not `NORM_RETAINED`. So the pre-repair four +(`logic`, `optional`, `diagnostic`, `occurrence_identity`) has become **two** among re-observed +members: `optional` and `diagnostic`. + +Two things make this a correction to the table rather than a new claim beside it. The +`NORM_RETAINED` cell for `occurrence_identity` is a **pre-repair** reading that this document +already declined to carry forward, and it is now known stale rather than merely unconfirmed — +a stale cell in a cited authority is the failure that survives because each reader assumes another +checked it. And `logic.dag` independently reproduced `NORM_OTHER` in the same batch of per-subject +runs, which is evidence that this receipt was **right about the mechanism and wrong only about the +post-repair state of the row it could not re-run**: the same repair moved both members the same way, +and the one it measured predicted the one it did not. + +**What this does not establish, and the distinction is the whole point of the `NORM_OTHER` stage.** +Leaving `NORM_RETAINED` is not evidence of repair. `NORM_OTHER` names *where* a module refuses and +never *why* — it is by definition neither retention nor a graft refusal, so the cause is +unclassified. `materialization_carriers`, `logic` and `occurrence_identity` all sit there for causes +no instrument in this lane identifies. A reader taking "no longer retained" as "fixed" is reading an +unclassified refusal as a clean bill; `occurrence_identity` still does not ingest. + +`node` remains unmeasured for cost reasons and stays post-repair unknown, as do the other rows the +paragraph above lists. + **Not repaired here:** the resolve-ready normalized-tree construction boundary. ("Promotion" deliberately avoided: that word belongs to the bootstrap-admission authority, and reusing it for the normalize-side carrier would collide two different authorities.) `NormalizedTree` is still `= Node` and `resolve` still consumes that alias, so *retained cannot reach resolve* remains true only by propagation, not by construction. From 8ab9076aece2972df1f2d539a61abe728d81b44f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 04:24:25 +0000 Subject: [PATCH 20/30] BL-1: seal NormalizedTree behind an admission door so retention cannot reach resolve normalize computed 'this root's body lowering left no wrapper behind' and had nowhere to put it: NormalizedTree was an alias for Node, so the fact travelled as diagnostics beside a bare Node and was dropped at the first FreeMonoid carrier before resolve. Restore the carrier through the root-carrying signatures and give it a door. NormalizedTree becomes a sole_constructor record; admit_normalized_tree refuses a root carrying wrapper-retention evidence. The retention recognizer lives once, beside its single producer in v2.std.compilers.body_lowering, and scans the whole diagnostic list -- rejected_with_pending prepends outer diagnostics, so a head-only read is systematically wrong. Rung: accepted refinement with executing refusal, not structural impossibility. Also records the typecheck-blindness receipt on the hollow-alias lane: the conversion reported 0 blocking errors with a record in a Node position AND a raw Node in the NormalizedTree field, and surfaced only under execution. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- ...ce_graft_body_dissolution_witness_test.dag | 2 +- docs/plans/hollow-alias-construction-wall.md | 23 +++++ src/v2/compiler/00_compile.dag | 2 +- src/v2/compiler/03_name_resolve.dag | 44 ++++----- src/v2/compiler/03_normalize.dag | 4 +- src/v2/compiler/03_resolve.dag | 10 +-- src/v2/compiler/normalized_tree.dag | 44 ++++++++- src/v2/compiler/program_assembly.dag | 15 ++-- src/v2/std/compilers/body_lowering.dag | 28 +++++- .../claim/normalized_tree_admission_test.dag | 89 +++++++++++++++++++ 10 files changed, 221 insertions(+), 40 deletions(-) create mode 100644 src/v2/test/claim/normalized_tree_admission_test.dag diff --git a/dag/test/claim/namespace_graft_body_dissolution_witness_test.dag b/dag/test/claim/namespace_graft_body_dissolution_witness_test.dag index 4cdaba16519..b4124bf6338 100644 --- a/dag/test/claim/namespace_graft_body_dissolution_witness_test.dag +++ b/dag/test/claim/namespace_graft_body_dissolution_witness_test.dag @@ -90,7 +90,7 @@ fn normalized_tree_optional(source: String) -> Node? { } fn normalized_root_node(t: v2.compiler.normalized_tree.NormalizedTree) -> Node { - t + t.root } fn tree_contains_atom(n: Node, sym: Symbol) -> Bool { diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 735a4056bc8..7a903f8fa59 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -79,3 +79,26 @@ These three are the **same defect class as §1–§4**, just observed at the opp ## 7. Scope boundary This note does not implement the lens, does not touch the emitter's `PhantomData` rendering path or the checkpoint-scalar-phantom emit wall (§5, Root-4), and does not run the corpus census. It is the design-note-first deliverable the operator asked for as the sequencing gate before implementation starts. Next step on operator go-ahead: implement the lens (§4.1), run it against the full corpus, report the *real* count (replacing the disclaimed §2 number), and name the promotion trigger's actual measured proof. + +## 8. Third specimen, and the typecheck-blindness receipt it produced (2026-08-14, warm-fox-179 · BL-1) + +### 8.1 `NormalizedTree = Node` is a third specimen of §1's shape + +`src/v2/compiler/normalized_tree.dag` declared `type NormalizedTree = Node` — the bodyless alias idiom §6's negative control 2 blesses. It is blessed there because an alias to a real carrier constructs real values, and that reasoning is correct as far as it goes. What this specimen adds is the *cost* of the idiom where the alias names an **admitted product**: `normalize` computes a fact (this root's body lowering left no wrapper behind), and the alias gives that fact nowhere to live, so it travelled as diagnostics beside a bare `Node` and was dropped at the first `FreeMonoid` carrier on the way to `resolve`. The alias was not merely cosmetic — it was load-bearing in the wrong direction, because `NormalizedTree` and `Node` were the *same type to the checker*, so every position that should have demanded an admitted tree accepted a raw one. + +BL-1 replaces it with `type NormalizedTree sole_constructor { root: Node }` plus `admit_normalized_tree`, the single door that refuses a root carrying wrapper-retention evidence. Rung, stated honestly: **accepted refinement with executing refusal**, not structural impossibility — the invalid state remains describable; what changed is that no `Accepted` program reaches the carrier while holding retention evidence. + +### 8.2 The receipt: the v2 typechecker did not see either half of the conversion + +This is the finding, and it is worth more than the specimen. When `NormalizedTree` was first converted from alias to record, the whole v2 pipeline reported **0 blocking errors** while: + +- a `NormalizedTree` record sat in positions typed `Node`, and +- a raw `Node` sat in the `NormalizedTree` field. + +Both directions were wrong; neither was reported. The defect surfaced only under **execution**, as `no field 'kind' on type 'NormalizedTree'` from a witness that actually ran the graft path. A green typecheck was not evidence for this change class, and the whole BL-1 attempt had to be reverted and re-done with execution checks after each signature move. + +The same blindness has a second, independently-found specimen on the `ContentHash` family lane (proud-badger-240, gunbc#8250): a family member used in union position, likewise unreported at typecheck and failing at runtime as a non-exhaustive match. Two lanes, two unrelated carriers, one shared root — **the v2 typechecker's nominal-type discrimination is not enforced at these seams**, so carrier-refinement work of exactly the kind this note designs is currently unguarded by the checker that would be its natural home. + +Consequence for this note's sequencing: §7's plan (lens first, promote to a typechecker refusal once proven zero-false-positive) assumes the typechecker *would* enforce the refusal once told to. That assumption is now **unverified** at these seams and is the named next-rung trigger for the promotion step. Until it is established by execution, a carrier-sealing change must carry executing witnesses on the real path, not a compile. + +Executing evidence for the specimen half: `src/v2/test/claim/normalized_tree_admission_test.dag` (retention refused at the door; retention found past the diagnostic head, proven discriminating by reverting the recognizer to a head-only read and observing the control flip to `false`; clean root admitted; unrelated diagnostic does not refuse; the production retention producer's own output refused) and `dag/test/claim/namespace_graft_body_dissolution_witness_test.dag` `witness_lowered_body_module_still_grafts` green through the sealed carrier. diff --git a/src/v2/compiler/00_compile.dag b/src/v2/compiler/00_compile.dag index 4e23f9a34fa..39cb38ef9f3 100644 --- a/src/v2/compiler/00_compile.dag +++ b/src/v2/compiler/00_compile.dag @@ -378,7 +378,7 @@ fn validate_then_compile( fn compile_ingest_with_admission( source: Medium, target: TargetModel, - peer_roots: FreeMonoid, + peer_roots: FreeMonoid, admission: Admission ) -> Outcome> { let lm = dag_language_model() diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 256e3b97394..3f7e2234663 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -281,7 +281,7 @@ fn module_root_name_find_fold( ) } -fn validate_module_roots(roots: FreeMonoid) -> Outcome> { +fn validate_module_roots(roots: FreeMonoid) -> Outcome> { match fold_list( xs: roots, empty: ModuleRootsValid { seen: Empty, diagnostics: v2.std.diagnostic.None }, @@ -292,8 +292,8 @@ fn validate_module_roots(roots: FreeMonoid) -> Outcome> { ModuleRootsProjectionRejected { diagnostics: d } => ModuleRootsProjectionRejected { diagnostics: d } ModuleRootsValid { seen: prior, diagnostics: acc_diag } => - if well_formed(n: root) { - match qualified_name_from_module_node(root: root) { + if well_formed(n: root.root) { + match qualified_name_from_module_node(root: root.root) { Rejected { diagnostics: d } => ModuleRootsProjectionRejected { diagnostics: rejected_with_pending(pending: acc_diag, rejected: d) @@ -344,7 +344,7 @@ fn validate_module_roots(roots: FreeMonoid) -> Outcome> { data module_root_lookup_note: String = "Typed lookup result beside module_root_find diagnostics. ModuleRootMissing and ModuleRootAmbiguous preserve the exact QualifiedName sought; diagnostics are projections of these results for consumers that still read Outcome." fn module_root_lookup( - roots: FreeMonoid, + roots: FreeMonoid, name: QualifiedName ) -> ModuleRootLookup { match fold_list( @@ -358,7 +358,7 @@ fn module_root_lookup( ManyPassingCandidates => Accepted { value: ManyPassingCandidates, diagnostics: v2.std.diagnostic.None } _ => - match qualified_name_from_module_node(root: root) { + match qualified_name_from_module_node(root: root.root) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: candidate, diagnostics: _ } => @@ -394,9 +394,9 @@ fn module_root_lookup( } fn module_root_find( - roots: FreeMonoid, + roots: FreeMonoid, name: QualifiedName -) -> Outcome { +) -> Outcome { match module_root_lookup(roots: roots, name: name) { ModuleRootFound { root: root } => Accepted { value: root, diagnostics: v2.std.diagnostic.None } @@ -540,7 +540,7 @@ fn admit_import_root_find( } fn admit_import_visible_entry( - roots: FreeMonoid, + roots: FreeMonoid, import_edge: Import, state: AdmissionState ) -> AdmissionState { @@ -555,7 +555,7 @@ fn admit_import_visible_entry( } fn admit_import_entry( - roots: FreeMonoid, + roots: FreeMonoid, import_edge: Import, state: AdmissionState, subject_qn: QualifiedName, @@ -596,7 +596,7 @@ fn admit_import_entry( } fn admit_imports( - roots: FreeMonoid, + roots: FreeMonoid, imports: FreeMonoid, state: AdmissionState, subject_qn: QualifiedName, @@ -629,16 +629,16 @@ fn admission_namespace_base(lm: LanguageModel) -> Namespace { } fn subject_module_root_for_name( - roots: FreeMonoid, + roots: FreeMonoid, name: QualifiedName -) -> Outcome { +) -> Outcome { module_root_find(roots: roots, name: name) } fn namespace_admission_initial_state( policy: NameResolutionPolicy, lm: LanguageModel, - subject_module_root: Node, + subject_module_root: NormalizedTree, subject_qn: QualifiedName ) -> AdmissionState { let base = admission_namespace_base(lm: lm) @@ -647,7 +647,7 @@ fn namespace_admission_initial_state( admission_initial(base: base) ImportScoped => admit_named_exports( - root: subject_module_root, + root: subject_module_root.root, origin: subject_qn, state: admission_initial(base: base) ) @@ -657,14 +657,14 @@ fn namespace_admission_initial_state( fn admitted_subject_from_bindings( bindings: Map, base: Namespace, - subject_module_root: Node, - validated_roots: FreeMonoid, + subject_module_root: NormalizedTree, + validated_roots: FreeMonoid, module_qn: QualifiedName ) -> AdmittedSubject { AdmittedSubject { namespace: Namespace { bindings: namespace_bindings_augment_from_module_atoms( - root: subject_module_root, + root: subject_module_root.root, bindings: bindings ), canonical_symbols: base.canonical_symbols, @@ -680,7 +680,7 @@ fn admitted_subject_from_bindings( fn namespace_for_subject( lm: LanguageModel, - roots: FreeMonoid, + roots: FreeMonoid, admission: Admission, index: SourceRootIndex, active_roots: FreeMonoid, @@ -698,7 +698,7 @@ fn namespace_for_subject( diagnostics: rejected_with_pending(pending: validate_diag, rejected: r) } Accepted { value: subject_module_root, diagnostics: _ } => - if well_formed(n: subject_module_root) { + if well_formed(n: subject_module_root.root) { let base = admission_namespace_base(lm: lm) let initial = namespace_admission_initial_state( policy: policy, @@ -765,7 +765,7 @@ fn namespace_for_subject( fn resolve_with_admission_policy( lm: LanguageModel, - roots: FreeMonoid, + roots: FreeMonoid, admission: Admission, index: SourceRootIndex, active_roots: FreeMonoid, @@ -802,7 +802,7 @@ fn resolve_with_admission_policy( fn resolve_with_admission( lm: LanguageModel, - roots: FreeMonoid, + roots: FreeMonoid, admission: Admission ) -> Outcome { resolve_with_admission_policy( @@ -817,7 +817,7 @@ fn resolve_with_admission( fn resolve_with_admission_grounded( lm: LanguageModel, - roots: FreeMonoid, + roots: FreeMonoid, admission: Admission, index: SourceRootIndex, active_roots: FreeMonoid diff --git a/src/v2/compiler/03_normalize.dag b/src/v2/compiler/03_normalize.dag index b9e5dae9b10..dec642c9a77 100644 --- a/src/v2/compiler/03_normalize.dag +++ b/src/v2/compiler/03_normalize.dag @@ -37,7 +37,7 @@ import v2.compiler.body_lowering_fold { body_lower_finish_for_normalize, body_lower_is_deferred_lower_at_normalize } -import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree } import v2.compiler.namespace_graft { module_header_containment_graft, namespace_graft_is_graft_candidate @@ -217,7 +217,7 @@ fn normalize(parse_tree: ParseTree) -> Outcome { Rejected { diagnostics: r } => Rejected { diagnostics: r } Accepted { value: grafted, diagnostics: d1 } => if well_formed(n: grafted) { - Accepted { value: grafted, diagnostics: d1 } + admit_normalized_tree(root: grafted, diagnostics: d1) } else { Rejected { diagnostics: rejected_with_pending( diff --git a/src/v2/compiler/03_resolve.dag b/src/v2/compiler/03_resolve.dag index 76f0fa814e7..c4dbc1b36bb 100644 --- a/src/v2/compiler/03_resolve.dag +++ b/src/v2/compiler/03_resolve.dag @@ -239,8 +239,8 @@ fn add_bind_atom_binder(n: Node, acc: Map) -> Map Namespace { Namespace { bindings: harvest_direct_atom_binding( - root: tree, - acc: add_module_named_exports(root: tree, acc: empty_map()) + root: tree.root, + acc: add_module_named_exports(root: tree.root, acc: empty_map()) ), canonical_symbols: dag_language_model_canonical_symbols(lm: lm), symbol_index: empty_symbol_index(), @@ -1035,12 +1035,12 @@ fn resolve_with_namespace_policy( lm: LanguageModel, policy: NameResolutionPolicy ) -> Outcome { - if well_formed(n: tree) { + if well_formed(n: tree.root) { resolve_node( ctx: resolve_ctx_init(namespace: namespace, lm: lm, policy: policy), - n: tree + n: tree.root ) } else { - Rejected { diagnostics: diagnostics_singleton(d: malformed_tree_diagnostic(n: tree)) } + Rejected { diagnostics: diagnostics_singleton(d: malformed_tree_diagnostic(n: tree.root)) } } } diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index 4a9843c2033..aa143037fc4 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -1,5 +1,47 @@ module v2.compiler.normalized_tree +import v2.std.compilers.body_lowering { body_lowering_carries_wrapper_retention } +import v2.std.diagnostic { + Accepted, + Diagnostic, + Diagnostics, + ExternalContractUnknown, + Outcome, + Rejected, + diagnostics_singleton, + Unavailable, + node_locus, + rejected_with_pending +} import v2.std.node { Node } -type NormalizedTree = Node +// A NormalizedTree is the ADMITTED product of normalize: a root whose own +// lowering left no wrapper behind. It was an alias for Node until BL-1; the +// alias is why retention evidence reached resolve as a bare Node and was +// dropped there. Sealing the carrier is what makes the drop unwritable. +type NormalizedTree sole_constructor { + root: Node, +} + +data normalized_tree_admission_rung_note: String = "Rung: ACCEPTED REFINEMENT WITH EXECUTING REFUSAL, not structural impossibility. The invalid state -- a root carrying wrapper-retention diagnostics presented as normalized -- remains describable; what changed is that admit_normalized_tree is the sole constructor, so no Accepted program reaches the carrier while holding retention evidence. Next-rung trigger: the retention disposition riding in the carrier's own type rather than being decided at the door." + +fn normalized_tree_retention_diagnostic(root: Node) -> Diagnostic { + Diagnostic { + reason: ^normalized_tree_reason_wrapper_retention_not_normalized, + at: node_locus(node: root), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn admit_normalized_tree(root: Node, diagnostics: Diagnostics) -> Outcome { + if body_lowering_carries_wrapper_retention(d: diagnostics) { + Rejected { + diagnostics: rejected_with_pending( + pending: diagnostics, + rejected: diagnostics_singleton(d: normalized_tree_retention_diagnostic(root: root)) + ) + } + } else { + Accepted { value: NormalizedTree { root: root }, diagnostics: diagnostics } + } +} diff --git a/src/v2/compiler/program_assembly.dag b/src/v2/compiler/program_assembly.dag index 4090e1e61c0..02938796127 100644 --- a/src/v2/compiler/program_assembly.dag +++ b/src/v2/compiler/program_assembly.dag @@ -1,17 +1,18 @@ module v2.compiler.program_assembly +import v2.compiler.normalized_tree { NormalizedTree } import v2.extdeps.languages.dag { qualified_name_from_module_node } type ProgramAssemblyFoldState = ProgramAssemblyFoldOk { - roots: FreeMonoid, + roots: FreeMonoid, index: v2.std.cross_tree.resolution.SourceRootIndex } | ProgramAssemblyFoldFailed { diagnostics: NonEmptyDiagnostics } -fn program_assembly_empty_roots() -> FreeMonoid { +fn program_assembly_empty_roots() -> FreeMonoid { Empty } @@ -20,7 +21,7 @@ fn program_assembly_read_to_normalized_root_prepared( lm: LanguageModel, prepared: PreparedGrammar, residue: Diagnostics -) -> Outcome { +) -> Outcome { match read.artifact.kind { SourceFile => bind_outcome( @@ -48,7 +49,7 @@ fn program_assembly_read_to_normalized_root_prepared( fn program_assembly_read_to_normalized_root( read: DagSourceReadWitness, lm: LanguageModel -) -> Outcome { +) -> Outcome { match prepare_grammar(grammar: lm.grammar) { Rejected { diagnostics: d } => Rejected { diagnostics: d } Accepted { value: prepared, diagnostics: residue } => @@ -74,7 +75,7 @@ fn program_assembly_fold_step( ProgramAssemblyFoldOk { roots: acc, index: idx } => match program_assembly_read_to_normalized_root_prepared(read: read, lm: lm, prepared: prepared, residue: residue) { Accepted { value: root, diagnostics: _ } => - match qualified_name_from_module_node(root: root) { + match qualified_name_from_module_node(root: root.root) { Accepted { value: qn, diagnostics: _ } => ProgramAssemblyFoldOk { roots: list_snoc_item(xs: acc, item: root), @@ -125,7 +126,7 @@ fn program_assembly_fold_ingest( fn module_roots_from_source_root_ingest( ingest: SourceRootIngest, lm: LanguageModel -) -> Outcome> { +) -> Outcome> { match program_assembly_fold_ingest(ingest: ingest, lm: lm) { ProgramAssemblyFoldOk { roots: roots, index: _ } => outcome_accepted(value: roots) @@ -135,7 +136,7 @@ fn module_roots_from_source_root_ingest( } fn assemble_program_from_module_roots( - roots: FreeMonoid, + roots: FreeMonoid, admission: Admission, lm: LanguageModel ) -> Outcome { diff --git a/src/v2/std/compilers/body_lowering.dag b/src/v2/std/compilers/body_lowering.dag index f413b29699e..59373ff9fd3 100644 --- a/src/v2/std/compilers/body_lowering.dag +++ b/src/v2/std/compilers/body_lowering.dag @@ -1,10 +1,15 @@ module v2.std.compilers.body_lowering import std.algebra { FreeMonoid } -import v2.std.algebra { list_append, list_snoc_item } +import v2.std.algebra { fold_list, list_append, list_snoc_item } import v2.std.collection { Absent, List, Present, list_at_optional } +import v2.std.logic { Bool } import v2.std.diagnostic { Diagnostic, + Diagnostics, + None, + NonEmptyDiagnostics, + Some, ExternalContractUnknown, Outcome, Unavailable, @@ -122,3 +127,24 @@ fn lower_block_body_atoms(atoms: List, witness: Node) -> Outcome { ) } } + +data body_lowering_retention_recognition_note: String = "Wrapper retention has exactly ONE producer -- body_lower_wrapper_retained_shell, which emits body_lowering_reason_wrapper_retained_emitted -- so recognising it is a fact about THIS module and belongs beside the producer. A consumer that re-spelled the symbol would be a second name for one concept, and would drift the moment the reason is renamed." + +fn body_lowering_diagnostic_is_wrapper_retained(d: Diagnostic) -> Bool { + d.reason == ^body_lowering_reason_wrapper_retained_emitted +} + +fn body_lowering_non_empty_carries_wrapper_retention(d: NonEmptyDiagnostics) -> Bool { + fold_list( + xs: d.tail, + empty: body_lowering_diagnostic_is_wrapper_retained(d: d.head), + cons: fn(acc, item) { acc || body_lowering_diagnostic_is_wrapper_retained(d: item) } + ) +} + +fn body_lowering_carries_wrapper_retention(d: Diagnostics) -> Bool { + match d { + None => false + Some { diagnostics: ne } => body_lowering_non_empty_carries_wrapper_retention(d: ne) + } +} diff --git a/src/v2/test/claim/normalized_tree_admission_test.dag b/src/v2/test/claim/normalized_tree_admission_test.dag new file mode 100644 index 00000000000..780fd9284c9 --- /dev/null +++ b/src/v2/test/claim/normalized_tree_admission_test.dag @@ -0,0 +1,89 @@ +module v2.test.claim.normalized_tree_admission + +import v2.compiler.body_lowering_fold { body_lower_wrapper_retained_shell } +import v2.compiler.normalized_tree { admit_normalized_tree } +import v2.std.diagnostic { + Accepted, + Diagnostic, + Diagnostics, + ExternalContractUnknown, + None, + Outcome, + NonEmptyDiagnostics, + Rejected, + Some, + Unavailable, + node_locus +} +import v2.std.logic { Bool } +import v2.std.node { Conj, Node, TypeNode, node_synthetic } + +data normalized_tree_admission_witness_note: String = "BL-1's discriminating evidence. The invalid state is a root carrying wrapper-retention evidence presented to resolve as normalized -- which was reachable while NormalizedTree was an alias for Node, because the carrier had no door to refuse at. These controls execute the door: retention refuses, a clean tree admits, and retention is found ANYWHERE in the diagnostic list rather than at its head, because rejected_with_pending prepends outer diagnostics and a head-only read is systematically wrong." + +fn probe_shell() -> Node { + node_synthetic(kind: TypeNode { connective: Conj }, children: []) +} + +fn unrelated_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^normalized_tree_probe_unrelated_reason, + at: node_locus(node: probe_shell()), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn retention_diagnostic() -> Diagnostic { + Diagnostic { + reason: ^body_lowering_reason_wrapper_retained_emitted, + at: node_locus(node: probe_shell()), + correction: Unavailable { reason: ExternalContractUnknown } + } +} + +fn is_rejected(o: Outcome) -> Bool { + match o { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } +} + +test fn a_retained_root_is_refused_at_the_carrier_door() -> Bool { + is_rejected(o: admit_normalized_tree( + root: probe_shell(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { head: retention_diagnostic(), tail: [] } + } + )) +} + +test fn retention_is_found_past_the_diagnostic_head_RED() -> Bool { + is_rejected(o: admit_normalized_tree( + root: probe_shell(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { + head: unrelated_diagnostic(), + tail: [retention_diagnostic()] + } + } + )) +} + +test fn a_clean_root_is_admitted_positive_control() -> Bool { + (is_rejected(o: admit_normalized_tree(root: probe_shell(), diagnostics: None)) == false) +} + +test fn an_unrelated_diagnostic_does_not_refuse_RED() -> Bool { + (is_rejected(o: admit_normalized_tree( + root: probe_shell(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { head: unrelated_diagnostic(), tail: [] } + } + )) == false) +} + +test fn the_production_retention_producer_is_refused_by_the_door() -> Bool { + match body_lower_wrapper_retained_shell(shell: probe_shell()) { + Accepted { value: v, diagnostics: d } => is_rejected(o: admit_normalized_tree(root: v, diagnostics: d)) + Rejected { diagnostics: _ } => false + } +} From 12466c1a3dd76a0e40dfbb1cf7db55d11898e569 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 04:26:37 +0000 Subject: [PATCH 21/30] BL-1: correct the typecheck-blindness receipt to wrapper-payload conflation The ContentHash specimen was characterised as a family member in union position. Read on main, Fnv1a64Structural is the coproduct's PAYLOAD, not a member -- and that is the stronger fact: no alias participates in it, so the class is not a quirk of alias declarations. Both specimens are one shape. The typechecker does not distinguish a wrapper from the thing it wraps. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 7a903f8fa59..6593c906971 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -97,8 +97,25 @@ This is the finding, and it is worth more than the specimen. When `NormalizedTre Both directions were wrong; neither was reported. The defect surfaced only under **execution**, as `no field 'kind' on type 'NormalizedTree'` from a witness that actually ran the graft path. A green typecheck was not evidence for this change class, and the whole BL-1 attempt had to be reverted and re-done with execution checks after each signature move. -The same blindness has a second, independently-found specimen on the `ContentHash` family lane (proud-badger-240, gunbc#8250): a family member used in union position, likewise unreported at typecheck and failing at runtime as a non-exhaustive match. Two lanes, two unrelated carriers, one shared root — **the v2 typechecker's nominal-type discrimination is not enforced at these seams**, so carrier-refinement work of exactly the kind this note designs is currently unguarded by the checker that would be its natural home. +The same blindness has a second, independently-found specimen on the `ContentHash` family lane (gunbc#8250). It is worth stating precisely, because the imprecise version of it ("a family member used in union position") understates what the pair proves. Read off `dag/std/content_hash.dag` on main: -Consequence for this note's sequencing: §7's plan (lens first, promote to a typechecker refusal once proven zero-false-positive) assumes the typechecker *would* enforce the refusal once told to. That assumption is now **unverified** at these seams and is the named next-rung trigger for the promotion step. Until it is established by execution, a carrier-sealing change must carry executing witnesses on the real path, not a compile. +``` +type ContentHash = + | Fnv1a64(Fnv1a64Structural) + | Sha256Hash(Sha256Digest) + | Sha1Hash(Sha1Digest) + | Sha512Hash(Sha512Digest) + +fn content_hash_atom(value: NonEmptyStr) -> Fnv1a64Structural +fn content_hash_of_value(value: NonEmptyStr) -> ContentHash +``` + +`Fnv1a64Structural` is the coproduct's **payload**, not one of its members. The witnesses passed the unwrapped payload where the tagged coproduct value was required; the typechecker accepted it, and at runtime `match hash { Fnv1a64(_) => … }` met a bare `Fnv1a64Structural { digest: … }` with no arm — `non-exhaustive pattern match on: Fnv1a64Structural { digest: f81a5e039343e65b }`. + +**So the two specimens are one shape, not two directions: the typechecker does not distinguish a wrapper from the thing it wraps.** Here it is `NormalizedTree` versus the `Node` it wrapped, admitted because the wrapper was an alias. There it is `ContentHash` versus the `Fnv1a64Structural` it wraps, admitted **with no alias involved at all**. That second half is the load-bearing one: it rules out the natural objection that this is a quirk of alias declarations. The class is **wrapper-payload conflation generally**, and an alias is merely its cheapest instance. + +Two lanes, two unrelated carriers, one shared root — **the v2 typechecker's nominal-type discrimination is not enforced at these seams**, so carrier-refinement work of exactly the kind this note designs is currently unguarded by the checker that would be its natural home. + +Consequence for this note's sequencing: §7's plan (lens first, promote to a typechecker refusal once proven zero-false-positive) assumes the typechecker *would* enforce the refusal once told to. That assumption is now **unverified** at these seams and is the named next-rung trigger for the promotion step. Note also that a lens scoped to hollow *declarations* would not have caught the `ContentHash` specimen at all, since no hollow declaration participates in it. Until it is established by execution, a carrier-sealing change must carry executing witnesses on the real path, not a compile. Executing evidence for the specimen half: `src/v2/test/claim/normalized_tree_admission_test.dag` (retention refused at the door; retention found past the diagnostic head, proven discriminating by reverting the recognizer to a head-only read and observing the control flip to `false`; clean root admitted; unrelated diagnostic does not refuse; the production retention producer's own output refused) and `dag/test/claim/namespace_graft_body_dissolution_witness_test.dag` `witness_lowered_body_module_still_grafts` green through the sealed carrier. From c02eab26ebbc2fa4003d945417c858e6c9c1d129 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 04:55:16 +0000 Subject: [PATCH 22/30] BL-1: delete the retention-recognizer fork, consume the producer's own Review 51940 is right and this was my error. body_lowering_fold already carried body_lowering_diagnostic_is_wrapper_retained and the whole-list body_lowering_any_diagnostic_is_wrapper_retained -- landed by BL-0 -- and I minted a second copy in v2.std.compilers.body_lowering under the SAME names, then pointed the admission door at the fork. Two modules could answer 'does this carry retention?' and drift when the reason symbol moved: the exact parallel authority the change was supposed to close, authored by the commit message that claimed to close it. The std/ copy is deleted whole (that file is now byte-identical to main). body_lowering_fold gains one Diagnostics-grain reading beside its existing pair, delegating to the same whole-list scan, and normalized_tree imports it from there. No cycle: body_lowering_fold does not reach normalized_tree. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 2 +- src/v2/compiler/body_lowering_fold.dag | 7 +++++ src/v2/compiler/normalized_tree.dag | 4 +-- src/v2/std/compilers/body_lowering.dag | 28 +------------------- 4 files changed, 11 insertions(+), 30 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 6593c906971..3ade0e41ed9 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -118,4 +118,4 @@ Two lanes, two unrelated carriers, one shared root — **the v2 typechecker's no Consequence for this note's sequencing: §7's plan (lens first, promote to a typechecker refusal once proven zero-false-positive) assumes the typechecker *would* enforce the refusal once told to. That assumption is now **unverified** at these seams and is the named next-rung trigger for the promotion step. Note also that a lens scoped to hollow *declarations* would not have caught the `ContentHash` specimen at all, since no hollow declaration participates in it. Until it is established by execution, a carrier-sealing change must carry executing witnesses on the real path, not a compile. -Executing evidence for the specimen half: `src/v2/test/claim/normalized_tree_admission_test.dag` (retention refused at the door; retention found past the diagnostic head, proven discriminating by reverting the recognizer to a head-only read and observing the control flip to `false`; clean root admitted; unrelated diagnostic does not refuse; the production retention producer's own output refused) and `dag/test/claim/namespace_graft_body_dissolution_witness_test.dag` `witness_lowered_body_module_still_grafts` green through the sealed carrier. +Executing evidence for the specimen half: `src/v2/test/claim/normalized_tree_admission_test.dag` (retention refused at the door; retention found past the diagnostic head, proven discriminating by reverting the recognizer to a head-only read and observing the control flip to `false`, using the whole-list scan at its single authority in `v2.compiler.body_lowering_fold`; clean root admitted; unrelated diagnostic does not refuse; the production retention producer's own output refused) and `dag/test/claim/namespace_graft_body_dissolution_witness_test.dag` `witness_lowered_body_module_still_grafts` green through the sealed carrier. diff --git a/src/v2/compiler/body_lowering_fold.dag b/src/v2/compiler/body_lowering_fold.dag index c4a9b3942c9..c04b9c4eb3d 100644 --- a/src/v2/compiler/body_lowering_fold.dag +++ b/src/v2/compiler/body_lowering_fold.dag @@ -150,6 +150,13 @@ fn body_lowering_any_diagnostic_is_wrapper_retained(ne: NonEmptyDiagnostics) -> ) } +fn body_lowering_diagnostics_carry_wrapper_retention(d: Diagnostics) -> Bool { + match d { + None => false + Some { diagnostics: ne } => body_lowering_any_diagnostic_is_wrapper_retained(ne: ne) + } +} + fn body_lower_outcome_is_wrapper_retained(o: Outcome) -> Bool { match o { Accepted { value: _, diagnostics: d } => diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index aa143037fc4..2a6de129af2 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -1,6 +1,6 @@ module v2.compiler.normalized_tree -import v2.std.compilers.body_lowering { body_lowering_carries_wrapper_retention } +import v2.compiler.body_lowering_fold { body_lowering_diagnostics_carry_wrapper_retention } import v2.std.diagnostic { Accepted, Diagnostic, @@ -34,7 +34,7 @@ fn normalized_tree_retention_diagnostic(root: Node) -> Diagnostic { } fn admit_normalized_tree(root: Node, diagnostics: Diagnostics) -> Outcome { - if body_lowering_carries_wrapper_retention(d: diagnostics) { + if body_lowering_diagnostics_carry_wrapper_retention(d: diagnostics) { Rejected { diagnostics: rejected_with_pending( pending: diagnostics, diff --git a/src/v2/std/compilers/body_lowering.dag b/src/v2/std/compilers/body_lowering.dag index 59373ff9fd3..f413b29699e 100644 --- a/src/v2/std/compilers/body_lowering.dag +++ b/src/v2/std/compilers/body_lowering.dag @@ -1,15 +1,10 @@ module v2.std.compilers.body_lowering import std.algebra { FreeMonoid } -import v2.std.algebra { fold_list, list_append, list_snoc_item } +import v2.std.algebra { list_append, list_snoc_item } import v2.std.collection { Absent, List, Present, list_at_optional } -import v2.std.logic { Bool } import v2.std.diagnostic { Diagnostic, - Diagnostics, - None, - NonEmptyDiagnostics, - Some, ExternalContractUnknown, Outcome, Unavailable, @@ -127,24 +122,3 @@ fn lower_block_body_atoms(atoms: List, witness: Node) -> Outcome { ) } } - -data body_lowering_retention_recognition_note: String = "Wrapper retention has exactly ONE producer -- body_lower_wrapper_retained_shell, which emits body_lowering_reason_wrapper_retained_emitted -- so recognising it is a fact about THIS module and belongs beside the producer. A consumer that re-spelled the symbol would be a second name for one concept, and would drift the moment the reason is renamed." - -fn body_lowering_diagnostic_is_wrapper_retained(d: Diagnostic) -> Bool { - d.reason == ^body_lowering_reason_wrapper_retained_emitted -} - -fn body_lowering_non_empty_carries_wrapper_retention(d: NonEmptyDiagnostics) -> Bool { - fold_list( - xs: d.tail, - empty: body_lowering_diagnostic_is_wrapper_retained(d: d.head), - cons: fn(acc, item) { acc || body_lowering_diagnostic_is_wrapper_retained(d: item) } - ) -} - -fn body_lowering_carries_wrapper_retention(d: Diagnostics) -> Bool { - match d { - None => false - Some { diagnostics: ne } => body_lowering_non_empty_carries_wrapper_retention(d: ne) - } -} From 902e23576ad88526ed59ba9b6cb49b834fdd836b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 06:21:39 +0000 Subject: [PATCH 23/30] BL-1: convert the name-resolve fixtures, and close three seams they exposed Review 51952 is right that the name-resolve fixtures still passed raw FreeMonoid. Converting them and RUNNING them exposed three genuine production seams in this diff, none reported by the typechecker: admit_import_root_find declared Outcome -> no field 'children' symbol_index_fill_module_roots declared FreeMonoid -> no field 'kind' ModuleRootFound / PassingCandidateFold declared Node -> no field 'kind' The third reached a shared std carrier, so PassingCandidateFold is parameterised over its candidate type: the cardinality of a search result is not a fact about what was searched, and it was declared over Node only because every consumer happened to fold over Node. A local fold type would have been a second name for one concept; projecting and re-wrapping would have re-minted the carrier outside the door. Fixtures admit through the real door, and the refusal arm fails the witness rather than fabricating a value. Records the denominator in section 8.2: 39 declarations across 7 modules, every Node-meeting point resolved as one of 16 .root projections, 1 list projection, or 3 converted callees. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 22 +++- src/v2/compiler/03_name_resolve.dag | 14 +-- src/v2/compiler/normalized_tree.dag | 14 +++ src/v2/std/compilers/target_model.dag | 12 +-- src/v2/std/inhabitant_neutralization.dag | 8 +- src/v2/std/passing_candidate_fold.dag | 6 +- .../name_resolve/admission_fail_closed.dag | 100 ++++++++++++------ .../name_resolve/malformed_imported_root.dag | 16 +-- 8 files changed, 134 insertions(+), 58 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 3ade0e41ed9..9c3584de532 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -88,9 +88,27 @@ This note does not implement the lens, does not touch the emitter's `PhantomData BL-1 replaces it with `type NormalizedTree sole_constructor { root: Node }` plus `admit_normalized_tree`, the single door that refuses a root carrying wrapper-retention evidence. Rung, stated honestly: **accepted refinement with executing refusal**, not structural impossibility — the invalid state remains describable; what changed is that no `Accepted` program reaches the carrier while holding retention evidence. -### 8.2 The receipt: the v2 typechecker did not see either half of the conversion +### 8.2 The receipt: the v2 typechecker does not distinguish a wrapper from the thing it wraps -This is the finding, and it is worth more than the specimen. When `NormalizedTree` was first converted from alias to record, the whole v2 pipeline reported **0 blocking errors** while: +This is the finding, and it is worth more than the specimen. + +**Lead with the live half, because it is what makes the class load-bearing rather than archaeological.** Doing BL-1 — a single carrier conversion, judged narrow at the design level and correctly so — produced **five** instances of this class. Two were found by review on other lanes. **Three were found inside the change that documents the class**, each invisible to the typechecker, each surfaced only by running the code, and each discovered one at a time because fixing one revealed the next: + +| # | site | declared | actually received | runtime | +|---|---|---|---|---| +| 1 | `03_name_resolve` `admit_import_root_find` | `found: Outcome` | `Outcome` | `no field 'children'` | +| 2 | `symbol_index_fill` `symbol_index_fill_module_roots` | `roots: FreeMonoid` | `FreeMonoid` | `no field 'kind'` | +| 3 | `03_name_resolve` `ModuleRootLookup.ModuleRootFound` and the shared `v2.std.passing_candidate_fold` `PassingCandidateFold.OnePassingCandidate` | `root` / `candidate: Node` | `NormalizedTree` | `no field 'kind'` | + +Instance 3 is the one worth dwelling on: the conflation reached a **shared std carrier** used by six modules, none of which had anything to do with this change. That is the class escaping the module that introduced it. + +**The consequence for estimating work.** A carrier conversion is design-narrow — evidence exists at every point it is needed, so no stage is re-plumbed — while being *execution-search-wide*, because the checker gives no signal and every seam must be found by running something that reaches it. Those are different quantities, and quoting the first for the second is how this change was scoped as small. + +**The denominator, since a green sweep is not a bound.** "Every witness I could execute is green" cannot rule out a fourth seam. What can be stated: the converted frontier is **39 declarations mentioning `NormalizedTree` across 7 modules**; every point where such a value meets a `Node`-declared position is resolved in exactly one of three ways, and there are no others — **16 `.root` projections** at genuine Node inspections, **1 order-preserving list projection** (`normalized_tree_roots_to_nodes`, for the symbol-index fill, which is about Node structure and not admission), and **3 callees converted** to carry the admitted type (the table above). That is a bounded, checkable claim; the green sweep is evidence for it, not a substitute. + +### 8.2.1 How the class first showed itself + +When `NormalizedTree` was first converted from alias to record, the whole v2 pipeline reported **0 blocking errors** while: - a `NormalizedTree` record sat in positions typed `Node`, and - a raw `Node` sat in the `NormalizedTree` field. diff --git a/src/v2/compiler/03_name_resolve.dag b/src/v2/compiler/03_name_resolve.dag index 3f7e2234663..a2bc6556224 100644 --- a/src/v2/compiler/03_name_resolve.dag +++ b/src/v2/compiler/03_name_resolve.dag @@ -3,7 +3,7 @@ module v2.compiler.name_resolve import v2.std.language_model { LanguageModel } import v2.compiler.symbol_index_fill { symbol_index_fill_module_roots } -import v2.compiler.normalized_tree { NormalizedTree } +import v2.compiler.normalized_tree { NormalizedTree, normalized_tree_roots_to_nodes } import v2.std.symbol_index { empty_symbol_index } import v2.compiler.resolve { DeclaredBinding, @@ -137,7 +137,7 @@ fn resolve_diagnostics(reason: Symbol) -> NonEmptyDiagnostics { // module_root_find refuses with the exact QualifiedName sought — rendered as a dotted string on the Textual diagnostic file locus (§3: structural QN is identity; dotted form is projection for display). ModuleRootLookup is the structural authority; diagnostics project from ModuleRootMissing rather than parsing display text back. type ModuleRootLookup - = ModuleRootFound { root: Node } + = ModuleRootFound { root: NormalizedTree } | ModuleRootMissing { requested: QualifiedName } | ModuleRootAmbiguous { requested: QualifiedName } | ModuleRootLookupRejected { diagnostics: NonEmptyDiagnostics } @@ -341,7 +341,7 @@ fn validate_module_roots(roots: FreeMonoid) -> Outcome, @@ -517,7 +517,7 @@ fn admit_named_exports( } fn admit_import_root_find( - found: Outcome, + found: Outcome, origin: QualifiedName, state: AdmissionState ) -> AdmissionState { @@ -525,9 +525,9 @@ fn admit_import_root_find( Rejected { diagnostics: d } => AdmissionRejected { diagnostics: d } Accepted { value: imported_root, diagnostics: _ } => - if well_formed(n: imported_root) { + if well_formed(n: imported_root.root) { admit_named_exports( - root: imported_root, + root: imported_root.root, origin: origin, state: state ) @@ -670,7 +670,7 @@ fn admitted_subject_from_bindings( canonical_symbols: base.canonical_symbols, symbol_index: symbol_index_fill_module_roots( index: empty_symbol_index(), - roots: validated_roots + roots: normalized_tree_roots_to_nodes(roots: validated_roots) ), module_qn: module_qn }, diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index 2a6de129af2..dd3d6d86cce 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -13,6 +13,8 @@ import v2.std.diagnostic { node_locus, rejected_with_pending } +import std.algebra { Empty, FreeMonoid } +import v2.std.algebra { fold_list, list_snoc_item } import v2.std.node { Node } // A NormalizedTree is the ADMITTED product of normalize: a root whose own @@ -45,3 +47,15 @@ fn admit_normalized_tree(root: Node, diagnostics: Diagnostics) -> Outcome) -> FreeMonoid { + fold_list( + xs: roots, + empty: Empty, + cons: fn(acc, root) { list_snoc_item(xs: acc, item: root.root) } + ) +} diff --git a/src/v2/std/compilers/target_model.dag b/src/v2/std/compilers/target_model.dag index 46d5c918fee..6f7ae2feb6f 100644 --- a/src/v2/std/compilers/target_model.dag +++ b/src/v2/std/compilers/target_model.dag @@ -4403,10 +4403,10 @@ fn target_operator_realization_catalog_wire_schema_valid(catalog: Node) -> Outco } fn target_operator_realization_catalog_lookup_step( - acc: PassingCandidateFold, + acc: PassingCandidateFold, row_node: Node, canonical_op: CanonicalOperation -) -> PassingCandidateFold { +) -> PassingCandidateFold { match target_operator_realization_row_wire_schema_valid(row: row_node) { Accepted { value: _, diagnostics: _ } => if target_operator_realization_row_matches_canonical_op( @@ -7631,10 +7631,10 @@ fn target_value_template_mismatch_diagnostic(source_carrier: Node) -> Diagnostic } fn target_atom_realization_lookup_step( - acc: PassingCandidateFold, + acc: PassingCandidateFold, row: TargetAtomRealization, source_carrier: Node -) -> PassingCandidateFold { +) -> PassingCandidateFold { if content_hash(n: row.source_carrier) == content_hash(n: source_carrier) { match acc { NoPassingCandidate => OnePassingCandidate { candidate: row.source_carrier } @@ -7952,11 +7952,11 @@ fn target_atom_realization_catalog_row_decode_invalid_diagnostic( } fn target_atom_realization_catalog_lookup_step( - acc: PassingCandidateFold, + acc: PassingCandidateFold, row_node: Node, source_carrier: Node, host_target_model: TargetModel -) -> PassingCandidateFold { +) -> PassingCandidateFold { match target_atom_realization_from_row_node( row_node: row_node, host_target_model: host_target_model diff --git a/src/v2/std/inhabitant_neutralization.dag b/src/v2/std/inhabitant_neutralization.dag index 7b6706ef35d..b9cd4fa5d40 100644 --- a/src/v2/std/inhabitant_neutralization.dag +++ b/src/v2/std/inhabitant_neutralization.dag @@ -210,11 +210,11 @@ fn inhabitant_aligns_with_selection_policy(candidate: Node, selection: Node) -> } fn target_inhabitant_selection_policy_fold_step( - acc: PassingCandidateFold, + acc: PassingCandidateFold, candidate: Node, std_projection: Symbol, selection_policy: Node -) -> PassingCandidateFold { +) -> PassingCandidateFold { match inhabitant_std_projection_symbol(inhabitant: candidate) { Accepted { value: proj, diagnostics: _ } => if (proj == std_projection) @@ -268,10 +268,10 @@ fn target_inhabitant_for_std_projection_with_selection_policy( } fn target_inhabitant_fold_step( - acc: PassingCandidateFold, + acc: PassingCandidateFold, candidate: Node, std_projection: Symbol -) -> PassingCandidateFold { +) -> PassingCandidateFold { match inhabitant_std_projection_symbol(inhabitant: candidate) { Accepted { value: proj, diagnostics: _ } => if proj == std_projection { diff --git a/src/v2/std/passing_candidate_fold.dag b/src/v2/std/passing_candidate_fold.dag index ca421eccc2b..a9cde96f9b4 100644 --- a/src/v2/std/passing_candidate_fold.dag +++ b/src/v2/std/passing_candidate_fold.dag @@ -1,8 +1,8 @@ module v2.std.passing_candidate_fold -import v2.std.node { Node } +data passing_candidate_fold_note: String = "Zero / one / many passing candidates. The candidate is a TYPE PARAMETER because the concept is about the CARDINALITY of a search result, not about what was searched for -- it was declared over Node when every consumer happened to fold over Node, and that accident became load-bearing the moment a consumer folded over an admitted carrier instead. Parameterising is the same move std/integer.dag makes for machine widths: one axis, not one type per inhabitant." -type PassingCandidateFold +type PassingCandidateFold = NoPassingCandidate - | OnePassingCandidate { candidate: Node } + | OnePassingCandidate { candidate: T } | ManyPassingCandidates diff --git a/src/v2/test/claim/name_resolve/admission_fail_closed.dag b/src/v2/test/claim/name_resolve/admission_fail_closed.dag index 307404f94fe..c8ef48e827f 100644 --- a/src/v2/test/claim/name_resolve/admission_fail_closed.dag +++ b/src/v2/test/claim/name_resolve/admission_fail_closed.dag @@ -14,6 +14,7 @@ import v2.compiler.name_resolve { subject_module_root_for_name, validate_module_roots } +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree } import v2.std.cross_tree.resolution { source_root_index_empty } import v2.extdeps.languages.dag { dag_grammar_atom, @@ -29,6 +30,8 @@ import v2.std.diagnostic { None, Outcome, Rejected, + bind_outcome, + outcome_accepted, Textual, diagnostics_eq } @@ -48,7 +51,8 @@ import v2.std.verification { fn resolve_module_not_found_preserves_missing_module_identity() -> Bool { let missing = fc_qn(seg: ^fc_missing_seg) - match subject_module_root_for_name(roots: fc_single_found_root(), name: missing) { + fc_with_roots(o: fc_single_found_root(), f: fn(roots) { + match subject_module_root_for_name(roots: roots, name: missing) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: d } => match d.head.at { @@ -58,6 +62,7 @@ fn resolve_module_not_found_preserves_missing_module_identity() -> Bool { _ => false } } + }) } fn fc_pass_node() -> Node { @@ -120,15 +125,34 @@ fn fc_qn(seg: Symbol) -> QualifiedName { Cons { head: seg, tail: Empty } } -fn fc_duplicate_roots() -> FreeMonoid { - Cons { - head: fc_module_root(seg: ^fc_dup_seg), - tail: Cons { head: fc_module_root(seg: ^fc_dup_seg), tail: Empty } +data fc_admitted_fixture_note: String = "These fixtures pass module roots to validate_module_roots / resolve_with_admission, which since BL-1 take FreeMonoid. They therefore admit each synthetic root through the real door rather than casting past it -- a fixture that reached the sealed carrier without admission would be the wrapper-payload conflation the receipt in docs/plans/hollow-alias-construction-wall.md section 8.2 records, planted in the witness population. Admission is threaded as an Outcome and its refusal arm FAILS the witness: a fixture that cannot be admitted is a broken fixture, and reporting that as a passing verdict is the fabrication these witnesses exist to catch." + +fn fc_with_roots( + o: Outcome>, + f: fn(FreeMonoid) -> Bool +) -> Bool { + match o { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => f(roots) } } -fn fc_single_found_root() -> FreeMonoid { - Cons { head: fc_module_root(seg: ^fc_found_seg), tail: Empty } +fn fc_admitted(n: Node) -> Outcome { + admit_normalized_tree(root: n, diagnostics: None) +} + +fn fc_duplicate_roots() -> Outcome> { + bind_outcome(o: fc_admitted(n: fc_module_root(seg: ^fc_dup_seg)), f: fn(a) { + bind_outcome(o: fc_admitted(n: fc_module_root(seg: ^fc_dup_seg)), f: fn(b) { + outcome_accepted(value: Cons { head: a, tail: Cons { head: b, tail: Empty } }) + }) + }) +} + +fn fc_single_found_root() -> Outcome> { + bind_outcome(o: fc_admitted(n: fc_module_root(seg: ^fc_found_seg)), f: fn(a) { + outcome_accepted(value: Cons { head: a, tail: Empty }) + }) } fn fc_malformed_subject_root() -> Node { @@ -156,8 +180,10 @@ fn fc_malformed_subject_root() -> Node { } } -fn fc_malformed_subject_roots() -> FreeMonoid { - Cons { head: fc_malformed_subject_root(), tail: Empty } +fn fc_malformed_subject_roots() -> Outcome> { + bind_outcome(o: fc_admitted(n: fc_malformed_subject_root()), f: fn(a) { + outcome_accepted(value: Cons { head: a, tail: Empty }) + }) } fn fc_import_missing_target() -> Import { @@ -169,7 +195,7 @@ fn fc_initial_admission_state() -> AdmissionState { } fn admit_import_missing_module_is_rejected() -> Bool { - let roots = fc_single_found_root() + fc_with_roots(o: fc_single_found_root(), f: fn(roots) { match admit_import_entry( roots: roots, import_edge: fc_import_missing_target(), @@ -181,12 +207,14 @@ fn admit_import_missing_module_is_rejected() -> Bool { AdmissionAccepted { bindings: _, origins: _ } => false AdmissionRejected { diagnostics: _ } => true } + }) } fn resolve_duplicate_module_roots_is_rejected() -> Bool { + fc_with_roots(o: fc_duplicate_roots(), f: fn(roots) { match resolve_with_admission( lm: dag_language_model_surface_empty_prelude(), - roots: fc_duplicate_roots(), + roots: roots, admission: Admission { subject: ResolutionSubject { name: fc_qn(seg: ^fc_dup_seg) @@ -197,12 +225,14 @@ fn resolve_duplicate_module_roots_is_rejected() -> Bool { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + }) } fn resolve_subject_name_not_in_roots_is_rejected() -> Bool { + fc_with_roots(o: fc_single_found_root(), f: fn(roots) { match resolve_with_admission( lm: dag_language_model_surface_empty_prelude(), - roots: fc_single_found_root(), + roots: roots, admission: Admission { subject: ResolutionSubject { name: fc_qn(seg: ^fc_missing_seg) @@ -213,34 +243,42 @@ fn resolve_subject_name_not_in_roots_is_rejected() -> Bool { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + }) } fn validate_module_roots_threads_accepted_projection_diagnostics() -> Bool { - match validate_module_roots(roots: fc_single_found_root()) { + fc_with_roots(o: fc_single_found_root(), f: fn(roots) { + match validate_module_roots(roots: roots) { Accepted { value: _, diagnostics: d } => diagnostics_eq(a: d, b: None) Rejected { diagnostics: _ } => false } + }) } fn validate_module_roots_merges_pending_before_duplicate_reject() -> Bool { - match validate_module_roots(roots: fc_duplicate_roots()) { + fc_with_roots(o: fc_duplicate_roots(), f: fn(roots) { + match validate_module_roots(roots: roots) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + }) } fn validate_module_roots_malformed_root_is_rejected() -> Bool { - match validate_module_roots(roots: fc_malformed_subject_roots()) { + fc_with_roots(o: fc_malformed_subject_roots(), f: fn(roots) { + match validate_module_roots(roots: roots) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + }) } fn resolve_malformed_subject_root_is_rejected() -> Bool { + fc_with_roots(o: fc_malformed_subject_roots(), f: fn(roots) { match resolve_with_admission( lm: dag_language_model_surface_empty_prelude(), - roots: fc_malformed_subject_roots(), + roots: roots, admission: Admission { subject: ResolutionSubject { name: fc_qn(seg: ^fc_malformed_seg) @@ -251,25 +289,24 @@ fn resolve_malformed_subject_root_is_rejected() -> Bool { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + }) } -fn fc_dual_import_roots() -> FreeMonoid { - Cons { - head: fc_module_root_export(seg: ^fc_subject_seg, export_sym: ^fc_export_sym), - tail: Cons { - head: fc_module_root_export(seg: ^fc_import_a_seg, export_sym: ^fc_export_a_sym), - tail: Cons { - head: fc_module_root_export(seg: ^fc_import_b_seg, export_sym: ^fc_export_b_sym), - tail: Empty - } - } - } +fn fc_dual_import_roots() -> Outcome> { + bind_outcome(o: fc_admitted(n: fc_module_root_export(seg: ^fc_subject_seg, export_sym: ^fc_export_sym)), f: fn(a) { + bind_outcome(o: fc_admitted(n: fc_module_root_export(seg: ^fc_import_a_seg, export_sym: ^fc_export_a_sym)), f: fn(b) { + bind_outcome(o: fc_admitted(n: fc_module_root_export(seg: ^fc_import_b_seg, export_sym: ^fc_export_b_sym)), f: fn(c) { + outcome_accepted(value: Cons { head: a, tail: Cons { head: b, tail: Cons { head: c, tail: Empty } } }) + }) + }) + }) } fn resolve_dual_imports_do_not_collide_on_header_metadata() -> Bool { + fc_with_roots(o: fc_dual_import_roots(), f: fn(roots) { match resolve_with_admission_policy( lm: dag_language_model_surface_empty_prelude(), - roots: fc_dual_import_roots(), + roots: roots, admission: Admission { subject: ResolutionSubject { name: fc_qn(seg: ^fc_subject_seg) @@ -289,14 +326,16 @@ fn resolve_dual_imports_do_not_collide_on_header_metadata() -> Bool { Accepted { value: _, diagnostics: _ } => true Rejected { diagnostics: _ } => false } + }) } fn resolve_with_admission_threads_validate_diagnostics_on_accept() -> Bool { - match validate_module_roots(roots: fc_dual_import_roots()) { + fc_with_roots(o: fc_dual_import_roots(), f: fn(roots) { + match validate_module_roots(roots: roots) { Accepted { value: _, diagnostics: validate_diag } => match resolve_with_admission_policy( lm: dag_language_model_surface_empty_prelude(), - roots: fc_dual_import_roots(), + roots: roots, admission: Admission { subject: ResolutionSubject { name: fc_qn(seg: ^fc_subject_seg) @@ -319,6 +358,7 @@ fn resolve_with_admission_threads_validate_diagnostics_on_accept() -> Bool { } Rejected { diagnostics: _ } => false } + }) } data claim_admit_import_missing_module_is_rejected: TestClaim = CompilesClaim { diff --git a/src/v2/test/claim/name_resolve/malformed_imported_root.dag b/src/v2/test/claim/name_resolve/malformed_imported_root.dag index 94dfa5bed6d..8f3549139a6 100644 --- a/src/v2/test/claim/name_resolve/malformed_imported_root.dag +++ b/src/v2/test/claim/name_resolve/malformed_imported_root.dag @@ -8,7 +8,9 @@ import v2.compiler.name_resolve { ImportVisible, admit_import_entry } +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree } import v2.std.cross_tree.resolution { source_root_index_empty } +import v2.std.diagnostic { Accepted, None, Outcome, Rejected, bind_outcome, outcome_accepted } import v2.extdeps.languages.dag { dag_grammar_atom, dag_named_edge @@ -82,11 +84,10 @@ fn malformed_entry_name() -> QualifiedName { Cons { head: ^malformed_seg, tail: Empty } } -fn roots_with_malformed_entry() -> FreeMonoid { - Cons { - head: malformed_root_node(), - tail: Empty - } +fn roots_with_malformed_entry() -> Outcome> { + bind_outcome(o: admit_normalized_tree(root: malformed_root_node(), diagnostics: None), f: fn(a) { + outcome_accepted(value: Cons { head: a, tail: Empty }) + }) } fn import_edge_for_malformed() -> Import { @@ -105,7 +106,9 @@ fn admit_result_is_rejected(state: AdmissionState) -> Bool { } fn witness_malformed_root_is_rejected_holds() -> Bool { - let roots = roots_with_malformed_entry() + match roots_with_malformed_entry() { + Rejected { diagnostics: _ } => false + Accepted { value: roots, diagnostics: _ } => admit_result_is_rejected( state: admit_import_entry( roots: roots, @@ -116,6 +119,7 @@ fn witness_malformed_root_is_rejected_holds() -> Bool { active_roots: Empty ) ) + } } data witness_malformed_root_is_rejected: Bool = witness_malformed_root_is_rejected_holds() From 423c4f92cdacaf147b8442bc151f751b06d66215 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 08:33:36 +0000 Subject: [PATCH 24/30] BL-1: convert the witness callers the floor found, and re-cut the denominator at its true boundary The floor reported nine runtime 'no field root on type Node' failures across three witness modules. All nine are mine: the first denominator was drawn at production modules reachable from the converted chain, and witness modules meet a converted value exactly as production modules do. Adds admit_normalized_roots -- the plural door, refusing as a whole so a partially admitted list has no representation -- and routes every hand-built fixture through it. Negative witnesses keep a distinct arm for fixture-admission failure so a broken fixture cannot masquerade as the rejection under test. Denominator re-cut as a caller census with its boundary named: 28 modules reference the converted APIs, 13 feed from normalize and are unaffected, 15 hand-build roots and 12 needed conversion. Residue stated rather than implied -- the instrument is execution, so a witness that meets a converted value and does not execute is broken silently; intersecting the 190-file no-executing-consumer roster against the callers yields 6 files, 4 hand-built, all executed green. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 13 +- src/v2/compiler/normalized_tree.dag | 21 +++ .../long/door_real_module_probe_test.dag | 60 +++++--- .../program_assembly_multi_file_test.dag | 60 +++++--- .../claim/manual/resolve_compile_anchor.dag | 61 +++++--- ...ame_resolve_cross_tree_resolution_test.dag | 139 +++++++++++------- .../parse/parse_binding_fidelity_test.dag | 118 +++++++++------ 7 files changed, 302 insertions(+), 170 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 9c3584de532..86efab73d84 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -104,7 +104,18 @@ Instance 3 is the one worth dwelling on: the conflation reached a **shared std c **The consequence for estimating work.** A carrier conversion is design-narrow — evidence exists at every point it is needed, so no stage is re-plumbed — while being *execution-search-wide*, because the checker gives no signal and every seam must be found by running something that reaches it. Those are different quantities, and quoting the first for the second is how this change was scoped as small. -**The denominator, since a green sweep is not a bound.** "Every witness I could execute is green" cannot rule out a fourth seam. What can be stated: the converted frontier is **39 declarations mentioning `NormalizedTree` across 7 modules**; every point where such a value meets a `Node`-declared position is resolved in exactly one of three ways, and there are no others — **16 `.root` projections** at genuine Node inspections, **1 order-preserving list projection** (`normalized_tree_roots_to_nodes`, for the symbol-index fill, which is about Node structure and not admission), and **3 callees converted** to carry the admitted type (the table above). That is a bounded, checkable claim; the green sweep is evidence for it, not a substitute. +**The denominator, since a green sweep is not a bound — and the boundary it is drawn at, because a denominator whose scope is implicit reads as complete and is not.** + +The first cut of this enumeration covered **production modules reachable from the converted chain**: 39 declarations mentioning `NormalizedTree` across 7 modules, every `Node`-meeting point resolved as 16 `.root` projections, 1 order-preserving list projection (`normalized_tree_roots_to_nodes`), or 3 converted callees. That claim held for what it covered — and the floor then failed with **nine more instances in three witness modules**, because *witness modules meet a converted value exactly as production modules do* and the first cut silently excluded them. The method was sound; the scope was wrong, and the scope was wrong because it was never written down. + +The corrected population is drawn at **every declaration that meets a converted value, production or witness**, and it is enumerated as a caller census rather than a declaration census: + +- **28 modules** reference the converted APIs (`resolve`, `resolve_with_admission*`, `assemble_program_from_module_roots`, `validate_module_roots`, `module_root_find`, `admit_import_entry`, `build_program_namespace`). +- **13** feed their roots from `normalize(...)`, whose result is already `NormalizedTree` — unaffected by construction. +- **15** hand-build roots and therefore had to admit them through the door; 12 required conversion, the rest resolved to prose mentions or ingest-fed roots. +- Everything converted is verified **by execution**, not by compile. + +**The residue this cannot reach, stated rather than implied.** The instrument is execution, so a witness that meets a converted value and *does not execute* is broken identically and silently right now, and nothing reports it. That set was measured rather than guessed: intersecting the corpus-wide roster of witnesses with no executing consumer (190 files carrying a bare `: TestClaim =`, from the lane working that population) against the converted-API callers yields **6 files, 4 of them hand-built** — all four accounted for and executed green here. The intersection is small, but it is small *as measured*, and it is the honest bound on what "executed green" covers. ### 8.2.1 How the class first showed itself diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index dd3d6d86cce..ca26f49713c 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -8,6 +8,7 @@ import v2.std.diagnostic { ExternalContractUnknown, Outcome, Rejected, + bind_outcome, diagnostics_singleton, Unavailable, node_locus, @@ -59,3 +60,23 @@ fn normalized_tree_roots_to_nodes(roots: FreeMonoid) -> FreeMono cons: fn(acc, root) { list_snoc_item(xs: acc, item: root.root) } ) } + +// The plural door. Admitting a list of roots is not a new decision -- it is the +// same one per element, refusing as a whole so a partially admitted list has no +// representation. +fn admit_normalized_roots( + roots: FreeMonoid, + diagnostics: Diagnostics +) -> Outcome> { + fold_list( + xs: roots, + empty: Accepted { value: Empty, diagnostics: diagnostics }, + cons: fn(acc, root) { + bind_outcome(o: acc, f: fn(admitted) { + bind_outcome(o: admit_normalized_tree(root: root, diagnostics: diagnostics), f: fn(one) { + Accepted { value: list_snoc_item(xs: admitted, item: one), diagnostics: v2.std.diagnostic.None } + }) + }) + } + ) +} diff --git a/src/v2/test/claim/long/door_real_module_probe_test.dag b/src/v2/test/claim/long/door_real_module_probe_test.dag index 035029ec22d..117083b5437 100644 --- a/src/v2/test/claim/long/door_real_module_probe_test.dag +++ b/src/v2/test/claim/long/door_real_module_probe_test.dag @@ -1,5 +1,10 @@ module v2.test.long.door_real_module_probe +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_roots } +import v2.std.diagnostic { Accepted, None, Outcome, Rejected } +import v2.compiler.name_resolve { resolve_with_admission_policy } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } + data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly @@ -70,8 +75,9 @@ fn quad_subject_with_import_use() -> Node { } } -fn quad_roots() -> FreeMonoid { - Cons { +fn quad_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: quad_subject_with_import_use(), tail: Cons { head: quad_module_root( @@ -81,7 +87,9 @@ fn quad_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn quad_admission() -> Admission { @@ -100,30 +108,38 @@ fn quad_admission() -> Admission { } test fn quad_namespace_only_resolve_accepts_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model(), - roots: quad_roots(), - admission: quad_admission(), - index: source_root_index_empty(), - active_roots: Empty, - policy: NamespaceOnlyY - ) { - Accepted { value: _, diagnostics: _ } => true + match quad_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model(), + roots: admitted, + admission: quad_admission(), + index: source_root_index_empty(), + active_roots: Empty, + policy: NamespaceOnlyY + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } test fn quad_import_scoped_admission_reds_ambiguous_export_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model(), - roots: quad_roots(), - admission: quad_admission(), - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: d } => d.head.reason == ^resolve_ambiguous_export + match quad_roots() { + Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model(), + roots: admitted, + admission: quad_admission(), + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: d } => d.head.reason == ^resolve_ambiguous_export + } } } diff --git a/src/v2/test/claim/manual/program_assembly_multi_file_test.dag b/src/v2/test/claim/manual/program_assembly_multi_file_test.dag index 7e912b25f92..7d84df5daed 100644 --- a/src/v2/test/claim/manual/program_assembly_multi_file_test.dag +++ b/src/v2/test/claim/manual/program_assembly_multi_file_test.dag @@ -1,5 +1,6 @@ module v2.test.manual.program_assembly_multi_file +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_roots } import extdeps.communication.medium { Lossless, Medium } import v2.compiler.name_resolve { Admission, @@ -21,7 +22,12 @@ import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { fold_list_node } import v2.std.artifact { Artifact, SourceFile, TestClaimArtifact } import v2.std.cross_tree.import_model { V2Tree } -import v2.std.diagnostic { Accepted, Rejected } +import v2.std.diagnostic { + Accepted, + None, + Outcome, + Rejected +} import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import v2.compiler.namespace_graft { module_header_containment_graft } import v2.std.node { @@ -108,8 +114,9 @@ fn pa_subject_with_import_use() -> Node { } } -fn pa_cross_file_roots() -> FreeMonoid { - Cons { +fn pa_cross_file_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: pa_graft_module_root(root: pa_subject_with_import_use()), tail: Cons { head: pa_graft_module_root( @@ -121,11 +128,14 @@ fn pa_cross_file_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } -fn pa_duplicate_module_roots() -> FreeMonoid { - Cons { +fn pa_duplicate_module_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: pa_graft_module_root( root: pa_module_root( seg: ^pa_subject_seg, @@ -143,7 +153,9 @@ fn pa_duplicate_module_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn pa_cross_file_admission() -> Admission { @@ -162,24 +174,32 @@ fn pa_cross_file_admission() -> Admission { } test fn program_assembly_cross_file_import_assembles_holds() -> Bool { - match assemble_program_from_module_roots( - roots: pa_cross_file_roots(), - admission: pa_cross_file_admission(), - lm: dag_language_model_surface_empty_prelude() - ) { - Accepted { value: _, diagnostics: _ } => true + match pa_cross_file_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted_roots, diagnostics: _ } => + match assemble_program_from_module_roots( + roots: admitted_roots, + admission: pa_cross_file_admission(), + lm: dag_language_model_surface_empty_prelude() + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } test fn program_assembly_duplicate_module_roots_rejects() -> Bool { - match assemble_program_from_module_roots( - roots: pa_duplicate_module_roots(), - admission: pa_cross_file_admission(), - lm: dag_language_model_surface_empty_prelude() - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true + match pa_duplicate_module_roots() { + Rejected { diagnostics: _ } => false + Accepted { value: admitted_roots, diagnostics: _ } => + match assemble_program_from_module_roots( + roots: admitted_roots, + admission: pa_cross_file_admission(), + lm: dag_language_model_surface_empty_prelude() + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } } } diff --git a/src/v2/test/claim/manual/resolve_compile_anchor.dag b/src/v2/test/claim/manual/resolve_compile_anchor.dag index 8377939de6b..15c2570b873 100644 --- a/src/v2/test/claim/manual/resolve_compile_anchor.dag +++ b/src/v2/test/claim/manual/resolve_compile_anchor.dag @@ -1,5 +1,6 @@ module v2.test.manual.resolve_compile_anchor +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_roots } import v2.compiler.name_resolve { Admission, Import, @@ -17,7 +18,12 @@ import v2.extdeps.languages.dag { } import std.algebra { Cons, Empty, FreeMonoid } import v2.std.algebra { fold_list_node } -import v2.std.diagnostic { Accepted, Outcome, Rejected } +import v2.std.diagnostic { + Accepted, + None, + Outcome, + Rejected +} import v2.std.logic { Bool } import v2.std.node { Atom, @@ -271,14 +277,17 @@ fn resolve_subject_name() -> QualifiedName { Cons { head: ^anchor_subject_path_segment, tail: Empty } } -fn resolve_roots_single_import() -> FreeMonoid { - Cons { +fn resolve_roots_single_import() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: resolve_subject_module_root_with_import_use(), tail: Cons { head: resolve_imported_module_root(), tail: Empty } - } + }, + diagnostics: None + ) } fn resolve_imported_use_tree() -> Node { @@ -294,27 +303,31 @@ fn resolve_import_target() -> QualifiedName { } fn resolve_with_admission_imported_binding_accepts() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: resolve_roots_single_import(), - admission: Admission { - subject: ResolutionSubject { - name: resolve_subject_name() - }, - imports: Cons { - head: Import { - target: resolve_import_target(), - visibility: ImportVisible - }, - tail: Empty - } - }, - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => true + match resolve_roots_single_import() { Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: Admission { + subject: ResolutionSubject { + name: resolve_subject_name() + }, + imports: Cons { + head: Import { + target: resolve_import_target(), + visibility: ImportVisible + }, + tail: Empty + } + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } diff --git a/src/v2/test/claim/name_resolve_cross_tree_resolution_test.dag b/src/v2/test/claim/name_resolve_cross_tree_resolution_test.dag index 949b3d97d1e..aa2d093da21 100644 --- a/src/v2/test/claim/name_resolve_cross_tree_resolution_test.dag +++ b/src/v2/test/claim/name_resolve_cross_tree_resolution_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.name_resolve_cross_tree_resolution +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_roots } import v2.compiler.name_resolve { Admission, Import, @@ -22,6 +23,8 @@ import v2.std.cross_tree.resolution { } import v2.std.diagnostic { Accepted, + None, + Outcome, Rejected, locus_eq, port_locus @@ -153,8 +156,9 @@ fn ct_both_trees() -> SourceRootSet { Cons { head: V2Tree, tail: Cons { head: DagTree, tail: Empty } } } -fn ct_dag_same_tree_roots() -> FreeMonoid { - Cons { +fn ct_dag_same_tree_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: ct_graft_module_root( root: ct_subject_with_import_use( qn_spine: ct_qn_node(seg: ^ct_dag_subject_seg), @@ -172,7 +176,9 @@ fn ct_dag_same_tree_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn ct_dag_same_tree_admission() -> Admission { @@ -188,8 +194,9 @@ fn ct_dag_same_tree_admission() -> Admission { } } -fn ct_v2_same_tree_roots() -> FreeMonoid { - Cons { +fn ct_v2_same_tree_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: ct_graft_module_root( root: ct_subject_with_import_use( qn_spine: ct_qn_spine( @@ -211,7 +218,9 @@ fn ct_v2_same_tree_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn ct_v2_same_tree_admission() -> Admission { @@ -227,8 +236,9 @@ fn ct_v2_same_tree_admission() -> Admission { } } -fn ct_cross_tree_roots() -> FreeMonoid { - Cons { +fn ct_cross_tree_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: ct_graft_module_root( root: ct_subject_with_import_use( qn_spine: ct_qn_spine( @@ -248,7 +258,9 @@ fn ct_cross_tree_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn ct_cross_tree_admission() -> Admission { @@ -278,8 +290,9 @@ fn ct_v2_to_dag_index() -> SourceRootIndex { ) } -fn ct_dag_to_v2_roots() -> FreeMonoid { - Cons { +fn ct_dag_to_v2_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: ct_graft_module_root( root: ct_subject_with_import_use( qn_spine: ct_qn_node(seg: ^ct_inv_dag_subject_seg), @@ -299,7 +312,9 @@ fn ct_dag_to_v2_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn ct_dag_to_v2_admission() -> Admission { @@ -330,63 +345,79 @@ fn ct_dag_to_v2_index() -> SourceRootIndex { } test fn witness_dag_same_tree_import_resolves_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: ct_dag_same_tree_roots(), - admission: ct_dag_same_tree_admission(), - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => true + match ct_dag_same_tree_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: ct_dag_same_tree_admission(), + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } test fn witness_v2_same_tree_import_resolves_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: ct_v2_same_tree_roots(), - admission: ct_v2_same_tree_admission(), - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => true + match ct_v2_same_tree_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: ct_v2_same_tree_admission(), + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } test fn witness_cross_tree_v2_to_dag_grounded_resolves_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: ct_cross_tree_roots(), - admission: ct_cross_tree_admission(), - index: ct_v2_to_dag_index(), - active_roots: ct_both_trees(), - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => true + match ct_cross_tree_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: ct_cross_tree_admission(), + index: ct_v2_to_dag_index(), + active_roots: ct_both_trees(), + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } test fn witness_cross_tree_dag_to_v2_grounded_denied_holds() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: ct_dag_to_v2_roots(), - admission: ct_dag_to_v2_admission(), - index: ct_dag_to_v2_index(), - active_roots: ct_both_trees(), - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: r } => - (r.head.reason == ^resolve_cross_tree_import_denied) - && locus_eq( - a: r.head.at, - b: port_locus(port: ^resolve_cross_tree_import_denied) - ) + match ct_dag_to_v2_roots() { + Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: ct_dag_to_v2_admission(), + index: ct_dag_to_v2_index(), + active_roots: ct_both_trees(), + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: r } => + (r.head.reason == ^resolve_cross_tree_import_denied) + && locus_eq( + a: r.head.at, + b: port_locus(port: ^resolve_cross_tree_import_denied) + ) + } } } diff --git a/src/v2/test/claim/parse/parse_binding_fidelity_test.dag b/src/v2/test/claim/parse/parse_binding_fidelity_test.dag index 1888dfbe6f4..74a16b6f025 100644 --- a/src/v2/test/claim/parse/parse_binding_fidelity_test.dag +++ b/src/v2/test/claim/parse/parse_binding_fidelity_test.dag @@ -1,5 +1,6 @@ module v2.test.parse.parse_binding_fidelity +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_roots, admit_normalized_tree } import v2.compiler.name_resolve { Admission, Import, @@ -35,6 +36,7 @@ import v2.std.optional { import v2.std.qualified_name { QualifiedName } import v2.std.diagnostic { Accepted, + None, Outcome, Rejected, bind_outcome, @@ -132,8 +134,9 @@ fn pbf_subject_with_import_use() -> Node { } } -fn pbf_cross_file_roots() -> FreeMonoid { - Cons { +fn pbf_cross_file_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: pbf_graft_module_root(root: pbf_subject_with_import_use()), tail: Cons { head: pbf_graft_module_root( @@ -145,31 +148,37 @@ fn pbf_cross_file_roots() -> FreeMonoid { ), tail: Empty } - } + }, + diagnostics: None + ) } fn pbf_cross_file_import_resolves() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: pbf_cross_file_roots(), - admission: Admission { - subject: ResolutionSubject { - name: Cons { head: ^pbf_subject_seg, tail: Empty } - }, - imports: Cons { - head: Import { - target: Cons { head: ^pbf_imported_seg, tail: Empty }, - visibility: ImportVisible - }, - tail: Empty - } - }, - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => true + match pbf_cross_file_roots() { Rejected { diagnostics: _ } => false + Accepted { value: admitted_roots, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted_roots, + admission: Admission { + subject: ResolutionSubject { + name: Cons { head: ^pbf_subject_seg, tail: Empty } + }, + imports: Cons { + head: Import { + target: Cons { head: ^pbf_imported_seg, tail: Empty }, + visibility: ImportVisible + }, + tail: Empty + } + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => true + Rejected { diagnostics: _ } => false + } } } @@ -191,13 +200,17 @@ fn pbf_unbound_atom_tree() -> Node { } fn pbf_resolve_unbound_symbol_rejects() -> Bool { + match admit_normalized_tree(root: pbf_unbound_atom_tree(), diagnostics: None) { + Rejected { diagnostics: _ } => false + Accepted { value: admitted_tree, diagnostics: _ } => match resolve( - tree: pbf_unbound_atom_tree(), + tree: admitted_tree, lm: dag_language_model_surface_empty_prelude() ) { Accepted { value: _, diagnostics: _ } => false Rejected { diagnostics: _ } => true } + } } fn pbf_subject_with_ambiguous_import_use() -> Node { @@ -225,8 +238,9 @@ fn pbf_subject_with_ambiguous_import_use() -> Node { } } -fn pbf_ambiguous_import_roots() -> FreeMonoid { - Cons { +fn pbf_ambiguous_import_roots() -> Outcome> { + admit_normalized_roots( + roots: Cons { head: pbf_graft_module_root(root: pbf_subject_with_ambiguous_import_use()), tail: Cons { head: pbf_graft_module_root( @@ -247,37 +261,43 @@ fn pbf_ambiguous_import_roots() -> FreeMonoid { tail: Empty } } - } + }, + diagnostics: None + ) } fn pbf_ambiguous_export_rejects() -> Bool { - match resolve_with_admission_policy( - lm: dag_language_model_surface_empty_prelude(), - roots: pbf_ambiguous_import_roots(), - admission: Admission { - subject: ResolutionSubject { - name: Cons { head: ^pbf_subject_seg, tail: Empty } - }, - imports: Cons { - head: Import { - target: Cons { head: ^pbf_ambiguous_import_a_seg, tail: Empty }, - visibility: ImportVisible + match pbf_ambiguous_import_roots() { + Rejected { diagnostics: _ } => false + Accepted { value: admitted, diagnostics: _ } => + match resolve_with_admission_policy( + lm: dag_language_model_surface_empty_prelude(), + roots: admitted, + admission: Admission { + subject: ResolutionSubject { + name: Cons { head: ^pbf_subject_seg, tail: Empty } }, - tail: Cons { + imports: Cons { head: Import { - target: Cons { head: ^pbf_ambiguous_import_b_seg, tail: Empty }, + target: Cons { head: ^pbf_ambiguous_import_a_seg, tail: Empty }, visibility: ImportVisible }, - tail: Empty + tail: Cons { + head: Import { + target: Cons { head: ^pbf_ambiguous_import_b_seg, tail: Empty }, + visibility: ImportVisible + }, + tail: Empty + } } - } - }, - index: source_root_index_empty(), - active_roots: Empty, - policy: ImportScoped - ) { - Accepted { value: _, diagnostics: _ } => false - Rejected { diagnostics: _ } => true + }, + index: source_root_index_empty(), + active_roots: Empty, + policy: ImportScoped + ) { + Accepted { value: _, diagnostics: _ } => false + Rejected { diagnostics: _ } => true + } } } From 28e0807baf1922cfbbaa177e3a714caba69bc098 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 08:58:34 +0000 Subject: [PATCH 25/30] BL-1: stop the plural door duplicating its shared diagnostics N+1 times Review 52023 flagged admit_normalized_roots re-passing the outer diagnostics into each per-element admit. It is a real defect, not just a smell: bind_outcome MERGES accumulated with incoming, the seed carried the shared set, and every step carried it again -- so an N-element list accumulated N+1 copies of one diagnostic set. Reachable only from tests today because every caller passes None, where merging is harmless. The obvious fix -- admit each element under None -- would have made the plural door total, i.e. a cast around the door rather than the door. So the decision stays per element (same verdict each time, since it reads only the shared diagnostics), the threading becomes explicit instead of bind_outcome, the refusal keeps carrying its pending set exactly once via admit_normalized_tree, and the accepted diagnostics are attached once at the end. plural_admission_does_not_duplicate_shared_diagnostics_RED asserts the result's diagnostics equal the input for a two-element list; restoring the bind_outcome threading flips it to false. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- src/v2/compiler/normalized_tree.dag | 35 +++++++++++++---- .../claim/normalized_tree_admission_test.dag | 38 ++++++++++++++++++- 2 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/v2/compiler/normalized_tree.dag b/src/v2/compiler/normalized_tree.dag index ca26f49713c..fba76665c5b 100644 --- a/src/v2/compiler/normalized_tree.dag +++ b/src/v2/compiler/normalized_tree.dag @@ -64,19 +64,38 @@ fn normalized_tree_roots_to_nodes(roots: FreeMonoid) -> FreeMono // The plural door. Admitting a list of roots is not a new decision -- it is the // same one per element, refusing as a whole so a partially admitted list has no // representation. +// +// The threading is explicit rather than bind_outcome because bind_outcome MERGES +// the accumulated diagnostics with each step's, and every step here carries the +// same shared diagnostics -- so threading them would accumulate N+1 copies of one +// set. The decision is still made per element (it is the same verdict each time, +// since it reads only the shared diagnostics), the refusal carries its pending +// set exactly once via admit_normalized_tree, and the accepted diagnostics are +// attached once at the end. fn admit_normalized_roots( roots: FreeMonoid, diagnostics: Diagnostics ) -> Outcome> { - fold_list( + match fold_list( xs: roots, - empty: Accepted { value: Empty, diagnostics: diagnostics }, + empty: Accepted { value: Empty, diagnostics: v2.std.diagnostic.None }, cons: fn(acc, root) { - bind_outcome(o: acc, f: fn(admitted) { - bind_outcome(o: admit_normalized_tree(root: root, diagnostics: diagnostics), f: fn(one) { - Accepted { value: list_snoc_item(xs: admitted, item: one), diagnostics: v2.std.diagnostic.None } - }) - }) + match acc { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: admitted, diagnostics: _ } => + match admit_normalized_tree(root: root, diagnostics: diagnostics) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: one, diagnostics: _ } => + Accepted { + value: list_snoc_item(xs: admitted, item: one), + diagnostics: v2.std.diagnostic.None + } + } + } } - ) + ) { + Rejected { diagnostics: r } => Rejected { diagnostics: r } + Accepted { value: all, diagnostics: _ } => + Accepted { value: all, diagnostics: diagnostics } + } } diff --git a/src/v2/test/claim/normalized_tree_admission_test.dag b/src/v2/test/claim/normalized_tree_admission_test.dag index 780fd9284c9..32ad56c12f7 100644 --- a/src/v2/test/claim/normalized_tree_admission_test.dag +++ b/src/v2/test/claim/normalized_tree_admission_test.dag @@ -1,7 +1,8 @@ module v2.test.claim.normalized_tree_admission import v2.compiler.body_lowering_fold { body_lower_wrapper_retained_shell } -import v2.compiler.normalized_tree { admit_normalized_tree } +import std.algebra { Cons, Empty, FreeMonoid } +import v2.compiler.normalized_tree { admit_normalized_roots, admit_normalized_tree } import v2.std.diagnostic { Accepted, Diagnostic, @@ -13,6 +14,7 @@ import v2.std.diagnostic { Rejected, Some, Unavailable, + diagnostics_eq, node_locus } import v2.std.logic { Bool } @@ -87,3 +89,37 @@ test fn the_production_retention_producer_is_refused_by_the_door() -> Bool { Rejected { diagnostics: _ } => false } } + +fn two_probe_roots() -> FreeMonoid { + Cons { head: probe_shell(), tail: Cons { head: probe_shell(), tail: Empty } } +} + +test fn plural_admission_does_not_duplicate_shared_diagnostics_RED() -> Bool { + match admit_normalized_roots( + roots: two_probe_roots(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { head: unrelated_diagnostic(), tail: [] } + } + ) { + Rejected { diagnostics: _ } => false + Accepted { value: _, diagnostics: d } => + diagnostics_eq( + a: d, + b: Some { + diagnostics: NonEmptyDiagnostics { head: unrelated_diagnostic(), tail: [] } + } + ) + } +} + +test fn plural_admission_refuses_a_retained_batch() -> Bool { + match admit_normalized_roots( + roots: two_probe_roots(), + diagnostics: Some { + diagnostics: NonEmptyDiagnostics { head: retention_diagnostic(), tail: [] } + } + ) { + Rejected { diagnostics: _ } => true + Accepted { value: _, diagnostics: _ } => false + } +} From f00837161a577d876fa06a07528fbe1bec0dbbbb Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 09:18:50 +0000 Subject: [PATCH 26/30] BL-1: convert the self-host intermediate the caller census missed Review 52034 found frontier_probe_first_missing_import_lookup declaring roots: FreeMonoid while receiving validated_roots from validate_module_roots and forwarding it into module_root_lookup -- the exact seam this PR closes, left open on a self-host consumer. The census had classified that module safe by checking WHERE its roots came from (an already-admitted ingest fold) rather than WHAT each intermediate declared. Provenance is not a substitute for declaration, and that is the third time this population was drawn at the wrong boundary. Replaces the judgement with a mechanical check: for every function, does it pass one of its own Node / FreeMonoid parameters into a converted API. Corpus wide it reports zero -- and the instrument is validated against the defect it must catch, reporting exactly the frontier_probe site when that parameter is reintroduced and zero when the fix is restored. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 4 ++++ src/v2/compiler/self_host/frontier_probe.dag | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 86efab73d84..6612ae7e8bc 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -115,6 +115,10 @@ The corrected population is drawn at **every declaration that meets a converted - **15** hand-build roots and therefore had to admit them through the door; 12 required conversion, the rest resolved to prose mentions or ingest-fed roots. - Everything converted is verified **by execution**, not by compile. +**A third scope correction, and the method that finally closed it.** Review 52034 then found a *fourth* instance the caller census still missed: `frontier_probe_first_missing_import_lookup` declared `roots: FreeMonoid`, received `validated_roots` from `validate_module_roots`, and forwarded it into `module_root_lookup`. The census had classified that module as safe by checking **where its roots came from** (an ingest fold, already admitted) rather than **what each intermediate function declared** — so an unconverted parameter sat in the middle of an otherwise-correct chain. + +That is the third time the population was drawn at the wrong boundary, and the pattern across all three is one thing: *provenance is not a substitute for declaration*. The check that actually decides this class is mechanical — for every function, does it pass one of its own `Node`-or-`FreeMonoid` parameters into a converted API? — and it is decidable from the containment tree without running anything. Run corpus-wide it reports **zero**, and the instrument is validated against the defect it must catch: reintroducing the `frontier_probe` parameter makes it report exactly that one site, and restoring the fix returns it to zero. A "zero found" from an unvalidated instrument would have been worth nothing, which is the same specification-without-execution trap one level up. + **The residue this cannot reach, stated rather than implied.** The instrument is execution, so a witness that meets a converted value and *does not execute* is broken identically and silently right now, and nothing reports it. That set was measured rather than guessed: intersecting the corpus-wide roster of witnesses with no executing consumer (190 files carrying a bare `: TestClaim =`, from the lane working that population) against the converted-API callers yields **6 files, 4 of them hand-built** — all four accounted for and executed green here. The intersection is small, but it is small *as measured*, and it is the honest bound on what "executed green" covers. ### 8.2.1 How the class first showed itself diff --git a/src/v2/compiler/self_host/frontier_probe.dag b/src/v2/compiler/self_host/frontier_probe.dag index 3698151decc..e11166d2a9c 100644 --- a/src/v2/compiler/self_host/frontier_probe.dag +++ b/src/v2/compiler/self_host/frontier_probe.dag @@ -1,5 +1,6 @@ module v2.compiler.self_host.frontier_probe +import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.self_host.compiler_closure_emit { emit_compiler_import_closure_from_ingest } @@ -270,7 +271,7 @@ fn frontier_probe_read_failure_reason(result: SourceRefReadResult) -> Symbol { } fn frontier_probe_first_missing_import_lookup( - roots: FreeMonoid, + roots: FreeMonoid, imports: FreeMonoid ) -> Optional { fold_list( From 58e74107e5a80a0ddb7ad903b14bcb3a3a78d7e4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 10:12:19 +0000 Subject: [PATCH 27/30] BL-1: close the conversion population by fixpoint over three syntactic shapes Review 52042 found a witness helper passing normalize's result into qualified_name_from_module_node without projecting .root -- a shape neither earlier check covered, on a path backing an enrolled long-lane witness. The class has three shapes and they compose into a fixpoint: A own Node parameter passed into a converted API B a NormalizedTree-producer binding passed into a Node-declared parameter C a function declaring -> Outcome while returning a producer Fixing a shape-C return type makes its consumers visible to shape B, and fixing those exposes more shape C. Iterated to convergence: A and B report zero; the four remaining C hits are confirmed false positives, feeding resolve, which takes the admitted carrier, with ResolvedTree = Node so their return is correct. Closed here: 6 witness/lens helpers, 6 return declarations, 8 fixpoint-surfaced consumers. Each shape was discovered only after a review or CI surfaced an instance -- with no checker signal there is no way to derive the shapes, only to enumerate them, and that is the finding worth keeping. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 12 ++++++++++++ .../lens/complexity_accumulator_copy/roster_gate.dag | 2 +- src/v2/lens/enforcement/cost_coverage.dag | 6 ++++-- src/v2/lens/vacuity.dag | 5 +++-- .../body_lowering/fn_body_seam_witness_helpers.dag | 2 +- .../wave1_gate1_b1_bare_call_helpers.dag | 2 +- .../claim/execution/pick_ingested_probe_test.dag | 2 +- .../pick_ingested_structural_lowering_test.dag | 7 ++++--- src/v2/test/claim/long/cwc_resolution_probe_test.dag | 10 ++++++---- .../long/vacuity_consumer_witness_long_test.dag | 4 ++-- .../validate_ingest_staging_stage_bisect_test.dag | 5 +++-- src/v2/test/claim/manual/body_lowering_match.dag | 2 +- .../claim/parse/parse_binding_fidelity_support.dag | 7 ++++--- src/v2/test/compiler/pipeline/stage_bridge.dag | 11 ++++++----- 14 files changed, 49 insertions(+), 28 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 6612ae7e8bc..7369784c38e 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -119,6 +119,18 @@ The corrected population is drawn at **every declaration that meets a converted That is the third time the population was drawn at the wrong boundary, and the pattern across all three is one thing: *provenance is not a substitute for declaration*. The check that actually decides this class is mechanical — for every function, does it pass one of its own `Node`-or-`FreeMonoid` parameters into a converted API? — and it is decidable from the containment tree without running anything. Run corpus-wide it reports **zero**, and the instrument is validated against the defect it must catch: reintroducing the `frontier_probe` parameter makes it report exactly that one site, and restoring the fix returns it to zero. A "zero found" from an unvalidated instrument would have been worth nothing, which is the same specification-without-execution trap one level up. +**The class has three syntactic shapes, and enumerating them by hand is the only instrument available.** Review 52042 then found a *fifth* instance in a shape neither earlier check covered. Each shape was discovered only after a review or CI surfaced an instance of it, which is itself the finding — a checker blind to wrapper-payload conflation gives no way to derive the shapes, only to enumerate them: + +| shape | the defect | found by | +|---|---|---| +| **A** | a function passes its own `Node` / `FreeMonoid` parameter into a converted API | review 52034 (`frontier_probe`) | +| **B** | a value bound from a `NormalizedTree` producer is passed into a `Node`-declared parameter | review 52042 (`wave1_gate1_b1` helper) | +| **C** | a function declares `-> Outcome` while its body returns a `NormalizedTree` producer | shape-B sweep, transitively | + +The three compose into a **fixpoint**, which is what finally closed the population: fixing a shape-C return type makes its consumers newly visible to shape B, and fixing those exposes more shape C. Iterated to convergence the sweeps report **A: 0, B: 0**; shape C's remaining 4 hits are confirmed false positives (they feed `resolve`, which *takes* the admitted carrier, and `ResolvedTree = Node` so their declared return is correct). Sweep A is validated against a known instance — reintroducing the `frontier_probe` parameter makes it report exactly that site. + +Population closed this way, beyond the caller census above: 1 self-host intermediate, 6 witness/lens helpers on shape B, 6 return declarations on shape C, and 8 consumers surfaced by the fixpoint. + **The residue this cannot reach, stated rather than implied.** The instrument is execution, so a witness that meets a converted value and *does not execute* is broken identically and silently right now, and nothing reports it. That set was measured rather than guessed: intersecting the corpus-wide roster of witnesses with no executing consumer (190 files carrying a bare `: TestClaim =`, from the lane working that population) against the converted-API callers yields **6 files, 4 of them hand-built** — all four accounted for and executed green here. The intersection is small, but it is small *as measured*, and it is the honest bound on what "executed green" covers. ### 8.2.1 How the class first showed itself diff --git a/src/v2/lens/complexity_accumulator_copy/roster_gate.dag b/src/v2/lens/complexity_accumulator_copy/roster_gate.dag index 0bf96f06471..0c761095037 100644 --- a/src/v2/lens/complexity_accumulator_copy/roster_gate.dag +++ b/src/v2/lens/complexity_accumulator_copy/roster_gate.dag @@ -35,7 +35,7 @@ fn source_findings(source_text: String) -> Optional> { match normalize(parse_tree: artifact.tree) { Rejected { diagnostics: _ } => Absent Accepted { value: tree, diagnostics: _ } => - Present { value: accumulator_copy_findings(root: tree) } + Present { value: accumulator_copy_findings(root: tree.root) } } } } diff --git a/src/v2/lens/enforcement/cost_coverage.dag b/src/v2/lens/enforcement/cost_coverage.dag index 4fd8dc2d218..c04db00958b 100644 --- a/src/v2/lens/enforcement/cost_coverage.dag +++ b/src/v2/lens/enforcement/cost_coverage.dag @@ -1,5 +1,7 @@ module v2.lens.enforcement.cost_coverage +import v2.compiler.normalized_tree { NormalizedTree } + type CostCoverageIngestVerdict = IngestParsedClean @@ -237,7 +239,7 @@ fn cost_coverage_file_receipt_from_module( fn cost_coverage_ingest_source_text( source_text: String, file: Symbol -) -> Outcome { +) -> Outcome { let lm = dag_language_model() match tokenize(text: source_text, file: file, rules: lm.lex) { Rejected { diagnostics: d } => @@ -280,7 +282,7 @@ fn cost_coverage_file_receipt(rel_path: String) -> CostCoverageFileReceipt { reason: d.head.reason ) Accepted { value: module_root, diagnostics: _ } => - cost_coverage_file_receipt_from_module(rel_path: rel_path, module_root: module_root) + cost_coverage_file_receipt_from_module(rel_path: rel_path, module_root: module_root.root) } } diff --git a/src/v2/lens/vacuity.dag b/src/v2/lens/vacuity.dag index 654466cb14a..8f027accef8 100644 --- a/src/v2/lens/vacuity.dag +++ b/src/v2/lens/vacuity.dag @@ -1,5 +1,6 @@ module v2.lens.vacuity +import v2.compiler.normalized_tree { NormalizedTree } import std.dissolution { DissolutionCondition, unbound_dissolution } import extdeps.filesystem.filesystem_io { Filesystem } @@ -312,7 +313,7 @@ data vacuity_bool_witness_body_lowering_ceiling_note: String = "SECOND, INDEPEND data vacuity_bool_witness_fn_decl_collection_ceiling_note: String = "THIRD, BROADER CEILING (empirically executed, not guessed; found while wiring the item-5 consumer witness's derived-site enumeration): fn-declaration COLLECTION itself — vacuity_collect_fn_decls, via dag_node_is_fn_decl/parse_production_emitted_identity_optional over normalize() output — returns EMPTY for real corpus files, independent of body shape and broader than the bare-`==`-body case above. This is NOT specific to vacuity.dag: src/v2/lens/enforcement/cost_coverage.dag's cost_coverage_collect_fn_decls is byte-identical logic over the byte-identical three-stage ingest (filesystem_read -> tokenize -> parse_module -> normalize), and running BOTH collectors over the SAME successfully-normalized module_root (src/v2/std/nat.dag, four known fn declarations) in one execution returned empty for both, ruling out a vacuity-specific bug. Independently reconfirmed by executing cost_coverage's OWN already-merged witnesses directly — cost_coverage_dag_logic_has_three_fn_bodies (asserts fn_attempted == 3 for dag/std/logic.dag) and cost_coverage_v2_tree_receipt_by_execution (asserts fn_attempted > 0 for real src/v2 files including nat.dag) — both FAIL by direct execution today. And reconfirmed a second time on a pristine origin/main checkout (git worktree add against origin/main, this branch's compiled claim_batch binary, cd into the worktree with relative --source-root paths): cost_coverage_dag_logic_has_three_fn_bodies FAILS on clean main too, so this is pre-existing and corpus-wide, not introduced by this branch and not limited to vacuity's own mandate. Consequence for this lens: vacuity_consumer_witness_live_derived_sites() (v2.test.claim.vacuity_consumer_witness) legitimately enumerates zero sites against its declared fixture file today, even though that file is independently confirmed to normalize() cleanly — the derived-site/shadow join still holds by real execution (empty == empty, never vacuously skipped), and the fixture-module-reach requirement (DESIGN.md §6 inert-lens hygiene) is separately satisfied by a direct compile-time symbol reference (vacuity_consumer_witness_fixture_module_reached_by_direct_call), not by fn-decl collection. This is the SAME \"general body producer\" DESIGN.md open thread the body-lowering ceiling above already cites — broader in scope (declaration discovery, not just one body shape) but the same authority and the same dissolve-on trigger, per §3 single authority, not a second independently-named trigger. Mechanism-vs-reachability stays separated exactly as above: classify_vacuity_same_eval_record's correctness is proven by the direct-call RED control regardless of this ceiling." -fn vacuity_witness_ingest_source_text(source_text: String, file: Symbol) -> Outcome { +fn vacuity_witness_ingest_source_text(source_text: String, file: Symbol) -> Outcome { let lm = dag_language_model() match tokenize(text: source_text, file: file, rules: lm.lex) { Rejected { diagnostics: d } => Rejected { diagnostics: d } @@ -468,7 +469,7 @@ fn vacuity_bool_witness_evidence(witness: BoolWitness) -> VacuityEvidence { Rejected { diagnostics: _ } => Unknown Accepted { value: module_root, diagnostics: _ } => match vacuity_find_fn_decl_by_name( - decls: vacuity_collect_fn_decls(root: module_root, acc: Empty), + decls: vacuity_collect_fn_decls(root: module_root.root, acc: Empty), name: witness.function ) { Absent => Unknown diff --git a/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag b/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag index dfba44d3db3..5d18f316af2 100644 --- a/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag +++ b/src/v2/test/claim/body_lowering/fn_body_seam_witness_helpers.dag @@ -157,7 +157,7 @@ fn wave1_gate1_a1_fn_body_seam_normalize_still_yields_arrow_transform_holds() -> match normalize(parse_tree: artifact.tree) { Rejected { diagnostics: _ } => false Accepted { value: tree, diagnostics: _ } => - match ingested_find_arrow_in_module(root: tree) { + match ingested_find_arrow_in_module(root: tree.root) { Absent => false Present { value: arrow } => match find_arrow_body_child(root: arrow) { diff --git a/src/v2/test/claim/body_lowering/wave1_gate1_b1_bare_call_helpers.dag b/src/v2/test/claim/body_lowering/wave1_gate1_b1_bare_call_helpers.dag index c6cd029010b..6a53e26a851 100644 --- a/src/v2/test/claim/body_lowering/wave1_gate1_b1_bare_call_helpers.dag +++ b/src/v2/test/claim/body_lowering/wave1_gate1_b1_bare_call_helpers.dag @@ -20,7 +20,7 @@ fn wave1_gate1_b1_resolve_source(source: String) -> Bool { match normalize(parse_tree: artifact.tree) { Rejected { diagnostics: _ } => false Accepted { value: root, diagnostics: _ } => - match qualified_name_from_module_node(root: root) { + match qualified_name_from_module_node(root: root.root) { Rejected { diagnostics: _ } => false Accepted { value: module_qn, diagnostics: _ } => match resolve_with_admission( diff --git a/src/v2/test/claim/execution/pick_ingested_probe_test.dag b/src/v2/test/claim/execution/pick_ingested_probe_test.dag index 992c51da6c7..58b462ccb7b 100644 --- a/src/v2/test/claim/execution/pick_ingested_probe_test.dag +++ b/src/v2/test/claim/execution/pick_ingested_probe_test.dag @@ -162,7 +162,7 @@ fn pick_probe_normalized_has_branch_holds() -> Bool { Accepted { value: artifact, diagnostics: _ } => match normalize(parse_tree: artifact.tree) { Accepted { value: normalized, diagnostics: _ } => - match find_arrow_body_child(root: pick_probe_find_arrow(root: normalized)) { + match find_arrow_body_child(root: pick_probe_find_arrow(root: normalized.root)) { Accepted { value: body, diagnostics: _ } => match body.kind { ComputationNode { behavior: Branch } => true diff --git a/src/v2/test/claim/execution/pick_ingested_structural_lowering_test.dag b/src/v2/test/claim/execution/pick_ingested_structural_lowering_test.dag index 6b7cd479bc9..8c1ee376b80 100644 --- a/src/v2/test/claim/execution/pick_ingested_structural_lowering_test.dag +++ b/src/v2/test/claim/execution/pick_ingested_structural_lowering_test.dag @@ -1,5 +1,6 @@ module v2.test.execution.pick_ingested_structural_lowering +import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.eval { EvalContext, TestClaimEvalSubject, @@ -120,7 +121,7 @@ fn pick_eval_int_literal_optional(lexeme: String) -> Optional { } } -fn pick_ingested_normalized_module_from_source(source: String) -> Outcome { +fn pick_ingested_normalized_module_from_source(source: String) -> Outcome { let lm = dag_language_model() bind_outcome( o: tokenize(text: source, file: ^pick_ingested_source_file, rules: lm.lex), @@ -166,7 +167,7 @@ fn pick_ingested_find_arrow_in_module(root: Node) -> Optional { fn pick_ingested_normalized_arrow_has_branch_body(source: String) -> Bool { match pick_ingested_normalized_module_from_source(source: source) { Accepted { value: normalized, diagnostics: _ } => - match pick_ingested_find_arrow_in_module(root: normalized) { + match pick_ingested_find_arrow_in_module(root: normalized.root) { Present { value: arrow } => match find_arrow_body_child(root: arrow) { Accepted { value: body, diagnostics: _ } => @@ -228,7 +229,7 @@ fn pick_ingested_normalized_branch_arms_magnitude_holds( ) -> Bool { match pick_ingested_normalized_module_from_source(source: source) { Accepted { value: normalized, diagnostics: _ } => - match pick_ingested_find_arrow_in_module(root: normalized) { + match pick_ingested_find_arrow_in_module(root: normalized.root) { Present { value: arrow } => match find_arrow_body_child(root: arrow) { Accepted { value: body, diagnostics: _ } => diff --git a/src/v2/test/claim/long/cwc_resolution_probe_test.dag b/src/v2/test/claim/long/cwc_resolution_probe_test.dag index 0e86f88b41d..efb762de714 100644 --- a/src/v2/test/claim/long/cwc_resolution_probe_test.dag +++ b/src/v2/test/claim/long/cwc_resolution_probe_test.dag @@ -1,5 +1,7 @@ module v2.test.long.cwc_resolution_probe +import v2.compiler.normalized_tree { NormalizedTree } + data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly data cwc_src_type_two: String = "module probe\ntype T = A | B\n" @@ -12,7 +14,7 @@ data cwc_src_type_malformed_coproduct: String = "module probe\ntype T = { x: Int data cwc_src_cwc_module: String = "module probe\ntype VariantNaming = AsAuthored | ScreamingSnakeCase\ntype VariantEncoding = StringVariant { naming: VariantNaming }\ntype CoproductWireContract { coproduct: String, encoding: VariantEncoding }\ntype ReviewEvent = Approve | RequestChanges | Comment | Pending\ndata review_event_wire_contract: CoproductWireContract = { coproduct: \"ReviewEvent\", encoding: StringVariant { naming: ScreamingSnakeCase } }\n" -fn cwc_norm_of(src: String) -> Outcome { +fn cwc_norm_of(src: String) -> Outcome { let lm = dag_language_model() bind_outcome( o: tokenize(text: src, file: ^cwc_probe_file, rules: lm.lex), @@ -90,7 +92,7 @@ fn cwc_norm_rejects(src: String) -> Bool { test fn cwc_non_coproduct_rhs_passes_through() -> Bool { match cwc_norm_of(src: cwc_src_type_scalar) { - Accepted { value: root, diagnostics: _ } => !cwc_tree_has_disj(n: root) + Accepted { value: root, diagnostics: _ } => !cwc_tree_has_disj(n: root.root) Rejected { diagnostics: _ } => false } } @@ -126,14 +128,14 @@ test fn cwc_cwc_module_resolve_accepts() -> Bool { test fn cwc_review_normalized_has_disj() -> Bool { match cwc_norm_of(src: cwc_src_type_review) { - Accepted { value: root, diagnostics: _ } => cwc_tree_has_disj(n: root) + Accepted { value: root, diagnostics: _ } => cwc_tree_has_disj(n: root.root) Rejected { diagnostics: _ } => false } } test fn cwc_review_disj_has_four_arms() -> Bool { match cwc_norm_of(src: cwc_src_type_review) { - Accepted { value: root, diagnostics: _ } => cwc_disj_arm_count(n: root) == 4 + Accepted { value: root, diagnostics: _ } => cwc_disj_arm_count(n: root.root) == 4 Rejected { diagnostics: _ } => false } } diff --git a/src/v2/test/claim/long/vacuity_consumer_witness_long_test.dag b/src/v2/test/claim/long/vacuity_consumer_witness_long_test.dag index 1859fb15e24..991d8c4d2d8 100644 --- a/src/v2/test/claim/long/vacuity_consumer_witness_long_test.dag +++ b/src/v2/test/claim/long/vacuity_consumer_witness_long_test.dag @@ -42,7 +42,7 @@ fn vacuity_consumer_witness_live_derived_sites() -> List { Rejected { diagnostics: _ } => Empty Accepted { value: module_root, diagnostics: _ } => fold_list( - xs: vacuity_collect_fn_decls(root: module_root, acc: Empty), + xs: vacuity_collect_fn_decls(root: module_root.root, acc: Empty), empty: Empty, cons: fn(acc, fn_decl) { match vacuity_fn_decl_name_optional(fn_decl: fn_decl) { @@ -151,7 +151,7 @@ test fn vacuity_consumer_witness_real_test_fn_gap_stays_unknown_by_execution() - Rejected { diagnostics: _ } => false Accepted { value: module_root, diagnostics: _ } => let fn_lookup = vacuity_find_fn_decl_by_name( - decls: vacuity_collect_fn_decls(root: module_root, acc: Empty), + decls: vacuity_collect_fn_decls(root: module_root.root, acc: Empty), name: target_fn ) let fn_genuinely_absent = match fn_lookup { diff --git a/src/v2/test/claim/long/validate_ingest_staging_stage_bisect_test.dag b/src/v2/test/claim/long/validate_ingest_staging_stage_bisect_test.dag index 594cf2a8061..886a0794514 100644 --- a/src/v2/test/claim/long/validate_ingest_staging_stage_bisect_test.dag +++ b/src/v2/test/claim/long/validate_ingest_staging_stage_bisect_test.dag @@ -1,5 +1,6 @@ module v2.test.long.validate_ingest_staging_stage_bisect +import v2.compiler.normalized_tree { NormalizedTree } import extdeps.communication.medium { Lossless, Medium } import v2.compiler.compile { LensGateWitness, always_required_lenses, @@ -65,7 +66,7 @@ fn bisect_through_parse() -> Outcome { ) } -fn bisect_through_normalize() -> Outcome { +fn bisect_through_normalize() -> Outcome { let lm = dag_language_model() bind_outcome( o: tokenize( @@ -189,7 +190,7 @@ fn bisect_parse_module_add_well_formed_after_normalize() -> Bool { match parse(tokens: token_stream, grammar: lm.grammar) { Accepted { value: parsed, diagnostics: _ } => match normalize(parse_tree: parsed) { - Accepted { value: normalized, diagnostics: _ } => well_formed(n: normalized) + Accepted { value: normalized, diagnostics: _ } => well_formed(n: normalized.root) Rejected { diagnostics: _ } => false } Rejected { diagnostics: _ } => false diff --git a/src/v2/test/claim/manual/body_lowering_match.dag b/src/v2/test/claim/manual/body_lowering_match.dag index 6fe530173e6..9fea93301d8 100644 --- a/src/v2/test/claim/manual/body_lowering_match.dag +++ b/src/v2/test/claim/manual/body_lowering_match.dag @@ -154,7 +154,7 @@ fn body_lowering_normalized_module_has_match() -> Bool { match normalize(parse_tree: tree) { Rejected { diagnostics: _ } => false Accepted { value: normalized, diagnostics: _ } => - body_lowering_tree_has_match_behavior(root: normalized) + body_lowering_tree_has_match_behavior(root: normalized.root) } } } diff --git a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag index 21991e8bb72..02cb40bfab1 100644 --- a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag +++ b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag @@ -1,5 +1,6 @@ module v2.test.parse.parse_binding_fidelity_support +import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.name_resolve { Admission, Import, @@ -72,7 +73,7 @@ fn pbf_parse_tokens(tokens: TokenStream) -> Outcome { ) } -fn pbf_normalized_add_module() -> Outcome { +fn pbf_normalized_add_module() -> Outcome { match pbf_tokenize() { Accepted { value: tokens, diagnostics: _ } => match pbf_parse_tokens(tokens: tokens) { @@ -87,7 +88,7 @@ fn pbf_normalized_add_module() -> Outcome { fn pbf_normalized_add_arrow() -> Optional { match pbf_normalized_add_module() { Accepted { value: tree, diagnostics: _ } => - ingested_find_arrow_in_module(root: tree) + ingested_find_arrow_in_module(root: tree.root) Rejected { diagnostics: _ } => Absent } } @@ -104,7 +105,7 @@ fn pbf_resolved_add_module() -> Outcome { fn pbf_resolved_add_arrow() -> Optional { match pbf_resolved_add_module() { Accepted { value: tree, diagnostics: _ } => - ingested_find_arrow_in_module(root: tree) + ingested_find_arrow_in_module(root: tree.root) Rejected { diagnostics: _ } => Absent } } diff --git a/src/v2/test/compiler/pipeline/stage_bridge.dag b/src/v2/test/compiler/pipeline/stage_bridge.dag index 69e13b8b07b..39b3e8e76e6 100644 --- a/src/v2/test/compiler/pipeline/stage_bridge.dag +++ b/src/v2/test/compiler/pipeline/stage_bridge.dag @@ -1,5 +1,6 @@ module v2.test.compiler.pipeline.stage_bridge +import v2.compiler.normalized_tree { NormalizedTree } import v2.compiler.infer { InferredTree, infer } import v2.compiler.normalize { normalize } import v2.compiler.parse { parse_module } @@ -107,7 +108,7 @@ fn pipeline_parse_source(source: String, file: Symbol) -> Outcome { ) } -fn pipeline_normalize_source(source: String, file: Symbol) -> Outcome { +fn pipeline_normalize_source(source: String, file: Symbol) -> Outcome { bind_outcome( o: pipeline_parse_source(source: source, file: file), f: fn(parse_tree) { @@ -209,7 +210,7 @@ fn pipeline_normalize_pick_then_arm_magnitude_relationship_holds() -> Bool { file: ^pipeline_corpus_pick_file ) { Accepted { value: tree, diagnostics: _ } => - match pipeline_find_arrow_in_module(root: tree) { + match pipeline_find_arrow_in_module(root: tree.root) { Present { value: arrow } => match find_arrow_body_child(root: arrow) { Accepted { value: body, diagnostics: _ } => @@ -265,7 +266,7 @@ fn pipeline_normalize_add_relationship_holds() -> Bool { file: ^pipeline_corpus_add_file ) { Accepted { value: tree, diagnostics: _ } => - pipeline_arrow_body_has_behavior(root: tree, wanted: Transform) + pipeline_arrow_body_has_behavior(root: tree.root, wanted: Transform) Rejected { diagnostics: _ } => false } } @@ -287,7 +288,7 @@ fn pipeline_match_corpus_normalize_relationship_holds() -> Bool { file: ^pipeline_corpus_match_file ) { Accepted { value: tree, diagnostics: _ } => - pipeline_arrow_body_has_behavior(root: tree, wanted: Match) + pipeline_arrow_body_has_behavior(root: tree.root, wanted: Match) Rejected { diagnostics: _ } => false } } @@ -298,7 +299,7 @@ fn pipeline_match_corpus_resolve_relationship_holds() -> Bool { file: ^pipeline_corpus_match_file ) { Accepted { value: tree, diagnostics: _ } => - pipeline_arrow_body_has_behavior(root: tree, wanted: Match) + pipeline_arrow_body_has_behavior(root: tree.root, wanted: Match) Rejected { diagnostics: _ } => false } } From 216e5a606d8364e9e0f4af580ea228eba311448c Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 12:24:24 +0000 Subject: [PATCH 28/30] BL-1: revert two over-applied .root projections, and record the direction asymmetry The 58e74107e red was NOT another instance of the invisible conflation. It was the inverse: .root applied to a genuine Node, which the checker DOES catch statically at resolve. Two sites, both introduced by my own bulk string replaces hitting call sites whose value came from resolve (ResolvedTree = Node) rather than from normalize -- parse_binding_fidelity_support pbf_resolved_add_arrow and stage_bridge pipeline_match_corpus_resolve_relationship_holds. Records the asymmetry in the receipt, because citing both directions as typecheck blindness would inflate the class: payload-supplied-where-wrapper- declared is invisible; wrapper-projection-on-payload is caught. Also records that what kept being wrong was the ENUMERATION, not the fix. The population was cut three times -- production, then _test modules, then support modules -- each drawn where the last failure landed rather than from a rule. All three were proxies keyed on path or role. The sweeps key on the only thing that matters, whether a declaration meets a converted value, and glob the whole tree with no path filter. Verified: all 18 files carrying an introduced projection resolve clean, and the three tree-wide sweeps report A=0, B=0, C=4-known-false-positives. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 4 ++++ src/v2/test/claim/parse/parse_binding_fidelity_support.dag | 2 +- src/v2/test/compiler/pipeline/stage_bridge.dag | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 7369784c38e..5979504c378 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -131,6 +131,10 @@ The three compose into a **fixpoint**, which is what finally closed the populati Population closed this way, beyond the caller census above: 1 self-host intermediate, 6 witness/lens helpers on shape B, 6 return declarations on shape C, and 8 consumers surfaced by the fixpoint. +**The two directions are not symmetric, and conflating them would overstate the finding.** A `Node` reaching a `NormalizedTree` position is **invisible** — accepted at typecheck, failing at runtime, and the whole subject of this receipt. The reverse, a `.root` projection applied to a genuine `Node`, is **caught statically at resolve**. Both occurred here: the conflation repeatedly, and the projection twice, when bulk edits applied `.root` to two sites whose values came from `resolve` (`ResolvedTree = Node`) rather than from `normalize`. The second class was found by the compiler within one cycle and never reached a wrong answer. So the honest claim is narrow: the checker does not distinguish a wrapper from its payload *when the payload is supplied where the wrapper is declared*, and it does distinguish the opposite. A receipt that cited both as blindness would be inflating its own class. + +**What kept being wrong was the enumeration, not the fix.** The population was cut three times — production modules reachable from the chain, then witness `_test.dag` modules, then witness *support* modules — and each cut was drawn where the previous failure had landed rather than derived from a rule. All three are proxies keyed on path or role. The rule that covers all of them, and that the sweeps now implement, refers to neither: **every declaration that meets a converted value**, since meeting one is a property of the code and not of where it lives. The sweeps glob the entire tree with no path, suffix, or directory filter. + **The residue this cannot reach, stated rather than implied.** The instrument is execution, so a witness that meets a converted value and *does not execute* is broken identically and silently right now, and nothing reports it. That set was measured rather than guessed: intersecting the corpus-wide roster of witnesses with no executing consumer (190 files carrying a bare `: TestClaim =`, from the lane working that population) against the converted-API callers yields **6 files, 4 of them hand-built** — all four accounted for and executed green here. The intersection is small, but it is small *as measured*, and it is the honest bound on what "executed green" covers. ### 8.2.1 How the class first showed itself diff --git a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag index 02cb40bfab1..9a0f0255e25 100644 --- a/src/v2/test/claim/parse/parse_binding_fidelity_support.dag +++ b/src/v2/test/claim/parse/parse_binding_fidelity_support.dag @@ -105,7 +105,7 @@ fn pbf_resolved_add_module() -> Outcome { fn pbf_resolved_add_arrow() -> Optional { match pbf_resolved_add_module() { Accepted { value: tree, diagnostics: _ } => - ingested_find_arrow_in_module(root: tree.root) + ingested_find_arrow_in_module(root: tree) Rejected { diagnostics: _ } => Absent } } diff --git a/src/v2/test/compiler/pipeline/stage_bridge.dag b/src/v2/test/compiler/pipeline/stage_bridge.dag index 39b3e8e76e6..64e1bd38b06 100644 --- a/src/v2/test/compiler/pipeline/stage_bridge.dag +++ b/src/v2/test/compiler/pipeline/stage_bridge.dag @@ -299,7 +299,7 @@ fn pipeline_match_corpus_resolve_relationship_holds() -> Bool { file: ^pipeline_corpus_match_file ) { Accepted { value: tree, diagnostics: _ } => - pipeline_arrow_body_has_behavior(root: tree.root, wanted: Match) + pipeline_arrow_body_has_behavior(root: tree, wanted: Match) Rejected { diagnostics: _ } => false } } From af6a19aac2f0e45d783b55f3304c0b78f500cc31 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 12:26:46 +0000 Subject: [PATCH 29/30] BL-1: correct 8.2's direction rule -- one symptom, two defects, truthfulness of the declaration is the discriminator The asymmetry this receipt claimed (payload-into-wrapper invisible, wrapper-onto-payload caught) is refuted by its own two runs: the SAME projection on the SAME two types produced 81 runtime failures on one floor run and 1 resolve failure on the next. Verified against both. The 81 came from validate_module_roots, whose fold binder is DECLARED NormalizedTree, so root.root is statically correct and the real defect sits upstream at the call boundary -- the invisible class, whose symptom is a projection failing at runtime wherever the untruthful value is finally read. The 1 came from pbf_resolved_add_arrow, whose binder is declared Node, so the declaration itself is the mismatch and resolve refuses it. The discriminator is whether the declaration at the site is truthful: a mismatch is caught exactly where it is expressible against a declaration, and invisible where the declaration is right and only the value flowing in is wrong. Also notes that 81 occurrences are not 81 instances -- they are one symptom of fewer boundary defects, read at many sites. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- docs/plans/hollow-alias-construction-wall.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/plans/hollow-alias-construction-wall.md b/docs/plans/hollow-alias-construction-wall.md index 5979504c378..190fd6acbb0 100644 --- a/docs/plans/hollow-alias-construction-wall.md +++ b/docs/plans/hollow-alias-construction-wall.md @@ -131,7 +131,14 @@ The three compose into a **fixpoint**, which is what finally closed the populati Population closed this way, beyond the caller census above: 1 self-host intermediate, 6 witness/lens helpers on shape B, 6 return declarations on shape C, and 8 consumers surfaced by the fixpoint. -**The two directions are not symmetric, and conflating them would overstate the finding.** A `Node` reaching a `NormalizedTree` position is **invisible** — accepted at typecheck, failing at runtime, and the whole subject of this receipt. The reverse, a `.root` projection applied to a genuine `Node`, is **caught statically at resolve**. Both occurred here: the conflation repeatedly, and the projection twice, when bulk edits applied `.root` to two sites whose values came from `resolve` (`ResolvedTree = Node`) rather than from `normalize`. The second class was found by the compiler within one cycle and never reached a wrong answer. So the honest claim is narrow: the checker does not distinguish a wrapper from its payload *when the payload is supplied where the wrapper is declared*, and it does distinguish the opposite. A receipt that cited both as blindness would be inflating its own class. +**One symptom, two defects, and the discriminator is neither the direction nor the stage.** Every failure this change produced reads identically — `no field 'root' on type 'Node'` — and they split across stages: **81 occurrences at runtime** on one floor run, **1 at resolve** on the next. A first draft of this paragraph explained that by direction (payload-into-wrapper invisible, wrapper-onto-payload caught) and was refuted by the receipt's own two runs, since both failures are literally the same projection on the same two types. + +What actually varies is **whether the declaration at the projection site is truthful**, because the checker validates a projection against the *declared* type and nothing else: + +- The 81 came from `validate_module_roots`, whose fold binder is declared `NormalizedTree` and whose `root.root` is therefore statically correct. The defect was upstream at the **call boundary** — fixtures passing `FreeMonoid` into a `FreeMonoid` parameter — which is the invisible class. The projection error is a *downstream symptom* surfacing at runtime, wherever the untruthful value finally gets read. +- The 1 came from `pbf_resolved_add_arrow`, where the binder is declared `Node` (from `pbf_resolved_add_module -> Outcome`, since `ResolvedTree = Node`) and a `.root` was written on it directly. Here the declaration *is* the mismatch, so resolve refuses it immediately. + +So the sharpened claim, which both runs support: **a mismatch is caught exactly where it is expressible against a declaration, and is invisible wherever the declaration is correct and only the value flowing into it is wrong.** That is why sloppy bulk-replace edits surfaced within one cycle while the nine seams did not — and it is a stronger statement of the blindness than a direction rule, because it says *where* the checker has nothing to check rather than merely that it failed to. Counting the 81 as 81 instances of the class would also be wrong: they are the symptom of a smaller number of boundary defects, read at many sites. **What kept being wrong was the enumeration, not the fix.** The population was cut three times — production modules reachable from the chain, then witness `_test.dag` modules, then witness *support* modules — and each cut was drawn where the previous failure had landed rather than derived from a rule. All three are proxies keyed on path or role. The rule that covers all of them, and that the sweeps now implement, refers to neither: **every declaration that meets a converted value**, since meeting one is a property of the code and not of where it lives. The sweeps glob the entire tree with no path, suffix, or directory filter. From a1316ea91f50cd77331aa6d11b6e2ffc2aaf7c9e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Fri, 14 Aug 2026 19:40:44 +0000 Subject: [PATCH 30/30] Admit the add-arrow fixture through the door instead of casting past it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main has been red since BL-1 on one witness, five merges deep: arrow_body_form_eval_value_vertical_holds fails with `no field 'root' on type 'Node'`. `resolve` declares `tree: NormalizedTree` and dereferences `tree.root`. The witness passed `dag_add_arrow_with_body_node()`, which returns a raw `Node`. ABF-1 was authored against the old bare alias (`type NormalizedTree = Node`) and merged after BL-1 sealed the carrier — both sides individually correct, the hazard is the merge order. Nothing refused it at compile time because `module_skips_direct_call_arg_check` exempts every `v2.*` module from the direct-call argument TYPE judgment, and this witness is `v2.test.claim.body_lowering`. So it surfaced only where `.root` was actually dereferenced, at runtime. The repair is the smaller one: the fixture reaches `resolve` through `admit_normalized_tree`, the `Outcome` is threaded, and the `Rejected` arm FAILS the witness. No `.root` accessor at the call site, no widening of `resolve` to take a `Node`, no weakening of the carrier — reaching past the door is exactly what BL-1 made unwritable, and a fixture that cast past it would plant the wrapper-payload conflation in the witness population. Pattern follows `admission_fail_closed.dag` (`fc_admitted` / `fc_with_roots`). Green by execution: the witness returns `true`. The discriminating RED needs no construction — it is main's current CI failure on this exact function. NOT fixed here, reported rather than silently left: three sibling sites in `manual/body_lowering_normalize_add.dag` pass raw `Node`s into the same parameter (its `body_lowering_normalized_module` is declared `-> Optional`). They are latent, not absent — `manual/` is outside per-PR discovery, so nothing executes them today. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_018vBm5HzpzKuQ5jJ6bjgKsQ --- .../arrow_body_form_witness_test.dag | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag index 6253006cb80..11afe8d5c43 100644 --- a/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag +++ b/src/v2/test/claim/body_lowering/arrow_body_form_witness_test.dag @@ -1,5 +1,6 @@ module v2.test.claim.body_lowering.arrow_body_form_witness +import v2.compiler.normalized_tree { NormalizedTree, admit_normalized_tree } import v2.compiler.resolve { resolve } import v2.extdeps.languages.dag { dag_add_arrow_with_body_node, @@ -10,7 +11,7 @@ import v2.extdeps.languages.dag { dag_match_arrow_with_body_node, dag_type_atom_node } -import v2.std.diagnostic { Accepted, Rejected } +import v2.std.diagnostic { Accepted, None, Outcome, Rejected } import v2.std.node { Arrow, Atom, @@ -115,26 +116,40 @@ fn arrow_body_form_zero_arrow_fixture() -> Optional { } } +// The add-arrow fixture reaches `resolve` through the admission door, never as a raw +// `Node`. `resolve` declares `tree: NormalizedTree`, and BL-1 sealed that carrier so the +// only way in is `admit_normalized_tree`; a fixture that reached past it would plant the +// wrapper-payload conflation this witness population exists to keep out. The `Rejected` +// arm FAILS the witness rather than skipping it — an unadmittable fixture is a defect in +// the fixture, not a reason to report green. +fn abf_admitted_add_arrow_tree() -> Outcome { + admit_normalized_tree(root: dag_add_arrow_with_body_node(), diagnostics: None) +} + test fn arrow_body_form_eval_value_vertical_holds() -> Bool { let lm = dag_language_model() match arrow_body_form_zero_arrow_fixture() { Absent => false Present { value: zero_arrow } => - match resolve(tree: dag_add_arrow_with_body_node(), lm: lm) { - Accepted { value: add_arrow, diagnostics: _ } => - arrow_body_form_eval_unary_int_holds( - callee: arrow_body_form_id_arrow_fixture(), - arg_lex: "7", - expected: 7 - ) - && arrow_body_form_eval_nullary_int_holds(callee: zero_arrow, expected: 0) - && arrow_body_form_eval_binary_int_holds( - callee: add_arrow, - left_lex: "2", - right_lex: "3", - expected: 5 - ) + match abf_admitted_add_arrow_tree() { Rejected { diagnostics: _ } => false + Accepted { value: admitted_tree, diagnostics: _ } => + match resolve(tree: admitted_tree, lm: lm) { + Accepted { value: add_arrow, diagnostics: _ } => + arrow_body_form_eval_unary_int_holds( + callee: arrow_body_form_id_arrow_fixture(), + arg_lex: "7", + expected: 7 + ) + && arrow_body_form_eval_nullary_int_holds(callee: zero_arrow, expected: 0) + && arrow_body_form_eval_binary_int_holds( + callee: add_arrow, + left_lex: "2", + right_lex: "3", + expected: 5 + ) + Rejected { diagnostics: _ } => false + } } } }