diff --git a/components/chain-table.tsx b/components/chain-table.tsx index 94790e6..0a733a2 100644 --- a/components/chain-table.tsx +++ b/components/chain-table.tsx @@ -25,10 +25,20 @@ * the → a clearly separated connector between chips — a lonely chip * reads as a standalone proposition (the "(no premises)" annotation is * dead in the visible UI; it survives only as the row's aria-label). - * Icons: clusters FolderIcon (open when expanded), chains WaypointsIcon, - * propositions chip-only. Chevrons rotate with expansion. Statement rows - * show propositions PLAIN (owner ruling: the derivation is already - * encoded in the chain line — no conclusion marker down there). + * Icons: ONLY the cluster FolderIcon survives (owner ruling, evening + * round 3: the chain waypoints icons are not needed — the chips + + * connectors already read as chains); propositions chip-only. Chevrons + * rotate with expansion. Statement rows show propositions PLAIN (owner + * ruling: the derivation is already encoded in the chain line — no + * conclusion marker down there). + * + * THE ALIGNMENT STAIRCASE (owner ruling, round 3): every level's + * checkbox sits DIRECTLY UNDER its parent's expander arrow — open a + * folder and the chain checkboxes line up with the arrow, same one + * level down. Geometry: the expander shares the checkbox's 14px width + * (h-5 w-3.5), so each nesting step is exactly checkbox(14) + gap(4) = + * 18px — clusters pl-3 (12px, checkbox at 12), chains pl-7.5 (30px = + * the cluster expander), statements pl-12 (48px = the chain expander). * * SORTING: all three columns sortable (owner ruling — "consistency is * key"): Name (A→Z), Props (largest-first), Created (newest-first); @@ -58,7 +68,6 @@ import { FolderOpenIcon, ListTreeIcon, SearchIcon, - WaypointsIcon, XIcon, } from "lucide-react"; import { chainsForSelection, formatChainLine, fuzzyScore } from "@/src/core"; @@ -255,9 +264,11 @@ export function ChainTableBody({ {/* The list: NO per-row lines (owner ruling) — the ONLY horizontal - separators sit between cluster groups; PROMINENT indentation - carries the hierarchy (clusters → chains pl-11 → statements - pl-[4.75rem]). All three columns sort, small chevron on active. */} + separators sit between cluster groups; the ALIGNMENT STAIRCASE + carries the hierarchy (owner round 3: each level's checkbox sits + under its parent's expander arrow — clusters pl-3, chains pl-7.5 + under the folder arrow, statements pl-12 under the chain arrow). + All three columns sort, small chevron on active. */}
diff --git a/docs/architecture.md b/docs/architecture.md index ac8d4fc..d28f6f5 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -382,7 +382,7 @@ Practice turns the graph into problem sets the learner reasons through out loud. **The jobs pattern — registry, not JobRecords.** Practice runs are NOT Workbench jobs: no `JobStore` records, no steer messages, nothing ever wakes the teacher — a practice run isn't chat-scoped (a set outlives its birth session and has no lesson to re-enter; its result is itself, persisted in its own store and read by its own page). What it shares with the Workbench is the observability surface: a `globalThis` run registry (the module-instance idiom — `__lhPracticeRuns`) with PER-PROBLEM ROWS (owner ruling, 2026-09-29 — matching the extraction per-chunk pattern): `setId:partition` while the partitioner runs ("Partitioning — title"), then one `setId:gen:problemId` per problem ("queued"→"generating", label "Problem i/N — title"), and one `setId:grade:problemId` per graded answer ("queued"→"grading", "Grading i/N — title") — each row its OWN abort controller and its OWN usage accumulation; the row key IS the feed id (the dock dup-key law) and the abort target. ABORT ROUTES BY ROW SHAPE: a gen-row abort kills THAT problem only (siblings continue; the set still settles `ready` with one errored problem), a partition-row abort ends the whole run, a grade-row abort drops that grade (answer kept — one attempt, the problem stays done); set-level abort (delete) composes from row aborts plus the synchronous persisted marks. Model calls — partitioner included — run under the SHARED background-LLM semaphore (`bg-llm-semaphore.ts`): one process-wide pool of 3 permits for ALL background LLM jobs, extraction chunks included (total background LLM concurrency bounded at 3, whoever calls). A boot sweep marks orphaned `partitioning`/`generating` sets as `error "Interrupted (server restarted)"` — once per process, and only when the set's `updatedAt` heartbeat is >120s stale (a live run's 30s FILE watchdog keeps the heartbeat fresh even across dev's per-worker processes — the registry is not shared between them; the guard trio closed a live-stamping bug from the first live test) — the job runner's crash-honesty at set grain. Store writes serialize behind a per-set mutex; the pipeline is the sole writer of `data/practice`. -**API + UI.** Creation is GRAPH-DOMAIN, not chat-domain (owner ruling 2026-09-29: "problem sets always take in nodes — graph objects"): `POST /api/practice/sets` takes `{nodeIds, params, title?, note?}` ONLY — no chatId, no clusterId; the UI resolves a picked cluster to its member node ids and passes the cluster's label as the title. The `note` is the learner's optional free-form creation input (owner, same evening: more learner control, no other purpose): trimmed, capped at 2000 characters, persisted on the set, and carried to the partitioner and every generator as `THE LEARNER'S NOTE` — an additional input the agents WEIGH but are never obliged to follow (the doctrine pins the stance: it never moves a problem off its rung or out of its band, and the partition laws always win). `GET /api/graph/clusters` serves the picker: the birth-session partition itself (`birthSessionPartition`, same ordering — size desc, ties by id) — EVERY group regardless of size (owner ruling, same evening: MIN_CLUSTER_SIZE is a hull-drawing rule; practice availability must not inherit a visual threshold), each as `{key, kind: "chat"|"notes", label, nodeIds, nodeCount}`; a cluster whose naming record is gone keeps `label: null` — honest, never guessed. 202 `{id}` with generation fire-and-forget; 400 empty selection (empty intersection with live nodes — the vanished-nodes edge) / bad params; 503 model/key refusal. `GET /api/practice/sets` · `GET/DELETE /api/practice/sets/[id]` (delete aborts live runs first) · `POST /api/practice/sets/[id]/answers` (202; 409 when not ready; 400 re-answering a submitted problem — one attempt). Surfaces: `/practice` (the list — THE shared **TopBar** (`components/top-bar.tsx`, standardization ruling 2026-09-29: one element, every surface — h-15 matching the sidebar's wordmark band, `lh-drag` with `lh-no-drag` carve-outs on interactive children, `select-none` so drags never highlight, the SidebarToggle reopen with its Electron traffic-light gutter, `px-3` + one border line; chat's rename mode passes `drag={false}` — the one escape hatch) with the New action at the right edge, status/counts rows, delete confirm, the new-set dialog with the named-level controls (a 5-detent ladder slider, a 3-detent cue slider, a Theory|Application toggle — each showing the level's name + definition), the cluster picker, and the optional free-form note textarea (typing-only). The SECOND creation entry (owner redesign, same evening): the graph view's **chain table** — a `Chains` button in the graph chrome (empty graph: hidden) opens a FULL-BLEED table overlay (absolute inside the graph area; the blur + translucent background IS the surface, no inner card; the top bar is THE shared TopBar — the standard title size and drag mechanics, the sidebar reopen, and the close X at the right edge, so home's window-drag mask never eats it (the overlay covers home's floating header while open, and the TopBar's own toggle is the reopen path); Esc or X dismiss). The hierarchy is three levels, one click each: CLUSTER (the birth session — a chat or a notes file; a chain lives where its CONCLUSION was born) → REASONING CHAIN → PROPOSITION STATEMENTS — expanding a chain reveals its members' statement text directly (names live in the chain line, never twice; a singleton reveals its one statement in a single click); everything starts collapsed. The table look (owner refinements, evening round 2): PROMINENT indentation carries the hierarchy (chains deep under their folder, statements deepest) and the ONLY lines left are the horizontal separators between cluster groups — per-row lines and the vertical column hairlines are gone ("anything else is better without them"); statement rows are PLAIN (the derivation already lives in the chain line — no conclusion marker). ALL THREE columns sort (owner: "consistency is key") — Name (A→Z default), Props (largest-first), Created (newest-first) — click toggles asc/desc, ordering both cluster rows and chains within; a SMALL chevron marks the active header. In the chain line every proposition name is a faint-outlined CHIP with the → a clearly separated connector between chips — a lonely chip reads as a standalone proposition (the "(no premises)" annotation is dead in the visible UI; it survives only as the row's aria-label); icons: clusters FolderIcon (open-folder when expanded), chains WaypointsIcon, propositions chip-only; chevrons rotate with expansion. Selection is checkboxes on whole clusters, chains, or single propositions — a flat node-id set (a node in several chains is one selection) — accumulated in a FLOATING BOTTOM BAR (the Google-Photos pattern: appears only when something is selected, never shifts the table layout) showing the agents' ACTUAL input (`chainsForSelection` over the selection: a premise picked without its conclusion counts as the singleton it becomes) as `N propositions → M chains`, with Clear and **Generate problem set** (the one action for now — the bar is a neutral action slot), which opens the SAME creation dialog in **custom mode**: no cluster picker — a selection summary (the derived shape + the dated default title the pipeline will stamp) — posting `{nodeIds, params, note}` with no title. A fuzzy search (names, aliases, statements) filters chains grouped + auto-expanded under their clusters (non-matching clusters hide; the collapsed view returns on clear). NO understanding-levels column (explicitly not built). The overlay is read-only toward the graph and `/practice/[id]` (the exam page — the shared TopBar, a levels + status line, numbered problems, markdown statements with math healing at the display seam, a textarea + focus-gated dictation bar per problem with its OWN Submit button — one attempt per problem, instant solution/rubric reveal, the score landing live). Entry: the sidebar's Practice nav row. +**API + UI.** Creation is GRAPH-DOMAIN, not chat-domain (owner ruling 2026-09-29: "problem sets always take in nodes — graph objects"): `POST /api/practice/sets` takes `{nodeIds, params, title?, note?}` ONLY — no chatId, no clusterId; the UI resolves a picked cluster to its member node ids and passes the cluster's label as the title. The `note` is the learner's optional free-form creation input (owner, same evening: more learner control, no other purpose): trimmed, capped at 2000 characters, persisted on the set, and carried to the partitioner and every generator as `THE LEARNER'S NOTE` — an additional input the agents WEIGH but are never obliged to follow (the doctrine pins the stance: it never moves a problem off its rung or out of its band, and the partition laws always win). `GET /api/graph/clusters` serves the picker: the birth-session partition itself (`birthSessionPartition`, same ordering — size desc, ties by id) — EVERY group regardless of size (owner ruling, same evening: MIN_CLUSTER_SIZE is a hull-drawing rule; practice availability must not inherit a visual threshold), each as `{key, kind: "chat"|"notes", label, nodeIds, nodeCount}`; a cluster whose naming record is gone keeps `label: null` — honest, never guessed. 202 `{id}` with generation fire-and-forget; 400 empty selection (empty intersection with live nodes — the vanished-nodes edge) / bad params; 503 model/key refusal. `GET /api/practice/sets` · `GET/DELETE /api/practice/sets/[id]` (delete aborts live runs first) · `POST /api/practice/sets/[id]/answers` (202; 409 when not ready; 400 re-answering a submitted problem — one attempt). Surfaces: `/practice` (the list — THE shared **TopBar** (`components/top-bar.tsx`, standardization ruling 2026-09-29: one element, every surface — h-15 matching the sidebar's wordmark band, `lh-drag` with `lh-no-drag` carve-outs on interactive children, `select-none` so drags never highlight, the SidebarToggle reopen with its Electron traffic-light gutter, `px-3` + one border line; chat's rename mode passes `drag={false}` — the one escape hatch) with the New action at the right edge, status/counts rows, delete confirm, the new-set dialog with the named-level controls (a 5-detent ladder slider, a 3-detent cue slider, a Theory|Application toggle — each showing the level's name + definition), the cluster picker, and the optional free-form note textarea (typing-only). The SECOND creation entry (owner redesign, same evening): the graph view's **chain table** — a `Chains` button in the graph chrome (empty graph: hidden) opens a FULL-BLEED table overlay (absolute inside the graph area; the blur + translucent background IS the surface, no inner card; the top bar is THE shared TopBar — the standard title size and drag mechanics, the sidebar reopen, and the close X at the right edge, so home's window-drag mask never eats it (the overlay covers home's floating header while open, and the TopBar's own toggle is the reopen path); Esc or X dismiss). The hierarchy is three levels, one click each: CLUSTER (the birth session — a chat or a notes file; a chain lives where its CONCLUSION was born) → REASONING CHAIN → PROPOSITION STATEMENTS — expanding a chain reveals its members' statement text directly (names live in the chain line, never twice; a singleton reveals its one statement in a single click); everything starts collapsed. The table look (owner refinements, evening round 2): PROMINENT indentation carries the hierarchy (chains deep under their folder, statements deepest) and the ONLY lines left are the horizontal separators between cluster groups — per-row lines and the vertical column hairlines are gone ("anything else is better without them"); statement rows are PLAIN (the derivation already lives in the chain line — no conclusion marker). ALL THREE columns sort (owner: "consistency is key") — Name (A→Z default), Props (largest-first), Created (newest-first) — click toggles asc/desc, ordering both cluster rows and chains within; a SMALL chevron marks the active header. In the chain line every proposition name is a faint-outlined CHIP with the → a clearly separated connector between chips — a lonely chip reads as a standalone proposition (the "(no premises)" annotation is dead in the visible UI; it survives only as the row's aria-label); icons: ONLY the cluster FolderIcon (open-folder when expanded) survives (owner ruling, evening round 3: the chain waypoints icons are not needed — the chips + connectors already read as chains); chevrons rotate with expansion and share the checkbox's 14px width, which makes the ALIGNMENT STAIRCASE exact (owner ruling, same round): every level's checkbox sits directly under its parent's expander arrow — open a folder and the chain checkboxes line up with the arrow, same one level down; 18px steps (clusters pl-3 → chains pl-7.5 → statements pl-12). Selection is checkboxes on whole clusters, chains, or single propositions — a flat node-id set (a node in several chains is one selection) — accumulated in a FLOATING BOTTOM BAR (the Google-Photos pattern: appears only when something is selected, never shifts the table layout) showing the agents' ACTUAL input (`chainsForSelection` over the selection: a premise picked without its conclusion counts as the singleton it becomes) as `N propositions → M chains`, with Clear and **Generate problem set** (the one action for now — the bar is a neutral action slot), which opens the SAME creation dialog in **custom mode**: no cluster picker — a selection summary (the derived shape + the dated default title the pipeline will stamp) — posting `{nodeIds, params, note}` with no title. A fuzzy search (names, aliases, statements) filters chains grouped + auto-expanded under their clusters (non-matching clusters hide; the collapsed view returns on clear). NO understanding-levels column (explicitly not built). The overlay is read-only toward the graph and `/practice/[id]` (the exam page — the shared TopBar, a levels + status line, numbered problems, markdown statements with math healing at the display seam, a textarea + focus-gated dictation bar per problem with its OWN Submit button — one attempt per problem, instant solution/rubric reveal, the score landing live). Entry: the sidebar's Practice nav row. **Model wiring.** Three roles — `partitioner`, `problemGenerator`, `grader` — all pinned to the **strong** slot (quality-critical, one settings edit from cheap) and in `BACKGROUND_MODEL_ROLES` (per-role overrides). The stage agents are single-submit loops on the shared `runSubmitAgent` seam (`submit-agent.ts`, extracted from extraction for exactly this): a `validate` hook turns a bad submit into a tool-input error the model retries against, within `maxTurns 4`; exhaustion fails the stage honestly (the set says so, the chains re-run cleanly on a new set). diff --git a/next-env.d.ts b/next-env.d.ts index ce4e94a..a419cbe 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -1,7 +1,7 @@ /// /// -import "./.next/types/routes.d.ts"; -import "./.next/types/root-params.d.ts"; +import "./.next/dev/types/routes.d.ts"; +import "./.next/dev/types/root-params.d.ts"; // NOTE: This file should not be edited // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/tests/render-chain-table.test.tsx b/tests/render-chain-table.test.tsx index dbe54a2..1e129ec 100644 --- a/tests/render-chain-table.test.tsx +++ b/tests/render-chain-table.test.tsx @@ -139,9 +139,9 @@ describe("ChainTableBody — THE shared TopBar + the lines law (owner, round 2)" const sep = html.split("border-b border-border/60").length - 1; assert.ok(sep >= 2, "separators between groups"); // chain rows themselves carry NO border — their entire class list is - // the indent law (pl-11 under the cluster folder, owner round 2); the - // chip outlines' border-border/70 is an outline, not a row line - assert.ok(html.includes('class="flex items-center gap-1 py-1 pl-11 pr-3"'), "the chain row: indent, no border"); + // the staircase law (pl-7.5 = under the cluster's expander arrow); + // the chip outlines' border-border/70 is an outline, not a row line + assert.ok(html.includes('class="flex items-center gap-1 py-1 pl-7.5 pr-3"'), "the chain row: staircase indent, no border"); }); }); @@ -154,7 +154,9 @@ describe("ChainTableBody — chain rows (chips + connectors)", () => { assert.ok(html.includes("Premise one"), "chip text"); assert.ok(html.includes('data-testid="chain-arrow-c1-1"'), "the connector"); assert.ok(html.includes("→"), "the arrow glyph"); - assert.ok(html.includes("lucide-waypoints"), "the chain icon"); + // owner ruling (round 3): the chain waypoints icons are GONE — the + // chips + connectors already read as chains; only folders keep icons + assert.ok(!html.includes("lucide-waypoints"), "no chain icon"); }); it("a singleton renders ONE lonely chip, no connector; the annotation survives only for screen readers", () => { @@ -184,9 +186,14 @@ describe("ChainTableBody — chain rows (chips + connectors)", () => { // owner ruling: the derivation is already encoded in the chain line — // no conclusion marker down here assert.ok(!html.includes("prop-conclusion"), "no conclusion marker anywhere"); - // statements indent DEEPEST (pl-[4.75rem]); chains pl-11 under clusters - assert.ok(html.includes("pl-[4.75rem]"), "the deepest indent"); - assert.ok(html.includes("pl-11"), "the chain indent"); + // THE ALIGNMENT STAIRCASE (owner ruling, round 3): each level's + // checkbox sits under its parent's expander arrow. The expander + // shares the checkbox's 14px width, so each step is 18px: clusters + // pl-3 (12) → chains pl-7.5 (30 = the cluster expander) → + // statements pl-12 (48 = the chain expander) + assert.ok(html.includes("pl-12"), "statements under the chain's expander"); + assert.ok(html.includes("pl-7.5"), "chains under the cluster's expander"); + assert.ok(html.includes("h-5 w-3.5"), "the expander shares the checkbox's 14px width"); }); });