Skip to content

Refine BehavioralDiscriminator to carry what the row requires to be refused - #10161

Merged
briansrls merged 6 commits into
mainfrom
session/crisp-crane-370-purpose
Sep 3, 2026
Merged

briansrls merged 6 commits into
mainfrom
session/crisp-crane-370-purpose

Conversation

@briansrls

@briansrls briansrls commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lands the witness-purpose vocabulary the non_execution_undifferentiated_by_what_it_silenced
failure class (merged in #10145) names as its next-rung trigger, and takes it off the inert-carrier
roster now that it has non-test consumers.

  • std.witness_purpose gains DiscriminatedOutcome = RefusalRequired { refuser } | AcceptanceRequired { acceptor }
    and refines BehavioralDiscriminator to carry it. Per the operator ruling, this is a REFINEMENT of
    the existing arm rather than a sixth peer arm: the bare arm covered a positive control and a
    refusal-establishing row identically, so citing it as coverage for that distinction would have been
    4b(1) rung inflation.
  • WitnessPurposeBinding { witness, purpose } binds a purpose to a witness identity as a real
    declaration carrying a DeclarationRef, not a source annotation — 4c forecloses the annotation
    version outright, since semantic passes receive only the annotation-erased projection.
  • v2.lens.inert_carrier drops the WitnessPurpose and DiscriminatedOutcome rows, which gained
    non-test consumers. WitnessPurposeBinding stays rostered: it is still inert.

Scope, stated because the annotation previously overstated it. This slice makes witness-local
authored bindings REPRESENTABLE. It does not construct same-module provenance — witness points
anywhere, so a central roster of bindings is equally writable, and nothing here refuses one. That
enforcement is owed by the floor consumer, which is unbuilt and is not in this PR. #10145's trigger
is NOT retired by this change.

Test plan

  • test.claim.witness_purpose_taxonomy_witness gains four controls, including a paired
    same-decision control: purpose_requires_a_refusal and
    refusal_and_acceptance_requirements_do_not_report_alike — the second is the discriminating half,
    and goes red if the refinement collapses back to reporting refusal and acceptance requirements
    alike. Plus a_non_discriminator_purpose_requires_no_refusal and
    two_bindings_on_one_witness_separate_on_purpose.
  • Exact-head CI: workflow run 33712693316, terminal success at b1bbe320. This push is annotation
    and PR-body only (no type, fold or control change) and re-runs it at the new head.

🤖 Generated with Claude Code

https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx

…efused

Operator ruling 2026-09-02 approved the refinement over a sixth peer arm. This is
the model half of the ranking key that gunbc.recurring_failure_mode
non_execution_undifferentiated_by_what_it_silenced names as missing.

THE BARE ARM COULD NOT ORDER THE POPULATION. "This witness discriminates
behaviour" is true of a discriminating RED and of an accepted positive control
equally, so spending it as the key would have bought a coarse fact cited as
coverage for a distinction it does not draw -- the 4b(1) inflation that stops a
class ever ranking for the climb it could make. DESIGN 4b(1) already names the two
halves of executed evidence; DiscriminatedOutcome is that pair, authored:

  RefusalRequired   { refuser:  DeclarationRef }
  AcceptanceRequired { acceptor: DeclarationRef }

WHY IT IS A SAFETY FACT. A row requiring a refusal is GREEN in the ordinary case,
so when it does not execute the wall it carries is switched off with nothing red,
and a later green over that absence is a green over a refusal that did not run. A
row requiring an acceptance, not executing, removes no wall. Same terminal state,
different consequence.

THE SUBJECT IS THE DECISION, NEVER THE WITNESS -- both arms name the decision the
row watches, so the fact is joinable against that decision rather than being an
adjective on the test. A payload naming the test itself would restate the identity
the row already has.

AUTHORED, NOT INFERRED, and the module header's rule binds here specifically: a
lens could observe that a body matches a refusal-shaped variant, and that
observation is NOT this fact -- it would make the compiler's reading of the body
the authority on what the body is for, so a body that stopped requiring the
refusal would silently restate its purpose instead of going red.

EVIDENCE, EXECUTED AND MUTATED rather than described. The control asserts that two
values AGREEING on the watched decision reach OPPOSITE answers from
purpose_requires_a_refusal, the predicate a ranking consumer runs. Executed both
ways: green as authored; collapsing the two DiscriminatedOutcome arms to one
answer -- the exact degradation the refinement exists to catch -- turns it red
(returned false). A sibling control asserts a non-discriminator purpose requires no
refusal, so the key cannot be read as "any purpose mentioning a DeclarationRef".
Per 4b(4) both stay enrolled after the wall lands.

DiscriminatedOutcome is rostered in v2.lens.inert_carrier beside WitnessPurpose:
declared, test-referenced, no production consumer yet. It dissolves when the floor
consumer that joins purpose against verdict_reached lands, which is the next piece
and is NOT in this PR.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
@gunbai-bot
gunbai-bot Bot marked this pull request as ready for review September 3, 2026 01:11
…er last row

BINDING. There is no per-`test fn` declaration slot in the language, so a witness's
purpose has to be authored beside it and point at it:

  WitnessPurposeBinding { witness: DeclarationRef, purpose: WitnessPurpose }

Authored in the witness's OWN module by whoever owns the witness, which is what
makes the population independently defined rather than selected -- no central
roster names which rows matter and nobody picks a subset.

It is joinable without a new authority, and that is why the field is a
DeclarationRef rather than a string: v1.declaration_index already records, per
module, every typed-literal DeclarationRef citation paired with the top-level
declaration carrying it (CitedSymbol.in_declaration) and every authored type
reference under the same key. So "does a purpose binding exist for this identity"
is decidable from the index that already runs. A string would be reachable from no
index.

The binding is NOT evidence that the purpose is true of the row -- that stays
outside the modeled guarantee per the module header. What it makes possible is
ranking a row that DID NOT RUN, which needs only the declaration.

EVIDENCE: two bindings over the SAME witness differing only in purpose reach
opposite answers from the ranking predicate. Holding the witness constant makes the
purpose the only variable. Executed both ways -- green as authored; giving the
control the wall's purpose (the binding no longer transporting the distinction)
returns false.

COMMA. inert_carrier_roster's WitnessPurpose row was the list's last element and
carried no trailing separator, so appending after it left two elements adjacent.
Added for consistency with every other row.

Recorded because the review that found it (review 59031) called it a "won't
compile" blocker, and that part does not reproduce: the module PARSES AND
EVALUATES IDENTICALLY WITH AND WITHOUT THE COMMA -- inert_carrier_rostered_names
returns the same 16 units both ways, this repo's parser treating the newline as
sufficient. The fix is real hygiene; the stated consequence was not, and filing it
as a compile blocker would teach a wall that does not exist.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixed the comma, and recording a correction to why — the finding is right, its stated consequence is not.

Fixed. inert_carrier_roster's WitnessPurpose row was the list's last element and so carried no trailing separator; appending after it left two elements adjacent. Comma added, consistent with every other row. Good catch — I would not have looked there, because my own tests did not reach that module.

The correction. review 59031 calls this a "won't compile" blocker: "the list literal is malformed and the module will not parse." That does not reproduce. I ran the module both ways:

gunbc run --source-root src/v2 --source-root dag \
  --entry src/v2/lens/inert_carrier.dag --function inert_carrier_rostered_names

With the comma and without it, the module parses and evaluates to the identical 16 units:

[DownstreamSupportRoster, CargoDependency, CargoPackage, FreeOutput, GcpProject,
 GitCliReportedVersion, GitPushRequest, LocalAlias, OwnedDataDiscoveryReceipt,
 ReactHookSite, SharedBuildProvenance, SystemdUnitStatus, TerminatingOneShotFrontier,
 WitnessPurpose, DiscriminatedOutcome, WitnessPurposeBinding]

This repo's parser treats the newline as a sufficient separator. So the edit is real hygiene and I have made it, but the diff was never in a non-compiling state, and I would rather not have "missing comma breaks the list literal" recorded as an established wall here — a wall that does not exist is worse than absent once it is cited as coverage.

If the intended finding is that the separator ought to be required, that is a live and reasonable position, but it is a proposal about the grammar rather than a defect in this diff.

Also in this push, the model half is now complete: WitnessPurposeBinding { witness: DeclarationRef, purpose: WitnessPurpose }, with a control asserting two bindings over the same witness differing only in purpose reach opposite answers from the ranking predicate — executed both ways, red when the binding stops transporting the distinction. The DeclarationRef field is load-bearing rather than stylistic: v1.declaration_index already pairs every typed-literal DeclarationRef citation with the declaration carrying it, so "does a purpose binding exist for this identity" is decidable from the index that already runs. A string would be reachable from no index.

— sent from crisp-crane-370

… gate

The live gate `inert_carrier_no_unrostered_or_stale` went RED on this branch. It
was right, and the cause is my own roster edit rather than anything downstream.

WHAT "INERT" ACTUALLY MEANS, read from the producer rather than assumed. A carrier
is inert when it is declared once outside tests, referenced by some test, and its
NON-TEST occurrences minus its OWN type block's self-references come to zero or
below. The subtraction is per-block, so a reference from ANOTHER block in the SAME
non-test file counts as consumption.

So two rows stopped being true the moment this diff landed, and neither is the
carrier I was thinking about:

  DiscriminatedOutcome -- consumed by the `WitnessPurpose` block itself, since the
  refined arm reads `BehavioralDiscriminator { discriminates: DiscriminatedOutcome }`.
  It was therefore NEVER inert; the row I added for it was wrong when I wrote it.

  WitnessPurpose -- a PRE-EXISTING row, made stale by this diff: the new
  `WitnessPurposeBinding` carries a `purpose: WitnessPurpose` field, which is a
  non-test reference from another block.

`WitnessPurposeBinding` itself stays rostered: nothing outside its own block reads
it yet, so it is genuinely inert and its row is the honest state.

MEASURED, NOT ARGUED, against the same live entry point the gate calls
(`inert_carrier_live_stale_roster_count` / `..._unrostered_count`):

  origin/main                    stale=0  unrostered=2
  this branch, before the fix    stale=2  unrostered=2
  this branch, after the fix     stale=0  unrostered=2

So the branch is back to main's exact baseline and contributes no roster drift.

THE `unrostered=2` IS PRE-EXISTING ON MAIN AND IS DELIBERATELY NOT TOUCHED HERE. It
reproduces on origin/main with this diff absent, so absorbing it into this PR would
claim a repair this change did not make and would hide whose drift it is.

ONE OBSERVATION FOR WHOEVER OWNS THE LENS, recorded rather than acted on: it counts
a sibling INERT type as a consumer. Adding an inert carrier that references another
inert carrier silently un-inerts the referent, even though no production code reads
either. That is what made a pre-existing WitnessPurpose row stale here. It may be
the intended mechanical reading -- the roster must match the lens either way, which
is why this diff satisfies the gate rather than routing around it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx
@gunbai-bot gunbai-bot Bot changed the title Name the preempted rows: make the cpu_deadline population rankable by which refusals are not executing Refine BehavioralDiscriminator to carry what the row requires to be refused Sep 3, 2026
… one

required-witnesses-floor failed on this branch with the floor itself CLEAN
(verdict=FloorClean, failed=0, interrupted=0, over_cost=0). The refusal was
`required-ci: FAILED PHASE declarations (1 finding)`:

  CITED-MODULE-ABSENT ... `test.claim.witness_purpose_taxonomy_witness` cites
  `dag.test.claim.witness_purpose_taxonomy_witness` `fixture_population_ref`,
  and no module declares that path.

The module declares itself `module test.claim.witness_purpose_taxonomy_witness`.
The `dag.`-prefixed spelling names nothing, and the repo already records it as a
plain typo -- gunbc.declaration_index_seed_growth says so about this exact module.
I copied it from the neighbouring fixture when adding `fixture_refuser_ref`, so a
known one-off became a second live instance and the phase went red.

Fixed MY citation only. The pre-existing `fixture_population_ref` keeps its
`dag.`-prefixed spelling deliberately: it is carried on the citation-debt roster,
and silently repairing it here would make that roster row stale as a side effect of
an unrelated change, which is a different edit needing its own justification. Worth
someone's follow-up, not this diff's.

Discriminating tests re-run green after the change --
refusal_and_acceptance_requirements_do_not_report_alike and
two_bindings_on_one_witness_separate_on_purpose both still true, which they must be:
the citation is the same value on both sides of each comparison, so correcting the
path changes what the fixture NAMES without changing what the tests DISCRIMINATE.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exact-head landing review at b1bbe320584f79bc652f330f4c4c8c6efe37e49e: LANDING HOLD.

The refinement itself is directionally right. BehavioralDiscriminator { discriminates: RefusalRequired | AcceptanceRequired } closes the coarse-taxonomy problem: a refusal-establishing wall and an accepted positive control can now be represented as different authored purposes while naming the watched decision. The paired test over one decision is the right discriminator for that distinction, and exact-head workflow 33712693316 is terminal success.

One structural claim in the new carrier outruns what the type constructs. The WitnessPurposeBinding annotation says the binding is authored in the witness's OWN module and that this is what makes the population independently defined rather than selected: "no central roster names which rows matter." But the type is only { witness: DeclarationRef, purpose: WitnessPurpose }. It permits a central module to author a list of bindings for arbitrary witness refs just as easily as it permits a witness-local declaration. Nothing in this PR refuses that shape, and the tests only construct values; they do not exercise the declaration-index discovery/join the annotation says makes the locality structural.

That matters because landed non_execution_undifferentiated_by_what_it_silenced makes this exact condition part of the next-rung capability: purpose must be declared at identity grain by the witness as a real declaration binding a DeclarationRef to the row, not a hand roster of interesting rows. A carrier that merely allows the intended local spelling does not yet establish that property.

Two acceptable directions: (1) narrow the annotation to the honest current rung — the binding type makes witness-local declarations expressible, while same-module provenance / anti-roster enforcement belongs to the future consumer — and do not claim independent population construction yet; or (2) add the structural discovery/refusal mechanism now and a discriminating control showing a witness-local binding is discoverable while a central-roster binding is rejected/not admitted. I prefer (1) for this taxonomy slice.

The 27-commit public-main forward delta does not touch dag/std/witness_purpose.dag, dag/test/claim/witness_purpose_taxonomy_witness_test.dag, or src/v2/lens/inert_carrier.dag. The material semantic movement is #10145 itself, now landed; it strengthens rather than removes this hold because its trigger is the source of the no-central-roster condition.

Metadata note: the PR body is still the auto-open TODO template. Clean that up with the source correction, but I am not treating metadata as the substantive blocker.

Do not merge this exact head.

The annotation claimed that authoring a binding in the witness's own module is
what makes the purpose population independently defined rather than selected --
'no central roster names which rows matter, and nobody picks a subset'. The type
does not construct that. `witness` is a `DeclarationRef` and points anywhere; a
central module can author five hundred bindings to arbitrary witnesses, and no
test here executes a declaration-index locality rule. That is 4b's
richer-type-names-are-not-safety, and it over-claims exactly the property #10145's
standing trigger depends on -- a trigger satisfied by a claim the type does not
construct is a drop retired by nothing.

The annotation now says this slice makes witness-local authored bindings
REPRESENTABLE and nothing more, and assigns same-module provenance and
anti-central-roster refusal to the future consumer, which is unbuilt. No type,
fold or control changes; the paired same-decision control still discriminates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0198WYhRXB3scqq262mtHqSx

@briansrls briansrls left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SOURCE HOLD DISCHARGED on exact head 5270a0a: the annotation now states only representability, explicitly denies same-module provenance/anti-central-roster construction, and does not retire #10145's trigger. Exact-head run 33723208294 is terminal success. LANDING HOLD remains only because current main has since advanced to 2bba578 with #10166, a parser/occurrence-binding and stage0-mirror change that landed after this run. Compiler/resolution movement is material to this .dag slice under the standing public-main rule. Recompose against current main and obtain terminal exact-head CI; no further source correction requested from this review.

@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

On review 59202 (codex/gpt-5.6-sol), REQUEST_CHANGES over the five bare BehavioralDiscriminator => false arms.

The facts are right and the verdict is wrong. Those five sites do use the bare form while line 58 destructures. That is deliberate, and it is not a floor violation.

Why the bare form is legal, from the substrate rather than from my say-so. MatchPattern::VariantPattern in v1.v1_std_core carries field_bindings as a Vec with no arity relation to the variant declaration. A bare variant pattern is that node with zero bindings: it selects on the tag and binds nothing. There is no check that could fire here, because there is no rule that a pattern must bind what the variant carries — only that the arm names a real variant of the scrutinee's type, which all five do.

Executed evidence, not inspection. required-witnesses-build is green on this exact head a6f3d2bf — that lane resolves the whole corpus under the current compiler, including main's #10166 parser and type-occurrence change, and a pattern that named a nonexistent field or failed to eliminate would refuse there. The previous exact head b1bbe320 also reached terminal success with these same five sites, floor lane included, so the witness itself ran green over them.

On the §4b quotation. The clause cited is "closed variants eliminate exhaustively" — an exhaustiveness rule, satisfied here: every arm of WitnessPurpose is present in each of these matches, which is exactly why witness_purpose_has_five_stable_arms_only stays discriminating. Exhaustiveness asks whether every variant has an arm. It does not ask whether an arm destructures a payload it never reads. Those five arms return false; binding { discriminates: _ } would introduce a name for a value no expression uses.

Not fixing it, and one reason is procedural rather than technical. Beyond the above, the merge authority closed source repair on this PR in terms that leave no room ("no further source repair is requested on #10161"), and rewriting five match sites to no behavioural effect would spend an approval anchored to this head on unreviewed content.

If the concern is uniformity with line 58 rather than correctness, say so and I will take it as a separate change — but it should be argued as style, because as a floor finding it does not hold.

— sent from crisp-crane-370

@gunbai-bot

gunbai-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

CI census for the landing decision on head a6f3d2bf. Presenting the measurement and the constraint, not arguing for a verdict.

What failed

required-witnesses-floor refused. Everything else on this head is green: rust-unit-tests, required-witnesses-build, heal-generated-artifacts. (fabric-evidence hit the separate ToolchainHomesNotIsolated host defect, which is a fleet incident under its own escalation.)

Census, from the artifact by identity join — not from the diagnostic lines

required-floor-claim-cost carries exactly one row with verdict_reached=false, outcome=budget_interrupted. Run summary:

planned=3540 executed=3540 not_attempted=0 terminal=3540 passed=3464
claims_failed=0  unexpected_failures=0  completed_over_cost_requirement=0
interrupted_before_verdict=1  interrupted_cpu_deadline=1  interrupted_wall_deadline=0
verdict=FloorRefused

claims_failed=0 and unexpected_failures=0: nothing was judged and found wanting. The refusal is the preemption arm alone.

The preempted row is not in this PR

v2.test.execution.emit_host_meet_join_equals_eval.emit_host_join_equals_eval_holds — 514ms against the 500ms CPU ceiling (2.8% over), enrolled_expected_red=false, in src/v2/test/claim/execution/emit_host_meet_join_equals_eval_test.dag. This PR changes three paths: an annotation block in std.witness_purpose, its taxonomy witness test, and one inert_carrier roster row. None can move a v2 emit-host execution row across a CPU ceiling.

Two independent measurements that it is load-dependent, not content

  1. main is passing. db3caed84f — the base this PR is recomposed onto — has three successful runs. 02efce0dd3 likewise.
  2. The same content passed the floor in a sibling run. Model the purpose standing, the non-execution consequence, and the admission wall #10174 carries this PR's commits and its head e7e5c144 is fully green, floor included: planned=executed=terminal=3548, interrupted_cpu_deadline=0, verdict=FloorClean. Identical source content, two runs, opposite outcomes.

So membership of the preempted population is redrawn per attempt by runner contention, and the fleet incident is plausibly upstream of it.

I have explicitly declined to re-run, and the bar is not mine

docs/design-failure-modes.md on main, in the row this work descends from: "A retry that draws a faster runner then buys a green OVER REFUSALS THAT DID NOT EXECUTE, which is why re-running is not an exit." Here 3540 claims were judged and one was silenced, so a reroll would assert a verdict that never happened. That is distinct from the toolchain-host failures earlier on this PR, where jobs died before a single claim was planned — there, nothing could be silenced, and I did re-run those.

The decision I am asking for

Whether a preemption-arm-only floor refusal on a foreign, load-dependent row is landable, given claims_failed=0, main green, and the same content green in a sibling run. I will not re-run to clear it and I am not merging.

One note for the record, since this PR is part of the work on that class: the silenced row is a positive control (meet_join_eval_verdict_is_pass). Its sibling ten lines down, emit_host_join_wrong_fixture_refuses_holds, asserts a refusal on a swapped-arms fixture. Preempting either prints a byte-identical line. That is precisely the distinction this PR's vocabulary exists to make expressible — and it remains only expressible, not enforced, exactly as the narrowed annotation says.

— sent from crisp-crane-370

@briansrls
briansrls merged commit 99b0aa7 into main Sep 3, 2026
5 of 7 checks passed
@briansrls
briansrls deleted the session/crisp-crane-370-purpose branch September 3, 2026 21:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant