From 9ba531d51ff770f18ff2f172e77de2f5748fb015 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 03:51:44 +0000 Subject: [PATCH 1/6] docs(spec): state RestServerConfig's embedder-only reachability in the schema and the ledger WIP: docblocks, liveness reachability rows and QA checklist closure. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- docs/qa/platform-checklist/FOLLOW-UPS.md | 2 +- .../platform-checklist/areas/api-backend.json | 6 +- packages/spec/liveness/batch_endpoints.json | 12 +-- packages/spec/liveness/crud_endpoints.json | 14 +-- .../spec/liveness/metadata_endpoints.json | 16 +-- packages/spec/src/api/rest-server.zod.ts | 100 ++++++++++++++---- 6 files changed, 107 insertions(+), 43 deletions(-) diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index bdfcbdecbe..8b56b929a7 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -568,7 +568,7 @@ card filed against it*. Recorded, not acted on — the channel question is #1173 | # | finding | evidence | captured in | handling | |---|---|---|---|---| | E1 | **`metadata.endpoints.items` gates four routes, three of which its declared meaning does not cover** — its `describe()` says "GET /meta/:type — List items of type", and it also gates `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and the **`POST {prefix}/_migrate-stored` write door**. An operator switching off a listing read silently disarms a migration door and the cross-type spec-validation sweep. `endpoints.item` is milder but the same shape: it also takes `{prefix}/book/:name/tree`. | `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` (four `endpoints.items` gates, four `endpoints.item` gates) vs `packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema` (one route named per switch) | api-backend.rest-metadata-config-contract (a clause requires the run to ENUMERATE each switch's real radius) | design/docs — filed as #15542, **RULED and closed**: every `endpoints.*` switch now gates exactly the face its name states. The whole-store family (`/diagnostics`, `/_drafts`, `POST {prefix}/_migrate-stored`) moved to a new key `maintenance`; `items` is down to its one declared mount; `item` gained the per-item `PUT`/`DELETE` and the history family it never gated (the converse mismatch, filed as #15854 and landed in the same PR); all four `describe()` strings now enumerate their mounts. ⛔ The checklist clause is **kept**, not retired — the run still ENUMERATES each switch's real radius from a route-table diff, and `areas/api-backend.json` revision 2 carries the new expected sets. | -| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code (`createRestApiPlugin({ api })`, `createHonoServerPlugin({ restConfig })`). A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543 | +| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code (`createRestApiPlugin({ api })`, `createHonoServerPlugin({ restConfig })`). A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this: the card's and the ruling's citation of `batch.maxBatchSize`'s docblock calling the cap *"deployment policy"* is **wrong — that phrase never occurred in the file**; the one real false-posture sentence was `metadata.maskObjectFields`'s `false` *"opts this server out"*, and the sweep found a third carrier the card missed, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not thread either. All three are repaired. | | E3 | **The MOUNT half of every sub-config switch is unpinned.** `packages/rest/src/rest-sub-config-parse-not-cast.test.ts` pins what a switch normalizes to, and `rest-batch-size-cap.test.ts` pins the cap's effect; nothing asserts that a `false` switch removes its route from the table `getRoutes()` returns. The declared-not-enforced direction — a switch that normalizes correctly and gates nothing — is exactly what no current test would catch. ⚠️ The card said **nine** switches; re-measured on `cc5b3dd0c27` the mount-gating population is **nineteen** — the twelve sub-config switches the card enumerates (its own list adds to twelve, not nine) plus the seven `api.enable*` gates in `registerRoutes`, which are the same seam and were equally unpinned. | the two test files above; the gates live in `registerCrudEndpoints` / `registerBatchEndpoints` / `registerMetadataEndpointsInner` / `registerRoutes` | the three config items (the mount clauses, each with the gap named in `knownGaps`) | test gap — filed as #15544, **closed by `packages/rest/src/rest-config-mount-table.pin.test.ts`**: all nineteen gates pinned as a set difference against the all-true baseline, each with its presence twin (**twenty** since #15542 added `metadata.endpoints.maintenance`; the pin's §0 count moves deliberately with each switch added or retired). ⚠️ The three config items' `knownGaps` still say the harness is the only observation — stale in the good direction, refresh pending (`areas/api-backend.json` was held by another branch when this landed); `rest-metadata-config-contract`'s `automated.ref` now names this pin, the other two are still owed. | ### 10c. Checked and CLEAN (so the next sweep does not re-derive) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index f9f3e50ab5..f4af0b4101 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1980,7 +1980,7 @@ "a scratch vitest harness in packages/rest for the variant half: construct a RestServer with the config under test, call registerRoutes(), and read the route table back with getRoutes() — packages/rest/src/rest-batch-size-cap.test.ts is the shape to copy" ], "knownGaps": [ - "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) or createHonoServerPlugin({ restConfig }) in embedder code. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured", + "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) or createHonoServerPlugin({ restConfig }) in embedder code. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", "the MOUNT half has no permanent pin: packages/rest/src/rest-sub-config-parse-not-cast.test.ts asserts the NORMALIZED CONFIG a switch produces, and nothing asserts that the route disappears from the route table — the scratch harness is the only observation until that pin exists (recorded in FOLLOW-UPS.md)" ] }, @@ -2091,7 +2091,7 @@ "a scratch vitest harness in packages/rest for the variant half — packages/rest/src/rest-batch-size-cap.test.ts already constructs a RestServer with a non-default cap and reads getRoutes(); copy that shape" ], "knownGaps": [ - "NO stock boot authors this sub-object (same door as the crud block: `os serve` fixes the config, `os dev` passes none), so every non-default clause is scored with oracle `test` against a real RestServer and the run record must say so", + "NO stock boot authors this sub-object (same door as the crud block: `os serve` fixes the config, `os dev` passes none), so every non-default clause is scored with oracle `test` against a real RestServer and the run record must say so. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", "the mount half of `enableBatchEndpoint` / `operations.*Many` has no permanent pin — the existing pins assert the normalized config and the cap, not the route table (recorded in FOLLOW-UPS.md)" ] }, @@ -2209,7 +2209,7 @@ "a scratch vitest harness in packages/rest for the variant half (construct, registerRoutes(), getRoutes())" ], "knownGaps": [ - "NO stock boot authors this sub-object (`os serve` fixes the config, `os dev` passes none), so every non-default clause is scored with oracle `test` and the run record must say so", + "NO stock boot authors this sub-object (`os serve` fixes the config, `os dev` passes none), so every non-default clause is scored with oracle `test` and the run record must say so. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", "OS_ALLOW_UNMASKED_OBJECT_METADATA is a deployment-wide escape hatch that also covers the runtime /metadata dispatcher; a boot that sets it makes the masking clause vacuous — record the env posture beside the verdict" ] }, diff --git a/packages/spec/liveness/batch_endpoints.json b/packages/spec/liveness/batch_endpoints.json index a51ab7f248..40d4282b3d 100644 --- a/packages/spec/liveness/batch_endpoints.json +++ b/packages/spec/liveness/batch_endpoints.json @@ -1,6 +1,6 @@ { "type": "batch_endpoints", - "_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "maxBatchSize": { "status": "live", @@ -8,7 +8,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`const maxBatch = batch.maxBatchSize ?? 200` — the cap every batch request is measured against)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish." + "note": "Live and load-bearing since #11984 gave it a real parse: before that a configured `0` was the live cap, because `0` is not nullish. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "enableBatchEndpoint": { "status": "live", @@ -16,7 +16,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (batch.enableBatchEndpoint && this.protocol.batchData)` gates the generic POST /data/:object/batch mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed." + "note": "Gates a mount, and the second conjunct is a runtime capability rather than a second authored input, so no producer beyond the config threading is needed. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "operations": { "children": { @@ -26,7 +26,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.createMany && this.protocol.createManyData)` gates the POST /data/:object/createMany mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Gates a mount." + "note": "Gates a mount. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "updateMany": { "status": "live", @@ -34,7 +34,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.updateMany && this.protocol.updateManyData)` gates the POST /data/:object/updateMany mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Gates a mount." + "note": "Gates a mount. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "deleteMany": { "status": "live", @@ -42,7 +42,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerBatchEndpoints (`if (operations.deleteMany && this.protocol.deleteManyData)` gates the POST /data/:object/deleteMany mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Gates a mount." + "note": "Gates a mount. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "upsertMany": { "status": "dead", diff --git a/packages/spec/liveness/crud_endpoints.json b/packages/spec/liveness/crud_endpoints.json index dcb17095d2..49a368b361 100644 --- a/packages/spec/liveness/crud_endpoints.json +++ b/packages/spec/liveness/crud_endpoints.json @@ -1,6 +1,6 @@ { "type": "crud_endpoints", - "_note": "CrudEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema, the `crud` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `operations.*` and `dataPrefix` gate and shape the mounted CRUD surface, while `patterns` and `objectParamStyle` are normalized and never read. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "_note": "CrudEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema, the `crud` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `operations.*` and `dataPrefix` gate and shape the mounted CRUD surface, while `patterns` and `objectParamStyle` are normalized and never read. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "operations": { "children": { @@ -10,7 +10,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.create)` gates the POST data route mount — an author who sets it false gets no create route at all)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Per key, not per object: since ADR-0122 `crud.operations` is the author state, so the parse applies each `.default(true)` whenever the object is present and normalizeConfig covers the one case the schema leaves open (an absent `operations` object)." + "note": "Per key, not per object: since ADR-0122 `crud.operations` is the author state, so the parse applies each `.default(true)` whenever the object is present and normalizeConfig covers the one case the schema leaves open (an absent `operations` object). REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "read": { "status": "live", @@ -18,7 +18,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.read)` gates the GET-by-id route mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same shape as `create`." + "note": "Same shape as `create`. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "update": { "status": "live", @@ -26,7 +26,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.update)` gates the PATCH route mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same shape as `create`." + "note": "Same shape as `create`. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "delete": { "status": "live", @@ -34,7 +34,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.delete)` gates the DELETE route mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same shape as `create`." + "note": "Same shape as `create`. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "list": { "status": "live", @@ -42,7 +42,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`if (operations.list)` gates the list route mounts)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same shape as `create`." + "note": "Same shape as `create`. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." } } }, @@ -58,7 +58,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerCrudEndpoints (`const dataPath = \\`${basePath}${crud.dataPrefix}\\`` — the base every data route is mounted under); also packages/rest/src/rest-server.ts#registerDataActionEndpoints, packages/rest/src/rest-server.ts#registerSharingEndpoints and packages/rest/src/rest-server.ts#registerBatchEndpoints (each builds its own dataPath the same way), and packages/rest/src/rest-server.ts#registerDiscoveryEndpoints (advertises it as `routes.data`)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Five independent consumers, so this key is live in the strong sense: changing it moves the mounted paths AND the advertised discovery document together." + "note": "Five independent consumers, so this key is live in the strong sense: changing it moves the mounted paths AND the advertised discovery document together. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "objectParamStyle": { "status": "dead", diff --git a/packages/spec/liveness/metadata_endpoints.json b/packages/spec/liveness/metadata_endpoints.json index b885efa437..2fff0d4ca4 100644 --- a/packages/spec/liveness/metadata_endpoints.json +++ b/packages/spec/liveness/metadata_endpoints.json @@ -1,6 +1,6 @@ { "type": "metadata_endpoints", - "_note": "MetadataEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema, the `metadata` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `prefix`, `enableCache`, `maskObjectFields` and four of the five `endpoints.*` switches are read; `cacheTtl` and `endpoints.schema` are not. (`endpoints.maintenance` was added 2026-09-06 by #15542 and is read from its first commit \u2014 see its row.) This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness.", + "_note": "MetadataEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema, the `metadata` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `prefix`, `enableCache`, `maskObjectFields` and four of the five `endpoints.*` switches are read; `cacheTtl` and `endpoints.schema` are not. (`endpoints.maintenance` was added 2026-09-06 by #15542 and is read from its first commit \u2014 see its row.) This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "prefix": { "status": "live", @@ -8,7 +8,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`const metaPath = \\`${basePath}${metadata.prefix}\\`` — the base every metadata route is mounted under, and the string the _migrate-stored audit line reports); also packages/rest/src/rest-server.ts#registerDiscoveryEndpoints (advertises it as `routes.metadata`)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Live in the same strong sense as `crud.dataPrefix`: it moves the mounted paths and the advertised discovery document together." + "note": "Live in the same strong sense as `crud.dataPrefix`: it moves the mounted paths and the advertised discovery document together. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "enableCache": { "status": "live", @@ -16,7 +16,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.enableCache && p.getMetaItemCached ...)` selects the cached read path; the uncached branch is the default when it is false)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "The key selects between two real read paths, so authoring it changes behaviour rather than only a header." + "note": "The key selects between two real read paths, so authoring it changes behaviour rather than only a header. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "cacheTtl": { "status": "dead", @@ -30,7 +30,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#resolveObjectMasker (`if (metaType !== 'object' || !this.config.metadata.maskObjectFields)` — false serves the full object schema to every authenticated caller, ADR-0106 D8)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Security-shaped and genuinely enforced: the opt-out reaches the masker, and `isObjectSchemaMaskingEnabled` also honours the OS_ALLOW_UNMASKED_OBJECT_METADATA deployment escape hatch that the runtime /metadata dispatcher shares." + "note": "Security-shaped and genuinely enforced: the opt-out reaches the masker, and `isObjectSchemaMaskingEnabled` also honours the OS_ALLOW_UNMASKED_OBJECT_METADATA deployment escape hatch that the runtime /metadata dispatcher shares. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "endpoints": { "children": { @@ -40,7 +40,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.types !== false)` gates the GET /meta route mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Read as `!== false`, so an author who omits the key gets the route; setting it false removes the mount." + "note": "Read as `!== false`, so an author who omits the key gets the route; setting it false removes the mount. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "items": { "status": "live", @@ -48,7 +48,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.items !== false)` gates the GET /meta/:type per-type list mount)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same `!== false` shape as `types`. RADIUS NARROWED 2026-09-06 (#15542): it used to gate four mounts \u2014 the per-type list plus the whole-store family (`/diagnostics`, `/_drafts` and the `POST /_migrate-stored` write door) \u2014 while its describe() named the list alone, so an operator who closed a listing read silently disarmed a migration door. The whole-store family now answers to its own key, `maintenance`, and this key gates exactly the one mount it names. Liveness is unchanged by that move (it was live before and is live now); what changed is the radius, which is the axis this ledger structurally cannot see \u2014 the mount table pin (packages/rest/src/rest-config-mount-table.pin.test.ts, the #15544 shape) is the instrument that does, and it asserts route ABSENCE per switch." + "note": "Same `!== false` shape as `types`. RADIUS NARROWED 2026-09-06 (#15542): it used to gate four mounts \u2014 the per-type list plus the whole-store family (`/diagnostics`, `/_drafts` and the `POST /_migrate-stored` write door) \u2014 while its describe() named the list alone, so an operator who closed a listing read silently disarmed a migration door. The whole-store family now answers to its own key, `maintenance`, and this key gates exactly the one mount it names. Liveness is unchanged by that move (it was live before and is live now); what changed is the radius, which is the axis this ledger structurally cannot see \u2014 the mount table pin (packages/rest/src/rest-config-mount-table.pin.test.ts, the #15544 shape) is the instrument that does, and it asserts route ABSENCE per switch. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "item": { "status": "live", @@ -56,7 +56,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (`if (metadata.endpoints.item !== false)` gates the per-item read block, and `registerPerItemRoute` \u2014 the same switch, spelled as a call \u2014 gates the per-item PUT/DELETE and the history family)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Same `!== false` shape as `types`. RADIUS WIDENED 2026-09-06 (#15854, ruled with #15542): it used to gate four READS (`/:type/:name`, `/references`, `/layers`, `/book/:name/tree`) and leave the per-item WRITES (`PUT` / `DELETE /:type/:name`) and the history family (`history`, `audit`, `diff`, `published`, `publish`, `rollback`) answering to `api.enableMetadata` alone \u2014 the converse of the `items` mismatch: an operator who closed the per-item surface left its writes mounted. It now gates the whole per-item face, twelve mounts, which is what the switch name has always promised. `GET /meta/object/:name/state/:field` is deliberately NOT part of that face (object FSM read, addressed by object name) and stays under `api.enableMetadata` alone." + "note": "Same `!== false` shape as `types`. RADIUS WIDENED 2026-09-06 (#15854, ruled with #15542): it used to gate four READS (`/:type/:name`, `/references`, `/layers`, `/book/:name/tree`) and leave the per-item WRITES (`PUT` / `DELETE /:type/:name`) and the history family (`history`, `audit`, `diff`, `published`, `publish`, `rollback`) answering to `api.enableMetadata` alone \u2014 the converse of the `items` mismatch: an operator who closed the per-item surface left its writes mounted. It now gates the whole per-item face, twelve mounts, which is what the switch name has always promised. `GET /meta/object/:name/state/:field` is deliberately NOT part of that face (object FSM read, addressed by object name) and stays under `api.enableMetadata` alone. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "maintenance": { "status": "live", @@ -64,7 +64,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#registerMetadataEndpointsInner (three `if (metadata.endpoints.maintenance !== false)` gates \u2014 `GET /meta/diagnostics`, `GET /meta/_drafts` and `POST /meta/_migrate-stored`)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "ADDED 2026-09-06 (#15542) as the whole-store family's own switch \u2014 the three mounts used to ride `endpoints.items`, whose declared meaning is the per-type list. Live from its first commit: it is a re-partition of an existing gated surface, not a newly declared key waiting for a consumer, and the mount table pin asserts its three routes leave when it is false. Same `!== false` shape as its siblings, default true, so an embedder who never authors it keeps every route it gates." + "note": "ADDED 2026-09-06 (#15542) as the whole-store family's own switch \u2014 the three mounts used to ride `endpoints.items`, whose declared meaning is the per-type list. Live from its first commit: it is a re-partition of an existing gated surface, not a newly declared key waiting for a consumer, and the mount table pin asserts its three routes leave when it is false. Same `!== false` shape as its siblings, default true, so an embedder who never authors it keeps every route it gates. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "schema": { "status": "dead", diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 879e7d01b9..87355424a2 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -21,6 +21,33 @@ import { retiredKey } from '../shared/retired-key'; * - Salesforce: REST API with Object CRUD * - Microsoft Dynamics: Web API with entity operations * - Strapi: Auto-generated REST endpoints + * + * WHO CAN WRITE THIS CONFIG (#15543) — read this before planning a deployment + * around any key below. A `RestServerConfig` is the ARGUMENT a host passes when + * it constructs the server: never a stack collection member, never a stored + * metadata row, never a file the CLI reads. Both doors are programmatic — + * `createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`) and + * `createHonoServerPlugin({ restConfig })` + * (`packages/plugins/plugin-hono-server/src/hono-plugin.ts`). + * + * No shipped boot path opens either door with a config of its own. `os serve` + * (`packages/cli/src/commands/serve.ts`) constructs the plugin with a fixed + * argument carrying exactly two CLI-derived keys — `api.enableProjectScoping` + * and `api.projectResolution` — and the dev plugin + * (`packages/plugins/plugin-dev/src/dev-plugin.ts`) calls + * `createRestApiPlugin()` with no config at all. So on a CLI-started deployment + * every OTHER key here is EMBEDDER-ONLY: the whole of `crud`, `metadata` and + * `batch`, and the rest of `api`. Its value is whatever the `.default()` below + * says, and no flag, env var or config file moves it. That is the recorded + * posture, not a gap awaiting a fix: the keys keep their runtime reads and + * their embedder consumer, and the reachability answer ADR-0049 asks for is + * written per key in the liveness ledger + * (`packages/spec/liveness/crud_endpoints.json`, `metadata_endpoints.json`, + * `batch_endpoints.json`). + * + * ⛔ So a docblock here must never describe a key as a deployment posture + * without saying who can actually set it. An operator reading this page is + * entitled to learn from it that they cannot set these. */ // ========================================== @@ -97,11 +124,17 @@ export const RestApiConfigSchema = lazySchema(() => z.object({ enableOpenApi: z.boolean().default(true).describe('Enable OpenAPI 3.1 spec & docs viewer endpoints'), /** - * Deployment-wide switch for the structured-search surface. `false` skips - * mounting the search endpoints entirely (`registerSearchEndpoints` is never - * called, so the routes 404), and the discovery capability block reports - * `search.enabled: false` regardless of what the underlying protocol could - * serve — declared and enforced at the mount, not advertised past it. + * Server-wide switch for the structured-search surface — and, like every key + * of this block except `enableProjectScoping` / `projectResolution`, + * ⛔ EMBEDDER-ONLY (#15543; see WHO CAN WRITE THIS CONFIG in the file + * header). `os serve` does not thread it, so a CLI-started deployment always + * gets the default and the search endpoints are mounted. + * + * For the host that CAN write it: `false` skips mounting the search endpoints + * entirely (`registerSearchEndpoints` is never called, so the routes 404), + * and the discovery capability block reports `search.enabled: false` + * regardless of what the underlying protocol could serve — declared and + * enforced at the mount, not advertised past it. * * Before this key had a declared seat the REST layer honoured it anyway, * reading its config raw through a cast — and this schema (a non-strict @@ -109,7 +142,8 @@ export const RestApiConfigSchema = lazySchema(() => z.object({ * silently turned search back on. Declared here so the opt-out survives its * own contract's parse. */ - enableSearch: z.boolean().default(true).describe('Enable structured search endpoints (deployment-wide search opt-out)'), + enableSearch: z.boolean().default(true) + .describe('Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`)'), /** * Enable project-scoped routing (/api/v1/environments/:environmentId/data/...) @@ -214,6 +248,12 @@ export type CrudOperation = z.input; /** * CRUD Endpoints Configuration Schema * Configuration for automatic CRUD endpoint generation + * + * Reachability: EMBEDDER-ONLY (#15543). Every key below is parsed and read at + * construction, and none of them is authorable from a CLI-started deployment — + * `os serve` passes only the two `api.*` keys named in the file header and the + * dev plugin passes nothing. `operations.*` and `dataPrefix` are therefore + * whatever their defaults say unless a host constructs the config itself. */ export const CrudEndpointsConfigSchema = lazySchema(() => z.object({ /** @@ -276,6 +316,15 @@ export type CrudEndpointsConfigParsed = z.infer z.object({ * name, label, type, picklist options, formula, `visibleWhen` predicate, * `defaultValue`, nor the `requiredPermissions` capability guarding it. * - * `false` opts this server out and serves the full schema to every - * authenticated caller, as releases before ADR-0106 did. The change is - * **disclosure only**: the data plane masks values and refuses forbidden - * writes either way, and the console reads field affordances from - * `/auth/me/permissions`, so toggling it never changes UI correctness. + * `false` serves the full schema to every authenticated caller, as releases + * before ADR-0106 did — but ⛔ only an EMBEDDER can write that `false`. This + * key is not reachable from a CLI-started deployment at all (see WHO CAN + * WRITE THIS CONFIG in the file header), so under `os serve` and the dev + * plugin the mask is on and stays on. The change is **disclosure only**: the + * data plane masks values and refuses forbidden writes either way, and the + * console reads field affordances from `/auth/me/permissions`, so toggling it + * never changes UI correctness. * - * Deployment-wide counterpart: `OS_ALLOW_UNMASKED_OBJECT_METADATA=1`, which - * also covers the runtime `/metadata` dispatcher (that path has no per-server - * REST config to read). Either opt-out disables the mask; neither is needed - * to keep it on. + * The opt-out a DEPLOYMENT can actually reach is the env var + * `OS_ALLOW_UNMASKED_OBJECT_METADATA=1`, which also covers the runtime + * `/metadata` dispatcher (that path has no per-server REST config to read). + * Either opt-out disables the mask and neither is needed to keep it on — but + * of the two only the env var is reachable without embedding, so a deployment + * that must serve unmasked schemas sets the env var, not this key. */ maskObjectFields: z.boolean().default(true) .describe('[ADR-0106 D8] Mask served object schemas to the caller\'s readable fields'), @@ -414,6 +468,13 @@ export type MetadataEndpointsConfigParsed = z.infer z.object({ /** * CRUD endpoints configuration */ - crud: CrudEndpointsConfigSchema.optional().describe('CRUD endpoints configuration'), + crud: CrudEndpointsConfigSchema.optional() + .describe('CRUD endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin)'), /** * Metadata endpoints configuration */ - metadata: MetadataEndpointsConfigSchema.optional().describe('Metadata endpoints configuration'), + metadata: MetadataEndpointsConfigSchema.optional() + .describe('Metadata endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin)'), /** * Batch endpoints configuration */ - batch: BatchEndpointsConfigSchema.optional().describe('Batch endpoints configuration'), + batch: BatchEndpointsConfigSchema.optional() + .describe('Batch endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin)'), /** * Route generation configuration From c9f36331d0d0f1e7ec973777dd6c25e2d966df7e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 03:52:50 +0000 Subject: [PATCH 2/6] chore: add changeset for the RestServerConfig reachability prose Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- ...est-server-config-embedder-only-reachability.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .changeset/rest-server-config-embedder-only-reachability.md diff --git a/.changeset/rest-server-config-embedder-only-reachability.md b/.changeset/rest-server-config-embedder-only-reachability.md new file mode 100644 index 0000000000..71a6f7546d --- /dev/null +++ b/.changeset/rest-server-config-embedder-only-reachability.md @@ -0,0 +1,14 @@ +--- +"@objectstack/spec": patch +--- + +`RestServerConfig` now documents its own reachability: the `crud` / `metadata` / `batch` blocks are embedder-only, and the schema says so instead of implying a deployment posture nobody can author. + +`RestServerConfig` is the argument a host passes when it constructs the REST server, and both doors are programmatic — `createRestApiPlugin({ api })` and `plugin-hono-server`'s `restConfig`. No shipped boot path opens either with a config of its own: `os serve` passes a fixed argument carrying exactly two CLI-derived keys (`api.enableProjectScoping`, `api.projectResolution`), and the dev plugin passes none at all. So on a CLI-started deployment every other key is whatever its `.default()` says, with no flag, env var or config file that moves it — and until now the schema did not say so anywhere an operator would look. + +- **The file header gains a `WHO CAN WRITE THIS CONFIG` section**, which is the part that reaches the generated reference page, and the `crud` / `metadata` / `batch` sub-schemas each gain a `Reachability: EMBEDDER-ONLY` line. The three keys' entries on the parent `RestServerConfig` table say it too, so the fact survives into `content/docs/references/api/rest-server.mdx` rather than living only in the TS source. +- **`metadata.maskObjectFields`'s docblock is corrected.** It said `false` "opts this server out and serves the full schema to every authenticated caller" and offered the env var as a "deployment-wide counterpart", as if a deployment could pick either. Only an embedder can write the key; the opt-out a deployment can actually reach is `OS_ALLOW_UNMASKED_OBJECT_METADATA=1`. This is the one on the list that reads as a security control (ADR-0106 D8), which is why it is called out here. +- **`api.enableSearch` is corrected the same way.** Its docblock called it a "Deployment-wide switch" and its `describe()` a "deployment-wide search opt-out"; `os serve` does not thread it either, so it is embedder-only like the rest of the block apart from the two project-scoping keys. +- **The liveness ledger answers the ADR-0049 question in writing.** Every `live` row in `liveness/crud_endpoints.json`, `metadata_endpoints.json` and `batch_endpoints.json` gains a `REACHABILITY` sentence, and each file's `_note` carries the measurement once. `status` and `verifiedAt` are untouched on purpose: `live` answers who *reads* a key, reachability answers who can *set* it, and adding the second re-verified no call graph. + +No behaviour changes and no schema shape changes — no key, default, bound or refusal moves, so the accept set is byte-identical. This is prose plus ledger rows, and the regenerated `content/docs/references/api/rest-server.mdx` that follows from the `describe()` edits. From 9ffbde5b19227eee810e843aa075b67bede6769d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 03:59:53 +0000 Subject: [PATCH 3/6] docs(spec): regenerate the api/rest-server reference; keep the contributor rule off the published page Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- content/docs/references/api/rest-server.mdx | 33 +++++++++++++++++---- packages/spec/src/api/rest-server.zod.ts | 11 ++++--- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index 6ad446c2d3..83af51d309 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -22,6 +22,29 @@ Architecture alignment: - Microsoft Dynamics: Web API with entity operations - Strapi: Auto-generated REST endpoints +WHO CAN WRITE THIS CONFIG (#15543) — read this before planning a deployment +around any key below. A `RestServerConfig` is the ARGUMENT a host passes when +it constructs the server: never a stack collection member, never a stored +metadata row, never a file the CLI reads. Both doors are programmatic — +`createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`) and +`createHonoServerPlugin({ restConfig })` +(`packages/plugins/plugin-hono-server/src/hono-plugin.ts`). + +No shipped boot path opens either door with a config of its own. `os serve` +(`packages/cli/src/commands/serve.ts`) constructs the plugin with a fixed +argument carrying exactly two CLI-derived keys — `api.enableProjectScoping` +and `api.projectResolution` — and the dev plugin +(`packages/plugins/plugin-dev/src/dev-plugin.ts`) calls +`createRestApiPlugin()` with no config at all. So on a CLI-started deployment +every OTHER key here is EMBEDDER-ONLY: the whole of `crud`, `metadata` and +`batch`, and the rest of `api`. Its value is whatever the `.default()` below +says, and no flag, env var or config file moves it. That is the recorded +posture, not a gap awaiting a fix: the keys keep their runtime reads and +their embedder consumer, and the reachability answer ADR-0049 asks for is +written per key in the liveness ledger +(`packages/spec/liveness/crud_endpoints.json`, `metadata_endpoints.json`, +`batch_endpoints.json`). + **Source:** `packages/spec/src/api/rest-server.zod.ts` @@ -205,7 +228,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints | | **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint | | **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints | -| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (deployment-wide search opt-out) | +| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs | | **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy | | **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | @@ -242,9 +265,9 @@ const result = BatchEndpointsConfigSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **api** | `{ version: string; basePath: string; apiPath?: string; enableCrud: boolean; … }` | optional | REST API configuration | -| **crud** | `{ operations?: object; dataPrefix: string }` | optional | CRUD endpoints configuration | -| **metadata** | `{ prefix: string; enableCache: boolean; maskObjectFields: boolean; endpoints?: object }` | optional | Metadata endpoints configuration | -| **batch** | `{ maxBatchSize: integer; enableBatchEndpoint: boolean; operations?: object }` | optional | Batch endpoints configuration | +| **crud** | `{ operations?: object; dataPrefix: string }` | optional | CRUD endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin) | +| **metadata** | `{ prefix: string; enableCache: boolean; maskObjectFields: boolean; endpoints?: object }` | optional | Metadata endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin) | +| **batch** | `{ maxBatchSize: integer; enableBatchEndpoint: boolean; operations?: object }` | optional | Batch endpoints configuration (embedder-only: written by a host that constructs this config, never by `os serve` or the dev plugin) | | **routes** | `{ }` | optional | Route generation configuration | | **openApi31** | `never` | optional | [REMOVED] `RestServerConfig.openApi31` was removed in @objectstack/spec 17 (ADR-0049) — no runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. | @@ -261,7 +284,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableBatch** | `boolean` | optional (default: `true`) | Enable batch operation endpoints | | **enableDiscovery** | `boolean` | optional (default: `true`) | Enable API discovery endpoint | | **enableOpenApi** | `boolean` | optional (default: `true`) | Enable OpenAPI 3.1 spec & docs viewer endpoints | -| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (deployment-wide search opt-out) | +| **enableSearch** | `boolean` | optional (default: `true`) | Enable structured search endpoints (server-wide search opt-out; embedder-only, not settable from `os serve`) | | **enableProjectScoping** | `boolean` | optional (default: `false`) | Enable project-scoped routing for data/meta/AI APIs | | **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | optional (default: `"auto"`) | Project ID resolution strategy | | **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17. Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. Run `os migrate meta --from 16` to list the mechanical edits for existing sources; apply them by hand. | diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 87355424a2..2c21a6f108 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -44,12 +44,15 @@ import { retiredKey } from '../shared/retired-key'; * written per key in the liveness ledger * (`packages/spec/liveness/crud_endpoints.json`, `metadata_endpoints.json`, * `batch_endpoints.json`). - * - * ⛔ So a docblock here must never describe a key as a deployment posture - * without saying who can actually set it. An operator reading this page is - * entitled to learn from it that they cannot set these. */ +// ⛔ CONTRIBUTOR RULE, deliberately a line comment and not part of the docblock +// above: `build-docs.ts` renders the module docblock verbatim onto the public +// reference page, and this sentence is addressed to whoever edits this file, not +// to the operator reading that page. No docblock here may describe a key as a +// deployment posture without saying who can actually set it — an operator +// reading the schema is entitled to learn from it that they cannot set these. + // ========================================== // REST API Configuration // ========================================== From 167ef867ba2650e0b8f6bb9ef92e21c1c93a0380 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 05:20:18 +0000 Subject: [PATCH 4/6] wip: patch round 1 corrections before rebuild Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- ...erver-config-embedder-only-reachability.md | 11 ++- docs/qa/platform-checklist/FOLLOW-UPS.md | 2 +- .../platform-checklist/areas/api-backend.json | 26 +++++- packages/spec/liveness/batch_endpoints.json | 2 +- packages/spec/liveness/crud_endpoints.json | 2 +- .../spec/liveness/metadata_endpoints.json | 4 +- packages/spec/src/api/rest-server.zod.ts | 87 ++++++++++++------- 7 files changed, 89 insertions(+), 45 deletions(-) diff --git a/.changeset/rest-server-config-embedder-only-reachability.md b/.changeset/rest-server-config-embedder-only-reachability.md index 71a6f7546d..dd40abf04e 100644 --- a/.changeset/rest-server-config-embedder-only-reachability.md +++ b/.changeset/rest-server-config-embedder-only-reachability.md @@ -4,11 +4,16 @@ `RestServerConfig` now documents its own reachability: the `crud` / `metadata` / `batch` blocks are embedder-only, and the schema says so instead of implying a deployment posture nobody can author. -`RestServerConfig` is the argument a host passes when it constructs the REST server, and both doors are programmatic — `createRestApiPlugin({ api })` and `plugin-hono-server`'s `restConfig`. No shipped boot path opens either with a config of its own: `os serve` passes a fixed argument carrying exactly two CLI-derived keys (`api.enableProjectScoping`, `api.projectResolution`), and the dev plugin passes none at all. So on a CLI-started deployment every other key is whatever its `.default()` says, with no flag, env var or config file that moves it — and until now the schema did not say so anywhere an operator would look. +`RestServerConfig` is the argument a host passes when it constructs the REST server, and there is exactly one door: `createRestApiPlugin({ api })`, whose `start()` is the only non-test site that reaches `new RestServer(...)`. No shipped boot path opens it with a config of its own — `os serve` reads the stack config's own top-level `api:` block and forwards exactly two keys out of it (`api.enableProjectScoping`, `api.projectResolution`), and the dev plugin passes none at all. So on a CLI-started deployment every other key is whatever its `.default()` says, and until now the schema did not say so anywhere an operator would look. - **The file header gains a `WHO CAN WRITE THIS CONFIG` section**, which is the part that reaches the generated reference page, and the `crud` / `metadata` / `batch` sub-schemas each gain a `Reachability: EMBEDDER-ONLY` line. The three keys' entries on the parent `RestServerConfig` table say it too, so the fact survives into `content/docs/references/api/rest-server.mdx` rather than living only in the TS source. -- **`metadata.maskObjectFields`'s docblock is corrected.** It said `false` "opts this server out and serves the full schema to every authenticated caller" and offered the env var as a "deployment-wide counterpart", as if a deployment could pick either. Only an embedder can write the key; the opt-out a deployment can actually reach is `OS_ALLOW_UNMASKED_OBJECT_METADATA=1`. This is the one on the list that reads as a security control (ADR-0106 D8), which is why it is called out here. -- **`api.enableSearch` is corrected the same way.** Its docblock called it a "Deployment-wide switch" and its `describe()` a "deployment-wide search opt-out"; `os serve` does not thread it either, so it is embedder-only like the rest of the block apart from the two project-scoping keys. +- **One documented carve-out, and it is the security-relevant key.** `RestServer.normalizeConfig` folds the environment into the *effective* value of `metadata.maskObjectFields`: `OS_ALLOW_UNMASKED_OBJECT_METADATA` turns the ADR-0106 D8 mask off whatever the key says. That env var is the only thing outside an embedder's argument that moves any value in this schema, and it is now stated on the key, on the sub-schema and in the file header. +- **`metadata.maskObjectFields`'s docblock is corrected.** It said `false` "opts this server out and serves the full schema to every authenticated caller" and offered the env var as a "deployment-wide counterpart", as if a deployment could pick either. Only an embedder can write the key; a deployment reaches the mask only through the env var. +- **`api.enableSearch` is corrected the same way.** Its docblock called it a "Deployment-wide switch" and its `describe()` a "deployment-wide search opt-out"; `os serve` does not forward it either, so it is embedder-only like the rest of the block apart from the two project-scoping keys. - **The liveness ledger answers the ADR-0049 question in writing.** Every `live` row in `liveness/crud_endpoints.json`, `metadata_endpoints.json` and `batch_endpoints.json` gains a `REACHABILITY` sentence, and each file's `_note` carries the measurement once. `status` and `verifiedAt` are untouched on purpose: `live` answers who *reads* a key, reachability answers who can *set* it, and adding the second re-verified no call graph. +⚠️ **A correction to the record this change is built on.** An earlier draft of these sentences named a second door, `createHonoServerPlugin({ restConfig })`. No such function exists — a definition probe returns zero across the tree, against a positive control that finds `createRestApiPlugin` at `packages/rest/src/rest-api-plugin.ts:115`. `HonoServerPlugin` is a class that declares a `restConfig?: RestServerConfig` option whose single reader takes `api.basePath` for the SPA fallback; it never constructs a REST server, so it is not a door onto any of these keys. The claim was inherited from prose that was already in the tree, and on a card whose whole subject is a declared posture nobody can reach, publishing a declared door that does not exist would have been the same defect one level up. Every place this change touches now says the corrected thing. + +⚠️ **`batch.maxBatchSize` really does describe itself as deployment policy — in another package.** The phrase does not occur in `packages/spec/src/api/rest-server.zod.ts`, but it exists verbatim in the REST server: *"The cap is deployment policy — `RestServerConfig.batch.maxBatchSize` (1..1000, default 200)"* at `packages/rest/src/rest-server.ts:2071`. Same defect class, different package, and not touched here — it is owed to a follow-up in `packages/rest`. + No behaviour changes and no schema shape changes — no key, default, bound or refusal moves, so the accept set is byte-identical. This is prose plus ledger rows, and the regenerated `content/docs/references/api/rest-server.mdx` that follows from the `describe()` edits. diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index 8b56b929a7..3cee405e1d 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -568,7 +568,7 @@ card filed against it*. Recorded, not acted on — the channel question is #1173 | # | finding | evidence | captured in | handling | |---|---|---|---|---| | E1 | **`metadata.endpoints.items` gates four routes, three of which its declared meaning does not cover** — its `describe()` says "GET /meta/:type — List items of type", and it also gates `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and the **`POST {prefix}/_migrate-stored` write door**. An operator switching off a listing read silently disarms a migration door and the cross-type spec-validation sweep. `endpoints.item` is milder but the same shape: it also takes `{prefix}/book/:name/tree`. | `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` (four `endpoints.items` gates, four `endpoints.item` gates) vs `packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema` (one route named per switch) | api-backend.rest-metadata-config-contract (a clause requires the run to ENUMERATE each switch's real radius) | design/docs — filed as #15542, **RULED and closed**: every `endpoints.*` switch now gates exactly the face its name states. The whole-store family (`/diagnostics`, `/_drafts`, `POST {prefix}/_migrate-stored`) moved to a new key `maintenance`; `items` is down to its one declared mount; `item` gained the per-item `PUT`/`DELETE` and the history family it never gated (the converse mismatch, filed as #15854 and landed in the same PR); all four `describe()` strings now enumerate their mounts. ⛔ The checklist clause is **kept**, not retired — the run still ENUMERATES each switch's real radius from a route-table diff, and `areas/api-backend.json` revision 2 carries the new expected sets. | -| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code (`createRestApiPlugin({ api })`, `createHonoServerPlugin({ restConfig })`). A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this: the card's and the ruling's citation of `batch.maxBatchSize`'s docblock calling the cap *"deployment policy"* is **wrong — that phrase never occurred in the file**; the one real false-posture sentence was `metadata.maskObjectFields`'s `false` *"opts this server out"*, and the sweep found a third carrier the card missed, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not thread either. All three are repaired. | +| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code — through the one real door, `createRestApiPlugin({ api })`. ⚠️ **This row used to name a second door, `createHonoServerPlugin({ restConfig })`; no such function exists** (definition probe: zero across the tree, against a positive control that finds `createRestApiPlugin` at `packages/rest/src/rest-api-plugin.ts:115`). `HonoServerPlugin` is a class whose `restConfig` option has one reader taking `api.basePath` for the SPA fallback, and it never constructs a REST server. Corrected 2026-09-08 with #15543. A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this, and the correction is about WHERE, not whether. The card and the ruling cite `batch.maxBatchSize`'s docblock as calling the cap *"deployment policy"* and attribute it to `packages/spec/src/api/rest-server.zod.ts`; the phrase **does not occur in that file** (control: `maxBatchSize` occurs there 3 times, so the zero is a real zero). It **does exist**, verbatim, in the REST server: *"The cap is deployment policy — `RestServerConfig.batch.maxBatchSize` (1..1000, default 200)"* at `packages/rest/src/rest-server.ts:2071`. ⇒ **The ruling misattributed the sentence to the wrong file; it did not invent it.** That carrier is real, is the same defect class, and is **owed to a successor card in `domain:rest`** — this PR does not touch `packages/rest`. Inside `packages/spec/src/api/rest-server.zod.ts` the population is: `metadata.maskObjectFields`'s `false` *"opts this server out"* (the ruling's one correctly-placed citation), plus a carrier the card missed entirely, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not forward either. Both are repaired here. | | E3 | **The MOUNT half of every sub-config switch is unpinned.** `packages/rest/src/rest-sub-config-parse-not-cast.test.ts` pins what a switch normalizes to, and `rest-batch-size-cap.test.ts` pins the cap's effect; nothing asserts that a `false` switch removes its route from the table `getRoutes()` returns. The declared-not-enforced direction — a switch that normalizes correctly and gates nothing — is exactly what no current test would catch. ⚠️ The card said **nine** switches; re-measured on `cc5b3dd0c27` the mount-gating population is **nineteen** — the twelve sub-config switches the card enumerates (its own list adds to twelve, not nine) plus the seven `api.enable*` gates in `registerRoutes`, which are the same seam and were equally unpinned. | the two test files above; the gates live in `registerCrudEndpoints` / `registerBatchEndpoints` / `registerMetadataEndpointsInner` / `registerRoutes` | the three config items (the mount clauses, each with the gap named in `knownGaps`) | test gap — filed as #15544, **closed by `packages/rest/src/rest-config-mount-table.pin.test.ts`**: all nineteen gates pinned as a set difference against the all-true baseline, each with its presence twin (**twenty** since #15542 added `metadata.endpoints.maintenance`; the pin's §0 count moves deliberately with each switch added or retired). ⚠️ The three config items' `knownGaps` still say the harness is the only observation — stale in the good direction, refresh pending (`areas/api-backend.json` was held by another branch when this landed); `rest-metadata-config-contract`'s `automated.ref` now names this pin, the other two are still owed. | ### 10c. Checked and CLEAN (so the next sweep does not re-derive) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index f4af0b4101..39bb0760ce 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1966,7 +1966,7 @@ "title": "RestServerConfig.crud is the CRUD surface's construction contract: five switches gate six mounts, dataPrefix moves the routes and their discovery advertisement together, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -1980,7 +1980,7 @@ "a scratch vitest harness in packages/rest for the variant half: construct a RestServer with the config under test, call registerRoutes(), and read the route table back with getRoutes() — packages/rest/src/rest-batch-size-cap.test.ts is the shape to copy" ], "knownGaps": [ - "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) or createHonoServerPlugin({ restConfig }) in embedder code. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", + "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) in embedder code. CORRECTED 2026-09-08 (#15543): this gap used to name a second door, createHonoServerPlugin({ restConfig }) — no such function exists (definition probe: zero across the tree, positive control finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts:115). HonoServerPlugin is a class whose restConfig option has one reader taking api.basePath for the SPA fallback; it never constructs a REST server, so a harness must not try to reach these keys through it. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", "the MOUNT half has no permanent pin: packages/rest/src/rest-sub-config-parse-not-cast.test.ts asserts the NORMALIZED CONFIG a switch produces, and nothing asserts that the route disappears from the route table — the scratch harness is the only observation until that pin exists (recorded in FOLLOW-UPS.md)" ] }, @@ -2068,6 +2068,12 @@ "date": "2026-09-04", "change": "new — `crud_endpoints` was UNCLASSIFIED in coverage.json: the checklist drove the mounted CRUD routes (query-contract-matrix, crud-roundtrip) but nothing covered the deployment config that decides WHICH of them exist, what they are mounted under, or that the two retired keys refuse. Authored against the shipped surface rather than the ledger's prose: the six-mounts-from-five-switches asymmetry (POST /query rides `operations.list`) and the discovery co-movement were read out of registerCrudEndpoints and registerDiscoveryEndpoints", "ref": "#14961" + }, + { + "revision": 2, + "date": "2026-09-08", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "ref": "#15543" } ] }, @@ -2076,7 +2082,7 @@ "title": "RestServerConfig.batch is the bulk surface's construction contract: three switches and enableBatchEndpoint gate four per-object mounts (each ANDed with its protocol member), maxBatchSize is the one cap every bulk door measures, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 1, + "revision": 2, "priority": "P2", "surface": "api", "personas": [ @@ -2185,6 +2191,12 @@ "date": "2026-09-04", "change": "new — `batch_endpoints` was UNCLASSIFIED in coverage.json: two items drove the bulk doors' behaviour on a default boot and neither covered the sub-object that mounts them. Two asymmetries were read out of registerBatchEndpoints rather than assumed: the cross-object POST /batch is NOT under enableBatchEndpoint, and all four per-object gates are ANDs with a protocol member, so a missing protocol member and a false switch are indistinguishable from the route table alone", "ref": "#14961" + }, + { + "revision": 2, + "date": "2026-09-08", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "ref": "#15543" } ] }, @@ -2193,7 +2205,7 @@ "title": "RestServerConfig.metadata is the metadata surface's construction contract: prefix moves eleven mounts and their discovery advertisement, four endpoint switches each gate exactly the face its name states (#15542/#15854), maskObjectFields is the ADR-0106 D8 disclosure gate, and both tombstones refuse at construction", "since": "v17", "status": "active", - "revision": 2, + "revision": 3, "priority": "P1", "surface": "api", "personas": [ @@ -2309,6 +2321,12 @@ "date": "2026-09-06", "change": "the radii this item enumerates MOVED, by ruling rather than by drift (#15542 comment 5557095147): every endpoints.* switch now gates exactly the face its name states. The whole-store operations (/diagnostics, /_drafts, POST /_migrate-stored) left `items` for a new key `maintenance`; `items` is down to its one declared mount; `item` gained the per-item PUT/DELETE and the history family it never gated. The acceptance clause is kept SATISFIABLE and non-vacuous exactly as the ruling requires — it still demands the run enumerate each switch's real radius from a route-table diff, and the new radii are what it enumerates; what changed is the expected sets, not the obligation to measure them", "ref": "#15542" + }, + { + "revision": 3, + "date": "2026-09-08", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "ref": "#15543" } ] }, diff --git a/packages/spec/liveness/batch_endpoints.json b/packages/spec/liveness/batch_endpoints.json index 40d4282b3d..f476219a7e 100644 --- a/packages/spec/liveness/batch_endpoints.json +++ b/packages/spec/liveness/batch_endpoints.json @@ -1,6 +1,6 @@ { "type": "batch_endpoints", - "_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", + "_note": "BatchEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#BatchEndpointsConfigSchema, the `batch` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `maxBatchSize`, `enableBatchEndpoint` and three of the four `operations.*` switches are read; `operations.upsertMany` and `defaultAtomic` are not. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and there is exactly ONE door, programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts), whose start() is the only non-test site reaching `new RestServer(...)`. CORRECTED 2026-09-08: an earlier draft of this sentence named a second door, createHonoServerPlugin({ restConfig }) \u2014 NO SUCH FUNCTION EXISTS. A definition probe returns zero across the tree against a positive control that finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts:115. The real shape: HonoServerPlugin is a CLASS (packages/plugins/plugin-hono-server/src/hono-plugin.ts:222) that declares a restConfig?: RestServerConfig option whose single reader (hono-plugin.ts:595) takes api.basePath for the SPA fallback; it never constructs a REST server, so it is not a door onto any key in this file. No shipped boot path opens the one real door with a config of its own: packages/cli/src/commands/serve.ts reads the stack config's own top-level `api:` block and forwards exactly two keys out of it (api.enableProjectScoping and api.projectResolution, serve.ts:3966-3968) into a fixed argument, through an `as any` cast; packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So the CLI does read a config file, it just forwards those two keys and nothing else, and every key in this file is EMBEDDER-ONLY. A CLI-started deployment therefore always gets the schema default for every key here. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "maxBatchSize": { "status": "live", diff --git a/packages/spec/liveness/crud_endpoints.json b/packages/spec/liveness/crud_endpoints.json index 49a368b361..f1054bc4d6 100644 --- a/packages/spec/liveness/crud_endpoints.json +++ b/packages/spec/liveness/crud_endpoints.json @@ -1,6 +1,6 @@ { "type": "crud_endpoints", - "_note": "CrudEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema, the `crud` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `operations.*` and `dataPrefix` gate and shape the mounted CRUD surface, while `patterns` and `objectParamStyle` are normalized and never read. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", + "_note": "CrudEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#CrudEndpointsConfigSchema, the `crud` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `operations.*` and `dataPrefix` gate and shape the mounted CRUD surface, while `patterns` and `objectParamStyle` are normalized and never read. This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and there is exactly ONE door, programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts), whose start() is the only non-test site reaching `new RestServer(...)`. CORRECTED 2026-09-08: an earlier draft of this sentence named a second door, createHonoServerPlugin({ restConfig }) \u2014 NO SUCH FUNCTION EXISTS. A definition probe returns zero across the tree against a positive control that finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts:115. The real shape: HonoServerPlugin is a CLASS (packages/plugins/plugin-hono-server/src/hono-plugin.ts:222) that declares a restConfig?: RestServerConfig option whose single reader (hono-plugin.ts:595) takes api.basePath for the SPA fallback; it never constructs a REST server, so it is not a door onto any key in this file. No shipped boot path opens the one real door with a config of its own: packages/cli/src/commands/serve.ts reads the stack config's own top-level `api:` block and forwards exactly two keys out of it (api.enableProjectScoping and api.projectResolution, serve.ts:3966-3968) into a fixed argument, through an `as any` cast; packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So the CLI does read a config file, it just forwards those two keys and nothing else, and every key in this file is EMBEDDER-ONLY. A CLI-started deployment therefore always gets the schema default for every key here. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "operations": { "children": { diff --git a/packages/spec/liveness/metadata_endpoints.json b/packages/spec/liveness/metadata_endpoints.json index 2fff0d4ca4..aeb2859b53 100644 --- a/packages/spec/liveness/metadata_endpoints.json +++ b/packages/spec/liveness/metadata_endpoints.json @@ -1,6 +1,6 @@ { "type": "metadata_endpoints", - "_note": "MetadataEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema, the `metadata` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `prefix`, `enableCache`, `maskObjectFields` and four of the five `endpoints.*` switches are read; `cacheTtl` and `endpoints.schema` are not. (`endpoints.maintenance` was added 2026-09-06 by #15542 and is read from its first commit \u2014 see its row.) This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and both doors are programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts) and createHonoServerPlugin({ restConfig }) (packages/plugins/plugin-hono-server/src/hono-plugin.ts). No shipped boot path opens either with a config of its own: packages/cli/src/commands/serve.ts constructs the plugin with a fixed argument carrying exactly two CLI-derived keys (api.enableProjectScoping and api.projectResolution, and through an `as any` cast at that), and packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So every key in this file is EMBEDDER-ONLY and a CLI-started deployment always gets the schema default. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", + "_note": "MetadataEndpointsConfigSchema — packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema, the `metadata` sub-object of RestServerConfig. It is not a metadata type, not a request body and not a manifest: it is part of the REST server's CONSTRUCTION ARGUMENT, so no registry has ever held it and no ratchet rooted in one could ask who reads it. The ledger governs it through the gate's SPEC_ONLY_SCHEMAS override, the same route `query` / `qa` / `manifest` take; check-liveness.mts carries the rationale, including why the four sub-objects are rooted separately instead of the whole RestServerConfigSchema (the walk drills one level, and rooting on the whole config would leave `metadata.endpoints.schema` and `batch.operations.upsertMany` with no row of their own). Seeded 2026-09-02 from the census filed with #14369, which is the second half of #11984's measurement: that PR made RestServer.normalizeConfig PARSE and CONSUME this sub-object instead of casting it. That settles accept/reject — an out-of-enum or out-of-range value is now refused at construction instead of sitting in the normalized config as if it were declared — and that is ALL it settles. Executing a declared contract does not give a key a consumer, which is exactly the distinction this file records. Mixed: `prefix`, `enableCache`, `maskObjectFields` and four of the five `endpoints.*` switches are read; `cacheTtl` and `endpoints.schema` are not. (`endpoints.maintenance` was added 2026-09-06 by #15542 and is read from its first commit \u2014 see its row.) This file RECORDS status; it decides nothing. The enforce-or-remove call per dead key (ADR-0049) is a follow-up on the human floor — the enforce route is a feature per key, and for a key that is published in an `@example` or in the generated reference docs the remove route is a capability retirement, not a tidy-up. Census method and scope, re-run at 2514d49f3 (2026-09-02): read sites in packages/rest/src non-test sources, excluding NormalizedRestServerConfig's type declaration and normalizeConfig itself (a key the normalizer writes into its own output is not thereby read); comments excluded; plus a repo-wide grep outside packages/spec and rest-server.ts, which finds only changesets, the generated reference docs and the #11984 refusal tests. objectui @d4c6a86 is clean (0 hits for every key here). The closed cloud runtime was not reachable from the measuring container, so the declared scope stays in-repo rather than claiming a sweep that was not run. AUTHOR-WARN CHANNEL: none exists for this type, and no entry here is marked `authorWarn` for that reason (`_authorWarnSkipped`). The CLI lint (packages/lint/src/lint-liveness-properties.ts) walks stack COLLECTIONS — `stack.flows`, `stack.views`, … — and a RestServerConfig is not part of a stack at all: it is the argument a host passes when it constructs the server. Marking an entry `authorWarn` here would produce a warning nothing can emit, which is the same silent no-op this ledger exists to catch, so the dead entries below carry their correction in `note` and the construction-time parse (#11984) is what actually reaches the author — for accept/reject, which is a different question from liveness. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): every `live` row in this file now carries a REACHABILITY sentence, and they all say the same thing because it is one measurement. A RestServerConfig is the ARGUMENT a host passes when it constructs the server, and there is exactly ONE door, programmatic: createRestApiPlugin({ api }) (packages/rest/src/rest-api-plugin.ts), whose start() is the only non-test site reaching `new RestServer(...)`. CORRECTED 2026-09-08: an earlier draft of this sentence named a second door, createHonoServerPlugin({ restConfig }) \u2014 NO SUCH FUNCTION EXISTS. A definition probe returns zero across the tree against a positive control that finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts:115. The real shape: HonoServerPlugin is a CLASS (packages/plugins/plugin-hono-server/src/hono-plugin.ts:222) that declares a restConfig?: RestServerConfig option whose single reader (hono-plugin.ts:595) takes api.basePath for the SPA fallback; it never constructs a REST server, so it is not a door onto any key in this file. No shipped boot path opens the one real door with a config of its own: packages/cli/src/commands/serve.ts reads the stack config's own top-level `api:` block and forwards exactly two keys out of it (api.enableProjectScoping and api.projectResolution, serve.ts:3966-3968) into a fixed argument, through an `as any` cast; packages/plugins/plugin-dev/src/dev-plugin.ts calls createRestApiPlugin() with no config at all. So the CLI does read a config file, it just forwards those two keys and nothing else, and every key in this file is EMBEDDER-ONLY. A CLI-started deployment therefore gets the schema default for every key here, with ONE carve-out that is also the security-relevant one: RestServer.normalizeConfig folds the environment into the EFFECTIVE value of maskObjectFields (packages/rest/src/rest-server.ts:4156 -> isObjectSchemaMaskingEnabled, packages/metadata-core/src/object-schema-fls.ts:198), so OS_ALLOW_UNMASKED_OBJECT_METADATA turns the ADR-0106 D8 mask off for a deployment that cannot reach the key. That env var is the only thing outside an embedder's argument that moves any value in this file. That is the RECORDED POSTURE, not a gap awaiting a fix: ruled 2026-09-07 (director seat, summon #17, decision batch #2) that the keys stay and keep their runtime reads, that threading a config through is not taken (a new authorable surface for no measured demand) and that retiring them is refused (they have an embedder consumer); the docblocks in packages/spec/src/api/rest-server.zod.ts state the reachability plainly, and this ledger is where the ADR-0049 question gets its written per-key answer. WHY REACHABILITY IS A SEPARATE AXIS FROM `status`: `live` means the runtime READS the key, which is the only thing this ledger's statuses classify, and it is correct here. Reachability answers who can WRITE it, which `live` never answered and which this card, #15542 and the three api-backend.rest-*-config-contract checklist items each had to re-derive from the boot paths because no file recorded it. The two never substitute for each other, and adding these sentences re-verified no call graph, so `verifiedAt` is deliberately NOT bumped by this edit.", "props": { "prefix": { "status": "live", @@ -30,7 +30,7 @@ "evidenceScope": "in-repo", "evidence": "packages/rest/src/rest-server.ts#resolveObjectMasker (`if (metaType !== 'object' || !this.config.metadata.maskObjectFields)` — false serves the full object schema to every authenticated caller, ADR-0106 D8)", "producer": "packages/rest/src/rest-server.ts#normalizeConfig (threads the authored value into `this.config`, which is the object every consumer below reads; the parsed sub-config's own `.default()`s supply the value when the author omits the key)", - "note": "Security-shaped and genuinely enforced: the opt-out reaches the masker, and `isObjectSchemaMaskingEnabled` also honours the OS_ALLOW_UNMASKED_OBJECT_METADATA deployment escape hatch that the runtime /metadata dispatcher shares. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." + "note": "Security-shaped and genuinely enforced: the opt-out reaches the masker, and `isObjectSchemaMaskingEnabled` also honours the OS_ALLOW_UNMASKED_OBJECT_METADATA deployment escape hatch that the runtime /metadata dispatcher shares. REACHABILITY (#15543, measured 2026-09-08 on origin/main 8ccf7a1df): the KEY is embedder-only \u2014 written only by a host that constructs a RestServerConfig, and no shipped boot path authors it, so under os serve and the dev plugin it is always true. \u26a0\ufe0f But the EFFECTIVE value is not the key: RestServer.normalizeConfig writes isObjectSchemaMaskingEnabled(metadata.maskObjectFields) (packages/rest/src/rest-server.ts:4156, packages/metadata-core/src/object-schema-fls.ts:198), so OS_ALLOW_UNMASKED_OBJECT_METADATA turns the ADR-0106 D8 mask OFF whatever the key says. That env var is the one opt-out a CLI-started deployment can reach, and this is the only key in this file whose effective value anything outside an embedder's argument moves. See this file's _note REACHABILITY paragraph. Liveness and verifiedAt are unaffected: `live` answers who READS this key, this row answers who can SET it." }, "endpoints": { "children": { diff --git a/packages/spec/src/api/rest-server.zod.ts b/packages/spec/src/api/rest-server.zod.ts index 2c21a6f108..49cdae4572 100644 --- a/packages/spec/src/api/rest-server.zod.ts +++ b/packages/spec/src/api/rest-server.zod.ts @@ -24,24 +24,41 @@ import { retiredKey } from '../shared/retired-key'; * * WHO CAN WRITE THIS CONFIG (#15543) — read this before planning a deployment * around any key below. A `RestServerConfig` is the ARGUMENT a host passes when - * it constructs the server: never a stack collection member, never a stored - * metadata row, never a file the CLI reads. Both doors are programmatic — - * `createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`) and - * `createHonoServerPlugin({ restConfig })` - * (`packages/plugins/plugin-hono-server/src/hono-plugin.ts`). + * it constructs the server: never a stack collection member and never a stored + * metadata row. * - * No shipped boot path opens either door with a config of its own. `os serve` - * (`packages/cli/src/commands/serve.ts`) constructs the plugin with a fixed - * argument carrying exactly two CLI-derived keys — `api.enableProjectScoping` - * and `api.projectResolution` — and the dev plugin + * There is exactly ONE door, and it is programmatic: + * `createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`), + * whose `start()` is the only non-test site that reaches `new RestServer(...)`. + * ⚠️ `HonoServerPlugin` (`packages/plugins/plugin-hono-server`) declares a + * `restConfig?: RestServerConfig` option, but it is NOT a second door: its one + * reader takes `api.basePath` for the SPA fallback, and that plugin never + * constructs a REST server, so nothing below `api.basePath` reaches anything. + * + * No shipped boot path opens that door with a config of its own. `os serve` + * (`packages/cli/src/commands/serve.ts`) reads the stack config's own top-level + * `api:` block and forwards exactly two keys out of it — + * `api.enableProjectScoping` and `api.projectResolution` — into a fixed + * argument, through an `as any` cast; the dev plugin * (`packages/plugins/plugin-dev/src/dev-plugin.ts`) calls - * `createRestApiPlugin()` with no config at all. So on a CLI-started deployment - * every OTHER key here is EMBEDDER-ONLY: the whole of `crud`, `metadata` and - * `batch`, and the rest of `api`. Its value is whatever the `.default()` below - * says, and no flag, env var or config file moves it. That is the recorded - * posture, not a gap awaiting a fix: the keys keep their runtime reads and - * their embedder consumer, and the reachability answer ADR-0049 asks for is - * written per key in the liveness ledger + * `createRestApiPlugin()` with no config at all. So the CLI does read a config + * file — it just forwards those two keys and nothing else. + * + * ⇒ On a CLI-started deployment every OTHER key here is EMBEDDER-ONLY: the + * whole of `crud`, `metadata` and `batch`, and the rest of `api`. Its value is + * whatever the `.default()` below says, and no flag, config file or CLI option + * moves it. + * + * ⚠️ ONE CARVE-OUT, and it is the security-relevant key: the REST server's + * `normalizeConfig` folds the environment into the effective value of + * `metadata.maskObjectFields` — `OS_ALLOW_UNMASKED_OBJECT_METADATA` turns the + * ADR-0106 D8 mask OFF for a deployment that cannot otherwise reach the key. + * That env var is the only thing outside an embedder's argument that changes + * any value here. See `maskObjectFields` below. + * + * This is the recorded posture, not a gap awaiting a fix: the keys keep their + * runtime reads and their embedder consumer, and the reachability answer + * ADR-0049 asks for is written per key in the liveness ledger * (`packages/spec/liveness/crud_endpoints.json`, `metadata_endpoints.json`, * `batch_endpoints.json`). */ @@ -322,12 +339,13 @@ export type CrudEndpointsConfigParsed = z.infer z.object({ * * `false` serves the full schema to every authenticated caller, as releases * before ADR-0106 did — but ⛔ only an EMBEDDER can write that `false`. This - * key is not reachable from a CLI-started deployment at all (see WHO CAN + * KEY is not reachable from a CLI-started deployment at all (see WHO CAN * WRITE THIS CONFIG in the file header), so under `os serve` and the dev - * plugin the mask is on and stays on. The change is **disclosure only**: the - * data plane masks values and refuses forbidden writes either way, and the - * console reads field affordances from `/auth/me/permissions`, so toggling it - * never changes UI correctness. + * plugin it is always `true`. The change is **disclosure only**: the data + * plane masks values and refuses forbidden writes either way, and the console + * reads field affordances from `/auth/me/permissions`, so toggling it never + * changes UI correctness. * - * The opt-out a DEPLOYMENT can actually reach is the env var - * `OS_ALLOW_UNMASKED_OBJECT_METADATA=1`, which also covers the runtime - * `/metadata` dispatcher (that path has no per-server REST config to read). - * Either opt-out disables the mask and neither is needed to keep it on — but - * of the two only the env var is reachable without embedding, so a deployment - * that must serve unmasked schemas sets the env var, not this key. + * ⚠️ The key being `true` is NOT the same as the mask being on. The REST + * server's `normalizeConfig` folds the environment into the effective value: + * `OS_ALLOW_UNMASKED_OBJECT_METADATA` turns the mask OFF whatever this key + * says, and it also covers the runtime `/metadata` dispatcher (that path has + * no per-server REST config to read). ⇒ Under `os serve` and the dev plugin + * the mask is on UNLESS that env var is set — it is the one opt-out a + * deployment can reach, and the one it must use, because this key is not + * reachable without embedding. Either opt-out disables the mask; neither is + * needed to keep it on. */ maskObjectFields: z.boolean().default(true) .describe('[ADR-0106 D8] Mask served object schemas to the caller\'s readable fields'), From 506e5c86404ebe5d74fb30abcdb27379ca9bc03f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 05:29:07 +0000 Subject: [PATCH 5/6] docs(spec): regenerate api/rest-server reference for the corrected reachability prose Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- content/docs/references/api/rest-server.mdx | 51 ++++++++++++++------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index 83af51d309..fa5e13fccb 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -24,24 +24,41 @@ Architecture alignment: WHO CAN WRITE THIS CONFIG (#15543) — read this before planning a deployment around any key below. A `RestServerConfig` is the ARGUMENT a host passes when -it constructs the server: never a stack collection member, never a stored -metadata row, never a file the CLI reads. Both doors are programmatic — -`createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`) and -`createHonoServerPlugin({ restConfig })` -(`packages/plugins/plugin-hono-server/src/hono-plugin.ts`). - -No shipped boot path opens either door with a config of its own. `os serve` -(`packages/cli/src/commands/serve.ts`) constructs the plugin with a fixed -argument carrying exactly two CLI-derived keys — `api.enableProjectScoping` -and `api.projectResolution` — and the dev plugin +it constructs the server: never a stack collection member and never a stored +metadata row. + +There is exactly ONE door, and it is programmatic: +`createRestApiPlugin({ api })` (`packages/rest/src/rest-api-plugin.ts`), +whose `start()` is the only non-test site that reaches `new RestServer(...)`. +⚠️ `HonoServerPlugin` (`packages/plugins/plugin-hono-server`) declares a +`restConfig?: RestServerConfig` option, but it is NOT a second door: its one +reader takes `api.basePath` for the SPA fallback, and that plugin never +constructs a REST server, so nothing below `api.basePath` reaches anything. + +No shipped boot path opens that door with a config of its own. `os serve` +(`packages/cli/src/commands/serve.ts`) reads the stack config's own top-level +`api:` block and forwards exactly two keys out of it — +`api.enableProjectScoping` and `api.projectResolution` — into a fixed +argument, through an `as any` cast; the dev plugin (`packages/plugins/plugin-dev/src/dev-plugin.ts`) calls -`createRestApiPlugin()` with no config at all. So on a CLI-started deployment -every OTHER key here is EMBEDDER-ONLY: the whole of `crud`, `metadata` and -`batch`, and the rest of `api`. Its value is whatever the `.default()` below -says, and no flag, env var or config file moves it. That is the recorded -posture, not a gap awaiting a fix: the keys keep their runtime reads and -their embedder consumer, and the reachability answer ADR-0049 asks for is -written per key in the liveness ledger +`createRestApiPlugin()` with no config at all. So the CLI does read a config +file — it just forwards those two keys and nothing else. + +⇒ On a CLI-started deployment every OTHER key here is EMBEDDER-ONLY: the +whole of `crud`, `metadata` and `batch`, and the rest of `api`. Its value is +whatever the `.default()` below says, and no flag, config file or CLI option +moves it. + +⚠️ ONE CARVE-OUT, and it is the security-relevant key: the REST server's +`normalizeConfig` folds the environment into the effective value of +`metadata.maskObjectFields` — `OS_ALLOW_UNMASKED_OBJECT_METADATA` turns the +ADR-0106 D8 mask OFF for a deployment that cannot otherwise reach the key. +That env var is the only thing outside an embedder's argument that changes +any value here. See `maskObjectFields` below. + +This is the recorded posture, not a gap awaiting a fix: the keys keep their +runtime reads and their embedder consumer, and the reachability answer +ADR-0049 asks for is written per key in the liveness ledger (`packages/spec/liveness/crud_endpoints.json`, `metadata_endpoints.json`, `batch_endpoints.json`). From b8cc67dabd77b2bd222981fc5e3543e71486953c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 8 Sep 2026 05:43:06 +0000 Subject: [PATCH 6/6] docs(qa): cite file plus symbol, never a source line number, in the checklist prose Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude --- docs/qa/platform-checklist/FOLLOW-UPS.md | 2 +- docs/qa/platform-checklist/areas/api-backend.json | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/qa/platform-checklist/FOLLOW-UPS.md b/docs/qa/platform-checklist/FOLLOW-UPS.md index 3cee405e1d..495bb8163e 100644 --- a/docs/qa/platform-checklist/FOLLOW-UPS.md +++ b/docs/qa/platform-checklist/FOLLOW-UPS.md @@ -568,7 +568,7 @@ card filed against it*. Recorded, not acted on — the channel question is #1173 | # | finding | evidence | captured in | handling | |---|---|---|---|---| | E1 | **`metadata.endpoints.items` gates four routes, three of which its declared meaning does not cover** — its `describe()` says "GET /meta/:type — List items of type", and it also gates `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and the **`POST {prefix}/_migrate-stored` write door**. An operator switching off a listing read silently disarms a migration door and the cross-type spec-validation sweep. `endpoints.item` is milder but the same shape: it also takes `{prefix}/book/:name/tree`. | `packages/rest/src/rest-server.ts#registerMetadataEndpointsInner` (four `endpoints.items` gates, four `endpoints.item` gates) vs `packages/spec/src/api/rest-server.zod.ts#MetadataEndpointsConfigSchema` (one route named per switch) | api-backend.rest-metadata-config-contract (a clause requires the run to ENUMERATE each switch's real radius) | design/docs — filed as #15542, **RULED and closed**: every `endpoints.*` switch now gates exactly the face its name states. The whole-store family (`/diagnostics`, `/_drafts`, `POST {prefix}/_migrate-stored`) moved to a new key `maintenance`; `items` is down to its one declared mount; `item` gained the per-item `PUT`/`DELETE` and the history family it never gated (the converse mismatch, filed as #15854 and landed in the same PR); all four `describe()` strings now enumerate their mounts. ⛔ The checklist clause is **kept**, not retired — the run still ENUMERATES each switch's real radius from a route-table diff, and `areas/api-backend.json` revision 2 carries the new expected sets. | -| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code — through the one real door, `createRestApiPlugin({ api })`. ⚠️ **This row used to name a second door, `createHonoServerPlugin({ restConfig })`; no such function exists** (definition probe: zero across the tree, against a positive control that finds `createRestApiPlugin` at `packages/rest/src/rest-api-plugin.ts:115`). `HonoServerPlugin` is a class whose `restConfig` option has one reader taking `api.basePath` for the SPA fallback, and it never constructs a REST server. Corrected 2026-09-08 with #15543. A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this, and the correction is about WHERE, not whether. The card and the ruling cite `batch.maxBatchSize`'s docblock as calling the cap *"deployment policy"* and attribute it to `packages/spec/src/api/rest-server.zod.ts`; the phrase **does not occur in that file** (control: `maxBatchSize` occurs there 3 times, so the zero is a real zero). It **does exist**, verbatim, in the REST server: *"The cap is deployment policy — `RestServerConfig.batch.maxBatchSize` (1..1000, default 200)"* at `packages/rest/src/rest-server.ts:2071`. ⇒ **The ruling misattributed the sentence to the wrong file; it did not invent it.** That carrier is real, is the same defect class, and is **owed to a successor card in `domain:rest`** — this PR does not touch `packages/rest`. Inside `packages/spec/src/api/rest-server.zod.ts` the population is: `metadata.maskObjectFields`'s `false` *"opts this server out"* (the ruling's one correctly-placed citation), plus a carrier the card missed entirely, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not forward either. Both are repaired here. | +| E2 | **No shipped boot path authors `RestServerConfig` at all.** `os serve` constructs the REST plugin with a fixed config (only `enableProjectScoping` / `projectResolution` are threaded) and the dev plugin calls `createRestApiPlugin()` with none, so `crud` / `metadata` / `batch` / `routes` are reachable only from embedder code — through the one real door, `createRestApiPlugin({ api })`. ⚠️ **This row used to name a second door, `createHonoServerPlugin({ restConfig })`; no such function exists** (definition probe: zero across the tree, against a positive control that finds `createRestApiPlugin` at `packages/rest/src/rest-api-plugin.ts#createRestApiPlugin`). `HonoServerPlugin` is a class whose `restConfig` option has one reader taking `api.basePath` for the SPA fallback, and it never constructs a REST server. Corrected 2026-09-08 with #15543. A deployment cannot set `batch.maxBatchSize`, move `crud.dataPrefix`, or opt out of ADR-0106 D8 masking without embedding. | `packages/cli/src/commands/serve.ts` (the fixed construction) · `packages/plugins/plugin-dev/src/dev-plugin.ts` (no config) | the three config items' `knownGaps` — every non-default clause is scored `oracle: test` in a harness, and the run record must say so instead of claiming a reconfigured deployment | capability gap — filed as #15543, **RULED 2026-09-07** (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」): the keys **stay** and keep their runtime reads; threading a config through is not taken (a new authorable surface for no measured demand, and blocked on #15542) and retiring them is refused (they have an embedder consumer). ⇒ **This is a recorded posture, not an open gap**, so the harness scoring in the three items' `knownGaps` is the permanent method, not a workaround pending a fix. The reachability answer is now written down in two places instead of being re-derived per run: the schema docblocks (`packages/spec/src/api/rest-server.zod.ts` → WHO CAN WRITE THIS CONFIG, plus a per-sub-object line on `crud` / `metadata` / `batch`) and a per-key REACHABILITY row in `packages/spec/liveness/{crud,metadata,batch}_endpoints.json`. ⚠️ Re-derived while closing this, and the correction is about WHERE, not whether. The card and the ruling cite `batch.maxBatchSize`'s docblock as calling the cap *"deployment policy"* and attribute it to `packages/spec/src/api/rest-server.zod.ts`; the phrase **does not occur in that file** (control: `maxBatchSize` occurs there 3 times, so the zero is a real zero). It **does exist**, verbatim, in the REST server: *"The cap is deployment policy — `RestServerConfig.batch.maxBatchSize` (1..1000, default 200)"* at `packages/rest/src/rest-server.ts#enforceBatchSize`. ⇒ **The ruling misattributed the sentence to the wrong file; it did not invent it.** That carrier is real, is the same defect class, and is **owed to a successor card in `domain:rest`** — this PR does not touch `packages/rest`. Inside `packages/spec/src/api/rest-server.zod.ts` the population is: `metadata.maskObjectFields`'s `false` *"opts this server out"* (the ruling's one correctly-placed citation), plus a carrier the card missed entirely, `api.enableSearch` (*"Deployment-wide switch"* in its docblock and *"deployment-wide search opt-out"* in its `describe()`), which `os serve` does not forward either. Both are repaired here. | | E3 | **The MOUNT half of every sub-config switch is unpinned.** `packages/rest/src/rest-sub-config-parse-not-cast.test.ts` pins what a switch normalizes to, and `rest-batch-size-cap.test.ts` pins the cap's effect; nothing asserts that a `false` switch removes its route from the table `getRoutes()` returns. The declared-not-enforced direction — a switch that normalizes correctly and gates nothing — is exactly what no current test would catch. ⚠️ The card said **nine** switches; re-measured on `cc5b3dd0c27` the mount-gating population is **nineteen** — the twelve sub-config switches the card enumerates (its own list adds to twelve, not nine) plus the seven `api.enable*` gates in `registerRoutes`, which are the same seam and were equally unpinned. | the two test files above; the gates live in `registerCrudEndpoints` / `registerBatchEndpoints` / `registerMetadataEndpointsInner` / `registerRoutes` | the three config items (the mount clauses, each with the gap named in `knownGaps`) | test gap — filed as #15544, **closed by `packages/rest/src/rest-config-mount-table.pin.test.ts`**: all nineteen gates pinned as a set difference against the all-true baseline, each with its presence twin (**twenty** since #15542 added `metadata.endpoints.maintenance`; the pin's §0 count moves deliberately with each switch added or retired). ⚠️ The three config items' `knownGaps` still say the harness is the only observation — stale in the good direction, refresh pending (`areas/api-backend.json` was held by another branch when this landed); `rest-metadata-config-contract`'s `automated.ref` now names this pin, the other two are still owed. | ### 10c. Checked and CLEAN (so the next sweep does not re-derive) diff --git a/docs/qa/platform-checklist/areas/api-backend.json b/docs/qa/platform-checklist/areas/api-backend.json index 39bb0760ce..637461eccc 100644 --- a/docs/qa/platform-checklist/areas/api-backend.json +++ b/docs/qa/platform-checklist/areas/api-backend.json @@ -1980,7 +1980,7 @@ "a scratch vitest harness in packages/rest for the variant half: construct a RestServer with the config under test, call registerRoutes(), and read the route table back with getRoutes() — packages/rest/src/rest-batch-size-cap.test.ts is the shape to copy" ], "knownGaps": [ - "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) in embedder code. CORRECTED 2026-09-08 (#15543): this gap used to name a second door, createHonoServerPlugin({ restConfig }) — no such function exists (definition probe: zero across the tree, positive control finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts:115). HonoServerPlugin is a class whose restConfig option has one reader taking api.basePath for the SPA fallback; it never constructs a REST server, so a harness must not try to reach these keys through it. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", + "NO stock boot authors this sub-object. `os serve` constructs the REST plugin with a fixed config (only project scoping is threaded) and `os dev` passes none at all, so `crud.operations` / `crud.dataPrefix` are reachable only through createRestApiPlugin({ api: ... }) in embedder code. CORRECTED 2026-09-08 (#15543): this gap used to name a second door, createHonoServerPlugin({ restConfig }) — no such function exists (definition probe: zero across the tree, positive control finds createRestApiPlugin at packages/rest/src/rest-api-plugin.ts#createRestApiPlugin). HonoServerPlugin is a class whose restConfig option has one reader taking api.basePath for the SPA fallback; it never constructs a REST server, so a harness must not try to reach these keys through it. Every non-default clause below is therefore scored with oracle `test` against a real RestServer, and the run record must say so instead of claiming a live deployment was reconfigured. RULED 2026-09-07 (#15543; director seat, summon #17, decision batch #2): this is the RECORDED POSTURE, not a gap awaiting a fix. The keys stay and keep their runtime reads, threading a config through is not taken and retiring them is refused, so the harness scoring described here is the PERMANENT method for every non-default clause rather than a workaround to be retired when some future boot path grows a flag. The schema now says so in its own docblocks (packages/spec/src/api/rest-server.zod.ts, WHO CAN WRITE THIS CONFIG) and the per-key reachability rows are in packages/spec/liveness/{crud,metadata,batch}_endpoints.json, so a runner no longer has to re-derive it from the boot paths.", "the MOUNT half has no permanent pin: packages/rest/src/rest-sub-config-parse-not-cast.test.ts asserts the NORMALIZED CONFIG a switch produces, and nothing asserts that the route disappears from the route table — the scratch harness is the only observation until that pin exists (recorded in FOLLOW-UPS.md)" ] }, @@ -2072,7 +2072,7 @@ { "revision": 2, "date": "2026-09-08", - "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (`packages/cli/src/commands/serve.ts#apiConfig`), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", "ref": "#15543" } ] @@ -2195,7 +2195,7 @@ { "revision": 2, "date": "2026-09-08", - "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (`packages/cli/src/commands/serve.ts#apiConfig`), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", "ref": "#15543" } ] @@ -2325,7 +2325,7 @@ { "revision": 3, "date": "2026-09-08", - "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (serve.ts:3966-3968), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", + "change": "the knownGaps were semantically re-characterised, not merely annotated: what they describe as the harness-scoring workaround is now the PERMANENT method, by ruling (#15543, director seat, summon #17, decision batch #2, 2026-09-07) that the crud / metadata / batch keys stay, are embedder-only, and get their reachability written into the schema docblocks and the liveness ledger instead of being re-derived from the boot paths at every run. A runner reading the old text would be waiting for a boot-path flag that is not coming, which is the semantic change this bump records. Two factual corrections ride along, both measured rather than inherited: the door `createHonoServerPlugin({ restConfig })` these gaps named DOES NOT EXIST (HonoServerPlugin is a class whose restConfig reader takes only api.basePath for the SPA fallback and never constructs a REST server), and `os serve` does read a config file — the stack's own top-level `api:` block — forwarding exactly two keys out of it (`packages/cli/src/commands/serve.ts#apiConfig`), so 'nothing the CLI reads' was an over-claim. The acceptance clauses, steps, variants and oracles are untouched: what moved is what a runner should conclude from a non-default clause being unreachable, not what it must measure.", "ref": "#15543" } ]