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
|
Contract reviewServed-tier: 27/27 Isolated contract review of PR #19130 (card #18605), rendered by an isolated review subagent dispatched by seat Ruling of record, quoted as what it is
① Derived judgments — from the diff, per declaration
The pin (
|
| mutation | prediction | observed |
|---|---|---|
kernel .default(true) → .default(false) |
absent cell + same-default red | 2 failed / 10 passed: agrees with the authority — absent, declares the key with the same type and default |
marketplace .default(true) → .default(false) |
its own default cell red | 1 failed / 11 passed: declares enableOnInstall in its own right, defaulting to true |
kernel z.boolean() → z.union([z.boolean(), z.string()]) |
string cell red | 1 failed: agrees with the authority — 'false' — a string |
kernel z.boolean() → z.boolean().nullable() |
null cell red | 1 failed: agrees with the authority — null |
kernel .default(true) → .optional() |
absent cell + same-default red | 2 failed, the same two as row 1 |
authority .default(true) → .optional() — the #19273 preview |
— | 3 failed / 9 passed: the authority's defaults to true, the copy's absent cell, the same-default assertion |
Coverage as measured: the five cells (absent / false / true / string / null) are held pairwise between the authority and the kernel copy, and absent, string and null each go red on drift alone; the false and true cells stayed green under every mutation above because none changed how a boolean is answered (a z.literal(true)-style narrowing would light them; not run). The marketplace declaration is NOT in the five-cell matrix: the pin holds its difference (listing-keyed, refuses the door's body in both directions) and its own absent → true cell. That is the ruling's reading of it ("means something else and stays"), so five-cell parity there would assert a sameness the ruling denies — recorded as the shape, not as a defect.
Forward reference: the pre-registered flip on #19273 exists in prose only (the ruling comment, the claim, the bodies of #19273 and #19277). Neither the test file nor any doc block names #19273; the last row above is what #19273's author will hit — three reds, the absent cell named in two of them. Non-blocking under letter A ("stands as is"); recorded so #19273's dispatch can cite it.
② Semver level
.changeset/18605-enable-on-install-one-authority.md: @objectstack/spec minor, body carries Clause-②: yes, no direction arm. check-changeset-no-major.mjs --base refs/review/main-19130 --event event-19130.json (the PR payload): true exit 0 — "LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch"; check-empty-changeset.mjs exit 0 (1 declaring changeset added, none from the merge base modified); check-adr-0087-registration.mjs exit 0 (no declared-breaking changeset). minor is consistent with yes (「yes 取至少 minor」) and with a published-description change on a released package (@objectstack/spec 17.4.0). No model identifier lands anywhere in the diff (0 hits for the id shape; lit control 43 hits for enableOnInstall).
On the Clause-②: yes itself (claim 5749153758), measured rather than inherited: the diff widens NO accept set and expands NO public surface — three .describe() strings, three doc blocks, four generated pages, one test, one changeset; the widening-tells detector fires nothing under no; every api-surface shard is byte-unchanged. Mechanically, the answer to the criterion (「本卡放宽接受集或扩大公开面吗」) is no. The yes is an over-declaration on the conservative arm: it is what the ruling's execution clause directs (「re-Claim:s with Clause-②: yes inline」), it is defensible under contract-review.md's content limb (the describe strings make falsifiable contract-semantics claims, which is exactly what this review verified), and its only effect is to route the PR through this review. Auditable, recorded here as what it is, and not a reason to fail the PR.
③ Boundary flags
- Dev's open question 1 (option B, sinking the literal): closed by letter A, not taken.
- Dev's open question 2 (the in-process door): The in-process
installPackagedoor ignores theenableOnInstallits own request schema declares — honour it the way the HTTP door does (successor of #18605, ruling batch #153 item 5 letter 1) #19277, out of scope here. - Acceptance note, fourth declaration
InstallPackageInput.enableOnInstall(contracts/package-service.ts:46): confirmed present, untouched, no implementer in this repo; not a finding on this PR. - Acceptance note,
content/docs/api/metadata-api.mdxhand-written body: the docs-drift bot (5736794328) lists it among 10 pages naming a touched anchor; advisory, nothing owed by this PR. - Serial constraint vs PR spec: pre-parse __proto__ guard on ObjectSchema.fields and AssignmentConfigSchema.assignments (#17852, #18847) #19147 (
4cdba20415, 13 files):comm -12over the two sorted file lists → 0 overlap (lit control: self-overlap 9). The references pages differ as well (spec: pre-parse __proto__ guard on ObjectSchema.fields and AssignmentConfigSchema.assignments (#17852, #18847) #19147: api/metadata, data/object, system/migration). mainmoved on one of the PR's generated files after the merge-base:content/docs/references/api/protocol.mdx(kanbanrows at :1674 / :1759, from aui/viewchange), a different hunk from the PR's :1913 row. Judged in the merged-tree probe under ①: both hunks survive regeneration; no action.- Governed surfaces: none of the 9 paths is a governed-surface path;
Governed Surface Queue Guardcompleted: success.
CI on head 5234daa021 (read 2026-09-20T10:13Z, conclusion per check name)
Lint & Repo Gates success · TypeScript Type Check success · Test Core success (shards 1–6 success) · Dogfood Regression Gate success (1–3 success) · Build Core success · Temporal Conformance (live PG + MySQL) success · Governed Surface Queue Guard success · Type Check · source gates / · consumer gates / · debt ledger / · workspace success · Check Changeset success · Build Docs success · Spec property liveness success · Dogfood Verify CLI success · Check Documentation Links success · Flag docs affected by code changes success · No other open PR may claim the same issue success · No other open PR may claim the same single-writer path success · Part-of PR must not also close its card success · The card this PR closes must claim this branch success · filter success · Auto Label and Check PR Size success (plus one skipped re-run each) · Console Pin Gate skipped · Packed-tarball smoke (opt-in) skipped. 0 reviews on the PR; auto_merge null; draft; mergeable_state: clean.
Carrier check: PM_SWEEP_REPO=objectstack-ai/objectstack node scripts/pm/check-clause2-carriers.mjs --pair 19130 — true exit 0 (10:13Z; run.script.blob a76e4ce0c1ce), governing claim 5749153758 with Clause-②: yes, needs:contract-review on both carriers, the earlier os-steve claim listed RETRACTED by its own Release: (5736832755).
Readings not taken: the pre-claim exit-4 (C2) state no longer exists on the board and could not be re-measured; pnpm typecheck for packages/spec was not re-run locally (CI's TypeScript Type Check and the four Type Check · jobs are the reading); a z.literal(true)-style narrowing to light the true / false cells individually was not run.
Implemented-by: claude/issue-18605-enable-on-install-one-authority
Reviewed-by: session_01HnRAeVTLJevtQ5iCPX6JSm
VERDICT: PASS
Generated by Claude Code
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