"use strict";
/**
 * Post-write audit for the database-operator's raw `write_neo4j_cypher` tool
 *. Sibling to `[graph-write] accepted` for the wrapped writers
 * (`memory-write`, `contact-create`, etc.) — same module so the
 * `[graph-*]` log family stays uniform across both write surfaces.
 *
 * Design posture:
 *   - Static parse first. Pre-flight regex on the cypher text catches missing
 *     provenance stamps and unknown edge types cheaply, no driver round-trip.
 *   - Dynamic orphan detection is the caller's responsibility — the graph-mcp
 *     shim runs the detection query against Neo4j after the upstream commits
 *     and passes the resulting elementId list as `orphanIds`. Keeping the
 *     module pure (no neo4j-driver import) makes it unit-testable without a
 *     live database.
 *   - Audit is observational. Warnings never block the write. The
 *     prompt-level Graph Stewardship Doctrine in
 *     [database-operator.md](../../../templates/specialists/agents/database-operator.md)
 *     names the discipline; the audit is the verification stream.
 *
 * String literals are stripped before pattern checks so that property values
 * containing words like `'createdByAgent in my bio'` cannot trip a false
 * provenance count, and edge-like patterns inside quoted strings cannot
 * trip a false unknown-type warning. The regex shape is duplicated from
 * [cypher-validate.ts](../../graph-mcp/src/cypher-validate.ts) intentionally:
 * graph-write does not depend on graph-mcp, and a one-line regex helper
 * doesn't justify a third package. Drift in either copy is a regression
 * surfaced by both the validator tests and the audit tests.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.auditCypherWrite = auditCypherWrite;
exports.formatAuditLine = formatAuditLine;
// Mirrors cypher-validate's pattern. Captures TYPE(|TYPE)* alternation; the
// audit splits on `|` to enumerate every referenced type.
const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
// Counts CREATE (n:Label) and MERGE (n:Label) statements that introduce a
// node. The trailing `[A-Z]` requires at least one label (so `CREATE INDEX`
// and bare `CREATE (a)-[:R]->(b)` shapes don't trip this — bare patterns
// reference a previously-MATCHed alias and don't introduce new nodes).
const CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
// Stamp tokens — at least one of these MUST appear per created node for the
// write to satisfy provenance discipline. The audit counts occurrences;
// stamp-count >= node-count is the pass criterion.
const PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
function stripStringLiterals(cypher) {
    // Identical regex to cypher-validate.ts. Replaces single- and double-
    // quoted literals with empty quotes so e.g. 'CREATE (n:Person)' inside a
    // property value cannot inflate the create-count.
    return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
}
function extractEdgeTypes(cleaned) {
    const out = new Set();
    // Use String.matchAll for stateless iteration — no shared regex lastIndex.
    for (const m of cleaned.matchAll(EDGE_PATTERN)) {
        for (const t of m[1].split("|")) {
            const clean = t.trim();
            if (clean)
                out.add(clean);
        }
    }
    return out;
}
function countCreateOrMergeNodes(cleaned) {
    const matches = cleaned.match(CREATE_OR_MERGE_NODE);
    return matches ? matches.length : 0;
}
function countProvenanceStamps(cleaned) {
    const matches = cleaned.match(PROVENANCE_TOKEN);
    return matches ? matches.length : 0;
}
function auditCypherWrite(input) {
    const warnings = [];
    const cleaned = stripStringLiterals(input.cypher);
    // 1. Unknown edge type — cypher names a type not in the live ontology.
    //    Enumerate every referenced type; emit one warning per unknown.
    const referencedTypes = extractEdgeTypes(cleaned);
    for (const t of referencedTypes) {
        if (!input.schema.relationshipTypes.has(t)) {
            warnings.push({ kind: "unknown-type-warning", type: t });
        }
    }
    // 2. Missing provenance — count CREATE/MERGE-with-label statements vs
    //    `createdBy*` token occurrences. Stamps may live in inline maps
    //    (`{createdByAgent: $agent}`) or SET clauses; both shapes contribute
    //    to the token count. The arithmetic is precision-imprecise — a
    //    multi-stamp single CREATE inflates `stamped`, a multi-CREATE single
    //    SET undercounts — but the directional signal (zero stamps for many
    //    creates) catches the failure mode this audit exists to surface.
    //
    //    nodesCreated=0 short-circuits the check: the upstream counter is
    //    ground truth for whether any node was actually persisted. An
    //    idempotent MERGE that matched an existing node has a static
    //    create-count of 1 but contributed no new node — no provenance was
    //    needed, and warning here would be a false positive.
    if (input.nodesCreated > 0) {
        const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
        if (createOrMergeNodes > 0) {
            const stamps = countProvenanceStamps(cleaned);
            if (stamps < createOrMergeNodes) {
                warnings.push({
                    kind: "missing-provenance-warning",
                    created: createOrMergeNodes,
                    stamped: stamps,
                });
            }
        }
    }
    // 3. Orphans — caller-supplied. The dynamic detection happens at the
    //    graph-mcp shim layer (which has the Neo4j driver); the audit module
    //    only renders the warning shape.
    if (input.orphanIds.length > 0) {
        warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
    }
    return warnings;
}
function formatAuditLine(line) {
    const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
    switch (line.kind) {
        case "accepted":
            return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
        case "orphan-warning":
            return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
        case "unknown-type-warning":
            return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
        case "missing-provenance-warning":
            return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
    }
}
//# sourceMappingURL=audit.js.map