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
18 changes: 18 additions & 0 deletions .audit/crossfire.tsv
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
ts phase decision why evidence result
2026-08-30T00:00:00Z isolation created feature/crossfire-trainer in a separate worktree at fetched origin/main keep the dirty main checkout and Sparring worktrees untouched git worktree list; commit 8952608 isolated branch ready
2026-08-30T00:00:01Z design kept DebateLab Tailwind v3, shadcn, Lucide, and framer-motion Adam explicitly required the existing product system and no new icon or design library package.json; tailwind.config.ts no new design dependencies
2026-08-30T00:00:02Z verification installed the locked dependency tree with npm ci required before current Vitest, build, and Playwright checks package-lock.json; npm ci 797 packages installed; host Node 24 differs from declared Node 22
2026-08-30T20:45:48Z audit marked the first three timestamps as placeholders and superseded them with this checkpoint the decision log must not present guessed times as exact evidence .audit/crossfire.tsv decisions are accurate; original placeholder times are not timing evidence
2026-08-30T20:49:57Z baseline ran the unchanged full Vitest coverage command before feature edits record the existing selected-scope gate and distinguish pre-existing flakes from Crossfire regressions npm run test:coverage 757 passed; LibraryPage folder-deactivation test timed out at 5 seconds
2026-08-30T21:17:19Z contracts validated Crossfire state, storage, auth cleanup, AI boundaries, source opt-in, and result handoff with focused tests keep illegal states and malformed model output outside the UI src/test/crossfire; src/test/ai/crossfire-client.test.ts; src/test/auth/AuthProvider.behavior.test.tsx 64 focused tests passed
2026-08-30T21:22:01Z backend verified Crossfire prompts and bounded provider body consumption under Deno-compatible tests prompt and response bounds are security requirements, not UI-only checks supabase/functions/ai/prompt-builder.test.ts; supabase/functions/_shared/prompts.test.ts; supabase/functions/_shared/research.test.ts 33 focused tests passed
2026-08-30T21:26:25Z coverage passed the full selected-scope Vitest gate after adding real Crossfire branch tests do not lower thresholds or exclude new maintained logic coverage; vitest.config.ts 74 files and 791 tests passed; 90.33 statements, 83.19 branches, 90.87 functions, 94.17 lines
2026-08-30T21:27:10Z deno checked Edge entrypoints and ran every Deno function test Crossfire changes share the production AI function and need runtime-specific proof supabase/functions 85 tests passed; four Edge entrypoints checked
2026-08-30T21:28:20Z browser passed the complete deterministic browser suite on desktop and mobile prove both Crossfire roles, the debrief, locales, and existing routes without real provider traffic e2e/crossfire.spec.ts; test-results 100 Playwright tests passed; four final-flow screenshots captured
2026-08-30T21:30:10Z formatting kept the repository's existing Deno formatting instead of rewriting seven whole function files deno fmt check proposed thousands of unrelated mechanical changes while ESLint, Deno check, and tests pass deno fmt --check output optional format check failed; no bulk rewrite applied
2026-08-30T21:36:08Z review fixed all five independent review findings before committing account isolation, bounded handoff and error bodies, radio keyboard behavior, and early exit were product requirements commits 42cdfc5 and 7b25c27; src/test/pages/CrossfirePage.test.tsx five findings fixed with regression tests
2026-08-30T21:36:08Z commits created two coherent local feature commits and kept the branch unpushed Adam authorized local commits but prohibited push and deployment commits 42cdfc5 and 7b25c27 core contracts and UI experience committed locally
2026-08-30T21:38:19Z re-review closed the account-switch persistence race and made server-side custom-prompt denial explicit the reviewer found a passive-effect ordering leak and wanted direct-request enforcement to be obvious src/pages/CrossfirePage.tsx; src/test/pages/CrossfirePage.test.tsx; supabase/functions/ai/index.ts stale account state never renders or saves under the next account; focused checks passed
2026-08-31T10:48:33Z PR review fixed all 11 CodeRabbit comments and reran every release gate the review found valid recovery, boundary, localization, and coverage gaps PR #12; 800 Vitest tests; 86 Deno tests; 100 Playwright tests all actionable review findings fixed; current local gates green
2026-08-31T10:55:00Z independent review found that the mounted account-switch path still overwrote the incoming account draft storage isolation required restoring the new owner state before persistence src/pages/CrossfirePage.tsx; src/test/pages/CrossfirePage.test.tsx merge held; owner transition and server regression coverage reopened
2026-08-31T11:01:05Z final remediation fixed the mounted owner transition, kept the 200 KB request cap, and added executable server normalization tests close the independent review findings before pushing a new PR head npm run lint; npm run typecheck; npm run test:coverage; npm run build; deno check; deno test; PORT=4175 npm run test:e2e 76 files and 804 Vitest tests, 88 Deno tests, and 100 Playwright tests passed
1 change: 1 addition & 0 deletions e2e/authed-pages.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const AUTH_ROUTES = [
{ path: '/app', name: 'home' },
{ path: '/app/generate', name: 'generate' },
{ path: '/app/attack', name: 'attack' },
{ path: '/app/crossfire', name: 'crossfire' },
{ path: '/app/brainstorm', name: 'brainstorm' },
{ path: '/app/factoids', name: 'factoids' },
{ path: '/app/ask', name: 'ask' },
Expand Down
132 changes: 132 additions & 0 deletions e2e/crossfire.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { expect, test, type Page } from '@playwright/test';
import { setLocale, stubSupabaseSession } from './helpers/auth';

type CrossfireRequest = {
feature: string;
input: {
operation: 'next-turn' | 'debrief';
expectedReply?: 'question' | 'answer';
config: { mode: 'question-ai' | 'ai-questions-user'; useSourceLibrary: boolean };
transcript: Array<{ id: string; text: string }>;
};
sources: unknown[];
};

async function installDeterministicBackend(page: Page, requests: CrossfireRequest[]) {
await page.route('**/*', async (route) => {
const url = new URL(route.request().url());
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
await route.continue();
return;
}

if (url.pathname.includes('/rest/v1/')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
headers: { 'content-range': '*/0' },
body: '[]',
});
return;
}

if (url.pathname.endsWith('/auth/v1/user')) {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ id: '00000000-0000-0000-0000-000000000001', email: 'e2e@example.com' }),
});
return;
}

