/** * Soft-delete primitive for the knowledge graph. * * MCP-tool callers must NEVER `DETACH DELETE` user-domain nodes. Use * `trashNode` to mark a node `:Trashed`; `restoreNode` removes the label; * `emptyTrash` hard-deletes nodes whose `trashedAt < now - graceDays`. The * 2026-04-20 incident wiped 19 nodes via a single autonomous `DETACH DELETE` * — Neo4j Community has no PITR, so properties were unrecoverable. This * primitive contains the blast radius for every caller. * * Unique-constraint handling: when the trashed node's labels carry single- * key UNIQUE constraints (e.g. `LocalBusiness.accountId`), the live values * are snapshotted into `_trashedKeys` (JSON) and nulled on the node, so * MERGE against the same key won't collide. `restoreNode` writes them back * and fails loudly when an active node already occupies the slot. * * Label-aware cascade: when `trashNode` trashes a `:Conversation`, it also * trashes every `(m:Message)-[:PART_OF]->(c)` Message in the same managed * transaction. This makes `MATCH (m:Trashed)` a correct audit primitive — * trashing a Conversation without cascading to its Messages silently * under-reports the blast radius. `restoreNode` reverses both sides. */ import type { Session } from "neo4j-driver"; export interface TrashParams { session: Session; accountId: string; elementId: string; /** Provenance marker — appears verbatim in `trashedBy` and `[trash:marked] by=`. */ by: string; reason?: string; } export interface TrashResult { trashed: boolean; alreadyTrashed: boolean; nodeId: string; /** Labels excluding `:Trashed`. */ labels: string[]; trashedAt: string; originalKeys: Record; } export declare function trashNode(params: TrashParams): Promise; export interface RestoreParams { session: Session; accountId: string; elementId: string; } export interface RestoreResult { restored: boolean; nodeId: string; labels: string[]; restoredKeys: Record; } export declare function restoreNode(params: RestoreParams): Promise; export interface EmptyTrashParams { session: Session; accountId: string; /** Default 30. */ graceDays?: number; dryRun?: boolean; /** Optional label whitelist — only nodes carrying any of these labels are eligible. */ labels?: string[]; /** Side-effect callback invoked before the `DETACH DELETE` for each * emptied node, e.g. to clean disk attachments or purge sibling JSONL * transcripts. * * Contract (Task 237): if the callback **throws**, the candidate is * **skipped this run** — no `DETACH DELETE` runs and no `[trash:emptied]` * line emits. The thrown message lands in * `[trash:empty-run] reason=onEmpty-failed` and the candidate becomes * eligible again on the next sweep. Pre-Task-237 consumers * (`removeAttachmentDir` for `:KnowledgeDocument`) cannot throw because * they wrap `fs.rm({force:true})`, so the contract change is invisible * to them. */ onEmpty?: (node: TrashCandidate) => Promise; } export interface TrashCandidate { elementId: string; labels: string[]; trashedAt: string; trashedBy: string | null; /** Original unique-key snapshot — useful for KnowledgeDocument disk cleanup. */ trashedKeys: Record; } export interface EmptyTrashResult { graceDays: number; dryRun: boolean; candidates: TrashCandidate[]; emptied: number; } export declare function emptyTrash(params: EmptyTrashParams): Promise; /** * Read-filter clause excluding trashed nodes for the given Cypher alias. * * Filters both the `:Trashed` label (primitive) and the `deletedAt` * property (KnowledgeDocument soft-delete). Belt-and-braces: a row marked * by either signal is excluded from reads. * * notTrashed("node") → "(NOT node:Trashed AND node.deletedAt IS NULL)" * notTrashed("related") → "(NOT related:Trashed AND related.deletedAt IS NULL)" */ export declare function notTrashed(alias: string): string; /** Runtime accessor for the static unique-keys map (for tests + sibling tooling). */ export declare function uniqueKeysForLabels(labels: string[]): string[]; /** Property names trashNode writes — exposed so callers can re-MERGE without colliding. */ export declare const TRASH_METADATA_PROPS: readonly string[]; //# sourceMappingURL=index.d.ts.map