Repository navigation
Commit 95f729a
Fixes #20320
Clause-②: yes (widening)
The runtime dispatcher's `/meta` reads now give the same answers as
`RestServer`'s. A host that mounts only the `${prefix}/*` catch-all
(`createHonoApp`, or any adapter on the public `HttpDispatcher` API)
serves `/meta` through the dispatcher. Triage's direction was to extend
the shared seam (AGENTS.md 〈Route & surface ownership〉 rule 1: one
implementation, two transports). ADR-0076 item 9 keeps the catch-all as
the fallback.
## What changed
**Row A: one list chain.** Everything `RestServer`'s `GET /meta/:type`
does to a list after the store read moved, unchanged, into
`createMetaListAnswer` in `packages/rest/src/meta-item-read-gate.ts`,
beside `createMetaListReadGate`. The steps, in `RestServer`'s order:
1. the #5224 `api` served-set face;
2. the per-caller list gate;
3. `?id=` for apps;
4. `?object=` for views;
5. the ADR-0046 doc locale collapse;
6. the doc content slim;
7. the transport's own ADR-0106 object mask (a port);
8. the translation.
`RestServer`'s handler keeps its entry: the repeated-parameter refusal,
the unknown-type refusal and the admitted `previewDrafts` declaration
from #20338, still ONE declaration ahead of every read of it. It then
calls the chain. Every exit of the dispatcher's list branch (protocol,
`MetadataService`, ObjectQL registry) calls the same chain. The
dispatcher's own `slimDocList` is gone. Its list answer now carries
`Vary: Accept-Language`, as `RestServer`'s does.
The chain needs the locale parse and the translation helpers, so those
moved too. `RestServer`'s private methods `extractLocale`,
`buildTranslationBundle`, `translateOptionsFor`, `packagedObjectBase`,
`translateMetaItems` and the module-level `isTranslatableMetaType` are
now one-line delegates to them. Nothing in `meta.ts` is a copy.
**Row B: one public-audience predicate.** `isPublicAudienceRead` moved
out of `RestServer`. It now takes a method, a route shape and the raw
`:type`. `RestServer`'s static maps its registered path onto a shape and
delegates. The dispatcher's anonymous gate at `handleMetadataRequest`'s
entry asks the same predicate with its own shapes: `list` for one
segment, `item` for two. The dispatcher has no `/book/:name/tree` route;
that path falls to the located `ROUTE_NOT_FOUND` tail. So the dispatcher
never names `book-tree`, and that path, like `/published`, keeps the
deny. The legacy one-segment object-name exit answers the anonymous
deny, so the exemption can never reach an object schema.
**The `?state=draft` row.** The dispatcher's item read declares
`?state=draft` next to `?preview=draft`, each with its
`mayReadPendingDrafts` admission, above every branch. It parses the
parameter as `RestServer` does. An admitted caller gets the protocol's
draft read for any type, the object branch included:
- `404 NO_DRAFT` answered as itself when nothing is pending;
- otherwise the draft through the per-caller gate under
`STORED_VERSION_DOOR_POLICY`. The constant moved to the shared module;
`RestServer` keeps its static name as an alias.
- `mayWriteItem` comes from the dispatcher's own save-door admission.
That admission is now spelled once, as a local in
`handleMetadataRequest`, and the `PUT` branch uses it too.
A caller the predicate does not admit gets the plain read, byte for
byte.
**Add-on.** The cached arm's `[#9741] Typed request` comment now says
the branch is unreachable for an ADMITTED switch. The query parameter
itself still reaches it.
**Gate ledger.** `scripts/check-route-envelope.mjs` declares `meta.ts`
`handBuilt: 2` (was 1), with a note. The dispatcher's list answer must
carry `Vary: Accept-Language`, and `deps.success` takes no headers.
## What a dispatcher-only host answered before (measured on `b1cbd9277`)
The census drives `dispatch()` and `RestServer` over the same fixtures:
- 18 type cells × 10 parameter probes × 4 callers = 720 list cells;
- anonymous public book and doc reads, with controls;
- the `?state=draft` cells.
List cells, by cause, before the fix:
| cause | cells |
|:--|--:|
| the same answer | 140 |
| `Vary` header only | 374 |
| items differ | 166 |
| status differs (anonymous `401` vs `200`) | 40 |
- `?id=crm` listed every visible app. `?object=lead` listed every view.
- `/meta/docs` served bodies. Every doc list kept `translations`, with
no locale collapse.
- Translatable lists were untranslated.
- `/meta/api` listed the unserved declaration.
- Anonymous `public` book and doc lists and items answered `401`.
- Admitted `?state=draft` answered the active item: 10 cells, builder
and author.
## Evidence
- **Red first.** The census file at the base: `198 failed | 13 passed
(211)`. On the fix: `211 passed (211)`.
- **REST unchanged.** `pnpm --filter @objectstack/rest test`: `211
passed` files, `3831 passed | 2 skipped` tests, with every existing REST
test unedited. `test:repo`: `8 passed`.
- **Runtime.** `pnpm --filter @objectstack/runtime test`: `283 passed`
files, `4073 passed | 1 skipped` tests. `test:repo`: `280 passed`, the
census included.
- **Runtime pins that moved, and why:**
- the list parity's anonymous test and the item parity's anonymous test:
book and doc reads now reach the §6.7 gate, as on REST, and every other
type keeps the deny;
- the draft-door ledger gains the `?state=draft` site.
- **Ablations.** Each was committed first, mutated through
`scripts/ablation-replace.mjs`, and restored with the restore proven:
blob equal to HEAD, empty `git diff HEAD`, zero markers left. The
subject resolves through the runtime vitest alias to
`packages/rest/src`, so no `dist` was involved. The suites run: the
census and `meta-list-read-gate-parity.test.ts`, 251 tests.
- **(A) The shared chain as a no-op: `19 failed`.** The direction was
NOT the predicted one. The 720 parity cells stayed green, because both
transports call the one chain and move together; that is what one
implementation means. The reference pins went red: the reference-moves
pin, 4 row-B list cells, and 14 #20237 reference and gate cells. The
first attempt was refused by the tool (its replacement contained its
anchor) and ran nothing; the second attempt is the measurement.
- **(A2) The dispatcher bypassing the chain's query and translation: `27
failed`.** 20 census cells went red (`?id=` 4, `?object=` 4,
`?include=content` 2, translation 10), plus 7 #20237 cells. This is the
ablation that shows the parity cells can fail.
- **(B) The dispatcher skipping `isPublicAudienceRead`: `49 failed`.**
40 anonymous book and doc census cells, 8 row-B cells and 1 #20237 cell
went red. The `401` controls stayed green.
## Gates, all on head `64a05bb97` (after merging `origin/main` at
`15bf186f5`)
- **`pnpm --filter @objectstack/rest test`:** 213 files passed; `3886
passed | 26 skipped`. `test:repo`: 1 file, `8 passed`.
- **`pnpm --filter @objectstack/runtime test`:** 283 files passed; `4073
passed | 1 skipped`. `test:repo`: 3 files, `280 passed`.
- **`pnpm --filter @objectstack/rest typecheck` and `pnpm --filter
@objectstack/runtime typecheck`:** exit 0, `check:test-typecheck` OK on
both.
- **`pnpm lint`:** exit 0, the whole repo, 133s.
- **`node scripts/check-issue-citations.mjs --base origin/main`:** exit
0.
- **`node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands`:** 81 commands derived. 80 were
run on this head, each exit recorded, and all exit 0 except:
- `pnpm check:dual-build-cjs-loads`, exit 3: **NOT MEASURED**,
`PREREQUISITE NOT MET`, because 36 packages have no `dist/` in this
worktree.
- `pnpm check:pm-dispatch-gates`, exit 124: **NOT MEASURED**, because
its self-test alone ran past a 580s foreground cap. The diff touches no
`scripts/pm/` file; the family comes from the `scripts/` path of
`check-route-envelope.mjs`.
- `pnpm check:dts-closure` first answered 1 on tree state: `client`,
`organizations` and `verify` had a `dist/` without `.d.ts`, and this
diff touches none of them. After rebuilding those three it answers 0;
that is the recorded reading.
- **Reconciliation:** `dispatch-gates --ran` answers `81 derived
famil(ies) accounted for — 79 run, 2 NOT-MEASURED` (the two named
above), exit 0.
- **`check-route-envelope.mjs` edited:** the script has no test file of
its own, and no test executes it. Its `--self-test` passes inside `pnpm
check:route-envelope`.
## Acceptance notes
- **Out of scope; the census found these and they are reported, not
fixed.** The same family: the dispatcher's `/meta` answering differently
from `RestServer`. Each is measured through `dispatch()` on the fixtures
above.
1. `GET /meta/totally_invented_type` answers `200 []` where `RestServer`
answers `400 INVALID_REQUEST` (the #9488 refusal is REST-only).
2. `?preview=DRAFT` from a builder: the dispatcher compares
case-sensitively and lists the published world, where `RestServer`
overlays the drafts.
3. The item read does not translate: `GET /meta/app/crm` with
`Accept-Language: zh-CN` answers `CRM` against `客户管理`.
4. The item read does not collapse a doc's locale: it keeps
`translations`, where REST answers `入门`.
5. There is no `/meta/book/:name/tree` route: `404 ROUTE_NOT_FOUND` to
an authenticated caller, where `RestServer` serves the tree.
6. The object branch ignores `?preview=draft`: a builder reads
`Invoice`, where REST answers `Invoice (draft)`.
- **Not measured; read at source only.**
- The dispatcher's list and item reads thread
`resolveActiveOrganizationId` for every type. `RestServer` threads
`organizationIdForMetaRead` over the folded type, and only for
org-overridable types.
- The dispatcher's object mask sets no `Cache-Control: private,
no-store` on an undetermined posture. `RestServer`'s list does.
- **Repeated parameters.** `RestServer` refuses a repeated `?id=` with
`400`. The Hono catch-all flattens the query to last-wins before
`dispatch()`, so the dispatcher never sees a repeat. That is the
adapter's seam, not this domain's.
- **Docs drift, not filed.**
`content/docs/permissions/system-context.mdx` row 49 lists `/layers`,
`?layers=true` and `/diff` as the doors where a caller the save verdict
admits reads an app's full stored version. It does not list
`?state=draft`: not on REST since #20290, and not on the dispatcher
after this PR.
- **Public API.** `@objectstack/rest` gains the exports
`createMetaListAnswer`, `translateMetaList`, `metaRequestLocale`,
`isPublicAudienceRead`, `STORED_VERSION_DOOR_POLICY` and their types.
Nothing is removed. That is a widening of its published surface, so the
changeset declares `@objectstack/rest` `minor` with `Clause-②: yes
(widening)`; `@objectstack/runtime` stays `patch`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01UYBdGBzWSrAMzpW8ah3GbP)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7d63088 commit 95f729a
11 files changed
Lines changed: 1693 additions & 527 deletions
File tree
- .changeset
- packages
- rest/src
- runtime
- src/domains
- scripts
| 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 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
86 | 86 | | |
87 | 87 | | |
88 | 88 | | |
89 | | - | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
90 | 107 | | |
91 | 108 | | |
92 | 109 | | |
93 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
94 | 116 | | |
95 | 117 | | |
| 118 | + | |
96 | 119 | | |
0 commit comments