/**
 * Per-action LLM micro-call orchestrator. Phase B of the
 * 2026-05-04-ANALYSE-VIA-FAITHFUL-SKELETON-AND-MICROCALLS plan.
 *
 * Architecture:
 *   - Skeleton emitter (Phase A) produced skeleton + actionStubs[]
 *   - This orchestrator walks actionStubs[], firing a focused LLM call
 *     per stub in parallel (with a concurrency cap)
 *   - Plus a single manifest call running alongside
 *   - Each call's prompt + response + parsed output + latency is
 *     forward-logged to MicrocallProvenance
 *   - Final assembly merges skeleton + filled action bodies + manifest
 *
 * Forward-logging discipline: every LLM call records WHICH stub it
 * filled, the assembled prompt, the raw response, the parsed body, and
 * timing metadata. The report assembler reads provenance directly. No
 * heuristic matching of LLM output to spec sections.
 */
import { runPrompt } from './prompt-runner.js';
const DEFAULT_CONCURRENCY = 10;
const DEFAULT_TIMEOUT_MS = 5 * 60_000;
/**
 * Run the per-action micro-call pass + manifest call.
 * Returns the filled action bodies indexed by yamlPath plus the
 * manifest yaml string (when fired) plus full provenance.
 */
export async function runActionMicrocalls(opts) {
    const startedAt = new Date().toISOString();
    const t0 = Date.now();
    const stubs = opts.skeletonProvenance.actionStubs;
    const concurrencyCap = opts.concurrencyCap ?? DEFAULT_CONCURRENCY;
    const timeoutMs = opts.timeoutMsPerCall ?? DEFAULT_TIMEOUT_MS;
    const runPromptFn = opts.runPromptOverride ?? runPrompt;
    // Skeleton context for per-action prompts: build a per-component
    // slice. Each microcall sees only its owning component's section
    // plus a one-line listing of sibling component names. Cross-component
    // references in requires/ensures are by-string (e.g. `"User exists"`)
    // — full schema visibility wasn't actually being used. Drops user
    // prompt from ~22KB → ~5KB on idle-meta-class codebases (engines
    // 6.27.11+). When the lookup misses (defensive — shouldn't happen),
    // falls back to the full skeleton.
    const skeletonByComponent = buildComponentSkeletons(opts.skeleton, opts.skeletonProvenance.actionStubs.map((s) => s.componentName));
    const actionBodies = new Map();
    const actionCalls = [];
    // Fire manifest call in parallel with the action calls (when configured).
    const manifestPromise = opts.manifestInputs
        ? runManifestCall({ ...opts, runPromptFn, timeoutMs })
        : Promise.resolve(null);
    // Concurrency-bounded action calls.
    let inFlight = 0;
    let nextIdx = 0;
    let completed = 0;
    await new Promise((resolve) => {
        if (stubs.length === 0) {
            resolve();
            return;
        }
        const dispatchNext = () => {
            while (inFlight < concurrencyCap && nextIdx < stubs.length) {
                const ordinal = nextIdx;
                const stub = stubs[nextIdx];
                nextIdx++;
                inFlight++;
                runActionCall({
                    ordinal, stub,
                    skeletonContext: skeletonByComponent.get(stub.componentName) ?? opts.skeleton,
                    candidateStepsByMethodKey: opts.candidateStepsByMethodKey,
                    sourceByMethodKey: opts.sourceByMethodKey,
                    processContextByMethodKey: opts.processContextByMethodKey,
                    model: opts.model, sessionId: opts.sessionId, timeoutMs,
                    runPromptFn,
                    disableRateLimitRetry: opts.disableRateLimitRetry ?? false,
                    correctionByYamlPath: opts.correctionByYamlPath,
                })
                    .then((record) => {
                    actionCalls.push(record);
                    if (record.parsedBody)
                        actionBodies.set(stub.yamlPath, record.parsedBody);
                    opts.onProgress?.(record);
                })
                    .catch((e) => {
                    actionCalls.push({
                        ordinal,
                        stubRef: {
                            componentName: stub.componentName,
                            ownerKind: stub.ownerKind,
                            ownerName: stub.ownerName,
                            actionName: stub.actionName,
                            yamlPath: stub.yamlPath,
                        },
                        promptHash: '',
                        promptBytes: 0,
                        responseBytes: 0,
                        latencyMs: 0,
                        parsedBody: null,
                        failure: e?.message ?? String(e),
                        responseTextSnippet: '',
                    });
                })
                    .finally(() => {
                    inFlight--;
                    completed++;
                    if (completed === stubs.length)
                        resolve();
                    else
                        dispatchNext();
                });
            }
        };
        dispatchNext();
    });
    const manifestRecord = await manifestPromise;
    // Sort actionCalls by ordinal for stable output.
    actionCalls.sort((a, b) => a.ordinal - b.ordinal);
    const endedAt = new Date().toISOString();
    const wallTimeMs = Date.now() - t0;
    const succeeded = actionCalls.filter((c) => c.parsedBody).length;
    const failed = actionCalls.length - succeeded;
    const avgLatency = actionCalls.length > 0
        ? actionCalls.reduce((s, c) => s + c.latencyMs, 0) / actionCalls.length
        : 0;
    return {
        actionBodies,
        manifestYaml: manifestRecord?.manifestYaml ?? null,
        provenance: {
            schemaVersion: '1.0',
            startedAt,
            endedAt,
            wallTimeMs,
            concurrencyCap,
            actionCalls,
            manifestCall: manifestRecord,
            totals: {
                actionCallsAttempted: actionCalls.length,
                actionCallsSucceeded: succeeded,
                actionCallsFailed: failed,
                avgLatencyMsPerCall: Math.round(avgLatency),
            },
        },
    };
}
/** Rate-limit retry policy (engines 6.27.8+). The claude-cli provider
 *  surfaces `Server is temporarily limiting requests · Rate limited`
 *  as a transient hard-fail; without retry we burn the entire microcall
 *  budget on the first sustained rate-limit window. Backoff schedule
 *  is loosely inspired by the Anthropic SDK's defaults: 5s, 15s, 45s.
 *  Caps total retry wait around 65s per call — short enough to
 *  recover from a typical 60s rate-limit window, long enough not to
 *  pile up on a sustained outage. */
