"use strict";
// SPDX-License-Identifier: AGPL-3.0-or-later
// Copyright (C) 2024-2026 Gracker (Chris)
// This file is part of SmartPerfetto. See LICENSE for details.
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.__testing = void 0;
exports.validateEmission = validateEmission;
exports.scanEmission = scanEmission;
exports.writeSkillNote = writeSkillNote;
exports.readSkillNotesFile = readSkillNotesFile;
/**
 * Validate, scan, and persist skill notes emitted by the review agent.
 *
 * The review agent only ever emits strict JSON — never writes files itself —
 * so the pipeline here is the trust boundary. Anything that fails schema,
 * security scan, or capacity gating is dropped with a logged reason; the
 * review job is still marked done because the failure is the agent's, not
 * a transient one we want to retry forever.
 *
 * Writes are atomic (tmp + rename) so a partial write never leaves a
 * corrupt notes file that breaks the next invoke_skill injection.
 *
 * See docs/architecture/self-improving-design.md §9 (Trust Boundary), §13.
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const runtimePaths_1 = require("../../runtimePaths");
const failureTaxonomy_1 = require("./failureTaxonomy");
const contentScanner_1 = require("./contentScanner");
/** Match the worktreeRunner whitelist style — keeps file names safe. */
const SKILL_ID_RE = /^[a-zA-Z0-9_]{1,80}$/;
const MAX_EVIDENCE_CHARS = 600;
const MAX_CONSTRAINTS_CHARS = 400;
const MAX_KEYWORDS = 6;
const MAX_KEYWORD_CHARS = 40;
const MAX_TOOLS = 6;
const MAX_TOOL_CHARS = 80;
const MAX_NOTE_BYTES = 4 * 1024;
const MAX_FILE_BYTES = 16 * 1024;
const NOTE_COOLDOWN_MS = 24 * 60 * 60 * 1000;
const DEFAULT_NOTES_DIR = (0, runtimePaths_1.backendLogPath)('skill_notes');
/**
 * Validate the agent emission against the schema. Truncates over-long fields
 * rather than failing on them — matches the validation style elsewhere in
 * selfImprove. Returns a normalized emission ready to be persisted.
 */
function validateEmission(raw, toolRegistry) {
    if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
        return { ok: false, reason: 'invalid_schema', details: 'emission must be a JSON object' };
    }
    const r = raw;
    if (typeof r.skillId !== 'string' || !SKILL_ID_RE.test(r.skillId)) {
        return { ok: false, reason: 'invalid_skill_id', details: `skillId must match ${SKILL_ID_RE.source}` };
    }
    if (!(0, failureTaxonomy_1.isKnownCategory)(r.failureCategoryEnum)) {
        return {
            ok: false,
            reason: 'unknown_category',
            details: `failureCategoryEnum must be a known FailureCategory (got ${String(r.failureCategoryEnum)})`,
        };
    }
    if (typeof r.evidenceSummary !== 'string' || r.evidenceSummary.trim().length === 0) {
        return { ok: false, reason: 'invalid_schema', details: 'evidenceSummary must be a non-empty string' };
    }
    const keywords = normalizeStringArray(r.candidateKeywords, MAX_KEYWORDS, MAX_KEYWORD_CHARS);
    if (keywords === 'invalid') {
        return { ok: false, reason: 'invalid_schema', details: 'candidateKeywords must be an array of strings' };
    }
    const constraints = typeof r.candidateConstraints === 'string'
        ? r.candidateConstraints.substring(0, MAX_CONSTRAINTS_CHARS)
        : '';
    const tools = normalizeStringArray(r.candidateCriticalTools, MAX_TOOLS, MAX_TOOL_CHARS);
    if (tools === 'invalid') {
        return { ok: false, reason: 'invalid_schema', details: 'candidateCriticalTools must be an array of strings' };
    }
    if (toolRegistry && tools.length > 0) {
        const unknownTool = tools.find(t => !toolRegistry.has(t));
        if (unknownTool) {
            return {
                ok: false,
                reason: 'invalid_schema',
                details: `candidateCriticalTools includes unknown tool/skill: ${unknownTool}`,
            };
        }
    }
    const value = {
        failureCategoryEnum: r.failureCategoryEnum,
        evidenceSummary: r.evidenceSummary.substring(0, MAX_EVIDENCE_CHARS),
        candidateKeywords: keywords,
        candidateConstraints: constraints,
        candidateCriticalTools: tools,
        skillId: r.skillId,
        failureModeHash: typeof r.failureModeHash === 'string' ? r.failureModeHash : undefined,
        sourceSessionId: typeof r.sourceSessionId === 'string' ? r.sourceSessionId : undefined,
        sourceTurnIndex: typeof r.sourceTurnIndex === 'number' ? r.sourceTurnIndex : undefined,
    };
    return { ok: true, value };
}
function normalizeStringArray(input, maxLen, maxItemChars) {
    if (input === undefined)
        return [];
    if (!Array.isArray(input))
        return 'invalid';
    const out = [];
    for (const item of input) {
        if (typeof item !== 'string')
            return 'invalid';
        out.push(item.substring(0, maxItemChars));
        if (out.length >= maxLen)
            break;
    }
    return out;
}
/**
 * Run security scanner across the emission's free-form text. Returns the
 * matches if any are found so the caller can log the offending excerpt.
 */
function scanEmission(emission) {
    const surfaces = [
        emission.evidenceSummary,
        emission.candidateConstraints || '',
        ...(emission.candidateKeywords || []),
        ...(emission.candidateCriticalTools || []),
    ];
    for (const text of surfaces) {
        const matches = (0, contentScanner_1.scanContent)(text);
        if (matches.length > 0)
            return (0, contentScanner_1.formatThreats)(matches);
    }
    return null;
}
/**
 * Atomically write a new note to `<notesDir>/<skillId>.notes.json`. The full
 * pipeline runs here: schema validation → security scan → capacity check →
 * tmp+rename write. Returns a structured outcome so the worker can log a
 * reason without throwing.
 */
function writeSkillNote(raw, opts = {}) {
    const validation = validateEmission(raw, opts.toolRegistry);
    if (!validation.ok)
        return { ok: false, reason: validation.reason, details: validation.details };
    const emission = validation.value;
    const threats = scanEmission(emission);
    if (threats) {
        return { ok: false, reason: 'security_scan', details: threats };
    }
    const now = opts.now ?? Date.now();
    const note = {
        id: `note-${now}-${Math.random().toString(36).substring(2, 8)}`,
        failureCategory: emission.failureCategoryEnum,
        failureModeHash: emission.failureModeHash,
        evidenceSummary: emission.evidenceSummary,
        candidateKeywords: emission.candidateKeywords ?? [],
        candidateConstraints: emission.candidateConstraints ?? '',
        candidateCriticalTools: emission.candidateCriticalTools ?? [],
        createdAt: now,
        cooldownUntil: now + NOTE_COOLDOWN_MS,
        byteSize: 0,
        sourceSessionId: emission.sourceSessionId,
        sourceTurnIndex: emission.sourceTurnIndex,
    };
    note.byteSize = Buffer.byteLength(JSON.stringify(note), 'utf-8');
    if (note.byteSize > MAX_NOTE_BYTES) {
        return { ok: false, reason: 'capacity_exceeded', details: `note=${note.byteSize}B > ${MAX_NOTE_BYTES}B` };
    }
    const dir = opts.notesDir ?? DEFAULT_NOTES_DIR;
    const filePath = path.join(dir, `${emission.skillId}.notes.json`);
    try {
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
    }
    catch (err) {
        return { ok: false, reason: 'io_error', details: err.message };
    }
    const existing = readSkillNotesFile(filePath);
    const existingFromSameSource = note.sourceSessionId && note.sourceTurnIndex !== undefined
        ? existing.notes.find(n => n.sourceSessionId === note.sourceSessionId
            && n.sourceTurnIndex === note.sourceTurnIndex
            && n.failureModeHash === note.failureModeHash)
        : undefined;
    if (existingFromSameSource) {
        // Same provenance + failure-mode is a duplicate emission (the worker
        // retried, or the agent looped). Keep the original to preserve audit.
        return {
            ok: true,
            noteId: existingFromSameSource.id,
            filePath,
            totalBytes: existing.totalBytes,
        };
    }
    const merged = {
        schemaVersion: 1,
        skillId: emission.skillId,
        notes: [...existing.notes, note],
        lastUpdated: now,
        totalBytes: 0,
    };
    // Drop oldest notes until the file fits the cap. Newer notes are more
    // valuable than older ones for cross-arch drift like Flutter Impeller.
    while (true) {
        const serialized = JSON.stringify(merged, null, 2);
        const totalBytes = Buffer.byteLength(serialized, 'utf-8');
        merged.totalBytes = totalBytes;
        if (totalBytes <= MAX_FILE_BYTES)
            break;
        if (merged.notes.length <= 1) {
            return { ok: false, reason: 'capacity_exceeded', details: `single note exceeds file cap (${totalBytes}B)` };
        }
        merged.notes.shift();
    }
    try {
        const tmp = `${filePath}.tmp`;
        fs.writeFileSync(tmp, JSON.stringify(merged, null, 2));
        fs.renameSync(tmp, filePath);
    }
    catch (err) {
        return { ok: false, reason: 'io_error', details: err.message };
    }
    return { ok: true, noteId: note.id, filePath, totalBytes: merged.totalBytes };
}
function readSkillNotesFile(filePath) {
    if (!fs.existsSync(filePath)) {
        return { schemaVersion: 1, skillId: path.basename(filePath, '.notes.json'), notes: [], lastUpdated: 0, totalBytes: 0 };
    }
    try {
        const parsed = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
        if (parsed && Array.isArray(parsed.notes))
            return parsed;
    }
    catch (err) {
        console.warn('[skillNotesWriter] failed to parse', filePath, err.message);
    }
    return { schemaVersion: 1, skillId: path.basename(filePath, '.notes.json'), notes: [], lastUpdated: 0, totalBytes: 0 };
}
exports.__testing = {
    MAX_NOTE_BYTES,
    MAX_FILE_BYTES,
    NOTE_COOLDOWN_MS,
    SKILL_ID_RE,
};
//# sourceMappingURL=skillNotesWriter.js.map