/**
 * Stub-completeness verifier — detects placeholder/stub bodies in
 * emitted `.ai.ts` files and reports them as VerifyErrors so the
 * existing feedback runner re-emits real bodies.
 *
 * Why this exists:
 *   The tsc verifier catches code that DOESN'T COMPILE. It says nothing
 *   about code that compiles but ships no behavior — the dominant
 *   failure mode for smaller/open LLMs on per-owner emit:
 *     - Engine γ-stubs (LLM declined → fallback placeholder file)
 *     - LLM-emitted throw stubs ("not implemented", "TODO: ...")
 *     - LLM-emitted trivial returns (`return null as any`)
 *
 *   Empirical at 2026-05-13:
 *     - idle-meta Ollama: 58% real-LLM fill, 80 tsc errors → 0 tsc
 *       errors looks clean but 42% of bodies are stubs.
 *     - idle-meta MarrBox: 69% real-LLM fill, 40 → ? tsc errors,
 *       31% bodies still stubs.
 *
 *   Reporting stubs as VerifyErrors lets the feedback runner re-emit
 *   them with a targeted prompt. The existing rollback machinery
 *   prevents regressions if the LLM can't do better.
 *
 * Design:
 *   - Regex-only — no TS compiler API import. The `.ai.ts` files have
 *     a narrow shape: top-level `export async function NAME(...):
 *     Promise<X> { ... }` declarations. Brace-balanced extraction with
 *     a single line scanner is enough.
 *   - Conservative: ONLY flags single-statement bodies that match
 *     stub patterns. A real function body with a stub-shaped throw
 *     inside a conditional is NOT flagged.
 *   - Severity `warning` not `error` — these don't break the build,
 *     but the feedback runner counts them as errors-to-fix.
 *
 * Initial release: `enabledByDefault: false` — opt in via
 * `SPECVERSE_REALIZE_POST_VERIFY_ENABLE=stub-completeness`. Once the
 * empirical hit-rate is measured we flip default-on.
 */
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join, relative } from 'path';
/** Stub error codes — surfaced to the feedback prompt as the verifier
 *  code; the LLM doesn't see them directly but they appear in the
 *  audit sidecar + logs. */
export const STUB_CODES = {
    GAMMA_FALLBACK: 'STUB001', // engine wrote the placeholder file
    LLM_THROW: 'STUB002', // LLM emitted "throw not implemented"
    TRIVIAL_RETURN: 'STUB003', // LLM emitted `return null|undefined|{}|[]`
    EMPTY_BODY: 'STUB004', // body is whitespace/comments only
    MISPLACED_BRACE: 'STUB005', // empty body + orphan top-level code follows
};
/** Resolve the behaviors directory tsc + this verifier scan. */
function resolveBehaviorsDir(ctx) {
    const sub = ctx.subpath ?? 'backend';
    return join(ctx.outputDir, sub, 'src', 'behaviors');
}
export function extractAsyncFunctions(source) {
    const lines = source.split('\n');
    const out = [];
    // Pattern: line starts (after optional `export ` + `async `) with
    // `async function NAME(`. We accept `export async function` (the
    // canonical case) plus the alternate `async function NAME(...){...}`
    // followed by `export { NAME as ... }` re-export form used for
    // reserved-word action names in the engine γ-fallback file.
    const declRe = /^(export\s+)?async\s+function\s+(\w+)\s*\(/;
    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        const m = declRe.exec(line);
        if (!m)
            continue;
        const name = m[2];
        // Walk forward to find the first `{` that opens the body. Skip
        // anything inside the parameter list / return-type annotation.
        let openIdx = -1;
        let parenDepth = 0;
        let lineCursor = i;
        let colCursor = m.index + m[0].length; // just past `(`
        // We've consumed up to the `(` of params; start at depth 1.
        parenDepth = 1;
        scanHeader: while (lineCursor < lines.length) {
            const s = lines[lineCursor];
            while (colCursor < s.length) {
                const ch = s[colCursor];
                if (parenDepth > 0) {
                    if (ch === '(')
                        parenDepth++;
                    else if (ch === ')')
                        parenDepth--;
                }
                else {
                    // After params closed: look for `{`.
                    if (ch === '{') {
                        openIdx = colCursor;
                        break scanHeader;
                    }
                }
                colCursor++;
            }
            lineCursor++;
            colCursor = 0;
        }
        if (openIdx === -1)
            continue;
        // Now brace-count forward from just past `{` until we hit the
        // matching `}`.
        let depth = 1;
        let bodyStartLineIdx = lineCursor;
        let bodyStartColIdx = openIdx + 1;
        let endLineIdx = -1;
        let endColIdx = -1;
        walkBody: for (let li = lineCursor; li < lines.length; li++) {
            const s = lines[li];
            const startCol = li === lineCursor ? openIdx + 1 : 0;
            for (let ci = startCol; ci < s.length; ci++) {
                const ch = s[ci];
                if (ch === '{')
                    depth++;
                else if (ch === '}') {
                    depth--;
                    if (depth === 0) {
                        endLineIdx = li;
                        endColIdx = ci;
                        break walkBody;
                    }
                }
            }
        }
        if (endLineIdx === -1)
            continue; // unbalanced; bail on this fn
        // Slice the body content (exclusive of the outer braces).
        let body;
        if (bodyStartLineIdx === endLineIdx) {
            body = lines[bodyStartLineIdx].slice(bodyStartColIdx, endColIdx);
        }
        else {
            const first = lines[bodyStartLineIdx].slice(bodyStartColIdx);
            const middle = lines.slice(bodyStartLineIdx + 1, endLineIdx).join('\n');
            const last = lines[endLineIdx].slice(0, endColIdx);
            body = [first, middle, last].filter((x) => x !== '').join('\n');
        }
        out.push({
            name,
            body,
            declLine: i + 1,
            bodyStartLine: bodyStartLineIdx + 1,
            endLine: endLineIdx + 1,
        });
        // Skip past the end so we don't re-scan content inside the body.
        i = endLineIdx;
    }
    return out;
}
/**
 * Strip line + block comments and blank lines, then trim.
 * Used to normalize bodies before stub-pattern matching.
 */
