"use strict";
/**
 * WorkflowParser — load + validate workflow YAML documents.
 *
 * Reads either a YAML string or a file path; returns a structured
 * { ok, workflow?, errors[] } result. Never throws on user input — the
 * worst case is `ok: false` with a list of stable error codes.
 *
 * Story 4.1 — OpenLife v1.2 Royal Stack.
 */
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.parseWorkflow = parseWorkflow;
exports.parseWorkflowFile = parseWorkflowFile;
const fs = __importStar(require("fs"));
const yaml = __importStar(require("js-yaml"));
const WorkflowSchema_1 = require("./WorkflowSchema");
const VALID_TYPES = new Set(['greenfield', 'brownfield', 'story-cycle', 'qa-loop', 'custom']);
/**
 * Parse a YAML string into a validated Workflow. Returns a structured
 * result; never throws.
 */
function parseWorkflow(yamlContent) {
    const errors = [];
    let doc;
    try {
        doc = yaml.load(yamlContent);
    }
    catch (err) {
        return {
            ok: false,
            errors: [{
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_YAML,
                    message: err instanceof Error ? err.message : 'YAML parse failed',
                }],
        };
    }
    if (doc === null || typeof doc !== 'object') {
        return {
            ok: false,
            errors: [{
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.MISSING_ROOT,
                    message: 'document must be a YAML mapping with a `workflow:` root key',
                }],
        };
    }
    const root = doc;
    if (!root.workflow || typeof root.workflow !== 'object') {
        return {
            ok: false,
            errors: [{
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.MISSING_ROOT,
                    message: 'top-level `workflow:` key is required',
                }],
        };
    }
    const wf = root.workflow;
    // Required scalars
    requireString(wf, 'id', errors);
    requireString(wf, 'name', errors);
    requireString(wf, 'version', errors);
    requireString(wf, 'type', errors);
    if (typeof wf.type === 'string' && !VALID_TYPES.has(wf.type)) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TYPE,
            message: `type must be one of: ${Array.from(VALID_TYPES).join(', ')}`,
            context: { received: wf.type },
        });
    }
    // v1.3 — optional Hermes fields (all backward-compatible)
    validateV13Fields(wf, errors);
    // sequence must exist and be an array
    if (!Array.isArray(wf.sequence)) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.MISSING_REQUIRED_FIELD,
            message: '`sequence` must be an array',
        });
        return { ok: false, errors };
    }
    // Validate each sequence entry + collect step ids
    const stepIds = new Set();
    const allKnownIds = new Set();
    const stepsForRequires = [];
    for (let i = 0; i < wf.sequence.length; i++) {
        const entry = wf.sequence[i];
        if ((0, WorkflowSchema_1.isPhaseMarker)(entry)) {
            continue; // phase markers don't need ids/agents
        }
        if (!(0, WorkflowSchema_1.isExecutableStep)(entry)) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_SCHEMA,
                message: `sequence[${i}] is neither a phase marker (needs phase:number+name) nor an executable step (needs id:string)`,
            });
            continue;
        }
        // Duplicate step id check
        if (stepIds.has(entry.id)) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.DUPLICATE_STEP_ID,
                message: `step id '${entry.id}' is used more than once`,
                context: { stepId: entry.id, index: i },
            });
        }
        else {
            stepIds.add(entry.id);
            allKnownIds.add(entry.id);
        }
        // Exactly one of agent/squad required
        const hasAgent = typeof entry.agent === 'string' && entry.agent.length > 0;
        const hasSquad = typeof entry.squad === 'string' && entry.squad.length > 0;
        if (hasAgent && hasSquad) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.AGENT_AND_SQUAD,
                message: `step '${entry.id}' specifies both agent and squad; pick one`,
                context: { stepId: entry.id },
            });
        }
        if (!hasAgent && !hasSquad) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.NEITHER_AGENT_NOR_SQUAD,
                message: `step '${entry.id}' must specify either agent or squad`,
                context: { stepId: entry.id },
            });
        }
        stepsForRequires.push(entry);
    }
    // Cross-check `requires` references after all step ids are collected
    for (const step of stepsForRequires) {
        const reqs = normalizeRequires(step.requires);
        for (const req of reqs) {
            // `requires` can reference either a step id OR an artifact path.
            // We can't validate artifact paths at parse time (they're created
            // by upstream steps at runtime), so we only flag references that
            // LOOK LIKE step ids (no slashes/extensions) but aren't found.
            if (looksLikeStepId(req) && !allKnownIds.has(req)) {
                errors.push({
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.UNKNOWN_REQUIRES,
                    message: `step '${step.id}' requires '${req}', which is not a known step id`,
                    context: { stepId: step.id, requires: req },
                });
            }
        }
    }
    // Circular requires detection (step-id refs only)
    const cycle = detectCycle(stepsForRequires);
    if (cycle) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.CIRCULAR_REQUIRES,
            message: `circular requires chain: ${cycle.join(' -> ')}`,
            context: { chain: cycle.join(',') },
        });
    }
    if (errors.length > 0) {
        return { ok: false, errors };
    }
    return { ok: true, workflow: wf, errors: [] };
}
/**
 * Parse a workflow YAML file from disk. Returns the same result shape as
 * parseWorkflow().
 */
