Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
108de45
🚧 Surround one expansion with its canonical observation (#881 PR 2, l…
taras Oct 9, 2026
adf51b9
🚧 Tell an element's observers where it has reached (#881 PR 2, layer 1b)
taras Oct 9, 2026
38fa9e7
🚧 Say where a source text's elements are, without running any of it (…
taras Oct 9, 2026
4ef4eef
🚧 Classify the new expansion seam as a gate (#881 PR 2, layer 3a)
taras Oct 9, 2026
4f5fc25
🚧 Surround structural work with the same observation (#881 PR 2, laye…
taras Oct 9, 2026
b122a9f
🚧 Keep what this process is doing, element by element (#881 PR 2, lay…
taras Oct 9, 2026
5dffeb0
🚧 Carry the reading to the screen, and say so in the specs (#881 PR 2…
taras Oct 9, 2026
9a1d347
🚧 Read an entry as output then source, fitted by the engine — layer 4…
taras Oct 9, 2026
5b75462
🚧 Give the Transcript the entry's reading, windowed and measured — la…
taras Oct 9, 2026
3d84196
🚧 Close the four claims the handback had left open
taras Oct 9, 2026
7f185e4
🚧 Finish C2 and L3's ordering, and correct a claim that was wrong
taras Oct 9, 2026
ce0ce3a
🚧 Give the narrow outlet the reading it routes a reader to
taras Oct 9, 2026
8c9543a
🐛 R1–R3 from the Planner's independent review
taras Oct 9, 2026
94a36e9
🚨 Take the scratch out of the product commit, and make K1 a control
taras Oct 9, 2026
ea95cca
🐛 R4: wrap a question's preview, so its tail is somewhere to scroll to
taras Oct 9, 2026
a49f282
✅ Check the badge alignment where it is drawn, not where it is composed
taras Oct 9, 2026
3815aef
✅ Check the rails and the one-badge rule in cells too
taras Oct 9, 2026
a85c36c
🚨 Drop the redundant scopes oxlint refuses in the lifecycle suite (#881)
taras Oct 10, 2026
ecca5c4
🐛 Keep a fatal failure fatal when the body failed too (#881)
taras Oct 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ packages/web/generated/
# that walked one would report on a file nobody wrote.
site/*.timestamp-*.mjs
.bench/

# Session scratch: a test log and the break-control record are evidence, and
# evidence belongs under .reviewer/ rather than in the product commit.
.cliall.log
.cli.log
.controls.json
.controls.py
52 changes: 52 additions & 0 deletions architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -2821,6 +2821,53 @@ actions is closed so every one is journalable.
</RefreshLogin>
```

### 1a. One element's expansion can be surrounded

`Component.expand` offers an observer the whole of one executable element's
expansion: before resolution and props validation, through the accepted body
and its owned cleanup, to the acceptance of what it produced. Core issues a
request and the public chain composes around it; a handler delegates that exact
request once with `next(request)`.

The authority is the shape bound execution already uses. The request is branded
with a private field, so a copy carrying the same members is not it; the claim
is spent once, so a repeated delegation runs nothing; and the settlement is
readable whether or not the chain returned normally, so a handler that catches
what canonical expansion raised has not rescued it. A handler that returns
without delegating refuses the work, and what a handler returns is ignored.

Observation reaches no outcome. Phases carry `Result<void>` and nothing else —
what the element produced, what it bound and which error identity core is
holding stay private — and a failure is reported as a detached, frozen `Error`
preserving the selected name, message and explanatory causes. Completion is
published only after the whole dispatch, middleware cleanup included, has
unwound and canonical acceptance is reconciled.

Each subscription is registered with the element's latest phase and then told
each change in order, ending after exactly one terminal observation. A reader
that is slow, absent or cancelled delays nothing and cancels nothing: execution
never waits for an observer, and a consumer that must see the terminal phase
belongs to an owner outliving the dispatch rather than to the handler's frame.

Every path that expands something an author wrote crosses this once: the
component paths, and the structural constructs `<If>`, `<Each>`, `<Loop>`,
`<All>` and `<Switch>`, plus the selected `<Case>`, the `<Else>` that runs and
each `<Spawn>` an `<All>` starts. An unselected branch expands nothing and is
told nothing.

### 1b. Where a text's elements are, without running it

`inspectSource(text, kind)` answers where each recognized element's delimiters
are and what it is called. It is the scanner's own reading, collected by the
same walk that decides what a fence, an inline code span, a quoted `>` and a
tag-like expression are, so there is no second grammar to disagree with the
first — and a nested collector is committed only with the parse that found it.

A document's body boundary is read lexically, matching the installed
extractor's envelope rule without calling its value parser, so inspecting a
document interprets neither its header nor its body. Definition parsing asserts
the two boundaries agree.

### 2. Decide once

An error no middleware handles is decided exactly once, where it is raised,
Expand Down Expand Up @@ -5336,6 +5383,8 @@ Status is measured against main.
| `<Output>` region `output` mode | an undecided error fails the document execution | built on main |
| `<Output>` rendering selection | chooses which regions of a body render, and buffers a root that declares one; it decides nothing about failure | built on main |
| `Expansion` / `getExpansion()` | describes the current logical element expansion | built on main |
| `Component.expand` | surrounds one element's complete expansion — before resolution, through the accepted body and its owned cleanup, to the acceptance of what it produced. Core issues a branded request the public chain composes around; a handler delegates it once, may refuse the work by throwing or by not delegating, and decides nothing about the outcome. Phases carry `Result<void>` and a detached, frozen report, so no canonical identity, binding or resource crosses. Completion follows the whole dispatch, middleware cleanup included. Each subscription is registered with the latest phase and then told each change in order, ending after one terminal observation; a slow, absent or cancelled reader delays and cancels nothing | built on this stack |
| `inspectSource()` | answers where a text's executable elements are written and what they are called, without resolving, compiling, evaluating or running any of it. The scanner's own reading, so fences, inline code, quoted delimiters and tag-like expressions are decided once; a document's body boundary is read lexically, without the frontmatter value parser, and definition parsing asserts the two agree | built on this stack |
| document targets | catalogs a root document's addressable static headings, resolves one selector to one exact target, and projects the document to it before expansion | built on the #412 stack |
| document-aware `xmd run … --help` | describes what one document declares and every target it addresses, each as a full document reference with the description its section states, by inspection alone | built on the #463 stack |
| standard-input root documents | `xmd run -` and `xmd run -- -` read the whole root document from standard input, once, to end of file, and run it through the ordinary run profile. Fixed grammar selects it — the explicit `run` command form plus a document argument that is exactly `-`, read from the parser's own unconsumed remainder so a `-` another option took as its value is not one — and every other spelling keeps the meaning it had: the shorthand `xmd -` executes the file named `-`, `xmd run -#Section` executes that file's `Section`, another command's `-` is that command's, and `--eval -` keeps its refusal. `-` is the one filename the option grammar leaves unwritable, so the reference grammar reaches it and nothing else beginning with `-` is read as a document. The parsed path and every recovered reference stay separate facts until the grammar is settled, so a command line naming two roots refuses in either order, before the read and before either candidate is inspected. The reader is a value each runtime-named entrypoint supplies and the shared CLI never reaches a stdin global; what comes back is `retainedSource("<stdin>", source)`, adding no root-source variant, constructor, digest member or public API. The complete input is acquired before inspection, provider setup, the secret-detection announcement, journal creation, root admission and execution, inside the run's existing deadline; a failed read is one fixed sentence carrying no host error, input or path, and cancellation tears the reader down without becoming one | built on this stack |
Expand Down Expand Up @@ -5696,6 +5745,9 @@ DurableEvents -> frozen ReplModel -> resolved immutable view
| `description.ts` | opaque immutable descriptions and the closed action boundary |
| `reconcile.ts` | reconciling descriptions into one mounted Freedom tree |
| `handoff.ts` | the acknowledged commit boundary |
| `source-reading.ts` | one entry as output and then source, with what each element is doing |
| `presentation-text.ts` · `presentation-style.ts` | what the characters of a reading are, and the colours it is drawn in |
| `fitting.ts` | engine-measured widths, and the rows a reading is cut into |
| `layout.ts` | deterministic placement at four sizes |
| `renderer.ts` | drawing the mounted tree, and the frame map |
| `frame.ts` | the one acknowledged frame stream |
Expand Down
23 changes: 22 additions & 1 deletion packages/cli/src/repl/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ import type {
AgentPromptPublisher,
ExecutionInstallation,
} from "@executablemd/core/host";
import type { ReplWaits } from "./lifecycle.ts";
import { DurableContext } from "@executablemd/durable-streams";
import type { DurableEvent } from "@executablemd/durable-streams";

Expand Down Expand Up @@ -426,7 +427,18 @@ function remove(attachment: Attachment, request: LiveRequest): void {
* so no one is woken. No teardown path appends a record, writes an audit or
* closes a coroutine.
*/
export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
export function useReplAgent(
mode: PermissionMode,
/**
* Where a pending request says the element asking for it is waiting, or none.
*
* Optional because the authority is complete without it: counting a wait is
* something a reading wants, not something deciding a permission needs. A
* kernel given none counts nothing, which is what an execution with no
* session reading behaves like.
*/
waits?: ReplWaits,
): Operation<ReplAgentKernel> {
return resource(function* (provide) {
const changes = createSignal<ReplAgentReading, never>();
/**
Expand Down Expand Up @@ -642,6 +654,15 @@ export function useReplAgent(mode: PermissionMode): Operation<ReplAgentKernel> {
// Decided before anything is published: an unowned request publishes no
// reading and no key, and never becomes a denial.
const held = owner(attachment, yield* currentCoroutine());
// The element this request is being asked on behalf of is waiting from
// here until somebody answers. Released in `ensure`, so abandoning the
// request at teardown releases the wait without inventing a decision —
// and so a request that is never answered leaves no element reading as
// waiting after its scope has gone.
const release = waits === undefined ? undefined : yield* waits.hold("permission");
if (release !== undefined) {
yield* ensure(release);
}
return yield* action<PermissionOutcome>(function (resolve) {
let settled = false;
const live: LiveRequest = {
Expand Down
Loading
Loading