fix(rest, runtime): the dispatcher's /meta doors scope to the vetted organization, and its item read, book tree and list answer what RestServer answers (#20408) - #20473
Conversation
…s (red on base) The census cells the dispatcher answers differently from RestServer on origin/main b285508: the org source of the /meta doors, the item read's translation, doc locale, sortability, Vary and draft switches, the list's unknown-type refusal and ?preview= casing, the book-tree route, and the undetermined-posture cache header. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…ree, the unknown-type refusal, the mask's cache posture and the caller's organization RestServer's list, item and book-tree handlers now hand their answers to the shared functions, step for step. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…item chain, book tree, refusal, mask posture and vetted organization Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…he execution context Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…spatcher-meta-item-parity
…rity Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
… and the H0 double refuses unsupported where shapes Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 53 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 143 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 78322724546f1547336cd714f971d0e307668105 && git checkout 78322724546f1547336cd714f971d0e307668105
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8e028591857980ae69b9f9badb380dfa61367e62 172d03701c7652fc84f6087d7723453049cb965d && git checkout -B drift-repro 8e028591857980ae69b9f9badb380dfa61367e62 && git merge --no-ff 172d03701c7652fc84f6087d7723453049cb965d
node scripts/docs-audit/affected-docs.mjs --json 8e028591857980ae69b9f9badb380dfa61367e62
|
Contract reviewServed-tier: ① Derived judgmentsGate readings on the head, read at 16:35 UTC — 34 check-runs, newest per name: 28 (1) The organization source — a data-scope fix, and the dispatcher's new source is exactly
No (2)
(3) The published surface. Right. Runtime accept-set changes the diff implies — each right, each a census cell: ② Semver level
③ Boundary flags
Implemented-by: VERDICT: FAIL |
… /meta read seam The book-tree route's corpus projection and the doc locale collapse now live in packages/rest/src/meta-item-read-gate.ts (createMetaBookTreeAnswer, and resolveDocLocale on the list, item and tree paths), so doc/description, doc/translations and doc/tags name that file. Nothing else in the ledger moves. Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
…spatcher-meta-item-parity
Contract reviewServed-tier: ① Derived judgmentsGate readings on the head, read at 17:23 UTC — 34 check-runs, newest per name, every DELTA, the one required change (round-0 record DELTA, the merge of (1) The organization source: a data-scope fix, and the dispatcher's new source is exactly (2)
(3) The published surface. Right. Runtime accept-set changes the diff implies, unchanged from round 0, each right and each a census cell: ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ellings, as RestServer does (objectstack-ai#20478) (objectstack-ai#20505) Fixes objectstack-ai#20478 Clause-②: yes The runtime dispatcher now serves the layered view on both of its spellings: `GET /meta/:type/:name/layers`, and the deprecated `?layers=` flag on the item read. It gives the answer `RestServer` gives, as ruling B on objectstack-ai#20156 (`5856774816`, item 2) set it, through the shared seam. There is no second implementation: `RestServer`'s layered helper hands its read to the same chain the dispatcher calls. ## H0, measured first (base `45f428d8f`, a scratch probe through `dispatch()` against `RestServer`) | request | caller | dispatcher | `RestServer` | |:--|:--|:--|:--| | `GET /meta/app/crm?layers=true` | member | `200 {type, name, item}` (the plain read), `Vary` only, no `Deprecation` | `200 {type, name, code, overlay, overlayScope, effective, ...}`, `Deprecation: true` and a `Link` to `/api/v1/meta/app/crm/layers` | | `GET /meta/app/crm?layers=true` | author (`manage_metadata`) | the plain read, nav **pruned** to `nav_leads` | every layer **whole** (`nav_leads`, `nav_finance_ledger`) | | `GET /meta/app/crm/layers` | both | `404 ROUTE_NOT_FOUND` ("Route Not Found: /meta/app/crm/layers") | the layered answer | The per-caller pruning from objectstack-ai#20156 reproduced as the card describes it: the member is pruned on both layers on `RestServer`, and the author is served whole there. The dispatcher's plain read pruned the author too. ## What changed - **The seam** (`packages/rest/src/meta-item-read-gate.ts`) gains the layered chain, `createMetaLayeredAnswer`. Everything `RestServer`'s `serveMetaItemLayered` did after the store read moved there, unchanged: 1. THE per-caller gate on every present layer, `effective` first, under `STORED_VERSION_DOOR_POLICY`: whole for a caller the save door admits (`mayWriteItem`), pruned as the plain read prunes for everyone else. Every layer is judged before any is served. 2. The ADR-0106 mask on every layer through `projectMetaObjectSchema`, and `private, no-store` for an undetermined posture. The protocol's answer is no longer mutated in place; the chain returns a copy, and the bytes on the wire are unchanged. The flag's parse (`wantsMetaItemLayers`: any non-empty value) and its headers (`metaItemLayersDeprecationHeaders`: `Deprecation: true`, plus the `Link` to the successor when the transport knows the item's path) are shared too. - **`RestServer`** keeps its read in `serveMetaItemLayered`: the ingress refusal of a repeated `?package=`, its environment, and its own execution-context site. It now takes the organization from `metaReadGate.metaReadOrganizationId`, which gives the same value as before (the fold over the vetted `tenantId`). It then hands the read to the chain. The item handler's flag asks the shared parse and header helpers. - **The dispatcher** (`packages/runtime/src/domains/meta.ts`) serves both spellings: - `GET /meta/:type/:name/layers`: exactly three segments, like `/published`. It keeps the anonymous deny, as on `RestServer`. It resolves the mask posture before the capability probe, and answers `501 NOT_IMPLEMENTED` with no layered read. - `?layers=` on the item read: answered first, before either draft switch, as on `RestServer`. Where the protocol has no layered read, the flag is the plain read. - Both spellings go through `answerMetaLayered`: the read in the caller's vetted partition (`metaReadOrganizationId`) and `?package=` scope, the chain, and this transport's envelope. The flag's `Deprecation` and `Link` ride every answer, refusals included, because `RestServer` sets them before it reads. - `saveVerdict`, the `PUT` door's admission, moved up to the top of `handleMetadataRequest`, so the `/layers` branch asks that same function. It stays inside the same symbol, so the elevation-read census is unchanged. - `withHeaders` is `successWithHeaders` generalised to any `deps.*` answer. It is still one hand-built site, and `check:route-envelope` stays at `handBuilt: 2`. - **`@objectstack/rest` root exports** (widening, `minor`, `Clause-②: yes`): values `createMetaLayeredAnswer`, `wantsMetaItemLayers`, `metaItemLayersDeprecationHeaders`; types `MetaLayeredAnswer`, `MetaLayeredRequest`. `@objectstack/runtime` stays a `patch`. ## The hypotheses - **H0:** confirmed, as in the table above. - **H1: confirmed with one adjustment.** Every step after the read moved unchanged: the gate, the pruning per ruling B item 2, the mask and the cache posture. The `Deprecation` / `Link` pair moved as a shared helper. The read stays in each transport, for two reasons: - The `Link` path is `RestServer`-only state: its `metaPath`. The dispatcher's catch-all is handed a path with the host prefix stripped. The dispatcher therefore builds the `Link` from the request's own URL (`createHonoApp` hands `dispatch()` the raw Fetch `Request`). A host that passes no URL gets `Deprecation` alone. - A first cut put the read inside the seam as well. That removed one `this.resolveExecCtx(environmentId, req)` site from `rest-server.ts`, and the existing `execctx-consumer-census.test.ts` pins that site count at 66 sites and 90 mentions. The chain was reshaped to start after the store read, exactly like `createMetaItemAnswer`, so that test passes unedited. Both reads take their organization from `metaReadOrganizationId`. - **H2: confirmed.** `layers` left the census's `ITEM_PARAMS_NOT_SERVED_HERE`, and the constant is retired: no exclusion is left. `?layers=true` and `?layers=` are item probes, derived like every other parameter, and `/layers` has its own route census derived from `RestServer`'s handler plus `serveMetaItemLayered`. Every answer compares `Deprecation` and `Link` too. The ablations (below) each reddened exactly the layers cells, and each restore is proven. - **H3: confirmed.** The dispatcher's layered read asks `metaReadOrganizationId(type, executionContext)`, the vetted `tenantId`. The org-scope pins drive both spellings through the REAL identity resolution on both transports. The raw-claim ablation below reddens exactly the ex-member rows. ## Evidence - **Reverse verification.** The final tests were run against the base sources: the four source files were restored from the merge base `1c1b8c809` into the tree only, with blob equality to base shown per file. Census: `110 failed | 539 passed (649)`. The 110 are: - 25 item `?layers=true` cells; - 75 `/layers` cells; - 8 undetermined-posture layered cells; - 2 layered controls. Org-scope: `4 failed | 11 passed (15)`, the 4 layered rows. After the run, the files were restored with `git checkout HEAD --`: each blob equals `HEAD`'s, `git diff HEAD` is empty, and `git status --porcelain` is empty. The first reverse run, on an earlier head against base `45f428d8f` (the same four blobs), read the same numbers. - **Ablations** on head `79c967f586`, through `scripts/ablation-replace.mjs`. Each anchor hit once, each mutation landed with a blob change, and each was restored with blob == `HEAD` and an empty `git diff HEAD`. The subject is reached through relative imports and the runtime vitest alias to `packages/rest/src`, so no `dist` was involved. | ablation | predicted | measured | |:--|:--|:--| | (A) the dispatcher's `?layers=` branch skipped | the flag cells only | census `28 failed / 621 passed`: all 28 are `?layers=true` cells (25 item, 2 undetermined, 1 anonymous control). Org `2 failed`, the two `?layers=true` rows | | (B) the dispatcher's `/layers` branch removed | the route cells only | census `82 failed / 567 passed`: 75 route cells, 6 undetermined route cells, the 501 control. Org `2 failed`, the two `/layers` rows | | (C) the layered read's organization from the RAW session claim | the ex-member rows only | census `649 passed`. Org `2 failed`: exactly the two ex-member rows; both current-member controls stay green | - **Suites.** - `pnpm --filter @objectstack/rest exec vitest run --project local`: `218 passed` files, `3937 passed / 34 skipped`. `--project repo`: `8 passed`. Head `79c967f586`; `fef1c8e660` changes only runtime comments. **No REST test file is edited.** - `pnpm --filter @objectstack/runtime exec vitest run --project local`: `285 passed` files, `4164 passed / 1 skipped`. `--project repo`: `718 passed`. Head `fef1c8e660`. - `pnpm --filter @objectstack/rest --filter @objectstack/runtime typecheck`: exit 0, `check:test-typecheck` OK on both, after building the closure (`pnpm turbo run build --filter='@objectstack/runtime...'`). - **Consumers the dispatcher's wire change reaches:** - `@objectstack/hono`: 5 files, 122 tests; - `@objectstack/http-conformance`: 8 files, 102 tests; - six dogfood files: 39 tests, all green (`showcase-object-extension-meta-read`, `showcase-object-extension-scalar-divergence`, `multi-package-artifact`, `meta-published-and-state-routes`, `route-ledger-live-mount-parity`, `dashboard-designer-roundtrip`). - **Gates, on head `fef1c8e660`, after merging `origin/main` at `1c1b8c809`:** - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 62 commands derived, every one run on this head. `--ran`: `62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3)`. - `check:dual-build-cjs-loads` first needed 38 missing `dist/`s built. - `check:type-check-debt` first answered `PREREQUISITE NOT MET` (exit 3) because the reverse-verification restore left `packages/rest` sources newer than its `dist`. It was re-run after `pnpm --filter @objectstack/rest build`: exit 0. - `pnpm lint` (`eslint . --no-inline-config`, the whole repo): exit 0. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. A first run named two moved comments whose cited cards are gone from the board (`objectstack-ai#10340`, `objectstack-ai#12195`); those comments were reworded. **Declared narrowing — verification ran UNLOCKED.** `scripts/pm/os-verify-lock.sh` could not take the shared verify lock on this host: no usable `flock`. The shared verify lock is declared Linux-only (`flock` is util-linux, and a stock macOS does not ship it), so the command below was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for this run, nor for any sibling agent in this container while it ran. pnpm turbo run build (the runtime closure, the rest and runtime packages, 38 packages for check:dual-build-cjs-loads), pnpm --filter @objectstack/rest test local / repo, pnpm --filter @objectstack/runtime test local / repo, pnpm --filter @objectstack/rest --filter @objectstack/runtime typecheck, pnpm lint, pnpm --filter @objectstack/hono --filter @objectstack/http-conformance test, the dogfood file run, the census and org-scope pin runs, the reverse verification and the three ablations ## Acceptance notes - **Out of scope, measured (class b, ADR-0045 §3): the layered view is an existence oracle for an unpublished app.** Measured as a member through `RestServer`'s route handlers: - `GET /meta/app/launchpad` (an unpublished app) answers `404 RESOURCE_NOT_FOUND` on the plain read, on `/layers` and on `?layers=true`; - `GET /meta/app/no_such_app` answers `404` on the plain read, but `200 {code: null, overlay: null, effective: null, ...}` on `/layers` and on `?layers=true`. So a non-builder can tell that an unpublished app exists, which ADR-0045 §3 rules "externally unobservable". This PR carries `RestServer`'s answer onto the dispatcher, as the triage direction requires. The dispatcher answered `404` / the plain read to both names before, so it now shares the oracle. The fix belongs in `createMetaLayeredAnswer`, one place for both transports. It changes `RestServer`'s reference answer for an absent name, so it is left for its own card. `Seam: spec:GetMetaItemLayeredResponseSchema → runtime:createMetaLayeredAnswer (packages/rest/src/meta-item-read-gate.ts)`. - **Out of scope, measured (class a): `RestServer`'s scoped `?layers=` `Link` names the route TEMPLATE.** With `enableProjectScoping`, `GET /api/v1/environments/env_1/meta/view/lead_all?layers=true` answers a `Link` naming `/api/v1/environments/:environmentId/meta/view/lead_all/layers`, with the literal `:environmentId`. The dispatcher builds its `Link` from the request's URL, so it names the real path; the census drives the unscoped mount, where the two are byte-equal. - **A transport difference kept on purpose:** a host that hands `dispatch()` a request with no URL gets `Deprecation` without a `Link`. The docblock of `requestedItemPath` says why. - **A stale note, not a count:** `scripts/check-route-envelope.mjs`'s `meta.ts` ledger note still describes the second hand-built site as "the /meta/:type list answer". It is now `withHeaders`, which `successWithHeaders` delegates to. The count (2) holds and the gate is green. The script is not in this claim; its next editor carries it. - **Repeated query parameters are still unchanged here**, as PR objectstack-ai#20473 recorded: `RestServer` refuses `?package=a&package=b` on the layered read. The dispatcher has no such gate, and Hono's catch-all keeps the last value. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20408
Clause-②: yes
The runtime dispatcher's
/metadoors now answer whatRestServer's answer, for the item read, the book-tree route and the list, and they scope a caller to the same organization. A host that mounts only the${prefix}/*catch-all serves/metathrough the dispatcher:createHonoApp, or any adapter written on the publicHttpDispatcherAPI. ADR-0076 item 9 keeps that catch-all as the fallback. Triage's direction was to extend the shared seam (AGENTS.md 〈Route & surface ownership〉 rule 1: one implementation, two transports). This PR extends the seam PR #20404 built inpackages/rest/src/meta-item-read-gate.ts, and builds no second one.First: the organization source was a cross-organization data-scope defect (H0, measured)
The card's first read-at-source item. Triage said a cross-org difference outranks the six rows, and it does differ.
/metadoor took its organization fromdeps.resolveActiveOrganizationId, which returns the auth session'sactiveOrganizationIdunchanged.RestServerreads the vetted value. It readsctx.tenantIdoff the execution context.resolveAuthzContextvets that value: under a wall-enforcing posture, it DROPS a claim naming an organization the caller no longer belongs to.Measured through
dispatch()againstRestServer. Both run the REAL identity resolution (resolveExecutionContext/computeExecCtx→resolveAuthzContext) under anisolatedposture. The subject isu_exmember: the session is stampedorg_alpha, and the onlysys_memberrow isorg_beta. Both principals hold one shared permission set, so only the organization claim separates the arms. Onb28550818:RestServerGET /meta/view/lead_allAlpha pipeline(org_alpha's overlay)All leads(env-wide)GET /meta/viewAlpha pipelineAll leadsGET /meta/view/lead_all?preview=draftAlpha pipelineAll leadsGET /meta/view/lead_all/publishedAlpha pipelineAll leadsGET /meta/view/lead_all?state=draft200, org_alpha's pending draft404 NO_DRAFTGET /meta/_drafts['alpha_board']['env_board']PUT /meta/view/lead_all(manage_metadata)org_alphaThe last row is a WRITE into the left organization's partition.
Controls, green on both transports: a current member reads its own organization, the double gates a non-overridable type's phantom row, and the ex-member switched to
org_betareadsorg_beta.The fix, in the seam:
metaCallerOrganizationId(caller)answers the vettedtenantId.metaReadOrganizationId(type, caller)answersorganizationIdForMetaReadover the folded type and that value.RestServer's list and item reads ask the second, and so does every dispatcher/metaread.PUT,_draftsand/publishedtake the first. That is whatRestServer's twins hand down (ctx.tenantId).meta.tsno longer callsdeps.resolveActiveOrganizationId.Pinned in
packages/runtime/src/domains/meta-read-org-scope-parity.test.ts: 11 tests, 7 red at the base and all green on the fix.The write door is a bounded in-place fix. The read doors are the card's scope; the
PUTrow goes beyond it, and all four conditions hold:meta.tsis this claim's file;The pin's
PUTrow is its evidence:['org_alpha']against[undefined]at the base, equal on the fix.The six rows, and what the census found beside them
Each row was re-measured first, and every one still reproduced on
b28550818. The census inmeta-list-projection-parity.test.tsnow has item, book-tree and cache-posture blocks. Like the list block, each is derived fromRestServer's handler: the query parameters it reads and the type literals it keys on, plus the shared functions it calls.GET /meta/totally_invented_type)200 []against400 INVALID_REQUEST?preview=DRAFT404against200) and 4 item body cellstranslationsmap kept, no collapse)GET /meta/book/:name/tree404 ROUTE_NOT_FOUNDagainst200/403;401against200for an anonymous reader of thepublicbook)?preview=draftCache-Controlon an undetermined postureVary: Accept-LanguagesortabilityThe last two rows are same-family divergences the item census found that the card does not list. The dispatcher's item answer carried no
Vary, and an object schema came with nosortability(#10235). The item chain closes both by construction.H1 was measured, and it differed: the list, the item read,
/publishedand the legacy one-segment object read all served an undetermined posture's unmasked schema with noCache-Control.What changed
The item read is one chain,
createMetaItemAnswer. EverythingRestServer'sGET /meta/:type/:namedoes after the store read moved there, unchanged:itemrequired and the rest pin declares a 404 #18066, before the gate);translateMetaEnvelope: the translation, andsortabilitybeside an object schema.RestServer's uncached arm calls the chain. So does every exit of the dispatcher's item read: the object branch, the generic branch, theMetadataServicefallback and the?state=draftread.RestServer.translateMetaItemandtranslateMetaEnvelopedelegate to the newtranslateMetaDocumentandtranslateMetaEnvelope. The cached arm keeps calling them.Row 6: an admitted
?preview=draftmakes the dispatcher's object branch ask the protocol first, as a scoped kernel always did. That protocol read now carries the requestRestServersends:?package=and the switch.Row 2: both
?preview=declarations inmeta.tsparse the value case-insensitively, asRestServer's do. The draft-door ledger inmeta-draft-read-builder-gate.test.tsnow names the new spelling.Row 1:
refuseUnknownMetaListTypemoved into the seam, unchanged, docblock included.RestServer's private method is a one-line delegate. The dispatcher's list branch asks it before any listing work. It fails open when the live type listing cannot be read, so a host with nogetMetaTypeskeeps the legacy one-segment object read.Row 5: the tree route's whole handler moved into
createMetaBookTreeAnswer: the reads, THEDocsAudience, the §6.7 gate, the locale collapse and the narrowed tree. The dispatcher servesGET /meta/book/:name/tree, with the type segment literal as onRestServer, andmetaReadRouteOfnames itbook-treefor the sharedisPublicAudienceRead.RestServer's two tree-only delegates (audienceBooksOf,resolveDocsAudience) went with it.H1: the list chain applies the object mask itself.
MetaListAnswerSources.maskObjectsbecameresolveObjectMasker. That port is new in this same release, withcreateMetaListAnswer.projectMetaObjectSchemaprojects each schema, so both transports make one decision.MetaListAnswerreportscacheControl, andRestServer's list writes it from the answer; its port used to write it./published, the legacy read) use the same projection. One helper inmeta.ts(successWithHeaders) carries the headers, socheck:route-envelope'shandBuilt: 2is unchanged.RestServer's answers are unchanged: every existing REST test passes unedited.Runtime pins that moved, and why:
/meta/book/public_guide/tree, now/meta/books/public_guide/tree: the singular spelling is a route here now.?preview=site spelling.meta-write-org-scope.test.tsandmeta-save-capability-gate.test.ts. Their execution context carried notenantIdwhile their auth session named an organization. That pairing is exactly the "dropped claim" state, which only the old raw-claim source read as org-scoped. Each now hands the organization on the execution context, as the real resolver does with no wall. 11 cases went red, and none of their assertions changed.Evidence
Red first. The census and pins at
410141ec34, on base sources: census259 failed | 247 passed (506), H07 failed | 4 passed (11).Reverse verification. The final tests, run against the BASE sources (the four source files restored from
b28550818into the tree only):267 failed | 263 passed (530)across the census, H0 and ledger files. Restored withgit checkout HEAD --, with proof: each blob equals HEAD's, andgit diff HEADis empty.Ablations at
7903048a75go throughscripts/ablation-replace.mjs: the anchor hit once, and each mutation landed and was restored with blob == HEAD3519d199a54cand an emptygit diff HEAD. The subject resolves through relative imports and the runtime vitest alias topackages/rest/src, so nodistwas involved. Suite: the census plus the H0 pins, 517 tests.refuseUnknownMetaListType11 failed | 506 passed: the 11totally_invented_typetests, nothing elsetranslateMetaEnvelopesortabilitycells40 failed | 477 passed:object/objectsinvoice 16 each, and the zh-CN cells ofapp/crm,apps/crm,app/helpdesk,page/home2 failed | 515 passed: the item read and its?preview=draftrowGates, all on head
cf85a44ad5(after mergingorigin/mainate956924e1)pnpm --filter @objectstack/rest test: 216 files passed;3912 passed | 26 skipped.test:repo: 1 file,8 passed.pnpm --filter @objectstack/runtime test: 284 files passed;4084 passed | 1 skipped.test:repo: 3 files,575 passed, the census included.pnpm --filter @objectstack/rest --filter @objectstack/runtime typecheck: exit 0, andcheck:test-typecheckis OK on both.pnpm lint(eslint . --no-inline-config, the whole repo): exit 0.node scripts/check-issue-citations.mjs --base origin/main: exit 0. The first read was unauthenticated and answered403/ exit 3 (PREREQUISITE NOT MET), so it was re-run with an authenticated read.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands: 62 commands derived, every one run on this head, all exit 0.check:dual-build-cjs-loadsfirst needed 8 missingdist/s built.--rananswers62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero …).@objectstack/hono5 files / 122 tests;@objectstack/http-conformance8 files / 102;@objectstack/clientclient.hono,client-url-conformanceandmeta-delete-item-carriers, 27; and six/metadogfood files, 72 (meta-published-and-state-routes,route-ledger-live-mount-parity,showcase-anonymous-deny-surfaces,showcase-object-extension-meta-read,dashboard-designer-roundtrip,meta-types-create-seed). All green.Declared narrowing — verification ran UNLOCKED.
scripts/pm/os-verify-lock.shcould not take the shared verify lock on this host: no usable
flock. The sharedverify lock is declared Linux-only (
flockis util-linux, and a stock macOS doesnot ship it), so the command below was run directly, without the lock —
a declared narrowing, not a silent one. No serialization guarantee held for this
run, nor for any sibling agent in this container while it ran.
Acceptance notes
?layers=on the dispatcher's item read.GET /meta/app/crm?layers=truethroughdispatch()answers200 {type, name, item}, the plain read.RestServeranswers the three-layer{type, name, code, overlay, effective}withDeprecation: true. The dispatcher serves no layered view at all:/meta/app/crm/layersanswers a located404 ROUTE_NOT_FOUND, which is loud. The flag, though, is silently a different representation. The item census nameslayersas its one declared exclusion (ITEM_PARAMS_NOT_SERVED_HERE), so every other new parameter still reddens it.domains/packages.tscallsdeps.resolveActiveOrganizationIdat 9 sites (publish-drafts, commits, uninstall, revert, duplicate-adopt, the export sweep). Not measured here. ReadingexecutionContext.tenantIdinHttpDispatcher.resolveActiveOrganizationIditself would close the class for every domain. That ishttp-dispatcher.ts, outside this claim.?package=asRestServer's does. The census double does not discriminate packages on item reads, so no cell moves on it.scripts/check-route-envelope.mjs'smeta.tsledger note still describes the second hand-built site as "the /meta/:type list answer". It is nowsuccessWithHeaders, which every/metaread answer that owes a header goes through. The count (2) holds, and the gate is green. The script is not in this claim; its next editor carries it.RestServer's item, list and tree handlers refuse a repeated single-valued parameter (refuseRepeatedQueryParams), and the dispatcher's/metadomain has no such gate. PR fix(rest, runtime): the dispatcher's /meta reads answer what RestServer's answer (#20320) #20404 recorded the list half.Generated by Claude Code