/**
 * V7 §2.5.2 — LLM-driven skill crystallization.
 *
 * Given a policy + its evidence, we call `SKILL_CRYSTALLIZE_PROMPT` to
 * produce a structured draft. The draft is normalised / clamped to avoid
 * surprising the packager if the LLM emits missing or weird fields.
 *
 * We never call the LLM without evidence — if the caller hands us zero
 * traces we fail fast with `skipped_reason="no-evidence"`.
 */
import { detectModelRefusal } from "../llm/refusal.js";
import { detectDominantLanguage, languageSteeringLine, } from "../llm/prompts/index.js";
import { SKILL_CRYSTALLIZE_PROMPT } from "../llm/prompts/skill-crystallize.js";
import { sanitizeDerivedList, sanitizeDerivedMarkdown, sanitizeDerivedMarkdownList, sanitizeDerivedText, } from "../safety/content.js";
import { MemosError } from "../../agent-contract/errors.js";
import { extractToolNames } from "./tool-names.js";
/**
 * Run one crystallization call and return a normalised draft.
 */
export async function crystallizeDraft(input, deps) {
    const { llm, log, config } = deps;
    if (input.evidence.length === 0) {
        log.warn("skill.crystallize.skip", {
            policyId: input.policy.id,
            reason: "no-evidence",
        });
        return { ok: false, skippedReason: "no-evidence" };
    }
    if (!config.useLlm || !llm) {
        const reason = !config.useLlm
            ? "useLlm disabled in config"
            : "llm client is null (provider not attached?)";
        log.warn("skill.crystallize.llm_unavailable", {
            policyId: input.policy.id,
            reason,
            fallback: "skipped",
        });
        return { ok: false, skippedReason: "llm-disabled" };
    }
    const userPayload = packPrompt(input, config);
    // Detect the language of the evidence so the crystallised skill's
    // human-facing fields (display_title, summary, preconditions, steps,
    // examples) come out in the same language the user was using. The
    // `name` slug stays snake_case regardless — enforced by `sanitiseName`.
    const evidenceLang = detectDominantLanguage([
        input.policy.title,
        input.policy.trigger,
        input.policy.procedure,
        ...input.evidence.flatMap((t) => [t.userText, t.agentText, t.reflection]),
    ]);
    try {
        const rsp = await llm.completeJson([
            { role: "system", content: SKILL_CRYSTALLIZE_PROMPT.system },
            { role: "system", content: languageSteeringLine(evidenceLang) },
            { role: "user", content: userPayload },
        ], {
            op: "skill.crystallize",
            phase: "skill",
            episodeId: input.episodeId,
            schemaHint: "skill-crystallize.v2",
        });
        const rawRefusal = detectModelRefusal(rsp.raw);
        if (rawRefusal) {
            const modelRefusal = {
                provider: rsp.provider,
                model: rsp.model,
                servedBy: rsp.servedBy,
                ...rawRefusal,
            };
            log.error("skill.crystallize.model_refusal", {
                policyId: input.policy.id,
                ...modelRefusal,
            });
            return { ok: false, skippedReason: "llm-refusal", modelRefusal };
        }
        const draft = normaliseDraft(rsp.value, input);
        const draftRefusal = detectModelRefusal(draft);
        if (draftRefusal) {
            const modelRefusal = {
                provider: rsp.provider,
                model: rsp.model,
                servedBy: rsp.servedBy,
                ...draftRefusal,
            };
            log.error("skill.crystallize.model_refusal", {
                policyId: input.policy.id,
                ...modelRefusal,
            });
            return { ok: false, skippedReason: "llm-refusal", modelRefusal };
        }
        if (deps.validate)
            deps.validate(draft);
        return { ok: true, draft };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        const rawPreview = rawPreviewFromError(err);
        const refusal = rawPreview ? detectModelRefusal(rawPreview) : null;
        if (refusal) {
            const modelRefusal = {
                provider: providerFromError(err) ?? llm.provider,
                model: llm.model,
                servedBy: llm.provider,
                ...refusal,
            };
            log.error("skill.crystallize.model_refusal", {
                policyId: input.policy.id,
                error: message,
                ...modelRefusal,
            });
            return { ok: false, skippedReason: "llm-refusal", modelRefusal };
        }
        log.error("skill.crystallize.failed", { policyId: input.policy.id, error: message });
        return { ok: false, skippedReason: `llm-failed: ${message}` };
    }
}
function rawPreviewFromError(err) {
    if (err instanceof MemosError && typeof err.details?.rawPreview === "string") {
        return err.details.rawPreview;
    }
    return null;
}
function providerFromError(err) {
    if (err instanceof MemosError && typeof err.details?.provider === "string") {
        return err.details.provider;
    }
    return null;
}
/**
 * Produce a deterministic JSON payload for the LLM.
 *
 * V7 §2.4.6: surface the policy's structured `decisionGuidance` as a
 * separate `repair_hints` field so the prompt schema is unambiguous —
 * the LLM never sees our internal storage shape, and the boundary text
 * stays a clean human-readable scope description.
 *
 * `counter_examples` are evidence rows with V < 0 — caller-provided
 * (see `core/skill/skill.ts::gatherCounterExamples`); the prompt
 * marks them optional so it's fine to omit.
 */