if (url.pathname.endsWith('/functions/v1/ai')) {
const body = route.request().postDataJSON() as CrossfireRequest;
requests.push(body);
if (body.input.operation === 'next-turn') {
const questionCount = requests.filter((request) =>
request.input.operation === 'next-turn' && request.input.expectedReply === 'question'
).length;
const output = body.input.expectedReply === 'answer'
? { type: 'answer', text: 'The case relies on reduced traffic exposure and documented collision risk.' }
: {
type: 'question',
text: questionCount === 1
? 'What evidence connects fewer private cars to safer streets?'
: 'How would you answer the strongest enforcement objection?',
};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ output }) });
return;
}

const output = {
concessionsSecured: [{ turnIds: ['T2'], claim: 'The answer accepts that the safety link depends on traffic exposure.' }],
controlledQuestions: [{ turnIds: ['T1', 'T2'], claim: 'The opening exchange keeps the discussion on the central causal warrant.' }],
weakOrRepetitiveQuestions: [{ turnIds: ['T1'], claim: 'The question could identify the type of evidence required more precisely.' }],
evasiveOrIncompleteAnswers: [{ turnIds: ['T2'], claim: 'The answer does not name a study or traffic count.' }],
missedFollowUps: [{ turnIds: ['T2'], claim: 'The next question should test the missing evidence.' }],
betterFollowUpQuestions: [{ turnIds: ['T2'], question: 'Which study measures that reduction in collision risk?', reason: 'It turns the general answer into a checkable claim.' }],
strongestExchange: { turnIds: ['T1', 'T2'], claim: 'The first exchange isolates the case\'s main empirical dependency.' },
};
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify({ output }) });
return;
}

await route.abort('blockedbyclient');
});
}

async function prepare(page: Page, requests: CrossfireRequest[]) {
await setLocale(page, 'en');
await stubSupabaseSession(page);
await page.addInitScript(() => {
for (const key of Object.keys(window.localStorage)) {
if (key.startsWith('debatelab-crossfire-draft:')) window.localStorage.removeItem(key);
}
});
await installDeterministicBackend(page, requests);
await page.goto('/app/crossfire');
await page.getByTestId('crossfire-resolution').fill('Resolved: cities should ban private cars downtown.');
await page.getByTestId('crossfire-argument').fill('Private cars create congestion and unsafe streets.');
}