function parseWorkflowFile(filePath) {
    if (!fs.existsSync(filePath)) {
        return {
            ok: false,
            errors: [{
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_YAML,
                    message: `workflow file not found: ${filePath}`,
                    context: { filePath },
                }],
        };
    }
    const content = fs.readFileSync(filePath, 'utf-8');
    return parseWorkflow(content);
}
// ─────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────
function requireString(obj, field, errors) {
    const v = obj[field];
    if (typeof v !== 'string' || v.length === 0) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.MISSING_REQUIRED_FIELD,
            message: `field '${field}' is required and must be a non-empty string`,
            context: { field },
        });
    }
}
function normalizeRequires(requires) {
    if (requires === undefined)
        return [];
    if (typeof requires === 'string')
        return [requires];
    if (Array.isArray(requires))
        return requires.filter((r) => typeof r === 'string');
    return [];
}
function looksLikeStepId(ref) {
    // Heuristic: step ids look like `kebab-case-words` — no slashes, no
    // dots, no extensions. Artifact paths look like `path/to/file.md`.
    return !ref.includes('/') && !ref.includes('.') && /^[a-z0-9-]+$/i.test(ref);
}
function detectCycle(steps) {
    const graph = new Map();
    for (const step of steps) {
        const reqs = normalizeRequires(step.requires).filter(looksLikeStepId);
        graph.set(step.id, reqs);
    }
    const WHITE = 0, GRAY = 1, BLACK = 2;
    const color = new Map();
    const parent = new Map();
    function dfs(node) {
        color.set(node, GRAY);
        for (const next of graph.get(node) || []) {
            if (!graph.has(next))
                continue; // not a known step id; skip
            const c = color.get(next) || WHITE;
            if (c === GRAY) {
                // Reconstruct cycle path: next … node -> next
                const cyc = [next];
                let curr = node;
                while (curr && curr !== next) {
                    cyc.unshift(curr);
                    const p = parent.get(curr);
                    curr = p === null ? undefined : p;
                }
                cyc.unshift(next);
                return cyc;
            }
            if (c === WHITE) {
                parent.set(next, node);
                const found = dfs(next);
                if (found)
                    return found;
            }
        }
        color.set(node, BLACK);
        return null;
    }
    for (const id of graph.keys()) {
        if ((color.get(id) || WHITE) === WHITE) {
            parent.set(id, null);
            const cyc = dfs(id);
            if (cyc)
                return cyc;
        }
    }
    return null;
}
// ────────────────────────────────────────────────────────────────
// Story 2.2 — v1.3 field validators (all optional fields, backward-compat).
// ────────────────────────────────────────────────────────────────
const VALID_MODES = new Set(['task', 'service']);
const VALID_STATUSES = new Set(['draft', 'tested', 'active', 'deprecated']);
const VALID_TRIGGERS = new Set(['manual', 'cron', 'webhook', 'event']);
function validateV13Fields(wf, errors) {
    // mode
    if (wf.mode !== undefined && !VALID_MODES.has(wf.mode)) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_MODE,
            message: `mode must be one of: ${Array.from(VALID_MODES).join(', ')}`,
            context: { received: String(wf.mode) },
        });
    }
    // status
    if (wf.status !== undefined && !VALID_STATUSES.has(wf.status)) {
        errors.push({
            code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TYPE,
            message: `status must be one of: ${Array.from(VALID_STATUSES).join(', ')}`,
            context: { received: String(wf.status) },
        });
    }
    // triggers
    if (wf.triggers !== undefined) {
        if (!Array.isArray(wf.triggers)) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TRIGGERS,
                message: 'triggers must be an array',
            });
        }
        else {
            for (const t of wf.triggers) {
                if (!VALID_TRIGGERS.has(t)) {
                    errors.push({
                        code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TRIGGERS,
                        message: `unknown trigger '${t}'; valid: ${Array.from(VALID_TRIGGERS).join(', ')}`,
                        context: { received: String(t) },
                    });
                }
            }
        }
    }
    // success_criteria
    if (wf.success_criteria !== undefined) {
        if (!Array.isArray(wf.success_criteria)) {
            errors.push({
                code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_SUCCESS_CRITERIA,
                message: 'success_criteria must be an array of {id, description} entries',
            });
        }
        else {
            for (let i = 0; i < wf.success_criteria.length; i++) {
                const c = wf.success_criteria[i];
                if (typeof c?.id !== 'string' || typeof c?.description !== 'string') {
                    errors.push({
                        code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_SUCCESS_CRITERIA,
                        message: `success_criteria[${i}] must have string id and description`,
                    });
                }
            }
        }
    }
    // tools
    if (wf.tools !== undefined) {
        const t = wf.tools;
        for (const key of ['allowed', 'restricted', 'forbidden']) {
            const v = t[key];
            if (v !== undefined && !Array.isArray(v)) {
                errors.push({
                    code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TOOLS_POLICY,
                    message: `tools.${key} must be a string array if present`,
                });
            }
            else if (Array.isArray(v)) {
                for (const item of v) {
                    if (typeof item !== 'string') {
                        errors.push({
                            code: WorkflowSchema_1.WORKFLOW_ERRORS.INVALID_TOOLS_POLICY,
                            message: `tools.${key} entries must be strings`,
                        });
                    }
                }
            }
        }
    }
}
