Owner 2026-09-30: the confirm dialog's pre-flight impact read had to
go — what full death takes is obvious, the per-open fetch flickered,
and richer facts would only confuse as deletion semantics evolve
(rewiring is on the table). The screen is now: Delete node? / Delete
N nodes?, the names in the mono block, one permanence line, Cancel /
Delete. No loading state, no gone state (a 404 mid-run lands in the
honest failure line), no sessions join — the joins prop died too.
The dead path is deleted end to end: GET /api/graph/nodes/[id],
graphNodeImpact (server), nodeDeletionImpact (core) and their tests.
WORDING: node/node(s) is THE UI word for graph objects, standardized
across every surface's copy (owner, same ruling): the dialog titles,
the chain table's counts/search/summary/delete aria-labels, the
practice dialog's cluster counts, selection line, and empty-cluster
hint. Internal identifiers (selection.propositions) unchanged.
Rewritten dialog pins (tests/delete-node-dialog.test.tsx): minimal
contract — no facts/loading/fetch copy, singular/plural titles, the
cap, 409 dock phrasing, busy states, no 'proposition' left in the
dialog.
"This is really the view deletion should be done in" — the chain table
becomes the graph's MANAGEMENT surface. The floating selection bar
gains Delete FENCED at its right end (owner-approved placement): a
divider quarantines the destructive action from Clear and Generate,
red destructive pair + trash icon, opening the SAME full-death confirm
dialog the canvas uses.
DeleteNodeDialog extended from one node to an array:
- one node renders exactly as always; many pluralize the title
(Delete N propositions?) and list the names in the mono block,
capped at 8 + 'and K more'
- the pre-flight impact read aggregates: summed edge counts, unioned
referencing sessions (deduped, first-seen order); all-404 = gone
- confirm runs the per-node DELETEs SEQUENTIALLY — a failure stops
the run (extraction_running is never plowed through); onDeleted
now carries every id that actually died
- the chain table prunes its selection on delete and announces
lh:graph-revision so the snapshot refetches
Drift fix found en route (surfaced as NaN in the aggregated facts):
the server's NodeDeletionImpact has been {edges, sessions} only since
the quiz-events ledger died in the practice-arc schema slim — the
dialog kept rendering quizEvents/observations as undefined. The dead
fact lines are removed; the dialog now shows exactly what the server
can honestly report.
Owner polish round (2026-09-24): the two destructive-confirmation
surfaces must read as ONE design. The node dialog was the good one —
the session dialog now wears its exact look and flow.
components/delete-session-dialog.tsx (full rework):
- Red serif "Delete session?" (was neutral "Delete "title"?") — the
node dialog's destructive title, text-destructive on font-title 2xl.
- The session name below the title in JetBrains Mono at text-lg,
NO code-block chrome — the font already says code. (The node
dialog's name got the same treatment: text-xs bg-muted code block
dies, mono text-lg line lives. Owner chose below-the-title over
parens-in-the-title: long titles wrap safely below, never inside
the 25px headline.)
- TWO OPTION BUTTONS -> CHECKBOX FLOW: the locked two-button design
(preserve-graph / delete-nodes) collapses to ONE unchecked-by-default
checkbox "Also remove its graph traces" (accent-primary, the settings
panel's native-checkbox idiom) carrying the graph-touching option's
consequence copy verbatim. Unchecked = preserve-graph; checked =
delete-nodes. The unchecked promise ("everything it added to the
graph stays unless you check below") lives in the intro line now.
- Bottom row = the node dialog's exact pair: quiet Cancel + destructive
red Delete (border + text, fill on hover), "Deleting…" on the Delete
button while busy. Try-again re-derives the mode from the checkbox
(retry IS Delete pressed again; the checkbox is disabled while busy).
- DeleteSessionBody exported pure (the settings/node dialog body
pattern) for SSR pins; TRACES_COPY exported for the copy pin.
DELETE_OPTIONS dies with the two-button design.
Tests: session-context.test.tsx's two-option pins rewritten for the
node-dialog shape (red title, mono name no-chrome, unchecked checkbox
default, red Delete pair, old testids absent); delete-node-dialog
pins the chrome-less text-lg name. 905/905. Live-verified on :3001:
both dialogs render 25px red serif titles, 16px mono names, matching
red Delete buttons; Esc closes both, nothing deleted in verification.
Owner polish dispatch (2026-09-23):
- Right-click no longer pins a node or opens the detail card: the
canvas pointerdown/up handlers now gate on the primary button, so
the context menu arrives alone.
- Node context menu: label is just "Delete" and the shell hugs its
content (w-44 -> w-fit); the menu body keeps focus for arrow
cycling but never shows the browser's default focus ring
(outline-none on the MenuShell body - applies to every menu).
- Delete confirm: "Delete node?" title in CMU Serif, red
(text-destructive); the node name sits below in JetBrains Mono
inline-code; the Delete button carries the destructive treatment
(border + tint, fill deepens on hover). Rest of the dialog stays
neutral.
- Sidebar rename row: text and metadata stay at the idle row's x -
the status spacer and the input's border/padding are gone; the
underline is the only editing cue.
Platform's 50bfcba engine, surfaced. Three pieces:
1. THE RIGHT-CLICK (graph-canvas.ts): a contextmenu listener — always
preventDefault (the native menu on a full-bleed custom canvas is
never useful) — picks the node under the pointer and hands the
canvas-relative position to a new onContext callback (null on empty
stage). pickAt already respected the visibility filters: a hidden
node cannot be right-clicked, same as it cannot be clicked. The
view opens a MenuShell anchored at a zero-size point-div positioned
at the pointer — MenuShell's placement machine measures it like any
element, so the menu gets the full discipline: placement, focus on
open, arrow cycling, honest-fit flip. Delete… is its one action;
the dismissal contract is the sidebar's (wave 6): outside-click /
Esc (claimed) close, and a camera move closes it too — a menu
floating over a moved graph is anchored to nothing.
2. THE CONFIRM (delete-node-dialog.tsx, portaled z-50 like the
session-delete dialog whose tone it follows): the pre-flight impact
read (GET /api/graph/nodes/[id]) renders the plain facts — link
count, quiz-answer count, session-note count (zeros stay visible —
hiding a zero fact would understate the blast), and the referencing
sessions with display names joined CLIENT-SIDE from the snapshot
(chat titles; imported-notes file paths; unknown ids honestly
themselves). "This cannot be undone" says the permanence. The 409
extraction_running renders the dock phrasing (EXTRACTION_RETRY_LINE —
the server names the extraction, the ACTIVITY DOCK is the surface
the user actually watches drain) with a Try-again; every other
failure shows the server's line verbatim. The honest 404 (deleted
elsewhere meanwhile) states it plainly and its Close rides the same
refresh. The pure body is exported separately (the settings-dialog
pattern) — SSR pins everything the live dialog shows.
3. SUCCESS: onDeleted dispatches lh:graph-revision with the returned
graphRevision — the wave-6 chain refetches snapshot + costs. The
detail card closes if it pointed at the dying node (the refetch
would close it anyway; the same frame should not show a card for a
node the user just killed). Clusters shrink / unrender below the
minimum on the server's read — the client just renders the
refreshed snapshot.
Tests: 867/867 (+8 — the join helper, honest plurals, the 409 phrasing
pin, the dialog body at loading/gone/impact/failure states).
Live-verified on :3001 against the real engine, scratch data: impact
facts match the API, the intercepted 409 shows the dock line and
Try-again recovers into a real delete, the three-member cluster died
below MIN_CLUSTER_SIZE server-side, the labeled seedplan cluster +
its canvas label died with it (cold load draws nothing stale; the
in-session refresh refetches and the dying node's label stops
drawing), other nodes survived, and the full menu contract holds
(empty stage → no menu; Esc, outside-click, camera move all close).