diff --git a/architecture.md b/architecture.md
index 72bada740..dc79fd543 100644
--- a/architecture.md
+++ b/architecture.md
@@ -3433,12 +3433,12 @@ filesystem for it to hand. What a fragment may *do* is then the ordinary
question asked of any caller: the provider that is installed, and the policy of
the scope the element runs in.
-The narrowing these revisions record is the one that still stands — an entry
-authorizes one component under one spelling — so core's entries state revision 2
-and the bundled Git Plugin's `
` states revision 3. **A run suspended under
-the earlier revisions refuses to resume rather than silently receiving the
-narrower grant**, which is what a revision is for: a continuation resumes only
-under the exact identities it was admitted with.
+The narrowing these changes record is the one that still stands — an entry
+authorizes one component under one spelling, and what that spelling runs as is
+the owning declaration's to state. **A run suspended under an earlier record
+shape refuses to resume rather than silently receiving a grant nobody compared**:
+a continuation resumes only under the exact identities and spellings it was
+admitted with.
`` is one Agent turn. The host hides no Agent loop inside it. Ordinary
authored Markdown owns iteration with ``, branches on whether a reply
@@ -3590,20 +3590,17 @@ reached when the two disagree. That check reads the engine's own account of the
element — not `Component.hasContent()`, not the method on the object the
component was handed, and not the trusted wrapper the evaluator answers the
import with, which collects what a read returned and is not the form dispatcher. So
-an admitted `File:read` does not become a write and an admitted `File:write`
-does not become a read, however many times something is asked.
+an admitted reading ` ` does not become a write and an admitted writing
+`… ` does not become a read, however many times something is asked.
The standard Deno workflow profile's write table is three entries, in the order
-it states them: core's paired `File:write`, the bundled Git Plugin's paired
-`Dir` under the versioned origin
-`@executablemd/workflow/composition/dir-v2#Dir` — built from the same
-implementation canonical core owns so the grant and the authored component
-cannot drift — and core's self-closing `@executablemd/core#File.Delete`. That origin
-names where released history recorded the entry, not what owns it today:
-canonical core owns ``, the Git Plugin states the identity its released
-runs retained, and the string stays unchanged so those runs still replay. A
-host's own
-extensions come after them. Selecting `write` intentionally authorizes Dir's
+it states them: core's paired ``, the paired `` the bundled Git
+Plugin asks for — the same implementation canonical core owns, under the
+identity core's own declaration offers that spelling as a write, so the grant
+and the authored component cannot drift — and core's self-closing
+``. What the Git Plugin decides is that a workflow run admits the
+name at all; an ordinary run admits none. A host's own extensions come after
+them. Selecting `write` intentionally authorizes Dir's
persistent recursive directory creation as well as the mutations File and
File.Delete perform. Each is the ordinary component and crosses `API.Files`
into the run's transaction-bound provider, so an ensured directory is retained
@@ -3614,16 +3611,17 @@ authors output around it. Local Git, Git-host, issue,
process, eval and exec, native-command, credential and external-write effects
are outside the class.
-The versioned Dir origin is a grant boundary, not a symbol decoration. The
-former `@executablemd/workflow/composition#Dir` identity authorized lexical
-placement that created nothing; it does not authorize the persistent mutation.
-A continuation whose retained write table names that former identity is refused
-before generated execution instead of inheriting a broader grant. Read-only
-admissions never selected the write table and remain unaffected.
+The identity a spelling runs under is a grant boundary, not a symbol
+decoration. An earlier `` grant authorized lexical placement that created
+nothing, and it does not authorize the persistent mutation this one does. A
+continuation whose retained write table names a different identity for that
+spelling is refused before generated execution instead of inheriting a broader
+grant. Read-only admissions never selected the write table and remain
+unaffected.
-That order is retained identity rather than presentation. A continuation is
+That order is retained grant rather than presentation. A continuation is
compared against the entries position by position, so adding the deletion
-identity to the table refuses a continuation that was admitted under the former
+spelling to the table refuses a continuation that was admitted under the former
two-entry one — before any generated effect, and deliberately: a run resumed
under a wider grant than it was admitted with is the thing the comparison
exists to refuse. A read-only admission never selected the write table and is
@@ -3656,31 +3654,29 @@ root must still be retained, and the root the run now stands on must be a
retained one, while additional roots and an advanced current root change
nothing the admission was granted under. A run that no longer retains an
admission root has lost the history the grant was made over and is refused
-before generated work. Every non-root term — the class selection, the selected
-pinned identities with their admitted forms, and the normalized request
-ceilings — compares whole and exactly, in canonical order. A pinned identity is
-one closed tagged record — whether canonical core supplies the body or the
-ordinary import chain answered for it, the origin, the component key and the
-revision — rather than an assembled spelling, so a reader comparing two
-admissions can say which term moved and no two hosts have to agree on a
-separator. The untagged version-1 record holds one opaque string instead —
-whatever the party that built the pinned entry chose, which for core was
-`@executablemd/core#File:read` and for the workflow host was
-`@executablemd/workflow/composition/dir-v2#Dir`, with no revision in either and
-no convention to read back out. Nothing recovers four structural terms from one
-of those, so nothing tries: a *capability* entry states the exact version-1
-strings it succeeds, and a retained string reconciles against that stated list
-and against nothing else. Stating one is an assertion by whoever owns the entry
-that the current grant is no wider than the old one; an entry that states none
-refuses every version-1 record naming it. A component answer states no alias and
-has nowhere to put one, because that arm did not exist when version-1 records
-were written. New records always state the structural identity. The run's own
-ceiling table is keyed by those terms encoded rather than joined, because a
-joined key is only as unique as the separator is illegal and nothing makes a
-space illegal in an origin or a component key: two entries whose terms differ
-only in where a space falls each keep their own admitted requests. Only the
-tables the selection reached take part, so changing a write table a read-only
-admission never drew on changes nothing it was granted under. Deciding the
+before generated work. Every non-root term — the class selection, the
+admitted spellings, and the normalized request ceilings — compares whole and
+exactly, in canonical order. The retained policy holds one row per admitted
+spelling: the name, the authored spelling, the absolute
+`https://executable.md/` identity that spelling runs under, whether canonical
+core supplies the body or the ordinary import chain answered for it, and which
+effect table admitted it. So a reader comparing two admissions can say which
+spelling of which name moved, and no host has to agree with another about how
+to spell anything — the identity is stated by the declaration that owns the
+implementation, where ` ` reads under
+`https://executable.md/components/file/read` and `… ` writes under
+`.../file/write`. There is one such record shape, marked as itself: a record
+an earlier build wrote — the untagged #369 one, or the tagged one whose
+identities were an origin, a component key and a revision — is refused as
+unreadable history before the admission it would have restored performs
+anything, rather than being migrated or read in part. An updated implementation
+offered under the same identity is not a changed grant, because between
+executions that is a newer build of the component the admission named. The run's
+own ceiling table is keyed by the entry itself rather than by anything
+describing one, so two entries that look alike each keep their own admitted
+requests. Only the tables the selection reached take part, so changing a write
+table a read-only admission never drew on changes nothing it was granted
+under. Deciding the
fragment inside that durable effect is what makes this hold: a continuation
restores what was admitted without consulting the current candidate at all, so
what expands is the source this run admitted rather than whatever a later caller
@@ -3712,15 +3708,16 @@ exactly; anything else is refused before `API.Fetch` is reached. A repository
component that takes the name `Fetch` is not that identity and acquires no
admission from it.
-`` is admitted only in its self-closing read form. The component reads
-when it has no content and writes when it has some, so the two forms are two
-identities and the constraint travels with the pinned one, decided in the
+``'s two spellings are two grants. The component reads when it has no
+content and writes when it has some, so a read-only selection admits the
+self-closing spelling alone, and which one an element wrote is decided in the
whole-fragment preflight rather than inside the component after earlier elements
-have run.
+have run. A repository component that takes the name `File` is admitted as
+neither spelling.
-Mutation-proposal admission and workflow-bundled Markdown component admission
-remain unbuilt. Directory registration is not among them: a workflow Agent is
-given no directory to register.
+Workflow-bundled Markdown component admission remains unbuilt. Directory
+registration is not among the gaps: a workflow Agent is given no directory to
+register.
#### Sealed evaluation entries
@@ -3752,21 +3749,29 @@ admitted one rather than its props, and that ceiling is installed as an ordinary
`Permissions` restriction for the invocation, so a contextual transformation that moves
the request off it is refused by the terminal rather than sent.
-A *component answer* names an implementation the ordinary import chain resolves, and
-states only the identity a provider must have registered for it; canonical execution
-resolves that name once — before the root import and before any document code, through
-the complete ordinary `Component.importComponent` chain and a private terminal that
-writes no record — and asks the identity owner about the exact final answer in one call.
-That call answers with the registered identity *and* core's own copy of what was
-registered, taken when the identity was registered: the check and the thing kept are one
-result, so nothing downstream reads the chain's object a second time and an answer whose
-members read differently on each read cannot pass a check with one reading and be sealed
-with another. The identity is then compared whole, and the copy — not the answer — is
-sealed behind the same revocation.
-
-One name states one identity, however many forms and tables hold it: two entries under
-one name are the two spellings of one component and share one lookup and one sealed
-implementation, while a second identity for that name, or a name held as both a
+A *component answer* names an implementation the ordinary import chain resolves. The
+entry states the name and the spellings it admits, and nothing about what is behind
+them: canonical execution resolves that name once — before the root import and before
+any document code, through the complete ordinary `Component.importComponent` chain and a
+private terminal that writes no record — and asks the publication owner about the exact
+final answer in one call. That call answers with core's own copy of the definition a
+provider published, taken at publication: the check and the thing kept are one result,
+so nothing downstream reads the chain's object a second time and an answer whose members
+read differently on each read cannot pass a check with one reading and be sealed with
+another. The copy — not the answer — is sealed behind the same revocation.
+
+What an admitted spelling then runs as comes from that definition's own declaration: an
+absolute `https://executable.md/` identity and one of the two effects, per authored
+spelling, read where the definition is published and retained with it. So the table says
+which names and spellings a fragment may write, and the owner says what writing them
+runs — an entry whose spelling its owner offers no generated use of, or offers as the
+other effect, refuses at capture rather than acquiring a grant from the table that named
+it. The installation origin a publication carries is provenance a reader and a
+documentation symbol are shown, and decides nothing.
+
+One name resolves to one implementation, however many spellings and tables hold it: two
+entries under one name are the two spellings of one component and share one lookup and
+one sealed definition, while two definitions under one name, or a name held as both a
capability and a component answer, refuses at capture rather than letting assembly order
decide.
@@ -3774,28 +3779,27 @@ Provider installation and the right to answer one occurrence are separate: insta
receives a registrar that installs import middleware, and every invocation of that
middleware receives a fresh request fixed to the exact name, position, provider
installation, origin and resolution-window object it was asked under. Only that request
-may register an identity, and it states the answer plus key and revision without
-restating the name. Canonical execution closes the request synchronously when its
+may publish, and it presents the definition it is answering with — not a name, a key or
+a revision beside it. Canonical execution closes the request synchronously when its
handler returns, fails or is cancelled; an outer request remains live while it delegates
-and may register a replacement after the inner handler returns. Enclosing resolution
+and may publish a replacement after the inner handler returns. Enclosing resolution
still closes in its own `finally` on success, fallback, failure or cancellation. A
-registration therefore requires the execution, request and exact current window to
+publication therefore requires the execution, request and exact current window to
remain active, the request's fixed name to match that window, and that provider
-installation not to have stated a different answer in the window.
+installation not to have published a different answer in the window. Publishing the same
+object again is what a reusable provider does for every import it answers, so a
+publication is compared against the copy taken the first time: an object edited after it
+was published cannot be rebaselined by a later window.
-Identification takes the expected window explicitly and accepts only a registration
+Identification takes the expected window explicitly and accepts only a publication
recorded for that exact object and name, so a stale request cannot answer a later
same-name resolution or retag itself while another name is live.
The installation is reusable: one provider may answer several admitted names and
repeated resolutions through distinct requests. All of this state is held in ordinary
private closures — no Context, shared symbol, public brand or module-global registry —
-and an unidentified middleware replacement remains a valid ordinary import answer
-outside fragment evaluation.
-
-A capability entry also states the exact version-1 identity strings it succeeds, which
-is the only thing a released untagged record reconciles against; a component answer
-states none. A capability-only profile performs no such lookup at all.
+and a middleware replacement that publishes nothing remains a valid ordinary import
+answer outside fragment evaluation.
Resolution happens at capture and never again: a fragment runs the sealed snapshot, a
continuation resolves once more in its own capture and reconciles before any effect, and
@@ -3805,7 +3809,7 @@ generated import, which only canonical execution answers.
A trusted layering control may enter one package bootstrap through an inherited layer
and an execution-local one — documentation, registrations and the provider together —
and the repeat is additive: the outer entry observes and delegates rather than
-registering a second identity for an implementation that already states what it is, and
+publishing a second answer for an implementation one publication already settled, and
a non-identical documentation overlap on one owner and component still refuses at the
child collection boundary, before the root import and therefore before the provider is
asked at all.
@@ -4670,13 +4674,16 @@ 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 registration states it back explicitly, and a handler delegating through a
-descendant scope answers the resolution it was opened in. One installation may
+runs, a publication presents it back explicitly, and a handler delegating through
+a descendant scope answers the resolution it was opened in. A provider publishes
+the definition and nothing else: which identity and effect each of its spellings
+runs as is on that definition, declared by whoever owns the implementation, so
+there is nothing beside it for a reader to reconcile. One installation may
answer more than one import with the same definition — a reusable provider owns
-one object and a registration is per window — and a second, *different* answer in
+one object and a publication 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.
+acquires no admitted 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
@@ -5568,9 +5575,9 @@ imports still require their own settled selection frame; a routed function
cannot repair a missing or multiply selected domain. Paired producers retain
their lexical site route and operational permission.
-A trusted provider may therefore delegate and register canonical self-closing
-`` as a component answer. Its default bare form reports the admitted
-vocabulary; its named form renders the enclosing authoring documentation with
+A trusted provider may therefore delegate canonical self-closing ``,
+which canonical core publishes for itself, and have it admitted as a component
+answer. Its default bare form reports the admitted vocabulary; its named form renders the enclosing authoring documentation with
narrowed availability. Verified protected documentation provenance survives
sealing even when the delegating provider has a different origin. That
provenance is descriptive data and never a callable lookup key.
diff --git a/packages/cli/src/evaluation-profile.ts b/packages/cli/src/evaluation-profile.ts
index fe96a11cf..cb33bc8ec 100644
--- a/packages/cli/src/evaluation-profile.ts
+++ b/packages/cli/src/evaluation-profile.ts
@@ -38,11 +38,11 @@
*
* `` is absent rather than present-and-bounded. An unbounded network
* read is a decision this command does not make on a document's behalf, and
- * admitting the identity with an empty ceiling would be a different thing from
- * not admitting it — the fragment would name a component it can never
- * successfully call.
+ * admitting the name with an empty ceiling would be a different thing from not
+ * admitting it — the fragment would name a component it can never successfully
+ * call.
*
- * ## No Workspace, and no alias
+ * ## No Workspace
*
* An ordinary run evaluates against no Workspace. Its admitted effects address
* the Files provider its own execution installed, and there is no immutable
diff --git a/packages/cli/src/workflow.ts b/packages/cli/src/workflow.ts
index 70cff4380..220057763 100644
--- a/packages/cli/src/workflow.ts
+++ b/packages/cli/src/workflow.ts
@@ -1076,8 +1076,9 @@ export function runWorkflow(
// about the component. Stated where the Workspace is attached — a
// completed replay restores its retained output and expands nothing, so
// it needs no ceiling of its own.
- // ``'s entry comes from the package that owns the component, so
- // the write table names one identity rather than two copies of it.
+ // ``'s entry comes from the Git Plugin, which decides that a
+ // workflow run has one at all; the identity it is admitted under is
+ // core's, from the declaration that owns the component.
...(completed || replay
? []
: [
diff --git a/packages/cli/tests/evaluate-workflow.test.ts b/packages/cli/tests/evaluate-workflow.test.ts
index 061d2747d..1bbb35d29 100644
--- a/packages/cli/tests/evaluate-workflow.test.ts
+++ b/packages/cli/tests/evaluate-workflow.test.ts
@@ -106,19 +106,15 @@ describe("Tier FE — the workflow host's profile", () => {
});
});
- it("FE23: Git's directory entry is the exact identity retained history holds", function* () {
- // Written out here rather than compared against the implementation, so a
- // change to either side is a change to this row. Every released journal
- // holds these strings: the origin a released build recorded, the revision
- // it recorded, and the version-1 alias whose grant this entry answers for.
+ it("FE23: Git's directory entry admits core's own Dir and states no identity", function* () {
+ // What this host decides is that a workflow run has a `` at all. What
+ // admitting it grants is core's: the identity and the effect its paired
+ // spelling runs under are on the declaration that owns the component, so
+ // there is nothing here for a host to restate or to move.
const entry = gitDirectoryEntry();
expect(entry.name).toBe("Dir");
- expect(entry.identity).toEqual({
- origin: "@executablemd/workflow/composition",
- key: "Dir",
- revision: "3",
- });
- expect(Reflect.get(entry, "legacy")).toEqual(["@executablemd/workflow/composition/dir-v2#Dir"]);
+ expect(Object.keys(entry)).not.toContain("identity");
+ expect(Object.keys(entry)).not.toContain("legacy");
// It is the directory capability, not some other entry wearing the name.
expect(Reflect.get(entry, "capability")).toBe("directory:ensure");
});
diff --git a/packages/cli/tests/plan-command-document.test.ts b/packages/cli/tests/plan-command-document.test.ts
index 0f182934a..1806effad 100644
--- a/packages/cli/tests/plan-command-document.test.ts
+++ b/packages/cli/tests/plan-command-document.test.ts
@@ -1184,12 +1184,7 @@ describe("Tier PI — read-only information requests", () => {
/** The admitted entry the barrier is written as. */
function heldEntry(): FragmentEntry {
- return {
- kind: "component-answer",
- name: "Held",
- identity: { origin: HELD_ORIGIN, key: "Held", revision: "1" },
- forms: ["self-closing"],
- };
+ return { kind: "component-answer", name: "Held", forms: ["self-closing"] };
}
/** One barrier: a body whose cleanup blocks until the case releases it. */
@@ -1209,6 +1204,13 @@ describe("Tier PI — read-only information requests", () => {
kind: "function",
name: "Held",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: {
+ "self-closing": {
+ identity: "https://executable.md/components/held/read",
+ effect: "read",
+ },
+ },
*fn(): Operation {
// Registered before anything can fail, so the barrier is reached
// whichever way this projection ends.
@@ -1219,7 +1221,7 @@ describe("Tier PI — read-only information requests", () => {
return "held";
},
},
- { origin: HELD_ORIGIN, key: "Held", revision: "1" },
+ { origin: HELD_ORIGIN },
),
};
}
diff --git a/packages/cli/tests/plan-component.test.ts b/packages/cli/tests/plan-component.test.ts
index d318b645b..3e7d1645f 100644
--- a/packages/cli/tests/plan-component.test.ts
+++ b/packages/cli/tests/plan-component.test.ts
@@ -2027,9 +2027,9 @@ describe("Tier FE — Plan produces text, Evaluate runs it", () => {
* The profile these rows evaluate under: the read entry, and never the wide
* one.
*
- * The entry's identity is the fixture's own, so the catalog an agent is shown
- * inside an evaluation names `test://fe713/read` and the admitted capability
- * is core's file read behind it.
+ * The entry's name is the fixture's own, so the catalog an agent is shown
+ * inside an evaluation names `` and the admitted capability is
+ * core's file read behind it.
*/
function fixtureProfile(): FragmentEvaluationInput {
return {
@@ -2037,7 +2037,6 @@ describe("Tier FE — Plan produces text, Evaluate runs it", () => {
{
...fileReadEntry(),
name: READ_COMPONENT.name,
- identity: { origin: READ_COMPONENT.origin, key: "FE713.Read", revision: "1" },
description: READ_COMPONENT.description,
},
],
@@ -2163,7 +2162,10 @@ describe("Tier FE — Plan produces text, Evaluate runs it", () => {
expect(seen).toHaveLength(1);
const catalog = seen[0] ?? "";
expect(catalog).toContain(READ_MARKER);
- expect(catalog).toContain(READ_COMPONENT.origin);
+ // Reported as the implementation it runs rather than as the name it was
+ // admitted under: the entry is core's own file read, which is what the
+ // host chose to admit under a name of its own.
+ expect(catalog).toContain("@executablemd/core");
// The wide-only component exists in this scope and is not in the profile,
// so its absence here is the narrowing rather than the fixture.
expect(catalog).not.toContain(WIDE_MARKER);
diff --git a/packages/cli/tests/workflow-agent.test.ts b/packages/cli/tests/workflow-agent.test.ts
index 4f2f46905..84477ac25 100644
--- a/packages/cli/tests/workflow-agent.test.ts
+++ b/packages/cli/tests/workflow-agent.test.ts
@@ -722,16 +722,21 @@ describe("Tier WAL — the workflow Agent observation loop", () => {
});
expect(attempt.failure).toBe(undefined);
- // The standard profile admitted it: one record, naming both write
- // identities and the classes the document selected.
+ // The standard profile admitted it: one record, naming both admitted
+ // spellings and the classes the document selected.
const recorded = admitted(attempt.events);
expect(recorded).toHaveLength(1);
const policy = JSON.stringify(recorded[0]);
- expect(policy).toContain("File:write");
- // The identity is a structural record, so the origin, the key and the
- // revision are read as themselves rather than as one assembled spelling.
- expect(policy).toContain('"origin":"@executablemd/workflow/composition"');
- expect(policy).toContain('"key":"Dir","revision":"3"');
+ // What each spelling runs as comes from the declaration that owns the
+ // implementation, so the row holds that URL and the table that admitted
+ // it rather than a spelling a host assembled.
+ expect(policy).toContain(
+ '"name":"File","form":"paired","identity":"https://executable.md/components/file/write"',
+ );
+ expect(policy).toContain(
+ '"name":"Dir","form":"paired","identity":"https://executable.md/components/dir/ensure"',
+ );
+ expect(policy).toContain('"effect":"write"');
expect(policy).toContain('"allow":["write"]');
// And the change is in the run's own Workspace, where an ordinary read
// beneath the fragment's own directory finds it. Anchored on the
diff --git a/packages/cli/tests/workflow-suspension.test.ts b/packages/cli/tests/workflow-suspension.test.ts
index 76b596201..ac610310e 100644
--- a/packages/cli/tests/workflow-suspension.test.ts
+++ b/packages/cli/tests/workflow-suspension.test.ts
@@ -821,6 +821,15 @@ const PROMPT_EFFECT = "agent_prompt"; // packages/core/src/agent/journal.ts
const EVALUATION_EFFECT = "generated_xmd"; // packages/core/src/generated-xmd.ts
const FILE_EFFECT = WORKSPACE_FILE;
+/**
+ * What core's own declaration offers the paired `` as.
+ *
+ * Spelled here rather than imported, because a case about what a retained
+ * admission holds has to name the string the journal holds: reading it from the
+ * component the resume also reads would agree with itself after a rename.
+ */
+const DIR_ENSURE = "https://executable.md/components/dir/ensure";
+
/** Every prompt this run's agent was actually asked. */
interface AgentCalls {
readonly prompts: string[];
@@ -1508,10 +1517,10 @@ the retained note
});
}
- // WGAC17: `allow={["write"]}` admits the versioned paired ``, and that
- // entry now authorizes persistent recursive creation. The former identity
- // authorized placement that created nothing, so a run admitted under it must
- // not silently receive the wider permission.
+ // WGAC17: `allow={["write"]}` admits the paired ``, which authorizes
+ // persistent recursive creation. A run admitted under a different identity
+ // for that spelling was granted something else, so it must not silently
+ // receive this permission on resume.
//
// Two halves, and neither is evidence alone. The control shows the current
// admission really does execute its generated `` and really does resume
@@ -1541,12 +1550,14 @@ the retained note
const suspended = retained(path);
expect(suspended.status).toBe("suspended");
- // The admission carries the versioned identity. It is a structural record
- // rather than a spelling, and the version is the `revision` member — so
- // that is what this reads, and what the refusal below moves.
+ // The admission carries the identity core's own declaration offers the
+ // paired spelling as, in the write table that admitted it — so that is what
+ // this reads, and what the refusal below moves.
expect(
readRecords(path).some(
- (record) => record.includes(`"key":"Dir"`) && record.includes(`"revision":"3"`),
+ (record) =>
+ record.includes(`"name":"Dir","form":"paired","identity":"${DIR_ENSURE}"`) &&
+ record.includes(`"effect":"write"`),
),
).toBe(true);
@@ -1616,19 +1627,18 @@ the retained note
expect(counts(path)).toEqual(atSuspension);
});
- it("WGAC17: a continuation retaining the former Dir identity refuses before generated execution and mutation", function* () {
+ it("WGAC17: a continuation retaining another Dir identity refuses before generated execution and mutation", function* () {
const store = yield* useRunStore();
const { runId, path, rendered, calls } = yield* wgac17(store);
- // The run becomes one admitted under the former identity. Nothing else
- // changes — same source, roots and selection — so the identity is the only
- // thing the resume can disagree about.
- //
- // The version is the `revision` member, and core's own entries state `2`,
- // so `"revision":"3"` names the Dir entry and nothing else. Moving it back
- // is exactly "admitted under the earlier revision", which is the grant the
- // former placement-only Dir had.
- rewriteRecords(path, (record) => record.replaceAll(`"revision":"3"`, `"revision":"2"`));
+ // The run becomes one admitted under a different identity for the same
+ // spelling of the same name. Nothing else changes — same source, roots and
+ // selection — so the identity is the only thing the resume can disagree
+ // about, and it is still a URL this engine can read: the refusal is the
+ // grant comparison rather than an unreadable record.
+ rewriteRecords(path, (record) =>
+ record.replaceAll(DIR_ENSURE, "https://executable.md/components/dir/placement"),
+ );
const beforeResume = counts(path);
const rootsBeforeResume = workspaceRootState(path);
@@ -1646,11 +1656,10 @@ the retained note
expect(workspaceRootState(path)).toEqual(rootsBeforeResume);
// Nothing after the wait reached the document either.
expect(rendered).toEqual([]);
- // And the refusal does not publish which identity moved. The identity is
- // four members now, so this reads the two a refusal could carry: the origin
- // that owns the implementation, and the member the rewrite actually moved.
+ // And the refusal does not publish which identity moved: neither the one
+ // the admission holds nor the one the rewrite put there.
const reported = resumed.written.err.join("\n");
- expect(reported).not.toContain("@executablemd/workflow/composition");
- expect(reported).not.toContain("revision");
+ expect(reported).not.toContain(DIR_ENSURE);
+ expect(reported).not.toContain("/components/dir/placement");
});
});
diff --git a/packages/core/host.ts b/packages/core/host.ts
index 422701678..df68282b6 100644
--- a/packages/core/host.ts
+++ b/packages/core/host.ts
@@ -97,7 +97,6 @@ export type {
FragmentEntry,
FragmentEvaluationInput,
FragmentForm,
- FragmentIdentity,
FragmentWorkspaceAccess,
} from "./src/evaluation-profile.ts";
/**
@@ -329,11 +328,11 @@ export {
export type {
GeneratedComponentForm,
GeneratedEffectClass,
+ GeneratedImplementation,
GeneratedMutation,
GeneratedObservation,
GeneratedRequest,
GeneratedXmdRequest,
- RetainedFragmentIdentity,
} from "./src/generated-xmd.ts";
/**
diff --git a/packages/core/mod.ts b/packages/core/mod.ts
index 8e898209f..17778873f 100644
--- a/packages/core/mod.ts
+++ b/packages/core/mod.ts
@@ -286,6 +286,13 @@ export {
registerComponents,
} from "./src/components/registration.ts";
export type { ComponentRegistration } from "./src/components/registration.ts";
+// What a registration says about the generated use of its own forms. The two
+// members a declaration states; nothing here reads or publishes a catalog of them.
+export type {
+ GeneratedEffect,
+ GeneratedForm,
+ GeneratedForms,
+} from "./src/components/generated-metadata.ts";
export { DEFAULT_INCLUDES, selectComponent } from "./src/components/select.ts";
export type { SelectOptions } from "./src/components/select.ts";
export { CORE_COMPONENT_NAMES } from "./src/components/registry.ts";
diff --git a/packages/core/src/component-answers.ts b/packages/core/src/component-answers.ts
index 2ee95e831..b1a7de870 100644
--- a/packages/core/src/component-answers.ts
+++ b/packages/core/src/component-answers.ts
@@ -3,37 +3,44 @@
*
* A `component-answer` entry names something the ordinary import chain
* resolves rather than an operation core supplies a body for. The host does
- * not hand over the implementation — it states which name, and which
- * structural identity a provider must have stated for that name. This module
- * is the seam a provider uses to state that identity.
+ * not hand over the implementation — it states which name, and which spellings
+ * of it the entry admits. This module is the seam a provider publishes the
+ * definition through, and what each of that definition's spellings runs as is
+ * declared on the definition itself
+ * (`components/generated-metadata.ts`).
*
* ## Being asked and answering are two separate rights
*
* A provider installs through a *registrar*, and receives no handle it can keep
- * and state an answer with. The registrar is the right to be asked; each
+ * and publish an answer with. The registrar is the right to be asked; each
* handler invocation then receives its own {@link ComponentAnswerRequest},
* which is the right to answer *that* asking and nothing else.
*
* An installation is reusable, but cannot identify which invocation is
* speaking. Each request captures its resolution by object identity and closes
* when its handler returns, throws or is cancelled, so a finished or losing
- * handler cannot state an answer to an import it is no longer deciding.
+ * handler cannot publish an answer to an import it is no longer deciding.
*
- * So `claim` takes no name. The name is fixed when the request is minted, from
- * what the chain asked, and a handler cannot state an answer for a component it
- * was never asked about.
+ * So `answer` takes the definition and nothing else. The name is fixed when the
+ * request is minted, from what the chain asked, so a handler cannot answer for
+ * a component it was never asked about; and the identity and effect of each
+ * spelling are the definition's own, so a provider cannot describe an
+ * implementation it did not supply. The installation origin it carries is
+ * provenance a reader and a documentation symbol are shown, and no part of what
+ * the implementation is.
*
* All of it is ordinary closures. There is no shared symbol, module registry or
* context name behind any of these objects, which is what lets a separately
* loaded copy of core hold one without any of those being a way in.
*
- * ## Identifying is not permission
+ * ## Publishing is not permission
*
- * An identity statement permits nothing. A fragment still needs the host entry,
- * an exact identity match, the right `allow` class, whole-fragment preflight
- * and canonical generated import — all of them independently. Middleware that
- * answers an import without stating an identity is an ordinary replacement
- * and stays exactly as valid as it was; it cannot enter an admitted fragment.
+ * A publication permits nothing. A fragment still needs the host entry, the
+ * spelling the definition's own declaration offers, the right `allow` class,
+ * whole-fragment preflight and canonical generated import — all of them
+ * independently. Middleware that answers an import without publishing it is an
+ * ordinary replacement and stays exactly as valid as it was; it cannot enter an
+ * admitted fragment.
*/
import type { Operation } from "effection";
@@ -85,8 +92,8 @@ export interface ComponentAnswerRegistrar {
/**
* One provider's installation, run during capture and before ordinary installs.
*
- * `origin` is the provider's name for itself and becomes the origin on every
- * identity its requests state. `install` registers whatever import middleware
+ * `origin` is the provider's name for itself, and the provenance every answer
+ * its requests publish carries. `install` registers whatever import middleware
* answers for its names.
*/
export interface ComponentAnswerInstallation {
diff --git a/packages/core/src/components/Evaluate.ts b/packages/core/src/components/Evaluate.ts
index d5b33e00c..bab37f9b9 100644
--- a/packages/core/src/components/Evaluate.ts
+++ b/packages/core/src/components/Evaluate.ts
@@ -79,10 +79,10 @@ import type { CapturedEntry, EvaluationProfile } from "../evaluation-profile.ts"
import { evaluateProtectedGeneratedXmd } from "../generated-xmd.ts";
import type {
GeneratedEffectClass,
+ GeneratedImplementation,
GeneratedMutation,
GeneratedObservation,
GeneratedXmdRequest,
- RetainedFragmentIdentity,
} from "../generated-xmd.ts";
import { ComponentInvocationError, invocationForm } from "../invocation-identity.ts";
import type {
@@ -322,13 +322,13 @@ interface SelectedTables {
}
/**
- * The host's captured tables, as the pinned identities the evaluator admits.
+ * The host's captured tables, as the pinned entries the evaluator admits.
*
* A class the host installed nothing for is refused here — before the durable
* name is assigned and before a program exists — rather than reaching the
- * evaluator as an empty table. The identity a run retains is built from the
- * three structural parts the host stated, so a reader comparing two admissions
- * can say which part moved.
+ * evaluator as an empty table. The identity a run retains for each admitted
+ * spelling comes from the definition that spelling runs, where the declaration
+ * that owns the implementation states it.
*
* The definition carries the *captured* schema rather than its own, so a host
* that edits the schema on the definition it handed over after installation
@@ -352,18 +352,14 @@ function selectedTables(
for (const entry of profile.read) {
observations.push({
name: entry.name,
- identity: pinned(entry),
definition: entry.definition,
+ kind: implementation(entry),
...(entry.dispatch === undefined ? {} : { dispatch: entry.dispatch }),
// An entry admitted for one spelling admits that one; an entry admitted
// for both is the form-insensitive component it has always been.
...(entry.forms.length === 1 && entry.forms[0] === "self-closing"
? { selfClosing: true }
: {}),
- // The version-1 strings this entry stated it succeeds, carried through
- // so a continuation admitted under one of them reconciles against this
- // entry. Nothing this run writes holds them.
- ...(entry.legacy === undefined ? {} : { legacy: entry.legacy }),
...(entry.requests === undefined ? {} : { requests: entry.requests.map(asRequest) }),
});
admitted.push(entry);
@@ -376,10 +372,9 @@ function selectedTables(
for (const entry of profile.write) {
mutations.push({
name: entry.name,
- identity: pinned(entry),
definition: entry.definition,
+ kind: implementation(entry),
...(entry.dispatch === undefined ? {} : { dispatch: entry.dispatch }),
- ...(entry.legacy === undefined ? {} : { legacy: entry.legacy }),
form: entry.forms.length === 2 ? "either" : (entry.forms[0] ?? "self-closing"),
});
admitted.push(entry);
@@ -389,20 +384,16 @@ function selectedTables(
}
/**
- * The identity a run retains for one captured entry.
+ * What kind of thing is behind one captured entry's name.
*
- * Structural rather than assembled: the four terms travel as themselves, so a
- * continuation compares them one at a time and a reader looking at two
- * admissions can say which of them moved. Nothing here is derived from the
- * implementation — an implementation is not an identity — and the kind travels
- * with the rest because an operation core supplies the body for and an answer
- * the import chain resolved are different grants under the same three names.
+ * It travels with the entry because an operation core supplies the body for and
+ * an answer the import chain resolved are different grants even under one
+ * identity: the first cannot be composed around and the second was resolved
+ * through middleware. The identity itself is not restated here — it is on the
+ * definition the entry already carries, where its owner declared it.
*/
-function pinned(entry: CapturedEntry): RetainedFragmentIdentity {
- const { origin, key, revision } = entry.identity;
- return entry.kind === "component-answer"
- ? { kind: "component-answer", origin, key, revision }
- : { kind: "capability", origin, key, revision };
+function implementation(entry: CapturedEntry): GeneratedImplementation {
+ return entry.kind === "component-answer" ? "component-answer" : "capability";
}
/** One captured ceiling, as the request record the evaluator compares against. */
diff --git a/packages/core/src/components/Syntax.ts b/packages/core/src/components/Syntax.ts
index 6b4e47c07..2d4f1e0f9 100644
--- a/packages/core/src/components/Syntax.ts
+++ b/packages/core/src/components/Syntax.ts
@@ -118,6 +118,13 @@ export const SYNTAX_PROTECTED: ExecutionBuiltComponent = {
origin: CORE_ORIGIN,
props,
forms: ["self-closing"],
+ // Offered to a generated fragment as a read, in the one spelling it has. The
+ // metadata travels to the definition each execution builds from this
+ // declaration, so an admitted ` ` is compared against the identity
+ // its owner states rather than one a host wrote beside the entry.
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/syntax/read", effect: "read" },
+ },
...documented({
description:
"Inspect available components and control-flow constructs. ` ` lists the " +
diff --git a/packages/core/src/components/component-resolution.ts b/packages/core/src/components/component-resolution.ts
index 7a439f84e..b067d6377 100644
--- a/packages/core/src/components/component-resolution.ts
+++ b/packages/core/src/components/component-resolution.ts
@@ -18,6 +18,7 @@
* nothing a handler still holds decides what is *invoked*.
*/
+import { definitionGeneratedRefusal } from "./generated-metadata.ts";
import type { ComponentDefinition, FunctionComponentDefinition, SourcePosition } from "../types.ts";
/** A definition an import may answer with. */
@@ -168,44 +169,31 @@ function read(inspect: () => T): T | undefined {
* resembles one.
*/
/**
- * A provider's stable statement about the implementation it supplied.
- *
- * `origin` is canonical execution's to fix, not the provider's: each provider
- * installation carries its origin into every request it opens, so one provider
- * cannot state an identity under another's name.
- */
-export interface AnswerIdentity {
- readonly origin: string;
- readonly key: string;
- readonly revision: string;
-}
-
-/** What a provider states, minus the part it does not get to choose. */
-export interface ClaimedIdentity {
- readonly key: string;
- readonly revision: string;
-}
-
-/**
- * One identified answer: what a provider stated, and what core kept of it.
+ * One published answer: core's own copy, and which provider installation
+ * published it.
*
* The two travel together because a caller needs both and must not obtain them
- * separately. The definition is core's own copy, taken when the identity was
- * recorded; it is what the caller keeps, and it is why nothing downstream ever
- * reads the object the public chain returned a second time.
+ * separately. The definition is core's copy, taken when it was published, and
+ * the origin is the publishing installation's — provenance a reader and a
+ * documentation symbol are shown, never a statement about what the
+ * implementation is.
*/
export interface IdentifiedAnswer {
- readonly identity: AnswerIdentity;
+ readonly origin: string;
readonly definition: ImportedDefinition;
}
/**
- * What one answer object was identified as, and which resolutions identified
+ * What one published answer was published as, and which resolutions published
* 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.
+ * One implementation says what it is once, so the name and the installation
+ * belong to the object: a new resolution cannot take it for another provider.
+ *
+ * What the implementation *is* is no longer stated beside it. The identity and
+ * the effect of each generated form are on the definition, declared by whoever
+ * owns the implementation, so a provider publishes the definition and states
+ * nothing a reader would have to reconcile with it.
*
* 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
@@ -216,38 +204,44 @@ export interface IdentifiedAnswer {
* cannot be forged into being the one the caller opened.
*
* A reusable provider returns one immutable definition to every import it
- * answers. Keying the identity record 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.
+ * answers, so the record outside the window keys the object alone and holds
+ * what every resolution of it has in common. Keying the whole record by the
+ * object would make the first resolution part of that object's permanent
+ * identity, and refuse the same definition the second time.
*/
-interface Claim {
+interface Answer {
readonly name: string;
- readonly identity: AnswerIdentity;
- /** Which provider installation stated it, so a second cannot overwrite. */
+ /**
+ * The installation origin that published it.
+ *
+ * Provenance: it says which provider installation spoke, for a reader and a
+ * documentation symbol. It is no part of what the implementation is — that is
+ * the definition's own metadata.
+ */
+ readonly origin: string;
+ /** Which provider installation published it, so a second cannot overwrite. */
readonly installation: object;
/**
- * Core's copy of the definition the first identity statement described.
+ * Core's copy of the definition the first publication supplied.
*
- * The identity above is a statement about *this* definition, so a later
- * identity statement of the same object has to be a statement about the same
- * definition. Without this baseline, a provider could identify 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.
+ * A publication is a statement about *this* definition, so a later
+ * publication of the same object has to supply the same definition. Without
+ * this baseline, a provider could publish an object, edit it, and have the
+ * next window retain the edited version — a different implementation, and
+ * different generated metadata, under the first one's publication.
*
- * Absent when the definition in the first identity statement could not be
- * copied at all, which is not a state a later identity statement can
- * improve: an answer core could not retain is not one it can start vouching
- * for because the object has since changed.
+ * Absent when the definition in the first publication could not be copied at
+ * all, which is not a state a later publication can improve: an answer core
+ * could not retain is not one it can start vouching for because the object
+ * has since changed.
*/
readonly baseline: ImportedDefinition | undefined;
/**
- * One retained copy per resolution this answer was identified in.
+ * One retained copy per resolution this answer was published in.
*
* Window keys are weak: retaining an answer does not keep its past
* resolutions alive. The outer record is weakly keyed by the answer, too.
- * Presence is the provenance check: a window that never identified this
+ * Presence is the provenance check: a window that never published this
* answer has no entry, and `identify` answers nothing for it however many
* other windows did. Neither weak association promises collection timing.
*/
@@ -256,19 +250,19 @@ interface Claim {
/** A provider request used after its execution ended. */
export const REVOKED_ANSWER_IDENTIFICATION =
- "the execution that installed this answer provider has ended, so nothing it states identifies " +
- "an implementation here";
+ "the execution that installed this answer provider has ended, so nothing it publishes " +
+ "identifies an implementation here";
-/** An identity stated outside the handler invocation it would have been an answer to. */
-export const SETTLED_CLAIM =
- "this resolution has settled, so a claim stated now identifies nothing. An answer is " +
- "identified by the handler invocation the import asked, while that invocation is still " +
+/** A definition published outside the handler invocation it would have answered. */
+export const SETTLED_ANSWER =
+ "this resolution has settled, so a definition published now identifies nothing. An answer is " +
+ "published by the handler invocation the import asked, while that invocation is still " +
"deciding; a handler that has returned, or one recording after the fact, is not supplying it.";
/** A second, different statement from one installation about one resolution. */
export const SPENT_OPPORTUNITY =
- "this provider already stated which implementation answers this import, and one import is one " +
- "implementation. The next resolution offers a fresh request; this one is decided.";
+ "this provider already published which implementation answers this import, and one import is " +
+ "one implementation. The next resolution offers a fresh request; this one is decided.";
/**
* One open resolution, and the only way to end it.
@@ -284,32 +278,41 @@ export interface ResolutionWindow {
readonly name: string;
/** Which resolution this is, for a reader following two of them. */
readonly occurrence: number;
- /** Stop accepting identity statements for this resolution. Idempotent. */
+ /** Stop accepting publications for this resolution. Idempotent. */
close(): void;
}
/**
- * What one handler invocation may state about the import it was asked.
+ * What one handler invocation may publish for the import it was asked.
*
* Minted per invocation, and closed the moment that invocation returns, throws
- * or is cancelled. This is what a provider states an identity through: the
+ * or is cancelled. This is what a provider publishes a definition through: the
* installation gives a provider the right to *be asked*, and the request gives
* it the right to answer this one asking. Separating them is what makes a
- * statement provable — a stable installation handle can only say "some
+ * publication provable — a stable installation handle can only say "some
* provider", while a request says "this handler, deciding this import, right
* now".
*
- * There is no `name` parameter on `claim`. The name is fixed when the request
- * is minted, from what the chain asked, so a handler cannot state an answer for
- * a component it was not asked about.
+ * `answer` takes the definition and nothing else. The name is fixed when the
+ * request is minted, from what the chain asked, so a handler cannot answer for
+ * a component it was not asked about; and which identity and effect each
+ * generated form runs as is the definition's own, declared by whoever owns the
+ * implementation. A provider that had to state those separately could describe
+ * an implementation it did not supply.
*/
export interface ComponentAnswerRequest {
/** The component this invocation was asked to resolve. */
readonly name: string;
/** Where the element that asked was written, when the chain knew. */
readonly position?: Readonly;
- /** State which implementation answers this import, as this provider. */
- claim(answer: ImportedDefinition, identity: ClaimedIdentity): ImportedDefinition;
+ /**
+ * Publish the definition that answers this import, as this provider.
+ *
+ * Synchronous, and it performs nothing: no component body runs, no I/O
+ * happens, and the definition's own generated metadata is read and validated
+ * off core's copy of it rather than off the object the chain still holds.
+ */
+ answer(definition: ImportedDefinition): ImportedDefinition;
}
/** One minted request, and the caller's own handle for ending it. */
@@ -330,7 +333,7 @@ export interface OpenAnswerRequest {
* One provider installation's private right to be asked.
*
* Holding this is the right to be asked, fixed to one origin. It is not the
- * right to answer: every statement goes through a request this mints for one
+ * right to answer: every publication goes through a request this mints for one
* invocation, so a retained installation handle can open a *new* request but
* cannot revive a settled one or reach another provider's.
*/
@@ -346,27 +349,22 @@ export interface ProviderInstallation {
open(name: string, position?: Readonly): OpenAnswerRequest;
}
-/** An identity a provider stated in a shape this execution cannot record. */
+/** A definition a provider published where this execution cannot record one. */
export class AnswerIdentityError extends Error {
override name = "AnswerIdentityError";
}
-/** The retained spelling of one identity, for a reader. */
-export function identityRecord(identity: AnswerIdentity): string {
- return `${identity.origin}#${identity.key}@${identity.revision}`;
-}
-
export class CanonicalImports {
readonly #issued = new WeakMap();
/**
- * The identities providers stated, in the same owner that holds issuance.
+ * The definitions providers published, in the same owner that holds issuance.
*
* One owner rather than two registries: retention, exact-object lookup, the
* `stillDescribes` comparison and the lifecycle are one question asked about
* one table, and a parallel WeakMap would be a second place for an answer to
* be verified from.
*/
- readonly #claims = new WeakMap();
+ readonly #answers = new WeakMap();
/**
* Whether this owner identifies anything yet.
*
@@ -502,13 +500,12 @@ export class CanonicalImports {
// would let a provider edit what a *later* reader of that element
// sees — a diagnostic seam turned into a write.
...(position === undefined ? {} : { position: capturePosition(position) }),
- claim(answer: ImportedDefinition, stated: ClaimedIdentity): ImportedDefinition {
+ answer(definition: ImportedDefinition): ImportedDefinition {
return owner.#record(
installation,
origin,
{ name, window: opened, live: () => live },
- answer,
- stated,
+ definition,
);
},
});
@@ -531,38 +528,32 @@ export class CanonicalImports {
live: () => boolean;
},
answer: ImportedDefinition,
- stated: ClaimedIdentity,
): ImportedDefinition {
if (!this.#active) {
throw new AnswerIdentityError(REVOKED_ANSWER_IDENTIFICATION);
}
- // Four questions, and each of them is about the invocation rather than
+ // Three questions, and each of them is about the invocation rather than
// about the provider: is this handler still deciding; is the resolution it
// was asked in the one still open, by identity; and is the name it was asked
// the name that resolution is deciding. A stable installation handle answers
- // none of them, which is why it does not claim.
+ // none of them, which is why it does not publish.
const open = asked.window;
if (!asked.live() || open === undefined || open !== this.#window || open.name !== asked.name) {
- throw new AnswerIdentityError(SETTLED_CLAIM);
+ throw new AnswerIdentityError(SETTLED_ANSWER);
}
- const identity = complete(origin, stated);
const held =
- typeof answer === "object" && answer !== null ? this.#claims.get(answer) : undefined;
+ typeof answer === "object" && answer !== null ? this.#answers.get(answer) : undefined;
if (held !== undefined) {
- // The same installation may restate this object's identity for another
- // import or repeat it within one resolution. A different installation
- // cannot restate it, even identically: the first provider's statement
- // stands, so a second cannot rename the first's implementation.
- if (
- held.installation !== installation ||
- held.name !== asked.name ||
- held.identity.origin !== identity.origin ||
- held.identity.key !== identity.key ||
- held.identity.revision !== identity.revision
- ) {
+ // What this object answers for was settled by its first publication.
+ // Publishing it again is what a provider installed twice does, and what a
+ // reusable one does for every import it answers; anything else is two
+ // providers disagreeing about one object, and the first publication
+ // stands — a later one that overwrote it would let a second provider take
+ // the first's implementation.
+ if (held.installation !== installation || held.name !== asked.name) {
throw new AnswerIdentityError(
- "this answer already carries an identity, and a second claim does not replace it. One " +
- "implementation states what it is once.",
+ `this answer was already published by "${held.origin}" for "${held.name}", and a ` +
+ "second publication does not replace it. One implementation is published once.",
);
}
if (held.copies.has(open)) {
@@ -582,9 +573,9 @@ export class CanonicalImports {
!stillDescribes(held.baseline, copy)
) {
throw new AnswerIdentityError(
- "this answer no longer describes the implementation its identity was stated about, so a " +
- "further import cannot be answered with it. One implementation states what it is " +
- "once, and stays that.",
+ "this answer no longer describes the implementation that was published, so a further " +
+ "import cannot be answered with it. One implementation is published once — its " +
+ "function, its schema and the generated metadata on it — and stays that.",
);
}
// The same definition answering a different import. That is one provider
@@ -597,20 +588,33 @@ export class CanonicalImports {
this.#spend(installation, open);
return answer;
}
- // Secondary, and about the provider rather than the invocation: naming a
+ // Secondary, and about the provider rather than the invocation: publishing a
// *different* implementation for an import this provider already answered
// is two answers where only one could be what it resolved to.
if (this.#spent.get(installation)?.has(open) === true) {
throw new AnswerIdentityError(SPENT_OPPORTUNITY);
}
- // Copied on the way in, so a later edit of the claimed object is visible as
- // the change it is.
+ // Copied on the way in, so a later edit of the published object is visible
+ // as the change it is.
const baseline = retain(answer);
+ // Read off core's own copy rather than off the object the chain still
+ // holds: a definition whose metadata answers one way for a check and
+ // another way for the read would otherwise be validated as one thing and
+ // retained as another. An answer that could not be copied at all is left
+ // for the comparison below to fail closed on, because there is nothing
+ // here to validate.
+ const refusal = baseline === undefined ? undefined : definitionGeneratedRefusal(baseline);
+ if (refusal !== undefined) {
+ throw new AnswerIdentityError(
+ `this answer ${refusal}. A generated form is offered under an absolute URL and one ` +
+ "effect, by the declaration that owns the implementation.",
+ );
+ }
const copies = new WeakMap();
copies.set(open, baseline);
- this.#claims.set(answer, {
+ this.#answers.set(answer, {
name: asked.name,
- identity,
+ origin,
installation,
baseline,
copies,
@@ -637,48 +641,51 @@ export class CanonicalImports {
}
/**
- * The identity stated for this exact answer, and core's own copy of it.
+ * Core's own copy of the definition a provider published for this exact
+ * answer and the installation that published it, or nothing when none did.
*
* Read after the whole public chain has returned, so what is asked about is
* the final answer rather than an intermediate one. Nothing here refuses: an
- * unidentified answer is an ordinary answer, and whether that is enough is
- * the caller's question.
+ * answer nobody published is an ordinary answer, and whether that is enough
+ * is the caller's question.
*
- * Both halves come back together, and the copy is the one taken when the
- * identity was recorded rather than one made now. Answering with the
- * identity alone would leave the caller holding the chain's object and
- * needing to copy it itself — one more read of a value the chain controls,
- * after the read this one checked. An alternating proxy or an accessor that
- * answers twice would pass the check and hand the second answer to the copy.
- * So the check and the thing kept are one result of one call, and the object
- * that travelled through the chain is never read again.
+ * The copy is the one taken when the definition was published rather than one
+ * made now, and it is what comes back. Answering with anything less would
+ * leave the caller holding the chain's object and needing to copy it itself —
+ * one more read of a value the chain controls, after the read this one
+ * checked. An alternating proxy or an accessor that answers twice would pass
+ * the check and hand the second answer to the copy. So the check and the
+ * thing kept are one result of one call, and the object that travelled
+ * through the chain is never read again. Which identity and effect each of
+ * its forms is offered under is on the copy, because it is the definition's
+ * own.
*/
identify(resolution: ResolutionWindow, answer: unknown): IdentifiedAnswer | undefined {
if (!this.#active || typeof answer !== "object" || answer === null) {
return undefined;
}
- const claim = this.#claims.get(answer);
- if (claim === undefined) {
+ const published = this.#answers.get(answer);
+ if (published === undefined) {
return undefined;
}
// The caller presents the resolution it opened, and this answer has to have
- // been claimed in *that* one, under the name it decides. Nothing here
+ // been published in *that* one, under the name it decides. Nothing here
// consults the owner's current window: provenance read out of mutable state
- // would be a claim about whenever the question was asked rather than about
- // which import the statement answered. A window that never claimed this
- // answer holds no copy of it, whatever its siblings did.
- if (claim.name !== resolution.name || !claim.copies.has(resolution)) {
+ // would describe whenever the question was asked rather than which import
+ // the publication answered. A window that never published this answer holds
+ // no copy of it, whatever its siblings did.
+ if (published.name !== resolution.name || !published.copies.has(resolution)) {
return undefined;
}
- // This window's own copy, taken when it claimed. A claimed object the chain
- // went on to edit is not the thing that was claimed: reading it runs
- // whatever it is made of, and a value that refuses to be compared has
+ // This window's own copy, taken when it was published. A published object
+ // the chain went on to edit is not the thing that was published: reading it
+ // runs whatever it is made of, and a value that refuses to be compared has
// failed the comparison.
- const canonical = claim.copies.get(resolution);
+ const canonical = published.copies.get(resolution);
if (canonical === undefined || !stillDescribes(canonical, answer)) {
return undefined;
}
- return Object.freeze({ identity: claim.identity, definition: canonical });
+ return Object.freeze({ origin: published.origin, definition: canonical });
}
/**
@@ -759,32 +766,6 @@ function capturePosition(position: Readonly): Readonly ${admitted.error.message}`);
+ }
return Object.freeze({
kind: "function",
name,
props: freezeThrough(props),
- forms: Object.freeze(declaredForms(declaration)),
+ forms,
+ generated: admitted.value,
...documented(documentation),
fn: formDispatcher(declaration),
});
@@ -113,28 +125,56 @@ function freezeJson(value: object): void {
/** The definitions themselves, in the order inspection reads them. */
const DEFINITIONS: readonly FunctionComponentDefinition[] = Object.freeze([
- fixed("File", fileForm, parseJsonObject(fileProps), {
- description:
- 'Read or write a file. ` ` reads the content; ' +
- '`… ` writes the content. The path is relative to the ' +
- "working directory.",
- as: "Optional. Captures the file's text instead of emitting it, in the reading form.",
- context: "The text to write, in the writing form.",
- }),
- fixed("File.Delete", fileDeleteForm, parseJsonObject(fileDeleteProps), {
- description:
- 'Delete a file. ` ` removes it. It does not error if the ' +
- "file doesn't exist, and produces no output.",
- as: "Optional. Captures the empty string this renders, as for any text component.",
- context: null,
- }),
- fixed("Dir", dirForm, parseJsonObject(dirProps), {
- description:
- 'Create or use a directory. `… ` changes the working directory ' +
- "for its content.",
- as: null,
- context: "The Markdown expanded in that directory.",
- }),
+ fixed(
+ "File",
+ fileForm,
+ parseJsonObject(fileProps),
+ {
+ description:
+ 'Read or write a file. ` ` reads the content; ' +
+ '`… ` writes the content. The path is relative to the ' +
+ "working directory.",
+ as: "Optional. Captures the file's text instead of emitting it, in the reading form.",
+ context: "The text to write, in the writing form.",
+ },
+ {
+ // The two spellings do different things, so each one is offered under its
+ // own identity and effect: a table admitting the reading form admits no
+ // write, and a continuation compares the URL the spelling actually ran as.
+ "self-closing": { identity: "https://executable.md/components/file/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/file/write", effect: "write" },
+ },
+ ),
+ fixed(
+ "File.Delete",
+ fileDeleteForm,
+ parseJsonObject(fileDeleteProps),
+ {
+ description:
+ 'Delete a file. ` ` removes it. It does not error if the ' +
+ "file doesn't exist, and produces no output.",
+ as: "Optional. Captures the empty string this renders, as for any text component.",
+ context: null,
+ },
+ {
+ "self-closing": { identity: "https://executable.md/components/file/delete", effect: "write" },
+ },
+ ),
+ fixed(
+ "Dir",
+ dirForm,
+ parseJsonObject(dirProps),
+ {
+ description:
+ 'Create or use a directory. `… ` changes the working directory ' +
+ "for its content.",
+ as: null,
+ context: "The Markdown expanded in that directory.",
+ },
+ {
+ paired: { identity: "https://executable.md/components/dir/ensure", effect: "write" },
+ },
+ ),
]);
const BY_NAME: ReadonlyMap = new Map(
diff --git a/packages/core/src/components/generated-metadata.ts b/packages/core/src/components/generated-metadata.ts
new file mode 100644
index 000000000..7029dd88b
--- /dev/null
+++ b/packages/core/src/components/generated-metadata.ts
@@ -0,0 +1,224 @@
+/**
+ * What a declaration says about the generated use of its own forms (spec §5.3).
+ *
+ * A generated fragment names a component, and what that name is admitted as —
+ * which URL a continuation is compared against, and whether the form observes
+ * or mutates — belongs to the declaration that owns the implementation. So the
+ * metadata travels on the declaration, per authored form, and every table that
+ * admits a name reads it off the definition it already holds: a host decides
+ * which names and spellings a fragment may write, and never what writing one
+ * runs as.
+ *
+ * A component that offers no generated use carries none, which is the ordinary
+ * case: metadata is what makes a form offerable, and offering none takes
+ * nothing away from authored use.
+ *
+ * ## Read once, validated and retained as one value
+ *
+ * A declaration crosses a package boundary, so the metadata is read the way
+ * durable input is read: exactly the two members, an identity in the accepted
+ * HTTPS namespace, one of the two effects, and a spelling the declaration
+ * itself accepts. The identity is stored and compared as the supplied string —
+ * nothing fetches it, normalizes it into an alias, or infers it from the
+ * implementation.
+ *
+ * One pass does all of it, and what it answers with is the copy. Validating the
+ * declarer's object and then copying it again would be two readings of a value
+ * the declarer controls: an accessor answering a valid URL for the check and
+ * another for the copy would have installed what the check never saw. So
+ * `admitGeneratedForms()` reads each member exactly once, into data of its own,
+ * and the caller retains that same data.
+ *
+ * It imports no registry and no protected table. Validating plain metadata
+ * through the component graph would reopen the initialization cycle that graph
+ * already exposes, and nothing here needs to know which components exist.
+ */
+
+import { Err, Ok } from "effection";
+import type { Result } from "effection";
+
+import { acceptedForms, formsRefusal } from "../invocation-forms.ts";
+import type { InvocationForm } from "../invocation-identity.ts";
+
+/** Whether a generated form observes or mutates. */
+export type GeneratedEffect = "read" | "write";
+
+/** What one authored form is offered for generated use as. */
+export interface GeneratedForm {
+ /** The absolute URL a continuation is compared against. */
+ readonly identity: string;
+ readonly effect: GeneratedEffect;
+}
+
+/** The generated use a declaration offers, by authored form. */
+export type GeneratedForms = Readonly>>;
+
+/** The namespace every generated identity is written in. */
+export const GENERATED_IDENTITY_ORIGIN = "https://executable.md";
+
+const EFFECTS: readonly GeneratedEffect[] = ["read", "write"];
+const FORMS: readonly InvocationForm[] = ["self-closing", "paired"];
+
+/** The authored form this value names, or nothing when it names none. */
+function authoredForm(value: unknown): InvocationForm | undefined {
+ return FORMS.find((form) => form === value);
+}
+
+/** The effect this value names, or nothing when it names none. */
+function generatedEffect(value: unknown): GeneratedEffect | undefined {
+ return EFFECTS.find((effect) => effect === value);
+}
+
+/**
+ * This declaration's generated metadata, read once into owned frozen data — or
+ * the reason it is unusable.
+ *
+ * `accepted` is the forms the declaration itself accepts, `undefined` keeping
+ * its ordinary meaning of both, because offering generated use of a form the
+ * component refuses to be written in is offering nothing that can run.
+ *
+ * Frozen through, because a shared definition is read by every execution in the
+ * process: a nested record left mutable would be what the next execution took
+ * its own first copy of, with the change already in it.
+ */
+export function admitGeneratedForms(
+ generated: unknown,
+ accepted: readonly InvocationForm[] | undefined,
+): Result {
+ if (typeof generated !== "object" || generated === null || Array.isArray(generated)) {
+ return Err(new Error("states generated forms that are not a map of authored forms"));
+ }
+ const offerable = accepted === undefined ? FORMS : accepted;
+ const captured: Partial> = {};
+ for (const key of Reflect.ownKeys(generated)) {
+ const form = authoredForm(key);
+ if (form === undefined) {
+ return Err(
+ new Error(`states generated metadata for "${String(key)}", which is not an authored form`),
+ );
+ }
+ if (!offerable.includes(form)) {
+ return Err(new Error(`offers generated use of its ${form} form, which it does not accept`));
+ }
+ // One read of each member, into data this module owns. Everything below —
+ // the checks and the retained copy alike — reads that data and never the
+ // declarer's object again.
+ const offered = read(Reflect.get(generated, form), form);
+ if (!offered.ok) {
+ return offered;
+ }
+ captured[form] = offered.value;
+ }
+ return Ok(Object.freeze(captured));
+}
+
+/** One offered form, read once and checked. */
+function read(value: unknown, form: InvocationForm): Result {
+ const malformed = Err(
+ new Error(`states a ${form} generated form that is not an identity and an effect`),
+ );
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
+ return malformed;
+ }
+ const members = Reflect.ownKeys(value);
+ if (members.length !== 2 || !members.includes("identity") || !members.includes("effect")) {
+ return malformed;
+ }
+ const identity = Reflect.get(value, "identity");
+ const effect = generatedEffect(Reflect.get(value, "effect"));
+ if (effect === undefined) {
+ return Err(new Error(`offers its ${form} form under an effect this engine does not have`));
+ }
+ if (typeof identity !== "string" || !isGeneratedIdentity(identity)) {
+ return Err(
+ new Error(
+ `offers its ${form} form under "${String(identity)}", which is not an absolute URL in ` +
+ GENERATED_IDENTITY_ORIGIN,
+ ),
+ );
+ }
+ return Ok(Object.freeze({ identity, effect }));
+}
+
+/**
+ * Why this metadata is unusable, or nothing when it is usable.
+ *
+ * The sentence alone, for a caller that refuses rather than retains: a
+ * registration, a factory declaration and a resolved provider definition each
+ * raise their own kind of failure with their own diagnostic context, and a
+ * shared throw here would take that context away from all three.
+ */
+export function generatedRefusal(
+ generated: unknown,
+ accepted: readonly InvocationForm[] | undefined,
+): string | undefined {
+ if (generated === undefined) {
+ return undefined;
+ }
+ const admitted = admitGeneratedForms(generated, accepted);
+ return admitted.ok ? undefined : admitted.error.message;
+}
+
+/**
+ * Whether this string is an identity a generated form may be offered under.
+ *
+ * Parsed as a URL and compared by origin, never by prefix: a host reading the
+ * text instead of the origin would admit `https://executable.md.example.com/`
+ * and `https://executable.md@elsewhere.example/`, which start with the accepted
+ * string and name somebody else.
+ */
+export function isGeneratedIdentity(identity: string): boolean {
+ try {
+ return new URL(identity).origin === GENERATED_IDENTITY_ORIGIN;
+ } catch {
+ return false;
+ }
+}
+
+/**
+ * The identity and effect one authored form is offered under, when it is.
+ *
+ * The one read every admitting table performs, so a selected form that carries
+ * no metadata is one answer rather than each caller's own lookup.
+ */
+export function offeredForm(
+ generated: GeneratedForms | undefined,
+ form: InvocationForm,
+): GeneratedForm | undefined {
+ return generated?.[form];
+}
+
+/**
+ * Why a definition's own generated metadata is unusable, or nothing.
+ *
+ * For a definition whose data is already this engine's: a provider's published
+ * answer is core's own structured copy, and a captured profile entry holds
+ * either that copy or one of core's own owners. Reading such a value twice
+ * says the same thing twice, so a caller that only has to refuse can ask for
+ * the sentence alone.
+ *
+ * It is not enough on its own for a value a caller still controls. There,
+ * `admitGeneratedForms()` is what to use: the data it answers with is what the
+ * caller retains, so the reading that was validated is the reading that is
+ * admitted — which is why direct generated admission reads a pinned definition
+ * through it rather than through this (`generated-xmd.ts`).
+ *
+ * A definition offering generated use is held to its own stated forms, through
+ * the same accepted-form contract a registration is. That value is read rather
+ * than trusted too: only genuine omission means both forms, so a malformed
+ * `forms` is refused instead of being filtered into a declaration that looks
+ * canonical. A definition offering no generated use is left alone, because it
+ * is asking for nothing that identity or effect decides.
+ */
+export function definitionGeneratedRefusal(definition: object): string | undefined {
+ const generated = Reflect.get(definition, "generated");
+ if (generated === undefined) {
+ return undefined;
+ }
+ const stated = Reflect.get(definition, "forms");
+ const badForms = formsRefusal(stated);
+ if (badForms !== undefined) {
+ return badForms;
+ }
+ return generatedRefusal(generated, acceptedForms(stated));
+}
diff --git a/packages/core/src/components/registration.ts b/packages/core/src/components/registration.ts
index 9670f2cb6..481c22ef2 100644
--- a/packages/core/src/components/registration.ts
+++ b/packages/core/src/components/registration.ts
@@ -18,7 +18,9 @@ import { Component } from "../component-api.ts";
import { updateOwn } from "../scope-local.ts";
import { RESERVED_STRUCTURAL } from "../structural.ts";
import { PROTECTED_COMPONENT_NAMES, protectedNameRefusal } from "./protected.ts";
-import { formsRefusal } from "../invocation-identity.ts";
+import { formsRefusal } from "../invocation-forms.ts";
+import { admitGeneratedForms } from "./generated-metadata.ts";
+import type { GeneratedForms } from "./generated-metadata.ts";
import { documentationOf } from "./documentation.ts";
import type { ComponentDocumentation } from "./documentation.ts";
import { compilePropsSchema, compileReturnsSchema } from "../validate.ts";
@@ -81,6 +83,14 @@ export interface ComponentRegistration extends ComponentDocumentation {
* decision.
*/
forms?: readonly InvocationForm[];
+ /**
+ * The generated use each authored form is offered for.
+ *
+ * Stated here because this registration owns the implementation: which URL a
+ * continuation compares and whether a spelling observes or mutates are facts
+ * about the component rather than about the host table that admits it.
+ */
+ generated?: GeneratedForms;
fn: FunctionComponent | TestHarnessComponentDefinition;
}
@@ -228,6 +238,19 @@ function assertUsableCaptures(
}
}
+/** What this scope accepted of a declaration, as its own data. */
+export interface AdmittedDeclaration {
+ /**
+ * The generated metadata this declaration offers, read once and validated.
+ *
+ * The value a caller retains. Reading the declarer's object again to build
+ * the definition would be a second reading of a value the declarer controls,
+ * and an accessor answering one identity for the check and another for the
+ * copy would install what the check never saw.
+ */
+ readonly generated?: GeneratedForms;
+}
+
/**
* What a component declaration has to be for this scope to accept it, whoever
* is declaring.
@@ -243,8 +266,10 @@ function assertUsableCaptures(
* Every check is on the declaration alone. Nothing here calls an
* implementation, so it is usable where there is no implementation yet.
*/
-export function* admitDeclaration(declaration: ComponentDeclaration): Operation {
- const { name, origin, props, returns, captures, forms } = declaration;
+export function* admitDeclaration(
+ declaration: ComponentDeclaration,
+): Operation {
+ const { name, origin, props, returns, captures, forms, generated } = declaration;
assertUsableName(name);
if (origin.length === 0) {
throw new ComponentRegistrationError(
@@ -260,6 +285,14 @@ export function* admitDeclaration(declaration: ComponentDeclaration): Operation<
if (badForms !== undefined) {
throw new ComponentRegistrationError(`the declaration for "${name}" ${badForms}`);
}
+ if (generated === undefined) {
+ return {};
+ }
+ const admitted = admitGeneratedForms(generated, forms);
+ if (!admitted.ok) {
+ throw new ComponentRegistrationError(`the declaration for "${name}" ${admitted.error.message}`);
+ }
+ return { generated: admitted.value };
}
/** Everything a declaration says about itself, apart from what implements it. */
@@ -270,6 +303,7 @@ export interface ComponentDeclaration extends ComponentDocumentation {
readonly returns?: ReturnsSchema;
readonly captures?: readonly string[];
readonly forms?: readonly InvocationForm[];
+ readonly generated?: GeneratedForms;
}
export function* registerComponents(
@@ -284,7 +318,7 @@ export function* registerComponents(
for (const registration of registrations) {
const { name, origin, fn, props, returns, captures, forms } = registration;
- yield* admitDeclaration(registration);
+ const admitted = yield* admitDeclaration(registration);
const kind = kindOf(registration);
const already = additions.get(name)?.[kind];
@@ -300,6 +334,7 @@ export function* registerComponents(
...(returns ? { returns } : {}),
...(captures && captures.length > 0 ? { captures } : {}),
...(forms ? { forms } : {}),
+ ...(admitted.generated === undefined ? {} : { generated: admitted.generated }),
...documentationOf(registration),
};
batch.set(name, { ...batch.get(name), [kind]: { definition, origin } });
diff --git a/packages/core/src/components/registry.ts b/packages/core/src/components/registry.ts
index 82f3ef794..415440402 100644
--- a/packages/core/src/components/registry.ts
+++ b/packages/core/src/components/registry.ts
@@ -35,6 +35,8 @@ import Test, { props as testProps } from "./Test.ts";
import { parseJsonObject } from "../json.ts";
import { declaredForms, formDispatcher } from "../invocation-identity.ts";
import { documented } from "./documentation.ts";
+import { admitGeneratedForms } from "./generated-metadata.ts";
+import type { GeneratedForms } from "./generated-metadata.ts";
import type { FirstPartyDocumentation } from "./documentation.ts";
import type {
FormDeclaration,
@@ -57,6 +59,15 @@ export const CORE_ORIGIN = "@executablemd/core";
interface CoreOptions {
returns?: ReturnsSchema;
captures?: readonly string[];
+ /**
+ * The generated use each authored form is offered for.
+ *
+ * On the registration that owns the implementation, because that is what a
+ * generated table admits: ` `, ` ` and `` are offered,
+ * each in the one spelling it is written in, and every other core default
+ * offers none.
+ */
+ generated?: GeneratedForms;
/**
* The forms this component accepts, when it is form-sensitive without being
* a dispatcher.
@@ -92,8 +103,16 @@ function core(
): [string, RegistryEntry] {
const declaration = typeof implementation === "function" ? undefined : implementation;
const fn = typeof implementation === "function" ? implementation : formDispatcher(implementation);
- const { returns, captures } = options;
+ const { returns, captures, generated } = options;
const forms = declaration === undefined ? options.forms : declaredForms(declaration);
+ // Read through the same validator a host's registration is, where it is
+ // declared: a core default's offered spellings are part of what every
+ // execution resolves, so a malformed one refuses here rather than at the
+ // first table that admits the name.
+ const offered = generated === undefined ? undefined : admitGeneratedForms(generated, forms);
+ if (offered !== undefined && !offered.ok) {
+ throw new Error(`canonical core's <${name}> ${offered.error.message}`);
+ }
return [
name,
{
@@ -105,6 +124,7 @@ function core(
...(returns === undefined ? {} : { returns }),
...(captures === undefined ? {} : { captures }),
...(forms === undefined ? {} : { forms }),
+ ...(offered === undefined || !offered.ok ? {} : { generated: offered.value }),
...documented(documentation),
fn,
},
@@ -149,7 +169,12 @@ export const CORE_REGISTRY: ComponentRegistry = new Map([
as: "Required. The validated answer, as a structured value.",
context: "The request message the person is shown.",
},
- { returns: parseJsonObject(elicitReturns) },
+ {
+ returns: parseJsonObject(elicitReturns),
+ generated: {
+ paired: { identity: "https://executable.md/components/elicit/ask", effect: "write" },
+ },
+ },
),
core("TempDir", TempDir, parseJsonObject(tempDirProps), {
description:
@@ -167,16 +192,26 @@ export const CORE_REGISTRY: ComponentRegistry = new Map([
as: null,
context: null,
}),
- core("Fetch", Fetch, parseJsonObject(fetchProps), {
- description:
- 'Fetch a URL. ` ` binds the ' +
- "response, and any status is data you can branch on. Without `as`, a non-2xx status " +
- "fails. Only GET and HEAD methods currently supported.",
- as:
- "Optional. Captures the response, which is what makes a non-2xx status data instead " +
- "of a failure.",
- context: null,
- }),
+ core(
+ "Fetch",
+ Fetch,
+ parseJsonObject(fetchProps),
+ {
+ description:
+ 'Fetch a URL. ` ` binds the ' +
+ "response, and any status is data you can branch on. Without `as`, a non-2xx status " +
+ "fails. Only GET and HEAD methods currently supported.",
+ as:
+ "Optional. Captures the response, which is what makes a non-2xx status data instead " +
+ "of a failure.",
+ context: null,
+ },
+ {
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/fetch/read", effect: "read" },
+ },
+ },
+ ),
core(
"Glob",
Glob,
@@ -189,7 +224,12 @@ export const CORE_REGISTRY: ComponentRegistry = new Map([
as: "Required. The matched paths, as a string array.",
context: null,
},
- { returns: parseJsonObject(globReturns) },
+ {
+ returns: parseJsonObject(globReturns),
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/glob/read", effect: "read" },
+ },
+ },
),
core("CodeBlock", codeBlockForm, parseJsonObject(codeBlockProps), {
description:
diff --git a/packages/core/src/evaluation-profile.ts b/packages/core/src/evaluation-profile.ts
index 40499f5c7..8ccf60383 100644
--- a/packages/core/src/evaluation-profile.ts
+++ b/packages/core/src/evaluation-profile.ts
@@ -4,8 +4,8 @@
*
* `` is a public component: any author may write it, and canonical
* core owns what it means. That settles *which implementation runs* and nothing
- * else — protection grants no permission. Canonical `` uses only
- * operations already captured in this profile, selected by `allow`. So the
+ * else — protection grants no permission. Canonical `` admits only
+ * the entries already captured in this profile, selected by `allow`. So the
* ceiling lives here, in a value the host hands canonical execution at the
* installation boundary, before a document exists to ask for it.
*
@@ -14,10 +14,10 @@
* A host states {@link FragmentEvaluationInput}: entry tables, an optional
* Workspace snapshot operation, and whether it accepts the released `source`
* spelling. Canonical execution turns that into a captured profile of its own
- * — every value deep-copied and frozen, every live operation read and bound
- * once — and keeps the captured one. A host that mutates its own arrays,
- * headers or tables after installation changes nothing about the execution,
- * because the execution is no longer reading them.
+ * — every value deep-copied and frozen, every admitted implementation resolved
+ * once and held to this execution — and keeps the captured one. A host that
+ * mutates its own arrays, headers or tables after installation changes nothing
+ * about the execution, because the execution is no longer reading them.
*
* Only the input type is exported, and only from the trusted `core/host`
* surface, because that is where `ExecutionInstallation` already is. The
@@ -57,31 +57,15 @@ import { isFormDispatcher } from "./invocation-identity.ts";
import type { ComponentInvocation, ComponentRouting } from "./invocation-identity.ts";
import type { FetchRequest } from "./fetch-request.ts";
import { normalizeFetchRequest, requestRecord } from "./fetch-request.ts";
-import { CORE_REVISION, pinnedJson } from "./generated-xmd.ts";
+import { JSON_COMPOSITION_IDENTITY, pinnedJson } from "./generated-xmd.ts";
import type { GeneratedRequest } from "./generated-xmd.ts";
+import { definitionGeneratedRefusal, offeredForm } from "./components/generated-metadata.ts";
+import type { GeneratedEffect } from "./components/generated-metadata.ts";
import type { FunctionComponent, FunctionComponentDefinition, Json, PropsSchema } from "./types.ts";
/** The two forms an element is actually written in. */
export type FragmentForm = "self-closing" | "paired";
-/**
- * What a run retains about which implementation an entry runs.
- *
- * Three structural parts rather than one opaque string, because a continuation
- * compares them and a reader has to be able to say which one moved. Never
- * derived from a function: an implementation is not an identity, and
- * serializing or inspecting one would make the retained policy depend on how a
- * host happened to write its code.
- */
-export interface FragmentIdentity {
- /** The package or host that owns this implementation. */
- readonly origin: string;
- /** Which component of that origin, including the form when forms differ. */
- readonly key: string;
- /** Which version of it. A changed revision is a changed grant. */
- readonly revision: string;
-}
-
/**
* One component a generated fragment may name, as the host states it.
*
@@ -90,26 +74,26 @@ export interface FragmentIdentity {
* this implementation come from", which is exactly the question an admission
* has to settle once.
*
- * Neither arm carries a function. A capability names an operation canonical
- * core supplies the body for; a component answer names something the ordinary
- * import chain resolves, which canonical capture does once — before any
- * document code — and then holds by value. What is journaled is the structural
- * identity, the forms and the limits.
+ * Neither arm carries a function. A capability names a component canonical
+ * core supplies; a component answer names something the ordinary import chain
+ * resolves, which canonical capture does once — before any document code — and
+ * then holds by value. What is journaled is the admitted spellings, the
+ * identity each of them runs under, and the limits.
*/
export type FragmentEntry = CapabilityEntry | ComponentAnswerEntry;
-/** An entry core supplies the body for, bound to the host's own operations. */
+/** An entry canonical core supplies the component for. */
export interface CapabilityEntry {
readonly kind: "capability";
/** The name a fragment writes. */
readonly name: string;
- readonly identity: FragmentIdentity;
/**
* The exact spellings this entry is admitted for.
*
* A component whose two spellings do different things is two entries under
- * one name — ` ` reads and `… ` writes — so the form
- * travels with the identity and preflight chooses between them.
+ * one name — ` ` reads and `… ` writes — and each spelling
+ * carries its own identity and effect on the declaration that owns the
+ * implementation, so preflight chooses between them before the first effect.
*/
readonly forms: readonly FragmentForm[];
/** The contract this entry's props are validated against. */
@@ -124,36 +108,17 @@ export interface CapabilityEntry {
*/
readonly description?: string;
/**
- * Which captured operation this entry runs.
+ * Which of core's own components this entry runs.
*
- * Not a definition, because a definition is where a provider lookup would go.
- * A host names the capability and hands its private operations to the
- * profile; canonical capture reads each one off exactly once and closes
- * core's own body over the bound result. A host therefore chooses *what a
- * fragment may do* and never *how it reaches it*
- * (`fragment-capabilities.ts`).
+ * A name rather than a definition, because the definition is core's to
+ * supply: capture answers with the ordinary ``, ``,
+ * ``, ``, `` or `` an author writes, held to the
+ * execution that captured it. So a host chooses *what a fragment may do* and
+ * never *how it reaches it*: the operation crosses the contextual provider
+ * and the consuming scope's `Permissions` exactly as an authored element's
+ * does (`fragment-capabilities.ts`).
*/
readonly capability: FragmentCapability;
- /**
- * The exact version-1 identity strings this entry is the successor of.
- *
- * Version-1 records retained one opaque string, chosen by whoever built the
- * pinned entry: core wrote `@executablemd/core#File:read`, the workflow host
- * wrote `@executablemd/workflow/composition/dir-v2#Dir`, and a host passing
- * its own observation wrote whatever it liked. There is no rule that recovers
- * four structural terms from one of those, and inferring one would be this
- * module deciding on a host's behalf that two grants are the same.
- *
- * So it is stated rather than derived, by the party that owns the entry.
- * Listing a string here is an assertion that this entry authorizes no more
- * than the entry that string named — a continuation admitted under it resumes
- * against this one. An entry that lists none is a new grant, and every
- * version-1 record naming it refuses.
- *
- * Only a capability arm has this. The component-answer arm did not exist when
- * version-1 records were written, so no such string ever described one.
- */
- readonly legacy?: readonly string[];
/**
* The exact requests this entry may perform, for an entry that performs any.
*
@@ -167,33 +132,26 @@ export interface CapabilityEntry {
/**
* An entry whose implementation the ordinary import chain answers for.
*
- * The host states *what it expects* — the name, the structural identity a
- * provider must state, and the forms — and states no implementation at all.
- * There is no definition, function, resolver, provider operation or context
- * handle on this arm to hold one.
+ * The host states *what it expects* — the name and the forms — and states no
+ * implementation at all. There is no definition, function, resolver, provider
+ * operation, identity or context handle on this arm to hold one.
*
* Canonical capture resolves the name once, before the root import and before
* any document code, through the complete ordinary `Component.importComponent`
- * chain; reads the identity the provider stated on that exact final answer;
- * compares it with what this entry expects; retains the answer defensively;
- * and seals the result. Props and the callable definition are derived from
- * that answer rather than restated here, so a host cannot describe a contract
- * the implementation does not have.
+ * chain; takes the definition the provider published on that exact final
+ * answer; retains it defensively; and seals the result. Props, the callable
+ * definition and the identity each admitted form runs under are all derived
+ * from that answer rather than restated here, so a host cannot describe a
+ * contract or an effect the implementation does not have.
*
* Resolution happens at capture and never again. A fragment invokes the sealed
* snapshot, and a continuation resolves once more in its own capture and
* reconciles before any effect. Calling a retained resolver later would see
* document-time middleware, which is the whole thing this avoids.
- *
- * This arm states no version-1 alias, and has nowhere to put one: version-1
- * records predate it, so no string an older run committed ever described an
- * implementation the import chain answered for.
*/
export interface ComponentAnswerEntry {
readonly kind: "component-answer";
readonly name: string;
- /** What a provider must have stated for this name, exactly. */
- readonly identity: FragmentIdentity;
readonly forms: readonly FragmentForm[];
readonly description?: string;
}
@@ -227,9 +185,11 @@ export interface FragmentEvaluationInput {
/**
* The entries the `write` class resolves to, when this host offers the class.
*
- * For both standard profiles this is exactly paired ``, paired ``
- * and self-closing ``. It does not include the read table:
- * `write` selects mutation and nothing else.
+ * The two standard profiles differ here: the ordinary one is paired
+ * ``, self-closing `` and paired ``, while the
+ * workflow one is paired ``, paired `` and self-closing
+ * ``. Neither includes the read table: `write` selects mutation
+ * and nothing else.
*/
readonly write?: readonly FragmentEntry[];
/** How this host answers for its Workspace, when it evaluates against one. */
@@ -248,43 +208,46 @@ export interface FragmentEvaluationInput {
* Core's ` `, admitted to observe and not to write.
*
* `` reads when it has no content and writes when it has some, so one
- * unconstrained entry would admit a write. The form travels with the identity
- * and preflight decides between the two — before the first effect, rather than
- * inside a body that has already been entered.
+ * unconstrained entry would admit a write. Each spelling carries its own
+ * identity and effect on core's own declaration, and preflight decides between
+ * the two — before the first effect, rather than inside a body that has already
+ * been entered.
*
- * An admitted read reaches the `readTextFile` the host handed the profile and
- * nothing else. Under a workflow run that is the transaction-bound operation,
- * so the read still crosses the run's own transaction; what it no longer does
- * is resolve a provider through the contextual API at the moment it runs.
+ * An admitted read is core's ordinary ` ` performing its ordinary
+ * operation: it resolves the contextual working directory and reads through
+ * `API.Files`, under the provider the host installed and the effective
+ * `Permissions` of the consuming scope. Under a workflow run that provider is
+ * the transaction-bound one, so the read crosses the run's own transaction for
+ * the same reason an authored read does.
*/
export function fileReadEntry(): CapabilityEntry {
- return coreEntry("File", "File:read", "file:read");
+ return coreEntry("File", "file:read");
}
/**
* Core's ` `, admitted to select paths and nothing else.
*
- * A value entry: the definition core builds for it declares what it binds, so a
- * generated ` ` written without `as` is refused before the host's
- * provider is asked to traverse anything. What it selects is bounded the same
- * way a read is — the captured `globFiles` operation and the captured working
- * directory — so a search never reaches `API.Files` or `Env.cwd`, and the paths
- * it answers with are the ones that provider produced.
+ * A value entry: core's own declaration states what it binds, so a generated
+ * ` ` written without `as` is refused before any provider is asked to
+ * traverse anything. What it selects is bounded the same way a read is — the
+ * contextual working directory it resolves, and the `API.Files` provider the
+ * host installed — so the paths it answers with are the ones that provider
+ * produced for that directory.
*/
export function globReadEntry(): CapabilityEntry {
- return coreEntry("Glob", "Glob", "files:glob");
+ return coreEntry("Glob", "files:glob");
}
/**
* The protected names canonical execution states what is behind, for a profile.
*
- * An allowlist rather than "every protected name", because stating an identity
- * is what makes a name admissible: a host that wrote ``'s own
- * canonical identity into its read table would otherwise obtain evaluation
- * inside evaluation, which is a different grant from being shown a vocabulary.
- * Core answers for the one protected name it offers a host an entry for, and a
- * hand-written entry for any other resolves to an answer carrying no identity
- * and is refused.
+ * An allowlist rather than "every protected name", because core publishing an
+ * answer is what makes a protected name admissible: a host that wrote
+ * `` into its read table would otherwise obtain evaluation inside
+ * evaluation, which is a different grant from being shown a vocabulary. Core
+ * answers for the one protected name it offers a host an entry for, and a
+ * hand-written entry for any other resolves to an answer nobody published and
+ * is refused.
*/
export const CANONICAL_PROFILE_ANSWERS: ReadonlySet = new Set([SYNTAX_COMPONENT]);
@@ -293,10 +256,11 @@ export const CANONICAL_PROFILE_ANSWERS: ReadonlySet = new Set([SYNTAX_CO
*
* Not a capability: ` ` is canonical core's protected component, and a
* capability arm would be core supplying a second body for a name it already
- * owns. So the entry states what a resolution must have answered with — core's
- * own origin, key and revision — and canonical capture resolves the name once
- * through the ordinary import chain, reconciles that identity, and seals the
- * protected answer through the execution's own protected-body route.
+ * owns. So the entry states the name and the one form it has, and canonical
+ * capture resolves it once through the ordinary import chain — where core
+ * publishes its own answer for it — and seals that answer through the
+ * execution's own protected-body route. The identity the fragment is admitted
+ * under is the one core's declaration states for that form.
*
* Admitting it grants no other permission. The symbols an occurrence renders
* describe the vocabulary the fragment has; naming a component in them is not
@@ -307,7 +271,6 @@ export function syntaxReadEntry(): ComponentAnswerEntry {
return {
kind: "component-answer",
name: SYNTAX_COMPONENT,
- identity: { origin: CORE_ORIGIN, key: SYNTAX_COMPONENT, revision: CORE_REVISION },
forms: ["self-closing"],
description:
"Render the components this evaluation may write, or selected documentation. " +
@@ -343,24 +306,24 @@ export function syntaxReadEntry(): ComponentAnswerEntry {
* a request with nothing in it.
*/
export function elicitWriteEntry(): CapabilityEntry {
- return coreEntry(ELICIT_COMPONENT, ELICIT_COMPONENT, "elicit:ask");
+ return coreEntry(ELICIT_COMPONENT, "elicit:ask");
}
/** Core's `… `, admitted to write and not to read. */
export function fileWriteEntry(): CapabilityEntry {
- return coreEntry("File", "File:write", "file:write");
+ return coreEntry("File", "file:write");
}
/**
* Core's ` `, in the one form it has.
*
- * One name, one identity: unlike ``, whose two spellings do different
- * things, this answers the self-closing form alone. Stating the form is what
- * puts the decision in preflight, before the fragment's first effect — a paired
- * spelling costs an earlier admitted element nothing.
+ * One name, one form: unlike ``, whose two spellings do different things,
+ * this answers the self-closing form alone. Stating the form is what puts the
+ * decision in preflight, before the fragment's first effect — a paired spelling
+ * costs an earlier admitted element nothing.
*/
export function fileDeleteEntry(): CapabilityEntry {
- return coreEntry("File.Delete", "File.Delete", "file:delete");
+ return coreEntry("File.Delete", "file:delete");
}
/**
@@ -368,30 +331,18 @@ export function fileDeleteEntry(): CapabilityEntry {
*
* The name is the host's because the component is: the workflow calls it
* ``, and an ordinary run admits no such thing. What core supplies is the
- * operation — `ensureDirectory`, from the profile — so the body a fragment
- * reaches is not the ordinary registration and cannot be composed around.
+ * component — its implementation, and the identity and effect its paired form
+ * is offered under — so a host chooses whether the name is admitted here and
+ * never what admitting it grants.
*/
-export function directoryEntry(
- identity: FragmentIdentity,
- name: string,
- /**
- * The version-1 strings this host's own directory entry succeeds.
- *
- * The host's to state, because the identity is: core never wrote one of
- * these, so it has nothing to assert about which older grant this entry is
- * the same as.
- */
- legacy?: readonly string[],
-): CapabilityEntry {
+export function directoryEntry(name: string): CapabilityEntry {
return {
kind: "capability",
name,
- identity,
forms: [...CAPABILITY_FORMS["directory:ensure"]],
capability: "directory:ensure",
props: capabilityProps("directory:ensure"),
description: CORE_DESCRIPTIONS["directory:ensure"],
- ...(legacy === undefined ? {} : { legacy }),
};
}
@@ -404,7 +355,7 @@ export function directoryEntry(
* and refusing everything it asks for.
*/
export function fetchEntry(requests: readonly GeneratedRequest[]): CapabilityEntry {
- return { ...coreEntry("Fetch", "Fetch", "fetch"), requests };
+ return { ...coreEntry("Fetch", "fetch"), requests };
}
/** What core's own entries tell an agent they do. */
@@ -423,45 +374,14 @@ const CORE_DESCRIPTIONS: Readonly> = Object.f
fetch: "Perform one admitted HTTP read. Written self-closing.",
});
-/**
- * The exact strings each of core's own entries was retained as under version 1.
- *
- * Enumerated literally rather than assembled from the origin and the key.
- * There was never a rule that produced these — a released build wrote each one
- * where it built the pinned entry — so a template here would be this module
- * inventing the convention the whole design says does not exist, and a later
- * change to `CORE_ORIGIN` would silently rewrite what an old journal is
- * compared against. Core states them because core owns these entries and is the
- * party that can assert the current one authorizes no more than the old did.
- */
-const CORE_LEGACY: Readonly> = Object.freeze({
- "file:read": Object.freeze(["@executablemd/core#File:read"]),
- // Core never pinned a search entry under version 1; there is no older grant
- // for this one to assert it authorizes no more than.
- "files:glob": Object.freeze([]),
- "file:write": Object.freeze(["@executablemd/core#File:write"]),
- "file:delete": Object.freeze(["@executablemd/core#File.Delete"]),
- // Core never pinned a directory entry under version 1; the workflow host did,
- // and states its own alias.
- "directory:ensure": Object.freeze([]),
- fetch: Object.freeze(["@executablemd/core#Fetch"]),
- // A new grant. No version-1 record ever admitted a generated question, so
- // there is no older string for this to assert it authorizes no more than —
- // and a record naming one refuses rather than resuming against this.
- "elicit:ask": Object.freeze([]),
-});
-
-function coreEntry(name: string, key: string, capability: FragmentCapability): CapabilityEntry {
- const legacy = CORE_LEGACY[capability];
+function coreEntry(name: string, capability: FragmentCapability): CapabilityEntry {
return {
kind: "capability",
name,
- identity: { origin: CORE_ORIGIN, key, revision: CORE_REVISION },
forms: [...CAPABILITY_FORMS[capability]],
capability,
props: capabilityProps(capability),
description: CORE_DESCRIPTIONS[capability],
- ...(legacy.length === 0 ? {} : { legacy }),
};
}
@@ -469,15 +389,16 @@ function coreEntry(name: string, key: string, capability: FragmentCapability): C
* One provider-backed name, already resolved and reconciled.
*
* Produced by canonical execution's capture step before this module is asked
- * to seal anything: the chain has answered, the provider's identity statement
- * has been read off that exact final object, it has been compared with what
- * the host entry expects, and the answer has been retained defensively. What
- * arrives here is the settled result, so nothing in this module resolves,
- * looks up, or holds a resolver it could call later.
+ * to seal anything: the chain has answered, the definition the provider
+ * published has been taken off that exact final object, and it has been
+ * retained defensively. What arrives here is the settled result, so nothing in
+ * this module resolves, looks up, or holds a resolver it could call later.
*/
export interface ResolvedAnswer {
/** Core's own retained copy, which is what a fragment invokes. */
readonly definition: FunctionComponentDefinition;
+ /** Which provider installation published it, as documentation provenance. */
+ readonly origin: string;
}
/** Every provider-backed name this capture resolved, by name. */
@@ -489,12 +410,26 @@ export type ResolvedAnswers = ReadonlyMap;
* `kind` survives capture because the retained record keeps it: a continuation
* comparing identities has to know whether the thing behind a name was core's
* own operation or a provider's answer, and the two are different grants.
+ *
+ * There is no identity on it. Which URL and effect each admitted form runs
+ * under is on the definition this entry already holds, declared by whoever owns
+ * the implementation, so a capture that restated them would be a second source
+ * for one fact.
*/
export interface CapturedEntry {
/** Documentation provenance of an exact routed answer; grants no callable permission. */
readonly protectedOrigin?: string;
readonly name: string;
- readonly identity: FragmentIdentity;
+ /**
+ * Where this entry's implementation came from, for a reader and for the
+ * admitted vocabulary's symbols.
+ *
+ * Core's own origin for an operation core supplies the body for, and the
+ * publishing installation's for a provider's answer. Documentation
+ * provenance: it is no part of what the implementation is, and nothing
+ * compares a continuation against it.
+ */
+ readonly origin: string;
readonly forms: readonly FragmentForm[];
readonly props: PropsSchema;
readonly kind: "capability" | "component-answer" | "composition";
@@ -524,15 +459,6 @@ export interface CapturedEntry {
* states none.
*/
readonly dispatch?: unknown;
- /**
- * The version-1 identity strings this entry succeeds, copied by value.
- *
- * Read off the host's array once at capture and frozen, like everything else
- * here: a host that appends to its own list afterwards is appending to an
- * object nothing is looking at, so it cannot widen which retained records
- * reconcile to this entry from inside its own `install()`.
- */
- readonly legacy?: readonly string[];
/** This entry's own ceiling, normalized once and canonically ordered. */
readonly requests?: readonly FetchRequest[];
}
@@ -590,23 +516,24 @@ export class EvaluationProfileError extends Error {
* One entry this execution has copied, before it knows what is behind a
* provider-backed name.
*
- * Everything a host stated is already settled here — the identity, the forms,
- * the schema for a capability, the ceiling. What is missing is exactly the part
- * a host does not state: a component answer's implementation and the contract
- * that comes with it.
+ * Everything a host stated is already settled here — the forms, the schema for
+ * a capability, the ceiling, and the effect table this entry came from. What is
+ * missing is exactly the part a host does not state: a component answer's
+ * implementation, the contract that comes with it, and the identity its own
+ * declaration offers each admitted form under.
*/
interface PreparedEntry {
readonly name: string;
- readonly identity: FragmentIdentity;
readonly forms: readonly FragmentForm[];
readonly kind: "capability" | "component-answer" | "composition";
+ /** Which table admitted it, so a form's offered effect is checked against it. */
+ readonly effect: GeneratedEffect;
readonly description?: string;
readonly props?: PropsSchema;
readonly capability?: FragmentCapability;
readonly definition?: FunctionComponentDefinition;
/** The owning component's own form dispatcher, when its implementation wraps one. */
readonly dispatch?: unknown;
- readonly legacy?: readonly string[];
readonly requests?: readonly FetchRequest[];
}
@@ -626,12 +553,15 @@ interface PreparedEntry {
*/
export interface PreparedProfile {
/**
- * The provider-backed names this profile admits, with what it expects of each.
+ * The provider-backed names this profile admits.
*
- * Empty for a capability-only profile, which performs no component-chain
- * lookup at all — a host that offers no provider answer pays for none.
+ * Names alone, because what is behind one is the definition the chain
+ * answers with: a host states which names it admits and the owning
+ * declaration states what each admitted form runs as. Empty for a
+ * capability-only profile, which performs no component-chain lookup at all —
+ * a host that offers no provider answer pays for none.
*/
- readonly answered: readonly { readonly name: string; readonly identity: FragmentIdentity }[];
+ readonly answered: readonly string[];
/** Whether the operations this preparation bound are still usable. */
readonly live: () => boolean;
/** End them. Registered by canonical execution before any installation runs. */
@@ -666,8 +596,8 @@ export function* prepareEvaluationProfile(
// so they arrive as one definition whose dispatch separates the two
// spellings, exactly as the ordinary `` does.
const built = buildDefinitions(input, capabilities);
- const read = yield* prepareEntries(input.read ?? [], input.fetchTimeout, built);
- const write = yield* prepareEntries(input.write ?? [], input.fetchTimeout, built);
+ const read = yield* prepareEntries(input.read ?? [], "read", input.fetchTimeout, built);
+ const write = yield* prepareEntries(input.write ?? [], "write", input.fetchTimeout, built);
if (read.length === 0 && write.length === 0) {
throw new EvaluationProfileError(
"an evaluation profile states no component at all. A host offering evaluation states what " +
@@ -693,9 +623,7 @@ export function* prepareEvaluationProfile(
const answered = answeredNames([...read, ...write]);
const workspace = input.workspace === undefined ? undefined : bindWorkspace(input.workspace);
return Object.freeze({
- answered: Object.freeze(
- [...answered.entries()].map(([name, identity]) => Object.freeze({ name, identity })),
- ),
+ answered: Object.freeze([...answered]),
live: () => capabilities.live(),
revoke: () => {
capabilities.revoke();
@@ -740,11 +668,7 @@ function compositionEntries(): readonly CapturedEntry[] {
[pinnedJson()].map((entry) =>
Object.freeze({
name: entry.name,
- identity: Object.freeze({
- origin: entry.identity.origin,
- key: entry.identity.key,
- revision: entry.identity.revision,
- }),
+ origin: CORE_ORIGIN,
forms: Object.freeze(["self-closing"]),
props: detach(entry.definition.props),
kind: "composition" as const,
@@ -852,18 +776,20 @@ function normalizedCeiling(
function* prepareEntries(
entries: readonly FragmentEntry[],
+ effect: GeneratedEffect,
fetchTimeout: number | undefined,
built: Map,
): Operation {
const prepared: PreparedEntry[] = [];
for (const entry of entries) {
- prepared.push(yield* prepareEntry(entry, fetchTimeout, built));
+ prepared.push(yield* prepareEntry(entry, effect, fetchTimeout, built));
}
return Object.freeze(prepared);
}
function* prepareEntry(
entry: FragmentEntry,
+ effect: GeneratedEffect,
fetchTimeout: number | undefined,
built: Map,
): Operation {
@@ -873,7 +799,6 @@ function* prepareEntry(
`an evaluation profile admitted "${entry.name}" for no authored form.`,
);
}
- const identity = captureIdentity(entry.identity, entry.name);
const described =
typeof entry.description === "string" && entry.description.length > 0
? { description: entry.description }
@@ -881,14 +806,15 @@ function* prepareEntry(
if (entry.kind === "component-answer") {
// Nothing about the implementation is settled here. What this entry states
- // is which name, and which identity a provider must have claimed for it;
- // the chain answers later, and props come from that answer rather than from
- // the host, so a host cannot describe a contract the implementation lacks.
+ // is which name and which forms; the chain answers later, and the props,
+ // the identity and the effect all come from that answer rather than from
+ // the host, so a host cannot describe a contract or an effect the
+ // implementation lacks.
return Object.freeze({
name: entry.name,
- identity,
forms,
kind: "component-answer" as const,
+ effect,
...described,
});
}
@@ -908,69 +834,40 @@ function* prepareEntry(
"an evaluation profile admitted a name with no operation behind it.",
);
}
- const legacy = captureLegacy(entry.legacy, entry.name);
- return Object.freeze({
+ const prepared: PreparedEntry = Object.freeze({
name: entry.name,
- identity,
forms,
props,
kind: "capability" as const,
+ effect,
capability: entry.capability,
...described,
definition: implementation.definition,
...(implementation.dispatch === undefined ? {} : { dispatch: implementation.dispatch }),
- ...(legacy === undefined ? {} : { legacy }),
...(requests === undefined ? {} : { requests }),
});
+ // Core supplies this body, so the owning declaration is already in hand: a
+ // capability admitted in the table its owner does not offer that form to
+ // refuses here, before an execution holds a profile at all.
+ admittedIdentities(prepared, implementation.definition);
+ return prepared;
}
/**
- * The version-1 aliases this entry states, copied and checked.
+ * The provider-backed names this profile admits.
*
- * Copied because the host's array is the host's, and checked because an alias
- * is a comparison term: an empty string would reconcile against nothing
- * usefully, and a duplicate would say one thing twice. A host stating an empty
- * list has stated no alias, which is the same as stating none.
- */
-function captureLegacy(
- legacy: readonly string[] | undefined,
- name: string,
-): readonly string[] | undefined {
- if (legacy === undefined) {
- return undefined;
- }
- const held = new Set();
- for (const alias of legacy) {
- if (typeof alias !== "string" || alias.length === 0) {
- throw new EvaluationProfileError(
- `an evaluation profile stated an empty version-1 identity for "${name}". An alias is a ` +
- "string an older run actually retained, and there is no such record holding nothing.",
- );
- }
- held.add(alias);
- }
- return held.size === 0 ? undefined : Object.freeze([...held]);
-}
-
-/**
- * The provider-backed names this profile admits, with the one identity each of
- * them states.
- *
- * A name is one component, so it has one implementation and one identity. Two
+ * A name is one component, so it resolves once and runs one implementation. Two
* entries under one name are the two spellings of that component — the
- * self-closing one admitted to observe and the paired one to mutate — and
- * stating a second identity for the second spelling would be stating that the
- * name means two things, with which of them a fragment reached decided by which
- * table admitted it. That is not a narrower grant than the host wrote down; it
- * is an ambiguous one, so it refuses at capture rather than resolving by
- * position.
+ * self-closing one admitted to observe and the paired one to mutate — and both
+ * read their own identity and effect off the one definition the chain answered
+ * with, so there is nothing for a second entry to disagree about.
*
- * A name a host holds as both a capability and a component answer is the same
- * ambiguity with the sharper edge: canonical core would supply one body and the
- * import chain the other, which are different grants under one spelling.
+ * A name a host holds as both a capability and a component answer is a real
+ * ambiguity, and refuses: canonical core would supply one body and the import
+ * chain the other, which are different grants under one spelling.
*/
-function answeredNames(prepared: readonly PreparedEntry[]): ReadonlyMap {
- const answered = new Map();
+function answeredNames(prepared: readonly PreparedEntry[]): ReadonlySet {
+ const answered = new Set();
const kinds = new Map();
for (const entry of prepared) {
const held = kinds.get(entry.name);
@@ -982,37 +879,17 @@ function answeredNames(prepared: readonly PreparedEntry[]): ReadonlyMap,
+ answered: ReadonlySet,
answers: ResolvedAnswers,
capabilities: CapturedCapabilities,
project: ComponentRouting["project"] | undefined,
): ReadonlyMap {
const sealed = new Map();
- for (const name of answered.keys()) {
+ for (const name of answered) {
// Resolved once, before any document code, through the complete ordinary
- // import chain — and already reconciled against what this entry expects.
+ // import chain, and published there by the provider that supplied it.
const resolved = answers.get(name);
if (resolved === undefined) {
throw new EvaluationProfileError(
@@ -1050,6 +927,7 @@ function sealAnswers(
sealed.set(
name,
Object.freeze({
+ origin: resolved.origin,
props: detach(answer.props),
definition: Object.freeze({ ...answer, fn: guard }),
...(protectedOrigin === undefined ? {} : { protectedOrigin }),
@@ -1082,9 +960,14 @@ function sealEntry(entry: PreparedEntry, sealed: ReadonlyMap as ${identityRecord(expected)}, and the import ` +
- `chain answered with ${identityRecord(stated)}. An admitted identity is the exact ` +
- "implementation a fragment may run."
- );
-}
+/** What a provider-backed name no provider published an answer for refuses with. */
+const UNPUBLISHED_ANSWER =
+ "an evaluation profile admits a component answer, and no provider published the " +
+ "implementation the import chain answered with. A fragment runs an answer its provider " +
+ "published, or it runs none.";
/**
* What canonical core itself holds for this name, or nothing.
*
* A protected component's implementation is the one this execution built; an
* ordinary core component's is the definition core registered. Both are core's
- * own object, and the identity comparison is the whole check: an answer that
- * is anything else — a repository component of the same name, a registration a
- * nested scope layered over, a middleware's replacement — is not this, so it
- * receives no canonical identity and the entry refuses it.
+ * own object, and object identity is the whole check: an answer that is
+ * anything else — a repository component of the same name, a registration a
+ * nested scope layered over, a middleware's replacement, a look-alike carrying
+ * core's own generated URLs — is not this, so core publishes nothing for it and
+ * the entry refuses it.
*/
function sealedByCore(inputs: ImportInputs, name: string): FunctionComponentDefinition | undefined {
return inputs.guarded.get(name) ?? coreComponentDefinition(name);
@@ -810,16 +789,17 @@ function sealedByCore(inputs: ImportInputs, name: string): FunctionComponentDefi
* is not authored work, and a durable record for it would be an effect a
* continuation then had to account for.
*
- * What comes back is asked about once. The provider's identity statement is
- * read off the *exact final answer* rather than off anything the chain held in
- * between, and that one call answers with both the identity and core's own
- * copy of what was identified — taken when the identity was recorded, before
- * the chain could see it again. So the value this run keeps is the value the
- * check was made about: there is no second read of the chain's object for an
- * alternating proxy or a computed member to answer differently.
- *
- * The identity is then compared whole with what the host entry expects, and the
- * copy — not the answer — is what the profile seals.
+ * What comes back is asked about once. Whether a provider published the *exact
+ * final answer* is asked of that object rather than of anything the chain held
+ * in between, and that one call answers with core's own copy of what was
+ * published — taken when it was published, before the chain could see it again
+ * — together with the publishing installation's origin. So the value this run
+ * keeps is the value the check was made about: there is no second read of the
+ * chain's object for an alternating proxy or a computed member to answer
+ * differently.
+ *
+ * What each admitted form of that definition runs as is the definition's own,
+ * and the profile holds every admitted form to it as it seals.
*/
function* resolveComponentAnswers(
prepared: PreparedProfile,
@@ -831,21 +811,13 @@ function* resolveComponentAnswers(
// A capability-only profile performs no component-chain lookup at all.
return answers;
}
- // The names this profile admits under canonical core's own identity. A
- // protected component is nobody's to answer for, so no provider states what
- // is behind one — core does, here, and only where the entry says it expects
- // core. A host that admits a protected name under a provider's identity is
- // still that provider's to claim, exactly as it was.
+ // The protected names this profile admits. A protected component is nobody's
+ // to answer for, so no provider publishes what is behind one — core does,
+ // here, and only for the one protected name it offers a host an entry for. A
+ // host that admits any other name is still the provider's to publish, exactly
+ // as it was.
const canonical = new Set(
- prepared.answered
- .filter(
- ({ name, identity }) =>
- CANONICAL_PROFILE_ANSWERS.has(name) &&
- identity.origin === CORE_ORIGIN &&
- identity.key === name &&
- identity.revision === CORE_REVISION,
- )
- .map(({ name }) => name),
+ prepared.answered.filter((name) => CANONICAL_PROFILE_ANSWERS.has(name)),
);
const canonicalProvider = canonical.size === 0 ? undefined : imports.provider(CORE_ORIGIN);
yield* scoped(function* () {
@@ -853,11 +825,13 @@ function* resolveComponentAnswers(
{
*importComponent([name, position], _next) {
const answer = yield* resolveImportUnrecorded(name, inputs);
- // Stated from the innermost position, on the exact object
+ // Published from the innermost position, on the exact object
// materialization produced, so a handler composed around this one
- // states nothing: middleware that returns this answer unchanged
- // leaves the claim on it, and middleware that returns anything else
- // returns something carrying no identity at all.
+ // publishes nothing: middleware that returns this answer unchanged
+ // leaves the publication on it, 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.
if (
canonicalProvider === undefined ||
!canonical.has(name) ||
@@ -867,7 +841,7 @@ function* resolveComponentAnswers(
}
const asked = canonicalProvider.open(name, position);
try {
- return asked.request.claim(answer, { key: name, revision: CORE_REVISION });
+ return asked.request.answer(answer);
} finally {
asked.close();
}
@@ -875,16 +849,16 @@ function* resolveComponentAnswers(
},
{ at: "min" },
);
- for (const { name, identity } of prepared.answered) {
- // One resolution, one claim window. It opens before the chain is asked
- // and closes however this iteration leaves — answered, refused, or
+ for (const name of prepared.answered) {
+ // One resolution, one publication window. It opens before the chain is
+ // asked and closes however this iteration leaves — answered, refused, or
// cancelled partway through — so a handler that lost this decision, or
- // one still holding its request while the *next* name resolves, states
+ // one still holding its request while the *next* name resolves, publishes
// nothing into a decision that is already made.
//
- // One call, one result. The claim and core's copy of what was claimed
- // come back together, and the object the chain returned is not read
- // again — so nothing this run keeps was decided by a second read.
+ // One call, one result. The publication and core's copy of what was
+ // published come back together, and the object the chain returned is not
+ // read again — so nothing this run keeps was decided by a second read.
const resolution = imports.beginResolution(name);
let identified;
try {
@@ -897,16 +871,9 @@ function* resolveComponentAnswers(
resolution.close();
}
if (identified === undefined) {
- throw new EvaluationProfileError(UNIDENTIFIED_ANSWER);
- }
- const { identity: stated, definition: own } = identified;
- if (
- stated.origin !== identity.origin ||
- stated.key !== identity.key ||
- stated.revision !== identity.revision
- ) {
- throw new EvaluationProfileError(differentAnswer(name, identity, stated));
+ throw new EvaluationProfileError(UNPUBLISHED_ANSWER);
}
+ const { origin, definition: own } = identified;
// An answer this execution could not keep a copy of never reaches here:
// 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
@@ -917,7 +884,7 @@ function* resolveComponentAnswers(
"definition that has no implementation to invoke. An admitted entry runs something.",
);
}
- answers.set(name, { definition: own });
+ answers.set(name, { definition: own, origin });
}
});
return answers;
@@ -3352,15 +3319,28 @@ function reconcile(document: Result | undefined, teardown: Error | undefin
* Shared by the components a host declares to an execution and by the private
* declarations one declared Markdown component carries, so a member added to
* one is not silently dropped from the other.
+ *
+ * The generated use each form is offered for is read through the validator that
+ * owns that contract, because it is the one member whose copy is a grant: an
+ * identity and an effect decide which implementation a continuation admits, so
+ * a reading taken later — or a nested object left live for the factory to be
+ * handed — would be a grant this declaration never made. What the validator
+ * answers with is what is retained, and what the forms are built from.
*/
function retainedIdentityComponent(component: IdentityComponent): IdentityComponent {
+ const name = component.name;
+ // Read before the metadata, so the forms the offer is held against are the
+ // forms this execution accepts the component in.
+ const forms = component.forms === undefined ? undefined : Object.freeze([...component.forms]);
+ const generated = admittedGenerated({ name, generated: component.generated, forms });
return Object.freeze({
- name: component.name,
+ name,
origin: component.origin,
props: detachedSchema(component.props),
...(component.returns === undefined ? {} : { returns: detachedSchema(component.returns) }),
...(component.captures === undefined ? {} : { captures: [...component.captures] }),
- ...(component.forms === undefined ? {} : { forms: [...component.forms] }),
+ ...(forms === undefined ? {} : { forms }),
+ ...(generated === undefined ? {} : { generated }),
...documentationOf(component),
factory: component.factory.bind(component),
});
diff --git a/packages/core/src/execution-declarations.ts b/packages/core/src/execution-declarations.ts
index 9d83ab055..140e32b7b 100644
--- a/packages/core/src/execution-declarations.ts
+++ b/packages/core/src/execution-declarations.ts
@@ -61,7 +61,7 @@ import type {
import { admitDeclaration, isComponentName } from "./components/registration.ts";
import { PROTECTED_COMPONENT_NAMES, protectedNameRefusal } from "./components/protected.ts";
import { RESERVED_STRUCTURAL } from "./structural.ts";
-import { formsRefusal } from "./invocation-identity.ts";
+import { formsRefusal } from "./invocation-forms.ts";
import { isBlankText } from "./structural-rules.ts";
import type { StructuralViolation } from "./structural-rules.ts";
import type {
@@ -330,7 +330,7 @@ export function isKnownKind(kind: unknown): kind is ExecutionDeclaration["kind"]
* a function returning one, so it stays as lazy and as cancellable here as it
* is anywhere else.
*/
-function* admitting(name: string, admission: Operation): Operation {
+function* admitting(name: string, admission: Operation): Operation {
try {
yield* admission;
} catch (error) {
diff --git a/packages/core/src/generated-xmd.ts b/packages/core/src/generated-xmd.ts
index 818e7ffa3..bd1fab52f 100644
--- a/packages/core/src/generated-xmd.ts
+++ b/packages/core/src/generated-xmd.ts
@@ -61,8 +61,8 @@
* so does this: every admitted invocation is checked against that same fact
* before the component runs, and refused when it is not the form its identity
* was chosen for. Nothing a handler answers reaches either read, so an admitted
- * `File:read` cannot be turned into a write, or an admitted `File:write` into a
- * read, however many times something is asked.
+ * reading ` ` cannot be turned into a write, or an admitted writing
+ * `… ` into a read, however many times something is asked.
*
* ## A resumed run is held to the ceilings it was admitted under
*
@@ -96,14 +96,14 @@
* ``, the Workspace file effect for `` — so a replay restores both
* without asking anyone anything a second time.
*
- * An identity in that record is a closed tagged structural record: what kind of
- * thing is behind the name, the origin that owns it, the component key, and the
- * revision. Four terms rather than one spelling, so a reader comparing two
- * admissions can say which of them moved and no two hosts have to agree on a
- * separator. Version 1 — the untagged #369 record — wrote one opaque string
- * instead, and stays both readable *and* resumable: a version-1 string is
- * reconciled against a current capability identity under the spelling that
- * build used. Nothing this build writes uses that spelling.
+ * An identity in that record is the URL the declaration that owns the
+ * implementation offers that spelling under — `https://executable.md/components/file/read`
+ * for ` ` and `.../file/write` for `… ` — retained beside
+ * what kind of thing answered for the name and which effect table admitted it.
+ * One component, one definition, one identity per form, all of them stated
+ * where the implementation is. A record written by an earlier build holds a
+ * shape this one does not have, and is refused as unreadable history before any
+ * generated work rather than read as whichever current shape it most resembles.
*/
import { createDurableOperation } from "@executablemd/durable-streams";
@@ -119,6 +119,9 @@ import type { ComponentResolution, ImportedDefinition } from "./components/compo
import { isComponentName } from "./components/registration.ts";
import { CORE_ORIGIN, CORE_REGISTRY } from "./components/registry.ts";
import { fixedFilesystemDefinition } from "./components/fixed-filesystem.ts";
+import { admitGeneratedForms, isGeneratedIdentity } from "./components/generated-metadata.ts";
+import type { GeneratedForms } from "./components/generated-metadata.ts";
+import { acceptedForms, formsRefusal } from "./invocation-forms.ts";
import { createBlockCounter, expandSegmentsWithin } from "./expand.ts";
import { extendPath } from "./expansion.ts";
import { prepareFetchRequest, requestRecord } from "./fetch-request.ts";
@@ -312,131 +315,37 @@ export type GeneratedRequest = Record;
export type GeneratedEffectClass = "read" | "write";
/**
- * Which implementation one admitted entry runs, as the run retains it.
+ * Whether canonical core supplies the body behind an admitted name, or the
+ * ordinary import chain answered for it.
*
- * A closed tagged record rather than a string, and structural rather than
- * assembled, because this is what a continuation compares. Four terms, each
- * with one meaning: a reader looking at two admissions can say *which* of them
- * moved, and no host has to agree with another about how to spell a separator.
- *
- * `kind` is part of the identity rather than beside it. An operation canonical
- * core supplies the body for and an implementation the ordinary import chain
- * answered with are different grants even under one origin, key and revision —
- * the first cannot be composed around and the second was resolved through
- * middleware — so a record that confused them would compare a fragment's
+ * Retained beside the identity, because the two are different grants even under
+ * one URL: the first cannot be composed around and the second was resolved
+ * through middleware, so a record that confused them would compare a fragment's
* admission equal to an admission it never had.
- *
- * Never derived from a function. An implementation is not an identity, and
- * serializing or inspecting one would make the retained policy depend on how a
- * host happened to write its code. Functions never enter the journal.
*/
-export type RetainedFragmentIdentity =
- | {
- readonly kind: "capability";
- readonly origin: string;
- readonly key: string;
- readonly revision: string;
- }
- | {
- readonly kind: "component-answer";
- readonly origin: string;
- readonly key: string;
- readonly revision: string;
- };
+export type GeneratedImplementation = "capability" | "component-answer";
/** The two ways a host can say what is behind an admitted name. */
-const IDENTITY_KINDS: readonly RetainedFragmentIdentity["kind"][] = [
- "capability",
- "component-answer",
-];
-
-/**
- * What a record retains for one entry: this version's identity, or version 1's.
- *
- * Version 1 retained one opaque string. It is kept as the string it is rather
- * than parsed into a shape it never had, and reconciled against a version-2
- * identity under the spelling the build that wrote it used — so an older record
- * stays readable *and* stays a grant, instead of being readable and useless.
- */
-type RetainedIdentity = RetainedFragmentIdentity | LegacyIdentity;
-
-/** One version-1 identity: the exact string an older build wrote. */
-interface LegacyIdentity {
- readonly legacy: string;
-}
+const IMPLEMENTATIONS: readonly GeneratedImplementation[] = ["capability", "component-answer"];
/**
- * One identity as a map key, for the run's own tables.
- *
- * Never retained and never compared: what a continuation is held to is the
- * record's four members, read one at a time. This exists because a `Map` needs
- * a primitive, and it carries the kind so two grants that differ only in how
- * they were resolved do not share a ceiling.
+ * Whether the row this run states is the one a retained row was admitted under.
*
- * The encoding is JSON rather than the four terms joined by a separator,
- * because a joined key is only as unique as the separator is illegal. A host
- * may state an origin or a key holding any character at all — a URL with a
- * space in it, a component key with a `#` — and two identities that differ only
- * in where the separator falls would key one entry's request ceiling under
- * another's. JSON escapes what it encodes, so distinct terms encode distinctly.
+ * Member by member: the URL the spelling runs under, the spelling itself, what
+ * kind of thing answered for the name, and which effect table admitted it. A
+ * changed URL, a form added, an implementation resolved another way and a read
+ * moved into the write table are each a different grant.
*/
-function identityKey(identity: RetainedFragmentIdentity): string {
- return JSON.stringify([identity.kind, identity.origin, identity.key, identity.revision]);
-}
-
-/**
- * Whether two version-2 identities describe the same implementation.
- *
- * Member by member, kind included: an operation canonical core supplies the
- * body for and an implementation the ordinary import chain answered with are
- * different grants even under one origin, key and revision.
- */
-function sameStructural(one: RetainedFragmentIdentity, other: RetainedFragmentIdentity): boolean {
+function sameRow(retained: RetainedEntry, current: RetainedEntry): boolean {
return (
- one.kind === other.kind &&
- one.origin === other.origin &&
- one.key === other.key &&
- one.revision === other.revision
+ retained.name === current.name &&
+ retained.form === current.form &&
+ retained.identity === current.identity &&
+ retained.kind === current.kind &&
+ retained.effect === current.effect
);
}
-/**
- * Whether the entry this run states is the one a retained record was admitted
- * under.
- *
- * Two version-2 records compare structurally, and two version-1 strings compare
- * as the strings they are.
- *
- * A version-1 string against a version-2 entry is the case that cannot be
- * decided by looking at either one. Version 1 retained one opaque value chosen
- * by whoever built the pinned entry — `@executablemd/core#File:read` from core,
- * `@executablemd/workflow/composition/dir-v2#Dir` from the workflow host, and
- * whatever a host passed to `pinnedComponent` from anyone else. No rule
- * recovers four structural terms from one of those, and a rule that appeared to
- * would only be reading the shapes core happens to use today back onto records
- * core did not write.
- *
- * So it is not inferred. The *current* entry states which version-1 strings it
- * succeeds, and a retained string reconciles only against that stated list.
- * Listing one is an assertion by the party that owns the entry; an entry that
- * lists none refuses every version-1 record naming it, which is the safe
- * direction. New records state the structural identity and never a string.
- */
-function sameIdentity(retained: RetainedIdentity, current: RetainedEntry): boolean {
- const here = current.identity;
- if ("legacy" in retained) {
- if ("legacy" in here) {
- return retained.legacy === here.legacy;
- }
- // Only a capability reconciles. The component-answer arm did not exist when
- // version-1 records were written, so no such string ever described one —
- // and the kind is checked here rather than left to the profile, because
- // this is the comparison a continuation is actually held to.
- return here.kind === "capability" && current.legacy?.includes(retained.legacy) === true;
- }
- return "legacy" in here ? false : sameStructural(retained, here);
-}
-
/**
* The authored forms one pinned identity runs in.
*
@@ -461,15 +370,21 @@ function authoredForms(form: GeneratedComponentForm): readonly AuthoredForm[] {
* One observation component a generated fragment may name, and the exact
* definition that name runs.
*
- * `identity` is the stable, non-secret descriptor the run retains: it says
- * which implementation was admitted, and holding it grants nothing. It is also
- * what a continuation is compared against, so changing the definition behind a
- * name means changing this.
+ * There is no identity beside the definition. Which URL each admitted spelling
+ * runs under, and whether that spelling observes or mutates, is declared by
+ * whoever owns the implementation and travels on the definition — so a host
+ * admits a name and never describes what admitting it grants. A continuation is
+ * compared against those URLs, so changing the definition behind a name means
+ * changing what its owner declares.
*/
export interface GeneratedObservation {
readonly name: string;
- readonly identity: RetainedFragmentIdentity;
readonly definition: FunctionComponentDefinition;
+ /**
+ * Whether canonical core supplies this body or the import chain answered for
+ * it. Omitted is core's own operation, which is what a pinned entry is.
+ */
+ readonly kind?: GeneratedImplementation;
/**
* The form dispatcher underneath this entry's implementation, when the
* definition wraps one.
@@ -480,76 +395,33 @@ export interface GeneratedObservation {
* the ordinary case: the definition's own `fn` is the dispatcher.
*/
readonly dispatch?: unknown;
- /**
- * The exact version-1 identity strings this entry states it succeeds.
- *
- * Stated by whoever owns the entry rather than derived from the identity
- * above, because version 1 retained one opaque value that nothing recovers
- * four structural terms from. A retained version-1 record reconciles against
- * this list and against nothing else.
- */
- readonly legacy?: readonly string[];
/**
* The exact requests this observation may perform, when it performs HTTP
- * reads at all. Present only on the pinned `` identity.
+ * reads at all. Present only on the pinned `` entry.
*/
readonly requests?: readonly GeneratedRequest[];
/**
* Whether only the self-closing form of this name is admitted.
*
- * A component whose two forms do different things has two identities, and a
- * host admitting one of them is not admitting the other. `` is the case
- * that matters: `hasContent()` is exactly `!selfClosing`, so the paired form
- * writes. The constraint therefore travels with the pinned identity and is
- * decided in preflight, before the first effect — not checked inside the
- * component after earlier elements have already run.
+ * A component whose two forms do different things offers each of them its own
+ * identity and effect, and a host admitting one of them is not admitting the
+ * other. `` is the case that matters: `hasContent()` is exactly
+ * `!selfClosing`, so the paired form writes. The constraint therefore travels
+ * with the entry and is decided in preflight, before the first effect — not
+ * checked inside the component after earlier elements have already run.
*/
readonly selfClosing?: boolean;
}
/**
- * The revision core's own entries state, wherever they are admitted from.
- *
- * One number for all of them, bumped whenever what any of these entries
- * authorizes changes, so a continuation admitted under an earlier revision is
- * refused rather than silently granted the newer admission.
- *
- * One constant rather than one per table. The pinned constructors here and the
- * evaluation profile's entries describe the same operations, and a reader
- * comparing two admissions has to be able to trust that they say so — two
- * numbers that had to be moved together would eventually not be.
- *
- * Revision 2 is where the operations behind these entries changed: an admitted
- * element used to invoke the ordinary component and resolve `API.Files` or
- * `API.Fetch` wherever it happened to run, and now it invokes a body closed over
- * the operations the host handed the profile.
- */
-export const CORE_REVISION = "2";
-
-/** One of core's own pinned identities, under core's origin and revision. */
-function coreIdentity(key: string): RetainedFragmentIdentity {
- return { kind: "capability", origin: CORE_ORIGIN, key, revision: CORE_REVISION };
-}
-
-/**
- * The exact strings core's own entries were retained as under version 1.
- *
- * Written out one by one, and deliberately not assembled from the origin and
- * the key. There was never a rule producing these — `pinnedFetch` wrote its
- * string, `pinnedFileRead` wrote its own, and a host writing a fourth was under
- * no obligation to resemble either — so a function that built them would be
- * inventing the rule this module exists to say does not exist. Enumerating them
- * is also what makes the set reviewable: adding an alias is adding a line, and
- * every line is an assertion that the current entry authorizes no more than the
- * one that string named.
+ * Core's ` `, as the composition table retains it.
*
- * Frozen and read by value at each call site. Nothing derives one, and a fifth
- * released alias would be a fifth literal here rather than a broader pattern.
+ * Json is trusted composition rather than an effect: there is no operation
+ * behind it to grant, so it carries no read or write form for an owner to
+ * offer. The identity is core's own, stated here because this is the one place
+ * that admits it.
*/
-const CORE_V1_FETCH = Object.freeze(["@executablemd/core#Fetch"]);
-const CORE_V1_FILE_READ = Object.freeze(["@executablemd/core#File:read"]);
-const CORE_V1_FILE_WRITE = Object.freeze(["@executablemd/core#File:write"]);
-const CORE_V1_FILE_DELETE = Object.freeze(["@executablemd/core#File.Delete"]);
+export const JSON_COMPOSITION_IDENTITY = "https://executable.md/components/json";
/**
* The pinned core `` identity, bounded to exactly these requests.
@@ -570,13 +442,7 @@ export function pinnedFetch(requests: readonly GeneratedRequest[]): GeneratedObs
"admitting to a generated fragment requires the exact requests it may perform.",
);
}
- return {
- name: "Fetch",
- identity: coreIdentity("Fetch"),
- legacy: CORE_V1_FETCH,
- definition,
- requests: [...requests],
- };
+ return { name: "Fetch", definition, requests: [...requests] };
}
/**
@@ -585,9 +451,10 @@ export function pinnedFetch(requests: readonly GeneratedRequest[]): GeneratedObs
* Core's own fixed definition, and only its self-closing spelling. ``
* reads when it has no content and writes when it has some, so admitting the
* unconstrained definition would admit a write — which is why this constructor
- * exists rather than a caller reaching for the definition itself and hoping. The
- * identity says so too, so a run that later admitted the unconstrained `File`
- * would be stating a different policy and a retained admission would refuse it.
+ * exists rather than a caller reaching for the definition itself and hoping.
+ * The identity the admission retains is the one core's declaration offers that
+ * spelling under, so a run that later admitted the paired form would be stating
+ * a different policy and a retained admission would refuse it.
*
* An admitted read invokes the ordinary `` component and therefore the
* installed Files provider, which under a workflow run is the transaction-bound
@@ -598,13 +465,20 @@ export function pinnedFileRead(): GeneratedObservation {
if (definition === undefined) {
throw new GeneratedXmdError("core supplies no File component to admit.");
}
- return {
- name: "File",
- identity: coreIdentity("File:read"),
- legacy: CORE_V1_FILE_READ,
- definition,
- selfClosing: true,
- };
+ return { name: "File", definition, selfClosing: true };
+}
+
+/**
+ * One component the trusted composition table admits, and its identity.
+ *
+ * Composition is core's own and performs nothing, so it is the one admitted
+ * thing whose identity is not read off an owner's generated metadata: a pure
+ * component offers no read or write form for an owner to declare.
+ */
+export interface CompositionEntry {
+ readonly name: string;
+ readonly identity: string;
+ readonly definition: FunctionComponentDefinition;
}
/**
@@ -614,23 +488,18 @@ export function pinnedFileRead(): GeneratedObservation {
* selects, because rendering a value it already holds performs nothing: there
* is no operation behind it to grant, so admitting it widens no permission. The
* exact core definition, so a same-name replacement anywhere reaches nothing
- * here — purity is a property of this identity, never inferred from a name.
+ * here — purity is a property of this entry, never inferred from a name.
*
- * Self-closing alone. `` refuses content of its own, and stating the form
- * here puts that refusal in preflight rather than inside the component after
- * earlier elements have run.
+ * Self-closing alone. `` refuses content of its own, and the composition
+ * table admits that spelling rather than both, so the refusal is in preflight
+ * rather than inside the component after earlier elements have run.
*/
-export function pinnedJson(): GeneratedObservation {
+export function pinnedJson(): CompositionEntry {
const definition = CORE_REGISTRY.get("Json")?.default?.definition;
if (definition === undefined || definition.kind !== "function") {
throw new GeneratedXmdError("core supplies no Json component to admit.");
}
- return {
- name: "Json",
- identity: coreIdentity("Json"),
- definition,
- selfClosing: true,
- };
+ return { name: "Json", identity: JSON_COMPOSITION_IDENTITY, definition };
}
/**
@@ -639,30 +508,25 @@ export function pinnedJson(): GeneratedObservation {
* Core's own, and not a ceiling a host states: a host cannot remove Json from a
* fragment's vocabulary by omitting it, and cannot add an effect to the table
* by naming one. A trusted host that wants another pure component states its
- * exact definition and identity, which is a different act from this one.
+ * exact definition, which is a different act from this one.
*/
-export function coreComposition(): readonly GeneratedObservation[] {
+export function coreComposition(): readonly CompositionEntry[] {
return [pinnedJson()];
}
/**
* One host-registered function observation component, by the exact definition
* the host holds.
+ *
+ * The definition states what each of its spellings is offered as, so there is
+ * nothing for the host to say about identity here.
*/
export function pinnedComponent(
name: string,
- identity: RetainedFragmentIdentity,
definition: FunctionComponentDefinition,
- /**
- * The version-1 strings this host's own entry succeeds.
- *
- * The host's to state, because under version 1 this constructor took whatever
- * string the host chose and retained it verbatim. Core has nothing to assert
- * about which of those an entry is the successor of.
- */
- legacy?: readonly string[],
+ kind?: GeneratedImplementation,
): GeneratedObservation {
- return { name, identity, definition, ...(legacy === undefined ? {} : { legacy }) };
+ return { name, definition, ...(kind === undefined ? {} : { kind }) };
}
/**
@@ -682,12 +546,14 @@ export function pinnedComponent(
*/
export interface GeneratedMutation {
readonly name: string;
- readonly identity: RetainedFragmentIdentity;
readonly definition: FunctionComponentDefinition;
+ /**
+ * Whether canonical core supplies this body or the import chain answered for
+ * it. Omitted is core's own operation, which is what a pinned entry is.
+ */
+ readonly kind?: GeneratedImplementation;
/** The form dispatcher underneath this entry's implementation, when it wraps one. */
readonly dispatch?: unknown;
- /** The exact version-1 identity strings this entry states it succeeds. */
- readonly legacy?: readonly string[];
readonly form: GeneratedComponentForm;
}
@@ -705,23 +571,17 @@ export function pinnedFileWrite(): GeneratedMutation {
if (definition === undefined) {
throw new GeneratedXmdError("core supplies no File component to admit.");
}
- return {
- name: "File",
- identity: coreIdentity("File:write"),
- legacy: CORE_V1_FILE_WRITE,
- definition,
- form: "paired",
- };
+ return { name: "File", definition, form: "paired" };
}
/**
* The pinned core `` identity, in the one form it has.
*
* Core's own fixed definition, and its self-closing spelling. One name, one
- * identity: unlike ``, whose two spellings do different things, this
+ * form: unlike ``, whose two spellings do different things, this
* component answers the self-closing form and refuses the paired one, so the
- * form stated here names what the identity is rather than narrowing it to part
- * of it. Stating it anyway is what puts the decision in preflight, before the
+ * form stated here names the whole of what core offers rather than narrowing it
+ * to part. Stating it anyway is what puts the decision in preflight, before the
* first effect of the fragment — a paired spelling costs an earlier admitted
* element nothing.
*
@@ -738,28 +598,23 @@ export function pinnedFileDelete(): GeneratedMutation {
if (definition === undefined) {
throw new GeneratedXmdError("core supplies no File.Delete component to admit.");
}
- return {
- name: "File.Delete",
- identity: coreIdentity("File.Delete"),
- legacy: CORE_V1_FILE_DELETE,
- definition,
- form: "self-closing",
- };
+ return { name: "File.Delete", definition, form: "self-closing" };
}
/**
* One host-owned mutation component, by the exact definition the host holds and
* the exact form it admits.
+ *
+ * The form chooses which of the definition's own offered spellings this entry
+ * admits; what that spelling runs as is the definition's to say.
*/
export function pinnedMutation(
name: string,
- identity: RetainedFragmentIdentity,
definition: FunctionComponentDefinition,
form: GeneratedComponentForm,
- /** The version-1 strings this host's own entry succeeds, as the host states them. */
- legacy?: readonly string[],
+ kind?: GeneratedImplementation,
): GeneratedMutation {
- return { name, identity, definition, form, ...(legacy === undefined ? {} : { legacy }) };
+ return { name, definition, form, ...(kind === undefined ? {} : { kind }) };
}
/** What a trusted host asks this evaluator to admit. */
@@ -780,9 +635,9 @@ export interface GeneratedXmdRequest {
readonly workspaceRoots?: readonly string[];
/** The one root admitted effects address, for a host that has one. */
readonly selectedRoot?: string;
- /** The pinned observation identities the `read` class resolves to. */
+ /** The pinned observation entries the `read` class resolves to. */
readonly observations: readonly GeneratedObservation[];
- /** The pinned mutation identities the `write` class resolves to. */
+ /** The pinned mutation entries the `write` class resolves to. */
readonly mutations?: readonly GeneratedMutation[];
/**
* Which effect classes this fragment may draw on. Omitted selects `read`
@@ -806,11 +661,21 @@ export interface GeneratedXmdRequest {
*/
interface Entry {
readonly name: string;
- readonly identity: RetainedFragmentIdentity;
+ /**
+ * The identity each admitted spelling runs under, read off the definition.
+ *
+ * One URL per form rather than one per entry, because that is where the
+ * declaration that owns the implementation puts it: ` ` and
+ * `… ` are one definition offering two spellings, and which one
+ * an element wrote decides which identity the admission retains.
+ */
+ readonly identities: ReadonlyMap;
readonly definition: FunctionComponentDefinition;
/** The form dispatcher under this entry's definition, when a host stated one. */
readonly dispatch?: unknown;
readonly forms: readonly AuthoredForm[];
+ /** Whether core supplies this body or the import chain answered for it. */
+ readonly kind: GeneratedImplementation;
/**
* Which table admitted this entry.
*
@@ -819,31 +684,29 @@ interface Entry {
* select between.
*/
readonly effect: GeneratedEffectClass | "composition";
- /** The version-1 identity strings this entry states it succeeds. */
- readonly legacy?: readonly string[];
readonly requests?: readonly GeneratedRequest[];
}
-/** The pinned identity of one admitted entry, as the run retains it. */
+/**
+ * One admitted spelling, as the run retains it.
+ *
+ * One row per name and form rather than per entry, because the identity and the
+ * effect belong to the spelling: a reader comparing two admissions sees exactly
+ * which spelling of which name moved, and the table a row came from is retained
+ * beside it rather than inferred from where it sits.
+ */
interface RetainedEntry {
readonly name: string;
- readonly identity: RetainedIdentity;
- readonly forms: readonly AuthoredForm[];
- /**
- * The version-1 strings this entry succeeds, on the *current* side only.
- *
- * Never read from a record and never written to one: a journal holds what was
- * admitted, and which older grants a current entry is willing to answer for
- * is a statement this run's host makes now. Reading it from the record would
- * let a retained value nominate its own successor.
- */
- readonly legacy?: readonly string[];
+ readonly form: AuthoredForm;
+ readonly identity: string;
+ readonly kind: GeneratedImplementation;
+ readonly effect: GeneratedEffectClass | "composition";
}
/** One element the fragment actually named, as the run retains it. */
interface RetainedInvocation {
readonly name: string;
- readonly identity: RetainedIdentity;
+ readonly identity: string;
readonly form: AuthoredForm;
}
@@ -867,19 +730,12 @@ interface Policy {
* rather than restating the same one.
*/
readonly workspace?: { readonly roots: readonly string[]; readonly selectedRoot: string };
- readonly allowed: readonly RetainedEntry[];
- readonly requests: readonly FetchRequest[];
/**
- * Which record shape this policy was read from, for a retained one.
- *
- * Absent on the policy a run states now, which is always current. A version-1
- * record was written before the trusted composition table existed, so it
- * names none, and holding it to entries that carry no effect operation would
- * refuse a continuation over a grant nobody widened.
+ * Every admitted spelling, in the order the selection resolved them:
+ * composition first, then the read table, then the write table.
*/
- readonly recordVersion?: 1 | 2;
- /** How many leading entries are composition, on the policy a run states now. */
- readonly composition?: number;
+ readonly allowed: readonly RetainedEntry[];
+ readonly requests: readonly FetchRequest[];
}
/** The decision this run recorded, restored from its own durable record. */
@@ -1135,15 +991,21 @@ function selectedEntries(
allow: readonly GeneratedEffectClass[],
): Entry[] {
const entries: Entry[] = [];
+ // One kept reading per definition object, so a name admitted for two
+ // spellings stays one component with one implementation.
+ const retained = new Map();
// Before either effect table and under every selection: composition is not a
// class `allow` chooses between, so its entries are in the policy the same
// way whatever the caller asked for, and in one canonical position.
for (const composition of coreComposition()) {
entries.push({
name: composition.name,
- identity: composition.identity,
+ // Core's own, and the one admitted identity not read off an owner's
+ // generated metadata: a pure component offers no effect to declare.
+ identities: new Map([["self-closing", composition.identity]]),
definition: composition.definition,
- forms: composition.selfClosing === true ? ["self-closing"] : AUTHORED_FORMS,
+ forms: ["self-closing"],
+ kind: "capability",
effect: "composition",
});
}
@@ -1152,16 +1014,28 @@ function selectedEntries(
throw new GeneratedXmdError("a generated-XMD allowlist selected `read` with no read table.");
}
for (const observation of request.observations) {
+ // Read once, before anything is derived from it. A host pins this
+ // definition directly, so nothing has validated what it offers — and
+ // everything below reads what this kept rather than the host's object: an
+ // identity that is not a URL in the accepted namespace, or a spelling its
+ // own declaration does not accept, is refused before the admission record
+ // and before any body, and a member that answers differently on a second
+ // reading never gets one.
+ const admitted = admitOffered(observation.name, observation.definition, retained);
+ // The spellings this component is offered to observe in, narrowed by the
+ // host where it constrained the entry to the self-closing one. Derived
+ // rather than assumed: a component offering one reading spelling admits
+ // that one, and a host cannot widen it to a spelling its owner never
+ // offered as a read.
+ const forms = observedForms(observation, admitted);
entries.push({
name: observation.name,
- identity: observation.identity,
- definition: observation.definition,
+ identities: offeredIdentities(observation.name, admitted, forms, "read"),
+ definition: admitted.definition,
...(observation.dispatch === undefined ? {} : { dispatch: observation.dispatch }),
- // An entry the host constrained to its self-closing spelling admits
- // that one; an unconstrained one admits both, as it always has.
- forms: observation.selfClosing === true ? ["self-closing"] : AUTHORED_FORMS,
+ forms,
+ kind: observation.kind ?? "capability",
effect: "read",
- ...(observation.legacy === undefined ? {} : { legacy: observation.legacy }),
...(observation.requests === undefined ? {} : { requests: observation.requests }),
});
}
@@ -1174,30 +1048,169 @@ function selectedEntries(
);
}
for (const mutation of mutations) {
+ const admitted = admitOffered(mutation.name, mutation.definition, retained);
+ const forms = authoredForms(mutation.form);
entries.push({
name: mutation.name,
- identity: mutation.identity,
- definition: mutation.definition,
+ identities: offeredIdentities(mutation.name, admitted, forms, "write"),
+ definition: admitted.definition,
...(mutation.dispatch === undefined ? {} : { dispatch: mutation.dispatch }),
- forms: authoredForms(mutation.form),
+ forms,
+ kind: mutation.kind ?? "capability",
effect: "write",
- ...(mutation.legacy === undefined ? {} : { legacy: mutation.legacy }),
});
}
}
return entries;
}
+/**
+ * One directly pinned definition, as this run keeps it.
+ *
+ * What a host pinned is read once into this, and the admission uses it for
+ * everything afterwards: which spellings are admitted, the URL each of them
+ * runs under, the rows the policy retains, and the definition a fragment's
+ * import is answered with. Checking the host's object and then reading it again
+ * would be two readings of a value the host controls — the journal would hold
+ * the second one, which is not what was validated.
+ */
+interface AdmittedDefinition {
+ /** The definition this run holds, with the metadata below on it. */
+ readonly definition: FunctionComponentDefinition;
+ /** The offered spellings, owned and frozen, or none for a definition offering none. */
+ readonly generated: GeneratedForms | undefined;
+}
+
+/**
+ * Hold one admitted definition to what its own declaration offers, and keep
+ * what it said.
+ *
+ * Every live generated admission reads this, because every one of them is
+ * handed a definition nothing else validated: a provider's published answer was
+ * read where it was published, and a directly pinned entry is read here. The
+ * Json composition entry is not among them — a pure component offers no
+ * spelling for an owner to declare, and its identity is core's own.
+ *
+ * Kept per definition *object*, not per entry, because a name admitted for two
+ * spellings is one component seen from two sides: ` ` and
+ * `… ` are one definition, and two copies of it would be one name
+ * with two implementations.
+ *
+ * The implementation crosses by reference, because a function is not copyable
+ * and is the one thing a fragment must invoke as itself.
+ */
+function admitOffered(
+ name: string,
+ definition: FunctionComponentDefinition,
+ retained: Map,
+): AdmittedDefinition {
+ const held = retained.get(definition);
+ if (held !== undefined) {
+ return held;
+ }
+ const refuse = (reason: string): never => {
+ throw new GeneratedXmdError(
+ `a generated-XMD allowlist admitted <${name}> whose declaration ${reason}.`,
+ );
+ };
+ // One read of every member, into this run's own object.
+ const copy: FunctionComponentDefinition = { ...definition };
+ const badForms = formsRefusal(copy.forms);
+ if (badForms !== undefined) {
+ refuse(badForms);
+ }
+ const offered =
+ copy.generated === undefined
+ ? undefined
+ : admitGeneratedForms(copy.generated, acceptedForms(copy.forms));
+ if (offered !== undefined && !offered.ok) {
+ refuse(offered.error.message);
+ }
+ const generated = offered === undefined || !offered.ok ? undefined : offered.value;
+ const admitted: AdmittedDefinition = Object.freeze({
+ definition: Object.freeze({
+ ...copy,
+ ...(generated === undefined ? {} : { generated }),
+ }),
+ generated,
+ });
+ retained.set(definition, admitted);
+ return admitted;
+}
+
+/**
+ * The spellings one observation entry admits.
+ *
+ * Every spelling its owner offers as a read, and the host may narrow that to
+ * the self-closing one. An entry whose owner offers no reading spelling at all
+ * is a table admitting a component to observe that does not observe, so it
+ * refuses here rather than at the first element that writes the name.
+ */
+function observedForms(
+ observation: GeneratedObservation,
+ admitted: AdmittedDefinition,
+): readonly AuthoredForm[] {
+ const offered = AUTHORED_FORMS.filter((form) => admitted.generated?.[form]?.effect === "read");
+ const forms =
+ observation.selfClosing === true ? offered.filter((form) => form === "self-closing") : offered;
+ if (forms.length === 0) {
+ throw new GeneratedXmdError(
+ `a generated-XMD allowlist admitted <${observation.name}> to observe, and the declaration ` +
+ "that owns that implementation offers no reading spelling of it.",
+ );
+ }
+ return forms;
+}
+
+/**
+ * The identity each admitted spelling of one entry runs under.
+ *
+ * Read off the reading this run kept, because the declaration that owns the
+ * implementation is what states it and a second reading of the host's object
+ * would be a different statement. Two refusals, both about the host table
+ * rather than about the fragment: a spelling its owner offers no
+ * generated use of has no identity for a continuation to be compared against,
+ * and a spelling offered as a read cannot be admitted in the write table —
+ * what a spelling does belongs to the component, so a table placing it
+ * elsewhere would grant what the implementation does not do.
+ */
+function offeredIdentities(
+ name: string,
+ admitted: AdmittedDefinition,
+ forms: readonly AuthoredForm[],
+ effect: GeneratedEffectClass,
+): ReadonlyMap {
+ const identities = new Map();
+ for (const form of forms) {
+ const offered = admitted.generated?.[form];
+ if (offered === undefined) {
+ throw new GeneratedXmdError(
+ `a generated-XMD allowlist admitted <${name}> in its ${form} form, and the declaration ` +
+ "that owns that implementation offers no generated use of it.",
+ );
+ }
+ if (offered.effect !== effect) {
+ throw new GeneratedXmdError(
+ `a generated-XMD allowlist admitted <${name}> in its ${form} form as a ${effect}, and ` +
+ `its owner offers that form as a ${offered.effect}.`,
+ );
+ }
+ identities.set(form, offered.identity);
+ }
+ return identities;
+}
+
/**
* The pinned identities this fragment may name, keyed by the name it writes.
*
* A name may hold more than one entry, because a component whose two spellings
- * do different things has two identities. What it may not hold is two entries
- * that could both answer for one element, or two definitions: an import is
- * asked for by name, so a name resolves to exactly one implementation and the
- * form chooses only which identity that implementation ran as. Either
- * ambiguity is a malformed host table, refused before anything is retained
- * rather than settled by whichever entry was listed first.
+ * do different things offers each of them its own identity. What it may not
+ * hold is two entries that could both answer for one element, or two
+ * definitions: an import is asked for by name, so a name resolves to exactly
+ * one implementation and the form chooses only which identity that
+ * implementation ran as. Either ambiguity is a malformed host table, refused
+ * before anything is retained rather than settled by whichever entry was listed
+ * first.
*/
function admitted(entries: readonly Entry[]): Map {
const table = new Map();
@@ -1276,8 +1289,8 @@ function sameStrings(one: readonly string[], other: readonly string[]): boolean
* about the candidate — it should fail as itself, before anything is appended,
* rather than be serialized into the journal as a refusal of the fragment.
*/
-function* normalizedCeilings(entries: readonly Entry[]): Operation> {
- const ceilings = new Map();
+function* normalizedCeilings(entries: readonly Entry[]): Operation {
+ const ceilings = new Map();
for (const entry of entries) {
if (entry.requests === undefined) {
continue;
@@ -1286,33 +1299,47 @@ function* normalizedCeilings(entries: readonly Entry[]): Operation;
+
/** The ceilings this run states, in the order the host stated them. */
function currentPolicy(
request: GeneratedXmdRequest,
allow: readonly GeneratedEffectClass[],
entries: readonly Entry[],
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
): Policy {
const requests: FetchRequest[] = [];
const allowed: RetainedEntry[] = [];
for (const entry of entries) {
- allowed.push({
- name: entry.name,
- identity: entry.identity,
- forms: entry.forms,
- // Carried on the current policy so a version-1 record can be reconciled
- // against it, and dropped by `policyRecord` so nothing this run writes
- // holds it.
- ...(entry.legacy === undefined ? {} : { legacy: entry.legacy }),
- });
- requests.push(...(ceilings.get(identityKey(entry.identity)) ?? []));
+ // One row per admitted spelling, in canonical form order within the entry,
+ // so the retained table says which spelling of which name was admitted as
+ // what rather than leaving the effect to be inferred from a position.
+ for (const form of AUTHORED_FORMS.filter((candidate) => entry.forms.includes(candidate))) {
+ const identity = entry.identities.get(form);
+ if (identity === undefined) {
+ throw new GeneratedXmdError(
+ `a generated-XMD allowlist admitted <${entry.name}> in its ${form} form with no ` +
+ "identity for it.",
+ );
+ }
+ allowed.push({
+ name: entry.name,
+ form,
+ identity,
+ kind: entry.kind,
+ effect: entry.effect,
+ });
+ }
+ requests.push(...(ceilings.get(entry) ?? []));
}
const workspace = workspaceBasis(request);
return {
@@ -1320,7 +1347,6 @@ function currentPolicy(
...(workspace === undefined ? {} : { workspace }),
allowed,
requests,
- composition: entries.filter((entry) => entry.effect === "composition").length,
};
}
@@ -1364,14 +1390,14 @@ function workspaceBasis(
}
/**
- * The policy as journal data, in the closed version-2 shape.
- *
- * Version 1 is the untagged #369 record, whose `roots` and `selectedRoot` sat
- * at the top level and were mandatory. Version 2 tags itself and carries the
- * Workspace basis as one optional member, because an ordinary host has none —
- * and telling the two apart matters: a reader that treated a missing basis as
- * an empty one would compare a Workspace-less admission equal to a Workspace
- * admission that had lost every root.
+ * The policy as journal data, in the one closed shape this build has.
+ *
+ * It tags itself, which is what lets a record written by any other build be
+ * recognized as a shape this one does not have rather than guessed at, and it
+ * carries the Workspace basis as one optional member because an ordinary host
+ * has none — a reader that treated a missing basis as an empty one would
+ * compare a Workspace-less admission equal to a Workspace admission that had
+ * lost every root.
*/
function policyRecord(policy: Policy): JsonObject {
return {
@@ -1387,38 +1413,15 @@ function policyRecord(policy: Policy): JsonObject {
}),
allowed: policy.allowed.map((entry) => ({
name: entry.name,
- identity: retainedIdentityRecord(entry.identity),
- forms: [...entry.forms],
+ form: entry.form,
+ identity: entry.identity,
+ kind: entry.kind,
+ effect: entry.effect,
})),
requests: policy.requests.map(requestRecord),
};
}
-/**
- * One identity as journal data.
- *
- * Total over both retained shapes, so a policy read back is written back as the
- * shape it was read as. What this run *states* is always the version-2 record —
- * the version-1 arm exists because a record read from an older journal is still
- * a policy, not because this build writes one.
- *
- * Distinct from the one-line spelling a *diagnostic* uses
- * (`components/component-resolution.ts`): that one is for a reader, and this one is
- * what a continuation compares, so they are named apart rather than allowed to
- * drift into each other.
- */
-function retainedIdentityRecord(identity: RetainedIdentity): Json {
- if ("legacy" in identity) {
- return identity.legacy;
- }
- return {
- kind: identity.kind,
- origin: identity.origin,
- key: identity.key,
- revision: identity.revision,
- };
-}
-
/**
* The policy a record holds, parsed rather than trusted.
*
@@ -1449,65 +1452,39 @@ function exactly(
}
/**
- * The policy a record holds, in whichever closed shape its version defines.
+ * The policy a record holds, in the one closed shape this build has.
*
- * The two versions are exact and disjoint. Version 1 is the untagged #369
- * record: no version member, the Workspace basis as two mandatory top-level
- * ones. Version 2 tags itself, carries the basis as one optional member, and
- * has no legacy root fields at all — so a record mixing the two, or tagging
- * itself with a version this build does not have, is refused rather than read
- * as whichever it most resembles.
+ * Exact: the version tag has to be this build's, the members have to be these
+ * members, and nothing is read partially. A record from any other build — the
+ * untagged #369 one, a tagged one holding structural identity objects, one
+ * carrying an extra member — describes a shape whose rules this build does not
+ * know, so it is refused rather than read as whichever shape it most resembles.
*/
function readPolicy(value: Json): Policy | undefined {
if (!isJsonObject(value)) {
return undefined;
}
- const tagged = Object.hasOwn(value, "version");
- if (tagged) {
- if (value.version !== RECORD_VERSION) {
- return undefined;
- }
- if (!exactly(value, ["version", "allow", "allowed", "requests"], ["workspace"])) {
- return undefined;
- }
- const workspace = Object.hasOwn(value, "workspace")
- ? readWorkspace(value.workspace)
- : undefined;
- if (workspace === MALFORMED) {
- return undefined;
- }
- return readPolicyTerms(value, workspace, 2);
+ if (value.version !== RECORD_VERSION) {
+ return undefined;
}
- if (!exactly(value, ["allow", "roots", "selectedRoot", "allowed", "requests"])) {
+ if (!exactly(value, ["version", "allow", "allowed", "requests"], ["workspace"])) {
return undefined;
}
- // A version-1 record always evaluated against a Workspace, so its basis is
- // mandatory and reads through the same validation a version-2 basis does.
- const workspace = readWorkspace({ roots: value.roots, selectedRoot: value.selectedRoot });
- if (workspace === MALFORMED || workspace === undefined) {
+ const workspace = Object.hasOwn(value, "workspace") ? readWorkspace(value.workspace) : undefined;
+ if (workspace === MALFORMED) {
return undefined;
}
- return readPolicyTerms(value, workspace, 1);
-}
-
-/** The terms both versions share, read closed. */
-function readPolicyTerms(
- value: JsonObject,
- workspace: { roots: readonly string[]; selectedRoot: string } | undefined,
- version: 1 | 2,
-): Policy | undefined {
const classes = readClasses(value.allow);
- const identities = readAllowed(value.allowed, version);
+ const allowed = readAllowed(value.allowed);
const requests = readRequests(value.requests);
- if (classes === undefined || identities === undefined || requests === undefined) {
+ if (classes === undefined || allowed === undefined || requests === undefined) {
return undefined;
}
return {
allow: classes,
...(workspace === undefined ? {} : { workspace }),
- allowed: identities,
+ allowed,
requests,
- recordVersion: version,
};
}
@@ -1613,76 +1590,55 @@ function readClasses(value: Json): GeneratedEffectClass[] | undefined {
: undefined;
}
-/** The forms one entry is admitted for: a non-empty set, canonically ordered. */
-function readForms(value: Json): AuthoredForm[] | undefined {
- if (!Array.isArray(value) || value.length === 0) {
- return undefined;
- }
- const forms: AuthoredForm[] = [];
- for (const form of value) {
- const parsed = AUTHORED_FORMS.find((known) => known === form);
- if (parsed === undefined || forms.includes(parsed)) {
- return undefined;
- }
- forms.push(parsed);
- }
- return sameStrings(
- forms,
- AUTHORED_FORMS.filter((known) => forms.includes(known)),
- )
- ? forms
- : undefined;
-}
-
/**
- * One retained identity, in whichever shape its version defines.
+ * One retained identity: the URL string the spelling ran under.
*
- * The two are exact and disjoint. Version 1 is a string and nothing else.
- * Version 2 is the closed tagged record and nothing else: a string, a missing
- * member, an extra member, an unknown kind and a non-string member are each
- * refused rather than read as whichever shape the value most resembles.
+ * A string in the accepted namespace and nothing else. An object — the
+ * structural identity an earlier build wrote — a relative reference, a URL
+ * somewhere else and an empty value are each refused rather than read as
+ * whichever shape the value most resembles.
*/
-function readIdentity(value: Json | undefined, version: 1 | 2): RetainedIdentity | undefined {
- if (version === 1) {
- return typeof value === "string" ? { legacy: value } : undefined;
- }
- if (!isJsonObject(value) || !exactly(value, ["kind", "origin", "key", "revision"])) {
- return undefined;
- }
- const { origin, key, revision } = value;
- const kind = IDENTITY_KINDS.find((known) => known === value.kind);
- if (
- kind === undefined ||
- typeof origin !== "string" ||
- typeof key !== "string" ||
- typeof revision !== "string"
- ) {
+function readIdentity(value: Json | undefined): string | undefined {
+ if (typeof value !== "string" || !isGeneratedIdentity(value)) {
return undefined;
}
- return { kind, origin, key, revision };
+ return value;
}
-function readAllowed(value: Json, version: 1 | 2): RetainedEntry[] | undefined {
+/** The effect table one row was admitted from. */
+function readEffect(value: Json | undefined): RetainedEntry["effect"] | undefined {
+ return value === "composition" ? "composition" : EFFECT_CLASSES.find((known) => known === value);
+}
+
+function readAllowed(value: Json): RetainedEntry[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
- const identities: RetainedEntry[] = [];
+ const allowed: RetainedEntry[] = [];
for (const entry of value) {
- if (!isJsonObject(entry) || !exactly(entry, ["name", "identity", "forms"])) {
+ if (!isJsonObject(entry) || !exactly(entry, ["name", "form", "identity", "kind", "effect"])) {
return undefined;
}
const { name } = entry;
- const identity = readIdentity(entry.identity, version);
- const forms = readForms(entry.forms);
- if (typeof name !== "string" || identity === undefined || forms === undefined) {
+ const identity = readIdentity(entry.identity);
+ const form = AUTHORED_FORMS.find((known) => known === entry.form);
+ const kind = IMPLEMENTATIONS.find((known) => known === entry.kind);
+ const effect = readEffect(entry.effect);
+ if (
+ typeof name !== "string" ||
+ identity === undefined ||
+ form === undefined ||
+ kind === undefined ||
+ effect === undefined
+ ) {
return undefined;
}
- identities.push({ name, identity, forms });
+ allowed.push({ name, form, identity, kind, effect });
}
- return identities;
+ return allowed;
}
-function readNamed(value: Json, version: 1 | 2): RetainedInvocation[] | undefined {
+function readNamed(value: Json): RetainedInvocation[] | undefined {
if (!Array.isArray(value)) {
return undefined;
}
@@ -1692,7 +1648,7 @@ function readNamed(value: Json, version: 1 | 2): RetainedInvocation[] | undefine
return undefined;
}
const { name } = entry;
- const identity = readIdentity(entry.identity, version);
+ const identity = readIdentity(entry.identity);
const form = AUTHORED_FORMS.find((known) => known === entry.form);
if (typeof name !== "string" || identity === undefined || form === undefined) {
return undefined;
@@ -1719,11 +1675,12 @@ function readNamed(value: Json, version: 1 | 2): RetainedInvocation[] | undefine
* refused before any generated work.
*
* Every other term is a host-stated ceiling and compares whole and exactly,
- * in order: a widened class selection, one identity behind a name replaced, a
- * form added, or one request added to the allowed set each make this false.
- * Widening is the case that matters most — a ceiling that still contains the
- * original request is precisely the one that comparing the *fragment* against
- * the *current* policy would wave through.
+ * in order: a widened class selection, one identity behind a spelling replaced,
+ * a form added, a spelling moved into the other effect table, or one request
+ * added to the allowed set each make this false. Widening is the case that
+ * matters most — a ceiling that still contains the original request is
+ * precisely the one that comparing the *fragment* against the *current* policy
+ * would wave through.
*/
function policyHolds(retained: Policy, current: Policy): boolean {
if (!workspaceHolds(retained.workspace, current.workspace)) {
@@ -1732,27 +1689,12 @@ function policyHolds(retained: Policy, current: Policy): boolean {
if (!sameStrings(retained.allow, current.allow)) {
return false;
}
- // A version-1 record predates the trusted composition table, so it is
- // reconciled against the effect entries alone. Composition grants nothing —
- // its entries carry no effect operation — so a continuation held to the
- // remainder is held to exactly the permissions it was admitted under.
- const stated =
- retained.recordVersion === 1
- ? current.allowed.slice(current.composition ?? 0)
- : current.allowed;
- if (retained.allowed.length !== stated.length) {
+ if (retained.allowed.length !== current.allowed.length) {
return false;
}
const replaced = retained.allowed.some((entry, index) => {
- const here = stated[index];
- return (
- here === undefined ||
- here.name !== entry.name ||
- // The retained side is what was granted; the current side is the entry
- // asking to answer for it, and only that side states version-1 aliases.
- !sameIdentity(entry.identity, here) ||
- !sameStrings(here.forms, entry.forms)
- );
+ const here = current.allowed[index];
+ return here === undefined || !sameRow(entry, here);
});
if (replaced) {
return false;
@@ -1815,7 +1757,7 @@ function* preflight(
id: string,
source: string,
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
): Operation {
// Read once, here, so a contextual default that refuses fails as itself
// rather than as a statement about the fragment. Every candidate request
@@ -1869,7 +1811,7 @@ function* admitCandidateRequest(props: Record): Operation,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
named: Planned[],
scope: Set,
lexical: Lexical = ROOT_LEXICAL,
@@ -1932,13 +1874,20 @@ function* walk(
throw new Refusal(form === "paired" ? "content" : "form");
}
if (entry.requests !== undefined) {
- const ceiling = ceilings.get(identityKey(entry.identity)) ?? [];
+ const ceiling = ceilings.get(entry) ?? [];
const candidate = yield* admitCandidateRequest(segment.props);
if (!ceiling.some((allowed) => sameRequest(allowed, candidate))) {
throw new Refusal("request");
}
}
- named.push({ name: entry.name, identity: entry.identity, form, entry });
+ // The identity this spelling runs under, from the entry's own
+ // definition. The form was decided from the scan just above, which is
+ // what separates ` ` from `… `.
+ const identity = entry.identities.get(form);
+ if (identity === undefined) {
+ throw new Refusal("form");
+ }
+ named.push({ name: entry.name, identity, form, entry });
// The children see this element's bindings but not its own `as`, and
// nothing they bind escapes back to its siblings. They are a component
// body rather than a transparent region, so the lexical facts reset:
@@ -2042,7 +1991,7 @@ const NO_GENERATED_CONTEXT: ReadonlySet = new Set(["Content", "Output",
function* structural(
segment: ComponentElement,
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
named: Planned[],
scope: Set,
lexical: Lexical,
@@ -2224,7 +2173,7 @@ interface Alternative {
function* alternatives(
branches: readonly Alternative[],
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
named: Planned[],
scope: Set,
lexical: Lexical,
@@ -2261,7 +2210,7 @@ function* alternatives(
function* transparent(
segments: readonly Segment[],
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
named: Planned[],
scope: Set,
lexical: Lexical,
@@ -2288,15 +2237,16 @@ function refuseStructure(
const GENERATED_XMD = "generated_xmd";
/**
- * The record shape new executions write.
- *
- * Version 1 is the untagged #369 record and stays readable: a run suspended
- * before this build resumes under exactly the ceilings it was admitted with.
- * Version 2 tags itself, which is what lets an untagged record be recognized as
- * the older shape rather than guessed at, and carries the Workspace basis as
- * one optional member because an ordinary host has none.
+ * The one record shape this build writes and reads.
+ *
+ * A fixed marker rather than a version to negotiate: every record carries it,
+ * and a record carrying anything else — the untagged #369 shape, or the tagged
+ * one whose identities were structural records of an origin, a key and a
+ * revision — is a shape whose rules this build does not know. Such a record is
+ * refused as unreadable history, before the admission it would have restored
+ * performs anything, rather than being migrated or read in part.
*/
-const RECORD_VERSION = 2;
+const RECORD_VERSION = 3;
/**
* What the durable admission records for this source.
@@ -2310,7 +2260,7 @@ function* admitSource(
id: string,
source: string,
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
policy: Policy,
): Operation {
try {
@@ -2321,7 +2271,7 @@ function* admitSource(
source,
named: named.map((entry) => ({
name: entry.name,
- identity: retainedIdentityRecord(entry.identity),
+ identity: entry.identity,
form: entry.form,
})),
policy: policyRecord(policy),
@@ -2348,7 +2298,7 @@ function* persistAdmission(
id: string,
source: string,
table: ReadonlyMap,
- ceilings: ReadonlyMap,
+ ceilings: Ceilings,
policy: Policy,
position: Readonly | undefined,
): Operation {
@@ -2375,17 +2325,15 @@ function readAdmission(value: Json): RetainedAdmission | undefined {
if (!isJsonObject(value)) {
return undefined;
}
- // Tagged at the result level as well as inside the policy, and the two must
- // agree: a record whose result claims one version and whose policy claims
- // another describes no shape this build has.
- const tagged = Object.hasOwn(value, "version");
- if (tagged && value.version !== RECORD_VERSION) {
+ // Marked at the result level as well as inside the policy, and both have to
+ // be this build's: a record missing either marker, or carrying another
+ // build's, describes no shape this one has.
+ if (value.version !== RECORD_VERSION) {
return undefined;
}
- const version = tagged ? ["version"] : [];
const { decision } = value;
if (decision === "refused") {
- if (!exactly(value, [...version, "decision", "construct"])) {
+ if (!exactly(value, ["version", "decision", "construct"])) {
return undefined;
}
const { construct } = value;
@@ -2396,24 +2344,18 @@ function readAdmission(value: Json): RetainedAdmission | undefined {
if (decision !== "admitted") {
return undefined;
}
- if (!exactly(value, [...version, "decision", "source", "named", "policy"])) {
+ if (!exactly(value, ["version", "decision", "source", "named", "policy"])) {
return undefined;
}
const { source } = value;
if (typeof source !== "string") {
return undefined;
}
- const invocations = readNamed(value.named, tagged ? 2 : 1);
+ const invocations = readNamed(value.named);
const retained = readPolicy(value.policy);
if (invocations === undefined || retained === undefined) {
return undefined;
}
- // The policy's own version has to be the one the result claimed. A tagged
- // result holding an untagged policy, or the reverse, is two shapes at once.
- const policyTagged = isJsonObject(value.policy) && Object.hasOwn(value.policy, "version");
- if (policyTagged !== tagged) {
- return undefined;
- }
return { decision, source, named: invocations, policy: retained };
}
diff --git a/packages/core/src/invocation-forms.ts b/packages/core/src/invocation-forms.ts
new file mode 100644
index 000000000..eef5b1773
--- /dev/null
+++ b/packages/core/src/invocation-forms.ts
@@ -0,0 +1,77 @@
+/**
+ * How an authored forms declaration may be written (spec §5.3).
+ *
+ * One contract, in a module of its own, because two kinds of declaration are
+ * held to it and neither may drift: what a registration or a factory says it
+ * accepts, and which of those spellings it offers for generated use
+ * (`components/generated-metadata.ts`). The second has to read the first, and
+ * reading it through the module that builds dispatchers would make the metadata
+ * validator depend on the component graph it is validating against.
+ */
+
+import type { InvocationForm } from "./invocation-identity.ts";
+
+/**
+ * The only ways a forms declaration may be written.
+ *
+ * A closed list rather than a set membership test, because the *order* is part
+ * of the declaration: one canonical spelling per meaning means two entries can be
+ * compared without normalizing, and a reader never has to wonder whether
+ * `["paired", "self-closing"]` said something different.
+ */
+const CANONICAL_FORMS: readonly (readonly InvocationForm[])[] = [
+ ["self-closing"],
+ ["paired"],
+ ["self-closing", "paired"],
+];
+
+/**
+ * Why `forms` is not a canonical declaration, or `undefined` when it is.
+ *
+ * Omission is canonical and means both forms, which is what every declaration
+ * meant before forms could be written. Anything present is held to the list
+ * above, so an empty array, a reversed pair, a repeated member and a form no
+ * invocation has are each refused where the declaration is made rather than
+ * reaching a document as documentation nothing can act on.
+ *
+ * A reason rather than a thrown error, because the callers raise different
+ * kinds of failure and each owns its own sentence. `unknown` rather than the
+ * declared type, because a host crosses this boundary from JavaScript.
+ */
+export function formsRefusal(forms: unknown): string | undefined {
+ if (forms === undefined) {
+ return undefined;
+ }
+ const canonical =
+ Array.isArray(forms) &&
+ CANONICAL_FORMS.some(
+ (accepted) =>
+ accepted.length === forms.length && accepted.every((form, index) => form === forms[index]),
+ );
+ if (canonical) {
+ return undefined;
+ }
+ return (
+ `declares forms ${JSON.stringify(forms)}: the accepted declarations are ` +
+ `["self-closing"], ["paired"] and ["self-closing", "paired"], or none at all for both. ` +
+ "A form is written once, and a pair is written self-closing first"
+ );
+}
+
+/**
+ * The forms a declaration accepts, having passed `formsRefusal()`.
+ *
+ * `undefined` for genuine omission, which means both — the one reading that
+ * widens anything, and the reason a malformed value is refused above rather
+ * than treated as absent here.
+ */
+export function acceptedForms(forms: unknown): readonly InvocationForm[] | undefined {
+ if (!Array.isArray(forms)) {
+ return undefined;
+ }
+ const accepted = CANONICAL_FORMS.find(
+ (candidate) =>
+ candidate.length === forms.length && candidate.every((form, index) => form === forms[index]),
+ );
+ return accepted === undefined ? undefined : [...accepted];
+}
diff --git a/packages/core/src/invocation-identity.ts b/packages/core/src/invocation-identity.ts
index 1d8507dd1..f41f055ea 100644
--- a/packages/core/src/invocation-identity.ts
+++ b/packages/core/src/invocation-identity.ts
@@ -52,6 +52,8 @@ import { useScope } from "effection";
import type { Operation, Scope } from "effection";
import { printErrors, printsErrors } from "./component-failures.ts";
import { documentationOf } from "./components/documentation.ts";
+import { admitGeneratedForms } from "./components/generated-metadata.ts";
+import type { GeneratedForms } from "./components/generated-metadata.ts";
import type { ComponentDocumentation } from "./components/documentation.ts";
import type { SyntaxReference } from "./syntax-reference.ts";
import type { EvaluationProfile } from "./evaluation-profile.ts";
@@ -147,6 +149,8 @@ export interface IdentityComponent extends ComponentDocumentation {
readonly captures?: readonly string[];
/** The authored forms this component accepts. Omitted means both. */
readonly forms?: readonly InvocationForm[];
+ /** The generated use each authored form is offered for, when it offers any. */
+ readonly generated?: GeneratedForms;
readonly origin: string;
factory(claim: IdentityClaimant): FunctionComponent;
}
@@ -381,6 +385,8 @@ export interface ProtectedDeclaration {
readonly returns?: ReturnsSchema;
readonly captures?: readonly string[];
readonly forms?: readonly InvocationForm[];
+ /** The generated use each authored form is offered for, when it offers any. */
+ readonly generated?: GeneratedForms;
readonly origin: string;
build(claim: IdentityClaimant): ProtectedBody;
}
@@ -629,53 +635,6 @@ export function isFormDispatcher(fn: unknown): boolean {
* answer, no method on the object handed in, and nothing a handler composed
* takes part in choosing it.
*/
-/**
- * The only ways a forms declaration may be written.
- *
- * A closed list rather than a set membership test, because the *order* is part
- * of the declaration: one canonical spelling per meaning means two entries can be
- * compared without normalizing, and a reader never has to wonder whether
- * `["paired", "self-closing"]` said something different.
- */
-const CANONICAL_FORMS: readonly (readonly InvocationForm[])[] = [
- ["self-closing"],
- ["paired"],
- ["self-closing", "paired"],
-];
-
-/**
- * Why `forms` is not a canonical declaration, or `undefined` when it is.
- *
- * Omission is canonical and means both forms, which is what every declaration
- * meant before forms could be written. Anything present is held to the list
- * above, so an empty array, a reversed pair, a repeated member and a form no
- * invocation has are each refused where the declaration is made rather than
- * reaching a document as documentation nothing can act on.
- *
- * A reason rather than a thrown error, because the two callers raise different
- * kinds of failure and each owns its own sentence. `unknown` rather than the
- * declared type, because a host crosses this boundary from JavaScript.
- */
-export function formsRefusal(forms: unknown): string | undefined {
- if (forms === undefined) {
- return undefined;
- }
- const canonical =
- Array.isArray(forms) &&
- CANONICAL_FORMS.some(
- (accepted) =>
- accepted.length === forms.length && accepted.every((form, index) => form === forms[index]),
- );
- if (canonical) {
- return undefined;
- }
- return (
- `declares forms ${JSON.stringify(forms)}: the accepted declarations are ` +
- `["self-closing"], ["paired"] and ["self-closing", "paired"], or none at all for both. ` +
- "A form is written once, and a pair is written self-closing first"
- );
-}
-
/**
* The forms a declaration accepts, in canonical order.
*
@@ -1027,6 +986,7 @@ export interface IdentityRegistration extends ComponentDocumentation {
readonly returns?: ReturnsSchema;
readonly captures?: readonly string[];
readonly forms?: readonly InvocationForm[];
+ readonly generated?: GeneratedForms;
readonly fn: FunctionComponent;
}
@@ -1061,6 +1021,37 @@ export interface IdentityInstallation {
activate(): void;
}
+/**
+ * One declaration's generated metadata, read once and validated.
+ *
+ * A declared identity component crosses the installation boundary from a host,
+ * and the private and protected arms never pass registration — so this is where
+ * their metadata is read, and the value it answers with is the one the built
+ * definition retains. Validating the host's object and copying it again would
+ * be two readings of a value the host controls.
+ *
+ * An execution captures the components it was declared before anything it
+ * installs runs, so it reads the host's metadata through this too (`execute.ts`)
+ * and hands the captured value back here. Reading core's own frozen copy a
+ * second time says what the first reading said.
+ */
+export function admittedGenerated(component: {
+ readonly name: string;
+ readonly generated?: GeneratedForms;
+ readonly forms?: readonly InvocationForm[];
+}): GeneratedForms | undefined {
+ if (component.generated === undefined) {
+ return undefined;
+ }
+ const admitted = admitGeneratedForms(component.generated, component.forms);
+ if (!admitted.ok) {
+ throw new ComponentInvocationError(
+ `the identity component "${component.name}" ${admitted.error.message}`,
+ );
+ }
+ return admitted.value;
+}
+
/**
* Mint this execution's domains and build the implementations from them.
*
@@ -1105,6 +1096,7 @@ export function installIdentities(
guarded.set(definition.name, definition);
}
for (const component of protectedComponents) {
+ const admitted = admittedGenerated(component);
const domain = mintDomain(component.name);
minted.set(component.name, domain);
const implementation = componentRouting.implementation(
@@ -1131,11 +1123,13 @@ export function installIdentities(
...(component.returns === undefined ? {} : { returns: component.returns }),
...(component.captures === undefined ? {} : { captures: component.captures }),
...(component.forms === undefined ? {} : { forms: component.forms }),
+ ...(admitted === undefined ? {} : { generated: admitted }),
...documentationOf(component),
fn: implementation,
});
}
for (const component of privateComponents) {
+ const admitted = admittedGenerated(component);
const domain = mintDomain(component.name);
minted.set(component.name, domain);
const implementation = component.factory(domain.claim);
@@ -1151,11 +1145,13 @@ export function installIdentities(
...(component.returns === undefined ? {} : { returns: component.returns }),
...(component.captures === undefined ? {} : { captures: component.captures }),
...(component.forms === undefined ? {} : { forms: component.forms }),
+ ...(admitted === undefined ? {} : { generated: admitted }),
...documentationOf(component),
fn: implementation,
});
}
for (const component of components) {
+ const admitted = admittedGenerated(component);
const domain = mintDomain(component.name);
minted.set(component.name, domain);
// Built here, and held by identity: this exact function is what canonical
@@ -1169,6 +1165,7 @@ export function installIdentities(
...(component.returns === undefined ? {} : { returns: component.returns }),
...(component.captures === undefined ? {} : { captures: component.captures }),
...(component.forms === undefined ? {} : { forms: component.forms }),
+ ...(admitted === undefined ? {} : { generated: admitted }),
...documentationOf(component),
fn: implementation,
});
diff --git a/packages/core/src/syntax-admitted.ts b/packages/core/src/syntax-admitted.ts
index 999d8f75b..e7ab1cc9e 100644
--- a/packages/core/src/syntax-admitted.ts
+++ b/packages/core/src/syntax-admitted.ts
@@ -55,10 +55,10 @@ const OWNED_CAPABILITIES: Readonly>>
* Core's documentation origin for this entry, when the entry is core's own
* component however the host admitted it.
*
- * Documentation provenance only. The identity stays what the host stated,
- * because that is what a continuation compares and what the grant was recorded
- * as — a host's directory entry is still the host's grant even though the
- * component behind the name is core's.
+ * Documentation provenance only. It changes nothing a continuation compares:
+ * what the grant was recorded as is the identity the owning declaration offers
+ * the admitted spelling under, and that is core's wherever core supplies the
+ * component behind the name.
*/
function ownedOrigin(entry: CapturedEntry): string | undefined {
if (entry.protectedOrigin !== undefined) {
@@ -71,12 +71,12 @@ function ownedOrigin(entry: CapturedEntry): string | undefined {
/**
* The symbols an evaluation admitted, in the order the tables state them.
*
- * The identity's origin is what each entry reports it came from, because that
- * is what the host stated and what a continuation is compared against. Reported
- * as a registration for ordinary entries. An exact protected answer keeps its
- * canonical documentation origin even when a different provider delegates it,
- * and so does a capability performing an operation canonical core owns a
- * component for, under that component's name.
+ * Each entry reports the origin its implementation came from: core's own where
+ * core supplies the body, and the publishing provider installation's where the
+ * import chain answered. Reported as a registration for ordinary entries. An
+ * exact protected answer keeps its canonical documentation origin even when a
+ * different provider delegates it, and so does a capability performing an
+ * operation canonical core owns a component for, under that component's name.
*/
export function admittedSymbols(entries: readonly CapturedEntry[]): SyntaxSymbols {
return {
@@ -97,7 +97,7 @@ function describe(entry: CapturedEntry): CompleteComponentSyntaxEntry {
name: entry.name,
origin:
owned === undefined
- ? { kind: "registered", origin: entry.identity.origin, reserved: false }
+ ? { kind: "registered", origin: entry.origin, reserved: false }
: { kind: "protected", origin: owned },
sourceKind: owned === undefined ? "registered" : "protected",
inspectability: "complete",
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 66e4d45e5..f73c033d1 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -10,6 +10,7 @@ import type { Json as DurableJson } from "@executablemd/durable-streams";
import type { TestHarnessComponentDefinition } from "./test-harness.ts";
import type { ComponentInvocation, InvocationForm } from "./invocation-identity.ts";
import type { ProtectedComponent } from "./components/protected.ts";
+import type { GeneratedForms } from "./components/generated-metadata.ts";
export type Json = DurableJson;
@@ -287,6 +288,17 @@ export interface FunctionComponentDefinition {
* declaration spelled where inspection can read it, so the two cannot drift.
*/
forms?: readonly InvocationForm[];
+ /**
+ * The generated use this component's forms are offered for, per form.
+ *
+ * Absent offers none, which is what an ordinary component offers: a
+ * generated fragment names a component only where the declaration that owns
+ * the implementation said which identity and effect that spelling runs as
+ * (`components/generated-metadata.ts`). Carrying it is not authority — a
+ * table still admits the name, and canonical resolution still decides which
+ * definition answers for it.
+ */
+ generated?: GeneratedForms;
/** Prose about the component, for a reader. Never read by execution. */
description?: string;
as?: string;
diff --git a/packages/core/tests/all.test.ts b/packages/core/tests/all.test.ts
index 5086a33c9..4d456d970 100644
--- a/packages/core/tests/all.test.ts
+++ b/packages/core/tests/all.test.ts
@@ -38,7 +38,6 @@ import type {
ExecutionInstallation,
GeneratedObservation,
GeneratedXmdRequest,
- RetainedFragmentIdentity,
} from "../host.ts";
import { forEach } from "@effectionx/stream-helpers";
import { collect } from "../src/collect.ts";
@@ -1239,14 +1238,14 @@ describe("Tier ALL — PA8: and cannot cross a spawn", () => {
const GENERATED_ROOT = "workflows/agent.md";
const GENERATED_SOURCE = "The host ran a generated fragment.\n";
-function hostIdentity(origin: string, key: string): RetainedFragmentIdentity {
- return { kind: "component-answer", origin, key, revision: "1" };
-}
-
const GENERATED_PROBE: FunctionComponentDefinition = {
kind: "function",
name: "Probe",
props: NO_PROPS,
+ forms: ["self-closing"],
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/probe/read", effect: "read" },
+ },
// deno-lint-ignore require-yield
*fn() {
return "probed";
@@ -1254,7 +1253,7 @@ const GENERATED_PROBE: FunctionComponentDefinition = {
};
function generatedProbe(): GeneratedObservation {
- return pinnedComponent("Probe", hostIdentity("test://probe", "Probe"), GENERATED_PROBE);
+ return pinnedComponent("Probe", GENERATED_PROBE, "component-answer");
}
function generatedRequest(source: string): GeneratedXmdRequest {
diff --git a/packages/core/tests/answer-identity.test.ts b/packages/core/tests/answer-identity.test.ts
index 38f374b9f..9e29f2e5e 100644
--- a/packages/core/tests/answer-identity.test.ts
+++ b/packages/core/tests/answer-identity.test.ts
@@ -1,27 +1,26 @@
/**
- * Tier CIV — what a stated identity is bound to, and when it stops meaning
+ * Tier CIV — what a published answer is bound to, and when it stops meaning
* anything.
*
* A generated fragment's admission is a decision about which implementation may
* run. A function carries no identity a run can retain — comparing one compares
- * how somebody wrote their code — so a provider states one. Everything here is
- * about the ways a stated identity could be weaker than it looks: outliving the
- * execution that minted it, outliving the *invocation* that made it, surviving
- * the answer being replaced or edited, or being overwritten by a second
- * provider.
+ * how somebody wrote their code — so a provider publishes the definition, and
+ * what each of its forms runs as is declared on it. Everything here is about
+ * the ways a publication could be weaker than it looks: outliving the execution
+ * that minted it, outliving the *invocation* that made it, surviving the answer
+ * being replaced or edited, or being overwritten by a second provider.
*
* The shape under test is the split between two separate rights. A provider
* installation is the right to be asked, and it is deliberately reusable: one
* installation answers several admitted names and the same name resolved more
* than once. A request is the right to answer one asking, and it is closed the
- * moment that handler invocation ends. Only a request records an identity,
- * which is what makes a statement provable — an installation handle can prove
- * "some provider this host installed" and nothing about which invocation is
- * speaking.
+ * moment that handler invocation ends. Only a request publishes, which is what
+ * makes a publication provable — an installation handle can prove "some
+ * provider this host installed" and nothing about which invocation is speaking.
*
- * The rows read what `identify()` answers rather than what a refusal says: an
- * identity statement that reported correctly while still identifying a
- * substituted object would satisfy an error-shape assertion and none of these.
+ * The rows read what `identify()` answers rather than what a refusal says: a
+ * publication that reported correctly while still vouching for a substituted
+ * object would satisfy an error-shape assertion and none of these.
*
* The owner is `CanonicalImports`, which already holds issuance and retention.
* One owner asks one question about one table; a second registry would be a
@@ -33,24 +32,21 @@ import { expect } from "@executablemd/test-support/expect";
import { scoped } from "effection";
import type { Operation } from "effection";
-import {
- AnswerIdentityError,
- CanonicalImports,
- identityRecord,
-} from "../src/components/component-resolution.ts";
+import { AnswerIdentityError, CanonicalImports } from "../src/components/component-resolution.ts";
import type {
- AnswerIdentity,
ComponentAnswerRequest,
ImportedDefinition,
ProviderInstallation,
ResolutionWindow,
} from "../src/components/component-resolution.ts";
-import type { Json } from "../src/types.ts";
+import type { GeneratedForms } from "../src/components/generated-metadata.ts";
+import type { FunctionComponentDefinition, Json } from "../src/types.ts";
const ORIGIN = "test://provider";
-const IDENTITY: AnswerIdentity = { origin: ORIGIN, key: "Open", revision: "1" };
-const OPEN = { key: "Open", revision: "1" } as const;
-const OTHER = { key: "Other", revision: "1" } as const;
+/** What this provider's definition offers its self-closing form as. */
+const OFFERED: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/open/read", effect: "read" },
+};
/** One owner, activated the way canonical execution activates it. */
function owner(): CanonicalImports {
@@ -99,12 +95,24 @@ function asking(
}
}
-/** One answer a provider might return, fresh each time. */
+/**
+ * One answer a provider might return, fresh each time.
+ *
+ * It carries the generated metadata a provider's own declaration states, since
+ * that is what a publication is now a statement about: the identity and effect
+ * of each form travel on the definition rather than beside it.
+ */
function answer(name = "Open"): ImportedDefinition {
+ return { ...bare(name), generated: OFFERED };
+}
+
+/** One answer offering no generated use of itself, which is the ordinary case. */
+function bare(name = "Open"): FunctionComponentDefinition {
return {
kind: "function",
name,
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
// deno-lint-ignore require-yield
*fn(): Operation {
return "";
@@ -112,6 +120,27 @@ function answer(name = "Open"): ImportedDefinition {
};
}
+/**
+ * One answer carrying metadata this engine may not be able to read.
+ *
+ * Assembled rather than declared, because the point of each of these is that a
+ * provider's definition crossed a package boundary carrying something the
+ * declared shape does not describe.
+ */
+function answerWith(generated: unknown): ImportedDefinition {
+ return Object.assign(bare(), { generated });
+}
+
+/** The generated identity on a retained definition, whichever arm it is. */
+function offeredIdentity(definition: ImportedDefinition | undefined): unknown {
+ const generated = definition === undefined ? undefined : Reflect.get(definition, "generated");
+ if (typeof generated !== "object" || generated === null) {
+ return undefined;
+ }
+ const form = Reflect.get(generated, "self-closing");
+ return typeof form === "object" && form !== null ? Reflect.get(form, "identity") : undefined;
+}
+
/** The implementation on a definition, whichever arm of the union it is. */
function implementationOf(definition: ImportedDefinition | undefined): unknown {
return definition === undefined ? undefined : (definition as { fn?: unknown }).fn;
@@ -127,36 +156,42 @@ function refusalOf(attempt: () => unknown): unknown {
}
}
-describe("Tier CIV — an identity belongs to one answer of one import", () => {
- it("CIV23: an identity is stated on the exact answer and read back", function* () {
+describe("Tier CIV — a publication belongs to one answer of one import", () => {
+ it("CIV23: a definition is published on the exact answer and read back", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
const { resolution, supplied } = resolving(imports, "Open", (resolution) => ({
resolution,
- supplied: asking(provider, "Open", (request) => request.claim(answer(), OPEN)),
+ supplied: asking(provider, "Open", (request) => request.answer(answer())),
}));
- expect(imports.identify(resolution, supplied)?.identity).toEqual(IDENTITY);
- expect(identityRecord(IDENTITY)).toBe("test://provider#Open@1");
+ const identified = imports.identify(resolution, supplied);
+ // The publishing installation's origin, which is provenance and nothing
+ // else: what the implementation *is* comes back on the definition, as the
+ // identity its own declaration offers that form under.
+ expect(identified?.origin).toBe(ORIGIN);
+ expect(offeredIdentity(identified?.definition)).toBe(
+ "https://executable.md/components/open/read",
+ );
});
- it("CIV24: identification answers with the claim-time copy, not the answer", function* () {
+ it("CIV24: identification answers with the publication-time copy, not the answer", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
const supplied = answer();
const resolution = resolving(imports, "Open", (resolution) => {
- asking(provider, "Open", (request) => request.claim(supplied, OPEN));
+ asking(provider, "Open", (request) => request.answer(supplied));
return resolution;
});
// One call answers both halves. The definition is core's own copy, taken
- // when the claim was recorded — so a caller that keeps what identification
+ // when it was published — so a caller that keeps what identification
// gave it has kept the object the check was made about, and never has to
// read the chain's object a second time to obtain one.
const identified = imports.identify(resolution, supplied);
- expect(identified?.identity).toEqual(IDENTITY);
+ expect(identified?.origin).toBe(ORIGIN);
expect(identified?.definition).not.toBe(supplied);
expect(identified?.definition.name).toBe("Open");
// The implementation crosses by reference, because a function is not
@@ -170,8 +205,9 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
const honest = answer();
const reads: string[] = [];
// The check/use gap, planted. Reading a member through `[[Get]]` alternates
- // between what was claimed and a substitution; comparing descriptors, which
- // is how the claim is checked, does not run this at all. So a caller that
+ // between what was published and a substitution; comparing descriptors,
+ // which is how a publication is checked, does not run this at all. So a
+ // caller that
// checked the descriptors and then *read* the object again to keep a copy
// would keep the substitution, and this row is what says nothing does.
const alternating = new Proxy(honest, {
@@ -185,14 +221,14 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
}) as ImportedDefinition;
const resolution = resolving(imports, "Open", (resolution) => {
- asking(provider, "Open", (request) => request.claim(alternating, OPEN));
+ asking(provider, "Open", (request) => request.answer(alternating));
return resolution;
});
const identified = imports.identify(resolution, alternating);
- // The claim itself was recorded from the first reading, and identification
- // answers with that recording.
- expect(identified?.identity).toEqual(IDENTITY);
+ // The publication itself was recorded from the first reading, and
+ // identification answers with that recording.
+ expect(identified?.origin).toBe(ORIGIN);
expect(identified?.definition.name).toBe("Open");
// The plant is live rather than inert: the very next read of the object the
// chain returned says something else, which is what a second read would
@@ -201,51 +237,46 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
expect(identified?.definition.name).toBe("Open");
});
- it("CIV23: restating exactly the same claim is idempotent", function* () {
+ it("CIV23: publishing exactly the same definition again is idempotent", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
const supplied = answer();
const resolution = resolving(imports, "Open", (resolution) => {
asking(provider, "Open", (request) => {
- request.claim(supplied, OPEN);
- // A provider installed twice states the same thing twice. That is not
- // two providers disagreeing, and it is not a conflict.
- expect(refusalOf(() => request.claim(supplied, OPEN))).toBe(undefined);
+ request.answer(supplied);
+ // A provider installed twice publishes the same thing twice. That is
+ // not two providers disagreeing, and it is not a conflict.
+ expect(refusalOf(() => request.answer(supplied))).toBe(undefined);
});
return resolution;
});
- expect(imports.identify(resolution, supplied)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, supplied)?.origin).toBe(ORIGIN);
});
- it("CIV23: a competing claim refuses and never overwrites the first", function* () {
+ it("CIV23: a competing publication refuses and never overwrites the first", function* () {
const imports = owner();
const first = imports.provider(ORIGIN);
const second = imports.provider("test://other");
const held = answer();
const resolution = resolving(imports, "Open", (resolution) => {
- asking(first, "Open", (request) => request.claim(held, OPEN));
- // Another installation, asked in the same live resolution, cannot rename
- // what the first stated. An overwrite would let a second provider take
+ asking(first, "Open", (request) => request.answer(held));
+ // Another installation, asked in the same live resolution, cannot take
+ // what the first published. An overwrite would let a second provider take
// the first's implementation.
asking(second, "Open", (request) => {
- expect(refusalOf(() => request.claim(held, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => request.answer(held))).toBeInstanceOf(AnswerIdentityError);
});
- // And the first installation's *next* invocation cannot restate it
- // differently either.
+ // And the first installation's *next* invocation cannot publish a
+ // different implementation for a resolution it has already answered.
asking(first, "Open", (request) => {
- expect(
- refusalOf(() => request.claim(held, { key: "Other", revision: "1" })),
- ).toBeInstanceOf(AnswerIdentityError);
- expect(refusalOf(() => request.claim(held, { key: "Open", revision: "2" }))).toBeInstanceOf(
- AnswerIdentityError,
- );
+ expect(refusalOf(() => request.answer(answer()))).toBeInstanceOf(AnswerIdentityError);
});
return resolution;
});
- expect(imports.identify(resolution, held)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, held)?.origin).toBe(ORIGIN);
});
it("CIV25: a request whose handler has returned states nothing", function* () {
@@ -263,11 +294,11 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
asking(provider, "Open", (request) => {
stale = request;
});
- expect(refusalOf(() => stale?.claim(late, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => stale?.answer(late))).toBeInstanceOf(AnswerIdentityError);
// The positive control in the same still-open window: a fresh invocation
- // of the same installation claims, so the refusal above is about the
+ // of the same installation publishes, so the refusal above is about the
// handler lease rather than about the window or the provider.
- asking(provider, "Open", (request) => request.claim(answer(), OPEN));
+ asking(provider, "Open", (request) => request.answer(answer()));
return resolution;
});
@@ -292,15 +323,15 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
// Resolution N+1 of the same name. The stale request names the right
// component and belongs to an import that is over.
- const { resolution, claimed } = resolving(imports, "Open", (resolution) => {
- expect(refusalOf(() => stale?.claim(fresh, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ const { resolution, published } = resolving(imports, "Open", (resolution) => {
+ expect(refusalOf(() => stale?.answer(fresh))).toBeInstanceOf(AnswerIdentityError);
// The same installation is asked again and answers this resolution: the
// installation is reusable, the request is not.
return {
resolution,
- claimed: asking(provider, "Open", (request) => {
+ published: asking(provider, "Open", (request) => {
requests.push(request);
- return request.claim(answer(), OPEN);
+ return request.answer(answer());
}),
};
});
@@ -308,10 +339,10 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
expect(requests).toHaveLength(2);
expect(requests[0]).not.toBe(requests[1]);
expect(imports.identify(resolution, fresh)).toBe(undefined);
- expect(imports.identify(resolution, claimed)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, published)?.origin).toBe(ORIGIN);
});
- it("CIV25: a stale request cannot retag while another name is being decided", function* () {
+ it("CIV25: a stale request cannot publish while another name is being decided", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
const substitute = answer("Other");
@@ -323,19 +354,19 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
});
});
- // `claim` takes no name, so the stale request cannot even ask about the
+ // `answer` takes no name, so the stale request cannot even ask about the
// name being decided: it is fixed to `Open`, which this resolution is not.
- const { resolution, claimed } = resolving(imports, "Other", (resolution) => {
+ const { resolution, published } = resolving(imports, "Other", (resolution) => {
expect(stale?.name).toBe("Open");
- expect(refusalOf(() => stale?.claim(substitute, OTHER))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => stale?.answer(substitute))).toBeInstanceOf(AnswerIdentityError);
return {
resolution,
- claimed: asking(provider, "Other", (request) => request.claim(answer("Other"), OTHER)),
+ published: asking(provider, "Other", (request) => request.answer(answer("Other"))),
};
});
expect(imports.identify(resolution, substitute)).toBe(undefined);
- expect(imports.identify(resolution, claimed)?.identity.key).toBe("Other");
+ expect(imports.identify(resolution, published)?.definition.name).toBe("Other");
});
it("CIV25: one installation answers two names through two distinct requests", function* () {
@@ -348,16 +379,16 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
const open = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(provider, "Open", (request) => {
+ published: asking(provider, "Open", (request) => {
seen.push(request);
- return request.claim(answer(), OPEN);
+ return request.answer(answer());
}),
}));
const other = resolving(imports, "Other", (resolution) => ({
resolution,
- claimed: asking(provider, "Other", (request) => {
+ published: asking(provider, "Other", (request) => {
seen.push(request);
- return request.claim(answer("Other"), OTHER);
+ return request.answer(answer("Other"));
}),
}));
@@ -365,8 +396,8 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
expect(seen).toHaveLength(2);
expect(seen[0]).not.toBe(seen[1]);
expect(seen.map((request) => request.name)).toEqual(["Open", "Other"]);
- expect(imports.identify(open.resolution, open.claimed)?.identity.key).toBe("Open");
- expect(imports.identify(other.resolution, other.claimed)?.identity.key).toBe("Other");
+ expect(imports.identify(open.resolution, open.published)?.definition.name).toBe("Open");
+ expect(imports.identify(other.resolution, other.published)?.definition.name).toBe("Other");
});
it("CIV25: identification takes the window it was asked about", function* () {
@@ -376,16 +407,16 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
const first = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(provider, "Open", (request) => {
+ published: asking(provider, "Open", (request) => {
requests.push(request);
- return request.claim(answer(), OPEN);
+ return request.answer(answer());
}),
}));
const second = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(provider, "Open", (request) => {
+ published: asking(provider, "Open", (request) => {
requests.push(request);
- return request.claim(answer(), OPEN);
+ return request.answer(answer());
}),
}));
@@ -394,10 +425,10 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
// opened, which is why the answer does not change with when it is asked.
expect(requests).toHaveLength(2);
expect(requests[0]).not.toBe(requests[1]);
- expect(imports.identify(first.resolution, first.claimed)?.identity).toEqual(IDENTITY);
- expect(imports.identify(second.resolution, second.claimed)?.identity).toEqual(IDENTITY);
- expect(imports.identify(first.resolution, second.claimed)).toBe(undefined);
- expect(imports.identify(second.resolution, first.claimed)).toBe(undefined);
+ expect(imports.identify(first.resolution, first.published)?.origin).toBe(ORIGIN);
+ expect(imports.identify(second.resolution, second.published)?.origin).toBe(ORIGIN);
+ expect(imports.identify(first.resolution, second.published)).toBe(undefined);
+ expect(imports.identify(second.resolution, first.published)).toBe(undefined);
});
it("CIV25: a request shows the asked position by value", function* () {
@@ -429,38 +460,39 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
});
});
- it("CIV23: a different object carries no claim, however alike", function* () {
+ it("CIV23: a different object carries no publication, however alike", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
- const { resolution, claimed } = resolving(imports, "Open", (resolution) => ({
+ const { resolution, published } = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(provider, "Open", (request) => request.claim(answer(), OPEN)),
+ published: asking(provider, "Open", (request) => request.answer(answer())),
}));
- expect(imports.identify(resolution, claimed)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, published)?.origin).toBe(ORIGIN);
// The outer-replacement case: a handler further out returns its own object,
- // so an intermediate claim does not travel with the name.
+ // so an intermediate publication does not travel with the name.
expect(imports.identify(resolution, answer())).toBe(undefined);
- // A copy of the claimed object is a different object too.
- expect(imports.identify(resolution, { ...claimed })).toBe(undefined);
+ // A copy of the published object is a different object too, and copying the
+ // generated metadata with it changes nothing.
+ expect(imports.identify(resolution, { ...published })).toBe(undefined);
});
- it("CIV23: editing the claimed answer invalidates the claim", function* () {
+ it("CIV23: editing the published answer invalidates the publication", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
- const { resolution, claimed } = resolving(imports, "Open", (resolution) => ({
+ const { resolution, published } = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(provider, "Open", (request) => request.claim(answer(), OPEN)),
+ published: asking(provider, "Open", (request) => request.answer(answer())),
}));
- expect(imports.identify(resolution, claimed)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, published)?.origin).toBe(ORIGIN);
- // The same object, edited after the claim by a handler further out. What
- // was claimed is no longer what is there.
- (claimed as { name: string }).name = "Substituted";
+ // The same object, edited after publication by a handler further out. What
+ // was published is no longer what is there.
+ (published as { name: string }).name = "Substituted";
- expect(imports.identify(resolution, claimed)).toBe(undefined);
+ expect(imports.identify(resolution, published)).toBe(undefined);
});
it("CIV25: an installation retained past its execution opens nothing", function* () {
@@ -468,34 +500,34 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
const provider = imports.provider(ORIGIN);
const { resolution, before } = resolving(imports, "Open", (resolution) => ({
resolution,
- before: asking(provider, "Open", (request) => request.claim(answer(), OPEN)),
+ before: asking(provider, "Open", (request) => request.answer(answer())),
}));
- expect(imports.identify(resolution, before)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, before)?.origin).toBe(ORIGIN);
imports.revoke();
// The installation is the object a provider kept. The window it would need
- // cannot be opened, and a request minted from it states nothing.
+ // cannot be opened, and a request minted from it publishes nothing.
expect(refusalOf(() => imports.beginResolution("Open"))).toBeInstanceOf(AnswerIdentityError);
expect(
- refusalOf(() => asking(provider, "Open", (request) => request.claim(answer(), OPEN))),
+ refusalOf(() => asking(provider, "Open", (request) => request.answer(answer()))),
).toBeInstanceOf(AnswerIdentityError);
- // And what it stated while the execution was live identifies nothing now:
+ // And what it published while the execution was live identifies nothing now:
// an admission may not be reconciled against a run that is over.
expect(imports.identify(resolution, before)).toBe(undefined);
expect(imports.identifying).toBe(false);
});
- it("CIV25: an owner starts inactive, so a claim before activation refuses", function* () {
+ it("CIV25: an owner starts inactive, so a publication before activation refuses", function* () {
const imports = new CanonicalImports();
// Canonical execution registers teardown, then activates, then installs
- // providers. A claim that landed before activation would be a claim with no
- // teardown behind it.
+ // providers. A publication that landed before activation would be one with
+ // no teardown behind it.
expect(imports.identifying).toBe(false);
expect(refusalOf(() => imports.beginResolution("Open"))).toBeInstanceOf(AnswerIdentityError);
expect(
refusalOf(() =>
- asking(imports.provider(ORIGIN), "Open", (request) => request.claim(answer(), OPEN)),
+ asking(imports.provider(ORIGIN), "Open", (request) => request.answer(answer())),
),
).toBeInstanceOf(AnswerIdentityError);
});
@@ -506,74 +538,111 @@ describe("Tier CIV — an identity belongs to one answer of one import", () => {
const shared = answer();
const one = resolving(first, "Open", (resolution) => {
- asking(first.provider(ORIGIN), "Open", (request) => request.claim(shared, OPEN));
+ asking(first.provider(ORIGIN), "Open", (request) => request.answer(shared));
return resolution;
});
// Live at the same time, and each answers only for what it recorded.
const two = resolving(second, "Open", (resolution) => {
expect(second.identify(resolution, shared)).toBe(undefined);
- asking(second.provider(ORIGIN), "Open", (request) =>
- request.claim(shared, { key: "Open", revision: "2" }),
- );
+ asking(second.provider("test://other"), "Open", (request) => request.answer(shared));
return resolution;
});
- expect(first.identify(one, shared)?.identity).toEqual(IDENTITY);
- expect(second.identify(two, shared)?.identity).toEqual({
- origin: ORIGIN,
- key: "Open",
- revision: "2",
- });
+ // One definition, published separately in each owner, and each owner
+ // answers with the provenance of the installation that published it there.
+ expect(first.identify(one, shared)?.origin).toBe(ORIGIN);
+ expect(second.identify(two, shared)?.origin).toBe("test://other");
// The positive control: tearing one down leaves the other working.
first.revoke();
expect(first.identify(one, shared)).toBe(undefined);
- expect(second.identify(two, shared)?.identity.revision).toBe("2");
+ expect(second.identify(two, shared)?.origin).toBe("test://other");
});
it("CIV23: a provider cannot state an origin canonical execution did not give it", function* () {
const imports = owner();
- // The installation carries the origin; the provider states only key and
- // revision. There is no member on the claim call to put another origin in.
- const { resolution, claimed } = resolving(imports, "Open", (resolution) => ({
+ // The installation carries the origin, and `answer` takes the definition
+ // and nothing else. There is no member on the call to put another origin
+ // in, and the provenance identification reports is the one this execution
+ // assigned.
+ const { resolution, published } = resolving(imports, "Open", (resolution) => ({
resolution,
- claimed: asking(imports.provider("test://assigned"), "Open", (request) =>
- request.claim(answer(), OPEN),
+ published: asking(imports.provider("test://assigned"), "Open", (request) =>
+ request.answer(answer()),
),
}));
- expect(imports.identify(resolution, claimed)?.identity.origin).toBe("test://assigned");
+ expect(imports.identify(resolution, published)?.origin).toBe("test://assigned");
});
- it("CIV23: a partial identity is refused rather than recorded", function* () {
+ it("CIV23: metadata this engine cannot read is refused rather than recorded", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
- const attempts = [
- { key: "", revision: "1" },
- { key: "Open", revision: "" },
+ // A provider's definition never passed registration, so its generated
+ // metadata is read here or nowhere. Each of these is a form offered under
+ // something a continuation could not be compared against.
+ const attempts: unknown[] = [
+ { "self-closing": { identity: "components/open/read", effect: "read" } },
+ { "self-closing": { identity: "https://executable.md.example.com/open", effect: "read" } },
+ { "self-closing": { identity: "https://executable.md/components/open/read", effect: "ask" } },
+ { paired: { identity: "https://executable.md/components/open/write", effect: "write" } },
];
- for (const attempt of attempts) {
- const supplied = answer();
+ for (const generated of attempts) {
+ const supplied = answerWith(generated);
const resolution = resolving(imports, "Open", (resolution) => {
asking(provider, "Open", (request) => {
- expect(refusalOf(() => request.claim(supplied, attempt))).toBeInstanceOf(
- AnswerIdentityError,
- );
+ expect(refusalOf(() => request.answer(supplied))).toBeInstanceOf(AnswerIdentityError);
});
return resolution;
});
- // Refused rather than partially recorded: a half identity would compare
- // equal to a different half identity.
+ // Refused rather than partially recorded: metadata read in part would
+ // admit a form under an identity nobody declared.
expect(imports.identify(resolution, supplied)).toBe(undefined);
}
});
- it("CIV23: an answer nobody claimed identifies nothing", function* () {
+ it("CIV23: a definition whose stated forms are not a declaration refuses", function* () {
+ const imports = owner();
+ const provider = imports.provider(ORIGIN);
+ // The forms a definition states are read the way its metadata is, and only
+ // genuine omission means both: a value that is not a canonical declaration
+ // cannot be read as one by dropping what it says, because the offered
+ // spelling is checked against it.
+ for (const forms of ["paired", ["self-closing", "unknown"], []]) {
+ const supplied = Object.assign(bare(), { generated: OFFERED, forms });
+ const resolution = resolving(imports, "Open", (resolution) => {
+ asking(provider, "Open", (request) => {
+ expect(refusalOf(() => request.answer(supplied))).toBeInstanceOf(AnswerIdentityError);
+ });
+ return resolution;
+ });
+ expect([forms, imports.identify(resolution, supplied)]).toEqual([forms, undefined]);
+ }
+ });
+
+ it("CIV23: an answer carrying no metadata is published and read back", function* () {
+ const imports = owner();
+ const provider = imports.provider(ORIGIN);
+ // The ordinary authored case: a provider answering an import that is not a
+ // generated admission offers no generated form, and publishing it is still
+ // a perfectly good answer. What it cannot do is satisfy an admitted entry,
+ // which refuses the missing form where the table is sealed.
+ const { resolution, supplied } = resolving(imports, "Open", (resolution) => ({
+ resolution,
+ supplied: asking(provider, "Open", (request) => request.answer(bare())),
+ }));
+
+ expect(imports.identify(resolution, supplied)?.origin).toBe(ORIGIN);
+ expect(offeredIdentity(imports.identify(resolution, supplied)?.definition)).toBe(undefined);
+ });
+
+ it("CIV23: an answer nobody published identifies nothing", function* () {
const imports = owner();
- // The ordinary case, and the reason this is not a permission: an unidentified
- // answer is a perfectly good answer. What it cannot be is the thing a
- // fragment runs, because a continuation would have nothing to compare.
+ // The ordinary case, and the reason this is not a permission: an answer
+ // nobody published is a perfectly good answer. What it cannot be is the
+ // thing a fragment runs, because a continuation would have nothing to
+ // compare.
resolving(imports, "Open", (resolution) => {
expect(imports.identify(resolution, answer())).toBe(undefined);
expect(imports.identify(resolution, undefined)).toBe(undefined);
@@ -611,12 +680,10 @@ describe("Tier PA10b — one serial resolution window", () => {
first.close();
const second = imports.beginResolution("Open");
try {
- expect(refusalOf(() => kept.request.claim(answer(), OPEN))).toBeInstanceOf(
- AnswerIdentityError,
- );
- // And the window that is open answers for what was claimed in it.
- const claimed = asking(provider, "Open", (request) => request.claim(answer(), OPEN));
- expect(imports.identify(second, claimed)?.identity).toEqual(IDENTITY);
+ expect(refusalOf(() => kept.request.answer(answer()))).toBeInstanceOf(AnswerIdentityError);
+ // And the window that is open answers for what was published in it.
+ const published = asking(provider, "Open", (request) => request.answer(answer()));
+ expect(imports.identify(second, published)?.origin).toBe(ORIGIN);
} finally {
kept.close();
second.close();
@@ -632,10 +699,10 @@ describe("Tier PA10b — one serial resolution window", () => {
const window = imports.beginResolution("Open");
try {
- const claimed = yield* scoped(function* (): Operation {
- return asking(provider, "Open", (request) => request.claim(answer(), OPEN));
+ const published = yield* scoped(function* (): Operation {
+ return asking(provider, "Open", (request) => request.answer(answer()));
});
- expect(imports.identify(window, claimed)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(window, published)?.origin).toBe(ORIGIN);
} finally {
window.close();
}
@@ -652,7 +719,7 @@ describe("Tier PA10b — one serial resolution window", () => {
window.close();
expect(
- refusalOf(() => asking(provider, "Open", (request) => request.claim(answer(), OPEN))),
+ refusalOf(() => asking(provider, "Open", (request) => request.answer(answer()))),
).toBeInstanceOf(AnswerIdentityError);
expect(imports.identify(window, answer())).toBe(undefined);
});
@@ -667,9 +734,9 @@ describe("Tier PA10c — one definition answering more than one import", () => {
const shared = answer();
const first = resolving(imports, "Open", (resolution) => {
- const supplied = asking(provider, "Open", (request) => request.claim(shared, OPEN));
+ const supplied = asking(provider, "Open", (request) => request.answer(shared));
expect(supplied).toBe(shared);
- expect(imports.identify(resolution, shared)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, shared)?.origin).toBe(ORIGIN);
return resolution;
});
@@ -677,9 +744,9 @@ describe("Tier PA10c — one definition answering more than one import", () => {
// over. The definition is the same object, and it is still this provider's
// answer to say.
resolvingLater(imports, provider, shared);
- // And the closed window still reads the claim it recorded, unchanged by the
+ // And the closed window still reads the publication it recorded, unchanged by the
// later one: each resolution keeps its own.
- expect(imports.identify(first, shared)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(first, shared)?.origin).toBe(ORIGIN);
});
it("PA10c: reuse cannot substitute between comparison and retention", function* () {
@@ -704,46 +771,46 @@ describe("Tier PA10c — one definition answering more than one import", () => {
});
const first = resolving(imports, "Open", (resolution) => {
- asking(provider, "Open", (request) => request.claim(shared, OPEN));
+ asking(provider, "Open", (request) => request.answer(shared));
return resolution;
});
expect(imports.identify(first, shared)?.definition.name).toBe("Open");
resolving(imports, "Open", (resolution) => {
asking(provider, "Open", (request) => {
- expect(refusalOf(() => request.claim(shared, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => request.answer(shared))).toBeInstanceOf(AnswerIdentityError);
return undefined;
});
expect(imports.identify(resolution, shared)).toBe(undefined);
});
});
- it("PA10c: an edited definition cannot be claimed again, and identifies nowhere", function* () {
+ it("PA10c: an edited definition cannot be published again, and identifies nowhere", function* () {
const imports = owner();
const provider = imports.provider(ORIGIN);
const shared = answer();
const first = resolving(imports, "Open", (resolution) => {
- asking(provider, "Open", (request) => request.claim(shared, OPEN));
- expect(imports.identify(resolution, shared)?.identity).toEqual(IDENTITY);
+ asking(provider, "Open", (request) => request.answer(shared));
+ expect(imports.identify(resolution, shared)?.origin).toBe(ORIGIN);
return resolution;
});
- // One member of the definition, edited after it was claimed and while the
- // stated identity says nothing changed. This is a different implementation
- // wearing the first one's revision.
+ // One member of the definition, edited after it was published. This is a
+ // different implementation asking to answer under the first one's
+ // publication.
shared.props = { type: "object", properties: {}, additionalProperties: true };
resolving(imports, "Open", (resolution) => {
asking(provider, "Open", (request) => {
- // Refused before anything is recorded: the identity was stated about
- // the definition core retained, and this object is no longer it.
- expect(refusalOf(() => request.claim(shared, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ // Refused before anything is recorded: the publication was about the
+ // definition core retained, and this object is no longer it.
+ expect(refusalOf(() => request.answer(shared))).toBeInstanceOf(AnswerIdentityError);
return undefined;
});
// So the new window vouches for nothing.
expect(imports.identify(resolution, shared)).toBe(undefined);
});
- // And the window that did claim it cannot identify the changed object
+ // And the window that did publish it cannot identify the changed object
// either, which is the change detection that was already there.
expect(imports.identify(first, shared)).toBe(undefined);
});
@@ -758,18 +825,18 @@ describe("Tier PA10c — one definition answering more than one import", () => {
const shared = answer();
const first = imports.beginResolution("Open");
- asking(provider, "Open", (request) => request.claim(shared, OPEN));
- expect(imports.identify(first, shared)?.identity).toEqual(IDENTITY);
+ asking(provider, "Open", (request) => request.answer(shared));
+ expect(imports.identify(first, shared)?.origin).toBe(ORIGIN);
first.close();
const second = imports.beginResolution("Open");
try {
- asking(provider, "Open", (request) => request.claim(shared, OPEN));
- expect(imports.identify(second, shared)?.identity).toEqual(IDENTITY);
+ asking(provider, "Open", (request) => request.answer(shared));
+ expect(imports.identify(second, shared)?.origin).toBe(ORIGIN);
// And each window keeps its own copy: the first still identifies what it
- // claimed, because provenance is a fact about that resolution rather than
+ // published, because provenance is a fact about that resolution rather than
// about which one is open now.
- expect(imports.identify(first, shared)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(first, shared)?.origin).toBe(ORIGIN);
} finally {
second.close();
}
@@ -785,11 +852,11 @@ describe("Tier PA10c — one definition answering more than one import", () => {
// that same window is two answers to one import, and that still refuses.
resolving(imports, "Open", (resolution) => {
asking(provider, "Open", (request) => {
- expect(request.claim(shared, OPEN)).toBe(shared);
- expect(refusalOf(() => request.claim(answer(), OPEN))).toBeInstanceOf(AnswerIdentityError);
+ expect(request.answer(shared)).toBe(shared);
+ expect(refusalOf(() => request.answer(answer()))).toBeInstanceOf(AnswerIdentityError);
return undefined;
});
- expect(imports.identify(resolution, shared)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, shared)?.origin).toBe(ORIGIN);
});
});
@@ -800,18 +867,19 @@ describe("Tier PA10c — one definition answering more than one import", () => {
const shared = answer();
resolvingLater(imports, provider, shared);
- // What this object is was settled by its first claim. A new resolution is a
- // new question about which import it answered, never a chance to restate it.
+ // What this object answers for was settled by its first publication. A new
+ // resolution is a new question about which import it answered, never a
+ // chance to publish it again elsewhere.
resolving(imports, "Other", (resolution) => {
asking(provider, "Other", (request) => {
- expect(refusalOf(() => request.claim(shared, OTHER))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => request.answer(shared))).toBeInstanceOf(AnswerIdentityError);
return undefined;
});
expect(imports.identify(resolution, shared)).toBe(undefined);
});
resolving(imports, "Open", (resolution) => {
asking(other, "Open", (request) => {
- expect(refusalOf(() => request.claim(shared, OPEN))).toBeInstanceOf(AnswerIdentityError);
+ expect(refusalOf(() => request.answer(shared))).toBeInstanceOf(AnswerIdentityError);
return undefined;
});
expect(imports.identify(resolution, shared)).toBe(undefined);
@@ -833,8 +901,8 @@ function resolvingLater(
shared: ImportedDefinition,
): void {
resolving(imports, "Open", (resolution) => {
- const supplied = asking(provider, "Open", (request) => request.claim(shared, OPEN));
+ const supplied = asking(provider, "Open", (request) => request.answer(shared));
expect(supplied).toBe(shared);
- expect(imports.identify(resolution, shared)?.identity).toEqual(IDENTITY);
+ expect(imports.identify(resolution, shared)?.origin).toBe(ORIGIN);
});
}
diff --git a/packages/core/tests/evaluate-component.test.ts b/packages/core/tests/evaluate-component.test.ts
index 0239566d3..cd06b9d94 100644
--- a/packages/core/tests/evaluate-component.test.ts
+++ b/packages/core/tests/evaluate-component.test.ts
@@ -41,7 +41,7 @@ import type { ElicitationRequest } from "../src/elicitation-api.ts";
import { retainedSource } from "../src/root-source.ts";
import { recordedFiles, useRecordedFiles } from "./support/fragment-files.ts";
import type { RecordedFiles } from "./support/fragment-files.ts";
-import { answerProvider, implementation } from "./support/answer-provider.ts";
+import { answerProvider, implementation, offering } from "./support/answer-provider.ts";
import type { Implementation, ProviderOptions } from "./support/answer-provider.ts";
import type { FunctionComponentDefinition } from "../src/types.ts";
import { ActiveProjection } from "../src/projection.ts";
@@ -62,21 +62,12 @@ function syntaxProfile(
options: {
transform?: (answer: FunctionComponentDefinition) => FunctionComponentDefinition;
retained?: FunctionComponentDefinition[];
- unclaimed?: boolean;
- revision?: string;
} = {},
): ExecutionInstallation {
let captured = false;
return {
evaluation: {
- read: [
- {
- kind: "component-answer",
- name: "Syntax",
- identity: { origin: "test://syntax-provider", key: "Syntax", revision: "1" },
- forms: ["self-closing"],
- },
- ],
+ read: [{ kind: "component-answer", name: "Syntax", forms: ["self-closing"] }],
},
componentAnswers: [
{
@@ -90,10 +81,6 @@ function syntaxProfile(
throw new Error("expected canonical Syntax");
}
options.retained?.push(answer);
- if (options.unclaimed) {
- return answer;
- }
- request.claim(answer, { key: "Syntax", revision: options.revision ?? "1" });
return options.transform?.(answer) ?? answer;
}
return answer;
@@ -120,7 +107,7 @@ describe("protected generated component answers", () => {
throw new Error("expected canonical Syntax");
}
const captured = yield* prepared.seal(
- new Map([["Syntax", { definition }]]),
+ new Map([["Syntax", { definition, origin: "@executablemd/core" }]]),
identity.componentRouting.project,
);
const symbols = admittedSymbols(captured.read);
@@ -170,16 +157,14 @@ describe("protected generated component answers", () => {
* may *run*, by identity rather than by spelling. The enclosing reference
* documents core's own ``, so an admitted entry reporting an ordinary
* registration is a different component under the same name — and a fragment
- * admitted to write a file would be told it could not. The grant stays the
- * host's: only the documentation origin is core's, which is why the host's own
- * directory identity below is untouched.
+ * admitted to write a file would be told it could not. What the host decides
+ * is whether the name is admitted at all; the identity the grant is recorded
+ * under is core's, from the declaration that owns the component.
*/
it("reports an admitted file operation as the component canonical core owns", function* () {
const prepared = yield* prepareEvaluationProfile({
read: [fileReadEntry()],
- // Deliberately a host's own identity, and deliberately not core's: it is
- // what a continuation compares, and nothing here may rewrite it.
- write: [directoryEntry({ origin: "test://host", key: "Dir", revision: "1" }, "Dir")],
+ write: [directoryEntry("Dir")],
});
try {
const captured = yield* prepared.seal(new Map());
@@ -197,9 +182,12 @@ describe("protected generated component answers", () => {
sourceKind: "protected",
},
]);
- // The grant itself is unchanged. A journal holds this string, and the
- // component moving into core did not move the host's entry with it.
- expect(captured.write.map((entry) => entry.identity.origin)).toEqual(["test://host"]);
+ // And the grant is core's, because the component is: the paired spelling
+ // runs under the identity core's own declaration offers it as.
+ expect(captured.write.map((entry) => entry.origin)).toEqual(["@executablemd/core"]);
+ expect(captured.write[0]?.definition.generated).toMatchObject({
+ paired: { identity: "https://executable.md/components/dir/ensure", effect: "write" },
+ });
} finally {
prepared.revoke();
}
@@ -276,7 +264,7 @@ describe("protected generated component answers", () => {
}
});
- it("refuses copied, mutated, wrapped, unclaimed, misidentified and wrong-form answers", function* () {
+ it("refuses copied, mutated, wrapped and replaced answers, and a wrong form", function* () {
const source = ` '} />`;
const ran: string[] = [];
const variants: {
@@ -287,7 +275,7 @@ describe("protected generated component answers", () => {
{
name: "copied",
options: { transform: (answer) => ({ ...answer }) },
- reason: "carries no identity",
+ reason: "no provider published",
},
{
name: "replaced body",
@@ -300,12 +288,12 @@ describe("protected generated component answers", () => {
},
}),
},
- reason: "carries no identity",
+ reason: "no provider published",
},
{
name: "wrapped",
options: { transform: independentWrapper },
- reason: "carries no identity",
+ reason: "no provider published",
},
{
name: "mutated",
@@ -315,13 +303,7 @@ describe("protected generated component answers", () => {
return answer;
},
},
- reason: "carries no identity",
- },
- { name: "unclaimed", options: { unclaimed: true }, reason: "carries no identity" },
- {
- name: "misidentified",
- options: { revision: "2" },
- reason: "chain answered with test://syntax-provider#Syntax@2",
+ reason: "no provider published",
},
];
for (const { name, options, reason } of variants) {
@@ -408,10 +390,7 @@ describe("protected generated component answers", () => {
yield* refusal(
run(source, [syntaxProfile({ transform: () => held })], new InMemoryStream(partial)),
),
- ).toContain("carries no identity");
- expect(
- yield* refusal(run(source, [syntaxProfile({ unclaimed: true })], new InMemoryStream(events))),
- ).toContain("carries no identity");
+ ).toContain("no provider published");
function inspect(value: unknown): void {
expect(typeof value).not.toBe("function");
if (typeof value === "object" && value !== null) {
@@ -444,11 +423,7 @@ function both(files: RecordedFiles): ExecutionInstallation {
return {
evaluation: {
read: [fileReadEntry()],
- write: [
- fileWriteEntry(),
- directoryEntry({ origin: "test://host", key: "Dir", revision: "1" }, "Dir"),
- fileDeleteEntry(),
- ],
+ write: [fileWriteEntry(), directoryEntry("Dir"), fileDeleteEntry()],
},
install: () => useRecordedFiles(files),
};
@@ -1233,18 +1208,13 @@ describe("Tier FE — one occurrence, one durable decision", () => {
);
const partial = complete.slice(0, admitted + 1);
- // The same fragment, admitted under a table that now states another
- // identity. A grant is the exact set it was made under.
+ // The same fragment, admitted under a read table that now admits a second
+ // component as well. A grant is the exact set of spellings it was made
+ // under, so a widened one is a different grant rather than the same one
+ // restated.
const second = recordedFiles({ "notes.md": NOTE });
const widened: ExecutionInstallation = {
- evaluation: {
- read: [
- {
- ...fileReadEntry(),
- identity: { origin: "@executablemd/core", key: "File:read", revision: "99" },
- },
- ],
- },
+ evaluation: { read: [fileReadEntry(), globReadEntry()] },
install: () => useRecordedFiles(second),
};
const failed = yield* refusal(run(source, [widened], new InMemoryStream(partial)));
@@ -1888,7 +1858,7 @@ describe("Tier FE34 — the shared read profile", () => {
expect(files.entries.get("notes.md")).toBe(NOTE);
});
- it("FE34: canonical Syntax is the answer at core's identity, and a replacement loses it", function* () {
+ it("FE34: canonical core publishes the Syntax answer, and a replacement loses it", function* () {
const files = recordedFiles({ "notes.md": NOTE });
const seen: FunctionComponentDefinition[] = [];
const failed = yield* refusal(
@@ -1907,11 +1877,11 @@ describe("Tier FE34 — the shared read profile", () => {
}),
);
- // Core states what is behind the protected name from the innermost
+ // Core publishes what is behind the protected name from the innermost
// position, on the exact object materialization produced. A handler that
- // returns anything else returns something carrying no identity at all.
+ // returns anything else returns something nobody published.
expect(seen.length).toBeGreaterThan(0);
- expect(failed).toContain("carries no identity");
+ expect(failed).toContain("no provider published");
expect(files.performed).toEqual([]);
});
@@ -2063,6 +2033,8 @@ describe("Tier FE34 — the shared read profile", () => {
kind: "function",
name: "Held",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offering("Held", "read"),
*fn(): Operation {
yield* ensure(function* () {
throw new Error("the fragment's cleanup failed");
@@ -2076,12 +2048,7 @@ describe("Tier FE34 — the shared read profile", () => {
fileReadEntry(),
globReadEntry(),
syntaxReadEntry(),
- {
- kind: "component-answer",
- name: "Held",
- identity: { origin: "test://provider", key: "Held", revision: "1" },
- forms: ["self-closing"],
- },
+ { kind: "component-answer", name: "Held", forms: ["self-closing"] },
],
},
install: () => useRecordedFiles(files),
@@ -2125,31 +2092,24 @@ describe("Tier FE34 — the shared read profile", () => {
expect(generatedRequestRefusal(classified)).toContain("NoSuchComponent");
});
- it("FE34: core answers for Syntax alone, so Evaluate cannot be admitted at its identity", function* () {
+ it("FE34: core answers for Syntax alone, so Evaluate cannot be admitted as an answer", function* () {
const files = recordedFiles({ "notes.md": NOTE });
const failed = yield* refusal(
run(` \\n'} />\n`, [
{
evaluation: {
- read: [
- {
- kind: "component-answer",
- name: "Evaluate",
- identity: { origin: "@executablemd/core", key: "Evaluate", revision: "2" },
- forms: ["self-closing"],
- },
- ],
+ read: [{ kind: "component-answer", name: "Evaluate", forms: ["self-closing"] }],
},
install: () => useRecordedFiles(files),
},
]),
);
- // Stating an identity is what makes a protected name admissible, and core
- // states one for the vocabulary and nothing else: evaluation inside
+ // Core publishing an answer is what makes a protected name admissible, and
+ // it publishes one for the vocabulary and nothing else: evaluation inside
// evaluation is a different grant from being shown what a fragment may
- // write. The hand-written entry resolves to an answer nothing identified.
- expect(failed).toContain("carries no identity");
+ // write. The hand-written entry resolves to an answer nobody published.
+ expect(failed).toContain("no provider published");
expect(files.performed).toEqual([]);
});
});
@@ -2158,49 +2118,30 @@ describe("Tier FE34 — the shared read profile", () => {
* Tier FE14 — an implementation the ordinary import chain answered for.
*
* A `component-answer` entry is the one arm of a profile where the host does
- * not supply the body. It states a name and the exact structural identity a
- * provider must have stated, and canonical execution resolves that name once —
- * before the root import, through the complete ordinary chain — reads the
- * identity off the exact final answer, compares it whole, and seals what it
- * retained.
+ * not supply the body. It states a name and the forms it admits, and canonical
+ * execution resolves that name once — before the root import, through the
+ * complete ordinary chain — takes the definition the provider published on the
+ * exact final answer, holds each admitted spelling to what that definition
+ * offers, and seals what it retained.
*
* So the rows here are about the two ways that could be weaker than it looks:
- * an answer nothing identified, and an answer that stopped being the thing that
- * was identified. Each refusal names what did *not* happen — no admission, no
+ * an answer nobody published, and an answer that stopped being the thing that
+ * was published. Each refusal names what did *not* happen — no admission, no
* body, no fragment effect — because a refusal that arrived after A ran would
* satisfy an error-shape assertion and none of these.
*/
-describe("Tier FE14 — the chain answers, and the answer is held to its identity", () => {
+describe("Tier FE14 — the chain answers, and the answer is held to what it offers", () => {
const OPEN = ` \\n'} as="answer" />\n\n \n`;
/** The entry a host states for a provider-backed name. */
- function admits(
- identity: { origin?: string; key?: string; revision?: string } = {},
- ): FragmentEvaluationInput {
- return {
- read: [
- {
- kind: "component-answer",
- name: "Open",
- identity: {
- origin: identity.origin ?? "test://provider",
- key: identity.key ?? "Open",
- revision: identity.revision ?? "1",
- },
- forms: ["self-closing"],
- },
- ],
- };
+ function admits(): FragmentEvaluationInput {
+ return { read: [{ kind: "component-answer", name: "Open", forms: ["self-closing"] }] };
}
/** One installation admitting ` ` and backing it with one provider. */
- function backed(
- answer: Implementation,
- options: ProviderOptions = {},
- identity: { origin?: string; key?: string; revision?: string } = {},
- ): ExecutionInstallation {
+ function backed(answer: Implementation, options: ProviderOptions = {}): ExecutionInstallation {
return {
- evaluation: admits(identity),
+ evaluation: admits(),
componentAnswers: [answerProvider("Open", answer.definition, options)],
};
}
@@ -2212,8 +2153,8 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
);
}
- /** The identity one admission retained for the name it admitted. */
- function retainedIdentity(event: DurableEvent): Json {
+ /** The row one admission retained for the name it admitted. */
+ function retainedRow(event: DurableEvent): Json {
const result = event.type === "yield" ? event.result : undefined;
if (result === undefined || result.status !== "ok" || !isRecord(result.value)) {
throw new Error("the admission recorded no result");
@@ -2222,13 +2163,13 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
if (!isRecord(policy) || !Array.isArray(policy.allowed)) {
throw new Error("the admission recorded no policy");
}
- // The trusted composition table leads every policy, so the entry this row
+ // The trusted composition table leads every policy, so the row this case
// is about is the first one the selection contributed.
const entry = policy.allowed.find((held) => isRecord(held) && held.name !== "Json");
if (!isRecord(entry)) {
throw new Error("the admission admitted nothing");
}
- return entry.identity ?? null;
+ return entry;
}
function isRecord(value: Json | undefined): value is Record {
@@ -2246,7 +2187,7 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
expect(A.invoked).toEqual(["A ran"]);
});
- it("FE14: a fresh admission records the exact structural identity", function* () {
+ it("FE14: a fresh admission records the URL the admitted spelling runs under", function* () {
const A = implementation("Open", "A ran");
const stream = new InMemoryStream();
yield* run(OPEN, [backed(A)], stream);
@@ -2256,19 +2197,21 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
if (admission === undefined) {
throw new Error("the run recorded no admission");
}
- // The four members as themselves, and the kind among them: an answer the
+ // The spelling, the URL its owner offers it under, which effect table
+ // admitted it, and what kind of thing answered for the name: an answer the
// chain resolved is a different grant from an operation core supplies the
- // body for, even under the same origin, key and revision.
- expect(retainedIdentity(admission)).toEqual({
+ // body for, even under the same URL.
+ expect(retainedRow(admission)).toEqual({
+ name: "Open",
+ form: "self-closing",
+ identity: "https://executable.md/components/open/read",
kind: "component-answer",
- origin: "test://provider",
- key: "Open",
- revision: "1",
+ effect: "read",
});
expect(A.invoked).toEqual(["A ran"]);
});
- it("FE14: an inner claim and an outer unclaimed answer refuse before any body", function* () {
+ it("FE14: an inner publication and an outer unpublished answer refuse before any body", function* () {
const A = implementation("Open", "A ran");
const B = implementation("Open", "B ran");
const files = recordedFiles({ "notes.md": NOTE });
@@ -2276,9 +2219,9 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
const failed = yield* refusal(
scoped(function* () {
- // A handler further out returns its own object. The inner provider's
- // claim was about the object it returned, and this is not that object —
- // so nothing identifies the answer the chain finally gave.
+ // A handler further out returns its own object. The inner provider
+ // published the object it returned, and this is not that object — so
+ // nobody published the answer the chain finally gave.
yield* Component.around({
*importComponent([name], next) {
const definition = yield* next(name);
@@ -2287,13 +2230,13 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
});
return yield* run(
OPEN,
- [{ ...backed(A), evaluation: admits({}), install: () => useRecordedFiles(files) }],
+ [{ ...backed(A), evaluation: admits(), install: () => useRecordedFiles(files) }],
stream,
);
}),
);
- expect(failed).toContain("carries no identity");
+ expect(failed).toContain("no provider published");
// Ahead of everything: no admission, neither body, no fragment effect.
expect(admissions(yield* stream.readAll())).toHaveLength(0);
expect(A.invoked).toEqual([]);
@@ -2301,7 +2244,7 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
expect(files.performed).toEqual([]);
});
- it("FE14: an inner claimed A and an outer claimed B compose, and only B runs", function* () {
+ it("FE14: an inner published A and an outer published B compose, and only B runs", function* () {
const A = implementation("Open", "A ran");
const B = implementation("Open", "B ran");
const delegated: unknown[] = [];
@@ -2310,29 +2253,29 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
// The positive control for the row above, and a real chain rather than one
// provider. Two providers answer for one name: the one installed first
// composes outermost, delegates — so the inner one genuinely answers and
- // claims A — and then returns its own claimed B. The difference from the
- // refusal above is only that the identity travels with the object the chain
- // finally gives back, which is what makes the substitution honest.
+ // publishes A — and then returns its own published B. The difference from
+ // the refusal above is only that the publication travels with the object
+ // the chain finally gives back, which is what makes the substitution
+ // honest.
const output = yield* run(OPEN, [
{
- evaluation: admits({ key: "Outer" }),
+ evaluation: admits(),
componentAnswers: [
answerProvider("Open", B.definition, {
- key: "Outer",
delegatesFirst: true,
delegated,
whileResolving(request) {
outerRequests.push(request.name);
},
}),
- answerProvider("Open", A.definition, { key: "Inner" }),
+ answerProvider("Open", A.definition, {}),
],
},
]);
// The inner half really answered: what delegation returned is the exact
- // object the inner provider claimed. So A was resolvable, and the row below
- // it is about which of two live answers runs rather than about one.
+ // object the inner provider published. So A was resolvable, and the row
+ // below it is about which of two live answers runs rather than about one.
expect(delegated[0]).toBe(A.definition);
expect(outerRequests).toEqual(["Open"]);
expect(String(output)).toContain("B ran");
@@ -2342,7 +2285,7 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
it("FE14: an answer whose contract reads differently each time cannot substitute", function* () {
const reads: string[] = [];
- // The claimed contract accepts one prop; the substitution accepts none.
+ // The published contract accepts one prop; the substitution accepts none.
const A = implementation("Open", "A ran", {
type: "object",
properties: { flag: { type: "string" } },
@@ -2350,10 +2293,10 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
});
// The check/use gap, planted. Reading the answer's schema alternates, and
- // comparing own descriptors — which is how the claim is checked — does not
- // run the trap. So the reading the capture *keeps* decides what a fragment
- // is validated against: the claim-time schema admits `flag`, and the
- // substitution refuses it.
+ // comparing own descriptors — which is how a publication is checked — does
+ // not run the trap. So the reading the capture *keeps* decides what a
+ // fragment is validated against: the publication-time schema admits `flag`,
+ // and the substitution refuses it.
const output = yield* run(` \\n'} as="answer" />\n`, [
{
evaluation: admits(),
@@ -2405,21 +2348,21 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
expect(A.invoked).toEqual([]);
});
- it("FE14: a provider still claiming when a fragment resolves is refused earlier", function* () {
+ it("FE14: a provider still publishing when a fragment resolves is refused earlier", function* () {
const A = implementation("Open", "A ran");
const stream = new InMemoryStream();
// The same substitution, one step earlier. The resolution this provider
- // answered settled during the capture, so there is no window left to state
- // an identity into — the claim refuses before the witness is consulted, and
- // an answer nothing could identify never reaches the fragment.
+ // answered settled during the capture, so there is no window left to
+ // publish into — the publication refuses before the witness is consulted,
+ // and an answer nobody could have published never reaches the fragment.
const failed = yield* refusal(
run(
OPEN,
[
{
evaluation: admits(),
- componentAnswers: [answerProvider("Open", A.definition, { reclaimsLater: true })],
+ componentAnswers: [answerProvider("Open", A.definition, { publishesLater: true })],
},
],
stream,
@@ -2430,11 +2373,11 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
expect(A.invoked).toEqual([]);
});
- it("FE14: an unidentified, a copied and a mutated answer each refuse", function* () {
+ it("FE14: an unpublished, a copied and a mutated answer each refuse", function* () {
const cases: readonly (readonly [string, ProviderOptions])[] = [
- ["nothing identified it", { unclaimed: true }],
- ["the chain returned a copy of what was claimed", { copied: true }],
- ["the claimed object was edited afterwards", { mutated: true }],
+ ["nobody published it", { unpublished: true }],
+ ["the chain returned a copy of what was published", { copied: true }],
+ ["the published object was edited afterwards", { mutated: true }],
];
for (const [what, options] of cases) {
@@ -2446,7 +2389,7 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
OPEN,
[
{
- evaluation: admits({}),
+ evaluation: admits(),
install: () => useRecordedFiles(files),
componentAnswers: [answerProvider("Open", A.definition, options)],
},
@@ -2455,7 +2398,7 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
),
);
- expect([what, failed.includes("carries no identity")]).toEqual([what, true]);
+ expect([what, failed.includes("no provider published")]).toEqual([what, true]);
// Before the root import, so there is no admission and no body — and the
// document's own effects never started either.
expect([what, admissions(yield* stream.readAll()).length]).toEqual([what, 0]);
@@ -2464,29 +2407,60 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
}
});
- it("FE14: an answer under another identity refuses before any body", function* () {
+ it("FE14: an answer stating forms this engine does not have refuses", function* () {
+ const files = recordedFiles();
+ const stream = new InMemoryStream();
+ // The same reading as the unit row, at the live boundary: the provider
+ // answers honestly with a definition whose stated forms are not a
+ // declaration this engine has, so the spelling it offers cannot be held to
+ // them and the publication refuses before the root import.
const A = implementation("Open", "A ran");
+ const stating = Object.assign({ ...A.definition }, { forms: "paired" });
+
+ const failed = yield* refusal(
+ run(
+ OPEN,
+ [
+ {
+ evaluation: admits(),
+ install: () => useRecordedFiles(files),
+ componentAnswers: [answerProvider("Open", stating)],
+ },
+ ],
+ stream,
+ ),
+ );
+
+ expect(failed).toContain("the accepted declarations are");
+ expect(admissions(yield* stream.readAll())).toHaveLength(0);
+ expect(A.invoked).toEqual([]);
+ expect(files.performed).toEqual([]);
+ });
+
+ it("FE14: an answer offering no generated use of the admitted spelling refuses", function* () {
const files = recordedFiles();
const stream = new InMemoryStream();
+ // The provider answers honestly, with a definition whose own declaration
+ // offers no generated use of the spelling this host admitted. There is no
+ // identity for a continuation to be compared against, so the entry refuses
+ // before the root import rather than running something unaccountable.
+ const A = implementation("Open", "A ran", undefined, null);
const failed = yield* refusal(
run(
OPEN,
[
{
- evaluation: admits({ revision: "1" }),
+ evaluation: admits(),
install: () => useRecordedFiles(files),
- // The provider claims honestly — for a revision this host did not
- // admit. An admitted identity is the exact implementation, and a
- // changed revision is a changed grant.
- componentAnswers: [answerProvider("Open", A.definition, { revision: "2" })],
+ componentAnswers: [answerProvider("Open", A.definition)],
},
],
stream,
),
);
- expect(failed).toContain("the exact implementation");
+ expect(failed).toContain("offers no generated use of it");
expect(admissions(yield* stream.readAll())).toHaveLength(0);
expect(A.invoked).toEqual([]);
});
@@ -2503,27 +2477,24 @@ describe("Tier FE14 — the chain answers, and the answer is held to its identit
const partial = complete.slice(0, admitted + 1);
// The resumed run supplies both implementations, live. The inner provider
- // answers the unchanged A under the identity the admission was made over;
- // the outer one delegates to it and then answers with its own claimed B,
- // under the identity this host now admits. So capture reconciles and seals
- // B — and then the *retained* policy refuses, because the admission was
- // made over `Open@1` and this run states `Other@1` behind the same name.
+ // answers the unchanged A, whose declaration offers the identity the
+ // admission was made over; the outer one delegates to it and then answers
+ // with its own published B, whose declaration offers that spelling under a
+ // different URL. So capture seals B — and then the *retained* policy
+ // refuses, because the admission was made over one URL and this run states
+ // another behind the same name.
const resumedA = implementation("Open", "A ran");
- const B = implementation("Open", "B ran");
+ const B = implementation("Open", "B ran", undefined, offering("Other", "read"));
const delegated: unknown[] = [];
const failed = yield* refusal(
run(
OPEN,
[
{
- evaluation: admits({ key: "Other" }),
+ evaluation: admits(),
componentAnswers: [
- answerProvider("Open", B.definition, {
- key: "Other",
- delegatesFirst: true,
- delegated,
- }),
- answerProvider("Open", resumedA.definition, { key: "Open" }),
+ answerProvider("Open", B.definition, { delegatesFirst: true, delegated }),
+ answerProvider("Open", resumedA.definition),
],
},
],
@@ -2917,7 +2888,7 @@ describe("Tier FE36 — canonical `` is a write a person answers", () =>
expect(wrapped.performed).toEqual([]);
});
- it("EL5: a continuation whose retained Elicit identity moved refuses before any effect", function* () {
+ it("EL5: a continuation that no longer admits Elicit refuses before any effect", function* () {
const files = recordedFiles();
const stream = new InMemoryStream();
yield* scoped(function* () {
@@ -2932,19 +2903,15 @@ describe("Tier FE36 — canonical `` is a write a person answers", () =>
expect(admitted).toBeGreaterThanOrEqual(0);
const partial = complete.slice(0, admitted + 1);
- // The same fragment and the same text, under a table that now states
- // another revision behind the same name.
+ // The same fragment and the same text, under a table that no longer admits
+ // the question at all. A grant is the exact set of spellings it was made
+ // under, so a table missing one is a different grant rather than the same
+ // one restated.
const second = recordedFiles();
const moved: ExecutionInstallation = {
evaluation: {
read: [fileReadEntry()],
- write: [
- fileWriteEntry(),
- {
- ...elicitWriteEntry(),
- identity: { origin: "@executablemd/core", key: "Elicit", revision: "99" },
- },
- ],
+ write: [fileWriteEntry()],
},
install: () => useRecordedFiles(second),
};
diff --git a/packages/core/tests/evaluate-provider-lifetime.test.ts b/packages/core/tests/evaluate-provider-lifetime.test.ts
index 1cf2f5e81..a683014fb 100644
--- a/packages/core/tests/evaluate-provider-lifetime.test.ts
+++ b/packages/core/tests/evaluate-provider-lifetime.test.ts
@@ -1,7 +1,7 @@
/**
* Tier FE15 — what a provider's answer is bound to, at execution scale.
*
- * `answer-identity.test.ts` holds the unit half: an identity belongs to one
+ * `answer-identity.test.ts` holds the unit half: a publication belongs to one
* object in one execution, and a request that outlived its invocation states
* nothing. These rows are the other half — the same guarantees driven through
* a real execution, where the provider is installed by a trusted host, the
@@ -37,24 +37,16 @@ import { prepareEvaluationProfile } from "../src/evaluation-profile.ts";
import type { ComponentAnswerEntry, ResolvedAnswer } from "../src/evaluation-profile.ts";
import { REVOKED_CAPABILITY } from "../src/fragment-capabilities.ts";
import { retainedSource } from "../src/root-source.ts";
-import { answerProvider, implementation } from "./support/answer-provider.ts";
+import { answerProvider, implementation, offering } from "./support/answer-provider.ts";
import type { Implementation } from "./support/answer-provider.ts";
import type { ComponentInvocation } from "../src/invocation-identity.ts";
const ROOT_PATH = "evaluate.md";
const OPEN = ` \\n'} as="answer" />\n\n \n`;
-/** What a provider states for the one name these rows admit. */
-const IDENTITY = { key: "Open", revision: "1" } as const;
-
-/** One provider-backed entry, under the identity this suite's providers state. */
+/** One provider-backed entry, in the one spelling this suite's providers offer. */
function answerEntry(name: string): ComponentAnswerEntry {
- return {
- kind: "component-answer",
- name,
- identity: { origin: "test://provider", key: name, revision: "1" },
- forms: ["self-closing"],
- };
+ return { kind: "component-answer", name, forms: ["self-closing"] };
}
/** The entry a host states for a provider-backed name. */
@@ -76,7 +68,7 @@ function refused(attempt: () => unknown): string {
const message = error instanceof Error ? error.message : String(error);
return message.split(",")[0]?.trim() ?? message;
}
- throw new Error("expected the claim to be refused");
+ throw new Error("expected the publication to be refused");
}
function run(
@@ -97,7 +89,7 @@ function refusalFrom(request: ComponentAnswerRequest | undefined): string {
throw new Error("the provider was never asked");
}
try {
- request.claim(implementation("Open", "late").definition, { key: "Open", revision: "1" });
+ request.answer(implementation("Open", "late").definition);
} catch (error) {
return error instanceof Error ? error.message : String(error);
}
@@ -163,7 +155,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
});
reached.resolve();
yield* suspend();
- return request.claim(A.definition, { key: "Open", revision: "1" });
+ return request.answer(A.definition);
});
},
},
@@ -240,8 +232,8 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
// Two providers under one name. The first is installed outermost, is
// genuinely asked, keeps the request it was handed and delegates without
- // claiming — which is what losing looks like. The second answers, and the
- // capture seals what it claimed.
+ // publishing — which is what losing looks like. The second answers, and
+ // the capture seals what it published.
//
// ` ` runs from the document, *after* the capture and while this
// execution is still live — the window a post-run row cannot reach. So what
@@ -263,10 +255,10 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
// import, and this handler returned before that. The execution is
// still very much alive — so what refuses is the invocation, not
// the teardown.
- attempted.push(refused(() => held.claim(A.definition, IDENTITY)));
+ attempted.push(refused(() => held.answer(A.definition)));
// The same for the winner's own implementation: there is no live
// request to retag it through either.
- attempted.push(refused(() => held.claim(B.definition, IDENTITY)));
+ attempted.push(refused(() => held.answer(B.definition)));
return "meddled";
},
},
@@ -304,7 +296,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
expect(A.invoked).toEqual([]);
});
- it("FE15: a losing handler cannot claim once it has returned, mid-resolution", function* () {
+ it("FE15: a losing handler cannot publish once it has returned, mid-resolution", function* () {
const A = implementation("Open", "A ran");
const B = implementation("Open", "B ran");
const inner: ComponentAnswerRequest[] = [];
@@ -314,7 +306,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
let innerSettled = false;
// The frozen mid-resolution race. The inner provider is asked, keeps its
- // request and returns without claiming; the outer provider is *still
+ // request and returns without publishing; the outer provider is *still
// running* the same resolution, and invokes the inner one's request from
// inside its own handler. The window is open, the name is right, the
// execution is live — and the invocation that would have been speaking is
@@ -324,7 +316,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
evaluation: admits(),
componentAnswers: [
{
- // The origin the profile admits: this is the provider whose claim
+ // The origin the profile admits: this is the provider whose answer
// has to be the one the capture reconciles.
origin: "test://provider",
*install(registrar: ComponentAnswerRegistrar): Operation {
@@ -342,10 +334,10 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
if (held === undefined) {
throw new Error("the inner provider was never asked");
}
- attempted.push(refused(() => held.claim(A.definition, IDENTITY)));
+ attempted.push(refused(() => held.answer(A.definition)));
// And the outer handler's own request is still live, so it may
- // claim its replacement before returning.
- return request.claim(B.definition, { key: "Open", revision: "1" });
+ // publish its replacement before returning.
+ return request.answer(B.definition);
});
},
},
@@ -357,7 +349,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
return yield* next();
}
innerSettled = true;
- // Answers, keeps its request, and claims nothing — so it loses
+ // Answers, keeps its request, and publishes nothing — so it loses
// the decision the outer handler goes on to make.
inner.push(request);
return A.definition;
@@ -370,8 +362,8 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
expect(delegated).toHaveLength(1);
expect(attempted).toEqual(["this resolution has settled"]);
- // The outer replacement is what ran, which is the positive half: claiming
- // after delegating has to keep working.
+ // The outer replacement is what ran, which is the positive half:
+ // publishing after delegating has to keep working.
expect(String(output)).toContain("B ran");
expect(B.invoked).toEqual(["B ran"]);
expect(A.invoked).toEqual([]);
@@ -398,7 +390,6 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
componentAnswers: [
answerProvider("Open", A.definition, { retain: stale }),
answerProvider("Other", B.definition, {
- key: "Other",
whileResolving: () => {
const held = stale[0];
if (held === undefined) {
@@ -408,7 +399,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
// even address the name being decided — and its own invocation
// is over.
expect(held.name).toBe("Open");
- attempted.push(refused(() => held.claim(late.definition, IDENTITY)));
+ attempted.push(refused(() => held.answer(late.definition)));
carried.push(A.definition);
},
}),
@@ -420,7 +411,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
expect(attempted).toEqual(["this resolution has settled"]);
expect(carried).toEqual([A.definition]);
// Both names ran what their own resolution sealed — which is also the
- // positive control that the second provider's own claim, made in the same
+ // positive control that the second provider's own publication, made in the same
// window as the refusal, was admitted.
expect(String(output)).toContain("A ran");
expect(String(output)).toContain("B ran");
@@ -463,10 +454,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
}
answered.push(request.name);
requests.push(request);
- return request.claim(held.definition, {
- key: request.name,
- revision: "1",
- });
+ return request.answer(held.definition);
});
},
},
@@ -501,6 +489,8 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
kind: "function" as const,
name: "Open",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing" as const],
+ generated: offering("Open", "read"),
*fn(): Operation {
first.push("A ran");
entered.resolve();
@@ -542,11 +532,11 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
// sealed profile — which is itself the point. A row that had to add a
// getter to observe this would be proving something about the getter.
const prepared = yield* prepareEvaluationProfile(admits());
- expect(prepared.answered).toEqual([
- { name: "Open", identity: { origin: "test://provider", key: "Open", revision: "1" } },
- ]);
+ expect(prepared.answered).toEqual(["Open"]);
- const answers = new Map([["Open", { definition: A.definition }]]);
+ const answers = new Map([
+ ["Open", { definition: A.definition, origin: "test://provider" }],
+ ]);
const profile = yield* prepared.seal(answers);
const sealed = profile.read[0]?.definition.fn;
if (typeof sealed !== "function") {
@@ -581,7 +571,7 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
expect(String(first)).toContain("A ran");
// The same document and the same profile, with no provider installed. The
- // first run's answer is not somewhere the second can find it: a claim leaves
+ // first run's answer is not somewhere the second can find it: a publication leaves
// no registry, no context and no module-level table behind it, so the name
// resolves to nothing at all rather than to what the first run admitted.
const failed = yield* refusal(run(OPEN, [{ evaluation: admits() }]));
@@ -599,6 +589,8 @@ describe("Tier FE15 — a provider's answer belongs to the execution that captur
kind: "function" as const,
name: "Open",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing" as const],
+ generated: offering("Open", "read"),
*fn(): Operation {
yield* ensure(function* () {
cleanup.push("body cleanup");
diff --git a/packages/core/tests/evaluation-profile.test.ts b/packages/core/tests/evaluation-profile.test.ts
index 291f35b37..73c35e3f3 100644
--- a/packages/core/tests/evaluation-profile.test.ts
+++ b/packages/core/tests/evaluation-profile.test.ts
@@ -28,7 +28,7 @@ import {
syntaxReadEntry,
} from "../src/evaluation-profile.ts";
import { CORE_ORIGIN } from "../src/components/registry.ts";
-import { CORE_REVISION } from "../src/generated-xmd.ts";
+import { SYNTAX_PROTECTED } from "../src/components/Syntax.ts";
import { props as globProps } from "../src/components/Glob.ts";
import { props as elicitProps } from "../src/components/Elicit.ts";
import { capabilityReturns } from "../src/fragment-capabilities.ts";
@@ -38,9 +38,11 @@ import type {
EvaluationProfile,
ComponentAnswerEntry,
FragmentEvaluationInput,
+ FragmentForm,
ResolvedAnswer,
ResolvedAnswers,
} from "../src/evaluation-profile.ts";
+import type { GeneratedForms } from "../src/components/generated-metadata.ts";
import type { Json } from "../src/types.ts";
describe("protected route projection", () => {
@@ -52,6 +54,13 @@ describe("protected route projection", () => {
name,
origin: "test://protected",
props: { type: "object" },
+ forms: ["self-closing" as const],
+ generated: {
+ "self-closing": {
+ identity: `https://executable.md/components/${name.toLowerCase()}/read`,
+ effect: "read" as const,
+ },
+ },
build: (claim) =>
function* (_props, invocation): Operation {
return `${name}:${yield* claim(invocation)}`;
@@ -92,17 +101,10 @@ describe("protected route projection", () => {
throw new Error("missing protected definition");
}
const prepared = yield* prepareEvaluationProfile({
- read: [
- {
- kind: "component-answer",
- name: "Admitted",
- identity: { origin: "test://provider", key: "Admitted", revision: "1" },
- forms: ["self-closing"],
- },
- ],
+ read: [{ kind: "component-answer", name: "Admitted", forms: ["self-closing"] }],
});
const captured = yield* prepared.seal(
- new Map([["Admitted", { definition: original }]]),
+ new Map([["Admitted", { definition: original, origin: "test://protected" }]]),
owner.componentRouting.project,
);
const sealed = captured.read[0]?.definition.fn;
@@ -197,7 +199,6 @@ function entry(overrides: Partial = {}): CapabilityEntry {
return {
kind: "capability",
name,
- identity: { origin: "test://host", key: "File:read", revision: "1" },
forms: ["self-closing"],
props: { type: "object", properties: {}, additionalProperties: false },
// The capability follows the name unless a row states otherwise: what these
@@ -246,7 +247,7 @@ describe("Tier EP — a captured profile stops reading the host's objects", () =
const read: CapabilityEntry[] = [entry()];
const captured = yield* capture({ read });
- read.push(entry({ name: "Added", identity: { origin: "t", key: "Added", revision: "1" } }));
+ read.push(entry({ name: "Added" }));
read.length = 0;
expect(captured.read).toHaveLength(1);
@@ -274,13 +275,16 @@ describe("Tier EP — a captured profile stops reading the host's objects", () =
expect(names).toEqual(["path"]);
});
- it("EP3: an identity the host edits after capture does not change what is compared", function* () {
- const identity = { origin: "test://host", key: "Probe", revision: "1" };
- const captured = yield* capture({ read: [entry({ identity })] });
+ it("EP3: forms the host edits after capture do not widen what is admitted", function* () {
+ const forms: FragmentForm[] = ["self-closing"];
+ const captured = yield* capture({ read: [entry({ forms })] });
- identity.revision = "2";
+ // The host's own array, pushed to after installation. Capture read it once
+ // and kept its own ordered copy, so a spelling added afterwards is added to
+ // an object nothing is looking at.
+ forms.push("paired");
- expect(captured.read[0]?.identity.revision).toBe("1");
+ expect(captured.read[0]?.forms).toEqual(["self-closing"]);
});
it("EP4: request headers and lists the host mutates do not widen the ceiling", function* () {
@@ -372,17 +376,13 @@ describe("Tier EP — a captured profile stops reading the host's objects", () =
const captured = yield* capture({
read: [
entry({ name: "First", requests: [{ url: "https://api.example.test/first" }] }),
- entry({
- name: "Second",
- identity: { origin: "test://host", key: "Second", revision: "1" },
- requests: [{ url: "https://api.example.test/second" }],
- }),
+ entry({ name: "Second", requests: [{ url: "https://api.example.test/second" }] }),
],
fetchTimeout: 1000,
});
// Flattening them would let the first entry's limit admit the second's
- // request: a ceiling belongs to the identity it bounds.
+ // request: a ceiling belongs to the entry that states it.
expect(captured.read[0]?.requests?.map((request) => request.url)).toEqual([
"https://api.example.test/first",
]);
@@ -429,15 +429,21 @@ describe("Tier EP — a captured profile stops reading the host's objects", () =
});
describe("Tier EP — a profile a host cannot state", () => {
- it("EP12: an entry with no complete identity refuses", function* () {
- const attempts: Partial[] = [
- { identity: { origin: "", key: "Probe", revision: "1" } },
- { identity: { origin: "test://host", key: "", revision: "1" } },
- { identity: { origin: "test://host", key: "Probe", revision: "" } },
- ];
- for (const attempt of attempts) {
- expect(yield* refusal({ read: [entry(attempt)] })).toContain("complete identity");
- }
+ it("EP12: an entry admitted in the table its owner does not offer refuses", function* () {
+ // What a spelling does belongs to the component. `` offers its paired
+ // form as a write, so admitting that spelling in the read table would hand
+ // a read selection a mutation — and `` offers no paired form
+ // at all, so admitting one would admit a spelling with no identity for a
+ // continuation to be compared against.
+ const wrongTable = yield* refusal({
+ read: [entry({ name: "File", capability: "file:write", forms: ["paired"] })],
+ });
+ expect(wrongTable).toContain("its owner offers that form as a write");
+
+ const unoffered = yield* refusal({
+ read: [entry({ name: "File.Delete", capability: "file:delete", forms: ["paired"] })],
+ });
+ expect(unoffered).toContain("offers no generated use of it");
});
it("EP13: an entry admitted for no form refuses", function* () {
@@ -479,32 +485,45 @@ describe("Tier EP — a profile a host cannot state", () => {
const captured = yield* capture({ read: [fileReadEntry(), globReadEntry()] });
const [file, glob] = captured.read;
- expect(file?.identity).toEqual({ origin: CORE_ORIGIN, key: "File:read", revision: "2" });
+ // The identity each spelling runs under is on the definition, where core's
+ // own declaration states it: the reading form observes and the writing form
+ // mutates, and one admitted entry carries both because one component does.
+ expect(file?.definition.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/file/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/file/write", effect: "write" },
+ });
+ expect(file?.origin).toBe(CORE_ORIGIN);
expect(file?.forms).toEqual(["self-closing"]);
expect(file?.definition.returns).toBe(undefined);
// The search is the same component an author writes: the ordinary props,
// the one spelling, and a declared return — which is what makes `as`
// mandatory before the host's provider is asked to traverse anything.
- expect(glob?.identity).toEqual({ origin: CORE_ORIGIN, key: "Glob", revision: "2" });
+ expect(glob?.definition.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/glob/read", effect: "read" },
+ });
expect(glob?.forms).toEqual(["self-closing"]);
expect(glob?.props).toEqual(globProps);
expect(glob?.definition.returns).toEqual({ type: "array", items: { type: "string" } });
- // A search is not an older grant renamed: nothing was ever retained under
- // a version-1 identity for it, so it answers for none.
- expect(glob?.legacy).toBe(undefined);
+ // One spelling, so one offered form: nothing admits a paired ``.
+ expect(Object.keys(glob?.definition.generated ?? {})).toEqual(["self-closing"]);
});
- it("EP22: canonical Syntax is admitted as an answer at core's own identity", function* () {
+ it("EP22: canonical Syntax is admitted as an answer, and states no identity", function* () {
const stated = syntaxReadEntry();
expect(stated.kind).toBe("component-answer");
expect(stated.name).toBe("Syntax");
- expect(stated.identity).toEqual({ origin: CORE_ORIGIN, key: "Syntax", revision: "2" });
expect(stated.forms).toEqual(["self-closing"]);
- // The arm carries no implementation at all: what is behind a protected name
- // is canonical resolution's to answer, not a host's to hand over.
+ // The arm carries no implementation and no identity at all: what is behind
+ // a protected name, and what admitting its one spelling grants, are
+ // canonical resolution's to answer rather than a host's to hand over.
expect(Object.keys(stated)).not.toContain("capability");
+ expect(Object.keys(stated)).not.toContain("identity");
+ // Core's own declaration is where that spelling's identity and effect are.
+ expect(SYNTAX_PROTECTED.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/syntax/read", effect: "read" },
+ });
});
it("EP23: one name admitted for two different components refuses", function* () {
@@ -525,12 +544,41 @@ describe("Tier EP — a profile a host cannot state", () => {
});
it("EP16: forms are canonically ordered, so two statements of one pair match", function* () {
- const one = yield* capture({
- read: [entry({ forms: ["paired", "self-closing"] })],
- });
- const other = yield* capture({
- read: [entry({ forms: ["self-closing", "paired"] })],
- });
+ // A provider whose component observes in both spellings, which is what a
+ // form-insensitive read is: both are offered as reads, so both may be
+ // admitted in the read table and the order the host wrote them in is not
+ // part of what it stated.
+ const both: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/pair/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/pair/also", effect: "read" },
+ };
+ const answers: ResolvedAnswers = new Map([
+ [
+ "Pair",
+ {
+ origin: "test://provider",
+ definition: {
+ kind: "function" as const,
+ name: "Pair",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ generated: both,
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ return "paired";
+ },
+ },
+ },
+ ],
+ ]);
+ const admitting = function* (forms: readonly FragmentForm[]): Operation {
+ const prepared = yield* prepareEvaluationProfile({
+ read: [{ kind: "component-answer", name: "Pair", forms }],
+ });
+ return yield* prepared.seal(answers);
+ };
+
+ const one = yield* admitting(["paired", "self-closing"]);
+ const other = yield* admitting(["self-closing", "paired"]);
expect(one.read[0]?.forms).toEqual(other.read[0]?.forms);
expect(one.read[0]?.forms).toEqual(["self-closing", "paired"]);
@@ -543,27 +591,34 @@ describe("Tier EP — a profile a host cannot state", () => {
* A provider-backed name is resolved once through the ordinary import chain and
* sealed. So what a profile may say about a name is settled here rather than at
* the lookup: two entries under one name are the two spellings of one
- * component, and a second identity for the second spelling would make which
- * implementation a fragment reached depend on which table admitted it.
+ * component, each reading its own identity and effect off the one definition
+ * the chain answered with.
*/
-describe("Tier EP — a provider-backed name states one identity", () => {
+describe("Tier EP — a provider-backed name resolves once to one definition", () => {
const OPEN: ComponentAnswerEntry = {
kind: "component-answer",
name: "Open",
- identity: { origin: "test://provider", key: "Open", revision: "1" },
forms: ["self-closing"],
};
- /** One implementation, as canonical execution hands sealing its answer. */
- function answered(): ResolvedAnswers {
+ /**
+ * One implementation, as canonical execution hands sealing its answer.
+ *
+ * It offers both spellings, each under its own identity and effect, because
+ * that is what a provider whose component reads one way and writes the other
+ * declares — and it is one definition doing it.
+ */
+ function answered(generated: GeneratedForms = BOTH_FORMS): ResolvedAnswers {
return new Map([
[
"Open",
{
+ origin: "test://provider",
definition: {
kind: "function",
name: "Open",
props: { type: "object", properties: {}, additionalProperties: false },
+ generated,
// deno-lint-ignore require-yield
*fn(): Operation {
return "opened";
@@ -574,11 +629,30 @@ describe("Tier EP — a provider-backed name states one identity", () => {
]);
}
+ /** What that provider's declaration offers each of its two spellings as. */
+ const BOTH_FORMS: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/open/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/open/write", effect: "write" },
+ };
+
/** Prepare a profile holding these entries, without sealing it. */
function prepare(input: Partial) {
return prepareEvaluationProfile({ read: [entry()], ...input });
}
+ /** What sealing this prepared profile refused with. */
+ function* sealing(
+ prepared: { seal: (answers: ResolvedAnswers) => Operation },
+ answers: ResolvedAnswers,
+ ): Operation {
+ try {
+ yield* prepared.seal(answers);
+ } catch (error) {
+ return error instanceof Error ? error.message : String(error);
+ }
+ throw new Error("expected sealing to be refused");
+ }
+
/** What preparing this profile refused with. */
function* refused(input: Partial): Operation {
try {
@@ -592,8 +666,7 @@ describe("Tier EP — a provider-backed name states one identity", () => {
it("EP18: one name across disjoint forms and tables is one lookup and one implementation", function* () {
// The self-closing spelling admitted to observe and the paired one to
// mutate: two entries, two forms, two tables, one component. The host says
- // the same identity for both, because there is only one thing behind the
- // name.
+ // the name, and the definition says what each spelling runs as.
const prepared = yield* prepare({
read: [OPEN],
write: [{ ...OPEN, forms: ["paired"] }],
@@ -601,15 +674,18 @@ describe("Tier EP — a provider-backed name states one identity", () => {
// Asked for once, however many entries hold it: a second lookup would be a
// second chance for the chain to answer differently.
- expect(prepared.answered).toEqual([
- { name: "Open", identity: { origin: "test://provider", key: "Open", revision: "1" } },
- ]);
+ expect(prepared.answered).toEqual(["Open"]);
const profile = yield* prepared.seal(answered());
const observing = profile.read[0];
const mutating = profile.write[0];
expect(observing?.forms).toEqual(["self-closing"]);
expect(mutating?.forms).toEqual(["paired"]);
+ // Two URLs and two effects, read off the one definition: the reading
+ // spelling and the writing one are different grants, and the implementation
+ // behind them is the same object.
+ expect(observing?.definition.generated).toEqual(BOTH_FORMS);
+ expect(observing?.origin).toBe("test://provider");
// One sealed implementation, shared. Two guards over one answer would be
// two lifetimes for one implementation, and which of them a fragment
// reached would depend on which table admitted it.
@@ -617,33 +693,25 @@ describe("Tier EP — a provider-backed name states one identity", () => {
expect(observing?.props).toBe(mutating?.props);
});
- it("EP19: a second identity for one name refuses, naming both", function* () {
- const cases: readonly (readonly [string, Partial])[] = [
- [
- "within one table",
- { read: [OPEN, { ...OPEN, identity: { ...OPEN.identity, revision: "2" } }] },
- ],
- [
- "across the two tables",
- {
- read: [OPEN],
- write: [{ ...OPEN, forms: ["paired"], identity: { ...OPEN.identity, key: "Other" } }],
- },
- ],
- [
- "under another origin",
- { read: [OPEN, { ...OPEN, identity: { ...OPEN.identity, origin: "test://other" } }] },
- ],
- ];
-
- for (const [where, input] of cases) {
- const failed = yield* refused(input);
- // Refused rather than resolved by position: a last-stated identity
- // winning would be a ceiling decided by the order a host assembled its
- // tables in.
- expect([where, failed.includes("One name states one identity")]).toEqual([where, true]);
- expect([where, failed.includes("test://provider#Open@1")]).toEqual([where, true]);
- }
+ it("EP19: an answer that does not offer the admitted spelling refuses at sealing", function* () {
+ // The implementation is resolved once and then held to what its own
+ // declaration offers. A host cannot describe a spelling the provider never
+ // offered, and cannot move one into the other effect table: either would be
+ // a grant the implementation does not have, and both refuse before an
+ // execution holds a profile.
+ const prepared = yield* prepare({ read: [OPEN], write: [{ ...OPEN, forms: ["paired"] }] });
+ const readOnly: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/open/read", effect: "read" },
+ };
+ expect(yield* sealing(prepared, answered(readOnly))).toContain("offers no generated use of it");
+
+ const bothRead: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/open/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/open/also", effect: "read" },
+ };
+ expect(yield* sealing(prepared, answered(bothRead))).toContain(
+ "its owner offers that form as a read",
+ );
});
it("EP20: one name held as both a capability and an answer refuses", function* () {
@@ -659,7 +727,7 @@ describe("Tier EP — a provider-backed name states one identity", () => {
});
describe("Tier EP — the entry that admits canonical ``", () => {
- it("EL1: the factory pins core's own body under core's identity, paired only", function* () {
+ it("EL1: the factory pins core's own body, paired only", function* () {
const entry = elicitWriteEntry();
// A capability rather than a component answer: what core owns here is the
@@ -671,11 +739,7 @@ describe("Tier EP — the entry that admits canonical ``", () => {
expect(entry.kind).toBe("capability");
expect(entry.capability).toBe("elicit:ask");
expect(entry.name).toBe("Elicit");
- expect(entry.identity).toEqual({
- origin: CORE_ORIGIN,
- key: "Elicit",
- revision: CORE_REVISION,
- });
+ expect(Object.keys(entry)).not.toContain("identity");
// Paired only. The question is the content, so a self-closing spelling
// would be a request with nothing in it.
expect(entry.forms).toEqual(["paired"]);
@@ -689,9 +753,6 @@ describe("Tier EP — the entry that admits canonical ``", () => {
// execution resolves eagerly, which is what stops an execution that never
// asks anything from depending on what `Elicit` resolves to.
expect(CANONICAL_PROFILE_ANSWERS.has("Elicit")).toBe(false);
- // A new grant: no version-1 record ever admitted a generated question, so
- // there is no older string this says it authorizes no more than.
- expect(entry.legacy).toBe(undefined);
// Two factories state the same entry and share nothing: a host that edited
// what it was given would be editing its own copy.
diff --git a/packages/core/tests/generated-composition.test.ts b/packages/core/tests/generated-composition.test.ts
index e180df3cc..b28613657 100644
--- a/packages/core/tests/generated-composition.test.ts
+++ b/packages/core/tests/generated-composition.test.ts
@@ -353,6 +353,10 @@ function unwinding(log: string[]): FunctionComponentDefinition {
kind: "function",
name: "Held",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/held/read", effect: "read" },
+ },
*fn() {
// Registered before anything the invocation could fail at, so the cleanup
// is established rather than merely queued behind a success.
@@ -369,14 +373,7 @@ function unwinding(log: string[]): FunctionComponentDefinition {
function holding(definition: FunctionComponentDefinition): ExecutionInstallation {
return {
evaluation: {
- read: [
- {
- kind: "component-answer",
- name: "Held",
- identity: { origin: "test://provider", key: "Held", revision: "1" },
- forms: ["self-closing"],
- },
- ],
+ read: [{ kind: "component-answer", name: "Held", forms: ["self-closing"] }],
},
componentAnswers: [answerProvider("Held", definition)],
};
diff --git a/packages/core/tests/generated-owner-metadata.test.ts b/packages/core/tests/generated-owner-metadata.test.ts
new file mode 100644
index 000000000..9c1b07658
--- /dev/null
+++ b/packages/core/tests/generated-owner-metadata.test.ts
@@ -0,0 +1,719 @@
+/**
+ * Tier GOM — what a declaration says about the generated use of its own forms
+ * (spec §§5.3, 5.6, 6.13).
+ *
+ * A generated fragment names a component, and two facts decide what naming it
+ * grants: which URL a continuation is compared against, and whether the
+ * spelling observes or mutates. Both belong to the declaration that owns the
+ * implementation, so this tier is about the declaration — the shapes it may
+ * state, the shapes it may not, and what survives construction.
+ *
+ * Three halves, failing in different places:
+ *
+ * **Validation.** A declaration crosses a package boundary, so the metadata is
+ * read the way durable input is read: exactly two members, an identity in the
+ * accepted HTTPS namespace, one of the two effects, and a spelling the
+ * declaration itself accepts. A refused registration batch installs nothing.
+ *
+ * **The shipped owners.** ``, ``, ``, ``,
+ * ``, ` ` and `` state exactly the table the
+ * specification does, with ``'s two spellings differing on one
+ * definition. `` stays trusted composition and `` offers
+ * nothing, because carrying metadata grants nothing and evaluation inside
+ * evaluation is not something a table may name.
+ *
+ * **Retention.** The shared fixed definitions are frozen through their nested
+ * metadata, so a change left behind by one execution is not what the next one
+ * takes its first copy of.
+ */
+
+import { describe, it } from "@executablemd/test-support/bdd";
+import { expect } from "@executablemd/test-support/expect";
+import { InMemoryStream } from "@executablemd/durable-streams";
+import { scoped } from "effection";
+import type { Operation } from "effection";
+
+import { collect } from "../src/collect.ts";
+import { executeInstalled } from "../host.ts";
+import type { ComponentAnswerInstallation, ExecutionInstallation } from "../host.ts";
+import { retainedSource } from "../src/root-source.ts";
+import type { IdentityComponent } from "../src/invocation-identity.ts";
+import { Component } from "../src/component-api.ts";
+import { registerComponents } from "../src/components/registration.ts";
+import type { ComponentRegistration } from "../src/components/registration.ts";
+import {
+ admitGeneratedForms,
+ definitionGeneratedRefusal,
+ generatedRefusal,
+ isGeneratedIdentity,
+ offeredForm,
+} from "../src/components/generated-metadata.ts";
+import type { GeneratedForms } from "../src/components/generated-metadata.ts";
+import { fixedFilesystemDefinition } from "../src/components/fixed-filesystem.ts";
+import { coreComponentDefinition } from "../src/components/registry.ts";
+import { SYNTAX_PROTECTED } from "../src/components/Syntax.ts";
+import { EVALUATE_PROTECTED } from "../src/components/Evaluate.ts";
+import { pinnedJson } from "../src/generated-xmd.ts";
+import type { ComponentRegistry, FunctionComponentDefinition, Json } from "../src/types.ts";
+
+/** The registry visible in the current scope. */
+function currentRegistry(): Operation {
+ return Component.operations.registry;
+}
+
+/** One registration, with whatever one row states about its generated forms. */
+function registration(overrides: Partial = {}): ComponentRegistration {
+ return {
+ name: "Probe",
+ origin: "test://owner",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ return "probed";
+ },
+ ...overrides,
+ };
+}
+
+/** What registering this batch refused with, or nothing when it installed. */
+function* refusal(registrations: readonly ComponentRegistration[]): Operation {
+ try {
+ yield* registerComponents([...registrations]);
+ } catch (error) {
+ return error instanceof Error ? error.message : String(error);
+ }
+ return undefined;
+}
+
+/** The metadata on the definition this name resolves to in the current scope. */
+function* registered(name: string): Operation {
+ const entry = (yield* currentRegistry()).get(name)?.default?.definition;
+ return entry === undefined || entry.kind !== "function" ? undefined : entry.generated;
+}
+
+const READ_PROBE: GeneratedForms = {
+ "self-closing": { identity: "https://executable.md/components/probe/read", effect: "read" },
+};
+
+describe("Tier GOM — a declaration states what its spellings are offered as", () => {
+ it("GOM1: an accepted map survives registration, copied and frozen", function* () {
+ expect(yield* refusal([registration({ generated: READ_PROBE })])).toBe(undefined);
+
+ const held = yield* registered("Probe");
+ expect(held).toEqual(READ_PROBE);
+ // Copied and frozen through, so the declarer's own object is not what the
+ // engine reads and a nested record cannot be edited afterwards.
+ expect(held).not.toBe(READ_PROBE);
+ expect(Object.isFrozen(held)).toBe(true);
+ expect(Object.isFrozen(offeredForm(held, "self-closing"))).toBe(true);
+ });
+
+ it("GOM1: a component offering nothing registers and keeps its ordinary use", function* () {
+ expect(yield* refusal([registration()])).toBe(undefined);
+
+ // Omission is the ordinary case: metadata is what makes a spelling
+ // offerable to a generated fragment, and offering none takes nothing away
+ // from the authored element.
+ expect(yield* registered("Probe")).toBe(undefined);
+ const entry = (yield* currentRegistry()).get("Probe");
+ expect(entry?.default?.origin).toBe("test://owner");
+ });
+
+ it("GOM1: an empty map offers no spelling, and still registers", function* () {
+ expect(yield* refusal([registration({ generated: {} })])).toBe(undefined);
+
+ const held = yield* registered("Probe");
+ expect(held).toEqual({});
+ expect(offeredForm(held, "self-closing")).toBe(undefined);
+ expect(offeredForm(held, "paired")).toBe(undefined);
+ });
+
+ it("GOM1: a malformed map refuses, and a refused batch installs nothing", function* () {
+ // Each of these is a declaration this engine cannot read: an identity that
+ // is not an absolute URL in the accepted namespace, a look-alike origin, an
+ // effect it does not have, a key that is not an authored form, a value that
+ // is not an identity and an effect, and a spelling the declaration itself
+ // does not accept.
+ const attempts: readonly (readonly [string, unknown, readonly string[] | undefined])[] = [
+ [
+ "a relative identity",
+ { "self-closing": { identity: "/probe", effect: "read" } },
+ undefined,
+ ],
+ [
+ "a look-alike origin",
+ {
+ "self-closing": {
+ identity: "https://executable.md.example.com/components/probe/read",
+ effect: "read",
+ },
+ },
+ undefined,
+ ],
+ [
+ "an identity that is not a URL at all",
+ { "self-closing": { identity: "probe", effect: "read" } },
+ undefined,
+ ],
+ [
+ "an effect this engine does not have",
+ {
+ "self-closing": {
+ identity: "https://executable.md/components/probe/read",
+ effect: "ask",
+ },
+ },
+ undefined,
+ ],
+ [
+ "a key that is not an authored form",
+ { either: { identity: "https://executable.md/components/probe/read", effect: "read" } },
+ undefined,
+ ],
+ ["a value that is not a record", { "self-closing": "read" }, undefined],
+ [
+ "a value carrying a member this engine does not know",
+ {
+ "self-closing": {
+ identity: "https://executable.md/components/probe/read",
+ effect: "read",
+ widened: true,
+ },
+ },
+ undefined,
+ ],
+ ["a map that is not a map", ["read"], undefined],
+ [
+ "a spelling the declaration does not accept",
+ { paired: { identity: "https://executable.md/components/probe/write", effect: "write" } },
+ ["self-closing"],
+ ],
+ ];
+
+ for (const [what, generated, forms] of attempts) {
+ yield* scoped(function* () {
+ const declared = Object.assign(
+ registration(forms === undefined ? {} : { forms: ["self-closing"] }),
+ { generated },
+ );
+ // Registered beside a component that is perfectly fine, so the row says
+ // the batch installed nothing rather than only that one entry refused.
+ const failed = yield* refusal([
+ registration({ name: "Fine", generated: READ_PROBE }),
+ declared,
+ ]);
+ expect([what, failed !== undefined]).toEqual([what, true]);
+ expect([what, yield* registered("Probe")]).toEqual([what, undefined]);
+ expect([what, yield* registered("Fine")]).toEqual([what, undefined]);
+ });
+ }
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM1: an identity is admitted by its origin rather than by its text", function* () {
+ expect(isGeneratedIdentity("https://executable.md/components/file/read")).toBe(true);
+ // Each of these starts with the accepted text and names somebody else, so a
+ // check written as a prefix comparison would admit all three.
+ expect(isGeneratedIdentity("https://executable.md.example.com/components/file/read")).toBe(
+ false,
+ );
+ expect(
+ isGeneratedIdentity("https://executable.md@elsewhere.example/components/file/read"),
+ ).toBe(false);
+ expect(isGeneratedIdentity("http://executable.md/components/file/read")).toBe(false);
+ expect(isGeneratedIdentity("components/file/read")).toBe(false);
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM1: a definition that never passed registration is read the same way", function* () {
+ // A provider's answer is a definition no registration validated, so its
+ // metadata is read off the object defensively — including the forms it
+ // states, because a definition offering a spelling it does not accept
+ // offers nothing that can run.
+ const offered: FunctionComponentDefinition = {
+ kind: "function",
+ name: "Probe",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: READ_PROBE,
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ return "probed";
+ },
+ };
+ expect(definitionGeneratedRefusal(offered)).toBe(undefined);
+ expect(definitionGeneratedRefusal({ ...offered, generated: {} })).toBe(undefined);
+ expect(definitionGeneratedRefusal({ kind: "function", name: "Probe" })).toBe(undefined);
+ expect(
+ definitionGeneratedRefusal(
+ Object.assign(
+ { ...offered },
+ {
+ generated: { paired: { identity: "https://executable.md/x", effect: "write" } },
+ },
+ ),
+ ),
+ ).toContain("does not accept");
+ expect(
+ definitionGeneratedRefusal(
+ Object.assign({ ...offered }, { generated: { "self-closing": 1 } }),
+ ),
+ ).toContain("not an identity and an effect");
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM1: an accessor is read once, and what was validated is what is retained", function* () {
+ // The check/use gap at the declaration boundary. The identity answers the
+ // accepted URL the first time it is read and `not-a-url` afterwards, so a
+ // registration that validated the declarer's object and then copied it
+ // again would install a value the check never saw.
+ const reads: string[] = [];
+ const alternating = {
+ "self-closing": {
+ get identity(): string {
+ reads.push("identity");
+ return reads.length === 1 ? "https://executable.md/components/probe/read" : "not-a-url";
+ },
+ effect: "read" as const,
+ },
+ };
+
+ yield* scoped(function* () {
+ expect(yield* refusal([registration({ generated: alternating })])).toBe(undefined);
+
+ // One read, and the registry holds exactly it. The plant is live rather
+ // than inert: the very next read of the declarer's object says something
+ // else, which is what a second reading would have installed.
+ expect(reads).toEqual(["identity"]);
+ expect(yield* registered("Probe")).toEqual({
+ "self-closing": { identity: "https://executable.md/components/probe/read", effect: "read" },
+ });
+ expect(alternating["self-closing"].identity).toBe("not-a-url");
+ });
+ });
+
+ it("GOM1: an accessor answering an unusable value first refuses the batch", function* () {
+ // The other side of the same read. What the one reading says is what the
+ // declaration states, so an accessor whose first answer is unusable refuses
+ // — and the batch installs nothing, however plausible a later reading is.
+ const reads: string[] = [];
+ const alternating = {
+ "self-closing": {
+ get identity(): string {
+ reads.push("identity");
+ return reads.length === 1 ? "not-a-url" : "https://executable.md/components/probe/read";
+ },
+ effect: "read" as const,
+ },
+ };
+
+ yield* scoped(function* () {
+ const failed = yield* refusal([
+ registration({ name: "Fine", generated: READ_PROBE }),
+ registration({ generated: alternating }),
+ ]);
+ expect(failed).toContain("not an absolute URL");
+ expect(reads).toEqual(["identity"]);
+ expect(yield* registered("Probe")).toBe(undefined);
+ expect(yield* registered("Fine")).toBe(undefined);
+ });
+ });
+
+ it("GOM1: malformed stated forms are refused rather than filtered", function* () {
+ // Only genuine omission means both forms. A value that is not a canonical
+ // declaration cannot be read as one by dropping what it says: a string, an
+ // unknown member, an empty list and a reversed pair each describe a
+ // declaration this engine does not have, so a definition offering generated
+ // use of itself alongside one of them offers nothing it can be held to.
+ const offered = {
+ kind: "function",
+ name: "Probe",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ generated: READ_PROBE,
+ };
+ for (const forms of ["paired", ["self-closing", "unknown"], [], ["paired", "self-closing"]]) {
+ expect([forms, definitionGeneratedRefusal({ ...offered, forms })]).toEqual([
+ forms,
+ expect.stringContaining("the accepted declarations are"),
+ ]);
+ }
+ // Omission is the one reading that widens anything, and a canonical
+ // declaration holding the offered spelling is accepted.
+ expect(definitionGeneratedRefusal(offered)).toBe(undefined);
+ expect(definitionGeneratedRefusal({ ...offered, forms: ["self-closing"] })).toBe(undefined);
+ // And a definition offering nothing is not held to its forms at all, because
+ // it is asking for nothing identity or effect decides.
+ expect(definitionGeneratedRefusal({ kind: "function", name: "Probe", forms: "paired" })).toBe(
+ undefined,
+ );
+ });
+
+ it("GOM1: the refusal is a sentence a caller words for itself", function* () {
+ // A sentence rather than a thrown error, because a registration, a factory
+ // declaration and a provider's published answer each refuse in their own
+ // error family and with their own diagnostic context.
+ expect(generatedRefusal(undefined, undefined)).toBe(undefined);
+ expect(generatedRefusal(READ_PROBE, undefined)).toBe(undefined);
+ expect(generatedRefusal(READ_PROBE, ["paired"])).toContain("does not accept");
+ // The copy and the verdict are one answer, so a caller retains exactly what
+ // was validated — frozen through, because a shared definition is read by
+ // every execution in the process.
+ const admitted = admitGeneratedForms(READ_PROBE, undefined);
+ expect(admitted.ok).toBe(true);
+ expect(admitted.ok ? admitted.value : undefined).toEqual(READ_PROBE);
+ expect(Object.isFrozen(admitted.ok ? admitted.value : undefined)).toBe(true);
+ expect(admitGeneratedForms("read", undefined).ok).toBe(false);
+ });
+});
+
+describe("Tier GOM — the owners of the shipped generated forms", () => {
+ /** Exactly what the specification's owner table states, read off each owner. */
+ const OWNERS: readonly (readonly [string, GeneratedForms])[] = [
+ [
+ "File",
+ {
+ "self-closing": { identity: "https://executable.md/components/file/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/file/write", effect: "write" },
+ },
+ ],
+ [
+ "File.Delete",
+ {
+ "self-closing": {
+ identity: "https://executable.md/components/file/delete",
+ effect: "write",
+ },
+ },
+ ],
+ [
+ "Dir",
+ { paired: { identity: "https://executable.md/components/dir/ensure", effect: "write" } },
+ ],
+ ];
+
+ for (const [name, generated] of OWNERS) {
+ // deno-lint-ignore require-yield
+ it(`GOM2: core's fixed <${name}> offers exactly its own spellings`, function* () {
+ const definition = fixedFilesystemDefinition(name);
+ expect(definition?.generated).toEqual(generated);
+ });
+ }
+
+ // deno-lint-ignore require-yield
+ it("GOM2: File's two spellings differ on one definition", function* () {
+ const definition = fixedFilesystemDefinition("File");
+ const reading = offeredForm(definition?.generated, "self-closing");
+ const writing = offeredForm(definition?.generated, "paired");
+
+ // One component, one definition, two grants: the identity a continuation is
+ // compared against and the effect a table admits it in both come from the
+ // spelling rather than from the name.
+ expect(reading?.effect).toBe("read");
+ expect(writing?.effect).toBe("write");
+ expect(reading?.identity).not.toBe(writing?.identity);
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM2: the registry's own offered components state their one spelling each", function* () {
+ expect(coreComponentDefinition("Glob")?.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/glob/read", effect: "read" },
+ });
+ expect(coreComponentDefinition("Fetch")?.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/fetch/read", effect: "read" },
+ });
+ expect(coreComponentDefinition("Elicit")?.generated).toEqual({
+ paired: { identity: "https://executable.md/components/elicit/ask", effect: "write" },
+ });
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM2: canonical Syntax offers a read, and Evaluate offers nothing", function* () {
+ expect(SYNTAX_PROTECTED.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/syntax/read", effect: "read" },
+ });
+ // Evaluation inside evaluation is not a grant a table may name, so the
+ // declaration offers no spelling at all — and carrying metadata would not
+ // have admitted it either.
+ expect(EVALUATE_PROTECTED.generated).toBe(undefined);
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM2: Json stays trusted composition, with no read or write form", function* () {
+ // Rendering a value it already holds performs nothing, so there is no
+ // effect for an owner to offer. The composition table states core's own
+ // identity for it, and that is the one admitted identity not read off an
+ // owner's metadata.
+ expect(coreComponentDefinition("Json")?.generated).toBe(undefined);
+ expect(pinnedJson().identity).toBe("https://executable.md/components/json");
+ });
+
+ // deno-lint-ignore require-yield
+ it("GOM2: every other core default offers nothing", function* () {
+ for (const name of ["TempDir", "Fail", "CodeBlock", "Parse", "SafeParse", "Test"]) {
+ expect([name, coreComponentDefinition(name)?.generated]).toEqual([name, undefined]);
+ }
+ });
+});
+
+describe("Tier GOM — a shared definition's metadata cannot be edited", () => {
+ // deno-lint-ignore require-yield
+ it("GOM3: a fixed definition's nested metadata is frozen through", function* () {
+ const definition = fixedFilesystemDefinition("File");
+ const generated = definition?.generated;
+ const reading = offeredForm(generated, "self-closing");
+ if (definition === undefined || generated === undefined || reading === undefined) {
+ throw new Error("core supplies no File metadata");
+ }
+
+ // This object is shared by every execution in the process, so a change left
+ // behind by one would be what the next one takes its own first copy of —
+ // which the per-execution comparison cannot catch. Freezing is what the
+ // sharing requires.
+ expect(Object.isFrozen(definition)).toBe(true);
+ expect(Object.isFrozen(generated)).toBe(true);
+ expect(Object.isFrozen(reading)).toBe(true);
+ expect(() => {
+ Object.assign(reading, { effect: "write" });
+ }).toThrow();
+ expect(() => {
+ Object.assign(generated, { paired: reading });
+ }).toThrow();
+
+ // And a fresh read sees the original contract rather than an attempted one.
+ expect(offeredForm(fixedFilesystemDefinition("File")?.generated, "self-closing")).toEqual({
+ identity: "https://executable.md/components/file/read",
+ effect: "read",
+ });
+ });
+});
+
+/**
+ * Tier GOM4 — a declared component's offer survives trusted capture.
+ *
+ * The other declaration path. A host may declare a component to the execution
+ * instead of registering one, and the execution builds the implementation from
+ * that declaration's factory — so the offer has to cross capture, which reads
+ * every member of the host's object once and holds what it read. A member read
+ * later would be read after an installation ran, and this is the member whose
+ * reading is a grant: a URL and an effect decide which implementation a
+ * continuation admits.
+ *
+ * Every row drives a real execution: the host declares the component, the
+ * execution builds it, a provider publishes that exact built object, and the
+ * fragment names it in the spelling its declaration offers. What the rows read
+ * is the admission the run retained, because a refusal assertion would pass for
+ * a run that admitted the wrong URL and happened to fail later.
+ */
+describe("Tier GOM — a declared component's offer survives capture", () => {
+ const ROOT_PATH = "owner.md";
+ /** A document naming the declared component in the spelling it offers. */
+ const NAMING_PROBE = ` \\n'} />\n`;
+ /** What the host's object says after capture has taken its reading. */
+ const SUBSTITUTE: GeneratedForms = {
+ "self-closing": {
+ identity: "https://executable.md/components/probe/substituted",
+ effect: "read",
+ },
+ };
+
+ /** One component a host declares, whose implementation its factory builds. */
+ function declaredProbe(generated: GeneratedForms | undefined): IdentityComponent {
+ return {
+ name: "Probe",
+ origin: "test://owner",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ ...(generated === undefined ? {} : { generated }),
+ factory() {
+ // deno-lint-ignore require-yield
+ return function* (): Operation {
+ return "the factory body ran";
+ };
+ },
+ };
+ }
+
+ /**
+ * A provider publishing the implementation this execution built.
+ *
+ * It holds no definition of its own: what a host-declared component resolves
+ * to is the object the execution built from the factory, so the provider
+ * reads that from the registry and publishes it. A row reads the published
+ * object to say what construction kept.
+ */
+ function publishing(published: FunctionComponentDefinition[]): ComponentAnswerInstallation {
+ let settled = false;
+ return {
+ origin: "test://publisher",
+ *install(registrar) {
+ yield* registrar.around(function* (request, next) {
+ if (request.name !== "Probe" || settled) {
+ return yield* next();
+ }
+ settled = true;
+ const built = (yield* currentRegistry()).get("Probe")?.default?.definition;
+ if (built === undefined || built.kind !== "function") {
+ throw new Error("the execution built no implementation for the declared component");
+ }
+ published.push(built);
+ return request.answer(built);
+ });
+ },
+ };
+ }
+
+ /** One host: a declared component, an entry admitting it, and its provider. */
+ function host(
+ component: IdentityComponent,
+ published: FunctionComponentDefinition[],
+ install?: () => Operation,
+ ): ExecutionInstallation {
+ return {
+ components: [component],
+ evaluation: { read: [{ kind: "component-answer", name: "Probe", forms: ["self-closing"] }] },
+ componentAnswers: [publishing(published)],
+ ...(install === undefined ? {} : { install }),
+ };
+ }
+
+ function run(
+ installations: readonly ExecutionInstallation[],
+ stream: InMemoryStream,
+ ): Operation {
+ return scoped(function* () {
+ return yield* collect(
+ yield* executeInstalled({ ...retainedSource(ROOT_PATH, NAMING_PROBE), stream }, [
+ ...installations,
+ ]),
+ );
+ });
+ }
+
+ function isRecord(value: Json | undefined): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+ }
+
+ /** The row this run's admission retained for the name it admitted. */
+ function* admittedRow(stream: InMemoryStream): Operation {
+ const events = yield* stream.readAll();
+ const event = events.find(
+ (held) => held.type === "yield" && held.description.type === "generated_xmd",
+ );
+ const result = event?.type === "yield" ? event.result : undefined;
+ if (result === undefined || result.status !== "ok" || !isRecord(result.value)) {
+ throw new Error("the run retained no admission");
+ }
+ const policy = result.value.policy;
+ if (!isRecord(policy) || !Array.isArray(policy.allowed)) {
+ throw new Error("the admission recorded no policy");
+ }
+ // Trusted composition leads every policy, so the row the selection
+ // contributed is the one after it.
+ const row = policy.allowed.find((held) => isRecord(held) && held.name === "Probe");
+ if (!isRecord(row)) {
+ throw new Error("the admission admitted nothing for the declared component");
+ }
+ return row;
+ }
+
+ /** What an admission retains for a form offered as a read. */
+ const PROBE_ROW = {
+ name: "Probe",
+ form: "self-closing",
+ identity: "https://executable.md/components/probe/read",
+ kind: "component-answer",
+ effect: "read",
+ };
+
+ it("GOM4: a host's factory declaration runs the spelling it offered", function* () {
+ const stream = new InMemoryStream();
+ const published: FunctionComponentDefinition[] = [];
+
+ const output = yield* run([host(declaredProbe(READ_PROBE), published)], stream);
+
+ // The built implementation carries its declaration's offer, the admission
+ // names that URL and effect, and the body a factory supplied is what ran.
+ expect(published[0]?.generated).toEqual(READ_PROBE);
+ expect(yield* admittedRow(stream)).toEqual(PROBE_ROW);
+ expect(String(output)).toContain("the factory body ran");
+ });
+
+ it("GOM4: capture reads the offer once, and construction uses that reading", function* () {
+ // The check/use gap at the installation boundary, where the host also gets
+ // to run code in between: the accessor answers the accepted URL the first
+ // time and a substituted one afterwards, so a construction that read the
+ // host's object again would build a component offering a URL capture never
+ // validated.
+ const reads: GeneratedForms[] = [];
+ const component: IdentityComponent = {
+ ...declaredProbe(undefined),
+ get generated(): GeneratedForms {
+ reads.push(READ_PROBE);
+ return reads.length === 1 ? READ_PROBE : SUBSTITUTE;
+ },
+ };
+ const stream = new InMemoryStream();
+ const published: FunctionComponentDefinition[] = [];
+
+ const output = yield* run([host(component, published)], stream);
+
+ // What the one reading said is what was built and what was admitted — and
+ // one reading is all there was. The plant is live rather than inert: the
+ // very next reading of the host's object offers the substituted URL.
+ expect(published[0]?.generated).toEqual(READ_PROBE);
+ expect(yield* admittedRow(stream)).toEqual(PROBE_ROW);
+ expect(reads).toHaveLength(1);
+ expect(component.generated).toEqual(SUBSTITUTE);
+ expect(String(output)).toContain("the factory body ran");
+ });
+
+ it("GOM4: an offer replaced from inside install() has replaced nothing", function* () {
+ // A host's installation callback runs before the declared components are
+ // built, so this is the window a live reference would be read through.
+ const offers: { generated: GeneratedForms } = { generated: READ_PROBE };
+ const component: IdentityComponent = {
+ ...declaredProbe(undefined),
+ get generated(): GeneratedForms {
+ return offers.generated;
+ },
+ };
+ const stream = new InMemoryStream();
+ const published: FunctionComponentDefinition[] = [];
+
+ const output = yield* run(
+ [
+ // deno-lint-ignore require-yield
+ host(component, published, function* () {
+ offers.generated = SUBSTITUTE;
+ }),
+ ],
+ stream,
+ );
+
+ expect(offers.generated).toEqual(SUBSTITUTE);
+ expect(published[0]?.generated).toEqual(READ_PROBE);
+ expect(yield* admittedRow(stream)).toEqual(PROBE_ROW);
+ expect(String(output)).toContain("the factory body ran");
+ });
+
+ it("GOM4: an unusable offer refuses the execution, having run nothing", function* () {
+ const stream = new InMemoryStream();
+ const published: FunctionComponentDefinition[] = [];
+ const unusable = { "self-closing": { identity: "not-a-url", effect: "read" as const } };
+
+ let refused = "";
+ try {
+ yield* run([host(declaredProbe(unusable), published)], stream);
+ } catch (error) {
+ refused = error instanceof Error ? error.message : String(error);
+ }
+
+ // Read where it is captured, so a declaration this engine cannot read
+ // refuses before an implementation is built, a provider is asked or
+ // anything is written down.
+ expect(refused).toContain('the identity component "Probe" offers its self-closing form');
+ expect(refused).toContain("not an absolute URL");
+ expect(published).toEqual([]);
+ expect(yield* stream.readAll()).toEqual([]);
+ });
+});
diff --git a/packages/core/tests/generated-xmd.test.ts b/packages/core/tests/generated-xmd.test.ts
index d702adf01..ab19e0d74 100644
--- a/packages/core/tests/generated-xmd.test.ts
+++ b/packages/core/tests/generated-xmd.test.ts
@@ -68,8 +68,8 @@ import type {
GeneratedMutation,
GeneratedObservation,
GeneratedXmdRequest,
- RetainedFragmentIdentity,
} from "../host.ts";
+import type { GeneratedEffect, GeneratedForms } from "../src/components/generated-metadata.ts";
import type { FunctionComponentDefinition, Json, JsonObject } from "../src/types.ts";
const ROOT_PATH = "workflows/agent.md";
@@ -81,14 +81,18 @@ const ROOT_PATH = "workflows/agent.md";
* asked for and before whatever the selection added. Written here once, because
* every record below has to account for it.
*/
-const COMPOSITION = [{ name: "Json", identity: pinnedJson().identity, forms: ["self-closing"] }];
+const COMPOSITION = [
+ {
+ name: "Json",
+ form: "self-closing",
+ identity: pinnedJson().identity,
+ kind: "capability",
+ effect: "composition",
+ },
+];
-/** The same entry as journal data, for a record a test forges by hand. */
-const COMPOSITION_RECORD: JsonObject = {
- name: "Json",
- identity: { ...pinnedJson().identity },
- forms: ["self-closing"],
-};
+/** The same row as journal data, for a record a test forges by hand. */
+const COMPOSITION_RECORD: JsonObject = { ...COMPOSITION[0] };
const ROOT_SOURCE = "The host ran a generated fragment.\n";
const URL_ONE = "https://api.example.test/one";
const URL_TWO = "https://api.example.test/two";
@@ -97,21 +101,30 @@ const ROOTS = ["workspace://primary", "workspace://secondary"];
const ADVANCED = "workspace://advanced";
/**
- * One host-stated identity, in the shape a run retains.
+ * What one host component's declaration offers a spelling as.
*
- * `component-answer`, because everything a test admits here is an
- * implementation the host holds rather than an operation core supplies the body
- * for — which is the distinction the kind exists to keep.
+ * The identity and the effect are on the definition, because that is where the
+ * declaration that owns the implementation states them: a host admits a name
+ * and a spelling, and what admitting it grants is the component's own.
*/
-function hostIdentity(origin: string, key: string, revision = "1"): RetainedFragmentIdentity {
- return { kind: "component-answer", origin, key, revision };
+function offers(
+ key: string,
+ effect: GeneratedEffect,
+ form: "self-closing" | "paired" = "self-closing",
+): GeneratedForms {
+ return { [form]: { identity: `https://executable.md/components/${key}`, effect } };
}
+/** The identity ` ` is admitted under, wherever a row names it. */
+const PROBE_IDENTITY = "https://executable.md/components/probe/read";
+
/** The one host observation component the tests admit beside ``. */
const PROBE: FunctionComponentDefinition = {
kind: "function",
name: "Probe",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offers("probe/read", "read"),
// deno-lint-ignore require-yield
*fn(): Operation {
return "probed";
@@ -119,7 +132,45 @@ const PROBE: FunctionComponentDefinition = {
};
function probe(): GeneratedObservation {
- return pinnedComponent("Probe", hostIdentity("test://probe", "Probe"), PROBE);
+ return pinnedComponent("Probe", PROBE, "component-answer");
+}
+
+/**
+ * One host component offered to observe in both spellings, and one offered to
+ * mutate in both.
+ *
+ * ` ` offers one reading spelling, which is the ordinary shape; these
+ * two are for the rows about what a table may say when a component offers both
+ * — the content of a paired read, and two entries fighting over one spelling.
+ */
+const READS: FunctionComponentDefinition = {
+ kind: "function",
+ name: "Reads",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/reads/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/reads/also", effect: "read" },
+ },
+ *fn(): Operation {
+ return yield* content();
+ },
+};
+
+const WRITES: FunctionComponentDefinition = {
+ kind: "function",
+ name: "Writes",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/writes/one", effect: "write" },
+ paired: { identity: "https://executable.md/components/writes/both", effect: "write" },
+ },
+ *fn(): Operation {
+ return yield* content();
+ },
+};
+
+function reads(): GeneratedObservation {
+ return pinnedComponent("Reads", READS, "component-answer");
}
describe("generated component resolution stays internal", () => {
@@ -363,34 +414,36 @@ describe("Tier GX — the trusted-host seam", () => {
throw new Error("the run recorded no generated-XMD admission");
}
expect(admission.description.name).toBe("generated:turn-1");
- // The version-2 shape: tagged, and carrying the Workspace basis as one
- // member rather than two top-level ones, because a host may have none.
- // The identity is the closed structural record rather than a spelling: four
- // members a reader can compare one at a time, so an admission that moved
- // says which part moved.
- const PROBE_IDENTITY = {
+ // The current shape: marked, and carrying the Workspace basis as one
+ // member rather than two top-level ones, because a host may have none. One
+ // row per admitted spelling, each holding the URL its owner offers that
+ // spelling under, what kind of thing answered for the name, and which
+ // effect table admitted it — so an admission that moved says which
+ // spelling of which name moved.
+ const PROBE_ROW = {
+ name: "Probe",
+ form: "self-closing",
+ identity: PROBE_IDENTITY,
kind: "component-answer",
- origin: "test://probe",
- key: "Probe",
- revision: "1",
+ effect: "read",
};
expect(admission.description.input).toMatchObject({
- version: 2,
+ version: 3,
workspace: { roots: ROOTS, selectedRoot: ROOTS[0] },
- allowed: [...COMPOSITION, { name: "Probe", identity: PROBE_IDENTITY }],
+ allowed: [...COMPOSITION, PROBE_ROW],
});
expect(admission.result).toMatchObject({
status: "ok",
value: {
decision: "admitted",
source: " \n",
- named: [{ name: "Probe", identity: PROBE_IDENTITY }],
+ named: [{ name: "Probe", identity: PROBE_IDENTITY, form: "self-closing" }],
// Retained in the result as well as the input, because durable replay
// matches an effect by type and name and never compares a description.
policy: {
- version: 2,
+ version: 3,
workspace: { roots: ROOTS, selectedRoot: ROOTS[0] },
- allowed: [...COMPOSITION, { name: "Probe", identity: PROBE_IDENTITY }],
+ allowed: [...COMPOSITION, PROBE_ROW],
requests: [],
},
},
@@ -506,7 +559,8 @@ describe("Tier GX — the complete fragment is read first", () => {
const transport = yield* useTransport(() => ({ status: 200 }));
const attempt = yield* evaluate(
- request("\n\n```bash exec\nprintf ran\n```\n\n \n", [
+ request("\n\n```bash exec\nprintf ran\n```\n\n \n", [
+ reads(),
pinnedFetch([ADMITTED_REQUEST]),
probe(),
]),
@@ -784,43 +838,23 @@ describe("Tier GX — the request a generated fragment may perform", () => {
});
/**
- * Two identities that differ only in where a space falls.
+ * One admitted read that states a ceiling and records what it was asked.
*
- * A ceiling belongs to the identity that states it, and the run keys its own
- * ceiling table by that identity. A key built by joining the four terms with
- * a separator is only as unique as the separator is illegal — and nothing
- * makes a space illegal in an origin or a component key. These two are the
- * smallest pair that collides under a space-joined key and not under the
- * terms themselves, so a table that shared one entry's limit with another
- * fails here rather than somewhere a host would have to notice.
+ * Two of these make the pair these rows need: each entry states its own
+ * ceiling, and the run keys its ceiling table by the entry itself rather than
+ * by anything describing it — so one entry's limit is never the other's,
+ * however alike two entries look.
*/
- const SPACED_ONE: RetainedFragmentIdentity = {
- kind: "capability",
- origin: "test://host a",
- key: "b",
- revision: "1",
- };
- const SPACED_OTHER: RetainedFragmentIdentity = {
- kind: "capability",
- origin: "test://host",
- key: "a b",
- revision: "1",
- };
-
- /** One admitted read that states a ceiling and records what it was asked. */
- function bounded(
- name: string,
- identity: RetainedFragmentIdentity,
- url: string,
- performed: string[],
- ): GeneratedObservation {
+ function bounded(name: string, url: string, performed: string[]): GeneratedObservation {
return {
name,
- identity,
+ kind: "component-answer",
requests: [{ url }],
definition: {
kind: "function",
name,
+ forms: ["self-closing"],
+ generated: offers(`${name.toLowerCase()}/read`, "read"),
props: {
type: "object",
properties: { url: { type: "string" } },
@@ -836,18 +870,18 @@ describe("Tier GX — the request a generated fragment may perform", () => {
};
}
- it("GX14b: two identities that collide under a joined key keep their own ceilings", function* () {
+ it("GX14b: two admitted reads keep their own ceilings", function* () {
const performed: string[] = [];
const attempt = yield* evaluate(
request(` \n\n \n`, [
- bounded("One", SPACED_ONE, URL_ONE, performed),
- bounded("Other", SPACED_OTHER, URL_TWO, performed),
+ bounded("One", URL_ONE, performed),
+ bounded("Other", URL_TWO, performed),
]),
);
- // Each element performed the request its own entry stated. A shared key
- // would have left one of the two holding the other's limit, and one of
- // these two admitted requests would have been refused.
+ // Each element performed the request its own entry stated. A shared
+ // ceiling would have left one of the two holding the other's limit, and one
+ // of these two admitted requests would have been refused.
expect(attempt.failure).toBe(undefined);
expect(performed).toEqual([`One ${URL_ONE}`, `Other ${URL_TWO}`]);
// And the retained policy holds both limits rather than one of them twice.
@@ -862,14 +896,13 @@ describe("Tier GX — the request a generated fragment may perform", () => {
const performed: string[] = [];
const attempt = yield* evaluate(
request(` \n`, [
- bounded("One", SPACED_ONE, URL_ONE, performed),
- bounded("Other", SPACED_OTHER, URL_TWO, performed),
+ bounded("One", URL_ONE, performed),
+ bounded("Other", URL_TWO, performed),
]),
);
// The negative half of the pair above. ` ` may perform exactly what
- // its own entry stated, and the other entry's limit is not its own however
- // the two identities are spelled.
+ // its own entry stated, and the other entry's limit is not its own.
expect(attempt.failure).toContain("did not admit");
expect(admittedFragments(attempt.events)).toHaveLength(0);
expect(performed).toEqual([]);
@@ -1124,11 +1157,12 @@ describe("Tier GX — nested generated effects belong to the owning expansion",
function durableWrite(executed: string[]): GeneratedMutation {
return pinnedMutation(
"Write",
- hostIdentity("test://durable-write", "Write"),
{
kind: "function",
name: "Write",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["paired"],
+ generated: offers("write/ensure", "write", "paired"),
*fn(): Operation {
const body = yield* content();
yield createDurableOperation(
@@ -1357,16 +1391,22 @@ describe("Tier GX — a malformed generated request reports its class, not itsel
/** The probe under its admitted identity, counting live invocations. */
function countingProbe(performed: string[]): GeneratedObservation {
- return pinnedComponent("Probe", hostIdentity("test://probe", "Probe"), {
- kind: "function",
- name: "Probe",
- props: { type: "object", properties: {}, additionalProperties: false },
- // deno-lint-ignore require-yield
- *fn(): Operation {
- performed.push("probed");
- return "probed";
+ return pinnedComponent(
+ "Probe",
+ {
+ kind: "function",
+ name: "Probe",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offers("probe/read", "read"),
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ performed.push("probed");
+ return "probed";
+ },
},
- });
+ "component-answer",
+ );
}
for (const [what, basis] of INCOHERENT) {
@@ -1423,75 +1463,16 @@ describe("Tier GX — a resumed run is held to the ceilings it was admitted unde
kind: "function",
name: "Probe",
props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ // A different implementation, offered under a different URL by its own
+ // declaration — which is what a changed grant behind one name looks like.
+ generated: offers("other/read", "read"),
// deno-lint-ignore require-yield
*fn(): Operation {
return "the other implementation ran";
},
};
- /**
- * The probe under its admitted identity, counting live invocations — so a
- * case proves whether a continuation reached the generated component rather
- * than inferring it from rendered text alone.
- */
- function countedProbe(performed: string[]): GeneratedObservation {
- return pinnedComponent("Probe", hostIdentity("test://probe", "Probe"), {
- kind: "function",
- name: "Probe",
- props: { type: "object", properties: {}, additionalProperties: false },
- // deno-lint-ignore require-yield
- *fn(): Operation {
- performed.push("probed");
- return "probed";
- },
- });
- }
-
- /**
- * The identity this run states for the host observation a version-1 record
- * named.
- *
- * A capability, because that is the only arm that existed when untagged
- * records were written: the component-answer arm came with this issue, so no
- * version-1 string ever described one.
- */
- const V1_IDENTITY: RetainedFragmentIdentity = {
- kind: "capability",
- origin: "test://probe",
- key: "Probe",
- revision: "1",
- };
-
- /**
- * The exact string a version-1 journal holds for it.
- *
- * Arbitrary, and that is the point. Under `bb2c1c49` this string was whatever
- * the host passed to `pinnedComponent`, retained verbatim — there was no
- * spelling rule at all, so nothing recovers `V1_IDENTITY` from it. The
- * current entry states it as an alias, which is the only thing that connects
- * the two.
- */
- const V1_SPELLING = "urn:host:probe/v1";
-
- /** The same counting probe, stating the version-1 identity it succeeds. */
- function versionOneProbe(performed: string[]): GeneratedObservation {
- return pinnedComponent(
- "Probe",
- V1_IDENTITY,
- {
- kind: "function",
- name: "Probe",
- props: { type: "object", properties: {}, additionalProperties: false },
- // deno-lint-ignore require-yield
- *fn(): Operation {
- performed.push("probed");
- return "probed";
- },
- },
- [V1_SPELLING],
- );
- }
-
/**
* A history left by a run interrupted during preparation: the admission
* committed, and nothing after it did.
@@ -1556,116 +1537,288 @@ describe("Tier GX — a resumed run is held to the ceilings it was admitted unde
}
/**
- * The untagged #369 policy, written out rather than derived.
+ * The policy a current build writes, written out rather than derived.
*
- * A run suspended before this build wrote exactly this: no version member,
- * and the Workspace basis as two mandatory top-level ones. Deriving it by
- * rewriting a record this build produced would prove only that the rewrite
- * and the reader agree; a literal is what an older journal actually holds.
+ * A literal, because what these rows are about is the record: deriving it by
+ * rewriting one this build produced would prove only that the rewrite and the
+ * reader agree. One row per admitted spelling, each holding the URL its owner
+ * offers that spelling under.
*/
- const V1_POLICY: JsonObject = Object.freeze({
+ const CURRENT_POLICY: JsonObject = Object.freeze({
+ version: 3,
allow: ["read"],
- roots: [...ROOTS],
- selectedRoot: ROOTS[0],
- allowed: [{ name: "Probe", identity: V1_SPELLING, forms: ["self-closing", "paired"] }],
+ workspace: { roots: [...ROOTS], selectedRoot: ROOTS[0] },
+ allowed: [
+ { ...COMPOSITION_RECORD },
+ {
+ name: "Probe",
+ form: "self-closing",
+ identity: PROBE_IDENTITY,
+ kind: "component-answer",
+ effect: "read",
+ },
+ ],
requests: [],
});
- /** One frozen version-1 admission, as an older build committed it. */
- function versionOneAdmission(source: string): DurableEvent {
+ /** One admission holding that policy, as the event a journal holds. */
+ function currentAdmission(source: string, policy: JsonObject = CURRENT_POLICY): DurableEvent {
return {
type: "yield",
coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: V1_POLICY },
+ description: { type: "generated_xmd", name: "generated:turn-1", input: policy },
result: {
status: "ok",
value: {
+ version: 3,
decision: "admitted",
source,
- named: [{ name: "Probe", identity: V1_SPELLING, form: "self-closing" }],
- policy: V1_POLICY,
+ named: [{ name: "Probe", identity: PROBE_IDENTITY, form: "self-closing" }],
+ policy,
},
},
};
}
- /** One frozen version-1 refusal, in the shape that build wrote refusals. */
- function versionOneRefusal(): DurableEvent {
- return {
- type: "yield",
- coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: V1_POLICY },
- result: { status: "ok", value: { decision: "refused", construct: "block" } },
- };
- }
-
/**
- * A history whose admission is that literal version-1 record.
+ * A history whose admission is one literal record.
*
* The events around it are a real run's, because what is under test is the
* record rather than the journal's framing.
*/
- function withVersionOne(
- events: readonly DurableEvent[],
- admission: DurableEvent,
- ): DurableEvent[] {
+ function withAdmission(events: readonly DurableEvent[], admission: DurableEvent): DurableEvent[] {
return events.map((event) =>
event.type === "yield" && event.description.type === "generated_xmd" ? admission : event,
);
}
/**
- * The literal version-1 admission a `bb2c1c49` workflow run committed for the
- * standard read table.
+ * The untagged #369 admission a released build committed.
*
- * Written out from what that build actually emitted rather than derived:
- * `pinnedFileRead()` retained `@executablemd/core#File:read` — one opaque
- * value, no revision in it, no separator convention to read back out — beside
- * the untagged policy shape with its two top-level Workspace members.
+ * Written out from what that build actually emitted: one opaque identity
+ * string per entry, no version member, and the Workspace basis as two
+ * mandatory top-level members. Nothing recovers a current row from it — there
+ * was no spelling rule to invert — so this build refuses it as unreadable
+ * history rather than reading the members it recognizes.
*/
- const V1_STANDARD_POLICY: JsonObject = Object.freeze({
- allow: ["read"],
- roots: [...ROOTS],
- selectedRoot: ROOTS[0],
- allowed: [{ name: "File", identity: "@executablemd/core#File:read", forms: ["self-closing"] }],
- requests: [],
+ const UNTAGGED_RECORD: DurableEvent = {
+ type: "yield",
+ coroutineId: "root",
+ description: {
+ type: "generated_xmd",
+ name: "generated:turn-1",
+ input: {
+ allow: ["read"],
+ roots: [...ROOTS],
+ selectedRoot: ROOTS[0],
+ allowed: [
+ { name: "Probe", identity: "urn:host:probe/v1", forms: ["self-closing", "paired"] },
+ ],
+ requests: [],
+ },
+ },
+ result: {
+ status: "ok",
+ value: {
+ decision: "admitted",
+ source: " \n",
+ named: [{ name: "Probe", identity: "urn:host:probe/v1", form: "self-closing" }],
+ policy: {
+ allow: ["read"],
+ roots: [...ROOTS],
+ selectedRoot: ROOTS[0],
+ allowed: [
+ { name: "Probe", identity: "urn:host:probe/v1", forms: ["self-closing", "paired"] },
+ ],
+ requests: [],
+ },
+ },
+ },
+ };
+
+ /**
+ * The tagged admission the build before this one committed.
+ *
+ * Version 2 held an identity as a closed structural record — what kind of
+ * thing was behind the name, the origin that owned it, the component key and
+ * the revision — and a list of forms per entry. Every term of that shape is
+ * gone, and reading one as though a URL could be recovered from it would
+ * compare a fragment's admission equal to an admission it never had.
+ */
+ const STRUCTURAL_RECORD: DurableEvent = {
+ type: "yield",
+ coroutineId: "root",
+ description: {
+ type: "generated_xmd",
+ name: "generated:turn-1",
+ input: {
+ version: 2,
+ allow: ["read"],
+ workspace: { roots: [...ROOTS], selectedRoot: ROOTS[0] },
+ allowed: [
+ {
+ name: "Probe",
+ identity: {
+ kind: "component-answer",
+ origin: "test://probe",
+ key: "Probe",
+ revision: "1",
+ },
+ forms: ["self-closing"],
+ },
+ ],
+ requests: [],
+ },
+ },
+ result: {
+ status: "ok",
+ value: {
+ version: 2,
+ decision: "admitted",
+ source: " \n",
+ named: [
+ {
+ name: "Probe",
+ identity: {
+ kind: "component-answer",
+ origin: "test://probe",
+ key: "Probe",
+ revision: "1",
+ },
+ form: "self-closing",
+ },
+ ],
+ policy: {
+ version: 2,
+ allow: ["read"],
+ workspace: { roots: [...ROOTS], selectedRoot: ROOTS[0] },
+ allowed: [
+ {
+ name: "Probe",
+ identity: {
+ kind: "component-answer",
+ origin: "test://probe",
+ key: "Probe",
+ revision: "1",
+ },
+ forms: ["self-closing"],
+ },
+ ],
+ requests: [],
+ },
+ },
+ },
+ };
+
+ /**
+ * The probe under its admitted identity, counting live invocations — so a
+ * case proves whether a continuation reached the generated component rather
+ * than inferring it from rendered text alone.
+ */
+ function countedProbe(performed: string[]): GeneratedObservation {
+ return pinnedComponent(
+ "Probe",
+ {
+ kind: "function",
+ name: "Probe",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offers("probe/read", "read"),
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ performed.push("probed");
+ return "probed";
+ },
+ },
+ "component-answer",
+ );
+ }
+
+ it("FE18/GX21z: a current record restores, and the admitted read performs once", function* () {
+ const first = yield* evaluate(request(" \n", [probe()]));
+ expect(first.output).toContain("probed");
+
+ // The positive control every refusal row below is measured against: the
+ // record this build writes, resumed against the entry it was admitted
+ // under, restores the decision and performs the work it authorized.
+ const performed: string[] = [];
+ const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
+ stream: new InMemoryStream(
+ withAdmission(yield* partial(first.events).readAll(), currentAdmission(" \n")),
+ ),
+ });
+
+ expect(again.failure).toBe(undefined);
+ expect(performed).toEqual(["probed"]);
+ expect(admissions(again.events)).toHaveLength(1);
});
- /** That admission, as the event an older journal holds. */
- function versionOneStandard(source: string): DurableEvent {
- return {
+ it("FE18/GX21z: the same URL with an updated implementation resumes", function* () {
+ const first = yield* evaluate(request(" \n", [probe()]));
+
+ // What a revision used to refuse. The implementation behind the name is a
+ // different function with different output, offered under the same URL by
+ // the same declaration — and between executions that is an updated build of
+ // the component the admission named rather than a changed grant.
+ const performed: string[] = [];
+ const updated = pinnedComponent(
+ "Probe",
+ {
+ kind: "function",
+ name: "Probe",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offers("probe/read", "read"),
+ // deno-lint-ignore require-yield
+ *fn(): Operation {
+ performed.push("probed again");
+ return "probed again";
+ },
+ },
+ "component-answer",
+ );
+ const again = yield* evaluate(request(" \n", [updated]), {
+ stream: new InMemoryStream(
+ withAdmission(yield* partial(first.events).readAll(), currentAdmission(" \n")),
+ ),
+ });
+
+ expect(again.failure).toBe(undefined);
+ expect(performed).toEqual(["probed again"]);
+ });
+
+ it("FE18/GX21x: a current refusal replays as the refusal it recorded", function* () {
+ const first = yield* evaluate(request(" \n", [probe()]));
+
+ const performed: string[] = [];
+ const refused: DurableEvent = {
type: "yield",
coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: V1_STANDARD_POLICY },
+ description: { type: "generated_xmd", name: "generated:turn-1", input: CURRENT_POLICY },
result: {
status: "ok",
- value: {
- decision: "admitted",
- source,
- named: [{ name: "File", identity: "@executablemd/core#File:read", form: "self-closing" }],
- policy: V1_STANDARD_POLICY,
- },
+ value: { version: 3, decision: "refused", construct: "block" },
},
};
- }
+ const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
+ stream: new InMemoryStream(
+ withAdmission(yield* duringPreparation(first.events).readAll(), refused),
+ ),
+ });
+
+ // Restoring a refusal means refusing again rather than re-deciding a
+ // fragment this run already declined.
+ expect(again.failure).toContain("executable code block");
+ expect(performed).toEqual([]);
+ });
/**
- * One literal version-1 admission, in the untagged shape and holding whatever
- * a released build retained for the entry the case is about.
+ * One admission whose policy and result carry whatever members a case wants.
+ *
+ * Built as a literal rather than by rewriting a real record, so a case states
+ * the exact shape this build must refuse.
*/
- function versionOneRecord(
- source: string,
- allow: readonly string[],
- allowed: readonly JsonObject[],
- requests: readonly JsonObject[] = [],
- ): DurableEvent {
- const policy: JsonObject = {
- allow: [...allow],
- roots: [...ROOTS],
- selectedRoot: ROOTS[0],
- allowed: allowed.map((entry) => ({ ...entry })),
- requests: requests.map((request) => ({ ...request })),
- };
+ function forgedAdmission(policy: JsonObject, extra: JsonObject = {}): DurableEvent {
return {
type: "yield",
coroutineId: "root",
@@ -1673,606 +1826,163 @@ describe("Tier GX — a resumed run is held to the ceilings it was admitted unde
result: {
status: "ok",
value: {
+ version: 3,
decision: "admitted",
- source,
- named: allowed.map((entry) => ({
- name: entry.name,
- identity: entry.identity,
- form: Array.isArray(entry.forms) ? entry.forms[0] : "self-closing",
- })),
+ source: " \n",
+ named: [{ name: "Probe", identity: PROBE_IDENTITY, form: "self-closing" }],
policy,
+ ...extra,
},
},
};
}
+ /** The probe's row, with whatever one case moves about it. */
+ function forgedRow(overrides: JsonObject): JsonObject {
+ return {
+ allowed: [
+ { ...COMPOSITION_RECORD },
+ {
+ name: "Probe",
+ form: "self-closing",
+ identity: PROBE_IDENTITY,
+ kind: "component-answer",
+ effect: "read",
+ ...overrides,
+ },
+ ],
+ };
+ }
+
/**
- * The workflow host's own ``, at the identity and alias it states.
+ * Every retained record this build refuses, and what is wrong with each.
*
- * Built here rather than imported, because what is under test is the seam a
- * host reaches: `pinnedMutation` takes the structural identity and the exact
- * version-1 string, and this is the string a released workflow run retained.
+ * Their positive control is the current record above: every row here is a
+ * record that would otherwise resume with one thing wrong, so a row that
+ * refuses is refusing on the defect it names rather than because a forged
+ * admission never resumes at all.
*/
- function dirEntry(executed: string[]): GeneratedMutation {
- return pinnedMutation(
- "Dir",
- {
- kind: "capability",
- origin: "@executablemd/workflow/composition",
- key: "Dir",
- revision: "3",
- },
- {
- kind: "function",
- name: "Dir",
- props: {
- type: "object",
- properties: { path: { type: "string" } },
- required: ["path"],
- additionalProperties: false,
- },
- // deno-lint-ignore require-yield
- *fn(props: Record): Operation {
- executed.push(`dir:${String(props.path)}`);
- return "";
- },
- },
- "paired",
- ["@executablemd/workflow/composition/dir-v2#Dir"],
- );
- }
-
- /**
- * Every version-1 identity a released build actually retained, and the entry
- * that states it as the one it succeeds.
- *
- * One row per alias rather than one row for the mechanism, because each of
- * these is a separate literal assertion: the alias is written out at the
- * entry, and a journal holding that exact string has to reconcile to it.
- * `` is here too — it is the workflow host's string, not core's, and the
- * host-supplied path has to work for the same reason core's does.
- */
- const RELEASED_ALIASES: readonly (readonly [
- string,
- string,
- () => GeneratedXmdRequest,
- readonly string[],
- readonly JsonObject[],
- readonly JsonObject[],
- ])[] = [
- [
- "core's Fetch",
- ` \n`,
- () => request(` \n`, [pinnedFetch([ADMITTED_REQUEST])]),
- ["read"],
- [
- {
- name: "Fetch",
- identity: "@executablemd/core#Fetch",
- forms: ["self-closing", "paired"],
- },
- ],
- [{ url: URL_ONE, method: "GET", headers: {} }],
- ],
- [
- "core's read-only File",
- ` \n`,
- () => request(` \n`, [pinnedFileRead()]),
- ["read"],
- [{ name: "File", identity: "@executablemd/core#File:read", forms: ["self-closing"] }],
- [],
- ],
+ const HOSTILE: readonly (readonly [string, DurableEvent])[] = [
+ // The two shapes earlier builds wrote. Neither is readable here, and
+ // neither is migrated: an old record is refused before the admission it
+ // would have restored performs anything.
+ ["the untagged #369 shape", UNTAGGED_RECORD],
+ ["the tagged structural-identity shape", STRUCTURAL_RECORD],
+ ["a marker this build does not have", forgedAdmission({ ...CURRENT_POLICY, version: 99 })],
[
- "core's paired File",
- `from the fragment \n`,
- () =>
- selecting(`from the fragment \n`, [], {
- allow: ["write"],
- mutations: [pinnedFileWrite()],
- }),
- ["write"],
- [{ name: "File", identity: "@executablemd/core#File:write", forms: ["paired"] }],
- [],
+ "a policy carrying legacy top-level roots",
+ forgedAdmission({ ...CURRENT_POLICY, roots: [...ROOTS], selectedRoot: ROOTS[0] }),
],
[
- "core's File.Delete",
- ` \n`,
- () =>
- selecting(` \n`, [], {
- allow: ["write"],
- mutations: [pinnedFileDelete()],
- }),
- ["write"],
- [
- {
- name: "File.Delete",
- identity: "@executablemd/core#File.Delete",
- forms: ["self-closing"],
- },
- ],
- [],
- ],
- ];
-
- for (const [what, source, candidate, allow, allowed, requests] of RELEASED_ALIASES) {
- it(`FE18/GX21z: a version-1 record naming ${what} resumes against its stated alias`, function* () {
- const root = yield* useWorkspace();
- yield* writeTextFile(join(root, "notes.md"), "the retained note\n");
- const transport = yield* useTransport(() => ({ status: 200, body: "body" }));
-
- const first = yield* scoped(function* () {
- yield* useWorkspaceFiles(root);
- return yield* evaluate(candidate());
- });
- expect([what, first.failure]).toEqual([what, undefined]);
-
- // The same run, resumed against the record a released build wrote for
- // this entry: the opaque string, no revision, the untagged policy shape.
- const again = yield* scoped(function* () {
- yield* useWorkspaceFiles(root);
- return yield* evaluate(candidate(), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* partial(first.events).readAll(),
- versionOneRecord(source, allow, allowed, requests),
- ),
- ),
- });
- });
-
- expect([what, again.failure]).toEqual([what, undefined]);
- // Restored rather than decided a second time.
- expect([what, admissions(again.events).length]).toEqual([what, 1]);
- expect(transport.performed.length).toBeGreaterThanOrEqual(0);
- });
- }
-
- it("FE18/GX21z: a version-1 record naming the workflow Dir resumes against its stated alias", function* () {
- // The host-supplied half of the same contract. `@executablemd/workflow/
- // composition/dir-v2#Dir` is the workflow run's string, stated by the
- // workflow entry rather than by core — and a journal holding it reconciles
- // for exactly the same reason core's do.
- const executed: string[] = [];
- const candidate = () =>
- selecting(`held \n`, [], {
- allow: ["write"],
- mutations: [dirEntry(executed)],
- });
-
- const first = yield* evaluate(candidate());
- expect(first.failure).toBe(undefined);
- expect(executed).toEqual(["dir:generated"]);
-
- const again = yield* evaluate(candidate(), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* partial(first.events).readAll(),
- versionOneRecord(
- `held \n`,
- ["write"],
- [
- {
- name: "Dir",
- identity: "@executablemd/workflow/composition/dir-v2#Dir",
- forms: ["paired"],
- },
- ],
- ),
- ),
- ),
- });
-
- expect(again.failure).toBe(undefined);
- expect(admissions(again.events)).toHaveLength(1);
- });
-
- it("FE18/GX21z: the standard core admission a released build wrote still resumes", function* () {
- // The record is `bb2c1c49`'s own: core's read-only `` retained as the
- // string that build committed, under the untagged policy shape. What lets
- // it resume is not a spelling rule — there is none to find — but that
- // `pinnedFileRead()` states that exact string as the version-1 identity it
- // succeeds.
- const root = yield* useWorkspace();
- yield* writeTextFile(join(root, "notes.md"), "the retained note\n");
-
- const first = yield* scoped(function* () {
- yield* useWorkspaceFiles(root);
- return yield* evaluate(request(` \n`, [pinnedFileRead()]));
- });
- expect(first.failure).toBe(undefined);
-
- const again = yield* scoped(function* () {
- const reads = yield* useWorkspaceFiles(root);
- const evaluated = yield* evaluate(request(` \n`, [pinnedFileRead()]), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* partial(first.events).readAll(),
- versionOneStandard(` \n`),
- ),
- ),
- });
- return { evaluated, reads: [...reads.performed] };
- });
-
- expect(again.evaluated.failure).toBe(undefined);
- // Restored rather than decided a second time: one record, carried over from
- // the run that wrote it in the older shape.
- expect(admissions(again.evaluated.events)).toHaveLength(1);
- });
-
- it("FE18/GX21z: an entry that states no alias refuses the record that named it", function* () {
- // The same released record, against a run whose `` entry does not
- // state that string. Reconciliation is an assertion the entry makes, so an
- // entry that makes none is a new grant — and the safe answer to "is this
- // the thing the record was admitted under" is the one that refuses.
- const root = yield* useWorkspace();
- yield* writeTextFile(join(root, "notes.md"), "the retained note\n");
- const first = yield* scoped(function* () {
- yield* useWorkspaceFiles(root);
- return yield* evaluate(request(` \n`, [pinnedFileRead()]));
- });
-
- const silent: GeneratedObservation = { ...pinnedFileRead(), legacy: undefined };
- const again = yield* scoped(function* () {
- const reads = yield* useWorkspaceFiles(root);
- const evaluated = yield* evaluate(request(` \n`, [silent]), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* duringPreparation(first.events).readAll(),
- versionOneStandard(` \n`),
- ),
- ),
- });
- return { evaluated, reads: [...reads.performed] };
- });
-
- expect(again.evaluated.failure).toContain("admitted under");
- // Ahead of the first generated effect: the refusal is the ceiling's, and
- // the admitted read never reached the provider.
- expect(again.reads).toEqual([]);
- });
-
- it("FE18/GX21z: an arbitrary version-1 host identity resumes against its stated alias", function* () {
- const first = yield* evaluate(request(" \n", [versionOneProbe([])]));
- expect(first.output).toContain("probed");
-
- // The other half of what version 1 could hold: whatever string the host
- // passed to `pinnedComponent`, retained verbatim. `urn:host:probe/v1`
- // resembles no structural identity at all, which is why nothing but the
- // entry's own stated alias could connect it to what this run admits.
- //
- // This is also the version-1 positive control for the `GX21y` table below:
- // every row there is this record with one thing wrong, so without this each
- // of them could be refusing because an untagged record never resumes at all.
- const performed: string[] = [];
- const again = yield* evaluate(request(" \n", [versionOneProbe(performed)]), {
- stream: new InMemoryStream(
- withVersionOne(yield* partial(first.events).readAll(), versionOneAdmission(" \n")),
- ),
- });
-
- expect(again.failure).toBe(undefined);
- expect(performed).toEqual(["probed"]);
- expect(admissions(again.events)).toHaveLength(1);
- });
-
- it("FE18/GX21z: a version-1 string no entry lists as its own refuses", function* () {
- const first = yield* evaluate(request(" \n", [versionOneProbe([])]));
-
- // Reconciliation is a stated alias, not a waiver. This run's entry lists a
- // different string, so the retained one names an entry it does not have.
- //
- // The probe still counts, so this says the component was not reached rather
- // than that no counting probe was admitted.
- const performed: string[] = [];
- const moved: GeneratedObservation = {
- ...versionOneProbe(performed),
- legacy: ["urn:host:probe/v0"],
- };
- const again = yield* evaluate(request(" \n", [moved]), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* duringPreparation(first.events).readAll(),
- versionOneAdmission(" \n"),
- ),
- ),
- });
-
- expect(again.failure).toContain("admitted under");
- expect(performed).toEqual([]);
- });
-
- it("FE18/GX21z: a version-1 string never describes a component answer", function* () {
- const first = yield* evaluate(request(" \n", [versionOneProbe([])]));
-
- // The component-answer arm did not exist when untagged records were
- // written, so no version-1 string ever described one. Reading this string as
- // though it might would compare a fragment's admission equal to an admission it
- // never had — even though the origin, the key and the revision all match.
- const performed: string[] = [];
- const answered: GeneratedObservation = {
- ...versionOneProbe(performed),
- identity: hostIdentity("test://probe", "Probe"),
- };
- const again = yield* evaluate(request(" \n", [answered]), {
- stream: new InMemoryStream(
- withVersionOne(
- yield* duringPreparation(first.events).readAll(),
- versionOneAdmission(" \n"),
- ),
- ),
- });
-
- expect(again.failure).toContain("admitted under");
- expect(performed).toEqual([]);
- });
-
- it("FE18/GX21x: a literal version-1 refusal replays as the refusal it recorded", function* () {
- const first = yield* evaluate(request(" \n", [probe()]));
-
- const performed: string[] = [];
- const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
- stream: new InMemoryStream(
- withVersionOne(yield* duringPreparation(first.events).readAll(), versionOneRefusal()),
- ),
- });
-
- // The refusal shape had no version member either, and restoring it means
- // refusing again rather than re-deciding a fragment an older build declined.
- expect(again.failure).toContain("executable code block");
- expect(performed).toEqual([]);
- });
-
- /**
- * One admission whose policy and result carry whatever members a case wants.
- *
- * Built as a literal rather than by rewriting a real record, so a case
- * states the exact shape this build must refuse.
- */
- function forgedAdmission(policy: JsonObject, extra: JsonObject = {}): DurableEvent {
- return {
- type: "yield",
- coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: policy },
- result: {
- status: "ok",
- value: {
- decision: "admitted",
- source: " \n",
- named: [{ name: "Probe", identity: "test://probe", form: "self-closing" }],
- policy,
- ...extra,
- },
- },
- };
- }
-
- /**
- * One version-2 admission whose identities are whatever a case wants.
- *
- * Version 2 is where the identity is the closed tagged record, so these are
- * the cases that say what "closed" means: a value that is not that record is
- * refused rather than read as whichever shape it most resembles.
- */
- function forgedVersionTwo(identity: Json, namedIdentity: Json = identity): DurableEvent {
- const policy: JsonObject = {
- version: 2,
- allow: ["read"],
- workspace: { roots: [...ROOTS], selectedRoot: ROOTS[0] },
- // A version-2 record this build wrote would hold the trusted composition
- // table ahead of the selection, so the control forges one that does. A row
- // below moves part of the probe's identity and nothing else.
- allowed: [
- { ...COMPOSITION_RECORD },
- { name: "Probe", identity, forms: ["self-closing", "paired"] },
- ],
- requests: [],
- };
- return {
- type: "yield",
- coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: policy },
- result: {
- status: "ok",
- value: {
- version: 2,
- decision: "admitted",
- source: " \n",
- named: [{ name: "Probe", identity: namedIdentity, form: "self-closing" }],
- policy,
- },
- },
- };
- }
-
- /** The identity this run actually states for ` `, as journal data. */
- const PROBE_RECORD: JsonObject = {
- kind: "component-answer",
- origin: "test://probe",
- key: "Probe",
- revision: "1",
- };
-
- /**
- * Every version-2 identity shape this build refuses.
- *
- * Separate from the version-1 table below because the two need different
- * resumes: these records name `PROBE_RECORD`, so the run has to state that
- * same component-answer identity, or every row would refuse on the kind
- * rather than on what it says it is about. Their positive control is
- * `forgedVersionTwo(PROBE_RECORD)`, which resumes under exactly that probe.
- */
- const HOSTILE_V2: readonly (readonly [string, DurableEvent])[] = [
- ["a version-2 identity written as a version-1 string", forgedVersionTwo("test://probe")],
- [
- "a version-2 identity with no kind",
- forgedVersionTwo({
- origin: "test://probe",
- key: "Probe",
- revision: "1",
- }),
+ "a policy carrying a member this build does not know",
+ forgedAdmission({ ...CURRENT_POLICY, widened: true }),
],
+ ["an empty class selection", forgedAdmission({ ...CURRENT_POLICY, allow: [] })],
+ ["one class selected twice", forgedAdmission({ ...CURRENT_POLICY, allow: ["read", "read"] })],
[
- "a version-2 identity naming a kind this build does not have",
- forgedVersionTwo({
- ...PROBE_RECORD,
- kind: "registration",
- }),
+ "classes out of canonical order",
+ forgedAdmission({ ...CURRENT_POLICY, allow: ["write", "read"] }),
],
+ // The row is closed in both directions: an extra member is refused, and
+ // each missing one is refused too, because every one of them is a term a
+ // continuation is held to.
[
- "a version-2 identity carrying an extra member",
- forgedVersionTwo({
- ...PROBE_RECORD,
- widened: true,
- }),
+ "a row carrying an extra member",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ widened: true }) }),
],
[
- "a version-2 identity whose revision is not a string",
- forgedVersionTwo({
- ...PROBE_RECORD,
- revision: 1,
+ "a row with no form",
+ forgedAdmission({
+ ...CURRENT_POLICY,
+ allowed: [
+ { ...COMPOSITION_RECORD },
+ {
+ name: "Probe",
+ identity: PROBE_IDENTITY,
+ kind: "component-answer",
+ effect: "read",
+ },
+ ],
}),
],
- ["a version-2 identity that is an array", forgedVersionTwo(["test://probe", "Probe", "1"])],
- ["a version-2 identity that is null", forgedVersionTwo(null)],
- // A closed record is closed in both directions: an extra member is refused
- // above, and each missing one is refused here. `kind` has its own row, so
- // these are the three that carry what the implementation actually is.
- [
- "a version-2 identity with no origin",
- forgedVersionTwo({ kind: "component-answer", key: "Probe", revision: "1" }),
- ],
- [
- "a version-2 identity with no key",
- forgedVersionTwo({ kind: "component-answer", origin: "test://probe", revision: "1" }),
- ],
[
- "a version-2 identity with no revision",
- forgedVersionTwo({ kind: "component-answer", origin: "test://probe", key: "Probe" }),
+ "a row naming a form this build does not have",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ form: "either" }) }),
],
- // The kind is compared, not merely parsed. Everything else about this record
- // is what this run states, and `capability` is a kind this build has — so a
- // row that refuses here is refusing on the kind alone, which is the whole
- // reason the kind travels inside the identity rather than beside it.
[
- "a version-2 identity whose kind is the other one",
- forgedVersionTwo({ ...PROBE_RECORD, kind: "capability" }),
+ "a row naming an effect this build does not have",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ effect: "ask" }) }),
],
- // One record, two shapes. The policy and the named invocation are read by
- // different readers, and a record that satisfied one while contradicting the
- // other would be a grant assembled out of two different versions.
- //
- // The first of these is the row that carries its own weight alone. A
- // retained `named` list is *parsed* and never compared — the policy is what
- // a continuation is held to — so the version strictness of that parse is the
- // only thing standing behind it. Relaxing version 2 to accept a version-1
- // string reddens this row and neither of the others, because those are
- // caught by the comparison as well.
[
- "a version-2 policy beside a version-1 named identity",
- forgedVersionTwo(PROBE_RECORD, V1_SPELLING),
+ "a row naming a kind this build does not have",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ kind: "registration" }) }),
],
+ // The identity is a URL in the accepted namespace and nothing else. A
+ // relative reference, a look-alike origin, the structural record an earlier
+ // build wrote and a value that is not a string at all are each refused
+ // rather than read as whichever shape they most resemble.
[
- "a version-1 named identity beside a version-2 policy",
- forgedVersionTwo(V1_SPELLING, PROBE_RECORD),
- ],
- ];
-
- /** Every retained shape this build refuses, and what is wrong with each. */
- const HOSTILE: readonly (readonly [string, DurableEvent])[] = [
- ["a version this build does not have", forgedAdmission({ ...V1_POLICY, version: 99 })],
- [
- "a version-1 policy carrying a version-2 workspace",
- forgedAdmission({
- ...V1_POLICY,
- workspace: { roots: [...ROOTS], selectedRoot: ROOTS[0] },
- }),
- ],
- [
- "a version-2 policy still carrying legacy roots",
- forgedAdmission({
- version: 2,
- allow: ["read"],
- roots: [...ROOTS],
- selectedRoot: ROOTS[0],
- allowed: V1_POLICY.allowed,
- requests: [],
- }),
+ "a row whose identity is relative",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ identity: "components/probe/read" }) }),
],
[
- "a policy carrying a member this build does not know",
+ "a row whose identity names a look-alike origin",
forgedAdmission({
- ...V1_POLICY,
- widened: true,
+ ...CURRENT_POLICY,
+ ...forgedRow({ identity: "https://executable.md.example.com/components/probe/read" }),
}),
],
- ["an empty class selection", forgedAdmission({ ...V1_POLICY, allow: [] })],
- ["one class selected twice", forgedAdmission({ ...V1_POLICY, allow: ["read", "read"] })],
- ["classes out of canonical order", forgedAdmission({ ...V1_POLICY, allow: ["write", "read"] })],
[
- "an allowed entry carrying an extra member",
+ "a row whose identity is a structural record",
forgedAdmission({
- ...V1_POLICY,
- allowed: [
- {
- name: "Probe",
- identity: "test://probe",
- forms: ["self-closing"],
- widened: true,
- },
- ],
+ ...CURRENT_POLICY,
+ ...forgedRow({
+ identity: { kind: "component-answer", origin: "test://probe", key: "Probe" },
+ }),
}),
],
[
- "an allowed entry with no forms",
- forgedAdmission({
- ...V1_POLICY,
- allowed: [{ name: "Probe", identity: "test://probe", forms: [] }],
- }),
+ "a row whose identity is an array",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ identity: [PROBE_IDENTITY] }) }),
],
- ["a repeated retained root", forgedAdmission({ ...V1_POLICY, roots: [ROOTS[0], ROOTS[0]] })],
- ["no retained roots at all", forgedAdmission({ ...V1_POLICY, roots: [], selectedRoot: "" })],
[
- "a selected root the basis does not hold",
- forgedAdmission({
- ...V1_POLICY,
- selectedRoot: "workspace://never-retained",
- }),
+ "a row whose identity is null",
+ forgedAdmission({ ...CURRENT_POLICY, ...forgedRow({ identity: null }) }),
],
+ ["a repeated retained root", forgedPolicyWorkspace([ROOTS[0], ROOTS[0]], ROOTS[0])],
+ ["no retained roots at all", forgedPolicyWorkspace([], "")],
+ ["a selected root the basis does not hold", forgedPolicyWorkspace(ROOTS, "workspace://never")],
[
"a request carrying a member this build does not know",
forgedAdmission({
- ...V1_POLICY,
+ ...CURRENT_POLICY,
requests: [{ url: URL_ONE, method: "GET", headers: {}, widened: true }],
}),
],
[
"a request whose timeout is not a number",
forgedAdmission({
- ...V1_POLICY,
+ ...CURRENT_POLICY,
requests: [{ url: URL_ONE, method: "GET", headers: {}, timeout: "soon" }],
}),
],
[
"a result carrying a member this build does not know",
- forgedAdmission(V1_POLICY, {
- widened: true,
- }),
+ forgedAdmission(CURRENT_POLICY, { widened: true }),
],
[
- "a tagged result holding an untagged policy",
+ "a result with no marker at all",
{
type: "yield",
coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: V1_POLICY },
+ description: { type: "generated_xmd", name: "generated:turn-1", input: CURRENT_POLICY },
result: {
status: "ok",
value: {
- version: 2,
decision: "admitted",
source: " \n",
- named: [{ name: "Probe", identity: "test://probe", form: "self-closing" }],
- policy: V1_POLICY,
+ named: [{ name: "Probe", identity: PROBE_IDENTITY, form: "self-closing" }],
+ policy: CURRENT_POLICY,
},
},
},
@@ -2282,103 +1992,86 @@ describe("Tier GX — a resumed run is held to the ceilings it was admitted unde
{
type: "yield",
coroutineId: "root",
- description: { type: "generated_xmd", name: "generated:turn-1", input: V1_POLICY },
+ description: { type: "generated_xmd", name: "generated:turn-1", input: CURRENT_POLICY },
result: {
status: "ok",
value: {
+ version: 3,
decision: "admitted",
source: " \n",
named: [
{
name: "Probe",
- identity: "test://probe",
+ identity: PROBE_IDENTITY,
form: "self-closing",
widened: true,
},
],
- policy: V1_POLICY,
+ policy: CURRENT_POLICY,
+ },
+ },
+ },
+ ],
+ [
+ "a named invocation whose identity is a legacy string",
+ {
+ type: "yield",
+ coroutineId: "root",
+ description: { type: "generated_xmd", name: "generated:turn-1", input: CURRENT_POLICY },
+ result: {
+ status: "ok",
+ value: {
+ version: 3,
+ decision: "admitted",
+ source: " \n",
+ named: [{ name: "Probe", identity: "urn:host:probe/v1", form: "self-closing" }],
+ policy: CURRENT_POLICY,
},
},
},
],
];
- for (const [what, admission] of HOSTILE_V2) {
- it(`FE18/GX21y: a retained record with ${what} refuses before any effect`, function* () {
- // Resumed under the component-answer probe these records name, so a row
- // refuses on the defect it is about rather than on a kind that never
- // matched. The control below resumes on the unmodified record.
- const first = yield* evaluate(request(" \n", [probe()]));
-
- const performed: string[] = [];
- const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
- stream: new InMemoryStream(
- withVersionOne(yield* duringPreparation(first.events).readAll(), admission),
- ),
- });
-
- expect([what, again.failure !== undefined]).toEqual([what, true]);
- expect([what, performed]).toEqual([what, []]);
+ /** One admission whose Workspace basis is whatever a case states. */
+ function forgedPolicyWorkspace(roots: readonly string[], selectedRoot: string): DurableEvent {
+ return forgedAdmission({
+ ...CURRENT_POLICY,
+ workspace: { roots: [...roots], selectedRoot },
});
}
for (const [what, admission] of HOSTILE) {
it(`FE18/GX21y: a retained record with ${what} refuses before any effect`, function* () {
- // The version-1 probe, because these rows are `V1_POLICY` with one thing
- // wrong: the base has to be a record that *would* resume, or each row
- // passes because an untagged record never resumes rather than because of
- // the defect it names. `GX21z` is that control.
- const first = yield* evaluate(request(" \n", [versionOneProbe([])]));
+ const first = yield* evaluate(request(" \n", [probe()]));
const performed: string[] = [];
- const again = yield* evaluate(request(" \n", [versionOneProbe(performed)]), {
+ const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
stream: new InMemoryStream(
- withVersionOne(yield* duringPreparation(first.events).readAll(), admission),
+ withAdmission(yield* duringPreparation(first.events).readAll(), admission),
),
});
// A record this build cannot read whole is not a grant. Reading the
- // members it recognizes would admit one on terms it never saw.
+ // members it recognizes would admit one on terms it never saw, and the
+ // refusal lands before the admitted read reaches a provider.
expect([what, again.failure !== undefined]).toEqual([what, true]);
expect([what, performed]).toEqual([what, []]);
});
}
- it("FE18/GX21y: the same forged record with the exact identity resumes", function* () {
- const first = yield* evaluate(request(" \n", [probe()]));
-
- // The positive control for the structural-identity rows above. Every one of
- // them is this record with one part of the identity moved, so this is what
- // proves each refuses for its own reason rather than because a forged
- // version-2 admission never resumes at all.
- const performed: string[] = [];
- const again = yield* evaluate(request(" \n", [countedProbe(performed)]), {
- stream: new InMemoryStream(
- withVersionOne(yield* partial(first.events).readAll(), forgedVersionTwo(PROBE_RECORD)),
- ),
- });
-
- expect(again.failure).toBe(undefined);
- expect(performed).toEqual(["probed"]);
- // Restored rather than decided a second time.
- expect(admissions(again.events)).toHaveLength(1);
- });
-
it("GX21: a changed identity behind the same name refuses before invoking it", function* () {
const first = yield* evaluate(request(" \n", [probe()]));
expect(first.output).toContain("probed");
const again = yield* evaluate(
- request(" \n", [
- pinnedComponent("Probe", hostIdentity("test://other", "Probe"), OTHER),
- ]),
+ request(" \n", [pinnedComponent("Probe", OTHER, "component-answer")]),
{ stream: duringPreparation(first.events) },
);
expect(again.failure).toContain("admitted under");
// Fixed: which identity moved is exactly what a refusal must not publish.
- expect(again.failure).not.toContain("test://other");
- expect(again.failure).not.toContain("test://probe");
+ expect(again.failure).not.toContain("components/other/read");
+ expect(again.failure).not.toContain(PROBE_IDENTITY);
expect(String(again.output ?? "")).not.toContain("the other implementation ran");
expect(persisted(again.events)).not.toContain("the other implementation ran");
});
@@ -2387,9 +2080,7 @@ describe("Tier GX — a resumed run is held to the ceilings it was admitted unde
const first = yield* evaluate(request(" \n", [probe()]));
const again = yield* evaluate(
- request(" \n", [
- pinnedComponent("Probe", hostIdentity("test://other", "Probe"), OTHER),
- ]),
+ request(" \n", [pinnedComponent("Probe", OTHER, "component-answer")]),
{ stream: partial(first.events) },
);
@@ -2624,13 +2315,14 @@ describe("Tier GX — the secret gate covers what is retained", () => {
if (admission === undefined || observation === undefined) {
throw new Error("the gate was not given both events");
}
- // The identity travels as its four members, so the gate sees the origin and
- // the key as themselves rather than as one assembled spelling.
+ // The whole retained admission reaches the gate before it persists: the
+ // candidate's own request, the Workspace basis, and the URL the admitted
+ // spelling runs under beside the effect table that admitted it.
for (const marker of [
"url-marker",
"workspace://primary",
- "@executablemd/core",
- '"key":"Fetch"',
+ "https://executable.md/components/fetch/read",
+ '"effect":"read"',
]) {
expect(admission).toContain(marker);
}
@@ -2741,13 +2433,17 @@ describe("Tier WGAC — the pinned read-only File", () => {
expect(admittedFragments(attempt.evaluated.events)).toHaveLength(1);
});
- it("WGAC1: the read identity is not the unconstrained File identity", function* () {
- // A retained admission resumes only under the identity it was granted with,
- // and the comparison is on this string.
- // Not the unconstrained ``: the read form is a key of its own, so a
- // run that later admitted the unconstrained one states a different identity.
- expect(pinnedFileRead().identity.key).toBe("File:read");
+ it("WGAC1: the read entry admits the reading spelling and nothing else", function* () {
+ // A retained admission resumes only under the identity the admitted
+ // spelling runs under, and ``'s two spellings are offered under two
+ // URLs: the reading form observes, the writing form mutates. So this entry
+ // is the self-closing one, and a run that admitted the paired spelling
+ // would be stating a different grant.
expect(pinnedFileRead().selfClosing).toBe(true);
+ expect(pinnedFileRead().definition.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/file/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/file/write", effect: "write" },
+ });
});
it("WGAC13: the pinned deletion is core's own definition, in its one form", function* () {
@@ -2755,17 +2451,15 @@ describe("Tier WGAC — the pinned read-only File", () => {
expect(deletion.name).toBe("File.Delete");
// What a retained admission is compared against, and what a continuation
- // that selected the write table is held to.
- expect(deletion.identity).toEqual({
- kind: "capability",
- origin: "@executablemd/core",
- key: "File.Delete",
- revision: "2",
- });
- // One name, one identity: the component answers the self-closing spelling
- // and refuses the paired one, so this states what the identity is rather
- // than narrowing it — and stating it is what decides a paired spelling in
- // preflight, before the fragment's first effect.
+ // that selected the write table is held to: the URL core's own declaration
+ // offers this spelling under, and the effect it offers it as.
+ expect(deletion.definition.generated).toEqual({
+ "self-closing": { identity: "https://executable.md/components/file/delete", effect: "write" },
+ });
+ // One name, one form: the component answers the self-closing spelling and
+ // refuses the paired one, so this states the whole of what core offers
+ // rather than narrowing it — and stating it is what decides a paired
+ // spelling in preflight, before the fragment's first effect.
expect(deletion.form).toBe("self-closing");
// The exact object core's registry holds, not one shaped like it. Expansion
// invokes a copy of this, and a same-shaped substitute is a different
@@ -2800,10 +2494,8 @@ describe("Tier WGAC — the pinned read-only File", () => {
admitted?.type === "yield" && admitted.result.status === "ok"
? admitted.result.value
: undefined;
- expect(JSON.stringify(named)).toContain(JSON.stringify(pinnedFileRead().identity));
- expect(JSON.stringify(named)).toContain(
- JSON.stringify(pinnedFetch([ADMITTED_REQUEST]).identity),
- );
+ expect(JSON.stringify(named)).toContain("https://executable.md/components/file/read");
+ expect(JSON.stringify(named)).toContain("https://executable.md/components/fetch/read");
// Ordinary composition: the File read renders its text, and the Fetch —
// which returns a value rather than text — renders nothing at all. Its
// response reached the fragment and the fragment did not ask for it, so
@@ -2897,13 +2589,17 @@ const NEST: FunctionComponentDefinition = {
kind: "function",
name: "Nest",
props: { type: "object", properties: {}, additionalProperties: false },
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/nest/read", effect: "read" },
+ paired: { identity: "https://executable.md/components/nest/write", effect: "write" },
+ },
*fn(): Operation {
return yield* content();
},
};
function nest(form: GeneratedComponentForm = "paired"): GeneratedMutation {
- return pinnedMutation("Nest", hostIdentity("test://nest", "Nest"), NEST, form);
+ return pinnedMutation("Nest", NEST, form, "component-answer");
}
/** One candidate, with the classes and the write table a run states for it. */
@@ -2979,10 +2675,18 @@ describe("Tier GXC — a selection is not a grant", () => {
...COMPOSITION,
{
name: "Probe",
- identity: hostIdentity("test://probe", "Probe"),
- forms: ["self-closing", "paired"],
+ form: "self-closing",
+ identity: PROBE_IDENTITY,
+ kind: "component-answer",
+ effect: "read",
+ },
+ {
+ name: "Nest",
+ form: "paired",
+ identity: "https://executable.md/components/nest/write",
+ kind: "component-answer",
+ effect: "write",
},
- { name: "Nest", identity: hostIdentity("test://nest", "Nest"), forms: ["paired"] },
],
});
});
@@ -3027,7 +2731,10 @@ describe("Tier GXC — a selection is not a grant", () => {
const attempt = yield* evaluate(
selecting("x \n", [probe()], {
allow: ["read", "write"],
- mutations: [nest("paired"), nest("either")],
+ mutations: [
+ pinnedMutation("Writes", WRITES, "paired", "component-answer"),
+ pinnedMutation("Writes", WRITES, "either", "component-answer"),
+ ],
}),
);
@@ -3039,9 +2746,7 @@ describe("Tier GXC — a selection is not a grant", () => {
const attempt = yield* evaluate(
selecting(" \n", [probe()], {
allow: ["read", "write"],
- mutations: [
- pinnedMutation("Probe", hostIdentity("test://probe-write", "Probe"), NEST, "paired"),
- ],
+ mutations: [pinnedMutation("Probe", NEST, "paired", "component-answer")],
}),
);
@@ -3050,6 +2755,185 @@ describe("Tier GXC — a selection is not a grant", () => {
expect(attempt.failure).toContain("one name with two definitions");
expect(admissions(attempt.events)).toHaveLength(0);
});
+
+ /**
+ * A directly pinned definition is one nothing validated on the way in.
+ *
+ * A provider's answer is read where it is published and core's own owners
+ * where they are declared, but a host holding a definition and pinning it
+ * reaches this table with whatever its declaration says. So the declaration
+ * is read here, before the admission record exists and before any body: a
+ * host cannot admit a spelling its own definition offers no generated use of,
+ * admit one under something that is not an identity, or describe the forms it
+ * accepts in a shape this engine does not have.
+ */
+ /**
+ * The probe, stating forms a declaration cannot state.
+ *
+ * Assembled rather than declared, because the point of each of these is that
+ * a host crossed this boundary from JavaScript with a value the declared
+ * shape does not describe.
+ */
+ function stating(forms: unknown): FunctionComponentDefinition {
+ return Object.assign({ ...PROBE }, { forms });
+ }
+
+ const MALFORMED: readonly (readonly [string, FunctionComponentDefinition, string])[] = [
+ [
+ "an identity that is not an absolute URL",
+ { ...PROBE, generated: { "self-closing": { identity: "not-a-url", effect: "read" } } },
+ "not an absolute URL",
+ ],
+ [
+ "an identity naming a look-alike origin",
+ {
+ ...PROBE,
+ generated: {
+ "self-closing": {
+ identity: "https://executable.md.example.com/components/probe/read",
+ effect: "read",
+ },
+ },
+ },
+ "not an absolute URL",
+ ],
+ [
+ "a spelling its own declaration does not accept",
+ { ...PROBE, forms: ["paired"] },
+ "does not accept",
+ ],
+ [
+ "forms this engine does not have",
+ stating(["self-closing", "unknown"]),
+ "the accepted declarations are",
+ ],
+ [
+ "forms stated as something other than a declaration",
+ stating("self-closing"),
+ "the accepted declarations are",
+ ],
+ ];
+
+ for (const [what, definition, refusal] of MALFORMED) {
+ it(`GXC2c: a directly pinned observation with ${what} is refused`, function* () {
+ const attempt = yield* evaluate(
+ request(" \n", [pinnedComponent("Probe", definition, "component-answer")]),
+ );
+
+ // The host's own error, before the candidate is admitted: no record was
+ // appended, so nothing restores it and the component never ran.
+ expect([what, attempt.failure]).toEqual([what, expect.stringContaining(refusal)]);
+ expect([what, admissions(attempt.events).length]).toEqual([what, 0]);
+ expect([what, String(attempt.output ?? "")]).toEqual([what, ""]);
+ });
+ }
+
+ it("GXC2c: a directly pinned mutation is read the same way", function* () {
+ const attempt = yield* evaluate(
+ selecting("x \n", [probe()], {
+ allow: ["read", "write"],
+ mutations: [
+ pinnedMutation(
+ "Nest",
+ { ...NEST, generated: { paired: { identity: "components/nest", effect: "write" } } },
+ "paired",
+ "component-answer",
+ ),
+ ],
+ }),
+ );
+
+ expect(attempt.failure).toContain("not an absolute URL");
+ expect(admissions(attempt.events)).toHaveLength(0);
+ });
+
+ /**
+ * The probe, with an identity that answers differently on a second reading.
+ *
+ * Assembled rather than declared, because the point is a member a host
+ * controls: `[[Get]]` answers the owner's URL once and something unusable
+ * afterwards, so a table that checked the host's object and then read it
+ * again to build the admission would retain a value the check never saw.
+ */
+ function alternating(
+ form: "self-closing" | "paired",
+ effect: GeneratedEffectClass,
+ reads: string[],
+ ): FunctionComponentDefinition {
+ const offered = {
+ get identity(): string {
+ reads.push("identity");
+ return reads.length === 1
+ ? `https://executable.md/components/probe/${effect}`
+ : "not-a-url";
+ },
+ effect,
+ };
+ return Object.assign({ ...PROBE }, { forms: [form], generated: { [form]: offered } });
+ }
+
+ it("GXC2c: a directly pinned observation is read once, and the admission holds it", function* () {
+ const reads: string[] = [];
+ const attempt = yield* evaluate(
+ request(" \n", [
+ pinnedComponent("Probe", alternating("self-closing", "read", reads), "component-answer"),
+ ]),
+ );
+
+ // One reading, and the record holds exactly it. The plant is live rather
+ // than inert: the next read of the host's object says `not-a-url`, which is
+ // what a second reading would have appended — and what the reader of that
+ // fresh record would then have refused, after the admission committed.
+ expect(reads).toEqual(["identity"]);
+ expect(attempt.failure).toBe(undefined);
+ expect(attempt.output).toContain("probed");
+ expect(recordedNames(admittedFragments(attempt.events)[0])).toEqual([
+ {
+ name: "Probe",
+ identity: "https://executable.md/components/probe/read",
+ form: "self-closing",
+ },
+ ]);
+ });
+
+ it("GXC2c: a directly pinned mutation is read once, and the admission holds it", function* () {
+ const reads: string[] = [];
+ const attempt = yield* evaluate(
+ selecting("x \n", [probe()], {
+ allow: ["read", "write"],
+ mutations: [
+ pinnedMutation(
+ "Writer",
+ alternating("paired", "write", reads),
+ "paired",
+ "component-answer",
+ ),
+ ],
+ }),
+ );
+
+ expect(reads).toEqual(["identity"]);
+ expect(attempt.failure).toBe(undefined);
+ const policy = recordedPolicy(admittedFragments(attempt.events)[0]);
+ const rows = isRecord(policy) && Array.isArray(policy.allowed) ? policy.allowed : [];
+ expect(rows).toContainEqual({
+ name: "Writer",
+ form: "paired",
+ identity: "https://executable.md/components/probe/write",
+ kind: "component-answer",
+ effect: "write",
+ });
+ });
+
+ it("GXC2c: the same table without the defect admits and runs", function* () {
+ // The positive control for the rows above: everything else about that table
+ // is what a host states, so each refusal is about the declaration it names.
+ const attempt = yield* evaluate(request(" \n", [probe()]));
+
+ expect(attempt.failure).toBe(undefined);
+ expect(attempt.output).toContain("probed");
+ expect(admissions(attempt.events)).toHaveLength(1);
+ });
});
describe("Tier GXC — a name is not a form", () => {
@@ -3074,8 +2958,16 @@ describe("Tier GXC — a name is not a form", () => {
expect(attempt.evaluated.failure).toBe(undefined);
// One name, two identities, chosen by how each element was written.
expect(recordedNames(admittedFragments(attempt.evaluated.events)[0])).toEqual([
- { name: "File", identity: pinnedFileRead().identity, form: "self-closing" },
- { name: "File", identity: pinnedFileWrite().identity, form: "paired" },
+ {
+ name: "File",
+ identity: "https://executable.md/components/file/read",
+ form: "self-closing",
+ },
+ {
+ name: "File",
+ identity: "https://executable.md/components/file/write",
+ form: "paired",
+ },
]);
expect(attempt.files).toEqual(["notes.md", "write:proposed.md"]);
expect(yield* readTextFile(join(root, "proposed.md"))).toBe("the fragment wrote this");
@@ -3322,16 +3214,14 @@ describe("Tier GXC — a resumed run is held to its classes and forms", () => {
"a widened admitted form",
{
allow: ["write"],
- mutations: [pinnedMutation("File", pinnedFileWrite().identity, NEST, "either")],
+ mutations: [pinnedMutation("File", WRITES, "either", "component-answer")],
},
],
[
"a replaced write identity",
{
allow: ["write"],
- mutations: [
- pinnedMutation("File", hostIdentity("test://other-write", "File"), NEST, "paired"),
- ],
+ mutations: [pinnedMutation("File", NEST, "paired", "component-answer")],
},
],
["an added write identity", { allow: ["write"], mutations: [pinnedFileWrite(), nest()] }],
@@ -3392,9 +3282,7 @@ describe("Tier GXC — a resumed run is held to its classes and forms", () => {
const again = yield* evaluate(
selecting(" \n", [probe()], {
allow: ["read"],
- mutations: [
- pinnedMutation("Nest", hostIdentity("test://replaced", "Nest"), NEST, "paired"),
- ],
+ mutations: [pinnedMutation("Nest", NEST, "paired", "component-answer")],
}),
{ stream: duringPreparation(first.events) },
);
@@ -3433,14 +3321,20 @@ describe("Tier GXC — the authored form survives the public content chain", ()
/** A read component that reports what the chain answers, so a lie is visible. */
function says(): GeneratedObservation {
- return pinnedComponent("Says", hostIdentity("test://says", "Says"), {
- kind: "function",
- name: "Says",
- props: { type: "object", properties: {}, additionalProperties: false },
- *fn(): Operation {
- return `says:${yield* hasContent()}`;
+ return pinnedComponent(
+ "Says",
+ {
+ kind: "function",
+ name: "Says",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: offers("says/read", "read"),
+ *fn(): Operation {
+ return `says:${yield* hasContent()}`;
+ },
},
- });
+ "component-answer",
+ );
}
const REPORTING: Array<[string, readonly boolean[]]> = [
@@ -3481,8 +3375,16 @@ describe("Tier GXC — the authored form survives the public content chain", ()
expect(String(attempt.evaluated.output)).toContain("the retained note");
// And the admission still names the identity and form it was granted for.
expect(recordedNames(admittedFragments(attempt.evaluated.events)[0])).toEqual([
- { name: "Says", identity: hostIdentity("test://says", "Says"), form: "self-closing" },
- { name: "File", identity: pinnedFileRead().identity, form: "self-closing" },
+ {
+ name: "Says",
+ identity: "https://executable.md/components/says/read",
+ form: "self-closing",
+ },
+ {
+ name: "File",
+ identity: "https://executable.md/components/file/read",
+ form: "self-closing",
+ },
]);
});
}
@@ -3524,8 +3426,16 @@ describe("Tier GXC — the authored form survives the public content chain", ()
expect(attempt.files).toEqual(["write:proposed.md"]);
expect(yield* readTextFile(join(root, "proposed.md"))).toBe("the fragment wrote this");
expect(recordedNames(admittedFragments(attempt.evaluated.events)[0])).toEqual([
- { name: "Says", identity: hostIdentity("test://says", "Says"), form: "self-closing" },
- { name: "File", identity: pinnedFileWrite().identity, form: "paired" },
+ {
+ name: "Says",
+ identity: "https://executable.md/components/says/read",
+ form: "self-closing",
+ },
+ {
+ name: "File",
+ identity: "https://executable.md/components/file/write",
+ form: "paired",
+ },
]);
});
}
diff --git a/packages/core/tests/shared-operation-permissions.test.ts b/packages/core/tests/shared-operation-permissions.test.ts
index 11b41ff14..76dd08abb 100644
--- a/packages/core/tests/shared-operation-permissions.test.ts
+++ b/packages/core/tests/shared-operation-permissions.test.ts
@@ -104,11 +104,7 @@ function ceiling(): ExecutionInstallation {
return {
evaluation: {
read: [fileReadEntry(), globReadEntry(), syntaxReadEntry()],
- write: [
- fileWriteEntry(),
- directoryEntry({ origin: "test://host", key: "Dir", revision: "1" }, "Dir"),
- fileDeleteEntry(),
- ],
+ write: [fileWriteEntry(), directoryEntry("Dir"), fileDeleteEntry()],
},
};
}
diff --git a/packages/core/tests/support/answer-provider.ts b/packages/core/tests/support/answer-provider.ts
index f3d1ed435..14a591e7d 100644
--- a/packages/core/tests/support/answer-provider.ts
+++ b/packages/core/tests/support/answer-provider.ts
@@ -2,10 +2,10 @@
* A trusted host's provider for one `component-answer` entry.
*
* The shape a real provider has: it composes `Component.importComponent`
- * middleware for its own name, and states an identity for the exact object it
- * returns using the request canonical execution minted for that handler
- * invocation. It holds no definition the profile could read, because the
- * profile arm it backs has nowhere to put one.
+ * middleware for its own name, and publishes the exact object it returns
+ * through the request canonical execution minted for that handler invocation.
+ * It holds no definition the profile could read, because the profile arm it
+ * backs has nowhere to put one.
*
* Every deviation a row needs is a member here rather than a second provider,
* so what one case changes about the honest one is visible in the case.
@@ -18,6 +18,7 @@ import type {
ComponentAnswerRegistrar,
ComponentAnswerRequest,
} from "../../host.ts";
+import type { GeneratedEffect, GeneratedForms } from "../../src/components/generated-metadata.ts";
import type { FunctionComponentDefinition, Json, PropsSchema } from "../../src/types.ts";
/** The contract an implementation states when a row does not care what it is. */
@@ -29,11 +30,20 @@ export interface Implementation {
readonly invoked: string[];
}
-/** One named implementation that records each time it is entered. */
+/**
+ * One named implementation that records each time it is entered.
+ *
+ * It offers generated use of its self-closing form, because that is what a
+ * provider's own declaration states and what an admitted entry reads: the
+ * identity and the effect are on the definition rather than beside it. A row
+ * that needs other metadata passes its own, and one that needs an
+ * implementation offering no generated use at all passes `null`.
+ */
export function implementation(
name: string,
label: string,
props: PropsSchema = NO_PROPS,
+ generated: GeneratedForms | null = offering(name, "read"),
): Implementation {
const invoked: string[] = [];
return {
@@ -42,6 +52,8 @@ export function implementation(
kind: "function",
name,
props,
+ forms: ["self-closing"],
+ ...(generated === null ? {} : { generated }),
// deno-lint-ignore require-yield
*fn(): Operation {
invoked.push(label);
@@ -51,22 +63,33 @@ export function implementation(
};
}
+/** What a provider's definition offers one spelling of `name` as. */
+export function offering(
+ name: string,
+ effect: GeneratedEffect,
+ form: "self-closing" | "paired" = "self-closing",
+): GeneratedForms {
+ return {
+ [form]: {
+ identity: `https://executable.md/components/${name.toLowerCase()}/${effect}`,
+ effect,
+ },
+ };
+}
+
/** How one row wants its provider to differ from the honest one. */
export interface ProviderOptions {
- /** The provider's own name for itself; the origin every identity statement carries. */
+ /** The provider's own name for itself; the provenance every publication carries. */
readonly origin?: string;
- /** The key and revision this provider states. */
- readonly key?: string;
- readonly revision?: string;
/** Every lookup this provider *answered*, in order. */
readonly lookups?: string[];
/** Every lookup this provider was asked at all, answered or delegated. */
readonly asked?: string[];
- /** Return the answer without stating an identity for it at all. */
- readonly unclaimed?: boolean;
- /** State the identity, then answer with a copy of the identified object. */
+ /** Return the answer without publishing it at all. */
+ readonly unpublished?: boolean;
+ /** Publish the definition, then answer with a copy of the published object. */
readonly copied?: boolean;
- /** State the identity, then edit the identified object. */
+ /** Publish the definition, then edit the published object. */
readonly mutated?: boolean;
/**
* Keep answering after the capture, rather than settling.
@@ -77,52 +100,51 @@ export interface ProviderOptions {
*/
readonly keepsAnswering?: boolean;
/**
- * Keep *stating identities* after the capture, rather than settling.
+ * Keep *publishing* after the capture, rather than settling.
*
* The other half of the same refusal, one step earlier: the resolution this
- * provider answered has settled, so there is no open window to state an
- * identity into and recording the identity refuses before the witness is
- * ever consulted.
+ * provider answered has settled, so there is no open window to publish into,
+ * and publishing refuses before the witness is ever consulted.
*/
- readonly reclaimsLater?: boolean;
+ readonly publishesLater?: boolean;
/**
- * Delegate first, and answer with this provider's own identified object.
+ * Delegate first, and answer with this provider's own published object.
*
* What an *outer* provider does. The one installed first composes outermost,
* so it sees the chain's answer before deciding, and returning its own
- * identified object is an honest substitution: the identity travels with the
- * object the chain finally gives back.
+ * published object is an honest substitution: the publication travels with
+ * the object the chain finally gives back.
*/
readonly delegatesFirst?: boolean;
/**
- * Delegate and answer with whatever came back, stating no identity.
+ * Delegate and answer with whatever came back, publishing nothing.
*
* What a *repeated* bootstrap does. A package entered through an inherited
* layer and again through a local one composes twice, and the outer entry
- * cannot state a second identity for the object the inner one already
- * identified — one implementation states what it is once. So it observes and
- * passes the answer through, which is what keeps the layering additive.
+ * cannot publish the object the inner one already published — one
+ * implementation is published once. So it observes and passes the answer
+ * through, which is what keeps the layering additive.
*/
readonly observesOnly?: boolean;
/** What delegating answered with, for a row that has to prove it happened. */
readonly delegated?: unknown[];
/**
- * Run this inside the live resolution, just before stating the identity.
+ * Run this inside the live resolution, just before publishing.
*
- * The one place a row can reach that is *during* an open identity-recording
- * window for this name — which is what a race between two resolutions has to
- * be observed from, since there is no other way to be inside one.
+ * The one place a row can reach that is *during* an open publication window
+ * for this name — which is what a race between two resolutions has to be
+ * observed from, since there is no other way to be inside one.
*/
readonly whileResolving?: (request: ComponentAnswerRequest) => void;
/**
* Answer with a value whose contract reads differently each time it is read.
*
* The check/use gap at execution scale. `[[Get]]` alternates between the
- * identified schema and a substitution; comparing own descriptors, which is
- * how the identity is checked, does not run the trap. So a capture that
+ * published schema and a substitution; comparing own descriptors, which is
+ * how a publication is checked, does not run the trap. So a capture that
* checked the answer and then *read it again* to take its copy would seal
* the substitution — and a fragment would be validated against a contract
- * the provider never identified.
+ * the provider never published.
*/
readonly alternating?: { readonly substitute: PropsSchema; readonly reads: string[] };
/** Where this provider leaves each request its handler received, for a row to keep. */
@@ -130,8 +152,8 @@ export interface ProviderOptions {
}
/**
- * One answer whose `props` alternates between what was identified and
- * something else, read by read.
+ * One answer whose `props` alternates between what was published and something
+ * else, read by read.
*
* Only `[[Get]]` is trapped, because that is what taking a copy of a definition
* uses and what comparing own descriptors does not. The schema is the member to
@@ -174,8 +196,6 @@ export function answerProvider(
options: ProviderOptions = {},
): ComponentAnswerInstallation {
const origin = options.origin ?? "test://provider";
- const key = options.key ?? name;
- const revision = options.revision ?? "1";
let settled = false;
return {
origin,
@@ -191,14 +211,14 @@ export function answerProvider(
options.retain?.push(request);
options.asked?.push(asked);
if (settled) {
- if (options.reclaimsLater) {
- // Still trying to *identify* an answer after its resolution
- // settled. There is no window open for this name, so the claim
- // itself is what refuses.
- return request.claim(answer, { key, revision });
+ if (options.publishesLater) {
+ // Still trying to publish an answer after its resolution settled.
+ // There is no window open for this name, so the publication itself
+ // is what refuses.
+ return request.answer(answer);
}
if (options.keepsAnswering) {
- // Still answering, without claiming. Whatever this returns is not
+ // Still answering, without publishing. Whatever this returns is not
// the object canonical execution issued for the import that asked,
// so the witness is what refuses.
return answer;
@@ -215,30 +235,31 @@ export function answerProvider(
if (options.delegatesFirst) {
// An outer provider sees the chain's answer before deciding. What it
// does with it is a row's business; recording it is what lets a row
- // say the inner half genuinely answered — and claiming *after*
+ // say the inner half genuinely answered — and publishing *after*
// delegating is what an outer replacement has to be able to do.
options.delegated?.push(yield* next());
}
options.whileResolving?.(request);
- if (options.unclaimed) {
+ if (options.unpublished) {
return answer;
}
const supplied =
options.alternating === undefined
? answer
: alternatingAnswer(answer, options.alternating.substitute, options.alternating.reads);
- const claimed = request.claim(supplied, { key, revision });
+ const published = request.answer(supplied);
if (options.copied) {
// The outer-replacement case: a handler further out returns its own
- // object, so the claim does not travel with the name.
- return { ...claimed };
+ // object, so the publication does not travel with the name — and
+ // copying the generated metadata with it changes nothing.
+ return { ...published };
}
if (options.mutated) {
- // The same object, edited after the claim. What was claimed is no
+ // The same object, edited after publication. What was published is no
// longer what is there.
- Object.assign(claimed, { name: `${name}Substituted` });
+ Object.assign(published, { name: `${name}Substituted` });
}
- return claimed;
+ return published;
});
},
};
diff --git a/packages/core/tests/syntax-component.test.ts b/packages/core/tests/syntax-component.test.ts
index 80b9f7c7c..1f3846f78 100644
--- a/packages/core/tests/syntax-component.test.ts
+++ b/packages/core/tests/syntax-component.test.ts
@@ -1014,18 +1014,9 @@ describe("Tier SYN — the named form", () => {
*/
const PROBE_LABEL = "probe ran";
- /** The provider-backed entry a layered host admits, at the identity it admits. */
- function layeredEntry(revision = "1"): FragmentEvaluationInput {
- return {
- read: [
- {
- kind: "component-answer",
- name: "Open",
- identity: { origin: "test://layer", key: "Open", revision },
- forms: ["self-closing"],
- },
- ],
- };
+ /** The provider-backed entry a layered host admits. */
+ function layeredEntry(): FragmentEvaluationInput {
+ return { read: [{ kind: "component-answer", name: "Open", forms: ["self-closing"] }] };
}
/** The registration the inner layer adds beside the bootstrap's own. */
@@ -1121,7 +1112,7 @@ describe("Tier SYN — the named form", () => {
}
let refused: unknown;
try {
- held.claim(implementation("Open", "late").definition, { key: "Open", revision: "1" });
+ held.answer(implementation("Open", "late").definition);
} catch (error) {
refused = error;
}
@@ -1164,14 +1155,16 @@ describe("Tier SYN — the named form", () => {
expect(answer.invoked).toEqual([]);
});
- it("FE30: a provider identity the layered host did not admit refuses", function* () {
+ it("FE30: a provider answer offering no admitted spelling refuses", function* () {
// The other refusal the provider half adds. The layering is honest and the
- // bootstrap's documentation and registrations are intact; what moved is the
- // revision the local provider claims. An admitted identity is the exact
- // implementation, so this refuses at capture — before the root import, and
- // therefore before any registration resolves or any fragment work happens.
+ // bootstrap's documentation and registrations are intact; what the local
+ // provider answers with is a definition whose own declaration offers no
+ // generated use of the spelling this host admitted. There is no identity
+ // for a continuation to be compared against, so this refuses at capture —
+ // before the root import, and therefore before any registration resolves or
+ // any fragment work happens.
const probed: string[] = [];
- const answer = implementation("Open", "the layered answer ran");
+ const answer = implementation("Open", "the layered answer ran", undefined, null);
const failed = yield* refusal(
scoped(function* () {
@@ -1183,12 +1176,9 @@ describe("Tier SYN — the named form", () => {
return yield* run(` \n\n \\n'} allow={["read"]} />\n`, [
stating(symbolsOf("Marker")).installation,
{
- evaluation: layeredEntry("1"),
+ evaluation: layeredEntry(),
componentAnswers: [
- answerProvider("Open", answer.definition, {
- origin: "test://layer",
- revision: "2",
- }),
+ answerProvider("Open", answer.definition, { origin: "test://layer" }),
],
},
]);
@@ -1196,7 +1186,7 @@ describe("Tier SYN — the named form", () => {
}),
);
- expect(failed).toContain("the exact implementation");
+ expect(failed).toContain("offers no generated use of it");
expect(answer.invoked).toEqual([]);
expect(probed).toEqual([]);
});
diff --git a/packages/core/tests/syntax-loaded-copy.test.ts b/packages/core/tests/syntax-loaded-copy.test.ts
index a2a4f6204..e02f50521 100644
--- a/packages/core/tests/syntax-loaded-copy.test.ts
+++ b/packages/core/tests/syntax-loaded-copy.test.ts
@@ -25,6 +25,13 @@
* answers; what is only provable here is that a *real* second copy is one of
* those answers.
*
+ * Tier FE14 in the middle asks the positive form of the same question about
+ * *provider* code: a module loaded with no access to canonical core at all
+ * receives the registrar and the request this execution minted, publishes its
+ * own definition through `answer(definition)`, and a generated fragment runs
+ * exactly that answer. The refusals above say what a second copy cannot do;
+ * that row says what one can.
+ *
* Tier FX at the end of this file asks the same question of the *other* shape
* in the protected tier. ``, `` and `` are answered
* with one ordinary definition built once per loaded copy rather than a body
@@ -38,7 +45,7 @@ import { describe, it } from "@executablemd/test-support/bdd";
import { expect } from "@executablemd/test-support/expect";
import { ensure, resource, scoped, until } from "effection";
import type { Operation } from "effection";
-import { rm } from "@effectionx/fs";
+import { rm, writeTextFile } from "@effectionx/fs";
import { exec } from "@effectionx/process";
import { API, Files, useHostFiles } from "@executablemd/runtime";
import { InMemoryStream } from "@executablemd/durable-streams";
@@ -51,7 +58,7 @@ import process from "node:process";
import { Component } from "../src/component-api.ts";
import { collect } from "../src/collect.ts";
import { executeInstalled } from "../host.ts";
-import type { ExecutionInstallation } from "../host.ts";
+import type { ComponentAnswerInstallation, ExecutionInstallation } from "../host.ts";
import { retainedSource } from "../src/root-source.ts";
import { SYNTAX_COMPONENT, props as syntaxProps } from "../src/components/Syntax.ts";
import { fixedFilesystemDefinition } from "../src/components/fixed-filesystem.ts";
@@ -130,6 +137,98 @@ function useSeparateCopy(): Operation {
});
}
+/**
+ * A provider module loaded with no access to canonical core.
+ *
+ * Written out and imported from a temporary directory, so the only things it
+ * can reach are the ones this execution hands it: the registrar its
+ * installation receives, and the request minted for each of its handler's
+ * invocations. It imports nothing — not core, not Effection — because
+ * publishing is one synchronous call on the request and delegation is one
+ * `yield*` of the operation it was given.
+ *
+ * The definition is the module's own object, built where the module is
+ * evaluated, and the identity it offers its self-closing spelling under is the
+ * module's own declaration. So what a fragment runs here is a provider's answer
+ * from outside this copy of core, admitted on the terms the declaration states.
+ */
+const LOADED_PROVIDER = `
+const invoked = [];
+const definition = {
+ kind: "function",
+ name: "Loaded",
+ props: { type: "object", properties: {}, additionalProperties: false },
+ forms: ["self-closing"],
+ generated: {
+ "self-closing": { identity: "https://executable.md/components/loaded/read", effect: "read" },
+ },
+ *fn() {
+ invoked.push("loaded answer ran");
+ return "LOADED ANSWER RAN";
+ },
+};
+
+let settled = false;
+
+export function installation(origin) {
+ return {
+ origin,
+ *install(registrar) {
+ yield* registrar.around(function* (request, next) {
+ // It answers the capture and settles, which is what a provider is for:
+ // the profile resolves its name once, before any document code, and a
+ // fragment then runs the sealed snapshot.
+ if (request.name !== "Loaded" || settled) {
+ return yield* next();
+ }
+ settled = true;
+ // The handed request, used for the one thing it is for. Nothing here
+ // holds a core module, a context, a symbol or a registry.
+ return request.answer(definition);
+ });
+ },
+ };
+}
+
+export function invocations() {
+ return [...invoked];
+}
+
+export function identity() {
+ return definition.generated["self-closing"].identity;
+}
+`;
+
+/** What that module exposes once it has been loaded. */
+interface LoadedProvider {
+ installation(origin: string): ComponentAnswerInstallation;
+ invocations(): readonly string[];
+ identity(): string;
+}
+
+function isLoadedProvider(value: unknown): value is LoadedProvider {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ typeof Reflect.get(value, "installation") === "function" &&
+ typeof Reflect.get(value, "invocations") === "function"
+ );
+}
+
+function useLoadedProvider(): Operation {
+ return resource(function* (provide) {
+ const directory = yield* until(mkdtemp(join(tmpdir(), "sl-provider-")));
+ yield* ensure(() => rm(directory, { recursive: true, force: true }));
+ const module = join(directory, "provider.js");
+ yield* writeTextFile(module, LOADED_PROVIDER);
+ const loaded: unknown = yield* until(import(`file://${module}`));
+ if (!isLoadedProvider(loaded)) {
+ throw new Error("the loaded module does not expose a provider installation");
+ }
+ yield* provide(loaded);
+ });
+}
+
/**
* A handler that delegates the protected import and then answers with something
* else.
@@ -245,17 +344,9 @@ describe("Tier SYN — a separately loaded protected implementation", () => {
};
const projected: unknown[] = [];
const observed: string[] = [];
- let captured = false;
const profile: ExecutionInstallation = {
evaluation: {
- read: [
- {
- kind: "component-answer",
- name: "Syntax",
- identity: { origin: "test://delegate", key: "Syntax", revision: "1" },
- forms: ["self-closing"],
- },
- ],
+ read: [{ kind: "component-answer", name: "Syntax", forms: ["self-closing"] }],
},
componentAnswers: [
{
@@ -266,11 +357,11 @@ describe("Tier SYN — a separately loaded protected implementation", () => {
if (request.name === "Syntax" && answer.kind === "function") {
observed.push(request.name);
projected.push(installed.componentRouting.project(answer.fn, forged));
- if (!captured) {
- captured = true;
- return request.claim(answer, { key: "Syntax", revision: "1" });
- }
}
+ // Delegated unchanged, so canonical core's own publication of the
+ // protected answer travels with it. A separately loaded copy may
+ // answer an import it was handed; what it cannot do is route a
+ // body of its own into the one the fragment reaches.
return answer;
});
},
@@ -289,6 +380,53 @@ describe("Tier SYN — a separately loaded protected implementation", () => {
});
});
+describe("Tier FE14 — a separately loaded provider publishes its own answer", () => {
+ it("FE14/U4-7: loaded provider code publishes through the handed request, and the fragment runs it", function* () {
+ const loaded = yield* useLoadedProvider();
+ const stream = new InMemoryStream();
+
+ // The admitted entry states the name and the spelling; what admitting it
+ // grants comes from the loaded module's own declaration.
+ const profile: ExecutionInstallation = {
+ evaluation: {
+ read: [{ kind: "component-answer", name: "Loaded", forms: ["self-closing"] }],
+ },
+ componentAnswers: [loaded.installation("test://loaded")],
+ };
+
+ const output = yield* scoped(function* () {
+ return yield* collect(
+ yield* executeInstalled(
+ {
+ ...retainedSource("documents/root.md", ` \\n'} />\n`),
+ stream,
+ includes: [],
+ },
+ [profile],
+ ),
+ );
+ });
+
+ // The loaded module's own body ran, once, and what it rendered is the
+ // fragment's output: the publication travelled with the object the chain
+ // returned, and canonical capture sealed that exact definition.
+ expect(String(output)).toContain("LOADED ANSWER RAN");
+ expect(loaded.invocations()).toEqual(["loaded answer ran"]);
+
+ // And the admission retains the identity that module declared for the
+ // spelling the fragment wrote — not a host-stated one, and not core's.
+ const admission = (yield* stream.readAll()).find(
+ (event) => event.type === "yield" && event.description.type === "generated_xmd",
+ );
+ const retained =
+ admission?.type === "yield" && admission.result.status === "ok"
+ ? admission.result.value
+ : undefined;
+ expect(JSON.stringify(retained)).toContain(loaded.identity());
+ expect(loaded.identity()).toBe("https://executable.md/components/loaded/read");
+ });
+});
+
describe("Tier FX — a fixed definition from a separately loaded copy", () => {
it("FX3g: another copy's File dispatcher answers for nothing, and reaches no I/O", function* () {
const copy = yield* useSeparateCopy();
diff --git a/packages/git/mod.ts b/packages/git/mod.ts
index 4f06bb181..a04a9409d 100644
--- a/packages/git/mod.ts
+++ b/packages/git/mod.ts
@@ -59,9 +59,10 @@
* These components were published by `@executablemd/workflow` before this
* package existed, and every journal, database row and artifact a released
* build wrote names that origin. Those strings identify retained history rather
- * than current source ownership, so they are preserved exactly here — including
- * the `@executablemd/workflow/composition` origin and the released
- * `@executablemd/workflow/composition/dir-v2#Dir` alias.
+ * than current source ownership, so the `@executablemd/workflow/composition`
+ * origin is preserved exactly here. What a generated `` runs as is not
+ * among them: that is stated by core's own declaration, which owns the
+ * implementation.
*/
export { default } from "./src/plugin.ts";
diff --git a/packages/git/src/composition/definitions.ts b/packages/git/src/composition/definitions.ts
index a094494f8..5f280cb3a 100644
--- a/packages/git/src/composition/definitions.ts
+++ b/packages/git/src/composition/definitions.ts
@@ -1,12 +1,12 @@
/**
- * This boundary's origin, and the generated-XMD identity it pins for ``.
+ * This boundary's origin, and the `` entry a workflow run admits.
*
- * The component itself is canonical core's
- * (`core/src/components/fixed-filesystem.ts`). What stays here is the *identity*
- * a workflow run's generated-XMD write table admits it under
- * (specs/workflow-workspace-spec.md §8.4): a host states which grant a fragment
- * is being given, and this host's grant is a string released builds already
- * wrote into their journals.
+ * The component is canonical core's
+ * (`core/src/components/fixed-filesystem.ts`), and so is what admitting it
+ * grants: core's own declaration states the identity and the effect its paired
+ * form runs as (specs/workflow-workspace-spec.md §8.4). What stays here is the
+ * host's decision to admit the name at all — a workflow run has a `` and
+ * an ordinary run has none.
*/
import { directoryEntry } from "@executablemd/core/host";
@@ -14,26 +14,14 @@ import type { FragmentEntry } from "@executablemd/core/host";
export const COMPOSITION_ORIGIN = "@executablemd/workflow/composition";
-/**
- * The origin released builds retained for ``'s generated-XMD entry.
- *
- * Written out rather than derived from this package's own name. It identifies
- * *retained history*: every journal a released build wrote holds this string,
- * and a continuation compares the write table position by position. The
- * component moved packages; what a run already recorded did not.
- */
-export const RETAINED_DIRECTORY_ORIGIN = "@executablemd/workflow/composition";
-
/**
* ``'s entry in a workflow run's generated-XMD write table.
*
* A host assembling the generated-evaluation profile asks for this rather than
- * writing the identity out again. Core supplies the body behind it, so this
- * names no definition of its own: what the grant admits is the one `` every
- * authored element reaches.
+ * writing the admission out again. Core supplies the body behind it and the
+ * identity it is admitted under, so this names no definition and no grant of
+ * its own: what it admits is the one `` every authored element reaches.
*/
export function gitDirectoryEntry(): FragmentEntry {
- return directoryEntry({ origin: RETAINED_DIRECTORY_ORIGIN, key: "Dir", revision: "3" }, "Dir", [
- "@executablemd/workflow/composition/dir-v2#Dir",
- ]);
+ return directoryEntry("Dir");
}
diff --git a/packages/workflow/src/deno/workspace/evaluate.ts b/packages/workflow/src/deno/workspace/evaluate.ts
index d57bbefa1..1bc52de5d 100644
--- a/packages/workflow/src/deno/workspace/evaluate.ts
+++ b/packages/workflow/src/deno/workspace/evaluate.ts
@@ -18,7 +18,9 @@
* existing middleware, effective Permissions and the run's effect transaction.
* Deletion remains a workspace_file effect. Relative paths use the run's contextual
* cwd, including Dir/Repository/Worktree placement, never the invocation host cwd.
- * This profile supplies identities, forms and ceilings rather than operations.
+ * This profile supplies admitted names, forms and ceilings rather than
+ * operations: which identity and effect each admitted spelling runs under comes
+ * from the declaration that owns its implementation.
*/
import { timeoutFetch } from "@executablemd/runtime";
@@ -49,9 +51,9 @@ export interface GeneratedEvaluationOptions {
/**
* The exact HTTP reads an admitted fragment may perform.
*
- * Empty admits `` not at all — the pinned identity is simply not on
- * the allowlist, which is a different thing from admitting it and refusing
- * every request.
+ * Empty admits `` not at all — the pinned entry is simply not on the
+ * allowlist, which is a different thing from admitting it and refusing every
+ * request.
*/
readonly requests?: readonly GeneratedRequest[];
/** Further read components this host admits beside core's pinned ones. */
diff --git a/packages/workflow/src/generated-observations.ts b/packages/workflow/src/generated-observations.ts
index c15aa7fc1..2cbe35b09 100644
--- a/packages/workflow/src/generated-observations.ts
+++ b/packages/workflow/src/generated-observations.ts
@@ -4,7 +4,7 @@
*
* Core owns the mechanics: parsing the candidate fragment, preflighting all of
* it before the first effect, resolving each name and authored form to exactly
- * one pinned identity, invoking only those, and recording one durable admission
+ * one pinned entry, invoking only those, and recording one durable admission
* before the first generated effect. What core does *not* own is which ceilings
* a workflow run is willing to state, and that is this module.
*
@@ -19,10 +19,10 @@
* have retained further roots and advanced the current one, while every
* non-root term is held exactly;
* - the exact HTTP reads a fragment may perform. An empty ceiling admits no
- * `` whatsoever: the pinned identity simply is not on the allowlist,
+ * `` whatsoever: the pinned entry simply is not on the allowlist,
* which is a different thing from admitting it and refusing every request;
- * - the read identities this run adds beyond core's own; and
- * - the mutation identities this run is willing to expose at all. They are
+ * - the read entries this run adds beyond core's own; and
+ * - the mutation entries this run is willing to expose at all. They are
* stated whether or not a caller asks for them, because what selects them is
* the caller's `allow`, and a class a run has no table for is refused before
* the candidate is parsed.
@@ -55,15 +55,15 @@ export interface GeneratedEvaluationPolicy {
readonly selectedRoot: string;
/** The exact HTTP reads a fragment may perform. Empty admits `` not at all. */
readonly requests: readonly GeneratedRequest[];
- /** The read identities this run states, beside the bounded `` below. */
+ /** The read entries this run states, beside the bounded `` below. */
readonly reads: readonly GeneratedObservation[];
- /** The mutation identities this run states. Absent admits `write` not at all. */
+ /** The mutation entries this run states. Absent admits `write` not at all. */
readonly writes?: readonly GeneratedMutation[];
/** The classes the caller selected. Omitted means `read`, as core's own default does. */
readonly allow?: readonly GeneratedEffectClass[];
}
-/** The read identities one policy resolves to, in the order a run states them. */
+/** The read entries one policy resolves to, in the order a run states them. */
function reads(policy: GeneratedEvaluationPolicy): GeneratedObservation[] {
const admitted: GeneratedObservation[] = [...policy.reads];
if (policy.requests.length > 0) {
diff --git a/packages/workflow/tests/generated-agent-component.test.ts b/packages/workflow/tests/generated-agent-component.test.ts
index 4b9a3bfe4..3b9c73b97 100644
--- a/packages/workflow/tests/generated-agent-component.test.ts
+++ b/packages/workflow/tests/generated-agent-component.test.ts
@@ -387,13 +387,20 @@ function* plant(database: WorkflowRunDatabase, path: string, content: string): O
}
/**
- * One capability identity, in the closed structural shape a run retains.
+ * One admitted spelling, as the row a run retains.
*
* Written out here rather than imported from the profile, so a row states what
- * the journal must hold rather than agreeing with whatever the profile computed.
+ * the journal must hold rather than agreeing with whatever the profile
+ * computed. The identity is the URL the declaration that owns the component
+ * offers that spelling under.
*/
-function capability(origin: string, key: string, revision: string): Record {
- return { kind: "capability", origin, key, revision };
+function row(
+ name: string,
+ form: "self-closing" | "paired",
+ identity: string,
+ effect: "read" | "write" | "composition",
+): Record {
+ return { name, form, identity, kind: "capability", effect };
}
describe("Tier WGAC — the registered Evaluate component", () => {
@@ -980,11 +987,7 @@ describe("Tier WGAC — the registered Evaluate component", () => {
* not the implementation's package. The profile supplies no directory entry
* of its own.
*/
-const HOST_DIRECTORY: FragmentEntry = directoryEntry(
- { origin: "@executablemd/workflow/composition", key: "Dir", revision: "3" },
- "Dir",
- ["@executablemd/workflow/composition/dir-v2#Dir"],
-);
+const HOST_DIRECTORY: FragmentEntry = directoryEntry("Dir");
/**
* Tier WGAC — the effect classes `` selects between
@@ -1072,8 +1075,12 @@ describe("Tier WGAC — the classes an authored element may select", () => {
const recorded = admissions(attempt.events);
expect(policyOf(recorded[0]!)?.allow).toEqual(["read"]);
// And the write table is not in the retained policy at all, so a run that
- // changed it has not changed what this admission was granted under.
- expect(JSON.stringify(policyOf(recorded[0]!))).not.toContain("File:write");
+ // changed it has not changed what this admission was granted under. Read
+ // as the identity the write spelling runs under, which is what a row in
+ // that table would hold.
+ expect(JSON.stringify(policyOf(recorded[0]!))).not.toContain(
+ "https://executable.md/components/file/write",
+ );
});
});
});
@@ -1103,26 +1110,10 @@ describe("Tier WGAC — the standard write table", () => {
expect(policy?.allowed).toEqual([
// The trusted composition table leads every policy, whatever the
// selection asked for.
- {
- name: "Json",
- identity: capability("@executablemd/core", "Json", "2"),
- forms: ["self-closing"],
- },
- {
- name: "File",
- identity: capability("@executablemd/core", "File:write", "2"),
- forms: ["paired"],
- },
- {
- name: "Dir",
- identity: capability("@executablemd/workflow/composition", "Dir", "3"),
- forms: ["paired"],
- },
- {
- name: "File.Delete",
- identity: capability("@executablemd/core", "File.Delete", "2"),
- forms: ["self-closing"],
- },
+ row("Json", "self-closing", "https://executable.md/components/json", "composition"),
+ row("File", "paired", "https://executable.md/components/file/write", "write"),
+ row("Dir", "paired", "https://executable.md/components/dir/ensure", "write"),
+ row("File.Delete", "self-closing", "https://executable.md/components/file/delete", "write"),
]);
});
});
@@ -1164,7 +1155,7 @@ describe("Tier WGAC — the standard write table", () => {
// rest of the ceiling. A host that states none is admitted under no
// directory entry at all, which is why every case here supplies one.
const attempt = yield* runDocument(database, evaluates(WRITES, ["write"]), {
- directory: directoryEntry({ origin: "tier-wgac", key: "Dir", revision: "1" }, "Dir"),
+ directory: directoryEntry("Dir"),
});
expect(attempt.failure).toBe(undefined);
@@ -1175,26 +1166,10 @@ describe("Tier WGAC — the standard write table", () => {
// continuation compares one directory grant and not two.
const policy = policyOf(admissions(attempt.events)[0]!);
expect(policy?.allowed).toEqual([
- {
- name: "Json",
- identity: capability("@executablemd/core", "Json", "2"),
- forms: ["self-closing"],
- },
- {
- name: "File",
- identity: capability("@executablemd/core", "File:write", "2"),
- forms: ["paired"],
- },
- {
- name: "Dir",
- identity: capability("tier-wgac", "Dir", "1"),
- forms: ["paired"],
- },
- {
- name: "File.Delete",
- identity: capability("@executablemd/core", "File.Delete", "2"),
- forms: ["self-closing"],
- },
+ row("Json", "self-closing", "https://executable.md/components/json", "composition"),
+ row("File", "paired", "https://executable.md/components/file/write", "write"),
+ row("Dir", "paired", "https://executable.md/components/dir/ensure", "write"),
+ row("File.Delete", "self-closing", "https://executable.md/components/file/delete", "write"),
]);
});
});
@@ -1208,7 +1183,7 @@ describe("Tier WGAC — the standard write table", () => {
// own keeps core's write and delete and this package's directory entry,
// and its own entry follows them.
const attempt = yield* runDocument(database, evaluates(WRITES, ["write"]), {
- writes: [directoryEntry({ origin: "tier-wgac", key: "Nested", revision: "1" }, "Nested")],
+ writes: [directoryEntry("Nested")],
});
expect(attempt.failure).toBe(undefined);
@@ -1216,33 +1191,13 @@ describe("Tier WGAC — the standard write table", () => {
const policy = policyOf(admissions(attempt.events)[0]!);
expect(policy?.allowed).toEqual([
- {
- name: "Json",
- identity: capability("@executablemd/core", "Json", "2"),
- forms: ["self-closing"],
- },
- {
- name: "File",
- identity: capability("@executablemd/core", "File:write", "2"),
- forms: ["paired"],
- },
+ row("Json", "self-closing", "https://executable.md/components/json", "composition"),
+ row("File", "paired", "https://executable.md/components/file/write", "write"),
// The released entry, still in its own position and unmoved by the
// addition beside it.
- {
- name: "Dir",
- identity: capability("@executablemd/workflow/composition", "Dir", "3"),
- forms: ["paired"],
- },
- {
- name: "File.Delete",
- identity: capability("@executablemd/core", "File.Delete", "2"),
- forms: ["self-closing"],
- },
- {
- name: "Nested",
- identity: capability("tier-wgac", "Nested", "1"),
- forms: ["paired"],
- },
+ row("Dir", "paired", "https://executable.md/components/dir/ensure", "write"),
+ row("File.Delete", "self-closing", "https://executable.md/components/file/delete", "write"),
+ row("Nested", "paired", "https://executable.md/components/dir/ensure", "write"),
]);
});
});
@@ -1289,15 +1244,13 @@ describe("Tier WGAC — the standard write table", () => {
// Admitted as the exact self-closing identity, and named by the fragment
// as the element was written.
const admission = admissions(attempt.events)[0]!;
- expect(policyOf(admission)?.allowed).toContainEqual({
- name: "File.Delete",
- identity: capability("@executablemd/core", "File.Delete", "2"),
- forms: ["self-closing"],
- });
+ expect(policyOf(admission)?.allowed).toContainEqual(
+ row("File.Delete", "self-closing", "https://executable.md/components/file/delete", "write"),
+ );
expect(recordedNames(admission)).toEqual([
{
name: "File.Delete",
- identity: capability("@executablemd/core", "File.Delete", "2"),
+ identity: "https://executable.md/components/file/delete",
form: "self-closing",
},
]);
diff --git a/packages/workflow/tests/generated-observations.test.ts b/packages/workflow/tests/generated-observations.test.ts
index ea2355f9c..17f101106 100644
--- a/packages/workflow/tests/generated-observations.test.ts
+++ b/packages/workflow/tests/generated-observations.test.ts
@@ -175,10 +175,10 @@ describe("Tier WGX — the roots a run selects", () => {
if (admission?.type !== "yield") {
throw new Error("the run recorded no generated-XMD admission");
}
- // The version-2 shape: a workflow host states a Workspace basis, and it
+ // The current shape: a workflow host states a Workspace basis, and it
// travels as one member because an ordinary host states none at all.
expect(admission.description.input).toMatchObject({
- version: 2,
+ version: 3,
workspace: { roots: [PRIMARY, SECONDARY], selectedRoot: PRIMARY },
});
});
diff --git a/scripts/deps.ts b/scripts/deps.ts
index 93fd94df4..e3fa3dfdc 100644
--- a/scripts/deps.ts
+++ b/scripts/deps.ts
@@ -46,6 +46,12 @@ const ENTRYPOINTS = [
"packages/cli/src/compiled.ts",
"scripts/compile.ts",
"scripts/verify-clean.ts",
+ // The npm build, which `scripts/tests/cli-npm-bin.test.ts` runs
+ // `--cached-only`. A typecheck of this graph is not enough: it reads dnt's
+ // types and leaves `@ts-morph/bootstrap`'s module bodies unfetched, so the
+ // build found them only when a test that fetches them happened to run first
+ // in the same shard.
+ "scripts/build-npm.ts",
];
function* deno(args: string[]): Operation {
diff --git a/specs/executable-mdx-spec.md b/specs/executable-mdx-spec.md
index a7f21436b..11dbea17f 100644
--- a/specs/executable-mdx-spec.md
+++ b/specs/executable-mdx-spec.md
@@ -1961,9 +1961,11 @@ run but are absent from the diagnostic trace.
| `src/fetch-request.ts` | `prepareFetchRequest()`, `FetchRequest`, `FetchRequestError` — what `` admits before transport (§6.18) |
| `src/fetch-response.ts` | `detachHeaders()`, `detachStatus()`, `FetchResponseRecord` — the response detached from the provider's (§6.18) |
| `src/fetch-journal.ts` | `persistFetch()` — the `fetch` durable effect (§6.18, §10.1) |
-| `src/generated-xmd.ts` | `evaluateGeneratedXmd()`, `pinnedFetch()`, `pinnedFileRead()`, `pinnedFileWrite()`, `pinnedFileDelete()`, `pinnedComponent()`, `pinnedMutation()`, `GeneratedEffectClass`, `GeneratedComponentForm`, `GeneratedMutation`, `GeneratedObservation`, `GeneratedRequest`, `GeneratedXmdError` — admitting Agent-generated source through the trusted-host seam under the always-on trusted composition table and a caller-selected subset of the closed effect classes `read` and `write`, resolving each name and authored form to one exact pinned identity — core Json, the self-closing `` read, the paired `