Skip to content

Let a Substitute site synthesize a miss value, and observe what it did - #133

Merged
maverox merged 1 commit into
mainfrom
work/on-miss-synthesis
Sep 14, 2026
Merged

maverox merged 1 commit into
mainfrom
work/on-miss-synthesis

Conversation

@maverox

@maverox maverox commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Stacking note — read before reviewing the diff. This PR's branch is built on #147, but GitHub shows its base as main because the stack tooling refuses a base change (422: part of a stack). So the diff below includes #147's five commits as well as this one.

Review only the top commit (feat(runtime): let a Substitute site synthesize…), or merge #147 first — this PR's diff cleans up by itself once it lands. The git history is correct and linear: 147 → 133 → 134 → 135.

What

Reconstructed gains Synthesized(T) and NoValue, so one closure answers both halves of a Substitute lookup: rebuild a hit, or derive a value from the query alone on a miss.

hit miss
has a value Value(T) Synthesized(T)
has none Failed(String) NoValue

on_miss = <expr> becomes sugar for the miss arm (Synthesized(expr)); declaring nothing becomes NoValue, the pre-existing fail-stop. Every existing site compiles unchanged.

Why determinism, not honesty

The value a miss arm returns must be a function of the query and nothing else. Replay needs same query -> same value, every run, or two replays of one candidate against one tape disagree with each other and "the candidate changed" cannot be separated from "the fabrication changed". It is also what keeps two different candidates comparable past the edge of the tape.

Answering a miss by running the real computation is the most honest option available and the worst one — at a deja::id seam it reintroduces exactly the entropy the seam exists to remove, on precisely the calls the seam failed to cover.

Why the accounting had to change with it

MissPolicy rode the query because the observation was emitted inside the lookup, before the seam decided — so the only thing available to stamp was what the boundary had declared. That was sound only while a declared on_miss could not decline.

NoValue makes declaration and outcome disagree. So the Substitute path becomes two-phase — substitute_peek → decide → substitute_observe — mirroring the execute-shadow lifecycle that has always worked this way. Deferring the emission is safe because the seam owns both fail-stops: it emits, then panics, in that order. No Drop guard, no unwind-safety question. Both seam families share one substitute_decide, so the emit-before-stop ordering lives in exactly one place.

MissPolicy, dispatch_or_miss and dispatch_async_or_miss are gone.

Two things that fall out

A hit that stopped the request is now visible. A Failed hit used to emit resolved: true, Provenance::Recorded and only then panic, so the ledger showed a cleanly-served call for a request that died. It is now stamped Stopped while staying resolved: true — a combination the two booleans structurally cannot express.

A hot-path cost disappears. The macro used to clone the args image per active call at every boundary with an on_miss. The seam builds the marker from the spec it already holds, so only a genuine miss pays.

This is one of three instances of the same principle

PR the implicit thing made explicit as
#133 a miss policy the seam inferred from a declaration a value the site returns
#135 a hash seed the collection took from Default a seed the site asks for
#147 identity inferred from a locus identity declared on the key

Each replaces something the system decided on the author's behalf with something the author has to say. The reason it keeps paying is that the implicit version is indistinguishable in the source from an oversight.

Evidence

just verify green. Five tests, mutation-checked because all passed first try and the pre-existing tests could not fail if the core claim were wrong — they were written when declaration and outcome could not disagree:

mutation stamped stopped-emits unrecon-hit derived-flags marker
emit after the stop ✗ ✗ ✗ ✗ ok
outcome ignores the return ✗ ✗ ✗ ✗ ok
absorbed decoupled ok ok ok ✗ ok
miss branch unreachable ok ok ok ok ✗

Two mutations are killed by exactly one test each, so neither is redundant, and the marker test is not vacuous.

Deliberately not in here

deja::synth helpers (#134). Collapsing outcome/absorbed/synthesized — an orchestrator change. A conditional decline from the macro: with the sugar a site always returns Synthesized; only a hand-built seam can inspect the query and return NoValue. The sugar is kept because it preserves every existing site.

🤖 Generated with Claude Code

`Reconstructed` gains `Synthesized(T)` and `NoValue`, so one closure answers
both halves of a Substitute lookup: rebuild a hit, or derive a value from the
query alone on a miss. `on_miss = <expr>` becomes sugar for the miss arm
(`Synthesized(expr)`); declaring nothing becomes `NoValue`, the pre-existing
fail-stop. Every existing site compiles unchanged.

The value a miss arm returns must be a function of the query and nothing else.
Determinism is the load-bearing property, ahead of honesty: replay needs
`same query -> same value, every run`, or two replays of one candidate against
one tape disagree with each other and "the candidate changed" cannot be
separated from "the fabrication changed". It is also what keeps two DIFFERENT
candidates comparable past the edge of the tape. Answering a miss by running
the real computation is the most honest option available and the worst one --
at a `deja::id` seam it reintroduces exactly the entropy the seam exists to
remove, on the calls the seam failed to cover.

That forces the accounting to change. `MissPolicy` rode the query because the
observation was emitted INSIDE the lookup, before the seam decided, so the only
thing available to stamp was what the boundary had DECLARED. That was sound
only while a declared `on_miss` could not decline. `NoValue` makes declaration
and outcome disagree, so the Substitute path becomes two-phase --
`substitute_peek` -> decide -> `substitute_observe` -- mirroring the
execute-shadow lifecycle that has always worked this way. Deferring the
emission is safe because the SEAM owns both fail-stops: it emits, then panics,
in that order. No Drop guard, no unwind-safety question. Both seam families
share one `substitute_decide`, so the emit-before-stop ordering lives in
exactly one place.

`MissPolicy`, `dispatch_or_miss` and `dispatch_async_or_miss` are gone; both
shapes call one seam and differ only in what the closure returns.

Two things fall out. A hit whose payload will not rebuild is now stamped
`Stopped` while staying `resolved: true` -- it used to emit `resolved: true,
Provenance::Recorded` and only then panic, so the ledger showed a cleanly
served call for a request that died, a combination the two booleans cannot
express. And the macro no longer clones the args image per active call at every
boundary with an `on_miss`: the seam builds the miss marker itself, from the
spec it already holds, so only a genuine miss pays.

`absorbed` (what the scorer reads) and `synthesized` (the V2 scaffold, until
now always false) are derived from `outcome` and stamped in one place, with a
test that they never drift. Collapsing them onto `outcome` is an orchestrator
change and is not in here.

The seam's doc block had drifted onto `Reconstructed` -- a plain `//` note
between it and the code left `dispatch` rendering undocumented and the enum
rendering with the seam's control flow on top of its own. Re-homed and updated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019FmXkygUmueraF9oR4oqwS
@maverox
maverox force-pushed the work/on-miss-synthesis branch from 7a604f7 to 222f36e Compare September 14, 2026 13:38
@maverox
maverox merged commit 0fdcdf9 into main Sep 14, 2026
10 checks passed
@maverox
maverox deleted the work/on-miss-synthesis branch September 14, 2026 15:06
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