/**
 * Practice Store (Phase 4)
 *
 * Best-practice "cards" learned from past deliveries and injected into future
 * prompts for similar tasks. A card records guidance distilled from a
 * *successful* run — the winning strategy seed and how many turns it took —
 * keyed by task keywords.
 *
 * Provenance matters: cards are derived from the harness's own strategy seeds
 * and gate outcomes, never from raw model output. This keeps the long-term
 * store free of model-authored text (a stored-prompt-injection vector).
 */
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
import { dirname, join } from 'path';
/** Strategy ids are harness-owned seed labels; this guards the template
 * against injection if a tampered store supplies an arbitrary string. */
const STRATEGY_RE = /^[a-z][a-z0-9-]{0,31}$/;
const STOP_WORDS = new Set([
    'the', 'a', 'an', 'to', 'of', 'and', 'or', 'in', 'on', 'for', 'with', 'that',
    'returns', 'return', 'create', 'add', 'make', 'write', 'into', 'from', 'as',
    'is', 'it', 'be', 'should', 'when', 'this', 'each', 'two', 'using',
]);
/** Extract lowercased keyword tokens from a task instruction. */
export function extractKeywords(text, max = 12) {
    const seen = new Set();
    const tokens = [];
    for (const raw of text.toLowerCase().split(/[^a-z0-9]+/)) {
        if (raw.length < 3 || STOP_WORDS.has(raw) || seen.has(raw))
            continue;
        seen.add(raw);
        tokens.push(raw);
        if (tokens.length >= max)
            break;
    }
    return tokens;
}
/** Jaccard-ish overlap: shared keywords / query keywords. */
function relevance(card, queryKeywords) {
    if (queryKeywords.length === 0)
        return 0;
    const cardSet = new Set(card.keywords);
    const shared = queryKeywords.filter((k) => cardSet.has(k)).length;
    return shared / queryKeywords.length;
}
const MIN_RELEVANCE = 0.25;
/** Default cosine-similarity floor for a semantic match to count. */
const MIN_SIMILARITY = 0.45;
/**
 * Text used to represent a card for embedding. Built only from the card's
 * keywords (which derive from the user instruction), never from model output
 * — the semantic path preserves the same provenance guarantee as keyword
 * retrieval.
 */
function cardEmbeddingText(card) {
    return card.keywords.join(' ');
}
/**
 * Retrieve practices by semantic similarity to the instruction, ranked by
 * cosine distance then reinforcement (success count, fewest turns). This
 * catches near-misses that keyword overlap drops — "parse a duration" vs
 * "parse durations into seconds".
 *
 * Fail-soft and degenerate-safe: if the embedding provider is unavailable,
 * throws, or yields no match above the similarity floor, it falls back to the
 * synchronous keyword retrieval so practices are never silently disabled.
 */
