fix(rest): the dispatcher's /packages domain is the one implementation of the package read and delete routes (#14503) - #16628
Conversation
…ackageRoutes (#14503) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…t; pins follow the one route Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ckage-routes-single-implementation
…lot-lookups pin follows Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
… baseline after the ledger row deletion Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…ckage-routes-single-implementation # Conflicts: # content/docs/permissions/system-context.mdx
… 54 names the write-only gate - changeset: the scoped mount is served by the dispatcher only where the @objectstack/hono catch-all is mounted; the uninstall's tenancy width narrows from allTenants to the caller's organization; GET /packages no longer merges durable sys_packages rows — each stated as deliberately removed, never silently dropped - content/docs/permissions/system-context.mdx row 54: the REST package gate guards POST /packages/publish (manage_metadata) only; the read cohort is row 55's Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
The REST registrar mounts POST /packages/publish only (#14503); the read cohort is the dispatcher domain's to enforce and is named in plain words, since the census gate reads a path#symbol in row prose as an anchor. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
📓 Docs Drift CheckThis PR changes 2 package(s): 34 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 29 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 57a8c6a9e7f51e73e55c3955d803541a4254e813 && git checkout 57a8c6a9e7f51e73e55c3955d803541a4254e813
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b38821d1ce220527e2a7f34e254a96c48e2a9ba3 a9f826933de660fc7aa51551132e96ab9f186f6d && git checkout -B drift-repro b38821d1ce220527e2a7f34e254a96c48e2a9ba3 && git merge --no-ff a9f826933de660fc7aa51551132e96ab9f186f6d
node scripts/docs-audit/affected-docs.mjs --json b38821d1ce220527e2a7f34e254a96c48e2a9ba3
|
- kernel/contracts/metadata-service.mdx: the /api/v1/packages family is the dispatcher domain's; the REST registrar contributes POST /packages/publish (the table had it at the bare POST /packages, which is the install route); the list is the installed set, not registry + database; the read answers the bare row and the dispatcher's 404 wording; the uninstall is organization-scoped - permissions/permission-sets.mdx: the uninstall's revocation report sits under data.persisted.cleanups on the surviving envelope Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
… 91/91 after the three package rows left (#14503) population 94 -> 91 and reachable 94 -> 91, derived from the tree with deriveProbeFileCensus(), not back-solved: each removed row carried family: 'packages', so the family key reached it; the blind spot stays 0 and the 19 families are unchanged (the publish row keeps the family). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…es the tracker id to a comment check:doc-authoring forbids an issue id inside a runtime string literal; the adjacent comment carries it for the reader who can resolve it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
Contract review (
|
| route | registrations on head | where |
|---|---|---|
GET /packages |
1 | packages/runtime/src/domains/packages.ts:573 via dispatcher-plugin.ts:1306 (mountPackagesRoute, unscoped) and the @objectstack/hono catch-all |
GET /packages/:id |
1 | packages/runtime/src/domains/packages.ts:1198 |
DELETE /packages/:id |
1 | packages/runtime/src/domains/packages.ts:1255 |
POST /packages/publish |
1 | packages/rest/src/package-routes.ts:407 (unchanged) |
packages/hono/src/*.ts and packages/plugins/plugin-hono-server/src/*.ts: zero /packages route registrations (grep, non-test). On main the REST registrar carried a second registration for each of the three (package-routes.ts:873, :967 and the list route). After the PR: exactly one each. Claim holds.
Anything silently gone? Unscoped: no — every in-tree composition that mounts the REST registrar also mounts the dispatcher (serve.ts, verify/harness.ts, plugin-dev), and on a stock boot the REST copies were never mounted (the gate ran before PackageServicePlugin registered), so the dispatcher was already the door. Scoped without createHonoApp: yes — those three URLs now fall to the transport's bare 404 with no refusal envelope. This is stated in the changeset and is the open fork; it is the maintainer's call, not a defect of this PR.
3. Wire contract on the surviving door
GET /packages/:id→{ success, data: <InstalledPackage row>, meta }. MatchesGetInstalledPackageResponseSchema(packages/spec/src/api/package-api.zod.ts:96-98,data: InstalledPackageSchemabare). FROMdata.package+sourceTOdata— stated in the changeset. ✅- 404 on both GET and DELETE →
deps.error("Package '<id>' not found", 404)→error.code = RESOURCE_NOT_FOUNDviapackages/runtime/src/error-envelope.ts:120(standardErrorCodeForHttpStatus,errors.zod.ts:176). ADR-0112 envelope holds; wording change stated. ✅ - Anonymous refusal: both doors used
shouldDenyAnonymous(packages.ts:552; removed RESTrefusePackageRequest). Same status/code. ✅ - Capability refusal code changed and is not in the changeset — see F1.
GET /packagesandDELETE /packages/:iddo not match theirpackages/specdeclarations — see F2 (pre-existing on the dispatcher, but the survivor is now the only implementation, so it is the contract).
Findings
F1 — 403 refusal code changes FORBIDDEN → PERMISSION_DENIED on the three routes; undisclosed.
packages/runtime/src/domains/packages.ts:249-256 (requireManageMetadata) and :281-290 (requireReadCapability) call deps.error(message, 403) with no explicit code; packages/runtime/src/error-envelope.ts:120 then derives standardErrorCodeForHttpStatus(403) = PERMISSION_DENIED (packages/spec/src/api/errors.zod.ts:175). The removed REST copies emitted sendError(res, 403, 'FORBIDDEN', …) (main package-routes.ts, the refusePackageRequest write/read branches). Same status, same message text, different error.code — a client branching on err.code === 'FORBIDDEN' for a package read/delete refusal stops matching on any composition that reached the REST copies. Both codes are ADR-0112 standard members, so the envelope holds. Expectation: one row in .changeset/rest-package-routes-single-implementation.md: capability refusals on GET /packages, GET /packages/:id, DELETE /packages/:id answer 403 PERMISSION_DENIED (FROM 403 FORBIDDEN), messages unchanged. Level stays minor.
F2 — the "surviving shape is the spec's own" claim is true only for GET /:id; the list and uninstall shapes do not match spec, and nothing pins them.
GET /packagesanswers{ packages, total }(packages.ts:585);ListInstalledPackagesResponseSchema(package-api.zod.ts:71-77) requireshasMore: z.boolean(), and declares inputsenabled/limit/cursorthe domain does not read (it readsstatusand an undeclaredtype,:576-581).DELETE /packages/:idanswers{ success, registryRemoved, persisted }(packages.ts:1296);UninstallPackageApiResponseSchema(package-api.zod.ts:358-364) requirespackageId.- The runtime ledger rows for
packages.list/packages.uninstall(packages/runtime/src/route-ledger.ts:336,:339) carry noresponseSchema; grep finds no test outsidepackages/specreferencing either schema.
This drift predates the PR (the REST copies did not match either), so it is not introduced here — but with one implementation there is now exactly one thing to reconcile. Expectation: (a) the changeset/PR body scope the spec-conformance sentence toGET /packages/:id; (b) a card, not a rider, for reconciling the list and uninstall doors withPackageApiContracts.listPackages/.uninstallPackage(or retiring those declarations under ADR-0049 if the dispatcher's shape is the ruled one).
F3 — scoped SDK binding is stale the moment this merges (disclosed, carded).
packages/client/src/index.ts:366-377: ScopedEnvironmentClient.packages.get(id, version?) returns { package: InstalledPackage } and its docblock says only the REST registrar serves the scoped mount. On this head the scoped door (through the catch-all) answers the bare row and ignores version. Held on #12034 (open, pm:awaiting-maintainer), acknowledged in the PR body. Expectation: #12034 queues in the same train, or its remaining half is named in the changeset as known-stale; either is acceptable. Not blocking.
4. Tests (read, not trusted)
- Reddens if a second REST implementation reappears:
packages/rest/src/package-publish-mount.test.ts:129-149(registrar returns exactly['POST /api/v1/packages/publish']with and without apackageservice, and asserts the three former twins absent);packages/rest/src/direct-mount-introspection.test.ts(ledger has only the publish row; the twins are neither mounted nor in OpenAPI on either boot, scoped base included);packages/rest/src/rest-route-ledger.conformance.test.ts:153-181(mounted direct-mount set ≡ ledger set in both directions, so a resurrected route without a row reds, and a stale row without a mount reds). ✅ - Survivor pinned:
packages/runtime/src/domains/packages-single-door.test.ts— 404 wording verbatim withRESOURCE_NOT_FOUND, retired spelling asserted absent, envelope key set['data','meta','success'], nopackagewrapper, nosourceon row or list rows, unscoped and/environments/:id/…throughdispatch()with the catch-all-derived subpath, two negative controls (ROUTE_NOT_FOUND). Identity goes through the real resolver, not a bypass. ✅ - Per removed route "gone or refused": the REST absence pins above cover "gone" at the registrar and ledger. There is no pin that the scoped URL on the
plugin-hono-server-only composition answers anything in particular — correct, because what it should answer is the open fork. - The 8 deleted REST pin files (
package-delete-status-classification,package-door-producer-key-carry,package-id-registry-read-refusal,package-list-durable-read-refusal,package-list-registry-read-refusal,package-list-writable-carry,package-registry-item-projection,package-routes-query-multiplicity) pinned REST-copy-only behaviour (registry+durable merge,source,?version=, REST's projection). Dispatcher-side equivalents exist for what survives:packages-writable-verdict.test.ts,packages-readonly-gate.test.ts,packages-serializable-response.test.ts,packages-uninstall-envelope.test.ts(PACKAGE_DELETE_PARTIAL). ✅ packages/runtime/src/domains/packages.tsdiff is comment-only (verified line by line); "behaviour untouched" holds.
5. Changeset and governed paths
.changeset/rest-package-routes-single-implementation.md:@objectstack/restminor — matches the ruling. FROM/TO stated for the/:idenvelope, 404 wording,source,?version=+ refusal, durable-row merge, DELETE tenancy width (allTenants: true→ caller's organization), discovery, and the scoped-mount residue. Missing: F1. No@objectstack/runtimechangeset — correct (comments + a test).- Governed paths (
docs/adr/**,.claude/**,skills/**,AGENTS.md,CLAUDE.md): none touched.content/docs/releases/: untouched. The threecontent/docsedits andpackages/qa/dogfood/test/authz-probe-blind-spot.census.ts(94→91,reachablemoved withpopulation, blind spot 0) read accurate against the code.
6. CI and mergeability
- Head
ba6b8311ed: 37 check runs, allsuccessexcept twoskipped(Console Pin Gate, packed-tarball opt-in); Vercel statussuccess. Lint & Repo Gates green on this head (thecheck:doc-authoringred was onc1344677dc, fixed). - GitHub reports
mergeable_state: unknown. Measured:git merge-tree --write-tree origin/main(8ccf7a1df) <head>exits 0, no conflicts. The branch is 51 commits behindmain(recorded base8341ed21c); a merge from main before ready-for-review is hygiene, not a blocker.
Summary for the PM
- Ruling A executed as ruled; step-2 fork correctly escalated (
5571983259) — that ruling is the landing gate, not this review. - Required before ready: F1 (one changeset row). Recommended: F2(a) one sentence; F2(b) a card.
- Not approving, not marking ready, no labels changed, no files edited.
Generated by Claude Code
…ckage-routes-single-implementation
…the C′ residue (#14503) Contract review 5578894182 F1/F2 and the director seat's step-2 ruling C′ (#14503 comment 5579647072): - F1: one row for the capability refusal `error.code` moving from the removed REST copies' explicit `403 FORBIDDEN` to the dispatcher's status-derived `403 PERMISSION_DENIED` (`standardErrorCodeForHttpStatus`), same status, same message text. - F2: the spec-conformance claim is scoped to `GET /packages/:id`; the list's missing `hasMore` and the uninstall's missing `packageId` are named as pre-existing drift, carded on #16781. - C′: the residue sentence — a `plugin-hono-server` composition with `enableProjectScoping` and no `createHonoApp` has no scoped `/packages` door until #16781 lands. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
…blish door (#14503) `origin/main` landed `package-door-16019-raw-statement-fault-code.test.ts` while this branch was open. It pins the raw-statement declared-fault `code` at BOTH REST package doors, and its two `DELETE /api/v1/packages/:id` cases drive a route this branch removes -- the merge is clean textually and red semantically (`Error: no handler for DELETE /api/v1/packages/:id`). Resolved by keeping this PR's deletion: the two DELETE cases and the `deleteWith` helper are removed, the docblock records why. They are not re-pointed at the dispatcher -- the surviving door uninstalls through `protocol.deletePackage` and the registry, never through `PackageService.delete`, so the producer this file doubles has no delete door left in `@objectstack/rest`. The producer-side half is untouched and still pinned in `service-package`'s `delete-driver-fault.test.ts`. `@objectstack/rest`: 184 files / 3057 tests pass (was 1 file / 2 tests red). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TezFG8ZMrNH6n5VTNpPpdH
Patch round — F1, F2 and the C′ residue (director seat ruling 5579647072)Head What changed1. 2. One semantic conflict, resolved by keeping this PR's deletion ( 3. F1 — capability-refusal 4. F2 — the spec-conformance claim scoped, the drift named as pre-existing ( 5. C′ residue, verbatim in the changeset ( Tests — counts on
|
| run | verdict | result |
|---|---|---|
closure build, --filter '@objectstack/rest^...' --filter '@objectstack/runtime^...' |
command-exit 0 |
94 Build success lines |
pnpm --filter @objectstack/rest test (before the fix) |
command-exit 1 |
Test Files 1 failed | 183 passed (184) · Tests 2 failed | 3057 passed (3059) |
pnpm --filter @objectstack/rest test (after) |
command-exit 0 |
Test Files 184 passed (184) · Tests 3057 passed (3057) |
pnpm --filter @objectstack/runtime test |
command-exit 0 |
Test Files 241 passed (241) · Tests 3350 passed (3350) |
pnpm --filter @objectstack/rest typecheck |
command-exit 0 |
tsc + check:test-typecheck — 0 files / 0 errors in debt |
pnpm --filter @objectstack/runtime typecheck |
command-exit 0 |
tsc + check:test-typecheck — 27 files / 191 errors held, unchanged |
Gate exit codes — verbatim, redirected before capture, never a bare $?
| gate | exit |
|---|---|
node scripts/check-changeset-no-major.mjs --base origin/main |
0 (✓ This diff introduces no major bump; LEVEL AXIS: NOT MEASURED — no pull_request payload locally, #4690) |
node scripts/check-changeset-no-major.mjs --self-test |
0 |
node scripts/check-adr-0087-registration.mjs --base origin/main |
0 |
node scripts/check-adr-0087-registration.mjs --self-test |
0 |
node scripts/check-empty-changeset.mjs --base origin/main |
0 (1 declaring changeset added) |
node scripts/check-empty-changeset.mjs --self-test |
0 |
node scripts/check-changeset-fixed.mjs |
0 |
pnpm check:changeset-gate-self-tests |
0 |
pnpm check:system-context-census |
0 (106 read sites, 103 of 103 required symbols cited) |
pnpm check:doc-authoring |
0 (398 docs, 46 skill files, 15124 spec strings, baseline held) |
pnpm check:nul-bytes |
0 (8276 files, no raw control bytes) |
pnpm check:test-source-alias |
0 |
pnpm check:cross-package-test-inputs |
0 |
pnpm check:objectui-changeset |
0 |
The two that were red on the raw diff — check:system-context-census and check:doc-authoring — are 0 on this head. No exit 3 (NOT MEASURED) in this round; no gate is reported as green that was not measured.
Declared narrowing. This round changed two files (the changeset and one @objectstack/rest test). The roster node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derives for the full PR diff at this head is 145 commands; the families that read this round's two paths are the changeset family, nul-bytes, test-source-alias and cross-package-test-inputs, all run above, plus the two docs gates the dispatch named. The rest of the farm is left to CI, which is the run that owns it. origin/main advanced to b38821d1c during this round (2 commits) — the merge above is against ed7243d52.
Not done, deliberately
- Not marked ready, not merged, no auto-merge, no label changes — the ruling reserves those for the director seat.
- No scoped
mountPackagesRoutevariant, and no change to either drifting response shape: both are finding(runtime): scoped/api/v1/environments/:id/packages[/:id]has no dispatcher door on aplugin-hono-server-only composition after #16628 (B′ follow-up to #14503) #16781's, per C′. - The ruling comment's ledger line reads
objectstack#12708; the card actually filed for B′ and named in the dispatch is finding(runtime): scoped/api/v1/environments/:id/packages[/:id]has no dispatcher door on aplugin-hono-server-only composition after #16628 (B′ follow-up to #14503) #16781, whose body cites this PR and the C′ ruling. finding(runtime): scoped/api/v1/environments/:id/packages[/:id]has no dispatcher door on aplugin-hono-server-only composition after #16628 (B′ follow-up to #14503) #16781 is what the changeset and body now reference.
Generated by Claude Code
Contract review (
|
| # | finding | expectation | on a9f8269 |
status |
|---|---|---|---|---|
| F1 | capability refusals answer 403 PERMISSION_DENIED where the REST copies answered 403 FORBIDDEN; undisclosed |
one changeset row, FROM/TO, level stays minor | Row present ("Capability refusals answer a different error.code. … FROM 403 FORBIDDEN TO 403 PERMISSION_DENIED"). Derivation re-verified: packages/runtime/src/domains/packages.ts:254 (requireManageMetadata) and :287 (requireReadCapability) call deps.error(message, 403) with no code; packages/runtime/src/error-envelope.ts buildApiError → input.code ?? promoted ?? standardErrorCodeForHttpStatus(httpStatus); packages/spec/src/api/errors.zod.ts HttpStatusErrorCodeMap[403] = 'PERMISSION_DENIED'. The two cohort messages quoted in the row are byte-identical to the source strings. Level minor unchanged. |
discharged |
| F2 | "surviving shape is the spec's own" true only for GET /:id; list lacks hasMore, uninstall lacks packageId; nothing pins them |
(a) scope the sentence; (b) a card | (a) Changeset: "Spec conformance on the surviving door is claimed for GET /packages/:id only", then both drift cases named with schema and field (ListInstalledPackagesResponseSchema requires hasMore; UninstallPackageApiResponseSchema requires packageId), stated as pre-existing, not introduced by this release, and the missing responseSchema on the runtime ledger rows packages.list / packages.uninstall (packages/runtime/src/route-ledger.ts:336, :339 — confirmed still without responseSchema) named. (b) Carded on #16781. |
discharged |
| F3 | scoped SDK packages.get(id, version?) stale on merge; carded #12034 |
name it or queue it; not blocking | Changeset's ?version= row names the SDK sender and #12034. Unchanged from the prior review; was not blocking. |
discharged (as disclosed) |
Also owed by the ruling: merge origin/main — done (3d35f8fcd, plain merge, base ed7243d52, no textual conflict; git merge-tree --write-tree origin/main a9f826933 against today's b38821d1c exits 0 as well). C′ residue verbatim — present as the last sentence of the changeset: "On a plugin-hono-server composition with enableProjectScoping and no createHonoApp, the scoped /api/v1/environments/:id/packages[/:id] routes have no door until #16781 lands (ruled C′ on #14503)."
Numbered verification
1. Diff surface and governed paths. git diff ed7243d52..a9f826933 --name-status: 38 files, +945 / −4032. 1 changeset added; 3 content/docs pages (none under releases/); 1 dogfood census; 26 files under packages/rest/src (8 test files deleted, 1 source-level module package-routes.ts cut 800 lines, ledger/server/composition/plugin edits, 13 tests re-pointed incl. the #16019 file); packages/runtime/src/domains/packages.ts (comment-only — diff read line by line, no executable hunk) plus the new packages-single-door.test.ts; 2 scripts. Governed paths (docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, content/docs/releases/): none — grep over the name list returns nothing. Governed Surface Queue Guard: success.
2. The semantic conflict — package-door-16019-raw-statement-fault-code.test.ts.
- What main landed (PR fix(driver-sql, driver-turso): SqlDriver.execute() declares a backend refusal as DATABASE_ERROR/500 instead of leaking the dialect's error object #16650,
001a83b04): a REST door pin with 6 cases; two driveDELETE /api/v1/packages/:idthroughregisterPackageRouteswith aPackageServicedouble whosedeletethrows the declaredDATABASE_ERROR/500fault (AFTER) or returns{ success: false }(BEFORE →PACKAGE_DELETE_FAILED). - Why it was red after the merge: on this branch the registrar mounts only
POST /packages/publish(packages/rest/src/package-routes.ts:406-407is the onlypath:), so the file's owndrive()throwsno handler for DELETE /api/v1/packages/:id. Read, not inferred — the helper is at:110. - What this PR did: removed the two
DELETEcases anddeleteWith, left the four publish-side cases byte-for-byte, and rewrote the docblock to say why ("no delete-door subject for THIS producer is left in this package to pin"). Diff read in full. - Is the looksLikeInternalErrorLeak recognises
no such column:but notno such function:— a SQLite parse failure echoes the raw engine message into the 500 body #16019 delete-doorcodestill pinned somewhere that survives? Producer half: yes —packages/services/service-package/src/delete-driver-fault.test.ts, its[#16019]block: the declared fault propagates identity-unchanged out ofPackageService.delete(rejects.toBe(fault), withstatus 500/code DATABASE_ERRORasserted on the object) and the undeclared ancestor still takes branch ③ — untouched by this PR. Door half: there is no surviving door for it, and that is correct, not a gap. The dispatcher'sDELETE /packages/:id(packages/runtime/src/domains/packages.ts:1255-1361) never callsPackageService.delete; it callsprotocol.deletePackage(...)and wraps that inerrorFromThrown(e, 500).PackageService.deleteis reached from there only insidepackages/metadata-protocol/src/protocol.ts:18543-18556, the "DB-created base packages vanish from GET /packages after server restart #2532 counterpart"sys_packagescleanup, which istry { pkgSvc.delete(id) } catch (e) { console.warn(...) }— a declaredDATABASE_ERRORfrom the producer is swallowed to a warning on this door and never reaches the wire. So the looksLikeInternalErrorLeak recognisesno such column:but notno such function:— a SQLite parse failure echoes the raw engine message into the 500 body #16019 door contract ("the declared fault's code answers at the door") has no subject on the survivingDELETE; re-pointing the two cases at the dispatcher would have asserted something false. - Verdict on the deletion: a legitimate narrowing, not "never skip a test". The subject of the two cases (a REST door that calls
PackageService.deletedirectly) is what the packages/rest package read routes claim to SHADOW their dispatcher twins, but the dispatcher answers /packages and /packages/:id on a stock showcase boot #14503 ruling removes; the cases are deleted with the subject, the reason is in the docblock, the producer-side pin survives, and noit.skip/describe.skip/.todowas introduced anywhere in the diff (grep over the 38 files: 0). The REST absence pins (package-publish-mount.test.ts:141,direct-mount-introspection.test.ts:135/289) red if the delete door ever comes back, which is the moment the door half would be owed again.
3. Changeset (.changeset/rest-package-routes-single-implementation.md): @objectstack/rest: minor — matches the ruling. F1 row: present and accurate (derivation in the table above). F2 scoping: present, names both drift cases and #16781. C′ residue: present, verbatim in the ruled form with #16781 as the <B′ card>. All earlier rows (FROM data.package TO data, source removed, durable-row merge removed, ?version= + refusal removed, 404 wording, DELETE tenancy width, discovery, ledger rows) unchanged from the prior review. No @objectstack/runtime changeset — still correct (comments + a test). Check Changeset: success.
4. PR body "Boundary" paragraph: cites "the director seat recorded C′ on #14503 (comment 5579647072, decision batch #84, 2026-09-08)" and "#16781 carries the scoped mountPackagesRoute variant … together with the F2 response-shape reconciliation"; the earlier "maintainer's call" phrasing is gone; the withdrawn "a repo not in this session" attribution stays withdrawn. ✅
5. Route census on the head — unchanged from ba6b831.
| route | registrations | where |
|---|---|---|
GET /packages |
1 | packages/runtime/src/domains/packages.ts via dispatcher-plugin.ts:1322 (mountPackagesRoute('get', '')) + the @objectstack/hono catch-all |
GET /packages/:id |
1 | packages.ts:~1198 via dispatcher-plugin.ts:1325 |
DELETE /packages/:id |
1 | packages.ts:1255 via dispatcher-plugin.ts:1326 |
POST /packages/publish |
1 | packages/rest/src/package-routes.ts:406-407; ledger row rest-route-ledger.ts:453 is the only packages row left |
packages/hono/src/*.ts and packages/plugins/plugin-hono-server/src/*.ts (non-test): zero /packages registrations. Every other /packages mention in packages/rest/src non-test files is docblock prose describing the removal.
6. CI on a9f826933 (read at posting time, 35 check runs): 24 success, 2 skipped (Console Pin Gate, Packed-tarball smoke opt-in), 9 in_progress, 0 failure. Still running at 06:06Z: Test Core 4/6 and 5/6, Dogfood Regression Gate 1/3–3/3, Temporal Conformance (live PG + MySQL), Build Core, Dogfood Verify CLI, Lint & Repo Gates. Already green: all four Type Check jobs, Test Core 1/2/3/6, Build Docs, Check Changeset, Governed Surface Queue Guard, the three claim/part-of guards, Spec property liveness, docs links. The verdict above is conditional on those nine completing green; the two that were red on earlier heads (Dogfood — the #13260 census; Lint & Repo Gates — check:doc-authoring) were green on ba6b831 and the round-5 diff touches neither input. mergeable_state reported blocked — that is the draft/required-review state, not a conflict; merge-tree against b38821d1c is clean.
Residual findings
R1 — informational, not blocking: the surviving DELETE /packages/:id cannot surface a sys_packages driver fault at all. Consequence of the reading in §2: on the one implementation, a durable-row delete that fails (no such table, FK restriction, lock timeout — the exact family delete-driver-fault.test.ts reproduces) is logged by protocol.deletePackage and the door still answers 200 { success: true, registryRemoved, persisted } — and per the #2532 comment at that site, the row left behind re-hydrates the package on the next restart. Pre-existing dispatcher behaviour, reachable on every stock boot before this PR, so not introduced here and not a defect of this PR; the REST copy that could answer 500 DATABASE_ERROR for it was never mounted on a stock boot. Expectation: none for landing. Recommended: one line on #16781 (which already owns the uninstall-shape reconciliation) or on #16019 noting that with the REST door gone, the declared delete fault has a producer pin but no wire-observable door, so the card's "two doors" narrative is now one door with a swallow.
R2 — housekeeping, not blocking: the ruling comment's ledger line reads objectstack#12708 while the filed card is #16781. Already disclosed by the patch round; the tree is consistent on #16781. Expectation: the director seat corrects the ledger line on #14503 if it is load-bearing anywhere; nothing owed in this PR.
Summary for the director seat
- All three ruled pre-landing items (F1 row, F2 scoping + card, C′ residue verbatim) and the
origin/mainmerge are on the tree as ruled. - The looksLikeInternalErrorLeak recognises
no such column:but notno such function:— a SQLite parse failure echoes the raw engine message into the 500 body #16019 test edit is a narrowing whose subject the ruling removed; the producer pin survives; no test is skipped. - Nothing blocks landing except CI finishing on this head. Not approving, not marking ready, no labels changed, no files edited; throwaway ref deleted.
Generated by Claude Code
Fixes #14503
Summary
GET /api/v1/packages,GET /api/v1/packages/:idandDELETE /api/v1/packages/:idnow have one implementation: the runtime dispatcher's/packagesdomain (packages/runtime/src/domains/packages.ts).@objectstack/rest'sregisterPackageRoutesno longer mounts its own copies of those three routes; it mountsPOST /api/v1/packages/publishand nothing else. This executes the maintainer's ruling on #14503 (2026-09-02, verbatim 「同意」, option A), in the ruled order: measure → verify compositions → delete with the survivor pinned.The SHADOWS sentence the card is about, the opposite "mounts FIRST in the production stack" claim in the same file, the
#7563docblock's "independent of composition order" claim, and the two module headers calling the?version=refusal "dormant behind a Hono collapse" are all taken with the deletion.Step 1 — the census, re-taken on today's tree (
ce8caba914, the branch base)The prior census (comment
5544498087) was measured before PR #15859; every verdict below is a fresh reading with its control on the same corpus.objectui@ pina472b07)content/docssource: 'registry' | 'database' | 'both'readerssource: 'database'on metadata-loader items, 5 in the producer itself, 2 SDK comments) — control.writable= 17.writable= 26?version=senders to a packages doorScopedEnvironmentClient.packages.get(id, version?)(packages/client/src/index.ts) — control?overwrite== 2?keepData== 1 (the Console's DELETE sends the dispatcher's parameter)The SDK sender is already carded: #12034 (open,
pm:awaiting-maintainer) holds the maintainer's 2026-08-26 ruling thatpackages.getbinds the bare row; its remaining half is exactly the scoped binding this PR now makes bindable. Nothing is ported into the dispatcher: a version-scoped durable lookup was a behaviour only the REST copy had, and porting it would change the dispatcher's data source — materially bigger than what was ruled.Step 2 — compositions, and the fork
Every in-tree composition that mounts the REST package registrar also mounts the dispatcher:
packages/cli/src/commands/serve.ts(createRestApiPlugin:4019,createDispatcherPlugin:4039),packages/verify/src/harness.ts(:594/:595),packages/plugins/plugin-dev/src/dev-plugin.ts(:856/:873). None turnsenableProjectScopingon (mergeBootConfigforces it false on the standalone boot).The deciding reading — driven through the real
@objectstack/honocatch-all on the built runtime dist, atce8caba914(kernel: session-backed identity, ObjectQL engine with a realSchemaRegistryholdingcom.acme.crmand the permission-set tables the shared authz resolver reads):⇒ A scoped
/packagesURL reaches the dispatcher's/packagesdomain through the catch-all (the dispatcher's spelling, single quotes, no trailing period). The fork the prior census reported has dissolved on today's tree; the three REST routes can be removed without any composition losing/packages.Boundary — corrected by the at-tier review (fork report
5571983259), and the reason this PR was held in draft. The composition that loses scoped/packagesunder the deletion is reachable from the open-core CLI, not only from the cloud repo:serve.tsskips the standalone boot whenshouldBootWithLibrary()is false (plugin-detection.ts— any host config, orOS_MODE=off) and then forwardsapi.enableProjectScopingverbatim to the REST plugin and the dispatcher plugin onHonoServerPlugin. Withplugin-hono-serverbut withoutcreateHonoApp, the dispatcher's explicit mounts register/packages*at the unscoped prefix only and the transport's sole route into the dispatcher (setFallbackHandler) serves declarativeapis:endpoints, so on that composition the REST mirror was the only door for scoped package reads and deletes — and after this PR there is none. My earlier boundary statement attributed that region to "a repo not in this session"; that attribution was wrong and is withdrawn here. Every consumer population reachable from this repo is zero for the scoped mount (measured with controls in the fork report); the residue is now stated in the changeset. The step-2 fork is ruled, not open: the director seat recorded C′ on #14503 (comment5579647072, decision batch #84, 2026-09-08) — land this PR now with the residue disclosed in the changeset, and carry the repair on a follow-up card rather than inventing a scoped dispatcher mount in flight. On aplugin-hono-servercomposition withenableProjectScopingand nocreateHonoApp, the scoped/api/v1/environments/:id/packages[/:id]routes have no door until #16781 lands (ruled C′ on #14503). #16781 carries the scopedmountPackagesRoutevariant — the automation / actions / ai pattern atdispatcher-plugin.ts:1653-1667, the same handler, no second implementation — together with the F2 response-shape reconciliation and a pin on that composition. ⛔ No scopedmountPackagesRoutevariant is added here.Step 3 — the deletion, and what pins the survivor
packages/rest/src/package-routes.ts: the three service-gated routes, their mount gate, theprotocoloption (only they read it), the read cohort of the gate, the registry projection helper and the compile-time pins for theprotocolseam are removed; the docblocks say what happened and why.direct-mount-composition.tsandrest-api-plugin.tsstop threadingprotocol.packages/rest/src/rest-route-ledger.ts: the three direct-mount rows are gone; the runtime route ledger carries the family.packages/rest/src/rest-server.ts:/discovery'sroutes.packagesis now derived from the publish mount (POST {base}/packages/publish→{base}/packages) instead of from the registrar's ownGET {base}/packagescopy. Measured consequence worth knowing: on a stockobjectstack serveboot that copy was never mounted (thepackageservice registers after the REST plugin starts), so REST's discovery omittedroutes.packageswhile the dispatcher served the family; it is advertised on every boot now.packages/runtime/src/domains/packages-single-door.test.ts(new) pins the surviving door — 404 wordingPackage 'ID' not foundwithRESOURCE_NOT_FOUND, the retired spelling asserted absent, the bare-row envelope{ data, meta, success }with nopackagewrapper and nosource, the list rows withoutsource— unscoped and environment-scoped, driven throughdispatch()with the catch-all-derived subpath (the fix(runtime): the dispatcher's scope strip matches/environments/, the prefix its own hint parser reads #15859 pattern; the hono package's suite aliases runtime to a mock, so the real-adapter drive above is the measurement and this file is the durable pin).packages/runtime/src/domains/packages.ts: the stale "which shadows it only when apackageservice is registered" sentence and the two "sibling REST door" references are corrected; behaviour untouched (main's fix(runtime): key package lifecycle state by project, not by environment alone #16572 merged cleanly beside it).packages/rest/src/query-multiplicity.tsheader: the "dormant" claim replaced with the measured fact (plugin-hono-server'sreadQuerykeeps arrays since 两个IHttpServer适配器对「重复的查询参数」给出不同形状:Hono 折叠成第一个值,node:http 给数组 #6878 / PR test(http-conformance): 「重复查询参数」的跨适配器分歧落锁为已知事实 (#6878 路线 1) #6941, pinned by the http-conformance suite).POST /packages/publish— same resolver, same gate, same three wire answers).scripts/check-undeclared-dep-imports.mjs: the type-only ledger row forpackage-routes.ts→@objectstack/metadata-protocolis deleted (the import went with the delete route; the gate reds on a stale row).content/docs/permissions/system-context.mdxandscripts/doc-authoring-prose-id.baseline.json: line-rot re-anchoring and the shrink-only baseline regeneration, both by the gates' own--fix/--census-ledgerremedies.Changeset
@objectstack/restminor (.changeset/rest-package-routes-single-implementation.md), per the ruling. It states FROM → TO forGET /packages/:id(data.package→data), records thesourcestamp and the?version=read + multiplicity refusal as deliberately removed, the 404 wording change, the DELETE semantics, and the discovery change.Round 5 adds the contract review's two required rows and the ruling's residue sentence: F1 — capability refusals on the three routes answer
403 PERMISSION_DENIEDwhere the removed REST copies answered403 FORBIDDEN(deps.error(message, 403)with no code,error-envelope.tsderiving it viastandardErrorCodeForHttpStatus), same status and same message text; F2 — the spec-conformance claim is scoped toGET /packages/:idalone, with the list's missinghasMoreand the uninstall's missingpackageIdnamed as pre-existing drift that this release does not introduce and does not change, carded on #16781; and the C′ residue stated verbatim.Verification
Every build/test ran through
scripts/pm/os-verify-lock.sh; VERDICT lines read, never a bare$?. Heads are named per reading because the branch grew across the run (final head60f7d46b49; the two commits after7647624a00touch only a docs page and a baseline JSON).pnpm --workspace-concurrency=2 --filter '@objectstack/hono^...' build, treece8caba914):build-exit=0, 97 "Build success" lines.@objectstack/rest, full suite at7647624a00(after mergingorigin/mainf2f6684cd5):Test Files 179 passed (179) · Tests 3008 passed (3008), exit 0.@objectstack/runtime, full suite at7647624a00:Test Files 239 passed (239) · Tests 3330 passed (3330), exit 0.74b480158b(18 rest files + the new runtime pin):18 passed / 360 testsand1 passed / 10 tests.@objectstack/clientledger consumers at60f7d46b49, against the rebuilt rest dist (rest-route-ledger-coverage,client-url-conformance,route-ledger-response-schema,packages-write-envelope,return-type-precision):5 passed / 14 tests.pnpm --filter @objectstack/rest typecheck,pnpm --filter @objectstack/runtime typecheck= tsc +check:test-typecheck) at60f7d46b49: both exit 0. (A first attempt was NOT MEASURED — TS7016 against apackages/spec/distthat had lost its declarations mid-run; rebuilt withpnpm --filter @objectstack/spec build,check-dts-emitted: 34/34, then measured.)pnpm --filter @objectstack/spec build && check:generated→ "All 15 generated artifacts are up to date".60f7d46b49(committed state; trap-restored; both blobs hash-equal to HEAD afterwards,git diff HEADempty): mutating the dispatcher's 404 spelling at its 6 sites turned the new runtime pin RED (4 failed / 6 passed— exactly the four wording cases, scoped and unscoped); resurrecting aGET /packagesroute in the registrar turned the REST absence pins RED (12 failed / 18 passedacrosspackage-publish-mount,direct-mount-introspection,rest-route-ledger.conformance). Both modules resolve fromsrcthrough relative imports, so no dist sits on the path and no build leg was owed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths); 79 families atf904e2d62e, 106 at60f7d46b49once the docs page joined the diff. Reconciliation--ran:106 derived, 106 run, 0 UNRUN. Green, except four NOT MEASURED (reported as such, not as passes):check:dual-build-cjs-loads(exit 3, PREREQUISITE NOT MET — needs a fullpnpm build),check:type-check-debt(exit 3, PREREQUISITE NOT MET — needs the full built closure),check:pm-dispatch-gates(timed out at the 170 s and 280 s per-command budgets),check:skill-examples(exit 1 — refuses without@objectstack/client-reactbuilt; unrelated to this diff). Three gates were red on the raw diff and are green after their own remedies, committed here:check:system-context-census(line rot,--fix),check:doc-authoring(prose-id baseline over-pinned the deleted ledger row,--census-ledgershrink),check:undeclared-dep-imports(staletype-onlyrow deleted with the import; its--self-testand real run both pass). The dispatch-namedcheck:nul-bytes: green.pnpm exec eslint --no-inline-config --format jsonover every.ts/.mjsthis diff adds or modifies — ① population: the repo'seslint . --no-inline-configwithignores: [...NEVER_LINTED](eslint.config.mjs); ②--format jsoncount: 44 files (the changed set after the merge, a superset of this branch's own 27), 0 errors, 0 warnings, exit 0, at60f7d46b49; ③ invariance: the config enables noparserOptions.projectand no typed@typescript-eslintrules (eslint.config.mjs:328), so nothing in this diff can move an untouched file's verdict.turbo ls --affectedlists 19 packages; only rest, runtime and the client ledger consumers were run here. The others (cli, client-react, cloud-connection, dogfood, downstream-contract, four examples, hono, http-conformance, organizations, plugin-auth, plugin-dev, service-sms, verify) consume rest/runtime and are left to CI — the behavioural change is confined to the REST registrar and its discovery projection, and the hono adapter's own suite aliases runtime to a mock.in_progressat the time of this body — headba6b8311ed(round 4: the census note's tracker id moved from the runtime string to an adjacent comment;pnpm check:doc-authoringexit 0 measured on this exact tree before the push) started 31 check runs (11success, 18 in progress, 2 skipped, read 80 s after the push). Onc1344677dceverything else was green —Dogfood Regression Gatepassed (the authz-conformancediscover()reaches 1 of 17 route registrars in rest-server.ts — the ratchet's route-completeness guarantee is false for 66 of 85 mounted routes #13260 census fix confirmed by CI) — andLint & Repo Gateswas red only oncheck:doc-authoringfor that id-in-string.check:doc-authoring 0; that reading was taken on the docs tree (edcb5808e7) before the census note existed and was carried forward unchecked — an ordering mistake on my side, not a CI/local divergence. Earlier history:40c7679eecred on Dogfood 3/3 (the census);edcb5808e7cancelled by the next push; the initial draft head60f7d46b49had 0 check runs.Round 2 — contract-review follow-ups (all owed under any ruling outcome)
origin/main(8341ed21ce) merged; the one conflict,content/docs/permissions/system-context.mdx, resolved by taking main's side (symbolicfile#functionanchors) and re-runningpnpm gen:system-context-censuson the merged tree (no further re-anchoring needed; gate green).rest-server.tsauto-merged — main's change there is a comment hunk near the/metaread door, far from the discovery hunks. ⛔ No hand-edited anchors, no rebase, no force-push.plugin-hono-server-without-createHonoAppcomposition above); the uninstall's tenancy width narrows from the REST copy'sdeletePackage({ packageId, allTenants: true })(package-wide,protocol.deletePackagefinds zerosys_metadatarows the data plane finds 3 of — uninstall leaves orphaned rows (persistence half of #7557) #7705 case 4) to the dispatcher's{ packageId, organizationId?, keepData? }resolved for the caller's organization;GET /packages(not only/:id) no longer merges durablesys_packagesrows, so a published-but-not-installed artifact is neither listed nor gettable on the surviving door. Level unchanged:@objectstack/restminor.POST /packages/publishgate (manage_metadata); the read cohort is cited to the dispatcher domain's own gate by symbol (the census gate holds numeric row references to declared keys, so no row number is used).Round 3 — docs-drift (bot comment on this PR, 34 hand-written pages + 4 release-owned)
Re-derived on this tree with
node scripts/docs-audit/affected-docs.mjs --json 8341ed21ce(38 rows, anchors read per row). Fixed only what this diff falsified:content/docs/kernel/contracts/metadata-service.mdx— the REST endpoints table said the REST layer "mounts package routes under/api/v1/packages" and listedGET /api/v1/packagesas "List all packages (registry + database)". The family is the dispatcher domain's now, the list is the installed set (published-but-not-installed artifacts are not merged in), the read answers the bare row with the dispatcher's 404 wording, the uninstall is organization-scoped. While editing that table, itsPOST /api/v1/packages"Publish" row was corrected toPOST /api/v1/packages/publish— pre-existing (the bare POST is the install route), not something this diff falsified, fixed because the row is the one route this PR keeps.content/docs/permissions/permission-sets.mdx— "the uninstall response reports the revocation undercleanups" described the removed REST envelope; on the surviving door it isdata.persisted.cleanups.Judged accurate and left alone (listed because they name a symbol or literal this diff touched, not because they went false):
api/metadata-api.mdx(itsGET /packages"List installed packages",GET /packages/:id,DELETE /packages/:id,PATCH …/enable|disablesections describe the surviving door; itsPOST /packages/publishresponse is shown flat — pre-existing, untouched);api/client-sdk.mdx(theclient.packages.*snippet is the dispatcher door; itsVALIDATION_ERRORrow is the code's general meaning);api/environment-routing.mdx(env.packages.list()on the scoped client — the residue is a maintainer decision, stated in the changeset, not a docs edit);api/error-catalog.mdx,api/error-handling-client.mdx,api/error-handling-server.mdx,automation/webhooks.mdx,data-modeling/drivers.mdx,protocol/kernel/error-handling.mdx,protocol/objectql/types.mdx,ui/forms.mdx(anchored by theRESOURCE_NOT_FOUND/VALIDATION_ERRORliterals — vocabulary pages);automation/flows.mdx,protocol/objectui/actions.mdx,ui/actions.mdx(anchored byerrorMessage, a field name of theirs unrelated to the removed projection list);concepts/metadata-lifecycle.mdx,deployment/environment-variables.mdx,kernel/services-checklist.mdx(getMetaItems— the protocol verb is unchanged; the checklist's "packagesis answered kernel-side by the/packagesdispatcher domain" is now exactly true);automation/approvals.mdx,deployment/cli.mdx,permissions/capabilities.mdx,permissions/access-recipes.mdx,ui/audience-based-interfaces.mdx(manage_metadata— the write cohort is unchanged; none states the REST package read gate);permissions/authentication.mdx,plugins/packages.mdx(createRestApiPlugin— signature unchanged; the service-package blurb describes the service, not the route);data-modeling/formulas.mdx,deployment/publish-and-preview.mdx(/api/v1/cloud/packagesis a different surface),deployment/self-hosting.mdx,getting-started/examples.mdx(GET /api/v1/packageslists the installed set — true),permissions/record-view-auditing.mdx,plugins/adding-a-metadata-type.mdx,protocol/kernel/http-protocol.mdx(the discovery example's"packages": "/api/v1/packages"is what is advertised),ui/apps.mdx(anendpoint: '/api/v1/packages'example — served);permissions/system-context.mdx(row 54 already corrected in round 2).Release-owned, not touched (
content/docs/releases/):implementation-status.mdx(anchored byregisterDiscoveryEndpoints; its discovery sentence is generic and still true),v15.mdx(PATCH /packages/:idhistory),v16.mdx(manage_metadata),v17.mdx(the #7780allTenantsnote is about the protocol verb, still true). None reads false on this diff; nothing for a docs-only PR.The two declared blind spots, swept by hand: no hand-written page documents the
?version=repeated-parameter refusal on the package routes (query-multiplicity.tsyielded no anchor; the onlyrepeated query parammention isclient-sdk.mdx's generalVALIDATION_ERRORrow, which the/metaand/dataread points still honour); the package client methods appear only inclient-sdk.mdx(unchanged behaviour),metadata-api.mdx(packages.install) andenvironment-routing.mdx(above).Docs gates on the tree:
check:doc-anchors,check:docs-single-h1,check:doc-authoring,check-doc-route-spelling --advisory,check:docs-transcript-drift,check:docs-redirects,check:system-context-census,check:nul-bytes— exits in the report.Round 3b — the #13260 authz-probe blind-spot census (CI red on
40c7679eec, Dogfood Regression Gate 3/3)packages/qa/dogfood/test/authz-probe-blind-spot.census.tspinspopulation/reachable/blindSpotper probe file; the three ledger rows this PR removes moved thepackages/rest/src/rest-route-ledger.tsrow. The census has no generator, so all three numbers were re-derived from the tree with the module's ownderiveProbeFileCensus()(a tsx script over every row, diffed against the recorded table) — ⛔ not back-solved from the assertion: derivedpopulation 91, reachable 91, controlsroute: '91 /family: '91 /RestRouteLedgerEntry2.reachablemoved withpopulation(94 → 91 on both), because each removed row carriedfamily: 'packages'and was reached by that family's key; the blind spot stays 0, the 19 families are unchanged (the publish row keeps the family), andBLIND_SPOT_TOTAL_STATIC/_RUNTIME(75 / 80, summed over the route surfaces, not the ledger rows) are untouched. After the edit every one of the 13 rows derivessame. The row's note and the header's "94 audited rows" record the re-measurement. The aggregator's second red (dogfood-3-of-3 MISSING) is this shard's missing attestation, not a second cause.Acceptance notes
direct-mount-composition.tsandrest-api-plugin.ts(theprotocoloption only the removed routes read);rest-route-ledger.ts(the three rows);rest-server.ts(theroutes.packagesprojection was keyed on the removed list route);scripts/check-undeclared-dep-imports.mjs(stale row);content/docs/permissions/system-context.mdxandscripts/doc-authoring-prose-id.baseline.json(gate remedies); 8 REST pin files deleted, 13 re-pointed, 1 runtime pin added; the changeset.packages/rest/src/rest-server.ts(two measurement narratives onGET /api/v1/packages),packages/runtime/src/package-door-error-parity.test.ts(header says the registrar "mounts FIRST in the production stack").client.packagesdeclares envelopes two mounted surfaces do not emit:getdiverges between them, andinstall/enable/disabledeclare{ package }where the only serving surface sends the bare row #12034,pm:awaiting-maintainer) —packages/client/src/index.tsstill says only the REST registrar serves the scoped mount and declaresScopedEnvironmentClient.packages.getas{ package: InstalledPackage }with aversion?argument; both describe the removed door. That card holds the maintainer's 2026-08-26 ruling to bind the bare row; this PR is the producer-side convergence it was waiting on.packages/spec/distwas rewritten without declaration files (dist mtime inside thecheck:*batch window; not attributed to a specific gate), which reds any later typecheck in the same tree with TS7016 until the package is rebuilt. Recovered by rebuilding spec; recorded so the next tree does not read the TS7016 as its own.#12034 remains open— this PR does not close it.维护者速读(草稿)
改了什么:
/api/v1/packages的读与删三条路由只剩一套实现(runtime dispatcher 的/packagesdomain);@objectstack/rest只再挂POST /packages/publish。REST 那一套自带的source戳与?version=读取随之删除,并在 changeset 里记为有意去掉。REST/discovery改由 publish 挂载点推导routes.packages。为什么改:同一网址两套实现且行为已分歧(404 措辞、
data.package外壳、source戳),注释宣称的「遮蔽」在标准启动上从未发生(服务注册晚于 REST 挂载判定,三条路由根本没挂)。2026-09-02 裁决 A:删一套,不修优先级。风险与代价(含回滚):曾经真的走到 REST 那一套的部署(
package服务先于 REST 启动注册的宿主)会看到GET /packages/:id的响应从data.package变成data、404 措辞变化、?version=不再被读取;in-tree 的所有组合均不受影响(标准启动本来就是 dispatcher 在答)。SDK 的 scopedpackages.get(id, version?)声明的{ package }类型随之失真,归 #12034 那张已裁未合的卡。回滚 = revert 本 PR(纯删除 + 注释,无数据迁移)。席位意见:(留空)
你要做的:确认 changeset 的 minor 定级与「有意去掉」两项;决定 #12034 的 SDK 收口是否随本 PR 排队。
Generated by Claude Code