✨ Address a root document's sections as targets - #421
Conversation
PR #421: ✨ Address a root document's sections as targets11 files, +3791 / -55 Scope🔴 PR has 3846 lines changed. Split into focused PRs. 🟡 3846 lines changed. PRs under 400 receive more thorough review. StructuralOxlint structural signals:
Slop✅ Slop indicators look low. Static AnalysisOxlint: 61 diagnostics across 5 files (9 rules) no-unnecessary-type-assertion (40): packages/core/src/scanner.ts, packages/core/src/definition.ts, packages/core/src/document-targets.ts (+1) CorrectnessNo extraneous code patterns detected. |
| } | ||
| // Only the outer edges are trimmed: whitespace beside a wildcard is part of | ||
| // what the author asked to match, while the whole level is compared against | ||
| // an already-trimmed label. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // an already-trimmed label. |
A root document now catalogs its own addressable static headings, resolves one selector to exactly one of them, and projects itself down to the preamble, each ancestor's own content, and that section's subtree before anything expands. Heading discovery parses a masked copy of the body, with the boundary scanner's top-level component spans blanked to spaces of the same length. Remark ends the HTML block it infers for a component at a blank line, so a component child's `#` line surfaces as a root heading without the mask; offsets, lines, and everything outside those spans are untouched, so the mask changes what is seen and never where anything is. Projection retains original source ranges and scans each one under its own origin instead of concatenating and rescanning, so skipped source cannot renumber what follows it and a retained element keeps its expansion ID. The exact resolved target, never the caller's glob, is what the durable root import records. A replay guard resolves the current selector against the recorded content and requires the recorded exact target, in the check phase so a completed journal cannot answer for a section it never ran. `xmd targets`, targeted `xmd run`, and the targeted workflow definition are the later layers of #412 and remain unbuilt.
A root import whose selector matched nothing failed the effect, so the journal kept only a serialized message and the replay guard delegated past every `err` result. A completed journal written by `Missing` then answered a later request for a section the document really has, with the old `Missing` error. A failed selection is an observation of the document, so it is recorded as one: its kind, the requested selector, the matches, and the catalog. The guard compares whole selection outcomes rather than target strings, reproduces a recorded failure from that record before the recorded Close can be reused, and reports any difference as stale input carrying no foreign object. `DocumentTargetError` now carries frozen, namespaced-tagged data and is recognized structurally, so inspection, a live run, and a replayed run all raise the same error with the same fields across separately loaded copies. A level is canonical only when decoding, label normalization, and canonical re-encoding reproduce it exactly, which refuses NFD, tabs, uncollapsed and edge whitespace, lowercase escapes, empty levels, raw operators, and a raw `#`. `formatDocumentReference()` only formats what `fileSource()` reads back. The recorded selector is sanitized invocation metadata: architecture.md now states that the exact canonical target is definition identity and a caller glob never substitutes for it.
"Not the root import" and "the root import, malformed" were one absent value, so a corrupted record fell through to the recorded terminal result and replayed an outcome the record no longer describes. The recorded root import is now a closed protocol: a repository selection with an optional exact canonical target, or a failed selection with an exact failure record. An unknown kind, a missing, unreadable, mistyped or extra member, and a noncanonical target are malformed, and malformed fails with one fixed cause-free diagnostic before the recorded Close can be reused — delegating nothing, executing nothing, appending nothing. The record carries the content it was taken from, so the selection is verified against that content rather than merely parsed: a recorded target must resolve to itself, and a recorded failure must be the failure that selector produces. `asDocumentTargetError()` returns a fresh local error built from reconstructed data instead of the candidate. An ordinary invocation failure has no fail-stop reason to preserve identity, and returning the candidate hands on whatever it owns — a list its owner can still rewrite, a revocable Proxy, a prototype with accessors. The data contract is closed to exactly five members with no symbol or non-enumerable extras, every list entry must be an exact canonical target, and the fields must describe an outcome selection could have reached.
8ede2f6 to
0422f60
Compare
PR A of the #412 stack. Rebased onto
mainat30dc295after #413 merged.Why
A document that holds several sections is all-or-nothing today: running it runs every component, code block, and resource in it. #412 makes a section addressable, so an author can run one part of a document — and so a future workflow run can record which part it ran.
This PR builds the core layer only: the target model, the public API, and durable identity.
xmd targets, targetedxmd run(PR B), and the version-2 workflow definition (PR C) come next.What changes
Before:
execute()andinspectDocument()take a root document and nothing else. The whole body runs.DocumentInfodescribes frontmatter and the return mode.After:
Both accept an optional
targetselector. A resolved target projects the document to the preamble, the direct content of every ancestor needed to reach the heading, and that heading's complete subtree — sibling subtrees never expand.DocumentInfogainstargets(the catalog) andtarget(the exact canonical selection). AfileSource("README.md#Test/Node")reference splits a path from a selector.How it works
Three decisions carry the design.
Heading discovery parses a masked copy. A Markdown parser reading raw XMD cannot tell a component's children from the root flow: Remark ends the HTML block it infers for
<Wrapper>at the first blank line, so a#line among the children surfaces as a root heading. Discovery therefore blanks the boundary scanner's top-level component spans to spaces of the same length before parsing. Lengths, newline positions, and everything outside those spans are untouched, so the mask changes what is seen, never where anything is. The scanner stays the authority on what a component is; it just records its spans now.Projection retains source ranges rather than rebuilding a document. Each retained range is scanned under the origin it has in the original file. Concatenating the ranges and scanning once would renumber every element after a skipped section, and a retained element's source position is exactly what its expansion ID is derived from.
The exact resolved target is identity; the selector is not.
**/N*andTest/Nodeare the same request if they name the same section. The durable root import records the selection outcome — the whole document, one exact target, or one structural failure — and a replay guard resolves the current selector against the recorded content before deciding whether the journal still describes this run. A caller's glob never occupies the exact-target field and never reaches a workflow definition.Review guide
Start with:
packages/core/tests/document-targets.test.ts— the contract in executable form.Then review:
specs/executable-mdx-spec.md§5.4 “Document targets” — the normative contract.packages/core/src/document-targets.ts— discovery, encoding, selectors, projection.packages/core/src/execute.ts—durableImportComponent's root branch andrefuseChangedRootTarget.packages/core/src/definition.ts,root-source.ts,inspect.ts,scanner.ts.Look carefully at:
packages/core/src/execute.ts—holdRootSelection()andrecordedRootImport(). The selection outcome comparison is where the shipped defect lived.checkphase, notdecide.durableRunreuses a recorded rootClosebefore any effect is replayed (packages/durable-streams/run.ts—runCheckPhaseruns first, thenreplayIndex.hasClose). A decide-phase guard would never run for a completed journal, which is precisely the run whose recorded target must still be the one being asked for. TX18 is the test that pins this.maskComponents()preserves offsets exactly. If it ever stopped doing so, every retained element's expansion ID would shift silently.What must stay true
maskComponents()with the identity function fails DT13 and nothing else.selectTarget()raisingDocumentTargetError, checked by DT23, TX12, TX13.holdRootSelection(), checked by TX15–TX21 and TX24–TX27. Removing the guard installation fails TX18–TX21 together.target-failureselection variant, checked by TX24 (the shipped defect) and TX25–TX27.DocumentTargetErroris recognized structurally, across loaded copies, and reconstructed locally — enforced by a namespaced tag plus a total validating boundary that builds a fresh error, checked by DT52–DT61.isCanonicalTarget(), checked by DT48–DT51.targetmember entirely for an untargeted import, checked by TX16 and TX22.How to verify it
execprovider that throws, not from absent output.Alpharefuses to be reused forBeta. It fails if the guard moves to the decide phase.Missingmust not answer a later request forBeta. It fails against the first commit.malformedback intounrelatedfails TX28–TX32 together and nothing else.Cafe%CC%81,a%09b, a raw#, NUL, and unpaired surrogates are refused, and that every formatted reference parses back to what it named.Every "did not run" assertion comes from a component that records its own invocation, because absent text cannot distinguish "skipped" from "rendered empty".
Results on
0422f60:deno task lintdeno task checkfrom a clean clone, unscopeddeno task check:jsrSuccess Dry run complete(host tree and clean clone)deno task test packagestsx --tsconfig tsconfig.node.json --test(Node 22.23.2, both suites)tsc --project tsconfig.node.jsonbun test(both suites)git diff --checkThe full local Deno suite was run rather than an affected subset, because replay boundaries make graph-based selection incomplete. The clean clone settles an earlier note in this PR: the ~51k errors an unscoped
deno task checkreported came entirely from untrackedspikes/349-dofs/vendorin the host tree, not from the repository.Scope
Included
fileSource(),formatDocumentReference(),DocumentInfo.targets/.target,DocumentTargetError.architecture.mdandspecs/executable-mdx-spec.md.Intentionally unchanged
packages/cliis touched;xmd targetsand targetedxmd runare PR B.xmd test. It keeps its existing path grammar and gains no target support.API.Files, its adapters, and 💥 Contain document filesystem access behind API.Files (#227) #403's fatal-cause precedence.execute()does no extra work.New abstractions
ComponentSpan/scanComponentSpans()exists because heading discovery needs the scanner's own boundary decision, and re-deriving it with a second parser is what this PR exists to avoid. Internal to the package.DocumentOutline/DocumentTargetexist because discovery, selection, and projection are three passes over one structure; consumers areparseRootMarkdownDefinition()andresolveDocumentTarget().DocumentTargetErrorexists because a caller has to tell "you named nothing" from "you named several", and needs the catalog to say so usefully. PR B's diagnostics are its second consumer.Each new abstraction has multiple concrete uses or a clear justification.
No speculative functionality is included.
New dependencies
None.
remarkandmdast-util-to-stringwere already@executablemd/coredependencies. Neither lockfile changes.Revisions
First —
568591eArchitecture review of the initial commit found one shipped defect and four contract gaps.
8ede2f6addresses all five.The defect. Reproduced before it was fixed:
The root import failed when a selector matched nothing, so the journal kept only a serialized message, the guard delegated past every
result.status === "err", and the completed root Close short-circuited everything after it.A failed selection is an observation of the document, not an accident: the text was read, and it does not offer what was asked for. It is now recorded as one — kind, selector, matches, catalog — and the failure is rebuilt from that record and raised. The guard compares whole selection outcomes rather than target strings, reproduces a recorded failure before the recorded Close can be reused, and reports every difference as
StaleInputErrorcarrying no foreign object. No authored effect runs on either path, and no human-readable message is parsed as protocol.Structural error on every public path.
DocumentTargetErrorcarries frozen data under a stable namespaced tag;isDocumentTargetError/asDocumentTargetError/parseDocumentTargetFailurerecognize it totally, so a failure from a separately loaded copy is recognized on the same terms as a local one, while a candidate carrying a cause, extra payload, mutable data, a disagreeing message, or an unreadable property is refused. The §5.4 caveat sayingexecute()exposes only name and message is removed, because it no longer does.Canonical validation. A level is canonical only when decode → normalize → re-encode reproduces it byte for byte, which refuses NFD, tabs, uncollapsed and edge whitespace, lowercase escapes, empty levels, raw operators, and a raw
#.formatDocumentReference()checks its own round trip rather than sampling it, rejecting NUL and unpaired surrogates.Architecture. The locked
workflow definitionterm now includes the exact canonical target when one is selected, and## Document targetsstates the distinction directly: the exact target is definition identity; a caller glob is non-authoritative invocation metadata that never substitutes for it.Second —
0422f60Fail closed on a malformed recorded root selection.
recordedRootImport()returned one absent value for "not the root import" and "the root import, malformed", so a corrupted record was delegated onward anddurableRunreused the recorded terminal result. The record is now a closed protocol with three answers — unrelated, malformed, read — and two supported shapes: a repository selection with an optional exact canonical target, and a failed selection with an exact failure record. An unknown kind, a missing, unreadable, mistyped or extra member, and a noncanonical target are malformed, and malformed throws one fixed cause-free diagnostic from the check phase, ahead of Close reuse, delegating nothing and appending nothing.The record is verified, not merely parsed: it carries the content it was taken from, so a recorded target must resolve to itself in that content and a recorded failure must be exactly the failure its selector produces. That is what refuses
available: ["../../etc/passwd"]— a structurally canonical four-level path the recorded document does not offer.A closed, locally owned recognition boundary.
asDocumentTargetError()no longer returns the candidate; it validates every field and builds a fresh local error. An ordinary invocation failure has no fail-stop reason to preserve identity, and returning the candidate hands on whatever it owns — a list its owner can still rewrite, a revocable Proxy, a prototype with accessors. The data contract is closed to exactly five members with no symbol or non-enumerable extras, every list entry must be an exact canonical target, and the fields must describe an outcome selection could have reached. The "handed onward by identity" claim is removed from the specification.Two implementation gaps were found by the new rows rather than assumed away: the journal failure record did not close its member set, and shape checking alone accepted a catalog the recorded document contradicts.
Risks and limitations
okeven when the run failed. Reviewers comparing journal shapes across versions should expect this.%HHsequence must now be written%25HHand one containing#must be written%23. This has no reachable consumer until PR B routes CLI arguments throughfileSource(); release notes belong with that PR.inlineSource(source), output, and expansion IDs are unchanged, and reverting the commit restores the previous behavior with no data migration.Scope confirmation