Skip to content

refactor(rest): move registerCrudEndpoints into crud-routes.ts (#23050 slice 12) - #24205

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23050-s12-crud-endpoints
Oct 17, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-23050-s12-crud-endpoints

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23050
Clause-②: no

Slice 12 of the rest-server.ts split: CRUD. It follows slice 1's pattern and slices 2–11's shape, with slice 11 (PR #24054) as the direct precedent. The body of RestServer#registerCrudEndpoints moves, re-indented one level and otherwise unedited, to one new module, packages/rest/src/crud-routes.ts. That body is the six record doors under {basePath}{crud.dataPrefix}: list, read, create, the QueryAST query, update and delete.

Nothing else moves:

  • the route table, the ordered registrations and every handler's source (whitespace-free digest) are byte-identical;
  • the published dist/*.d.ts is byte-identical, and so are both bundles' export lists;
  • no token of the body changed.

rest-server.ts goes from 9,512 to 9,086 lines (base cb8756b00). #23050 stays open for the last slice, /meta (registerMetadataEndpointsInner), which this PR does not touch.

packages/spec/liveness/crud_endpoints.json gets six evidence-path substitutions. The consumer text, quoted code, producers, verdicts and statuses are unchanged. This is the one packages/spec exception declared to the spec seat on #6017 as 6235976157, under slice 11's precedent (ruling A 6225503839).

The pattern, applied

Members the moved body reads. Measured by an AST walk of the method (TypeScript compiler API, base cb8756b00). The method spans :8323–:8759, and the body inside it spans :8324–:8758 (435 lines).

kind member reads class declaration
field config 1 (:8324, the body's first statement) :1997, private
field routeManager 6, the six register calls :1998, private
method resolveProtocol 6 :2376, private async
method resolveExecCtx 6 :2863, private async
method enforceAuth 6 :2444, private
method enforceApiAccess 6 :2631, private async

The body has no bare this, no element access on this, no RestServer. static access and no function keyword.

Control walk (the same walker on the same base): the registerUiEndpoints stub reads routeManager, resolveRequestEnvironmentId, resolveExecCtx, enforceAuth, enforceEnvironmentOwnership and resolveProtocol once each. That is slice 10's host literal, member for member.

Module-level names the body reads (the same walk):

  • Read only by this body, so the import moves with it. A pointer comment is left in rest-server.ts' import block for each:
  • Still read elsewhere, so the import stays. The module imports the same names from the same modules: mapDataError, refuseUnknownQueryParams, refuseRepeatedQueryParams, zodIssuesToFields and validationFailure.
  • ServerScopedDataRequest is imported from data-action-routes.ts, where slice 1 moved it. Its last reader in rest-server.ts was this body, so rest-server.ts no longer imports it. The slice-1 note there now says so.
  • Two names are declared in rest-server.ts itself, stay there and are imported from it:
    • DATA_RECORD_READ_PARAMS. It was already exported, and its pin test (rest-server-closed-query-params.test.ts) imports it from there. This is slice 3's shape for APPROVAL_REQUEST_LIST_PARAMS.
    • versionTokenRefusal. It gains export for this module only, the shape slices 1 and 2 used for their helpers. It does not move, because the runtime dispatcher's /data twin (packages/runtime/src/domains/data.ts:79) names packages/rest/src/rest-server.ts as the home of its full argument, and packages/runtime/** is outside this slice. Moving it would make that pointer false.
    • The package entry re-exports neither name, and the .d.ts is byte-identical (below). Both are read only at request time, so the two modules importing each other cannot see them uninitialised.

Host shape (slice 1's shape B). CrudRouteHost has 6 members, in the order the body first reads them, each typed RestServer['m'].

  • The class keeps the private method's name, docblock and position. Its JSDoc plus signature (base :8320–:8323, head :8316–:8319) diffs to 0.
  • Its body is the host literal plus registerCrudEndpoints.call(host, basePath). Methods go as late-bound closures, (...a) => this.m(...a).
  • The call site in registerRoutes' registerForBase is unchanged: if (this.config.api.enableCrud) { this.registerCrudEndpoints(bp); }, after registerDataActionEndpoints. registerRoutes (75 lines) diffs to 0 between base and head.

Fields, checked one by one:

field read when reassigned after registration? handed over as
config registration only: const { crud } = this.config; is the body's first statement, and the handlers close over crud, operations, dataPath and isScoped no, assigned only in the constructor (:2138 at head) value
routeManager registration only (the six register calls) its one swap (:5166/:5170 at head) wraps registerMetadataEndpointsInner, not this registrar value

No provider field is read. The protocol, the caller's context, the anonymous-deny gate and the per-object API exposure gate are each resolved per request by a METHOD, so no getter is owed.

Route switches. The registrar has its own switches: api.enableCrud, the five crud.operations (list gates two mounts) and crud.dataPrefix. So slice 10/11's nine configurations are extended with four CRUD ones (below).

Late binding, measured. A one-off vitest file was copied in, run under the verify lock and deleted; it is not committed.

  • Setup. A source RestServer gets the four methods planted BEFORE registerRoutes(), and all six doors are driven. Then four new methods are planted AFTER registration, and the six doors are driven again.
  • Head: MEASURE before: GET 200 {"records":[{"id":"early"}]} | GET 200 {"id":"early"} | POST 201 {"id":"early"} | POST 200 {"records":[{"id":"early"}]} | PATCH 200 {"id":"early"} | DELETE 200 {"id":"early","success":true}. After the re-plant, every door answers "late". The order is late:proto,late:ctx,late:auth,late:api ×6, with early calls after re-plant 0. Tests 1 passed (1).
  • Ablation: the host literal's enforceApiAccess closure was replaced by this.enforceApiAccess.bind(this) through scripts/ablation-replace.mjs (anchor 1 → 0, blob bc2c21614369 → d8d11e7ef378). early:api then runs on all six doors after the re-plant: AssertionError: expected [ 'early:api', 'early:api', …(4) ] to deeply equal [], Tests 1 failed (1). The tool restored the file: blob bc2c21614369 equals HEAD, and git diff HEAD is empty.
  • Base leg: the base rest-server.ts (blob 5a0f948c2e8a) was put in the tree under a trap. It printed the same two MEASURE lines byte for byte, Tests 1 passed (1). Restored: blob equals HEAD, git diff HEAD empty.

Proof of move-only

  • Moved body. Base rest-server.ts lines 8324–8758, with one 4-space indent removed, against crud-routes.ts lines 107–541: diff exits 0 over all 435 lines, so no token changed.

    • Every non-empty line of the range carries the method indent.
    • No line has trailing whitespace.
    • No line has an odd backtick count, so no template literal spans the cut.
  • .d.ts. packages/rest/dist/index.d.ts and index.d.cts were built at base cb8756b00 before any edit and at 45d976972. Both legs are sha256 1f0f455d24836d47de60716233f1bd74d1738c04046974019264b17b5261bbe8, and cmp exits 0 on each pair.

    • The head bundles carry a head-only marker: registerCrudEndpoints.call hits 1 in each of index.js and index.cjs, and 0 at base. So the declaration was rebuilt from the moved source.
    • Exporting versionTokenRefusal from rest-server.ts moved no declaration. That matches slice 1's measurement: exporting a function or a constant does not change the bundle's .d.ts.
  • Export lists. The base and head ESM and CJS bundles each load with 39 exports and one sorted export-name list (sha256 b3246be6a1718700… on all four).

  • Routes (RestServer.getRoutes()). A source RestServer was booted on a recording mock server, with a protocol answering every member. Each dump holds:

    • getRoutes() (functions as arity);
    • the ordered server registration calls;
    • a whitespace-free sha256 of every handler's source, plus that source.

    It was dumped under thirteen configs:

    • slice 10/11's nine: default; scoped-optional; scoped-required; every provider set; enableDiscovery: false; enableOpenApi: false; both off under scoped-optional; enableSearch: false under scoped-optional; enableUi: false under scoped-optional;
    • this slice's four, for the registrar's own switches: enableCrud: false under scoped-optional; crud.operations { list: false, delete: false } under scoped-optional; all five operations false; crud.dataPrefix: '/objects' under scoped-optional.

    Results:

    • Route counts are 75 / 150 / 75 / 75 / 73 / 73 / 142 / 148 / 148 / 138 / 144 / 69 / 150, with 6 / 12 / 6 / 6 / 6 / 6 / 12 / 12 / 12 / 0 / 6 / 0 / 12 CRUD-tagged routes. The first nine are slice 11's figures.
    • routes.json, calls.json and handlers.json are byte-identical between base and head in all thirteen configs. The sha256 over the 39 dump files is 8e9544b5b00f03081398465aa73416f51aee7a4aeef956ed550283937017daa8 on both legs, and the default config's routes.json is 6931d1a5d1827fea… on both.
    • A second base run was identical to the first (diff -r exit 0). Every handler digest is identical, because no token changed.
  • The handler digest can fail. A positive control added one character to the moved PATCH handler through ablation-replace.mjs: 'Invalid update request' became 'Invalid update request!' (anchor 1 → 0, blob d25749c75351 → 251ede3ffa43).

    • Routes and calls were unchanged in all thirteen configs.
    • The handler digests moved in exactly the PATCH …/:object/:id door of each mounted base, and nowhere else. That is 1 or 2 per config, and none in the two configs where that door is not mounted.
    • Restored: blob d25749c75351 equals HEAD, git diff HEAD empty.
  • The route ledger the gates read (packages/rest/src/rest-route-ledger.ts, packages/runtime/src/route-ledger.ts) is not edited. Its conformance tests against getRoutes() pass in the local run below.

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

packages/rest/src/rest-server-registrar-files.ts gains crud-routes.ts, with a [#23050 slice 12] comment (length 12 → 13). Every reader that pins its length re-reads its number:

reader sum over the list change
execctx-consumer-census.test.ts 62 sites = 16 + 9 + 5 + 7 + 8 + 1 + 5 + 2 + 1 + 1 + 1 + 0 + 6; mentions 125 → 129 The six CRUD sites (bare, each guarded on its next line) moved. The four new mentions are the seam: the forwarder, two, and the host interface's member, two. rest-server.ts goes 60 → 56. Thirteen forwarders = (13 − 1) + 3 − 2. Length 12 → 13
meta-draft-read-door-census.test.ts 8 = 6 + 0 + 0 + 0 + 0 + 2 + 0 ×7 The CRUD doors read no draft. Length 12 → 13
rest-server-query-number-census.test.ts 9 = 8 + 1 + 0 ×11 The CRUD doors coerce no number. Length 12 → 13
rest-server-canonical-query-ast.test.ts (repo project) 4 = 0 + 3 + 0 ×10 + 1 query: slots The list door's caller-supplied query: req.query moved, so the floors are rest-server.ts 1 → 0 and crud-routes.ts: 1. Length 12 → 13
rest-server-docblock-position.test.ts floors 4 + 3 + 3 + 8 + 3 + 8 + 5 + 3 + 3 + 4 + 3 + 3 = 50 The floor is crud-routes.ts: 3: the module header, the host interface's block and the function's. The CRUD body held no TSDoc block, so rest-server.ts keeps its 163. Length 12 → 13
ui-view-route-identity.measurement.test.ts this.enforceAuth( 59 → 60, this.resolveExecCtx( 74 → 75 +1 each for the host literal's forwarder; the six call sites are counted in both readings. Length 12 → 13
ui-view-route-tenancy.measurement.test.ts as above; the ownership guard is still 1 call site + 1 forwarder Length 12 → 13
dogfood authz-conformance.test.ts (realtime tripwires) 0 hits over the thirteen files Length 12 → 13
dogfood authz-probe-blind-spot.census.ts, rest-server.ts row 70 / 19 / 51 unchanged; this.routeManager.register( 63 = 12 + 9 + 5 + 7 + 8 + 1 + 6 + 4 + 1 + 1 + 1 + 2 + 6; private register*Endpoints( 16; enforceAuth 108 → 112 New control crud-routes.ts: this.routeManager.register( 6; registrar list length 13; populationRule names the new module. enforceAuth is RE-ANCHORED, not relaxed: the six call sites moved and are counted in both readings, and the four new occurrences are the seam

Readers outside the list:

  • scripts/check-route-envelope.mjs (a gate's target table, domain:spec).
    • crud-routes.ts is declared dialectOnly with ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the eleven earlier modules are. Its measured counts are stringError 3 and siblingCode 3: the three request gates' 400 VALIDATION_FAILED bodies ('Invalid create request', 'Invalid query request', 'Invalid update request').
    • rest-server.ts drops by exactly those bodies: stringError 11 → 8 and siblingCode 7 → 4, each recorded as "NOT progress — the same MOVE".
    • The sums are unchanged: 8 + 13 + 8 + 1 + 0 + 0 + 0 + 0 + 1 + 0 + 1 + 4 + 3 = 39, and 4 + 15 + 8 + 9 + 4 + 1 + 0 + 0 + 3 + 0 + 1 + 6 + 3 = 54.
    • Its output differs from base only in the rest-server.ts row and the new row (22 → 23 modules audited, 13 → 14 ratcheted).
  • packages/spec/liveness/crud_endpoints.json. Six evidence paths packages/rest/src/rest-server.ts#registerCrudEndpoints → packages/rest/src/crud-routes.ts#registerCrudEndpoints: the five operations.* rows (if (operations.X)) and the first citation of the dataPrefix row (const dataPath = …crud.dataPrefix…). Every quoted fragment now lives in crud-routes.ts. See the next section.
  • docs/qa/platform-checklist/areas/api-backend.json (domain:devx, slice 7/11's precedent). Three sources name crud-routes.ts; the text after each anchor is unchanged, and no history entry is added:
    • api-backend.query-contract-matrix: rest-server.ts (POST /data/:object/query answers a FindDataRequestSchema failure …) → crud-routes.ts (…).
    • api-backend.aggregate-contract-matrix: rest-server.ts#data (POST /data/:object/query …) → crud-routes.ts#data (…).
    • api-backend.rest-crud-config-contract: rest-server.ts#registerCrudEndpoints (the six mounts and their five gates), (normalizeConfig …), (parseDeclaredSubConfig …), (getRoutes …) → crud-routes.ts#registerCrudEndpoints (the six mounts and their five gates) + packages/rest/src/rest-server.ts (normalizeConfig …), (parseDeclaredSubConfig …), (getRoutes …). The three helpers did not move, and slice 7 split the same source with + in the same way.
    • check:platform-checklist: 853 of 863 anchors resolve on both trees; cited sources go 400 → 401.
  • No ADR anchor. The CRUD body cites no ADR. check-adr-anchors reads 70 anchored files and 1,782 decision-letter citations at base, at the move commit and at the head. scripts/adr-anchors/packages__rest__src__rest-server.ts.json anchors ADR-0045's filterAppForUser, which did not move.
  • No content/docs/** anchor. Measured: no page under content/docs (references and releases excluded) cites the CRUD code by path, symbol or line.

Red before the re-points, each measured with the readers at their base text:

  • At the move commit c8f1643c8 (the list not yet grown):
    • local read Test Files 1 failed | 290 passed (291), Tests 3 failed | 6004 passed | 326 skipped (6333). The failures are the execctx census: expected 56 to be 62, then expected 40 to be 46 twice. The six CRUD sites had left the reach of a reader that did not yet know the module.
    • repo read Tests 2 failed | 252 passed | 12 skipped (266). These are the canonical-query-AST floor (expected 0 to be greater than or equal to 1) and its slot sum (expected 3 to be 4).
    • check:route-envelope read crud-routes.ts NOT DECLARED, stringError: found 8, declared 11 and siblingCode: found 4, declared 7.
  • At 3e58c49cb (the list grown, the readers not yet re-pointed):
    • local (the six list readers) read Test Files 6 failed (6), Tests 7 failed | 115 passed (122). These are the six length pins (expected [ 'rest-server.ts', …(12) ] to have a length of 12 but got 13) and the docblock pin's unfloored new module.
    • repo read Tests 3 failed | 76 passed | 13 skipped (92): the length pin and both floors.
    • The dogfood pair read Tests 2 failed | 106 passed (108): the tripwire's length pin, and control "registrar list length" moved in packages/rest/src/rest-server.ts: expected 13 to be 12.
  • Each is green after the re-point (45d976972, then the merge head).

The packages/spec reader, re-pointed (the declared exception)

crud_endpoints.json cited rest-server.ts#registerCrudEndpoints for the code that gates and prefixes the CRUD mounts, and that code now lives in crud-routes.ts. check:liveness stays green either way:

  • the stub keeps the symbol registerCrudEndpoints in rest-server.ts;
  • rest-server.ts still names every key (operations, dataPrefix) in normalizeConfig.

So without the substitution the six rows would have read as anchored while pointing at a file that no longer holds the quoted code. That is the dead-pin shape the card warns about, and no gate would have caught it. The substitution keeps them pointing at the consumer.

check:liveness at the head exits 0. It reads symbol anchors: 945 pointer(s) written path#symbol, 945 naming a symbol the cited file contains. and key-mention anchoring: 735 (entry, cited file) pair(s) asked, 734 anchored, 1 exempt. (734 / 733 / 1 at base). The dataPrefix row now cites two files, crud-routes.ts for the CRUD mount and rest-server.ts for its four sibling consumers, so it asks one more pair, and that pair anchors.

No rest_api.json row cites the moved code. That was measured over every liveness file by extracting each backtick-quoted fragment from the rows that cite rest-server.ts. Only the six crud_endpoints.json rows quote code found in the CRUD body and nowhere else in rest-server.ts. rest_api.json's enableCrud row cites registerRoutes' if (this.config.api.enableCrud), which did not move.

Changesets:

  • .changeset/23050-s12-crud-liveness-citations.md is a @objectstack/spec patch (Clause-②: no), because liveness/ ships in that package's files[].
  • .changeset/23050-s12-crud-routes-move.md is a @objectstack/rest patch (Clause-②: no, move-only), because the shipped dist/index.js and index.cjs bytes change: the head-only marker above. The declarations and exports do not change.
  • skip-changeset does not apply.

Readers examined and not re-pointed

  • Prose that names the registrar without a file or line, and is still true:

    • registerRoutes' comment ("the greedy GET /data/:object/:id matcher in registerCrudEndpoints");
    • DATA_RECORD_READ_PARAMS' docblock ("Measured, at registerCrudEndpoints' read-record handler");
    • ui-view-route-identity.measurement.test.ts (byRegistrar.get('registerCrudEndpoints'), read off the mounted registrar at runtime);
    • the dogfood census note;
    • docs/qa/platform-checklist/FOLLOW-UPS.md E3;
    • packages/spec/src/api/rest-server.zod.ts and the 18.api__CrudEndpointsConfig__patterns retired-key entry ("the server's registerCrudEndpoints");
    • the generated content/docs/references/api/rest-server.mdx.
  • crud_endpoints.json's patterns note says "every CRUD route is mounted from the fixed method/path pairs in rest-server.ts#registerCrudEndpoints". It is a dead row's retirement record, dated 2026-09-03, and it still resolves by name to the stub. It was left as written, together with the dataPrefix row's four sibling citations (#registerDataActionEndpoints, #registerSharingEndpoints, #registerBatchEndpoints, #registerDiscoveryEndpoints), which cite bodies earlier slices moved. That is outside this slice's declared substitution (see Acceptance notes).

  • docs/adr/0036-… cites @objectstack/rest registerCrudEndpoints(). It is Tier H and not edited, and the name still resolves.

  • The runtime /data twin's pointer (packages/runtime/src/domains/data.ts:79, "versionTokenRefusal in packages/rest/src/rest-server.ts") stays true, because the function stays there.

  • Repo tests outside packages/rest that read packages/rest/src:

    • core's authz-store-unavailable.test.ts classifies rest-server.ts as guarded for a seam the CRUD body never calls;
    • runtime's meta-list-projection-parity.test.ts reads only the GET {metaPath}/:type handler;
    • spec's five list packages/rest test FILES by name.

    None reads the moved code, and all three test:repo runs are green below.

  • Gates that walk packages/rest/src reach the moved code in its new file. Their scan outputs were compared between a worktree at origin/main 2bcaefba9 and the final head c333dafda. They differ only by the one scanned file or by this diff's own re-points:

    • check-meta-type-normalized 40 → 41 files;
    • check-undeclared-dep-imports +1 file, +2 specifiers;
    • check-dispatcher-error-vocabulary and check-error-status-conformance +1 file, classified sites unchanged;
    • check-affected-docs route sources 30 → 31 and registration call sites 25 → 26, by the *-routes.ts convention;
    • check-scripts-symbol-anchors 3910 → 3914 file-level anchors (the route-envelope gate's new comments);
    • check:platform-checklist cited sources 400 → 401;
    • check:liveness one more key-mention pair, anchored;
    • check-route-envelope the two rows above.

    These are identical: check-system-context-census, check-single-authz-resolver, check-rest-log-declared, check-durability-degradation-log-level, check-filter-alias-parity and check-adr-anchors (70 anchored files, 1,782 decision-letter citations). At the move commit, against base cb8756b00, the four walkers already read the one extra file, before any re-point.

Acceptance notes

  • versionTokenRefusal's docblock still says "this file THROWS it into the route's catch". The throw now sits in crud-routes.ts, in the same RestServer doors. This is a prose pointer, not edited by a move (no carrier).
  • The dataPrefix row's four sibling citations and the patterns note cite rest-server.ts#register… names whose bodies slices 1, 2, 4, 7 and this one moved. Each still resolves by name to its stub, and check:liveness anchors them by file. Re-pointing them is outside this slice's declared packages/spec substitution. Carrier: the domain:spec seat, through the seat's [PM seat] domain:spec — ⏳ vacant #6017 declaration; nothing filed.
  • check:liveness' key-mention axis is file-level. A row that cites a stub's name in a file that still names the key elsewhere reads as anchored after its quoted code moves. This slice re-points by hand. No new gate: card rule 5.
  • Carried from earlier slices, untouched: ui-routes.ts' "52 sites" comment and email-routes.ts' double-mount header (no non-move edit of either text here).

Gates and tests

Final head c333dafda. It is one merge of origin/main 2bcaefba9 (#24170, #24177, #24180, #24178, #24181). None of those touches a file in this diff. rest-server.ts' blob is 5a0f948c2e8a at cb8756b00 and at 2bcaefba9. main's #24178 changed data-action-routes.ts' export handler, so the merge-head proofs are taken against origin/main itself:

  • .d.ts. A base leg put origin/main's rest-server.ts into the merged tree under a trap and built @objectstack/rest. It gives 1f0f455d… with the marker at 0. Restored (blob and git diff HEAD proof) and rebuilt, the head gives 1f0f455d… with the marker at 1. The export lists are 39 / b3246be6… on both bundles.
  • Routes. The thirteen-config dumps with origin/main's rest-server.ts equal the merge head's (diff -r exit 0, sha256 1d7630283baf… on both legs). They differ from cb8756b00's only in the GET …/data/:object/export handler digest, which is fix(rest)!: a schema-derived export skips the fields declared hidden #24178.

Commits:

  • c8f1643c8, the move;
  • 3e58c49cb, the list;
  • 45d976972, the re-points;
  • b588a78b6, the changesets;
  • c333dafda, the merge.

All builds and suites ran through os-verify-lock, and every exit was written to a file before any pipe. At c333dafda:

  • Build. pnpm --workspace-concurrency=2 --filter '@objectstack/rest...' --filter '@objectstack/verify...' --filter '@objectstack/dogfood^...' build exit 0. Then pnpm turbo run build --concurrency=2 (whole workspace) exit 0: Tasks: 77 successful, 77 total.
  • Generated artifacts. pnpm --filter @objectstack/spec check:generated exit 0: All 14 generated artifacts are up to date. main moved packages/spec.
  • Typecheck. pnpm --filter @objectstack/rest typecheck exit 0: check:test-typecheck: OK … 0 file(s) / 0 error(s).
  • @objectstack/rest. local: Test Files 292 passed (292), Tests 6015 passed | 326 skipped (6341). repo, which is its test:repo: Test Files 5 passed (5), Tests 258 passed | 13 skipped (271).
  • Dogfood pair (authz-probe-blind-spot.test.ts, authz-conformance.test.ts): Test Files 2 passed (2), Tests 108 passed (108).
  • Spec liveness suites (packages/spec local, scripts/liveness/): Test Files 10 passed (10), Tests 269 passed (269).
  • test:repo of every package whose repo tests read packages/rest:
    • @objectstack/core: Tests 84 passed (84);
    • @objectstack/runtime: Tests 808 passed (808);
    • @objectstack/spec: Test Files 67 passed (67), Tests 1115 passed (1115).
  • Lint. pnpm exec eslint --no-inline-config over the 13 changed .ts / .mjs files: exit 0, 0 errors, 0 warnings. The repo-wide pnpm lint belongs to CI (Lint & Repo Gates).
  • Gate union. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths) at c333dafda derives 94 commands. Its stderr names the commit, and the --repo assertion holds. The change set is 17 paths vs merge base 2bcaefba9, 1,197 changed lines (+714 / −483).
    • All 94 ran. pnpm check:dual-build-cjs-loads first answered exit 3, PREREQUISITE NOT MET (no whole-workspace dist). It was re-run after the whole-workspace build and exits 0, and that measured exit is the one in the --ran file. The other dist readers (check:dts-closure, check:lean-entry-closure, check:sourcemap-no-sources-content, check:published-files, check:entry-guard) were re-run then too, all exit 0.
    • --ran: Run reconciliation — 94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN. The exit is 0.
    • check:route-envelope exit 0: 23 route module(s) audited: 8 conformant, 14 ratcheted, 1 exempt.
    • check:liveness exit 0 (above).
    • check:pm-dispatch-gates exit 0.
    • check:type-check-debt exit 0.
  • Not governed. node scripts/pm/check-governed-merges.mjs --test over the 17 paths, --additions 714 --deletions 483, reads 0 of 17 path(s) hit the register and NOT governed, under the 3,000-line threshold.

Not run locally: check:dev-prereqs and check:skill-frame-freshness (local-only families). This diff touches no packages/spec build input, since liveness/ ships as files and is never built, and no four-axis frame file. check-dev-prereqs --self-test is in the union.


Generated by Claude Code

…23050 slice 12)

The body of RestServer#registerCrudEndpoints (the six record doors under
{basePath}{crud.dataPrefix}) moves, re-indented one level and otherwise
unedited, to packages/rest/src/crud-routes.ts as
registerCrudEndpoints(this: CrudRouteHost, basePath). The class keeps the
private method, its docblock and its position; its body is a host literal
(config and routeManager by value, four methods as late-bound closures)
plus registerCrudEndpoints.call(host, basePath).

Body-only imports move with the body. versionTokenRefusal stays in
rest-server.ts (the runtime /data twin names that file) and is exported
for the module; DATA_RECORD_READ_PARAMS was already exported.

Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
Co-authored-by: Claude <noreply@anthropic.com>
Every whole-file reader of RestServer's registrars reads the list, so the
moved CRUD body is back in their reach; each reader's length pin re-reads
its number in the next commit.

Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
Co-authored-by: Claude <noreply@anthropic.com>
 slice 12)

The registrar-list readers re-read their numbers over thirteen files
(sums unchanged); check-route-envelope declares crud-routes.ts at its
measured dialect facts (stringError 3, siblingCode 3; rest-server.ts
11 -> 8 and 7 -> 4, NOT progress, the same MOVE); the liveness ledger's
six CRUD evidence paths and three platform-checklist sources name
crud-routes.ts, where the cited code now lives. Consumer text, verdicts
and statuses unchanged.

Claude-Session: https://claude.ai/code/session_01BVm1pAXVXQ3PjC42j1kPFf
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Oct 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 26 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/liveness/crud_endpoints.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

45 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/liveness/crud_endpoints.json) — pages documenting those are invisible to this run
  • 5 cross-cutting symbol(s) contributed no route anchor: enforceAuth (23 routes), resolveExecCtx (28 routes), routeManager (19 routes), enforceApiAccess (9 routes), resolveProtocol (25 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: /data/:object (route, 66 pages)
  • 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 — 146 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 b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f42dca22b8992692a90d77009f20c7e8cbc9ff6a — the merge of head c333dafdad8300584b647b5d8429870843cda97c into base b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0, 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 f42dca22b8992692a90d77009f20c7e8cbc9ff6a && git checkout f42dca22b8992692a90d77009f20c7e8cbc9ff6a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0 c333dafdad8300584b647b5d8429870843cda97c && git checkout -B drift-repro b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0 && git merge --no-ff c333dafdad8300584b647b5d8429870843cda97c

node scripts/docs-audit/affected-docs.mjs --json b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0

⚠️ 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 b1845ed637fe469f079ef1cdfc96f8a47b5ca9e0 → 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

席位核对 — ACCEPT · domain:cli seat 1 · os-sales · session session_01BVm1pAXVXQ3PjC42j1kPFf · 2026-10-17T17:57Z

Head c333dafda. This is the seat's review of record for #23050 slice 12 (claim 6235962355). None of the four contract faces is touched:

  • Clause-②: no, with no widening or narrowing;
  • no packages/spec/src/** path;
  • no governed path (check-governed-merges: 0 of 17).

So, by contract-review.md, CI plus the seat's own read decide it, and no second agent runs.

  • Form:

    • Draft → main, titled refactor(rest): ….
    • The body's first line is Part of #23050, with no closing keyword (scanned): the /meta slice remains. Clause-②: no is line-initial.
    • Two patch changesets. The @objectstack/rest one is owed because dist/index.{js,cjs} bytes change while the .d.ts and the export list do not. The @objectstack/spec one is owed because liveness/ ships.
  • The maintainer's five rules, executed:

    1. Move-only. The seat diffed it independently: base rest-server.ts:8324-8758, dedented one level, is identical to crud-routes.ts:107-541, all 435 lines.
    2. One named host. CrudRouteHost hands over exactly the six members an AST walk found: config and routeManager by value, the four methods as late-bound closures. Late binding was measured with an ablation and a base leg. The stub keeps the method's name, docblock and position, so the ADR cites that resolve by name (0036, 0076, 0111, 0138, all Tier H) still resolve.
    3. Byte-identical. Route table, ordered registrations and handler digests are identical under 13 configs. A one-character positive control moved exactly the PATCH door's digest. .d.ts/.d.cts sha256 is 1f0f455d… on base, move head and merge head, and both bundles keep 39 exports.
    4. /meta is untouched.
    5. No new gate.
  • Every by-path reader moved in the same PR, and none went silently empty:

    • the 9 registrar-list readers (sums unchanged);
    • the dogfood census, re-anchored with a new crud-routes.ts control;
    • check:route-envelope (8 / 4 in rest-server.ts, 3 / 3 in crud-routes.ts, sums 39 / 54 unchanged);
    • check:liveness (735 / 734 / 1);
    • the platform checklist (853/863 anchors on both trees).

    The red-before-re-point readings (two stages) show each reader really reads the moved code.

  • The dev's deviations, accepted:

    • versionTokenRefusal stays in rest-server.ts and gains export (the shape of slices 1 and 2), because a held-off packages/runtime path names that file as its home. The .d.ts is unchanged.
    • DATA_RECORD_READ_PARAMS is imported, not handed over: it is a module constant, slice 3's shape.
    • The claim's expected rest_api.json rows do not exist, as measured.
  • Cross-lane declarations:

  • Out-of-scope notes, routed:

    • The stale stub citations in crud_endpoints.json, route_generation.json and rest_api.json go to the spec seat with the 6238125655 amendment, not filed: they resolve by name and the gate stays green.
    • The versionTokenRefusal docblock and check-nul-bytes.mjs' stale line pointer: Acceptance notes, carrier: none.
    • ADR-0036 joins the maintainer's Tier H name-resolution carry.
  • Next slice: /meta. [finding] email_template: a Studio save of the same template in a second locale replaces the first locale's overlay at the next boot (sys_metadata keeps one overlay row per type and name) #22877 stage 2 has landed (038d4c9588), so the card's order gate is met. The dev's report names its prerequisites:

    • its own slicing plan, since about 3,000 lines sit inside the guarded routeManager swap;
    • TransportScopedMetaRequest's re-home;
    • the readers of metadata_endpoints.json, doc.json, book.json and system-context.mdx:184;
    • the census spelling private registerMetadataEndpoints(.
  • Landing: not governed (0 of 17 paths; 1197 changed lines ≤ 3000). The PR goes ready with auto-merge armed once every check on this head is green.

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

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants