/**
 * Deny-list storage + agent-directed expunge helper.
 *
 * Historical note: this file once hosted `runPrune`, a four-rule autonomous
 * engine (orphan-delete, deny-list null, candidate-merge flag, contradiction
 * flag) invoked by a 03:00 cron and the `graph-prune-run` MCP tool. The
 * 2026-04-20 incident — one `graph-prune-run` call wiped 19 nodes via the
 * orphan-delete rule — ended the autonomous-rule doctrine for graph hygiene. The LLM agent now applies
 * discretion on what to null / delete / flag, using `memory-update`,
 * `memory-delete` (soft-delete), and `conversation-memory-expunge`
 * on a per-case basis. The operator-curated deny-list remains as a stored
 * signal the agent reads and reasons about; nothing auto-applies it.
 *
 * What this file still exports:
 *   - `DenyList`, `loadDenylist`, `saveDenylist`, `denylistPath` — operator
 *     curation storage for the `graph-prune-denylist-{add,list,remove}` tools.
 *     Reads are advisory context for the agent, not automatic hits.
 *   - `resolveConfigDir` — brand config-dir resolver, shared with other plugins.
 *   - `expungeConversationMemory` — the helper behind the manual
 *     `conversation-memory-expunge` tool. Agent-directed: caller passes a
 *     sessionId + pattern, the helper nulls matching string properties on
 *     `:Memory` nodes linked to that conversation. No bulk logic.
 */
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { homedir } from "node:os";
export function denylistPath(configDir) {
    return resolve(configDir, "graph-prune-denylist.json");
}
export function loadDenylist(configDir) {
    const path = denylistPath(configDir);
    if (!existsSync(path)) {
        return { values: [], updatedAt: new Date(0).toISOString() };
    }
    try {
        const raw = JSON.parse(readFileSync(path, "utf-8"));
        return {
            values: Array.isArray(raw.values) ? raw.values.filter((v) => typeof v === "string") : [],
            updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : new Date(0).toISOString(),
        };
    }
    catch (err) {
        process.stderr.write(`[denylist] failed to parse ${path}: ${err instanceof Error ? err.message : String(err)} — treating as empty\n`);
        return { values: [], updatedAt: new Date(0).toISOString() };
    }
}
export function saveDenylist(configDir, list) {
    mkdirSync(configDir, { recursive: true });
    writeFileSync(denylistPath(configDir), JSON.stringify(list, null, 2) + "\n", "utf-8");
}
export function resolveConfigDir() {
    const platformRoot = process.env.PLATFORM_ROOT;
    if (!platformRoot) {
        throw new Error("PLATFORM_ROOT environment variable is required");
    }
    const brandPath = resolve(platformRoot, "config", "brand.json");
    if (!existsSync(brandPath)) {
        throw new Error(`brand.json not found at ${brandPath} — platform not properly installed`);
    }
    const brand = JSON.parse(readFileSync(brandPath, "utf-8"));
    if (!brand.configDir) {
        throw new Error(`brand.json at ${brandPath} is missing the configDir field`);
    }
    return resolve(homedir(), brand.configDir);
}
export async function expungeConversationMemory(driver, accountId, sessionId, pattern) {
    const session = driver.session();
    try {
        const matchRes = await session.run(`MATCH (m:Memory)-[:PART_OF|:ABOUT]->(c:Conversation {sessionId: $sessionId})
       WHERE m.accountId = $accountId
       WITH m, [k IN keys(m) WHERE k <> '_denylistAudit'
                               AND k <> 'embedding'
                               AND CASE
                                 WHEN m[k] IS NULL THEN false
                                 WHEN toString(m[k]) = m[k] THEN m[k] CONTAINS $pattern
                                 ELSE false
                               END] AS hits
       RETURN id(m) AS nid, hits, size(collect(m)) AS scanned`, { accountId, sessionId, pattern });
        let propertiesNulled = 0;
        const affectedNodeIds = [];
        let nodesScanned = 0;
        for (const r of matchRes.records) {
            nodesScanned += 1;
            const nid = Number(r.get("nid"));
            const hits = r.get("hits");
            if (hits.length === 0)
                continue;
            affectedNodeIds.push(nid);
            const setClauses = hits.map((k) => `m.\`${k.replace(/`/g, "")}\` = null`).join(", ");
            const auditEntry = JSON.stringify({
                ts: new Date().toISOString(),
                properties: hits,
                manualPattern: pattern,
                sessionId,
            });
            await session.run(`MATCH (m) WHERE id(m) = $nid
         SET ${setClauses},
             m._denylistAudit = coalesce(m._denylistAudit, '') + $entry + '\n'`, { nid, entry: auditEntry });
            propertiesNulled += hits.length;
        }
        return {
            sessionId,
            pattern,
            nodesScanned,
            propertiesNulled,
            affectedNodeIds,
        };
    }
    finally {
        await session.close();
    }
}
//# sourceMappingURL=graph-prune.js.map