practice fix — creation is graph-domain: cluster picker, nodeIds-only API (owner ruling 2026-09-29)

Owner ruling: 'problem sets always take in nodes — graph objects' —
practice is graph-domain, not chat-domain. No session-created sets.

API: POST /api/practice/sets takes {nodeIds, params, title?} ONLY —
the chatId branch, UnknownChatError, and the any-provenance filter are
deleted; the selector is nodeIds (readNodes filtered to the selection,
unknown ids drop, empty live intersection → 400 empty_selection);
title optional from the UI (the picker's cluster label), dated default
stays. chatStore leaves the pipeline deps entirely (labels resolve
store-side in the clusters service).

GET /api/graph/clusters (new): the birth-session groups the graph view
renders — graphSnapshot's clusters verbatim (same ordering, same
MIN_CLUSTER_SIZE presentation filter), mapped to {key, kind
chat|notes, label (null when the naming record is gone — honest,
never guessed), nodeIds, nodeCount}; corrupt/empty graph → empty
list. GraphClusterGroup contract in core/contracts/graph.ts (client-
safe — the dialog imports it).

practice-dialog: session picker → cluster picker (rows show label +
proposition count, notes clusters marked); Create posts the cluster's
nodeIds + label as title + params. 400 empty-selection copy reworded
for the vanished-nodes edge ('Nothing practicable is left in that
cluster — the graph changed.'); 503 deep link unchanged. Preselected
mode gone — the dialog is the only creation entry for now (the graph-
view cluster entry comes later with free-form selection).

Sidebar: session right-click 'Practice this session's propositions'
removed (item + state + dialog wiring); the /practice nav row stays.

Tests: pipeline — chatId/any-provenance tests die, nodeIds selection
pins added (unknown-id filtering, dated-default title, title hint
rides, empty/vanished → EmptySelectionError); render — cluster picker
pins (label+count rows, notes kind, unlabeled cluster muted, empty
graph line), preselected pins removed; 16 render + 13 pipeline.

Docs: architecture.md §7 API+UI paragraph rewritten (graph-domain
ruling, /api/graph/clusters, cluster picker as the only entry, no
404-unknown-chat); README creation-flow line updated.

1130/1130, tsc 0, lint 0, build 0.
This commit is contained in:
amosblomqvist
2026-09-29 19:26:37 +03:00
parent 451caba1b1
commit f39e1b4294
13 changed files with 205 additions and 199 deletions
-24
View File
@@ -9,7 +9,6 @@ import type { ChatDeleteMode } from "@/src/core";
import { GraduationCapIcon, PanelLeftCloseIcon, PanelLeftOpenIcon, PlusIcon, SearchIcon, SettingsIcon } from "lucide-react";
import { ActivitySection } from "@/components/activity-dock";
import { CoverageMenuBlock } from "@/components/extraction-coverage";
import { PracticeDialog } from "@/components/practice-dialog";
import { LhMark } from "@/components/lh-mark";
import { BrailleSpinner } from "@/components/braille-spinner";
import { KeyboardShortcuts } from "@/components/keyboard-shortcuts";
@@ -398,9 +397,6 @@ function SessionsSidebar() {
const ctxAnchorRef = useRef<HTMLElement | null>(null);
const [renamingId, setRenamingId] = useState<string | null>(null);
const [deleting, setDeleting] = useState<ChatListItem | null>(null);
// Practice slice 5 (2026-09-29): the right-click "Practice this session's
// propositions" opens the creation dialog PRESELECTED on that session.
const [practiceChat, setPracticeChat] = useState<{ chatId: string; title: string } | null>(null);
const openContext = (e: React.MouseEvent<HTMLButtonElement>, chat: ChatListItem) => {
e.preventDefault(); // suppress the native menu
@@ -736,21 +732,6 @@ function SessionsSidebar() {
Delete…
</button>
<div className="my-0.5 border-t border-border/60" aria-hidden="true" />
{/* Practice (slice 5): the session's propositions → a problem set.
Sits ABOVE the coverage block — an action like Extract, not
management like Rename/Delete. */}
<button
type="button"
data-testid="ctx-practice"
onClick={() => {
const target = chats?.find((c) => c.id === ctxChatId);
if (target) setPracticeChat({ chatId: target.id, title: target.title });
setCtxChatId(null);
}}
className="flex items-center rounded-lg px-2.5 py-1.5 text-left text-foreground transition-colors hover:bg-accent"
>
Practice this session&apos;s propositions
</button>
<CoverageMenuBlock chatId={ctxChatId} />
</MenuShell>
)}
@@ -762,11 +743,6 @@ function SessionsSidebar() {
onClose={() => setDeleting(null)}
onDeleted={onDeleted}
/>
{/* The preselected practice dialog (slice 5) — portaled by the
component itself, here for state locality like the delete dialog. */}
{practiceChat && (
<PracticeDialog preselected={practiceChat} onClose={() => setPracticeChat(null)} />
)}
</aside>
);
}