Skip to content

fix(corpus): CHAOS-5722 score any_of serve branches from the persisted semantic state - #75

Merged
chrisgeo merged 2 commits into
mainfrom
fix/5722-anyof-serve-branch-scoring
Sep 13, 2026
Merged

chrisgeo merged 2 commits into
mainfrom
fix/5722-anyof-serve-branch-scoring

Conversation

@chrisgeo

@chrisgeo chrisgeo commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Ticket: CHAOS-5722

TEST-EVIDENCE

What this change accomplishes, and for whom. The corpus yardstick harness scores each any_of row's declared alternatives and reports the best-ranked one. A served, correct answer's serve branch was capped at unscored forever (no wire field could confirm it), while a wrongly-certified refuse branch on the same row could reach agree — so the published numbers made a correct serve look worse than an incorrect refuse. This change lets a serve branch reach agree when the served result's own PERSISTED engine state (acr's M2 semantic_state column, read by an injected adapter — the scorer itself never opens a connection) confirms the family and a passed validation gate, so chris and the team reading yardstick output see a serve's real correctness instead of a policy artifact.

Executed claim. pnpm test:corpus (fresh pnpm install in the lane worktree, corpus/test_semantic_verdict.py run standalone too) is GREEN at the pushed tip: 33/33 semantic_verdict controls, 2/2 smoke controls over all 36 real corpus rows, 2/2 real vendored-fixture proof controls (9 reps of cv-discovered-team-series, figures byte-identical to the pre-existing published totals for that fixture), 4/4 schema-driven shape controls. Command and output:

$ pnpm test:corpus
...
PASS: 33 semantic_verdict controls
PASS: 2 semantic_verdict smoke controls over 36 real corpus rows
PROOF_TOTALS {"family_confirmation": {"unavailable": 9}, "family_relation": {"changed": 3, "same": 6}, "unscored": {"true": 9}, "window_binding": {"verified": 9}}
PASS: 2 semantic_verdict proof controls over real vendored proof data
PASS: 4 schema-driven shape controls

Full ask-dev quality gate at the pushed tip, fresh pnpm install: prettier --check . rc=0, pnpm lint rc=0, pnpm typecheck rc=0, pnpm test:unit (vitest) rc=0, NODE_ENV=production pnpm build (next build) rc=0.

Surface touched. corpus/semantic_verdict.py (new PersistedSemanticStateUnreadable, _score_persisted_family_confirmation, persisted_semantic_state=None parameter on score_branch/score/build_verdict, POLICY_VERSION bump), corpus/expect_schema.py (SEMANTIC_STATE_ABSENT/SEMANTIC_STATE_UNREADABLE read from the synced wire schema's own semantic_reading.reason enum, not hand-copied), corpus/README.md (doc). No acr change, no wire/contract change, no corpus row expect value changed (D24's three real any_of rows are untouched data).

Red-first. Every new behavior is exercised by a test that is RED without this change: corpus/test_semantic_verdict.py before this diff has no persisted_semantic_state parameter at all, so every new test below fails with TypeError: score() got an unexpected keyword argument (or, for the renamed default-path test, asserts the OLD family_confirmation_unavailable reason) on the pre-change source; green on this branch.

Guard input-domain table (_score_persisted_family_confirmation, the new validator this change adds — every cell executed in test_semantic_verdict.py, PASS in the run above):

