Skip to content

Track undeclared effect expectations at dispatch sites - #7276

Merged
briansrls merged 5 commits into
mainfrom
claude/expectation-wall-and-gate-gauntlet
Jul 26, 2026
Merged

briansrls merged 5 commits into
mainfrom
claude/expectation-wall-and-gate-gauntlet

Conversation

@briansrls

Copy link
Copy Markdown
Contributor

Summary

Introduces ExpectationDeclaration to make undeclared effect expectations observable and countable at their dispatch sites, replacing silent unwrap_or defaults with typed, located tracking. This enables prioritization and shrinking of the frontier of sites that have not yet declared their expectation.

Key Changes

  • New type ExpectationDeclaration: Distinguishes between Declared(ExpectedOutcome) and UntracedDefault, making the absence of a declaration a first-class value rather than a silent default.

  • Expectation frontier tracking: Added thread-local counter UNTRACED_EXPECTATION_SITES that records service.op → dispatch count for every site that dispatches without declaring expect:. Exposed via untraced_expectation_frontier() for observability.

  • Updated dispatch boundary: eval_service_call now takes ExpectationDeclaration instead of bare ExpectedOutcome. The resolution from declaration to outcome happens at the boundary and is counted, making the substitution typed, located, and observable.

  • Interpreter seams annotated: Three internal dispatch sites (eval_method_call, resolve_env_var_token, eval_filesystem_read_builtin) now explicitly construct ExpectationDeclaration::Declared(ExpectedOutcome::ExpectSuccess) with rationale comments explaining why each is not OutcomeIsData.

  • Receipt emission: claim_batch now emits [expectation-frontier] to stderr when undeclared sites exist, showing site count, total dispatch count, and per-site breakdown.

  • Test coverage: New witness expectation_frontier_witness_test.dag validates that:

    • Entries with undeclared effects emit the frontier receipt
    • Fully declared entries do not emit it
    • The mechanism is observable and countable
  • CI integration: Updated commit_workflow.dag to project gates onto FalsifierCadenceJob surface and added corresponding gate-half wall predicates (roster_has_no_bare_deenrolled_gate, live_roster_has_no_bare_deenrolled_gate) mirroring the witness-half enforcement.

Implementation Details

The change is deliberately not a behavior change: UntracedDefault resolves to the same ExpectSuccess that the prior unwrap_or produced. What changes is observability — the substitution is now typed, located, and counted, so the frontier is measurable and can be shrunk under evidence rather than remaining invisible by construction.

The dissolution condition is corpus-wide: when untraced_expectation_frontier() returns empty, every effect that ran declared its expectation, and the UntracedDefault arm can be deleted as a hard typed refusal at the boundary.

Per DESIGN §5, this is an absorbing fallback made interim and observable: a site re-arms to Declared only when someone has established what it expects, never speculatively (a wrong ExpectFailure would silence a real fault).

https://claude.ai/code/session_013GELyMsZrGgZRrxte2TCsk

claude added 5 commits July 26, 2026 02:25
Found by audit while scoping the three measured gate flips, not by a red.

The FalsifierCadenceJob lane shipped with a witness projector and no gate
projector, while witness_entries_from_enrollments drops CommitSpecGate rows
by construction. So a gate moved to surfaces: [FalsifierCadenceJob] projected
into nothing -- it left the per-PR floor and arrived on no lane -- and the
bare-deenrollment wall could not catch it, because that wall filters on
enrollment_is_witness_claim, which answers false for exactly this row kind.

A gate was therefore the one enrollment shape whose de-enrollment was
invisible: a one-line surface edit that reads in the diff identically to a
legitimate re-home. That is the shape the lane exists to forbid, in the one
row kind the lane never covered -- and the next three planned edits were all
gates.

Three pieces, landing together because any two without the third re-open it:

- project_falsifier_gates: the missing gate projector.
- gunbc_falsifier_gate_batch: the executing consumer, so the surface is not
  enrollment-by-illusion. Appends nothing at landing (no gate rides the
  cadence yet), so the batch chain is byte-identical until a flip lands.
- gate_enrollment_is_scheduled / roster_has_no_bare_deenrolled_gate: the
  wall, with the surface asymmetry reasoned rather than assumed -- the regen
  job counts for gates (it executes them) and never for witnesses (it does
  not), so one predicate could not serve both without wronging one kind.