export function stripCommentsAndBlanks(body) {
    // Block comments first (greedy, multi-line capable).
    let s = body.replace(/\/\*[\s\S]*?\*\//g, '');
    // Line comments.
    s = s.replace(/(^|\n)\s*\/\/[^\n]*/g, (_, p1) => p1 ?? '');
    // Collapse multiple newlines + trim.
    s = s.split('\n').map((l) => l.trimEnd()).filter((l) => l.trim() !== '').join('\n').trim();
    return s;
}
// γ-fallback engine placeholders. Two producers:
//   - per-owner-runner.renderFallbackPlaceholderFile → "(per-owner emit fell back)"
//   - per-action-recovery.renderActionGammaStub      → "(per-action recovery fell back)"
// Both are ENGINE placeholders (STUB001, auto-reemit-eligible), NOT the
// LLM's own "not implemented" throw (STUB002, surface-spec-gap).
const GAMMA_FALLBACK_RE = /^throw new Error\s*\(\s*["'`].*?not implemented \((?:per-owner emit|per-action recovery) fell back\).*?["'`]\s*\)\s*;?$/s;
const LLM_THROW_RE = /^throw new Error\s*\(\s*["'`][^"'`]*\b(not\s+implement\w*|not\s+yet|todo|placeholder|stub|requires|fallback|missing|unimplement\w*)\b[^"'`]*["'`]\s*\)\s*;?$/is;
const TRIVIAL_RETURN_RE = /^return\s+(?:null|undefined|\{\s*\}|\[\s*\])(\s+as\s+\w+)?\s*;?$/s;
/**
 * Detect the "misplaced `}`" emission pattern surfaced by marrbox on
 * `GameConditionController.loadGameData` (2026-05-14). When the LLM
 * emits a stray `}` right after the [PRE-BAKED] comment block, the
 * brace-walker (correctly) treats it as the function's end. The
 * function body classifies as `empty`, but the steps that were meant
 * to be inside the body are now orphan top-level code:
 *
 *   export async function loadGameData(_input: any): Promise<any> {
 *     // [PRE-BAKED] Step 1: ...
 *     // TODO: ...
 *     }                              ← misplaced
 *     // [WRITE] Step 2: ...         ← orphan
 *     const v3BaseUrl = ...;         ← orphan
 *     return { ... };                ← orphan, tsc TS1108
 *
 * The plain STUB004 message ("empty body") gives the LLM no clue why
 * its previous emission was wrong; the reemit typically repeats the
 * same mistake. STUB005's message names the specific shape so the
 * LLM can correct the brace placement.
 *
 * Heuristic: between `fn.endLine + 1` and `nextDeclLine - 1` (or EOF
 * if no next function), strip comments + blanks; if any non-trivial
 * line remains AND we see at least one of:
 *   - `// [WRITE]` or `// [PRE-BAKED]` marker (these belong INSIDE
 *     function bodies; their presence at top level is diagnostic)
 *   - a top-level `return` statement (illegal TS — TS1108)
 *   - a top-level `const`/`let`/`var` declaration that isn't followed
 *     by `export`/`function` (i.e. not a module-level binding)
 * → it's misplaced-brace orphan code.
 *
 * Returns a preview of the first ~5 orphan lines, or `null` if not
 * misplaced-brace.
 */
export function detectMisplacedBrace(source, fn, nextDeclLine) {
    const lines = source.split('\n');
    const startIdx = fn.endLine; // 1-based endLine → 0-based first line after = endLine
    const stopIdx = nextDeclLine !== null ? nextDeclLine - 1 : lines.length;
    if (startIdx >= stopIdx)
        return null;
    const range = lines.slice(startIdx, stopIdx);
    // Look for diagnostic markers.
    let hasInsideMarker = false;
    let hasOrphanStatement = false;
    const orphanLines = [];
    for (const raw of range) {
        const trimmed = raw.trim();
        if (trimmed === '')
            continue;
        // Markers that should only appear INSIDE function bodies.
        if (/^\/\/\s*\[(WRITE|PRE-BAKED|PRE_BAKED)\]/.test(trimmed)) {
            hasInsideMarker = true;
            if (orphanLines.length < 5)
                orphanLines.push(trimmed);
            continue;
        }
        // Module-level legal tokens (skip — not orphan).
        if (/^(import|export|function|async\s+function|class|interface|type|enum|declare|\/\/|\/\*|\*)/.test(trimmed)) {
            continue;
        }
        if (trimmed.startsWith('*/') || trimmed.startsWith('*'))
            continue;
        if (trimmed === '}' || trimmed === '};' || trimmed === ');' || trimmed === '})' || trimmed === '});') {
            // Stray closers from orphan bodies. Diagnostic, but not by themselves.
            if (orphanLines.length < 5)
                orphanLines.push(trimmed);
            continue;
        }
        // Statement-level keywords / patterns that are illegal at module top.
        if (/^(return|await|if\s*\(|for\s*\(|while\s*\(|switch\s*\(|throw\s|break|continue)/.test(trimmed)) {
            hasOrphanStatement = true;
            if (orphanLines.length < 5)
                orphanLines.push(trimmed);
            continue;
        }
        // Bare `const`/`let`/`var` could be legal module-level, but in the
        // body of an .ai.ts file (which only contains function declarations)
        // they are diagnostic. Treat as orphan in conjunction with other
        // signals — not on their own.
        if (/^(const|let|var)\s/.test(trimmed)) {
            if (orphanLines.length < 5)
                orphanLines.push(trimmed);
            continue;
        }
        // Identifier-followed-by-call (e.g. `gameDataResponse.edges.forEach`)
        // is also a statement-level pattern, diagnostic when paired with
        // markers above.
        if (/^[A-Za-z_$][\w$]*[.[(]/.test(trimmed)) {
            if (orphanLines.length < 5)
                orphanLines.push(trimmed);
            continue;
        }
    }
    if (!hasInsideMarker && !hasOrphanStatement)
        return null;
    return orphanLines.join('\n');
}
/**
 * Classify a function body as a stub (or not). Conservative: requires
 * the body to be a single-statement match to avoid false positives on
 * real bodies that happen to contain a throw deep inside a conditional.
 */
export function classifyBody(body) {
    const stripped = stripCommentsAndBlanks(body);
    if (stripped === '') {
        return { kind: 'empty', code: STUB_CODES.EMPTY_BODY };
    }
    // Single-statement check: count semicolons + ensure no control-flow
    // keywords at the top level. We don't try to detect every multi-
    // statement function; instead, we use the simplest signal — if the
    // stripped body matches a single-statement stub regex end-to-end,
    // it's a stub.
    if (GAMMA_FALLBACK_RE.test(stripped)) {
        return { kind: 'gamma-fallback', code: STUB_CODES.GAMMA_FALLBACK };
    }
    if (LLM_THROW_RE.test(stripped)) {
        return { kind: 'llm-throw', code: STUB_CODES.LLM_THROW };
    }
    if (TRIVIAL_RETURN_RE.test(stripped)) {
        return { kind: 'trivial-return', code: STUB_CODES.TRIVIAL_RETURN };
    }
    return null;
}
/** Render a feedback-prompt-friendly message for a stub match. */
function messageFor(kind, fnName, orphanPreview) {
    switch (kind) {
        case 'gamma-fallback':
            return (`Function '${fnName}' is a γ-stub placeholder (per-owner emit fell back). ` +
                `Emit a real implementation per the [PRE-BAKED] action contract in the spec.`);
        case 'llm-throw':
            return (`Function '${fnName}' has a placeholder body that throws 'not implemented' ` +
                `(or similar). Emit a real implementation per the [PRE-BAKED] action contract.`);
        case 'trivial-return':
            return (`Function '${fnName}' returns a trivial placeholder (null/undefined/{}). ` +
                `Emit a real implementation per the [PRE-BAKED] action contract.`);
        case 'empty':
            return (`Function '${fnName}' has an empty body. ` +
                `Emit a real implementation per the [PRE-BAKED] action contract.`);
        case 'misplaced-brace': {
            const preview = orphanPreview ? `\nOrphan code sample:\n${orphanPreview}` : '';
            return (`Function '${fnName}' has an empty body AND top-level code follows the ` +
                `closing '}'. This is a misplaced-brace bug: the '}' that closes the function ` +
                `was emitted right after the [PRE-BAKED] / [WRITE] step comments, leaving the ` +
                `actual step bodies as illegal top-level statements (tsc TS1108). Fix: place ` +
                `the closing '}' AFTER the last step body — not between the step comments and ` +
                `their implementations.${preview}`);
        }
    }
}
/** Recursively list `*.ai.ts` files under a directory. Defensive — the
 *  behaviors dir is normally flat but engines may add subdirs later. */
function listAiTsFiles(dir) {
    if (!existsSync(dir))
        return [];
    const out = [];
    for (const name of readdirSync(dir)) {
        const p = join(dir, name);
        let st;
        try {
            st = statSync(p);
        }
        catch {
            continue;
        }
        if (st.isDirectory())
            out.push(...listAiTsFiles(p));
        else if (st.isFile() && p.endsWith('.ai.ts'))
            out.push(p);
    }
    return out;
}
export const STUB_COMPLETENESS_VERIFIER = {
    id: 'stub-completeness',
    name: 'Stub Body Completeness',
    // Default-on as of engines 6.56.0 (Phase 2 of
    // 2026-05-13-VERIFIER-DIAGNOSTIC-TREATMENT-SPLIT). Safe to flip
    // because the feedback runner's defaultTreatmentSelector routes
    // STUB002 to surface-spec-gap (no reemit), preventing the cross-
    // file regressions measured at engines 6.54.0 on idle-meta.
    // STUB001/003/004 still auto-reemit (high-success on γ-fallback
    // recovery, measured at engines 6.54.0 on nestjs-billing: 7/7 fixed).
    enabledByDefault: true,
    applies: (ctx) => {
        if (ctx.targetLanguage && ctx.targetLanguage !== 'typescript')
            return false;
        const dir = resolveBehaviorsDir(ctx);
        return existsSync(dir) && listAiTsFiles(dir).length > 0;
    },
    verify: async (ctx) => {
        const start = Date.now();
        const dir = resolveBehaviorsDir(ctx);
        const files = listAiTsFiles(dir);
        const errors = [];
        const notes = [];
        let totalFunctions = 0;
        let totalStubs = 0;
        for (const absFile of files) {
            let source;
            try {
                source = readFileSync(absFile, 'utf8');
            }
            catch (e) {
                notes.push(`could not read ${absFile}: ${e?.message ?? String(e)}`);
                continue;
            }
            const fns = extractAsyncFunctions(source);
            totalFunctions += fns.length;
            for (let fnIdx = 0; fnIdx < fns.length; fnIdx++) {
                const fn = fns[fnIdx];
                const match = classifyBody(fn.body);
                if (!match)
                    continue;
                // Promote 'empty' to 'misplaced-brace' when orphan top-level
                // code follows the (apparent) closing `}`. Same severity, same
                // treatment routing — only the message text changes, so the
                // LLM gets a richer diagnostic on reemit.
                let resolvedKind = match.kind;
                let resolvedCode = match.code;
                let orphanPreview;
                if (match.kind === 'empty') {
                    const nextDeclLine = fnIdx + 1 < fns.length ? fns[fnIdx + 1].declLine : null;
                    const preview = detectMisplacedBrace(source, fn, nextDeclLine);
                    if (preview !== null) {
                        resolvedKind = 'misplaced-brace';
                        resolvedCode = STUB_CODES.MISPLACED_BRACE;
                        orphanPreview = preview;
                    }
                }
                totalStubs++;
                errors.push({
                    file: relative(ctx.outputDir, absFile),
                    line: fn.declLine,
                    col: 1,
                    severity: 'warning',
                    code: resolvedCode,
                    message: messageFor(resolvedKind, fn.name, orphanPreview),
                });
            }
        }
        notes.push(`scanned ${files.length} .ai.ts file(s), ${totalFunctions} function(s); ` +
            `flagged ${totalStubs} stub(s)`);
        return {
            verifierId: 'stub-completeness',
            durationMs: Date.now() - start,
            ok: true,
            errors,
            notes,
        };
    },
};
//# sourceMappingURL=stub-completeness.js.map