Repository navigation
refactor(rest): move registerUiEndpoints into ui-routes.ts (#23050 slice 10) - #23990
Conversation
slice 10) The body of RestServer#registerUiEndpoints (GET {basePath}/ui/view/:object/:type) moves, re-indented one level and otherwise unedited, to packages/rest/src/ui-routes.ts. The class keeps the private method and its docblock as a UiRouteHost literal plus .call. TransportScopedMetaRequest moves with it (exporting it in place changes the published d.ts by one line, measured), and rest-server.ts imports it back. The registrar list gains ui-routes.ts. Readers are re-pointed in the next commit. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
… slice 10) Every reader of the registrar list re-reads its length (10 -> 11) with its sum unchanged. The two ui-view measurement tests slice registerUiEndpoints' body where it now lives (ui-routes.ts) instead of the host literal, and the ownership guard stays pinned at exactly one call site, with this slice's one host-literal forwarder pinned beside it. check-route-envelope declares ui-routes.ts at the counts that left rest-server.ts (sums 39 and 54 unchanged). Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 18 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin be80cf625e5a1a8006275e1ea7200925b00f481a && git checkout be80cf625e5a1a8006275e1ea7200925b00f481a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be 5da02e4b97d67a88a6a47f766613e51c1a12bc61 && git checkout -B drift-repro 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be && git merge --no-ff 5da02e4b97d67a88a6a47f766613e51c1a12bc61
node scripts/docs-audit/affected-docs.mjs --json 2e1562091cf850d7f3d1057f5ebb82e9ff8cb0be
|
Contract reviewServed-tier: Face: the declaration face. This is a move-only slice of
The 13-file list holds no published schema and no governed path. Inputs:
Method: read-only. Every count below was read off the git objects of the merge base and the head with The merge base's ① Derived judgmentsNo accept set changes and no public surface changes. Each change the diff makes is judged below.
② Semver level
③ Boundary flagsEvery dev flag is answered or escalated. The flags come from the PR body's Acceptance notes and from the report's (
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · #23050 slice 10 (UI view) ·
Seat ruling on the open question (where
Carried, not edited here. A new head would need a new review, even for text that is only comments.
Landing:
Generated by Claude Code |
Part of #23050
Clause-②: no
Slice 10 of the
rest-server.tssplit, by slice 1's pattern (landed note6136102437, steps 1–6), slice 2's getter refinement (6189011700) and slices 3–9's shape (slices 8 and 9: PR #23882, PR #23959). The body ofRestServer#registerUiEndpoints(GET {basePath}/ui/view/:object/:type, the identity- and ownership-gated door onto the protocol'sgetUiView) moves to ONE new module,packages/rest/src/ui-routes.ts. The route table, the ordered registrations, every handler's source and digest, the publisheddist/*.d.tsand both bundles' export lists are byte-identical. Every reader whose reading included a moved line now reads it where it is, with its sum unchanged.No token of the body changed. The body calls no class
static, so thestaticexception (6205442867) is not used. It reads no provider field, so the provider ruling (6215084478) does not apply: nothing in the host is a getter. The ruled order of its guards is unchanged, byte for byte and at runtime (below).One module-level type moved with the body, by pattern step 5. The body types its request through
TransportScopedMetaRequest, a type alias declared inrest-server.tsand not exported. The claim says to export helpers from where they are declared. For a type alias, that changes the published.d.ts(measured below, twice), and slice 1's step 5 already rules for exactly this case: "A type alias moves with the body, because exporting one changes the.d.ts." So the alias block (its docblock and declaration, 40 lines) moved verbatim toui-routes.ts, gaining onlyexport.rest-server.tsimports it back for its 12 other typed uses. This mirrors slice 1'sServerScopedDataRequest. The alias's final home is asked as an open question in the report, as slice 1's was.rest-server.tsgoes from 9,995 to 9,915 lines (base68ffa5dc1a). The card stays open for the later slices; #23050 remains open after this PR.The pattern, applied
Members the moved body reads, from an AST walk of the method (TypeScript compiler API with a type checker, symbol-resolved, base
68ffa5dc1a):registerUiEndpoints(JSDoc:8284, method:8313–:8374, body:8314–:8373, 60 lines).routeManagerregistercall (:8318):2026, privateresolveRequestEnvironmentId:8329):2322, private asyncresolveExecCtx:8342):2872, private asyncenforceAuth:8343):2472, privateenforceEnvironmentOwnership:8344):2597, privateresolveProtocol:8345):2404, private asyncThe body has no bare
this, no element access onthis, nofunctionkeyword and noRestServer.NAMEaccess. The seat's text count (the same six members, one read each) is confirmed.Control walk (the same walker, same base): the
registerSearchEndpointsstub (:8999) readsrouteManager,resolveProtocol,resolveExecCtxandenforceAuthonce each (slice 9's host literal).enforceEnvironmentOwnershipitself (:2597) readsextractProjectIdHeaderonce, plusANONYMOUS_DENY_STATUSandANONYMOUS_DENY_BODYfrom@objectstack/core. Both match what their source says.Module-level reads (the same walk):
handleRouteError(./error-response.js): still used acrossrest-server.ts, so its import stays; the module imports the same name from the same module.GetUiViewRequest(type,@objectstack/spec/api): read ONLY by this body (2 hits inrest-server.tsat base: the import and this use). Its import name moved with the body, leaving a two-line pointer comment in the import block, as slice 6 did forErrorCode.TransportScopedMetaRequest(declared:299): see above and the measurement below.Host shape (slice 1's shape B):
UiRouteHost(6 members) lists exactly the members the body reads, each typedRestServer['m'], in the body's read order. The class keeps the private method with its name, docblock and position. Its body is now a host literal plusregisterUiEndpoints.call(host, basePath). Methods go as late-bound closures,(...a) => this.m(...a). The call site inregisterRoutes'registerForBase(this.registerUiEndpoints(bp);, base:4805, head:4772, underapi.enableUi) is unchanged:registerRoutes(75 lines) diffs to 0 between base and head.Fields, checked one by one:
routeManagerregistercall, a top-level statement of the body)registerMetadataEndpointsInner, not this registrarNo other field is read. The environment, the caller, the ownership verdict and the protocol are each resolved per request by a method, so no getter is owed.
Late binding and the guard order, measured (a one-off vitest file, copied in, run under the verify lock, deleted; not committed). A source
RestServergets five instrumented members planted on the instance BEFOREregisterRoutes(), and the door is driven. Then five new ones are planted AFTER registration, and the door is driven again. Each member records its call.MEASURE before: 200 {"answeredBy":"early",…} | order early:env,early:ctx,early:auth,early:own,early:protoandMEASURE after: 200 {"answeredBy":"late","env":"env_late",…} | order late:env,late:ctx,late:auth,late:own,late:proto | early calls after re-plant 5. Result:Tests 1 passed (1). The members reached are the late ones, in the ruled order (resolve the environment once, resolve identity in it, refuse anonymity, compare ownership, then resolve the protocol).scripts/ablation-replace.mjs: the host literal'senforceEnvironmentOwnership: (...a) => this.enforceEnvironmentOwnership(...a),was replaced byenforceEnvironmentOwnership: this.enforceEnvironmentOwnership.bind(this),. Anchor 1 → 0; blobb429a90b19d3→97583add2404.order late:env,late:ctx,late:auth,late:proto, with the early guard called a sixth time, andTests 1 failed (1).b429a90b19d3equalsHEAD, andgit diff HEADis empty.rest-server.ts(blob1c7f7e45a921) was restored into the tree under a trap, thengit checkout HEADwith blob and diff proof. The answers are the same as the head's, andTests 1 passed (1).Proof of move-only
rest-server.tslines 8314–8373, with one 4-space indent removed, againstui-routes.tslines 118–177.diffexits 0 over the 60 lines.rest-server.tslines 260–299 againstui-routes.tslines 54–93.diffexits 0 once theexportkeyword on the last line is set aside. A//note above it says why it moved, and that the block's "here" and "this file" still meanrest-server.ts, where the doors it describes live.:8284–:8313, head:8251–:8280) diff to 0..d.ts:packages/rest/dist/index.d.tsandindex.d.ctswere built at base68ffa5dc1a(before any edit), at the merge head5da02e4b9, and again after the whole-workspace build.7489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9.registerUiEndpoints.call: 1 hit in each ofindex.jsandindex.cjs, 0 at base), so the declaration was rebuilt from the moved source, not replayed.src/index.tsis untouched.ablation-replace.mjs, restored by blob and diff proof, and rebuilt back to7489de84…):export type TransportScopedMetaRequest…at its declaration givesdist/index.d.tssha256a82fe845e80c…: line 1625, a blank line, disappears.export type { TransportScopedMetaRequest };statement gives the samea82fe845e80c…and the same one-line change.ServerScopedDataRequest. Moving the alias, by contrast, leaves the.d.tsbyte-identical (above).b3246be6a1718700…on all four). No UI-module name is among them.RestServerruns on a recording mock server, with a protocol that answers every member. It is dumped under slice 9's eight configs plus a ninth,enableUi: falseunder scoped-optional, the door's own switch. The eight are: default; scoped-optional; scoped-required; every provider set;enableDiscovery: false;enableOpenApi: false; both off under scoped-optional;enableSearch: falseunder scoped-optional.getRoutes()(functions as arity); the ordered server registration calls; a whitespace-free sha256 of every handler's source; and that source.68ffa5dc1avs merge head5da02e4b9(diff -rexits 0). A second base run was identical to the first.6931d1a5d182…/ccb3bd12f10b…) are the ones slices 5–9 reported for it.'…protocol implementation'→'…protocol implementation!', throughablation-replace.mjs; anchor 1 → 0, blob2eab547e5bba→1d0809727b0b).GET /api/v1/ui/view/:object/:type, andGET /api/v1/environments/:environmentId/ui/view/:object/:typewhere scoped. Nothing else differed, and nothing differed underenableUi: false.2eab547e5bbaequalsHEAD.Readers re-pointed (sums unchanged, no gate loosened)
packages/rest/src/rest-server-registrar-files.tsgainsui-routes.tswith a[#23050 slice 10]comment (length 10 → 11). Every reader that pins its length re-reads its number:rest-server-query-number-census.test.tsrest-server-canonical-query-ast.test.ts(repoproject)query:slotsui-routes.ts: 0; length 10 → 11meta-draft-read-door-census.test.tsrest-server-docblock-position.test.tsui-routes.ts: 4(the module header, the moved alias's block, the host interface's and the function's).rest-server.tsgives up only the alias's block: 164 → 163, floor 100. Length 10 → 11execctx-consumer-census.test.tsrest-server.ts, interface member ×2 in the module);rest-server.ts59 → 60. Forwarders 12 = (11 − 1) + 3 − 1; neither map gains an entryui-view-route-identity.measurement.test.tsthis.enforceAuth(59,this.resolveExecCtx(74 over the list (58 / 73 before, +1 each for the UI literal's forwarders; floors 40 hold)ui-view-route-tenancy.measurement.test.tsauthz-conformance.test.ts(realtime tripwires)authz-probe-blind-spot.census.ts,rest-server.tsrowthis.routeManager.register(63 = 20 + 9 + 5 + 7 + 8 + 1 + 6 + 4 + 1 + 1 + 1;private register*Endpoints(16ui-routes.ts: this.routeManager.register(1;registrar list length11;enforceAuth104 → 108 re-anchored (the one call site moved and is counted in both readings; the four new occurrences are the seam once; no call site added);populationRulenames the new moduleThe two ui-view measurement tests read this body directly, so they are re-pointed with care.
rest-server.tsfromprivate registerUiEndpoints(toprivate registerCrudEndpoints(. That span is now the host literal, whose forwarders spellthis.enforceAuth(,this.resolveExecCtx(,this.enforceEnvironmentOwnership(andthis.resolveProtocol(in the very order the tenancy test asserts, while calling none of them in a route. A slice there would pass the order assertions vacuously.ui-routes.ts, fromexport function registerUiEndpoints(to the function's own closing brace (the first column-0 brace after it), so neither the module header nor a later declaration can satisfy it.toContainand every order assertion is unchanged. That the private method still mounts this one route is held, as before, by the identity test's §6 runtime attribution (byRegistrar.get('registerUiEndpoints')equals the one UI route).this.enforceEnvironmentOwnership(over the list was pinned at exactly 1, "a single new call site plus its definition". Over the eleven files the spelling now reads 2: the one call site, which moved toui-routes.ts, plus this slice's host-literal forwarder.(...a) => this.enforceEnvironmentOwnership(...a))toBe(1), and call sites (spellings minus forwarders)toBe(1).Readers outside the list:
scripts/check-route-envelope.mjs(a gate's target paths,domain:spec):ui-routes.tsis declareddialectOnlywith ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the nine earlier modules are, at its measuredstringError 1/siblingCode 1. Its one hand-built refusal is the 501{ error: 'UI View resolution not supported by protocol implementation', code: 'NOT_IMPLEMENTED' }.rest-server.tsdrops by exactly that body:stringError16 → 15 andsiblingCode14 → 13, each recorded as "NOT progress — the same MOVE".origin/mainonly in therest-server.tsrow and the new row (20 → 21 modules, 11 → 12 ratcheted).--self-testis green.ADR-NNNN Dk.check-adr-anchorsoutput is identical atorigin/mainand head (1770 decision-letter citations).check:platform-checklistoutput is identical atorigin/mainand head.Red before the re-points, each measured with the move in place (
9c34e1a99, the readers at their base text):@objectstack/restlocalreadTest Files 6 failed | 281 passed (287)andTests 7 failed | 5895 passed | 326 skipped (6228). The failures were the four length pins (execctx, meta-draft, query-number, docblock), the docblock pin's unfloored new module, and both ui-view sliced readings:expected 'private registerUiEndpoints(basePath:…' to contain 'p.getUiView(', and the same forconst isScoped = ….rest-server-canonical-query-ast.test.ts(repo) readTests 2 failed | 248 passed | 11 skipped (261).Tests 2 failed | 106 passed (108): the realtime tripwire's length pin, andcontrol "registrar list length" moved … expected 11 to be 10.check:route-envelopereadui-routes.ts NOT DECLARED, plusrest-server.tsstringError: found 15, declared 16andsiblingCode: found 13, declared 14.toContain, so its exact ownership pin was not observed red in that run. It was counted directly instead: 1 spelling at base, 2 at the move commit (1 call site plus 1 forwarder).Readers examined and not re-pointed
packages/spec(stopped at, per the dispatch, not edited): no reader there includes a moved line.packages/spec/liveness/rest_api.jsoncitesrest-server.ts#registerRoutesfor theif (this.config.api.enableUi)gate, the call site that did not move. Its second citation isregisterDiscoveryEndpoints, slice 7's.packages/specnamesTransportScopedMetaRequestin comments as living inpackages/rest(e.g.protocol.zod.ts), never by file, and it still does.check:livenessoutput is identical atorigin/mainand head.rest-route-ledger.ts,packages/runtime/src/route-ledger.tsandexecctx-consumer-census.test.ts(dated history). The same holds fordata-action-routes.ts'{@link TransportScopedMetaRequest}, which resolves by name exactly as it did before.docs/audits/2026-06-handwritten-docs-accuracy-followups.mdcitesrest-server.ts:2603-2612, a dated audit line outside both moved ranges (:260–:299,:8284–:8374). ReleasedCHANGELOG.mdtext: not edited.packages/rest/srcreach the moved code in its new file. Their outputs atorigin/mainand head differ only by the one scanned file:check-meta-type-normalized38 → 39;check-dispatcher-error-vocabularyandcheck-error-status-conformance+1 file (57 classified sites unchanged);check-undeclared-dep-imports+1 file, +1 specifier;check-affected-docs: route sources 28 → 29 and registration call sites 23 → 24, by the*-routes.tsconvention;check-scripts-symbol-anchors: +4 file-level anchors, the gate's new comments.origin/mainand head:check-adr-anchors,check-system-context-census,check-single-authz-resolver,check-rest-log-declared,check-durability-degradation-log-level,check-filter-alias-parityandcheck:platform-checklist.rest-server.ts:NNNNline citation in a gated document falls in a moved range.Acceptance notes
objectql-slot-consumer-census.test.ts(slice 8's extra reader) is not touched: the UI body reads no provider slot, and it stays green.[commit cc837dbfe]comments in the body still read true in the module: they describe the handler they sit in, and nameexecctx-consumer-census.test.tsby file, which still counts this site.rest-server.ts, where those doors live; the note above it inui-routes.tssays so rather than editing the moved text.Gates and tests
Measured at this branch: base
68ffa5dc1a; the move9c34e1a99; the re-pointsc9b5a6440; and one merge oforigin/main0b6956490(5da02e4b9: #23881 plugin-audit, cli and two checklist areas; #23937 spec tests; #23966 create-objectstack; no file in this diff). All builds and tests went throughos-verify-lock. Every exit code was captured to a file before any pipe. Final head:5da02e4b9.pnpm --workspace-concurrency=2 --filter '@objectstack/rest...' --filter '@objectstack/verify...' buildexited 0 at the merge head. Thenpnpm turbo run build --concurrency=2, the whole workspace, exited 0 (Tasks: 76 successful, 76 total), after which the@objectstack/rest.d.ts/.d.ctsstill equal base.pnpm --filter @objectstack/rest typecheck: exit 0 (tsc --noEmit, thencheck:test-typecheck: 0 file(s) / 0 error(s)).pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 287 passed (287), Tests 5902 passed / 326 skipped (6228).pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: Test Files 5 passed (5), Tests 250 passed / 11 skipped (261).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).Lint:
pnpm exec eslint --no-inline-config --format jsonover the 13 changed files: exit 0, 13 results, 0 errors, 0 warnings. The published.d.tsis byte-identical, so no consumer outsidepackages/restcan lint differently. Repo-widepnpm lintis CI's (Lint & Repo Gates).check:dev-prereqsandcheck:skill-frame-freshness(local-only) were not run: this diff touches nopackages/specbuild input and no four-axis frame file.No changeset: nothing any released package publishes changes (the
.d.ts, export lists and routes above).skip-changesetis the seat's to apply on its own check.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, with no paths, at5da02e4b9. Its stderr names that commit, the--repoassertion holds, and it reads 13 paths against merge base0b6956490, 477 changed lines.5da02e4b9after the whole-workspace build, and every one exited 0.--ranreads:Run reconciliation — 79 derived, 79 run, 0 NOT-MEASURED, 0 UNRUN.pnpm check:route-envelope,pnpm --filter @objectstack/spec run check:liveness,pnpm check:durability-log-level,pnpm check:dispatcher-error-vocabulary,pnpm check:dts-closure,pnpm check:dual-build-cjs-loads,pnpm check:published-files,pnpm check:cross-package-test-inputs,pnpm check:nul-bytesandpnpm check:pm-dispatch-gates(the battery took 299.7s on a shared box).check-adr-anchorsandcheck-platform-checklistare not derived; both also ran, exited 0, and printed output identical toorigin/main.origin/mainmoved on after the one merge, to2e1562091(five commits: fix(service-automation): a failing parallel keeps every branch's steps in the run log #23970, feat(platform-objects): User Preferences and Identity Links leave Setup's menu; the User page gains a Preferences tab #23936, test(dogfood): pin the undeclared-key refusal on the suite's SQL driver arms (#15953 slice 7) #23976, chore(objectui): move the console pin to f8267df93aba, past the spec deletion of checkPageComponentViewData #23973, fix(cli,runtime): refuse an uncreatable state, output or cache directory at once instead of looping in recursive mkdir #23979). None touches a file in this diff,packages/rest, the gate or the dogfood authz files.scripts/check-cli-test-child-env.mjs,scripts/ci/spec-shape-retirements.jsonandscripts/sdui-manifest.record.json. All belong to families this diff does not touch.Per command (exit code), 79 lines:
Cross-lane files in this diff:
scripts/check-route-envelope.mjs(domain:spec, a gate's target paths: one newMODULESentry and therest-server.tsrow's two counts, each with a history comment; sums unchanged). The dogfood files (packages/qa/dogfood/test/authz-conformance.test.ts,authz-probe-blind-spot.census.ts) are re-pointed as in slices 3–9. No checklist file and no ADR anchor is touched.check-governed-merges --testover the 13 paths: NOT governed (0 of 13 paths hit the register); 477 changed lines.Generated by Claude Code