function packPrompt(input, config) {
    const repairHints = input.policy.decisionGuidance;
    const policy = {
        id: input.policy.id,
        title: input.policy.title,
        trigger: input.policy.trigger,
        procedure: input.policy.procedure,
        verification: input.policy.verification,
        boundary: input.policy.boundary,
        support: input.policy.support,
        gain: input.policy.gain,
    };
    const evidence = input.evidence.slice(0, config.evidenceLimit).map((t) => ({
        id: t.id,
        episodeId: t.episodeId,
        reflection: t.reflection,
        user: capString(t.userText, config.traceCharCap),
        agent: capString(t.agentText, config.traceCharCap),
        value: Number.isFinite(t.value) ? t.value : 0,
        alpha: typeof t.alpha === "number" ? t.alpha : null,
        tags: t.tags,
    }));
    const counterExamples = (input.counterExamples ?? [])
        .slice(0, Math.max(0, config.evidenceLimit))
        .map((t) => ({
        id: t.id,
        episodeId: t.episodeId,
        reflection: t.reflection,
        user: capString(t.userText, config.traceCharCap),
        agent: capString(t.agentText, config.traceCharCap),
        value: Number.isFinite(t.value) ? t.value : 0,
        tags: t.tags,
    }));
    const evidenceTools = Array.from(extractToolNames(input.evidence));
    const payload = {
        policy,
        evidence,
        evidence_tools: evidenceTools,
        naming_space: input.namingSpace,
    };
    if (counterExamples.length > 0)
        payload.counter_examples = counterExamples;
    if (repairHints.preference.length > 0 ||
        repairHints.antiPattern.length > 0) {
        payload.repair_hints = {
            preference: repairHints.preference,
            antiPattern: repairHints.antiPattern,
        };
    }
    return JSON.stringify(payload);
}
/**
 * Clamp + shape-guard the LLM response. We intentionally never throw on
 * missing fields — the caller's validator decides whether a draft is good
 * enough to persist.
 */
