Skip to content

Commit f08cdad

Browse files
committed
Merge branch 'design-audit/fixes' into design-audit/shell
2 parents 21a2607 + 80f9490 commit f08cdad

54 files changed

Lines changed: 8048 additions & 182 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/skills/verify-debatelab/features/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Each feature file starts with an H1 title and one paragraph describing the user-
4444
- [Library sources](./library-sources.md): add, pause, delete, grounding, and URL import (`recipes/library.spec.ts`).
4545
- [AI tools](./ai-tools.md): Attack, Brainstorm, Factoids, and Ask streamed, cited, and with web search (`recipes/ai-tools.spec.ts`).
4646
- [Current Debate](./current-debate.md): start, the first-debate offer and the different-motion choice, sources and results scoped to a debate, switching, and the first onboarding step (`recipes/current-debate.spec.ts`); debate context review and pins (`e2e/full-stack/debate-context.spec.ts`).
47+
- [Model defaults](./model-defaults.md): Pro accounts start on Claude Opus 5.5, Basic accounts on Claude Haiku 5.5 with Pro models locked, and retired model IDs move to Opus 5.5 (`recipes/model-defaults.spec.ts`).
4748
- [Practice](./practice.md): a full Lincoln-Douglas sparring round with verdict, and Crossfire in both directions with debrief, each filed in its debate (`recipes/practice.spec.ts`).
4849
- [Voice rounds](./voice-rounds.md): spoken Sparring and Crossfire on the browser voice, with no ElevenLabs key (`recipes/voice-rounds.spec.ts`).
4950
- [Workspace autosave](./workspace-autosave.md): a slow Crossfire or Sparring save from a Workspace never overwrites newer text (`recipes/workspace-autosave.spec.ts`). Needs `up --collab`; skips without it.

