From 814c2ad41223e8dc5221cf11ab1eafed6d9af622 Mon Sep 17 00:00:00 2001 From: amosblomqvist Date: Sun, 4 Oct 2026 21:34:24 +0300 Subject: [PATCH] delete the batch practice-problems module (superseded by practice sessions) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner ruling 2026-10-04: the module is a sunk cost, retrievable from history if ever revisited. Removed: core practice contracts + pipeline helpers, the PracticeStore port + adapter + data dir path, server service + API routes (sets, clusters picker, practice aborts), the practice pages + dialog/exam/list UI, chain-table's Generate handoff, sidebar nav, activity-dock practice rows, and the partitioner/ problemGenerator/grader model roles (BACKGROUND_MODEL_ROLES 8 → 5). Survivors, moved to core/chains.ts (pure + browser-safe): ReasoningChain, chainsForSelection, formatChainLine — the chain table and graph emission still consume them. --- app/api/activity/abort/route.ts | 16 +- app/api/graph/clusters/route.ts | 11 - app/api/practice/sets/[id]/answers/route.ts | 20 - app/api/practice/sets/[id]/route.ts | 29 - app/api/practice/sets/route.ts | 28 - app/practice/[id]/page.tsx | 17 - app/practice/page.tsx | 14 - components/activity-dock.tsx | 62 +- components/chain-table.tsx | 35 +- components/graph-view.tsx | 9 +- components/practice-dialog.tsx | 466 --------- components/practice-exam.tsx | 445 -------- components/practice-list.tsx | 304 ------ components/setup-dialog.tsx | 2 +- components/sidebar.tsx | 17 +- config.ts | 9 +- src/agent/subagents/bg-llm-semaphore.ts | 8 +- src/agent/subagents/extraction.ts | 8 +- src/agent/subagents/practice-pipeline.ts | 652 ------------ src/agent/subagents/practice.ts | 567 ----------- src/agent/subagents/submit-agent.ts | 2 +- src/agent/tools.ts | 2 +- src/core/chains.ts | 67 ++ src/core/contracts.ts | 3 +- src/core/contracts/graph.ts | 24 +- src/core/contracts/practice.ts | 200 ---- src/core/events.ts | 4 +- src/core/graph.ts | 11 +- src/core/index.ts | 2 +- src/core/ports.ts | 28 - src/core/practice.ts | 119 --- src/core/settings.ts | 9 - src/server/chat-handler.ts | 17 - src/server/events-stream.ts | 2 +- src/server/graph.ts | 25 +- src/server/index.ts | 1 - src/server/observability.ts | 28 - src/server/practice.ts | 152 --- src/server/stores.ts | 2 - src/store/practice.ts | 192 ---- tests/practice-agents.test.ts | 404 -------- tests/practice-pipeline.test.ts | 1019 ------------------- tests/practice-service.test.ts | 69 -- tests/practice-store.test.ts | 184 ---- tests/practice.test.ts | 153 --- tests/render-chain-table.test.tsx | 6 +- tests/render-practice.test.tsx | 438 -------- tests/settings-body.test.ts | 2 +- tests/settings.test.ts | 5 +- tests/zero-keys.test.tsx | 3 - 50 files changed, 125 insertions(+), 5767 deletions(-) delete mode 100644 app/api/graph/clusters/route.ts delete mode 100644 app/api/practice/sets/[id]/answers/route.ts delete mode 100644 app/api/practice/sets/[id]/route.ts delete mode 100644 app/api/practice/sets/route.ts delete mode 100644 app/practice/[id]/page.tsx delete mode 100644 app/practice/page.tsx delete mode 100644 components/practice-dialog.tsx delete mode 100644 components/practice-exam.tsx delete mode 100644 components/practice-list.tsx delete mode 100644 src/agent/subagents/practice-pipeline.ts delete mode 100644 src/agent/subagents/practice.ts create mode 100644 src/core/chains.ts delete mode 100644 src/core/contracts/practice.ts delete mode 100644 src/core/practice.ts delete mode 100644 src/server/practice.ts delete mode 100644 src/store/practice.ts delete mode 100644 tests/practice-agents.test.ts delete mode 100644 tests/practice-pipeline.test.ts delete mode 100644 tests/practice-service.test.ts delete mode 100644 tests/practice-store.test.ts delete mode 100644 tests/practice.test.ts delete mode 100644 tests/render-practice.test.tsx diff --git a/app/api/activity/abort/route.ts b/app/api/activity/abort/route.ts index b6a348a..2f8eccb 100644 --- a/app/api/activity/abort/route.ts +++ b/app/api/activity/abort/route.ts @@ -17,7 +17,7 @@ * Extraction abort is idempotent by design (aborting with nothing in * flight is success — the blob is gone either way) -> 200. */ -import { abortExtraction, abortJob, abortTurn, practice } from "@/src/server"; +import { abortExtraction, abortJob, abortTurn } from "@/src/server"; export async function POST(req: Request) { let body: unknown; @@ -27,11 +27,9 @@ export async function POST(req: Request) { return Response.json({ error: "Invalid JSON body" }, { status: 400 }); } const { kind, id } = (body ?? {}) as { kind?: string; id?: string }; - if ((kind !== "job" && kind !== "extraction" && kind !== "turn" && kind !== "practice") || typeof id !== "string" || id === "") { - return Response.json({ error: 'Body must be {kind: "job"|"extraction"|"turn"|"practice", id: string}' }, { status: 400 }); + if ((kind !== "job" && kind !== "extraction" && kind !== "turn") || typeof id !== "string" || id === "") { + return Response.json({ error: 'Body must be {kind: "job"|"extraction"|"turn", id: string}' }, { status: 400 }); } - // Practice row keys carry ":" (setId:gen:problemId) — the charset - // admits it for every kind (job/turn/extraction ids never use it). if (!/^[A-Za-z0-9:_-]+$/.test(id)) { return Response.json({ error: "Invalid id" }, { status: 400 }); } @@ -51,14 +49,6 @@ export async function POST(req: Request) { } // kind === "extraction": true cancellation; idempotent (nothing in // flight is success — same dock outcome) - if (kind === "practice") { - // practice restructure (owner, 2026-09-29): id is the ROW key — - // abort routes by shape (a gen row kills THAT problem only; the - // partition row ends the run; a grade row drops that grade). - // Idempotent like extraction (nothing in flight is success). - practice.abortPracticeRow(id); - return Response.json({ ok: true }); - } abortExtraction(id); return Response.json({ ok: true }); } diff --git a/app/api/graph/clusters/route.ts b/app/api/graph/clusters/route.ts deleted file mode 100644 index 8669b13..0000000 --- a/app/api/graph/clusters/route.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * GET /api/graph/clusters — the birth-session groups for the practice - * creation picker (owner ruling 2026-09-29: sets are created from graph - * clusters, node ids only). Thin transport; the grouping is the graph - * view's own (server/graph.ts → graphSnapshot's clusters). - */ -import { graphClusters } from "@/src/server"; - -export async function GET() { - return Response.json({ clusters: graphClusters() }, { headers: { "cache-control": "no-store" } }); -} diff --git a/app/api/practice/sets/[id]/answers/route.ts b/app/api/practice/sets/[id]/answers/route.ts deleted file mode 100644 index 2e54636..0000000 --- a/app/api/practice/sets/[id]/answers/route.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * POST /api/practice/sets/[id]/answers — submit answers for grading - * (202; one grading job per answered problem runs in the background). - * Body: {answers: [{problemId, text}]}. Set must be ready; answers must - * be non-empty text on known, not-yet-answered problems (400/409 via the - * service's typed-error mapping). ONE ATTEMPT (owner 2026-09-29): once a - * problem carries an answer it is done — re-submission is refused. - */ -import { submitPracticeAnswersResponse } from "@/src/server"; - -export async function POST(req: Request, { params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - let body: unknown; - try { - body = await req.json(); - } catch { - return Response.json({ error: "Invalid JSON body" }, { status: 400 }); - } - return submitPracticeAnswersResponse(id, body); -} diff --git a/app/api/practice/sets/[id]/route.ts b/app/api/practice/sets/[id]/route.ts deleted file mode 100644 index 54c4e4c..0000000 --- a/app/api/practice/sets/[id]/route.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * GET /api/practice/sets/[id] — one problem set (the exam page polls - * this while status ≠ ready; graph-view's reveal-poll idiom). - * DELETE — abort the set's live jobs, then drop the record (the unified - * delete doctrine: no orphans, no zombies). 404 when never existed. - */ -import { getPracticeSet, practice, practiceErrorResponse } from "@/src/server"; - -export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - try { - const set = getPracticeSet(id); - if (!set) return Response.json({ error: "no practice set", code: "not_found" }, { status: 404 }); - return Response.json({ set }); - } catch (err) { - return practiceErrorResponse(err); - } -} - -export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - try { - const existed = await practice.deleteProblemSet(id); - if (!existed) return Response.json({ error: "no practice set", code: "not_found" }, { status: 404 }); - return Response.json({ ok: true }); - } catch (err) { - return practiceErrorResponse(err); - } -} diff --git a/app/api/practice/sets/route.ts b/app/api/practice/sets/route.ts deleted file mode 100644 index 8985da4..0000000 --- a/app/api/practice/sets/route.ts +++ /dev/null @@ -1,28 +0,0 @@ -/** - * POST /api/practice/sets — create a problem set (202 {id}; generation - * runs in the background — the fire-and-forget pipeline). Body: - * {nodeIds, params, title?, note?} — a node selection, the cluster's - * label, and the learner's optional - * free-form note - * Validation + typed-error mapping live in the service (server/practice). - * GET — the set list (createdAt desc, the store's order). - */ -import { createPracticeSetResponse, listPracticeSets, practiceErrorResponse } from "@/src/server"; - -export async function POST(req: Request) { - let body: unknown; - try { - body = await req.json(); - } catch { - return Response.json({ error: "Invalid JSON body" }, { status: 400 }); - } - return createPracticeSetResponse(body); -} - -export async function GET() { - try { - return Response.json({ sets: listPracticeSets() }); - } catch (err) { - return practiceErrorResponse(err); - } -} diff --git a/app/practice/[id]/page.tsx b/app/practice/[id]/page.tsx deleted file mode 100644 index fb3aee6..0000000 --- a/app/practice/[id]/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/** - * One exam page (slice 5): the set server-prefetched, notFound() on an - * unknown id; the client body owns polling (generation + grading), - * answering, and the reveal. - */ -import { notFound } from "next/navigation"; -import { getPracticeSet } from "@/src/server"; -import { PracticeExam } from "@/components/practice-exam"; - -export const dynamic = "force-dynamic"; - -export default async function Page({ params }: { params: Promise<{ id: string }> }) { - const { id } = await params; - const set = getPracticeSet(id); - if (!set) notFound(); - return ; -} diff --git a/app/practice/page.tsx b/app/practice/page.tsx deleted file mode 100644 index 8a41512..0000000 --- a/app/practice/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -/** - * The practice list page (slice 5): the set list, server-prefetched - * (force-dynamic — a static list would go stale between requests), the - * client body owns polling + dialogs. Reads through the server barrel - * only (app/ never touches src/store directly). - */ -import { listPracticeSets } from "@/src/server"; -import { PracticeListPage } from "@/components/practice-list"; - -export const dynamic = "force-dynamic"; - -export default function Page() { - return ; -} diff --git a/components/activity-dock.tsx b/components/activity-dock.tsx index 5e9273f..2a8bf34 100644 --- a/components/activity-dock.tsx +++ b/components/activity-dock.tsx @@ -1,7 +1,7 @@ "use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react"; -import { ActivityIcon, BotIcon, FileTextIcon, GraduationCapIcon, PenToolIcon, PlayIcon, SearchIcon, SquareIcon, WaypointsIcon } from "lucide-react"; +import { ActivityIcon, BotIcon, FileTextIcon, PenToolIcon, PlayIcon, SearchIcon, SquareIcon, WaypointsIcon } from "lucide-react"; import { cn } from "@/lib/utils"; import type { CallUsage, JobKind, JobRecord } from "@/src/core"; import { isModelsRefusal, isProviderKeyRefusal, jobCost } from "@/src/core"; @@ -65,24 +65,9 @@ export interface FeedExtraction { startedAt: number; } -/** A live practice row — per-problem (owner ruling, 2026-09-29), - * matching the extraction per-chunk pattern. id is the ROW KEY - * (setId:partition | setId:gen:pid | setId:grade:pid) — the abort - * seam is row-keyed; the label carries the i/N. */ -export interface FeedPractice { - id: string; - kind: "generation" | "grading"; - title: string; - phase: string; - startedAt: number; - problemIndex?: number; - problemTotal?: number; -} - export interface ActivityFeed { jobs: FeedJob[]; extractions: FeedExtraction[]; - practice?: FeedPractice[]; } const EMPTY_FEED: ActivityFeed = { jobs: [], extractions: [] }; @@ -98,12 +83,12 @@ const ActivityContext = createContext<{ feed: ActivityFeed; refresh: () => void * THE ACTIVITY FEED, PUSH-DRIVEN (architecture S5, 2026-10-02): the * adaptive poll is RETIRED — the dock refetches exactly when truth moves. * Every cause is announced on the events channel: extraction phase - * transitions and practice rows (activity-changed), job writes + * transitions (activity-changed), job writes * (jobs-changed — the running-jobs blobs), and channel resync (reconnect / * dev-restart catch-up) — plus the one client-local cause that predates * the channel: visibility, a tab becoming visible refetches at once. - * Announced fetches ride a 250ms trailing debounce (extraction phases and - * practice progress burst); resync and visibility are immediate. A failed + * Announced fetches ride a 250ms trailing debounce (extraction phase + * bursts); resync and visibility are immediate. A failed * fetch keeps the last feed (the dock never flickers to empty on a blip). */ export function ActivityProvider({ children }: { children: React.ReactNode }) { @@ -122,7 +107,7 @@ export function ActivityProvider({ children }: { children: React.ReactNode }) { if (res.ok) { const data = (await res.json()) as Partial; if (alive) - setFeed({ jobs: data.jobs ?? [], extractions: data.extractions ?? [], practice: data.practice ?? [] }); + setFeed({ jobs: data.jobs ?? [], extractions: data.extractions ?? [] }); } } catch { // transport blip: keep the last feed, announce again next event @@ -177,18 +162,14 @@ export function useActivity(): { feed: ActivityFeed; refresh: () => void } { // ─── Pure presentation helpers (exported for tests) ────────────────────────── -/** One dock blob: the unified view of a job, an extraction run, or a - * practice run. */ +/** One dock blob: the unified view of a job or an extraction run. */ export interface ActivityBlob { - /** job.id, the extraction job's row id (source id, or "src#i" for - * chunk jobs — unique per row, per-chunk jobs owner 2026-09-28), or - * the practice SET id (the abort seam is set-keyed). */ + /** job.id, or the extraction job's row id (source id, or "src#i" for + * chunk jobs — unique per row, per-chunk jobs owner 2026-09-28). */ id: string; - process: "job" | "extraction" | "practice"; - /** Sub-agent kind for jobs (extraction/practice blobs have none). */ + process: "job" | "extraction"; + /** Sub-agent kind for jobs (extraction blobs have none). */ jobKind?: JobKind; - /** Practice run kind (generation | grading) — practice blobs only. */ - practiceKind?: "generation" | "grading"; /** Extraction phase (extracting/auditing/merging/embedding); jobs carry none. */ phase?: string; title: string; @@ -269,20 +250,6 @@ export function toBlobs(feed: ActivityFeed): ActivityBlob[] { activity: "", }); } - for (const run of feed.practice ?? []) { - blobs.push({ - id: run.id, // the ROW key — practice abort is row-keyed - process: "practice", - practiceKind: run.kind, - phase: run.phase, - title: run.title, - chatId: "", // practice is not chat-scoped - startedAt: run.startedAt, - cost: null, // practice runs ride no live cost channel (v1) - context: null, - activity: "", - }); - } blobs.sort((a, b) => a.startedAt - b.startedAt); return blobs; } @@ -346,7 +313,7 @@ export function ActivitySection({ feed: feedProp }: { feed?: ActivityFeed }) { body: JSON.stringify({ kind: blob.process, // Extractions abort by SOURCE id (chatId) — chunk rows share one - // source and die together; practice aborts by SET id (blob.id). + // source and die together. id: blob.process === "extraction" ? blob.chatId : blob.id, }), }); @@ -420,14 +387,11 @@ function ActivityBlobRow({ abortState: AbortState | undefined; onAbort: () => void; }) { - const Icon = - blob.process === "extraction" ? WaypointsIcon : blob.process === "practice" ? GraduationCapIcon : (blob.jobKind && KIND_ICON[blob.jobKind]) || BotIcon; + const Icon = blob.process === "extraction" ? WaypointsIcon : (blob.jobKind && KIND_ICON[blob.jobKind]) || BotIcon; const label = blob.process === "extraction" ? `Extraction — ${blob.phase ?? "running"}` - : blob.process === "practice" - ? `Practice — ${blob.phase ?? "running"}` - : blob.jobKind === "research" + : blob.jobKind === "research" ? "Researcher" : blob.jobKind === "demo" ? "Demo builder" diff --git a/components/chain-table.tsx b/components/chain-table.tsx index eb09a0f..d333e05 100644 --- a/components/chain-table.tsx +++ b/components/chain-table.tsx @@ -40,7 +40,7 @@ * the Google-Photos pattern — appears only when something is selected, * showing the agents' ACTUAL input (chainsForSelection over the flat * node-id selection: a premise without its conclusion counts as the - * singleton it becomes) + Clear + Generate problem set. DELETE (owner, + * singleton it becomes) + Clear. DELETE (owner, * 2026-09-29 night: "this is really the view deletion should be done * in") rides the same bar FENCED at the right end — divider + the * destructive red pair — and opens the SAME full-death confirm dialog @@ -51,8 +51,7 @@ * auto-expanded under their clusters (non-matching clusters hide; the * collapsed view returns on clear). * - * Read-only toward the graph. Named neutrally (ChainTable, not - * Practice*) — only the Generate handoff knows about practice. + * Read-only toward the graph. */ import { useEffect, useMemo, useState } from "react"; @@ -70,7 +69,6 @@ import { import { chainsForSelection, formatChainLine, fuzzyScore } from "@/src/core"; import type { GraphChain, GraphNode } from "@/src/core"; import { TopBar } from "@/components/top-bar"; -import { PracticeDialog } from "@/components/practice-dialog"; import { DeleteNodeDialog } from "@/components/delete-node-dialog"; import { LatexGlyph } from "@/components/latex-glyph"; @@ -113,7 +111,6 @@ export function ChainTableBody({ onToggleChain, onToggleNodes, onClear, - onGenerate, onDelete, onClose, }: { @@ -134,7 +131,6 @@ export function ChainTableBody({ * proposition checks all reduce to this. */ onToggleNodes: (ids: string[], next: boolean) => void; onClear: () => void; - onGenerate: () => void; /** Opens the full-death confirm dialog (owner, 2026-09-29 night). */ onDelete: () => void; onClose: () => void; @@ -235,7 +231,7 @@ export function ChainTableBody({
{/* THE shared top bar (standardization ruling): title at the standard size, drag mechanics, sidebar reopen, traffic-light gutter — the - same element as every chat/practice surface. Search + close + same element as every chat surface. Search + close carve out with lh-no-drag (the mask law: later in the DOM). */} Chains @@ -498,15 +494,7 @@ export function ChainTableBody({ > Clear - - {/* The danger fence (owner ruling): a divider quarantines + {/* The danger fence (owner ruling): a divider quarantines the destructive action at the bar's right end — the safe actions and the kill switch never blend. The full-death confirm dialog catches the misclick. */} @@ -576,7 +564,7 @@ function RowCheckbox({ } /** The graph view's overlay wrapper: owns query + sort + selection + - * expansion + the custom practice dialog; mounts ABSOLUTE INSIDE the + * expansion; mounts ABSOLUTE INSIDE the * graph area, full-bleed (owner ruling: the translucent blurred surface * IS the overlay — no inner card). Esc + the top-bar X dismiss (there * is no backdrop margin to click anymore). Read-only toward the graph. */ @@ -600,19 +588,18 @@ export function ChainTableOverlay({ const [selection, setSelection] = useState>(() => new Set()); const [expandedClusters, setExpandedClusters] = useState>(() => new Set()); const [expandedChains, setExpandedChains] = useState>(() => new Set()); - const [dialogOpen, setDialogOpen] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); // Esc dismisses (unless a dialog has focus — each dialog's own Esc // owns its layer; all listen on window, so the guards are explicit). useEffect(() => { - if (!open || dialogOpen || deleteOpen) return; + if (!open || deleteOpen) return; const onKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") onClose(); }; window.addEventListener("keydown", onKeyDown); return () => window.removeEventListener("keydown", onKeyDown); - }, [open, dialogOpen, deleteOpen, onClose]); + }, [open, deleteOpen, onClose]); if (!open) return null; @@ -665,18 +652,10 @@ export function ChainTableOverlay({ onToggleChain={(id) => setExpandedChains((prev) => toggleIn(prev, id))} onToggleNodes={toggleNodes} onClear={() => setSelection(new Set())} - onGenerate={() => setDialogOpen(true)} onDelete={() => setDeleteOpen(true)} onClose={onClose} />
- {dialogOpen && ( - setDialogOpen(false)} - /> - )} {deleteOpen && ( ({ id: n.id, name: n.name }))} diff --git a/components/graph-view.tsx b/components/graph-view.tsx index 12e3649..2cf337b 100644 --- a/components/graph-view.tsx +++ b/components/graph-view.tsx @@ -880,12 +880,11 @@ export function GraphChromeRow({ )} )} - {/* The chain table (chains overlay, owner plan 2026-09-29) — the - graph's free-form practice entry. Own container (36px band), - between the search and Import. */} + {/* The chain table (chains overlay, owner plan 2026-09-29). Own + container (36px band), between the search and Import. */} {!isEmpty && (
- + - ))} - {clusters.length === 0 && ( -
- No clusters yet — a cluster appears once a session births its - first node. -
- )} -
- - )} - - {/* CUSTOM mode: no picker — the chain table already chose. The - dated default title (the pipeline's own `Practice — date`, - stamped server-side when no title is posted) + the derived - shape of the handoff. */} - {mode === "custom" && ( -
- - Selection - -
-
Practice — {new Date().toISOString().slice(0, 10)}
-
- {selection?.propositions ?? 0} {selection?.propositions === 1 ? "node" : "nodes"} →{" "} - {selection?.chains ?? 0} {selection?.chains === 1 ? "chain" : "chains"} -
-
-
- )} - - {/* The three parameters — named levels, never a number to - interpolate (owner, 2026-09-29). Stepped sliders snap to the - detents; each shows the level's NAME + its one-line - definition; presentation is a two-segment toggle. */} -
-
- - Ladder - 0 · 1 · 2 · 3 · 4 - - - onParam({ - ladderRung: Number( - e.target.value, - ) as PracticeParams["ladderRung"], - }) - } - className="h-1.5 w-full accent-primary" - /> - - - {LADDER_RUNGS[params.ladderRung].name} - - - {" "} - — {LADDER_RUNGS[params.ladderRung].definition} - - -
-
- - Cue distance - 0 · 1 · 2 - - - onParam({ - cueDistance: Number( - e.target.value, - ) as PracticeParams["cueDistance"], - }) - } - className="h-1.5 w-full accent-primary" - /> - - - {CUE_LEVELS[params.cueDistance].name} - - - {" "} - — {CUE_LEVELS[params.cueDistance].definition} - - -
-
- - Presentation - -
- {(["theory", "application"] as const).map((pres) => ( - - ))} -
- - {PRESENTATIONS[params.presentation].definition} - -
- {/* The learner's free-form note — the last word before create: - wishes for the problems, weighed by the agents, nothing owed. */} -
- - Anything else? (optional) - -