Skip to content

refactor(rest): move registerBatchEndpoints into batch-routes.ts (#23050 slice 2) - #23642

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23050-s2-batch-endpoints
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-23050-s2-batch-endpoints

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23050
Clause-②: no

Slice 2 of the rest-server.ts split, by slice 1's pattern (landed note 6136102437, steps 1-6): the body of RestServer#registerBatchEndpoints (the cross-object transactional POST {basePath}/batch and the per-object POST /data/:object/batch, createMany, updateMany, deleteMany) moves, unedited, to the new module packages/rest/src/batch-routes.ts. It is move-only. Routes, the route ledgers and the published dist/*.d.ts are byte-identical, and every reader whose reading included a moved line now reads it where it is. rest-server.ts goes from 12,997 to 12,521 lines. The card stays open for the later slices.

The pattern, applied

Members the moved body reads, from an AST walk of the body (base e611e2bda line numbers): fields protocol :1959, config :1960, routeManager :1961, objectQLProvider :2012. Methods resolveProtocol :2339, enforceAuth :2407, enforceApiAccess :2594, enforceBatchSize :2662, loadObjectItems :2797, resolveExecCtx :2807. That is 10 members. The body has no bare this and no element access on this.

Host shape (slice 1's shape B): the class keeps private registerBatchEndpoints(basePath) with its name, docblock and position. Its body is now one BatchRouteHost literal and registerBatchEndpoints.call(host, basePath). Each interface member is typed RestServer['m']. Methods go as late-bound closures, (...a) => this.m(...a).

Fields, checked one by one:

field read when reassigned after registration? handed over as
config registration only (crud, batch) no (constructor only) value
routeManager registration only (every register call) its one swap (:5670/:5674) wraps registerMetadataEndpointsInner, not this registrar value
protocol registration only (the four mount conditions) no (constructor only) value
objectQLProvider request time (the cross-object door's wiredEngineOrLoud(Boolean(this.objectQLProvider), ...)) not by the class, but the field is not readonly getter

The getter is measured, not argued. A one-off script boots a source RestServer with no provider, calls POST /api/v1/batch with { operations: [] }, then assigns objectQLProvider on the instance AFTER registerRoutes() and calls again:

  • this head: 501 NOT_IMPLEMENTED, then 200 {"results":[]}. The door reads the live field, as it did when it read this.objectQLProvider on the server.
  • by-value ablation (scripts/ablation-replace.mjs, the getter replaced by objectQLProvider: this.objectQLProvider,): 501, then 501 again. The value shape would freeze the field at registration. Restored: blob bee3d9b7bf56 equals HEAD, git diff HEAD empty.

Module-level helpers: apiAccessDenialFromEnable was already exported. wiredEngineOrLoud gains export (one word, same line). The package entry re-exports neither. ServerScopedDataRequest is imported from data-action-routes.ts, where slice 1 moved it; no type alias is exported from rest-server.ts.

Proof of move-only

  • .d.ts: packages/rest/dist/index.d.ts and index.d.cts are byte-identical to base e611e2bda, sha256 7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9 for all four files (cmp silent), rebuilt at the final head 43903e699 (the merged origin/main touched no file in packages/rest). The ESM and CJS bundles both load, with 39 exports each and one sorted export-name list (sha256 prefix 84008c9643d55420); packages/rest/src/index.ts is untouched. The bundle JS bytes differ, because code moved within it.
  • Routes (base e611e2bda, head 43903e699): a source RestServer on a recording mock server, with a protocol that implements every member (so all five batch routes mount), dumped under three configs. getRoutes() (functions recorded as arity), the ordered server registration calls, and a whitespace-free source hash of every handler are byte-identical base vs head, 9 dumps of 9:
    • default: routes ee3a764e19bd...ecde0d, calls 354b948093a5...bc1a6e, handler sources d8a6201db1e0...e4c767 (75 routes, 5 batch)
    • scoped-optional: eb7a21beb5de...56d66a / 6b60e9f79a6b...c4c0d6 / 38c46865076...6c6c74 (150 routes, 10 batch)
    • scoped-required: d6d0e5068384...9263e6 / 805edda24595...befc38 / c077f433ff44...515295 (75 routes, 5 batch)
  • Route ledgers: packages/rest/src/rest-route-ledger.ts, packages/runtime/src/route-ledger.ts and packages/rest/src/index.ts are untouched.
  • Moved body: base rest-server.ts lines 12355-12848 (git show e611e2bda:packages/rest/src/rest-server.ts | sed -n 12355,12848p), with one 4-space indent level removed (sed 's/^ //'), against batch-routes.ts lines 90-583: diff exits 0 and prints nothing (494 lines).
  • Moved-line accounting (git diff --color-moved=plain --color-moved-ws=allow-indentation-change f7cd580bf HEAD over the two files): 501 deleted and 508 added lines are detected as moved. The non-moved hunks:
    • rest-server.ts (+26 / -503): the stub body (18 lines); the import of the new module plus its one-line comment; two pointer comments where imports the move left unused were (omitInternalFieldsFromWriteResponse; the four bulk request types BatchDataRequest / CreateManyDataRequest / UpdateManyDataRequest / DeleteManyDataRequest); the unused DroppedFieldsEvent import removed; export on wiredEngineOrLoud.
    • batch-routes.ts: the module docblock, the imports, the BatchRouteHost interface and its docblock, and the function docblock and signature.

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

  • scripts/check-route-envelope.mjs (a gate's target paths; see the note at the end): batch-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 data-action-routes.ts is. rest-server.ts stringError 26 to 18 and siblingCode 39 to 31; the new module carries 8 and 8, both measured by the gate. Sums 18 + 13 + 8 = 39 and 31 + 15 + 8 = 54, unchanged. The gate's output differs from base f7cd580bf only in those lines and the counts (12 to 13 modules, 3 to 4 ratcheted).
  • packages/rest/src/execctx-consumer-census.test.ts: reads three files; 62 sites = 48 + 9 + 5, unchanged. Mentions 81 to 85: the four new ones are this slice's seam (forwarder, two; interface member, two). There are now two forwarder frames, and siteOfStack steps over both. The control pins one forwarder per moved module (SITE_FILES.length - 1), so a module moved without joining the census is red.
  • packages/rest/src/objectql-slot-consumer-census.test.ts: the batch door, the third consumer, now lives in batch-routes.ts. The census reads both files: 3 invocation sites = 2 + 1, all 3 through wiredEngineOrLoud, and the batch one is pinned to batch-routes.ts. The no-alias check runs over both files. The host interface's RestServer['objectQLProvider'] is a type that reads nothing, so it is set aside before the check and pinned at exactly one. One new control: one get objectQLProvider() in rest-server.ts per censused module, so a slot handed to an uncensused module is red.
  • ui-view-route-identity.measurement.test.ts and ui-view-route-tenancy.measurement.test.ts: the whole-file spelling checks count over the three registrar files. this.enforceAuth( 48 to 49 and this.resolveExecCtx( 63 to 64 (the +1 each is the forwarder), this.enforceEnvironmentOwnership( stays 1.
  • packages/qa/dogfood/test/authz-probe-blind-spot.census.ts: the rest-server row's population also reads batch-routes.ts, so 70 / 19 / 51 is unchanged; the five moved routes are still RestServer's and still in the blind spot. this.routeManager.register( stays 63 (49 + 9 + 5), with a new control for the module's own 5. enforceAuth 64 to 68, re-anchored: four seam occurrences, no call site added.
  • docs/qa/platform-checklist/areas/api-backend.json: the batch-config item's source rest-server.ts#registerBatchEndpoints (the four per-object gates ...) now names batch-routes.ts#registerBatchEndpoints for the gates, and rest-server.ts#enforceBatchSize for the parentheticals that stay in rest-server.ts (enforceBatchSize, normalizeConfig, parseDeclaredSubConfig). Both anchors are declarations. check:platform-checklist output is identical base vs head.

Red before the re-points: the rest suite showed 5 failed (execctx census x3, slot census x2). The dogfood census with its base text against the moved tree read population 65 against 70, and this.routeManager.register( 58 against 63.

Readers examined and not re-pointed, because the moved lines contribute zero to them (measured on the moved body):

  • rest-server-query-number-census (0 coercions), rest-server-canonical-query-ast (0 query: slots), meta-draft-read-door-census (0 draft switches), rest-server-docblock-position (0 docblocks moved), the dogfood realtime tripwires (0 mechanics, 0 route hits).
  • auth-gate-allowlist-fault-window, execctx-authz-input-seam-reachability, runtime meta-list-projection-parity: they slice other members.
  • Gates with output identical base f7cd580bf vs head 43903e699: check-durability-degradation-log-level, check-system-context-census, check-single-authz-resolver, check-rest-log-declared, check:liveness. Identical except one more scanned file: check-meta-type-normalized, check-nul-bytes, check-adr-anchors, check-undeclared-dep-imports (+3 @objectstack/* specifiers: the new module's 4 imports less the one rest-server.ts dropped).
  • The ADR anchor packages__rest__src__rest-server.ts.json (ADR-0045, filterAppForUser) is unaffected.

Acceptance notes

  • packages/spec readers, not edited (the claim keeps packages/spec out): packages/spec/liveness/batch_endpoints.json has five evidence strings that name rest-server.ts#registerBatchEndpoints and quote a moved line (const maxBatch = batch.maxBatchSize ?? 200, and the four if (... && this.protocol.X) mount conditions). crud_endpoints.json dataPrefix names it too, for dataPath. check:liveness reads them as before and its output is identical base vs head: the symbol anchor is the private method that still registers these routes, and every key-mention (maxBatchSize, enableBatchEndpoint, createMany, updateMany, deleteMany, dataPrefix) still occurs in rest-server.ts outside the moved range. The quoted code now lives in batch-routes.ts. Named in the report for the domain:spec seat.
  • docs/adr/0034-...md names rest-server.ts registerBatchEndpoints in a status block. The private method is still the registrar, so the pointer still lands. It is a governed surface and is not edited here.
  • docs/qa/platform-checklist/areas/integration-system.json cites rest-server.ts#deleteMany for the deleteMany door's parse. That anchor lands on the deleteMany: boolean member of NormalizedRestServerConfig (:1244, outside the moved range), base and head. batch-routes.ts declares no deleteMany, so a re-point there would not land. Not edited.
  • Coverage of future code, a question for the seat: the whole-file rule readers above with zero contribution keep reading rest-server.ts (plus data-action-routes.ts where slice 1 added it). New code written later in batch-routes.ts is outside them. Slice 1 set the rule "re-point only readers the moved lines contribute to", and this slice follows it. One shared list of the files RestServer's registrars live in is the long-run shape; it is raised in the report, not done here.
  • Lines of rest-server.ts between :323 and the moved block shift by -1, and lines after it by -477. No line citation in the repo points into the moved range (base :12351-:12849); the highest is :11108, in a release-owned CHANGELOG.
  • ServerScopedDataRequest stays in data-action-routes.ts (slice 1's ruling A: the CRUD slice rehomes it). batch-routes.ts imports it type-only from there.

Gates and tests

All at 43903e699 (the slice plus one merge of origin/main f7cd580bf, which touched none of these files).

  • pnpm --filter @objectstack/rest test: Test Files 285 passed (285), Tests 5845 passed / 326 skipped (6171), the same as base e611e2bda. VERDICT command-exit 0.
  • pnpm --filter @objectstack/rest test:repo: Test Files 5 passed (5), Tests 213 passed / 2 skipped (215), the same as base. VERDICT command-exit 0.
  • pnpm --filter @objectstack/rest typecheck: exit 0 (tsc --noEmit plus check:test-typecheck: 0 files / 0 errors).
  • pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts: 2 files, 108 passed. VERDICT command-exit 0.
  • Lint: eslint --no-inline-config --format json over packages/rest, scripts/check-route-envelope.mjs and the dogfood census: 323 files, 0 errors, 0 warnings. Every touched lintable file is in the result set. Repo-wide pnpm lint is CI's.
  • dispatch-gates --commands --repo objectstack-ai/objectstack, no paths: 80 commands. --ran: 80 derived, 79 run, 1 NOT-MEASURED, 0 UNRUN. 78 exit 0.
    • pnpm check:platform-checklist exits 1 with the same 8 findings as base f7cd580bf (none in api-backend.json); the whole output is identical base vs head.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3; workspace packages without dist/ in this container). Stand-in: packages/rest/dist/index.cjs and index.js both load, 39 exports each. CI runs the gate over a full build.
    • pnpm check:route-envelope and pnpm check:durability-log-level are both in the derivation and both exit 0.

Changeset: none. @objectstack/rest ships files: ["dist", "README.md", "CHANGELOG.md"]. Its dist/index.d.ts and index.d.cts are byte-identical, its export list is identical, and its route table is identical under three configs. The bundle JS differs only by where the code sits. skip-changeset is the seat's to apply on its own check.

Contract review: this PR edits a gate script's target paths (scripts/check-route-envelope.mjs, one new MODULES entry and two lowered counts on rest-server.ts), as slice 1 did.

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:durability-log-level exit 0
  • 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

Slice 2 of the rest-server.ts split, by slice 1's pattern: the body of
RestServer#registerBatchEndpoints moves, re-indented and otherwise
unedited, to packages/rest/src/batch-routes.ts. The class keeps the
private method and its docblock as a BatchRouteHost literal plus .call.

Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
Co-Authored-By: Claude <noreply@anthropic.com>
…tes.ts

Every reader whose reading included a line of registerBatchEndpoints'
body now reads it in batch-routes.ts, with its sum unchanged: the
resolveExecCtx and objectQLProvider consumer censuses, the ui-view
registrar spelling checks, the dogfood blind-spot census, the
route-envelope ledger and one platform-checklist source anchor.

Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
Co-Authored-By: Claude <noreply@anthropic.com>
The host interface spells the slot once as an indexed-access TYPE,
which reads nothing; that one spelling is set aside and pinned at one,
so the dynamic-read check holds over both files.

Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
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 48 documentable anchor(s).

34 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 23e689dc7d1de961a168997685295ddf009d5c2c.

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

What this run could not see
  • 6 cross-cutting symbol(s) contributed no route anchor: enforceApiAccess (10 routes), enforceAuth (23 routes), enforceBatchSize (5 routes), resolveExecCtx (28 routes), resolveProtocol (26 routes), routeManager (22 routes)
  • 1 anchor(s) matched too much of the corpus to be a work list: /data/:object (route, 66 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 23e689dc7d1de961a168997685295ddf009d5c2c → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 23e689dc7d1de961a168997685295ddf009d5c2c

⚠️ 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 23e689dc7d1de961a168997685295ddf009d5c2c → 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: PR #23642 at 43903e6999 (#23050 slice 2, Part of #23050). It queues after one CONTRACT_REVIEW_TIER review on its head, owed for the gate edit, and a true-green head

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T08:48Z · holder of claim 6186685682. Report: os-dev-report 6188163874.

席位核对

  1. Shape, read on GitHub:
    • The PR is a draft against main, head 43903e6999. Line 1 is Part of #23050, and the body carries no closing keyword. Line 2 is a bare Clause-②: no. Assignee os-bill.
    • Files: 9, +749 / −536. check-governed-merges --pr 23642: NOT governed, 1285 changed lines. No content/docs/releases/ change.
  2. Move-only, measured by the seat:
    • origin/main's rest-server.ts :12355-:12848 (the body of registerBatchEndpoints, 494 lines), with one 4-space indent level removed, equals batch-routes.ts :90-:583 at the head: diff exit 0.
    • Every line rest-server.ts gains is one of three things: the BatchRouteHost literal plus .call stub, the import and its pointer comments, or export on wiredEngineOrLoud. The private method, its name, JSDoc and position stay, so the .d.ts keeps it.
  3. The pattern, read against slice 1's (6136102437, steps 1–6):
    • 10 members, each typed RestServer['m']; methods as late-bound closures.
    • config, routeManager and protocol go by value: each is read only at registration, and is not reassigned after it on this path.
    • objectQLProvider goes as a getter, because the cross-object door reads it at request time. The dev measured that choice (501 → 200 after a post-registration assignment) and ablated it (by value freezes 501).
  4. Proofs (the report's proofs, with commands): the .d.ts / .d.cts sha256 is identical base vs head; getRoutes(), the ordered registrations and each handler's source hash are identical under 3 configurations; every re-pointed reader's sum is unchanged, red before the re-point.
  5. Tests and gates:
    • @objectstack/rest: 5845 passed (local), identical to base; 213 passed (repo); typecheck exit 0. Dogfood censuses: 108 passed.
    • Local union: 80 derived, 79 run, 1 NOT MEASURED (check:dual-build-cjs-loads, prerequisite; CI reads it). check:platform-checklist exit 1 shows the same 8 findings as base, none in a touched file.
    • CI on this head, read in this act: 39 check-runs, 24 success, 6 skipped, 8 in_progress. One failure, Check Changeset: no label yet (see 6).
  6. Changeset:
    • The seat checked the criterion for itself:
      • the published .d.ts is byte-identical;
      • the ESM and CJS bundles export the same 39 names;
      • routes and handlers are byte-identical;
      • no behaviour moves.
    • So no release note is owed, as for slice 1 (skip-changeset, landed bcebd8409d). The seat applies skip-changeset in this act, on its own check; the dev wrote no label, by the dispatch.
  7. Surface:
    • Inside the claim: rest-server.ts, batch-routes.ts, and the readers whose reading included a moved line.
    • In this lane (packages/qa): four packages/rest census and measurement tests, and the dogfood census. Each carries a pairing control, so the next slice cannot leave it silently empty.
    • Cross-lane, declared by the seat after the fact, as for slice 1:
      • scripts/check-route-envelope.mjs (domain:spec): one new MODULES row and two lowered rest-server.ts counts, sums unchanged (stringError 39 = 18 + 13 + 8, siblingCode 54 = 31 + 15 + 8);
      • docs/qa/platform-checklist/areas/api-backend.json (domain:devx): one source string split onto the two declarations.
  8. Open questions, answered:
    • (1) The six packages/spec/liveness evidence strings (batch_endpoints.json ×5, crud_endpoints.json dataPrefix) quote code that now lives in batch-routes.ts. check:liveness is unchanged and green, and the anchors still land on the private method. They join slice 1's two carried strings: the CRUD-doors slice's claim names all of them, and their routing is settled there. ⛔ Not edited here; no card now.
    • (2) Zero-contribution whole-file readers: this slice keeps slice 1's rule (A); every sum is unchanged. The next slice's claim introduces option B: one list of the files RestServer's registrars live in, which those readers import. That is not a new gate, it widens existing readers, and it closes the silent-empty shape the card forbids.
  9. Out of scope: integration-system.json's rest-server.ts#deleteMany lands on a config boolean, at base and head. It goes to Acceptance notes (carrier none), as written.

Landing: the edit to scripts/check-route-envelope.mjs's targets owes one CONTRACT_REVIEW_TIER review before the queue (the claim's own line, as slice 1 did). The seat dispatches it now to an isolated agent and verifies it at source. The PR stays draft until that record reads PASS on the current head and every check is green.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 43903e69992fc281f2515d03c1c66bd47b1a3315
Independence: INDEPENDENT AGENT
Local-runs: none

Inputs: card #23050 (body and every comment, the slice 1 rulings and this slice's claim, report and ACCEPT included), PR #23642 (body, file list, net diff against main at the head; 9 files, +749 / −536, merge-base f7cd580bf, three commits plus one merge of origin/main), and the check-runs on the head. The head is origin/main plus this branch and nothing else. Everything below was read from git objects and the API; nothing was built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each named right or wrong.

  1. Public surface of @objectstack/rest — unchanged. RIGHT. packages/rest/src/index.ts and packages/rest/package.json are untouched (I diffed both against origin/main: empty). The package has one exports entry (., the bundled dist/index.*), so a module-level export reaches a consumer only through index.ts, and index.ts names neither wiredEngineOrLoud, apiAccessDenialFromEnable, BatchRouteHost nor batch-routes (0 hits). The private registerBatchEndpoints stays on the class with its name, JSDoc and position (rest-server.ts :12351–:12371), which is what the bundled .d.ts carries for a private member. The dev's measured sha256 identity of dist/index.d.ts / index.d.cts against base is therefore what the structure requires, not a coincidence; I did not rebuild. export on wiredEngineOrLoud is one word on one line (:1544). No type alias was exported from rest-server.ts; ServerScopedDataRequest is imported type-only from data-action-routes.ts, where slice 1 put it (ruling A on slice 1's open question 2).

  2. Accept set — no request schema, query set or body parse moves. RIGHT. My own diff: origin/main rest-server.ts :12355–:12848 with one 4-space indent level removed against the head's batch-routes.ts :90–:583 is identical, 494 lines, zero differences. The five routes, their mount conditions (batch.enableBatchEndpoint && this.protocol.batchData, and the three operations.* ANDs), their handlers and their bodies are the same text. The route table is identical by construction; the dev's 9-of-9 route/handler dumps under three configs are consistent with that and I did not re-run them. rest-route-ledger.ts and the runtime route-ledger.ts are untouched (diff empty).

  3. The host shape (slice 1's shape B, steps 1–6). RIGHT, and the one field that differs from slice 1 is right for a measured reason. Ten members. In the moved body I count: this.config read once, at the top, at registration; this.protocol read exactly four times, all in the mount conditions, at registration; this.routeManager.register( five times, at registration; this.objectQLProvider four reads, all inside the cross-object handler, at REQUEST time. On the class, protocol and config are assigned only in the constructor (:2103, :2104); routeManager is reassigned only at :5669 / :5673 around the metadata registrar, and registerBatchEndpoints is called from registerRoutes at :4781, outside that window. So by-value for config, routeManager, protocol is sound, and the getter for objectQLProvider (assigned at :2110, not readonly) is required, as the dev's 501-then-200 measurement and its by-value ablation showed. The six methods are declared on the class (:2338 resolveProtocol, :2406 enforceAuth, :2593 enforceApiAccess, :2661 enforceBatchSize, :2796 loadObjectItems, :2806 resolveExecCtx) and go as late-bound closures, so instance and prototype spies still bind. One nuance, named and judged harmless: through the host, the provider is invoked with the host literal as its receiver rather than the server. The shipped provider is an arrow function (rest-api-plugin.ts :388) that never reads its receiver, and the field's declared type names no receiver, so no behaviour moves. The import cycle (batch-routes.ts imports two functions and the RestServer type from rest-server.ts, which imports the registrar back) is the shape slice 1 landed (data-action-routes.ts :90) and both sides use the other's values only at call time.

  4. scripts/check-route-envelope.mjs — the gate was followed, not loosened, and cannot go silently empty. RIGHT. The gate's dialectOnly audit is exact equality in both directions (auditDialectOnly: found above declared is "a NEW non-conforming body", found below declared is "bank it — lower the declared number"), so after the move the old 26 / 39 on rest-server.ts would have been RED and 18 / 31 is the gate's own prescription. The new module is discovered by the *-routes.ts walk, and "a module discovered by the scan but absent from the table is an ERROR" (the header, and the undeclared-module branch), while unfoundDeclarations reds a declared module that is not on disk — neither direction can be silent. My hand count of the moved body: eight bodies whose error is a bare string with a sibling code (four single-line .json({ error: ..., code: ... }) and four multi-line), so stringError: 8 and siblingCode: 8 are what left rest-server.ts; 18 + 13 + 8 = 39 and 31 + 15 + 8 = 54, unchanged. The entry inherits dialectOnly with the required ratchet and declares no write-site key, as the gate demands. Lint & Repo Gates (which hosts pnpm check:route-envelope, lint.yml :2796) is success on this head.

  5. execctx-consumer-census.test.ts. RIGHT, and tighter. SITE_FILES gains batch-routes.ts; the stack regex names it; 62 sites = 48 + 9 + 5 (five this.resolveExecCtx( calls in the moved body, my count). Mentions 81 to 85: the forwarder line carries two and the interface member two. The forwarder is now a set, pinned to exactly SITE_FILES.length − 1 — one per moved module — so a third module moved without joining the census, or a respelt forwarder, reds here.

  6. objectql-slot-consumer-census.test.ts. RIGHT, and tighter. The census reads two files; 3 invocation sites = 2 + 1, all via wiredEngineOrLoud, and the batch door is pinned to batch-routes.ts. The no-alias check now runs over both files; the one RestServer['objectQLProvider'] TYPE spelling is set aside and pinned at exactly 1 in the module and 0 in rest-server.ts, so the set-aside cannot widen. One get objectQLProvider() per censused module is pinned. The readObjectQLProvider closure is a plain this.objectQLProvider read, not an invocation (invocationSites requires a call), so it is not a fourth consumer, and it is spelled as the field read the census watches, not as a receiver alias.

  7. ui-view-route-identity / ui-view-route-tenancy measurement tests. RIGHT. REGISTRAR_SOURCES gains the module; the reverse checks are floors (greater than 40) over the whole spelling, so the move neither loosens nor empties them, and the 48 to 49 / 63 to 64 the dev reports is the forwarder.

  8. packages/qa/dogfood/test/authz-probe-blind-spot.census.ts. RIGHT, re-anchored not relaxed. The population text appends the module, so 70 / 19 / 51 holds; this.routeManager.register( is counted over all three files (63 = 49 + 9 + 5) with a new control for the module's own 5; enforceAuth is counted over the same concatenation (occurrences(all, ...)), so the five moved call sites are in both readings and the +4 are the two seam spellings — my body count is five enforceAuth, consistent with 64 to 68.

  9. docs/qa/platform-checklist/areas/api-backend.json. RIGHT. The source string now carries two symbol anchors, both at declarations: batch-routes.ts#registerBatchEndpoints (:176 of the module) and rest-server.ts#enforceBatchSize (:2661). The per-file anchor count is a floor (anchorFloorProblems reds only below the baseline's 41, or on a file with no floor), so a string that gains an anchor passes.

  10. Readers of rest-server.ts by path that were NOT edited — correctly not re-pointed. check-single-authz-resolver lists the file as a DELEGATOR and walks all of packages/ (the moved body carries no resolver spelling; the new module is in its walk); check-system-context-census anchors rest-server.ts#enforceAuth, a symbol that stays (the body names isSystem only in a comment); check-durability-degradation-log-level, check-meta-type-normalized, check-nul-bytes, check-undeclared-dep-imports and check-adr-anchors are walks that pick the new file up (+1 file, as the dev's base-vs-head diff says); the ADR anchor packages__rest__src__rest-server.ts.json pins ADR-0045 filterAppForUser, untouched. No repo line citation points into base :12351–:12849 (grep over origin/main: none).

  11. The rest-server.ts non-moved hunks. The stub body; the module import and its one-line comment; two pointer comments where the imports the move left unused were; the DroppedFieldsEvent import removed; export on one function. Nothing else. rest-server.ts 12,997 to 12,520 lines; batch-routes.ts 584.

Wrong: nothing found.

② Semver level

  • The PR body's second line reads Clause-②: no, and that is right: no accept set widens or narrows — no schema, query set, request parse or route changes (①2).
  • skip-changeset is right for @objectstack/rest under the criterion the lane adopted at slice 1 and this slice's claim restated (no changeset when the published surface is byte-identical): the bundled .d.ts is unchanged by structure (①1), the export list is unchanged, the route table and every handler are the same text (①2), and no behaviour moves. The honest residual, named: the shipped dist/index.js / index.cjs bytes DO change because code moved within the bundle, so "publishes nothing" is true at the consumer-visible surface, not at the byte level — the same residual slice 1 landed under (bcebd8409d, PASS record 6135787991). A patch changeset would publish a release line with nothing in it for a consumer; the Changesets fixed group would bump every public package for it. No changeset is the right declaration.
  • The label is on the PR (skip-changeset, applied by the seat after its own check, as the ACCEPT 6188218040 records). The Check Changeset check-run that reads failure on this head completed BEFORE the label was applied; the run the labeled event started afterwards reads skipped, which is pr-automation.yml's own condition (!contains(labels, 'skip-changeset') plus the live label re-read) and is the designed pass state. The earlier red is stale, not a finding.

③ Boundary flags

Every dev flag and every open_questions entry, answered or escalated.

  1. open_questions[0] — six packages/spec/liveness evidence strings (batch_endpoints.json ×5, crud_endpoints.json dataPrefix) name rest-server.ts#registerBatchEndpoints and QUOTE lines that now live in batch-routes.ts. Answered by the seat as option B (fold into the CRUD-doors slice's cross-seat declaration), with "no card now". My reading of the gate: check-liveness resolves a path#symbol evidence anchor by word in the cited FILE (evidence.mts isSymbolNamed) and the key-mention check is the file-level backstop, so the anchor still lands on the private registrar and Spec property liveness is success on this head — the gate's target did not move and the gate is not loosened or empty. What moved is the prose: the quoted reads are no longer in the file the string names, and the mentions that still satisfy the file-level backstop in rest-server.ts are the normalizer's and the type's, which the ledger's own _note says are not reads. ESCALATED, non-blocking: the carrier is a sentence in a future claim, not a record on this card. The seat's landing note should carry the six strings by name, as 6136102437 carried slice 1's two, so the CRUD slice's claim is written from it; if that slice is not the next dispatch, a domain:spec card is the durable form.

  2. open_questions[1] — zero-contribution whole-file readers (query-number census, canonical-query-AST census, meta-draft-read-door census, docblock-position pin, the dogfood realtime tripwires) were not extended. Answered: slice 1's rule A holds for this slice because every sum is unchanged and the moved body contributes zero to each (0 coercions, 0 query: slots, 0 draft switches, 0 moved docblocks). One concrete instance of the gap option B closes: the three NEW docblocks in batch-routes.ts (module, interface, function) are outside rest-server-docblock-position.test.ts's read — new code, not moved, so rule A does not reach them. The seat's choice to introduce one shared registrar-file list at the next slice is the right close; not this PR's defect.

  3. Deviation — files beyond the claim's literal surface (packages/qa/dogfood/.../authz-probe-blind-spot.census.ts, docs/qa/platform-checklist/areas/api-backend.json, and the gate script itself). Right: each is a reader whose reading included a moved line (①4, ①8, ①9), which the claim orders re-pointed in the same PR; the cross-seat declarations are the seat's, as at slice 1.

  4. Deviation — check:dual-build-cjs-loads NOT MEASURED locally. Answered by the head: ci.yml hosts it in Build Core (:2643), which is success on this head.

  5. The dogfood census edit is verified pre-merge only by the dev's local run (108 passed). The Dogfood Regression Gate (n/3) shards skip on PRs; the carrier context is success. Its first CI reading is main's hourly run after landing — the census's own comment says so. Accepted residual, the same as slice 1's; if it reds, it is this lane's main-red under the two-speed doctrine. Named, not blocking.

  6. out_of_scope — integration-system.json rest-server.ts#deleteMany lands on NormalizedRestServerConfig's boolean, not the door. Pre-existing, base and head, untouched by this PR; an acceptance note is the right carrier (a note that names its evidence, no card).

  7. docs/adr/0034 names rest-server.ts registerBatchEndpoints in a status block. Governed surface, not edited; the private method is still the registrar, so the pointer lands. Right.

  8. Attribution deviation (model-free commit trailers, session-URL PR footer). Not a contract matter; conforms to AGENTS.md.

  9. Check-runs on the head, read at 2026-10-15T09:04Z: 34 success, 10 skipped, 1 failure, nothing in progress. The five required contexts — Lint & Repo Gates, TypeScript Type Check, Test Core (all six shards), Build Core, Governed Surface Queue Guard — are success. The one failure is the pre-label Check Changeset run (②); the run the label event started reads skipped, which is that workflow's designed exempt state. Tier S landing still waits on the seat's own reading of every check, separately from this record.

Implemented-by: claude/issue-23050-s2-batch-endpoints
Reviewed-by: session_01FLwgbTSmGMrqsXkBDsiwfG

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note: the contract-review record 6188485996 (PASS on 43903e6999) is verified and adopted, and the PR queues now · domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T09:05Z · holder of claim 6186685682.

  • Verified as served. The reviewer ran isolated, and its transcript shows the contract-review tier on every turn. Its Head-sha is the PR head.
  • Verified at source. The record's ① 4 reads check-route-envelope's dialectOnly audit as exact in both directions. The new module is found by the *-routes.ts walk, and an undeclared module is an error, so neither direction can go silently empty. Its hand count of the moved body (8 / 8) matches the seat's sums. ① 2's 494-line identity matches the seat's own diff.
  • Its non-blocking escalation is taken. This card's landing note will carry the six packages/spec/liveness strings by name (batch_endpoints.json ×5, crud_endpoints.json dataPrefix), as 6136102437 carried slice 1's two. The CRUD-doors slice's claim is then written from it. If that slice is not the next one dispatched, a domain:spec card becomes the durable form.
  • CI on 43903e6999, read in this act:
    • the latest run of every check is success or an expected skip;
    • Check Changeset is skipped on the labeled run, which supersedes the pre-label failure;
    • the five required contexts are green, and git merge-tree against origin/main is clean.

The seat flips the PR ready and arms auto-merge through the relay.


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