Skip to content

Add Product Owner collaboration to the Architect role - #769

Merged
taras merged 1 commit into
mainfrom
architect/product-owner-rulebooks
Sep 6, 2026
Merged

Add Product Owner collaboration to the Architect role#769
taras merged 1 commit into
mainfrom
architect/product-owner-rulebooks

Conversation

@taras

@taras taras commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Why

The Architect currently consults the Product Owner only when it discovers a material choice. That lets user-facing interfaces, lasting architecture conventions, and product verification become settled without the iterative collaboration needed to build reusable product rules.

What changes

Before:

The Architect could finalize an apparently obvious interface or verification design independently. Product Owner feedback had no dedicated rulebooks, promotion process, or delegated path for applying an accepted rule later.

After:

The Product Owner leads public-interface, durable architecture-convention, and product-verification design. The Architect brings a first pass, applies approved rules autonomously, and interviews the Product Owner only where novelty, ambiguity, conflict, or an exception remains.

Approved reusable rules are recorded immediately. Every rule is one sentence of at most 160 characters, with rationale and examples kept outside it.

How it works

feature inventory → apply approved rules → show conforming decisions
                  → interview unresolved decisions → approve and record rules
                  → finalize architecture and verification

The role uses three rulebooks:

  • .agents/product-interface.md
  • .agents/architecture-rules.md
  • .agents/product-verification.md

When the Product Owner says something is not understood, the Architect stops and uses the five-part Current, Intent, Reconsideration, Proposed, Feedback interview.

Component descriptions inherit the general product-interface process while retaining their specialized writing rules.

Review guide

Start with: .agents/architect.md

Then review:

  1. The collaboration, misunderstanding-interview, delegation, and rule-promotion process.
  2. The human-first interface rules and feature inventory.
  3. The architecture ownership boundary and existing-debt treatment.
  4. The Product Owner-led verification rules.
  5. The inherited component-description interview.

Look carefully at:

  • Approved rules delegate only conforming decisions; new rules still require explicit Product Owner approval.
  • Rule approval authorizes only the corresponding rulebook edit, not GitHub or other project changes.
  • Human clarity remains the standard even when agents author most XMD.

What must stay true

  • The Architect brings a concrete first pass instead of asking the Product Owner to design from a blank page.
  • Conforming decisions stay visible without requiring repeated approval.
  • Interface and product verification approval precede finalized architecture and implementation handoffs.
  • Adjacent debt becomes a proposed follow-up Story instead of silently expanding a feature.
  • The Architect remains separate from implementation and existing GitHub authorization boundaries remain intact.

How to verify it

  • git diff --check passes.
  • A rule-length scan reports no maintained rule over 160 characters.
  • Every referenced rulebook and inherited guide exists.

Scope

Included

  • Product Owner collaboration in the Architect role.
  • Product-interface, architecture, and product-verification rulebooks.
  • The 160-character rule format and explicit promotion/delegation process.
  • Product-interface inheritance for component descriptions.

Intentionally unchanged

Follow-up debt

Risks and limitations

  • The initial rulebooks are intentionally small. Their authority grows only through explicit Product Owner interviews and approval.
  • Recovery is to revert this documentation-only commit; it changes no runtime or stored data.

Scope confirmation

  • Every changed file supports the purpose described above.
  • Unrelated cleanup and formatting changes are excluded.
  • Generated or mechanical changes are clearly identified.
  • The description matches the final diff and verification.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

PR #769: Add Product Owner collaboration to the Architect role

