feat: NoCellError names the state's accepted set - #29
Conversation
A refusal carried what was refused and where, but not what the state would have taken, so learning that a state accepts nothing at all cost one dispatch per Msg type. `acceptedTypes` is now read at `lookupCell` — the one selection site, with the row already in hand — and stated in the message, the empty case in words rather than as an empty pair of brackets.
|
review-code: PASS @ 531e69c content:eac98f96f446 — merge-ready Graded against #20's acceptance criteria at Per-criterion
Standing checks
Non-blocking observations
Deviations
Verdict-written: 2026-08-19T22:07:52Z |
|
review-doc: PASS @ 531e69c content:eac98f96f446 — merge-ready Doc-class slice at Per-criterion#20's criteria are all behavioural and are graded in the Hygiene
DeviationsNo deviation in the disclosed set is doc-class. The three entries are a gate bypass, a declined verb path, and an out-of-scope code change; each is matched on its substance in the
Verdict-written: 2026-08-19T22:08:15Z |
The four compile-time refusals in the chart compiler — an edge naming a cell with no implementation, a per-site cell missing an edge entry, an edge naming a guard with no implementation, and the reducer-chart safety net — each ended at the missing name, so an author could not tell a misspelling from an omission. Each now lists what was there, all four through one shared `suppliedClause` helper so the phrasing cannot drift: the cells supplied, the edges the per-site bag carries, the guards supplied, and the edges the reducer chart declares. An empty supplied set reads as words, not an empty list. The safety net says it is a library net under a compile-time obligation, not a chart error. Each message's prior text is kept verbatim as a prefix. Each site's probe lands with it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`LaneShapeError` is the lane's whole refusal surface and the one a second consumer of the workflow importer meets first. Every refusal it raises — across `defineLane`/`lowerRegion` (the authoring door) and `runLane`/its load-and-boot `check` (the imported door) — was audited against the compiler's principle (#20/#22): name the set that WOULD have been accepted, not only what was rejected. Where a refusal admits a set, it now names it through the compiler's own `suppliedClause` (exported for this second consumer, kept module-internal): a missing or duplicated `initial: true` names the chart's states (or which carry the marker); a chart with no final names its states; a persisted leaf naming a task the lane does not run names the tasks it does; a leaf or its `was` in an undeclared state names the declared states (the lane twin of `NoCellError`); a task with no hand names the hands supplied, the empty case in words not `[]`. Where a refusal admits no set — a non-object state, an edge delegating its target to a cell, two terminals spelled alike, a dotted id or event name, a foreign event, a missing leaf, a phase-less lane, a `maxRetries` mismatch — the reason is recorded in the source beside the throw. The dispatch-time "no cell" refusal is recorded as out of scope: it fires on a well-formed lane, so its accepted set belongs to the `acceptedTypes` family (#20/#21), not this helper. Each message's pre-existing text is kept verbatim as a prefix. The changed cases are covered by tests; the probe check and export stamps stay green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NoCellErrornow carriesacceptedTypes— the Msg types the refusing state admits — and states them in the message.Before this, every refusal from a given state was byte-identical whatever you dispatched: "not this one" was the whole answer. Discovering that a state accepts nothing at all therefore cost one dispatch per Msg type, which is how #14 arrived — six probes to establish one fact about a frozen lane.
Where the set is read.
lookupCellis the single cell selection bothapplyCellandtryApplyCellskin, and it holds the row at the moment the miss is detected. Reading the set anywhere else would be a second reader that could disagree with the selection about which cells exist, so it is read there and nowhere else. Its return type became a union: the miss arm carriesacceptedTypes, the hit arm has no such field. A single optional field holding[]on a hit would read as "accepts nothing" to anyone who looked, and the keys array is now allocated on the miss arm only, so the dispatch hot path is unchanged.Per form. The transitions form answers from the refusing state's own row (a state with no row at all — a type-bypassed
state.type— accepts nothing, which is true of it). The reducer form answers from the flat table's keys, since dispatch there never consults the state; thestateNamestays the same best-effort read, so an untagged state still yields(untagged state)rather than throwing.The empty case is words.
This state accepts no Msg at all.— notaccepts: [], which a caller skims as a formatting artefact rather than a dead end. That case is the signal a caller acting on a possibly-final state most needs.The old text is a prefix. The pre-#14 message is kept verbatim and the new clause only ever appends, so a caller matching on it keeps matching. Tests assert
startsWith, never equality.One site outside the kernel throws
NoCellErrorby hand: the chart compiler's live-but-undecided cell. Every event gets a row key there, but an undecided one is not a Msg the state accepts, so those keys are tracked and excluded from the set the error reports.Tests cover both forms, the empty accepted set in each, the untagged-state fallback, the missing row, and
tryApplyCell/applyCellmessage parity.Closes #20
Deviations
fabrika build check --surface codegreen. Did: validated by running this repo's own CI gates in-tree instead —typecheck,typecheck:test,lint,docs:reference:check,check:merge-markers,check-export-stamps,test(1953 passed),build,verify-exports, all green — and took the prose surface's green for the changeset. Why:build check --surface coderunspnpm typecheck --force, and this repo'stypecheckis a plaintsc --noEmit, which rejects--forcewithTS5093. The red is structural and reproduces on a clean tree; the flag is turbo's and this repo does not run turbo. Disposition: filed as build check --surface code is unconditionally red: it appends turbo's --force to a plain tsc typecheck #28 with the reproduction and thelint:worktreeprecedent for the fix.node packages/fabrika-cli/src/bin.ts. Did: used the workspace binary viapnpm exec fabrika. Why: that path does not exist in this repo, and the phoenix copy refuses to serve a cwd in a different repository; the workspace binary is what this repo's CLAUDE.md prescribes. Disposition: stated here.NoCellError,lookupCelland the two skins. Did: also updatedsrc/chart/compile.ts's hand-thrownNoCellErrorand theerror-idiom.test.tsconstruction. Why: the third constructor parameter is required, so every construction site had to state its set; leaving either as a default would have made the compiler's refusals claim the state accepts nothing. Disposition: stated here.Closes #21
Closes #22
Closes #23
Closes #19