Commit 9449512
Fixes #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 #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 #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 (`#10340`, `#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 #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>
1 parent dc07593 commit 9449512
7 files changed
Lines changed: 776 additions & 138 deletions
File tree
- .changeset
- packages
- rest/src
- runtime/src/domains
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
109 | 116 | | |
110 | 117 | | |
111 | 118 | | |
112 | 119 | | |
| 120 | + | |
113 | 121 | | |
114 | 122 | | |
115 | 123 | | |
116 | 124 | | |
| 125 | + | |
117 | 126 | | |
118 | 127 | | |
119 | 128 | | |
120 | 129 | | |
121 | 130 | | |
122 | 131 | | |
123 | 132 | | |
| 133 | + | |
124 | 134 | | |
125 | 135 | | |
126 | 136 | | |
| |||
131 | 141 | | |
132 | 142 | | |
133 | 143 | | |
| 144 | + | |
| 145 | + | |
134 | 146 | | |
135 | 147 | | |
136 | 148 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
52 | 55 | | |
53 | 56 | | |
54 | 57 | | |
| |||
2448 | 2451 | | |
2449 | 2452 | | |
2450 | 2453 | | |
| 2454 | + | |
| 2455 | + | |
| 2456 | + | |
| 2457 | + | |
| 2458 | + | |
| 2459 | + | |
| 2460 | + | |
| 2461 | + | |
| 2462 | + | |
| 2463 | + | |
| 2464 | + | |
| 2465 | + | |
| 2466 | + | |
| 2467 | + | |
| 2468 | + | |
| 2469 | + | |
| 2470 | + | |
| 2471 | + | |
| 2472 | + | |
| 2473 | + | |
| 2474 | + | |
| 2475 | + | |
| 2476 | + | |
| 2477 | + | |
| 2478 | + | |
| 2479 | + | |
| 2480 | + | |
| 2481 | + | |
| 2482 | + | |
| 2483 | + | |
| 2484 | + | |
| 2485 | + | |
| 2486 | + | |
| 2487 | + | |
| 2488 | + | |
| 2489 | + | |
| 2490 | + | |
| 2491 | + | |
| 2492 | + | |
| 2493 | + | |
| 2494 | + | |
| 2495 | + | |
| 2496 | + | |
| 2497 | + | |
| 2498 | + | |
| 2499 | + | |
| 2500 | + | |
| 2501 | + | |
| 2502 | + | |
| 2503 | + | |
| 2504 | + | |
| 2505 | + | |
| 2506 | + | |
| 2507 | + | |
| 2508 | + | |
| 2509 | + | |
| 2510 | + | |
| 2511 | + | |
| 2512 | + | |
| 2513 | + | |
| 2514 | + | |
| 2515 | + | |
| 2516 | + | |
| 2517 | + | |
| 2518 | + | |
| 2519 | + | |
| 2520 | + | |
| 2521 | + | |
| 2522 | + | |
| 2523 | + | |
| 2524 | + | |
| 2525 | + | |
| 2526 | + | |
| 2527 | + | |
| 2528 | + | |
| 2529 | + | |
| 2530 | + | |
| 2531 | + | |
| 2532 | + | |
| 2533 | + | |
| 2534 | + | |
| 2535 | + | |
| 2536 | + | |
| 2537 | + | |
| 2538 | + | |
| 2539 | + | |
| 2540 | + | |
| 2541 | + | |
| 2542 | + | |
| 2543 | + | |
| 2544 | + | |
| 2545 | + | |
| 2546 | + | |
| 2547 | + | |
| 2548 | + | |
| 2549 | + | |
| 2550 | + | |
| 2551 | + | |
| 2552 | + | |
| 2553 | + | |
| 2554 | + | |
| 2555 | + | |
| 2556 | + | |
| 2557 | + | |
| 2558 | + | |
| 2559 | + | |
| 2560 | + | |
| 2561 | + | |
| 2562 | + | |
| 2563 | + | |
| 2564 | + | |
| 2565 | + | |
| 2566 | + | |
| 2567 | + | |
| 2568 | + | |
| 2569 | + | |
| 2570 | + | |
| 2571 | + | |
| 2572 | + | |
| 2573 | + | |
| 2574 | + | |
| 2575 | + | |
| 2576 | + | |
| 2577 | + | |
| 2578 | + | |
| 2579 | + | |
| 2580 | + | |
| 2581 | + | |
| 2582 | + | |
| 2583 | + | |
| 2584 | + | |
| 2585 | + | |
| 2586 | + | |
| 2587 | + | |
| 2588 | + | |
| 2589 | + | |
| 2590 | + | |
| 2591 | + | |
| 2592 | + | |
| 2593 | + | |
| 2594 | + | |
| 2595 | + | |
| 2596 | + | |
| 2597 | + | |
| 2598 | + | |
| 2599 | + | |
| 2600 | + | |
| 2601 | + | |
| 2602 | + | |
| 2603 | + | |
| 2604 | + | |
| 2605 | + | |
| 2606 | + | |
| 2607 | + | |
| 2608 | + | |
| 2609 | + | |
| 2610 | + | |
| 2611 | + | |
| 2612 | + | |
| 2613 | + | |
| 2614 | + | |
| 2615 | + | |
| 2616 | + | |
| 2617 | + | |
| 2618 | + | |
| 2619 | + | |
| 2620 | + | |
| 2621 | + | |
| 2622 | + | |
| 2623 | + | |
| 2624 | + | |
| 2625 | + | |
| 2626 | + | |
| 2627 | + | |
2451 | 2628 | | |
2452 | 2629 | | |
2453 | 2630 | | |
| |||
0 commit comments