Add <Syntax> to list available XMD symbols (#758) - #759
Conversation
|
Warning This PR changes release configuration (scripts/build-npm.ts) without touching PR #759: Add to list available XMD symbols (#758)68 files, +7305 / -596 Scope🔴 PR has 7901 lines changed. Split into focused PRs. 🟡 7901 lines changed. PRs under 400 receive more thorough review. 🟡 68 files changed. Are all changes related? 🟡 Changes span 13 directories. 🟡 PR mixes config and source changes. StructuralOxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 114 diagnostics across 18 files (20 rules) no-unused-vars (28): packages/cli/tests/support/plan-harness.ts, packages/cli/src/authorship-profile.ts, packages/core/src/agent/components.ts (+6) CorrectnessNo extraneous code patterns detected. |
|
The private Plan-only Syntax contract at #758 and this PR body now carry the public protected contract. Correct the branch so canonical core owns |
What a document may write here is a public question, and this makes the
answer public too. `<Syntax />` is now a component canonical core owns,
available throughout XMD: it renders the catalog for the site it is written
at, in the Markdown `xmd syntax` prints, from one construction and one
renderer — so an operator printing a profile and an agent being told what to
write are never given different accounts of one environment.
A new canonical protected tier sits after structural syntax and ahead of
every host or author tier. It is the resolver's own table rather than a
registration, so a repository `Syntax.md`, a bundled `Syntax`, an ordinary or
reserved registration and a host's declared Markdown are each refused where
the claim is made, and import middleware may observe, delegate or refuse the
import without answering one.
Canonical execution carries the catalog lexically on its own expansion
authority, built from the selection inputs it captured before any
installation, middleware or document code ran — or from the one catalog a
trusted host stated for its profile. Each occurrence claims the identity the
execution minted, observes once, and retains exactly `{ catalog }`; a
continuation hostile-parses that record and restores what the run actually
showed.
`<Plan>` returns to five private capabilities. The packaged bytes write the
same public `<Syntax as="syntax" />` any document writes, and `xmd plan`
states the `run` profile's catalog at the execution boundary rather than
through Plan's private assembly.
|
The accepted #758 contract changed after the latest product discussion. Please re-read #758 and this PR body before continuing. The correction now also includes:
Head |
) Two entries in the catalog said something untrue, each by borrowing a neighbouring origin kind. `<Syntax>` reported as `{ kind: "registered", reserved: true }`, and rendered as "reserved registration". A reserved registration is a *host* installing something under a name it wants kept: it can be absent from another run, replaced by a different host, or refused when two hosts claim it. None of that is true of a name canonical core owns, so a reader deciding whether they could supply `Syntax` themselves got exactly the wrong answer. It now reports `{ kind: "protected", origin }` and renders as "protected component", in the structured entry, the rendered Markdown and `inspectComponent` alike. A workflow-bundle member reported as `{ kind: "repository", path }`, which reads as a file the reader could edit. It is the exact blob `sourceHash` names, fixed when the run was defined. It now reports `{ kind: "workflow", path, sourceHash }` and renders the abbreviated object id beside the path. Category placement keyed off the `repository` kind, so the new kind is named there too — a bundle member is the run author's own Markdown and stays under user-provided. Both are additions to a closed set, so the catalog is version 2 rather than a silent widening of version 1: a version-1 reader was promised those origins were all of them, and neither emitting an unknown kind nor reusing a neighbour keeps that promise. Two durable shapes are now documented rather than merely implemented. `syntax_catalog` joins the journal effect table with its exact name and closed `{ catalog: string }` payload, and the `import_component` protocol gains `{ kind: "protected" }` — closed on that one member, because a protected component has no path, no origin to look up and no implementation to serialize. Its replay behavior is stated: the running execution supplies the implementation it built, and an execution that built none refuses rather than resolving the name again, which would run whatever is offered under that name today. The evidence tier moves off `SC` and `SL`, which already name Sample component and Own-scope context updates, to `SYN`. SY19, SY20 and the Evaluate clause of SY21 move to #713. They describe what `<Evaluate>` does with a narrower catalog, and #759 installs one nowhere; worse, `<Syntax>` is not in the generated-XMD pinned identity table, so it cannot be invoked inside a fragment until #713 admits it. What #759 owes is the seam, and SYN25b proves it: a fixed narrower observation answers with exactly the catalog it was handed and adds nothing of its own.
The compact catalog answers *what may I write here*. It does not answer *how do
I use this one*, and an agent handed seventy entries to explain one component
has to guess. So `<Syntax>` gains an optional `names`:
<Syntax names={["Elicit", "File"]} />
which renders each selected component's catalog metadata followed by its
long-form documentation, once each, in catalog order whatever order they were
asked for in. `as` captures the same text. The bare form is unchanged.
The documentation is a package's own. A registration bundle keeps
`components.md` beside the boundary it documents, located from the owning
module's URL — never the working directory and never `--include`, because a
documentation set that moved with the caller would describe a different product
depending on where somebody stood. `deno compile --include` embeds it, the npm
build copies it, and JSR publishes the source, so all four distributions load
the same bytes.
The index validates rather than trusts. A level-two heading is an exact
component name, text before the first documents the bundle, and level-three and
deeper stay in the section they are written in so a component's own
documentation can have structure. A heading that names something the package
does not supply, one that appears twice, and one that is not a component name at
all each refuse the whole index. Headings inside fenced blocks are examples, not
sections. A component with no section is legal and renders the sentence #758
states for one.
Documentation joins to metadata by name *and* owning package, so a repository
`Elicit.md` never receives the built-in's prose: it has a repository origin,
which names no package, and the join has nothing to match on.
The observation carries two inputs now, and that is why it is an object rather
than a string. *What may I write* and *what may I read about* are different
questions, and a narrowing evaluation boundary answers them differently on
purpose: selection reads the enclosing authoring catalog, so a nested author can
be told how `<Elicit>` works where they may not run one, and every rendered
entry states whether it is available in the current evaluation. #713 installs
that boundary; this proves the seam.
`xmd syntax Elicit` is the same lookup — one selection, one index, one renderer —
so the command and the component cannot describe one component two ways. The
compact catalog and version-2 `--json` are untouched; documentation is prose
rather than a catalog member.
Also reconciles the CLI help's stale version-1 claim.
…og (#758) The index checked its headings against whichever catalog was in scope. Those are different sets, and conflating them is a real bug: a narrowing evaluation boundary carries a catalog holding a handful of admitted components, so validating core's own `components.md` against it reported `Elicit` as something `@executablemd/core` does not supply — and the two-input seam refused to build its index at all. A heading has to name a component this *build* ships; which of them a given site can select is what the selection answers. The known set is now core's own registrations plus the protected tier, read from the same declarations execution reads. Adds the evidence the amended contract asks for: - the narrowing seam, proved without an `<Evaluate>`: a narrowed observation reports the narrowed vocabulary bare, documents the enclosing catalog by name, and marks availability truthfully in both directions; - named retention — the occurrence retains its final rendered text, a continuation restores it without rereading documentation, a corrupted record refuses; - the index itself: bundle prose and per-component sections, deeper headings kept inside their section, a fenced heading read as the example it is, and refusals for a duplicate section, a heading that is not a component name, a heading naming something the package does not supply, and one component documented twice; - the join, positively and negatively: a registration and a protected component both receive the package's prose, a repository replacement of the same name receives none; - `xmd syntax Elicit` equivalence, with the compact catalog and an unknown name as controls; - the compiled binary documenting a component from a directory that is not the checkout, which is what proves the embedded asset rather than the entry. The new test file runs under Deno, Node and Bun, so it joins all three shards rather than needing an exclusion. Reconciles the remaining stale version-1 claims: the `SyntaxCatalog` paragraph and the `xmd syntax` row in architecture.md, and the Syntax Markdown suite.
#758) Four corrections from review, and one of them was a real hole. **Availability compared spelling.** The named form selects its reference entry from the enclosing authoring catalog and marks it available if the current evaluation can run it — but it asked whether anything *called* that could run. The whole point of the two inputs is that the enclosing catalog may hold a different component under the same name, so a reference entry for the built-in `Elicit` beside an admitted repository `Elicit.md` reported the built-in as available: an author told they may execute the thing they were just shown. Availability now compares the complete identity — name and every member of the origin, so a workflow blob differs from another by `sourceHash` and a declared component by `digest` — with negative controls for each way two components can share a name. **The heading grammar excluded namespaced components.** A private regex accepted one capitalised segment, so `File.Delete`, `Session.Launch` and `PullRequest.Reviews` could not be documented or looked up at all. It now uses the canonical `isComponentName()` rather than a second copy of the rule. **Fences closed on the wrong condition.** An example written in four backticks containing a three-backtick block ended at the inner one, so everything after it was read as documentation and a `## Heading` in the example started a section. A fence now closes only on the same character at least as long as the one that opened it. **Shared core read the filesystem directly.** `node:fs/promises` is replaced by the host filesystem operation the root document's own read goes through — not the document-facing `Files` authority, since this is the engine reading its own package. Resolution stays package-relative. Also adds the multi-source assembly the complete index needs: contributions are supplied by trusted host installation, each naming its file and the components it must account for, rather than the index being hardcoded to core. And a Markdown component's own document is now readable as its long-form documentation, since a repository, bundled or declared component belongs to the run rather than to a package and no `components.md` documents it. Reconciles the last version-1 claim in Tier SX and adds its named-lookup row.
**The read was reachable from a document.** Last round I moved the package asset
read onto the runtime `readTextFile`, reasoning it was the trusted path the root
document's own read uses. It is `API.Fs`, which a running document can compose
around — so a repository component, an eval block or an installed handler could
answer the read and decide what the product says about itself. It now goes
through the direct Effection filesystem, which no document-scoped middleware
sits in front of. SYN40 plants `API.Fs` middleware around a repository component
that wraps the named form and proves the canonical prose survives and the read
never reaches that Api; routing it back through `API.Fs` fails that case.
**Coverage is exact.** A first-party package now documents every component it
supplies: a missing section refuses the whole index, as an unknown heading and a
duplicate already did. A partially documented package is not a valid build,
because a reader cannot tell an undocumented component from one with nothing to
say. SYN38 is replaced accordingly — deleting any one built-in's section fails,
with a fully covered package as the positive control.
So core is complete: the ten components it registered without documentation, and
the seven Agent components, each with a section written against its actual
contract rather than its catalog line. The Agent boundary keeps its own
`components.md` beside `agent/components.ts`.
**The assembly is real.** `DocumentationContribution` was scaffolding nothing
used; the run profile now contributes the Agent boundary beside the registry it
installs, from the same declarations, captured before any document code exists.
Sets merge per owner rather than replacing — core has two boundaries under one
origin, and keying by owner alone made the second hide the first, which is what
made `xmd syntax Elicit` refuse until it was fixed.
**Three parser corrections.** The heading grammar is `isComponentName()` rather
than a private regex, so `File.Delete` — a real core component — can be
documented at all. A closing fence must be the same character, at least as long
as the opener, and carry only trailing whitespace, so a same-length delimiter
followed by text stays inside a longer example. Markdown-backed documentation is
split by the canonical frontmatter parser rather than a delimiter search.
**`names` selects components.** A structural construct is not one, so
`<Syntax names={["If"]} />` refuses; the fallback sentence is proved with an
undocumented repository component, which is the case it is actually for.
Both assets ship through source, npm and compiled layouts.
`xmd syntax Prompt` printed Prompt's documentation while
`<Syntax names={["Prompt"]} />` answered with the fallback sentence. One
product, two answers to one question, and the fallback was the wrong one: the
command assembled the profile's contributions and the component reached a
core-only index, because `documentationIndexFor()` defaulted to none and only
the command passed anything.
The contributions now travel the way the catalog beside them does — captured at
the installation boundary before any document code exists, carried by value on
`ExecutionInstallation.documentation`, and handed to the root observation.
Several are ordinary rather than refused, unlike the single catalog: one
registration boundary is one file, and a profile installing four packages has
four. `fixedCatalogObservation` takes them too, so narrowing what may *execute*
does not narrow what an author may read about — #713 still installs the
executable catalog.
Three documentation errors, each caught against the registration rather than by
rereading the prose:
- `<Glob>` takes `include={["docs/**/*.md"]}`, a list of patterns, not a
`pattern` string;
- `<CodeBlock>` is self-closing with `value={…}`, and chooses a fence the value
cannot break out of;
- `<Test>` runs under `xmd test` *or* inside `<Testing>`, not only the latter.
That is the reviewer's point about prose review not being verification, and it
is well taken: all three read plausibly and all three were wrong.
Five registration boundaries now each own a `components.md` beside themselves, with a contribution derived from the declarations that boundary registers rather than a list somebody maintains: - core's own components, and its Agent registrations; - the CLI's `<Verbose>`; - testing — `<Testing>`, `<AssertThrows>`, the fourteen assertions, and the five execution-harness components; - web's `<WebForm>`; - all thirteen repository-composition components, `<Dir>` included — the one registered from a definition rather than spelled inline, and so the easiest to miss if the set were hand-written. Each section is written against the component's actual declaration: its forms, its props, whether `as` is required, what activates it, and what it refuses. The last round shipped three examples that read plausibly and were wrong, so this round every example was checked against the registration it documents rather than against my memory of it. **A build cannot produce a distribution without the complete index.** `scripts/validate-documentation.ts` runs the same assembly the `run` profile does, and `deno task build` and `build-npm.ts` both pass through it. Copying the assets was never the check: a package built from a drifted set installs cleanly and refuses the first time somebody asks it for documentation. Each failure class is mutation-tested against real shipped files — a deleted section, an unknown heading, a duplicated section, and drift in a package outside core. Wiring the boundaries immediately found the next gap: four of the six assets were absent from the compiled `--include` list and the npm copy list, so the binary resolved `xmd syntax Git.Commit` to a missing-asset error. All six now ship through compiled, npm and JSR — the JSR dry run lists every one. Also adds the cancellation case: a named observation halted mid-flight completes its teardown and commits no successful `syntax_catalog` record. And the compiled probe now compares its output byte-for-byte with the source CLI for `Git.Commit`, a component outside core's own documentation file, so the comparison exercises a second copied asset path rather than re-proving the first.
The narrowing seam required an evaluator to hold the raw documentation
contributions and hand them back to build a narrowed observation. That list is
execution-private for a reason, and rebuilding an index from it is how two
indexes drift apart. `CatalogObservation` now derives its own:
observation.narrow(admitted)
What comes back reports the admitted vocabulary from `observe()` and keeps *this*
observation's authoring catalog and documentation index for `document()`. #713
needs the admitted catalog and nothing else. Proved by breaking it: a narrowed
observation that drops the enclosing reference fails SYN25e and nothing else.
Contributions are snapshotted field by field at the boundary — owner, asset and
text copied, the name set materialized into one this module owns. A contribution
is a caller's object: its array can be reordered, its source replaced, its `Set`
added to after capture, and an iterable can answer differently the second time it
is walked. SYN25f mutates all of those after capture and proves the observed
documentation and coverage are the captured values.
SYN46 now proves cancellation *reached* the named documentation work rather than
arriving before it: the observation records entry, then teardown, in that order,
and a cancellation that never got inside would leave the first marker absent. It
also states the mechanism correctly — a durable operation records its event on
completion, so a cancelled one commits nothing at all rather than committing a
failure.
SX17 compares the complete rendered output of `xmd syntax Prompt` and
`<Syntax names={["Prompt"]} />` rather than phrases from it. Substring agreement
would pass just as happily if one surface kept the heading and silently dropped
the documentation.
Prose reconciled with what the code enforces: a first-party component with no
section refuses the index, and the no-documentation sentence is for a custom
component. `documentation-index.ts` and `architecture.md` said the opposite.
…distributions (#758) **The snapshot was taken too late.** It happened inside `rootCatalogObservation()`, which runs long after `install()`. So an installation could hand over a contribution, then rewrite its own source text and name set from inside its own `install()` operation, and a document would be told whatever it changed them to. The capture moves to `runInvocation()`, before any `install()` runs, and only the captured value travels onward. SYN25g proves it end to end through `executeInstalled()`: an installation that mutates its source, owner and `Set` from `install()` gets the pre-installation snapshot back. It fails against 2e1ba28. **Cancellation now reaches the documentation work.** SYN46 records entry into the observation and its teardown, in that order — a cancellation arriving before the work would leave the first marker absent — and states the mechanism correctly: a durable operation records on completion, so a cancelled one commits nothing rather than committing a failure. **The npm boundary was not what I said it was.** I reported it as an environment fault after a `spawn sh ENOENT`. Reproduced directly with the harness's own environment, the build succeeds: exit 0, `npm install` clean, every package built. The earlier failure was transient and my attribution to a stale PATH was wrong — `/bin/sh` resolves here and always did. The probe now also runs the emitted binary's named lookup for `Git.Commit`, a component outside core's own documentation file, and compares it byte-for-byte with the source CLI. **A JSR consumer actually runs.** SYN47 stages core and its siblings outside the workspace, writes a consumer with an import map of its own that names no path in this repository, and asks it for two components' documentation. Listing the asset in a dry run proves it is in the payload; only this proves a consumer can load it. Verified discriminating: resolving the asset from the process working directory instead of the module URL fails it.
) **SYN46 suspended in the wrong operation.** It stood in catalog discovery, which runs before the documentation work and proves nothing about it. There was no seam inside index construction to stand in, so this adds one: a module-private asset reader in `component-documentation.ts`, substitutable only through the source module and deliberately absent from `mod.ts` — not a provider, not a Context, not a package hook, so nothing a document or an installed package reaches can replace it. SYN46 now suspends while the named lookup is reading the packaged asset, with the catalog already built and the durable operation already open. Bypassing index construction makes it fail. **SYN47 was proving less than it claimed.** A recursive copy of a source directory would pass even if the publish filter dropped every asset, so it was not publication evidence. It now runs `deno publish --dry-run` for each package, asserts the assets are in what the filter *selected*, and stages exactly those files — so a filter that excluded an asset fails at staging rather than being copied around it. The consumer writes ordinary XMD and invokes the public `<Syntax names={…}>` surface rather than calling the index directly, and its complete rendered output is compared with the same program resolved against the workspace source: one document, one profile, two resolutions, so the only thing the comparison can differ on is the distribution. The casts are gone; both manifests parse through validated schemas. **One exclusion I nearly got wrong.** I excluded `documentation-validation.test.ts` from Node and Bun on the reasoning that its subject is a Deno entrypoint. It is not: the test spawns `deno run` as a subprocess, which works from any runtime, and it passes under Bun. Excluding it would have lost portable coverage for no reason, so the exclusion is removed and only the genuinely Deno-specific JSR probe is excluded — verified by running both files directly under Node and Bun.
The seam I added for SYN46 was a module-scoped mutable variable with an exported setter. One variable shared by every execution in the process: two runs would read through each other's reader, and substituting one changed what an unrelated execution was told the product says. A test-only intent does not make shared mutable state execution-local. The reader is now a value. It travels from where the execution is built — `runInvocation` → `invoke` → `executeDocument` → the observation — and the observation holds its own, so narrowing carries it and two executions in one process cannot reach each other's. There is no setter, no Context, no provider, no installation field and no hook: nothing a document, a component or an installed package can name reaches it at all. Production still uses the direct Effection filesystem. `executeReadingAssetsWith()` replaces `withAssetReader()`: it *builds a new execution* around a reader rather than changing anything an existing one holds, and is exported from `src/execute.ts` alone — not from `mod.ts`, not from `host.ts`. Importing it from a repository component gives you the ability to start your own execution, which you already had; it gives you no way to touch the current one's reader. SYN48 is the control that makes this a fact rather than a claim: one execution suspended inside documentation-index construction, a second ordinary execution overlapping it in the same process. The ordinary one reads canonical documentation and completes independently. Restoring the module-global implementation makes SYN48 fail, which is the check I should have written the first time — SYN46 alone passes under both designs.
| // catalog the Agent must be shown is the one that run will have. Stated at | ||
| // the execution boundary and captured before any installed code — no prop | ||
| // on the thin adapter, and nothing the Component projects, could supply | ||
| // another. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // another. |
| // own documentation says. This is the engine reading an immutable asset | ||
| // out of its own package, so it goes to the filesystem directly, at a URL | ||
| // derived from this module — package-relative whatever the working | ||
| // directory and search path are. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // directory and search path are. |
| // Built from what this execution captured before any installation, | ||
| // middleware or document code ran, and asked only when an occurrence | ||
| // observes: a run whose document never writes `<Syntax />` enumerates | ||
| // nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing. |
| // in: inspection installs no bundle, and a name nothing supplies is exactly | ||
| // the absence the catalog reports by leaving it out. | ||
| // An unresolved name is exactly the absence the catalog reports by leaving | ||
| // it out. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it out. |
| const bodies = new WeakMap<object, ProtectedBody>(); | ||
| return { | ||
| implementation(name, build, claim): FunctionComponent { | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| // may execute is replaced, so a nested author keeps the documentation | ||
| // they had and every entry reports its availability against the | ||
| // admission. | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| ): CatalogObservation { | ||
| const captured = snapshotContributions(documentation); | ||
| return observing( | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
| function* () { | ||
| return reference; | ||
| }, | ||
| // deno-lint-ignore require-yield |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // deno-lint-ignore require-yield |
CI found a regression the whole local battery missed.
The protected tier is present in every execution, so `ExecutionImports` is now
built unconditionally — it used to be `undefined` for a run with no bundle and
no declarations. The root import read that as permission to authorize:
imports === undefined ? imported : imports.authorize("__root__", imported)
With the authority always present, that asks a question nothing answers —
`__root__` is claimed by no tier unless a bundle closes the execution — and an
ordinary run's root refuses with *this execution authorizes no import of this
name*. It now asks only when a tier actually closes the name, which is the rule
every other import already followed, and restores the previous behaviour exactly:
absent authority and unclosed name both skip authorization, a bundle still
closes the execution and still authorizes its root.
Also restores the `documentation-validation.test.ts` exclusion I removed last
round. My reasoning then was that the test shells out to `deno` and so is
portable, and it does pass under a local Bun — because a developer machine has
Deno installed. The CI Bun shard does not: every case fails with `Executable not
found in $PATH: "deno"`. Needing the `deno` executable is exactly as
disqualifying as calling `Deno.*`, and the local pass was the misleading signal.
Verified: `deno task test packages/core/tests/` — 352 passed, 0 failed.
| // catalog the Agent must be shown is the one that run will have. Stated at | ||
| // the execution boundary and captured before any installed code — no prop | ||
| // on the thin adapter, and nothing the Component projects, could supply | ||
| // another. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // another. |
| // own documentation says. This is the engine reading an immutable asset | ||
| // out of its own package, so it goes to the filesystem directly, at a URL | ||
| // derived from this module — package-relative whatever the working | ||
| // directory and search path are. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // directory and search path are. |
| // Nothing about the answer is recorded: what this name means is | ||
| // core's own, so a replay asks the copy of core that is running | ||
| // rather than restoring an origin a registry would have to still | ||
| // hold. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // hold. |
| // Built from what this execution captured before any installation, | ||
| // middleware or document code ran, and asked only when an occurrence | ||
| // observes: a run whose document never writes `<Syntax />` enumerates | ||
| // nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // nothing. |
| // in: inspection installs no bundle, and a name nothing supplies is exactly | ||
| // the absence the catalog reports by leaving it out. | ||
| // An unresolved name is exactly the absence the catalog reports by leaving | ||
| // it out. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // it out. |
…registers (#758) **A named root occurrence asked its catalog twice.** `rootCatalogObservation()` supplied `current` as both the reference and the executable operation, and `document()` called both — so one occurrence invoked the trusted catalog contribution twice. Wasteful, and worse than wasteful: the environment can move between the two calls, and an entry's metadata would then come from a different catalog than the availability printed beside it. The observation now takes the authoring catalog and an *optional* admission. At a root there is no admission, so one resolution answers both questions. Under narrowing the two are genuinely different values, and the bare form reports the admission without asking the enclosing catalog at all. SYN49 proves it against a contribution that changes between calls — one occurrence, one call, both decisions from that value — with two occurrences still independent and a continuation restoring the retained text without asking again. **A nested `<Execution host="run">` was told less than it could run.** `testing-host.ts` installs the run profile's registrations and its `<Plan>` declaration, but passed no documentation, so a child's index held core's contributions alone: `<Syntax names={["WebForm"]} />` rendered the entry for a component the child can execute and then said it was undocumented. The contributions now travel beside the declarations they belong to. Audited the other host assemblies. `upgrade.ts` and `authorship-profile.ts` register no run-profile components, and `plan-component.ts` installs the registry for *validation* rather than an execution, so none of them can drift this way. `cli.ts` and `testing-host.ts` are the two run-profile executions, and both now assemble registrations and documentation from `useRunProfileRegistry()` and `runProfileDocumentation()` side by side. The nested regression names its fixture `lookup.md` rather than `webform.md`: this filesystem is case-insensitive, so a document of that name is found as the repository component `WebForm`, shadows the registration, and makes the fallback correct — the case would have been measuring the wrong thing and passing for it. Built on e90fe2e rather than d9985f1: that commit is the CI fix for the root import, which this keeps.
`SyntaxCatalog` becomes `SyntaxSymbols`, `CatalogObservation` becomes `SyntaxReference`, and the reference answers `symbols()`, `documentation()` and `available()`. The old names are removed rather than aliased: there are no users to migrate, and two names for one thing is how the two documentation lists drifted in the first place. Documentation now composes with the components it describes. Each package's bootstrap installs its registrations and its documentation in one call, through the additive `Documentation` Api; canonical core is the terminal, and every wrapper delegates before appending. Canonical execution collects once, after the trusted host's bootstrap and before the root import, and snapshots by value — so middleware a running document installs composes into a chain nothing reads, two contributions for one component refuse whichever order they were bootstrapped in, and sibling scopes stay isolated. That replaces the central `runProfileDocumentation()` list and the `ExecutionInstallation` `documentation` field, which a host had to keep in step with its registrations by hand and did not: a nested run registered `<WebForm>` and reported it undocumented. `xmd syntax` enters the same declarative bootstraps rather than splicing their registration arrays, so the command that has the components has the words that describe them.
Three suites assert against a built artifact rather than the source tree, so none of them typechecks against `Syntax.ts` and none was in the local matrix. All three still held the previous wording or the removed installation field. The staged JSR consumer now bootstraps with `useAgentComponents()` instead of handing `executeInstalled` a `documentation` field that no longer exists. That field was silently ignored — the generated consumer is a string, so nothing typechecked it — and the consumer rendered `<Prompt>` as undocumented. The replacement is one call, which is the point of the bootstrap being one thing. The npm-binary and compiled-binary suites assert the approved description, which changed with the rename.
Why
xmd syntaxtells you which components and control-flow constructs are available from outside a document. A document cannot yet ask the same question about the site where it is written or request the long-form documentation for only the components it needs.Closes #758. Supplies the documentation index #678 consumes for the website and release reference.
What changes
<Syntax>becomes a public text component canonical core owns:A symbol is one available component or control-flow construct. The bare form lists the symbols at the authored site. The named form renders each selected component's symbol metadata followed by its long-form documentation, once in symbol order.
ascaptures either form.xmd syntax Elicituses the same index, selection and renderer as named<Syntax>.Approved description:
Architecture
Canonical protection. Core claims the Syntax name ahead of every host or author tier. Repository files, bundles, declarations, registrations, import middleware, document context and another loaded package copy cannot answer for or replace it.
A lexical syntax reference. Canonical expansion carries a private
SyntaxReferencewithsymbols(),documentation(names)andavailable(symbols). A root reads oneSyntaxSymbolsvalue once per occurrence. A canonical<Evaluate>boundary derives a reference with the admitted symbols available while retaining the enclosing authoring reference for documentation. Symbol text carries no component definition or execution authority.Documentation follows package bootstrap. Each package installs its components and documentation contribution through one stable, namespaced contextual middleware API. Core supplies its own documentation as the terminal. Canonical execution collects and snapshots the composed contributions after trusted host bootstrap and before root import or document execution. Later document or component middleware cannot change the snapshot. Duplicate contributions refuse without using middleware order as precedence, and separate bootstrap scopes remain isolated.
This replaces the centralized
runProfileDocumentation()assembly and equivalent per-hostExecutionInstallation.documentationlists. Ordinary and nested run profiles receive matching documentation because they invoke the same package bootstraps.One package-owned index. Every first-party registration boundary ships
components.mdbeside the components it owns. The index validates exact coverage and joins documentation to symbol metadata by component name and origin. A repository replacement never receives a built-in's prose.xmd syntax NAME, named<Syntax>, and #678 read the same index.Durable reading. One Syntax occurrence performs one
syntax_symbolsdurable operation and retains exactly{ symbols: string }. Continuation hostile-parses and restores that rendered text without reading the filesystem, package middleware, host or lexical reference again. Failed and cancelled reads commit no successful value.Honest provenance.
SyntaxSymbolsversion 2 addsprotectedandworkfloworigin kinds. A protected symbol is core-owned rather than a reserved registration. A workflow symbol identifies the exact bundled path and blob object id rather than pretending to be an editable repository file.Naming and migration
This change uses
SyntaxSymbols,SyntaxReference,SyntaxSymbolsProvider,symbols(),documentation()andavailable(). It removes the earlier unmergedCatalogObservation,syntax_catalogand{ catalog }names.The released
SyntaxCatalogTypeScript name is replaced bySyntaxSymbolswithout a deprecated alias as an explicit pre-1.0 source migration. The version-2 JSON projection retains itsversionandcategoriesfields.Review guide
Start with the Syntax component and lexical reference, then trace protected selection, package bootstrap middleware, execution-time snapshotting, the documentation index, CLI rendering, Plan's public invocation, distribution assets, and the nested run host.
Structural checks:
How to verify it
Run the frozen #758 command plus the named-root single-read and nested-host long-documentation regressions. Return the exact commands and results with a new signed feedback commit; do not wait for CI.
Scope
Included: public protected Syntax, bare and named forms, bootstrap-composed package documentation, the validated index,
xmd syntax NAME, version-2 symbol provenance, Plan's public invocation, exact durable retention, and supported distribution evidence.<Evaluate>installs the narrowed reference under #713; this PR supplies and proves theavailable(admittedSymbols)seam.#762 owns the later
xmd planinformation-request loop.