5 files, +257 / -46

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 3549fe5 into main Sep 6, 2026
36 of 37 checks passed
@taras
taras deleted the architect/product-owner-rulebooks branch September 6, 2026 08:45
taras added a commit that referenced this pull request Sep 7, 2026
* ✨ Make `<Evaluate>` evaluate complete XMD programs (#713)

`<Plan>` produces a program and never runs it. `<Evaluate>` is now the
composition site where one runs: paired content renders a producer into a
private program buffer, `program={value}` supplies complete-program source
directly, and both admit a complete root and evaluate it in the current
execution — its lifecycle, journal, cancellation scope, providers, working
directory and the authority in force where the element was written.

The forms are disjoint and ambiguity refuses before program content is produced
and before any program effect runs. The root deciding what happens is the
program: its frontmatter, props schema, `returns` and `<Output>` selection all
apply, root props are the explicit `props` object rather than the caller's, and
a value root requires `as`.

Complete programs record their own `evaluate_program` durable event; the
restricted `generated_xmd` admission #369 delivered is unchanged and
complete-program support is not reachable through `source` or `allow`. A
partial continuation expands the retained source and restores completed nested
effects; a changed program at the same occurrence is stale input and neither
source runs.

Canonical execution answers the new `Component.expandProgram()` from the frame
it already holds, which is what makes site authority a fact about the frame
rather than something a prop, binding or middleware return value could supply —
and what keeps a producer's private closure from crossing into the program.

* 🔒 Hold a complete-program continuation to its retained admission (#713)

The `evaluate_program` record is hostile data and was read as though it were
not. It is now two closed shapes — admitted and refused — with every nested
record closed too, so a missing, additional or misspelled member at any depth
is a record this evaluation did not write.

Shape is not enough, because every member of the record is a claim about
another one. Before anything expands, the retained source must hash to the
retained digest, reparsing it must produce the retained root mode and a valid
body structure, the retained elements must be the ones that source writes in
that order and those forms, and the supplied props must validate to the
retained validated props. A record failing any of them is refused as unreadable
and neither the retained nor the current program performs an effect.

The admission also retains what each name the program writes resolved to at
this site, which restores the current-site compatibility contract: a
continuation whose site now answers one of those names with a different
identity, or admits it in a different form, refuses before either
implementation runs, while an unchanged site resumes.

Resolution stays behind canonical execution. The execution builds a resolver,
carries it on the expansion authority, and settles the comparison itself before
the first program effect. `Component.resolveProgramSite()` lets the admission
retain what the site answered; nothing rests on that answer, because the
canonical comparison is what decides — so short-circuiting it refuses the
evaluation rather than widening it. `ExpansionAuthority` is still reachable from
no component, and no prop, binding, middleware result or public context state
supplies authority.

* 🔑 Take a program component identity from the answer the chain supplies (#713)

The retained identity came from an independent `selectComponent()` call, which
is not what runs. `Component.importComponent` middleware may answer without
delegating or replace what came back, so a second resolution described a
definition nobody invokes: two different middleware answers retained one
identity, compared equal, and a continuation ran the other one.

Resolution now goes through the ordinary chain and takes the identity from the
final answer. A canonical tier keeps its canonical identity. An answer an
identified provider supplied keeps that provider's own — a stable origin, its
key for the name, and a revision that changes with the implementation — stated
at the installation boundary through `useImportProvider()`, where canonical
execution mints a claimant for that execution and binds the terms to the exact
answer in execution-private state. Never on the definition, which an answer can
copy; never through a replaceable Context answer.

An answer nobody identified still works for ordinary expansion and cannot back
a durable grant: a continuation would have nothing to compare. Two live
providers under one origin refuse, and so does a second claim on one answer.

Identities are closed tagged records now, so a missing, empty, additional or
mis-tagged member is unreadable like every other corrupted member.

The comparison and the invocation are one decision: canonical execution keeps
its own copy of each witnessed answer and authorizes the program's imports
against it, so an answer that changed between the check and the use is refused
where it would be invoked rather than silently preferred.

* 🧱 Resolve a program under the site's closed authority, once, before its import (#713)

Three corrections.

**Closed authority was overwritten.** The program authority replaced the site's
`imports` with one that forgot its tiers, and resolution asked the chain without
applying them, so an identified provider could replace a bundled or declared
component and be admitted as its identity. Resolution now applies the site's
closed authority exactly as ordinary expansion does, and the program's expansion
keeps the tiers it already had. A provider-supplied answer is witnessed rather
than issued, so it authorizes nothing for a name a tier closed — the guarantee
that sharing one witness table had quietly broken.

**Resolution now journals nothing.** The selection half of an import and the
definition it produces are separated from the durable operation, so a
resolution-only path settles the site without recording anything.
`evaluate_program` commits before the program's own ordinary import.

**Expansion no longer asks again.** The answers that passed reconciliation reach
expansion as settled answers: an element whose name is among them does not enter
`Component.importComponent` at all. The chain is entered twice for one
evaluation, never a third time. The authored element still records one ordinary
import after the admission, restored from the already-authorized answer with
identity-domain and form selection intact.

Provider identity is read exactly once at registration and closed over as copied
primitives, so a getter cannot answer the duplicate check and the claim
differently and a later edit changes nothing. Structural syntax resolves
directly to its structural identity: no lookup, no provider, no import record.

* 🪢 Bind a program's settled imports to their authored occurrences (#713)

The settlement map was keyed by name, so a program writing `<Open /><Open />`
collapsed two independently resolved answers into one and invoked the first
twice. Each occurrence now carries its own settlement, keyed by where the
element was written and paired with the admission's retained order, so two
elements resolved to two implementations invoke those two in order.

An unresolved occurrence is a settled outcome as well. It was absent from the
map, so the element fell through to the ordinary open chain and could be
answered by a lookup reconciliation never made. It now reports the ordinary
unresolved failure without consulting anybody, and the chain is entered exactly
twice for one evaluation whatever an occurrence settled to.

Settlements belong to the program's own parsed body. A component the program
invokes expands its own bytes at its own offsets, which nothing reconciled, so
carrying them in would have made a settlement a name-wide override of somebody
else's element.

The nested durable record is a protocol rather than a marker. A settled
occurrence records exactly `settled` and `name`, and a continuation parses that
as the hostile replay data it is before invoking anything: a missing,
additional, mistyped or unknown member, and a record naming another component,
each refuse.

A refusal this boundary raises during resolution — a second provider claiming
one answer, an answer changed after it was claimed — now reaches the caller
instead of being read as "this name resolves to nothing".

* 🛡️ Detach a settled import before reading it (#713)

`readSettledImport()` cast the restored value and inspected it directly, so a
value that answers differently on a second read decided what a continuation
invoked. It now detaches the value into plain JSON under a failure boundary and
inspects only that copy: a member read once cannot answer twice, and a value
that will not detach is simply not the record. The admission's own restored
result is detached under the same boundary for the same reason.

The prose said one settled `import_component` per settled occurrence. It is one
per *resolved* occurrence: an unresolved one loads no component and records no
such event, which PE41 now asserts.

A retained value that refuses to be read at all — a Proxy trapping `ownKeys` or
a descriptor, a throwing accessor, a cycle — never reaches this parser. The
run's own retention check reads every retained result before the document body
starts and refuses there, in the journal's own words. That is a persistence
boundary rather than this one, so the evidence proves what this boundary can
promise: the component is never invoked.

* 🩹 Keep the workflow `<Evaluate>` case on the runtime that has the command (#713)

EP9 proves a workflow run is given one `<Evaluate>` and not two, and it proves
it by starting a real run. `xmd workflow` exists on the Deno entrypoints alone,
so under Node and Bun the command refused before a run existed and the case
asserted nothing — which is why `test-node (2/7)` and `test-bun (3/4)` failed
while the same file passed locally under Deno.

The case moves to its own file and joins the Deno-only exclusions with its
reason and issue, the way every other test of a workflow command does. Tier EP's
portable half stays where it was and still runs on all three runtimes.

* 🔒 Require scope-bound event listeners in Effection code (#748) (#757)

An event listener an Effection operation installs is state that operation
owns, but nothing about an event source enforces that. A handler attached to a
socket, a child process or a DOM target survived the operation that attached
it, and kept writing to state the abandoned owner left behind. Cleanup that
runs when the event fires is not cleanup: a cancelled wait is exactly the case
where the event never arrives.

`@effectionx/node` moves to 0.2.5, whose `once()` registers nothing until it is
interpreted and detaches in a `finally` — after delivery, a halt, or losing a
race. Every direct pin moves together; `@effectionx/process` stays at 0.8.1,
which leaves one audited transitive 0.2.4 lock entry that nothing here
resolves.

`local/require-scope-bound-event-registration` makes the policy a blocking
gate. It resolves sources, owners, `action` and `ensure` through their imports
rather than their spelling, and an owner is the nearest generator or `action()`
executor and nothing wider. Cleanup counts as established only when it is in
place before the thing it releases exists: a lexical `finally` entered before
the subscription, an `ensure()` that *completed* before it, or the cleanup an
`action()` returns. An `ensure()` yielded afterwards is rejected, because
entering it is itself a suspension — an owner halted there unwinds with no
cleanup registered at all, measured, with the listener still attached.

Every applicable registration is migrated onto one of those shapes. The three
owners that own a child process register their cleanup before spawning it, and
that cleanup keeps every handler attached until the child's `close` — the only
event that says the process and the pipes this run inherited are finished. An
assigned exit status is not that boundary, so none of them reads one: a
cancelled command still captures what its child wrote on the way out, and still
escalates SIGTERM to SIGKILL exactly as before.

Readiness races are interpreted inline, in the same synchronous run as the
`connect`, `listen` or `spawn` they watch, because a spawned race attaches its
arms a turn later and the source can settle in that turn. The foreground reaper
names its exit handler and releases it in the one funnel every settlement
passes through. `nodeResponseChannel` becomes an `Operation`, so both of its
handlers belong to the request task. The form responder keeps its request
observed until that request's own `close`, so the peer's hang-up reaches a
listener instead of the process. Web server teardown detaches every accepted
socket's close handler before it suspends, then ends request tasks before
destroying their connections. The workflow and CLI HTTP fixtures read request
bodies in tasks of the server's own scope.

Every distinct manual owner is cancelled while its listeners are live. Each
case measures the baseline the runtime was already holding, reads the vector
once with the owner running, halts it before the event that would have settled
it, reads the vector again before replaying anything, then replays and proves
no capture, callback or result moved. Where a source is a child process or
accepted socket an operation does not hand out, a package-private observation
seam carries it; no public package contract widened.

This is a lifecycle change. Event ordering, output aggregation, response bytes,
failure precedence, process signalling and every public result are unchanged.

* Add <Syntax> to list available XMD symbols (#758) (#759)

* ♻️ Separate Plan syntax observation (#758)

* ✨ Make `<Syntax />` a public protected component (#758)

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.

* 🏷️ Give protected and bundled components their own catalog origins (#758)

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.

* ✨ Let `<Syntax>` render selected components' documentation (#758)

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.

* 🔍 Validate the documentation index against the package, not the catalog (#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.

* 🩹 Compare component identities, not names, when reporting availability (#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.

* 📚 Document every component core supplies, and protect the assets (#758)

**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.

* 🔗 Carry package documentation through the execution boundary (#758)

`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.

* 📖 Document every first-party component, and gate the build on it (#758)

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.

* 🪟 Derive a narrowed observation from the enclosing one (#758)

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.

* 🔒 Snapshot documentation at the installation boundary, and close the 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 2e1ba288.

**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.

* 🧾 Cancel inside the index, and stage what publish actually selects (#758)

**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.

* 🧵 Give each execution its own packaged-asset reader (#758)

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.

* 🚑 Authorize the root import only where a tier closes it (#758)

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.

* 🎯 Resolve one root catalog per occurrence, and document what a child 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 e90fe2e1 rather than d9985f1c: that commit is the CI fix for the root
import, which this keeps.

* ♻️ Settle Syntax terminology and bootstrap documentation

`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.

* 🚑 Carry the settled Syntax terminology into the distribution suites

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.

* docs: add Product Owner design rulebooks (#769)

* Make repeated documentation bootstrap idempotent (#758) (#765)

* 🚑 Refuse a repeated bootstrap, and finish the Syntax vocabulary

Collection refused two contributions for one component of one package only
when they named different assets. An exact repetition — what a profile that
called one bootstrap twice produces — passed. Whether the second call also
installed a provider, a launcher or an execution policy is not a question this
boundary can answer, and the assembly is wrong either way, so it refuses now.

It has to refuse at collection rather than in the named form's index. A
document that writes bare `<Syntax />` builds no index, and one that writes no
`<Syntax>` at all builds no reference either, so deferring would let both run
to completion on an assembly nobody validated. SYN25l proves all three
document shapes refuse, with a single bootstrap as the positive control.

architecture.md said the assembled contributions travel by value on the
execution installation, which stopped being true when the installation field
was removed. It now states what is implemented: bootstraps install additive
namespaced middleware, core is the terminal, and execution collects once after
trusted bootstrap and snapshots by value before the root import.

The rest is vocabulary the rename left behind — the `xmd syntax` broken-pipe
diagnostic an operator actually reads, the `--json` help text, comments in
`plan-component.ts` and `protected.ts`, the spec's "lexical observation", and
the test helpers and authored bindings in the Syntax suite. Uses of
"observation" that name the error-observation boundary, the document-target
catalog and the diagnostic catalog are other product concepts and are left
alone.

* 📝 State the documentation ownership boundary the implementation has

The Syntax section still said contributions "travel by value on the execution
installation", describing the `ExecutionInstallation.documentation` field #758
removed. It now states what is implemented: a package bootstrap installs its
registrations and additive `Documentation` middleware together, canonical core
is the middleware terminal, and canonical execution collects the composed
contributions once after trusted bootstrap and snapshots them by value before
the root import. That captured snapshot — not a field on an installation — is
what `xmd syntax` and an authored `<Syntax>` both consume.

The duplicate rule is stated where it is enforced: any second contribution for
one owning package and component refuses during collection, an exact repetition
included, because collection is the only boundary every execution passes
through. SYN25l joins the Tier SYN table with the three document shapes it
covers and its positive control.

The rest finishes the vocabulary audit this feature promised, in the focused
specification section and the Syntax and `xmd syntax` test titles, helpers and
comments. Uses of "observation" that name the import chain's own observing, the
document-target catalog and the diagnostic catalog are other product concepts
and are untouched.

No runtime behavior changes.

* 🚑 Let a package be bootstrapped at more than one layer

Refusing every second contribution broke `xmd workflow`. The
repository-composition vocabulary is deliberately installed twice — once by an
ordinary run's bootstrap and again inside the workflow attachment, because
either may be the only one — and the attachment's scope descends from the run's,
so both wrappers sit in one chain. Collection saw two identical contributions
and refused, and seven CI shards failed on it.

`installation.ts` says so in its own words: "One array, three consumers." The
repeat is the product's layering, not a wrong assembly, and an identical
contribution cannot change what a document is told — which is the thing the
refusal exists to protect.

So a bootstrap that finds its own statement already in the chain does not append
it again. Two contributions are the same statement when they agree on the owning
package, the asset and the exact set of components; differing in any of the
three makes them two statements, and two statements about one component still
refuse at collection.

SYN25l now proves the repeat is idempotent — the named form, the bare form and a
document with no `<Syntax>` all work, and the collected snapshot holds the
contribution exactly once — with two bootstraps naming different assets as the
control that keeps it from passing vacuously. The specification and
architecture.md say the same.

* ♻️ Compare a documentation contribution by all four of its values

Coalescing on owner, asset and component set left the documentation text out,
so two bootstraps naming one path and reading different bytes — a stale build
tree, a substituted reader — would have coalesced silently and kept whichever
ran first. Equality now covers all four values a contribution is: owning
package, asset identity, exact text, and component-name set. By value, so a
fresh object each ask and a name set built in a different order are still the
same statement.

What reaches collection is therefore a real disagreement, and its refusal says
which of the three differs rather than guessing. Two owners documenting a
same-spelled component still coexist, because documentation joins by name and
origin; so do disjoint sets from one owner.

SYN25l becomes five rows: value-identical repetition adds nothing for the named
form, the bare form and a document with no `<Syntax>`, with one bootstrap
capturing exactly what two capture; a changed asset, text or overlapping set
refuses in either order; distinct owner and disjoint set are the controls that
keep that from over-refusing; overlapping executions stay isolated; and a
layered trusted bootstrap keeps every layer's registrations *and* one
documentation value.

The specification and architecture.md state the four values, the coalescing
rule, and the two non-conflicts.

* ♻️ Classify duplicate documentation at collection, not in the helper

`Documentation` is public. A package may compose `around(...)` itself and hand
back two value-identical contributions without going through
`contributeDocumentation()`, and that assembly is exactly as valid — but
classification lived in the helper, so collection saw two contributions for one
owner and component and refused. Correctness depended on which spelling a
package happened to choose.

Collection now owns it: snapshot the composed chain by value first, fold every
completely identical contribution to one, then classify what is left. The helper
appends and nothing more, so there is one rule in one place rather than two that
can drift.

Non-identical contributions overlapping one owner and component still refuse
before root execution in either order, and a different owner or a disjoint set
is still no conflict.

SYN25l.6 installs two direct middleware wrappers that never call the helper —
fresh objects, the same four values, name sets in opposite orders — and requires
the named form, the bare form and a no-`<Syntax>` root to work with one
contribution captured, then requires the three disagreements reached that way to
refuse in both orders. Moving classification back into the helper fails that row
and passes every other one in the tier.

* 🧱 Close the generated-XMD durable protocol and state the evaluation profile

Foundation for #713's protected <Evaluate>, not the surface itself. Six things
the amended contract needs before a component can be built on them.

The retained record is versioned. Version 2 tags itself at both the result and
the policy level and carries the Workspace basis as one optional member, because
an ordinary host evaluates against no Workspace and a reader that treated a
missing basis as an empty one would compare those two equal. The untagged #369
record stays readable as version 1, proved against frozen literal records rather
than version-2 records mechanically rewritten — a rewrite proves only that the
rewriter and the reader agree.

Both versions are now closed at every level: policy, Workspace basis, allowed
entry, named invocation, and request. A member this build does not know means a
record written under rules it does not have, so reading the members it
recognizes would admit a grant on terms it never saw. `parseRequestRecord()` is
the wrong reader for a retained ceiling — it ignores unknown members and treats
a malformed timeout as an absent one — so requests read through a closed reader
here instead.

Exact text joins the ceilings a continuation is held to. An admission is a
decision about one fragment; a caller now holding different text is asking for a
decision nobody made, so it refuses rather than silently expanding the retained
copy. GX24/GX24b asserted the superseded behavior and now assert this, with
GX24c as the unchanged-text control.

A Workspace basis is validated where it is stated, not only where it is
compared: non-empty unique roots and a selected root the host retains. A basis
with no roots or a selected root nothing holds is not a stricter grant, it is
nothing for a continuation to be held to.

`ExecutionInstallation.evaluation` carries one host profile, refused in the
plural on the same terms as `symbols`, and reaches canonical expansion on the
private authority. The deep by-value capture, the per-entry request ceilings and
the captured provider operations are the next commit; this one places the
singleton and its carrier.

* 🧱 Capture the evaluation profile by value, per entry, once

Advisories 4, 5 and 6 on the profile foundation. Still not the surface.

A host now states `FragmentEvaluationInput` — entry tables, an optional
Workspace snapshot operation, its resolved Fetch timeout, and whether it accepts
the released `source` spelling — and canonical execution captures a profile of
its own from it before any `install()` runs. Every structural value is
deep-copied and frozen and every live operation is read and bound once, so a
host that edits its own arrays, schemas, headers or roots afterwards is editing
objects nothing is looking at. The previous `Object.freeze` around the outer
array said by-value and was not.

An entry binds `{origin, key, revision}` rather than one opaque string, because
a continuation compares it and a reader has to be able to say which part moved.
It is never derived from a function: an implementation is not an identity.

Request ceilings belong to the entry that owns them. Flattening them would let
one entry's limit admit another entry's request, so each entry normalizes,
deduplicates and canonically orders its own — against the timeout the host
resolved when it built the profile, through `normalizeFetchRequest()`, rather
than a context read at whichever point the comparison later happens.

The Workspace is an operation rather than a value because a run's own progress
advances it: a basis captured at installation is stale by the second evaluation
in one run. Canonical Evaluate will ask per invocation. An ordinary host has
none, which stays distinct from having an empty one.

Only the host input type is exported, and only from `core/host`, beside the
`ExecutionInstallation` it belongs to. The captured profile and its bound
operations are unexported: ordinary core publishes no getter for active
authority and no way to install a provider.

* ✨ Make `<Evaluate>` canonical, public and protected (#713)

`<Evaluate>` was a registration the workflow host declared, so what an
unaudited fragment is allowed to do depended on which package answered a
name. It is now canonical core's, in the protected tier beside `<Syntax>`,
and a host states only the ceiling.

- `Evaluate.ts` is the protected component: two disjoint input forms
  (`text`, or paired content that produces the program), `allow` selecting
  among the host's tables, and the result binding observations beside the
  fragment's rendered output.
- `syntax-admitted.ts` describes what a fragment may write, so a producer
  rendered inside the paired form is told the narrowed vocabulary rather
  than the enclosing site's.
- Canonical expansion delivers a one-shot content projector directly to a
  protected body. Only the ordinary handle is published through
  `ActiveProjection`.
- Core's entry constructors (`fileReadEntry`, `fileWriteEntry`,
  `fileDeleteEntry`, `fetchEntry`) carry structural `{origin, key, revision}`
  identities, so a changed grant refuses a continuation instead of widening
  it silently.
- `xmd run` and its run children state a Files-only profile; the workflow
  attachment states its Workspace-bound one and keeps the released `source`
  spelling working.

The workflow's `evaluationComponents()` is gone: nothing registers this name.

* 🔒 Give an admitted fragment its own operations, not the document's

An admitted `<File />` was core's ordinary component, which resolves
`API.Files` when it runs. So the authority a generated fragment exercised
was whatever the document, a repository component or middleware had
installed by then — and a handler composed around one could observe,
rewrite or answer a fragment's read.

A profile now states operations rather than definitions. A host names a
capability; `captureEvaluationProfile()` reads each method off the host's
object once, binds it behind a revocation the execution owns, and closes
core's own body over the result. A fragment reaches those five file
operations and one transport, and nothing else: not the rest of the host's
provider, not `API.Files`, not `API.Fetch`, not `API.Env`.

- `fragment-capabilities.ts` holds the bodies and the bound operations.
  One name resolves to one definition whose dispatch separates its
  spellings, as the ordinary `<File>` does.
- A directory scopes its content through the evaluation's own cursor, not
  the contextual environment, so nothing outside a fragment can move where
  its content writes.
- An admitted request is journaled through the same `persistFetch` an
  authored `<Fetch>` uses and answers with the same record.
- Core identities go to revision 2 and `Dir` to 3: the authority behind
  them narrowed, so a continuation granted under the composable one is
  refused rather than re-granted.
- Operations are revoked at execution teardown, registered before the
  first `install()`.

FE13 proves the separation, with the document's provider installed nearer
and throwing if reached. FE3 proves the producer projection bypasses
`content`, `tryContent` and `hasContent`, with an ordinary component in the
same run as a positive control that the handlers are live.

* ✅ Freeze the `<Evaluate>` acceptance matrix and settle its prose (#713)

The component and its captured capabilities were already green; what was
missing was evidence for most of the contract and prose that still
described the workflow registration this replaced.

Evidence, in three new homes plus the existing core tier:

- core `evaluate-component.test.ts` gains the constructs a fragment may not
  carry, `as` and its absence, two occurrences taking two durable names,
  exact-text and moved-ceiling continuations truncated to the admission,
  an unreadable retained record, and a document binding every composable
  channel it has before an `<Evaluate>` beneath it.
- core `evaluate-loaded-copy.test.ts` separates ownership from authority:
  the name cannot be registered, a middleware answer is refused however
  honest, honest delegation and a deliberate refusal both still work, a
  replacement written to ignore `allow` is never entered, and a repository
  `File` wins at an authored site while still not entering a fragment.
- cli `evaluate-component.test.ts` drives real `xmd run`, and reads the
  source symbols for the protected origin and approved description.
- cli `evaluate-workflow.test.ts` is Deno-only against a real run
  database: `source` and `text` behave identically there with no warning,
  `program` is a spelling no profile has, and the fragment reaches the
  run's Workspace rather than the caller's disk.

Two continuation rows needed the document to be byte-identical across
attempts, because the occurrence identity is part of the durable name — a
changed document makes a different record rather than a stale one, so a
producer that renders differently is what actually reaches the refusal.

Prose: architecture and both specs now describe a protected component
whose host supplies a ceiling rather than an implementation, entries that
name capabilities rather than definitions, the two disjoint input forms,
and the one-shot projection. New §5.3.2 and the Tier FE acceptance table.

The npm boundary asserts `<Evaluate>` explicitly, because `--changed`
cannot discover it: nothing in the emitted package shares a path with the
core sources the name is defined in.

Compatibility: core entries state revision 2 and `<Dir>` revision 3. A run
suspended under the earlier revisions refuses to resume rather than
silently receiving the narrower grant.

* 🔒 Give the protected content projection a testable lifetime (#713)

The one-shot projector was two booleans inside `expand.ts`, and its guards
were unreachable from a document: only canonical `<Evaluate>` consumes a
projector and it consumes one once, so no black-box test could make a
second call, retain a callback past a body, or race two. A test that could
would need a protected component of its own, which is a hole in the tier
the projector exists inside.

So the state machine is its own package-internal module. `expand.ts` keeps
what to project, under which authority and in which scope; the lease keeps
how many times it may answer and for how long. Three explicit states
rather than two flags, and consumption happens before the underlying
operation can suspend — a machine that marked itself spent on completion
would let a concurrent call project the same content twice.

`protected-content.ts` is imported by `expand.ts` and its own unit test and
by nothing else: no barrel, no host surface, no package export, no Context,
no Component api, no test-support.

Tier PC proves the machine directly, counting the underlying operation
rather than only the error: first call exact and second refused with one
run; a closed lease never constructing the operation; a retained callback
refusing; a concurrent call during a suspended first; spent after failure
and after cancellation; two independent leases; and idempotent close.

Integration evidence stays where it belongs. FE2 now pins the admitted
source to the exact projected bytes — the content region's own newlines
included, nothing trimmed and nothing added — and FE17 cancels inside a
live producer, waits for its cleanup, and proves no admission and no
fragment operation, with an early-halt negative control beside it.

* ✅ Prove replay and the compiled `<Evaluate>` boundary (#713)

FE11 replays a completed evaluation over its whole retained history. The
second run's recorder holds different bytes under the same path, so a
re-read would show up in the rendered output rather than only in a count —
and the output is identical to the first run's while the recorder's log
stays empty.

FE28 gains the compiled boundary beside the npm one, in the suite that
already asks `dist/xmd` what it carries. Both are asserted explicitly
because `--changed` cannot see either: nothing in the emitted package or
the binary shares a path with the core sources the name is defined in, and
no test shard builds the binary at all. The closed schema travels with it,
so a build shipping a widened …
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant