fix(metadata-protocol): the in-process install door honours enableOnInstall - #19338
huangyiirene wants to merge 3 commits into
Conversation
…Install` `MetadataProtocol.installPackage` read `manifest` + `settings` only, so a caller that asked for `enableOnInstall: false` got an ENABLED install with no refusal and no warning — declared but not enforced on a published option. It now applies the same rule the HTTP door does (ruling batch #157 item 5 letter C): `true` enables, `false` disables, an ABSENT key makes no lifecycle call at all. Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…ableOnInstall` Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE Co-authored-by: Claude <noreply@anthropic.com>
…-process-install-honours-flag
📓 Docs Drift CheckThis PR changes 1 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 11 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 beb47cb30bc67b17ef9d965779ef51b26ab82e04 && git checkout beb47cb30bc67b17ef9d965779ef51b26ab82e04
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 74fb2f7a808371650b8d91e64d72156bec09aac4 2c8e2667c997d156b6a75272a2200bcb0c4d2855 && git checkout -B drift-repro 74fb2f7a808371650b8d91e64d72156bec09aac4 && git merge --no-ff 2c8e2667c997d156b6a75272a2200bcb0c4d2855
node scripts/docs-audit/affected-docs.mjs --json 74fb2f7a808371650b8d91e64d72156bec09aac4
|
Fixes #19277
Clause-②: no
InstallPackageRequestSchema.enableOnInstall(packages/spec/src/kernel/package-registry.zod.ts) is the request contract of the in-processObjectStackProtocol.installPackage/MetadataProtocol.installPackageprimitive. The implementation readrequest.manifestandrequest.settingsand nothing else, so a caller that asked forenableOnInstall: falsegot an ENABLED install — no refusal, no warning, no effect. Declared but not enforced on a published option, which ADR-0049 (enforce-or-remove) and Prime Directive #10 refuse outright.Ruling batch #153 item 5 letter 1 (#18605, record
5724940709) kept this declaration as a COPY of the HTTP request key with the SAME meaning, so the disposition is enforce, not retire.What changed
MetadataProtocol.installPackagenow applies the same rule the HTTP door applies, through the same registry verbsPATCH /packages/:id/enableandPATCH /packages/:id/disableuse:enableOnInstalltrueenablePackage— clears a disable, including a boot-seeded onefalsedisablePackage— the row and itsstatusboth move=== true/=== false, never a truthiness test and never a??default — the three states are the contract. A non-boolean value is read as absent rather than coerced.The card (written 2026-09-20T09:06Z) asks for 「the registry row's
enabled(andstatus) followenableOnInstall ?? trueon install and on re-install」. PR #19291 (4fef271b7, 2026-09-20T11:10Z) re-ruled exactly those cells under maintainer ruling batch #157 item 5 letter C (「缺省 = 保持,有旗 = 设置」), which is younger than this card's own ruling.?? trueon re-install is precisely what the HTTP door stopped doing.The direction 「honour it the way the HTTP door does」 is self-updating and still governs, so the matrix below was read off
packages/runtime/src/domains/packages-install-enable-on-install.test.tsonorigin/main, not off the card's prose. The four cells checked, and they match the dispatch's table exactly::154:233[#18877 re-ruled]re-install, flag ABSENT:262enableOnInstall: true:278[#18877 re-ruled]BARE re-install⛔ One HTTP-door cell has no analogue at this seam: the BARE body form (a manifest posted as the whole body) does not exist in-process —
InstallPackageRequestalways carriesmanifestas a field. What is pinned instead is the third state's boundary: a non-boolean value is read as ABSENT.⛔ What this seam does NOT write
The runtime's durable disabled-package file is keyed by environment (
setPackageDisabled(environmentId, id, disabled),packages/runtime/src/package-state-store.ts), and anInstallPackageRequestcarries no environment — so that key cannot even be formed here. The module also lives in@objectstack/runtime, which depends on@objectstack/metadata-protocoland not the other way round. The HTTP door owns that half and writes it from the row it returned.So
enableOnInstallthrough the in-process primitive moves the registry row — what every in-process reader serves from — for the life of the process. This is exactly the scope the card's acceptance names (「registry row + status」). It is stated in the code, in the changeset and here rather than left to be rediscovered; see acceptance notes for the follow-up it earns.No behaviour change for any caller on the tree
The card's own measurement, re-verified rather than inherited. Radius:
packages/**,examples/**,apps/**in this repo, at2c8e2667c.packages/runtime/src/domains/packages.ts:769—protocolSvc.installPackage({ manifest, settings: body.settings }). The key is deliberately not forwarded; the door performs the flip itself.packages/metadata-protocol/src/protocol.ts(duplicatePackage) —this.installPackage({ manifest: dupManifest }). Flag absent.Those are the only two call sites. ⇒ confirmed: no existing caller sets the key, so this is observable only to a caller that sets it — one that until now got silence.
Verification
Gates — ⛔ not a list taken on trust: derived from the actual changed files with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, each exit code landed to a file before any pipe, then reconciled:All 61 exit 0, measured at
2c8e2667c. Three of them (check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt) first answered exit 3 = PREREQUISITE NOT MET; that was cleared with a full workspace build and they were re-run, ⛔ never read as a pass.Tests
pnpm --filter @objectstack/metadata-protocol testpnpm --filter @objectstack/objectql testpnpm --filter @objectstack/{metadata-protocol,objectql} typecheckpnpm --filter @objectstack/runtime exec vitest run src/domains/packagespnpm lint(repo-wideeslint . --no-inline-config)Ablation — the new pin is proven able to fail.
packages/objectqlresolves@objectstack/metadata-protocolthrough itsexports, i.e.dist/, with no vitest alias (it is aKNOWN_UNALIASED_TEST_IMPORTSentry), so the mutation was rebuilt and proven present in the artifact before the run's colour was read:The 5 cases that stay green under the ablation are the control legs — fresh-absent, fresh-true, the non-boolean cell, seeded-absent and the unseeded control — none of which depends on a flag arm. Nothing of the ablation is left in the tree; the mutation script carried a
traponEXIT INT TERMwith absolute paths.Acceptance notes
1. ⭐ A published description is falsified by this PR, and it is fenced out of this card.
packages/spec/src/kernel/package-registry.zod.ts:325ships this.describe()text, which reaches the published reference page (content/docs/references/kernel/package-registry.mdx:187andcontent/docs/references/api/protocol.mdx:1913):The doc block above it says the same at length (「This contract's own implementation does not read the key」), and
packages/spec/src/api/package-api.zod.ts:305carries a second copy. As of this PR all three are false. They were written by #19130, which merged at 11:10Z — two hours after this card was filed — so the card's author could not have fenced around them.⛔ Not fixed here: the card and the dispatch both fence
packages/specout (「the declaration half belongs to #19273」), and editing a.describe()pulls in the whole spec generated-artifact family (gen:schema,gen:docs,check:generated) plus a second package's changeset — a new verification surface, so the bounded-in-place-fix exemption does not hold. It belongs to #19273, whose open question is already 「once the runtime honours 「缺省 = 保持」, what should the publishedenableOnInstalldeclaration say?」. Recorded here and in the report so it is not rediscovered as drift. No gate goes red on it:check:docscompares the generated page against the describe, and both still agree with each other.2. The durable half of the in-process door, noted not filed. A caller that sets
enableOnInstall: falsein-process now gets a disable that is real in the registry and absent from the runtime's disable file, so a restart re-enables it. That is narrower than the pre-PR gap (where the key did nothing at all) but newly reachable, and it cannot be closed at this seam: the record is keyed by an environment the request does not carry. Closing it means either givingInstallPackageRequestan environment or giving the caller the durable verb — a contract decision, not an implementation one. Who would meet this: only a caller that sets the key, of which there are none on the tree today.3.
.changeset/18605-enable-on-install-one-authority.md(unreleased) states 「Its published description now records that this layer does not read it」. If it and this PR's changeset ship in the same release, one release's notes will say both. Belongs with finding 1, in #19273.Nothing else was touched: this diff is
packages/metadata-protocol/src/protocol.ts, one new test file underpackages/objectql/src/, and the changeset.Generated by Claude Code