const RATE_LIMIT_BACKOFF_MS = [5_000, 15_000, 45_000];
const RATE_LIMIT_MAX_RETRIES = RATE_LIMIT_BACKOFF_MS.length;
/** True when the error message looks like a transient rate-limit. */
function isRateLimitError(message) {
    const m = message.toLowerCase();
    return (m.includes('rate limit') ||
        m.includes('temporarily limiting requests') ||
        m.includes('429') ||
        m.includes('quota exceeded'));
}
async function runActionCall(p) {
    const t0 = Date.now();
    const key = `${p.stub.candidateMethodRef.className}::${p.stub.candidateMethodRef.methodIndex}`;
    const candidateSteps = p.candidateStepsByMethodKey.get(key) ?? '(no candidate steps)';
    const processContext = p.processContextByMethodKey?.get(key) ?? '(no related processes)';
    // TODO #22: feed the raw method source so the LLM abstracts to business
    // intent instead of transcribing the candidate-step `[kind]` timeline.
    const sourceBody = p.sourceByMethodKey?.get(key) ?? '';
    // Action stub yaml — caller assembled the original skeleton; we
    // reproduce a minimal stub here for the prompt.
    const actionStub = `${p.stub.actionName}:\n  # source: ${p.stub.sourceLocation.filePath}:${p.stub.sourceLocation.lineRange[0]}-${p.stub.sourceLocation.lineRange[1]}`;
    let promptHash = '';
    let promptBytes = 0;
    let rateLimitRetries = 0;
    let lastError = null;
    // Retry loop: re-fire on rate-limit errors with exponential backoff.
    // Other errors (timeout, parse-fail downstream) bail immediately
    // since they're not transient. When disableRateLimitRetry is set
    // (surgical-retry path), we cap at one attempt — see runSurgicalRetry.
    const maxRetries = p.disableRateLimitRetry ? 0 : RATE_LIMIT_MAX_RETRIES;
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            const result = await p.runPromptFn({
                operation: 'analyse-action',
                values: {
                    ownerKind: p.stub.ownerKind,
                    ownerName: p.stub.ownerName,
                    actionName: p.stub.actionName,
                    skeletonContext: p.skeletonContext,
                    actionStub,
                    candidateSteps,
                    processContext,
                    sourceFile: p.stub.sourceLocation.filePath,
                    sourceStartLine: String(p.stub.sourceLocation.lineRange[0]),
                    sourceEndLine: String(p.stub.sourceLocation.lineRange[1]),
                    sourceBody,
                    // Analyse-verify re-fire correction (empty on the normal first pass,
                    // so the prompt's {{correction}} slot renders as a blank line and
                    // substitute() never leaves a literal token).
                    correction: p.correctionByYamlPath?.get(p.stub.yamlPath) ?? '',
                },
                model: p.model,
                timeoutMs: p.timeoutMs,
                onAssembled: ({ system, user }) => {
                    promptBytes = (system?.length ?? 0) + (user?.length ?? 0);
                    // Lightweight content fingerprint for reproducibility checks.
                    promptHash = simpleHash(system + '\0' + user);
                },
            });
            const parsedBody = await parseActionBodyYaml(result.text);
            const latencyMs = Date.now() - t0;
            return {
                ordinal: p.ordinal,
                stubRef: {
                    componentName: p.stub.componentName,
                    ownerKind: p.stub.ownerKind,
                    ownerName: p.stub.ownerName,
                    actionName: p.stub.actionName,
                    yamlPath: p.stub.yamlPath,
                },
                promptHash,
                promptBytes,
                responseBytes: result.text.length,
                latencyMs,
                parsedBody,
                failure: parsedBody ? null : 'response did not contain a parseable yaml body',
                responseTextSnippet: result.text.slice(0, 4096),
                rateLimitRetries,
            };
        }
        catch (e) {
            lastError = e;
            const errorMessage = e?.message ?? String(e);
            // Only retry on rate-limit errors; everything else is terminal.
            if (!isRateLimitError(errorMessage) || attempt >= maxRetries) {
                break;
            }
            const delayMs = RATE_LIMIT_BACKOFF_MS[attempt];
            rateLimitRetries++;
            await new Promise((resolve) => setTimeout(resolve, delayMs));
        }
    }
    // Exhausted retries (or hit a non-retryable error).
    return {
        ordinal: p.ordinal,
        stubRef: {
            componentName: p.stub.componentName,
            ownerKind: p.stub.ownerKind,
            ownerName: p.stub.ownerName,
            actionName: p.stub.actionName,
            yamlPath: p.stub.yamlPath,
        },
        promptHash,
        promptBytes,
        responseBytes: 0,
        latencyMs: Date.now() - t0,
        parsedBody: null,
        failure: lastError?.message ?? String(lastError),
        responseTextSnippet: '',
        rateLimitRetries,
    };
}
async function runManifestCall(p) {
    if (!p.manifestInputs)
        return null;
    const t0 = Date.now();
    let promptHash = '';
    let promptBytes = 0;
    let lastError = null;
    // Same rate-limit retry policy as runActionCall (and same opt-out).
    const maxRetries = p.disableRateLimitRetry ? 0 : RATE_LIMIT_MAX_RETRIES;
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            const result = await p.runPromptFn({
                operation: 'manifest',
                values: p.manifestInputs,
                model: p.model,
                timeoutMs: p.timeoutMs,
                onAssembled: ({ system, user }) => {
                    promptBytes = (system?.length ?? 0) + (user?.length ?? 0);
                    promptHash = simpleHash(system + '\0' + user);
                },
            });
            const rawManifest = extractFirstYamlBlock(result.text);
            // The prompt asks for the bare body (`name: …`, `capabilityMappings: …`)
            // so the LLM doesn't have to know the realize-loader's container shape.
            // Normalize here: prepend `specVersion: "1.0"` if absent. Without it the
            // loader rejects with `Invalid manifest format: missing "manifests"
            // container or "specVersion"` (manifest-loader.ts:150).
            const manifestYaml = rawManifest ? ensureManifestSpecVersion(rawManifest) : null;
            return {
                promptHash,
                promptBytes,
                responseBytes: result.text.length,
                latencyMs: Date.now() - t0,
                manifestYaml,
                failure: manifestYaml ? null : 'response did not contain a yaml block',
            };
        }
        catch (e) {
            lastError = e;
            const errorMessage = e?.message ?? String(e);
            if (!isRateLimitError(errorMessage) || attempt >= maxRetries) {
                break;
            }
            await new Promise((resolve) => setTimeout(resolve, RATE_LIMIT_BACKOFF_MS[attempt]));
        }
    }
    return {
        promptHash,
        promptBytes,
        responseBytes: 0,
        latencyMs: Date.now() - t0,
        manifestYaml: null,
        failure: lastError?.message ?? String(lastError),
    };
}
/** Extract the first ```yaml fenced block from an LLM response. Falls
 *  back to a bare-yaml mode (no fence) when the response begins with
 *  one of the known top-level action-body keys — handles LLMs that
 *  follow the prompt's content but skip the fence. */
