/**
 * Per-action recovery tier — Phase 5a of
 * `2026-05-14-PER-ACTION-RECOVERY-TIER.md`.
 *
 * The realize per-owner runner has three tiers:
 *
 *   Tier 1 — whole-owner emit (`per-owner-emit.ts`). One LLM call emits
 *            the entire `<Owner>.ai.ts`. Works for the easy majority.
 *   Tier 2 — THIS MODULE. When Tier 1 fails (structural-preservation
 *            reject, parse failure, LLM decline), instead of dropping
 *            straight to a γ-stub file, retry the owner ACTION-BY-ACTION
 *            — but with the FULL owner context in every call.
 *   Tier 3 — γ-stub placeholder file (`renderFallbackPlaceholderFile`
 *            in `per-owner-runner.ts`). Only reached if Tier 2 produced
 *            ZERO real bodies.
 *
 * "Wide context, narrow output" — the distinction from the abandoned
 * Realize L3 per-action emit: L3 gave the LLM NARROW context (one
 * action's steps, no class shell, no capabilities) and it starved
 * (29/94 actions came back as throw-stubs). This keeps the FULL owner
 * context — every action's [PRE-BAKED] scaffolding, the target runtime,
 * manifest capabilities, V2 clients, cross-service operation surface —
 * in every per-action call, and only narrows the OUTPUT to one function
 * body. The reasoning load per call drops (the model holds one function
 * in its head, not all of them) without re-introducing context
 * starvation.
 *
 * Motivation (Phase 3 trial, 2026-05-14): the quant-fidelity gap between
 * Ollama (Q4 GGUF) and MarrBox (FP8) — SAME model, Qwen3-Coder-30B-A3B
 * — localized to the single hardest owner (`ReactiveStateEngine`, 7
 * interrelated functions). The Mac Mini's 24GB RAM structurally pins it
 * at ~Q4 (Q6/Q8 don't fit), so "run a better quant" is closed. This
 * makes the TASK easier instead.
 *
 * Phase 5a scope: declaration-order emission, full owner SCAFFOLDING
 * context per call. Phase 5b adds dependency-ordered emission +
 * already-emitted siblings shown as concrete code. Phase 5c adds the
 * action-count cap telemetry.
 */
import { generateText } from 'ai';
import { assembleSystemWithSkills } from '../ai/prompt-runner.js';
import { resolveProviderId } from '../ai/model-resolver.js';
import { buildPerActionScaffolding, } from './per-owner-emit.js';
import { validateLlmOutputPreservesConventions, DEFAULT_STRUCTURAL_DROP_THRESHOLD, } from './structural-validator.js';
import { extractTypescriptBlock } from './owner-emit-shared.js';
import { safeFunctionName } from '@specverse/types';
/**
 * Render a γ-stub for a single action whose per-action recovery emit
 * failed. The error string says "per-action recovery fell back" — the
 * stub-completeness verifier's GAMMA_FALLBACK_RE matches this (STUB001,
 * auto-reemit-eligible), distinct from an LLM's own "not implemented"
 * throw (STUB002, surface-spec-gap). Reserved-word action names get the
 * re-export-rename treatment so the file still parses.
 */
function renderActionGammaStub(actionName) {
    const body = `  throw new Error("${actionName}: not implemented (per-action recovery fell back)");`;
    const { name: safeName, reserved } = safeFunctionName(actionName);
    if (reserved) {
        return (`/** Placeholder — per-action recovery failed for ${actionName}. */\n` +
            `async function ${safeName}(_input: any): Promise<any> {\n${body}\n}\n` +
            `export { ${safeName} as ${actionName} };`);
    }
    return (`/** Placeholder — per-action recovery failed for ${actionName}. */\n` +
        `export async function ${safeName}(_input: any): Promise<any> {\n${body}\n}`);
}
/** Collect the pre-baked snippets for ONE action (its matched steps). */
function prebakedForAction(action) {
    const out = [];
    action.perStepMatches.forEach((match, idx) => {
        if (match?.matched && match.call) {
            out.push({
                stepNum: idx + 1,
                stepText: action.spec.steps[idx] ?? '',
                snippet: match.call,
            });
        }
    });
    return out;
}
/** esbuild parse-check a single per-action emission as a TS module.
 *  Returns null on success, an error string on failure. */
