Skip to content

Commit e8fb46d

Browse files
Programcodeclaude
andcommitted
Start a debate on the first Generate instead of asking (MD-10, design choice)
With no Current Debate, Generate creates a debate for its motion and says so in a toast, instead of opening "Start a debate with this motion?". The strip offers Use once beside Start debate, so a one-off run is still a choice. A second submit while the debate is being created does nothing, and a failed create falls back to the dialog. Other tools keep the dialog. Adam to confirm. This commit holds the whole change, including tests, recipes, docs and the production canary, so it reverts alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 9f6c75b commit e8fb46d

24 files changed

Lines changed: 198 additions & 71 deletions

‎.claude/skills/verify-debatelab/SKILL.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ It also prints the commit the stack started at and whether there are uncommitted
7878
**Scripted (preferred for proof).** Recipes are Playwright specs in `.claude/skills/verify-debatelab/recipes/`. They import from `./fixtures`, which re-exports `e2e/full-stack/stack.ts` and `e2e/helpers/voice.ts` and adds the following:
7979

8080
- A fresh demo-code user per test, signed in through the real Login page and `demo-code-login`, and deleted afterwards. Use the `signedIn` fixture for the page and `user` for `user.email` and `await user.userId()`. The user has seen the onboarding tour and has no Current Debate.
81-
- The Current Debate helpers: `startDebate(page, motion)` starts one from the header, `chooseForMotion(page, "create" | "once")` answers the motion dialog, and `debateControl(page)` is the visible header control on either viewport.
81+
- The Current Debate helpers: `startDebate(page, motion)` starts one from the header, `chooseForMotion(page, "create" | "once")` answers the motion dialog, `chooseOneOff(page)` picks one-off mode on the debate strip before a submit, and `debateControl(page)` is the visible header control on either viewport.
8282
- The voice helpers: `stubSpeech(page)` installs a scripted microphone, recogniser and browser voice; `say(page, words)` speaks; `spokenByBrowser(page)` reads back what the browser voice said.
8383
- `admin`: a service-role client for reading rows as a second view.
8484
- `fake` / `providerCall(feature)`: what the Edge Function actually sent upstream. Use `fake.fail(feature, status)` to force a provider error and `fake.truncate(feature)` (fake mode only) to cut the next reply off at its length cap. Each call records the `maxTokens` it asked for.
@@ -88,7 +88,7 @@ It also prints the commit the stack started at and whether there are uncommitted
8888
- `evidence`: `step(line)`, `aria(name, locator?)`, `shot(name)` and `json(name, value)`.
8989
- Automatic failure on any uncaught page error, horizontal overflow, or remote Supabase request.
9090

91-
Every result, source and practice round belongs to a Current Debate or is explicitly one-off. A tool given a motion when no debate exists opens `Start a debate with this motion?`; a motion that differs from the Current Debate's opens `This is a different motion`. Both open after the submit click and before the AI request, so create the `aiResponse()` promise, click, answer with `chooseForMotion`, then await. Or call `startDebate` first, after which tools prefill its motion and open no dialog. The Library's `Add Source` is disabled until a debate exists. See `features/current-debate.md`.
91+
Every result, source and practice round belongs to a Current Debate or is explicitly one-off. A tool given a motion when no debate exists opens `Start a debate with this motion?`, except Generate, which creates the debate for its motion without asking and shows the toast `Started a debate for this motion` (to keep a Generate run one-off, call `chooseOneOff(page)` before clicking `Generate Debate Case`); a motion that differs from the Current Debate's opens `This is a different motion`. Both open after the submit click and before the AI request, so create the `aiResponse()` promise, click, answer with `chooseForMotion`, then await. Or call `startDebate` first, after which tools prefill its motion and open no dialog. The Library's `Add Source` is disabled until a debate exists. See `features/current-debate.md`.
9292

