/**
 * `runAnalyse()` — high-level orchestrator for analysing a source codebase
 * into a SpecVerse spec.
 *
 * Public API for `spv ai analyse` (TODO #21 phase 4) and the eval harness.
 * One call, full pipeline:
 *
 *   1. (optional) inject pre-extracted structural facts via runPrepass
 *   2. assemble + call the analyse prompt → specs/draft.specly + main.specly
 *   3. spv validate; if invalid, retry once with the parser error fed back
 *   4. (optional) verify-analyse pass → specs/verified.specly + main.specly
 *   5. spv validate; record validate.txt
 *   6. (optional) materialize project/ via spv init + spv realize
 *
 * Caller responsibilities:
 *   - Materialize source into `inputDir` (or just point to an existing dir)
 *   - Decide where outputs go via `outputDir`
 *   - Score against expected_* fields if running eval
 *   - Compose final README + manifest with caseMeta + score
 *
 * The runner returns enough information for the caller to compose those
 * artifacts. It writes manifest.json itself with what it knows; callers
 * can re-write it with additional fields after scoring.
 */
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync, cpSync, statSync, readdirSync } from 'fs';
import { join, relative } from 'path';
import { execSync, spawnSync } from 'child_process';
import { createHash } from 'crypto';
import { promptsDir, llmOutputDir, factsDir, specsDir, ensureSubdirs, writeStageSpec, gateVerifyAgainstDraft, writeProjectReadme, writeManifest, writeReadme, } from './run-paths.js';
import { runPrompt, extractSpecAndManifest, promptTag } from './prompt-runner.js';
import { resolveModel, resolveProviderId } from './model-resolver.js';
import { mkdirSync as _mkdirSync } from 'fs';
/**
 * Write the LLM-emitted manifest block to outputDir/manifests/implementation.yaml.
 * Idempotent — last write wins (verify pass overwrites the analyse-pass manifest
 * if it produced an improved one). Per TODO #36, this lets `spv realize` find
 * a sibling manifest alongside the spec.
 */
function writeManifestArtifact(outputDir, manifestYaml) {
    const manifestsDir = join(outputDir, 'manifests');
    _mkdirSync(manifestsDir, { recursive: true });
    writeFileSync(join(manifestsDir, 'implementation.yaml'), manifestYaml);
}
function runValidate(specPath) {
    const result = spawnSync('spv', ['validate', specPath], {
        encoding: 'utf8',
        timeout: 30_000,
    });
    if (result.status === 0)
        return { ok: true, errors: '' };
    const errors = (result.stderr || '') + (result.stdout || '');
    return { ok: false, errors: errors.trim() };
}
async function realizeProject(outputDir) {
    const projectPath = join(outputDir, 'project');
    const mainSpec = join(specsDir(outputDir), 'main.specly');
    if (!existsSync(mainSpec))
        return { ok: false, error: 'specs/main.specly missing' };
    if (existsSync(projectPath))
        rmSync(projectPath, { recursive: true, force: true });
    // SPECVERSE_USER_CWD is captured by the spv binary entry point and inherited
    // by all subprocess spv calls. Generated CLI handlers resolve paths against
    // it (process.env.SPECVERSE_USER_CWD || process.cwd()), so without an
    // override the child ignores execSync's cwd and writes ./project/ next to
    // the harness's cwd — colliding across multiple cases in one run.
    try {
        execSync(`spv init project --template default`, {
            cwd: outputDir, stdio: 'pipe', timeout: 60_000,
            env: { ...process.env, SPECVERSE_USER_CWD: outputDir },
        });
    }
    catch (err) {
        return { ok: false, error: `spv init failed: ${err.message}` };
    }
    cpSync(mainSpec, join(projectPath, 'specs', 'main.specly'));
    // Realize timeout: large codebases (idle-meta-class — 30+ owners,
    // per-owner LLM emit + recovery tier + post-emit feedback) routinely
    // exceed the old 15-min cap. The 2026-05-14 comparison matrix saw
    // idle-meta realize clock 19-37 min; the 15-min execSync timeout
    // killed it mid-emit and surfaced the truncated stderr (a benign
    // name-collision warning) as if it were the failure cause. Default
    // 40 min; override via SPECVERSE_ANALYSE_REALIZE_TIMEOUT_MS.
    const realizeTimeoutMs = parseInt(process.env.SPECVERSE_ANALYSE_REALIZE_TIMEOUT_MS ?? '', 10) || 40 * 60_000;
    try {
        execSync(`spv realize all specs/main.specly`, {
            cwd: projectPath, stdio: 'pipe', timeout: realizeTimeoutMs,
            env: { ...process.env, SPECVERSE_USER_CWD: projectPath },
        });
    }
    catch (err) {
        const e = err;
        // execSync kills the child with a signal on timeout. Distinguish
        // that from a genuine non-zero exit — otherwise the "error" is just
        // the truncated stderr of a process cut off mid-run (typically a
        // benign warning that happened to be at the top of the buffer),
        // which misdiagnoses a slow realize as a broken one.
        if (e.killed || e.signal) {
            const mins = Math.round(realizeTimeoutMs / 60_000);
            return {
                ok: false,
                error: `spv realize timed out after ${mins}min — raise ` +
                    `SPECVERSE_ANALYSE_REALIZE_TIMEOUT_MS for large codebases`,
            };
        }
        const stderr = e.stderr ? String(e.stderr).slice(0, 500) : '';
        return { ok: false, error: `spv realize failed: ${stderr || e.message}` };
    }
    return { ok: true };
}
/**
 * Execute the full analyse pipeline against a source directory and write
 * the structured run output. Returns paths + status; caller composes
 * scoring / README finalization separately.
 */