test('Question the AI completes a grounded Crossfire debrief', async ({ page }, testInfo) => {
const requests: CrossfireRequest[] = [];
await prepare(page, requests);
await page.getByTestId('mode-question-ai').click();
await page.getByTestId('style-cooperative').click();
await page.getByTestId('source-grounding').click();
await page.getByTestId('start-crossfire').click();

await page.getByTestId('crossfire-input').fill('What evidence supports the safety mechanism?');
await page.getByTestId('submit-turn').click();
await expect(page.getByText('The case relies on reduced traffic exposure and documented collision risk.')).toBeVisible();
await page.getByTestId('end-crossfire').click();

await expect(page.getByTestId('crossfire-debrief')).toBeVisible();
await expect(page.getByText('T1', { exact: true }).first()).toBeVisible();
await expect(page.getByText('T2', { exact: true }).first()).toBeVisible();
expect(requests[0].input).toMatchObject({ operation: 'next-turn', expectedReply: 'answer', config: { mode: 'question-ai', useSourceLibrary: true } });
expect(requests[0].input.transcript.at(-1)?.text).toBe('What evidence supports the safety mechanism?');
expect(requests.at(-1)?.input.operation).toBe('debrief');
await page.screenshot({ path: testInfo.outputPath('crossfire-question-ai-final.png'), fullPage: true });
});

