Skip to content

fix(cli): mount the always-on package-registry capability at its spec-declared provider (#19387) - #19983

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-19387-package-registry-mount
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-19387-package-registry-mount

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #19387

Clause-②: no

package-registry is on the spec's always-on slate, so objectstack serve appends it to every app's requires. But Serve.CAPABILITY_PROVIDERS did 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 no package service, 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 / updatePackage now find the package service 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's PLATFORM_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. hasPluginMatching used to read only the app's own plugins[]. Now it also reads resolverMounted, the provider instances this loop has already mounted, each added after its kernel.use succeeds. Without this, the table entry alone gives an app that declares marketplace a 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: every ALWAYS_ON_CAPABILITIES token must key a CAPABILITY_PROVIDERS entry or a CAPABILITY_TO_TIER tier. Those are the only two ways serve mounts a token, and CAPABILITY_TO_TIER is 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 to EXPECTED_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 boots serve twice on one SQLite file, installs a package through POST /api/v1/packages, restarts, and reads it back. It also boots a marketplace declarer and asserts that exactly one PackageServicePlugin is mounted.
  • packages/metadata-protocol/src/protocol.ts (patch round, b5b9b64c, prose only). The installPackage docblock 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 in dist. It now says that serve mounts PackageServicePlugin for package-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/cli patch and @objectstack/metadata-protocol patch.

Measured, before and after

Fixture: an app that declares neither marketplace nor package-registry (the examples/app-crm shape in the one way that matters). Booted with serve --dev on a SQLite file.

probe origin/main 2c1011b this branch
POST /api/v1/packages 201, plus [protocol.installPackage] no 'package' service — 'com.example.survivor' registered in-memory only (will not survive a restart) 201, no fallback line
restart on the same DB, then GET /api/v1/packages/com.example.survivor 404 200, and the boot logs the rehydration of com.example.survivor from sys_packages
banner plugin row no PackageServicePlugin PackageServicePlugin ×1

Declarer fixture (requires: ['marketplace']), with three builds of serve.ts:

build Plugin superseded: 'package-service' PackageServicePlugin in the banner row
origin/main absent ×1
table entry only (no resolverMounted) present (WARN) ×2
this branch absent ×1

Declarer impact of this diff (acceptance 3)

Measured: unaffected. An app that declares marketplace boots with the same single PackageServicePlugin as before. No supersede warning, no second instance, and POST /api/v1/packages persists as it did. The only in-repo declarer is examples/app-showcase/objectstack.config.ts:107 (git grep over examples/, packages/, apps/, content/, skills/ for 'marketplace' in a requires array). 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_PROVIDERS maps both tokens to @objectstack/service-package. The row's own comment says that sharing is what the carve-out INHERITED, and that repointing marketplace at 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 a Seam: 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'] and requires: ['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 of marketplace takes effect at once, with no alias window. A marketplace declaration 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 whose requires: ['marketplace'] would start mounting the browse plugins. That is a runtime-meaning change of a requires token, 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_PROVIDERS entry. Once the pin above holds, an always-on token cannot reach it: every slate token has a mount, and @objectstack/spec and @objectstack/cli release in one fixed changeset 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 with packages/cli is not built (published-subpath-console.pin, published-subpath-hook-body.pin), a build prerequisite. After pnpm --filter @objectstack/cli build those 2 files pass, 29 tests. Totals: 3116 passed, 29 skipped.
  • pnpm --filter @objectstack/cli run typecheck: exit 0. The check:test-typecheck debt holds at 3 files, 28 errors. tsc -p tsconfig.test.json --listFilesOnly includes all 3 touched test files.
  • The new e2e, OS_TEST_TIERS=nightly … vitest run --project integration test/serve-package-registry-always-on.e2e.test.ts: 5/5 passed.
  • Neighbouring nightly serve boots: every other *.e2e.test.ts in packages/cli/test that boots serve (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 with git diff HEAD empty):

  1. Delete the 'package-registry' table entry, then run the vocabulary and identity pins: red, as expected. The new pin reports expected [ 'package-registry' ] to deeply equal [], and the identity table's coverage assertion goes red too.
  2. Same deletion, then run the e2e: 3 red (banner row, in-memory fallback line, post-restart 404). The declarer block stays green.
  3. Delete 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.js directly, and the e2e spawns bin/run-dev.js through tsx, so both read src/.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 6496714 derived 64 commands. All were run with the exit code captured before any pipe, and --ran reconciled 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-test needed the pinned fixture commit 621a4876, which a shallow clone lacks. It passed after git fetch --depth=1 origin 621a4876….
  • check:dual-build-cjs-loads and check:i18n-coverage needed build output. They passed after turbo run build --filter=!@objectstack/docs.

pnpm lint (the full eslint . --no-inline-config run): exit 0 at 6496714.

Acceptance notes

  • The dispatch premise that hasPluginMatching stops a double mount was false (measured). That check reads only the app's own plugins[], never what the resolver has mounted. The resolverMounted list is the in-scope repair.
  • Prose that goes stale with this diff, left for the files' next editors (those files are outside this PR's surface):
  • os migrate plan reports, for information only, tables with a platform prefix that no object declares. By reading packages/cli/src/utils/unmanaged-tables.ts, sys_packages (raw DDL) should now appear there for any stock database that serve has booted, as it already would for marketplace declarers. NOT MEASURED.
  • On the in-memory driver (memory://) a stock boot now logs four more WARN lines (measured): three Raw execution not supported in InMemory driver for the sys_packages DDL and query, and Package hydration from sys_packages SKIPPED. marketplace declarers already got these.

Generated by Claude Code

…-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
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Sep 24, 2026
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/metadata-protocol, touching 3 documentable anchor(s).

20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375.

⛔ 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): node scripts/docs-audit/affected-docs.mjs --json 2c1011b01bc071c545f72f2761647b8d9ab56375 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5625bcd1aafda0ac75e626a41661117be66ab5ac — the merge of head b5b9b64c946cf3158223fa2ff2e471ff11bfa915 into base 2c1011b01bc071c545f72f2761647b8d9ab56375, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2c1011b01bc071c545f72f2761647b8d9ab56375 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Face review (post-hoc, released)