field cell verdict/reason test
adapter not supplied (None) unscored/semantic_state_absent test_family_match_is_not_confirmation_without_a_persisted_link, test_build_verdict_with_no_persisted_state_argument_is_backward_compatible
adapter returns None (no row) unscored/semantic_state_absent test_persisted_state_absent_stays_unscored
adapter raises PersistedSemanticStateUnreadable unscored/semantic_state_unreadable test_persisted_state_unreadable_stays_unscored
adapter returns non-dict (str/int/list) unscored/semantic_state_unreadable test_persisted_state_non_dict_return_is_unreadable
result_id absent/null/empty string/wrong scalar type unscored/semantic_state_absent, adapter never called test_malformed_result_id_is_absent_and_adapter_is_never_called
format_version matches known version proceeds every agree/disagree test
format_version unknown/wrong value unscored/semantic_state_version_unknown test_persisted_state_unknown_format_version_is_unscored
family absent/null/wrong scalar type unscored/semantic_state_unreadable test_persisted_state_malformed_shape_is_unreadable
family valid, mismatched vs. observed disagree/persisted_family_mismatch test_persisted_family_mismatch_is_disagree
validation absent/null/wrong container type (string, list) unscored/semantic_state_unreadable test_persisted_state_malformed_shape_is_unreadable
gate_outcome absent/null/wrong scalar type unscored/semantic_state_unreadable test_persisted_state_malformed_shape_is_unreadable
gate_outcome valid but not the accepted member unscored/gate_outcome_not_accepted test_persisted_gate_outcome_not_accepted_is_not_agree (sweeps refused_basis/rejected_invalid/not_proposed/empty)
gate_outcome accepted member, family matches agree/family_confirmed test_persisted_link_promotes_serve_to_agree_and_outranks_a_wrongly_certified_refuse, test_persisted_link_fields_are_published_in_branch_results
window-binding audit mismatch (a POSITIVE, proven mistake in the window mechanics) short-circuits before the adapter, disagree test_adapter_is_never_called_when_window_binding_mismatches_or_family_changed
window-binding audit unknown with NO exchange at all (an ordinary single-turn serve) does NOT block; adapter still consulted test_persisted_link_promotes_a_single_turn_serve_with_no_window_exchange_at_all
window-binding audit unknown with an exchange attempted but inconclusive (a corpus-capture gap, e.g. a missing confirmation record) does NOT block; adapter still consulted test_persisted_link_promotes_a_serve_whose_window_exchange_audit_is_merely_inconclusive
family-relation audit changed short-circuits before the adapter test_adapter_is_never_called_when_window_binding_mismatches_or_family_changed
branch population scalar/absent expect adapter never called test_adapter_is_never_called_for_a_scalar_row
branch population refuse/decline/clarify branch adapter never called test_adapter_is_never_called_for_a_refuse_branch
_RANK ordering unchanged pinned test_rank_ordering_is_unchanged
scalar rows untouched by this change pinned test_scalar_row_is_untouched_by_the_new_machinery (pre-existing, still green), test_build_verdict_runs_over_every_real_corpus_row

Window-binding gate is != "mismatch", deliberately not == "verified". The window audit (audit_window_exchange) and the persisted-family link are independent evidence about independent questions — the audit asks whether a window-clarification receipt was correctly offered and applied, the persisted link asks whether the engine's own stored record of what it served and validated matches what was declared. An ordinary single-turn serve never goes through a window-clarification exchange at all, so the audit's own default (window_binding="unknown", reason="unsupported_exchange") has nothing to confirm or deny about the persisted link's own question; only a POSITIVE, proven mistake in the window mechanics ("mismatch") contradicts it. Both an absent exchange and an inconclusive one (a two-turn exchange the corpus harness could not fully read back, e.g. a missing confirmation record) read as "unknown" and are exercised as separate, named cells in the domain table above — neither blocks a real, independently-grounded persisted link, and the real-world neg-explicit-comparison row in the RESCORE table below is exactly this shape (a served row with no window-clarification exchange at all).

Class sweep. The absent/unreadable-reason pair (SEMANTIC_STATE_ABSENT/SEMANTIC_STATE_UNREADABLE) is read once from the synced wire schema's semantic_reading.reason enum (expect_schema._read_semantic_reading_reasons, fails loudly at import if the enum ever stops being exactly these two members) and consumed from that single source everywhere this change needs it — there is no second hand-typed copy of the two tokens to drift.