async function parseCheckModule(code) {
    try {
        const esbuild = await import('esbuild');
        await esbuild.transform(code, { loader: 'ts', format: 'esm', target: 'es2022' });
        return null;
    }
    catch (err) {
        return err?.errors?.[0]?.text || err?.message || 'unknown parse error';
    }
}
/** Build the per-action recovery user prompt. Full owner context +
 *  "emit ONLY this one action". */
function buildActionPrompt(request, target) {
    const allScaffolding = buildPerActionScaffolding(request.actions);
    const targetScaffolding = buildPerActionScaffolding([target]);
    const parts = [];
    parts.push(`You are recovering ONE function for the \`${request.ownerName}\` ` +
        `${request.ownerKind}. The whole-owner emit for it could not be ` +
        `produced coherently, so each action is being emitted on its own — ` +
        `but with the FULL owner context below. Emit ONLY the one action ` +
        `named in EMIT TARGET.`);
    parts.push('');
    parts.push(`## TARGET RUNTIME`);
    parts.push(request.targetRuntime ?? 'Node.js (backend)');
    parts.push('');
    parts.push(`## AVAILABLE CAPABILITIES`);
    parts.push(request.availableCapabilities ?? '(none — only `input: any` is available)');
    parts.push('');
    parts.push(`## AVAILABLE CLIENTS`);
    parts.push(request.availableClients ?? '(none)');
    parts.push('');
    parts.push(`## AVAILABLE SPEC SURFACE (other services / controllers — call these, don't reimplement)`);
    parts.push(request.availableSpecSurface ?? '(no other services / controllers in this spec)');
    parts.push('');
    parts.push(`## ALL ACTIONS ON THIS OWNER (context — do NOT emit these)`);
    parts.push(allScaffolding);
    parts.push('');
    parts.push(`## EMIT TARGET: ${target.actionName}`);
    parts.push(targetScaffolding);
    parts.push('');
    parts.push(`Emit ONLY \`export async function ${target.actionName}(...)\` inside one ` +
        '```typescript code block.');
    parts.push(`- Put any imports the function needs on single lines ABOVE it.`);
    parts.push(`- Honour every [PRE-BAKED] snippet verbatim — do not paraphrase.`);
    parts.push(`- Do NOT emit the other actions; they are shown for context only.`);
    return parts.join('\n');
}
/** Pull single-line \`import ...\` statements out of an emission. The
 *  realize-emit skill convention emits single-line imports; multi-line
 *  imports (rare) are left in place and ride the post-emit tsc verifier. */
function extractImports(code) {
    const imports = [];
    const restLines = [];
    for (const line of code.split('\n')) {
        if (/^\s*import\s.+;?\s*$/.test(line) && line.includes('from')) {
            imports.push(line.trim());
        }
        else {
            restLines.push(line);
        }
    }
    return { imports, rest: restLines.join('\n').trim() };
}
/** Assemble the final `<Owner>.ai.ts` from per-action results: hoisted
 *  + deduped imports, then each function (real emission or γ-stub). */
