fix(cli): mount the always-on package-registry capability at its spec-declared provider (#19387) - #19983
Conversation
…-declared provider Serve.CAPABILITY_PROVIDERS now keys package-registry at @objectstack/service-package (PackageServicePlugin), the provider the spec's PLATFORM_CAPABILITY_PROVIDERS row declares. The capability resolver also remembers the providers it mounted itself, so an app that declares marketplace (same provider) gets one PackageServicePlugin rather than a superseded duplicate. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
…ot-level proof for package-registry The vocabulary pin turns the prose that declined to assert the package-registry absence into the assertion it was waiting for: every PLATFORM_ALWAYS_ON_CAPABILITIES token keys a CAPABILITY_PROVIDERS entry or a CAPABILITY_TO_TIER tier. The identity table gains the new row, and a nightly e2e boots serve twice on one database to show an API-created package survives a restart, plus a marketplace declarer mounting one PackageServicePlugin. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
📓 Docs Drift CheckThis PR changes 2 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 33 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 5625bcd1aafda0ac75e626a41661117be66ab5ac && git checkout 5625bcd1aafda0ac75e626a41661117be66ab5ac
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2c1011b01bc071c545f72f2761647b8d9ab56375 b5b9b64c946cf3158223fa2ff2e471ff11bfa915 && git checkout -B drift-repro 2c1011b01bc071c545f72f2761647b8d9ab56375 && git merge --no-ff b5b9b64c946cf3158223fa2ff2e471ff11bfa915
node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375
|
…ackage-registry runtime half is missing A stock objectstack serve boot now mounts PackageServicePlugin for the always-on package-registry token, so the paragraph that said a stock boot still takes the in-memory-only branch was made false by the previous commits on this branch. Prose only; the in-memory branch stays the documented degraded path for hosts that mount no provider. The changeset gains the @objectstack/metadata-protocol patch line. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnPAC1UsTGfHPXVUCL6iLn
Face review (post-hoc, released)Served-tier: Scope: the changeset (and named docs) of PR #19983, released; this review cannot block anything and exists to find false released prose. Sentence verdicts(
Still true on origin/main?Yes. FindingNONE. Reviewed-by: VERDICT: CLEAN Generated by Claude Code |
Fixes #19387
Clause-②: no
package-registryis on the spec's always-on slate, soobjectstack serveappends it to every app'srequires. ButServe.CAPABILITY_PROVIDERSdid not key it, and the resolver's no-provider branch says nothing for a token the app did not declare itself. So a stock app got nopackageservice, and a package created through the API was lost on restart. This PR is the runtime half of ruling A′ (decision batch #125 item 2):protocol.installPackage/updatePackagenow find thepackageservice on a stock boot.#17676 remains open. Its item 5 (three probes agreeing across a restart) is its own acceptance and is not claimed here.
What changes
packages/cli/src/commands/serve.ts, the mount table.CAPABILITY_PROVIDERS['package-registry']now points at@objectstack/service-package/PackageServicePlugin. That is the provider the spec'sPLATFORM_CAPABILITY_PROVIDERS['package-registry']row declares. The existing drift pin (open-edition service tokens name the SAME package as serve CAPABILITY_PROVIDERS) now covers this row too.serve.ts, the capability resolver.hasPluginMatchingused to read only the app's ownplugins[]. Now it also readsresolverMounted, the provider instances this loop has already mounted, each added after itskernel.usesucceeds. Without this, the table entry alone gives an app that declaresmarketplacea second PackageServicePlugin (measured, see below).packages/cli/test/serve-capability-vocabulary.test.ts. The prose that deliberately declined to pin the absence is now the pin: everyALWAYS_ON_CAPABILITIEStoken must key aCAPABILITY_PROVIDERSentry or aCAPABILITY_TO_TIERtier. Those are the only two waysservemounts a token, andCAPABILITY_TO_TIERis the named list whose docblock says why its tokens have no provider entry. A future slate entry with no mount goes red on the PR that adds it.packages/cli/test/serve-capability-identity.test.ts. The new row is added toEXPECTED_PROVIDER_NAME, whose coverage assertion requires every table key.packages/cli/test/serve-package-registry-always-on.e2e.test.ts(nightly tier by name, integration project by behaviour). It bootsservetwice on one SQLite file, installs a package throughPOST /api/v1/packages, restarts, and reads it back. It also boots amarketplacedeclarer and asserts that exactly one PackageServicePlugin is mounted.packages/metadata-protocol/src/protocol.ts(patch round,b5b9b64c, prose only). TheinstallPackagedocblock said the runtime half of the split had not landed and that a stock boot still took the in-memory-only branch. This diff makes that false, and the docblock ships indist. It now says thatservemountsPackageServicePluginforpackage-registry, and that the in-memory-only branch is for hosts that mount no provider. One hunk, comment lines only..changeset/19387-package-registry-mount.md:@objectstack/clipatch and@objectstack/metadata-protocolpatch.Measured, before and after
Fixture: an app that declares neither
marketplacenorpackage-registry(theexamples/app-crmshape in the one way that matters). Booted withserve --devon a SQLite file.origin/main2c1011bPOST /api/v1/packages[protocol.installPackage] no 'package' service — 'com.example.survivor' registered in-memory only (will not survive a restart)GET /api/v1/packages/com.example.survivorcom.example.survivorfromsys_packagesDeclarer fixture (
requires: ['marketplace']), with three builds ofserve.ts:Plugin superseded: 'package-service'origin/mainresolverMounted)Declarer impact of this diff (acceptance 3)
Measured: unaffected. An app that declares
marketplaceboots with the same single PackageServicePlugin as before. No supersede warning, no second instance, andPOST /api/v1/packagespersists as it did. The only in-repo declarer isexamples/app-showcase/objectstack.config.ts:107(git grepoverexamples/,packages/,apps/,content/,skills/for'marketplace'in arequiresarray). This file is read-only for this PR and does not change.The declarer migration moves with the repoint, not with this diff. Today the spec's
PLATFORM_CAPABILITY_PROVIDERSmaps both tokens to@objectstack/service-package. The row's own comment says that sharing is what the carve-out INHERITED, and that repointingmarketplaceat the browse surface "moves the runtime's own resolver with it". This table follows the spec map, and the existing drift pin enforces that. So the repoint starts at that spec row (the spec seat's lane) and this mount table follows it. It is reported to the seat as aSeam:finding in the dispatch report, not done here.ADR-0087 disposition. For this diff: not applicable. Nothing an author writes changes shape, meaning or validity.
requires: ['marketplace']andrequires: ['package-registry']both stay valid and still mount the persistence plugin. For the repoint, the recommendation passed to the seat is: under 〈阶段姿态〉 (「速度优先于兼容。用的人少:退役立即生效,无过渡窗口、无别名双拼」) the meaning change ofmarketplacetakes effect at once, with no alias window. Amarketplacedeclaration made for the store becomes redundant, because the store is always on. The case the carrier must spell out is an out-of-repo declarer whoserequires: ['marketplace']would start mounting the browse plugins. That is a runtime-meaning change of arequirestoken, not a stored or authored shape, so the recommendation is that no ADR-0087 conversion entry is owed. The carrier decides.Why no runtime warning in the no-provider branch
That branch is reached only when a token has no
CAPABILITY_PROVIDERSentry. Once the pin above holds, an always-on token cannot reach it: every slate token has a mount, and@objectstack/specand@objectstack/clirelease in onefixedchangeset group (.changeset/config.json). A published CLI therefore never runs against a slate its pin did not see. A warning there would be unreachable code. A comment at the branch records this.Tests (at 6496714)
pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2: 222 files. On first run, 220 passed and 2 were refused withpackages/cli is not built(published-subpath-console.pin,published-subpath-hook-body.pin), a build prerequisite. Afterpnpm --filter @objectstack/cli buildthose 2 files pass, 29 tests. Totals: 3116 passed, 29 skipped.pnpm --filter @objectstack/cli run typecheck: exit 0. Thecheck:test-typecheckdebt holds at 3 files, 28 errors.tsc -p tsconfig.test.json --listFilesOnlyincludes all 3 touched test files.OS_TEST_TIERS=nightly … vitest run --project integration test/serve-package-registry-always-on.e2e.test.ts: 5/5 passed.*.e2e.test.tsinpackages/cli/testthat bootsserve(17 files) passed with 75/75 tests. A boot-composition change reaches them, and they run only nightly.Reverse checks (each mutation went through
scripts/ablation-replace.mjs; the tool confirmed the anchor moved from 1 to 0 and the blob changed, then restored the file and checked it matched HEAD withgit diff HEADempty):'package-registry'table entry, then run the vocabulary and identity pins: red, as expected. The new pin reportsexpected [ 'package-registry' ] to deeply equal [], and the identity table's coverage assertion goes red too.404). The declarer block stays green.resolverMounted.push(provider);, then run the e2e: 1 red (Plugin superseded: 'package-service'on the declarer boot). The stock block stays green.No dist leg was needed. The unit pins import
../src/commands/serve.jsdirectly, and the e2e spawnsbin/run-dev.jsthrough tsx, so both readsrc/.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat 6496714 derived 64 commands. All were run with the exit code captured before any pipe, and--ranreconciled them:64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. Three first answered exit 3 (PREREQUISITE NOT MET) and passed once the prerequisite was supplied:check-plugin-teardown-shape --self-testneeded the pinned fixture commit621a4876, which a shallow clone lacks. It passed aftergit fetch --depth=1 origin 621a4876….check:dual-build-cjs-loadsandcheck:i18n-coverageneeded build output. They passed afterturbo run build --filter=!@objectstack/docs.pnpm lint(the fulleslint . --no-inline-configrun): exit 0 at 6496714.Acceptance notes
hasPluginMatchingstops a double mount was false (measured). That check reads only the app's ownplugins[], never what the resolver has mounted. TheresolverMountedlist is the in-scope repair.packages/spec/src/kernel/platform-capabilities.ts: the SCOPE note on thepackage-registryslate entry, and the comment on its provider row. Both say the entry is inert underserve.: corrected in this PR (patch round,packages/metadata-protocol/src/protocol.ts: theinstallPackagedocblockb5b9b64c), because it ships indist. The spec-side note above is carried on A writable package created viaPOST /api/v1/packagesis registered in memory only — it does not survive a restart, and leaves its published metadata orphaned #17676 with themarketplacerepoint seam.os migrate planreports, for information only, tables with a platform prefix that no object declares. By readingpackages/cli/src/utils/unmanaged-tables.ts,sys_packages(raw DDL) should now appear there for any stock database thatservehas booted, as it already would formarketplacedeclarers. NOT MEASURED.memory://) a stock boot now logs four more WARN lines (measured): threeRaw execution not supported in InMemory driverfor thesys_packagesDDL and query, andPackage hydration from sys_packages SKIPPED.marketplacedeclarers already got these.Generated by Claude Code