RESCORE (read-only, real trial-store data — numbers of record are unchanged; this is a proof-of-effect replay, not a landed rescore). Reproduced the three archived any_of rows named in the ticket across all 3 reps of the pinned proof at acr 0af9fa84 (~/.cache/acr-kiac-askdev/proofs/2026-09-13-main-0af9fa84/), reading each row's last_result_id and re-running this branch's semantic_verdict.build_verdict() with a persisted_semantic_state adapter backed by a REAL, read-only query against the k3s trial store:

kubectl -n acr-trial-data exec trial-postgres-7f4fd4b5db-dpqx2 -- psql -U postgres -d acr_kiac_askdev -At -F'|' \
  -c "select result_id, (semantic_state is not null), semantic_state->>'format_version', \
      semantic_state->>'family', semantic_state->'validation'->>'gate_outcome' \
      from acr.context_fabric_investigation_results where result_id in (...)"

All 9 result rows carried format_version=semantic-state.v1, gate_outcome=passed. The row's own served/refused observed family was read from the archived attempt artefacts (.../seq/shard-00/replicate/<id>-rep<n>-t*-a*.json), never re-derived. Corpus question TEXT is not reproduced anywhere below or in this repo — row ids only.

corpus_id (any_of row) rep old verdict/reason new verdict/reason
cv-scoped-projects-by-team-bounded 1,2,3 agree / no_match: did not serve, as declared unchanged — the row never served (bucket unserved), so its serve branch's own legacy verdict stays disagree and never reaches the persisted-link check
cv-c7-org-drivers 1,2,3 agree / no_match: did not serve, as declared unchanged — this row declares no serve branch at all (refuse/clarify only)
neg-explicit-comparison 1 unscored / family_confirmation_unavailable agree / family_confirmed
neg-explicit-comparison 2 unscored / family_confirmation_unavailable agree / family_confirmed
neg-explicit-comparison 3 unscored / family_confirmation_unavailable agree / family_confirmed

3 of 9 rescored rows changed. The two rows the ticket cited as "wrongly-certified refuse" are unaffected by this change exactly as designed (their serve branch, where either present, never reaches the new check because it was never served); the one row that actually served and was wrongly capped now scores at its correct agree, resolving the asymmetry the ticket names. POLICY_VERSION bumped to any_of-fail-closed-v2-persisted-family-link so this rescore is never mistaken for a like-for-like comparison against a v1 run.

SELF-REVIEW. Verified against the pushed tip: (1) every non-serve branch and every scalar/absent-expect row never invokes the adapter (call-count doubles, above) — the M2 read is strictly additive to the serve-branch path; (2) every pre-existing test in test_semantic_verdict.py/test_semantic_verdict_smoke.py/test_semantic_verdict_proof.py still passes unmodified except the one test whose assertion this ticket's own behavior change targets (test_family_match_is_not_confirmation_without_a_persisted_link, renamed from test_family_match_is_not_confirmation, reason string updated from the pre-change token to the D49 token — same verdict tier, unscored, both before and after); (3) build_verdict/score/score_branch all keep persisted_semantic_state as a trailing optional parameter after every pre-existing positional argument, so acr's unmodified scripts/corpus/semantic_verdict_bridge.py (which this ticket does not touch) keeps calling build_verdict() exactly as it does today, scoring every any_of serve branch as unscored/semantic_state_absent (same rank as before, corrected reason token) until a separate acr-side change wires a live adapter in; (4) the two D49 reason tokens are read from the synced wire schema, not hand-typed, so they cannot silently diverge from the engine's own disclosure; (5) _GATE_OUTCOME_PASSED/_SEMANTIC_STATE_FORMAT_VERSION are hand-mirrored acr internal constants (never on the wire, so no schema exists to read them from) — each cites its acr source file in a comment, the same mirroring discipline this file's own _SERVED_HTTP_STATUSES already uses.