function assembleOwnerFile(ownerName, perAction) {
    const allImports = new Set();
    const bodies = [];
    for (const entry of perAction) {
        if (entry.real) {
            const { imports, rest } = extractImports(entry.code);
            for (const imp of imports)
                allImports.add(imp);
            bodies.push(rest);
        }
        else {
            bodies.push(entry.code);
        }
    }
    const header = [
        '/**',
        ` * ${ownerName} — Per-owner AI behaviors (per-action recovery)`,
        ' *',
        ' * The whole-owner emit failed; each action below was emitted on its',
        ' * own with full owner context (Tier 2). Actions that still could not',
        ' * be emitted carry a γ-stub — the post-emit feedback loop will retry',
        ' * those (they are STUB001, auto-reemit-eligible).',
        ' *',
        ` * Generated: ${new Date().toISOString().split('T')[0]}`,
        ' */',
    ].join('\n');
    const importBlock = [...allImports].sort().join('\n');
    return [header, '', importBlock, importBlock ? '' : null, bodies.join('\n\n'), '']
        .filter((s) => s !== null)
        .join('\n');
}
/**
 * Build a {@link PerActionRecoveryFn}. The returned function loops over
 * the owner's actions, emits each with full owner context, validates
 * (structural + parse), γ-stubs the ones that fail, and assembles the
 * owner file. Returns null only when EVERY action γ-stubbed (caller
 * then uses the Tier 3 placeholder, which is equivalent).
 */
export function createPerActionRecovery(opts = {}) {
    const timeoutMs = opts.timeoutMs ?? 5 * 60_000;
    const skills = opts.skills ?? ['realize-emit'];
    const providerId = resolveProviderId();
    const systemPrompt = assembleSystemWithSkills('', providerId, skills);
    const envThreshold = process.env.SPECVERSE_REALIZE_STRUCTURAL_DROP_THRESHOLD;
    const dropThreshold = opts.dropThreshold ??
        (envThreshold !== undefined && !Number.isNaN(parseFloat(envThreshold))
            ? parseFloat(envThreshold)
            : DEFAULT_STRUCTURAL_DROP_THRESHOLD);
    const callLlm = opts.generateOverride ??
        (async (system, user) => {
            const result = await Promise.race([
                generateText({
                    model: opts.model,
                    prompt: user,
                    ...(system ? { system } : {}),
                }),
                new Promise((_, reject) => setTimeout(() => reject(new Error('per-action recovery timeout')), timeoutMs)),
            ]);
            return result.text ?? '';
        });
    return async (request) => {
        const perAction = [];
        let realCount = 0;
        for (const action of request.actions) {
            const user = buildActionPrompt(request, action);
            let emitted = null;
            try {
                const text = await callLlm(systemPrompt, user);
                emitted = extractTypescriptBlock(text);
            }
            catch {
                emitted = null;
            }
            if (!emitted || emitted.trim().length === 0) {
                perAction.push({ actionName: action.actionName, code: renderActionGammaStub(action.actionName), real: false });
                continue;
            }
            // Gate 1: parse check.
            const parseError = await parseCheckModule(emitted);
            if (parseError !== null) {
                if (process.env.SPECVERSE_VERBOSE) {
                    console.error(`   ✗ per-action recovery parse-failed for ${request.ownerName}.${action.actionName}: ${parseError.slice(0, 160)}`);
                }
                perAction.push({ actionName: action.actionName, code: renderActionGammaStub(action.actionName), real: false });
                continue;
            }
            // Gate 2: structural preservation (this action's snippets only).
            const structural = validateLlmOutputPreservesConventions(emitted, prebakedForAction(action), dropThreshold);
            if (structural.ok === false) {
                if (process.env.SPECVERSE_VERBOSE) {
                    console.error(`   ✗ per-action recovery structural-failed for ${request.ownerName}.${action.actionName}`);
                }
                perAction.push({ actionName: action.actionName, code: renderActionGammaStub(action.actionName), real: false });
                continue;
            }
            perAction.push({ actionName: action.actionName, code: emitted, real: true });
            realCount++;
        }
        // Zero real bodies → no better than the Tier 3 placeholder. Return
        // null so the caller uses its own (reserved-word-safe) placeholder.
        if (realCount === 0)
            return null;
        if (process.env.SPECVERSE_VERBOSE) {
            console.error(`   ↻ per-action recovery for ${request.ownerName}: ${realCount}/${request.actions.length} action(s) recovered`);
        }
        return assembleOwnerFile(request.ownerName, perAction);
    };
}
//# sourceMappingURL=per-action-recovery.js.map