Proven discriminating by execution, not by inspection: widening the gate
predicate to credit the pre-push hook reds the new RED control
(gauntlet_lane_gate_bare_deenrollment_RED_control_holds FAIL), and reverting
greens it. Both new claims are enrolled on the roster in the same change --
the file lives under long/, which discovery excludes, so the roster is its
only execution path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GELyMsZrGgZRrxte2TCsk
source_root_ingest (427s) and self_host_realized_comparison (223s) leave the
per-PR floor for the 4-hour falsifier cadence. Basis is the [gate-warm-cost]
receipt the floor already emits: a real measurement at the grain the placement
law requires for a GATE, which is why these could be priced when the 36
CommitWitnessClaim rows still cannot -- the TSV measures gates, and the
witness-side blocker is per-ROW receipts. Conflating those grains would be the
fabrication the placement law exists to forbid.

Both are orders above the 5s fast-lane budget, so Gauntlet is not a preference
but the only admissible placement; no signature can make a 427s check
fast-lane-eligible. Expected recovery ~650s of the 36.5min PR floor.

THE THIRD GATE IS NOT FLIPPED. CheapClaimPoolGate (75s) is a declared member
of gunbc_ci_cheap_floor_gate_membership, making it both a DataDependsOn
predecessor of the compile root and a floor_resource_anchor whose exclusion
from corpus->gate serialization is what that list's own note says avoids a
scheduler deadlock. Moving it is a scheduler rewire, not a surface edit, for
10% of the measured prize. It stays per-PR pending an operator call.

Three couplings the flip surfaced, each fixed at its authority:

- Clamp params are index-aligned and hand-maintained, so two rows had to go.
  Which two was NOT the obvious answer: verified by execution that emit_host
  sits at batch index 4 both before and after, so the dead rows are the last
  two (600s/420s), not the 120s/600s pair the schedule order suggests. The
  length witness proves the count and cannot prove the mapping -- recorded,
  because a plausible-but-wrong deletion here is silent.

- The source-ingest ordering witness asserted a SCHEDULER property against a
  spec derived from the live roster, so the flip made it unsatisfiable. Made
  synthetic instead of deleted, exactly as heavy_pair_spec already does for a
  deleted gate: dropping it would have bought a planner coverage loss that has
  nothing to do with where the gate runs.

- witness_gate_roster_matches_coproduct_arms bag-equates enrolled gates with
  every arm of the Gate coproduct -- the construction-shaped "no gate is
  unenrolled", stronger than the per-row wall because it reds even when no
  roster row was edited. It went red for exactly the right reason: its
  universe of executing surfaces was two and there are now three. Extended by
  projecting the cadence gates into the bag, NOT by dropping the flipped arms
  or slackening to subset -- either would have silenced the wall in the same
  motion that made it informative.

Extension proven rather than asserted: enrolling EmitHostGate nowhere reds
BOTH the totality witness and the new gate wall, and restoring greens both.
ci.yml confirmed unaffected (heal produces no diff; its 8 source_root_ingest
matches are the discover_source_root_ingest bin name).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GELyMsZrGgZRrxte2TCsk
Operator ruling 2026-07-25: "we should never have an undeclared expectation;
get rid of the fallback."

The boundary used to close the gap itself --
declared_expectation.unwrap_or(ExpectSuccess) -- which is textbook absorbing
fallback (DESIGN 5). The missing declaration was answered with a plausible one
INSIDE the callee, so it left no trace: the frequency of undeclared sites was
zero by construction and the deficit could never rank for fixing.

eval_service_call now takes ExpectationDeclaration = Declared(ExpectedOutcome)
| UntracedDefault, so absence is a value the caller must construct rather than
a default the callee manufactures. Per the operator guardrail this is a
construction wall, NOT a refusal sweep: UntracedDefault resolves to the same
ExpectSuccess -- no behaviour change, no floor-time refusal on sites nobody has
traced -- but is typed, located and COUNTED, surfaced as [expectation-frontier].

The frontier is real, not notional: 340 service-op call sites in dag/ + src/v2
against 16 carrying `expect:`, and one ordinary witness run reports 67
undeclared Filesystem.Read dispatches. 324 mechanical annotations are
deliberately NOT in this change -- re-arming is evidence-gated, because a
speculative ExpectFailure silences a real fault.

