Skip to content

Commit bcebd84

Browse files
refactor(rest): move registerDataActionEndpoints into data-action-routes.ts (#23050 slice 1) (#23116)
Part of #23050 Clause-②: no Slice 1 of the `rest-server.ts` split: the body of `RestServer#registerDataActionEndpoints` (record clone, the sync and async import doors, the import-job reads, undo and cancel, the streaming export) moves, unedited, to the new module `packages/rest/src/data-action-routes.ts`. It is move-only. Routes, the route ledger and the published `dist/*.d.ts` are byte-identical, and every reader that read the moved lines by path now reads them where they are. The card stays open for the later slices. ## The pattern later slices repeat **Members the moved body reads** (base `36b2889cdd` line numbers): fields `config` :1959, `routeManager` :1960, `cancelledImportJobs` :2064 (readonly). Methods `resolveProtocol` :2338, `enforceAuth` :2390, `enforceApiAccess` :2572, `enforceExportPermission` :2675, `enforceImportTemplateGates` :2736, `resolveExecCtx` :2785, `resolveMessageTranslator` :3890, `translateMetaItem` :3912, `answerImportTemplate` :10161, `resolveSecurityService` :10267. That is 13 members, found by an AST walk of the body. **Three shapes, measured on both criteria:** | shape | published `.d.ts` | `tsc --noEmit` | repeatable | |---|---|---|---| | A. The class keeps only the call at `registerRoutes` and the method is deleted | changes: tsup emits private members with their JSDoc, so base `dist/index.d.ts` lines 3153-3163 (the docblock plus `private registerDataActionEndpoints;`) would go | n/a | yes | | C. The class passes itself: `registerDataActionEndpoints.call(this as unknown as DataActionRouteHost, basePath)` | identical | **exit 2**: TS6133 x5, because `noUnusedLocals` stops seeing reads made through the cast (`cancelledImportJobs`, `enforceExportPermission`, `enforceImportTemplateGates`, `resolveMessageTranslator`, `answerImportTemplate`). The tsup DTS build fails the same way | only if the class also reads every member some other way | | **B. The class builds a host literal (chosen)** | **identical** | exit 0 | yes, as below | How B works: - The class keeps `private registerDataActionEndpoints(basePath)` with the same name, docblock and position. Its body is now one `DataActionRouteHost` literal and `registerDataActionEndpoints.call(host, basePath)`. - `DataActionRouteHost` lists every member the body reads, and only those. Each one is typed as the class member itself (`RestServer['enforceAuth']`, an indexed access), so a renamed or respelled member is a compile error. There is no cast, and no member of `RestServer` becomes public or protected. - Methods are passed as late-bound closures, `(...a) => this.m(...a)`. A spy or test double set on the instance or prototype after registration is still the one called, as before. - Fields are passed by value. That equals a live read here only because `cancelledImportJobs` is readonly and `config` / `routeManager` are read only while registration runs. A later slice whose body reads a reassignable field at request time passes a getter instead, checked field by field. - The moved body keeps saying `this.` (it is a `this: DataActionRouteHost` function), so the moved lines are unedited. - Module-level helpers stay in `rest-server.ts` and are `export`ed for the module (12 functions and constants). The package entry does not re-export them. - One exception, measured: exporting the `ServerScopedDataRequest` type alias from `rest-server.ts` changed `dist/index.d.ts` by one blank line, because rollup-plugin-dts emits the alias and then tree-shakes it. A second copy is ruled out by the card. So the alias moved verbatim, with its docblock, into the new module, and `rest-server.ts` imports it type-only. The CRUD, form and batch doors in `rest-server.ts` now import it from there. A later slice can rehome it. ## Proof of move-only - **`.d.ts`**: `packages/rest/dist/index.d.ts` and `index.d.cts` are byte-identical to base. sha256 is `991a8fc9cae955b56745db0b3b978a60d0d3a8f918a3a05e39ee8176601e5998` for both, before and after (`cmp` silent), rebuilt at the final head. The ESM bundle's export list is unchanged (34 names). The bundle JS bytes differ, because code moved within it. - **Routes**: a booted `RestServer` (source, recording mock server) was dumped under three configs (default, scoped-optional, scoped-required). Both `getRoutes()` and the ordered server registration calls are byte-identical, 6 dumps of 6: - default `routes.sha256=1e1859c0ff16…babd2`, calls `bc7196b91265…3fef8e` (75 routes, 9 data-action) - scoped-optional `d79275a35e12…4d74b09` / `2fa050c0d6e0…bd3650` (150 routes, 18 data-action) - scoped-required `066d85bfcdef…aa5fc6` / `7595d734a014…08775e7` (75 routes, 9 data-action) - **Route ledger**: `rest-route-ledger.ts` and `runtime/src/route-ledger.ts` are untouched. `check:route-ledger-census` output is identical base vs head. - **Moved block** (`git diff --color-moved=plain --color-moved-ws=allow-indentation-change 36b2889 HEAD` over the two files): 1098 deleted and 1115 added lines are detected as moved. Without `allow-indentation-change`, the one-level re-indent hides most of the move. The non-moved hunks: - `rest-server.ts`: the stub body (the host literal, 20 lines); one import line (plus its one-line comment); the three-line pointer comment where the alias was; `export` added to 12 module-level declarations; imports the move left unused removed (`ListImportJobsRequestSchema`, `IMPORT_JOB_MAX_ROWS` with its comment, `referenceFieldNames`, `formatRowForJson`, `cellFontColor`, `readTemplateMode`, `runImport`, `prepareImportRequest`); the `#20061` schema-import comment corrected from "three doors" to two plus a pointer. - `data-action-routes.ts`: the module docblock, the imports, `export` on the moved alias line, the `DataActionRouteHost` interface, and the function docblock and signature. ## Readers re-pointed (same rule, sums unchanged, no gate loosened) - `scripts/check-route-envelope.mjs`: the new module is declared `dialectOnly` (the guarantee these bodies had in `rest-server.ts`). rest-server's stringError goes 39 to 26, and the new module carries 13. siblingCode goes 54 to 39, and the new module carries 15. Both new counts are measured by the gate, and both sums are unchanged. - `packages/rest/src/execctx-consumer-census.test.ts`: - Sites are keyed `file:line` over both files: 62 = 53 + 9. - The stack attribution steps over the host literal's one forwarder frame. The forwarder's line is pinned. - Mentions go from 77 to 81. The four new mentions are the seam itself (the forwarder and the interface member), not consumers. - `rest-server-query-number-census.test.ts`: reads both files. The ledger is unchanged, because keys are routes, not files. - `rest-server-canonical-query-ast.test.ts`: the query-slot floor of 4 becomes 1 + 3 (the new module is censused under the same door rule), and the two by-name literals are asserted where they now live. - `ui-view-route-identity.measurement.test.ts` and `ui-view-route-tenancy.measurement.test.ts`: the whole-file spelling reverse checks count over both files. - `rest-server-docblock-position.test.ts`: the module is read under the same no-stacked-docblock rule, floor 4 (its measured count), because the alias docblock moved there. - `packages/qa/dogfood/test/authz-probe-blind-spot.census.ts`: - The rest-server row's registration population reads the module too, so 70 / 19 / 51 is unchanged. The nine moved routes are still RestServer's and still in the blind spot. - A new control covers the module's own share (9). - `enforceAuth` goes from 57 to 61, re-anchored: the four new occurrences are the seam, and no call site was added. - `docs/qa/platform-checklist/areas/records-forms.json`: the clone item's source anchor now names `data-action-routes.ts#registerDataActionEndpoints`. **Readers examined and not re-pointed, because the moved lines contribute zero to them** (measured on the module): - The dogfood realtime TRIPWIREs on `rest-server.ts`: 0 mechanics hits and 0 route hits. - `objectql-slot-consumer-census`: 0 provider reads. A future read would have to add a member to the host literal in `rest-server.ts`, where this census reads. - `meta-draft-read-door-census`: 0 draft switches. - `auth-gate-allowlist-fault-window`, `execctx-authz-input-seam-reachability`, `runtime meta-list-projection-parity`: they slice other registrars. - These gates' outputs are identical base vs head, apart from scanned-file counts going up by one: `check-durability-degradation-log-level`, `check-system-context-census`, `check-single-authz-resolver`, `check-undeclared-dep-imports`, `check-dispatcher-error-vocabulary`, `check-meta-type-normalized`. - The ADR anchor `packages__rest__src__rest-server.ts.json` (ADR-0045, `filterAppForUser`) is unaffected. ## Acceptance notes - Two `packages/spec/liveness` evidence strings name `rest-server.ts#registerDataActionEndpoints` (`crud_endpoints.json` `dataPrefix`, `route_generation.json` `overrides`). They still resolve, to the private method that registers these routes by calling the moved body, and `check:liveness` is green. Their prose about `dataPath` and `body.overrides` now describes code in `data-action-routes.ts`. They are not edited here, because the claim keeps `packages/spec` out of this slice. Named in the report. - `check:platform-checklist` is red on the base tree with the same 8 findings, none in `records-forms.json`. The output is identical base vs head. - Lines of `rest-server.ts` after the moved block shift by about 1,100. No line citation in the repo points into the moved range, base 9107-10137 or 311-372. ## Gates All at `5a05b4bde6` (the slice plus a merge of `origin/main` `965a1bfc72`; no overlap with this diff). - `pnpm --filter @objectstack/rest test`: Test Files 275 passed (275) before (`36b2889cdd`) and after; Tests 5759 passed | 326 skipped (6085) before, 5761 passed | 326 skipped (6087) after (+2 = the docblock pin's new block for the moved module). VERDICT command-exit 0. - `pnpm --filter @objectstack/rest test:repo`: Test Files 5 passed (5) before and after; Tests 209 passed | 1 skipped (210) before, 213 passed | 2 skipped (215) after (+5 = the canonical-query-ast census's new file entry: 4 cases + 1 skipIf). VERDICT command-exit 0. - `pnpm --filter @objectstack/rest typecheck`: exit 0 (`tsc --noEmit` and `check:test-typecheck`: 0 files / 0 errors in the debt ledger). - `pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts`: 2 files, 92 passed. VERDICT command-exit 0. - Lint, proven narrowing: `eslint --no-inline-config --format json` over the 10 touched lintable files gives 10 results, 0 errors, 0 warnings. ESLint's own `isPathIgnored` is false for each one, and `calculateConfigForFile` gives 2 to 6 rules each. The config has no `parserOptions.project` (no type-aware lint), and its only file inputs (`eslint.config.mjs`, `scripts/slot-lookup-baseline.json`, `scripts/query-options-erasure-baseline.json`) are untouched, so no untouched file's verdict can move. - `dispatch-gates` derivation with no paths: 79 commands. `--ran` reconciliation: 79 derived, 78 run, 1 NOT-MEASURED, 0 UNRUN. - 77 of 79 exit 0. - `pnpm check:platform-checklist` exits 1 with the same 8 findings as the base tree (none in `records-forms.json`). - NOT MEASURED: `check:dual-build-cjs-loads`, reason: PREREQUISITE NOT MET (exit 3, 19 workspace packages have no `dist/` in this container). As a narrow stand-in, `packages/rest/dist/index.cjs` and `index.js` both load with 34 exports each. CI runs the gate over a full build. Per command (exit code): - `node scripts/check-ci-filter-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs` exit 0 - `node scripts/check-closing-keyword-parity.mjs --self-test` exit 0 - `node scripts/check-comment-mask-adoption.mjs` exit 0 - `node scripts/check-comment-mask-adoption.mjs --self-test` exit 0 - `node scripts/check-comment-mask-corpus.mjs` exit 0 - `node scripts/check-declaration-mirrors.mjs` exit 0 - `node scripts/check-declaration-mirrors.mjs --self-test` exit 0 - `node scripts/check-dts-emitted.mjs --self-test` exit 0 - `node scripts/check-issue-citations.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs` exit 0 - `node scripts/check-keyed-text-bounds.mjs --self-test` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs` exit 0 - `node scripts/check-platform-object-tenancy-census.mjs --self-test` exit 0 - `node scripts/check-plugin-teardown-shape.mjs` exit 0 - `node scripts/check-plugin-teardown-shape.mjs --self-test` exit 0 - `node scripts/check-registry-log-declared.mjs` exit 0 - `node scripts/check-registry-log-declared.mjs --self-test` exit 0 - `node scripts/check-rest-log-spy-declared.mjs` exit 0 - `node scripts/check-rest-log-spy-declared.mjs --self-test` exit 0 - `node scripts/check-scripts-symbol-anchors.mjs` exit 0 - `node scripts/check-scripts-symbol-anchors.mjs --self-test` exit 0 - `node scripts/check-self-test-wired.mjs` exit 0 - `node scripts/check-self-test-wired.mjs --self-test` exit 0 - `node scripts/check-self-test-workflow-commands.mjs` exit 0 - `node scripts/check-self-test-workflow-commands.mjs --self-test` exit 0 - `node scripts/check-system-context-census.mjs` exit 0 - `node scripts/check-system-context-census.mjs --self-test` exit 0 - `node scripts/check-undeclared-dep-imports.mjs` exit 0 - `node scripts/check-undeclared-dep-imports.mjs --self-test` exit 0 - `node scripts/check-whole-set-label-write.mjs` exit 0 - `node scripts/check-whole-set-label-write.mjs --self-test` exit 0 - `node scripts/docs-audit/check-affected-docs.mjs` exit 0 - `node scripts/docs-audit/check-drift-comment.mjs` exit 0 - `node scripts/pm/bare-root-worklist.mjs --self-test` exit 0 - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` exit 0 - `pnpm --filter @objectstack/spec run check:duration-unit-keys` exit 0 - `pnpm --filter @objectstack/spec run check:empty-state` exit 0 - `pnpm --filter @objectstack/spec run check:liveness` exit 0 - `pnpm --filter @objectstack/spec run check:strictness-ledger` exit 0 - `pnpm --filter @objectstack/spec run check:variant-docs` exit 0 - `pnpm check:agent-test-spelling` exit 0 - `pnpm check:authz-resolver` exit 0 - `pnpm check:bash32-floor` exit 0 - `pnpm check:cli-command-ids` exit 0 - `pnpm check:cross-package-test-inputs` exit 0 - `pnpm check:dispatcher-error-vocabulary` exit 0 - `pnpm check:doc-authoring` exit 0 - `pnpm check:driver-memory-census` exit 0 - `pnpm check:dts-closure` exit 0 - `pnpm check:dual-build-cjs-loads` exit 3 - `pnpm check:engine-double-contract` exit 0 - `pnpm check:entry-guard` exit 0 - `pnpm check:error-status-conformance` exit 0 - `pnpm check:gitlink-declared` exit 0 - `pnpm check:issue-citations` exit 0 - `pnpm check:lean-entry-closure` exit 0 - `pnpm check:logger-receiver-detach` exit 0 - `pnpm check:nul-bytes` exit 0 - `pnpm check:objectql-double-limit` exit 0 - `pnpm check:org-identifier` exit 0 - `pnpm check:page-declaration-shape` exit 0 - `pnpm check:parse-guard` exit 0 - `pnpm check:platform-checklist` exit 1 - `pnpm check:pm-dispatch-gates` exit 0 - `pnpm check:pnpm-filter-targets` exit 0 - `pnpm check:published-files` exit 0 - `pnpm check:query-options-erasure` exit 0 - `pnpm check:ratchet-remedy-authority` exit 0 - `pnpm check:refd-timer-probe` exit 0 - `pnpm check:route-envelope` exit 0 - `pnpm check:slot-lookup` exit 0 - `pnpm check:sourcemap-no-sources-content` exit 0 - `pnpm check:test-source-alias` exit 0 - `pnpm check:tier-file-adoption` exit 0 - `pnpm check:type-check-coverage` exit 0 - `pnpm check:type-check-debt` exit 0 - `pnpm check:watch-hint-literal` exit 0 - `pnpm check:where-matcher` exit 0 --- _Generated by [Claude Code](https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f451a59 commit bcebd84

11 files changed

Lines changed: 1501 additions & 1184 deletions

‎docs/qa/platform-checklist/areas/records-forms.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@
165165
"dogfood-verification skill §3",
166166
"examples/app-showcase/src/data/objects/account.object.ts (requiredness + format/conditional validations)",
167167
"packages/runtime/src/route-ledger.ts (/data CRUD routes)",
168-
"packages/rest/src/rest-server.ts#registerDataActionEndpoints (POST /data/:object/:id/clone → registerDataActionEndpoints) + packages/metadata-protocol/src/protocol.ts#cloneData (cloneData: enable.clone gate, findOne-in-caller-context, CLONE_STRIP_FIELDS; the static-readonly strip is engine.insert's since #14147)",
168+
"packages/rest/src/data-action-routes.ts#registerDataActionEndpoints (POST /data/:object/:id/clone → registerDataActionEndpoints) + packages/metadata-protocol/src/protocol.ts#cloneData (cloneData: enable.clone gate, findOne-in-caller-context, CLONE_STRIP_FIELDS; the static-readonly strip is engine.insert's since #14147)",
169169
"packages/rest/src/rest-route-ledger.ts (POST /api/v1/data/:object/:id/clone, client data.clone)",
170170
"objectui: e2e/live/record-history-display.spec.ts",
171171
"cross-ref: the inline-edit atomic two-surface behavior (ONE Save bar / ONE PATCH carrying exactly the changed keys + ifMatch) is folded into records-forms.concurrent-edit-conflict, not here"

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

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -486,7 +486,8 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
486486
'route registration sites — `this.routeManager.register(` call sites, LESS the one inside ' +
487487
'`registerPerItemRoute` (the shared forwarder, not a route; its extent is bounded by the declaration\'s own ' +
488488
'indentation and the subtrahend is pinned at 1 by the `forwarder slice:` control, never inferred from a ' +
489-
'terminator spelling), PLUS `registerPerItemRoute(` call sites; ' +
489+
'terminator spelling), PLUS `registerPerItemRoute(` call sites; counted over this file AND ' +
490+
'`data-action-routes.ts`, the module `registerDataActionEndpoints`\' body moved to; ' +
490491
'reachable = those inside registerMetadataEndpoints',
491492
// [#15542 / #15854] ⭐ THE POPULATION RULE LEARNED A SECOND SPELLING, and
492493
// the numbers it produces did NOT move: 80 / 19 / 61, exactly as before.
@@ -593,17 +594,30 @@ export const PROBE_FILE_CENSUS: readonly ProbeFileReading[] = [
593594
// `registerMetadataEndpoints`, so it was a blind spot; its handler opened
594595
// with `enforceAuth`, so that control drops by one. The registrar
595596
// survives, so `private register*Endpoints(` stays 16.
597+
// [#23050 slice 1] 70 / 19 / 51 UNCHANGED, and that is the measurement:
598+
// `registerDataActionEndpoints`' body (nine routes, all outside
599+
// `registerMetadataEndpoints`, so all nine in the blind spot) moved to
600+
// `data-action-routes.ts`, and the population rule reads that module too —
601+
// see the derivation. `this.routeManager.register(` stays 63 over both files
602+
// (54 here + 9 there, the new control), `private register*Endpoints(` stays
603+
// 16 (the registrar is still declared here), and `enforceAuth` 57 -> 61 is
604+
// RE-ANCHORED, not relaxed: the nine call sites moved with their routes and
605+
// are counted in both readings; the four new occurrences are the seam that
606+
// carries the move — the host literal's forwarder (`enforceAuth: (...a) =>
607+
// this.enforceAuth(...a)`, two) and the host interface's member
608+
// (`enforceAuth: RestServer['enforceAuth']`, two). No call site was added.
596609
controls: {
597610
'private register*Endpoints(': 16,
598611
'this.routeManager.register(': 63,
612+
'data-action-routes.ts: this.routeManager.register(': 9,
599613
// Both halves of the new rule carry their own control, so neither can go
600614
// silently to zero: a helper deleted and its routes inlined back would
601615
// still read population 80, and only these two controls would notice the
602616
// shape moved and force this provenance to be re-read.
603617
'registerPerItemRoute(': 8,
604618
'const registerPerItemRoute =': 1,
605619
'forwarder slice: this.routeManager.register(': 1,
606-
enforceAuth: 57,
620+
enforceAuth: 61,
607621
},
608622
note:
609623
'The single non-tripwire probe names ONE registrar of 16. The other 15 can never mint a key: ' +
@@ -896,6 +910,18 @@ export function deriveProbeFileCensus(): {
896910
// counting. Counting it would double-count every helper-routed route.
897911
{
898912
const src = read('packages/rest/src/rest-server.ts');
913+
// [#23050 slice 1] ONE POPULATION, NOW TWO FILES — the same lesson as the
914+
// second spelling above. `registerDataActionEndpoints`' body moved, whole and
915+
// unedited, to `data-action-routes.ts`: its nine `this.routeManager.register(`
916+
// sites still register `RestServer` routes (the private registrar is still
917+
// here and calls the moved body), so they are still this row's population.
918+
// Reading `rest-server.ts` alone would record 61 / 19 / 42 — a nine-route hole
919+
// in the blind spot this census keeps honest, opened by a move that changed
920+
// no route. The moved module defines no registrar (`private register…(`) and
921+
// no `registerPerItemRoute`, so appending it moves neither the forwarder
922+
// slice nor the `reachable` slice; its own control below proves it is read.
923+
const moved = read('packages/rest/src/data-action-routes.ts');
924+
const all = `${src}\n${moved}`;
899925
const registrarRe = /^\s*private\s+register[A-Za-z]*Endpoints\s*\(/gm;
900926
const mountRe = /this\.routeManager\.register\(/g;
901927
// Matches the CALL sites only. The declaration reads
@@ -991,11 +1017,14 @@ export function deriveProbeFileCensus(): {
9911017
const start = decls[metaIdx]?.at ?? 0;
9921018
const end = decls[metaIdx + 1]?.at ?? src.length;
9931019
files.set('packages/rest/src/rest-server.ts', {
994-
population: sites(src),
1020+
population: sites(all),
9951021
reachable: sites(src.slice(start, end)),
9961022
controls: {
9971023
'private register*Endpoints(': occurrences(src, /private\s+register[A-Za-z]*Endpoints\s*\(/g),
998-
'this.routeManager.register(': occurrences(src, /this\.routeManager\.register\(/g),
1024+
'this.routeManager.register(': occurrences(all, /this\.routeManager\.register\(/g),
1025+
// [#23050 slice 1] The moved module's own share, so a reading that
1026+
// silently stopped appending it cannot hide behind the total above.
1027+
'data-action-routes.ts: this.routeManager.register(': occurrences(moved, /this\.routeManager\.register\(/g),
9991028
'registerPerItemRoute(': occurrences(src, /registerPerItemRoute\(/g),
10001029
'const registerPerItemRoute =': occurrences(src, /const\s+registerPerItemRoute\s*=/g),
10011030
// ⭐ The one control here that is NOT a whole-file count, deliberately:
@@ -1004,7 +1033,7 @@ export function deriveProbeFileCensus(): {
10041033
// real population drop. The four counts around it cannot — all four are
10051034
// green while the slice is running 3873 lines long.
10061035
'forwarder slice: this.routeManager.register(': occurrences(forwarderSlice(src), mountRe),
1007-
enforceAuth: occurrences(src, /enforceAuth/g),
1036+
enforceAuth: occurrences(all, /enforceAuth/g),
10081037
},
10091038
});
10101039
}

0 commit comments

Comments
 (0)