Repository navigation
refactor(rest): move registerEmailEndpoints into email-routes.ts (#23050 slice 8) - #23882
Conversation
Slice 8 of the rest-server.ts split, move only: the body of
RestServer#registerEmailEndpoints (POST {basePath}/email/send) moves,
re-indented one level and otherwise unedited, to email-routes.ts. The
class keeps the private method and its docblock as a host literal plus
.call; routeManager goes by value, emailServiceProvider as a getter.
seamOrUndefined, the one helper declared in rest-server.ts that the body
reads, is exported from there. The registrar list gains the module.
Readers are re-pointed in the next commit.
Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT
Co-authored-by: Claude <noreply@anthropic.com>
Every reader whose reading included a line of registerEmailEndpoints' body now reads it in email-routes.ts, each with its sum unchanged: - the registrar-list readers re-read their numbers at length 9 (query-number census, canonical query-AST census, draft-door census, docblock-position pin, execctx consumer census, the two ui-view measurements, the dogfood tripwires and blind-spot census row); - the objectQLProvider census reads its emailServiceProvider controls in email-routes.ts, where the slot's two reads and one call now are; - check-route-envelope declares email-routes.ts at stringError 1 / siblingCode 3, and rest-server.ts drops 17 -> 16 / 17 -> 14 (sums 39 and 54 unchanged); - the integration-system checklist item's source and verify prose name email-routes.ts. Claude-Session: https://claude.ai/code/session_018t6a5rvqPRvEDMCVVW9kYT Co-authored-by: Claude <noreply@anthropic.com>
…ssue-23050-s8-email-endpoints
📓 Docs Drift CheckThis PR changes 1 package(s): 32 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 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 abd4663d03b59e1da60c04d08d5062fac18b43f7 && git checkout abd4663d03b59e1da60c04d08d5062fac18b43f7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 944f03850ec68ffb52374ce51424074f9e43b564 35f207b70e9155039b2f71a65cbd9e9c682a7ad9 && git checkout -B drift-repro 944f03850ec68ffb52374ce51424074f9e43b564 && git merge --no-ff 35f207b70e9155039b2f71a65cbd9e9c682a7ad9
node scripts/docs-audit/affected-docs.mjs --json 944f03850ec68ffb52374ce51424074f9e43b564
|
Contract reviewServed-tier: Face: the declaration face. This is a move-only slice of
The 15-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 ① 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 two places: the PR body's Acceptance notes, and the report's (
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT · #23050 slice 8 (email) ·
Seat ruling on the review's escalation (③ item 1, the provider's call receiver). Later slices keep the getter shape for a provider field, unbound, as slices 2–8 did. Reason: no shipped provider reads Carried, not edited here (a new head would need a new review for comment-only text):
Landing: Generated by Claude Code |
Part of #23050
Clause-②: no
Slice 8 of the
rest-server.tssplit, by slice 1's pattern (landed note6136102437, steps 1–6), slice 2's getter refinement (6189011700) and slices 3–7's shape (PR #23670, PR #23705, PR #23719, PR #23729, PR #23763): the body ofRestServer#registerEmailEndpoints(POST {basePath}/email/send, the transactional send door onto theIEmailServiceprovider) moves to ONE new module,packages/rest/src/email-routes.ts. Routes, the ordered registrations, every handler's source digest, the publisheddist/*.d.tsand both bundles' export lists are byte-identical, and 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(AST walk below), so thestaticexception the claim names up front (seat ruling6205442867) is not used, and no receiver token was rewritten.rest-server.tsgoes from 10,127 to 10,047 lines (basef7e836da1e). The card stays open for the later slices.The pattern, applied
Members the moved body reads, from an AST walk of the method (TypeScript compiler API, symbol-resolved, base
f7e836da1e):registerEmailEndpoints(JSDoc:9063, method:9082-:9177, body:9083-:9176, 94 lines).routeManagerregistercall:2022, privateemailServiceProvider:2091, private, notreadonlyresolveExecCtx:2868, private asyncenforceAuth:2468, privateNo bare
this, no element access onthis, nothis-rebindingfunction, and noRestServer.NAMEaccess (static reads: none). Module-level reads:logError(./log.js): 6 more uses inrest-server.ts, so it stays there; the module imports it from the same module.seamOrUndefined: declared inrest-server.ts(:1493), 5 more uses there. It isexported fromrest-server.tsand imported by the module: one declaration, never a copy.src/index.tsdoes not re-export it, so the.d.tsdoes not move (measured below). The same class of edit as slice 5'sexport function mayReadPendingDrafts.Mechanism hypotheses from the dispatch, measured:
routeManager,emailServiceProvider,resolveExecCtxandenforceAuth, and nothing else.tenancyServiceProvider,kernelManager,resolveRequestEnvironmentId,resolveAnonymousFormTenancyandreadFormObjectDefinitionsare NOT read by it: they belong to the next method,resolveAnonymousFormTenancy(:9185, a forms helper; the same walk on it readskernelManager,resolveRequestEnvironmentIdandtenancyServiceProvider). The claim's "about 210 lines" ran past the method's end; the method is 96 lines (plus its 19-line JSDoc), and nothing past:9177moved.RestServer.NAME(call in the body.Host shape (slice 1's shape B):
EmailRouteHost(4 members) lists exactly the members the body reads, each typedRestServer['m']. The class keeps the private method with its name, docblock and position; its body is now a host literal plusregisterEmailEndpoints.call(host, basePath). Methods go as late-bound closures,(...a) => this.m(...a). The call site inregisterRoutes'registerForBase(this.registerEmailEndpoints(bp);,:4806at base, unconditional per base) is unchanged.Fields, checked one by one:
routeManagerregistercall, a top-level statement of the body):5175/:5179at base) wrapsregisterMetadataEndpointsInner, not this registraremailServiceProvider:2173), but notreadonlyThe getter is measured, not argued. A one-off vitest file (copied in, run, deleted) boots a source
RestServerwith no email provider, drivesPOST /api/v1/email/send, assigns a provider on the instance AFTERregisterRoutes(), and drives it again:501then200(MEASURE provider-after-registration: 501 -> 200), the late provider'ssendcalled once. A second case setsresolveExecCtxand a spy onenforceAuthAFTERregisterRoutes(): both are the ones the door calls (sentBy: 'late-user'stamped).Tests 3 passed (3).scripts/ablation-replace.mjs: the literal'sget emailServiceProvider() { return readEmailServiceProvider(); },replaced byemailServiceProvider: this.emailServiceProvider,(anchor 1 → 0, replacement 0 → 1, blob2ba4159180→aad755d1ef). The door freezes at registration:expected … [ 200 ], received501.Tests 1 failed | 2 passed (3). Restored by the tool's trap: blob2ba4159180equalsHEAD,git diff HEADempty. Source path (relative import), so nodistleg applies.rest-server.tsrestored into the tree with a trap, thengit checkout HEADand blob/diff proof):501 -> 200,Tests 3 passed (3), the same answers as the head.Proof of move-only
.d.ts:packages/rest/dist/index.d.tsandindex.d.ctsbuilt at basef7e836da1e(this worktree, before any edit), at the move commit679fe5b05and at the merge head35f207b70(and again after the whole-workspace build) are byte-identical: sha2567489de84bc6feccb7cb1f28bcbe4a97775324959c61c9bfc35e8a8c45a6f3bc9for all files,cmpsilent. The head bundles carry a head-only marker (readEmailServiceProvider: 2 hits in each ofindex.jsandindex.cjs, 0 at base), so the declaration was rebuilt from the moved source, not replayed.EmailRouteHost,email-routesandseamOrUndefinedappear 0 times in the.d.ts;src/index.tsis untouched.b3246be6a1718700…on all four). No email orseamOrUndefinedname is among them.RestServeron a recording mock server, with a protocol that answers every member, dumped under slice 7's seven configs: default; scoped-optional; scoped-required; every provider set;enableDiscovery: false;enableOpenApi: false; and both off under scoped-optional. Each dump holdsgetRoutes()(functions as arity), the ordered server registration calls, and a whitespace-free sha256 of every handler's source plus the source itself.'[REST] Email send unhandled error:'→…error!', throughablation-replace.mjs; anchor 1 → 0, blob30a8f5a0c8→78879bb810). Routes and calls were unchanged in all seven configs, and the handler digests differed in exactly the email door of each mounted base (POST /api/v1/email/send, andPOST /api/v1/environments/:environmentId/email/sendwhere scoped), nothing else. Restored, blob30a8f5a0c8equalsHEAD.rest-server.tslines 9083-9176, one 4-space indent removed, againstemail-routes.tslines 70-163:diffexits 0 (94 lines). The body has no whitespace-only line, no trailing whitespace and no template literal spanning lines.git diff --color-moved=plain --color-moved-ws=allow-indentation-changebase →679fe5b05over the two files: all 94 deleted body lines are detected as moved; the one other deleted line isseamOrUndefined's declaration gainingexport.rest-server.tsis +15 / −95 there: the stub body (host literal plus.call, +12 / −94), the module's import and its comment (+2), and theexport(+1 / −1).Readers re-pointed (sums unchanged, no gate loosened)
packages/rest/src/rest-server-registrar-files.tsgainsemail-routes.tswith a[#23050 slice 8]comment (length 8 → 9). 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:slotsemail-routes.ts: 0, length 8 → 9meta-draft-read-door-census.test.tsrest-server-docblock-position.test.tsemail-routes.ts: 3(header, host interface, function; the body held no TSDoc block);rest-server.tskeeps 164 blocks, floor 100; length 8 → 9execctx-consumer-census.test.tsrest-server.ts, interface member ×2 in the module);rest-server.ts57 → 58 (one site out, two forwarder mentions in). Forwarders 10 = (9 − 1) + 3 − 1; neither map gains an entry, because the module carries one registrar and it reads the seamui-view-route-identity.measurement.test.ts/ui-view-route-tenancy.measurement.test.tsthis.enforceAuth(57,this.resolveExecCtx(72 over the list (56 / 71 before, +1 each for the email literal's forwarders); ownership guard 1authz-conformance.test.ts(realtime tripwires)authz-probe-blind-spot.census.ts,rest-server.tsrowthis.routeManager.register(63 = 22 + 9 + 5 + 7 + 8 + 1 + 6 + 4 + 1;private register*Endpoints(16email-routes.ts: this.routeManager.register(1;registrar list length9;enforceAuth96 → 100 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 moduleReaders that read
rest-server.tsby path outside the list:objectql-slot-consumer-census.test.ts: its §1 controls useemailServiceProvider, the sibling slot, as their known-present read and as the negative for the window predicate. Those reads moved:readSitesover baserest-server.tsfound:9093,:9122(invocation:9122, wrapped byseamOrUndefined(); at the move commitrest-server.tsholds only the host literal's getter line, sowrappedIn('emailServiceProvider', 'seamOrUndefined(')read 0 and the test went red. The controls now reademail-routes.ts(EMAIL_SOURCE): reads:80,:109, invocation:109;seamOrUndefined(1,wiredEngineOrLoud(0, as before. TheobjectQLProvidercensus itself (3 consumers overSLOT_FILES) is unchanged, andSLOT_FILESis not widened (the module holds noobjectQLProvidersite).scripts/check-route-envelope.mjs(a gate's target paths,domain:spec):email-routes.tsis discovered by the*-routes.tsconvention and declareddialectOnlywith ratchet [tracking] Envelope-position convergence line for packages/rest's flat dialect — the live ratchet owner #9559, as the seven earlier modules are, at its measuredstringError 1(the non-object body's 400INVALID_REQUEST,'JSON body required') andsiblingCode 3(INVALID_REQUEST,VALIDATION_FAILED,EMAIL_SEND_FAILED); the two 501{ code, message }bodies have noerrorkey and are counted by neither.rest-server.tsgoes 17 → 16 and 17 → 14, so the sums 16 + 13 + 8 + 1 + 0 + 0 + 0 + 0 + 1 = 39 and 14 + 15 + 8 + 9 + 4 + 1 + 0 + 0 + 3 = 54 are unchanged (the gate compares both counts exactly, in both directions). Output differs from base only in those two rows (18 → 19 modules, 9 → 10 ratcheted).--self-testgreen. Each history entry says "NOT progress — the same MOVE", as slices 1–3 wrote.docs/qa/platform-checklist/areas/integration-system.json(domain:devx): the email item's source entrypackages/rest/src/rest-server.ts#registerEmailEndpoints (… enforceAuth 401, non-object→400 INVALID_REQUEST, VALIDATION_FAILED passthrough, 501 no-provider, 500 EMAIL_SEND_FAILED)lists the body's answers, so it now namespackages/rest/src/email-routes.ts#registerEmailEndpoints; the item'sverifyprose said "(rest-server.ts registerEmailEndpoints: …)" for the same answers and now saysemail-routes.ts.check:platform-checklist: symbol anchors 735/745 → 735/745 resolved (the re-pointed anchor resolves), cited sources 347 → 348.ADR-NNNN Dk, andcheck-adr-anchorsreads 1764 decision-letter citations at base and at head.Red before the re-points, each measured with the move in place (
679fe5b05): the sevenlocalreaders readTest Files 7 failed (7),Tests 8 failed | 121 passed (129)(six length pins, the docblock pin's unfloored new module, and theobjectQLProvidercensus'sseamOrUndefined(control at 0).rest-server-canonical-query-ast.test.ts(repo) readTests 2 failed | 61 passed | 9 skipped (72). The dogfood census readcontrol "registrar list length" moved … expected 9 to be 8;authz-conformance.test.tscould not load at that point (@objectstack/verifywas not built yet), so its length pin's red was not observed, only its green after the re-point.tsc --noEmitwas clean, butcheck:test-typecheckfound 2 type errors, the two typed floor tables without the new module.check:route-envelopereademail-routes.ts NOT DECLARED(MODULES and EXPRESS_RESPONSE_MODULES) andrest-server.ts16 vs 17 / 14 vs 17.Readers examined and not re-pointed
packages/spec(stopped at, per the dispatch, not edited):packages/spec/src/api/discovery.zod.ts(about:396) says "the REST server'sregisterEmailEndpointsmounts undergetApiBasePath()". It names the method, not a file, and the method still registers the door (through the module), so it stays true. Nopackages/spec/livenessrow cites the email registrar or a moved line, andcheck:livenessoutput is identical at base and head.rest-server.ts'getMountedEmailRouteBasedocblock namesregisterEmailEndpointsas the recorder of the send route. It ships in the published.d.ts(a public method), and it stays true. Not edited.packages/rest/src/discovery-routes.ts(about:304),discovery-advertised-direct-mounts.parity.test.ts,packages/metadata-protocol/src/discovery-schema-conformance.test.ts,rest.test.ts's email describe block,rest-api-plugin.ts's provider comment.meta-state-route-engine-outage.test.ts§5 says "POST /email/sendcarried the other.catch(() => undefined)in this file": dated repair history aboutrest-server.ts; the test drives the door over HTTP and reads no source. Not edited.packages/rest/CHANGELOG.md(released text) anddocs/audits/2026-07-dispatcher-client-route-coverage.md(a dated audit namingPOST /email/send, no file or line): not edited.rest-server.ts:NNNNline citation in the tree falls inside the moved range (:9063-:9177); the highest cited line is:5872. As with every earlier slice's import, every line after the new import (:342) moved down by 2 (and every line after the stub by 2 − 80); none of those citations is in a gated document (check:platform-checklistreports 0 surviving line citations;check:scripts-symbol-anchors0 line anchors on tracked targets).REST_ROUTE_LEDGERnamesPOST /api/v1/email/sendby route, not by file: unchanged.Acceptance notes
this.emailServiceProvider!(environmentId); through the host that call's receiver is the host literal, where at base it was theRestServer(the one-off file's third case recordsreceiver === server:trueat base,falseat head). Only a provider written as a non-arrowfunctionthat readsthiscould observe it. The shipped provider (rest-api-plugin.ts, about:406) is anasyncarrow, and the constructor parameter's type declares nothis. Every earlier slice that moved a provider call through a getter (objectQLProvider,approvalsServiceProvider, the two sharing providers,analyticsServiceProvider,securityServiceProvider) has the same property. Recorded, not changed: a bound hand-over would be a new host shape, which this slice may not introduce.[#15405]comment's references ("theif (!this.emailServiceProvider)guard above", "rest-api-plugin.tshands over") still read true in the module.Gates and tests
Measured at this branch: base
f7e836da1e, the move679fe5b05, the re-points626eb4c15, and one merge oforigin/main6422da58e8(35f207b70: one commit, #23854, inpackages/speccontracts andservice-automation; 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:35f207b70.pnpm install --frozen-lockfileexit 0;pnpm --filter '@objectstack/rest...' --filter '@objectstack/verify...' buildexit 0; thenpnpm turbo run build --concurrency=2, the whole workspace, exit 0 (76 tasks, 74 cached), 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 286 passed (286), Tests 5892 passed / 326 skipped (6218).pnpm --filter @objectstack/rest exec vitest run --project repo --maxWorkers=2: Test Files 5 passed (5), Tests 242 passed / 9 skipped (251).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).node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, no paths, at35f207b70(stderr names that commit; the--repoassertion holds; 15 paths against merge base6422da58e): 80 commands, all run at35f207b70. 79 exited 0 on the first pass;pnpm check:dual-build-cjs-loadsexited 3 (PREREQUISITE NOT MET: nodistfor 18 packages) and exited 0 after the whole-workspace build above.--ran:Run reconciliation — 80 derived, 80 run, 0 NOT-MEASURED, 0 UNRUN.Among them:pnpm check:route-envelope,pnpm check:platform-checklist,pnpm --filter @objectstack/spec run check:liveness,pnpm check:durability-log-level,pnpm check:dispatcher-error-vocabulary,pnpm check:dts-closure,pnpm check:published-files,pnpm check:cross-package-test-inputs,pnpm check:nul-bytesandpnpm check:pm-dispatch-gates.check-adr-anchors(not derived) also run: exit 0.f7e836da1evs head35f207b70, diffed: identical forcheck-single-authz-resolver,check-system-context-census,check-rest-log-declared,check:rest-log-spy-declared,check-durability-degradation-log-level,check-filter-alias-parity,check-adr-symbol-anchorsandcheck:liveness. Differing only by this diff's new file:check-meta-type-normalized(36 → 37 files),check-affected-docs(route sources 26 → 27, registration call sites 21 → 22, by the*-routes.tsconvention, no edit),check-dispatcher-error-vocabularyandcheck-error-status-conformance(+1 scanned file each; 57 unregistered sites and 465 producer sites unchanged),check-undeclared-dep-imports(+1 file),check-route-envelope(the two rows above),check-scripts-symbol-anchors(+4 file-level anchors, the gate's new comments) andcheck:platform-checklist(348 cited sources).check-adr-anchorsreads 1764 decision-letter citations at both; its +2 citations / +1 file andcheck-undeclared-dep-imports' +1 specifier aremain's (feat(spec,automation): FlowRuntimeState.unboundCause classifies an unbound flow row beside its reason #23854's changeset andengine.tsimport).pnpm exec eslint --no-inline-config --format jsonover the 14 changed code files: 0 errors, 0 warnings (the 15th changed file is the checklist JSON, whicheslint .never reads). 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..d.ts, export lists and routes above).skip-changesetis the seat's to apply on its own check.Cross-lane files in this diff:
scripts/check-route-envelope.mjs(domain:spec, a gate's target paths: one new MODULES entry and therest-server.tsrow's two counts, sums unchanged) anddocs/qa/platform-checklist/areas/integration-system.json(domain:devx, one source anchor and oneverifyphrase). The dogfood files (packages/qa/dogfood/test/authz-conformance.test.ts,authz-probe-blind-spot.census.ts) are re-pointed as in slices 3–7.check-governed-merges --testover the 15 paths: NOT governed; 426 changed lines.Generated by Claude Code