Skip to content

Commit a9073d8

Browse files
committed
refactor(rest): re-point every reader of the moved discovery and OpenAPI lines
Each reader keeps its sum: the registrar-list readers pin length 8, the execctx census counts the discovery registrar as one that reads no resolveExecCtx, the dogfood census row gains its module control, the route-envelope gate declares the module at 0 / 0 (and its slice-6 history entry says four arms, not two), a new ADR anchor restores the decision-letter reading, and the checklist and prose pointers name the module. Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
1 parent 234537d commit a9073d8

15 files changed

Lines changed: 164 additions & 38 deletions

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

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

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

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

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

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

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

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

‎packages/rest/src/execctx-consumer-census.test.ts‎

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,12 @@ const SOURCE = readFileSync(resolve(HERE, 'rest-server.ts'), 'utf8');
9797
* [#23050 slice 6] …and `registerSecurityExplainEndpoints`' and
9898
* `registerSecurityEndpoints`' bodies to `security-routes.ts`, with their five
9999
* (two and three) — the second module on the list to carry TWO registrars.
100+
*
101+
* [#23050 slice 7] …and `registerDiscoveryEndpoints`' and
102+
* `registerOpenApiEndpoints`' bodies to `discovery-routes.ts`, with their two
103+
* (none and two) — the third module to carry TWO registrars, and the first
104+
* moved registrar whose body reads no `resolveExecCtx` at all (the discovery
105+
* document resolves no caller).
100106
*/
101107
const SITE_FILES = REST_SERVER_REGISTRAR_FILES;
102108
const SOURCES = new Map<string, string>(SITE_FILES.map((file) => [
@@ -109,14 +115,16 @@ const siteKey = (file: string, line: number) => `${file}:${line}`;
109115
* moved registrar (`registerDataActionEndpoints`, `registerBatchEndpoints`,
110116
* `registerApprovalsEndpoints`, `registerSharingEndpoints`,
111117
* `registerSharingRuleEndpoints`, `registerAnalyticsEndpoints`,
112-
* `registerSecurityExplainEndpoints`, `registerSecurityEndpoints`) hands
113-
* its moved body forwards each member to the server with
114-
* `(...a) => this.<member>(...a)`, so a moved site's stack shows that forwarder
115-
* above the site itself. The site is the first censused frame that is not one.
116-
* Found by their spelling, and §2 pins ONE per moved REGISTRAR — one per moved
117-
* module, plus {@link EXTRA_MOVED_REGISTRARS} — so a respelt forwarder — or a
118-
* module moved without joining `SITE_FILES` — is a control failure there rather
119-
* than every moved site silently attributed to it.
118+
* `registerSecurityExplainEndpoints`, `registerSecurityEndpoints`,
119+
* `registerOpenApiEndpoints`) hands its moved body forwards each member to the
120+
* server with `(...a) => this.<member>(...a)`, so a moved site's stack shows
121+
* that forwarder above the site itself. The site is the first censused frame
122+
* that is not one. Found by their spelling, and §2 pins ONE per moved REGISTRAR
123+
* whose body reads the seam — one per moved module, plus
124+
* {@link EXTRA_MOVED_REGISTRARS}, less {@link MOVED_REGISTRARS_READING_NO_EXECCTX}
125+
* — so a respelt forwarder — or a module moved without joining `SITE_FILES` —
126+
* is a control failure there rather than every moved site silently attributed
127+
* to it.
120128
*/
121129
const FORWARDER_LINES = SOURCE.split('\n').flatMap((l, i) =>
122130
l.includes('resolveExecCtx: (...a) => this.resolveExecCtx(...a),') ? [i + 1] : []);
@@ -127,10 +135,28 @@ const FORWARDERS = new Set(FORWARDER_LINES.map((line) => siteKey('rest-server.ts
127135
* own host literal and so through its own forwarder.
128136
*
129137
* [#23050 slice 6] `security-routes.ts` carries two as well, on the same terms.
138+
*
139+
* [#23050 slice 7] So does `discovery-routes.ts` (the discovery and OpenAPI
140+
* bodies), each handed over by its own host literal — but see
141+
* {@link MOVED_REGISTRARS_READING_NO_EXECCTX}: only one of its two literals
142+
* carries the forwarder.
130143
*/
131144
const EXTRA_MOVED_REGISTRARS: Readonly<Partial<Record<(typeof SITE_FILES)[number], number>>> = {
132145
'sharing-routes.ts': 1,
133146
'security-routes.ts': 1,
147+
'discovery-routes.ts': 1,
148+
};
149+
/**
150+
* [#23050 slice 7] The moved registrars whose bodies read no `resolveExecCtx`,
151+
* so whose host literals hand over no forwarder — measured by the AST walk of
152+
* the body, not assumed. `registerDiscoveryEndpoints`' body resolves no caller
153+
* (the discovery document is served to anyone), so its host literal names no
154+
* `resolveExecCtx` member; its module's other registrar, the OpenAPI pair,
155+
* reads it twice and carries the forwarder. A body that starts reading the
156+
* seam gains a forwarder, and §2's exact count reds until this entry goes.
157+
*/
158+
const MOVED_REGISTRARS_READING_NO_EXECCTX: Readonly<Partial<Record<(typeof SITE_FILES)[number], number>>> = {
159+
'discovery-routes.ts': 1,
134160
};
135161

136162
/** A stack frame in any censused file, as `<file>:<line>:<column>`. */
@@ -616,13 +642,27 @@ describe('[#13160] §2 the consumer surface, counted from the tree', () => {
616642
// members, four). Eight forwarders now, one per moved registrar:
617643
// (7 - 1) + 2, the second module carrying two registrars counted in
618644
// `EXTRA_MOVED_REGISTRARS`.
619-
expect(SITE_FILES).toHaveLength(7);
645+
//
646+
// [#23050 slice 7] 62 sites UNCHANGED / 109 → 113 mentions — the same MOVE.
647+
// `registerOpenApiEndpoints`' body left for `discovery-routes.ts` with its
648+
// two BARE sites (the document and its viewer), and
649+
// `registerDiscoveryEndpoints`' with none: 25 + 9 + 5 + 7 + 8 + 1 + 5 + 2
650+
// = 62 sites. The four new mentions are this slice's seam, the two
651+
// spellings ONCE (one host literal's forwarder, two; one host
652+
// interface's member, two): `rest-server.ts` stays at 57 (two sites
653+
// out, one forwarder's two mentions in). Nine forwarders now:
654+
// (8 - 1) + 3 - 1 — the third module carrying two registrars counted in
655+
// `EXTRA_MOVED_REGISTRARS`, and the discovery registrar, which reads no
656+
// `resolveExecCtx`, in `MOVED_REGISTRARS_READING_NO_EXECCTX`.
657+
expect(SITE_FILES).toHaveLength(8);
620658
expect(SITES.length).toBe(62);
621-
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(109);
659+
expect([...SOURCES.values()].reduce((n, text) => n + text.split('resolveExecCtx').length - 1, 0)).toBe(113);
622660
const extraMovedRegistrars = Object.values(EXTRA_MOVED_REGISTRARS).reduce((n, k) => n + (k ?? 0), 0);
623-
expect(extraMovedRegistrars).toBe(2);
624-
expect(FORWARDER_LINES, 'one host-literal resolveExecCtx forwarder per moved registrar: one was respelt or removed, or a module moved without joining SITE_FILES')
625-
.toHaveLength(SITE_FILES.length - 1 + extraMovedRegistrars);
661+
expect(extraMovedRegistrars).toBe(3);
662+
const registrarsReadingNoExecCtx = Object.values(MOVED_REGISTRARS_READING_NO_EXECCTX).reduce((n, k) => n + (k ?? 0), 0);
663+
expect(registrarsReadingNoExecCtx).toBe(1);
664+
expect(FORWARDER_LINES, 'one host-literal resolveExecCtx forwarder per moved registrar whose body reads it: one was respelt or removed, or a module moved without joining SITE_FILES')
665+
.toHaveLength(SITE_FILES.length - 1 + extraMovedRegistrars - registrarsReadingNoExecCtx);
626666
});
627667

628668
it('the split is 16 locally caught / 46 bare — NOT 12 / 46, which does not add to 62', () => {

‎packages/rest/src/meta-draft-read-door-census.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,8 +248,9 @@ describe('[#20338] census: every draft switch in rest-server.ts is ledgered', ()
248248
// never its file, so no ledger row changed: 8 = 6 + 0 + 0 + 0 + 0 + 2,
249249
// length 5 → 6. [#23050 slice 6] The two security bodies
250250
// (`security-routes.ts`) read no draft: 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0,
251-
// length 6 → 7.
252-
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(7);
251+
// length 6 → 7. [#23050 slice 7] Nor do the discovery and OpenAPI bodies
252+
// (`discovery-routes.ts`): 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0, length 7 → 8.
253+
expect(REST_SERVER_REGISTRAR_FILES).toHaveLength(8);
253254
expect(SOURCE_FILES.map((file) => file.fileName)).toEqual([...REST_SERVER_REGISTRAR_FILES]);
254255
expect(SITES).toHaveLength(8);
255256
});

‎packages/rest/src/openapi-builtin-paths.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@
2929
* owns — `components.schemas`, `info`, `securitySchemes`.
3030
*
3131
* This module is the rest half. It reads the routes the server has actually
32-
* mounted and returns the `paths` section for them; `rest-server.ts` installs
33-
* it in place of whatever the static artifact carried, so the stale section
32+
* mounted and returns the `paths` section for them; the OpenAPI document's
33+
* handler (`discovery-routes.ts` since #23050 slice 7, `rest-server.ts` before)
34+
* installs it in place of whatever the static artifact carried, so the stale section
3435
* cannot leak through even while the spec-side generation is still there
3536
* (leg 2 = #5744, blocked by this one). Discarding rather than merging is the
3637
* point: a serve-time merge with a wrong static section republishes the wrong
@@ -236,8 +237,9 @@ export interface BuiltinPathsResult {
236237
/**
237238
* Build the built-in section for one base path from the routes mounted under it.
238239
*
239-
* Pure: no clock, no I/O, no server. `rest-server.ts` hands it
240-
* `routeManager.getAll()` at request time; the tests hand it fixtures.
240+
* Pure: no clock, no I/O, no server. The OpenAPI document's handler
241+
* (`discovery-routes.ts` since #23050 slice 7, `rest-server.ts` before) hands
242+
* it `routeManager.getAll()` at request time; the tests hand it fixtures.
241243
*/
242244
export function buildBuiltinPaths(
243245
routes: readonly BuiltinRouteFact[],

0 commit comments

Comments
 (0)