function extractFirstYamlBlock(text) {
    const fenced = text.match(/```ya?ml\s*\n([\s\S]*?)```/i);
    if (fenced?.[1])
        return fenced[1];
    // Generic ``` (no language tag) — also valid yaml in many cases.
    const generic = text.match(/```\s*\n((?:steps|requires|ensures|emits)[\s\S]*?)```/);
    if (generic?.[1])
        return generic[1];
    // Bare mode: the response starts with one of our keys, no fence.
    const trimmed = text.trim();
    if (/^(steps|requires|ensures|emits)\s*:/.test(trimmed)) {
        return trimmed;
    }
    return null;
}
/** Parse an action body yaml — expects steps/requires/ensures/publishes keys.
 *  Tolerates LLMs that wrap the body inside the action name (e.g.
 *  `applyEffect: { steps: [...] }`) — we unwrap a single-key parent
 *  whose child has the expected body shape. */
export async function parseActionBodyYaml(text) {
    const yamlBody = extractFirstYamlBlock(text);
    if (!yamlBody)
        return null;
    let parsed;
    try {
        const yamlModule = await import('js-yaml');
        // Engines 6.30.4+: sanitize backslashes in double-quoted strings.
        // YAML's double-quoted-string spec defines a fixed set of escapes
        // (\n, \t, \\, \"`, etc.); a backslash followed by any other char
        // (e.g. `\w`, `\s`, `\d`) is undefined behaviour, and js-yaml throws.
        // LLM responses about source containing regex literals (e.g.
        // `/(\w+)\s*\(/g`) hit this — Sonnet emits `"... /(\w+)\s*\(/g"`
        // verbatim. Pre-process the body to escape rogue backslashes.
        const sanitized = sanitizeYamlBackslashes(yamlBody);
        try {
            parsed = yamlModule.load(sanitized);
        }
        catch {
            // Engines 6.30.5+: yaml.load failed, try the regex line-fallback.
            // Common LLM-emitted shapes that confuse js-yaml: multi-line step
            // bullets with parenthetical continuations; `- key: value` mapping
            // forms; semicolons in the middle of plain scalars. The fallback
            // extracts steps/requires/ensures by line-pattern, treating each
            // `- foo` as a string. May lose continuation content but recovers
            // the structural skeleton — better than null.
            parsed = lineBasedYamlFallback(sanitized);
            if (!parsed)
                return null;
        }
        if (!parsed || typeof parsed !== 'object')
            return null;
        const bodyKeys = ['steps', 'requires', 'ensures', 'publishes', 'emits', 'description'];
        const hasBodyShape = (o) => o && typeof o === 'object'
            && bodyKeys.some((k) => k in o);
        // Unwrap single-key parent if present (`{ actionName: { steps: ... } }`).
        if (!hasBodyShape(parsed)) {
            const topKeys = Object.keys(parsed);
            if (topKeys.length === 1 && hasBodyShape(parsed[topKeys[0]])) {
                parsed = parsed[topKeys[0]];
            }
        }
        if (!hasBodyShape(parsed))
            return null;
        // Engines 6.30.5+: when a list entry is `{key: value}` (LLM emitted
        // `- validate: body.nodes ...` as a YAML mapping rather than as a
        // string), flatten to "key: value" so it survives the string filter.
        // Was silently dropping these before, leaving spec actions with
        // shorter step lists than the LLM intended.
        const toStringList = (raw) => {
            if (!Array.isArray(raw))
                return undefined;
            return raw.map((s) => {
                if (typeof s === 'string')
                    return s;
                if (s && typeof s === 'object' && !Array.isArray(s)) {
                    const entries = Object.entries(s);
                    if (entries.length === 1) {
                        const [k, v] = entries[0];
                        if (typeof v === 'string')
                            return `${k}: ${v}`;
                        if (Array.isArray(v) && v.every((x) => typeof x === 'string')) {
                            return `${k}: ${v.join('; ')}`;
                        }
                    }
                }
                return null;
            }).filter((s) => typeof s === 'string' && s.length > 0);
        };
        const out = {};
        if (parsed.steps)
            out.steps = toStringList(parsed.steps);
        if (parsed.requires)
            out.requires = toStringList(parsed.requires);
        if (parsed.ensures)
            out.ensures = toStringList(parsed.ensures);
        if (parsed.publishes)
            out.publishes = toStringList(parsed.publishes);
        else if (parsed.emits)
            out.publishes = toStringList(parsed.emits);
        return out;
    }
    catch {
        return null;
    }
}
/**
 * Engines 6.30.5+. Line-based fallback parser when js-yaml throws on
 * LLM-emitted "almost-yaml". Extracts the standard body keys (steps,
 * requires, ensures, publishes, emits, description) by matching their
 * header lines and capturing subsequent `- foo` items as strings.
 *
 * Loses some content fidelity vs a real yaml parse — multi-line step
 * continuations are dropped because we only keep lines starting with
 * `- ` — but recovers the structural skeleton when js-yaml chokes on
 * subtle syntax errors (parenthetical continuations, semicolons in
 * plain scalars, mapping-as-step + indent confusion). Better than
 * dropping the whole microcall.
 *
 * Returns an object shaped like a yaml parse result, or null if no
 * recognisable section headers are found.
 */
