diff --git a/architecture.md b/architecture.md
index fa26f8e2..12e45b4b 100644
--- a/architecture.md
+++ b/architecture.md
@@ -136,7 +136,7 @@ and categorization rather than clarify them, so both stay exactly as written.
| `JournalProvenance` | a non-operational, equality-only witness that a live publication stream descends from the exact journal backend a provider selected for one workflow run; it grants no append, read, execution, publication or reconciliation capability, and is meaningful only because the provider retains the witness it established and later requires exact equality |
| result object | the value a component binds instead of failing: `{ok: true, value}` or `{ok: false, …}`, whose failure members the component declares |
| syntax symbols | the complete, versioned description of what a document may write in one directory under one host profile: every structural construct the engine reserves, and the one implementation selection chooses for every other name. It is observation — describing an environment installs no operational state, runs nothing and journals nothing — and it is produced once per request and projected, never rediscovered per format. `xmd syntax` prints one for an environment nobody is running; canonical `` renders one for the site an element was written at, from the same construction and the same Markdown renderer |
-| execution environment | the private bundle of services and records one execution supplies while its document expands: what resolves a name, what routes it to a retained implementation, what the execution declared, and the records it keeps about its own identities, forms, vocabulary and source output. The execution builds it from what it captured and admitted, owns it, and passes it by value through canonical expansion — never through a context, because a context resolves by name and a name is not a secret. An expansion driven directly is handed none at all, which is what "nothing is installed here" means; there is no partial one. Its shape is an architecture and product boundary |
+| execution environment | the private bundle of services and records one execution supplies while its document expands: what resolves a name, what routes it to a retained implementation, what the execution declared, and the records it keeps about its own identities, forms, vocabulary and source output. The execution builds it from what it captured and admitted, owns it, and passes it by value through canonical expansion — never through a context, because a context resolves by name and a name is not a secret. An expansion driven directly is handed none at all, which is what "nothing is installed here" means; there is no partial one. Its shape is an architecture and product boundary, which is why the resolution an execution performs for one *import* is not on it: that terminal is handed to expansion by hand, as an internal argument beside this bundle, named by no entry point and reachable only by having been given it |
| syntax reference | the engine-owned lexical answer to "what may a document write here", carried by value on canonical core's execution environment beside what resolves an import. The execution builds one at its root from the selection inputs it captured before any installation, middleware or document code ran, or from the one set of symbols a trusted host stated for its profile; a trusted canonical evaluation boundary replaces it for the subtree it evaluates. It answers with text and decides nothing: a component the symbols name is not a component anything may run |
| run profile declarations | the component registrations a first-party package makes, held as plain values apart from the middleware, providers, activation and launchers its installer also arranges. The installer registers exactly those values and inspection reads exactly those values, so what a run installs and what the symbols report cannot drift |
| Plugin | trusted code installed once before an execution imports a root document. XMD bundles exactly one — `@executablemd/git` — and activates it for the run-profile commands: `run`, `plan`, `syntax`, and `workflow start`, `resume` and `fork`. The `xmd test` root is not one of them, because a harness that quietly gained the repository vocabulary would be claiming names its children are entitled to shadow; a nested `` child assembles the profile for itself rather than inheriting what its parent declined. Everything else an operator selects with `--plugin`, and a package that happens to be installed stays inert until it is named. A plain structural value carrying a name and one optional `install`; what it contributes is the existing `ExecutionInstallation` members, and what it composes through are the three stable contextual APIs below. Selecting one runs its module, and selected Plugin code can execute whatever the surrounding runtime permits: it is trusted, and it is not sandboxed |
@@ -1743,8 +1743,8 @@ component neither imports it nor grants its effects.
Bindings and structural constructs are engine-owned language syntax and require
no profile entry. Every built-in construct is available inside a generated
fragment with its ordinary semantics — `Content`, `Output`, `Return`, `Let`,
-`Each`, `If`, `Else`, `Switch`, `Case`, `Loop`, `Break`, `PrintErrors`,
-`Answers` and `Answer` — decided by the same source rules ordinary validation
+`Each`, `If`, `Else`, `Switch`, `Case`, `Loop`, `Break`, `All`, `Spawn`,
+`PrintErrors`, `Answers` and `Answer` — decided by the same source rules ordinary validation
and expansion read, so a construct means one thing whether a person or an Agent
wrote it. Preflight dispatches a reserved name to those rules before the
admitted table is consulted, which is why a construct is never reported as a
@@ -1766,8 +1766,10 @@ nothing it binds survives it, because that body may run no times at all — only
`` branches of a `` are checked from one incoming snapshot, so no
alternative supplies a binding to another; the union of what they can produce
becomes visible afterwards, which keeps reading a binding only one arm makes a
-runtime failure rather than a preflight refusal. ``, `` and
-`` bodies read and write the enclosing environment. Constructs keep their
+runtime failure rather than a preflight refusal. Each `` under an ``
+is checked from that same incoming snapshot and contributes nothing back, which
+is the binding contract the children actually run under. ``,
+`` and `` bodies read and write the enclosing environment. Constructs keep their
exact spelling in the retained source, never join the retained named-component
list, and change no record version.
@@ -3197,6 +3199,9 @@ for the loop it exits.
One state exists per execution of one value body. ``, ``, ``,
`` and `` keep the ambient one; a component invocation hides the caller's from the
invoked body, so a component's own `` satisfies its own declaration; a
+`` passes none either, and the shared rule refuses the `` that
+would have reached for one, so concurrent children cannot race to claim a value
+owned outside their ``; a
nested value body installs a separate one, and both executions stand; and
content the caller projected restores the caller's, through a Markdown
`` and a function component's `content()` alike, with every
@@ -3279,6 +3284,64 @@ partial replay rebuilds the selection through ordinary expansion while completed
effects in the chosen branch replay from their own records, and a completed
document replay reuses the retained result without expanding anything.
+### Concurrent spawned children
+
+`` and `` are the engine's own syntax, not components, so core owns
+what they mean and no repository file, registration, bundle or Plugin can supply
+either name. `` runs its direct `` children at the same time and
+waits for all of them. It takes no props, produces no value, and requires at
+least two spawns — one alone is the sequential document it was already written
+as.
+
+The whole structure is decided from source before a child is constructed: the
+paired forms, the absent props, the two-spawn minimum, that only blank text sits
+between the spawns, that no `` is written outside its direct ``, and
+that no `` or `` inside a spawn reaches for an owner outside it.
+One shared rule states all of it, and expansion, non-executing validation and
+the generated-fragment preflight read the same result — so a malformed construct
+starts no child anywhere, including in a fragment an Agent generated.
+
+**Concurrent expansion shares immutable inputs and nothing else.** Every child
+gets a binding environment and live overlay snapshotted from the one that
+reached the ``, an eval scope of its own, a fresh block counter, a private
+output buffer, its own checked-failure ledger, a copied hide set and an
+expansion path extended by its authored ordinal. Nothing merges back, so a name
+one child binds or rebinds is invisible to its siblings and to the work after
+``, and a resource it retains is released when it ends. The outer
+`` and `` owners are cleared at the boundary; a loop or value
+component created wholly inside a child establishes its own.
+
+**Durable identity comes from the source, never from the schedule.** The
+existing durable structured-concurrency substrate is the implementation: in
+source order, before any child starts, each `` receives one child
+coroutine of the one that reached the ``, and nesting produces the existing
+hierarchical identity. Effects inside a child keep their ordinary descriptions
+under that coroutine, so two children may derive the same local block id without
+colliding, and work after `` continues on the parent counter it would have
+had without any child. Each successful child closes with the ordered `{ text, exact }` runs it
+rendered. Exact presentation is a provenance recorded against the segment
+objects expansion marked, and those segments do not cross the join — so the runs
+are what carries it, live and on replay alike, and a retained close that does
+not read as that shape fails the run instead of rebuilding part of a document.
+
+**Output is authored order; the journal is completion order.** A child emits
+into its private buffer, and `` appends the renderings to its caller's
+region only after every child has succeeded. Which child finished first may
+decide when its records append and never what the document renders. A complete
+replay reads the child closes and runs no spawned work; a partial replay
+restores the children that closed and resumes only the unrecorded ones under the
+same identities. Inserting, removing or reordering spawns is a definition change
+and takes the existing divergence path rather than being reassigned by position
+or completion time.
+
+**Failure is the existing fail-fast join.** The first child failure fails the
+``, cancels every unfinished sibling and does not return until all of them
+have finished tearing down. No private buffer is emitted, records acknowledged
+before the failure stay durable, and interrupted work is never recorded as
+complete. Cancelling the parent takes the same ownership path, and no task,
+subscription, retained resource or buffer survives its spawn. No scheduler, no
+new record family and no document-visible concurrency control is introduced.
+
## Foreground commands
An executable block is a foreground child process. Its output reaches the reader
@@ -3930,7 +3993,8 @@ matching name says nothing about which registration answered. What settles it is
the selection itself, recorded where it is made:
- expansion opens an **execution-private import frame** before it asks the
- public import chain anything;
+ public import chain anything, and hands it to the terminal it builds for that
+ one import;
- canonical core resolution records, inside that frame, the exact name it was
asked for and the exact implementation it selected — the function object this
execution built from the host's factory, by identity;
@@ -3947,10 +4011,62 @@ provenance and no permission: what a handler decides is which implementation
runs, and an implementation running where canonical resolution did not select it
names nothing. Nested and re-entrant frames stay contained, because a frame is
opened per import and settled the moment that import answers, however it
-answered; concurrent interleaving leaves more than one selection in a frame and
-so yields no domain, which is the safe direction; and a replay decides the same
-way, against this execution's own factory-created implementation rather than
-against anything a previous run recorded.
+answered; and a replay decides the same way, against this execution's own
+factory-created implementation rather than against anything a previous run
+recorded.
+
+**A frame is owned by the import's own terminal, not by an Effection scope and
+not by the execution.** `` gives two `` children imports in flight at
+the same time, so "the frame this selection belongs to" can be neither "the last
+one anybody opened" nor "the one opened where this code happens to be running".
+Each import creates its own frame and its own `Component` Api descriptor whose
+core `importComponent` handler closes over that frame; the engine asks through
+that descriptor, and the selection is recorded by the terminal `next` reaches:
+
+- the descriptor carries the same stable Api name as the public one, so it
+ receives the same installed middleware in the same order — **a stable name
+ shares middleware, and shares no terminal authority**;
+- the resolution that terminal performs is the execution's own, handed to
+ expansion as an internal argument beside the execution environment rather than
+ on it, and carried by every recursion that can invoke a component — so a
+ component body, a branch and a spawned child resolve the way their execution
+ does, while an expansion driven directly and a generated fragment are handed
+ none and answer through the ordinary public chain;
+- middleware may observe, delegate, replace or refuse the answer, and may run
+ `next` in any descendant scope: `next` still terminates in the terminal that
+ dispatch was created with, so delegating through a scope of its own keeps the
+ identity;
+- sibling spawns resolve through different descriptors, so one spawned child's
+ selection cannot land in another's frame, and closing or cancelling one
+ neither clears nor authorizes the other;
+- nesting is unchanged and needs no stack: an inner authored import is its own
+ dispatch with its own terminal, so the outer one is neither shadowed nor
+ inferred from scheduling;
+- a handler that starts an import of its own by calling the public operation is
+ making a **separate** import, not delegating the authored one — it may resolve
+ normally, and it grants the authored import no identity; and
+- two selections in *one* frame — a handler delegating twice — still yield no
+ domain, which remains the safe direction.
+
+The frame is a closure and nothing else: it is in no map, under no key, and
+reachable only from the dispatch that opened it. Nothing is published, keyed or
+read from — not a scope, not a context, not a token, and not a durable coroutine
+id, which non-journaled expansion does not have. A counterfeit descriptor made
+with the same Api name changes only what is asked through that counterfeit; it
+cannot become the terminal of the engine's own call. Live expansion and replay
+decide this the same way.
+
+Canonical answer windows are owned the same way and by the same kind of value —
+an explicit object rather than a scope — but serially: component answers are
+resolved in one capture phase before any document expands, so one window is open
+at a time. Each provider request captures that window object before its handler
+runs, a claim states it back explicitly, and a handler delegating through a
+descendant scope answers the resolution it was opened in. One installation may
+answer more than one import with the same definition — a reusable provider owns
+one object and a claim is per window — and a second, *different* answer in
+either still refuses. Concurrent document imports happen after capture, with no
+window open at all: such an import may be answered as ordinary middleware, and
+acquires no profile identity.
The engine then mints one issuance per invocation, carrying that domain — or
none — the authored name for what a refusal says, and the frame the body is
diff --git a/packages/cli/tests/syntax-cli.test.ts b/packages/cli/tests/syntax-cli.test.ts
index 957363ea..6967d450 100644
--- a/packages/cli/tests/syntax-cli.test.ts
+++ b/packages/cli/tests/syntax-cli.test.ts
@@ -326,6 +326,7 @@ describe("Tier SX — the run profile the command describes", () => {
// The whole structural vocabulary this profile publishes, so a construct
// returning to it has to be written down here.
expect([...names(structural.entries)].sort()).toEqual([
+ "All",
"Answer",
"Answers",
"Break",
@@ -339,6 +340,7 @@ describe("Tier SX — the run profile the command describes", () => {
"Output",
"PrintErrors",
"Return",
+ "Spawn",
"Switch",
]);
for (const name of ["Terminal.Grid", "Terminal"]) {
diff --git a/packages/core/src/body-structure.ts b/packages/core/src/body-structure.ts
index c0ff0c68..99e24142 100644
--- a/packages/core/src/body-structure.ts
+++ b/packages/core/src/body-structure.ts
@@ -81,13 +81,20 @@ function collectOutputs(bodySegments: Segment[], minimumDepth: number): Componen
* this walk does not see is the only thing that still cannot declare one —
* another component's definition, and markdown produced at runtime — because
* it reads this body's source AST and nothing else.
+ *
+ * A `` is the one region of this body's own source the walk stops at.
+ * The spawned child owns no value body, so a `` inside one neither
+ * satisfies this body's declaration nor is an undeclared return of it: the
+ * `` rule owns that diagnostic, and counting it here would report one
+ * mistake as two while letting a spawn stand in for the return the body still
+ * has to write.
*/
function collectReturns(bodySegments: Segment[]): ComponentElement[] {
const declared: ComponentElement[] = [];
const walk = (segments: Segment[]): void => {
for (const segment of segments) {
- if (segment.type !== "component") {
+ if (segment.type !== "component" || segment.name === "Spawn") {
continue;
}
if (segment.name === "Return") {
diff --git a/packages/core/src/component-answers.ts b/packages/core/src/component-answers.ts
index 3ce4d1a2..05ba2a83 100644
--- a/packages/core/src/component-answers.ts
+++ b/packages/core/src/component-answers.ts
@@ -114,6 +114,11 @@ export function componentAnswerRegistrar(
*around(handler: ComponentAnswerHandler): Operation {
yield* Component.around({
*importComponent([name, position], next) {
+ // One request for this handler invocation, capturing the resolution
+ // window that is open right now. Minted here rather than carried on
+ // the installation: one installation serves every name it registered,
+ // and a retained handle states nothing about which resolution is
+ // being decided.
const asked = installation.open(name, position);
try {
return yield* handler(
diff --git a/packages/core/src/component-api.ts b/packages/core/src/component-api.ts
index 9ce938c3..f65dd7f9 100644
--- a/packages/core/src/component-api.ts
+++ b/packages/core/src/component-api.ts
@@ -175,16 +175,92 @@ export interface ComponentApi {
registry: ComponentRegistry;
}
-export const Component: Api = createApi("Component", {
+/**
+ * The mark an empty public terminal puts on what it raises: a namespaced,
+ * non-enumerable own property carrying the name that was asked for.
+ *
+ * A property rather than a class, because the readers are in a different copy of
+ * this module than the writer as often as not. A component loaded from disk with
+ * `--include`, and a middleware package holding its own copy of core, each build
+ * their own `Component` descriptor with their own empty terminal and their own
+ * error class — and an execution has to recognize "nothing answered this" across
+ * exactly that seam, which `instanceof` cannot do. The name is namespaced so it
+ * collides with nothing, and non-enumerable so it does not travel into whatever
+ * a reporter serializes.
+ *
+ * It conveys one fact — *this call reached an empty public terminal, asking for
+ * this name* — and no authority. Anything may put it on anything; all it can
+ * cause is the ordinary resolution an unanswered import gets anyway.
+ */
+const MISSING_IMPORT_PROVIDER = "@executablemd/core/missing-import-provider";
+
+/**
+ * What the public descriptor's own terminal raises when nothing answered.
+ *
+ * An execution's provider delegates to `next` first and performs its ordinary
+ * resolution only for this, so a descriptor that supplied a terminal of its own
+ * is answered by that terminal and never resolved twice. Exported for core,
+ * published from no package entry point, and carrying nothing but the name that
+ * was asked.
+ */
+export class MissingImportProvider extends Error {
+ override name = "MissingImportProvider";
+ constructor(asked: string) {
+ super(
+ `Component.importComponent("${asked}") has no provider. Install one with ` +
+ `Component.around({ importComponent }, { at: "min" }) before expansion.`,
+ );
+ Object.defineProperty(this, MISSING_IMPORT_PROVIDER, {
+ value: { asked },
+ enumerable: false,
+ writable: false,
+ configurable: false,
+ });
+ }
+}
+
+/**
+ * Whether `error` is an empty public terminal reporting that nothing answered an
+ * import of exactly `asked`.
+ *
+ * The whole mark is parsed rather than trusted: an own, non-enumerable property
+ * under the namespaced name, holding an object whose complete own-key set is one
+ * member, `asked`, whose value is the name this call asked for. Nothing is cast;
+ * a payload carrying anything besides that member, however hidden, is not the
+ * mark; and a mark that names another component — a foreign terminal's report
+ * about a *different* import, arriving here on some other failure — is not this
+ * call's.
+ */
+export function isMissingImportProvider(error: unknown, asked: string): boolean {
+ if (typeof error !== "object" || error === null) {
+ return false;
+ }
+ const mark = Object.getOwnPropertyDescriptor(error, MISSING_IMPORT_PROVIDER);
+ if (mark === undefined || mark.enumerable) {
+ return false;
+ }
+ const value: unknown = mark.value;
+ // The complete own-key set, so "one member" means one: `Object.keys()` would
+ // report a payload carrying hidden extras — a non-enumerable member, a symbol —
+ // as the single-member mark it is not.
+ if (typeof value !== "object" || value === null || Reflect.ownKeys(value).length !== 1) {
+ return false;
+ }
+ return Object.getOwnPropertyDescriptor(value, "asked")?.value === asked;
+}
+
+/**
+ * Every default but the registry, which each descriptor is given its own of:
+ * a `Map` living at module scope would be one table shared by every run in the
+ * process, and the lint rule that says so is right.
+ */
+const COMPONENT_DEFAULTS: Omit = {
// deno-lint-ignore require-yield
*importComponent(
name: string,
_position?: Readonly,
): Operation {
- throw new Error(
- `Component.importComponent("${name}") has no provider. Install one with ` +
- `Component.around({ importComponent }, { at: "min" }) before expansion.`,
- );
+ throw new MissingImportProvider(name);
},
// deno-lint-ignore require-yield
*applyModifiers(_modifiers: Modifier[], block: CodeBlockContext): Operation {
@@ -259,9 +335,52 @@ export const Component: Api = createApi("Component",
*handleFailure(failure: ComponentFailure): Operation {
throw failure.error;
},
+};
+
+export const Component: Api = createApi("Component", {
+ ...COMPONENT_DEFAULTS,
registry: new Map(),
});
+/**
+ * Ask one import through a descriptor whose terminal belongs to that import.
+ *
+ * The name is what shares middleware: a descriptor created with the stable
+ * `Component` name receives every handler installed anywhere, in the order they
+ * were installed, exactly as the public descriptor does. What a name does not
+ * share is the default handler — each `createApi()` instance owns its own — so
+ * the chain composed here terminates in the continuation this caller passed and
+ * in nothing a handler can reach, replace or reorder.
+ *
+ * That is the whole of the correlation. A middleware may delegate `next` through
+ * any descendant Effection scope and still arrive here; two imports asking at
+ * the same time have two descriptors and two terminals; and a counterfeit
+ * descriptor built with the same name changes only the calls made through it.
+ */
+export function importThroughTerminal(
+ name: string,
+ position: Readonly | undefined,
+ terminal: (
+ asked: string,
+ at: Readonly | undefined,
+ ) => Operation,
+): Operation {
+ const owned = createApi("Component", {
+ ...COMPONENT_DEFAULTS,
+ // Its own, and empty: what a registration installed is middleware, which
+ // this descriptor shares by name, so a registry asked through it is answered
+ // there exactly as it is through the public one.
+ registry: new Map(),
+ *importComponent(
+ asked: string,
+ at?: Readonly,
+ ): Operation {
+ return yield* terminal(asked, at);
+ },
+ });
+ return owned.operations.importComponent(name, position);
+}
+
export const importComponent: Operations["importComponent"] =
Component.operations.importComponent;
export const applyModifiers: Operations["applyModifiers"] =
diff --git a/packages/core/src/component-failures.ts b/packages/core/src/component-failures.ts
index fb8bc2c3..9db405ed 100644
--- a/packages/core/src/component-failures.ts
+++ b/packages/core/src/component-failures.ts
@@ -20,7 +20,7 @@
*/
import { Component, raise } from "./component-api.ts";
-import { attributeCause, ErrorMode } from "./errors.ts";
+import { attributeCause, documentationError, ErrorMode } from "./errors.ts";
import type { ComponentFailure, ErrorSegment, FunctionComponent } from "./types.ts";
import type { Operation } from "effection";
@@ -92,6 +92,27 @@ export function containedLedger(inherited: CheckedFailures | undefined): Checked
return { authorized: inherited?.authorized ?? false };
}
+/**
+ * Refuse a successful outcome for work that suffered an unauthorized checked
+ * command failure.
+ *
+ * The failure was already raised where the command ran, and something enclosing
+ * it — a `printErrors(fn)` component like ``, or one that caught the
+ * `ContentError` its projected content raised — printed it and returned. Those
+ * boundaries decide how a failure of their own is reported. Whether a command
+ * that exited nonzero failed the run is not theirs to decide (#441).
+ *
+ * Whoever owns the ledger says so: the execution for the run's own, and a
+ * spawned child for the one it keeps to itself, which is how a checked failure
+ * inside one `` fails that child rather than stopping a sibling's walk.
+ */
+export function* refuseCheckedFailure(checkedFailures: CheckedFailures): Operation {
+ const segment = checkedFailures.failure;
+ if (segment !== undefined) {
+ throw yield* documentationError(segment, "output");
+ }
+}
+
/**
* Continue after this component fails, reporting the failure as a printed error.
*
diff --git a/packages/core/src/components/component-resolution.ts b/packages/core/src/components/component-resolution.ts
index e46a6c1f..39393220 100644
--- a/packages/core/src/components/component-resolution.ts
+++ b/packages/core/src/components/component-resolution.ts
@@ -199,25 +199,54 @@ export interface IdentifiedAnswer {
readonly definition: ImportedDefinition;
}
-/** One claim this owner recorded, with core's own copy of what was claimed. */
+/**
+ * What one answer object was claimed to be, and which resolutions claimed it.
+ *
+ * One implementation says what it is once, so the name, the identity and the
+ * installation belong to the object: a new resolution cannot rename it, restate
+ * its origin or revision, or take it for another provider.
+ *
+ * Which import it answered is a different question, and the answer to it is per
+ * resolution — so the copies are keyed by the window *object*, not by a number
+ * describing one. A caller asking about provenance presents the window it is
+ * holding, so the comparison is between two references to one thing rather than
+ * between a record and whatever the owner's mutable current state happens to
+ * say. A number would have to be trusted against that mutable state; an object
+ * cannot be forged into being the one the caller opened.
+ *
+ * A reusable provider returns one immutable definition to every import it
+ * answers. Keying the claim by the object alone made the first resolution part
+ * of the object's permanent identity and refused the same definition the second
+ * time, which is a provider being punished for not copying itself.
+ */
interface Claim {
readonly name: string;
readonly identity: AnswerIdentity;
/** Which provider installation stated it, so a second cannot overwrite. */
readonly installation: object;
/**
- * The exact resolution this was an answer to.
+ * Core's copy of the definition the first claim described.
+ *
+ * The identity above is a statement about *this* definition, so a later claim
+ * of the same object has to be a claim about the same definition. Without this
+ * baseline, a provider could claim an object, edit it, and have the next window
+ * retain the edited version under the identity and revision that described the
+ * original — a different implementation wearing the first one's name.
*
- * The window *object*, not a number describing one. Provenance is a question
- * about which import a statement answered, and a caller asking it presents
- * the window it is holding — so the comparison is between two references to
- * one thing rather than between a record and whatever the owner's mutable
- * current state happens to say. A number would have to be trusted against
- * that mutable state; an object cannot be forged into being the one the
- * caller opened.
+ * Absent when the first claim could not be copied at all, which is not a state
+ * a later claim can improve: an answer core could not retain is not one it can
+ * start vouching for because the object has since changed.
*/
- readonly window: ResolutionWindow;
- readonly canonical: ImportedDefinition | undefined;
+ readonly baseline: ImportedDefinition | undefined;
+ /**
+ * One retained copy per resolution this answer was claimed in.
+ *
+ * Weak on the window, so a claim lasts exactly as long as somebody still holds
+ * the resolution it belongs to. Presence is the provenance check: a window
+ * that never claimed this answer has no entry, and `identify` answers nothing
+ * for it however many other windows did.
+ */
+ readonly copies: WeakMap;
}
/** A provider request used after its execution ended. */
@@ -300,7 +329,14 @@ export interface OpenAnswerRequest {
* cannot revive a settled one or reach another provider's.
*/
export interface ProviderInstallation {
- /** Begin one handler invocation's request for one asked name. */
+ /**
+ * Begin one handler invocation's request for one asked name.
+ *
+ * The request captures the resolution that is open when it is minted, by
+ * identity, which is how it finds the asking it answers. Nothing about where
+ * the handler runs takes part in that, and nothing about it reaches the
+ * provider.
+ */
open(name: string, position?: Readonly): OpenAnswerRequest;
}
@@ -335,7 +371,7 @@ export class CanonicalImports {
*/
#active = false;
/**
- * The one resolution a claim may be stated during, when one is open.
+ * The one resolution this owner has open, if it has one.
*
* A provider installation outlives every resolution it takes part in, while a
* fresh request belongs to one handler invocation. Canonical execution asks
@@ -348,25 +384,36 @@ export class CanonicalImports {
* belonging to a resolution other than the live one, and the name keeps a
* handler settled for one name from recording under it while a different
* name is being resolved.
+ *
+ * One window at a time, because component answers are resolved in one serial
+ * capture phase before any document expands: the execution asks for each
+ * admitted name in turn, and nothing concurrent is resolving beside it. A
+ * request captures the window object itself before its handler runs, so a
+ * handler delegating through any number of descendant scopes still answers the
+ * resolution it was opened in — no scope takes part in this at all.
*/
#window: ResolutionWindow | undefined;
/** How many resolutions this owner has opened, so each one is its own. */
#occurrences = 0;
/**
- * The resolution each installation last stated an answer for.
+ * The resolutions each installation has already stated an answer for.
*
* Secondary. What proves a statement belongs to an import is the request's
* captured window, not this; this only keeps one provider from naming two
* different implementations for one import, where only one of them could be
* what it resolved to.
*
- * Keyed by the installation's own frozen token and holding the window object,
- * so an installation stays reusable: it answers several admitted names and
- * the same name resolved more than once, because each of those is a different
- * window. Spending the installation itself would break a valid multi-name
- * provider, which the contract does not ask a host to split up.
+ * A set of windows per installation rather than the last one it answered.
+ * Windows follow one another, and one installation may legitimately answer in
+ * window A and then in window B — while a request kept from A may still try to
+ * state a second, different answer for A after B was answered. Remembering only
+ * the most recent window would have let that one through.
+ *
+ * Keyed by the installation's own frozen token, so an installation stays
+ * reusable: it answers several admitted names and the same name resolved more
+ * than once, because each of those is a different window.
*/
- readonly #spent = new WeakMap