Skip to content

refactor(rest): move registerApprovalsEndpoints into approvals-routes.ts, with one registrar list (#23050 slice 3) - #23670

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Part of #23050
Clause-②: no

Slice 3 of the rest-server.ts split, by slice 1's pattern (landed note 6136102437, steps 1-6) with slice 2's getter refinement (6189011700): the body of RestServer#registerApprovalsEndpoints (the approval request list and read, the approve / reject decisions, recall, the revise / resubmit flow moves, the reassign / remind / request-info / comment thread interactions and the audit trail; 12 routes from 7 registration sites) moves, unedited, to the new module packages/rest/src/approvals-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.

The slice also adds the one list the seat ruled for (6189011700, answering PR #23642's open question 2): packages/rest/src/rest-server-registrar-files.ts, the files RestServer's registrars live in. The whole-file readers of rest-server.ts import it, keep their sums over it, and pin its length. It is not a gate.

rest-server.ts goes from 12,520 to 12,070 lines. The card stays open for the later slices.

The pattern, applied

Members the moved body reads, from an AST walk of the method at base c7dd48ab4 (line numbers there): fields routeManager :1960 and approvalsServiceProvider :2031; methods enforceAuth :2406 and resolveExecCtx :2806. That is 4 members (7 reads each of routeManager, enforceAuth and resolveExecCtx, 2 of approvalsServiceProvider). The body has no bare this and no element access on this.

Module-level names the body reads: APPROVAL_REQUEST_LIST_PARAMS, UNDECLARED_WHOLE_NUMBER_PARAM and readDeclaredQueryNumber, all three already exported from rest-server.ts; and strandedDecisionDetails and sendError (as sendEnvelopeError) from @objectstack/types, classifiedRefusalAnswer, nestedRefusalEnvelope, sandboxFaultAnswer and handleRouteError from ./error-response.js, logError from ./log.js, refuseRepeatedQueryParams and refuseUnknownQueryParams from their modules. No export was added anywhere. strandedDecisionDetails was used only by the moved body, so its import and its comment move with it, and rest-server.ts keeps a two-line pointer comment where it was.

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

Fields, checked one by one:

field read when reassigned after registration? handed over as
routeManager registration only (every register call, the three route factories' included, since each factory runs during registration) its one swap (registerMetadataEndpoints) wraps registerMetadataEndpointsInner, not this registrar value
approvalsServiceProvider request time (resolveService, from every handler) not by the class (constructor only), but the field is not readonly getter

The getter is measured, not argued. A one-off test boots a source RestServer with no provider, drives GET /api/v1/approvals/requests/:id, then assigns approvalsServiceProvider on the instance AFTER registerRoutes() and drives it again:

  • this head: 501 NOT_IMPLEMENTED, then 200 {"id":"r1","status":"pending"}. The door reads the live field, as it did when it read this.approvalsServiceProvider on the server.
  • by-value ablation (scripts/ablation-replace.mjs, the getter replaced by approvalsServiceProvider: this.approvalsServiceProvider,; anchor 1 to 0, replacement 0 to 1, read back on disk inside the run): 501, then 501 again. The value shape would freeze the field at registration. Restored by the tool and by a trap: blob da10d3841b4d equals HEAD, git diff HEAD empty, the one-off test file deleted.

The registrar list

packages/rest/src/rest-server-registrar-files.ts exports REST_SERVER_REGISTRAR_FILES (rest-server.ts, data-action-routes.ts, batch-routes.ts, approvals-routes.ts), its element type, and the repo-relative directory for readers outside the package. It is test-side only: nothing in src/index.ts reaches it, so tsup never emits it (measured: dist/ names it nowhere), the same placement as src/http-request-test-builder.ts.

The five zero-contribution whole-file readers now read every file on it. Each keeps its sum and pins the length (4). The moved body contributes zero to each, measured per file with each reader's own rule at base and head:

reader reads sum over the list what pins it
rest-server-query-number-census.test.ts numeric coercions 9 = 8 + 1 + 0 + 0 new case: length 4, files in list order, 9 sites; the ledger is unchanged
rest-server-canonical-query-ast.test.ts query: slots 4 = 1 + 3 + 0 + 0 per-file floors keyed by the list's own type; new control: length 4, floor keys equal the list, census order, sum 4
meta-draft-read-door-census.test.ts draft switches 8 = 8 + 0 + 0 + 0 new case: length 4, files in list order, 8 sites; the node helpers now read each node's own source file; the ledger is unchanged
rest-server-docblock-position.test.ts stacked docblocks, per-module floors floors 4 + 3 + 3 = 10 over the three modules floors keyed by the list's type; new case: length 4, floor keys equal the list less rest-server.ts, sum 10
dogfood realtime tripwires (authz-conformance.test.ts) transport mechanics and route paths 0 hits over the four files the two rest-server.ts probes gain alsoReads (every other list file); probeSource reads them with file; the ZERO case pins length 4 and that each probe's text is exactly the four files joined

The tripwire probes' file stays packages/rest/src/rest-server.ts and their keys are unchanged, so the probe-table census (entries 25 / files 20 / keys 33) and the matrix header's count do not move.

Three readers that slices 1 and 2 extended by hand, because the moved lines contributed to them, now read the list too rather than gaining a fourth name: execctx-consumer-census.test.ts, the two ui-view-route-*.measurement.test.ts files and the dogfood blind-spot census's rest-server.ts row (sums below).

Proof of move-only

  • .d.ts: packages/rest/dist/index.d.ts and index.d.cts are byte-identical to base c7dd48ab4, sha256 7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9 for all four files (cmp silent), rebuilt at the final head 9305039c6. The ESM and CJS bundles load, 39 exports each, one sorted export-name list (sha256 b3246be6a171... base and head). packages/rest/src/index.ts is untouched. The bundle JS bytes differ, because code moved within it.
  • Routes (base c7dd48ab4, head 9305039c6): a source RestServer on a recording mock server, with a protocol that implements every member, 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 3c5754404a85...6319dd, handler sources 236986995419...40ef05 (75 routes, 12 approvals)
    • scoped-optional: eb7a21beb5de...56d66a / 64c05f47329e...6b5f00 / 6e7abd35ba0c...70ab2c (150 routes, 24 approvals)
    • scoped-required: d6d0e5068384...9263e6 / 8ede6c93247d...a94b02e / 54656e8e8abf...ac66c424 (75 routes, 12 approvals)
  • 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 11887-12347 (git show c7dd48ab4:packages/rest/src/rest-server.ts | sed -n 11887,12347p), with one 4-space indent level removed (sed 's/^ //'), against approvals-routes.ts lines 87-547: diff exits 0 and prints nothing (461 lines). The body holds no multi-line string or template literal, so re-indenting changes no runtime string.
  • Moved-line accounting (git diff --color-moved=plain --color-moved-ws=allow-indentation-change c7dd48ab4 HEAD over the two files): 464 deleted and 471 added lines are detected as moved. rest-server.ts is +15 / -465: the 12-line stub, the new import plus its comment (2), and the 2-line pointer comment replacing the strandedDecisionDetails import (5 lines); one }; line the diff keeps in place. approvals-routes.ts adds the module docblock, the imports, the ApprovalsRouteHost 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): approvals-routes.ts is discovered by the *-routes.ts convention and declared dialectOnly with ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the two earlier modules are. rest-server.ts stringError 18 to 17 and siblingCode 31 to 22; the new module carries 1 and 9, both measured by the gate. Sums 17 + 13 + 8 + 1 = 39 and 22 + 15 + 8 + 9 = 54, unchanged. The gate's output differs from base only in those lines and the module counts (13 to 14 modules, 4 to 5 ratcheted).
  • packages/runtime/src/dispatcher-error-vocabulary.ts, the declaration half of check:dispatcher-error-vocabulary (also a gate target path): the runtime-pinned row for the template family APPROVAL_*_FAILED names approvals-routes.ts where it named rest-server.ts. Re-pointed, not re-judged: 57 sites, 57 classified, as at base. The file is not reachable from @objectstack/runtime's entry, so nothing published moves.
  • docs/qa/platform-checklist/areas/approvals.json: the two anchors rest-server.ts#flowMoveRoute and rest-server.ts#handleApprovalError name approvals-routes.ts, where both declarations now live.
  • docs/qa/platform-checklist/areas/records-forms.json: rest-server.ts#mapping resolved, at base, to the only mapping declaration in that file, which is handleApprovalError's prefix map. It now names approvals-routes.ts#mapping, the same declaration. See the acceptance notes: that anchor's prose describes something the declaration never held.
    • check:platform-checklist output is identical base vs head for all three anchors (same per-family resolved counts, so no floor moves).
  • packages/rest/src/execctx-consumer-census.test.ts: reads the list; 62 sites = 41 + 9 + 5 + 7, unchanged. Mentions 85 to 89: the four new ones are this slice's seam (forwarder, two; interface member, two). Three forwarders now, one per moved module, pinned as before (SITE_FILES.length - 1); the stack-frame pattern is built from the list.
  • packages/qa/dogfood/test/authz-probe-blind-spot.census.ts: the rest-server.ts row's population rule reads the list, so 70 / 19 / 51 is unchanged; the seven moved registration sites are still RestServer's and still in the blind spot. this.routeManager.register( stays 63 (42 + 9 + 5 + 7), with one control per module (the new one 7) and registrar list length 4. enforceAuth 68 to 72, re-anchored: four seam occurrences, no call site added.
  • ui-view-route-identity.measurement.test.ts and ui-view-route-tenancy.measurement.test.ts: REGISTRAR_SOURCES is the list. Over it this.enforceAuth( reads 50 and this.resolveExecCtx( 65 (49 and 64 at base, +1 each for the forwarder); this.enforceEnvironmentOwnership( stays 1. Both pin the list's length.
  • rest-approvals-wire-codes.test.ts: the header's present-tense pointer to the three route factories names approvals-routes.ts.

Red before the re-points, each measured with the move in place and that reader not yet moved. The suites ran at eaadafb41, the move alone: the rest suite read Tests 5 failed | 5840 passed | 326 skipped (6171), all five in execctx-consumer-census.test.ts (62 sites read 55, 46 bare read 39 twice, and the two driven sweeps 40 against at least 46 and 26 against at least 30); the dogfood pair read Tests 2 failed | 106 passed (108), population 63 against 70 and the control this.routeManager.register( 56 against 63.

  • check:route-envelope: approvals-routes.ts NOT DECLARED, and rest-server.ts stringError 17 against 18, siblingCode 22 against 31.
  • check:dispatcher-error-vocabulary: [stale-row] ... 'APPROVAL_*_FAILED' at packages/rest/src/rest-server.ts (objlittemplate) but the scan no longer finds it, and [unclassified-site] packages/rest/src/approvals-routes.ts.
  • check:platform-checklist: 11 problems against base's 8, the three ABSENT SYMBOL anchors above.
  • ⚠️ The two ui-view readers would have stayed GREEN: their whole-file checks are floors (greater than 40), and without the list they read 43 and 58. That is the silent narrowing the list exists to close, and the reason they import it rather than staying as they were.

Readers examined and not re-pointed

  • auth-gate-allowlist-fault-window.measurement.test.ts, execctx-authz-input-seam-reachability.test.ts, runtime meta-list-projection-parity.test.ts: they slice other members of rest-server.ts (the gate block, computeExecCtx, /meta).
  • core authz-store-unavailable.test.ts: it discovers transports by resolveAuthzContext, which the moved body does not call.
  • The ADR anchor packages__rest__src__rest-server.ts.json (ADR-0045, filterAppForUser) is unaffected. docs/adr/0076-...md names registerApprovalsEndpoints as an example; the private method is still the registrar, so the pointer still lands.
  • Gates with output identical base vs head (pre-merge reading at 60d82243b): check-durability-degradation-log-level, check-system-context-census, check-single-authz-resolver, check-rest-log-declared, check:liveness, check:cross-package-test-inputs, check:issue-citations, check:test-source-alias, check:rest-log-spy-declared. Identical except the scanned-file counts (+2, the two new files): check-meta-type-normalized, check-nul-bytes, check-adr-anchors, check:error-status-conformance, check:doc-authoring, check-undeclared-dep-imports (+1 specifier). check-scripts-symbol-anchors +4 file-level anchors (the new comments in check-route-envelope.mjs); check-affected-docs 21 to 22 route sources (the new module, admitted by the filename convention).
  • No line citation in the repo points into the moved range (base :11869-:12348); the only rest-server.ts:NNNNN citation at or above :11000 is :11108, in a release-owned CHANGELOG.

Acceptance notes

  • packages/spec prose, not edited (the claim keeps packages/spec out): packages/spec/src/api/error-code-ledger.zod.ts names rest-server.ts as the home of the approvals route factories and handleApprovalError in three comments (about :199, :291 and :1274). They are // comments, read by no gate. They now describe code in approvals-routes.ts. Named in the report for the domain:spec seat.
  • docs/qa/platform-checklist/areas/records-forms.json's #mapping anchor (the OCC item, "CONCURRENT_UPDATE → 409 mapping") lands, at base and at head, on handleApprovalError's prefix map, which holds no CONCURRENT_UPDATE row; packages/rest/src spells CONCURRENT_UPDATE only in one comment. The move keeps it landing where it landed. Re-judging which declaration that item should cite is not a move.
  • packages/plugins/plugin-approvals/src/record-reader-visibility.test.ts (about :363) cites "rest-server.ts handleApprovalError" in a comment. Another lane's file, read by nothing; not edited.
  • scripts/check-self-test-wired.mjs's header quotes a past [stale-row] message that names rest-server.ts. It is a quotation of output, not a pointer; not edited.
  • rest-approvals-wire-codes.test.ts keeps one dated measurement sentence ("deleting the row from rest-server.ts reds THREE cases"), true when written; only its present-tense pointer was re-pointed.
  • docs/adr/0076-...md D11 records the maintainer's 2026-08-15 ruling that rest-server.ts would be delineated, not split; the card's later direction (quoted on [maintainer] refactor(rest): split packages/rest/src/rest-server.ts (13,701 lines, 70 commits in 30 days) along its register*Endpoints seams, one move-only slice per PR, /meta after #22877 stage 2 #23050) is what slices 1 to 3 execute. The ADR is a governed surface and is not edited here.
  • Lines of rest-server.ts from about :42 to :318 shift by -3, from :322 to the moved block by -1, and after it by -450 (base :12351 is head :11901).

Gates and tests

Measured at 9305039c6: this branch (base c7dd48ab4) plus one merge of origin/main 1ced0f82b, which touched no file in this diff (cli, metadata-core, pm scripts and four other checklist areas). All builds and tests went through os-verify-lock; exit codes were captured to files before any pipe.

  • pnpm --filter @objectstack/rest test (project local): Test Files 285 passed (285), Tests 5852 passed / 326 skipped (6178). Base c7dd48ab4: 5845 passed / 326 skipped (6171); the +7 are this PR's new cases (query-number, meta-draft and docblock list pins, plus the docblock pin's two modules that are new to it).
  • pnpm --filter @objectstack/rest test:repo (project repo): Test Files 5 passed (5), Tests 222 passed / 4 skipped (226). Base: 213 passed / 2 skipped (215); the +9 / +2 are the canonical-query-AST census's two new modules and its list control.
  • pnpm --filter @objectstack/rest typecheck: exit 0 (tsc --noEmit, then check:test-typecheck: 0 file(s) / 0 error(s)).
  • pnpm --filter @objectstack/dogfood exec vitest run test/authz-probe-blind-spot.test.ts test/authz-conformance.test.ts: Test Files 2 passed (2), Tests 108 passed (108).
  • pnpm --filter @objectstack/runtime exec vitest run src/error-envelope.conformance.test.ts (the test that imports the edited ledger): 53 passed. pnpm --filter @objectstack/runtime typecheck: exit 0.
  • NOT MEASURED: pnpm --filter @objectstack/dogfood typecheck, reason: exit 2 on 52 errors, 51 of them TS2307 for workspace packages with no dist in this container (connector-mcp, connector-rest, connector-openapi, cloud-connection, setup, connector-slack, account) and one TS7006 behind them in an untouched file; none in either touched dogfood file. CI's typecheck builds first.
  • Lint: eslint --no-inline-config --format json over packages/rest, the two touched dogfood files, packages/runtime/src/dispatcher-error-vocabulary.ts and scripts/check-route-envelope.mjs: 327 files, 0 errors, 0 warnings, every touched lintable file in the result set. Repo-wide pnpm lint is CI's.
  • dispatch-gates --commands --repo objectstack-ai/objectstack, no paths, after the last commit: 80 commands (stderr names commit 9305039c6). --ran: Run reconciliation — 80 derived, 79 run, 1 NOT-MEASURED, 0 UNRUN. 79 exit 0.
    • 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 in the derivation and both exit 0. pnpm check:platform-checklist exits 0 at this head (origin/main brought the fix for its eight pre-existing findings); before the merge its output was identical to base's.
  • check:dev-prereqs and check:skill-frame-freshness (local-only) were not run: no packages/spec build input and no four-axis frame file is touched.

Per command (exit code):

  • node scripts/check-ci-filter-parity.mjs exit 0
  • node scripts/check-closing-keyword-parity.mjs --self-test exit 0
  • node scripts/check-closing-keyword-parity.mjs exit 0
  • node scripts/check-comment-mask-adoption.mjs --self-test exit 0
  • node scripts/check-comment-mask-adoption.mjs exit 0
  • node scripts/check-comment-mask-corpus.mjs exit 0
  • node scripts/check-declaration-mirrors.mjs --self-test exit 0
  • node scripts/check-declaration-mirrors.mjs 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 --self-test exit 0
  • node scripts/check-keyed-text-bounds.mjs exit 0
  • node scripts/check-platform-object-tenancy-census.mjs --self-test exit 0
  • node scripts/check-platform-object-tenancy-census.mjs exit 0
  • node scripts/check-plugin-teardown-shape.mjs --self-test exit 0
  • node scripts/check-plugin-teardown-shape.mjs exit 0
  • node scripts/check-registry-log-declared.mjs --self-test exit 0
  • node scripts/check-registry-log-declared.mjs exit 0
  • node scripts/check-rest-log-spy-declared.mjs --self-test exit 0
  • node scripts/check-rest-log-spy-declared.mjs exit 0
  • node scripts/check-scripts-symbol-anchors.mjs --self-test exit 0
  • node scripts/check-scripts-symbol-anchors.mjs exit 0
  • node scripts/check-self-test-wired.mjs --self-test exit 0
  • node scripts/check-self-test-wired.mjs exit 0
  • node scripts/check-self-test-workflow-commands.mjs --self-test exit 0
  • node scripts/check-self-test-workflow-commands.mjs exit 0
  • node scripts/check-system-context-census.mjs --self-test exit 0
  • node scripts/check-system-context-census.mjs exit 0
  • node scripts/check-undeclared-dep-imports.mjs --self-test exit 0
  • node scripts/check-undeclared-dep-imports.mjs exit 0
  • node scripts/check-whole-set-label-write.mjs --self-test exit 0
  • node scripts/check-whole-set-label-write.mjs 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 0
  • 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

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. @objectstack/runtime's one edit is in a module its entry does not reach (rebuilt at 9305039c6: packages/runtime/dist holds no file containing APPROVAL_*_FAILED, UNREGISTERED_CODE_SITES or approvals-routes.ts, while the control HttpDispatcher is found in 4 files). The other files are tests, a private package, a script and QA data. skip-changeset is the seat's to apply on its own check.

Contract review: this PR edits two gate target paths: scripts/check-route-envelope.mjs (one new MODULES entry and two lowered rest-server.ts counts) and the file of one row in packages/runtime/src/dispatcher-error-vocabulary.ts, the declaration half of check:dispatcher-error-vocabulary.


Generated by Claude Code

…pprovals-routes.ts

Move-only: the body moves unedited, re-indented one level, and the class
keeps the private method with its docblock as an ApprovalsRouteHost literal
plus .call. routeManager goes by value (read only at registration);
approvalsServiceProvider goes as a getter (read at request time, not
readonly). strandedDecisionDetails' import moves with handleApprovalError.

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

packages/rest/src/rest-server-registrar-files.ts is the one list of the files
RestServer's registrars live in. The zero-contribution whole-file readers
(query-number census, canonical-query-AST census, meta-draft door census,
docblock-position pin, dogfood realtime tripwires) and the contributing ones
(execctx census, ui-view measurements, dogfood blind-spot census) read every
file on it, keep their sums, and pin its length. The route-envelope MODULES
entry, the dispatcher-error-vocabulary row and three checklist anchors follow
the moved lines, each sum unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FLwgbTSmGMrqsXkBDsiwfG
…ire-code pin's prose

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/runtime, touching 44 documentable anchor(s).

49 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 57994c311f4acc576e1a1eb9ad4162d191d15905.

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

What this run could not see
  • 3 cross-cutting symbol(s) contributed no route anchor: enforceAuth (23 routes), resolveExecCtx (28 routes), routeManager (22 routes)
  • 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 — 35 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 57994c311f4acc576e1a1eb9ad4162d191d15905 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 57994c311f4acc576e1a1eb9ad4162d191d15905

⚠️ 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 57994c311f4acc576e1a1eb9ad4162d191d15905 → 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 #23670 at 9305039c63 (#23050 slice 3, Part of #23050). It queues after one CONTRACT_REVIEW_TIER review on its head, owed for the gate edits, and a true-green head

domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T12:11Z · holder of claim 6190013305. Report: os-dev-report 6191685890.

席位核对

  1. Shape, read on GitHub:
    • The PR is a draft against main, head 9305039c63. Line 1 is Part of #23050, and the body carries no closing keyword. Line 2 is a bare Clause-②: no. Assignee os-bill.
    • Files: 17, +901 / −524. check-governed-merges --pr 23670: NOT governed, 1425 changed lines. No content/docs/releases/ change.
  2. Move-only, measured by the seat:
    • rest-server.ts :11887-:12347 at base c7dd48ab4 (the body of registerApprovalsEndpoints, 461 lines), with one indent level removed, equals approvals-routes.ts :87-:547 at the head.
    • Every line rest-server.ts gains is one of three things: the ApprovalsRouteHost literal plus .call stub, the import and its comment, or the 2-line pointer left by the moved strandedDecisionDetails import. No export was added.
  3. The pattern:
    • 4 members. routeManager goes by value: it is read only at registration.
    • approvalsServiceProvider goes as a getter: it is read at request time in every handler, and it is not readonly. The dev measured it (501 → 200 after a post-registration assignment) and ablated it (by value: 501, 501).
    • This is slice 2's refinement (6189011700), applied as ruled.
  4. The registrar list (the seat's ruling 6189011700): packages/rest/src/rest-server-registrar-files.ts, test-side and reached by no entry.
    • Its five named readers (query-number, canonical-query-AST, meta-draft-read-door, the docblock-position pin and the dogfood realtime tripwires) read every file on it, keep their per-file sums, and pin its length at 4.
    • The dev widened it to four more readers. That deviation is accepted: those readers' floors would have stayed green over a narrowed reading (ui-view: 43 and 58 against a floor of > 40), which is the silent shape the card forbids. Each pins length 4.
  5. Proofs (the report's proofs, with commands):
    • the .d.ts / .d.cts sha256 is identical base vs head;
    • getRoutes(), the ordered calls and each handler's source hash are identical under 3 configurations;
    • every re-pointed reader's sum is unchanged, red before the re-point.
  6. Tests and gates:
    • @objectstack/rest local: 5852 passed (+7 list pins). repo: 222 passed. Typecheck exit 0. Dogfood: 108 passed.
    • Local union: 80 derived, 79 run, 1 NOT MEASURED (check:dual-build-cjs-loads, prerequisite).
    • CI on this head, read in this act: 25 success, 6 skipped, 7 in_progress (Test Core 1–6/6, Type Check · workspace). One failure, Check Changeset, because no label is on yet (see 7).
  7. Changeset:
    • The seat checked the criterion for itself: the published .d.ts is byte-identical, the bundles export the same 39 names, the routes and handlers are byte-identical, and no behaviour moves.
    • skip-changeset is applied by the seat in this act, as for slices 1 and 2.
  8. Surface:
    • In this lane: packages/runtime/src/dispatcher-error-vocabulary.ts, one ledger row re-pointed (the declaration half of check-dispatcher-error-vocabulary), plus the rest and dogfood readers.
    • Cross-lane, declared by the seat after the fact: scripts/check-route-envelope.mjs (domain:spec): one new MODULES row and two lowered counts, sums unchanged (39 = 17 + 13 + 8 + 1, 54 = 22 + 15 + 8 + 9).
    • Cross-lane, declared: docs/qa/platform-checklist/areas/approvals.json and records-forms.json (domain:devx).
  9. Open question 1, answered: the three comments in packages/spec/src/api/error-code-ledger.zod.ts (about :199-:203, :291, :1274) join the domain:spec pointers this card carries (slice 1's two, and slice 2's six in 6189011700). The slice that moves the CRUD doors names them all. ⛔ Not edited here.
  10. Out of scope:

Landing:


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9305039c63991fede85feddd2ea86dcdc1c10cb7
Independence: INDEPENDENT AGENT
Local-runs: none
Rendered: 2026-10-15T12:25Z

Face: two gates' target paths edited by a move-only refactor (scripts/check-route-envelope.mjs lowers rest-server.ts's two dialect counts and adds a module row; packages/runtime/src/dispatcher-error-vocabulary.ts, the declaration half of check:dispatcher-error-vocabulary, re-points one row's file), plus one new list of the files RestServer's registrars live in, read by nine whole-file readers. No published schema and no governed path is in the 17-file list. Inputs: card #23050 (body and all 10 comments, rulings 6135541164, 6136102437, 6189011700 and the claim 6190013305 included), PR #23670 (body, file list, net diff against main at merge-base 1ced0f82b), and the check-runs on this head. Read-only: the moved body, the member reads and every count below were read off the git objects of the merge-base and the head (git show, text diff, grep); nothing was built, tested or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, each judged:

  1. Published surface of @objectstack/rest: unchanged — right. The rest-server.ts hunks add no export and change no public/protected/private/readonly keyword (both grepped over the diff); src/index.ts is untouched; approvals-routes.ts is not re-exported from the entry, so its registerApprovalsEndpoints and ApprovalsRouteHost stay out of the bundled declaration; the private registerApprovalsEndpoints keeps its name, docblock and position (base :11886, head :11885). Nothing a consumer can import, call or address changes. The dev's sha readings of dist/index.d.ts and index.d.cts are theirs, not re-run here, and are what this construction predicts.

  2. Route table and accept sets: unchanged — right. Base rest-server.ts lines 11887–12347, with one 4-space indent level removed, equal head approvals-routes.ts lines 87–547 line for line (461 lines, text diff empty). The registration call site this.registerApprovalsEndpoints(bp) is unchanged (:4760 to :4759), so the same seven registration sites register the same 12 routes in the same order. The closed query set (refuseUnknownQueryParams(req, res, APPROVAL_REQUEST_LIST_PARAMS)) and the repeated-parameter list travel unedited, so no accept set widens or narrows. The moved text holds no multi-line string or template literal that re-indenting could change (every backtick line is a comment or a single-line template).

  3. Host seam: exactly the members the body reads — right. this. reads in the body are routeManager 7, enforceAuth 7, resolveExecCtx 7, approvalsServiceProvider 2, identical at base and head, with no bare this and no element access; ApprovalsRouteHost declares exactly those four as indexed accesses on RestServer. routeManager goes by value (read only during registration; the class's one reassignment wraps registerMetadataEndpointsInner, not this registrar); approvalsServiceProvider goes as a getter because resolveService reads it at request time and the field is not readonly — slice 2's refinement applied as 6189011700 requires. Methods go as late-bound closures. No helper is copied or re-derived; the three module-level helpers were already exported from rest-server.ts, and the resulting import cycle resolves only at call time. strandedDecisionDetails moved with its only user; tsc (Type Check success on this head) is what proves nothing else in rest-server.ts still needed it.

  4. scripts/check-route-envelope.mjs (gate target path): followed the move, not loosened — right. rest-server.ts stringError 18 to 17 and siblingCode 31 to 22; the new row packages/rest/src/approvals-routes.ts at stringError: 1, siblingCode: 9, dialectOnly with ratchet: '#9559'; the sibling rows data-action-routes.ts 13/15 and batch-routes.ts 8/8 untouched (read at the merge-base). Sums 17 + 13 + 8 + 1 = 39 and 22 + 15 + 8 + 9 = 54, equal to base. Counted by hand on the moved text: nine { code, error } bodies (the mapped answer in handleApprovalError, the 404 in the single read, and the terminal 500 arms of list, read, decision, recall, flow-move, thread and actions) and one string-literal error (the 404's template), so the new row equals what left. Not loosened: inside rest-server.ts (itself dialectOnly) these lines were held to the two dialect facts and no write-site count; the new row holds them to exactly that. The gate discovers *-routes.ts by convention and refuses an undeclared module, and the row lands in the same PR as the file, so no gap exists. Gate verdict: pnpm check:route-envelope runs in Lint · docs & code guards — success on this head, aggregated into Lint & Repo Gates — success.

  5. packages/runtime/src/dispatcher-error-vocabulary.ts (gate declaration half): re-pointed, not re-judged — right. The APPROVAL_*_FAILED row (shape objlittemplate, door rest, verdict runtime-pinned, pin rest-approvals-wire-codes.test.ts) changes only its file; the three template arms it classifies (decisionRoute, flowMoveRoute, threadRoute) are in the moved text. The pin test boots RestServer and drives routes; it reads no source by path, so it needed only its prose pointer updated. The module is imported by no runtime source file (only by tests and the gate script) and is not exported from packages/runtime/src/index.ts, so @objectstack/runtime publishes nothing new. Gate verdict: pnpm check:dispatcher-error-vocabulary runs in Lint · docs & code guards — success.

  6. The registrar list rest-server-registrar-files.ts and its readers: widened and pinned, nothing loosened, nothing can go silently empty — right. The list is test-side (unreachable from src/index.ts), so it adds nothing to the published surface. Each reader's pins, reproduced from the head tree:

    • execctx-consumer-census.test.ts: 62 sites exact; resolveExecCtx mentions 85 to 89 (read: 61 + 12 + 7 + 9 = 89; the +4 are the forwarder's two and the interface member's two); forwarder lines pinned at SITE_FILES.length - 1 = 3 (read: :9321, :11894, :11915); the stack-frame regex is built from the list.
    • authz-probe-blind-spot.census.ts: this.routeManager.register( 63 = 42 + 9 + 5 + 7 (read, each file); one control per module (new 7); registrar list length 4; private register*Endpoints( 16 and registerPerItemRoute( 8 (read, unchanged); enforceAuth 68 to 72 (read; the four are the seam, no call site added). 70 / 19 / 51 unchanged by the row's own rule.
    • ui-view-route-identity and -tenancy: floors above 40 over the list now carry a length pin; actuals 50 and 65 (read; base 49 and 64, +1 forwarder each). These two would have stayed green over a narrowed reading, so the list tightens them.
    • rest-server-query-number-census.test.ts: 9 sites exact, list order pinned; the moved body holds no numeric coercion (it calls readDeclaredQueryNumber, a declared read).
    • rest-server-canonical-query-ast.test.ts: per-file floors typed as a Record over the list's own type (a listed file without a floor is a compile error), sum pinned 4; approvals-routes.ts and batch-routes.ts hold 0 query: literals (read).
    • meta-draft-read-door-census.test.ts: 8 sites exact; node helpers now use getText() / getStart() against each node's own SourceFile, which is the correct multi-file form; 0 draft tokens in the three moved modules (read).
    • rest-server-docblock-position.test.ts: floors typed over the list, 4 + 3 + 3 = 10 pinned (read: 4, 3, 3 stacked docblocks).
    • dogfood realtime tripwires: alsoReads widens the read only; the ZERO case pins length 4 and asserts each probe's text is exactly the four files joined; 0 transport tokens over the four files (read). file and keys are unchanged, so the probe-table census does not move.
      Every list read is a bare readFileSync (a missing path throws), every reader pins length 4, and the typed floor tables refuse an unfloored file, so a module added to the list cannot widen a reader unnoticed and a module dropped from it cannot narrow one. The dogfood relative import of rest source has precedent (rest-route-ledger.js at base) and turbo.json hashes packages/rest/src/** for @objectstack/dogfood#test; check:cross-package-test-inputs runs in Lint · CI, release & engine gates — success.
  7. Platform-checklist anchors (approvals.json two, records-forms.json one): land on declarations — right. flowMoveRoute, handleApprovalError and mapping are const declarations in approvals-routes.ts. check:platform-checklist is out of per-PR CI by maintainer decision (checklist-status.yml), so this is a static read; the main watchdog is the mechanical check after landing.

  8. ADR anchor and the sweep for what the dev might have missed — right. scripts/adr-anchors/packages__rest__src__rest-server.ts.json anchors filterAppForUser (ADR-0045), outside the moved range; check:adr-anchors runs in Lint · repo & tooling gates — success. At head, outside CHANGELOG.md, no rest-server.ts# pointer names a moved symbol and no rest-server.ts: line citation falls in 118xx–123xx; the only prose still naming rest-server.ts for approvals code is the three items the dev flagged (below).

② Semver level

  • skip-changeset is right for @objectstack/rest here. The criterion is the one the dispatch set (6190013305) and slices 1–2 landed on (6136102437, 6189011700): a byte-identical published .d.ts, export list and route table. ① items 1–3 give that by construction; the bundle's JS bytes differ only by where the code sits. A move that changes no behaviour is not a feature, functional improvement or fix, so no patch changeset is owed under Post-Task Checklist 3.
  • @objectstack/runtime: the edited module is unreachable from the package entry (① item 5); nothing published moves.
  • The other files are tests, QA data, one script and a private package.
  • Clause-②: no — right. No accept set widens or narrows: the query sets travel unedited, no spec key moves, no route is added or removed.
  • Check-run note: Check Changeset concluded failure on its first run (12:05Z, before the label) and skipped on the re-run after skip-changeset was applied (12:12Z). It is not one of the five required contexts, and the label is the seat's own act per the claim.

③ Boundary flags

Every dev flag (PR acceptance notes, os-dev-report 6191685890 open_questions, deviations and out_of_scope_findings), answered or escalated:

  1. open_questions[0] — three comments in packages/spec/src/api/error-code-ledger.zod.ts (about :199, :291, :1274) name rest-server.ts for code now in approvals-routes.ts. Answered: not this PR's (the claim keeps packages/spec out; no gate reads them). ESCALATED to the seat as option B: add the three to the domain:spec touch already carried by 6189011700 (slice 1's two pointers, slice 2's six liveness strings); if the CRUD slice is not the next dispatched, that note's domain:spec card is the carrier.
  2. records-forms.json #mapping anchor resolves, at base and head, to handleApprovalError's prefix map, which holds no CONCURRENT_UPDATE row. Answered: re-pointing to the same declaration is the move-only act; re-judging the anchor is not. ESCALATED: the seat's cross-lane declaration on [PM seat] domain:devx @ objectstack — 🟢 os-sales · session_014eFvbvGK4FjXvscggDNTD4 · full-test wave (maintainer) #6023 (owed for records-forms.json and approvals.json, as for slices 1–2) names this mis-anchored item so the checklist owner re-judges it.
  3. ADR-0076 D11 still records the 2026-08-15 ruling that rest-server.ts is "delineated, not split"; [maintainer] refactor(rest): split packages/rest/src/rest-server.ts (13,701 lines, 70 commits in 30 days) along its register*Endpoints seams, one move-only slice per PR, /meta after #22877 stage 2 #23050 executes the maintainer's later verbatim direction. Answered: this slice does not wait on it — the maintainer's words on the card are the newer decision, triage and two landed slices rest on it, and the ADR's own 2026-08-20 revision says reopening is an ordinary Prime Directive [WIP] Add Chinese version of the documentation #13 decision argued from a measurement, which the card carries. ESCALATED to the maintainer through the seat: Prime Directive [WIP] Add Chinese version of the documentation #13 wants the amended status line on D11; docs/adr/** is Tier H, so it is a card for the maintainer's hand (or an authorized approval), never a rider on this PR. The dev noted it unfiled; it needs a durable carrier.
  4. Prose with no reader — plugin-approvals/src/record-reader-visibility.test.ts about :363, check-self-test-wired.mjs's quoted past message, the dated sentence in rest-approvals-wire-codes.test.ts. Answered: leaving them is right; note only.
  5. rest/runtime: under a wall, the /meta history, audit and package-commits doors admit setup.access, which organization admins hold, so an organization admin reads the environment-wide authoring trail of any item #23546 (the /meta trail doors) had not landed when the PR opened and may add draft switches to rest-server.ts, where the meta-draft census now pins 8 exact. Answered: a landing-order question, not a contract one — the queue rebuilds on current main and the exact pin reds loudly for whichever lands second. The seat watches the queue.
  6. NOT MEASURED locally — dogfood typecheck (no dist in the container) and check:dual-build-cjs-loads (prerequisite not met). Answered by the head's check-runs: Type Check · workspace success, TypeScript Type Check success, Build Core success.
  7. Gate verdicts on this head, latest run per name, as read at 12:25Z: Lint & Repo Gates success; TypeScript Type Check success; Build Core success; Governed Surface Queue Guard success; Dogfood Regression Gate success; Test Core shards 1, 5 and 6 success, shards 2, 3 and 4 in_progress (started 12:06Z). 28 success, 9 roster skips, 0 failures among completed latest runs. This record's PASS is the contract verdict; the Tier S landing rule still wants every required check at success before the queue — in_progress is not a pass, so the seat queues only once the three shards conclude success.

Implemented-by: claude/issue-23050-s3-approvals-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 6192006790 (PASS on 9305039c63) is verified and adopted, and the PR queues now · domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG · 2026-10-15T12:32Z · holder of claim 6190013305.

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