export async function retrievePracticesSemantic(store, instruction, retriever, options = {}) {
    const limit = options.limit ?? 3;
    const minSim = options.minSimilarity ?? MIN_SIMILARITY;
    const cards = store.all();
    if (cards.length === 0)
        return [];
    try {
        const queryVec = await retriever.embed(instruction.toLowerCase());
        if (!Array.isArray(queryVec) || queryVec.length === 0) {
            return store.retrieve(instruction, limit);
        }
        // Isolate per-card failures: a single card whose embedding errors or
        // whose vector dimension mismatches the query (cosineSimilarity throws)
        // must not collapse the whole batch into keyword fallback. A failed card
        // simply scores -1 and drops out below the floor.
        const scored = await Promise.all(cards.map(async (card) => {
            try {
                const vec = await retriever.embed(cardEmbeddingText(card));
                const sim = retriever.cosineSimilarity(queryVec, vec);
                return { card, sim: Number.isFinite(sim) ? sim : -1 };
            }
            catch {
                return { card, sim: -1 };
            }
        }));
        const hits = scored
            .filter((s) => s.sim >= minSim)
            .sort((a, b) => {
            if (b.sim !== a.sim)
                return b.sim - a.sim;
            if (b.card.successCount !== a.card.successCount)
                return b.card.successCount - a.card.successCount;
            return a.card.bestTurns - b.card.bestTurns;
        })
            .slice(0, limit)
            .map((s) => s.card);
        // Degenerate provider (e.g. unfitted TF-IDF) or genuinely no relevant
        // card — fall back to keyword overlap rather than inject nothing.
        return hits.length > 0 ? hits : store.retrieve(instruction, limit);
    }
    catch {
        return store.retrieve(instruction, limit);
    }
}
/** In-memory store — the base implementation; the file store persists it. */
export class InMemoryPracticeStore {
    cards;
    constructor(cards = []) {
        this.cards = cards;
    }
    retrieve(instruction, limit = 3) {
        const keywords = extractKeywords(instruction);
        return this.cards
            .map((card) => ({ card, score: relevance(card, keywords) }))
            .filter(({ score }) => score >= MIN_RELEVANCE)
            .sort((a, b) => {
            if (b.score !== a.score)
                return b.score - a.score;
            if (b.card.successCount !== a.card.successCount)
                return b.card.successCount - a.card.successCount;
            return a.card.bestTurns - b.card.bestTurns;
        })
            .slice(0, limit)
            .map(({ card }) => card);
    }
    record(input) {
        const strategy = STRATEGY_RE.test(input.strategy) ? input.strategy : 'direct';
        // Dedup on strategy, not rendered guidance (which varies by turn count).
        const existing = this.cards.find((c) => c.strategy === strategy);
        if (existing) {
            existing.successCount += 1;
            existing.bestTurns = Math.min(existing.bestTurns, input.turns);
            existing.keywords = [...new Set([...existing.keywords, ...input.keywords])];
            existing.guidance = distillPractice(strategy, existing.bestTurns);
            return;
        }
        this.cards.push({
            id: this.nextId(),
            strategy,
            keywords: input.keywords,
            guidance: distillPractice(strategy, input.turns),
            successCount: 1,
            bestTurns: input.turns,
        });
    }
    /** Smallest unused pN id — robust to merges and dropped (corrupt) cards. */
    nextId() {
        const used = new Set(this.cards.map((c) => c.id));
        for (let i = 1;; i++) {
            const id = `p${i}`;
            if (!used.has(id))
                return id;
        }
    }
    all() {
        // Copy so callers can't mutate the store's internal array (matches the
        // fresh-array convention of retrieve()).
        return [...this.cards];
    }
}
/** File-backed practice store (JSON). Self-heals on missing/corrupt files. */
export class FilePracticeStore extends InMemoryPracticeStore {
    path;
    constructor(path) {
        super(FilePracticeStore.load(path));
        this.path = path;
    }
    /**
     * Load cards from disk, validating every field. The on-disk file is NOT
     * trusted to supply guidance text — guidance is regenerated from the
     * validated strategy + bestTurns via distillPractice, so a tampered file
     * cannot inject prompt text (the read path enforces the same provenance
     * the write path guarantees). Structurally-invalid cards are dropped.
     */
    static load(path) {
        if (!existsSync(path))
            return [];
        let parsed;
        try {
            parsed = JSON.parse(readFileSync(path, 'utf-8'));
        }
        catch {
            return [];
        }
        if (!Array.isArray(parsed))
            return [];
        const cards = [];
        for (const raw of parsed) {
            if (typeof raw !== 'object' || raw === null)
                continue;
            const c = raw;
            if (typeof c.strategy !== 'string' || !STRATEGY_RE.test(c.strategy))
                continue;
            if (!Array.isArray(c.keywords) || !c.keywords.every((k) => typeof k === 'string'))
                continue;
            const successCount = Number(c.successCount);
            const bestTurns = Number(c.bestTurns);
            if (!Number.isFinite(successCount) || successCount < 1)
                continue;
            if (!Number.isFinite(bestTurns) || bestTurns < 1)
                continue;
            cards.push({
                id: typeof c.id === 'string' ? c.id : `p${cards.length + 1}`,
                strategy: c.strategy,
                keywords: c.keywords,
                guidance: distillPractice(c.strategy, bestTurns), // regenerated, never trusted from disk
                successCount: Math.floor(successCount),
                bestTurns: Math.floor(bestTurns),
            });
        }
        return cards;
    }
    record(input) {
        super.record(input);
        this.persist();
    }
    persist() {
        try {
            mkdirSync(dirname(this.path), { recursive: true });
            // Atomic write: temp file + rename, so a crash mid-write cannot corrupt
            // the store and we never follow a pre-planted symlink at the final path.
            const tmp = `${this.path}.tmp`;
            writeFileSync(tmp, JSON.stringify(this.all(), null, 2), 'utf-8');
            renameSync(tmp, this.path);
        }
        catch {
            // Persistence is best-effort; in-memory state remains valid
        }
    }
}
/** Default on-disk location for a project's learned practices. */
export function defaultPracticePath(projectRoot) {
    return join(projectRoot, '.uap', 'delivery-practices.json');
}
/**
 * Distill a one-line practice from a successful delivery. Provenance-safe:
 * built only from the winning strategy and turn count, not model output.
 */
export function distillPractice(winningStrategy, turns) {
    const strategy = winningStrategy ?? 'direct';
    if (turns === 1) {
        return `A '${strategy}' approach solved a similar task on the first attempt — lead with it.`;
    }
    return `A '${strategy}' approach converged on a similar task in ${turns} turns — prefer it and verify against the gates early.`;
}
//# sourceMappingURL=practice.js.map