test(metadata-protocol): makeImpl gains the service-ABSENT composition (#17676 ruling A' items 2/3) - #19385
Conversation
#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>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 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 — 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 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
|
…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>
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/speccarve-out and landed via carrier #18053. Item 4 is the spec seat's ADR read. This PR is the engine lane's half:Item 3 — the service-ABSENT composition
makeImpl()hard-wiredservices = 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: falsebuilds a services registry with nopackageentry at all — what a host that mounts noPackageServicePluginhands the protocol.hydrate: [...]replays durable manifests through the registry's owninstallPackageverb, which is literally whatPackageServicePlugin.start()does when it reconcilessys_packagesback into the registry. A host composed with the rows a previous host persisted is that process after a restart.registrynow mirrors the realSchemaRegistrypackage 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
installPackageandupdatePackagedegraded 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 thepackageservice is absent (e.g. themarketplacecapability is off)". After the carve-out that names the wrong capability: persistence ispackage-registry, an always-on core token, andmarketplacenow 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.updatePackagepoints at the same note, because the ruling names both primitives.Ruling item 2 says these primitives "therefore always find the
packageservice on a stock boot". Measured onorigin/mainatc334ba0f3a, they do not, and item 1 landing did not change that:PLATFORM_ALWAYS_ON_CAPABILITIEScontainspackage-registry, andserveforce-appends every slate token to each app'srequires(serve.ts, theALWAYS_CAPSloop).Serve.CAPABILITY_PROVIDERSkeys it. That registry keysmarketplaceand does not keypackage-registry.if (!spec) … continue, and becausepackage-registryis inPLATFORM_CAPABILITY_TOKENSthe resolver does not even warn. So the always-on entry is silently inert.objectstack devboot of an app that does not itself declarerequires: ['marketplace'](examples/app-crmdeclares['ui', 'automation']),POST /api/v1/packagesstill reachesprotocol.installPackagewith nopackageservice 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:
—
packages/spec/src/kernel/platform-capabilities.ts, and the same note inpackages/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 inpackages/cli/src/commands/serve.ts. And it is not a one-line map entry: the spec's own provider row records thatmarketplaceandpackage-registrypoint at the same package today, that this is what the carve-out inherited rather than decided, and that "repointingmarketplaceat 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.pnpm --filter @objectstack/metadata-protocol testadcd4a161)pnpm --filter @objectstack/metadata-protocol typechecktsc --noEmit; it does compile this test file — an earlier run of it caught a TS2353 in the new fixture type)pnpm --filter '@objectstack/metadata-protocol^...' buildnode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands--ran(each linecommand :: exit N).withallowInlineConfig: falseadcd4a161)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 withpnpm 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 tosrc/, notdist/(an earlier failing run namedsrc/protocol.ts:6:1in its stack), so no rebuild sits between the mutation and the run.console.warnininstallPackage's no-service branch replaced with a no-op — anchorx1 → x0, blob653d2155fb74 → 7072a8718166, both verified by re-reading the file.1 failed | 10 passed (11)— the service-absent pin went red on the product change, so it reads the primitive and not the fake.653d2155fb74, identical toHEAD, andgit diff HEADempty.No ablation artifact remains in the tree.
Changeset
Clause-②: no, and apatchchangeset for@objectstack/metadata-protocol—.changeset/17676-package-registry-docblock-capability-name.md.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-protocolis not private, itsfiles[]shipsdist, and the corrected docblock reachesdist/index.js,dist/index.cjs,dist/index.d.tsanddist/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.tsin their editor that the absent-service branch is what you get "e.g. themarketplacecapability 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 underskip-changesetthis correction would sit onmainunshipped, 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:
warn. AGENTS.md's degradation-log-level rule grades a durability degradation aterror("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 anif (!service)composition branch "is usually functional and belongs atwarn", so theelsearm is genuinely arguable; thecatcharm around thepublishcall is the one the rule says it bites.pnpm check:durability-log-leveldoes not see either, becausepkgSvc.publishis not inDURABILITY_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.mjsreported STALE TREE on both derivations (two of its own input scripts moved onorigin/mainwhile 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.5651690676— the "may item 3 be delivered independently of item 1" question — returns 404 and was destroyed with the suspendedos-muskidentity. 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