Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 13 additions & 22 deletions docs/relaunch/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@
Agents append one entry per tie-break decision (01-product.md §0.4). Newest first.
Format: `YYYY-MM-DD · WP-XX · decision · why · alternatives considered`.

- 2026-10-27 · WP-01 · `/dev/components` ships in every build; "dev only" is enforced by noindex metadata + zero inbound links (no nav/sitemap entry) · WP-00 fixed `pnpm e2e` to test the production build via `next start`, so a NODE_ENV or env-var gate would break the axe e2e locally and in CI · gating the route on `NODE_ENV=development`; a `NEXT_PUBLIC_*` flag set in the CI e2e step
- 2026-10-27 · WP-01 · Toast = shadcn `sonner` with the Toaster pinned bottom-right + 4 s (§5.3); dropped the generated `next-themes` wiring, sonner `theme="system"` follows prefers-color-scheme like every other component · the registry has no radix `toast` for the radix style ("only available for Base UI projects" per CLI) and sonner is shadcn's current default · hand-rolled wrapper on radix Toast primitives
- 2026-10-27 · WP-01 · shadcn `--muted-foreground` maps to §5.2 `--body`, not `--faint`: faint on surface-2/3 sits at/below the 4.5:1 AA floor (4.51:1 / ~4.2:1) and muted-foreground renders on muted surfaces (dialog footers, segmented tabs); faint remains in use for text on bg/surface (4.83:1) via custom components · acceptance says fix the component mapping, not the spec tokens · mapping muted-foreground to faint and flagging the AA miss in the PR
- 2026-10-27 · WP-01 · §5.2 focus ring (2px accent + 2px offset) is one global unlayered `:focus-visible` outline; shadcn's per-component `focus-visible:ring-3 ring-ring/50` classes were removed so indicators don't double up · the written spec wins over shadcn defaults · keeping shadcn rings (3px, 50% alpha, no offset)
- 2026-10-27 · WP-01 · axe scans of the open dropdown menu and tooltip are scoped to the open portal (`data-radix-popper-content-wrapper`); dialog/sheet scans stay full-page · Radix renders menus modal: the whole background is aria-hidden while focus is trapped, so full-page scans report hidden-but-focusable background nodes plus page-level heading/region rules — artifacts of the correct modal pattern, not real defects · disabling those rules for the whole suite
- 2026-10-27 · WP-01 · mock.css component styles are realized as React components (shadcn adaptations + `components/app/`) using the tokens; globals.css carries only the §5.2 tokens, shadcn mapping, type scale, radii, shadow, focus and motion · one source of truth per component instead of duplicated utility classes · literal port of `.btn/.pill/.quote/...` classes into globals.css
- 2026-10-27 · WP-01 · Button sizes remapped to §5.3 32/40/48 px (sm/default/lg); dense xs/icon-xs stay 28 px for dialog/sheet close affordances · honors §5.3 while keeping shadcn's dense variants usable · deleting the dense sizes
- 2026-10-27 · WP-02 · Keyless preview mode: when Clerk keys AND DATABASE_URL are both absent, `getAuthUserId()` returns a fixed `user_preview` id and DB-backed routes answer 503; if either is configured the app fails closed (DB-without-Clerk → no user) · previews/CI/e2e must render shells with zero secrets (handoff rule) without faking a provider; the half-configured window (DB, no auth) must never expose data · env flag like ALLOW_KEYLESS_AUTH; dev-only gating (Vercel preview is NODE_ENV=production)
- 2026-10-27 · WP-02 · ClerkProvider, clerkMiddleware (in `proxy.ts`, Next 16 convention) and `<SignIn/>/<SignUp/>` mount only when `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`+`CLERK_SECRET_KEY` are set; /sign-in shows a plain "Sign in isn't set up in this preview." card otherwise · app must build/preview keyless; rendering Clerk components keyless throws · always mounting Clerk with dummy keys (fake provider)
- 2026-10-27 · WP-02 · `user.deleted` deletes the users row; workspaces/shows/episodes cascade via FK, feedback + usage_events keep with SET NULL · account deletion should wipe personal data (GDPR-friendly) while the owner keeps the learning signal from feedback notes · soft-delete flag (no column in §7.1)
- 2026-10-27 · WP-02 · POST /api/show is idempotent: if the caller already has a show it returns 200 with the existing row instead of a duplicate · refresh during onboarding must not create two shows (shows.workspace_id is unique) · 409 error the client must handle
- 2026-10-27 · WP-02 · RSS prefill = hand-rolled extraction of channel `<title>` + `itunes:image`/`<image><url>` (with CDATA/entity decode, relative-URL resolution); no XML parser dependency · MVP only needs two fields and RSS import is explicitly out of scope; §0.4 fewer dependencies · fast-xml-parser
- 2026-10-27 · WP-02 · SSRF guard: literal IPs checked without DNS, hostnames resolved with `dns.lookup(all)` and every address must be `unicast` after unwrapping IPv4-mapped IPv6, redirects followed manually (≤3) and re-validated per hop, 5 MB counted-body cap, one shared 5 s deadline · §7.5 requires private-IP blocking; a hostile feed must not bounce us to an internal service or an infinite redirect · allowlist-only domains (breaks real feeds)
- 2026-10-27 · WP-02 · RSS prefill fills only EMPTY fields (name when blank; artwork always) · typed values must never be silently overwritten · always overwrite
- 2026-10-27 · WP-02 · A readable feed with neither title nor artwork is treated as unreadable → the exact U2 error copy · otherwise the user sees success and no prefill happens, which feels broken · 200 with nulls
- 2026-10-27 · WP-02 · Columns without a stated default use boring conventions: timestamps `defaultNow()`, arrays `default []`, counters `default 0`, boolean flags `default false`; status enums have NO default (inserts state one explicitly); `plan_source`, `current_period_end`, ls ids, feed/voice fields stay nullable · §0.4 rule 1 while keeping §7.1's stated defaults byte-exact · inventing defaults for status enums (could mask pipeline bugs)
- 2026-10-27 · WP-02 · /app/episodes (MK-3 empty state) + /app/episodes/new (MK-4 intake shell) shipped as static shells · §5.4 says `/app (→ episodes)`, so U1's post-signup flow needs a real target; WP-06 replaces the list · leaving /app dead-ending after onboarding skip
- 2026-10-27 · WP-02 · Settings Voice/Billing/Account and onboarding step 2 are stubs with copy "This page is coming soon." + the stepper stays navigable (step 2 has "Skip for now") · those WPs (07/08) build the real pages; routes must exist per §5.4 · omitting routes until their WP lands (dead tabs)
- 2026-10-27 · WP-02 · Clerk webhook verifies via the `svix` package directly (payload + svix-* headers) instead of `@clerk/nextjs/webhooks` verifyWebhook · same Svix signatures, but plain `Request` typing and testable without Next-specific request shapes · verifyWebhook (needs RequestLike cast)
- 2026-10-27 · WP-02 · Playwright `E2E_PORT` env override (default 3000, command unchanged) · sibling agents run e2e concurrently on this machine and race for :3000; CI path stays byte-identical · hard-coding a different port (would break the documented CI flow)
- 2026-10-27 · WP-02 · `cost_usd` is `numeric(12,6)` and `units` int · money estimates should not accumulate float error; drizzle maps numeric to string · doublePrecision (simpler, lossy)
Format: `YYYY-MM-DD · WP-XX · decision · why · alternatives considered`.
- 2026-10-27 · WP-04 · WP-02 integration landed in this branch via merge: `resolveAppSettingsStore()` now returns a real Drizzle store over `app_settings` (WP-02 schema), null only when `DATABASE_URL` is unset (keyless preview); `requireOwner` uses `lib/env.isClerkConfigured()` for clean 503-vs-401 classification and still denies keyless previews (admin AI switching must never work where auth is fake) · completes the documented WP-02 seams instead of leaving them stubbed; U18 now exercises the real store path in tests via the same interface · keeping the null stub until WP-09

