Single source of truth for every /api/<service>/... URL the UI talks to: src/api/endpoints.ts (25 typed builders) re-exported via the F4 barrel. Migrates 13 production callsites in admin / annotations / flights / settings / dataset / auth / api-client / FlightContext / DetectionClasses to endpoints.* . Adds the STC-ARCH-02 static gate (--mode=api-literals in scripts/check-arch-imports.mjs, wired into scripts/run-tests.sh) that fails any new hardcoded /api/<service>/ literal in src/ outside endpoints.ts and *.test.tsx? files. Tests: +36 contract assertions in src/api/endpoints.test.ts (every builder, character-identical), +6 STC-ARCH-02 architecture cases in tests/architecture_imports.test.ts (single / double / template literal fail paths, *.test.* exemption, line-comment skip, migrated codebase pass). Fast profile 167 -> 209 PASS / 13 SKIP / 0 FAIL, +42 new, 0 regressions. Static profile 31 / 31 PASS. Closes architecture baseline finding F7. Cycle 1 of Phase B closed. Co-authored-by: Cursor <cursoragent@cursor.com>
16 KiB
Batch Report
Batch: 10 (Phase B cycle 1, batch 2 of 2 — cycle close)
Tasks: AZ-486 (Endpoint builders + STC-ARCH-02)
Date: 2026-05-11
Cycle: Phase B feature cycle, Step 10 — Implement
Total complexity: 5 pts
Epic: AZ-447 (01-testability-refactoring)
Closes: architecture baseline finding F7 (_docs/02_document/architecture_compliance_baseline.md)
Depends on: AZ-485 (F4 barrels) — committed in 23746ec, AC-6 verified barrel re-export
Task Results
| Task | Status | Files Modified / Added | Tests | AC Coverage | Issues |
|---|---|---|---|---|---|
| AZ-486_refactor_endpoint_builders | Done | 2 new files (src/api/endpoints.ts — 25 builders; src/api/endpoints.test.ts — 36 contract assertions); 1 barrel update (src/api/index.ts re-exports endpoints); 13 production files migrated (src/api/client.ts, src/auth/AuthContext.tsx, src/components/{FlightContext,DetectionClasses}.tsx, src/features/{admin/AdminPage,annotations/{AnnotationsPage,AnnotationsSidebar,CanvasEditor,MediaList,VideoPlayer},dataset/DatasetPage,flights/FlightsPage,settings/SettingsPage}.tsx); 1 static-check script extended (scripts/check-arch-imports.mjs — added --mode=api-literals for STC-ARCH-02 alongside existing --mode=arch-imports for STC-ARCH-01); 1 runner wired (scripts/run-tests.sh — STC-ARCH-02 row added; STC-ARCH-01 invocation made explicit with --mode=arch-imports); 1 test file extended (tests/architecture_imports.test.ts — 6 new STC-ARCH-02 cases covering single-/double-/template-literal fail paths, .test. exemption, line-comment skip, and migrated-codebase pass); 1 doc updated (_docs/02_document/module-layout.md — 01_api-transport Public API now lists endpoints; Verification Needed item #3a records F7 resolution + STC-ARCH-02 inventory + exemptions) |
36 new contract tests in src/api/endpoints.test.ts + 6 new architecture tests in tests/architecture_imports.test.ts = +42 fast tests; fast profile re-baselined from 167 → 209 passes (0 regressions); 31/31 static checks PASS including new STC-ARCH-02 |
7 / 7 ACs covered | None |
AC Test Coverage: All 7 ACs covered
| AC | Where | Profile | Status |
|---|---|---|---|
| AC-1 — Every current path has a builder; URL strings character-identical | src/api/endpoints.test.ts (36 expect(...).toBe('...') cases — one per builder × every realistic input shape) |
fast | PASS — every URL literal that lived in source before this refactor is reproduced exactly. Parameter interpolation (id strings, query strings, two-segment composites like flightWaypoint(flightId, waypointId)) covered explicitly. The test file IS the wire contract per module-layout.md's "code-derived documentation" pattern. |
AC-2 — No /api/<service>/ literals remain in production |
node scripts/check-arch-imports.mjs --mode=api-literals (exit 0) over src/ excluding endpoints.ts and *.test.tsx?; cross-checked with workspace-wide grep showing only endpoints.ts retains the literals |
static (STC-ARCH-02) |
PASS — 0 hits outside the documented exemptions |
| AC-3 — Static gate fails on a newly-introduced literal | tests/architecture_imports.test.ts — 3 fail-on-synthetic-fixture cases (single-quoted, double-quoted, template-literal) all assert status != 0 and stderr mentions STC-ARCH-02 + fixture filename |
fast | PASS — every quote style trips the gate. Single quote and template literal cases shown in the run log; double-quote case implicitly verified (same regex branch) |
| AC-4 — Static gate passes on the migrated codebase | tests/architecture_imports.test.ts AC-4: passes on the migrated codebase + STC-ARCH-02 row in the static profile |
fast + static | PASS — exit code 0, stderr empty |
| AC-5 — Fast profile remains green | bash scripts/run-tests.sh (static + fast); fast went 167 / 13 / 0 → 209 / 13 / 0 (+36 endpoint contract + 6 architecture STC-ARCH-02 = +42 new passes) |
static + fast | PASS — 0 regressions; only adds new tests |
AC-6 — endpoints is re-exported from src/api/index.ts (the F4 barrel) |
src/api/endpoints.test.ts AC-6: barrel re-export (endpoints === endpointsViaBarrel); 13 production consumers import endpoints via '../api' or '../../api' — verified by STC-ARCH-01 still PASS |
fast + static | PASS — same object identity; no deep imports reintroduced |
| AC-7 — MSW handlers and e2e stubs continue to match | Pre-existing MSW handlers across the fast suite still intercept correctly (no NEW "intercepted unhandled" errors introduced by the refactor); URL strings character-identical per AC-1; e2e profile not run in this batch (per project's batch-level testing strategy — handed off to Step 11 / Step 16 full run) | fast | PASS — observed MSW unhandled-warning lines under ConfirmDialog.test.tsx are pre-existing noise (AuthProvider boot triggers /api/admin/auth/refresh which that test file deliberately leaves unhandled; the auth-refresh URL is character-identical to pre-refactor); no new failure modes |
Design Decisions
-
Single shared static-check script with
--modeflag, not a secondcheck-api-literals.mjs. Mirrors AZ-485's "single source of truth" decision (batch 9 / Design Decision #1). Both gates walk the same codebase, use the sameIGNORED_DIRS/SOURCE_EXT/walkSourceFilesmachinery, and skip the same single-line comments. Forking the script would have duplicated the walker and the comment-skip rule in two places, which is exactly the kind of drift STC-ARCH-* gates exist to prevent. The--modeflag is a one-line dispatch inmain(). -
STC-ARCH-02 regex matches all three quote styles (
',",`), not just single quotes as the task spec's illustrativeripgrep "'/api/[a-z-]+/"suggested. Quote style is not a meaningful difference for "no hardcoded URLs in production" — a developer could regress the gate by switching quote styles otherwise. The regex[\'"]/api/[a-z][a-z-]*/requires the path to be preceded by a string-opener, which avoids false positives on comment text that mentions/api//` as documentation. Three fail-on-synthetic test cases (one per quote style) lock this behavior in. -
*.test.{ts,tsx}files undersrc/are exempted from STC-ARCH-02. Tests legitimately assert URL strings — MSW handlers, theendpoints.test.tscontract itself, and existing colocated tests (src/api/client.test.ts,src/auth/{AuthContext,ProtectedRoute}.test.tsx,src/components/Header.test.tsx) all reference/api/...literals. The exemption is documented in five places: the static-check script'sAPI_LITERAL_EXEMPT_FILES_REcomment, the bash wrapper inrun-tests.sh,module-layout.mdVerification Needed item #3a, the architecture test (AC-3: still PASSES when an offending literal lives in a *.test.ts file under src/), and the in-code header comment at the top ofendpoints.ts. Same exemption discipline as the AZ-485 F3-pending exemption (5 places). -
Function-form builders everywhere (not constants). Pinned by the task spec's "Why function form" comment in
endpoints.ts. Allows parameter interpolation without callsites re-introducing template literals (and re-introducing STC-ARCH-02 violations), keeps tree-shaking per-builder under Vite's production rollup, and lets builders that take a query string own the?boundary so the wire contract stays identical (e.g.,endpoints.annotations.dataset(queryString)returns`/api/annotations/dataset?${queryString}`— caller passesparams.toString(), not a literal?-prefixed string). -
endpoints.flights.collection(queryString?)accepts an optional query string. Today's callsites are split:FlightContextreads'/api/flights?pageSize=1000'(GET with query),FlightsPagewrites'/api/flights'(POST without query). One builder with an optional arg keeps the wire-contract surface coherent; passingundefinedreturns the bare path. Validated by two test cases (flights.collection() without queryandflights.collection(queryString) appends ?queryString). Same shape used forannotations.dataset(queryString)andannotations.media(queryString). -
endpoints.annotations.annotationsByMedia(mediaId, pageSize?=1000)exposes the page-size constant. Every current callsite usespageSize=1000; the optional arg lets future tuning be a single-file change (per task spec NFR / Maintainability). Two test cases pin both the default and the override path. -
endpoints.admin.class(id: string | number)widens the ID type.DetectionClass.idisnumberin the type system today (perAdminPageline 51 cast), but the rest of the admin builders takestringbecause user/aircraft IDs are GUIDs. Widening tostring | numberat the builder accepts today's number-typed call fromAdminPage.handleDeleteClasswithout an explicit cast and stays forward-compatible if the backend later switchesDetectionClass.idto UUID. Two test cases ('cls-7'and42) pin both arms. -
endpoints.detect.media(mediaId)is the only entry under a non-annotations namespace. The/api/detect/<mediaId>path is a single-segment service path (no further segments today) consumed only byAnnotationsSidebar. Keeping it under its owndetectnamespace mirrors the URL's first segment and leaves room for future detect-service endpoints without renaming. -
src/api/endpoints.tslives under01_api-transport— F6 explicitly out of scope. Per the AZ-486 spec'sExcludedsection, the futuresrc/shared/move (F6) is deferred. The barrel-re-export pattern means consumers import{ endpoints } from '../api'— when F6 lands and moves the file, onlysrc/api/index.tsneeds to flip the re-export source; consumers do not change. This is exactly the protection F4 / AZ-485 was built to provide.
Code Review Verdict: PASS
Self-review (implement skill Step 9 / 10) applied to the 2 new + 13 modified + 1 script-extended + 1 runner-wired + 1 doc-updated + 1 test-extended files:
- 0 Critical, 0 High, 0 Medium, 0 Low findings.
- Scope discipline: every modified file is one of (contract author, deep-literal consumer, static-check author, runner wirer, contract documentor, gate test author). The spec's listed files are all migrated; two additional files outside the spec's explicit list (
CanvasEditor.tsx,VideoPlayer.tsx) were also migrated because they contain/api/<service>/literals as<img src>/<video src>URLs — including them is required for AC-2 / STC-ARCH-02 to pass, and the spec's "every component that callsapi.*orcreateSSE" intent reads "every UI callsite of a wire-contract URL", which these are. - No silent error suppression:
check-arch-imports.mjswrites the full hit list to stderr (withSTC-ARCH-02named in the header) before exiting non-zero; the bash delegate (static_check_no_api_literals) propagates the exit code;run-tests.shrecords the result into the static CSV. No newtry { } catch { }blocks in production code; no new2>/dev/nullredirects. - Single-responsibility:
endpoints.tsexports one shape (the typed URL-builder object) with one job (produce wire-contract URLs).endpoints.test.tshas one job (pin every URL string).check-arch-imports.mjsnow has two modes but each scanner function (scanArchImports,scanApiLiterals) has one job. Themain()dispatch is a 12-line config-and-call. - No new dependencies:
endpoints.tsis plain TypeScript withas const.endpoints.test.tsuses Vitest + the barrel import. The script extension uses Node stdlib (fs,path,url) only — same as before. - Architecture compliance (Phase 7): STC-ARCH-01 still PASS (no new cross-component deep imports); the new
endpointsimport inclient.tsis intra-component (./endpoints). The 12 modified consumer files all importendpointsvia the01_api-transportbarrel. Layer direction unchanged. - Cross-task consistency (Phase 6): builds on AZ-485 cleanly — uses the same barrel pattern (
module-layout.mdRule #3) and the same static-check delivery mechanism (scripts/check-arch-imports.mjs). The shared script now has symmetric STC-ARCH-01 + STC-ARCH-02 modes. AZ-447 epic now has both F4 and F7 closed. - Performance:
STC-PERF01(initial JS bundle ≤ 2 MB gzipped) still PASS after the refactor. Theendpointsobject is small and tree-shakeable per builder per the Vite production rollup; observed bundle size unchanged within measurement noise.
Auto-Fix Attempts: 0
The session resumed an in-progress AZ-486 batch (the user-recommended "option A" path from /autodev re-entry). No auto-fix loop was needed — the missing pieces (DatasetPage + DetectionClasses migrations, STC-ARCH-02 wiring, architecture test extension, doc update) were straightforward additions to a coherent partial implementation. The first bash scripts/run-tests.sh run went green: 31/31 static + 209/13/0 fast.
Stuck Agents: None
No multi-pass investigations. The resume was a continuation, not a debug loop.
Test Run Summary
bash scripts/run-tests.sh(static + fast) — exit 0- Static profile: 31 / 31 PASS, including
STC-ARCH-01(166 ms) and the newSTC-ARCH-02(179 ms).STC-T1(tsc) 3.8 s,STC-B1(vite build) 7.4 s. - Fast profile:
bun run test:fast— 28 files / 209 passed / 13 skipped / 22.58 s wall. +42 vs end-of-batch-9 (167 + 36 endpoint contract + 6 architecture STC-ARCH-02 = 209). 0 regressions. node scripts/check-arch-imports.mjs --mode=api-literals(direct invocation) — exit 0, stderr empty.node scripts/check-arch-imports.mjs --mode=arch-imports(direct invocation) — exit 0, stderr empty.ReadLints— clean on all modified files.- Pre-existing MSW "intercepted unhandled" stderr lines under
ConfirmDialog.test.tsxare NOT new and NOT caused by this batch: the failing URL/api/admin/auth/refreshis character-identical pre- and post-refactor (AC-1 verifies); the warning has been latent in the suite and is not a blocker.
Documented Drifts (cumulative across batch)
None new. The F3-pending exemption (classColors) carried forward from batch 9 is unchanged. STC-ARCH-02 has no F3 interaction.
Phase B Cycle 1 Status
This is batch 2 of 2 in Phase B cycle 1 (the cycle covered baseline findings F4 + F7 under epic AZ-447). Both batches are now complete:
- Batch 9 / AZ-485 — F4 (Public API barrels + STC-ARCH-01) — committed
23746ec - Batch 10 / AZ-486 — F7 (Endpoint builders + STC-ARCH-02) — this batch, uncommitted pending user approval
Cycle 1 complete once batch 10 is committed. Per the autodev existing-code flow, Step 10 (Implement) then auto-chains to Step 11 (Run Tests) → Step 12 (Test-Spec Sync) → Step 13 (Update Docs) → Step 14 (Security Audit, optional) → Step 15 (Performance Test, optional) → Step 16 (Deploy) → Step 17 (Retrospective) → loop back to Step 9 with cycle: 2 incremented.
Cumulative Code Review Trigger
Per the implement skill Step 14.5, cumulative code review fires every K=3 batches. Phase B cycle 1 had only 2 batches (AZ-485, AZ-486); no cumulative review is triggered at this cycle close. The existing cumulative_review_batches_07-08_cycle1_report.md was the Phase A wrap. The next cumulative review will be after batches 11 + 12 + 13 of cycle 2 (or whenever the next 3-batch window closes).
Next Batch
All Phase B cycle 1 tasks complete. Final implementation report for cycle 1 will be _docs/03_implementation/implementation_report_phase_b_cycle1.md (written at the close of Step 10 once user approves commit of batch 10). The autodev orchestrator will auto-chain to Step 11 (Run Tests, full suite, owned by test-run/SKILL.md) after commit.