function lineBasedYamlFallback(yamlText) {
    const sectionKeys = new Set(['steps', 'requires', 'ensures', 'publishes', 'emits', 'description']);
    const result = {};
    let currentKey = null;
    let currentList = null;
    const finishCurrent = () => {
        if (currentKey && currentList) {
            result[currentKey] = currentList;
        }
        currentKey = null;
        currentList = null;
    };
    for (const line of yamlText.split('\n')) {
        // Section header at top-level indent (0 or 2 spaces).
        const headerMatch = line.match(/^( {0,4})([a-z][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
        if (headerMatch && sectionKeys.has(headerMatch[2])) {
            finishCurrent();
            currentKey = headerMatch[2];
            const inline = headerMatch[3].trim();
            if (currentKey === 'description' && inline) {
                result['description'] = inline.replace(/^["']|["']$/g, '');
                currentKey = null;
            }
            else if (inline && !inline.startsWith('|') && !inline.startsWith('>') && inline !== '[]') {
                // Inline scalar value (rare for arrays). Wrap as single-element list.
                currentList = [inline];
            }
            else {
                currentList = [];
            }
            continue;
        }
        if (!currentKey || !currentList)
            continue;
        // Match list items.
        const itemMatch = line.match(/^\s*-\s*(.+)$/);
        if (itemMatch) {
            const item = itemMatch[1].trim().replace(/^["']|["']$/g, '');
            if (item)
                currentList.push(item);
        }
    }
    finishCurrent();
    return Object.keys(result).length > 0 ? result : null;
}
/** Parse an `spv validate` error output and classify each error by
 *  whether it falls under one of the skeleton's action stubs. */
export function classifyValidationErrors(errorText, skeletonProvenance) {
    const stubPaths = skeletonProvenance.actionStubs.map((s) => s.yamlPath);
    // Sort by length descending so we match the most specific stub first.
    const stubPathsByLen = [...stubPaths].sort((a, b) => b.length - a.length);
    const actionPathErrors = new Map();
    const structuralErrors = [];
    for (const line of errorText.split('\n')) {
        const trimmed = line.trim();
        if (!trimmed || trimmed.startsWith('Validation failed'))
            continue;
        // Errors look like: `/components/X/services/Y/.../Z: error message`
        // Convert leading slash-path to dotted yamlPath.
        const m = trimmed.match(/^\/([^\s:]+)(?::|$|\s)/);
        if (!m) {
            structuralErrors.push(trimmed);
            continue;
        }
        const yamlPath = m[1].replace(/\//g, '.');
        // Find the longest stub path that is a prefix of this error path.
        const matchingStub = stubPathsByLen.find((sp) => yamlPath === sp || yamlPath.startsWith(sp + '.'));
        if (matchingStub) {
            if (!actionPathErrors.has(matchingStub))
                actionPathErrors.set(matchingStub, []);
            actionPathErrors.get(matchingStub).push(trimmed);
        }
        else {
            structuralErrors.push(trimmed);
        }
    }
    return { actionPathErrors, structuralErrors };
}
/** Re-fire microcalls for action stubs whose previous body produced
 *  validation errors. Builds a focused prompt-input set that includes
 *  the prior failed body + the specific error, asking the LLM to fix it.
 *
 *  Skeleton-structural errors in `classification.structuralErrors` are
 *  NOT retried — they're skeleton-emitter bugs. The caller (analyse-
 *  runner) records them as humanReviewItems via the verify recorder. */
export async function runSurgicalRetry(opts) {
    const { skeleton, skeletonProvenance, candidateStepsByMethodKey, sourceByMethodKey, processContextByMethodKey, prior } = opts;
    const stubsToRetry = skeletonProvenance.actionStubs.filter((s) => prior.classification.actionPathErrors.has(s.yamlPath));
    if (stubsToRetry.length === 0) {
        return { actionBodies: new Map(prior.actionBodies), retriedStubs: 0, retryProvenance: [] };
    }
    // Re-fire microcalls for affected stubs, with the prior failed body +
    // the specific error feeding back into the prompt as additional context.
    // Reuses the standard analyse-action prompt; the candidate-step timeline
    // alone is usually enough for the LLM to produce a clean body. (More
    // sophisticated error-feedback prompting could ship as v2 if needed.)
    const targetedProvenance = {
        ...skeletonProvenance,
        actionStubs: stubsToRetry,
    };
    // Surgical retry runs serially with rate-limit retry disabled. The
    // first-pass orchestrator already tried 4× per stub on 429s; doing
    // it again here just compounds amplification (4 × 4 = 16 invocations
    // worst-case per stub) without improving content quality. Failed
    // surgical retries leave the prior body — assembleFinalSpec emits a
    // placeholder for missing bodies that schema-validates cleanly.
    const result = await runActionMicrocalls({
        skeleton,
        skeletonProvenance: targetedProvenance,
        candidateStepsByMethodKey,
        sourceByMethodKey,
        processContextByMethodKey,
        model: opts.model,
        concurrencyCap: 1,
        disableRateLimitRetry: true,
        timeoutMsPerCall: opts.timeoutMsPerCall,
        runPromptOverride: opts.runPromptOverride,
        onProgress: opts.onProgress,
        correctionByYamlPath: opts.correctionByYamlPath,
    });
    // Merge: take prior bodies, replace the retried ones with new bodies
    // (when parse succeeded). Failed retries leave the prior body alone.
    const merged = new Map(prior.actionBodies);
    for (const [path, body] of result.actionBodies) {
        merged.set(path, body);
    }
    return {
        actionBodies: merged,
        retriedStubs: stubsToRetry.length,
        retryProvenance: result.provenance.actionCalls,
    };
}
/**
 * Extract just the named component's slice from the full skeleton.
 * Preserves the file preamble (leading `#` comments + the `components:`
 * header line) and adds a one-line listing of sibling component names
 * so the LLM still has landscape awareness without paying the full
 * skeleton cost.
 *
 * Cuts the per-call user prompt by ~80% on idle-meta-class codebases
 * (24 entities across 8 components → ~17.8KB skeleton → ~1KB slice).
 *
 * Falls back to the full skeleton when the component header isn't found
 * (defensive — shouldn't happen since stubs are produced from the same
 * skeleton, but a fallback keeps the path safe under future refactors).
 */
export function extractComponentSlice(skeleton, componentName) {
    const lines = skeleton.split('\n');
    const compHeaderRe = /^ {2}([A-Z][A-Za-z0-9_]*):\s*$/;
    let componentsHeaderIdx = -1;
    let compStartIdx = -1;
    let compEndIdx = lines.length;
    const otherComponents = [];
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        if (componentsHeaderIdx === -1) {
            if (/^components:\s*$/.test(line))
                componentsHeaderIdx = i;
            continue;
        }
        const m = line.match(compHeaderRe);
        if (!m)
            continue;
        const name = m[1];
        if (name === componentName) {
            compStartIdx = i;
        }
        else if (compStartIdx !== -1 && compEndIdx === lines.length) {
            compEndIdx = i;
            otherComponents.push(name);
        }
        else {
            otherComponents.push(name);
        }
    }
    if (componentsHeaderIdx === -1 || compStartIdx === -1)
        return skeleton;
    const preamble = lines.slice(0, componentsHeaderIdx + 1);
    const otherListing = otherComponents.length > 0
        ? [`  # Sibling components (names only): ${otherComponents.join(', ')}`]
        : [];
    const compBlock = lines.slice(compStartIdx, compEndIdx);
    return [...preamble, ...otherListing, ...compBlock].join('\n');
}
/** Build a per-component skeleton-slice map. Computed once per
 *  microcall run; each stub looks up its component's slice. */
function buildComponentSkeletons(skeleton, componentNames) {
    const result = new Map();
    for (const name of componentNames) {
        if (result.has(name))
            continue;
        result.set(name, extractComponentSlice(skeleton, name));
    }
    return result;
}
/**
 * Engines 6.30.4+. Pre-process a YAML body to escape rogue backslashes
 * inside double-quoted strings. YAML's double-quoted spec defines exactly
 * this set of valid escapes: \0 \a \b \t \n \v \f \r \e \space \" \\ \/
 * \N \_ \L \P \xNN \uNNNN \UNNNNNNNN. Any other `\X` is undefined; js-yaml
 * throws.
 *
 * LLM responses about source containing regex literals (e.g. `/(\w+)\s/g`)
 * include those backslashes verbatim in YAML strings, which crashes parsing.
 * This function walks each double-quoted string and doubles up any
 * backslash that ISN'T followed by a known-valid escape char.
 *
 * Single-quoted strings, plain scalars, and content outside strings are
 * left untouched (single-quoted YAML strings treat backslash literally,
 * so they don't need fixing).
 */
function sanitizeYamlBackslashes(yamlBody) {
    // Walk char by char, tracking double-quoted-string state. When inside,
    // any `\X` where X is not in the valid-escape set gets `\` doubled.
    const VALID_ESCAPES = new Set([
        '0', 'a', 'b', 't', 'n', 'v', 'f', 'r', 'e', ' ', '"', '\\', '/',
        'N', '_', 'L', 'P', 'x', 'u', 'U',
        '\t', // tab continuation
    ]);
    let out = '';
    let inDouble = false;
    let inSingle = false;
    for (let i = 0; i < yamlBody.length; i++) {
        const c = yamlBody[i];
        if (!inDouble && !inSingle) {
            if (c === '"')
                inDouble = true;
            else if (c === "'")
                inSingle = true;
            out += c;
            continue;
        }
        if (inSingle) {
            if (c === "'")
                inSingle = false;
            out += c;
            continue;
        }
        // inDouble = true
        if (c === '\\') {
            const next = yamlBody[i + 1];
            if (next !== undefined && VALID_ESCAPES.has(next)) {
                // Valid escape — pass through.
                out += c + next;
                i++;
            }
            else if (next === undefined) {
                // Trailing lone backslash — keep as-is; let js-yaml decide.
                out += c;
            }
            else {
                // Rogue backslash — double it so the parser sees a literal `\`.
                out += '\\\\';
            }
            continue;
        }
        if (c === '"') {
            inDouble = false;
            out += c;
            continue;
        }
        out += c;
    }
    return out;
}
/** Tiny non-cryptographic hash — sufficient for reproducibility fingerprinting. */
function simpleHash(s) {
    let h = 5381;
    for (let i = 0; i < s.length; i++)
        h = ((h << 5) + h + s.charCodeAt(i)) | 0;
    return (h >>> 0).toString(16);
}
/**
 * Merge the skeleton with the filled action bodies. Returns the final
 * spec yaml. Uses simple line-based replacement: for each yamlPath in
 * actionBodies, find the action's stub in the skeleton and inject the
 * body fields immediately after the stub line.
 *
 * The skeleton's stub format is:
 *   <indent>actionName:
 *   <indent>  # body filled by per-action LLM micro-call (skeleton-emitter stub)
 *
 * We replace the comment line with the action body fields at body-indent.
 *
 * `opts.behavioursSkipped` distinguishes the two empty-body situations so the
 * placeholder description tells the truth: when the behaviours pass was never
 * run (skeleton-only analyse), no microcall was attempted and no
 * `microcall-decisions.json` is written — emitting "microcall failed" there is
 * misleading. The genuine per-stub failure wording is reserved for the path
 * where a microcall ran and returned nothing.
 */
export function assembleFinalSpec(skeleton, skeletonProvenance, actionBodies, opts = {}) {
    const lines = skeleton.split('\n');
    // Build a quick map: yamlPath → starting line number of the stub.
    const stubLineByPath = new Map();
    for (const stub of skeletonProvenance.actionStubs) {
        const emission = skeletonProvenance.emissions.find((e) => e.yamlPath === stub.yamlPath && e.rule === 'action-stub-from-business-method');
        if (emission)
            stubLineByPath.set(stub.yamlPath, emission.lineRange[0]);
    }
    // Walk EVERY action stub (not just the ones with bodies). Stubs whose
    // microcall failed get a minimal-valid fallback body so the spec
    // validates without a giant-call retry. Failed stubs are visible
    // in `microcall-decisions.json` for human review.
    // Walk in REVERSE source-order so line edits don't invalidate later
    // positions.
    const allStubPaths = skeletonProvenance.actionStubs
        .map((s) => s.yamlPath)
        .sort((a, b) => (stubLineByPath.get(b) ?? 0) - (stubLineByPath.get(a) ?? 0));
    for (const yamlPath of allStubPaths) {
        const stubLineIdx = stubLineByPath.get(yamlPath);
        if (stubLineIdx === undefined)
            continue;
        const stubLine = lines[stubLineIdx - 1] ?? '';
        const bodyIndent = (stubLine.match(/^(\s*)/)?.[1] ?? '') + '  ';
        const body = actionBodies.get(yamlPath);
        const bodyLines = [];
        if (body) {
            // Microcall succeeded — emit its authored body.
            if (body.steps?.length) {
                bodyLines.push(`${bodyIndent}steps:`);
                for (const s of body.steps)
                    bodyLines.push(`${bodyIndent}  - ${quoteIfNeeded(s)}`);
            }
            if (body.requires?.length) {
                bodyLines.push(`${bodyIndent}requires:`);
                for (const r of body.requires)
                    bodyLines.push(`${bodyIndent}  - ${quoteIfNeeded(r)}`);
            }
            if (body.ensures?.length) {
                bodyLines.push(`${bodyIndent}ensures:`);
                for (const e of body.ensures)
                    bodyLines.push(`${bodyIndent}  - ${quoteIfNeeded(e)}`);
            }
            if (body.publishes?.length) {
                bodyLines.push(`${bodyIndent}publishes:`);
                for (const ev of body.publishes) {
                    // Schema expects each publishes entry to be a simple event-name
                    // identifier. LLMs sometimes inject YAML mapping syntax
                    // (`EventName: { fields }`) or em-dash commentary. Sanitize to
                    // the leading identifier-shaped portion; if none, quote the full
                    // string to keep yaml valid.
                    const idMatch = ev.match(/^[A-Za-z_][A-Za-z0-9_]*/);
                    const safe = idMatch ? idMatch[0] : null;
                    if (safe)
                        bodyLines.push(`${bodyIndent}  - ${safe}`);
                    else
                        bodyLines.push(`${bodyIndent}  - ${quoteIfNeeded(ev)}`);
                }
            }
        }
        else if (opts.behavioursSkipped) {
            // Behaviours pass was never run (skeleton-only analyse). No microcall
            // was attempted, so don't claim one "failed" or point at a
            // `microcall-decisions.json` that is never written. Emit a minimal-valid
            // stub that names the real next step.
            bodyLines.push(`${bodyIndent}description: "(no behaviour body — run \`spv ai behaviours <spec>\` to fill)"`);
            bodyLines.push(`${bodyIndent}steps: []`);
        }
        else {
            // Microcall failed — emit a minimal-valid stub so the spec passes
            // schema validation. The action's name is preserved; the body is
            // marked for human review (visible via `microcall-decisions.json`).
            bodyLines.push(`${bodyIndent}description: "(microcall failed; see microcall-decisions.json)"`);
            bodyLines.push(`${bodyIndent}steps: []`);
        }
        // If body has no content (rare — succeeded but empty), emit minimal too.
        if (bodyLines.length === 0) {
            bodyLines.push(`${bodyIndent}description: "(microcall returned empty body)"`);
            bodyLines.push(`${bodyIndent}steps: []`);
        }
        // Replace the placeholder comment line (right after the actionName line).
        const placeholderArrIdx = stubLineIdx;
        if (lines[placeholderArrIdx]?.trim().startsWith('# body filled')) {
            lines.splice(placeholderArrIdx, 1, ...bodyLines);
        }
        else {
            lines.splice(placeholderArrIdx, 0, ...bodyLines);
        }
    }
    // Post-process: declare every event referenced in any `publishes:`
    // block at the owning component level. Without this, the structural
    // invariant `every-emit-references-declared-event` fails (warning
    // only, but the spec is incomplete — events should be first-class
    // declarations under the component, not implicit).
    return injectEventDeclarations(lines.join('\n'));
}
/**
 * Walk the spec, collect every event name in `publishes:` blocks per
 * owning component, and inject minimal `events:` declarations after each
 * affected component's `version:` line.
 *
 * Idempotent: if a component already has an `events:` block declaring
 * the event, leave it alone. Adds only what's missing.
 */
function injectEventDeclarations(spec) {
    const lines = spec.split('\n');
    // Pass 1: collect publishes per component and existing event names per
    // component.
    const componentLineOf = new Map(); // specName → line idx of header
    const versionLineOf = new Map(); // specName → line idx of `    version: "..."`
    const publishedByComponent = new Map(); // specName → event names
    const declaredByComponent = new Map(); // specName → event names already in events:
    let currentComponent = null;
    let inPublishesBlock = false;
    let inEventsBlock = false;
    let eventsBlockIndent = -1;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i] ?? '';
        const compMatch = line.match(/^  ([A-Z][A-Za-z0-9_]*):\s*$/);
        if (compMatch) {
            currentComponent = compMatch[1];
            componentLineOf.set(currentComponent, i);
            inPublishesBlock = false;
            inEventsBlock = false;
            continue;
        }
        if (currentComponent && /^    version:/.test(line)) {
            versionLineOf.set(currentComponent, i);
            continue;
        }
        if (currentComponent && /^    events:\s*$/.test(line)) {
            inEventsBlock = true;
            eventsBlockIndent = 4;
            continue;
        }
        if (inEventsBlock) {
            // Stop the events block when we hit a sibling section (4-indent)
            // or shallower (component header).
            const m = line.match(/^( {0,4})(\S)/);
            if (m && m[1].length <= 4 && !/^      [A-Z]/.test(line)) {
                // Either a 4-indent sibling (new section) or component header.
                if (m[1].length < eventsBlockIndent || /^[^ ]/.test(line)) {
                    inEventsBlock = false;
                }
                else if (m[1].length === eventsBlockIndent && !line.includes(':')) {
                    inEventsBlock = false;
                }
                else if (/^    [a-z]/.test(line)) {
                    // New 4-indent key like `models:`, `controllers:`, etc.
                    inEventsBlock = false;
                }
            }
            // Match event names declared inside events block (6-indent identifier).
            const evDecl = line.match(/^      ([A-Z][A-Za-z0-9_]*):\s*$/);
            if (evDecl && inEventsBlock) {
                const set = declaredByComponent.get(currentComponent) ?? new Set();
                set.add(evDecl[1]);
                declaredByComponent.set(currentComponent, set);
            }
        }
        // publishes: blocks are at variable depth (action body or model behavior).
        if (currentComponent && /publishes:\s*$/.test(line)) {
            inPublishesBlock = true;
            continue;
        }
        if (inPublishesBlock) {
            const itemMatch = line.match(/^\s*-\s+([A-Z][A-Za-z0-9_]*)\s*$/);
            if (itemMatch) {
                const set = publishedByComponent.get(currentComponent) ?? new Set();
                set.add(itemMatch[1]);
                publishedByComponent.set(currentComponent, set);
                continue;
            }
            // Anything else terminates the publishes block.
            if (line.trim() !== '')
                inPublishesBlock = false;
        }
    }
    // Pass 2: for each component, compute the missing-event set and emit
    // declarations after the version line. Process components in REVERSE
    // line order so insertions don't shift earlier indices.
    const insertions = [];
    for (const [comp, published] of publishedByComponent) {
        const declared = declaredByComponent.get(comp) ?? new Set();
        const missing = [...published].filter((ev) => !declared.has(ev)).sort();
        if (missing.length === 0)
            continue;
        const versionLine = versionLineOf.get(comp);
        if (versionLine === undefined)
            continue;
        const out = [];
        if (!declaredByComponent.has(comp)) {
            out.push('    events:');
        }
        for (const ev of missing) {
            out.push(`      ${ev}:`);
            out.push(`        description: "Auto-declared by skeleton emitter — published by an action in this component."`);
        }
        insertions.push({ atLineIdx: versionLine + 1, lines: out });
    }
    insertions.sort((a, b) => b.atLineIdx - a.atLineIdx);
    for (const ins of insertions) {
        lines.splice(ins.atLineIdx, 0, ...ins.lines);
    }
    return lines.join('\n');
}
/**
 * Normalise an LLM-emitted manifest body so the realize manifest-loader
 * accepts it. The loader (manifest-loader.ts) accepts either
 * `manifests:` container or `specVersion:` at top-level — the prompt
 * deliberately produces neither (it asks for a bare manifest body), so
 * we prepend `specVersion: "1.0"` here. Idempotent: if the LLM already
 * emitted `specVersion:` (or wrapped in `manifests:`), pass through.
 */
export function ensureManifestSpecVersion(yaml) {
    if (/^\s*manifests\s*:/m.test(yaml))
        return yaml;
    if (/^\s*specVersion\s*:/m.test(yaml))
        return yaml;
    return `specVersion: "1.0"\n${yaml}`;
}
function quoteIfNeeded(s) {
    // Always emit double-quoted form. Backslashes and embedded quotes
    // MUST be escaped — the prior "default to quoted" branch dropped
    // escaping, which broke whenever the LLM produced bullets like
    // `result matches "${prefix}-${id}"` (YAML treats the inner quote
    // as terminating the outer string and chokes on the rest).
    return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
//# sourceMappingURL=microcall-orchestrator.js.map