/**
 * Frontmatter enrichment staleness detection.
 *
 * The matcher already splits every block into sentences and hashes each one on
 * every save (see node-fingerprint.ts). We reuse that machinery here — no new
 * algorithm, no new splitter. Save-time staleness is a small tag-on after the
 * matcher: harvest the current sentence-hash set + char count, compare against
 * the at-enrichment baseline stored in frontmatter, set `enrichmentStale: true`
 * when either threshold trips.
 *
 * Volume ratio captures growth and shrinkage symmetrically. Jaccard distance
 * over the sentence-hash set captures rewrites at constant length. Either
 * tripping flags the doc.
 *
 * OpenWriter owns "is this doc stale". The agent clears the flag via
 * mark_enriched (Phase 4). Both sides read the same field, never compute it
 * independently.
 *
 * See brief: 2026-05-18-frontmatter-enrichment-system.
 */
import { splitSentences, simpleHash } from './node-fingerprint.js';
/** Volume-ratio threshold above which a doc is flagged stale by size delta. */
export const DEFAULT_ENRICHMENT_VOLUME_THRESHOLD = 1.5;
/**
 * Jaccard-distance threshold above which a doc is flagged stale by drift.
 *
 * 0.10 catches in-place architectural rewrites that volume-ratio misses —
 * e.g. inserting a Status Update section into a 200-sentence doc adds
 * ~25 new sentences, ~0 removed: distance = 25/225 ≈ 0.11, trips at 0.10.
 * Won't false-positive on routine editing — a 3-sentence paragraph addition
 * to a 200-sentence doc is 3/203 ≈ 0.015, well below.
 *
 * Tightened from 0.3 → 0.10 after 2026-05-19 brief reported the Argument
 * Arc class of rewrites slipping through.
 */
export const DEFAULT_ENRICHMENT_DRIFT_THRESHOLD = 0.10;
/**
 * Flatten every block's per-sentence hashes into one sorted unique set.
 * Sorted so the on-disk representation is stable across saves (no spurious
 * frontmatter diffs from set-order drift). Unique so duplicate sentences
 * in the same doc don't double-count in the Jaccard math.
 */
export function harvestSentenceHashes(blocks) {
    const set = new Set();
    for (const block of blocks) {
        const sentences = splitSentences(block.text || '');
        for (const s of sentences) {
            set.add(simpleHash(s.text + s.terminator));
        }
    }
    return Array.from(set).sort();
}
/** Total char count across all blocks' text — the volume signal. */
export function harvestCharCount(blocks) {
    let n = 0;
    for (const b of blocks)
        n += (b.text || '').length;
    return n;
}
/**
 * Symmetric size delta. Returns 1 when sizes match, grows toward infinity as
 * they diverge in either direction. Handles zero-size docs safely.
 */
export function volumeRatio(current, baseline) {
    if (current === 0 && baseline === 0)
        return 1;
    if (current === 0 || baseline === 0)
        return Infinity;
    return Math.max(current, baseline) / Math.min(current, baseline);
}
/**
 * Jaccard distance over two sentence-hash sets. 0 = identical, 1 = disjoint.
 * (union - intersection) / union. Empty-vs-empty returns 0.
 */
export function jaccardDistance(a, b) {
    if (a.length === 0 && b.length === 0)
        return 0;
    const setA = new Set(a);
    const setB = new Set(b);
    let intersection = 0;
    for (const x of setA)
        if (setB.has(x))
            intersection++;
    const union = setA.size + setB.size - intersection;
    return union === 0 ? 0 : (union - intersection) / union;
}
/**
 * Compute staleness for a single doc given current matcher-derived signals
 * and the at-enrichment baseline stored in its frontmatter.
 *
 * Returns true when:
 *   - the doc has never been enriched (no lastEnrichedAt) — brief: "absent flag = stale"
 *   - volumeRatio trips its threshold
 *   - Jaccard drift trips its threshold
 *
 * Thresholds: doc-level overrides first, then global defaults. Workspace-level
 * overrides (per the brief) will be layered in when the surfacing handlers
 * (Phase 6) get a workspace pointer — for now the doc carries no workspace
 * reference in writeToDisk's scope.
 */
export function isEnrichmentStale(currentSentenceHashes, currentCharCount, metadata, workspaceOverrides) {
    // Never enriched → stale by default. New docs land here.
    if (!metadata.lastEnrichedAt)
        return true;
    const baselineHashes = Array.isArray(metadata.lastEnrichedSentences)
        ? metadata.lastEnrichedSentences
        : [];
    const baselineChars = typeof metadata.lastEnrichedCharCount === 'number'
        ? metadata.lastEnrichedCharCount
        : 0;
    const volTh = pickThreshold(metadata.enrichmentVolumeThreshold, workspaceOverrides?.volume, DEFAULT_ENRICHMENT_VOLUME_THRESHOLD);
    const driftTh = pickThreshold(metadata.enrichmentDriftThreshold, workspaceOverrides?.drift, DEFAULT_ENRICHMENT_DRIFT_THRESHOLD);
    if (volumeRatio(currentCharCount, baselineChars) >= volTh)
        return true;
    if (jaccardDistance(currentSentenceHashes, baselineHashes) >= driftTh)
        return true;
    return false;
}
function pickThreshold(docLevel, wsLevel, fallback) {
    if (typeof docLevel === 'number' && docLevel > 0)
        return docLevel;
    if (typeof wsLevel === 'number' && wsLevel > 0)
        return wsLevel;
    return fallback;
}