9393
```bash
9494
$H drive all # every recipe, both viewports
@@ -105,7 +105,7 @@ Fake mode also runs `e2e/full-stack/`. Live mode runs the recipes, `e2e/live/`,
105105

106106
To prove a feature without a recipe, copy `recipes/case-generation.spec.ts` to `recipes/<feature>.spec.ts` and use the selectors from the matching file in `features/`. Recipes are maintained files. Keep the ones worth keeping.
107107

108-
**Interactive.** Run `$H user` to mint a code. It prints `DEMO_CODE=...`, and `down` deletes that user. Then point a browser tool (Playwright MCP `browser_navigate` etc.) at `http://127.0.0.1:18081/login`, choose the `Demo code` tab, fill the `Demo code` textbox, and click `Sign in with demo code`. Save your own screenshots into `.verify/artifacts/<run-id>/`. A new account shows the onboarding tour on `/app`. Its first step offers `Start debate`; to skip the tour, click `Skip Tour` (`[data-test-id="button-skip"]`). Then start a debate from the header (`Start debate`, test id `current-debate-desktop` or `current-debate-mobile`) or answer the motion dialog the first tool opens: `Start debate` files the work in a new debate, `Not now, use it once` keeps it one-off. Interactive runs have no remote-Supabase guard. Doctor's local-mode check is what protects you.
108+
**Interactive.** Run `$H user` to mint a code. It prints `DEMO_CODE=...`, and `down` deletes that user. Then point a browser tool (Playwright MCP `browser_navigate` etc.) at `http://127.0.0.1:18081/login`, choose the `Demo code` tab, fill the `Demo code` textbox, and click `Sign in with demo code`. Save your own screenshots into `.verify/artifacts/<run-id>/`. A new account shows the onboarding tour on `/app`. Its first step offers `Start debate`; to skip the tour, click `Skip Tour` (`[data-test-id="button-skip"]`). Then start a debate from the header (`Start debate`, test id `current-debate-desktop` or `current-debate-mobile`) or answer the motion dialog the first tool opens: `Start debate` files the work in a new debate, `Not now, use it once` keeps it one-off. Generate skips that dialog and starts the debate itself; its strip's `Use once` keeps a run one-off. Interactive runs have no remote-Supabase guard. Doctor's local-mode check is what protects you.
109109

110110
Selectors: use accessible names and roles as the feature files list them (`getByRole("button", { name: "Generate Debate Case" })`). The exceptions use `data-testid`: Crossfire's controls, the Current Debate control, strip and dashboard, and the voice controls (`voice-mode`, `voice-mic`). Use `exact: true` for short names such as `Start` or `Save`, because the header's `Start debate` also matches `Start`. Never use coordinates.
111111

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ This directory is the maintained source for verifying DebateLab's user-facing be
77
- `bin/verify.sh up` has printed `READY http://127.0.0.1:18081`, and `bin/verify.sh doctor` exits 0.
88
- The browser client is the `up` instance at `http://127.0.0.1:18081`. Never use the `debatelab` container on `:8080` or a plain `npm run dev`, because both reach production.
99
- Each scripted test signs in with its own fresh demo-code user (a Pro account with a daily limit of 999) and deletes it afterwards. The fixture marks the onboarding tour as seen, and the user starts with no Current Debate.
10-
- Most tools work inside a Current Debate (see [current-debate](./current-debate.md)). Start one with `startDebate(page, motion)` first, or answer the motion dialog that opens after submit with `chooseForMotion(page, "create" | "once")`. The Library's `Add Source` stays disabled until a debate exists.
10+
- Most tools work inside a Current Debate (see [current-debate](./current-debate.md)). Start one with `startDebate(page, motion)` first, or answer the motion dialog that opens after submit with `chooseForMotion(page, "create" | "once")`. Generate opens no dialog when there is no debate: it starts one for its motion, so call `chooseOneOff(page)` before submitting to keep a run one-off. The Library's `Add Source` stays disabled until a debate exists.
1111
- In fake mode, model answers come from the fake provider and contain the marker `FAKE-PROVIDER`. In live mode (`up --live`), every chat call goes to real OpenRouter pinned to `meta/muse-spark-1.3-contributor`, research uses real Exa, and answers are real model text.
1212
- The feature files quote fake-mode text such as `FAKE-PROVIDER case thesis`. Recipes never assert it: they read the `ai` response body with `aiResponse()` and check that its fields are on screen, so the same recipe passes in both modes.
1313

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A signed-in user enters a motion on Generate and receives a structured debate ca
88
- `gen-grounded` includes active Library sources in the system prompt (see [library-sources](./library-sources.md)).
99
- `gen-error` shows `Generation Failed` on a provider error, and the button becomes enabled again for a retry.
1010
- `gen-quota` shows the dialog `Daily Access Paused` when the account has no quota, without calling the provider.
11-
- `gen-save` saves a one-off case (the motion dialog answered with `Not now, use it once`) under a title into `cases` with `debate_id: null`. It appears in History under `One-off and unsorted work`, including from a fresh browser context.
11+
- `gen-save` saves a one-off case (`Use once` chosen on the debate strip before generating) under a title into `cases` with `debate_id: null`. It appears in History under `One-off and unsorted work`, including from a fresh browser context.
1212
- `gen-regenerate` regenerates one section. Only that section shows the overlay `Regenerating <section>…`; the rest of the case and the result toolbar stay on screen, and every regenerate button and the form's submit button are disabled until it finishes.
1313
- `gen-export` downloads a `.pdf` (starting with `%PDF`) and a `.docx` (starting with `PK`).
1414
- `gen-flowchart` builds a flowchart from the Current Debate's case. `/app/flowchart` opens the template; the button `Build from current case` replaces it with the thesis, one node per contention (claim and evidence), the predicted attacks, and the counters, fitted to the canvas. Node titles the app wrote (`Thesis`, `Argument 1`) follow the UI language.
@@ -25,10 +25,10 @@ Preconditions:
2525
- Doctor passes. The recipe `recipes/case-generation.spec.ts` covers every sub-feature below: `bin/verify.sh drive recipes/case-generation`.
2626