- 2026-10-27 · WP-04 · Groq fallback models: `openai/gpt-oss-120b` (long) / `openai/gpt-oss-20b` (fast) · §8.1 requires the choice to be made from Groq's current model list at build time; console.groq.com/docs/models (2026-10-27) lists the llama-3.3/3.1 models as Enterprise ("contact sales") while gpt-oss-120b/20b are self-serve with public prices ($0.15/$0.60 and $0.075/$0.30 per M) and 131k context · llama-3.3-70b-versatile long + llama-3.1-8b-instant fast
- 2026-10-27 · WP-04 · `lib/ai/prices.ts` carries only vendor-verified list prices (Groq, OpenAI, Anthropic — fetched from their pricing docs 2026-10-27, source stamped per row); GLM rows are absent → cost 0 · hard rule: no invented numbers; GLM coding-plan calls are subscription-covered anyway, and bigmodel.cn list prices were not verifiable at build time · estimated GLM prices from memory
- 2026-10-27 · WP-04 · `requireOwner` denies with 503 when Clerk is unconfigured or `OWNER_EMAILS` is unset (warn-once log) — no insecure fallback · M13 makes /admin owner-only; a header/env-based identity bypass would be fake auth; /admin lights up when WP-02 ships Clerk middleware · allowing requests in dev when Clerk keys are absent
- 2026-10-27 · WP-04 · `AppSettingsStore` (`get/put` on the `app_settings` key) is an injected interface; until WP-02's DB lands `resolveAppSettingsStore()` returns null — GET serves the env/default view, PUT returns 503 `settings_store_unavailable` · keeps WP-04 free of schema ownership and gives WP-02 a one-function seam; U18 is tested against the documented interface with an in-memory store · inventing a Drizzle table in WP-04
- 2026-10-27 · WP-04 · `@clerk/nextjs` reuse: WP-04 aligns on WP-02's `@clerk/nextjs@^7.9.20` (same version both branches chose independently) and reads the real `currentUser()` via dynamic import · it is the §0.2-locked auth dependency; dynamic import + try/catch degrades to `auth_unavailable` (deny) when keys are absent · stubbing auth without the package (fake import, bundler-fragile)
- 2026-10-27 · WP-04 · deterministic fallback content is NOT held to the §7.2 AI minima (e.g. exactly 8 titles, ≥5 chapters) — the engine returns what the transcript honestly yields, and artifact status `fallback` (U10 banner) marks it · padding to satisfy a schema would fabricate content; WP-06 renders fallback packages with the banner · duplicating/padding engine output to pass Zod
- 2026-10-27 · WP-04 · quote claims from the model are resolved to verbatim offsets (§7.3) BEFORE Zod validation, with candidate backfill guaranteeing required counts (blog 2, newsletter 1, quotes 5) · schemas store resolved QuoteRefs, so resolution must precede validation; backfill keeps counts honest (transcript text only) · validating raw `{segmentId, exact}` claims against offset schemas
- 2026-10-27 · WP-04 · selection precedence is wholesale: a valid `app_settings.ai` object wins over env entirely; env (`AI_PROVIDER` + optional `AI_MODEL_*`) is the fallback layer; no field-by-field mixing · mixing settings fields with env models makes admin switches silently overridable and untestable; §8.1 lists the order, not a merge rule · per-field precedence
- 2026-10-27 · WP-04 · the §8.4 URL allow list matches subpaths of an allowed origin (`https://show.example` allows `https://show.example/ep-12`) · the show's settings hold its own site/link; subpath links are not hallucinated foreign domains; WP-03's exact-match primitive stays untouched (its tests pin it) and the prefix rule lives in the WP-04 wiring layer · changing `lib/artifacts/postcheck.ts` semantics
- 2026-10-27 · WP-04 · `usage_events.units` for `kind=llm` = total tokens (prompt + completion); cost_usd = in/out-priced estimate · §7.1 leaves `units` open; one number keeps the row compact and cost is derived from the same table · output tokens only
- 2026-10-27 · WP-04 · live provider smoke test lives in `tests/unit/live.ai.test.ts` behind `describe.skipIf(!GLM_API_KEY || CI)` — no separate script/runner · zero new tooling; CI (no keys) always skips; the run command is documented in the file header · a scripts/*.mjs runner with tsx
- 2026-10-27 · WP-04 · OpenAI/OpenRouter placeholder defaults (`gpt-4o`/`gpt-4o-mini`, `anthropic/claude-sonnet-4.5`/`openai/gpt-4o-mini`) exist so a preset is never model-less; §8.1 says these providers are "set via env" and env/settings always override them · leaving defaults empty and failing late · throwing when models are unset
- 2026-10-27 · WP-03 · zod 4.6.7 for `lib/artifacts/schemas.ts` (spec text predates zod 4) · §0.4 rule 6: verified the API against the installed package; note zod 4 objects have no `.min/.max/.length` — count constraints live on `z.array(...)` · staying on zod 3
- 2026-10-27 · WP-03 · Legacy `validateSuite` (engine/glm.js) is not ported as-is; its quote-integrity core (`isExactQuote`, `normalizeQuoteStyle`) is ported into `lib/quotes/normalize.ts` and the suite-level validator is replaced by §7.3 resolve-by-reference (`resolveQuotes`) + per-artifact Zod validation · the new architecture stores QuoteRefs, not model text, so suite-shape validation is obsolete; spec wins over legacy JS · porting validateSuite into web/lib
- 2026-10-27 · WP-03 · Quote matching is segment-anchored: a model quote is searched in its claimed segment + the next 2 (§7.3), not anywhere in the transcript like legacy `isExactQuote` · spec wins; a verbatim quote claimed on the wrong segment is dropped and refilled from candidates · keeping the legacy whole-source scan
Expand Down
18 changes: 18 additions & 0 deletions web/app/api/admin/ai/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// §7.5 — GET/PUT /api/admin/ai (owner only, M13). Thin wrappers over the
// framework-free handlers in lib/ai/admin.ts. The app_settings store arrives
// through the documented WP-02 seam; until the DB layer lands, PUT returns 503
// and GET serves the env/default view.
import { adminAiGet, adminAiPut } from "@/lib/ai/admin";
import { resolveAppSettingsStore } from "@/lib/ai/settings-server";


export async function GET() {
const res = await adminAiGet({ store: resolveAppSettingsStore() });
return Response.json(res.body, { status: res.status });
}

export async function PUT(request: Request) {
const body = await request.json().catch(() => null);
const res = await adminAiPut({ store: resolveAppSettingsStore() }, body);
return Response.json(res.body, { status: res.status });
}
12 changes: 12 additions & 0 deletions web/app/api/admin/ai/test/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// §7.5 — POST /api/admin/ai/test (owner only): tiny prompt through the current
// (or body-specified) provider; returns OK/latency/usage or the error.
import { adminAiTest, type AdminTestBody } from "@/lib/ai/admin";
import { resolveAppSettingsStore } from "@/lib/ai/settings-server";

export const maxDuration = 60;

export async function POST(request: Request) {
const body = ((await request.json().catch(() => ({}))) ?? {}) as AdminTestBody;
const res = await adminAiTest({ store: resolveAppSettingsStore() }, body);
return Response.json(res.body, { status: res.status });
}
Loading
Loading