Served-tier: CONTRACT_REVIEW_TIER
Merge-sha: cdc1ae03dd277edac2742a6dfe4eb2bf61afefc5
Local-runs: none

Scope: the changeset (and named docs) of PR #19983, released; this review cannot block anything and exists to find false released prose.

Sentence verdicts

(.changeset/19387-package-registry-mount.md at the merge; no docs page in scope)

  1. Title: "objectstack serve mounts the always-on package-registry capability, so a package created through the API survives a restart on a stock boot." TRUE. packages/cli/src/commands/serve.ts:1973-1977 (new 'package-registry' row at @objectstack/service-package / PackageServicePlugin); pinned by packages/cli/test/serve-package-registry-always-on.e2e.test.ts:234-296 (stock app: plugin mounted, no in-memory fallback, package still there after a restart on the same database).
  2. "package-registry has been on the always-on slate (PLATFORM_ALWAYS_ON_CAPABILITIES) since the marketplace / package-registry split, and serve appended it to every app's requires." TRUE. packages/spec/src/kernel/platform-capabilities.ts:312,355; serve.ts:1244 (derived slate), :2815-2820 (the append loop).
  3. "But Serve.CAPABILITY_PROVIDERS did not key it, and the resolver's no-provider branch says nothing about a token the app did not declare itself." TRUE. The pre-change table had no such row; the warn is gated on declaredRequires.has(cap) && !PLATFORM_CAPABILITY_TOKENS.includes(cap) (serve.ts:~4618), which a force-appended vocabulary token fails on both conjuncts.
  4. "So an app that did not declare requires: ['marketplace'] got no package service. POST /api/v1/packages answered 201, printed no 'package' service ... registered in-memory only (will not survive a restart), and GET /api/v1/packages/:id answered 404 after a restart." TRUE. Warn string at packages/metadata-protocol/src/protocol.ts:22837; 201 / warn / post-restart 404 measured in the dev report on package-registry is on the always-on slate but Serve.CAPABILITY_PROVIDERS does not key it — the always-on mount is silently inert (#17676 ruling A' runtime half) #19387 (comment 5815997116) and pinned by the e2e file's three stock-app assertions.
  5. "package-registry now mounts PackageServicePlugin from @objectstack/service-package, the provider the spec's PLATFORM_CAPABILITY_PROVIDERS row declares for it." TRUE. platform-capabilities.ts:179.
  6. "A stock boot creates sys_packages and replays it at start, so installs and manifest edits made through the API persist." TRUE. packages/services/service-package/src/index.ts:366-370 (CREATE TABLE at init), :572-622 (hydration at start).
  7. "Apps that declare marketplace boot as before, with one PackageServicePlugin. marketplace resolves to the same provider. The capability resolver now remembers the providers it has mounted itself, so the always-on token does not mount a second copy." TRUE. serve.ts:~4592-4598 (resolverMounted, consulted by hasPluginMatching), :~4689,:~4701 (pushed after a successful kernel.use); e2e :299-316 (declarer: one instance shared by both tokens).
  8. "Without that change, a declarer's boot would print Plugin superseded: 'package-service'." TRUE. Plugin name packages/services/service-package/src/index.ts:350; warn verb packages/core/src/plugin-registration.ts:71; measured twice in the dev report's table-only ablation.
  9. "A stock database gains one table, sys_packages. PackageServicePlugin creates it with raw DDL, as it already did for marketplace declarers." TRUE. index.ts:650-670.
  10. "On the in-memory driver (memory://), which has no raw SQL, the boot now logs that the DDL was not run and that package hydration was skipped. Packages there last only as long as the process, as before." TRUE. The driver logs Raw execution not supported in InMemory driver for each statement (index.ts:227-228 records the seam), and the plugin warns Package hydration from sys_packages SKIPPED ... (index.ts:637-640); dev report measured +4 WARN lines on memory://.
  11. "--preset minimal still opts out of the whole slate. protocol.installPackage keeps its in-memory-only branch as the documented degraded path for hosts that mount no provider." TRUE. serve.ts:2816 (if (presetName !== 'minimal')); protocol.ts:22837 branch kept.
  12. "@objectstack/metadata-protocol: the installPackage docblock no longer says the runtime half is missing. ... It now says that objectstack serve mounts PackageServicePlugin for package-registry ... The docblock ships in dist. No behaviour changes." TRUE. protocol.ts:22643-22660 (comment-only hunk); packages/metadata-protocol/tsup.config.ts sets dts on, so the method's JSDoc ships in the declaration output (the JS output strips comments; the sentence holds for dist/index.d.ts).

Still true on origin/main?

Yes. serve.ts:1973 (row), :4605-4721 (resolverMounted), :2811 (minimal opt-out) unchanged; platform-capabilities.ts:206 still declares @objectstack/service-package for the token; the docblock at protocol.ts:23959-23972 still reads "the runtime half of that split has landed (#19387)". packages/cli/CHANGELOG.md:2135 and packages/metadata-protocol/CHANGELOG.md:1001 carry the reviewed text verbatim under cdc1ae0.

Finding

NONE.

Reviewed-by: session_01VvcEokUG1tvVxkceYfR5XB

VERDICT: CLEAN


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

1 participant