2727
- **Start the debate.** `await startDebate(page, motion)`. On `/app/generate`, the textbox named `Motion / Topic *` already holds the motion.
28-
- **Generate.** Click the button named `Generate Debate Case`. The text `FAKE-PROVIDER case thesis` and `FAKE-PROVIDER contention 3` appear, along with a toast `Generate complete`. Without a Current Debate, fill the motion yourself and answer the dialog `Start a debate with this motion?` with `chooseForMotion`.
28+
- **Generate.** Click the button named `Generate Debate Case`. The text `FAKE-PROVIDER case thesis` and `FAKE-PROVIDER contention 3` appear, along with a toast `Generate complete`. Without a Current Debate, fill the motion yourself: Generate starts a debate for it without a dialog and shows the toast `Started a debate for this motion`. To keep the run one-off, call `chooseOneOff(page)` before clicking.
2929
- **Provider side effect.** `providerCall("generate")`'s `user` contains `Motion: <motion>`. `ai_requests` for the user contains `{feature: "generate", status: "success"}`.
3030
- **Filed in the debate.** `cases` for the user is one row whose `debates(motion)` is the motion. On a fresh browser context, `/app/history` shows it as a heading in the `Cases` tab panel, and `Load` reopens it.
31-
- **Save (one-off).** Generate with no debate and choose `chooseForMotion(page, "once")`. Click the button named `Save` (use `exact`), fill the dialog field labelled `Case title`, and click the dialog's `Save` button. `cases` for the user is `[{title, debate_id: null}]`. On a fresh browser context, `/app/history` shows `One-off and unsorted work` with a list item holding the title.
31+
- **Save (one-off).** With no debate, call `chooseOneOff(page)`, then generate. Click the button named `Save` (use `exact`), fill the dialog field labelled `Case title`, and click the dialog's `Save` button. `cases` for the user is `[{title, debate_id: null}]`. On a fresh browser context, `/app/history` shows `One-off and unsorted work` with a list item holding the title.
3232
- **Error.** Call `await fake.fail("generate", 500)`, start the debate, and generate. An `alert` holding `Generation Failed` stays above the results with a `Retry` button; clicking `Retry` succeeds and removes it.
3333
- **Cut off.** Call `await fake.truncate("generate")`, start the debate, and generate. The first reply stops halfway with `finish_reason: "length"`; the Edge Function retries once with a larger cap, and the case appears with no error. `fake.requests()` has two `generate` entries, the second with a larger `maxTokens`. Fake mode only.
3434
- **Cancel.** While a generation runs, the results column shows `Writing your case…` and a `Cancel` button. Cancel stops the request and shows no error.

‎.claude/skills/verify-debatelab/features/current-debate.md‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Each student works in one Current Debate at a time: a motion with its own Librar
55
## Sub-features
66

77
- `debate-start` starts a debate from the header. The control's `aria-label` becomes `Current debate: <motion>`, and `debates` and `current_debates` get the row.
8-
- `debate-offer` opens the dialog `Start a debate with this motion?` when a tool runs with a motion and no Current Debate exists. `Start debate` creates the debate and the run lands in it. `Not now, use it once` runs without one.
8+
- `debate-autostart`: Generate with no Current Debate creates one for its motion without asking, files the case in it, and shows the toast `Started a debate for this motion`. Its strip offers `Use once` beside `Start debate`. Other tools still open the dialog `Start a debate with this motion?`: `Start debate` creates the debate and the run lands in it, `Not now, use it once` runs without one.
99
- `debate-once` opens the dialog `This is a different motion` when a Current Debate exists and the tool has another motion. `Use once without saving to a debate` runs with no debate material and saves nothing to a debate. `Create a new debate for this motion` switches to a new one.
1010
- `debate-scope` keeps sources and results in their debate. The Library, the dashboard at `/app/debate`, History, and every AI request's system prompt see only the Current Debate's material.
1111
- `debate-switch` switches back to an earlier debate through `Switch debate`, and the Library and History follow.
@@ -24,11 +24,12 @@ Each student works in one Current Debate at a time: a motion with its own Librar
2424
Preconditions:
2525

