[AZ-486] F7 endpoint builders + STC-ARCH-02 (cycle 1 close)

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>
This commit is contained in:
Oleksandr Bezdieniezhnykh
2026-05-11 23:03:45 +03:00
parent 23746ec61d
commit 8a461a2051
23 changed files with 777 additions and 127 deletions
@@ -1,158 +0,0 @@
# Endpoint builders — replace hardcoded `/api/<service>/...` strings
**Task**: AZ-486_refactor_endpoint_builders
**Name**: Introduce `endpoints.ts` and replace hardcoded API paths
**Description**: Add `src/api/endpoints.ts` exporting typed endpoint builders, replace every hardcoded `/api/<service>/...` string literal in production code with the corresponding builder call, and add a static check that flags new string literals. Closes architecture baseline finding **F7**.
**Complexity**: 5 points
**Dependencies**: AZ-485_refactor_public_api_barrels (F4 lands first so `endpoints` ships through the `src/api` barrel)
**Component**: `01_api-transport` (owner of new file + barrel re-export) + every component that calls `api.*` or `createSSE`: `02_auth`, `03_shared-ui`, `06_annotations`, `07_dataset`, `08_admin`, `09_settings`, `05_flights`
**Tracker**: AZ-486
**Epic**: AZ-447
## Problem
`_docs/02_document/architecture_compliance_baseline.md` Finding **F7** (Medium / Architecture): every `api.*()` and `createSSE()` callsite repeats `/api/<service>/<path>` as a string literal. ~25 hardcoded paths across 11 source files (`src/auth/AuthContext.tsx`, `src/api/client.ts`, `src/features/{admin,settings,annotations,dataset,flights}/**`, `src/components/{FlightContext,DetectionClasses}.tsx`).
Consequences (per ADR-006 Consequences and the baseline doc):
1. Every test fixture must duplicate paths — and MSW handlers, e2e stubs, and unit tests all drift independently.
2. Any nginx-route rename (ADR-006 prefix-strip changes) touches every feature.
3. There is no single source of truth for the wire-contract paths.
`module-layout.md` Verification Needed item references the same observation. Step 4 (testability) deferred this finding to Phase B per the per-finding routing decision.
## Outcome
- A new module `src/api/endpoints.ts` exports a typed `endpoints` object with function-form builders for every path in use today.
- Every callsite of `api.get/post/put/upload/del` and `subscribeSSE`/`createSSE` across `src/**` (excluding `src/api/endpoints.ts` itself and test files) uses an `endpoints.*` call — no string literals matching `/api/<service>/` remain in production code.
- The `endpoints` symbol is re-exported from `src/api/index.ts` (the F4 barrel).
- A new static check `STC-ARCH-02` fails the static profile if any production file (excluding `endpoints.ts`, tests, and MSW handlers) contains a string literal matching `/api/<service>/`.
- Unit tests assert each builder returns the contract-correct URL string.
- MSW handlers and e2e stubs continue to match the exact same URLs — no wire-contract change.
- `_docs/02_document/module-layout.md` adds `endpoints.ts` to the `01_api-transport` Public API and adds `STC-ARCH-02` to the static-check inventory.
## Scope
### Included
- New file `src/api/endpoints.ts` with the `endpoints` object — function form everywhere, e.g.:
- `endpoints.admin.authRefresh()``'/api/admin/auth/refresh'`
- `endpoints.admin.users()``'/api/admin/users'`
- `endpoints.admin.user(id)` → `` `/api/admin/users/${id}` ``
- `endpoints.flights.aircrafts()``'/api/flights/aircrafts'`
- `endpoints.flights.liveGps(flightId)` → `` `/api/flights/${flightId}/live-gps` ``
- `endpoints.annotations.classes()`, `endpoints.annotations.annotations()`, `endpoints.annotations.dataset()`, `endpoints.annotations.datasetBulkStatus()`, `endpoints.annotations.datasetClassDistribution()`, `endpoints.annotations.mediaBatch()`, `endpoints.annotations.settingsSystem()`, `endpoints.annotations.settingsDirectories()`, `endpoints.annotations.settingsUser()`, `endpoints.annotations.detection(query?)`, …
- Update `src/api/index.ts` (barrel from F4) to re-export `endpoints`.
- Replace ~25 hardcoded path literals in:
- `src/auth/AuthContext.tsx`
- `src/api/client.ts` (the refresh callsite)
- `src/features/admin/AdminPage.tsx`
- `src/features/settings/SettingsPage.tsx`
- `src/features/annotations/AnnotationsPage.tsx`
- `src/features/annotations/AnnotationsSidebar.tsx`
- `src/features/annotations/MediaList.tsx`
- `src/features/dataset/DatasetPage.tsx`
- `src/features/flights/FlightsPage.tsx`
- `src/components/FlightContext.tsx`
- `src/components/DetectionClasses.tsx`
- Add unit tests in `src/api/endpoints.test.ts` (one assertion per builder verifying the literal URL string — the test file IS the contract).
- Add static check `STC-ARCH-02` to `scripts/run-tests.sh` (ripgrep `'/api/[a-z-]+/'` across `src/**` excluding `endpoints.ts` and `*.test.{ts,tsx}` and `tests/**`).
- Update `_docs/02_document/module-layout.md` `01_api-transport` row to add `endpoints` to Public API and add `STC-ARCH-02` to the static-check inventory.
### Excluded
- F6 (introduce `src/shared/`) — `endpoints.ts` lives at `src/api/endpoints.ts` for now (under `01_api-transport`). When/if F6 lands later it can move to `src/shared/endpoints.ts` with no callsite change (barrel insulates callers).
- The base URL itself (`/api`) — `getApiBase()` already exists in `src/api/client.ts` and is handled separately. `endpoints.ts` returns paths starting with `/api/`; the client prepends the base.
- Tests and MSW handlers — tests CAN use `endpoints.*` for readability, but their hardcoded paths are not in scope of this task's deletion sweep. The static check explicitly exempts test paths.
- `mission-planner/**` — untouched (deferred per F1).
- Any change to wire-contract paths. The literal URL strings produced by builders MUST exactly match the strings currently in code (and exactly match what MSW/e2e stubs intercept today).
## Acceptance Criteria
**AC-1: All current paths have builders**
Given the post-change `src/api/endpoints.ts`,
When the unit test enumerates every builder and asserts the produced URL,
Then every URL currently in source (per the F7 inventory above) is reproduced exactly — character-identical to today's literal.
**AC-2: No hardcoded `/api/<service>/` literals remain in production**
Given the post-change repo,
When `ripgrep "'/api/[a-z-]+/"` runs over `src/**` excluding `src/api/endpoints.ts`, `**/*.test.{ts,tsx}`, and `tests/**`,
Then zero matches are found.
**AC-3: Static gate STC-ARCH-02 fails on a synthetic literal**
Given the post-change static profile,
When a synthetic edit reintroduces `await api.get('/api/admin/users/me')` to any production file,
Then `bash scripts/run-tests.sh --static` exits non-zero with `STC-ARCH-02` named in the failure line.
**AC-4: Static gate STC-ARCH-02 passes on the migrated codebase**
Given the post-change repo,
When `bash scripts/run-tests.sh --static` runs,
Then it exits zero and the static report shows `STC-ARCH-02` as PASS.
**AC-5: Fast profile remains green**
Given the post-change repo,
When `bash scripts/run-tests.sh --fast` runs,
Then it reports the same PASS / SKIP / FAIL counts as the pre-change baseline (163 PASS / 13 SKIP / 0 FAIL plus the new `endpoints.test.ts` PASSes) with zero new failures and zero regressions.
**AC-6: Endpoint builders are exposed through the F4 barrel**
Given the post-change repo,
When any production file imports `{ endpoints }` from `'../api'` (or relative equivalent),
Then the import resolves through `src/api/index.ts` and `endpoints` is the typed object defined in `src/api/endpoints.ts`.
**AC-7: MSW handlers and e2e stubs continue to match**
Given the post-change repo,
When the fast and (deferred-but-runnable) e2e profiles run,
Then every MSW intercept hits its target unchanged — no "intercepted a request without a matching request handler" error appears, confirming character-identical URLs.
## Non-Functional Requirements
**Performance**
- Initial JS bundle gzipped size MUST remain ≤ 2 MB (existing `STC-PERF01`). The `endpoints` object is tree-shakeable per builder; impact ≤ 1 KB.
**Maintainability**
- A nginx-route rename (per ADR-006) requires editing one file (`endpoints.ts`) — validated by AC-2.
**Compatibility**
- Zero wire-contract change (validated by AC-1 character-equality + AC-7 MSW + e2e).
## Unit Tests
| AC Ref | What to Test | Required Outcome |
|--------|--------------|------------------|
| AC-1 | Every builder produces the contract-correct URL string | `endpoints.admin.authRefresh()` === `'/api/admin/auth/refresh'`; same for every builder, character-identical |
| AC-1 | Builders that take params interpolate correctly | `endpoints.admin.user('abc')` === `'/api/admin/users/abc'` |
| AC-3 | STC-ARCH-02 fails on synthetic deep-literal | Static profile non-zero, error names `STC-ARCH-02` |
| AC-4 | STC-ARCH-02 passes on migrated codebase | Static profile zero, STC-ARCH-02 PASS row |
| AC-6 | `endpoints` is re-exported from `src/api/index.ts` | `import { endpoints } from 'src/api'` resolves; the imported value is identical to the one in `src/api/endpoints.ts` |
## Blackbox Tests
| AC Ref | Initial Data/Conditions | What to Test | Expected Behavior | NFR References |
|--------|-------------------------|--------------|-------------------|----------------|
| AC-2 | Repo after migration | `ripgrep "'/api/[a-z-]+/"` over `src/` minus exemptions | Zero matches | Maintainability |
| AC-5 | Fast profile | `scripts/run-tests.sh --fast` | 163 PASS + new `endpoints.test.ts` PASSes / 13 SKIP / 0 FAIL | Compat |
| AC-7 | Fast profile | MSW unhandled-request gate | No "intercepted a request without a matching request handler" errors | Compat |
## Constraints
- Lands AFTER 01_refactor_public_api_barrels (F4). The `endpoints` symbol is re-exported from `src/api/index.ts` (the barrel); without F4, callsites would deep-import from `src/api/endpoints` and reintroduce the F4 violation.
- The literal URLs produced by builders MUST be character-identical to today's literals. AC-1 validates this in unit tests; AC-7 validates it against MSW handlers; the (deferred) e2e profile validates it against the suite-e2e nginx routes.
- All changes land in ONE commit (the static check would otherwise fail on intermediate commits).
- `mission-planner/**` MUST be untouched.
## Risks & Mitigation
**Risk 1: A path literal is missed and remains in source**
- *Risk*: 25 sites is enough for a manual edit to miss one. The miss would not show up in fast tests (MSW intercepts both styles); STC-ARCH-02 is the only gate that catches it.
- *Mitigation*: STC-ARCH-02 is the SINGLE source of truth for "no literals remain". The static profile is run BEFORE commit; commit is blocked if STC-ARCH-02 fails.
**Risk 2: An optional query-string param is missed in the builder API**
- *Risk*: e.g. `endpoints.annotations.detection()` may need to accept an optional `imageId` query string; missing the param forces the caller back to string concatenation, defeating the abstraction.
- *Mitigation*: Inventory the existing callsites BEFORE writing builders. Every callsite's full URL shape (path + query) must map cleanly to one builder. Document the inventory in the batch report.
**Risk 3: F6 lands later and `endpoints.ts` needs to move to `src/shared/endpoints.ts`**
- *Risk*: A future F6 task may move the file.
- *Mitigation*: Acceptable. Callers import from the `src/api` barrel (or whatever barrel ends up re-exporting `endpoints` after the move). A single barrel edit re-routes all consumers. This is exactly the benefit F4 was meant to provide.
## Contract
This task produces the wire-path contract for the UI ↔ nginx layer. The contract surface IS the `endpoints` object as exported from `src/api/endpoints.ts`. The accompanying unit test (`src/api/endpoints.test.ts`) asserts every URL string and serves as the contract documentation — any future path change MUST update both the builder and the test in the same commit.
A standalone contract file at `_docs/02_document/contracts/api-transport/endpoints.md` MAY be added in a follow-up task; for this task the test file is the authoritative contract per `module-layout.md`'s "code-derived documentation" pattern.