/**
 * Drift comparison service — fleet-general write integrity layer
 * (item-3-snapshot-export).
 *
 * Compares a canonical Neotoma snapshot export against any external
 * state source via a pluggable `ExternalParser` interface. v0.1 ships
 * with the identity JSON parser; fleet-specific parsers (AIBTC
 * MEMORY.md ASMR, LangGraph state dumps, internal SQLite mirrors)
 * register via {@link registerExternalParser} without touching core.
 *
 * Output contract:
 *   * `missing_in_neotoma` — entities the external source knows about
 *     but Neotoma does not (by `entity_id`).
 *   * `missing_in_external` — entities Neotoma has but the external
 *     source does not.
 *   * `field_diffs` — per-field value disagreements for entities
 *     present in both.
 *   * `provenance_gaps` — entities with concerning provenance posture
 *     (unclassified observation_source, anonymous/unverified
 *     attribution) even when values agree.
 *
 * Design notes:
 *   * Deliberately does NOT care which side is "right" — the report is
 *     symmetric and the caller decides how to reconcile.
 *   * Parsers are pure functions (raw string → normalised shape) so
 *     they're easy to unit test and swap. Core does not import any
 *     fleet-specific code.
 */
// ---------------------------------------------------------------------
// Parser registry
// ---------------------------------------------------------------------
const parserRegistry = new Map();
export function registerExternalParser(parser) {
    parserRegistry.set(parser.name, parser);
}
export function getExternalParser(name) {
    return parserRegistry.get(name);
}
export function listExternalParsers() {
    return [...parserRegistry.values()];
}
/**
 * Built-in identity JSON parser. Accepts either a bare array of
 * {@link NormalizedExternalEntity} or an object `{ entities: [...] }`.
 * Keeps the exporter round-trip-safe: calling `diff(export, export)`
 * with this parser produces an empty report.
 */
export const identityJsonParser = {
    name: "json",
    description: "Identity JSON parser. Accepts `{ entities: [...] }` or a bare array matching NormalizedExternalEntity. The fleet-neutral default.",
    parse(raw) {
        const parsed = JSON.parse(raw);
        const entities = Array.isArray(parsed)
            ? parsed
            : Array.isArray(parsed.entities)
                ? parsed.entities
                : [];
        const typed = entities
            .filter((e) => typeof e?.entity_id === "string" && e.entity_id.length > 0)
            .map((e) => ({
            entity_id: e.entity_id,
            entity_type: e.entity_type,
            canonical_name: e.canonical_name ?? null,
            snapshot: (e.snapshot ?? {}),
        }));
        return {
            source_label: "json",
            exported_at: typeof parsed.exported_at === "string"
                ? parsed.exported_at
                : undefined,
            entities: typed,
        };
    },
};
registerExternalParser(identityJsonParser);
// ---------------------------------------------------------------------
// Comparator
// ---------------------------------------------------------------------
export function compareSnapshots(neotomaExport, externalSnapshot) {
    const neotomaById = new Map();
    for (const e of neotomaExport.entities)
        neotomaById.set(e.entity_id, e);
    const externalById = new Map();
    for (const e of externalSnapshot.entities)
        externalById.set(e.entity_id, e);
    const missing_in_neotoma = [];
    for (const [id, ext] of externalById) {
        if (!neotomaById.has(id))
            missing_in_neotoma.push(ext);
    }
    const missing_in_external = [];
    for (const [id, neo] of neotomaById) {
        if (!externalById.has(id))
            missing_in_external.push(neo);
    }
    const field_diffs = [];
    const provenance_gaps = [];
    let inCommon = 0;
    for (const [id, neo] of neotomaById) {
        const ext = externalById.get(id);
        if (!ext)
            continue;
        inCommon += 1;
        const fields = new Set([
            ...Object.keys(neo.snapshot ?? {}),
            ...Object.keys(ext.snapshot ?? {}),
        ]);
        for (const field of fields) {
            const nv = neo.snapshot?.[field];
            const xv = ext.snapshot?.[field];
            if (!deepEqual(nv, xv)) {
                field_diffs.push({
                    entity_id: id,
                    entity_type: neo.entity_type,
                    field,
                    neotoma_value: nv,
                    external_value: xv,
                    neotoma_source_observation_id: neo.provenance?.[field],
                });
            }
        }
        const gap = diagnoseProvenanceGap(neo);
        if (gap)
            provenance_gaps.push(gap);
    }
    return {
        schema_version: "0.1.0",
        generated_at: new Date().toISOString(),
        neotoma_source: "neotoma",
        external_source: externalSnapshot.source_label,
        summary: {
            total_neotoma: neotomaExport.entities.length,
            total_external: externalSnapshot.entities.length,
            in_common: inCommon,
            missing_in_neotoma: missing_in_neotoma.length,
            missing_in_external: missing_in_external.length,
            field_diffs: field_diffs.length,
            provenance_gaps: provenance_gaps.length,
        },
        missing_in_neotoma,
        missing_in_external,
        field_diffs,
        provenance_gaps,
    };
}
function diagnoseProvenanceGap(entity) {
    const reasons = [];
    const unclassified = (entity.observation_source_histogram?.unclassified ?? 0) > 0;
    if (unclassified)
        reasons.push("unclassified_observation_source");
    const tiers = entity.attribution_fingerprint?.tiers ?? {};
    if ((tiers.anonymous ?? 0) > 0)
        reasons.push("anonymous_attribution");
    if ((tiers.unverified_client ?? 0) > 0)
        reasons.push("unverified_client_attribution");
    if (reasons.length === 0)
        return null;
    return {
        entity_id: entity.entity_id,
        entity_type: entity.entity_type,
        reasons,
    };
}
function deepEqual(a, b) {
    if (a === b)
        return true;
    if (a == null || b == null)
        return a === b;
    if (typeof a !== typeof b)
        return false;
    if (typeof a !== "object")
        return false;
    if (Array.isArray(a) !== Array.isArray(b))
        return false;
    if (Array.isArray(a) && Array.isArray(b)) {
        if (a.length !== b.length)
            return false;
        for (let i = 0; i < a.length; i += 1) {
            if (!deepEqual(a[i], b[i]))
                return false;
        }
        return true;
    }
    const aObj = a;
    const bObj = b;
    const keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
    for (const key of keys) {
        if (!deepEqual(aObj[key], bObj[key]))
            return false;
    }
    return true;
}
//# sourceMappingURL=drift_comparison.js.map