Repository navigation
Replace host evaluation profiles with Component.generated (#181) - #920
Conversation
A trusted host used to state what a generated fragment may do as a whole
`ExecutionInstallation.evaluation` profile: two contract tables of pinned
definitions, a Workspace basis read per invocation, a frozen Fetch timeout and
an exact request inventory. Most of that restated facts the components
themselves already owned, and the parts that did not — the retained roots and
the request allowlist — were live state a continuation was then compared
against.
A host now offers one thing: `Component.generated`, an ordinary contextual value
property mapping component name to the authored forms it offers. Core reads that
answer once, after initialization and the captured installation callbacks and
before public `Execution.execute` policy, validates it whole, resolves each
selected name once through the ordinary import chain, and retains the
implementation with its declaration's metadata. Which identity a spelling runs
under, which class it reaches, what its props are validated against and what
documents it all come from the declaration that owns the implementation.
Before:
- a host wrote `evaluation: { read: [fileReadEntry(), …], write: […] }`, and two
installations stating one refused;
- selecting `Glob`, `Fetch` or `Elicit` ran a body pinned past whatever the host
had legitimately registered for that name;
- `<Fetch>` carried an exact request inventory that preflight classified the
element against, and the admission retained it;
- the admission retained the run's Workspace roots and compared a continuation
against them.
After:
- a host composes `Component.generated` middleware; two handlers compose, and an
empty or malformed selection refuses setup rather than being defaulted;
- every selected name resolves to the actual host answer, and a replacement
offering no generated use of the selected spelling refuses setup;
- a destination bound is a live `Permissions.fetch` restriction in the owner
that performs the request, asked of the transformed request immediately before
transport, so a dynamic destination is an ordinary input to preflight. The
attachment reads what the host stated once and derives both halves from that
copy, so the reading that offers `<Fetch>` is the reading whose bound was
installed. The
bound is the complete request — destination, method and every header as it
will be sent — and a request or a bound that names one header twice is refused
rather than normalized, because a transport joins differently cased
occurrences into one sent header that neither of them stated;
- the `generated_xmd` record is marker 4: the selected rows and the class
selection, with no Workspace basis and no request inventory. Marker-3 records
are refused as unreadable history.
How it works:
host middleware answers Component.generated
-> invoke(): init, installation callbacks, documentation
-> assembleComponents(): registry, bundle, declarations, protected tier,
identity domains
-> admitGeneratedSelection(): one read, validated whole
-> resolveSelectedComponents(): one import per name, published where the
answer was produced
-> captureEvaluationProfile(): read/write tables derived from each owner's
generated metadata
-> public Execution.execute policy
-> executeDocument() reuses that same assembly
Canonical component assembly moved out of `executeDocument()` into the
invocation owner so the selection can resolve against the host environment
before policy runs; authored execution reuses the result rather than building a
second one. What that assembly does not decide is where the document's own
imports resolve from: those still go through the options the canonical request
settled to, so a policy's legitimate `request.withOptions()` search-path
replacement reaches authored imports and this execution's own description, while
the capture keeps the paths it resolved against.
Removed: `FragmentEvaluationInput`, `ExecutionInstallation.evaluation`,
`statesEvaluation`, `TWO_PROFILES`, the entry constructors,
`src/fragment-capabilities.ts`, `workspaceRootSelection`, the generated
`fetchTimeout` capture, and the `generated-observations.ts` wrapper whose only
consumer was its own tests. The refusal normalization that module also owned —
an admitted component's ordinary `Err` becoming a marked generated-request
refusal with a safe reason — moved into the capture guard.
Hosts migrated together: ordinary CLI run, REPL entries (the selection is data
on the profile, offered in each entry's own execution scope as the ordinary
fallback, so a plugin's or a host installation's own selection is the one that
entry runs under), isolated `host="run"` children, the Plan writer, and the
Workflow attachment, which offers its selection inside `host.attach` and states
its optional request bound beside it. Every ordinary host's default goes through
one helper that delegates first and answers only on `undefined`.
| // The owner's own dispatcher, when its answer has one. The definition above | ||
| // runs behind core's lifetime guard, so what a selection would read off it | ||
| // is core's function rather than the owner's — and a form dispatcher read | ||
| // off the wrong function selects no body at all. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // off the wrong function selects no body at all. |
| // nothing about it would tell an agent a name and no more. | ||
| // The owning declaration's own prose. A host selects a name and says | ||
| // nothing about it, so what describes an admitted entry is what describes | ||
| // the component everywhere else. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // the component everywhere else. |
| "File.Delete": ["self-closing"], | ||
| // Offered only where this host bounds what a request may reach. A name | ||
| // whose every request the live terminal would refuse is a name a fragment | ||
| // could write and never successfully call. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // could write and never successfully call. |
| const components = options.components; | ||
| // `bounds`, not the options: the name is offered exactly where a request | ||
| // policy was installed, so a reading that stated no bound cannot be followed | ||
| // by one that offers `<Fetch>` under no policy at all. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // by one that offers `<Fetch>` under no policy at all. |
| // Ambiguity is refused, never resolved. This bound admits a request whose | ||
| // headers it can read exactly, and two spellings of one name are not one | ||
| // header anybody stated — so there is nothing here to compare and the | ||
| // request is not this bound's. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // request is not this bound's. |
PR #920: Replace host evaluation profiles with Component.generated (#181)71 files, +4779 / -4893 Scope🔴 PR has 9672 lines changed. Split into focused PRs. 🟡 9672 lines changed. PRs under 400 receive more thorough review. 🟡 71 files changed. Are all changes related? 🟡 Changes span 8 directories. StructuralOxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 110 diagnostics across 20 files (19 rules) no-unused-vars (23): packages/core/src/components/Evaluate.ts, packages/cli/src/plan-writer-profile.ts, packages/cli/src/repl/program.ts (+6) CorrectnessFILE: packages/core/src/components/Evaluate.ts FILE: packages/cli/src/plan-writer-profile.ts FILE: packages/cli/src/repl/program.ts FILE: packages/core/src/components/Fetch.ts FILE: packages/core/src/components/Elicit.ts FILE: packages/cli/src/repl/program.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/evaluation-profile.ts FILE: packages/core/src/generated-selection.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts FILE: packages/cli/src/repl/expansion.ts FILE: packages/cli/src/cli.ts FILE: packages/core/src/evaluation-profile.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts FILE: packages/core/src/execute.ts |
…ult (#181) An `xmd workflow` execution that this host offers no generated selection — a completed replay, and both passes of a fork's compatibility preflight — was executed under the ordinary `xmd run` default instead. That default is contextual middleware installed in the scope enclosing every command, and it answers wherever nothing nearer does, so an execution that stated nothing inherited a vocabulary the workflow host never offered. With a host that registers the suspension `<Elicit>`, the leaked default then selected a name whose implementation in that run offers no generated use, and the whole fork was refused: the candidate definition diverges from the inherited history after 0 of 4 events, at the fork's own run record (workflow_run): a host selected "Elicit" in its paired form, and the declaration that owns that implementation offers no generated use of it. Each such execution now states `undefined` for itself — the answer core starts from, and what this command's installations stated before the selection became contextual. `PRR27` in `packages/cli/tests/pull-request-read-fork.test.ts` is the end-to-end evidence: it forks a run that already read a pull request, and it fails without this. `offering()` and the new statement live in `workflow-generated.ts` because the command imports the fork preflight, so the preflight cannot import the command back.
Four of this stack's new assertions sorted with `toSorted()`, which is ES2023.
`deno check` accepts it and `pnpm exec tsc --project tsconfig.node.json` does
not, so every `test-node` shard failed before running a test:
error TS2550: Property 'toSorted' does not exist on type 'string[]'.
`Object.keys()` hands back a fresh array, so sorting it in place moves nothing
a caller holds — which is what the rows beside these already do, for this
reason.
| // the scope around this already offers, and only absence defaults. An empty | ||
| // or malformed selection is a host statement core validates and refuses; | ||
| // replacing it here would answer a host's mistake with this command's | ||
| // permission. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // permission. |
| // offered its own would be choosing its ceiling instead of being given | ||
| // one. The target-run description `installSyntax()` bound above stays | ||
| // separate — describing a component neither imports it nor grants its | ||
| // effects. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // effects. |
| // compared against the copy taken here, and middleware that returns | ||
| // anything else returns something nobody published. A foreign | ||
| // implementation carrying core's own URLs is not this object, so | ||
| // copying them publishes nothing either. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // copying them publishes nothing either. |
| // identification refuses it above, because there would be nothing to hand | ||
| // back. What is left is an answer that copied fine and is not something a | ||
| // fragment can invoke. | ||
| // fragment can invoke — a Markdown definition, or a structural one. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // fragment can invoke — a Markdown definition, or a structural one. |
| // The paths *this capture* resolves against, copied: the selection below is | ||
| // resolved against the environment the host assembled, and a later edit to | ||
| // the caller's own array describes a different one. What the document's | ||
| // authored imports resolve against is the accepted request's, read there. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // authored imports resolve against is the accepted request's, read there. |
| // The private closures, built from the minted implementations rather than | ||
| // from the declarations: what a private name resolves to is the function | ||
| // this execution built, and it names nothing once the execution is torn | ||
| // down above. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // down above. |
| // middleware or document code ran, except the search paths — those are the | ||
| // accepted request's, so the caller closes over this and supplies them. Asked | ||
| // only when an occurrence renders: a run whose document never writes | ||
| // `<Syntax />` enumerates nothing. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // `<Syntax />` enumerates nothing. |
| } | ||
| // One read of this member, into data this module owns. The copy below is | ||
| // what every check and the retained map read; the host's object is never | ||
| // consulted again. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // consulted again. |
A host selects a name, and canonical execution resolves it through the ordinary
import chain — so a repository component on the search path is what a selected
name means. Those files could state nothing about generated use: the metadata
for `<Elicit>`, `<Glob>` and `<Fetch>` lived in core's registry, beside the
registration rather than on the component. Core's own component sources on a
search path were therefore a *different* declaration from the registration of
the same implementation, and selecting the name refused the run:
xmd test packages/core/src → 10 of 10 documents failed
xmd -e '<File path="x" />' --include packages/core/src/components
→ a host selected "Elicit" in its paired form, and the declaration that
owns that implementation offers no generated use of it.
Both are shipped commands: `xmd test <directory>` puts each document's own
directory on the search path, and CI smoke-tests the built-ins that way.
A function component may now export `generated`, read through the same
`admitGeneratedForms()` validator a registration goes through and held to the
forms the component accepts. `Elicit.ts`, `Glob.ts` and `Fetch.ts` state theirs
there, and the registry reads those values instead of restating them — one
declaration per component, whichever way an execution reached it. A module that
exports none is offered for no generated use, exactly as before; one that
exports something unusable refuses where it is loaded.
Exporting it is not permission. A fragment still needs the host's selection, the
authored form, the `allow` class and whole-fragment admission, and every effect
still crosses the installed providers and the effective `Permissions`.
`U5-3: a repository component is offered for what it declares, and no more`
drives both halves through the real loader: the offered copy runs in the
fragment, and a same-shaped copy that declares nothing refuses the selection.
Why
Hosts currently configure generated evaluation by assembling read/write contract tables, duplicating each component's forms, props and identity. That also mixes the generated vocabulary with live request policy and Workspace state.
This PR replaces that input with
Component.generated: a contextual selection of component names and authored forms. Core retains the selected implementations before execution policy runs; live Files and Fetch operations use the providers and permissions of the scope performing them.This is U5, the final upstream prerequisite for #181, following #905, #906, #912 and #919. #902's consumer integration follows this merge and removes its existing forwarding.
What changes
Before: hosts supplied
ExecutionInstallation.evaluation, with separate read/write tables. Duplicate profiles refused, some names bypassed host replacements, and generated admission retained Workspace and request inventories.After: hosts compose one name/form selection. For example, middleware installed in the execution's setup scope can offer File reads:
Core resolves File once and derives its props, implementation, read effect and identity URL from the owning definition. A paired File remains unadmitted.
undefinedmeans no generated evaluation. Defined empty or malformed selections refuse setup before policy, root import or a paired producer.undefined, including each REPL entry. Custom selections are preserved without automatic union.How it works
Authored imports still use the detached options accepted by execution policy, including supported search-path replacements. Those changes cannot widen the already captured generated selection.
A bounded Workflow host derives both implicit Fetch membership and its live Permissions restriction from the same captured request list.
Review guide
packages/core/src/generated-selection.tsandcomponent-api.ts: the value property, whole-selection validation and owned capture.packages/core/src/execute.ts: assembly before policy, canonical publication at the point that produces the answer, and authored resolution on settled options.packages/core/src/evaluation-profile.tsandgenerated-xmd.ts: retained definitions, lifetime guards, safe request-refusal classification for Plan recovery, and marker-4 reconciliation.packages/workflow/src/deno/workspace/evaluate.ts: undefined-only fallback, explicit Workflow vocabulary, and live request bounds.What must stay true
How to verify it
The focused regressions prove distinct boundaries:
At reviewed head
86c10507: the affected Workflow group passed 30 tests/224 steps; workflow-fetch and workflow-agent passed 2/19; execution-protocol, generated-selection and REPL passed 13/127. The Workflow regression suite passed 3/15, and check/lint passed. The Planner independently reran that suite and the retained request-capture probe.Earlier focused baseline
2f105ed7: all twelve frozen commands passed, including Node and Bun (257 tests each), documentation, dead-code, build and compiled/npm-bin journeys (7/21). The final two-file Workflow correction reran its affected evidence; this does not claim every baseline command ran again at the final head. Delivery verification remains separate.Scope
Included
Intentionally unchanged
New abstractions
generated-selection.tsvalidates the host's JavaScript-facing value before resolution.useGeneratedFallback()shares undefined-only composition across ordinary host setup and REPL entries.The test selection helper supplies the same contextual property from either a surrounding scope or an installation callback.
Each new abstraction has concrete consumers or a required boundary.
No speculative functionality is included.
Risks and limitations
ExecutionInstallation.evaluation,FragmentEvaluationInputand the old public entry/evaluation builders are removed. In-repository hosts migrate in this PR.Scope confirmation