function normaliseDraft(raw, input) {
    const rawName = String(raw.name ?? "").trim();
    const name = sanitiseName(rawName || `skill_${input.policy.id.slice(-6)}`);
    const displayTitle = sanitizeDerivedText(raw.display_title ?? raw.displayTitle ?? input.policy.title ?? name) ||
        name;
    const summary = sanitizeDerivedText(raw.summary);
    const parameters = asArray(raw.parameters).map(coerceParameter).filter(Boolean);
    const preconditions = sanitizeDerivedMarkdownList(asStringArray(raw.preconditions));
    const steps = asArray(raw.steps).map(coerceStep).filter(Boolean);
    const examples = asArray(raw.examples).map(coerceExample).filter(Boolean);
    const tags = dedupeLc(sanitizeDerivedList(asStringArray(raw.tags)));
    // V7 §2.4.6 — coerce both `decision_guidance` (preferred LLM key)
    // and `decisionGuidance` (camelCase fallback). Caps at 5 entries each
    // to keep the skill body skim-able and the prompt budget bounded.
    const decisionGuidance = coerceDecisionGuidance(raw.decision_guidance ?? raw.decisionGuidance);
    const tools = dedupeLc(sanitizeDerivedList(asStringArray(raw.tools)));
    return {
        name,
        displayTitle,
        summary,
        parameters,
        preconditions,
        steps,
        examples,
        tags,
        decisionGuidance,
        tools,
    };
}
function coerceDecisionGuidance(raw) {
    if (!raw || typeof raw !== "object") {
        return { preference: [], antiPattern: [] };
    }
    const o = raw;
    const pref = dedupeLc(sanitizeDerivedMarkdownList(asStringArray(o.preference))).slice(0, 5);
    const anti = dedupeLc(sanitizeDerivedMarkdownList(asStringArray(o.anti_pattern ?? o.antiPattern))).slice(0, 5);
    return { preference: pref, antiPattern: anti };
}
function sanitiseName(raw) {
    const lower = raw.toLowerCase();
    const out = lower
        .replace(/[^a-z0-9]+/g, "_")
        .replace(/^_+|_+$/g, "")
        .slice(0, 32);
    return out.length > 0 ? out : "skill";
}
function asArray(x) {
    return Array.isArray(x) ? x : [];
}
function asStringArray(x) {
    return asArray(x).map((v) => String(v).trim()).filter((s) => s.length > 0);
}
function dedupeLc(arr) {
    const out = [];
    const seen = new Set();
    for (const s of arr) {
        const key = s.toLowerCase();
        if (seen.has(key))
            continue;
        seen.add(key);
        out.push(s);
    }
    return out;
}
function coerceParameter(x) {
    if (!x || typeof x !== "object")
        return null;
    const o = x;
    const name = sanitizeDerivedText(o.name);
    if (!name)
        return null;
    const t = String(o.type ?? "string").toLowerCase();
    const allowed = new Set(["string", "number", "boolean", "enum"]);
    const type = (allowed.has(t) ? t : "string");
    const out = {
        name,
        type,
        required: Boolean(o.required ?? false),
        description: sanitizeDerivedMarkdown(o.description),
    };
    if (type === "enum") {
        out.enumValues = sanitizeDerivedMarkdownList(asStringArray(o.enum));
    }
    return out;
}
function coerceStep(x) {
    if (!x || typeof x !== "object")
        return null;
    const o = x;
    const title = sanitizeDerivedText(o.title);
    const body = sanitizeDerivedMarkdown(o.body);
    if (!title && !body)
        return null;
    return { title: title || body.slice(0, 32), body };
}
function coerceExample(x) {
    if (!x || typeof x !== "object")
        return null;
    const o = x;
    const input = sanitizeDerivedMarkdown(o.input);
    const expected = sanitizeDerivedMarkdown(o.expected);
    if (!input && !expected)
        return null;
    return { input, expected };
}
function capString(s, cap) {
    if (s.length <= cap)
        return s;
    return s.slice(0, cap) + "…";
}
/**
 * A sensible default validator used both in production and in tests.
 * Throws if the draft is structurally unusable (no name, no steps, no summary).
 */
export function defaultDraftValidator(draft) {
    if (!draft.name)
        throw new Error("skill.crystallize.invalid: missing name");
    if (!draft.summary)
        throw new Error("skill.crystallize.invalid: missing summary");
    if (draft.steps.length === 0)
        throw new Error("skill.crystallize.invalid: missing steps");
}
//# sourceMappingURL=crystallize.js.map