Repository navigation
💥 Let a component say what its own spellings are offered as (#181) - #919
Conversation
A generated fragment names a component, and two facts decide what naming it grants: the identity a continuation is compared against, and whether the spelling observes or mutates. Both were stated beside the definition, by whichever host assembled the table, so one component's two spellings were two host-stated grants and a host could describe an effect the implementation does not have. Both now travel on the declaration that owns the implementation, per authored spelling: `<File />` is offered under `https://executable.md/components/file/read` as a read and `<File>…</File>` under `.../file/write` as a write, on one definition. A provider publishes that definition through its existing request — `answer(definition)` in place of `claim(answer, identity)` — and the admitting tables read each selected spelling's identity and effect off the definition they already hold, refusing a spelling its owner offers no generated use of or offers as the other effect. The retained admission holds one row per admitted spelling: the name, the spelling, that URL, what kind of thing answered for the name, and the effect table that admitted it. There is one closed record shape, marked as itself; a record an earlier build wrote is refused as unreadable history before the admission it would have restored performs anything, and an updated implementation under the same URL is no longer refused for lacking a revision. Hosts keep deciding which names and spellings a run admits and stop restating what admitting them grants: the Git Plugin's `<Dir>` entry carries no identity or compatibility constant, and `FragmentIdentity`, `CORE_REVISION`, the version-1 alias lists and the structural answer identity are gone.
#181) The Planner's probe found three gaps in the owner-metadata boundary. A directly pinned entry reached the generated table unread: a definition accepting only its paired form still ran `<Probe />`, and an identity that is not a URL was appended as an admission before the reader of that fresh record rejected it. The declaration is read now in `selectedEntries`, for observations and mutations alike, so a malformed one refuses before any record exists and before any body runs. The captured profile reads a sealed definition the same way. The accepted-forms contract moves to `invocation-forms.ts`, which the metadata validator can read without going through the component graph. Only a genuinely omitted `forms` means both spellings, so `forms: "paired"` and `["self-closing", "unknown"]` are refused rather than treated as absent or filtered into a declaration that looks canonical. Registration validated the declarer's object and then copied it again, so an identity accessor answering a valid URL for the check and `not-a-url` for the copy installed the value the check never saw. `admitGeneratedForms()` now reads each member exactly once and answers with the copy, and registration, every identity-component arm and core's own owners retain that same value; a failed capture leaves the whole batch uninstalled. U4-7's positive case is covered: a provider module loaded from a temporary directory, holding no core module at all, publishes its own definition through the handed `answer(definition)`, the fragment runs that body, and the admission retains the identity the loaded module's own declaration states.
Direct generated admission validated an owned copy of a host's metadata and then threw it away: `observedForms`, `offeredIdentities` and the entry's own definition each read the host's object again. An identity answering the owner's URL on its first reading and `not-a-url` afterwards therefore appended an admission holding the second one — a record the reader of that same fresh journal then refused as unreadable history, after it had committed. Both public pinned constructors reached it. `admitOffered()` now keeps what it read. One pass copies the definition, reads each member of its metadata once into the frozen map this run owns, validates that map, and answers with both; the selected spellings, their URLs, the retained policy rows and the definition a fragment's import is answered with all come from it. The reading kept is kept per definition object, so a name admitted for two spellings is still one component with one implementation, and the implementation itself still crosses by reference. The refusal-only helper stays where the data is already this engine's — a provider's published answer is core's structured copy, and a captured profile entry holds that copy or one of core's own owners — and says so, so it is not reached for again as the only defense on a value a caller still controls.
A host may declare a component to an execution instead of registering one, and capture reads every member of that declaration once because reading one later would be reading it after an installation ran. The member this slice adds was not among them: `retainedIdentityComponent()` copied the name, origin, schemas, forms, prose and factory, so a valid offer was dropped before the implementation was built. The supported path then failed on honest input — the provider published the constructed definition, and sealing refused the entry because the definition it was handed offered nothing. Ordinary registration of the same declaration worked. Capture now reads the offer through the validator that owns the contract, and retains what it answers with. It is the one member whose copy is a grant — a URL and an effect decide which implementation a continuation admits — so a reading taken later, or a nested object left live for construction to be handed, would be a grant the declaration never made. The forms are read first, into the copy, so the offer is held against the spellings this execution accepts. The built implementation, the admitted rows and the definition a fragment runs all come from that one reading, and a declaration this engine cannot read refuses where it is captured, before anything is built, asked or written down. Four rows in the existing GOM tier drive it through a real execution: the host declares, the execution builds, the provider publishes that exact object, and the fragment names the offered spelling. Dropping the member again fails the three positive rows with the refusal this round reported; retaining the host's object instead of the validated reading admits the substituted URL.
807f525 to
021fa6c
Compare
| // the write table names one identity rather than two copies of it. | ||
| // `<Dir>`'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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // core's, from the declaration that owns the component. |
| } 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // members a declaration states; nothing here reads or publishes a catalog of them. |
| // 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 `<Syntax />` is compared against the identity | ||
| // its owner states rather than one a host wrote beside the entry. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // its owner states rather than one a host wrote beside the entry. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // none of them, which is why it does not publish. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // here to validate. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // as it was. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // publishes nothing either. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // read again — so nothing this run keeps was decided by a second read. |
| 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // forms this execution accepts the component in. |
| ): Entry[] { | ||
| const entries: Entry[] = []; | ||
| // One kept reading per definition object, so a name admitted for two | ||
| // spellings stays one component with one implementation. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // spellings stays one component with one implementation. |
PR #919: 💥 Let a component say what its own spellings are offered as (#181)48 files, +4155 / -3004 Scope🔴 PR has 7159 lines changed. Split into focused PRs. 🟡 7159 lines changed. PRs under 400 receive more thorough review. 🟡 48 files changed. Are all changes related? 🟡 Changes span 8 directories. StructuralOxlint structural signals:
Slop
Oxlint slop signals:
Static AnalysisOxlint: 60 diagnostics across 10 files (15 rules) no-shadow (12): packages/core/src/components/Syntax.ts, packages/core/src/generated-xmd.ts, packages/core/src/execute.ts CorrectnessNo extraneous code patterns detected. |
The chapter that owns sealed evaluation entries still specified the protocol this slice replaced: a host entry stating an expected identity, one identity per name across its spellings, a provider supplying a key and a revision, and capability entries reconciling released untagged records through stated aliases. It now says what the entry states — the name and the spellings — and what the publication states: core's copy of the definition, with each spelling's URL and effect read off the declaration that owns the implementation. Every request and window lifetime constraint is kept, including the first-publication baseline a later window cannot rebaseline. Two paragraphs above it claimed `<File>` is admitted only to read and that mutation admission is unbuilt, which the write table in the same chapter contradicts. The profile's own comments described the private filesystem route that is gone. An admitted capability is core's ordinary component: it resolves the contextual working directory and performs its operation through `API.Files` or `API.Fetch` under the installed provider and the consuming scope's Permissions, rather than over an operation the host handed the profile. The `write` table documented one set of entries for both standard profiles, where the ordinary one admits `<Elicit>` and the workflow one admits `<Dir>`. The reference gained the two shapes a reader had to infer from tests: a declaration offering both of its spellings, keyed by spelling with the literal effects, and a provider publishing that definition through `request.answer()`. Historical asides go with them — what the metadata used to be stated beside, and what an origin has always been — leaving the present role each one has.
| } 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // members a declaration states; nothing here reads or publishes a catalog of them. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // here to validate. |
| 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // no copy of it, whatever its siblings did. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // first table that admits the name. |
| 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // forms this execution accepts the component in. |
| 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // generated metadata: a pure component offers no effect to declare. |
| `a generated-XMD allowlist admitted <${name}> whose declaration ${reason}.`, | ||
| ); | ||
| }; | ||
| // One read of every member, into this run's own object. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // One read of every member, into this run's own object. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // copying the generated metadata with it changes nothing. |
| } | ||
| 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 |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // The same object, edited after publication. What was published is no |
… build (#181) Three CI jobs failed on this branch, and the local feedback set could not see any of them. Two were suites outside it still asserting the protocol this slice replaced. WGAC17 read the retained Dir admission as `"key":"Dir"` with `"revision":"3"`, and then moved the revision to prove a continuation admitted under the earlier grant refuses. Both halves now read what the record holds: the name, the spelling, the identity core's own declaration offers that spelling as, and the table that admitted it — and the refusal half moves the identity to another URL in the accepted namespace, so the record stays readable and what refuses is still the grant comparison rather than unreadable history. It asserts the refusal publishes neither identity. WAL9 read the same record as `File:write` and as an origin/key/revision triple, and now reads both admitted spellings under the URLs their owner offers them as. WGAC9's negative control asserted the absence of `File:write`, a string no record can hold any more, so it was vacuous; it now names the write identity. The third was a shard that could no longer build an npm package offline. `cli-npm-bin.test.ts` runs `scripts/build-npm.ts` with `--cached-only`, and the dnt graph's `@ts-morph/bootstrap` module bodies were in the cache only because a test that fetches them happened to run first in the same shard. Adding a test file moved the shard boundary and separated them. `scripts/deps.ts` now caches that entrypoint, which is what it exists for: a typecheck of the same graph reads dnt's types and leaves those bodies unfetched, so with a scratch `DENO_DIR` the build reports exactly CI's missing specifier after a check and completes after the entrypoint install. The prose those cases describe is corrected with them: the architecture's grant boundary is the identity a spelling runs under rather than a versioned origin, the Git Plugin preserves its released origin but no `dir-v2` alias, and WGAC10, WGAC17 and ORC6i state the admitted spellings and their URLs.
| 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // no copy of it, whatever its siblings did. |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // offered as a read. |
| `a generated-XMD allowlist admitted <${name}> whose declaration ${reason}.`, | ||
| ); | ||
| }; | ||
| // One read of every member, into this run's own object. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // One read of every member, into this run's own object. |
| } | ||
| 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 |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // The same object, edited after publication. What was published is no |
| // 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. |
There was a problem hiding this comment.
Redundant comment — restates what the code does.
| // as it was. |
Why
A generated fragment names a component, and two facts decide what naming it grants: the identity a continuation is compared against, and whether that spelling observes or mutates. Both were stated beside the definition, by whichever host assembled the admitting table — so one component's two spellings were two host-stated grants, every host had to restate them, and a host could describe an effect the implementation does not have.
This puts both on the declaration that owns the implementation, per authored spelling, and makes every admitting table read them off the definition it already holds.
It is the fourth prerequisite in the #181 stack (after #905, #906 and #912) and is based on
main. #902's consumer work stays held behind the remaining prerequisite.What changes
Before:
identity: { origin, key, revision }beside each entry, and the Git Plugin carried<Dir>'s released origin, revision and a version-1 alias list so old journals still reconciled.<File>'s reading and writing spellings were two host-stated grants under one triple-per-name rule.request.claim(answer, { key, revision })— it stated what its implementation was, separately from supplying it.After:
generated: per authored spelling, an absolutehttps://executable.md/identity and one ofread/write.<File />is…/components/file/readas a read and<File>…</File>is…/components/file/writeas a write, on one definition.directoryEntry("Dir")carries no identity, andgitDirectoryEntry()carries no compatibility constant.request.answer(definition). The installation origin stays provenance a reader and a documentation symbol are shown, and no part of what the implementation is.How it works
A registration, a factory declaration and a resolved provider definition all reach one validator.
admitGeneratedForms()reads each member of the metadata exactly once into frozen owned data, validates that data, and answers with it, so the reading that was validated is the reading that is retained —Result<GeneratedForms>, not a check beside a copy. The canonical authored-forms contract moved tosrc/invocation-forms.tsso the metadata validator can read it without going through the component graph it validates against.Every live admission then reads the definition it was handed, because each of them is handed one nothing else validated: a provider's answer is read where it is published, a directly pinned entry where
evaluateGeneratedXmd()selects it, and a captured profile entry where it is sealed. Direct admission keeps what it read — per definition object, so a name admitted for two spellings stays one component with one implementation — and derives the selected spellings, their URLs, the retained rows and the definition a fragment's import is answered with from that one value.Review guide
Start with:
packages/core/src/components/generated-metadata.tsThen review:
packages/core/src/types.tsandsrc/components/registration.ts— the declaration member, and capture-validate-retain as one valuesrc/components/fixed-filesystem.ts,src/components/registry.ts,src/components/Syntax.ts— the shipped owner tablesrc/components/component-resolution.ts—answer(definition)in place ofclaim(answer, identity)src/generated-xmd.ts— form/identity derivation, and the one closed record shapesrc/evaluation-profile.ts,src/execute.ts,src/components/Evaluate.ts— capture with no host-stated identitypackages/git/src/composition/definitions.ts,packages/workflow/src/**,packages/cli/src/**Look carefully at:
generated-xmd.ts::admitOffered— the reading it keeps is what the journal holds; a check beside a re-read is exactly the defect two review rounds found.component-resolution.ts::#record— the publication baseline is validated once, and a changed object cannot be rebaselined by a later window.execute.ts::retainedIdentityComponent— a host's declaration crosses capture, so the offer is read there, once, and the implementation is built from that reading.What must stay true
generatedoff the owning definition at every admitting table; checked byEP12,EP19,GXC2candGOM2.admitGeneratedForms()answering with its own frozen copy, and by direct admission and trusted capture keeping it; checked by the twoGOM1accessor rows, theGOM4capture rows and the twoGXC2calternating-read rows.isGeneratedIdentity()parsing the origin rather than a prefix; checked byGOM1and the look-alike-originGXC2crow.EP18,GXC2b.FE18/GX21ytable.allowclass, whole-fragment preflight and canonical generated import; checked byFE14andFE34.How to verify it
GOM1–GOM3prove a declaration's metadata is validated, copied, frozen and read once, and fail if a malformed map installs, an accessor's second reading is retained, or a shared fixed definition's nested metadata is mutable.GXC2cproves a directly pinned observation and mutation are held to their own declaration and admitted from one reading, and fails if a non-URL identity, a look-alike origin, an unaccepted spelling or malformedformsreaches a record — or if an alternating identity is read twice.GOM4proves a host's factory declaration keeps its offer through capture: the built implementation runs the offered spelling under the URL and effect the admission retained, the offer is read once, a replacement from insideinstall()replaces nothing, and an unusable one refuses before anything is built, asked or journaled.EP18–EP20prove one provider-backed name resolves once to one definition with per-spelling identities, and fail if a table can describe a spelling or effect the implementation does not offer.FE18/GX21zandFE18/GX21yprove the current record restores, the same URL with an updated implementation resumes, and 22 malformed or earlier-build records refuse before any effect.FE14/U4-7(Deno-only) proves provider code loaded from a temporary directory, holding no core module, publishes through the handedanswer(definition)and that the fragment runs that body under the identity the loaded module declares.WGAC,cli/tests/evaluate-workflow.test.tsandgit/tests/repository-components.test.tsprove the shipped hosts' tables, ordering and Permissions behavior survive the schema change.Ten frozen feedback commands, all green at
a26adf1b(rebased ontomainatb5a2b3cc): Deno groups26/151,36/333,14/83,21/131,19/141; Node315 pass; Bun315 pass;deno task lint:dead-code;deno task build; compiled6/16.deno task lintanddeno task checkare also clean.Scope
Included
generatedon component declarations, resolved function definitions and the identity/protected/private construction paths, validated and retained as one value.<File>(read/write),<File.Delete>,<Dir>,<Glob>,<Fetch>,<Syntax />,<Elicit>.request.answer(definition)replacingclaim(answer, identity), with the installation origin as provenance.architecture.md's generated-identity boundary.Intentionally unchanged
ExecutionInstallation.evaluation, its read/write host selection, the one-profile rule,statesEvaluation, Workspace input,fetchTimeoutand the static request ceilings — all removed by the next prerequisite, not this one.<Json>stays always-on trusted composition: it offers no read or write spelling, and its identity is core's own internal…/components/json.<Evaluate>offers no generated use at all, so no table can name it.<Test>harness objects stay refused.New abstractions
components/generated-metadata.tsexists because three declaration paths and three admitting tables need one reading of the same fact; consumers are registration,installIdentities, the core registry and fixed tier, publication,generated-xmd.tsandevaluation-profile.ts.invocation-forms.tsexists because the metadata validator must read the accepted-forms contract thatregistration.ts,declared-markdown.tsandexecution-declarations.tsalready use, without importing the component graph (which would reopen the initialization cycle U3 exposed).Risks and limitations
FragmentIdentity,RetainedFragmentIdentity,CORE_REVISION,AnswerIdentity/ClaimedIdentity,identityRecordand everylegacyalias path are gone;pinnedComponent,pinnedMutationanddirectoryEntrytake fewer arguments. In-repo consumers are migrated in this PR.SyntaxSymbols, because the implementation is core's; previously it reported the host's stated identity origin.packages/core/tests/generated-xmd.test.tsandevaluate-component.test.tscarry fixture and record-shape changes rather than new behavior; where a row's old identity premise became false it was replaced with the current-contract assertion rather than deleted.Scope confirmation