2626
- Doctor passes. The recipe `recipes/current-debate.spec.ts` covers every sub-feature: `bin/verify.sh drive recipes/current-debate`. The maintained specs are `e2e/full-stack/current-debate.spec.ts`, `first-run.spec.ts`, and `debate-context.spec.ts`.
27-
- Helpers, re-exported from `recipes/fixtures.ts`: `startDebate(page, motion)`, `chooseForMotion(page, "create" | "once")`, and `debateControl(page)` (the visible header control on either viewport).
27+
- Helpers, re-exported from `recipes/fixtures.ts`: `startDebate(page, motion)`, `chooseForMotion(page, "create" | "once")`, `chooseOneOff(page)` (one-off mode from the debate strip, before a Generate submit), and `debateControl(page)` (the visible header control on either viewport).
2828

2929
- **Start.** `await startDebate(page, motion)`. It clicks the header control, fills the dialog field labelled `Motion`, and clicks the dialog's `Start debate`. End state: `debateControl(page)` has `aria-label` `Current debate: <motion>`, and `current_debates.debate_id` for the user is that `debates` row.
3030
- **Scope.** Add a source on `/app/library` with a unique marker (see [library-sources](./library-sources.md)); the test id `library-debate` names the motion. Run Brainstorm or Generate: the motion field already holds the debate's motion and no dialog opens. End state: the newest `providerCalls()` entry for the feature has this debate's marker in `system` and not another debate's. `sources.debate_id`, `cases.debate_id`, and `brainstorms.debate_id` point at the debate they were made in.
31-
- **Offer.** With no debate, fill a tool's motion and submit. The dialog `Start a debate with this motion?` contains the motion. Click its `Start debate`. End state: the result shows, the strip reads `Working in: <motion>`, and `debates` holds exactly that motion.
31+
- **Offer.** With no debate, fill Brainstorm's motion and submit. The dialog `Start a debate with this motion?` contains the motion. Click its `Start debate` (`chooseForMotion(page, "create")`).
32+
- **Autostart.** With no debate, fill Generate's motion and click `Generate Debate Case`. No dialog opens; the toast `Started a debate for this motion` shows. End state: the result shows, the strip reads `Working in: <motion>`, and `debates` holds exactly that motion.
3233
- **Use once.** With a debate, submit a different motion. The dialog `This is a different motion` opens. `chooseForMotion(page, "once")`. End state: the provider call's `system` has no debate marker, and the header still names the old debate.
3334
- **Dashboard.** `/app/debate`: `current-debate-motion` has the motion, `debate-side-pro` reads `In favor`, `debate-side-con` reads `Against`, and `debate-sources` and `debate-recent` list only this debate's work.
3435
- **Switch.** Click `debateControl(page)`, then the dialog's `Switch debate`, then `Switch to <motion>`. End state: the Library shows only that debate's sources. In History, the `Brainstorms` and `Cases` tabs show only its results.

‎.claude/skills/verify-debatelab/recipes/case-generation.spec.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { readFile } from "node:fs/promises";
2-
import { admin, AI_TIMEOUT, aiResponse, chooseForMotion, type Evidence, expect, expectProviderCalls, fake, LIVE, signIn, startDebate, test } from "./fixtures";
2+
import { admin, AI_TIMEOUT, aiResponse, type Evidence, expect, expectProviderCalls, fake, LIVE, signIn, startDebate, test, chooseOneOff } from "./fixtures";
33

44
// Feature map: features/case-generation.md.
55
const motion = "This House would make public transport free at the point of use";
@@ -94,11 +94,11 @@ test("gen-save: a one-off case is saved under a title and reopens from History o
9494
await page.goto("/app/generate");
9595
await expect(page.getByRole("heading", { name: "Ready to Generate" })).toBeVisible();
9696
await page.getByRole("textbox", { name: "Motion / Topic *" }).fill(motion);
97+
// No Current Debate: use the motion once, so the case belongs to no debate until saved.
98+
await chooseOneOff(page);
9799
const response = aiResponse(page, "generate");
98100
await page.getByRole("button", { name: "Generate Debate Case" }).click();
99-
// No Current Debate: decline the offer, so the case belongs to no debate until saved.
100-
await chooseForMotion(page, "once");
101-
await evidence.step(`Generate: motion "${motion}", chose Not now, use it once`);
101+
await evidence.step(`Generate: motion "${motion}", chose Use once`);
102102
const output = (await response).output as unknown as CaseOutput;
103103
await expect(page.getByText(output.caseStructure.thesis).first()).toBeVisible();
104104
await evidence.shot("01-generated-one-off");

0 commit comments

Comments
 (0)