test('Let the AI question me alternates roles and completes a debrief', async ({ page }, testInfo) => {
const requests: CrossfireRequest[] = [];
await prepare(page, requests);
await page.getByTestId('mode-ai-questions-user').click();
await page.getByTestId('style-aggressive').click();
await page.getByTestId('start-crossfire').click();

await expect(page.getByText('What evidence connects fewer private cars to safer streets?')).toBeVisible();
await page.getByTestId('crossfire-input').fill('Traffic exposure falls when trips shift to transit and walking.');
await page.getByTestId('submit-turn').click();
await expect(page.getByText('How would you answer the strongest enforcement objection?')).toBeVisible();
await page.getByTestId('end-crossfire').click();

await expect(page.getByTestId('crossfire-debrief')).toBeVisible();
expect(requests[0].input).toMatchObject({ operation: 'next-turn', expectedReply: 'question', config: { mode: 'ai-questions-user', useSourceLibrary: false } });
expect(requests[1].input.transcript.at(-1)?.text).toBe('Traffic exposure falls when trips shift to transit and walking.');
expect(requests.at(-1)?.input.operation).toBe('debrief');
await page.screenshot({ path: testInfo.outputPath('crossfire-ai-questions-user-final.png'), fullPage: true });
});
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export default defineConfig({
},
],
webServer: {
command: 'npm run dev',
command: `npm run dev -- --port ${PORT}`,
url: baseURL,
reuseExistingServer: !process.env.CI,
timeout: 120_000,
Expand Down
2 changes: 2 additions & 0 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const AttackPage = lazy(() => import("./pages/AttackPage"));
const BrainstormPage = lazy(() => import("./pages/BrainstormPage"));
const FactoidsPage = lazy(() => import("./pages/FactoidsPage"));
const AskPage = lazy(() => import("./pages/AskPage"));
const CrossfirePage = lazy(() => import("./pages/CrossfirePage"));
const SettingsPage = lazy(() => import("./pages/SettingsPage"));
const HistoryPage = lazy(() => import("./pages/HistoryPage"));
const LibraryPage = lazy(() => import("./pages/LibraryPage"));
Expand Down Expand Up @@ -118,6 +119,7 @@ function AnimatedRoutes() {
<Route path="/app/brainstorm" element={<BrainstormPage />} />
<Route path="/app/factoids" element={<FactoidsPage />} />
<Route path="/app/ask" element={<AskPage />} />
<Route path="/app/crossfire" element={<CrossfirePage />} />
<Route path="/app/history" element={<HistoryPage />} />
<Route path="/app/library" element={<LibraryPage />} />
<Route path="/app/timer" element={<TimerPage />} />
Expand Down
12 changes: 10 additions & 2 deletions src/components/features/attack/AttackResults.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// src/components/features/attack/AttackResults.tsx
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { AlertCircle, AlertTriangle, Info, Scale, Lightbulb, MessageSquare } from 'lucide-react';
import { AlertCircle, AlertTriangle, Info, Scale, Lightbulb, MessageSquare, Flame } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { SectionCard } from '@/components/ui/section-card';
import { Progress } from '@/components/ui/progress';
Expand All @@ -18,12 +18,14 @@ import { GoogleDriveAuthModal } from '@/components/features/export/GoogleDriveAu
import { SourceQualityList } from '@/components/features/shared/SourceQualityList';
import type { DiscoveredSource } from '@/lib/ai/client';
import { openExternalUrl } from '@/lib/native';
import { Button } from '@/components/ui/button';


interface AttackResultsProps {
result: AttackOutput;
discoveredSources?: DiscoveredSource[];
onDiscuss?: () => void;
onPracticeCrossfire?: () => void;
}

const severityConfig = {
Expand Down Expand Up @@ -83,7 +85,7 @@ function WeaknessCard({ weakness }: { weakness: Weakness }) {
);
}

export function AttackResults({ result, discoveredSources, onDiscuss }: AttackResultsProps) {
export function AttackResults({ result, discoveredSources, onDiscuss, onPracticeCrossfire }: AttackResultsProps) {
const { t } = useTranslation();
const [isExportingGDocs, setIsExportingGDocs] = useState(false);
const [showDriveAuth, setShowDriveAuth] = useState(false);
Expand Down Expand Up @@ -195,6 +197,12 @@ export function AttackResults({ result, discoveredSources, onDiscuss }: AttackRe
isExportingGDocs={isExportingGDocs}
onDiscuss={onDiscuss}
discussLabel={t('attackResults.discuss')}
secondaryActions={onPracticeCrossfire ? (
<Button variant="outline" size="sm" onClick={onPracticeCrossfire}>
<Flame className="mr-2 h-4 w-4" />
{t('crossfire.openFromResult', 'Open in Crossfire')}
</Button>
) : undefined}
/>
</div>
</div>
Expand Down
13 changes: 10 additions & 3 deletions src/components/features/generate/GenerateResults.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Target, Shield, Swords, AlertTriangle, MessageSquare, Save, GitBranch, Sparkles, Clipboard, Undo2 } from 'lucide-react';
import { Target, Shield, Swords, AlertTriangle, MessageSquare, Save, GitBranch, Sparkles, Clipboard, Undo2, Flame } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { SectionCard } from '@/components/ui/section-card';
Expand All @@ -26,6 +26,7 @@ interface GenerateResultsProps {
onSave?: () => void;
onUndo?: () => void;
onImportFlowchart?: () => void;
onPracticeCrossfire?: () => void;
}

function ArgumentCard({ contention, index }: { contention: Contention; index: number }) {
Expand Down Expand Up @@ -76,7 +77,7 @@ function formatDuration(ms: number | undefined, t: (key: string, options?: Recor
return t('generateResults.durationS', { value });
}

export function GenerateResults({ result, discoveredSources, onRegenerateSection, onDiscuss, onSave, onUndo, onImportFlowchart }: GenerateResultsProps) {
export function GenerateResults({ result, discoveredSources, onRegenerateSection, onDiscuss, onSave, onUndo, onImportFlowchart, onPracticeCrossfire }: GenerateResultsProps) {
const { t } = useTranslation();
const [isExportingGDocs, setIsExportingGDocs] = useState(false);
const [showDriveAuth, setShowDriveAuth] = useState(false);
Expand Down Expand Up @@ -188,8 +189,14 @@ export function GenerateResults({ result, discoveredSources, onRegenerateSection
{t('generateResults.clipboard', 'Clipboard')}
</Button>
)}
secondaryActions={(onSave || onDiscuss || onImportFlowchart || onUndo) ? (
secondaryActions={(onSave || onDiscuss || onImportFlowchart || onUndo || onPracticeCrossfire) ? (
<>
{onPracticeCrossfire && (
<Button variant="outline" size="sm" onClick={onPracticeCrossfire}>
<Flame className="mr-2 h-4 w-4" />
{t('crossfire.openFromResult', 'Open in Crossfire')}
</Button>
)}
{onSave && (
<Button variant="outline" size="sm" onClick={onSave}>
<Save className="mr-2 h-4 w-4" />
Expand Down
4 changes: 3 additions & 1 deletion src/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import {
Clock,
Timer,
Database,
GitBranch
GitBranch,
Crosshair
} from 'lucide-react';
import { NavLink } from '@/components/NavLink';
import { Button } from '@/components/ui/button';
Expand All @@ -30,6 +31,7 @@ const navItems = [
{ path: '/app/brainstorm', key: 'brainstorm', icon: Lightbulb },
{ path: '/app/factoids', key: 'factoids', icon: BookOpen },
{ path: '/app/ask', key: 'ask', icon: MessageCircle },
{ path: '/app/crossfire', key: 'crossfire', icon: Crosshair },
{ path: '/app/library', key: 'library', icon: Database },
{ path: '/app/timer', key: 'timer', icon: Timer },
{ path: '/app/flowchart', key: 'flowchart', icon: GitBranch },
Expand Down
Loading
Loading