export async function runAnalyse(opts) {
    const startedAt = new Date().toISOString();
    const t0 = Date.now();
    ensureSubdirs(opts.outputDir);
    // Verify-stage recorder (#audit-trail-2026-05-03 plan, engines 6.22.x+).
    // Captures every schema-validation pass + retry event + structural-check
    // result at the moment they happen during the analyse pipeline.
    const { InMemoryVerifyRecorder, categoriseValidationError } = await import('../audit/verify-recorder.js');
    const { runAllChecks } = await import('../audit/verify-checks.js');
    const verifyRecorder = new InMemoryVerifyRecorder();
    // Wrap runValidate so every call records into the verify recorder. Also
    // runs structural checks after a successful schema-validation pass.
    // The js-yaml import is hoisted out of the function so it loads once
    // (ESM project — `require()` is not available without createRequire).
    const yamlModule = await import('js-yaml');
    const recordedRunValidate = (specPath, kind = 'schema-validation') => {
        const t = Date.now();
        const r = runValidate(specPath);
        const errorList = r.ok ? [] : r.errors.split('\n').filter((l) => l.trim());
        verifyRecorder.recordPass({
            kind,
            ok: r.ok,
            errorCount: errorList.length,
            errors: errorList.slice(0, 50), // cap to keep sidecar bounded
            durationMs: Date.now() - t,
        });
        // Structural checks run after a clean schema-validation pass. They
        // surface SpecVerse-semantic invariant violations as human-review
        // items without failing the validation gate.
        if (r.ok && existsSync(specPath)) {
            try {
                const parsed = yamlModule.load(readFileSync(specPath, 'utf8'));
                const checkResults = runAllChecks(parsed);
                for (const cr of checkResults)
                    verifyRecorder.recordStructuralCheck(cr);
            }
            catch (e) {
                verifyRecorder.recordHumanReviewItem({
                    severity: 'warning',
                    kind: 'structural-checks-skipped',
                    message: `Could not parse spec for structural checks: ${e?.message ?? e}`,
                });
            }
        }
        return r;
    };
    // 1. Resolve facts content (if any). Two sources: --facts <path> (manual)
    // or runPrepass on sourceDir (--auto-facts). Manual wins if both passed.
    let factsContent = '';
    let factsSource = null;
    let methodFactsContent = '';
    // Hold raw method fact sheets for the deterministic cross-check (#32 v2).
    let methodFactSheetsRaw;
    // #27-C: pre-classified candidate-step timelines are surfaced
    // alongside method facts as a third structured-input stream.
    let candidateStepsContent = '';
    // Hoisted so the microcall branch (Phase B) can build the skeleton.
    let prepassFacts;
    if (opts.autoFacts && !opts.factsPath) {
        const { runPrepass, formatFactsAsMarkdown, formatMethodFactsAsMarkdown, formatCandidateMethodsAsMarkdown } = await import('./../analyse-prepass/index.js');
        const facts = await runPrepass(opts.sourceDir);
        prepassFacts = facts;
        factsContent = formatFactsAsMarkdown(facts);
        methodFactsContent = formatMethodFactsAsMarkdown(facts.methodFactSheets);
        candidateStepsContent = formatCandidateMethodsAsMarkdown(facts.candidateMethods);
        methodFactSheetsRaw = facts.methodFactSheets;
        factsSource = `auto (${facts.meta.backend}, adapters: ${facts.meta.adaptersRun.join(', ') || 'none'})`;
        writeFileSync(join(factsDir(opts.outputDir), 'facts.md'), factsContent);
        if (methodFactsContent) {
            writeFileSync(join(factsDir(opts.outputDir), 'method-facts.md'), methodFactsContent);
        }
        if (candidateStepsContent) {
            writeFileSync(join(factsDir(opts.outputDir), 'candidate-steps.md'), candidateStepsContent);
        }
        try {
            writeFileSync(join(factsDir(opts.outputDir), 'facts.json'), JSON.stringify(facts, null, 2));
        }
        catch { /* non-serializable; skip */ }
        // Audit-trail sidecars (#audit-trail-2026-05-03 plan, engines 6.22.0+).
        // These let `spv report run` regenerate the markdown report without
        // heuristic re-parsing of facts.md / candidate-steps.md.
        if (facts.auditDecisions) {
            try {
                const t1 = Date.now();
                const stats = {
                    schemaVersion: '1.0',
                    extractedAt: new Date().toISOString(),
                    sourceDir: opts.sourceDir,
                    backend: facts.meta.backend,
                    adaptersRun: facts.meta.adaptersRun,
                    durationMs: facts.meta.durationMs,
                    totalDurationMs: t1 - t0,
                    ...facts.auditDecisions.stats,
                };
                writeFileSync(join(factsDir(opts.outputDir), 'extract-decisions.json'), JSON.stringify(facts.auditDecisions, null, 2));
                writeFileSync(join(factsDir(opts.outputDir), 'extract-stats.json'), JSON.stringify(stats, null, 2));
                // Source fingerprint — SHA256 of the input tree for reproducibility.
                const fingerprint = await computeSourceFingerprint(opts.sourceDir);
                writeFileSync(join(factsDir(opts.outputDir), 'source-fingerprint.json'), JSON.stringify(fingerprint, null, 2));
            }
            catch (e) {
                // Sidecar writes are advisory — don't fail the run on serialization issues.
                console.error(`[audit] sidecar emit failed: ${e?.message ?? e}`);
            }
        }
    }
    else if (opts.factsPath) {
        factsContent = readFileSync(opts.factsPath, 'utf8');
        factsSource = `manual (${opts.factsPath})`;
        writeFileSync(join(factsDir(opts.outputDir), 'facts.md'), factsContent);
    }
    const userPrefix = factsContent
        ? 'Before reading the source, here is a deterministic structural\n' +
            'inventory extracted from the source (NOT LLM judgment). Treat as\n' +
            'ground truth for what symbols/files exist; verify SpecVerse semantics\n' +
            '(attributes, relationship cardinality, lifecycle states) by reading\n' +
            'source. Do not invent entities not in this inventory.\n\n' +
            '---BEGIN STRUCTURAL FACTS---\n' +
            factsContent.trim() + '\n' +
            '---END STRUCTURAL FACTS---\n\n' +
            // #27-C: candidate-step timelines are appended when present.
            // The analyse prompt knows to consume them as `behaviors[].steps[]`
            // structural anchors (see analyse.prompt.yaml v9.7+).
            (candidateStepsContent
                ? '---BEGIN CANDIDATE BEHAVIOR STEPS---\n' +
                    candidateStepsContent.trim() + '\n' +
                    '---END CANDIDATE BEHAVIOR STEPS---\n\n'
                : '')
        : '';
    // 2. Resolve model (caller may have pre-built one for session caching).
    const model = opts.model ?? resolveModel({
        timeout: opts.timeoutMs ?? 15 * 60_000,
        sessionId: opts.sessionId,
    });
    // 3. Analyse pass — two paths:
    //    (a) skeleton + per-action micro-calls — DEFAULT when prepassFacts
    //        are available. Opt out with explicit `useMicrocalls: false`.
    //    (b) legacy single-call analyse — selected when no prepassFacts OR
    //        when explicitly opted out. Preserved for backward compat.
    //
    // The effective gate resolves opts.useMicrocalls (explicit choice) against
    // the prerequisite (prepassFacts must exist). Explicit true with no
    // prepassFacts still throws (sensible — user asked for skeleton but
    // didn't enable autoFacts). Undefined falls back to "true iff prepassFacts".
    const effectiveUseMicrocalls = opts.useMicrocalls ?? !!prepassFacts;
    let draftResult;
    let draftSpec;
    let draftManifest;
    // Microcall state hoisted out of the if-block so the retry path
    // (TODO #152 surgical retry) can re-fire failed microcalls instead
    // of calling the giant analyse prompt.
    let microcallState = null;
    if (effectiveUseMicrocalls) {
        if (!prepassFacts) {
            throw new Error('useMicrocalls requires autoFacts (skeleton emitter consumes prepass facts)');
        }
        // Skeleton emitter (Phase A) — deterministic facts → faithful skeleton.
        const { emitFaithfulSkeleton } = await import('./skeleton-emitter.js');
        const { skeleton, provenance: skeletonProvenance, classesByComponent: skeletonClassesByComponent, entityHome: skeletonEntityHome } = emitFaithfulSkeleton(prepassFacts);
        writeFileSync(join(specsDir(opts.outputDir), 'skeleton.specly'), skeleton);
        writeFileSync(join(factsDir(opts.outputDir), 'skeleton-provenance.json'), JSON.stringify(skeletonProvenance, null, 2));
        // Build candidateStepsByMethodKey from facts. Each method's timeline as
        // markdown text the per-action prompt can consume directly.
        const candidateStepsByMethodKey = new Map();
        for (const cls of prepassFacts.candidateMethods ?? []) {
            for (let mi = 0; mi < cls.methods.length; mi++) {
                const m = cls.methods[mi];
                const lines = m.candidates.map((c, i) => `${i + 1}. [${c.kind}] ${c.summary}`);
                candidateStepsByMethodKey.set(`${cls.entityName}::${mi}`, lines.join('\n'));
            }
        }
        // Engines 6.32.0+ (TODO #51 phase 2C): build per-stub Process context
        // from facts.processes. GitNexus-only signal — degrades to empty Map
        // when running on the walk backend or when the source has no
        // significant cross-method flows.
        const { buildProcessContextByMethodKey } = await import('./processes-to-microcall-context.js');
        const processContextByMethodKey = buildProcessContextByMethodKey({
            processes: prepassFacts.processes,
            actionStubs: skeletonProvenance.actionStubs,
        });
        // ── Manifest emission (engines 6.29.0+: deterministic, no LLM) ──
        // The previous `manifestInputs` LLM call was a lookup-table operation
        // dressed up as a prompt. Replaced with `emitManifest` which reads
        // adaptersRun + entity signals and emits factory choices via a
        // hard-coded mapping. Faster, cheaper, no parse-failure risk, no
        // hallucinated factory names.
        const { emitManifest, fetchRegistryFactoryNames } = await import('./manifest-emitter.js');
        // Opt-in registry augmentation (SPECVERSE_REGISTRY_FACTORIES=1): lets the
        // emitted manifest reference factories published in the registry without
        // the bundled allow-list dropping them. Best-effort + offline-safe; OFF by
        // default so analyse stays network-free and fast (the registry carries no
        // factory records yet — Phase 4).
        const registryFactories = process.env['SPECVERSE_REGISTRY_FACTORIES'] === '1'
            ? await fetchRegistryFactoryNames()
            : undefined;
        const manifestEmission = emitManifest({
            facts: prepassFacts,
            specName: opts.caseName ?? 'AnalysedSpec',
            registryFactories,
        });
        draftManifest = manifestEmission.manifestYaml;
        console.log(`  manifest: emitted with ${Object.keys(manifestEmission.factoryChoices).length} factory choice(s)`);
        // ── Behaviour generation toggle (engines 6.29.0+) ────────────────
        // When `withBehaviours` is false, skip the per-action LLM calls
        // entirely. The skeleton already has placeholder bodies that
        // assembleFinalSpec emits as `description + steps:[]`.
        const withBehaviours = opts.withBehaviours !== false;
        const { assembleFinalSpec } = await import('./microcall-orchestrator.js');
        if (withBehaviours) {
            // Run the orchestrator for behaviour bodies.
            const { runActionMicrocalls } = await import('./microcall-orchestrator.js');
            const totalStubs = skeletonProvenance.actionStubs.length;
            const concurrencyCap = opts.microcallConcurrency ?? 5;
            console.log(`  microcalls: dispatching ${totalStubs} action stub(s) (concurrency=${concurrencyCap}, timeout=5m/call)`);
            const microcallStart = Date.now();
            let completed = 0;
            const microcallResult = await runActionMicrocalls({
                skeleton,
                skeletonProvenance,
                candidateStepsByMethodKey,
                processContextByMethodKey,
                model,
                // Default 5 (was 10) — empirically claude-cli rate-limits at >5 in
                // parallel for sustained calls. Override via --microcall-concurrency
                // when running against a higher-tier API key.
                concurrencyCap,
                timeoutMsPerCall: 5 * 60_000,
                // Manifest now emitted deterministically (above); skip the LLM
                // manifest call.
                manifestInputs: undefined,
                onProgress: (record) => {
                    completed++;
                    const status = record.parsedBody ? '✓' : '✗';
                    const owner = record.stubRef.ownerName;
                    const action = record.stubRef.actionName;
                    const lat = (record.latencyMs / 1000).toFixed(1);
                    const retried = record.rateLimitRetries && record.rateLimitRetries > 0
                        ? ` [retried ${record.rateLimitRetries}×]` : '';
                    const fail = record.parsedBody ? '' : ` — ${(record.failure ?? '').slice(0, 60)}`;
                    console.log(`    ${String(completed).padStart(String(totalStubs).length)}/${totalStubs} ${status} ${owner}.${action} (${lat}s)${retried}${fail}`);
                },
            });
            const microcallElapsed = ((Date.now() - microcallStart) / 1000).toFixed(1);
            const ms = microcallResult.provenance.totals;
            console.log(`  microcalls: ${ms.actionCallsSucceeded}/${ms.actionCallsAttempted} succeeded in ${microcallElapsed}s`);
            // Forward-log: write microcall provenance sidecar.
            writeFileSync(join(llmOutputDir(opts.outputDir), 'microcall-decisions.json'), JSON.stringify(microcallResult.provenance, null, 2));
            // Save state for surgical retry.
            microcallState = {
                skeleton,
                skeletonProvenance,
                candidateStepsByMethodKey,
                processContextByMethodKey,
                actionBodies: microcallResult.actionBodies,
            };
            // Assemble final spec from skeleton + filled bodies.
            draftSpec = assembleFinalSpec(skeleton, skeletonProvenance, microcallResult.actionBodies);
        }
        else {
            // Behaviours skipped — the spec uses skeleton bodies (placeholder
            // description + empty steps) which assembleFinalSpec emits via its
            // "microcall failed" fallback path. Pass an empty actionBodies map.
            console.log(`  behaviours: skipped (run \`spv ai behaviours <spec>\` to fill bodies later)`);
            draftSpec = assembleFinalSpec(skeleton, skeletonProvenance, new Map());
            // No microcall sidecar to write — there were none.
            // microcallState stays null so surgical-retry path is naturally skipped.
        }
        // Append a deterministic deployments block — one production-environment
        // instance per detected controller / service / entity / view. Matches
        // the expected analysed-spec shape (Dom Williams 2026-05-06 review:
        // "where does it say I'll have one of these and two of those").
        // Skipped when the spec is essentially empty (no entities and no
        // walked classes) to avoid emitting an empty deployments stanza.
        const hasContent = (prepassFacts.entities?.length ?? 0) > 0
            || skeletonClassesByComponent.size > 0
            || (prepassFacts.views?.length ?? 0) > 0;
        if (hasContent) {
            const { emitDefaultDeployments } = await import('./deployment-emitter.js');
            const { deploymentsYaml, counts } = emitDefaultDeployments({
                facts: prepassFacts,
                classesByComponent: skeletonClassesByComponent,
                entityHome: skeletonEntityHome,
            });
            draftSpec = draftSpec.replace(/\n*$/, '\n') + deploymentsYaml + '\n';
            console.log(`  deployments: emitted production-default with ${counts.controllers} controller(s), ${counts.services} service(s), ${counts.storage} storage, ${counts.views} view(s), ${counts.infrastructure} infra, ${counts.communications} comms`);
        }
        // Synthesize a draftResult equivalent for downstream code paths.
        draftResult = {
            text: draftSpec,
            tokens: { input: null, output: null },
            duration_ms: microcallState?.actionBodies ? Date.now() : 0, // ms approx; full timing lives in microcall sidecar
            system: withBehaviours
                ? '(microcall path — multiple system prompts)'
                : '(analyse path — deterministic skeleton, no LLM)',
            user: withBehaviours
                ? '(microcall path — multiple user prompts)'
                : '(analyse path — deterministic skeleton, no LLM)',
            providerId: opts.providerId ?? resolveProviderId(),
            modelId: opts.modelId ?? 'default',
        };
    }
    else {
        // Existing one-shot path.
        draftResult = await runPrompt({
            operation: 'analyse',
            values: {
                implementationPath: opts.sourceDir,
                spec: '',
                environment: 'development',
            },
            model,
            timeoutMs: opts.timeoutMs ?? 15 * 60_000,
            userPrefix,
            onAssembled: ({ system, user }) => {
                writeFileSync(join(promptsDir(opts.outputDir), 'analyse-system.txt'), system);
                writeFileSync(join(promptsDir(opts.outputDir), 'analyse-user.txt'), user);
            },
        });
        const extracted = extractSpecAndManifest(draftResult.text);
        draftSpec = extracted.spec;
        draftManifest = extracted.manifest;
    }
    const draftSpecPath = writeStageSpec(opts.outputDir, 'draft', draftSpec);
    if (draftManifest)
        writeManifestArtifact(opts.outputDir, draftManifest);
    writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse.txt'), draftResult.text);
    // Audit-trail attribution moved to a separate focused LLM call after
    // final-validate (engines 6.22.4+, assets 1.13.1+). See plan:
    // specverse-self/docs/plans/2026-05-03-PIPELINE-AUDIT-TRAIL-INSTRUMENTATION.md
    // and TODO #47 for spec-call decomposition follow-up work.
    // 4. Validate draft; retry up to MAX_RETRIES times if invalid. Each retry
    // feeds the most recent validation error + the prior draft back to the LLM
    // so it can fix one defect at a time. Larger specs surface multiple subtle
    // YAML defects in succession (cal-com case, 2026-04-28); a single retry is
    // not enough.
    const MAX_RETRIES = 3;
    let validateAfterDraft = recordedRunValidate(draftSpecPath);
    let draftRetried = false;
    if (!validateAfterDraft.ok) {
        writeFileSync(join(opts.outputDir, 'validate-error-1.txt'), validateAfterDraft.errors);
        // Microcalls path → surgical retry: parse error paths, re-fire only
        // affected microcalls. Skeleton-structural errors are recorded as
        // humanReviewItems and NOT retried (those are code bugs, not LLM-fixable).
        if (effectiveUseMicrocalls && microcallState) {
            const { classifyValidationErrors, runSurgicalRetry, assembleFinalSpec } = await import('./microcall-orchestrator.js');
            let priorError = validateAfterDraft.errors;
            let currentBodies = microcallState.actionBodies;
            for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
                const classification = classifyValidationErrors(priorError, microcallState.skeletonProvenance);
                for (const e of classification.structuralErrors) {
                    verifyRecorder.recordHumanReviewItem({
                        severity: 'error',
                        kind: 'skeleton-structural-error',
                        message: e,
                    });
                }
                if (classification.actionPathErrors.size === 0) {
                    // No action-stub errors — only structural. LLM retry can't fix
                    // those; fail validation cleanly.
                    break;
                }
                const retryStart = Date.now();
                const surgical = await runSurgicalRetry({
                    skeleton: microcallState.skeleton,
                    skeletonProvenance: microcallState.skeletonProvenance,
                    candidateStepsByMethodKey: microcallState.candidateStepsByMethodKey,
                    processContextByMethodKey: microcallState.processContextByMethodKey,
                    prior: { actionBodies: currentBodies, classification },
                    model,
                    concurrencyCap: opts.microcallConcurrency ?? 5,
                    timeoutMsPerCall: 5 * 60_000,
                });
                currentBodies = surgical.actionBodies;
                const retrySpec = assembleFinalSpec(microcallState.skeleton, microcallState.skeletonProvenance, currentBodies);
                const retrySpecPath = join(specsDir(opts.outputDir), `draft-retry-${attempt}.specly`);
                writeFileSync(retrySpecPath, retrySpec);
                const validateAfterRetry = recordedRunValidate(retrySpecPath);
                writeFileSync(join(opts.outputDir, `validate-after-retry-${attempt}.txt`), validateAfterRetry.ok ? 'OK' : validateAfterRetry.errors);
                verifyRecorder.recordRetry({
                    trigger: attempt === 1 ? 'first-validate-failed' : `retry-${attempt - 1}-failed`,
                    errorCount: priorError.split('\n').filter((l) => l.trim()).length,
                    errorCategories: categoriseValidationError(priorError),
                    tokenCountIn: undefined,
                    tokenCountOut: undefined,
                    latencyMs: Date.now() - retryStart,
                    itemsChangedSummary: `surgical retry: ${surgical.retriedStubs} stub(s) re-fired (${classification.structuralErrors.length} structural error(s) skipped)` +
                        (validateAfterRetry.ok
                            ? `, resolved ${priorError.split('\n').length} error line(s)`
                            : `, ${validateAfterRetry.errors.split('\n').length} error line(s) remain`),
                    ok: validateAfterRetry.ok,
                });
                if (validateAfterRetry.ok) {
                    draftSpec = retrySpec;
                    writeStageSpec(opts.outputDir, 'draft', retrySpec);
                    validateAfterDraft = validateAfterRetry;
                    draftRetried = true;
                    break;
                }
                priorError = validateAfterRetry.errors;
            }
        }
        else if (effectiveUseMicrocalls && opts.withBehaviours === false) {
            // Skeleton-only path (engines 6.29.4+): when the user asked for a
            // mechanical-only run (--no-behaviours), DO NOT fire a giant LLM
            // retry to fix validation errors. The user explicitly opted out of
            // LLM calls; surfacing the validation error is the right behaviour.
            // Common causes of validate failure on a skeleton-only spec:
            //   - deployment-emitter dup-key (should now be fixed)
            //   - skeleton structural emit bug
            // Both are deterministic-emit bugs — not LLM-fixable, and we want
            // the user to see the real error, not a hallucinated repair.
            console.log('  validate failed; --no-behaviours mode skips LLM retry. See validate-error-1.txt');
        }
        else {
            // Legacy giant-call path retry — full spec re-emission via analyse prompt.
            let priorDraft = draftSpec;
            let priorError = validateAfterDraft.errors;
            for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
                try {
                    const retryResult = await runPrompt({
                        operation: 'analyse',
                        values: {
                            implementationPath: opts.sourceDir,
                            spec: '',
                            environment: 'development',
                        },
                        model,
                        timeoutMs: opts.timeoutMs ?? 15 * 60_000,
                        userPrefix: `Your previous draft (attempt ${attempt}) failed \`spv validate\` with the following error:\n\n` +
                            '```\n' + priorError + '\n```\n\n' +
                            'Your previous output:\n```specly\n' + priorDraft + '\n```\n\n' +
                            'Fix the error and emit a corrected spec. Keep everything else identical.\n\n',
                        onAssembled: ({ system, user }) => {
                            writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-system.txt`), system);
                            writeFileSync(join(promptsDir(opts.outputDir), `analyse-retry-${attempt}-user.txt`), user);
                        },
                    });
                    const { spec: retrySpec, manifest: retryManifest } = extractSpecAndManifest(retryResult.text);
                    const retrySpecPath = join(specsDir(opts.outputDir), `draft-retry-${attempt}.specly`);
                    writeFileSync(retrySpecPath, retrySpec);
                    writeFileSync(join(llmOutputDir(opts.outputDir), `analyse-retry-${attempt}.txt`), retryResult.text);
                    const validateAfterRetry = recordedRunValidate(retrySpecPath);
                    writeFileSync(join(opts.outputDir, `validate-after-retry-${attempt}.txt`), validateAfterRetry.ok ? 'OK' : validateAfterRetry.errors);
                    verifyRecorder.recordRetry({
                        trigger: attempt === 1 ? 'first-validate-failed' : `retry-${attempt - 1}-failed`,
                        errorCount: priorError.split('\n').filter((l) => l.trim()).length,
                        errorCategories: categoriseValidationError(priorError),
                        itemsChangedSummary: validateAfterRetry.ok
                            ? `retry-${attempt} resolved ${priorError.split('\n').length} error line(s)`
                            : `retry-${attempt} still has ${validateAfterRetry.errors.split('\n').length} error line(s)`,
                        ok: validateAfterRetry.ok,
                    });
                    if (validateAfterRetry.ok) {
                        draftSpec = retrySpec;
                        writeStageSpec(opts.outputDir, 'draft', retrySpec);
                        if (retryManifest)
                            writeManifestArtifact(opts.outputDir, retryManifest);
                        validateAfterDraft = validateAfterRetry;
                        draftRetried = true;
                        break;
                    }
                    priorDraft = retrySpec;
                    priorError = validateAfterRetry.errors;
                }
                catch {
                    break;
                }
            }
        }
    }
    // 5. Verify pass (optional).
    let finalSpec = draftSpec;
    let finalStage = 'draft';
    let verifyDurationMs = 0;
    if (opts.verify) {
        try {
            // Deterministic behavior cross-check (#32 v2). Runs BEFORE the LLM
            // call. Compares draft.publishes / draft.ensures against
            // MethodFactSheet.emits / dbWrites. Each violation is a factual
            // mismatch the LLM should treat as ground truth, not LLM judgment.
            let crossCheckMd = '';
            if (methodFactSheetsRaw && methodFactSheetsRaw.length > 0) {
                try {
                    const { crossCheckBehaviors, formatBehaviorViolationsAsMarkdown } = await import('./behavior-cross-check/index.js');
                    const yaml = await import('js-yaml');
                    const parsedDraft = yaml.load(draftSpec);
                    const violations = crossCheckBehaviors(parsedDraft, methodFactSheetsRaw);
                    if (violations.length > 0) {
                        crossCheckMd = formatBehaviorViolationsAsMarkdown(violations);
                        writeFileSync(join(factsDir(opts.outputDir), 'behavior-cross-check.md'), crossCheckMd);
                    }
                }
                catch { /* spec didn't parse cleanly enough for cross-check; skip */ }
            }
            const verifyResult = await runPrompt({
                operation: 'verify-analyse',
                values: {
                    implementationPath: opts.sourceDir,
                    draft: draftSpec,
                    structuralFacts: factsContent ||
                        '(no structural facts available — extract-facts pre-pass was not run)',
                    methodFacts: methodFactsContent ||
                        '(no method fact sheets available — backend did not extract per-method facts)',
                    behaviorCrossCheck: crossCheckMd ||
                        '(no deterministic violations — proceed with LLM-judgment checks)',
                },
                model,
                timeoutMs: opts.timeoutMs ?? 15 * 60_000,
                onAssembled: ({ system, user }) => {
                    writeFileSync(join(promptsDir(opts.outputDir), 'verify-system.txt'), system);
                    writeFileSync(join(promptsDir(opts.outputDir), 'verify-user.txt'), user);
                },
            });
            const { spec: verifiedSpec, manifest: verifiedManifest } = extractSpecAndManifest(verifyResult.text);
            writeFileSync(join(llmOutputDir(opts.outputDir), 'verify.txt'), verifyResult.text);
            verifyDurationMs = verifyResult.duration_ms;
            writeStageSpec(opts.outputDir, 'verified', verifiedSpec);
            // Gate: keep the verify output only if it validates no worse than the draft.
            // A verify pass that returns prose or a schema-invalid spec (without
            // throwing) must not clobber a valid draft.
            const gate = gateVerifyAgainstDraft(opts.outputDir, draftSpec, verifiedSpec, runValidate);
            finalSpec = gate.finalSpec;
            finalStage = gate.finalStage;
            if (gate.reverted) {
                console.warn('[verify-analyse] verify output failed validation; kept the valid draft.');
            }
            else if (verifiedManifest) {
                writeManifestArtifact(opts.outputDir, verifiedManifest);
            }
        }
        catch { /* fallback: keep draft as final */ }
    }
    // 5b. Quint deterministic gate (TODO #31). Optional — opt in via
    // `verifyBehavior: true`. The actual gate runs as JS-side value-level
    // invariant checks (states unique, transitions reference declared states,
    // behavior-requires reference real states). The generated Quint module is
    // also written to `verify.qnt` and run through `quint typecheck` as a
    // syntax/type sanity check on the generator itself; both signals are
    // logged to `verify-behavior.txt`. With `strictBehavior: true`, any JS
    // violation aborts the workflow. Skips cleanly when there are no
    // lifecycles in the spec. See
    // docs/plans/2026-04-30-DETERMINISTIC-VERIFICATION-GATES.md.
    if (opts.verifyBehavior) {
        try {
            const { generateQuintFromSpec } = await import('./../inference/quint-gen/index.js');
            const { runQuintTypecheck, formatQuintResult } = await import('./../inference/quint-gen/runner.js');
            const yaml = await import('js-yaml');
            const parsed = yaml.load(finalSpec);
            const quintGen = generateQuintFromSpec(parsed);
            const lines = [];
            lines.push(`Quint 5b deterministic gate — ${quintGen.modelCount} model(s), ${quintGen.behaviorPreconditionCount} behavior precondition(s)`);
            if (quintGen.skipped) {
                lines.push(`Skipped: ${quintGen.skipReason}`);
            }
            else {
                const qntPath = join(opts.outputDir, 'verify.qnt');
                writeFileSync(qntPath, quintGen.content);
                // Sanity check the generated Quint syntax (best-effort; skips if quint not installed)
                const typecheckResult = runQuintTypecheck(qntPath);
                lines.push('');
                lines.push('--- Generator syntax sanity check (quint typecheck) ---');
                lines.push(formatQuintResult(typecheckResult, qntPath));
                // Value-level invariant violations — the actual gate
                lines.push('');
                lines.push('--- Lifecycle invariant violations (the actual gate) ---');
                if (quintGen.violations.length === 0) {
                    lines.push(`No violations. All ${quintGen.modelCount} model lifecycle(s) cleanly typecheck.`);
                }
                else {
                    lines.push(`${quintGen.violations.length} violation(s):`);
                    for (const v of quintGen.violations) {
                        lines.push(`  - [${v.kind}] ${v.message}`);
                    }
                }
                writeFileSync(join(opts.outputDir, 'verify-behavior.txt'), lines.join('\n'));
                if (quintGen.violations.length > 0 && opts.strictBehavior) {
                    throw new Error(`Quint 5b gate failed (--strict-behavior, ${quintGen.violations.length} violations): ${quintGen.violations[0].message}`);
                }
            }
            if (quintGen.skipped) {
                writeFileSync(join(opts.outputDir, 'verify-behavior.txt'), lines.join('\n'));
            }
        }
        catch (err) {
            if (opts.strictBehavior)
                throw err;
            writeFileSync(join(opts.outputDir, 'verify-behavior.txt'), `Quint 5b gate errored (non-strict): ${err?.message ?? String(err)}`);
        }
    }
    // 5.5. Normalise pass (engines 6.49.x, Phase 1b of 2026-05-13-NORMALISE-PHASE).
    //
    // Reshapes the faithful spec into a canonical SpecVerse-idiomatic form
    // via the deterministic rule manifest at `engines/src/normalise/`.
    // Saves the pre-normalise spec as `specs/faithful.specly` for inspection,
    // then overwrites `specs/main.specly` with the canonical version.
    //
    // Best-effort: any failure inside the normalise pass is caught and
    // logged via the audit file; main.specly is left as the faithful form
    // and downstream phases (validate, realize) proceed without normalise.
    // This keeps the analyse pipeline robust against bugs in new rules
    // without rolling back the whole run.
    const finalSpecPath = join(specsDir(opts.outputDir), 'main.specly');
    try {
        if (existsSync(finalSpecPath)) {
            const yamlMod = await import('js-yaml');
            const normaliseMod = await import('../normalise/index.js');
            const originalContent = readFileSync(finalSpecPath, 'utf8');
            const parsed = (yamlMod.load(originalContent) ?? {});
            // Project-level rule overrides — `.normalise.json` in the source dir
            // (the codebase being analysed). Phase 5 of the proposal.
            const overrides = normaliseMod.loadNormaliseOverrides(opts.sourceDir);
            const runResult = normaliseMod.runNormalise(parsed, {
                source: 'analyse',
                disabledRules: overrides.disabledRules,
                enabledRules: overrides.enabledRules,
            });
            const firedRules = runResult.results.filter((r) => !r.skipped && r.matchCount > 0);
            if (firedRules.length > 0) {
                writeFileSync(join(specsDir(opts.outputDir), 'faithful.specly'), originalContent);
                const canonicalYaml = yamlMod.dump(runResult.spec, {
                    lineWidth: 100,
                    noRefs: true,
                });
                writeFileSync(finalSpecPath, canonicalYaml);
            }
            writeFileSync(join(opts.outputDir, 'normalise.json'), JSON.stringify({
                schemaVersion: '1.0',
                ranAt: new Date().toISOString(),
                overridesSource: overrides.source,
                overridesWarnings: overrides.warnings,
                stats: runResult.stats,
                results: runResult.results,
            }, null, 2));
        }
    }
    catch (err) {
        writeFileSync(join(opts.outputDir, 'normalise.json'), JSON.stringify({
            schemaVersion: '1.0',
            ranAt: new Date().toISOString(),
            error: err?.message ?? String(err),
        }, null, 2));
    }
    // 6. Final validation.
    const validateFinal = recordedRunValidate(finalSpecPath);
    writeFileSync(join(opts.outputDir, 'validate.txt'), validateFinal.ok ? 'OK' : validateFinal.errors);
    // 6.5. Audit-trail attribution call (focused, small, post-validate).
    // Runs only when:
    //   (a) candidate-step timelines were generated (we're auto-facts mode), AND
    //   (b) final spec validated cleanly (no point attributing an invalid spec), AND
    //   (c) the behaviours phase ran — attribution maps action bodies back to
    //       candidate-steps, so it's meaningless on a skeleton-only spec
    //       where bodies are placeholders. (engines 6.29.3+)
    if (validateFinal.ok && candidateStepsContent && opts.withBehaviours !== false) {
        try {
            const finalSpec = readFileSync(finalSpecPath, 'utf8');
            const attributeResult = await runPrompt({
                operation: 'attribute',
                values: {
                    spec: finalSpec,
                    candidateSteps: candidateStepsContent,
                },
                model,
                timeoutMs: 5 * 60_000, // 5 minutes — attribution is a small focused call
                onAssembled: ({ system, user }) => {
                    writeFileSync(join(promptsDir(opts.outputDir), 'attribute-system.txt'), system);
                    writeFileSync(join(promptsDir(opts.outputDir), 'attribute-user.txt'), user);
                },
            });
            writeFileSync(join(llmOutputDir(opts.outputDir), 'attribute.txt'), attributeResult.text);
            const attribution = await extractAttributionBlock(attributeResult.text);
            if (attribution) {
                writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse-attribution.json'), JSON.stringify({ schemaVersion: '1.0', attribution: attribution.attribution ?? [] }, null, 2));
                writeFileSync(join(llmOutputDir(opts.outputDir), 'analyse-decisions.json'), JSON.stringify({ schemaVersion: '1.0', elidedWalkerMethods: attribution.elidedWalkerMethods ?? [] }, null, 2));
            }
            else {
                verifyRecorder.recordHumanReviewItem({
                    severity: 'warning',
                    kind: 'attribute-block-missing',
                    message: 'Attribute LLM call returned response without AUDIT-ATTRIBUTION block; report assembler falls back to name-similarity heuristic.',
                });
            }
        }
        catch (e) {
            verifyRecorder.recordHumanReviewItem({
                severity: 'warning',
                kind: 'attribute-call-failed',
                message: `Attribute LLM call failed: ${e?.message ?? e}. Report assembler falls back to heuristic.`,
            });
        }
    }
    // Drain verify recorder into sidecars.
    try {
        const verifyDir = join(opts.outputDir, 'verify');
        if (!existsSync(verifyDir))
            mkdirSync(verifyDir, { recursive: true });
        writeFileSync(join(verifyDir, 'invariants.json'), JSON.stringify(verifyRecorder.buildInvariants(), null, 2));
        writeFileSync(join(verifyDir, 'retries.json'), JSON.stringify(verifyRecorder.buildRetries(), null, 2));
    }
    catch (e) {
        console.error(`[audit] verify sidecar emit failed: ${e?.message ?? e}`);
    }
    // 7. Optional realize.
    let realize = null;
    if (opts.realize && validateFinal.ok) {
        realize = await realizeProject(opts.outputDir);
    }
    writeProjectReadme(opts.outputDir, {
        caseName: opts.caseName ?? opts.caseMeta?.name ?? 'spec',
        realized: realize,
    });
    // 8. Manifest.
    const wallTimeMs = Date.now() - t0;
    const draftDurationMs = draftResult.duration_ms;
    const manifest = {
        kind: 'analyse',
        case: opts.caseName ?? opts.caseMeta?.name ?? 'spec',
        runFolder: opts.outputDir.split('/').pop() ?? '',
        startedAt,
        endedAt: new Date().toISOString(),
        wallTimeMs,
        provider: opts.providerId ?? resolveProviderId(),
        model: opts.modelId ?? 'default',
        prompt: opts.prompt ?? 'analyse-v9',
        promptTag: promptTag(),
        workflow: opts.verify
            ? 'analyse → spv-validate → verify-analyse → spv-validate'
            : 'analyse → spv-validate',
        verify: !!opts.verify,
        factsPath: factsSource,
        validateFinal: validateFinal.ok ? 'OK' : validateFinal.errors.slice(0, 200),
        realize: opts.realize ? (realize?.ok ? 'OK' : realize?.error || 'skipped') : false,
        sourcePath: opts.sourceDir,
        experimentRunId: opts.experimentRunId ?? null,
    };
    writeManifest(opts.outputDir, manifest);
    // Default README — caller (harness) may overwrite after scoring with
    // a richer version including domain coverage / validity battery.
    writeReadme(opts.outputDir, {
        caseEntry: opts.caseMeta ?? { name: opts.caseName ?? 'spec' },
        manifest,
        runFolder: manifest.runFolder,
        kind: 'analyse',
    });
    return {
        outputDir: opts.outputDir,
        draftSpecPath,
        finalSpecPath,
        finalStage,
        validateFinal,
        realize,
        draftDurationMs,
        verifyDurationMs,
        wallTimeMs,
        manifest,
        draftRetried,
        validateAfterDraftOk: validateAfterDraft.ok,
    };
}
/**
 * Compute SHA256 fingerprint of a source tree for reproducibility records.
 * Skips node_modules, dist, .git, and any path containing those segments.
 */
async function computeSourceFingerprint(rootPath) {
    const skipDirs = new Set(['node_modules', 'dist', '.git', '.next', '.turbo', '.cache', '.specverse']);
    const files = [];
    let totalBytes = 0;
    const walk = (dir) => {
        let entries;
        try {
            entries = readdirSync(dir, { withFileTypes: true }).map((e) => ({
                name: e.name,
                isDir: e.isDirectory(),
            }));
        }
        catch {
            return;
        }
        entries.sort((a, b) => a.name.localeCompare(b.name));
        for (const e of entries) {
            if (skipDirs.has(e.name))
                continue;
            const full = join(dir, e.name);
            if (e.isDir) {
                walk(full);
            }
            else {
                try {
                    const st = statSync(full);
                    if (!st.isFile())
                        continue;
                    const data = readFileSync(full);
                    totalBytes += data.length;
                    files.push({
                        rel: relative(rootPath, full),
                        bytes: data.length,
                        hash: createHash('sha256').update(data).digest('hex'),
                    });
                }
                catch { /* unreadable file — skip */ }
            }
        }
    };
    walk(rootPath);
    // Combine per-file hashes into a single tree hash (order-stable via sort).
    files.sort((a, b) => a.rel.localeCompare(b.rel));
    const treeHash = createHash('sha256');
    for (const f of files) {
        treeHash.update(f.rel).update('\0').update(f.hash).update('\0');
    }
    return {
        schemaVersion: '1.0',
        rootPath,
        sha256: treeHash.digest('hex'),
        fileCount: files.length,
        byteCount: totalBytes,
        scannedAt: new Date().toISOString(),
    };
}
/**
 * Extract the AUDIT-ATTRIBUTION YAML block from a v9.17+ analyse LLM
 * response. The block follows the spec/manifest blocks and is keyed by
 * a sentinel comment on its first line. Returns null when not present
 * (older prompts, or LLM omitted the block).
 */
async function extractAttributionBlock(text) {
    // Match a fenced yaml block that begins with the AUDIT-ATTRIBUTION sentinel comment.
    const re = /```ya?ml\s*\n#\s*AUDIT-ATTRIBUTION\s*\n([\s\S]*?)```/i;
    const m = text.match(re);
    if (!m)
        return null;
    const body = m[1];
    if (!body)
        return null;
    try {
        const yaml = await import('js-yaml');
        const parsed = yaml.load(body);
        if (parsed && typeof parsed === 'object')
            return parsed;
        return null;
    }
    catch {
        return null;
    }
}
//# sourceMappingURL=analyse-runner.js.map