‎.claude/skills/verify-debatelab/features/case-generation.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ Preconditions:
4141
- `Save` appears both on the result toolbar and in the dialog. Scope the second click to `getByRole("dialog")`. The toolbar `Save` exists only for one-off results.
4242
- A case filed in a debate is titled with the motion, which the header control also shows (truncated and hidden). Look for it inside the History panel, not with a page-wide `getByText`.
4343
- The client files a result after it renders. Wait for the result on screen (and the row, if the next step reloads) before a `page.goto`, or the reload can drop it.
44-
- The model label in the result header (for example `anthropic/claude-sonnet-5`) is the model the function requested. The answer itself came from the fake provider.
44+
- The model label in the result header (for example `anthropic/claude-opus-5.5`) is the model the function requested. The answer itself came from the fake provider.
4545
- `Google Docs` export needs Google OAuth and cannot be proven locally.
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Model defaults and the model selector
2+
3+
Every AI tool has a model selector. Basic accounts can pick Claude Haiku 5.5, GPT-6 Luna, or Muse Spark 1.3 (`meta/muse-spark-1.3-contributor`); everything else is Pro. A new Pro account starts every tool on Claude Opus 5.5, and a Basic (trial) account on Claude Haiku 5.5. Pro-only models show a `Pro` badge and are disabled for Basic accounts. A setting saved with a retired catalog ID is rewritten on the next load: GPT-5.6 Luna moves to Claude Haiku 5.5, Claude Sonnet 5 and Opus 5 to Claude Opus 5.5, GPT-5.6 Sol and Terra to GPT-6.1 Sol, DeepSeek V4 Flash to Claude Haiku 5.5, and Muse Spark 1.3 to its contributor tier. The Edge Functions resolve a retired ID from an older client the same way. A retired Basic ID never resolves to a Pro model. The catalog and defaults live in `supabase/functions/_shared/model-catalog.ts`.
4+
5+
## Sub-features
6+
7+
- `model-pro-default`: a new Pro account's `user_settings.feature_configs` hold `anthropic/claude-opus-5.5` for every tool, the selector shows `Claude Opus 5.5`, and a Generate run records `model: anthropic/claude-opus-5.5` in `ai_requests`.
8+
- `model-basic-default`: a trial account with no saved settings gets `anthropic/claude-haiku-5.5` everywhere. `Claude Opus 5.5` and `Claude Sonnet 5.5` are disabled options, and a Generate run records Haiku 5.5.
9+
- `model-retired`: saved `anthropic/claude-sonnet-5` configs become `anthropic/claude-opus-5.5` on screen and in `user_settings`.
10+
11+
## How to get to it (user POV)
12+
13+
- Any tool page, for example `/app/generate`: the combobox named `Select AI model for generate` sits beside the tool's inputs.
14+
15+
## Driving it with verify.sh + Playwright
16+
17+
- `bin/verify.sh drive recipes/model-defaults` covers all three sub-features.
18+
- Demo sign-in grants Pro. For the Basic path the recipe sets `subscription_tier: "trial"`, a future `trial_ends_at`, clears `grace_period_ends_at` and `daily_limit_override`, and empties `feature_configs`, then reloads so the client re-hydrates as a new trial account.
19+
- End states are read from `user_settings.feature_configs` and `ai_requests.model` through `admin`, and from the fake provider's recorded `model`.
20+
21+
## Gotchas
22+
23+
- Fake mode records the requested model but answers with fake text, and live mode rewrites every chat call to the Muse Spark contributor pin. Neither proves that OpenRouter accepts the Claude 5.5 IDs; that needs a direct OpenRouter call.
24+
- Existing saved choices of a model still in the catalog are kept. Only retired IDs and unsaved settings move to the new defaults.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { Page } from "@playwright/test";
2+
import { admin, AI_TIMEOUT, aiResponse, chooseForMotion, expect, providerCalls, test } from "./fixtures";
3+
4+
// Feature map: features/model-defaults.md.
5+
const motion = "This House would make public transport free at the point of use";
6+
const OPUS = "anthropic/claude-opus-5.5";
7+
const HAIKU = "anthropic/claude-haiku-5.5";
8+
9+
async function settings(userId: string) {
10+
const { data, error } = await admin.from("user_settings").select("feature_configs").eq("user_id", userId).single();
11+
if (error) throw error;
12+
return data.feature_configs as Record<string, { model: string }>;
13+
}
14+
15+
async function updateSettings(userId: string, values: Record<string, unknown>) {
16+
const { error } = await admin.from("user_settings").update(values).eq("user_id", userId);
17+
if (error) throw error;
18+
}
19+
20+
async function generateOnce(page: Page) {
21+
await page.getByRole("textbox", { name: "Motion / Topic *" }).fill(motion);
22+
const response = aiResponse(page, "generate");
23+
await page.getByRole("button", { name: "Generate Debate Case" }).click();
24+
await chooseForMotion(page, "once");
25+
return response;
26+
}
27+
28+
test("model-pro-default: a new Pro account starts on Claude Opus 5.5 and generates with it", async ({ signedIn: page, user, evidence }) => {
29+
const userId = await user.userId();
30+
await expect.poll(async () => (await settings(userId)).generate?.model, { timeout: AI_TIMEOUT }).toBe(OPUS);
31+
32+
await page.goto("/app/generate");
33+
const selector = page.getByRole("combobox", { name: "Select AI model for generate" });
34+
await expect(selector).toContainText("Claude Opus 5.5");
35+
await selector.click();
36+
await expect(page.getByRole("option", { name: /Claude Haiku 5\.5/ })).toBeEnabled();
37+
await expect(page.getByRole("option", { name: /Claude Sonnet 5\.5/ })).toBeEnabled();
38+
await evidence.shot("01-pro-model-list");
39+
await page.keyboard.press("Escape");
40+
await evidence.step("New Pro account: selector shows Claude Opus 5.5, all Claude 5.5 models enabled");
41+
42+
const body = await generateOnce(page);
43+
expect((body.meta as { resolvedModel?: string } | undefined)?.resolvedModel ?? OPUS).toBe(OPUS);
44+
const calls = await providerCalls();
45+
// Live mode rewrites the upstream model to the pin, so read what the app asked for.
46+
expect(calls.filter((call) => call.feature === "generate").map((call) => call.requestedModel ?? call.model)).toContain(OPUS);
47+
const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId);
48+
expect(usage).toContainEqual(expect.objectContaining({ feature: "generate", status: "success", model: OPUS }));
49+
await evidence.json("02-ai-requests", usage);
50+
await evidence.shot("02-generated-with-opus");
51+
});
52+
53+
test("model-basic-default: a Basic account starts on Claude Haiku 5.5 with only Haiku, Luna and Muse Spark unlocked", async ({ signedIn: page, user, evidence }) => {
54+
const userId = await user.userId();
55+
// Demo sign-in grants Pro; turn the account into a fresh trial with nothing saved.
56+
await updateSettings(userId, {
57+
subscription_tier: "trial",
58+
trial_ends_at: new Date(Date.now() + 2 * 86_400_000).toISOString(),
59+
grace_period_ends_at: null,
60+
daily_limit_override: null,
61+
feature_configs: {},
62+
});
63+
await page.goto("/app/generate");
64+
await expect.poll(async () => (await settings(userId)).generate?.model, { timeout: AI_TIMEOUT }).toBe(HAIKU);
65+
66+
const selector = page.getByRole("combobox", { name: "Select AI model for generate" });
67+
await expect(selector).toContainText("Claude Haiku 5.5");
68+
await selector.click();
69+
await expect(page.getByRole("option", { name: /Claude Opus 5\.5/ })).toBeDisabled();
70+
await expect(page.getByRole("option", { name: /Claude Sonnet 5\.5/ })).toBeDisabled();
71+
await expect(page.getByRole("option", { name: /Gemini 3\.8 Flash/ })).toBeDisabled();
72+
await expect(page.getByRole("option", { name: /GPT-6 Luna/ })).toBeEnabled();
73+
await expect(page.getByRole("option", { name: /Muse Spark 1\.3/ })).toBeEnabled();
74+
await evidence.shot("01-basic-model-list");
75+
await page.keyboard.press("Escape");
76+
await evidence.step("Basic account: selector shows Claude Haiku 5.5; Luna and Muse Spark enabled; Opus, Sonnet and Gemini locked behind Pro");
77+
78+
await generateOnce(page);
79+
const { data: usage } = await admin.from("ai_requests").select("feature,status,model").eq("user_id", userId);
80+
expect(usage).toContainEqual(expect.objectContaining({ feature: "generate", status: "success", model: HAIKU }));
81+
await evidence.json("02-ai-requests", usage);
82+
});
83+
84+
test("model-retired: a saved Claude Sonnet 5 setting moves to Claude Opus 5.5", async ({ signedIn: page, user, evidence }) => {
85+
const userId = await user.userId();
86+
const retired = Object.fromEntries(["generate", "attack", "brainstorm", "factoids", "ask", "sparring"].map((feature) => [
87+
feature,
88+
{ provider: "openrouter", model: "anthropic/claude-sonnet-5", temperature: 0.7, reasoningEffort: "low" },
89+
]));
90+
await updateSettings(userId, { feature_configs: retired });
91+
await page.evaluate(() => localStorage.removeItem("debate-trainer-settings"));
92+
await page.goto("/app/generate");
93+
94+
await expect(page.getByRole("combobox", { name: "Select AI model for generate" })).toContainText("Claude Opus 5.5");
95+
await expect.poll(async () => Object.values(await settings(userId)).map((config) => config.model), { timeout: AI_TIMEOUT })
96+
.toEqual(Array(6).fill(OPUS));
97+
await evidence.shot("01-retired-migrated");
98+
await evidence.step("Saved Sonnet 5 configs were rewritten to Opus 5.5 in user_settings");
99+
});

