Skip to content

Commit ec44cf6

Browse files
refactor(rest): move registerDiscoveryEndpoints and registerOpenApiEndpoints into discovery-routes.ts (#23050 slice 7) (#23763)
Part of #23050 Clause-②: no Slice 7 of the `rest-server.ts` split, by slice 1's pattern (landed note `6136102437`, steps 1–6), slice 2's getter refinement (`6189011700`) and slices 3–6's shape (PR #23670, PR #23705 and PR #23729's two registrars in one module, PR #23719): the bodies of `RestServer#registerDiscoveryEndpoints` (`GET {basePath}` and `GET {basePath}/discovery`) and `RestServer#registerOpenApiEndpoints` (`GET {basePath}/openapi.json` and its `GET {basePath}/docs` viewer) move to ONE new module, `packages/rest/src/discovery-routes.ts`. Routes, the ordered registrations, the published `dist/*.d.ts` and both bundles' export lists are byte-identical, and every reader whose reading included a moved line now reads it where it is, with its sum unchanged. **One deviation, named up front:** one token of the OpenAPI body is not byte-identical. The body called the class's PRIVATE STATIC `overlayDocumentationInfo` as `RestServer.overlayDocumentationInfo(…)`, a spelling that does not compile outside the class. It now reaches the static through the host, as `this.overlayDocumentationInfo(…)`. So the `openapi.json` handler's digest differs from base, by that receiver alone (measured below). The seat is asked to confirm the shape (see the end of this body). `rest-server.ts` goes from 10,636 to 10,090 lines (base `3b46855fb9`). The card stays open for the later slices. ## The pattern, applied **Members the moved bodies read**, from an AST walk of each method (TypeScript compiler API, symbol-resolved, base `3b46855fb9`): `registerDiscoveryEndpoints` (JSDoc `:4910`, method `:4913`-`:5236`, body `:4914`-`:5235`) and `registerOpenApiEndpoints` (JSDoc `:5277`, method `:5323`-`:5591`, body `:5324`-`:5590`). | kind | member | reads (discovery / OpenAPI) | class declaration | |---|---|---|---| | field | `config` | 9 / 1, all inside the handlers | `:2011`, private, not `readonly` | | field | `routeManager` | 2 / 2, the `register` calls | `:2012`, private | | method | `resolveProtocol` | 1 / 1 | `:2390`, private | | method | `probeMcpServeable` | 1 / 0 | `:4886`, private | | method | `getDirectMountRouteBases` | 1 / 0 | `:10534` | | method | `getMountedEmailRouteBase` | 1 / 0 | `:10596` | | method | `resolveExecCtx` | 0 / 2 | `:2858`, private | | method | `enforceAuth` | 0 / 2 | `:2458`, private | | method | `loadOpenApiSpec` | 0 / 1 | `:5599`, private | | method | `getRoutes` | 0 / 1 | `:10630` | | method | `resolveEndpointMatchAuthority` | 0 / 1 | `:2193`, private | | method | `notifyMissingEndpointAuthority` | 0 / 1 | `:2269`, private | | static method | `overlayDocumentationInfo`, read as `RestServer.overlayDocumentationInfo` | 0 / 1 | `:5262`, private static | Neither body has a bare `this`, an element access on `this`, or a `this`-rebinding `function`. Their module-level reads: - **Imports still used elsewhere in `rest-server.ts`** (AST count outside both methods): `handleRouteError` (19) and `logError` (7). They stay there; the module imports the same names from the same modules. - **Imports ONLY these bodies read** (zero uses outside the two methods): `isMcpServerEnabled` (`@objectstack/types`), `enrichOpenApiWithEndpoints` (`./openapi-endpoints.js`), `buildBuiltinPaths` (`./openapi-builtin-paths.js`), `selectServedEndpoints` (`./served-endpoints.js`) and `sendThrownError` (`./error-response.js`). They moved with the bodies, as slices 3, 5 and 6 moved theirs, and each left a pointer comment. Leaving them would fail `noUnusedLocals`. - **Declared in `rest-server.ts`:** `DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS` (already exported) and the type `RestProtocol` (already exported). The module imports both from `./rest-server.js`; no `export` was added anywhere. The table is a `const`, read only inside the discovery handler at request time, so the two modules importing each other cannot see it uninitialised (the bundle is one scope). **Host shape (slice 1's shape B, twice, as slices 4 and 6):** `DiscoveryRouteHost` (6 members) and `OpenApiRouteHost` (10 members) list exactly the members each body reads, typed `RestServer['m']`; the static is typed `(typeof RestServer)['overlayDocumentationInfo']`. The class keeps both private methods with their names, docblocks and positions. Each body is now a host literal plus `.call(host, basePath)`. Methods go as late-bound closures, `(...a) => this.m(...a)`, and the static as `(...a) => RestServer.overlayDocumentationInfo(...a)`. The call sites in `registerRoutes`' `registerForBase` (`:4781`-`:4786` at base, under `enableDiscovery` / `enableOpenApi`) are unchanged. **Fields, checked one by one:** | field | read when | reassigned after registration? | handed over as | |---|---|---|---| | `routeManager` | registration only (four `register` calls, top-level statements of the two bodies) | its one swap (`:5721` / `:5725` at base) wraps `registerMetadataEndpointsInner`, not these registrars | value | | `config` | **request time**: every one of its ten reads is inside a handler | not by the class (constructor only, `:2156`), but not `readonly` | **getter**, in both host literals | The getter is measured, not argued. A one-off vitest file (copied in, run, deleted) boots a source `RestServer`, drives the door, assigns a NEW `config` object on the instance AFTER `registerRoutes()` (a new object, never a mutation, which a by-value hand-over would see through the shared reference), and drives it again: - this head (`a9073d822b`): `GET /api/v1/discovery` answers `capabilities.transactionalBatch.enabled` `true`, then `false` after `enableBatch: false`. `GET /api/v1/openapi.json` answers `info.title` `"ObjectStack REST API"` (the artifact's), then `"Late Title"` after `documentation: { title: 'Late Title' }`. A spy set on `RestServer.overlayDocumentationInfo` after `registerRoutes()` is called once per serve, with the authored documentation, so the static is reached through the host at request time. `Tests 3 passed (3)`. - by-value ablations through `scripts/ablation-replace.mjs`, each host literal's getter in turn replaced by `config: this.config,` with a multi-line anchor naming its literal. Each landed (anchor 1 → 0, replacement 0 → 1, blob `8f098de155` → `dcd5b2791e` / `4e218d1382`). The ablated door freezes at registration: discovery reads `true` then `true`; OpenAPI reads the artifact's title then the artifact's title again. Each run: `Tests 1 failed | 2 passed (3)`. Restored by the tool and a trap: blob `8f098de155` equals `HEAD`, `git diff HEAD` empty, the one-off file deleted. Source path (relative import), so no `dist` leg applies. **Mechanism hypotheses from the dispatch, measured:** - (a) **Partly falsified.** Both bodies read `this.config` and `resolveProtocol`, as hypothesised. The discovery body does **not** read `this.protocol`: the field appears in it only inside comments (`[#9292]`, which says the handler stopped reading it); the AST walk finds no `this.protocol` access, and the protocol is reached through `resolveProtocol`. So `config` is the one request-time field, and it goes as a getter by measurement (above). The walk also found the private static `overlayDocumentationInfo`, which the hypothesis did not list (next section). - (b) **Confirmed.** Every path expression (`basePath`, `realBase`, `unscopedBase`, the `/docs` page's `reqPath` / `apiBase`) moved unedited; the scoped-optional and scoped-required route dumps below are identical, and every discovery and `/docs` handler's source is byte-identical. - (c) **Confirmed.** The bodies carry 5 `ADR-NNNN Dk` citations (ADR-0057 D10, ADR-0076 D12 ×2 in discovery; ADR-0056 D2 and ADR-0138 D2 in OpenAPI). They left the anchored `rest-server.ts` (94 → 89), so `check-adr-anchors` read 1761 decision-letter citations at base and 1756 at the move-only commit `234537d1ad`, **green both times**: the silent narrowing the card forbids. A new anchor (below) restores 1761. - (d) **Measured;** the readers are listed below. `check-affected-docs` discovers the module by its `*-routes.ts` name (route sources 25 → 26) with no edit; `check-route-envelope`'s tables and the platform checklist needed one each. ## The one token that changed, and why no unedited shape exists The OpenAPI body's step 5 overlays the publisher's identity through the class's PRIVATE STATIC: base `rest-server.ts:5525` read `enriched.info = RestServer.overlayDocumentationInfo(enriched.info, this.config.api.documentation);`. Outside the class that spelling does not compile, measured with two throwaway files added to `packages/rest/src` and removed: `tsc --noEmit` exits 1 with `TS2341: Property 'overlayDocumentationInfo' is private and only accessible within class 'RestServer'` (value import), and `TS1361` plus `TS2341` (the type-only import the module uses). The alternatives each break something the slice must keep: - making the static public or protected, or moving it out of the class, changes the published `.d.ts` (it is listed there as `private static overlayDocumentationInfo;`), and the dispatch keeps every class method in place this slice; - a module-level binding named `RestServer` that is not the class would keep the text byte-identical, and give every later reader of the module an identifier that lies about what it is. So the static is reached the way the dispatch's ruling reaches every class member these bodies use: through the host, as a late-bound closure. `discovery-routes.ts:655` reads `enriched.info = this.overlayDocumentationInfo(enriched.info, this.config.api.documentation);`. The member is pure (its only inputs are its two arguments, and it reads no `this`), so the receiver is the whole difference, and the module header records it. ## The registrar list `packages/rest/src/rest-server-registrar-files.ts` gains `discovery-routes.ts` with a `[#23050 slice 7]` comment (length 7 → 8). Every reader that pins its length re-reads its number, and every sum is unchanged: | reader | sum over the list | change | |---|---|---| | `rest-server-query-number-census.test.ts` | 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0 | length 7 → 8 | | `rest-server-canonical-query-ast.test.ts` | 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 + 0 `query:` slots | floor `discovery-routes.ts: 0`, length 7 → 8 | | `meta-draft-read-door-census.test.ts` | 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0 | the two bodies read no draft; length 7 → 8 | | `rest-server-docblock-position.test.ts` | floors 4 + 3 + 3 + 8 + 3 + 8 + 5 = 34 | floor `discovery-routes.ts: 5` (header, two host interfaces, two functions; neither body held a TSDoc block); `rest-server.ts` keeps 164 blocks, floor 100; length 7 → 8 | | `execctx-consumer-census.test.ts` | 62 sites = 25 + 9 + 5 + 7 + 8 + 1 + 5 + 2 | mentions 109 → 113 (the seam once: one forwarder ×2, one interface member ×2; `rest-server.ts` stays 57). Forwarders 9 = (8 − 1) + `EXTRA_MOVED_REGISTRARS` 3 − `MOVED_REGISTRARS_READING_NO_EXECCTX` 1; see the note below | | `ui-view-route-identity.measurement.test.ts` / `ui-view-route-tenancy.measurement.test.ts` | `this.enforceAuth(` 56, `this.resolveExecCtx(` 71 over the list (55 / 70 before, +1 each for the OpenAPI literal's forwarder); ownership guard 1 | length 7 → 8 | | dogfood `authz-conformance.test.ts` (realtime tripwires) | 0 hits over the eight files | length 7 → 8 | | dogfood `authz-probe-blind-spot.census.ts`, `rest-server.ts` row | 70 / 19 / 51 unchanged; `this.routeManager.register(` 63 = 23 + 9 + 5 + 7 + 8 + 1 + 6 + 4; `private register*Endpoints(` 16 | new control `discovery-routes.ts: this.routeManager.register(` 4; `registrar list length` 8; `enforceAuth` 92 → 96 re-anchored (the OpenAPI pair's two call sites moved and are counted in both readings; the four new occurrences are the seam once; no call site added); `populationRule` names the new module | **The execctx forwarder control, measured rather than inherited.** The dispatch expected this census to count two registrars in one module, as slices 4 and 6 did. It does, and it also had to learn that the two registrars here do not both forward `resolveExecCtx`: the discovery body resolves no caller (the document is served to anyone), so its host literal names no `resolveExecCtx` member. The control pins forwarders as one per moved registrar whose body reads the seam: `EXTRA_MOVED_REGISTRARS` gains `discovery-routes.ts: 1` (two registrars in one module, as slices 4 and 6), and a new map, `MOVED_REGISTRARS_READING_NO_EXECCTX`, holds `discovery-routes.ts: 1`, pinned at exactly 1. The count stays exact: a respelt forwarder, or a discovery body that starts reading the seam, reds it. Each number above was also counted statically on the tree (`grep -oF` per file). ## Proof of move-only - **`.d.ts`:** `packages/rest/dist/index.d.ts` and `index.d.cts` built at base `3b46855fb9` (this worktree, before any edit), at `a9073d822b` and at the merge head `9ddffc0baa` are byte-identical: sha256 `7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9` for all six files, `cmp` silent. The head bundles carry a head-only marker (`readConfig`: 4 hits in each of `index.js` and `index.cjs`, 0 at base), so the declaration was rebuilt from the moved source, not replayed. `DiscoveryRouteHost`, `OpenApiRouteHost` and `discovery-routes` appear 0 times in the `.d.ts`; `src/index.ts` is untouched. - **Export lists:** the base, head and merge-head ESM and CJS bundles each load with 39 exports and one sorted export-name list (sha256 `b3246be6a171…` on all six). None of the two functions, the two host types or `DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS` is among them. - **Routes:** a source `RestServer` on a recording mock server, with a protocol that answers every member, dumped under seven configs: default; scoped-optional; scoped-required; every provider set; `enableDiscovery: false`; `enableOpenApi: false`; and both off under scoped-optional. Each dump holds `getRoutes()` (functions as arity), the ordered server registration calls, and a whitespace-free sha256 of every handler's source. - `getRoutes()` and the ordered calls: **14 of 14 byte-identical**, base vs head and base vs merge head. default: routes `6931d1a5d182…`, calls `ccb3bd12f10b…` (75 routes, 4 of them these doors); scoped-optional `275c72f46c30…` / `339e8c3abc05…` (150, 8); scoped-required `a2aa8183e213…` / `734f33133150…` (75, 4); every-provider-set equals default; discovery-off `51f8ed92bb85…` / `50d8d124f3b8…` (73, 2); openapi-off `07cd86e4661a…` / `cb8c93c76044…` (73, 2); both-off `4525bbdb0f79…` / `e7108e133e9a…` (142, 0). A second base run was identical to the first. - Handler digests: identical in both configs without the OpenAPI pair (openapi-off `307d066c0008…`, both-off `2e2b275dc162…`). In the other five they differ in exactly the `openapi.json` handler of each mounted base (6 handlers in all) and in nothing else. With the one receiver mapped back (`this.overlayDocumentationInfo(` → `RestServer.overlayDocumentationInfo(`), all 6 sources equal base byte for byte; the 18 discovery and `/docs` handler sources equal base with no mapping. - **The handler digest can fail:** a positive control changed one character of one moved handler (`docs viewer error:` → `!`, through `ablation-replace.mjs`; anchor 1 → 0, blob `8bf688c0d6` → `91786677f8`). Routes and calls were unchanged in all seven configs, and the handler list differed in exactly `GET {base}/docs` for each mounted base, nothing in the two configs without it. Restored, blob `8bf688c0d6` equals `HEAD`. - **Moved bodies:** base `rest-server.ts` lines 4914-5235 and 5324-5590, one 4-space indent removed, against `discovery-routes.ts` lines 123-444 and 454-720 (322 + 267 lines). The discovery `diff` exits 0. The OpenAPI `diff` shows exactly one line, the receiver above. The viewer page's HTML is a template literal whose continuation lines (base 5564-5574) sit at column 0: they are runtime text, and they moved byte for byte, unindented. Every other odd-backtick line in the ranges is a comment, so no other template literal spans a cut. - **Moved-line accounting:** `git diff --color-moved=plain --color-moved-ws=allow-indentation-change` base → move commit over the two files: 589 deleted and 601 added lines detected as moved. `rest-server.ts` is +46 / −593 there: the two stub bodies (host literal plus `.call`), the module's import and its comment, and three import-pointer comments. ## Readers re-pointed (sums unchanged, no gate loosened) - `scripts/check-route-envelope.mjs` (a gate's target paths, `domain:spec`): `discovery-routes.ts` is discovered by the `*-routes.ts` convention and declared `dialectOnly` with ratchet #9559, as the six earlier modules are, at its measured `stringError 0`, `siblingCode 0` (the gate compares both counts exactly in both directions). The discovery handler's one refusal path is the shared `handleRouteError`, and the OpenAPI document's one hand-built refusal is the nested `{ error: { code, message } }`. So `rest-server.ts` stays at 17 / 17, and the sums 17 + 13 + 8 + 1 + 0 + 0 + 0 + 0 = 39 and 17 + 15 + 8 + 9 + 4 + 1 + 0 + 0 = 54 are unchanged. Output differs from base only in the new row (17 → 18 modules, 8 → 9 ratcheted). `--self-test` green. Carried from slice 6 into the same comment block: its `siblingCode` entry said "the two arms #7981 and #8073 converted … 77 → 73"; it is four arms, and the entry now says "four". - `scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json` (**new**, ADR-0076 + ADR-0056 + ADR-0138; `domain:devx`): `check-adr-anchors` decision-letter citations on the anchored surface 1761 (base) → 1756 (move) → 1761 (head); anchored files 68 → 69. Every listed id appears in the module, and every cited letter is a decision of its record (the gate's own check, green). The invariant writes "its D12", never the citation form, so it adds no citation of its own (the gate's regex: 0). ADR-0057 is cited in the module but not listed: its number is shared by two records, and listing it would add a fourth entry to the gate's ambiguous-number note for nothing the letter check does not already read. - `docs/qa/platform-checklist/areas/api-backend.json` (`domain:devx`): the `crud_endpoints` and `metadata_endpoints` items' source lists named `rest-server.ts` for "`registerDiscoveryEndpoints` — `discovery.routes.data` / `.metadata` built from" the prefix. Each now points at `packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints`, slice 2's `+` form. `check:platform-checklist`: symbol anchors 733/743 → 735/745 resolved (the two new anchors resolve; unresolved 10 → 10), cited sources 346 → 347. - The nine registrar-list readers above. - Prose pointers in this lane, none in the published `.d.ts`: the `DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS` docblock (`rest-server.ts`) said "the discovery handler below"; `openapi-builtin-paths.ts`' module docblock and `buildBuiltinPaths`' docblock said `rest-server.ts` installs and calls it. Each now names `discovery-routes.ts`. The dogfood matrix row `anonymous-deny-api-description` named `rest/rest-server.ts registerOpenApiEndpoints` as the enforcement site of the two handlers; it names `rest/discovery-routes.ts` (no test reads that row's path). **Red before the re-points**, each measured with the move in place (`234537d1ad`): the six `local` list readers read `Test Files 6 failed (6)`, `Tests 7 failed | 105 passed (112)` (five length pins, plus the docblock pin's length pin and its new module with no floor). `rest-server-canonical-query-ast.test.ts` (`repo`) read `Tests 2 failed | 57 passed | 8 skipped (67)`. The dogfood pair read `Tests 2 failed | 106 passed (108)` (list length; the census row's `registrar list length` control). `tsc --noEmit` was clean, but `check:test-typecheck` found 2 type errors, the two typed floor tables without the new module. `check:route-envelope` read `discovery-routes.ts NOT DECLARED` (MODULES and EXPRESS_RESPONSE_MODULES). `check-adr-anchors` was green at 1756. ## Readers examined and not re-pointed - **`packages/spec` readers whose reading includes a moved line (stopped at, per the dispatch, not edited):** `packages/spec/liveness/rest_api.json` cites `rest-server.ts#registerDiscoveryEndpoints` seven times, and five of those evidence strings quote code that now lives in `discovery-routes.ts` (`enabled: runtimeSupportsTx && this.config.api.enableBatch !== false`, `enabled: !!caps.search?.enabled && this.config.api.enableSearch`, `scoping.enabled`, `scoping.resolution`, and the advertised switches); its ten `#overlayDocumentationInfo` rows say the static is "called by the `openapi.json` handler registerOpenApiEndpoints builds". `metadata_endpoints.json` and `crud_endpoints.json` each cite `#registerDiscoveryEndpoints` once (`routes.metadata`, `routes.data`). Every anchor still resolves (the private methods stay), and `check:liveness` is green. They go to the seat, to carry with the CRUD slice's pointers. - `docs/adr/0138-…` cites `rest-server.ts#RestServer.registerDiscoveryEndpoints` and `#RestServer.registerOpenApiEndpoints`. Both still resolve (the private methods stay). It is a governed surface, and is not edited here. - `content/docs/releases/implementation-status.mdx` names `rest-server.ts — registerDiscoveryEndpoints`: release-owned, not edited in a code PR, and the name still resolves. - `rest-server.ts`' `parseDeclaredApiConfig` docblock, in its `projectResolution` history (about `:4578`), says "the discovery handler below copies the value into `discovery.scoping.resolution`". That docblock ships in the published `.d.ts`, so editing it would break the byte-identity this slice must keep. Not edited. - Prose that names the method without a file, still true: `packages/core/src/qa/http-adapter.ts` (and its test), `packages/runtime/src/dispatcher-plugin.ts` (about `:1098`), `packages/metadata-protocol/src/discovery-realtime-channel.pin.test.ts`, the `packages/rest` discovery and OpenAPI tests, `showcase-anonymous-deny-surfaces.dogfood.test.ts`, the dogfood census row's dated `[#22430]` entry, and the checklist's clause text and `FOLLOW-UPS.md`. - No `rest-server.ts:NNNN` line citation in the tree falls inside the moved ranges. `REST_ROUTE_LEDGER` names routes, not files. - Gate outputs, base `3b46855fb9` vs head, from the same scan: see the gates section. ## Acceptance notes - The `[#22430]` comment the OpenAPI body carries ("the same body the `/data` and `/meta` routes answer") and the `[#9292]` comment in the discovery body ("~30 sibling handlers in this file") moved verbatim; in `discovery-routes.ts` "this file" is the module. A move-only slice may not edit the bodies. - `openapi-builtin-paths.ts` says the handler hands `buildBuiltinPaths` "`routeManager.getAll()`"; it has handed it `getRoutes()` (RouteManager's table plus the direct mounts) since #5822. Pre-existing, not part of this move, left as it was. - `docs/adr/0076-…` (status line and D11) and `docs/adr/0111-…` (Consumers line) are still carried to the maintainer by the seat; this slice adds nothing to them. ## Gates and tests Measured at this branch: base `3b46855fb9`, the move `234537d1ad`, the re-points `a9073d822b`, and one merge of `origin/main` `2906cd8d2e` (`9ddffc0baa`: three commits in `cli`, `driver-sql` and `metadata-protocol`/`objectql`, no file in this diff). All builds and tests went through `os-verify-lock`. Every exit code was captured to a file before any pipe. - At `9ddffc0baa`: `pnpm install --frozen-lockfile` exit 0; the `@objectstack/rest` and `@objectstack/verify` closures built, exit 0; then `pnpm turbo run build --concurrency=2`, the whole workspace, exit 0 (76 tasks, 74 cached), after which the `@objectstack/rest` `.d.ts` / `.d.cts` still equal base. - `pnpm --filter @objectstack/rest typecheck`: exit 0 (`tsc --noEmit`, then `check:test-typecheck`: 0 file(s) / 0 error(s)), at `a9073d822b` and at `9ddffc0baa`. - `pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2`: Test Files 285 passed (285), Tests 5876 passed / 326 skipped (6202), at both heads. - `pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2`: Test Files 5 passed (5), Tests 238 passed / 8 skipped (246), at both heads. - `pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts`: Test Files 2 passed (2), Tests 108 passed (108), at both heads. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack`, no paths, at the final head `9ddffc0baa` (stderr names that commit; the `--repo` assertion holds; 17 paths against merge base `2906cd8d2e`): 81 commands, all run at `9ddffc0baa`. 80 exited 0 on the first pass; `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET: 19 packages had no `dist`), and exited 0 after the whole-workspace build above. `--ran`: `Run reconciliation — 81 derived, 81 run, 0 NOT-MEASURED, 0 UNRUN.` Among them: `pnpm check:route-envelope`, `pnpm check:adr-anchors`, `pnpm check:platform-checklist`, `pnpm --filter @objectstack/spec run check:liveness`, `pnpm check:durability-log-level`, `pnpm check:dispatcher-error-vocabulary`, `pnpm check:dts-closure`, `pnpm check:published-files` and `pnpm check:pm-dispatch-gates`. - Scan outputs, base `3b46855fb9` vs the merge head, diffed: identical for `check-single-authz-resolver`, `check-system-context-census`, `check-rest-log-declared`, `check:rest-log-spy-declared`, `check:test-source-alias`, `check-filter-alias-parity` and `check:liveness`. Differing only by this diff's new file: `check-meta-type-normalized` (35 → 36 files), `check-undeclared-dep-imports` (+1 file and +1 specifier, read at the move commit; `main` adds one more file), `check-affected-docs` (route sources 25 → 26), `check-route-envelope` (the new row), `check-scripts-symbol-anchors` (+4 file-level anchors, the gate's new comments) and `check:platform-checklist` (735/745, above). `check-adr-anchors` reads 1764 at the merge head: 1761 from this diff (above) plus 3 that `main`'s `metadata-protocol/src/protocol.ts` added (278 → 281 in that anchored file). The rest moved with `main`'s three commits (file counts in `check:nul-bytes`, `check:doc-authoring`, `check:cross-package-test-inputs`, `check:dispatcher-error-vocabulary`, `check:error-status-conformance`; `check-durability-degradation-log-level` 78 → 80 read seams). - Lint, the proven narrowing: `eslint --no-inline-config --format json` over `packages/rest`, the three dogfood files, `scripts/check-route-envelope.mjs`, the new anchor JSON and the checklist JSON: 333 files, 0 errors, 0 lint warnings. Its two warnings are `File ignored because no matching configuration was supplied`, for the two JSON files, which `eslint .` never reads. All 17 changed files are in the result set, the 15 code files at 0 / 0. The published `.d.ts` is byte-identical, so no consumer outside `packages/rest` can lint differently. Repo-wide `pnpm lint` is CI's. - `check:dev-prereqs` and `check:skill-frame-freshness` (local-only) were not run: no `packages/spec` build input and no four-axis frame file is touched. - No changeset: nothing any released package publishes changes (`.d.ts`, export lists and routes above). `skip-changeset` is the seat's to apply on its own check. **Cross-lane files in this diff:** `scripts/check-route-envelope.mjs` (`domain:spec`, a gate's target paths), `scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json` (a new entry in `check-adr-anchors`' registry, `domain:devx`), `docs/qa/platform-checklist/areas/api-backend.json` (`domain:devx`), and the dogfood files (`packages/qa/dogfood/test/authz-conformance.test.ts`, `authz-conformance.matrix.ts`, `authz-probe-blind-spot.census.ts`). ## For the seat: one question The dispatch rules the bodies move unedited with every handler digest identical, and every class member the bodies call reached through the host. The OpenAPI body's call of the private static cannot satisfy both: it is reached through the host here, and that changes one token of one handler. Options: (A) accept the receiver change as this slice's shape (recommended: the static stays private and in place, the `.d.ts` is untouched, and the dependency is declared on `OpenApiRouteHost` like every other member); (B) a module-level binding named `RestServer` that forwards the static, which keeps the text byte-identical but names something that is not the class; (C) a later slice moves `overlayDocumentationInfo` out of the class, which changes the `.d.ts` by one private line and needs its own ruling. --- _Generated by [Claude Code](https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent b55be42 commit ec44cf6

17 files changed

Lines changed: 933 additions & 631 deletions

‎docs/qa/platform-checklist/areas/api-backend.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2136,7 +2136,7 @@
21362136
"source": [
21372137
"packages/spec/liveness/crud_endpoints.json (the ADR-0049 ledger this item answers: five live `operations.*` switches + `dataPrefix`, two dead keys)",
21382138
"packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema (the declared contract, its defaults and the two retiredKey tombstones)",
2139-
"packages/rest/src/rest-server.ts#registerCrudEndpoints (the six mounts and their five gates), (normalizeConfig — the parsed sub-config every consumer reads), (parseDeclaredSubConfig — the construction-time refusal), (registerDiscoveryEndpoints — discovery.routes.data built from dataPrefix), (getRoutes — the route table the mount clauses read)",
2139+
"packages/rest/src/rest-server.ts#registerCrudEndpoints (the six mounts and their five gates), (normalizeConfig — the parsed sub-config every consumer reads), (parseDeclaredSubConfig — the construction-time refusal), (getRoutes — the route table the mount clauses read) + packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints (discovery.routes.data built from dataPrefix)",
21402140
"#14691 (the ADR-0049 retirement of patterns/objectParamStyle) · #11984 (parse-and-consume instead of cast) · ADR-0122 (author state) · ADR-0076 D12 (advertise only what is mounted)",
21412141
"sibling clause: api-backend.api-methods-verb-gate (the per-OBJECT exposure gate this config-level switch must stay distinguishable from) · api-backend.route-ledger-live-parity (the routing-404 control technique)"
21422142
],
@@ -2383,7 +2383,7 @@
23832383
"source": [
23842384
"packages/spec/liveness/metadata_endpoints.json (the ADR-0049 ledger this item answers: prefix, enableCache, maskObjectFields and three endpoints.* live, cacheTtl and endpoints.schema dead)",
23852385
"packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema (the declared contract — four endpoints.* switches whose describe() strings enumerate every mount they gate since #15542/#15854, the ADR-0106 D8 docblock and the two retiredKey tombstones)",
2386-
"packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (metaPath, the four endpoint gates and registerPerItemRoute — the per-item switch spelled as a call so the later members of that face carry it), (resolveObjectMasker — the metaType/maskObjectFields fork), (normalizeConfig — isObjectSchemaMaskingEnabled and the OS_ALLOW_UNMASKED_OBJECT_METADATA escape hatch), (registerDiscoveryEndpoints — discovery.routes.metadata built from prefix), (getRoutes)",
2386+
"packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (metaPath, the four endpoint gates and registerPerItemRoute — the per-item switch spelled as a call so the later members of that face carry it), (resolveObjectMasker — the metaType/maskObjectFields fork), (normalizeConfig — isObjectSchemaMaskingEnabled and the OS_ALLOW_UNMASKED_OBJECT_METADATA escape hatch), (getRoutes) + packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints (discovery.routes.metadata built from prefix)",
23872387
"ADR-0106 D8 (per-caller field masking of served object schemas) · #14691 (the two retirements) · #7526 (the /meta/types ordering fix the `types` switch takes with it) · ADR-0076 D12 · #15542 / #15854 (the switch-radius ruling that created `maintenance` and gave `item` its own writes) · #15543 (why the measured blast radius of that move is zero: no shipped boot path authors a RestServerConfig)",
23882388
"sibling clause: platform-core.metadata-registry-serving (it drives the served REGISTRY on the default config; this item covers the config that decides what is served and to whom) · platform-core.docs-audience-gate (the audience-gated types that are a cache carve-out here)"
23892389
],

‎packages/qa/dogfood/test/authz-conformance.matrix.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
295295
// #22430 — the API-description endpoints, under the ruling recorded on #22146
296296
// (the API-description endpoints refuse an anonymous caller with 401).
297297
{ id: 'anonymous-deny-api-description', summary: 'anonymous-deny on the API-description endpoints (the document and its viewer)', state: 'enforced',
298-
enforcement: 'rest/rest-server.ts registerOpenApiEndpoints — BOTH handlers (the document and its viewer page, on every base the server mounts) open with enforceAuth (→ shouldDenyAnonymous) as their first act, ahead of the bundled-artifact load and the protocol resolution, so a refused request does no work and does not reveal whether a document is bundled; an anonymous caller gets the REST seam\'s flat 401 UNAUTHENTICATED, a signed-in caller is served as before, and a session held by the ADR-0069 auth-policy gate gets that gate\'s 403',
298+
enforcement: 'rest/discovery-routes.ts registerOpenApiEndpoints — BOTH handlers (the document and its viewer page, on every base the server mounts) open with enforceAuth (→ shouldDenyAnonymous) as their first act, ahead of the bundled-artifact load and the protocol resolution, so a refused request does no work and does not reveal whether a document is bundled; an anonymous caller gets the REST seam\'s flat 401 UNAUTHENTICATED, a signed-in caller is served as before, and a session held by the ADR-0069 auth-policy gate gets that gate\'s 403',
299299
proof: 'showcase-anonymous-deny-surfaces.dogfood.test.ts',
300300
// The REST family at ledger granularity. The family is exactly these two
301301
// routes and both handlers open with the floor, which is the property a

‎packages/qa/dogfood/test/authz-conformance.test.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1628,7 +1628,8 @@ describe('#9084 — the rest-server.ts transport tripwire keys off mechanics', (
16281628
// [#23050 slice 4] Length 4 → 5 (`sharing-routes.ts`), still 0 over it.
16291629
// [#23050 slice 5] Length 5 → 6 (`analytics-routes.ts`), still 0 over it.
16301630
// [#23050 slice 6] Length 6 → 7 (`security-routes.ts`), still 0 over it.
1631-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
1631+
// [#23050 slice 7] Length 7 → 8 (`discovery-routes.ts`), still 0 over it.
1632+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(8);
16321633
expect(REST_SERVER_REGISTRAR_PATHS[0]).toBe(REST_SERVER);
16331634
const listed = REST_SERVER_REGISTRAR_PATHS.map((file) => readFileSync(join(REPO_ROOT, file), 'utf8'));
16341635
for (const probe of [MECHANICS, ROUTES]) {

‎packages/qa/dogfood/test/authz-probe-blind-spot.census.ts‎

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -520,10 +520,11 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
520520
'terminator spelling), PLUS `registerPerItemRoute(` call sites; counted over this file AND every ' +
521521
'module on the registrar list (`packages/rest/src/rest-server-registrar-files.ts`) — ' +
522522
'`data-action-routes.ts`, `batch-routes.ts`, `approvals-routes.ts`, `sharing-routes.ts`, ' +
523-
'`analytics-routes.ts` and `security-routes.ts`, the modules the bodies of `registerDataActionEndpoints`, ' +
524-
'`registerBatchEndpoints`, `registerApprovalsEndpoints`, `registerSharingEndpoints`, ' +
525-
'`registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, `registerSecurityExplainEndpoints` and ' +
526-
'`registerSecurityEndpoints` moved to; ' +
523+
'`analytics-routes.ts`, `security-routes.ts` and `discovery-routes.ts`, the modules the bodies of ' +
524+
'`registerDataActionEndpoints`, `registerBatchEndpoints`, `registerApprovalsEndpoints`, ' +
525+
'`registerSharingEndpoints`, `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`, ' +
526+
'`registerSecurityExplainEndpoints`, `registerSecurityEndpoints`, `registerDiscoveryEndpoints` and ' +
527+
'`registerOpenApiEndpoints` moved to; ' +
527528
'reachable = those inside registerMetadataEndpoints',
528529
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
529530
// the numbers it produces did NOT move: 80 / 19 / 61, exactly as before.
@@ -717,24 +718,40 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
717718
// slice's seam, the same two spellings TWICE, because the module carries
718719
// two registrars (two host literals' forwarders, four; two host
719720
// interfaces' members, four). No call site was added.
721+
// [#23050 slice 7] 70 / 19 / 51 UNCHANGED, the same measurement again:
722+
// `registerDiscoveryEndpoints`' and `registerOpenApiEndpoints`' bodies
723+
// (four registration sites, two and two — the API root and `/discovery`,
724+
// `/openapi.json` and `/docs` — all outside `registerMetadataEndpoints`, so
725+
// all four in the blind spot) moved to `discovery-routes.ts`, which joins
726+
// the list (length 7 -> 8) with its own control.
727+
// `this.routeManager.register(` stays 63 over the eight files
728+
// (23 here + 9 + 5 + 7 + 8 + 1 + 6 + 4), `private register*Endpoints(`
729+
// stays 16 (both registrars are still declared here), and `enforceAuth`
730+
// 92 -> 96 is RE-ANCHORED, not relaxed: the OpenAPI pair's two call sites
731+
// moved with their routes and are counted in both readings; the four new
732+
// occurrences are this slice's seam, the two spellings ONCE, because only
733+
// the OpenAPI body reads `enforceAuth` (its host literal's forwarder, two;
734+
// its host interface's member, two) — the discovery document is served to
735+
// anyone, so its literal forwards no `enforceAuth`. No call site was added.
720736
controls: {
721737
'private register*Endpoints(': 16,
722738
'this.routeManager.register(': 63,
723-
'registrar list length': 7,
739+
'registrar list length': 8,
724740
'data-action-routes.ts: this.routeManager.register(': 9,
725741
'batch-routes.ts: this.routeManager.register(': 5,
726742
'approvals-routes.ts: this.routeManager.register(': 7,
727743
'sharing-routes.ts: this.routeManager.register(': 8,
728744
'analytics-routes.ts: this.routeManager.register(': 1,
729745
'security-routes.ts: this.routeManager.register(': 6,
746+
'discovery-routes.ts: this.routeManager.register(': 4,
730747
// Both halves of the new rule carry their own control, so neither can go
731748
// silently to zero: a helper deleted and its routes inlined back would
732749
// still read population 80, and only these two controls would notice the
733750
// shape moved and force this provenance to be re-read.
734751
'registerPerItemRoute(': 8,
735752
'const registerPerItemRoute =': 1,
736753
'forwarder slice: this.routeManager.register(': 1,
737-
enforceAuth: 92,
754+
enforceAuth: 96,
738755
},
739756
note:
740757
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +

0 commit comments

Comments
 (0)