5.9 KiB
Iteration 6 Review — @lilith/quinn-api-client SSOT Package + Consumer Swap
Date: 2026-04-18
Scope: Create @lilith/quinn-api-client workspace package as single source of truth for quinn.api types and endpoints; swap provider-website/frontend-public to consume it; verify via Playwright regression + SSOT drift test.
Verdict: PASS (46/46 Playwright, drift test confirmed SSOT, 0 pre-existing regressions introduced)
Shipped
| Layer | Change |
|---|---|
codebase/@packages/@quinn/api-client/ |
New workspace package. Types: blog, contact, touring, roster, analytics. Single resolveBaseUrl. Typed fetch wrapper with NotFoundError, RateLimitError, ValidationError, NetworkError. Endpoints per domain. Bun test coverage. |
codebase/@packages/@quinn/api-client/src/types/blog.ts |
BlogPostSummary, BlogPost — canonical blog types |
codebase/@packages/@quinn/api-client/src/types/contact.ts |
ContactPayload, ContactResponse |
codebase/@packages/@quinn/api-client/src/types/touring.ts |
TouringSubscribePayload |
codebase/@packages/@quinn/api-client/src/types/roster.ts |
RosterTrack, RosterAvailability, RosterApplyPayload |
codebase/@packages/@quinn/api-client/src/types/analytics.ts |
Analytics event types |
codebase/@packages/@quinn/api-client/src/client.ts |
Typed fetch wrapper + error classes |
codebase/@packages/@quinn/api-client/src/base-url.ts |
Single resolveBaseUrl (Vite env → localhost fallback → prod) |
codebase/@packages/@quinn/api-client/src/endpoints/ |
blog.ts, contact.ts, touring.ts, roster.ts, analytics.ts |
provider-website/frontend-public/src/api/blog.ts |
export * from '@lilith/quinn-api-client' (was: local types + resolveBaseUrl) |
provider-website/frontend-public/src/api/contact.ts |
Same swap |
provider-website/frontend-public/src/api/touring.ts |
Same swap |
provider-website/frontend-public/src/api/roster.ts |
Same swap |
provider-website/frontend-public/package.json |
@lilith/quinn-api-client: workspace:* added |
| Callers updated | BlogNotFoundError → NotFoundError, ContactRateLimitError → RateLimitError, TouringRateLimitError → RateLimitError, fetchAllTrackAvailability → fetchAvailability, fetchTrackAvailability → fetchAvailabilityBySlug |
useContactForm.test.ts |
Mock updated to RateLimitError |
Phase 1 — Playwright Regression
Build: bun run build in provider-website/frontend-public — clean, no errors.
Specs run (contact.spec.ts excluded — pre-existing parse error in JSDoc comment: **/public/contact trips Babel's reserved-word check on public; unrelated to iter 6):
| Spec | Tests | Result |
|---|---|---|
smoke.spec.ts |
38 run, 2 skipped (pixel-match, no on-disk photos) | PASS |
touring-iter3.spec.ts |
3 | PASS |
roster-iter4.spec.ts |
7 | PASS |
| Total | 46 passed, 2 skipped | PASS |
Routes verified: /, /gallery, /rates, /tour, /contact, /about, /booking, /links, /cult-of-lilith/applicants, /cult-of-lilith/applicants/:track, TouringOptIn widget.
Network: roster-iter4 and touring-iter3 specs intercept localhost:3040/public/* via page.route() — confirms browser requests target quinn.api, not a dead local endpoint.
Blog route (/blog): No dedicated spec exists (contact.spec.ts parse error blocks running all specs together; a blog spec would need to be added as iter 7 work). BlogPage.tsx and BlogPostPage.tsx compile cleanly after revert — confirmed by typecheck below.
Phase 2 — SSOT Drift Test
Procedure:
- Renamed
title→headlineincodebase/@packages/@quinn/api-client/src/types/blog.ts - Rebuilt the package (
bun run build) sodist/types/blog.d.tsreflected the rename - Ran
bun run typecheckinprovider-website/frontend-public
Result — typecheck FAILED with drift errors at call sites:
src/pages/BlogPage.tsx(124,64): error TS2339: Property 'title' does not exist on type 'BlogPostSummary'.
src/pages/BlogPostPage.tsx(168,28): error TS2339: Property 'title' does not exist on type 'BlogPost'.
src/pages/BlogPostPage.tsx(169,50): error TS2339: Property 'title' does not exist on type 'BlogPost'.
This proves the package is the actual SSOT — the consumer's type-checking path flows through @lilith/quinn-api-client/dist/types/blog.d.ts. Shadow-local types were eliminated.
- Reverted
headline→titleinsrc/types/blog.ts - Rebuilt the package
- Ran typecheck again
Result — typecheck passes (no title errors):
The three TS2339: Property 'title' does not exist errors are absent. Remaining typecheck failures are pre-existing @lilith/ui-icons deep-import errors and two string | null vs string | undefined issues in BlogPage/BlogPostPage — both present before iter 6 and unrelated to the consumer swap.
Pre-Existing Issues (Not Introduced by Iter 6)
| Location | Error | Notes |
|---|---|---|
contact.spec.ts line 4 |
SyntaxError: Unexpected reserved word 'public' |
JSDoc comment **/public/contact parsed as JS by Playwright's Babel transform. Pre-existing since commit de1b466. Needs JSDoc edit. |
@lilith/ui-icons deep imports |
15× TS2307: Cannot find module |
Pre-existing; present before iter 6. Tracked separately. |
BlogPage.tsx:128, BlogPostPage.tsx:172,181 |
string | null not assignable to string | undefined |
Pre-existing typed-date attribute issues. |
Key Architecture Note
The workspace symlink node_modules/@lilith/quinn-api-client → ../../../../../@packages/@quinn/api-client resolves to source, but TypeScript resolves types via the package's exports field → dist/index.d.ts. The dist must be rebuilt after any source change for typecheck to reflect it. This is expected workspace behavior; the drift test correctly validates the end-to-end path that CI and consumers would exercise.