‎docs/architecture.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,9 +128,9 @@ Supabase Auth owns user sessions. Protected routes provide a client navigation b
128128

129129
Checkout and portal functions create Stripe sessions after authenticated ownership and redirect validation. The webhook updates subscription state and records event processing for idempotency. AI handlers record the provider-reported usage and charge for each request.
130130

131-
All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, Luna Basic default, Pro-only entries, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge.
131+
All model requests use the single OpenRouter backend route and `OPENROUTER_API_KEY`. The shared ten-model catalog, its defaults (Claude Haiku 5.5 for Basic, Claude Opus 5.5 for Pro), Pro-only entries, retired-ID successors, and request controls live in [`supabase/functions/_shared/model-catalog.ts`](../supabase/functions/_shared/model-catalog.ts). OpenRouter usage remains raw in the audit row, and the reported charge is unknown when the provider does not return a complete cost. The opponent's voice is the one exception to the OpenRouter route: the `voice` function calls ElevenLabs with `ELEVENLABS_API_KEY`. ElevenLabs reports billed characters, not dollars, so [`supabase/functions/_shared/cost.ts`](../supabase/functions/_shared/cost.ts) prices those rows from a dated list-price snapshot; promotions and plan credits are not modelled, so a voice row can overstate the charge.
132132

133-
`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts and normally resolves to GLM 5.3 Flash; manual GLM selection keeps its Pro restriction. Auto Pro remains Pro-only. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit.
133+
`Auto Fast` and `Auto Pro` are virtual selections, not provider model IDs. The Edge Functions resolve them through TypeSafe Jev on OpenRouter before calling the concrete writing model. Auto Fast is available to Basic accounts, routes only among the Basic models (Claude Haiku 5.5, GPT-6 Luna, Muse Spark 1.3 Contributor), and normally resolves to Claude Haiku 5.5. Auto Pro remains Pro-only and normally resolves to Claude Opus 5.5. Provider calls, usage rows, and response metadata record the concrete resolved model, while response metadata also retains the requested automatic selection. A Jev router call uses the same OpenRouter key and does not consume a second daily quota unit.
134134

135135
## Observability
136136

‎docs/testing.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ OPENROUTER_API_KEY=... deno run --allow-read --allow-write --allow-env=OPENROUTE
8686
--allow-net=openrouter.ai scripts/prompt-examples.ts supabase/functions /tmp/after.json
8787
```
8888

