Skip to content

test(metadata-protocol): makeImpl gains the service-ABSENT composition (#17676 ruling A' items 2/3) - #19385

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-17676-api-package-survives-restart
Sep 20, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-17676-api-package-survives-restart

Conversation

@huangyiirene

@huangyiirene huangyiirene commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Part of #17676 — ruling A′ items 2 and 3.

⛔ Deliberately not a closing reference: ruling item 5's acceptance (three probes agreeing across a restart) is not met by this PR, so the card must stay open. What is left, and why it is not in this diff, is stated under "The fork" below.

Clause-②: no

What the ruling asked for

Decision batch #125 item 2, letter A′ (maintainer 「同意」 2026-09-13), five items across three lanes. Item 1 was the packages/spec carve-out and landed via carrier #18053. Item 4 is the spec seat's ADR read. This PR is the engine lane's half:

2. protocol.installPackage and protocol.updatePackage therefore always find the package service on a stock boot; their in-memory-only branches stay as the documented degraded path for reduced hosts.
3. durable-package.test.ts's makeImpl() gains the service-ABSENT composition so the pin can express the bug this card found.
5. The three probes (Studio writable list, GET /api/v1/data/NS_OBJ, GET /api/v1/meta/object/NS_OBJ/published) agree after a restart — that is the acceptance.

Item 3 — the service-ABSENT composition

makeImpl() hard-wired services = new Map([['package', …]]), so it could build only the service-PRESENT host and no pin in this file could fail the way the card failed. It now composes either host:

  • packageService: false builds a services registry with no package entry at all — what a host that mounts no PackageServicePlugin hands the protocol.
  • hydrate: [...] replays durable manifests through the registry's own installPackage verb, which is literally what PackageServicePlugin.start() does when it reconciles sys_packages back into the registry. A host composed with the rows a previous host persisted is that process after a restart.
  • The fake registry now mirrors the real SchemaRegistry package API by name (installPackage / getPackage / getAllPackages / updatePackageManifest), so a pin written against it is a pin about the primitive's real collaborator rather than an invented one.

Four new pins on top of it: the service-absent installPackage and updatePackage degraded paths (registry write lands, nothing durable, the degradation is loud), the three-way split after a restart, and a service-present control where the same sequence agrees instead — the control is what keeps the split pin from being vacuously green.

Item 2 — the branches stay; the note describing them did not survive item 1

No behaviour change: the in-memory-only branches are exactly as they were. ⭐ Item 2 does not say to delete them, and a change that removed them would have over-reached.

What did change is documentation that item 1 made false. installPackage's docblock read "when the package service is absent (e.g. the marketplace capability is off)". After the carve-out that names the wrong capability: persistence is package-registry, an always-on core token, and marketplace now names only the optional catalogue half. The note now says so, says why the branch is kept, and records the measured state of the split. updatePackage points at the same note, because the ruling names both primitives.

⚠️ The fork — item 2's factual half is FALSE on today's tree

Ruling item 2 says these primitives "therefore always find the package service on a stock boot". Measured on origin/main at c334ba0f3a, they do not, and item 1 landing did not change that:

  • PLATFORM_ALWAYS_ON_CAPABILITIES contains package-registry, and serve force-appends every slate token to each app's requires (serve.ts, the ALWAYS_CAPS loop).
  • The capability resolver then mounts a token only when Serve.CAPABILITY_PROVIDERS keys it. That registry keys marketplace and does not key package-registry.
  • A token with no provider entry hits if (!spec) … continue, and because package-registry is in PLATFORM_CAPABILITY_TOKENS the resolver does not even warn. So the always-on entry is silently inert.
  • ⇒ on a stock objectstack dev boot of an app that does not itself declare requires: ['marketplace'] (examples/app-crm declares ['ui', 'automation']), POST /api/v1/packages still reaches protocol.installPackage with no package service and still registers the package in memory only. The defect this card reports is live.

This is not a new reading. Both spec-lane artifacts say the same thing in their own words and both name the engine lane as the owner:

⚠️ SCOPE, measured on serve's capability resolver at c17ff70: a slate entry is force-appended to every app's requires, and the CLI then mounts it only if Serve.CAPABILITY_PROVIDERS keys the token. That registry keys marketplace, not this token, so under objectstack serve this entry is inert until the runtime half of the same ruling lands (#17676 items 2/3/5, the engine lane).

— packages/spec/src/kernel/platform-capabilities.ts, and the same note in packages/cli/test/serve-capability-vocabulary.test.ts, which deliberately declines to pin the absence so the engine lane's own change is not turned red for doing the ruled thing.

That remaining half is not in this diff, by scope and by shape. The dispatch fences this card at packages/metadata-protocol/src/; the resolver lives in packages/cli/src/commands/serve.ts. And it is not a one-line map entry: the spec's own provider row records that marketplace and package-registry point at the same package today, that this is what the carve-out inherited rather than decided, and that "repointing marketplace at the browse surface moves the runtime's own resolver with it". Which of those two shapes to take is a decision, not a mechanical edit, and it changes the boot composition of every app — a verification surface well outside this one.

What is left — ⛔ item 5 is NOT met

Item 5's acceptance is three HTTP probes agreeing across a restart of a booted composition. This is a unit tier over the protocol primitive, and a unit tier cannot restart a server. What the new pins do instead is pin the one fact the three probes disagree about — whether the package crosses the boundary at all — at the seam that decides it, with the registry reads standing for the two registry-backed probes and the metadata read for the third. The test file says this in a scope banner so a green run here is not over-read. ⛔ Item 5 stays open, and it cannot be met at all until the resolver half above lands.

Verification

Run under scripts/pm/os-verify-lock.sh; exit codes captured before any pipe.

what command result
package suite pnpm --filter @objectstack/metadata-protocol test exit 0 — 182 files passed / 3 skipped, 2600 tests passed, 19 skipped (at adcd4a161)
package typecheck pnpm --filter @objectstack/metadata-protocol typecheck exit 0 (tsc --noEmit; it does compile this test file — an earlier run of it caught a TS2353 in the new fixture type)
dependency closure pnpm --filter '@objectstack/metadata-protocol^...' build exit 0
derived gate families node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands 53 derived, 53 run, 50 exit 0
gate reconciliation same tool with --ran (each line command :: exit N) exit 0 — 53 accounted, 0 UNRUN
repo-wide lint ESLint API over . with allowInlineConfig: false exit 0 — 6934 files, 0 errors, 0 warnings (the union, not a narrowed subset, at adcd4a161)

Three gates first exited 3 — PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:lean-entry-closure, check:type-check-debt): each reads built output and the worktree had none. The prerequisite was cleared with pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' (72/72 tasks, exit 0) and all three then exited 0. ⛔ An exit 3 is recorded as NOT MEASURED, never as a pass.

Ablation — the new pin can fail

Driven through scripts/ablation-replace.mjs, so the mutation is proved on disk rather than assumed from an exit code. The test imports ./index.js — a same-package relative specifier that vitest resolves to src/, not dist/ (an earlier failing run named src/protocol.ts:6:1 in its stack), so no rebuild sits between the mutation and the run.

  • mutation: the console.warn in installPackage's no-service branch replaced with a no-op — anchor x1 → x0, blob 653d2155fb74 → 7072a8718166, both verified by re-reading the file.
  • result: 1 failed | 10 passed (11) — the service-absent pin went red on the product change, so it reads the primitive and not the fake.
  • restore: blob back to 653d2155fb74, identical to HEAD, and git diff HEAD empty.

No ablation artifact remains in the tree.

Changeset

Clause-②: no, and a patch changeset for @objectstack/metadata-protocol — .changeset/17676-package-registry-docblock-capability-name.md.

⚠️ This PR first proposed skip-changeset; the seat overruled it, on the measurement this branch itself took. That measurement is recorded here because it is what decides the question rather than a preference. AGENTS.md Post-Task Checklist §3 scopes the label to "a diff that publishes nothing from any released package". Measured: @objectstack/metadata-protocol is not private, its files[] ships dist, and the corrected docblock reaches dist/index.js, dist/index.cjs, dist/index.d.ts and dist/index.d.cts — subject count 1 each, with a positive control present in the two JS outputs — because tsup keeps JSDoc. ⇒ the label's precondition is false by measurement.

And it is a fix rather than a cosmetic. The old sentence told a consumer reading dist/index.d.ts in their editor that the absent-service branch is what you get "e.g. the marketplace capability is off", which stopped being true the moment ruling A′ item 1 landed. A false TSDoc in shipped content is a defect on its own terms — and under skip-changeset this correction would sit on main unshipped, the false sentence still going out, until some unrelated change happened to bump the package.

⛔ The changeset claims no behaviour change and says so in its own body: the in-memory branches are byte-identical, and this card's defect is not repaired here.

⛔ No label write was made in either round. The seat holds the labels endpoint, and with a changeset present there is no longer a label to apply.

Acceptance notes

Noted in passing, ⛔ not filed and ⛔ not addressed here:

  • The three degradation sites in these two primitives log at warn. AGENTS.md's degradation-log-level rule grades a durability degradation at error ("a write that claims to persist does not… nothing looks broken"), which is exactly this seam's shape — the door answered 201. The same section also says an if (!service) composition branch "is usually functional and belongs at warn", so the else arm is genuinely arguable; the catch arm around the publish call is the one the rule says it bites. pnpm check:durability-log-level does not see either, because pkgSvc.publish is not in DURABILITY_CRITICAL_CALLEES. Left alone: changing a log level is not in this ruling, and the ruling's own remedy is to stop hosts taking the branch at all.
  • dispatch-gates.mjs reported STALE TREE on both derivations (two of its own input scripts moved on origin/main while this branch worked). Discharged empirically rather than by merging: the command list was derived twice, before and after a fetch, and the two are byte-identical.
  • Comment 5651690676 — the "may item 3 be delivered independently of item 1" question — returns 404 and was destroyed with the suspended os-musk identity. It could not be read, so it is recorded NOT MEASURED. This PR proceeds on the PM's reading that item 1 having landed resolves it by circumstance.

Generated by Claude Code


Generated by Claude Code

#17676)

Ruling A' item 3 (decision batch #125 item 2, maintainer approved 2026-09-13):
`durable-package.test.ts`'s `makeImpl()` could only build the service-PRESENT
host, so no pin in it could fail the way #17676 failed. It now composes either
host, mirrors the real `SchemaRegistry` package API by name, and can replay a
durable `sys_packages` row the way `PackageServicePlugin.start()` does — which
is what lets a pin cross a restart boundary at the unit tier.

Four new pins: the service-absent `installPackage` / `updatePackage` degraded
path (in-memory write lands, nothing durable, the degradation is loud), the
three-way split after a restart, and the service-present control where the
same sequence agrees instead.

Item 2 is docs-only here: the in-memory branches STAY. `installPackage`'s note
no longer names `marketplace` as the owning capability (item 1 carved the
persistence half out as always-on `package-registry`) and records the measured
gap — `Serve.CAPABILITY_PROVIDERS` does not key the new token, so the branch is
still the one a stock boot takes.

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
…s a real manifest

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

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

Which tree this was computed on

This run read content/docs from 61e708a6b0677002d0f7dc5e70f28f704f36a802 — the merge of head e12c577eafa716d81d964ee5922ba4f5d715ab52 into base a88a97332fc90cb04e9dbae2d4e2f5aa56cab708, 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 61e708a6b0677002d0f7dc5e70f28f704f36a802 && git checkout 61e708a6b0677002d0f7dc5e70f28f704f36a802
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a88a97332fc90cb04e9dbae2d4e2f5aa56cab708 e12c577eafa716d81d964ee5922ba4f5d715ab52 && git checkout -B drift-repro a88a97332fc90cb04e9dbae2d4e2f5aa56cab708 && git merge --no-ff e12c577eafa716d81d964ee5922ba4f5d715ab52

node scripts/docs-audit/affected-docs.mjs --json a88a97332fc90cb04e9dbae2d4e2f5aa56cab708

⚠️ 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 a88a97332fc90cb04e9dbae2d4e2f5aa56cab708 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…ted docblock (#17676)

The seat overruled the `skip-changeset` judgement on the measurement this
branch itself took: `@objectstack/metadata-protocol` is not private, `files[]`
ships `dist`, and the corrected JSDoc reaches all four dist entries because
tsup keeps it. AGENTS.md Post-Task Checklist §3 scopes that label to a diff
that publishes nothing from any released package, so its precondition is false
here — and a false TSDoc in shipped content is a defect, which makes this a
patch rather than a cosmetic.

No behaviour moves; the changeset says so explicitly so it cannot be read as
the card's defect being repaired.

Claude-Session: https://claude.ai/code/session_01NcPSwnmJHczmTu6FG7NMjE
Co-authored-by: Claude <noreply@anthropic.com>
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

Development

Successfully merging this pull request may close these issues.

2 participants