Skip to content

Commit 9fe494d

Browse files
authored
Show live component lifecycle and generated source in the REPL (#881, 2/3) (#886)
* 🚧 Surround one expansion with its canonical observation (#881 PR 2, layer 1a) Core issues a branded request before an element is resolved, the public `Component.expand` chain composes around it, and exactly one delegation of that exact request runs the work. The terminal keeps the canonical outcome in private state, so a handler that catches what canonical expansion raised does not rescue it, and one that returns without delegating refuses the work. Phases are published to each subscriber as its latest observation plus ordered changes, so a late reader is told where the element is rather than walked through a history it missed, and a slow or absent reader delays nothing. The invocation boundary now tells an observer when the body's own work ended, before its ordered teardown runs, which is the EXIT a reader watching a destructor needs. Work in progress: ACTIVE publication and the structural paths follow. * 🚧 Tell an element's observers where it has reached (#881 PR 2, layer 1b) ACTIVE where accepted work starts, in all three body paths, and EXIT with its reason when that work ends — before the invocation's ordered teardown, which is the stretch a reader watching a destructor is interested in. COMPLETE still waits for the whole dispatch to unwind. The first observation tier drives real expansions through the public chain: the ordered sequence, a failed body's detached report, an element resolution refused, two readers, a reader that never reads, a handler that returns without delegating, a counterfeit request, a repeated delegation, a caught canonical failure that stays failed, and a late reader told where the element is rather than replayed. Writing it found the hazard the contract warns about: a subscription spawned on the handler's own frame dies when dispatch unwinds, which is before COMPLETE is published. Subscriptions belong to an owner that outlives the dispatch, and the tier's harness now shows that. * 🚧 Say where a source text's elements are, without running any of it (#881 PR 2, layer 2) `inspectSource(text, kind)` answers where each recognized element's delimiters are and what the author called it. It is the scanner's own reading — 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 rule without calling its value parser: a byte-order mark, `---` on its own line, `----` that is not one, an optional header language, the first closing line, and the newline after it. `parseSource` asserts the two agree, so a divergence is loud rather than a quiet offset drift. Probed against the engine over BOM, CRLF, a header language, an absent closing delimiter and a `---` inside a list: every case agrees, and the one the value parser refuses is the one inspection reads without it. The tier cuts every range out of the exact input it was given, and checks that what inspection reports is what the engine's own scan recognized — including an incomplete passive tag, which is prose, and leaves no guessed children. * 🚧 Classify the new expansion seam as a gate (#881 PR 2, layer 3a) `Component.expand` surrounds an element's whole expansion, which makes it a step in the walk that reached it rather than a walk of its own. Counting it as a walk would make every element a separate expansion to pause and release, and a structural element crossing the seam would release the hold its enclosing walk is still standing in. The boundary inventory test caught the new member the moment it existed, which is what it is for. * 🚧 Surround structural work with the same observation (#881 PR 2, layer 1c) `<If>`, `<Each>`, `<Loop>`, `<All>` and `<Switch>` each cross the seam once, through one shared helper rather than five copies of the reconciliation. The selected `<Case>`, the `<Else>` that actually runs and every `<Spawn>` an `<All>` really starts are elements of their own; an unselected branch expands nothing and is told nothing. Structural syntax has no separate body to enter, so ACTIVE is published where its region begins and EXIT when the region ends, with cancellation assumed until the work says otherwise. The tier now counts boundaries per construct, checks that exactly one terminal observation reaches each, and shows an unchosen `<Case>` and the component inside it were never elements at all. Two cleanup cases pin EXIT to the stretch a destructor runs in, and a failing destructor to a failed completion. The whole Core suite passes: 440 tests, 3253 steps. * 🚧 Keep what this process is doing, element by element (#881 PR 2, layer 3b) A private lifecycle module holds the session's reading. Each actual call of an element gets a key of this entry's own, so a document that writes one element twice is two calls and a repeated logical id never overwrites anything; the enclosing key is bound *around* the delegation, following the controller's own `CurrentWalk` composition, so descendants know which call they are in while their capability environment stays as narrow as it was. Consumers run on the session's owner as siblings of the dispatch. That is not a preference: Core publishes the terminal phase after the dispatch unwinds, so a consumer owned by the handler would be gone before it was told, and one the handler joined would wait for itself. A question counts a wait against the element that is asking it, read from the lineage where the wait is taken, so two concurrent questions are two waiting elements rather than one session that is "busy". Ordinary suspension counts nothing. ACTIVE is now published where resolution and validation accept the element, which is where the accepted work starts on every body path — a refusal above it is an element that never became active at all. The session's liveness release moved to the entry's own finalizer, so `live` turns false after the execution, the output consumer and the Agent attachment have all released, and the completion wake is announced even when nothing was appended. Tier L drives real sessions: repeated calls, nested lineage, a counted question wait that stops when answered, ordinary work counting nothing, an entry that takes its reading with it, and observed and unobserved runs rendering the same thing. * 🚧 Carry the reading to the screen, and say so in the specs (#881 PR 2, layers 3c/4a) The live reading reaches the view through the path the Agent reading already takes, and a frame frozen at a recorded position leaves it out — stated once in `viewFor`, beside the other live halves it already suppresses, rather than decided again at each surface. A held expansion counts a wait of its own, so an element standing at a boundary reads as waiting rather than as slow. Both window controls are spelled `[↑ earlier]` and `[↓ later]` everywhere a reading has a window. The semantic actions and activation are unchanged and no shortcut is introduced; the three suites that read those labels off a real terminal pass unchanged otherwise. architecture.md gains the two Core contracts and their modules; specs/executable-mdx-spec.md gains §5.7 expansion observation and §5.8 source inspection; specs/repl-spec.md gains what this process is doing element by element — entered, active, waiting, exited, settled — and that none of it is written down, reconstructed cold or shown at a frozen position. * 🚧 Read an entry as output then source, fitted by the engine — layer 4a/4b The reading itself: `source-reading.ts` turns one model entry into Output then Source, and `fitting.ts` cuts that reading to a measured width by asking the engine how wide text actually is. Static syntax acquires no phase. An element reads as observed only where an observation was attributed to it — this entry, its recorded source position and the fragment that position belongs to — so a retained prefix shows the rails that mean nothing was observed, which is the truth about it. Generated source replaces its producer inside the enclosure bytes the author wrote, and keeps its own offsets as a child region rather than being spliced: splicing would renumber the positions its own elements were recorded against. A refused, absent or ambiguous admission keeps the producer. Wrapping is engine answers all the way down. A UTF-16 code unit is not a cell, so every width is measured and every cut is at a grapheme boundary; the rail, the gap and the status column are reserved from measurements rather than from character counts, with the cleanup wait's two readings setting the column so a phase change cannot reflow somebody's document. A region with no room for one grapheme is a frame refusal, never a clip. S1–S4 in `repl-source-reading.test.ts`: 33 steps, every width from a real engine. Two defects it found and this fixes — a self-closing element lost the badge it had settled with, and badges align at the right inner edge rather than at a shared start column. The archive's `L` and `RAIL` constants join the independent reference fixture. * 🚧 Give the Transcript the entry's reading, windowed and measured — layer 4 The transcript pane now holds one entry's reading: Output, then Source, sharing one vertical window with its own earlier/later controls outside the moving content. Every row is fitted by `prepareFrame` from engine answers, carries its rail and, where an observation was attributed to it, the reading of the element that owns it against the pane's right inner edge. The defect this found in itself: `ReplMeasured.boundsOf` closes over engine state, not over a copy, so reading it after the reading's probe render answered from the probe tree. Every window's capacity came out wrong — the question drawer was admitted shorter than the one on screen, a form field lost its Tab stop, and the journey typed two answers into one field. Every bound this pass gives is now copied into a map before anything else is measured, which is what `planning-measurement.md` said to do and what the existing passes already did by accident of ordering. Also found: a self-closing element lost the badge it settled with; badges align at the right inner edge, not at a shared start column; and the reading's two controls have to be reserved in the pass that measures the window, or the one it grows is drawn over the reading's last row. The replaced presentation premise, itemized. The transcript's record rows are gone, so fourteen assertions across five suites were re-anchored onto what the product now says — each with the claim it is still making and why its old anchor cannot carry it: - a recorded result, an outcome, a reason and live text read off the reading's Output half rather than off a row keyed by the record's position; - a wrapped reason is recovered from its rows instead of ending in an ellipsis, which is the stronger claim; - a fact about an event is read where one still is, and metadata that was a transcript record is the catalog's or the Bindings column's; - a nested scope's retained answers are read by selecting that scope, which is where the product says they were asked; - two journey probes that searched the screen for a question's words now assert the absence of a question and click inside the drawer that is asking — the reading shows the schema those words were written in, so the words were never the claim. `repl-source-reading.test.ts` carries S1–S4, 33 steps, every width from a real engine. The frozen focused command: Core 53 tests / 404 steps, CLI 94 / 310, both exit 0. `deno check --frozen` clean, `git diff --check` clean. Specs and architecture say what exists: the reading, its four output readings, observation versus syntax, generated replacement, and that nothing here is clipped or estimated. * 🚧 Close the four claims the handback had left open **A pending permission is a waiting element.** The authority holds a `"permission"` wait on the element the request was asked inside, released in `ensure` — so a request abandoned at teardown releases its wait without inventing a decision nobody made. L2's third reason is now counted like the other two, and two cases in `repl-agent-execution.test.ts` assert it on the `<Prompt>` whose turn owns the request, not on the session. **Each call of one element is its own reading.** Several occurrences at one authored position — what a `<Loop>` produces — are read in the order they were observed, each in its own source group with its own badge and its own rail. Showing only the last said the earlier calls never happened. **A continuation keeps its own column.** A wrapped line's later rows are set in to the whitespace its first row was written at. Without it a continuation sat further left than the row it continues and read as a new, shallower line — which is a claim about structure the source does not make. Display only: the line is still recovered exactly from its rows. **C3 and C4 are completed.** An observed run is compared with an unobserved one byte for byte, not screen for screen: same elements, same identities, same positions, same rendered result. A reader that takes one phase and stops, and a reader whose scope is cancelled mid-expansion, each delay and cancel nothing. Three new controls join the five: don't hold the permission wait; read only the last call; start a continuation at the margin. All eight break their frozen suites and restore byte-identically. Core 53 tests / 407 steps, CLI 108 / 363, both exit 0. `deno check --frozen` clean, `git diff --check` clean. The spec says all three of the new behaviours. * 🚧 Finish C2 and L3's ordering, and correct a claim that was wrong **C2's remaining rows.** An import that returned is not an element that finished: while the body is held the element is entered and has no terminal phase at all. A printed failure — a return the schema refuses, or a body whose component prints its own errors — completes `Ok` and puts the problem in the document, which is the frozen "preserve printed semantic failure" and exactly why a reading takes its outcome from the phase and never from the prose. Recovering a child's failure does not fail the parent, with the unrecovered case beside it so the assertion is not about a parent that can never fail. COMPLETE follows the whole dispatch, stated as the thing that ordering decides: a subscription on the handler's own frame never sees it and one on an owner that outlives the dispatch does. Three of those four assertions were first written as guesses and two were backwards. A probe against the real engine replaced them: the product completes a printed failure `Ok`, and the drain order of an asynchronous consumer cannot witness a publication order. **L3's ordering.** An installation of the entry's own reads, at the moment it is released, what the session was still saying about itself — so the claim that everything the entry held comes down before `live = false` and the ready wake is observed rather than read off the comment that states it. Plus the wake arriving with no input, and a late update failing to revive a reading the entry took. **A correction.** The last handback reported that a fence-admitted generated fragment's source is not substituted. There is no such fragment: `components/Evaluate.ts` is the only caller of the generated-XMD admission, so `<Evaluate>` is the only element that makes one and the frozen rule covers every admission there is. The claim is withdrawn, not explained. K9 joins the controls: say the entry is not live before releasing what it held, and L3's ordering case fails. All nine break and restore byte-identically. The frozen focused command, run exactly as written: Core 53 / 411 steps, CLI 94 / 313, both exit 0. `repl-lifecycle` and `repl-agent-execution` are reported separately at 14 / 52 rather than folded into it. * 🚧 Give the narrow outlet the reading it routes a reader to The Entries surface is the catalog *and* the reading of the entry it has selected. A narrow frame has one column for both, so both are in it: the catalog takes its own height while it fits and its share when it does not — exactly the rule the sidebar already uses — and the reading takes what is left. This is not a routing change. Which surface a narrow frame shows is still the route's, and the footer still keeps its seven rows. What changes is that the Entries outlet shows what the Entries surface *is*: routing a reader to the surface their entry is on and then showing them nothing of it was the defect, and every 72×20 capture in the gallery showed it. `UI13` now asserts the reading is placed at all three profiles, in whichever region the profile has for it — `content` at narrow, `transcript` above it. Its old narrow leg asserted the opposite, that no row is placed there at all; that premise is what this replaces, and the claim it was making (the reason is drawn inside its own region, footer untouched) is unchanged and now made at every size rather than two. `TL6` skips a covered line that is only a window-control label. Both the outlet and the drawer spell `[^ earlier]` the same way, so finding one inside the drawer's rectangle says the drawer has its own rather than that something behind it showed through — a false positive this slice created by giving narrow a second pair of controls, not a coverage defect. K10 joins the controls: route a narrow frame to Entries and show it no reading. All ten break and restore byte-identically. Frozen focused command as written: Core 53 / 411, CLI 94 / 313, both exit 0; `repl-lifecycle` + `repl-agent-execution` beside it at 14 / 52. * 🐛 R1–R3 from the Planner's independent review **R1 — a terminal carried a request its invocation did not issue.** The brand check proved a request was Core's and unspent; nothing proved it was *this* element's. An authentic, unconsumed request from another live invocation, delegated through this one's `next`, reached the claim — it spent that element's one claim and ran this body, and the refusal that followed came after the effect. Each terminal now compares the delegated request with the one it issued, by identity, before the claim and before any body. The Planner's probe went from `bodies:["A"]` to `bodies:[]`. **R2 — the lexical envelope disagreed with the installed extractor**, and the assertion in `parseSource` turned that into a refusal of documents the engine accepts. I wrote the helper from the delimiter prose; the real rule came from comparing against `matter()` over twenty-four envelope shapes. A BOM is never body even with no header; an opener with no closing line consumes the whole text; a closing line *starts with* `---` and the body begins immediately after those three characters, plus one line ending if one is there. `SI2: an unterminated header is not a header` asserted the opposite of all of this and is corrected: that text is a header with no body, and its value parser refuses it outright, so inspection claiming an executable element inside a document nothing will run was the worse of the two answers. A new case checks offset *and* suffix against `matter()` across every shape. **R3 — published observations were mutable.** One phase object reaches every subscriber and is retained as the latest, so a reader could rewrite a terminal phase to `active` and a late reader would be told that. Phases are frozen before publication, and an AggregateError's `errors` array is frozen as well as the error around it. Also corrected, as the review requires: `renderer.measure` already returns a frozen copy of each bound, so the handback's claim that `boundsOf` closes over live engine state was wrong. The geometry snapshot it justified is removed — the journeys pass without it, which is the test that says it was never the fix. K11–K13 join the controls, one per finding. All thirteen break and restore byte-identically. Frozen commands: Core 53 / 414, CLI 94 / 313, supplemental 14 / 52, all exit 0. * 🚨 Take the scratch out of the product commit, and make K1 a control The Planner's journey review found three things beyond R1–R3, all mine. **A 2.8 MB test log was committed into the branch.** `.cliall.log` entered at `108fa00c` and `.controls.json` at `a210e4eb`; `git diff --check c7d5476..HEAD` exits 2 on the log's trailing whitespace, so the branch-level whitespace gate has been failing while the working-tree check I was running said clean. An empty working tree does not check the committed change. Both are gone and `.gitignore` now names them: a test log and a control record are evidence, and evidence belongs under `.reviewer/`. **K1 was not the control it said it was.** It claimed "remove the rails" and recoloured one — and when I made it actually remove them, it came back UNBROKEN. Nothing asserted a rail reaches a row: every case read `line.rail`, which is the model's answer, so the reading could have lost its rails entirely and the suite would have passed. `repl-source-reading.test.ts` now checks that every composed row opens with the rail glyph, under the rail role naming the region the row is in, with an observed rail among them so painting every row alike does not satisfy it. K1 removes the rails and fails; the recolouring mutation stays as K1b, named for what it does. **A control record kept only the matcher line.** `expect(received).toBe( expected)` says nothing about what broke. The runner now preserves the whole first failure, so K1's record reads `- "│" + " "` against the row key. Fourteen controls, all breaking and restoring byte-identically. Frozen commands: Core 53 / 414, CLI 94 / 314, supplemental 14 / 52, all exit 0. * 🐛 R4: wrap a question's preview, so its tail is somewhere to scroll to The re-review closed R1–R3 and found one blocking defect left. A question's message was split at its newlines and each logical line became one drawer row. A drawer row is one row — so a long line had nowhere to put its tail, and scrolling could only ever reveal rows that were prepared. At 72×20 the README confirmation showed `"summary": "A lightweight workspace for coordinati` and no continuation. Approve was still reachable, which is why reaching the controls never caught it. The preview is now fitted by the same engine-measured wrapping the reading uses, at the drawer's own width, in `prepareFrame` beside it. Its rows are what the description places, what the window counts and what a scroll moves through. `fitPlain` is the reading's cutting without the rail and status reservation: read-only text in a box uses all of the box. `drawerContentRows` is deleted. The review cited it as counting logical lines, which it did — and nothing called it. A dead function stating a rule the product no longer follows is worse than no function. The regression took two corrections worth keeping. Asserting on description labels proved nothing: the label always held the whole line, and the loss happened at the row. Asserting that the decisions stay reachable proved nothing either — the reviewed build satisfied that. What discriminates is that no message row is wider than the drawer it was measured for, together with every logical line being recoverable from the rows it was cut into. K14 restores one row per logical line and fails on `drawer:message:4` overflowing. Fifteen controls, all breaking and restoring byte-identically. Core 53 / 414, CLI 94 / 315, supplemental 14 / 52, all exit 0. * ✅ Check the badge alignment where it is drawn, not where it is composed Self-audit, prompted by the shape of what the reviews keep finding. Twice now a control was vacuous because the assertion behind it read the model where the claim was about the screen: K1 read `line.rail` and never asked whether a rail reached a row; K14's first draft read a description's label, which always held the whole line while the drawn row lost its tail. Badge alignment had the same shape. "All lifecycle glyphs/words align at the measured source pane's right inner edge" is a frozen requirement, and everything asserting it read the composed run strings — this terminal's own answer about itself. Nothing checked a cell. P1-T3 now commits a frame with one observed element and finds the row that actually says `● ACTIVE`: its last cell is the transcript interior's last cell, it is drawn in the archive's own literal for that phase, and the source beside it keeps its own colours. K15 shortens the pad by three columns and it fails at 121 instead of 124. Two fixture details the check needed, both real product behaviour: a fence is not an element the scanner reports, so observing at one matches nothing; and an occurrence whose position names no path is not matched to an entry's own source, which is what keeps another document's position from claiming a row here. The control runner now writes itself and its record into the review packet, so a stale copy cannot describe a different list than the one that ran — it did twice while I was adding to it. Sixteen controls, all breaking and restoring byte-identically. Core 53 / 414, CLI 94 / 316, supplemental 14 / 52, all exit 0. * ✅ Check the rails and the one-badge rule in cells too Finishing the audit the last commit started. Three frozen visual claims were asserted against the model rather than the screen; one is now cells, and these are the other two. **Every reading row opens with its rail, in cells**, in the rail's own ink, with the observed region drawn in the archive's `active` literal so painting every row alike does not satisfy it. K1 checks the composed runs; nothing checked what the terminal drew. **Exactly one row says the badge.** "Only the first visual row carries each badge" is frozen: a second would say the phase changed between two halves of one element. K16 puts the badge on every row of its delimiter — and came back **UNBROKEN**, because the fixture's tag fitted on one row. A rule about continuation rows cannot be tested by an element that has none. The observed element is now long enough to wrap, and K16 fails. That is the third vacuous control this slice has produced, all the same shape: an assertion that cannot reach the thing it claims. K1 read the model where the claim was the screen, K14's first draft read a label where the loss was at the row, and K16 read a fixture that could not exhibit the defect. Each was found by making the control honest, never by reading the code. Seventeen controls, all breaking and restoring byte-identically. Core 53 / 414, CLI 94 / 316, supplemental 14 / 52, all exit 0. * 🚨 Drop the redundant scopes oxlint refuses in the lifecycle suite (#881) * 🐛 Keep a fatal failure fatal when the body failed too (#881) An ordinary body failure hid a `DurablePersistenceError` or a Files fatal raised while expansion middleware unwound: `observeExpansion` reported and threw the canonical settlement before looking at what the dispatch raised on its way out, so `durabilityFailure()` found nothing and enclosing reconciliation read the run as merely incorrect rather than unable to persist. Reconcile the two halves after the whole dispatch has unwound, in the order `expand.ts` already ranks bound failures: durability, then a Files fatal, then the canonical failure over a later ordinary middleware one. The earlier canonical failure is still preferred within each fatal kind, and middleware still cannot rescue a canonical failure. The selected failure is both what is thrown and what the one detached terminal report conveys.
1 parent 160e354 commit 9fe494d

40 files changed

Lines changed: 7373 additions & 1084 deletions

‎.gitignore‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,10 @@ packages/web/generated/
2727
# that walked one would report on a file nobody wrote.
2828
site/*.timestamp-*.mjs
2929
.bench/
30+
31+
# Session scratch: a test log and the break-control record are evidence, and
32+
# evidence belongs under .reviewer/ rather than in the product commit.
33+
.cliall.log
34+
.cli.log
35+
.controls.json
36+
.controls.py

‎architecture.md‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2821,6 +2821,53 @@ actions is closed so every one is journalable.
28212821
</RefreshLogin>
28222822
```
28232823

2824+
### 1a. One element's expansion can be surrounded
2825+
2826+
`Component.expand` offers an observer the whole of one executable element's
2827+
expansion: before resolution and props validation, through the accepted body
2828+
and its owned cleanup, to the acceptance of what it produced. Core issues a
2829+
request and the public chain composes around it; a handler delegates that exact
2830+
request once with `next(request)`.
2831+
2832+
The authority is the shape bound execution already uses. The request is branded
2833+
with a private field, so a copy carrying the same members is not it; the claim
2834+
is spent once, so a repeated delegation runs nothing; and the settlement is
2835+
readable whether or not the chain returned normally, so a handler that catches
2836+
what canonical expansion raised has not rescued it. A handler that returns
2837+
without delegating refuses the work, and what a handler returns is ignored.
2838+
2839+
Observation reaches no outcome. Phases carry `Result<void>` and nothing else —
2840+
what the element produced, what it bound and which error identity core is
2841+
holding stay private — and a failure is reported as a detached, frozen `Error`
2842+
preserving the selected name, message and explanatory causes. Completion is
2843+
published only after the whole dispatch, middleware cleanup included, has
2844+
unwound and canonical acceptance is reconciled.
2845+
2846+
Each subscription is registered with the element's latest phase and then told
2847+
each change in order, ending after exactly one terminal observation. A reader
2848+
that is slow, absent or cancelled delays nothing and cancels nothing: execution
2849+
never waits for an observer, and a consumer that must see the terminal phase
2850+
belongs to an owner outliving the dispatch rather than to the handler's frame.
2851+
2852+
Every path that expands something an author wrote crosses this once: the
2853+
component paths, and the structural constructs `<If>`, `<Each>`, `<Loop>`,
2854+
`<All>` and `<Switch>`, plus the selected `<Case>`, the `<Else>` that runs and
2855+
each `<Spawn>` an `<All>` starts. An unselected branch expands nothing and is
2856+
told nothing.
2857+
2858+
### 1b. Where a text's elements are, without running it
2859+
2860+
`inspectSource(text, kind)` answers where each recognized element's delimiters
2861+
are and what it is called. It is the scanner's own reading, collected by the
2862+
same walk that decides what a fence, an inline code span, a quoted `>` and a
2863+
tag-like expression are, so there is no second grammar to disagree with the
2864+
first — and a nested collector is committed only with the parse that found it.
2865+
2866+
A document's body boundary is read lexically, matching the installed
2867+
extractor's envelope rule without calling its value parser, so inspecting a
2868+
document interprets neither its header nor its body. Definition parsing asserts
2869+
the two boundaries agree.
2870+
28242871
### 2. Decide once
28252872

28262873
An error no middleware handles is decided exactly once, where it is raised,
@@ -5336,6 +5383,8 @@ Status is measured against main.
53365383
| `<Output>` region `output` mode | an undecided error fails the document execution | built on main |
53375384
| `<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 |
53385385
| `Expansion` / `getExpansion()` | describes the current logical element expansion | built on main |
5386+
| `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 |
5387+
| `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 |
53395388
| 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 |
53405389
| 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 |
53415390
| 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 |
@@ -5696,6 +5745,9 @@ DurableEvents -> frozen ReplModel -> resolved immutable view
56965745
| `description.ts` | opaque immutable descriptions and the closed action boundary |
56975746
| `reconcile.ts` | reconciling descriptions into one mounted Freedom tree |
56985747
| `handoff.ts` | the acknowledged commit boundary |
5748+
| `source-reading.ts` | one entry as output and then source, with what each element is doing |
5749+
| `presentation-text.ts` · `presentation-style.ts` | what the characters of a reading are, and the colours it is drawn in |
5750+
| `fitting.ts` | engine-measured widths, and the rows a reading is cut into |
56995751
| `layout.ts` | deterministic placement at four sizes |
57005752
| `renderer.ts` | drawing the mounted tree, and the frame map |
57015753
| `frame.ts` | the one acknowledged frame stream |

‎packages/cli/src/repl/agent.ts‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import type {
7272
AgentPromptPublisher,
7373
ExecutionInstallation,
7474
} from "@executablemd/core/host";
75+
import type { ReplWaits } from "./lifecycle.ts";
7576
import { DurableContext } from "@executablemd/durable-streams";
7677
import type { DurableEvent } from "@executablemd/durable-streams";
7778

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

0 commit comments

Comments
 (0)