From a634401d6f4fab8715f39e9377e139664f43b448 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 23 Aug 2026 07:31:47 +0000 Subject: [PATCH 1/3] Bind a qualified pattern head from the scrutinee, as the bare spelling already does Two spellings of one pattern name the same declaration, so they must bind the same node. lookup_variant_in_type forks on whether the head contains a dot: the bare branch answers from the SCRUTINEE, which carries the instantiation; the dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION. So the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off it reported "no field 'root' on type 'T'" -- measured, not inferred, on a two-function probe whose only difference is the spelling of the head. Admission is unchanged: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all. Only the bound node's source changes once admission succeeds, and the fallback arm reproduces the previous answer exactly. RECEIPTS. Discriminating RED proven in both directions on the same corpus: on the pre-fix binary the qualified arm returns false with the diagnostic above and the bare control returns true; after regen and rebuild both return true. One generated file drifted -- v1_compiler_infer_patterns.rs, this repair -- and the second pass reports first_generation_equal=true. Co-Authored-By: Claude Opus 5 (1M context) --- .../qualified_pattern_head_witness_test.dag | 29 +++++++++++++++++++ dag/test/fixture/qualpat_provider.dag | 11 +++++++ src/v1/04_patterns.dag | 20 ++++++++++++- .../stage0/src/v1_compiler_infer_patterns.rs | 12 +++++++- 4 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 dag/test/claim/qualified_pattern_head_witness_test.dag create mode 100644 dag/test/fixture/qualpat_provider.dag diff --git a/dag/test/claim/qualified_pattern_head_witness_test.dag b/dag/test/claim/qualified_pattern_head_witness_test.dag new file mode 100644 index 00000000000..a7b13fd1dd6 --- /dev/null +++ b/dag/test/claim/qualified_pattern_head_witness_test.dag @@ -0,0 +1,29 @@ +module test.claim.qualified_pattern_head_witness_test + +data qualified_pattern_head_note: String = "A QUALIFIED PATTERN HEAD MUST BIND WHAT THE BARE SPELLING BINDS. Two spellings of one pattern name the same declaration, so they must bind the same node; only the authored string differs. MECHANISM: lookup_variant_in_type forks on whether the head contains a dot. The bare branch answers from the SCRUTINEE, which carries the instantiation. The dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION -- uninstantiated -- so the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off the bound value reported no such field on type 'T'. MEASURED, minimal, both arms in one fixture: one generic coproduct, one concrete record, and two probe functions differing only in the spelling of the head. A NON-GENERIC coproduct cannot discriminate this at all -- declaration and instantiation coincide there -- which is why the fixture is generic. THE FIX PRESERVES ADMISSION: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all, so a head naming some other coproduct's variant misses exactly as before. What changes is only where the bound NODE comes from once admission succeeds -- the scrutinee when it carries that variant, the declaration otherwise, which is the pre-existing answer for a scrutinee that does not. ZERO-DRIFT: the added lookup runs only inside the dotted branch, so no bare pattern reaches it, and the fallback arm reproduces the previous answer byte-for-byte. WHY MAIN IS GREEN WITHOUT THIS: the v1 seed closure matches almost exclusively on bare heads, so an ordinary regen exercises the bare branch only -- which is exactly why this witness authors the qualified arm rather than relying on the corpus to contain one. The population that reaches this path grows with the namespace work, where every pattern head is qualified by construction. dissolve-on: never -- permanent regression control for the spelling-identity law in pattern position." + +fn w_qualified_pattern_head_binds_the_instantiation() -> Bool { + compile_dag_rust_emit_check( + "module qualpat_qual_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload \}\n\nfn qualpat_qual_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_qual_probe_mod.rs", + ["fn qualpat_qual_read", ".root"], + [] + ) +} + +fn w_bare_pattern_head_is_unchanged() -> Bool { + compile_dag_rust_emit_check( + "module qualpat_bare_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_bare_read(r: QualpatResult) -> String \{\n match r \{\n QualpatOk \{ value: v \} => v.root\n QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "src/qualpat_bare_probe_mod.rs", + ["fn qualpat_bare_read", ".root"], + [] + ) +} + +test fn qualified_pattern_head_binds_the_instantiation() -> Bool { + w_qualified_pattern_head_binds_the_instantiation() +} + +test fn bare_pattern_head_is_unchanged() -> Bool { + w_bare_pattern_head_is_unchanged() +} diff --git a/dag/test/fixture/qualpat_provider.dag b/dag/test/fixture/qualpat_provider.dag new file mode 100644 index 00000000000..a10215fb189 --- /dev/null +++ b/dag/test/fixture/qualpat_provider.dag @@ -0,0 +1,11 @@ +module test.fixture.qualpat_provider + +data qualpat_provider_fixture_note: String = "Provider fixture for test.claim.qualified_pattern_head_witness_test: one GENERIC coproduct and one concrete record to instantiate it with, which is the smallest pair that can distinguish a pattern head bound from the DECLARATION (payload type is the parameter T) from one bound from the SCRUTINEE (payload type is the argument). A non-generic coproduct cannot discriminate at all, because declaration and instantiation coincide there. Self-contained by construction: compile_dag_rust_emit_check assembles its source set by following imports, so a fixture that reaches into a THIRD module makes the harness refuse the closure and the control arm goes red for a reason that has nothing to do with the subject. The names are deliberately unlike anything in the seed, so a homonym in the bare-name registry cannot satisfy the assertions by accident." + +type QualpatPayload { + root: String +} + +type QualpatResult + = QualpatOk { value: T } + | QualpatErr { code: Int } diff --git a/src/v1/04_patterns.dag b/src/v1/04_patterns.dag index 345343333e4..66ec9e88f05 100644 --- a/src/v1/04_patterns.dag +++ b/src/v1/04_patterns.dag @@ -262,6 +262,20 @@ fn variant_not_found_result(scrut: Node, variant_name: String, module_name: Stri ) } +// A qualified pattern head must bind what the bare spelling binds. The dotted +// branch answers from the SYMBOL INDEX, which returns the coproduct's +// DECLARATION -- uninstantiated -- while the bare branch answers from the +// scrutinee, which carries the instantiation. So a bare `Ok { value: r }` bound +// r to the scrutinee's type ARGUMENT and the qualified spelling of the same +// pattern bound it to the declaration's type PARAMETER, so every field read off +// r reported no such field on type 'T'. Measured on a two-function probe whose +// only difference is the spelling of the head. +// +// The index lookup still runs first and still decides ADMISSION: a head naming +// a variant of some other coproduct misses there exactly as before. What changes +// is where the bound NODE comes from once admission succeeds -- the scrutinee +// when it carries that variant, the declaration otherwise, which is the +// pre-existing answer for a scrutinee that does not. fn lookup_variant_in_type(scrut: PatternSubject, variant_name: String, module_name: String, env: TypeEnv, field_binding_count: Int) -> NodeLookupResult { match scrut { PatternLookupBlocked => @@ -275,7 +289,11 @@ fn lookup_variant_in_type(scrut: PatternSubject, variant_name: String, module_na Present { value: resolved } => if (resolved.connective == Disj) { match find_child_named(n: resolved, name: qualified_last_segment(name: variant_name), source_indices: source_indices) { - Present { value: variant_child } => node_lookup_resolved(node: variant_child) + Present { value: variant_child } => + match find_child_named(n: expand_scrut_type_for_variant_lookup(scrut_node: scrut_node, env: env), name: qualified_last_segment(name: variant_name), source_indices: source_indices) { + Present { value: scrut_child } => node_lookup_resolved(node: scrut_child) + Absent => node_lookup_resolved(node: variant_child) + } Absent => variant_not_found_result(scrut: scrut_node, variant_name: variant_name, module_name: module_name, source_indices: source_indices) } diff --git a/src/v1/stage0/src/v1_compiler_infer_patterns.rs b/src/v1/stage0/src/v1_compiler_infer_patterns.rs index 53f09373f1f..4c46ac8baf0 100644 --- a/src/v1/stage0/src/v1_compiler_infer_patterns.rs +++ b/src/v1/stage0/src/v1_compiler_infer_patterns.rs @@ -582,7 +582,17 @@ pub fn lookup_variant_in_type( qualified_last_segment(variant_name.clone()), source_indices.clone(), ) { - Some(variant_child) => node_lookup_resolved(variant_child.clone()), + Some(variant_child) => match find_child_named( + expand_scrut_type_for_variant_lookup( + scrut_node.clone(), + env.clone(), + ), + qualified_last_segment(variant_name.clone()), + source_indices.clone(), + ) { + Some(scrut_child) => node_lookup_resolved(scrut_child.clone()), + None => node_lookup_resolved(variant_child.clone()), + }, None => variant_not_found_result( scrut_node.clone(), variant_name.clone(), From c18ea7fa336b867b9b567766e2887296d77875e4 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 23 Aug 2026 09:12:26 +0000 Subject: [PATCH 2/3] De-confound the witness pair: the arms differed in imports, not only in head spelling The floor reported the qualified arm at 58800ms CPU against a 5000ms budget with 1.21GB RSS growth while the bare arm passed under budget, and I read that as a cost of the qualified pattern head. It is not yet evidence of that. The qualified probe imported two names and the bare probe imported four, so the arms could differ in source-closure construction, import binding, symbol-index use and cache temperature as well as in the spelling under test. The pair was a controlled experiment for the SEMANTIC discriminator and not for the cost one -- a control must name its adversary, and cost was an adversary these arms never excluded. Both probes now import all four names, leaving the two pattern heads as the only difference. The semantic RED is unchanged and was re-proven in both directions after the edit, against binaries built from the pre-fix and post-fix mirrors: pre-fix qualified=false bare=true post-fix qualified=true bare=true No generated file changes; the seed is untouched by this commit. Co-Authored-By: Claude Opus 5 (1M context) --- dag/test/claim/qualified_pattern_head_witness_test.dag | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dag/test/claim/qualified_pattern_head_witness_test.dag b/dag/test/claim/qualified_pattern_head_witness_test.dag index a7b13fd1dd6..9552bd46680 100644 --- a/dag/test/claim/qualified_pattern_head_witness_test.dag +++ b/dag/test/claim/qualified_pattern_head_witness_test.dag @@ -1,10 +1,10 @@ module test.claim.qualified_pattern_head_witness_test -data qualified_pattern_head_note: String = "A QUALIFIED PATTERN HEAD MUST BIND WHAT THE BARE SPELLING BINDS. Two spellings of one pattern name the same declaration, so they must bind the same node; only the authored string differs. MECHANISM: lookup_variant_in_type forks on whether the head contains a dot. The bare branch answers from the SCRUTINEE, which carries the instantiation. The dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION -- uninstantiated -- so the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off the bound value reported no such field on type 'T'. MEASURED, minimal, both arms in one fixture: one generic coproduct, one concrete record, and two probe functions differing only in the spelling of the head. A NON-GENERIC coproduct cannot discriminate this at all -- declaration and instantiation coincide there -- which is why the fixture is generic. THE FIX PRESERVES ADMISSION: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all, so a head naming some other coproduct's variant misses exactly as before. What changes is only where the bound NODE comes from once admission succeeds -- the scrutinee when it carries that variant, the declaration otherwise, which is the pre-existing answer for a scrutinee that does not. ZERO-DRIFT: the added lookup runs only inside the dotted branch, so no bare pattern reaches it, and the fallback arm reproduces the previous answer byte-for-byte. WHY MAIN IS GREEN WITHOUT THIS: the v1 seed closure matches almost exclusively on bare heads, so an ordinary regen exercises the bare branch only -- which is exactly why this witness authors the qualified arm rather than relying on the corpus to contain one. The population that reaches this path grows with the namespace work, where every pattern head is qualified by construction. dissolve-on: never -- permanent regression control for the spelling-identity law in pattern position." +data qualified_pattern_head_note: String = "A QUALIFIED PATTERN HEAD MUST BIND WHAT THE BARE SPELLING BINDS. Two spellings of one pattern name the same declaration, so they must bind the same node; only the authored string differs. MECHANISM: lookup_variant_in_type forks on whether the head contains a dot. The bare branch answers from the SCRUTINEE, which carries the instantiation. The dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION -- uninstantiated -- so the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off the bound value reported no such field on type 'T'. MEASURED, minimal, both arms in one fixture: one generic coproduct, one concrete record, and two probe functions differing only in the spelling of the head. A NON-GENERIC coproduct cannot discriminate this at all -- declaration and instantiation coincide there -- which is why the fixture is generic. THE FIX PRESERVES ADMISSION: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all, so a head naming some other coproduct's variant misses exactly as before. What changes is only where the bound NODE comes from once admission succeeds -- the scrutinee when it carries that variant, the declaration otherwise, which is the pre-existing answer for a scrutinee that does not. ZERO-DRIFT: the added lookup runs only inside the dotted branch, so no bare pattern reaches it, and the fallback arm reproduces the previous answer byte-for-byte. WHY MAIN IS GREEN WITHOUT THIS: the v1 seed closure matches almost exclusively on bare heads, so an ordinary regen exercises the bare branch only -- which is exactly why this witness authors the qualified arm rather than relying on the corpus to contain one. The population that reaches this path grows with the namespace work, where every pattern head is qualified by construction. dissolve-on: never -- permanent regression control for the spelling-identity law in pattern position. IDENTICAL IMPORT LISTS ARE LOAD-BEARING AND WERE NOT AT FIRST. The first revision imported only QualpatResult and QualpatPayload into the qualified probe and all four names into the bare one, because the qualified probe does not NEED the variant names in scope. That made the two arms differ in the import set as well as in the head spelling, so they could differ in source-closure construction, import binding, symbol-index use and cache temperature -- and the floor duly reported the qualified arm at 58800ms CPU against a 5000ms budget with 1.21GB RSS growth while the bare arm passed. That measurement is CONFOUNDED: the arms were a controlled experiment for the SEMANTIC discriminator and not for the COST one, which is the control-independence failure this repository already names -- a control must name its adversary, and cost was an adversary these arms never excluded. Both probes now import all four names, so the only surviving difference is the spelling of the two pattern heads. The semantic RED is unaffected and was re-proven in both directions after the change: on the pre-fix binary the qualified arm returns false and the bare arm returns true; on the post-fix binary both return true. Whether the cost delta survives the corrected control is a question for the hermetic floor, which is the only instrument that can see it -- measured wet, both arms cost 98.7s locally and the delta is simply absent, because the wet corpus load dominates and amortizes whatever the floor pays per claim." fn w_qualified_pattern_head_binds_the_instantiation() -> Bool { compile_dag_rust_emit_check( - "module qualpat_qual_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload \}\n\nfn qualpat_qual_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", + "module qualpat_qual_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_qual_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", "src/qualpat_qual_probe_mod.rs", ["fn qualpat_qual_read", ".root"], [] From d0cad887974b54c7c398a77f33146e04345adaac Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 23 Aug 2026 11:05:44 +0000 Subject: [PATCH 3/3] Move the witness to the census grain: it was measuring emission for a claim about resolution The subject is a BINDING fact and a binding fact is decided at typecheck. The witness asked it through compile_dag_rust_emit_check, which parses, resolves, typechecks, EMITS RUST, and then -- per the compiler's own compile_dag_diagnostic_census_row_note -- collapses the whole result to a Bool, discarding which judgment fired. The enrolled claim duly cost 58579ms CPU against a 5000ms budget with 1.21GB RSS growth while its bare control passed under budget. compile_dag_diagnostic_census reports the causal judgment directly as typed rows. That makes this witness narrower in subject, MORE discriminating -- it names the diagnostic instead of collapsing to false -- and cheaper for a principled reason rather than a convenient one: emission is downstream of the fact being tested, so removing it removes work, not evidence. CensusNotRunnable is a failure carrying its own cause and is never the expected red. Could-not-measure and measured-nothing are different states and only one of them is evidence. MEASURED, one fixture, both probe sources carrying identical imports so the only difference is the two pattern heads: pre-fix qualified OBSERVED[1] InternalError | no field 'root' on type 'T' | blocking=true | n=1 (enrolled fn returns false) pre-fix bare OBSERVED[0] post-fix qualified OBSERVED[0] (enrolled fn returns true) post-fix bare OBSERVED[0] (enrolled fn returns true) THE COST OBSERVATION IS NOT REPAIRED BY THIS CHANGE AND IS NOT CLAIMED TO BE. It is carried forward in the pull request body with its two ruled-out causes, its unattributed owners, and its next discriminator. Co-Authored-By: Claude Opus 5 (1M context) --- .../qualified_pattern_head_witness_test.dag | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/dag/test/claim/qualified_pattern_head_witness_test.dag b/dag/test/claim/qualified_pattern_head_witness_test.dag index 9552bd46680..a8fb8d708b0 100644 --- a/dag/test/claim/qualified_pattern_head_witness_test.dag +++ b/dag/test/claim/qualified_pattern_head_witness_test.dag @@ -1,29 +1,24 @@ module test.claim.qualified_pattern_head_witness_test -data qualified_pattern_head_note: String = "A QUALIFIED PATTERN HEAD MUST BIND WHAT THE BARE SPELLING BINDS. Two spellings of one pattern name the same declaration, so they must bind the same node; only the authored string differs. MECHANISM: lookup_variant_in_type forks on whether the head contains a dot. The bare branch answers from the SCRUTINEE, which carries the instantiation. The dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION -- uninstantiated -- so the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT, and every field read off the bound value reported no such field on type 'T'. MEASURED, minimal, both arms in one fixture: one generic coproduct, one concrete record, and two probe functions differing only in the spelling of the head. A NON-GENERIC coproduct cannot discriminate this at all -- declaration and instantiation coincide there -- which is why the fixture is generic. THE FIX PRESERVES ADMISSION: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all, so a head naming some other coproduct's variant misses exactly as before. What changes is only where the bound NODE comes from once admission succeeds -- the scrutinee when it carries that variant, the declaration otherwise, which is the pre-existing answer for a scrutinee that does not. ZERO-DRIFT: the added lookup runs only inside the dotted branch, so no bare pattern reaches it, and the fallback arm reproduces the previous answer byte-for-byte. WHY MAIN IS GREEN WITHOUT THIS: the v1 seed closure matches almost exclusively on bare heads, so an ordinary regen exercises the bare branch only -- which is exactly why this witness authors the qualified arm rather than relying on the corpus to contain one. The population that reaches this path grows with the namespace work, where every pattern head is qualified by construction. dissolve-on: never -- permanent regression control for the spelling-identity law in pattern position. IDENTICAL IMPORT LISTS ARE LOAD-BEARING AND WERE NOT AT FIRST. The first revision imported only QualpatResult and QualpatPayload into the qualified probe and all four names into the bare one, because the qualified probe does not NEED the variant names in scope. That made the two arms differ in the import set as well as in the head spelling, so they could differ in source-closure construction, import binding, symbol-index use and cache temperature -- and the floor duly reported the qualified arm at 58800ms CPU against a 5000ms budget with 1.21GB RSS growth while the bare arm passed. That measurement is CONFOUNDED: the arms were a controlled experiment for the SEMANTIC discriminator and not for the COST one, which is the control-independence failure this repository already names -- a control must name its adversary, and cost was an adversary these arms never excluded. Both probes now import all four names, so the only surviving difference is the spelling of the two pattern heads. The semantic RED is unaffected and was re-proven in both directions after the change: on the pre-fix binary the qualified arm returns false and the bare arm returns true; on the post-fix binary both return true. Whether the cost delta survives the corrected control is a question for the hermetic floor, which is the only instrument that can see it -- measured wet, both arms cost 98.7s locally and the delta is simply absent, because the wet corpus load dominates and amortizes whatever the floor pays per claim." +import gunbc.compile_diagnostic_census { CompileDiagnosticCensus, CensusObserved, CensusNotRunnable, census_blocking_rows } -fn w_qualified_pattern_head_binds_the_instantiation() -> Bool { - compile_dag_rust_emit_check( - "module qualpat_qual_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_qual_read(r: test.fixture.qualpat_provider.QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", - "src/qualpat_qual_probe_mod.rs", - ["fn qualpat_qual_read", ".root"], - [] - ) -} +data qualified_pattern_head_note: String = "A QUALIFIED PATTERN HEAD MUST BIND WHAT THE BARE SPELLING BINDS. Two spellings of one pattern name the same declaration, so they must bind the same node; only the authored string differs. MECHANISM: lookup_variant_in_type forks on whether the head contains a dot. The bare branch answers from the SCRUTINEE, which carries the instantiation. The dotted branch answered from the SYMBOL INDEX, which returns the coproduct's DECLARATION -- uninstantiated -- so the payload bound to the declaration's type PARAMETER instead of the scrutinee's type ARGUMENT. THE FIX PRESERVES ADMISSION: the index lookup still runs first and still decides whether the head names a variant of this coproduct at all; only the bound node's source changes once admission succeeds, and the fallback arm reproduces the previous answer. WHY THE CENSUS GRAIN AND NOT AN EMIT CHECK, which is the part worth reading. The subject here is a BINDING fact, and a binding fact is decided at typecheck. The first revision of this witness asked it through compile_dag_rust_emit_check, which parses, resolves, typechecks, EMITS RUST, and then -- per compile_dag_diagnostic_census_row_note -- collapses the whole result to a Bool, discarding WHICH judgment fired. So it measured emission for a proposition about resolution, and the enrolled claim duly cost 58579ms CPU against a 5000ms budget with 1.21GB RSS growth while its bare control passed. compile_dag_diagnostic_census reports the causal judgment directly as typed rows, which makes this witness narrower in subject, MORE discriminating (it names the diagnostic instead of collapsing to false), and cheaper for a principled reason rather than a convenient one -- emission is downstream of the fact being tested, so removing it removes work, not evidence. THE COST OBSERVATION IS NOT REPAIRED BY THIS CHANGE AND IS NOT CLAIMED TO BE; it is carried forward as a separate finding in the pull request that lands this, with its ruled-out causes and its next discriminator. MEASURED, all four cells, one fixture, both probe sources carrying IDENTICAL imports so the only difference is the two pattern heads -- pre-fix binary: qualified census OBSERVED[1] InternalError | no field 'root' on type 'T' | blocking=true | n=1, bare census OBSERVED[0]; post-fix binary: qualified OBSERVED[0], bare OBSERVED[0]. The earlier revision's arms differed in their import lists as well as in the head spelling, which made them a controlled experiment for the semantic discriminator and not for anything else; that is fixed here and was fixed before this grain change. A NON-GENERIC coproduct cannot discriminate this at all -- declaration and instantiation coincide there -- which is why the fixture is generic. CensusNotRunnable is a FAILURE with its own cause, never the expected red: could-not-measure and measured-nothing are different states and only one of them is evidence. dissolve-on: never -- permanent regression control for the spelling-identity law in pattern position." + +data qualpat_qualified_probe_source: String = "module qualpat_qual_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_qual_read(r: QualpatResult) -> String \{\n match r \{\n test.fixture.qualpat_provider.QualpatOk \{ value: v \} => v.root\n test.fixture.qualpat_provider.QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n" + +data qualpat_bare_probe_source: String = "module qualpat_bare_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_bare_read(r: QualpatResult) -> String \{\n match r \{\n QualpatOk \{ value: v \} => v.root\n QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n" -fn w_bare_pattern_head_is_unchanged() -> Bool { - compile_dag_rust_emit_check( - "module qualpat_bare_probe_mod\n\nimport test.fixture.qualpat_provider \{ QualpatResult, QualpatPayload, QualpatOk, QualpatErr \}\n\nfn qualpat_bare_read(r: QualpatResult) -> String \{\n match r \{\n QualpatOk \{ value: v \} => v.root\n QualpatErr \{ code: _ \} => \"\"\n \}\n\}\n", - "src/qualpat_bare_probe_mod.rs", - ["fn qualpat_bare_read", ".root"], - [] - ) +fn qualpat_probe_has_no_blocking_diagnostic(source: String) -> Bool { + match compile_dag_diagnostic_census(source) { + CensusObserved { rows: rows } => count(census_blocking_rows(rows: rows)) == 0 + CensusNotRunnable { cause: _ } => false + } } test fn qualified_pattern_head_binds_the_instantiation() -> Bool { - w_qualified_pattern_head_binds_the_instantiation() + qualpat_probe_has_no_blocking_diagnostic(source: qualpat_qualified_probe_source) } test fn bare_pattern_head_is_unchanged() -> Bool { - w_bare_pattern_head_is_unchanged() + qualpat_probe_has_no_blocking_diagnostic(source: qualpat_bare_probe_source) }