spec: one authority for enableOnInstall, and a read-out of its other two declarations - #19130
Conversation
…ne authority `enableOnInstall` is declared in three published schemas. The install door's request contract (`api/package-api.zod.ts`) is the one authority: it is the contract of the door that honours the key. The other two are re-read here. - `kernel/InstallPackageRequestSchema` is a COPY of the request key, restated on the in-process protocol primitive. It is held to the authority by a parity pin rather than by a structural reference: the authority sits above `kernel/` in the module graph, so `…Schema.shape.enableOnInstall` spelled there is an import cycle that dies under `OS_EAGER_SCHEMAS=1`. - `marketplace/MarketplaceInstallRequestSchema` means something else and stays: its subject is a marketplace listing, its door is the control plane's, and its key is one translation upstream of the door key. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
…able-on-install-one-authority
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 20c6f3722cd63d92db0efd93c42c0b3937a7e2f6 && git checkout 20c6f3722cd63d92db0efd93c42c0b3937a7e2f6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781 5234daa021ad86dfd5502406ad1926adb9c82ed0 && git checkout -B drift-repro 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781 && git merge --no-ff 5234daa021ad86dfd5502406ad1926adb9c82ed0
node scripts/docs-audit/affected-docs.mjs --json 9bb059dbfe1fdf6443cb9f76266d2a0ebff71781
|
Fixes #18605
Clause-②: yes — carrier: the changeset
.changeset/18605-enable-on-install-one-authority.md(@objectstack/spec, minor). Three published declarations' stated meaning moves; the accept set does not move at all.Ruling bullet 1 was already discharged by PR #18752 — this PR did not skip it
Batch #153 item 5, letter 1 carries two bullets. The first one — the install door writes the registry row's
enabledfromenableOnInstall ?? true— landed with PR #18752 (card #18058), and this claimant re-derived that against the merged diff before planning, rather than inheriting the card's text:packages/runtime/src/domains/packages.tsreadsbody.enableOnInstall === falseoff the WRAPPED body, flips the registry row through the same callPATCH /packages/:id/disableuses, and then persists the row the door returned to the durable state file.packages/runtime/src/domains/packages-install-enable-on-install.test.tspins it, header and all:falseinstalls disabled in all three records,falsealso movesstatus,trueinstalls enabled, an absent key defaults totrue, the disable is durable across a restart, and a re-install withtrueclears it.origin/mainas of this branch's merge (asserted by quoted-exact-namegit grepagainstorigin/main).So the card's own premise — "honoured by no handler" — is false for the authority's door on today's
main, and this PR carries bullet 2 and only bullet 2.Bullet 2, verbatim
The read-out, per declaration
All three read
enableOnInstall: z.boolean().default(true)with byte-identical description text, so identical shape carried no information. What distinguishes them is the request each sits on and the door that serves it.api/PackageInstallRequestSchemaPOST /api/v1/packages, the door that honours the keykernel/InstallPackageRequestSchemaObjectStackProtocol.installPackagemarketplace/MarketplaceInstallRequestSchemaInstalledPackage.enabled, a different key in the same file. The marketplace declaration is not a listing attribute:MarketplaceListingSchemadoes not carry it; it sits on the install request besidelistingId.The authority —
api/PackageInstallRequestSchema.enableOnInstallIt is the authority because it is the request contract of the door that honours the key. Its published description now says so, so a reader of the reference page can tell which of three rows is the one that acts: "honoured at POST /api/v1/packages: the installed row's
enabledis written from this key". Its doc block carries the map to the other two, so nobody has to re-derive this reading a third time.Re-read ① —
kernel/InstallPackageRequestSchema.enableOnInstallis a COPYSame type, same default, same meaning, restated one layer down on the in-process protocol primitive. Two measured facts decide it:
MetadataProtocol.installPackagereadsrequest.manifestandrequest.settingsand nothing else (packages/metadata-protocol/src/protocol.ts). The key reaches no code that acts on it there.installPackage({ manifest, settings })and performs the enable/disable flip itself afterwards, because the durable half must follow the ROW that door returned rather than the request's intent. That is deliberate and documented at the call site.It is therefore a copy, and per the ruling it must not be left unreferenced. The reference is documentary in the declaration and MECHANICAL in a pin, for a reason that was measured rather than assumed — see the next section.
packages/spec/src/api/package-install-one-authority.test.tsparses the authority and the copy over one matrix (absent,false,true, a string,null) and reds on any cell where they disagree, so the copy can no longer drift from the authority silently.Re-read ② —
marketplace/MarketplaceInstallRequestSchema.enableOnInstallmeans something elseSame words, a different commitment, and the difference is the subject of the request it sits on:
listingId,version,licenseKey,tenantId). The authority's subject is a MANIFEST. Neither body can be sent where the other is expected, which the pin asserts in both directions.POST /api/v1/marketplace/install; a runtime mounts/api/v1/marketplace/*only as a read-only proxy to the configured control plane (MarketplaceProxyPlugin).docs/design/marketplace-publishing.md§4.3 spells the flow out: the channel fetches the artefact and validates the licence and only THEN maps what it holds into a platform install. So this key is what a caller asks the marketplace to request on its behalf — one translation upstream of the door key.cloud/MarketplaceInstallRequestbefore it moved into this namespace (packages/spec/scripts/lib/renamed-defs.ts). One shared declaration would let a narrowing at the platform door silently narrow a control-plane contract that no PR in this repo can see.So it stays, and its published description now says which of the two it is.
The prescription that is not executable as written, and the measurement
"A copy of the request key is folded to a reference" reads naturally as
enableOnInstall: PackageInstallRequestSchema.shape.enableOnInstallat the copy's site. That spelling is not available in this direction, and it is not a style preference — it is measured.The authority sits ABOVE both copies in the module graph:
PackageInstallRequestSchemais built fromManifestSchemaandInstalledPackageSchema(declared inkernel/package-registry.zod.ts) and fromArtifactReferenceSchema(declared inmarketplace/marketplace.zod.ts). A reference from either copy up to the authority is therefore an import cycle, and it is not a cycle thelazySchemaproxy absorbs: underOS_EAGER_SCHEMAS=1— the modegen:schema,gen:authorable-surface-baseandcheck:authorable-surfacerun in — the factory bodies evaluate at module load and the cycle dies.Measured on this branch, both directions, each against a control that passes on the unmodified tree:
kernel/package-registry.zod, unmodifiedenableOnInstalldefaults totrueenableOnInstall: PackageInstallRequestSchema.shape.enableOnInstallReferenceError: Cannot access 'InstalledPackageSchema' before initialization, raised fromapi/package-api.zod.tsthroughlazySchemamarketplace/marketplace.zod, unmodifiedenableOnInstalldefaults totrueReferenceError: Cannot access 'ArtifactReferenceSchema' before initializationUnder the default lazy mode both treatments load fine, which is the dangerous half: the runtime would be green and the generator would die.
Both treatments were reverted and the revert proven by blob hash against
HEAD(git hash-objectequal,git diff HEADempty) before anything else was written.⇒ The only structural fold available would be to move the key's literal into a module BELOW both copies and have the authority import it. That was deliberately not taken unilaterally: it moves the declaration out of
package-api.zod.ts, which is the file the ruling names as the one authority, so it changes the ruling's own terms. It is recorded as an open question below rather than performed.What moved on the published surface
.describe()strings — the textcontent/docs/references/**renders, and the only half of a doc block an author reading the reference page ever sees.PackageInstallBodyrenders its wrapped branch).What did not move: no key added, removed, renamed or retyped, no default changed.
check:api-surface,check:api-surface-declarations,check:authorable-surface,check:export-originsandcheck:declaration-mapare all green with no regeneration — theapi-surface-declarationsshards this card was flagged for do not move, because a.describe()change does not change a.d.tstype.Verification
Run on the merged tree (
git merge origin/mainthroughscripts/pm/os-regen-merge.sh), exit codes captured before any pipe.pnpm --filter @objectstack/spec testpnpm --filter @objectstack/spec typecheckpnpm --filter @objectstack/spec check:generatedeslint . --no-inline-config --format jsonscripts/pm/dispatch-gates.mjsand runGate families run locally, all exit 0:
check:nul-bytes,check-spec-docblock-symbol-anchors,check:duration-unit-keys,check:cross-package-test-inputs,check:test-source-alias,check-adr-0087-registration --base origin/main,check-changeset-no-major --base origin/main,check-empty-changeset --base origin/main,check:changeset-gate-self-tests,check:pm-widening-tells,check:exported-any,check:dual-source-exports,check:entry-nameability,check:variant-docs,check:empty-state,check:llms-txt,check:browser-reachable-entries,check:skill-examples,check-doc-frontmatter,check-docs-section-name,check-doc-route-spelling --advisory,docs-audit/check-affected-docs,check:doc-anchors,check:docs-single-h1.check:skill-examplesfirst exited 1 on a build prerequisite (@objectstack/client-reacthad no.d.ts), not on this diff; afterpnpm --filter '@objectstack/client-react^...' buildit exits 0 over 258 prose examples. The remaining families the derivation names are CI's farm and are not claimed here.Reverse verification of the new pin — the fix was committed first, the mutation landed through
scripts/ablation-replace.mjs(anchor hit x1, blob64a17a8bc364tof2cacf8c304b), and the restore was proven againstHEADrather than against an exit code:enableOnInstalldefault mutatedtruetofalseTests 2 failed | 10 passed— the two that fail are the absent-key parity cell and the same-default assertion64a17a8bc364),git diff HEADemptyAcceptance notes
Observed while reading, deliberately not fixed here and not filed:
packages/spec/src/contracts/package-service.tsdeclares a FOURTHenableOnInstall, on the plain TS interfaceInstallPackageInputforIPackageService. It is outside the ruling's three schemas (not Zod, not on the authorable surface), andIPackageServicehas no implementation in this repo — the only place the key is read is an inline fake insidepackage-service.test.ts(enabled: input.enableOnInstall !== false). Noted, not filed: the interface is a contract with no consumer here, so nobody is currently misled by it. Carrier if it ever needs one: whoever implementsIPackageService.content/docs/api/metadata-api.mdxdocuments the install body inline rather than from the contract, so it will not follow a future change to it. Noted, not filed: a hand-written page drifting from a schema is not one of the three filing classes, and no PR or person is presently heading for that file. Carrier: none.Open question recorded for the seat, not answered here
The structural fold is available in exactly one shape: move the key's single literal into a module below both copies and have the request contract import it. That would give literally one Zod declaration of the key instead of a pin holding two in step — but it takes the declaration out of
package-api.zod.ts, which the ruling names as the one authority. Whether the ruling prefers one literal in a lower module or the authority's file keeping its own literal with a mechanical pin is a question about the ruling's terms, so it is recorded rather than decided by the claimant. The current shape is the one that changes nothing the ruling said.A second, separate question the re-read surfaced: the copy's own door (
ObjectStackProtocol.installPackage) still does not honour the key. Making it honour the key would be new runtime behaviour at a door the ruling did not name — it is safe (every present caller omits the key, so nothing changes today), but it is not this card's to authorise.Generated by Claude Code