Skip to content

✨ Slice A of #848: project REPL views from durable history - #849

Merged
taras merged 2 commits into
mainfrom
agent/issue-848-model
Sep 27, 2026
Merged

taras merged 2 commits into
mainfrom
agent/issue-848-model

Conversation

@taras

@taras taras commented Sep 27, 2026

Copy link
Copy Markdown
Owner

Why

Issue #848 asks for a REPL that can run one XMD entry and reconstruct it from a
URL. It is delivered as five slices; this is Slice A, the durable
reconstruction kernel
— the layer that turns one execution's Journal into
something a screen could be drawn from. It references #848 and does not close
it: only Slice E makes xmd repl exist.

What changes

Before: nothing outside the runtime can read one execution's history back as a
view. <Elicit> records its answer, but not the question's shape, so a reader
of the history cannot say which fields the person was shown.

After: given an array of the repository's real Yield | Close events, code can
obtain a deeply frozen ReplModel for the whole Journal or one inclusive
checkpoint prefix, and address any part of it with an xmd://repl/... location
that decodes, canonically encodes and resolves to the exact objects in that
model. <Elicit> retains its compiled schema as descriptive input beside the
answer.

There is intentionally no executable REPL after this slice.

How it works

DurableEvent[] → projectRepl(events, marker?) → frozen ReplModel
xmd://repl/…   → decodeLocation → ReplRoute → resolveLocation(model) → exact objects

projectRepl walks the vocabulary an ordinary execution already appends — root
and nested import_component, eval, generated_xmd, elicit, and the root
Close — and recognizes no record of its own. A checkpoint marker is derived
from protocol identity (yield:<coroutine>:<ordinal>, close:<coroutine>), so
the same event has the same marker in every process that reads the file and
nothing extra is stored to make that true.

Review guide

Start with: packages/cli/tests/repl-model.test.ts

Then review:

  1. packages/cli/src/repl/model.ts — projection, refusals, detachment
  2. packages/cli/src/repl/route.ts — the pure codec and the resolver
  3. packages/core/src/elicit-journal.ts — the one durable change
  4. packages/cli/tests/fixtures/repl/ — the entry the evidence really runs

Look carefully at:

  • ownerOf — attribution is by the source path a position names, and an owner
    that is absent, ambiguous or unreadable refuses rather than being guessed.
  • detach — what the model retains is copied out of the events, so projection
    neither freezes nor modifies its input.

What must stay true

  • An elicit record's identity is still type and name, and the
    schema-plus-message fingerprint is still the stale-input guard — enforced by
    keeping the schema in the description beside them, checked by
    packages/core/tests/elicit-component.test.ts.
  • A projection never changes its input — enforced by detach, checked by M3.
  • A marker never falls back to the head — enforced by projectRepl, checked by
    M1.
  • No DurableEvent reaches a consumer of the model — checked by M3's structural
    walk.

How to verify it

deno task test \
  packages/core/tests/elicit-component.test.ts \
  packages/cli/tests/repl-model.test.ts \
  packages/cli/tests/repl-route.test.ts
deno task check

9 tests, 49 steps. Every journal under test is produced by really executing the
reference entry, so the projector is held to what the current runtime writes
rather than to a recorded array.

  • M1 proves empty/intermediate/terminal prefixes project deterministically
    and frozen, and fails if an unknown marker, a second entry, work after
    settlement, a repeated close or an unreadable payload were accepted.
  • M2 proves the real payloads reach their documented scopes, and fails if a
    removed, repointed or corrupt source position were attached to a guess.
  • M3 proves resolution returns the model's exact objects, that the model is
    frozen everywhere reachable, that it shares no object with the event graph,
    and that the events come out unfrozen and byte-identical.
  • E1 proves the schema is in the description and the answer in its result,
    that replay still skips the provider, and that a changed schema or message is
    refused.
  • R1/R2 prove equivalent spellings decode alike, canonical round-trips hold,
    and every missing target or illegal combination refuses.

Nine deliberate defects were introduced one at a time and each makes the
corresponding test fail.

Scope

Included

  • the normalized Elicit schema as durable descriptive input
  • packages/cli/src/repl/{model,route}.ts and their focused tests
  • real-event fixtures and the affected Elicit Journal spec paragraph
  • a remeasured test-weights.json

