The remedy for undeletable nodes (preserve-graph orphans from session deletion; notes-import nodes with no chat behind them): DELETE /api/graph/nodes/[id], full-death semantics — the user pointing at the node IS the intent; references from other sessions do NOT preserve it. - core/graph-deletion.ts deleteNodeTraces: the node's every line, every edge touching it, every quiz/observation event naming it, its mapping lines, and its resting position — one synchronous atomic rewriteLedgers call; only the ledgers that changed rewrite (mtimes stay honest). Blast radius is exactly the rows naming the node id; notes source records stay (deleting a proposition is not deleting the file). Derived data self-heals on read: the birth cluster shrinks below MIN_CLUSTER_SIZE and stops rendering. - nodeDeletionImpact (GET /api/graph/nodes/[id]): the confirm dialog's pre-flight facts — edge count, quiz/observation history, referencing sessions. The snapshot's edges + nodeSessions cover part of this, but the raw event history was nowhere on the client — hence the read. - Guards (server/graph.ts deleteGraphNode), decided + documented: NO turn/jobs gates (they never write graph ledgers; the orchestration is synchronous end to end, nothing interleaves). THE EXTRACTION GATE: 409 while ANY pipeline run is registered — a mid-flight run holds pre-deletion findings and could re-reference the dying node. The run is never aborted (its spend belongs to another session — outside this deletion's blast radius; the UI retries once the dock drains. A run started after the deletion is legitimate re-learning: fresh ledgers, fresh node id, and mergeFindings' existingIds guard ignores linkTo ids that no longer exist — no dangling edges, no resurrection. Live-verified on dev data: impact read, full-death stats, honest 404 on repeat, and the file cluster fell 3→2 and unrendered itself. 859/859 tests, typecheck/lint/build green. EOF )
42 lines
2.1 KiB
TypeScript
42 lines
2.1 KiB
TypeScript
import { deleteGraphNode, graphNodeImpact } from "@/src/server";
|
|
|
|
/**
|
|
* GET /api/graph/nodes/[id] — the deletion confirm dialog's pre-flight
|
|
* read (node interlude, owner-locked full-death semantics): what deleting
|
|
* this node would take with it. Edges touching it, its quiz/observation
|
|
* history, and the sessions that reference it — the facts the user weighs
|
|
* before pointing a destructive operation at the graph. 404 when the node
|
|
* does not exist.
|
|
*/
|
|
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const impact = graphNodeImpact(id);
|
|
if (impact === null) return Response.json({ error: "No such node" }, { status: 404 });
|
|
return Response.json(impact);
|
|
}
|
|
|
|
/**
|
|
* DELETE /api/graph/nodes/[id] — single-node deletion, FULL DEATH (owner
|
|
* ruling 2026-09-23: the user pointing at the node is intent — references
|
|
* from other sessions do not preserve it). The node's every line, every
|
|
* edge touching it, every quiz/observation event naming it, its mapping
|
|
* lines, and its position die in one atomic ledger rewrite
|
|
* (core/graph-deletion.ts deleteNodeTraces); derived data (birth
|
|
* partition, clusters) self-heals on read.
|
|
*
|
|
* Guards (server/graph.ts deleteGraphNode): 409 while ANY extraction run
|
|
* is registered — a mid-flight run holds pre-deletion findings and could
|
|
* re-reference the dying node; the run is never aborted (its spend belongs
|
|
* to another session), the UI retries once the dock drains. No turn/jobs
|
|
* gates: they never write the graph ledgers, and this orchestration is
|
|
* synchronous end to end — nothing interleaves. The response carries what
|
|
* was removed plus the new graphRevision (the refetch signal).
|
|
*/
|
|
export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
const { id } = await params;
|
|
const outcome = deleteGraphNode(id);
|
|
if (!outcome.ok) {
|
|
return Response.json({ error: outcome.error, code: outcome.code }, { status: outcome.status });
|
|
}
|
|
return Response.json({ stats: outcome.stats, graphRevision: outcome.graphRevision });
|
|
} |