RISK-NOTES

  • Scope: this PR changes only corpus/ scoring machinery in ask-dev. No acr code, no wire contract, no corpus row expect value, and no production consumer of the corpus scorer is touched. The new persisted_semantic_state parameter is optional and defaults to the pre-existing conservative behavior (unscored) everywhere it is omitted.
  • Behavior change (intentional, scoped by ticket, and UNCONDITIONAL — not gated on an adapter being wired in): every any_of serve branch that used to reach the old ceiling reason now reports one of D49's two closed tokens (semantic_state_absent/semantic_state_unreadable) instead of the old family_confirmation_unavailable — same unscored rank, different reason string, for EVERY caller of this ticket's score/score_branch/build_verdict, whether or not it passes a persisted_semantic_state adapter (an omitted adapter reads as semantic_state_absent, the same token an adapter that found no row would produce). A report or dashboard that greps the old reason string by name needs updating the moment this PR lands, independent of whether the trial-store adapter is wired in anywhere yet. Grepped both repos for the old string as a live consumer, not just a docs mention: none found outside this PR's own docs and this module's unrelated audit_window_exchange internal reason field (a different value, on a different key, unaffected by this change).
  • POLICY_VERSION bump: any_of-fail-closed-v2-persisted-family-link. Any consumer that compares scorer output across versions without checking policy_version risks reading a policy change as a correctness change; the field already exists on every published record for exactly this reason.
  • score/score_branch internal tuple arity changed (each any_of branch result is now a 3-tuple (verdict, reason, detail), was 2-tuple) — verified by grep that no caller anywhere in either repo destructures this shape directly: score_branch is called only from score() in this same file, and score() itself is called only from build_verdict() in this same file plus this file's own tests (already updated to the new shape); acr's runner calls only build_verdict(), which publishes branch results as dicts, never raw tuples, so this internal shape is not part of any cross-file or cross-repo contract.
  • Trust boundary: the scorer trusts whatever format_version/family/validation.gate_outcome the caller's adapter returns; it never opens a connection itself and performs no authentication of the adapter's source. A companion, already-reviewed acr-side PR is what wires a real Postgres-backed adapter into acr's runner and is where that trust boundary is actually established — this PR alone ships no reachable production caller of the adapter parameter (acr's own semantic_verdict_bridge.py is untouched here; see that companion PR for the wiring and its own forward-compatibility guard against an ask-dev pin that predates this parameter).
  • Hand-mirrored constants: _GATE_OUTCOME_PASSED ("passed") and _SEMANTIC_STATE_FORMAT_VERSION ("semantic-state.v1") are copied by hand from acr's Go source (neither is on the wire, so there is no schema to read them from programmatically) — an acr-side rename of either constant needs a matching edit here, same as the existing _SERVED_HTTP_STATUSES mirror this file already carries.
  • No new dependency, no schema change: expect_schema.py's new reader adds a defensive RuntimeError if the synced schema's semantic_reading.reason enum ever stops being exactly the two known members, so a future wire change fails the corpus test suite loudly rather than silently mis-scoring.

@linear-code

linear-code Bot commented Sep 13, 2026

Copy link
Copy Markdown

CHAOS-5722

…state

An any_of serve branch's family-confirmation link now reads the served
result's persisted semantic state (acr M2 semantic_state column) through
an injected persisted_semantic_state(result_id) adapter, instead of
staying uniformly unscored/family_confirmation_unavailable while a
wrongly-certified refuse branch on the same row can agree. Reuses the
engine's own two closed absent/unreadable reason tokens for the new
unscored reasons.
@chrisgeo
chrisgeo force-pushed the fix/5722-anyof-serve-branch-scoring branch from a952c27 to 9944712 Compare September 13, 2026 22:32
…dence

score_branch's window-binding gate is != "mismatch", not == "verified" --
an ordinary single-turn serve or a merely inconclusive window-clarification
audit must not block a real, independently-grounded persisted-family link.
That intent was implicit in the code and easy to misread as an oversight;
this spells it out in score_branch's own docstring and adds executed
coverage for both shapes (no exchange at all, and an exchange attempted
but unreadable).
@chrisgeo
chrisgeo merged commit 5a4913e into main Sep 13, 2026
13 checks passed
@chrisgeo
chrisgeo deleted the fix/5722-anyof-serve-branch-scoring branch September 13, 2026 23:19
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