|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +// [#15542 / #15854] The `metadata.endpoints.*` switches were re-partitioned so that |
| 4 | +// each gates exactly the face its name states. Nothing is renamed, nothing is |
| 5 | +// retired and no stored row changes shape — a `RestServerConfig` is plugin TS |
| 6 | +// configuration, never a stack collection member or a `sys_metadata` row (the |
| 7 | +// `openApi31` precedent, #4579), so there is no D2 conversion to graduate here. |
| 8 | +// What an embedder is owed is a PRESCRIPTION, because the mounted route table their |
| 9 | +// existing config produces has moved in both directions, and the compiler cannot |
| 10 | +// tell them: every key is optional and boolean, so the old spelling still compiles |
| 11 | +// and still parses. That is exactly the residue D2 cannot express, which is why this |
| 12 | +// is a semantic entry rather than a conversion. |
| 13 | +import type { SemanticMigration } from '../../types.js'; |
| 14 | + |
| 15 | +export const entry: SemanticMigration = { |
| 16 | + id: 'metadata-endpoints-switch-radius-repartitioned', |
| 17 | + surface: 'restServer.metadata.endpoints.items / restServer.metadata.endpoints.item', |
| 18 | + replacement: |
| 19 | + 'An `endpoints.*` switch now gates exactly the face its name states, reads and writes alike. ' |
| 20 | + + '`items` gates `GET {prefix}/:type` and nothing else; the whole-store operations it used to take ' |
| 21 | + + 'with it — `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and the `POST {prefix}/_migrate-stored` ' |
| 22 | + + 'write door — answer to the new key `endpoints.maintenance` (default `true`). `item` now gates the ' |
| 23 | + + 'WHOLE per-item face: `GET` / `PUT` / `DELETE {prefix}/:type/:name`, `/references`, `/layers`, the ' |
| 24 | + + 'history family (`/history`, `/audit`, `/diff`, `/published`, `/publish`, `/rollback`) and ' |
| 25 | + + '`GET {prefix}/book/:name/tree`. ⇒ An embedder that authored `endpoints: { items: false }` to close ' |
| 26 | + + 'the whole-store family writes `endpoints: { items: false, maintenance: false }`. An embedder that ' |
| 27 | + + 'authored `endpoints: { item: false }` to close only the per-item READS has no key that keeps the ' |
| 28 | + + 'writes: the per-item face is one face, so leave `item` on and close the surface at ' |
| 29 | + + '`api.enableMetadata`, or per object at `enable.apiEnabled` / `enable.apiMethods`. ' |
| 30 | + + '`types` is unchanged and `api.enableMetadata` remains the master switch above all four.', |
| 31 | + reason: |
| 32 | + 'Not losslessly convertible, and not compiler-carried either — the two channels that would otherwise ' |
| 33 | + + 'reach a consumer are both blind here. No key is renamed, removed or retyped: every one is an ' |
| 34 | + + 'optional boolean, so `{ items: false }` compiles and parses exactly as before and simply mounts a ' |
| 35 | + + 'different route table. A D2 conversion would have to GUESS which of the four routes the author ' |
| 36 | + + 'meant to close, and the two readings differ by a write door — rewriting `{ items: false }` to ' |
| 37 | + + '`{ items: false, maintenance: false }` preserves the old mounts but presumes an intent the author ' |
| 38 | + + 'never expressed, while leaving it alone re-mounts `POST {prefix}/_migrate-stored`. That is a ' |
| 39 | + + 'judgment, so it is delegated rather than automated. The change itself is the ADR-0049 ' |
| 40 | + + 'declared-vs-enforced defect in the direction the liveness ledger structurally cannot look: all ' |
| 41 | + + 'three keys were genuinely live, and what had drifted was each one\'s RADIUS against its own ' |
| 42 | + + '`describe()` — `items` gated a migration write door while naming a listing read (#15542), and ' |
| 43 | + + '`item` gated four reads while its own `PUT` / `DELETE` and the history family answered to ' |
| 44 | + + '`api.enableMetadata` alone (#15854). Ruled together by the maintainer as one principle. Measured ' |
| 45 | + + 'population at the time of the move: ZERO — no shipped boot path constructs a `RestServerConfig` ' |
| 46 | + + '(#15543), so only programmatic embedders can have authored these keys at all.', |
| 47 | + acceptanceCriteria: |
| 48 | + 'For each `RestServerConfig` the consumer constructs, `new RestServer(...).registerRoutes()` followed ' |
| 49 | + + 'by `getRoutes()` yields the route table the consumer intends — specifically: with ' |
| 50 | + + '`endpoints.items: false` authored, `GET {prefix}/diagnostics`, `GET {prefix}/_drafts` and ' |
| 51 | + + '`POST {prefix}/_migrate-stored` are PRESENT unless `endpoints.maintenance: false` is also authored; ' |
| 52 | + + 'and with `endpoints.item: false` authored, `PUT {prefix}/:type/:name`, ' |
| 53 | + + '`DELETE {prefix}/:type/:name` and the six history routes are ABSENT. A consumer that authored ' |
| 54 | + + 'neither key is unaffected and needs no change: all four switches default `true` and the default ' |
| 55 | + + 'route table is byte-identical to before. The reference measurement is ' |
| 56 | + + '`packages/rest/src/rest-config-mount-table.pin.test.ts`, which asserts each switch\'s radius as a ' |
| 57 | + + 'set difference against the all-true baseline in both directions.', |
| 58 | +}; |
0 commit comments