Intentionally unchanged

  • no storage, session, controller, component, renderer, host or command — those
    are Slices B–E
  • no specs/repl-spec.md and no broad architecture rules yet
  • core execution and replay semantics are untouched

Generated or mechanical changes

  • test-weights.json is the artifact of Measure test weights run
    36270414251
    against 970bc0db, committed unchanged. The shard counts do not move: the
    measured floors are 14/8/4 and the installed counts are already 15/10/5.

Risks and limitations

  • A journal whose elicit record predates this change retains no schema, and
    the projector refuses it rather than showing an answer without its question.
    Nothing in the repository writes such a record and reads it through this
    projector, so no migration affordance is offered.
  • The root Close has three shapes — a document result, a protocol err, and
    cancelled — and only the first carries rendered output.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and test results.

Slice A of #848: the durable reconstruction kernel. Code outside the runtime can
take an array of the repository's real `Yield | Close` events and obtain a
deeply frozen view of one execution, for the whole Journal or for one inclusive
checkpoint prefix, and can address any part of that view with a URL. There is no
REPL yet — no storage, no session, no component tree, no command — and nothing
in core's execution semantics changed.

`<Elicit>` now retains the compiled schema beside its answer. The validated
answer is still the record's result and the fingerprint over schema and message
is still the only guard; the schema travels in the same description under
`executablemd.elicitation-schema`, where a source position already travels, so
`type` and `name` still decide what a replay matches. Historical inspection
needs to know which fields the person was shown, and the provider that could
have said so is not running any more. `@executablemd/core/host` publishes the
field name and a parsing reader for it, because a description is journal data
and a schema that merely looked plausible would reach a form as one.

`packages/cli/src/repl/model.ts` projects the vocabulary an ordinary execution
already appends — root and nested `import_component`, `eval`, `generated_xmd`,
`elicit`, and the root `Close` — into immutable structural values, deep-frozen,
with no REPL record, no cache, and no `DurableEvent` leaving the module. Every
retained value is detached from the event that carried it — copied, then frozen
— so the model cannot be changed by whoever still holds the events, and the
events are neither frozen nor modified by having been read: a projector that
froze its input would make a caller's own data immutable as a side effect of
being looked at. A
checkpoint marker is derived from protocol identity, `yield:<coroutine>:<ordinal>`
or `close:<coroutine>`, so the same event has the same marker in every process
that reads the file and nothing extra is written down to make that true.
Attribution is by the source path a position names: an effect whose owner is
absent, ambiguous or unreadable refuses, because a binding attached to a guessed
scope is a value shown where nothing published it.

`packages/cli/src/repl/route.ts` is the pure half of navigation. `decodeLocation`
decides only what a grammar can decide, `encodeLocation` emits one canonical
spelling, and `resolveLocation` answers with the exact objects the projection
holds. The two are separate so that a typo in a location is never answered with
a guess about the history.

Evidence runs against a journal produced by really executing the reference entry
rather than a recorded array, so the projector is held to the vocabulary the
current runtime writes. The fixture holds one of each thing this slice projects:
a durable evaluation publishing JSON, one nested component occurrence whose
source the run retains, one generated fragment admitted from a value that
evaluation published, and one validated question whose answer changes what
renders after it.

One thing the vocabulary turned out to say that a reader has to handle: the root
`Close` records three outcomes, not one — a document result, a protocol `err`
for a run that failed before producing one, and `cancelled` — so `ReplTerminal`
has three statuses and only the first carries rendered output. A serialized
`err` also carries a stack naming host paths, so the transcript shows its
message and nothing else.
Slice A adds `repl-model.test.ts` and `repl-route.test.ts`, so the corpus moved
and every runtime's partition was reading a fallback weight for both. Measured
by **Measure test weights** run 36270414251 against `970bc0db`, on
`ubuntu-latest`, and committed exactly as the artifact came out.

The shard counts do not move. The measured floors are 14, 8 and 4 for Deno,
Node and Bun; the installed counts are 15, 10 and 5, so each is already above
its floor and no five-run evidence asks for a change.
@taras
taras merged commit a2f298c into main Sep 27, 2026
43 of 44 checks passed
@taras
taras deleted the agent/issue-848-model branch September 27, 2026 20:03
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