// Graph-write precondition gate.
//
// Refuses any `memory-write` / `memory-update` against a non-exempt node
// label until the account's AdminUser exists AND at least one of
// (LocalBusiness, personal-profile Person) exists. The purpose is doctrine
// enforcement: SOUL is personality, factual user/business data lives in the
// graph, and the agent must establish operator identity before writing
// user-domain nodes (WebSite, Service, etc.). Prose rules drifted across past
// sessions; this gate removes the LLM from the decision path entirely
// (feedback_deterministic_means_remove_llm.md).
//
// Two satisfaction shapes:
//   1. Business-owner mode — AdminUser + LocalBusiness
//   2. Personal mode — AdminUser + Person {role: "admin-personal"}
//
// The personal-profile bootstrap exists because Maxy's Solo licence target is
// individual operators with no business at all; forcing them through a
// business-profile flow created a silent failure where someone with an
// employer registered that employer as a `LocalBusiness`.
//
// The exempt set is the base case that breaks the chicken-and-egg: the agent
// must be able to write Person, LocalBusiness, and AdminUser *before* anything
// else — otherwise no onboarding can ever complete. Public-agent customer
// capture writes `Person`, so exempting it keeps that flow working even when
// the admin side has not run business-profile.
//
// Cypher safety: COUNT subqueries return 0 for missing matches, which is the
// correct semantic here — no MATCH-SET silent-no-op risk (sprint-learning #5
// applies to mutations, not COUNTs).
//
// Scope: this gate lives only in the MCP memory-write/update path the agent
// calls. Server-side writers (neo4j-store, workflow-execute, admin/index)
// are owned by the provenance doctrine — disjoint enforcement.
//
// TODO (perf, post-Phase-0): once Person counts grow into the thousands per
// account, add a composite index on Person(accountId, role) so the
// hasPersonalProfile subquery doesn't full-scan. Same scaling concern applies
// to LocalBusiness(accountId) and AdminUser(accountId). Not in 704 scope.
/** Compiled-truth cap. ≤2000 chars ≈ 300 words — forces the rewriter to summarise, not transcribe. */
export const COMPILED_TRUTH_MAX_CHARS = 2000;
/**
 * Property-level write gate (Task 306, extended Task 392). The cap applies
 * to both `compiledTruth` and its public twin `compiledTruthPublic`; either
 * field exceeding COMPILED_TRUTH_MAX_CHARS rejects the write. Used by both
 * `memory-write` and `memory-update`; the rewriter calls it before writing too.
 *
 * Pure / synchronous on purpose — no session needed. Loud-fail on
 * violation so an over-producing rewriter surfaces immediately instead
 * of silently truncating the operator's summary.
 */
export function checkPropertyWriteGate(args) {
    for (const field of ["compiledTruth", "compiledTruthPublic"]) {
        const value = args.properties[field];
        if (typeof value === "string" && value.length > COMPILED_TRUTH_MAX_CHARS) {
            console.log(`[graph-write-gate] reject reason=compiled-truth-too-long field=${field} len=${value.length} cap=${COMPILED_TRUTH_MAX_CHARS}`);
            return {
                ok: false,
                reason: "compiled-truth-too-long",
                message: `${field} length ${value.length} exceeds cap of ${COMPILED_TRUTH_MAX_CHARS} chars. ` +
                    `The rewriter must produce a tighter summary.`,
            };
        }
    }
    return { ok: true };
}
const EXEMPT_LABELS = new Set([
    "Person",
    "LocalBusiness",
    "AdminUser",
]);
/**
 * Labels the agent must never write directly via memory-write /
 * memory-update. `:CompiledTruthRevision` (compiled-truth rewriter) is read-only after
 * write: the only writer is the rewriter (composed via the revision
 * helper, in the same transaction as the live `compiledTruth` SET), and
 * the only mutation path is the dream-cycle prune. Direct writes from
 * the agent would let an LLM forge or rewrite audit history — defeating
 * the point of having it.
 */
const FORBIDDEN_LABELS = new Set([
    "CompiledTruthRevision",
]);
/**
 * Label-level write gate (compiled-truth rewriter). Rejects any agent-side write
 * whose labels include a FORBIDDEN_LABEL. Pure / synchronous — no
 * session needed.
 */
export function checkLabelWriteGate(args) {
    for (const label of args.labels) {
        if (FORBIDDEN_LABELS.has(label)) {
            console.log(`[graph-write-gate] reject reason=forbidden-label label=${label}`);
            return {
                ok: false,
                reason: "forbidden-label",
                forbiddenLabel: label,
                message: `:${label} is read-only and may not be written via memory-write or memory-update. ` +
                    `It is produced by the compiled-truth rewriter (compiled-truth rewriter) and pruned by the dream-cycle.`,
            };
        }
    }
    return { ok: true };
}
const NEXT_MOVE = "Run the business-profile skill to establish the admin user and business " +
    "identity, or complete the personal-profile bootstrap (onboarding step 9 " +
    "personal mode), before writing this node.";
export async function checkGraphWriteGate(args) {
    const { session, accountId, labels } = args;
    if (labels.some((l) => EXEMPT_LABELS.has(l))) {
        return { ok: true };
    }
    // COUNT subquery form — one query, no cartesian-product surprises,
    // account-scoped so multi-account installs don't cross-contaminate.
    // Four subqueries: AdminUser-with-accountId, AdminUser-any (subReason
    // discriminator), LocalBusiness, Person-with-admin-role.
    const result = await session.run(`RETURN
       COUNT { MATCH (au:AdminUser {accountId: $accountId}) } > 0 AS hasAdmin,
       COUNT { MATCH (au:AdminUser) } > 0 AS hasAnyAdminUser,
       COUNT { MATCH (lb:LocalBusiness {accountId: $accountId}) } > 0 AS hasBusiness,
       COUNT { MATCH (p:Person {accountId: $accountId, role: "admin-personal"}) } > 0 AS hasPersonalProfile`, { accountId });
    const record = result.records[0];
    const hasAdmin = Boolean(record?.get("hasAdmin"));
    const hasAnyAdminUser = Boolean(record?.get("hasAnyAdminUser"));
    const hasBusiness = Boolean(record?.get("hasBusiness"));
    const hasPersonalProfile = Boolean(record?.get("hasPersonalProfile"));
    if (hasAdmin && (hasBusiness || hasPersonalProfile)) {
        return { ok: true };
    }
    // Reason stays backward-compatible. subReason is the additive
    // discriminator that distinguishes the two no-admin-user sub-cases —
    // critical for operator diagnosis: "no-admin-user-node" means a clean DB
    // (run seed); "admin-user-no-accountid" means legacy install where the seed
    // ran pre-912 (boot-time self-heal repairs this; if the subReason still
    // fires post-912, the heal didn't run).
    let reason;
    let subReason;
    if (!hasAdmin) {
        reason = "no-admin-user";
        subReason = hasAnyAdminUser ? "admin-user-no-accountid" : "no-admin-user-node";
    }
    else {
        reason = "no-local-business";
        subReason = "no-business-or-personal-profile";
    }
    console.log(`[graph-write-gate] reject accountId=${accountId.slice(0, 8)}… ` +
        `reason=${reason} subReason=${subReason} nodeLabels=${labels.join(",")}`);
    return { ok: false, reason, subReason, message: NEXT_MOVE };
}
//# sourceMappingURL=graph-write-gate.js.map