Skip to content

refactor(rest): move registerDiscoveryEndpoints and registerOpenApiEndpoints into discovery-routes.ts (#23050 slice 7) - #23763

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23050-s7-discovery-endpoints
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-23050-s7-discovery-endpoints

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23050
Clause-②: no

Slice 7 of the rest-server.ts split, by slice 1's pattern (landed note 6136102437, steps 1–6), slice 2's getter refinement (6189011700) and slices 3–6's shape (PR #23670, PR #23705 and PR #23729's two registrars in one module, PR #23719): the bodies of RestServer#registerDiscoveryEndpoints (GET {basePath} and GET {basePath}/discovery) and RestServer#registerOpenApiEndpoints (GET {basePath}/openapi.json and its GET {basePath}/docs viewer) move to ONE new module, packages/rest/src/discovery-routes.ts. Routes, the ordered registrations, the published dist/*.d.ts and both bundles' export lists are byte-identical, and every reader whose reading included a moved line now reads it where it is, with its sum unchanged.

One deviation, named up front: one token of the OpenAPI body is not byte-identical. The body called the class's PRIVATE STATIC overlayDocumentationInfo as RestServer.overlayDocumentationInfo(…), a spelling that does not compile outside the class. It now reaches the static through the host, as this.overlayDocumentationInfo(…). So the openapi.json handler's digest differs from base, by that receiver alone (measured below). The seat is asked to confirm the shape (see the end of this body).

rest-server.ts goes from 10,636 to 10,090 lines (base 3b46855fb9). The card stays open for the later slices.

The pattern, applied

Members the moved bodies read, from an AST walk of each method (TypeScript compiler API, symbol-resolved, base 3b46855fb9): registerDiscoveryEndpoints (JSDoc :4910, method :4913-:5236, body :4914-:5235) and registerOpenApiEndpoints (JSDoc :5277, method :5323-:5591, body :5324-:5590).

kind member reads (discovery / OpenAPI) class declaration
field config 9 / 1, all inside the handlers :2011, private, not readonly
field routeManager 2 / 2, the register calls :2012, private
method resolveProtocol 1 / 1 :2390, private
method probeMcpServeable 1 / 0 :4886, private
method getDirectMountRouteBases 1 / 0 :10534
method getMountedEmailRouteBase 1 / 0 :10596
method resolveExecCtx 0 / 2 :2858, private
method enforceAuth 0 / 2 :2458, private
method loadOpenApiSpec 0 / 1 :5599, private
method getRoutes 0 / 1 :10630
method resolveEndpointMatchAuthority 0 / 1 :2193, private
method notifyMissingEndpointAuthority 0 / 1 :2269, private
static method overlayDocumentationInfo, read as RestServer.overlayDocumentationInfo 0 / 1 :5262, private static

Neither body has a bare this, an element access on this, or a this-rebinding function. Their module-level reads:

  • Imports still used elsewhere in rest-server.ts (AST count outside both methods): handleRouteError (19) and logError (7). They stay there; the module imports the same names from the same modules.
  • Imports ONLY these bodies read (zero uses outside the two methods): isMcpServerEnabled (@objectstack/types), enrichOpenApiWithEndpoints (./openapi-endpoints.js), buildBuiltinPaths (./openapi-builtin-paths.js), selectServedEndpoints (./served-endpoints.js) and sendThrownError (./error-response.js). They moved with the bodies, as slices 3, 5 and 6 moved theirs, and each left a pointer comment. Leaving them would fail noUnusedLocals.
  • Declared in rest-server.ts: DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS (already exported) and the type RestProtocol (already exported). The module imports both from ./rest-server.js; no export was added anywhere. The table is a const, read only inside the discovery handler at request time, so the two modules importing each other cannot see it uninitialised (the bundle is one scope).

Host shape (slice 1's shape B, twice, as slices 4 and 6): DiscoveryRouteHost (6 members) and OpenApiRouteHost (10 members) list exactly the members each body reads, typed RestServer['m']; the static is typed (typeof RestServer)['overlayDocumentationInfo']. The class keeps both private methods with their names, docblocks and positions. Each body is now a host literal plus .call(host, basePath). Methods go as late-bound closures, (...a) => this.m(...a), and the static as (...a) => RestServer.overlayDocumentationInfo(...a). The call sites in registerRoutes' registerForBase (:4781-:4786 at base, under enableDiscovery / enableOpenApi) are unchanged.

Fields, checked one by one:

field read when reassigned after registration? handed over as
routeManager registration only (four register calls, top-level statements of the two bodies) its one swap (:5721 / :5725 at base) wraps registerMetadataEndpointsInner, not these registrars value
config request time: every one of its ten reads is inside a handler not by the class (constructor only, :2156), but not readonly getter, in both host literals

The getter is measured, not argued. A one-off vitest file (copied in, run, deleted) boots a source RestServer, drives the door, assigns a NEW config object on the instance AFTER registerRoutes() (a new object, never a mutation, which a by-value hand-over would see through the shared reference), and drives it again:

  • this head (a9073d822b): GET /api/v1/discovery answers capabilities.transactionalBatch.enabled true, then false after enableBatch: false. GET /api/v1/openapi.json answers info.title "ObjectStack REST API" (the artifact's), then "Late Title" after documentation: { title: 'Late Title' }. A spy set on RestServer.overlayDocumentationInfo after registerRoutes() is called once per serve, with the authored documentation, so the static is reached through the host at request time. Tests 3 passed (3).
  • by-value ablations through scripts/ablation-replace.mjs, each host literal's getter in turn replaced by config: this.config, with a multi-line anchor naming its literal. Each landed (anchor 1 → 0, replacement 0 → 1, blob 8f098de155 → dcd5b2791e / 4e218d1382). The ablated door freezes at registration: discovery reads true then true; OpenAPI reads the artifact's title then the artifact's title again. Each run: Tests 1 failed | 2 passed (3). Restored by the tool and a trap: blob 8f098de155 equals HEAD, git diff HEAD empty, the one-off file deleted. Source path (relative import), so no dist leg applies.

Mechanism hypotheses from the dispatch, measured:

  • (a) Partly falsified. Both bodies read this.config and resolveProtocol, as hypothesised. The discovery body does not read this.protocol: the field appears in it only inside comments ([#9292], which says the handler stopped reading it); the AST walk finds no this.protocol access, and the protocol is reached through resolveProtocol. So config is the one request-time field, and it goes as a getter by measurement (above). The walk also found the private static overlayDocumentationInfo, which the hypothesis did not list (next section).
  • (b) Confirmed. Every path expression (basePath, realBase, unscopedBase, the /docs page's reqPath / apiBase) moved unedited; the scoped-optional and scoped-required route dumps below are identical, and every discovery and /docs handler's source is byte-identical.
  • (c) Confirmed. The bodies carry 5 ADR-NNNN Dk citations (ADR-0057 D10, ADR-0076 D12 ×2 in discovery; ADR-0056 D2 and ADR-0138 D2 in OpenAPI). They left the anchored rest-server.ts (94 → 89), so check-adr-anchors read 1761 decision-letter citations at base and 1756 at the move-only commit 234537d1ad, green both times: the silent narrowing the card forbids. A new anchor (below) restores 1761.
  • (d) Measured; the readers are listed below. check-affected-docs discovers the module by its *-routes.ts name (route sources 25 → 26) with no edit; check-route-envelope's tables and the platform checklist needed one each.

The one token that changed, and why no unedited shape exists

The OpenAPI body's step 5 overlays the publisher's identity through the class's PRIVATE STATIC: base rest-server.ts:5525 read enriched.info = RestServer.overlayDocumentationInfo(enriched.info, this.config.api.documentation);. Outside the class that spelling does not compile, measured with two throwaway files added to packages/rest/src and removed: tsc --noEmit exits 1 with TS2341: Property 'overlayDocumentationInfo' is private and only accessible within class 'RestServer' (value import), and TS1361 plus TS2341 (the type-only import the module uses). The alternatives each break something the slice must keep:

  • making the static public or protected, or moving it out of the class, changes the published .d.ts (it is listed there as private static overlayDocumentationInfo;), and the dispatch keeps every class method in place this slice;
  • a module-level binding named RestServer that is not the class would keep the text byte-identical, and give every later reader of the module an identifier that lies about what it is.

So the static is reached the way the dispatch's ruling reaches every class member these bodies use: through the host, as a late-bound closure. discovery-routes.ts:655 reads enriched.info = this.overlayDocumentationInfo(enriched.info, this.config.api.documentation);. The member is pure (its only inputs are its two arguments, and it reads no this), so the receiver is the whole difference, and the module header records it.

The registrar list

packages/rest/src/rest-server-registrar-files.ts gains discovery-routes.ts with a [#23050 slice 7] comment (length 7 → 8). Every reader that pins its length re-reads its number, and every sum is unchanged:

reader sum over the list change
rest-server-query-number-census.test.ts 9 = 8 + 1 + 0 + 0 + 0 + 0 + 0 + 0 length 7 → 8
rest-server-canonical-query-ast.test.ts 4 = 1 + 3 + 0 + 0 + 0 + 0 + 0 + 0 query: slots floor discovery-routes.ts: 0, length 7 → 8
meta-draft-read-door-census.test.ts 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 + 0 the two bodies read no draft; length 7 → 8
rest-server-docblock-position.test.ts floors 4 + 3 + 3 + 8 + 3 + 8 + 5 = 34 floor discovery-routes.ts: 5 (header, two host interfaces, two functions; neither body held a TSDoc block); rest-server.ts keeps 164 blocks, floor 100; length 7 → 8
execctx-consumer-census.test.ts 62 sites = 25 + 9 + 5 + 7 + 8 + 1 + 5 + 2 mentions 109 → 113 (the seam once: one forwarder ×2, one interface member ×2; rest-server.ts stays 57). Forwarders 9 = (8 − 1) + EXTRA_MOVED_REGISTRARS 3 − MOVED_REGISTRARS_READING_NO_EXECCTX 1; see the note below
ui-view-route-identity.measurement.test.ts / ui-view-route-tenancy.measurement.test.ts this.enforceAuth( 56, this.resolveExecCtx( 71 over the list (55 / 70 before, +1 each for the OpenAPI literal's forwarder); ownership guard 1 length 7 → 8
dogfood authz-conformance.test.ts (realtime tripwires) 0 hits over the eight files length 7 → 8
dogfood authz-probe-blind-spot.census.ts, rest-server.ts row 70 / 19 / 51 unchanged; this.routeManager.register( 63 = 23 + 9 + 5 + 7 + 8 + 1 + 6 + 4; private register*Endpoints( 16 new control discovery-routes.ts: this.routeManager.register( 4; registrar list length 8; enforceAuth 92 → 96 re-anchored (the OpenAPI pair's two call sites moved and are counted in both readings; the four new occurrences are the seam once; no call site added); populationRule names the new module

The execctx forwarder control, measured rather than inherited. The dispatch expected this census to count two registrars in one module, as slices 4 and 6 did. It does, and it also had to learn that the two registrars here do not both forward resolveExecCtx: the discovery body resolves no caller (the document is served to anyone), so its host literal names no resolveExecCtx member. The control pins forwarders as one per moved registrar whose body reads the seam: EXTRA_MOVED_REGISTRARS gains discovery-routes.ts: 1 (two registrars in one module, as slices 4 and 6), and a new map, MOVED_REGISTRARS_READING_NO_EXECCTX, holds discovery-routes.ts: 1, pinned at exactly 1. The count stays exact: a respelt forwarder, or a discovery body that starts reading the seam, reds it.

Each number above was also counted statically on the tree (grep -oF per file).

Proof of move-only

  • .d.ts: packages/rest/dist/index.d.ts and index.d.cts built at base 3b46855fb9 (this worktree, before any edit), at a9073d822b and at the merge head 9ddffc0baa are byte-identical: sha256 7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9 for all six files, cmp silent. The head bundles carry a head-only marker (readConfig: 4 hits in each of index.js and index.cjs, 0 at base), so the declaration was rebuilt from the moved source, not replayed. DiscoveryRouteHost, OpenApiRouteHost and discovery-routes appear 0 times in the .d.ts; src/index.ts is untouched.
  • Export lists: the base, head and merge-head ESM and CJS bundles each load with 39 exports and one sorted export-name list (sha256 b3246be6a171… on all six). None of the two functions, the two host types or DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS is among them.
  • Routes: a source RestServer on a recording mock server, with a protocol that answers every member, dumped under seven configs: default; scoped-optional; scoped-required; every provider set; enableDiscovery: false; enableOpenApi: false; and both off under scoped-optional. Each dump holds getRoutes() (functions as arity), the ordered server registration calls, and a whitespace-free sha256 of every handler's source.
    • getRoutes() and the ordered calls: 14 of 14 byte-identical, base vs head and base vs merge head. default: routes 6931d1a5d182…, calls ccb3bd12f10b… (75 routes, 4 of them these doors); scoped-optional 275c72f46c30… / 339e8c3abc05… (150, 8); scoped-required a2aa8183e213… / 734f33133150… (75, 4); every-provider-set equals default; discovery-off 51f8ed92bb85… / 50d8d124f3b8… (73, 2); openapi-off 07cd86e4661a… / cb8c93c76044… (73, 2); both-off 4525bbdb0f79… / e7108e133e9a… (142, 0). A second base run was identical to the first.
    • Handler digests: identical in both configs without the OpenAPI pair (openapi-off 307d066c0008…, both-off 2e2b275dc162…). In the other five they differ in exactly the openapi.json handler of each mounted base (6 handlers in all) and in nothing else. With the one receiver mapped back (this.overlayDocumentationInfo( → RestServer.overlayDocumentationInfo(), all 6 sources equal base byte for byte; the 18 discovery and /docs handler sources equal base with no mapping.
  • The handler digest can fail: a positive control changed one character of one moved handler (docs viewer error: → !, through ablation-replace.mjs; anchor 1 → 0, blob 8bf688c0d6 → 91786677f8). Routes and calls were unchanged in all seven configs, and the handler list differed in exactly GET {base}/docs for each mounted base, nothing in the two configs without it. Restored, blob 8bf688c0d6 equals HEAD.
  • Moved bodies: base rest-server.ts lines 4914-5235 and 5324-5590, one 4-space indent removed, against discovery-routes.ts lines 123-444 and 454-720 (322 + 267 lines). The discovery diff exits 0. The OpenAPI diff shows exactly one line, the receiver above. The viewer page's HTML is a template literal whose continuation lines (base 5564-5574) sit at column 0: they are runtime text, and they moved byte for byte, unindented. Every other odd-backtick line in the ranges is a comment, so no other template literal spans a cut.
  • Moved-line accounting: git diff --color-moved=plain --color-moved-ws=allow-indentation-change base → move commit over the two files: 589 deleted and 601 added lines detected as moved. rest-server.ts is +46 / −593 there: the two stub bodies (host literal plus .call), the module's import and its comment, and three import-pointer comments.

Readers re-pointed (sums unchanged, no gate loosened)

  • scripts/check-route-envelope.mjs (a gate's target paths, domain:spec): discovery-routes.ts is discovered by the *-routes.ts convention and declared dialectOnly with ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the six earlier modules are, at its measured stringError 0, siblingCode 0 (the gate compares both counts exactly in both directions). The discovery handler's one refusal path is the shared handleRouteError, and the OpenAPI document's one hand-built refusal is the nested { error: { code, message } }. So rest-server.ts stays at 17 / 17, and the sums 17 + 13 + 8 + 1 + 0 + 0 + 0 + 0 = 39 and 17 + 15 + 8 + 9 + 4 + 1 + 0 + 0 = 54 are unchanged. Output differs from base only in the new row (17 → 18 modules, 8 → 9 ratcheted). --self-test green. Carried from slice 6 into the same comment block: its siblingCode entry said "the two arms [finding] registerSecurityEndpoints answers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981 and [finding] the two registrars ADJACENT to #7981 still answer the retired flat / bare-string error dialects (ADR-0112) #8073 converted … 77 → 73"; it is four arms, and the entry now says "four".
  • scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json (new, ADR-0076 + ADR-0056 + ADR-0138; domain:devx): check-adr-anchors decision-letter citations on the anchored surface 1761 (base) → 1756 (move) → 1761 (head); anchored files 68 → 69. Every listed id appears in the module, and every cited letter is a decision of its record (the gate's own check, green). The invariant writes "its D12", never the citation form, so it adds no citation of its own (the gate's regex: 0). ADR-0057 is cited in the module but not listed: its number is shared by two records, and listing it would add a fourth entry to the gate's ambiguous-number note for nothing the letter check does not already read.
  • docs/qa/platform-checklist/areas/api-backend.json (domain:devx): the crud_endpoints and metadata_endpoints items' source lists named rest-server.ts for "registerDiscoveryEndpoints — discovery.routes.data / .metadata built from" the prefix. Each now points at packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints, slice 2's + form. check:platform-checklist: symbol anchors 733/743 → 735/745 resolved (the two new anchors resolve; unresolved 10 → 10), cited sources 346 → 347.
  • The nine registrar-list readers above.
  • Prose pointers in this lane, none in the published .d.ts: the DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS docblock (rest-server.ts) said "the discovery handler below"; openapi-builtin-paths.ts' module docblock and buildBuiltinPaths' docblock said rest-server.ts installs and calls it. Each now names discovery-routes.ts. The dogfood matrix row anonymous-deny-api-description named rest/rest-server.ts registerOpenApiEndpoints as the enforcement site of the two handlers; it names rest/discovery-routes.ts (no test reads that row's path).

Red before the re-points, each measured with the move in place (234537d1ad): the six local list readers read Test Files 6 failed (6), Tests 7 failed | 105 passed (112) (five length pins, plus the docblock pin's length pin and its new module with no floor). rest-server-canonical-query-ast.test.ts (repo) read Tests 2 failed | 57 passed | 8 skipped (67). The dogfood pair read Tests 2 failed | 106 passed (108) (list length; the census row's registrar list length control). tsc --noEmit was clean, but check:test-typecheck found 2 type errors, the two typed floor tables without the new module. check:route-envelope read discovery-routes.ts NOT DECLARED (MODULES and EXPRESS_RESPONSE_MODULES). check-adr-anchors was green at 1756.

Readers examined and not re-pointed

  • packages/spec readers whose reading includes a moved line (stopped at, per the dispatch, not edited): packages/spec/liveness/rest_api.json cites rest-server.ts#registerDiscoveryEndpoints seven times, and five of those evidence strings quote code that now lives in discovery-routes.ts (enabled: runtimeSupportsTx && this.config.api.enableBatch !== false, enabled: !!caps.search?.enabled && this.config.api.enableSearch, scoping.enabled, scoping.resolution, and the advertised switches); its ten #overlayDocumentationInfo rows say the static is "called by the openapi.json handler registerOpenApiEndpoints builds". metadata_endpoints.json and crud_endpoints.json each cite #registerDiscoveryEndpoints once (routes.metadata, routes.data). Every anchor still resolves (the private methods stay), and check:liveness is green. They go to the seat, to carry with the CRUD slice's pointers.
  • docs/adr/0138-… cites rest-server.ts#RestServer.registerDiscoveryEndpoints and #RestServer.registerOpenApiEndpoints. Both still resolve (the private methods stay). It is a governed surface, and is not edited here.
  • content/docs/releases/implementation-status.mdx names rest-server.ts — registerDiscoveryEndpoints: release-owned, not edited in a code PR, and the name still resolves.
  • rest-server.ts' parseDeclaredApiConfig docblock, in its projectResolution history (about :4578), says "the discovery handler below copies the value into discovery.scoping.resolution". That docblock ships in the published .d.ts, so editing it would break the byte-identity this slice must keep. Not edited.
  • Prose that names the method without a file, still true: packages/core/src/qa/http-adapter.ts (and its test), packages/runtime/src/dispatcher-plugin.ts (about :1098), packages/metadata-protocol/src/discovery-realtime-channel.pin.test.ts, the packages/rest discovery and OpenAPI tests, showcase-anonymous-deny-surfaces.dogfood.test.ts, the dogfood census row's dated [#22430] entry, and the checklist's clause text and FOLLOW-UPS.md.
  • No rest-server.ts:NNNN line citation in the tree falls inside the moved ranges. REST_ROUTE_LEDGER names routes, not files.
  • Gate outputs, base 3b46855fb9 vs head, from the same scan: see the gates section.

Acceptance notes

  • The [#22430] comment the OpenAPI body carries ("the same body the /data and /meta routes answer") and the [#9292] comment in the discovery body ("~30 sibling handlers in this file") moved verbatim; in discovery-routes.ts "this file" is the module. A move-only slice may not edit the bodies.
  • openapi-builtin-paths.ts says the handler hands buildBuiltinPaths "routeManager.getAll()"; it has handed it getRoutes() (RouteManager's table plus the direct mounts) since rest 的 9 条 direct-mount 路由对 RestServer 不可枚举 —— 因此进不了 /openapi.json,也进不了任何运行时自省 #5822. Pre-existing, not part of this move, left as it was.
  • docs/adr/0076-… (status line and D11) and docs/adr/0111-… (Consumers line) are still carried to the maintainer by the seat; this slice adds nothing to them.

Gates and tests

Measured at this branch: base 3b46855fb9, the move 234537d1ad, the re-points a9073d822b, and one merge of origin/main 2906cd8d2e (9ddffc0baa: three commits in cli, driver-sql and metadata-protocol/objectql, no file in this diff). All builds and tests went through os-verify-lock. Every exit code was captured to a file before any pipe.

  • At 9ddffc0baa: pnpm install --frozen-lockfile exit 0; the @objectstack/rest and @objectstack/verify closures built, exit 0; then pnpm turbo run build --concurrency=2, the whole workspace, exit 0 (76 tasks, 74 cached), after which the @objectstack/rest .d.ts / .d.cts still equal base.
  • pnpm --filter @objectstack/rest typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: 0 file(s) / 0 error(s)), at a9073d822b and at 9ddffc0baa.
  • pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 285 passed (285), Tests 5876 passed / 326 skipped (6202), at both heads.
  • pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: Test Files 5 passed (5), Tests 238 passed / 8 skipped (246), at both heads.
  • pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts: Test Files 2 passed (2), Tests 108 passed (108), at both heads.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, at the final head 9ddffc0baa (stderr names that commit; the --repo assertion holds; 17 paths against merge base 2906cd8d2e): 81 commands, all run at 9ddffc0baa. 80 exited 0 on the first pass; pnpm check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 19 packages had no dist), and exited 0 after the whole-workspace build above. --ran: Run reconciliation — 81 derived, 81 run, 0 NOT-MEASURED, 0 UNRUN. Among them: pnpm check:route-envelope, pnpm check:adr-anchors, pnpm check:platform-checklist, pnpm --filter @objectstack/spec run check:liveness, pnpm check:durability-log-level, pnpm check:dispatcher-error-vocabulary, pnpm check:dts-closure, pnpm check:published-files and pnpm check:pm-dispatch-gates.
  • Scan outputs, base 3b46855fb9 vs the merge head, diffed: identical for check-single-authz-resolver, check-system-context-census, check-rest-log-declared, check:rest-log-spy-declared, check:test-source-alias, check-filter-alias-parity and check:liveness. Differing only by this diff's new file: check-meta-type-normalized (35 → 36 files), check-undeclared-dep-imports (+1 file and +1 specifier, read at the move commit; main adds one more file), check-affected-docs (route sources 25 → 26), check-route-envelope (the new row), check-scripts-symbol-anchors (+4 file-level anchors, the gate's new comments) and check:platform-checklist (735/745, above). check-adr-anchors reads 1764 at the merge head: 1761 from this diff (above) plus 3 that main's metadata-protocol/src/protocol.ts added (278 → 281 in that anchored file). The rest moved with main's three commits (file counts in check:nul-bytes, check:doc-authoring, check:cross-package-test-inputs, check:dispatcher-error-vocabulary, check:error-status-conformance; check-durability-degradation-log-level 78 → 80 read seams).
  • Lint, the proven narrowing: eslint --no-inline-config --format json over packages/rest, the three dogfood files, scripts/check-route-envelope.mjs, the new anchor JSON and the checklist JSON: 333 files, 0 errors, 0 lint warnings. Its two warnings are File ignored because no matching configuration was supplied, for the two JSON files, which eslint . never reads. All 17 changed files are in the result set, the 15 code files at 0 / 0. The published .d.ts is byte-identical, so no consumer outside packages/rest can lint differently. Repo-wide pnpm lint is CI's.
  • check:dev-prereqs and check:skill-frame-freshness (local-only) were not run: no packages/spec build input and no four-axis frame file is touched.
  • No changeset: nothing any released package publishes changes (.d.ts, export lists and routes above). skip-changeset is the seat's to apply on its own check.

Cross-lane files in this diff: scripts/check-route-envelope.mjs (domain:spec, a gate's target paths), scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json (a new entry in check-adr-anchors' registry, domain:devx), docs/qa/platform-checklist/areas/api-backend.json (domain:devx), and the dogfood files (packages/qa/dogfood/test/authz-conformance.test.ts, authz-conformance.matrix.ts, authz-probe-blind-spot.census.ts).

For the seat: one question

The dispatch rules the bodies move unedited with every handler digest identical, and every class member the bodies call reached through the host. The OpenAPI body's call of the private static cannot satisfy both: it is reached through the host here, and that changes one token of one handler. Options: (A) accept the receiver change as this slice's shape (recommended: the static stays private and in place, the .d.ts is untouched, and the dependency is declared on OpenApiRouteHost like every other member); (B) a module-level binding named RestServer that forwards the static, which keeps the text byte-identical but names something that is not the class; (C) a later slice moves overlayDocumentationInfo out of the class, which changes the .d.ts by one private line and needs its own ruling.


Generated by Claude Code

…discovery-routes.ts

Move only. The bodies of RestServer#registerDiscoveryEndpoints and
RestServer#registerOpenApiEndpoints move, re-indented one level, to one new
module; the class keeps both private methods as a host literal plus .call.
routeManager goes by value, config as a getter. The one changed token is the
OpenAPI body's call of the private static overlayDocumentationInfo, now
reached through the host. The registrar list gains the module (7 -> 8).
Readers are re-pointed in the following commit.

Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU
Co-authored-by: Claude <noreply@anthropic.com>
…API 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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 36 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/openapi-builtin-paths.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/connect-mcp.mdx (via /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints))
  • content/docs/api/client-sdk.mdx (via /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints))
  • content/docs/api/environment-routing.mdx (via /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))
  • content/docs/api/index.mdx (via /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints))
  • content/docs/concepts/north-star.mdx (via /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))
  • content/docs/deployment/publish-and-preview.mdx (via /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))
  • content/docs/deployment/single-project-mode.mdx (via /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))
  • content/docs/kernel/runtime-services/storage-service.mdx (via /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints))
  • content/docs/permissions/authorization.mdx (via enforceAuth (symbol, a field of interface OpenApiRouteHost), enforceAuth (literal, a string literal in enforceAuth))
  • content/docs/permissions/system-context.mdx (via enforceAuth (symbol, a field of interface OpenApiRouteHost), enforceAuth (literal, a string literal in enforceAuth))
  • content/docs/protocol/kernel/http-protocol.mdx (via /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints), /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))
  • content/docs/protocol/kernel/plugin-spec.mdx (via /projects/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints))
  • content/docs/ui/forms.mdx (via enforceAuth (symbol, a field of interface OpenApiRouteHost), enforceAuth (literal, a string literal in enforceAuth), /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))

⛔ 1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via registerDiscoveryEndpoints (symbol, a method of class RestServer; a top-level function), /api/v1/discovery (route, a path literal in a comment in registerDiscoveryEndpoints), /environments/:environmentId (route, a path literal in a comment in registerDiscoveryEndpoints; a path literal in registerDiscoveryEndpoints; a path literal in registerOpenApiEndpoints))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/src/openapi-builtin-paths.ts) — pages documenting those are invisible to this run
  • 4 cross-cutting symbol(s) contributed no route anchor: enforceAuth (23 routes), resolveExecCtx (28 routes), routeManager (20 routes), resolveProtocol (25 routes)
  • 2 anchor(s) matched too much of the corpus to be a work list: /api/v1 (route, 96 pages), /api/v1/data (route, 40 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 29c7157af4fed4738b3da14bbfb008496cbd1f6a → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 6f0e0502015d1a54a2891ce52fad725e1bae17cc — the merge of head 9ddffc0baad2ca64051703b98f8fc8032d05ebe5 into base 29c7157af4fed4738b3da14bbfb008496cbd1f6a, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6f0e0502015d1a54a2891ce52fad725e1bae17cc && git checkout 6f0e0502015d1a54a2891ce52fad725e1bae17cc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 29c7157af4fed4738b3da14bbfb008496cbd1f6a 9ddffc0baad2ca64051703b98f8fc8032d05ebe5 && git checkout -B drift-repro 29c7157af4fed4738b3da14bbfb008496cbd1f6a && git merge --no-ff 9ddffc0baad2ca64051703b98f8fc8032d05ebe5

node scripts/docs-audit/affected-docs.mjs --json 29c7157af4fed4738b3da14bbfb008496cbd1f6a

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 29c7157af4fed4738b3da14bbfb008496cbd1f6a → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 9ddffc0baad2ca64051703b98f8fc8032d05ebe5
Local-runs: none

Face: the declaration face. This is a move-only refactor of @objectstack/rest, with one named exception of one token, which the seat has ruled on. The diff does the following:

  • two registrar bodies move into one new module;
  • one gate's target paths are re-pointed (scripts/check-route-envelope.mjs);
  • one entry is added to a gate's registry (scripts/adr-anchors/);
  • two platform-checklist source lists and one dogfood matrix row are re-pointed;
  • the registrar list that nine readers read grows 7 → 8.

The 17-file list holds no published schema and no governed path. record-recognisers --template --paths over it derives TIER_DEFAULT, and Governed Surface Queue Guard concluded success. Since then, main has gained the narrowing face (#23750, 29c7157af4). This PR's body reads Clause-②: no with no arm, and the tree carries no changeset, so that recogniser reads the declaration face too.

Inputs:

The reviewer is an isolated agent working read-only. Every count below was read off the git objects of the merge-base and the head with these tools:

  • git show and text diff;
  • grep, and a scan of this. reads and import bindings with comments and strings stripped;
  • the gates' own patterns, applied to text: check-adr-anchors' DECISION_CITATION, check-route-envelope's two dialect rules, and the dogfood tripwire's two regexes.

Nothing was built, tested or re-run. The merge-base's rest-server.ts blob (4867a14302) is the same blob as at the dev's base 3b46855fb9, so the PR's base line numbers apply as written.

① Derived judgments

No accept set and no public surface changes. Each change the diff makes is judged below.

  1. The discovery body is byte-identical, and the OpenAPI body differs in exactly one token: right.

    • Base rest-server.ts :4914–:5235, with one 4-space indent removed, equals head discovery-routes.ts :123–:444. That is 322 lines, and 0 differ.
    • Base :5324–:5590, de-indented the same way, against head :454–:720 (267 lines): exactly one line differs.
      • It is base :5525 / head :655: RestServer.overlayDocumentationInfo( became this.overlayDocumentationInfo(.
      • The rest of that line is equal.
    • The only unindented lines in either range are the /docs viewer's template-literal continuation (base :5564–:5574). They are runtime HTML at column 0, and they moved byte for byte.
    • The other odd-backtick lines in the ranges (four in discovery, two in OpenAPI) are line comments. So no other template literal spans the cut, and removing the indent changes no runtime string.
    • Both private methods keep their name, docblock and position. The call sites in registerForBase (head :4791–:4796) are unchanged, so the four routes register in the same order on every base.
    • The rest of the rest-server.ts side is:
      • the two stubs (a host literal plus .call);
      • the new import and its comment;
      • three import-pointer comments;
      • the re-worded DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS docblock.
    • Its top-level export lines are identical at base and head. The file goes from 10,636 to 10,090 lines.
  2. Each host lists exactly what its body reads: right.

    • The discovery body's this. reads (comments and strings excluded):
      • config 9 and routeManager 2;
      • resolveProtocol, probeMcpServeable, getDirectMountRouteBases and getMountedEmailRouteBase, 1 each.
      • DiscoveryRouteHost declares exactly those 6.
    • The OpenAPI body's reads:
      • config 1, routeManager 2, resolveExecCtx 2 and enforceAuth 2;
      • loadOpenApiSpec, getRoutes, resolveProtocol, resolveEndpointMatchAuthority and notifyMissingEndpointAuthority, 1 each;
      • after the one token, overlayDocumentationInfo 1.
      • OpenApiRouteHost declares exactly those 10.
    • The discovery body reads no this.protocol.
      • The name appears only in the [#9292] comment (discovery-routes.ts :128, :151).
      • The body reaches the protocol through resolveProtocol.
      • So the dev's finding that hypothesis (a) is partly falsified is right.
    • Neither body has a bare this, an element access on this, or a function token.
    • Each host is an object literal of its interface, typed by indexed access on the class, so a missing or an extra member is a compile error. TypeScript Type Check and the four Type Check · lanes concluded success.
  3. Value or getter, member by member: right.

    • routeManager goes by value in both literals.
      • All four register calls are top-level statements of the bodies, so they run during registration.
      • The class reassigns the field only at head :5175 / :5179, the try/finally around registerMetadataEndpointsInner. registerRoutes calls these registrars outside it.
      • Each literal is built when its stub runs. So the two tests that call registerOpenApiEndpoints directly read the field exactly as base did.
    • config goes as a getter in both literals.
      • Each of the ten reads is inside a handler, so each happens at request time: discoveryHandler spans :124–:423 and holds its nine reads at :204–:413; openApiHandler holds its one read at :655.
      • The field is not readonly (:2021), and only the constructor assigns it (:2166). A by-value copy would not follow a later reassignment; the getter would.
      • The getter reads through a closure over the server (readConfig), never through the host. This is slice 2's refinement, applied field by field.
      • The dev's by-value ablations are theirs, not re-run here. They match this reading.
    • Methods go as late-bound closures that call the method on the server. So the receiver of every helper is still the server, and a spy set after registration is still reached.
  4. Ruling A on the private static is right. This review would have ruled the same.

    • The receiver change cannot alter an answer.
      • The static (head :4964) reads no this and no arguments. Its only inputs are its two parameters, and it returns bundled or a new object.
      • The host member is a closure that calls RestServer.overlayDocumentationInfo with the same arguments, and it reads that member at call time. So the receiver (RestServer), the late binding and the arguments are the base call's.
      • The one runtime difference is one more stack frame.
    • B is refused, rightly. A module-level binding named RestServer would keep the bytes, but it names something that is not the class.
    • C is worse. Moving the static out of the class removes a private static line from the published .d.ts. If it also left rest-server.ts, it would strand the ten packages/spec/liveness/rest_api.json rows anchored at rest-server.ts#overlayDocumentationInfo, which this slice may not edit. A keeps all ten exact.
    • A fourth shape, not among the options, is worse too: an element access, RestServer['overlayDocumentationInfo'](…), on a value import of the class.
      • It also changes the token.
      • It defeats private on purpose.
      • It bypasses the named host that the card's ask 2 requires.
    • A is the only shape that both:
      • changes one token and nothing that is published;
      • reaches the class member through the one named context object the card asks for.
    • The card's contract holds. Each handler's logic is unchanged, and the route table is unchanged.
  5. The imports moved as a pure move: right.

    • The bodies read these module-level bindings of base rest-server.ts:
      • seven imports: isMcpServerEnabled (@objectstack/types), enrichOpenApiWithEndpoints, buildBuiltinPaths, selectServedEndpoints, sendThrownError, handleRouteError and logError;
      • two of the file's own declarations: DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS and the type RestProtocol.
    • The module imports each from the same specifier.
    • The first five imports have no code use left in rest-server.ts, so they left it. Each left an accurate pointer comment.
    • handleRouteError and logError still have code uses there, so they stay.
    • DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS and RestProtocol were already exported (base :884 and :257). No export was added.
    • The value import makes the two modules import each other. data-action-routes.ts, batch-routes.ts, approvals-routes.ts and analytics-routes.ts already do the same.
      • The table is read only inside the handler, so no evaluation order can see it uninitialised.
      • RestServer is imported as a type only.
  6. The published surface is unchanged: right.

    • @objectstack/rest publishes dist only. tsup builds it from the single entry src/index.ts, with splitting: false, and emits the .d.ts from that entry.
    • src/index.ts is untouched. It re-exports from ./rest-server.js by name only, and it names neither the new module nor DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS.
    • The host interfaces appear only inside private method bodies, which declaration emit does not reach.
    • The private methods' docblocks and the private static member are unchanged. The one docblock this diff re-words in rest-server.ts sits on a const that no exported declaration reaches.
    • The dev's .d.ts sha and export-list readings are theirs, not re-run here. They are what this construction predicts. The bundle's JS bytes change; its behaviour does not.
  7. scripts/check-route-envelope.mjs was re-pointed, not loosened. Its comments state the counts right.

    • By the gate's own rule on the module:
      • stringError is 0: no error key has a string initializer;
      • siblingCode is 0: no body has code beside error at its top level.
    • The answers each handler builds:
      • the one hand-built refusal is the nested { error: { code: 'OPENAPI_UNAVAILABLE', message } } (:478–:479);
      • the discovery handler's catch is handleRouteError;
      • the two OpenAPI catch-alls are sendThrownError.
    • So rest-server.ts stays at 17 / 17, and both sums hold:
      • stringError: 17 + 13 + 8 + 1 + 0 + 0 + 0 + 0 = 39;
      • siblingCode: 17 + 15 + 8 + 9 + 4 + 1 + 0 + 0 = 54.
    • The table grows from 17 to 18 rows, with 8 → 9 ratcheted. The new row is dialectOnly with ratchet #9559, like the six moved modules before it.
    • The slice 6 fix is right. The siblingCode entries above it record four arms: [finding] registerSecurityEndpoints answers THREE different error-envelope shapes on the same three routes (ADR-0112) #7981's two handleError arms (77 → 75) and [finding] the two registrars ADJACENT to #7981 still answer the retired flat / bare-string error dialects (ADR-0112) #8073's two 500 arms (75 → 73). This closes slice 6's non-blocking item and the release's carry.
    • The Response-envelope guard step concluded success in Lint · docs & code guards.
  8. The new ADR anchor is right, needed and in proportion. Omitting ADR-0057 is right.

    • Why it is needed:
      • DECISION_CITATION finds 5 citations in the module: ADR-0057 D10, ADR-0076 D12 ×2, ADR-0056 D2 and ADR-0138 D2. That is exactly the 3 + 2 in the two moved ranges.
      • rest-server.ts goes from 94 to 89.
      • The letter layer reads every citation in every anchored file (check-adr-anchors.mjs :930). Without this entry, those five would leave its reading while the gate stayed green. That is the dead-pin shape the card forbids.
    • The listed ids: each appears in the module, and each cited letter is a decision of its record:
      • ADR-0076 ### D12: honest capabilities;
      • ADR-0056 ### D2: anonymous default-deny;
      • ADR-0138 ### D2: the closed list of doors.
    • The invariant adds no citation. It writes "its D12" and "its D2", so the gate's pattern finds 0 in it.
    • Its attributions match the records and the moved code's own comments. The OpenAPI single-owner clause rests on the code's "(GET /openapi.json 有两个属主:rest-server 真serve,http-dispatcher 的 generateOpenApi 分支全仓无实现(ADR-0076 D1 影子重复) #5078, ADR-0076)". GET /openapi.json 有两个属主:rest-server 真serve,http-dispatcher 的 generateOpenApi 分支全仓无实现(ADR-0076 D1 影子重复) #5078 frames a second /openapi.json owner as ADR-0076's shadow-duplication class, so the attribution holds.
    • ADR-0057 is one of the three numbers in KNOWN_NUMBER_COLLISIONS (two records share it). Leaving it out narrows nothing, and listing it would add a fourth entry to the ambiguous-number note:
      • the letter layer reads and resolves ADR-0057 D10 whatever the list says;
      • the base anchor for rest-server.ts lists only ADR-0045, so the id layer never held this citation at base either.
    • Note, not a defect: the invariant says each handler's first act refuses an anonymous caller.
      • In fact the identity read (resolveExecCtx) comes first, because the refusal needs it.
      • The moved [#22430] comment and the matrix row use the same wording.
      • The invariant's closing clause states the property exactly: a door that answers before the anonymous refusal is the drift.
    • It is a registry entry of an existing gate, not a new gate. The ADR anchors + number uniqueness step concluded success in Lint · repo & tooling gates.
  9. The registrar list and its readers were re-counted exactly. Nothing was loosened. The list grows 7 → 8 by an appended entry. Each reader was recounted at head over the eight files:

    • execctx census:
      • resolveExecCtx mentions go 109 → 113 (57 + 12 + 7 + 9 + 12 + 3 + 9 + 4).
      • rest-server.ts stays at 57: two sites moved out, and one forwarder's two mentions came in.
      • The module holds the two sites and one interface member's two mentions.
      • Forwarder lines in rest-server.ts go 8 → 9.
    • MOVED_REGISTRARS_READING_NO_EXECCTX is an exact re-count, not a tolerance.
      • The forwarder count is pinned at exactly (8 − 1) + 3 − 1 = 9, and both maps' sums are pinned exactly, at 3 and 1.
      • A respelt or dropped forwarder reads 8 and reds.
      • A discovery body that starts reading the seam gains a forwarder, reads 10 and reds.
      • The alternative, a resolveExecCtx forwarder on a host whose body never reads it, would break the rule in item 2.
      • Like EXTRA_MOVED_REGISTRARS, the map's keys are summed, not located. All forwarders live in rest-server.ts, so a line count could not locate them either. The control is exactly as strong as before.
    • dogfood blind-spot row:
      • this.routeManager.register( is 63 = 23 + 9 + 5 + 7 + 8 + 1 + 6 + 4: rest-server.ts went 27 → 23, and the module holds 4.
      • private register*Endpoints( is 16, registerPerItemRoute( 8 and const registerPerItemRoute = 1, each unchanged. The population 70 / 19 / 51 is unchanged.
      • enforceAuth goes 92 → 96 = 41 + 11 + 7 + 9 + 12 + 3 + 9 + 4.
        • rest-server.ts stays at 41: two call sites out, the forwarder's two mentions in.
        • The module holds the two sites and the interface member's two mentions.
        • No call site was added. The control is re-anchored on the same population with the seam counted, as slices 4–6 did.
      • The new control discovery-routes.ts: this.routeManager.register( reads 4, and the list length 8.
    • ui-view pair: this.enforceAuth( goes 55 → 56 and this.resolveExecCtx( 70 → 71, one forwarder each. The ownership guard stays at 1, and the floors of 40 are unchanged.
    • docblock pin:
      • The module holds exactly 5 TSDoc blocks (the header, two interfaces, two functions), and its floor is 5.
      • The moved ranges held 0, so rest-server.ts stays at 164 against its floor of 100.
      • The floors' sum goes 29 → 34: the new module's actual 5 added to an unchanged 29. The sum is pinned.
    • query-number 9, canonical-query 4, meta-draft 8: the module has 0 numeric coercions, 0 query: literals and 0 draft tokens. Each floor is 0, and each sum is unchanged.
    • dogfood realtime tripwire: both probe regexes read 0 over all eight files.
    • All nine importers of rest-server-registrar-files.ts are in the diff.
  10. The other re-points are pure re-points: right.

    • api-backend.json: in the crud_endpoints and metadata_endpoints source lists, the (registerDiscoveryEndpoints — …) clause leaves the rest-server.ts# entry.
      • It returns as + packages/rest/src/discovery-routes.ts#registerDiscoveryEndpoints (…), with its explanatory text unchanged. That is slice 2's form (:2261).
      • The new anchor names an exported function at that path.
      • check:platform-checklist is kept out of per-PR CI by design (lint.yml). The dev's local reading (735 / 745 resolved, unresolved 10 → 10) is theirs.
    • The matrix row anonymous-deny-api-description: only rest/rest-server.ts → rest/discovery-routes.ts changes. The enforcement it describes is the moved body's, unchanged.
    • Prose: the two openapi-builtin-paths.ts docblocks and the DISCOVERY_ROUTE_KEY_TO_SERVICE_SLOTS docblock are comment-only edits, accurate about where the caller now lives.
    • Note: the re-worded buildBuiltinPaths sentence still says the handler hands it "routeManager.getAll()". Since rest 的 9 条 direct-mount 路由对 RestServer 不可枚举 —— 因此进不了 /openapi.json,也进不了任何运行时自省 #5822 the handler has passed this.getRoutes() (discovery-routes.ts :513). The dev names this stale fact as pre-existing. It is not a contract defect.
    • Other whole-file readers of rest-server.ts that are not on the list read no moved line:
      • auth-gate-allowlist-fault-window reads the auth-gate block;
      • execctx-authz-input-seam-reachability reads computeExecCtx;
      • objectql-slot-consumer-census reads the objectql slot sites;
      • runtime meta-list-projection-parity reads the /meta routes.
    • The scripts/ gates that name rest-server.ts read rows the move does not touch:
      • check-single-authz-resolver's DELEGATORS;
      • check-system-context-census's enforceAuth row;
      • check-undeclared-dep-imports' objectql probe row.
    • check-meta-type-normalized and check-affected-docs find the new file by directory and by the *-routes.ts convention.

② Semver level

  • skip-changeset is right. Nothing any released package publishes changes, in surface or in behaviour.
    • For @objectstack/rest, the .d.ts and the export list hold by construction (① item 6).
    • The routes and their registration order hold by the move (① item 1).
    • The one changed token cannot change an answer (① item 4).
    • The rest is:
      • tests, including @objectstack/dogfood's (private: true);
      • comments;
      • one gate's registry entry and one gate's target paths;
      • the checklist JSON.
    • A move that changes no behaviour is not a feature, an improvement or a fix. So AGENTS.md's changeset rule (item 3) owes no patch changeset, the same reading slices 1–6 landed on.
  • Clause-②: no is right. No accept set widens or narrows.
    • The four doors read only req.params.environmentId and the request path, as before.
    • No route is added or removed, and no spec key moves.
    • No (narrowing) arm is owed.
  • The changeset check's history:
    • Check Changeset concluded failure at 00:43:25Z, before the label.
    • skip-changeset was written at 00:48:56Z by objectstack-fleet[bot], the seat's write path.
    • The re-run concluded skipped at 00:48:59Z.

③ Boundary flags

Every dev flag is answered or escalated. The flags come from the PR body and from the report's (6205396354) open_questions, deviations and out_of_scope_findings.

  1. open_questions[0], the private static. Answered by the seat's ruling A (6205442867). This review concurs on the merits (① item 4). The seat's own fix, a later order that names the class-static exception up front, is the right repair to the dispatch wording.
  2. Deviation: the execctx forwarder control. Answered: it is an exact re-count (① item 9).
  3. Deviation: hypothesis (a) partly falsified. Answered: verified (① items 2 and 3).
  4. Deviation: "two arms" → "four arms". Answered: right (① item 7). Slice 6's carry is closed.
  5. Deviation: the ADR anchor without ADR-0057. Answered: right (① item 8). The anchor is cross-lane and declared on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023 (6205458368).
  6. Deviation: readers edited beyond the claim's named families. These are the checklist, the matrix row and three prose pointers. Answered: each reads a moved line, and each edit is a pure re-point (① item 10).
  7. Deviation: parseDeclaredApiConfig's "the discovery handler below" was not edited. Answered: right. That docblock sits on a class member and ships in the .d.ts. Slice 6 handled resolveSecurityService's docblock the same way.
  8. out_of_scope_findings: the packages/spec readers. These are rest_api.json's seven #registerDiscoveryEndpoints evidence rows (five quote moved code), plus one row each in metadata_endpoints.json and crud_endpoints.json. ESCALATED to the seat, to travel with the CRUD slice's pointers as the claim orders.
    • Every anchor still resolves to the private method, which delegates to the body.
    • Spec property liveness concluded success. No gate went silent, and a reader is one hop from the code.
    • Under ruling A, the ten #overlayDocumentationInfo rows stay exact.
  9. out_of_scope_findings: governed and release-owned prose.
    • docs/adr/0138-… carries two rest-server.ts#RestServer.register…Endpoints anchors. Both resolve, and they are carried to the maintainer (Tier H), as declared on [PM seat] domain:devx @ objectstack — ⏳ vacant #6023.
    • implementation-status.mdx is release-owned.
    • Answered: leaving both is right. The ADR symbol anchors resolve step concluded success.
  10. Moved-text deixis and the stale getAll(). The [#9292] comment (discovery-routes.ts :128–:129) says "~30 sibling handlers in this file", and in the module "this file" no longer holds them. The stale getAll() is the note in ① item 10. Note only, 承接者:无. A move-only slice may not edit the bodies, and the first non-move edit of each can fix it.
  11. Deviation: one merge of origin/main (2906cd8d2e). It is disjoint from this diff.
  12. Deviations: check:dual-build-cjs-loads exiting 3 then 0, the eslint lock reading, the trailers, the stray root pid files and the worktree cleanup. None is a contract matter.
  13. The dogfood pair has no PR-time verdict.
    • On this head, the sharded Dogfood Regression Gate leg and Dogfood Verify CLI skipped, and Dogfood Regression Gate concluded success.
    • The edits are attested by the dev's local run (108 passed) and by the static recount in ① item 9, which matches every pinned number.
    • Answered: not blocking, by CI's design. The seat watches main's first full battery, as it did for slices 4–6.
  14. The Docs Drift Check (advisory) lists 13 hand-written pages and one release-owned page, through route and symbol anchors. The routes, their order and every handler's logic are unchanged, so this diff can falsify none of them.
  15. Tier: the file list derives TIER_DEFAULT, and that is the tier this record serves, as the claim promised.
  16. PR shape:
    • The PR is a draft with base main.
    • Line 1 is Part of #23050 and line 2 is Clause-②: no. The body has no closing keyword, and Part-of PR must not also close its card concluded success.
    • The assignee is sunhaolin, and the labels are size/xl, tests and skip-changeset.
    • Marking it ready is the seat's act.
  17. Gate verdicts on this head. All 45 runs had completed when read. Taking the latest run per name (41 names) gives 32 success, 9 skipped and 0 failure. The one superseded failure is the pre-label Check Changeset (②). The successes include:
    • Build Core;
    • Test Core (1–6 and the aggregate). It runs test test:repo, so it covers rest-server-canonical-query-ast.test.ts too;
    • TypeScript Type Check and the four Type Check · lanes;
    • all eight Lint · lanes and Lint & Repo Gates;
    • Spec property liveness, Governed Surface Queue Guard, and both No other open PR may claim … checks.

Implemented-by: claude/issue-23050-s7-discovery-endpoints
Reviewed-by: session_01Q9PZ5Pzqcus4woVvTZwWNU

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

✅ ACCEPT: PR #23763 at 9ddffc0baa (#23050 slice 7), round 0. Every check on this head is green or a roster skip. The isolated review record is 6205886625 (PASS) · 2026-10-16T01:25Z

domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6204191684. Report: the os-dev-report on #23050. Seat ruling on its one open question: 6205442867 (A).

Checklist, read against GitHub and origin/main, not against the report:

  • PR shape: draft, base main, title refactor(rest):, assignee sunhaolin. Line 1 is Part of #23050: the card stays open for later slices. Line 2 is Clause-②: no.
  • Scope: 17 files, +933 / −631.
    • The move: rest-server.ts loses the bodies of registerDiscoveryEndpoints (322 lines: GET {basePath} and {basePath}/discovery) and registerOpenApiEndpoints (267 lines: GET {basePath}/openapi.json and {basePath}/docs) to the one new discovery-routes.ts, and keeps two host-literal stubs. 10,636 → 10,090 lines.
    • The registrar list grows to 8.
    • Every reader whose reading includes a moved line is re-pointed, with its sum unchanged.
    • check-governed-merges --pr 23763: NOT governed (0 of 17 paths). git merge-tree is clean against origin/main 4fe8b3def0, and no file main changed since the merge base is in this diff.
  • Move-only, proven, with one ruled token:
    • The discovery body is byte-identical after de-indentation (322 lines). The OpenAPI body differs in exactly one token: RestServer.overlayDocumentationInfo(…) → this.overlayDocumentationInfo(…), because a private static does not compile outside the class (TS2341, measured). This seat ruled A on it (6205442867).
    • getRoutes() and the ordered registrations are 14/14 identical under 7 configurations (discovery and OpenAPI on, each off, both off, and scoped). Every handler's hash is identical except openapi.json's, which equals base once the receiver is mapped back (6 of 6). A positive control proves the hash catches a one-character edit.
    • The published .d.ts / .d.cts are byte-identical (sha256 7489de84…), and both bundles' export lists are unchanged (39 each). Nothing new is exported.
    • routeManager goes by value. config goes as a getter in both host literals, measured: a post-registration assignment answers the new value, and each by-value ablation freezes its door. Hypothesis (a) was partly falsified: discovery reads no this.protocol.
    • The bodies' own imports moved with them, and no helper was copied or re-derived.
  • Changeset: none. This seat applied skip-changeset on its own check: no .changeset file is touched, and the published surface is byte-identical.
  • Gates:
    • Local, at 9ddffc0baa: 81 derived, 81 run, 0 unmeasured, all exit 0 (check:dual-build-cjs-loads first exited 3 for missing dist, and 0 after the workspace build). The @objectstack/rest suite: 285 files, 5876 passed (repo project: 238 passed). The typecheck exits 0. The dogfood pair: 108 passed.
    • Gate evidence: the check-runs pinned to 9ddffc0baa, read at this stamp. 41 distinct checks: 32 success, including Lint · ESLint, the Type Check family, all six Test Core shards, Build Core and Governed Surface Queue Guard, and 9 skipped.
      • 8 of the skips are roster names: Auto Label, Build Docs, Check PR Size, Console Pin Gate, Dogfood Regression Gate (matrix), Dogfood Verify CLI, Packed-tarball smoke, Temporal Conformance.
      • The ninth is Check Changeset, which skipped on its re-run after the skip-changeset label (its first run, before the label, was the expected red).
      • Nothing is running and nothing failed.
  • The review: isolated, served at TIER_DEFAULT (the tier record-recognisers.mjs --template --paths derives for this file list), record 6205886625, PASS.

Cross-lane, declared before review:

  • scripts/check-route-envelope.mjs on domain:spec's post (6205451586): the new module declared at 0 / 0, rest-server.ts unchanged at 17 / 17, the sums 39 and 54 held, plus slice 6's "two arms" → "four arms" comment fix.
  • The new scripts/adr-anchors/packages__rest__src__discovery-routes.ts.json and two api-backend.json source lists on domain:devx's post (6205458368).
  • The dogfood files are this lane's. packages/spec is not touched; its readings are carried below.

Accepted deviations:

  • the one ruled receiver token above;
  • the execctx census's new MOVED_REGISTRARS_READING_NO_EXECCTX control (discovery reads no resolveExecCtx, so its literal has no forwarder), pinned at exactly 1 so the forwarder count stays exact;
  • the ADR anchor not listing ADR-0057 (a number two records share);
  • readers beyond the claim's named families, each a reader of a moved line: the platform checklist's two source lists, the dogfood matrix row's prose, and three rest-lane prose pointers (none in the .d.ts);
  • rest-server.ts' parseDeclaredApiConfig docblock left saying "the discovery handler below", because it ships in the .d.ts;
  • origin/main 2906cd8d2e merged once before the PR.

The review's non-blocking notes: the reworded buildBuiltinPaths docblock still says routeManager.getAll() (pre-existing; it has been getRoutes() since #5822); the [#9292] comment's "this file" now means the module; the new anchor's "first act" wording is loose (the identity read precedes the refusal). Each is a comment, and each is carried to the first non-move edit of its text.

Carried, not edited here:

  • To the CRUD-doors slice's packages/spec pointers: packages/spec/liveness/rest_api.json (seven #registerDiscoveryEndpoints citations, five quoting code now in discovery-routes.ts, and ten #overlayDocumentationInfo rows describing its caller), plus one #registerDiscoveryEndpoints citation each in metadata_endpoints.json and crud_endpoints.json. Every anchor still resolves, and check:liveness is green.
  • For the maintainer (Tier H): docs/adr/0138-… cites rest-server.ts#RestServer.registerDiscoveryEndpoints and #registerOpenApiEndpoints; they still resolve, but the bodies now live in discovery-routes.ts.
  • 「承接者:无」: moved comments whose "this file" now means the module ([#9292], [#22430]); openapi-builtin-paths.ts' pre-existing routeManager.getAll() wording (it has been getRoutes() since rest 的 9 条 direct-mount 路由对 RestServer 不可枚举 —— 因此进不了 /openapi.json,也进不了任何运行时自省 #5822); content/docs/releases/implementation-status.mdx (release-owned, still resolves).

Landing: pr_ready + automerge_enable through the relay, in this act. On landing, #23050 is released to pm:queue for its next slice. This seat watches main's first full battery after this landing for the dogfood pair, as for slices 4–6. Per the maintainer's 「当前任务处理完,合并后就改为串行」, this seat dispatches serially (batch 1) after this landing.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants