"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.FeedbackPipeline = void 0;
/**
 * FeedbackPipeline — state machine for the feedback → case → skill
 * draft → review flow (Plan 44 M1, Spark #95).
 *
 * Each `FeedbackPipelineEntry` rides this machine:
 *
 *     feedback → case_draft → skill_draft → reviewed → merged
 *                                                   ↘  rejected
 *
 * The transitions are intentionally linear with one branch at the
 * `reviewed` stage. No back-edges (a rejected entry stays rejected;
 * resurrecting it means a fresh feedback intake). The reviewer field
 * is required at any stage advance from `reviewed` onward.
 *
 * Storage: independent JSON file at
 * `backend/logs/feedback_pipeline.json`. Same atomic temp+rename
 * pattern as the other Plan 44 stores.
 *
 * Out of scope:
 * - Case draft generation (the actual case is created by the caller
 *   and referenced via the shared `CaseRef` shape; this module only
 *   tracks the pipeline state, not the case content).
 * - Skill draft generation (caller-driven).
 * - MCP tool surface — feedback pipeline is operator-side, not
 *   agent-callable.
 *
 * @module feedbackPipeline
 */
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
/** Allowed forward transitions. The graph is intentionally narrow —
 * any other transition throws so operator scripts cannot accidentally
 * advance entries past review without the right inputs. */
const ALLOWED_TRANSITIONS = {
    feedback: ['case_draft', 'rejected'],
    case_draft: ['skill_draft', 'rejected'],
    skill_draft: ['reviewed', 'rejected'],
    reviewed: ['merged', 'rejected'],
    merged: [], // terminal
    rejected: [], // terminal
};
/** Stages that require a reviewer name on advance. */
const REQUIRES_REVIEWER = new Set([
    'reviewed',
    'merged',
    'rejected',
]);
class FeedbackPipeline {
    constructor(storagePath) {
        this.entries = new Map();
        this.loaded = false;
        /** High-water mark of stamped `updatedAt` values. Subsequent stamps
         * are guaranteed strictly greater so sorting by `updatedAt` desc
         * stays deterministic even when many ops land in the same ms — a
         * real concern for rapid-fire pipelines. The mark is reseeded from
         * the loaded entries so persisted ordering survives across
         * instances. */
        this.updatedAtFloor = 0;
        this.storagePath = storagePath;
    }
    load() {
        if (this.loaded)
            return;
        this.loaded = true;
        if (!fs.existsSync(this.storagePath))
            return;
        try {
            const raw = fs.readFileSync(this.storagePath, 'utf-8');
            const parsed = JSON.parse(raw);
            if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.entries))
                return;
            for (const e of parsed.entries) {
                this.entries.set(e.entryId, e);
                if (e.updatedAt > this.updatedAtFloor)
                    this.updatedAtFloor = e.updatedAt;
            }
        }
        catch {
            // Corrupted JSON: file preserved, in-memory cache stays empty.
        }
    }
    /** Monotonic stamp — strictly greater than every previously
     * returned value, even when wall clock has not advanced. */
    nextUpdatedAt() {
        const wall = Date.now();
        const next = Math.max(wall, this.updatedAtFloor + 1);
        this.updatedAtFloor = next;
        return next;
    }
    /** Create a new entry at the `feedback` stage. Throws when an entry
     * with the same id already exists — the operator should resolve the
     * existing one (advance or reject) before creating a new one. */
    createEntry(opts) {
        this.load();
        if (this.entries.has(opts.entryId)) {
            throw new Error(`FeedbackPipelineEntry '${opts.entryId}' already exists; advance or reject first`);
        }
        const entry = {
            entryId: opts.entryId,
            feedbackId: opts.feedbackId,
            stage: 'feedback',
            updatedAt: this.nextUpdatedAt(),
        };
        this.entries.set(opts.entryId, entry);
        this.persist();
        return entry;
    }
    /**
     * Advance an existing entry to the next stage. Throws on:
     * - Entry not found
     * - Target stage not in `ALLOWED_TRANSITIONS[currentStage]`
     * - Target stage requires a reviewer and `opts.reviewer` is missing
     *   or whitespace
     */
    advance(entryId, opts) {
        this.load();
        const existing = this.entries.get(entryId);
        if (!existing) {
            throw new Error(`FeedbackPipelineEntry '${entryId}' not found`);
        }
        const allowed = ALLOWED_TRANSITIONS[existing.stage];
        if (!allowed.includes(opts.stage)) {
            throw new Error(`Illegal transition from '${existing.stage}' to '${opts.stage}'; allowed: [${allowed.join(', ')}]`);
        }
        const trimmedReviewer = opts.reviewer?.trim();
        if (REQUIRES_REVIEWER.has(opts.stage) && !trimmedReviewer) {
            throw new Error(`Advancing to '${opts.stage}' requires a reviewer name`);
        }
        const advanced = {
            ...existing,
            stage: opts.stage,
            updatedAt: this.nextUpdatedAt(),
            ...(trimmedReviewer ? { reviewer: trimmedReviewer } : {}),
            // Preserve existing fields; allow per-call updates of case + skillDraftId.
            ...(opts.case !== undefined ? { case: opts.case } : {}),
            ...(opts.skillDraftId !== undefined
                ? { skillDraftId: opts.skillDraftId }
                : {}),
        };
        this.entries.set(entryId, advanced);
        this.persist();
        return advanced;
    }
    /** Read an entry by id. */
    getEntry(entryId) {
        this.load();
        return this.entries.get(entryId);
    }
    /**
     * List entries optionally filtered by stage. Default sort is by
     * `updatedAt` descending so the most-recently-touched entries
     * surface first in operator UIs.
     */
    listEntries(opts = {}) {
        this.load();
        let out = Array.from(this.entries.values());
        if (opts.stage)
            out = out.filter(e => e.stage === opts.stage);
        out.sort((a, b) => b.updatedAt - a.updatedAt);
        return out;
    }
    /** Remove an entry. Returns whether anything was removed. */
    removeEntry(entryId) {
        this.load();
        const had = this.entries.delete(entryId);
        if (had)
            this.persist();
        return had;
    }
    /** Per-stage counts for dashboards. */
    getStats() {
        this.load();
        const out = {
            feedback: 0,
            case_draft: 0,
            skill_draft: 0,
            reviewed: 0,
            merged: 0,
            rejected: 0,
        };
        for (const e of this.entries.values())
            out[e.stage]++;
        return out;
    }
    persist() {
        const dir = path.dirname(this.storagePath);
        if (!fs.existsSync(dir))
            fs.mkdirSync(dir, { recursive: true });
        // Per-process unique tmp suffix — Codex round E P1#5.
        const tmp = `${this.storagePath}.tmp.${process.pid}.${Math.random().toString(36).slice(2)}`;
        const envelope = {
            schemaVersion: 1,
            entries: Array.from(this.entries.values()),
        };
        fs.writeFileSync(tmp, JSON.stringify(envelope, null, 2), 'utf-8');
        fs.renameSync(tmp, this.storagePath);
    }
}
exports.FeedbackPipeline = FeedbackPipeline;
//# sourceMappingURL=feedbackPipeline.js.map