89-
This sends the fourteen prompt examples to one real model (GPT-5.6 Luna by default) and writes the raw outputs for manual review. Point the first argument at a directory holding an older `_shared/prompts.ts` and `ai/prompt-builder.ts` to compare revisions. It runs no web research and is a bounded manual check, not a model matrix or an automated quality gate.
89+
This sends the fourteen prompt examples to one real model (GPT-6 Luna by default) and writes the raw outputs for manual review. Point the first argument at a directory holding an older `_shared/prompts.ts` and `ai/prompt-builder.ts` to compare revisions. It runs no web research and is a bounded manual check, not a model matrix or an automated quality gate.
9090

9191
### Live Jev contract smoke
9292

@@ -215,7 +215,7 @@ To check one feature by hand, or keep evidence for a change, use the same stack
215215
LIVE_PROVIDERS=1 LIVE_OPENROUTER_API_KEY=... LIVE_EXA_API_KEY=... ./.superset/db-gate.sh
216216
```
217217

218-
Live mode uses the same locked stack but skips pgTAP and the fake-provider specs. It runs `e2e/live/` on desktop Chromium against real OpenRouter, Jev, and Exa. Imported web pages are fetched live too. The fake provider becomes a recording proxy. It forwards every chat call to OpenRouter with the model pinned to `LIVE_MODEL` (default `meta/muse-spark-1.3-contributor`; CI reads the `LIVE_MODEL` repository variable, which is unset), passes Jev decisions through unchanged, and refuses calls past `MAX_UPSTREAM_CALLS` (200). The default is the cheapest model that supports every request parameter the app sends, including `response_format` and reasoning effort. It is test-only and not in the app catalog, so users cannot pick it. Meta is its only provider, so an outage there fails the gate instead of failing over.
218+
Live mode uses the same locked stack but skips pgTAP and the fake-provider specs. It runs `e2e/live/` on desktop Chromium against real OpenRouter, Jev, and Exa. Imported web pages are fetched live too. The fake provider becomes a recording proxy. It forwards every chat call to OpenRouter with the model pinned to `LIVE_MODEL` (default `meta/muse-spark-1.3-contributor`; CI reads the `LIVE_MODEL` repository variable, which is unset), passes Jev decisions through unchanged, and refuses calls past `MAX_UPSTREAM_CALLS` (200). The default is the cheapest model that supports every request parameter the app sends, including `response_format` and reasoning effort. It is also the Basic catalog model "Muse Spark 1.3", so a live run that selects it cannot be told apart from the pin by the upstream model alone; recipes check the requested model instead. Meta is its only provider, so an outage there fails the gate instead of failing over.
219219

220220
The specs cover web-researched case generation, verified Factoids citations, Ask with web search, URL import, streamed Ask, Attack, Brainstorm, a judged sparring round, and Crossfire with live Jev signals. Outputs vary, so tests assert structure rather than text:
221221

@@ -288,8 +288,8 @@ npx playwright test --config=playwright.production.config.ts
288288
- every browser-facing Edge Function answers a preflight that allows the site origin (`workspace-invites` is excluded until production enables Workspaces);
289289
- `ai` returns 401, not a 5xx, to an unauthenticated call;
290290
- the canary account signs in, opens Generate, and Settings shows its Pro plan (a row-level-security read), without spending an AI request;
291-
- the dedicated canary account gets a streamed GPT-5.6 Luna answer from `ai-stream`;
292-
- the canary account generates a three-contention case with GPT-5.6 Luna through `ai`.
291+
- the dedicated canary account gets a streamed Claude Haiku 5.5 answer from `ai-stream`;
292+
- the canary account generates a three-contention case with Claude Haiku 5.5 through `ai`.
293293

294294
The last two checks are the only production writes, and they touch only the canary account: its `ai_requests` usage rows and whatever the Generate page saves for it. The dedicated canary account has Pro promo access capped at 10 requests a day. Captures are disabled because they could retain its session.
295295

‎e2e/animation-polish.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { expect, test, type Page } from '@playwright/test';
22
import { seedCurrentDebate, stubSupabaseSession } from './helpers/auth';
33
import { stubAIResponses } from './fixtures/ai-responses';
44

5-
const featureConfig = { provider: 'openrouter', model: 'z-ai/glm-5.3-flash', temperature: 0.7 };
5+
const featureConfig = { provider: 'openrouter', model: 'z-ai/glm-5.3-flash' };
66
const STATE_TIMEOUT_MS = 15_000;
77

88
async function prepareMotionFixture(page: Page, reducedMotion: 'no-preference' | 'reduce') {

‎e2e/authed-pages.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ test('visible Ask advanced controls use the non-streaming contract', async ({ pa
100100
outputLanguage: 'en',
101101
theme: 'light',
102102
featureConfigs: {
103-
ask: { provider: 'openai', model: 'gpt-5.4-mini', temperature: 0.7 },
103+
ask: { provider: 'openai', model: 'gpt-5.4-mini' },
104104
},
105105
},
106106
version: 0,

0 commit comments

Comments
 (0)