Commit 14ebdf1
authored
* ✨ Reconcile the REPL component tree
Slice C of #848: the keyed composition kernel. A parent describes its complete
child set; that set reconciles into one mounted Freedom tree; a normalized
event walks its target's own ancestry and comes back as a typed action. No
terminal, no bytes, no cell geometry, no command — Slices D and E consume this.
`description.ts` is the contract. A description is an immutable value with a
sibling key, a component-type identity, the component's own input, the
placement its parent chose, and its keyed children. Identity is parent, key and
type, so redescription keeps a node and a different key or type replaces it;
position is not identity. There is no module-scoped registry: what a mounted
component can do lives in its own node's `NodeData` and goes away exactly when
the node does.
A component is two halves, and the split is what makes a commit mean something
exact. `attach` is synchronous and runs *inside* the commit, so the moment a
reconciliation is acknowledged every described node already renders what it
renders and claims what it claims; it may return a teardown, which the commit
registers, so a node removed before any ongoing work had a turn still tears
down once. `lifetime` is the node's ongoing work in its own scope. A task
starts a turn after it is spawned, so nothing that has to be true at commit
time can live there — and for the same reason a redescription is delivered to
`onInput` inside the commit rather than published on a stream a subscriber may
not have reached yet.
`reconcile.ts` validates the whole desired set, over every descendant, before
touching anything: a reconciler that mutated as it walked would leave a tree
that is neither the old one nor the new one when it met a duplicate key at the
end. Then it adds and updates before removing, so a replaced region is never
momentarily empty and focus always has a survivor to land on; then it sets
canonical order from the described positions. Removal runs the branch's own
teardown and awaits Freedom's. Focus is derived from what is mounted rather
than remembered across commits.
`handoff.ts` is how a description reaches the tree and how its author learns it
arrived. `next()` resolves after that exact set committed — not a tick later,
not when a queue accepted it. Cancelling abandons an uncommitted offer without
touching the tree, and teardown refuses a later offer rather than leaving it
waiting for a worker that is gone.
Freedom is vendored from `bombshell-dev/playground@8be97e72`, nine sources and
the upstream notice, as a pristine copy and a patched copy. `MANIFEST.json`
records a SHA-256 for both copies of every file; the drift test refuses an
extra, missing or changed file, proves every unpatched production file equals
upstream byte for byte, localizes each difference to one named patch, and
proves production imports the patched copy from exactly one module and the
pristine copy from none.
Two patches, each with the pristine behaviour kept as its permanent control.
`useRoot()` parents the tree to the caller's scope so it inherits that scope's
contexts and dies with it — upstream has no such surface and its root is
parented to Effection's global. Focus removal decides by subtree containment
rather than by identity, because a drawer closes by removing the branch *above*
the focused control, which upstream leaves focused on a node about to be
destroyed.
The package's manifest asks for `effection@4.1.0-alpha.9`; this repository
stays on stable 4.1.0 and the suite re-proves compatibility rather than
inheriting the claim. The upstream notice is MIT while the package metadata
says ISC; provenance records both and resolves neither.
Five review corrections.
A description is now issued, never written. `describe()` and `component()` are
the only ways to make one, and what each holds is behind a private field — so a
plain object of the right shape is not a description, statically or at runtime,
and there is nothing on the outside of one to reach into. Input is detached as
it is read: copied into the description and frozen as the copy is built, so a
caller that mutates its own object afterwards, or while an offer is still
waiting, changes nothing, and nothing the caller still owns is frozen out from
under it. A component's input is view data, which is what lets the whole
pipeline be concretely typed — there is no `as` left in either module.
A modal branch is a real focus root. Modal intent is its own thing rather than a
member of the opaque placement a parent chose, and it is bound to the mounted
node: the push happens once the branch's own children exist, and the pop runs
when the branch goes, so the stack never keeps an entry for a node that does
not. While it is mounted, focus traversal cannot reach the tree underneath; the
same descriptions without the push walk straight out of it, which is the
control.
There is one way in. The normalized event is a discriminated union, so a key
event carrying a frame is not a value anybody can build, and Backtab arrives
through `dispatch` exactly as Enter, a pointer activation and Escape do — the
host-callable focus shortcut is gone, because a second input path would have
its own rules and a modal root would not contain it. The composition surface is
parameterized by the caller's own closed action union, and a dispatch answers
with a closed outcome: an action a node claimed, a focus move the tree owns, or
an event that reached no live node.
Cancellation owns an uncommitted offer. The commit runs in the calling
operation's own lifetime under a lock that is handed straight from one holder to
the next, rather than in a worker this module owns: an offer cancelled while
waiting its turn leaves the line and the tree never hears of it, and a holder
cancelled mid-commit still releases the lock, so nobody behind it is stranded.
`PROVENANCE.md` records that upstream carries an MIT notice and that the private
package's metadata says ISC, and states that this repository makes no
determination about which controls. The manifest's provenance digest was
recomputed and the drift evidence is green.
Second round of review corrections.
The construction surface is closed. `ReplDescription` and `ReplComponent` are
exported as types and not as values: there is no constructor to reach, no
static issuer and no reader that takes an arbitrary value and hands its
contents back, and `ReplDescribed` is no longer exported at all. `describe()`
and `component()` are the only construction, and what a reader can see of a
description is read-only. A surface test asserts the module's exact exports, so
an alternate issuer or unwrapper reappearing fails rather than being noticed
later. The three `as const` assertions are gone; the dispatch outcomes are
typed at their declarations instead.
A modal branch is inaccessible and its stack is safe. A pointer naming a live
node in the current frame is dropped when that node is outside the active focus
root, which is what makes the branch underneath unreachable rather than merely
covered. The stack is kept centrally and made equal to what is described, in
pre-order — an outer drawer is pushed before the inner one it contains and the
stack unwinds from the top — and a preserved node whose modal intent changes
pushes or pops without losing the lifetime it already has. Two modal roots on
branches neither of which contains the other are refused in preflight, because
there is no order to push or pop them in.
Reconciliation is serial and has an explicit boundary. Preparation is
synchronous: the tree reaches its new shape in one turn, so no cancellation can
leave it halfway. Retiring the branches it replaced is the tree's own work,
owned by the tree and cleared by the retirement itself — a caller halted while
waiting for it would otherwise put the slot back while the teardown was still
running, and the next commit, seeing nothing outstanding, would overlap it. The
one suspension before any mutation is the previous commit's retirement, which is
where the new controls cancel: the abandoned branch is never mounted, no
concurrent commit is released, nothing is left pending and a later offer
completes.
Third round of review corrections.
An issued handle is opaque, frozen and authentic. A description and a component
now carry no public members at all: what each holds is a private field, which
property access, enumeration and reflection cannot reach, and both the instance
and its prototype are frozen on the way out — so an `input` or an `attach`
shadowed onto one after issuance is refused rather than becoming what the tree
reads or runs. The composition implementation reads them through one seam of
its own, and that seam refuses anything this factory did not issue. `describe()`
brand-checks the component before issuing, so a plain object with the right
members, arriving through an untyped call, never reaches attachment.
Focus and modality are preflighted as one question. They were counted
separately, so a page asking for focus beside a modal passed validation and
then threw `Cannot focus a node outside the active focus root` — after
preparation had already mounted the whole new tree. A single explicit focus
claim that the innermost modal path does not contain is now an `Err` before
anything is attached, including a claim inside an outer modal but outside the
nested one it contains. The positive control is that a claim the innermost
modal does contain is still admitted and still wins focus.
The last immutability hole. The handles were frozen and the records behind them
were not, so the seam the reconciler reads through handed back a writable copy
of the component's definition and of the described record: `Reflect.set` could
replace the stored `attach`, and the already-issued component would then run it.
`readonly` is a statement to a compiler; the freeze is what makes it true at
runtime. Both records are frozen before they are stored, and the evidence now
reads them through that seam, walks everything they retain, proves each record
and list is frozen and each mutation refused, and mounts the already-issued
description to show the original input and original behavior still hold.
* ⚡ Remeasure test weights at 1019ea5
Measured on run 36293682586 at the accepted Slice C implementation head, and
committed unchanged. Its floors are 10 deno, 5 node and 3 bun against installed
counts of 15/10/5, so nothing the measurement says requires a shard count to
move. The totals sit below the previous measurement's; a floor is what the
weights make possible, not what the shard count should be.
1 parent 9c50a21 commit 14ebdf1
30 files changed
Lines changed: 5312 additions & 1075 deletions
File tree
- .agents
- packages/cli
- src/repl
- vendor/freedom
- patched
- lib
- upstream
- lib
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
15 | 15 | | |
16 | 16 | | |
17 | 17 | | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
18 | 25 | | |
19 | 26 | | |
20 | 27 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
9 | 9 | | |
10 | 10 | | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5307 | 5307 | | |
5308 | 5308 | | |
5309 | 5309 | | |
| 5310 | + | |
| 5311 | + | |
| 5312 | + | |
| 5313 | + | |
| 5314 | + | |
| 5315 | + | |
| 5316 | + | |
| 5317 | + | |
| 5318 | + | |
| 5319 | + | |
| 5320 | + | |
| 5321 | + | |
| 5322 | + | |
| 5323 | + | |
| 5324 | + | |
| 5325 | + | |
| 5326 | + | |
| 5327 | + | |
| 5328 | + | |
| 5329 | + | |
| 5330 | + | |
| 5331 | + | |
| 5332 | + | |
| 5333 | + | |
| 5334 | + | |
| 5335 | + | |
| 5336 | + | |
| 5337 | + | |
| 5338 | + | |
| 5339 | + | |
| 5340 | + | |
| 5341 | + | |
| 5342 | + | |
| 5343 | + | |
| 5344 | + | |
| 5345 | + | |
| 5346 | + | |
5310 | 5347 | | |
5311 | 5348 | | |
5312 | 5349 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
77 | | - | |
| 77 | + | |
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| |||
0 commit comments