The three sites that looked like migration debt are not. :5162, :5222 and :7493
are interpreter-OWN seams with no .dag call node to carry `expect:`, so an
explicit Declared there is the correct form, not a leftover default. One of
them did hide a real finding, recorded rather than fixed: resolve_env_var_token
swallows failure into None, the classic outcome-is-data tell -- but its None
already means BOTH "variable unset" and "dispatch broke", so re-arming it to
OutcomeIsData would trade a crude loud arm for a silent conflation. The fix is
a pair (split the consumer's None, then re-arm), which is its own change.

Witnessed by a present-and-absent pair, both live on the bin-wet lane: an entry
with undeclared effects MUST emit the receipt, one whose effects all declare
MUST NOT. Either claim alone is satisfiable by a broken counter -- one that
always fires passes the first, one that never fires passes the second -- so
neither is dropped. Measured differentially before enrolling: silent on the
annotated entry, "1 site(s), 67 dispatch(es)" on the unannotated one.

Subject roots are witness_layer_roots, not compile_clean_source_roots: the
latter appends src/v1, putting v1.std.core.Node and v2.std.node.Node in one
pool, and the subject then dies on ~100 ambiguous-reference errors before
dispatching a single effect -- a child that fails to resolve emits no receipt
and would red this witness for a reason unrelated to the mechanism. Found by
execution, not by reading.

Dissolve-on: the counted frontier reaches zero corpus-wide, at which point the
UntracedDefault arm deletes and an undeclared site becomes a hard typed refusal
-- absence unwritable rather than merely counted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GELyMsZrGgZRrxte2TCsk
CI run 30188211737 redded "floor resolve count 1 differs from declared 3:
duplicate-computation debt changed". The floor step itself SUCCEEDED
(floor_outcome=success); what fired is the post-floor receipt gate, which
exists precisely to force a conscious update when the debt moves.

This is the flip's benefit surfacing as a receipt. ci_floor_resolve_receipt_note
states the law that predicts it: resolves_total = 1 (the batch-1 whole-tree
materialization) + the entry classes whose closure ESCAPES it. Both flipped
gates were such entries -- SourceRootIngestGate on
floor_source_root_ingest_gate_entry and SelfHostReadsRealBytesGate on
floor_self_host_realized_comparison_gate_entry, each a RunnableSingleClaim on
its own entry file paying its own cold closure resolve. Re-homing them to the
falsifier cadence removed both, leaving the anchor resolve alone.

So the drop is the ratchet direction that note already names ("collapses the
per-entry resolves back toward 1, lowering this row consciously"), and it is a
wall-clock-independent MEASUREMENT of what the flips bought: two whole-tree
cold resolves per PR, gone.

Recorded as Receipt 6 with the distinction that matters kept explicit: the
escape set is empty today by ROSTER, not by construction, so this does NOT
discharge the terminal condition -- the persistent content-keyed store still
owes its own permanent pin at 1. And 1 is now live debt: if either gate returns
to the per-PR surface, or any new escaping entry class enrolls, this gate reds
on the run that does it and the number goes back up consciously, exactly as it
came down here.

ci.yml regenerated via main_wet so the emitted gate compares against 1 (the
drift gate would otherwise red on the stale "-ne 3"), rather than leaving it
for the heal job to push.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013GELyMsZrGgZRrxte2TCsk
@briansrls
briansrls merged commit 1f5da95 into main Jul 26, 2026
5 checks passed
@briansrls
briansrls deleted the claude/expectation-wall-and-gate-gauntlet branch July 26, 2026 12:52
gunbai-bot Bot pushed a commit that referenced this pull request Jul 26, 2026
Main added this wall (#7276) against roadmap_belt_actuate_witnesses /
generated_artifact_drift_witnesses after sweep A deleted those
aggregators. Point at surviving effect-bearing leaves instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Jul 26, 2026
Main added this wall (#7276) against roadmap_belt_actuate_witnesses /
generated_artifact_drift_witnesses after sweep A deleted those
aggregators. Point at surviving effect-bearing leaves instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Jul 26, 2026
Main added this wall (#7276) against roadmap_belt_actuate_witnesses /
generated_artifact_drift_witnesses after sweep A deleted those
aggregators. Point at surviving effect-bearing leaves instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
gunbai-bot Bot pushed a commit that referenced this pull request Jul 26, 2026
Main added this wall (#7276) against roadmap_belt_actuate_witnesses /
generated_artifact_drift_witnesses after sweep A deleted those
aggregators. Point at surviving effect-bearing leaves instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
briansrls pushed a commit that referenced this pull request Jul 26, 2026
* WIP: test umbrella dissolution

* Fix regen drift: register hygiene in layout and emit schedule U3 note.

rustfmt places test_module_hygiene alphabetically; seed-retained intrinsic registration keeps self-compile from dropping the mod.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore fallback_arm_census falsifier substrate-long-lane enrollment.

Review 43070: umbrella dissolution had dropped the five Class-C nightly rows (and exclusion) with no re-home, leaving the long-lane witnesses with zero executing consumers.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Enrollment: retarget roadmap_belt_actuate bin_wet to file-grain.

Umbrella dissolution promoted 37 leaf test fns; the wet roster still named
the deleted aggregator, so discovery deferred them with zero wet consumers
(UnexecutedDeferredWitness ~38). File-grain empty function expands via
expand_explicit_entries — same pattern as other dissolved enrollments.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Admission: expand file-grain bin_wet keys to leaf test fns.

Phase 0(b) matched deferred rows against the unexpanded `entry::`
consumer key from `f: ""`, so roadmap_belt's 38 leaf witnesses still
looked unexecuted after the wet roster retarget. Expand empty function
the same way execution already does (enumerate_entry_test_fns).

Co-authored-by: Cursor <cursoragent@cursor.com>

* fmt: wrap file-grain admission expand assertion.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Known-RED: re-enroll english_emit ingest-round-trip leaf.

Umbrella dissolution dropped probe_red for
english_emit_add_emit_ingest_round_trip_holds and left only prose_holds —
the discriminating S2 ingest control named in known_red_frontier_note.
Promote the ingest leaf to test fn, enroll it, dissolve the aggregator,
and promote the sibling serialize/grammar leaves so they stay live.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Drop logic_truth_tables_all umbrella with its known-RED probe.

Enrollment already removed the aggregator probe_red row; the excluded
file still declared the test fn, so Phase 0(b) admission refused an
unexecuted deferred witness. Leaves stay the three known-RED controls.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Align logic_ground_truth exclusion dissolve-on count with three leaves.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Retarget no_fake_anomalies at a surviving belt leaf.

roadmap_belt_actuate_witnesses was dissolved; the anomaly-glyph wall still
named the umbrella as its passing corpus entry.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix three discovery reds exposed by umbrella dissolution.

Restore the readback roster ratchet at length 5; invert the transport
RestNetwork hermetic RED into a green leaf; treat file-grain wet
enrollment (function: "") as matching any backing function so emit
coverage SelfEmittedNative rows stay backed after f: "" retargets.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Replace illegal // comments in emit_coverage_frontier.dag.

.dag has no slash-comment syntax; the prior note panicked parse
(expected expression, found Slash) and failed regen/heal.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Re-home english_emit serialize/grammar helpers under known-red leaf.

matches_serialize and grammar_inverse were wrongly promoted to test fn
with no roster consumer (file is OfflineLocalRecipe). Demote to plain
fns and force-eval them from english_emit_add_ingest_round_trip_holds,
matching main's helper shape without an Offline-orphan pair.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Restore mock-only commit enrollments for classical_not and native_only.

File-grain check_fns: [] pulled wet equals_eval / native-run legs into
the per-PR execution corpus; those files intentionally enroll only the
mock-shape aggregates (real compile/run stays on the wet nightly lane).

Co-authored-by: Cursor <cursoragent@cursor.com>

* Retarget expectation_frontier off dissolved umbrella names.

Main added this wall (#7276) against roadmap_belt_actuate_witnesses /
generated_artifact_drift_witnesses after sweep A deleted those
aggregators. Point at surviving effect-bearing leaves instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* WIP: test umbrella dissolution

* Roster ct_render_rust_applied_type_qualified_base_test scaffold blob.

Coverage witness compiler_tests_rust_blobs_are_all_rostered reds when a
ct_ blob lands unmarked; this PR's touch pulled that live-tree check into
the affected set and exposed the unrostered #7269 render blob.

Co-authored-by: Cursor <cursoragent@cursor.com>

* WIP: test umbrella dissolution

* Make witness-admission scaffold receipt rg-count truthful.

Review 43319: bare-token `rg … == 1` was false (header/literal/test multi-hit).
Pin the declaration with IDENT+TYPE_ANN split across comment lines so the
contiguous pattern matches once — only the const declaration.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Keep join on meet_join cold roster and ledger (review 43336).

Umbrella dissolution left only meet enrolled; join is a peer primary agreement witness and must stay on the native-cache cold falsifier and proactive verification ledger.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Dedupe ct_render_rust_applied_type scaffold roster row (review 43345).

Main already carried two identical declarations and roster slots; this PR had added a third. Keep a single LanguageSourceScaffoldRow and one roster entry for the blob.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Brian Searls <briansearls1@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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.

2 participants