import fs from "fs";
import path from "path";
import { formatRound } from "./trajectory.js";
import { readRecord, findRecordHash, resolveWorkspaceHashForRecord } from "./record-store.js";
import { buildRecordReaderIndex, formatReaderView } from "./record-reader.js";
import { saveTempFile } from "./temp-store.js";
import {
    buildGuardEvidenceIndexes,
    type GuardEvidenceAssetInput,
    type GuardEvidenceIndexMode,
} from "./guard-evidence-index.js";
import type { GuardState } from "./guard-store.js";
import { callModelResponse } from "./model-bridge.js";
import { loadConversationData } from "./conversation-bridge.js";
import type { Chain, DataChain } from "./chain.js";
import { DEFAULT_ANTIGRAVITY_LS_MODEL } from "./ls-model-defaults.js";

/**
 * Stage Guard 比对引擎
 * 
 * 读取 Plan/Task 局部片段 + Record 局部视图 + 对话执行证据 → 构建 Guard Prompt → 解析结果
 *
 * v1.13.4 起使用索引驱动分段取证：
 * 1. Plan/Task 先抽当前 Stage 相关 section、头部规则、尾部和小本本，不再全量拼接。
 * 2. 强制证据区固定保留命令、报告、run/obs、文件路径等证据 manifest。
 * 3. Record 只作为局部 Guard 视图，对话原文用高证据格式补充工具结果、代码编辑和文件视图。
 * 4. coverage 不足时可返回 EVIDENCE_INSUFFICIENT，不计入真实未通过次数。
 */

const FLASH_MODEL = process.env.MEMORY_STORE_LS_MODEL || DEFAULT_ANTIGRAVITY_LS_MODEL;
const FLASH_TIMEOUT = Number(process.env.MEMORY_STORE_GUARD_TIMEOUT || 120_000);
const CODEX_GUARD_SYNC_TIMEOUT = Number(process.env.MEMORY_STORE_CODEX_GUARD_TIMEOUT || 90_000);
const CODEX_GUARD_BACKGROUND_TIMEOUT = Number(process.env.MEMORY_STORE_CODEX_GUARD_BACKGROUND_TIMEOUT || 5 * 60_000);
const MAX_PROMPT_CHARS = 500_000;
const PROMPT_TEMPLATE_OVERHEAD = 3_000;
const TASK_DOC_BUDGET = Number(process.env.MEMORY_STORE_GUARD_TASK_DOC_BUDGET || 100_000);
const PLAN_DOC_BUDGET = Number(process.env.MEMORY_STORE_GUARD_PLAN_DOC_BUDGET || 80_000);
const RECORD_CONTEXT_BUDGET = Number(process.env.MEMORY_STORE_GUARD_RECORD_CONTEXT_BUDGET || 40_000);
const EXECUTION_RECORD_BUDGET = Number(process.env.MEMORY_STORE_GUARD_EXECUTION_BUDGET || 120_000);
const EVIDENCE_CONTEXT_BUDGET = Number(process.env.MEMORY_STORE_GUARD_EVIDENCE_BUDGET || 80_000);

// ============= 类型定义 =============

export interface GuardCheckResult {
    passed: boolean;
    summary: string;
    missingItems: string[];
    rawResponse: string;
    reportPath?: string;
    infrastructureError?: boolean;
    evidenceInsufficient?: boolean;
    evidenceInsufficientReason?: string;
    selfReferenceResolved?: boolean;
    selfReferenceItems?: string[];
    evidenceIndexManifestPath?: string;
}

interface GuardExtractionSegment {
    label: string;
    startLine: number;
    endLine: number;
    reason: string;
    chars: number;
}

interface GuardSourceCoverage {
    file: string;
    basename: string;
    totalLines: number;
    totalChars: number;
    mode: "task" | "plan";
    segments: GuardExtractionSegment[];
    unreadRanges: string[];
    anchors: string[];
    fallbackUsed: boolean;
    truncated: boolean;
}

interface GuardEvidenceManifest {
    commands: string[];
    reports: string[];
    runIds: string[];
    observationIds: string[];
    files: string[];
    stageGuardReports: string[];
}

interface GuardInputBundle {
    planContent: string;
    taskContent: string;
    coverageText: string;
    coverage: {
        confidence: "high" | "medium" | "low";
        truncationRisk: "none" | "low" | "medium" | "high";
        sources: GuardSourceCoverage[];
        coverageGaps: string[];
    };
    evidenceText: string;
    evidenceManifest: GuardEvidenceManifest;
    evidenceSourceText: string;
}

export interface GuardLocatorSuggestion {
    file: string;
    startLine: number;
    endLine: number;
    reason?: string;
}

export interface GuardMaterializedLocatorSuggestions {
    taskSegments: string[];
    planSegments: string[];
    evidenceSources: string[];
}

interface FlashCallResult {
    text: string | null;
    error?: string;
    infrastructureError?: boolean;
}

export function isGuardInfrastructureError(result: GuardCheckResult): boolean {
    return result.infrastructureError === true;
}

export function isGuardEvidenceInsufficient(result: GuardCheckResult): boolean {
    return result.evidenceInsufficient === true;
}

// ============= Flash 调用（带重试） =============

async function callFlash(prompt: string): Promise<FlashCallResult> {
    return callFlashWithChain(prompt, "auto");
}

async function callFlashWithChain(
    prompt: string,
    modelChain: Chain = "auto",
    options: { background?: boolean; dataChain?: DataChain } = {},
): Promise<FlashCallResult> {
    const isCodexOnly = modelChain === "codex";
    const isClaudeCodeOnly = modelChain === "claude-code";
    const timeoutMs = isCodexOnly
        ? (options.background ? CODEX_GUARD_BACKGROUND_TIMEOUT : Math.min(FLASH_TIMEOUT, CODEX_GUARD_SYNC_TIMEOUT))
        : isClaudeCodeOnly
            ? Number(process.env.MEMORY_STORE_CC_GUARD_TIMEOUT_MS || FLASH_TIMEOUT)
        : FLASH_TIMEOUT;
    const resp = await callModelResponse(FLASH_MODEL, prompt, modelChain || "auto", timeoutMs, { allowClaudeCodeFallback: options.dataChain === "claude-code" });
    if (resp.text) return { text: resp.text };

    if (isCodexOnly || isClaudeCodeOnly || resp.error?.includes("Codex 模型桥") || resp.error?.includes("Claude Code CLI")) {
        console.error(`[guard-engine] 模型桥失败，不重试: ${resp.error || "unknown error"}`);
        return { text: null, error: resp.error || "模型桥调用失败", infrastructureError: true };
    }

    console.error(`[guard-engine] Flash 首次失败，5s 后重试...`);
    await new Promise(r => setTimeout(r, 5000));
    const retry = await callModelResponse(FLASH_MODEL, prompt, modelChain || "auto", FLASH_TIMEOUT, { allowClaudeCodeFallback: options.dataChain === "claude-code" });
    return {
        text: retry.text,
        error: retry.text ? undefined : retry.error || resp.error || "Flash 模型调用失败",
        infrastructureError: !retry.text,
    };
}

// ============= Guard 输入取证 =============

function clamp(value: number, min: number, max: number): number {
    return Math.max(min, Math.min(max, value));
}

function lineRangeLabel(startLine: number, endLine: number): string {
    return `L${startLine}-${endLine}`;
}

function truncateText(text: string, maxChars: number): string {
    if (text.length <= maxChars) return text;
    return `${text.slice(0, Math.max(0, maxChars - 80))}\n\n...[truncated ${text.length} -> ${maxChars} chars]`;
}

function deriveGuardAnchors(stageId?: string): string[] {
    const anchors = new Set<string>();
    const raw = (stageId || "").trim();
    if (raw) anchors.add(raw);
    const genericTokens = new Set([
        "stage",
        "guard",
        "evidence",
        "implementation",
        "extraction",
        "check",
        "task",
        "plan",
    ]);

    const uMatches = raw.match(/U\d+/giu) || [];
    for (const item of uMatches) anchors.add(item.toUpperCase());

    const stageMatches = raw.match(/Stage\s*\d+[A-Z]?(?:-\d+[A-Z]?)?/giu) || [];
    for (const item of stageMatches) {
        anchors.add(item.replace(/\s+/g, " ").trim());
        anchors.add(item.replace(/\s+/g, ""));
    }

    const number = raw.match(/\b(\d{1,3})(?:[A-Z])?\b/u);
    if (number) {
        anchors.add(`Stage ${number[1]}`);
        anchors.add(`Stage${number[1]}`);
    }

    for (const token of raw.split(/[\s:：,，/\\()[\]{}|"'`]+/u)) {
        const cleaned = token.trim();
        if (genericTokens.has(cleaned.toLowerCase())) continue;
        if (cleaned.length >= 4) anchors.add(cleaned);
    }

    return [...anchors].filter(Boolean);
}

interface HeadingLine {
    lineNumber: number;
    level: number;
    text: string;
}

function parseHeadings(lines: string[]): HeadingLine[] {
    const headings: HeadingLine[] = [];
    lines.forEach((line, index) => {
        const match = line.match(/^(#{1,6})\s+(.+?)\s*$/u);
        if (!match) return;
        headings.push({ lineNumber: index + 1, level: match[1].length, text: match[2].trim() });
    });
    return headings;
}

function lineMatchesAnyAnchor(line: string, anchors: string[]): boolean {
    const lower = line.toLowerCase();
    return anchors.some(anchor => anchor && lower.includes(anchor.toLowerCase()));
}

function findHeadingSection(headings: HeadingLine[], lines: string[], anchors: string[]): { startLine: number; endLine: number; anchor: string } | null {
    if (headings.length === 0 || anchors.length === 0) return null;
    const matches = headings
        .filter(heading => lineMatchesAnyAnchor(`${heading.text} ${lines[heading.lineNumber - 1] || ""}`, anchors))
        .sort((a, b) => b.lineNumber - a.lineNumber);
    const selected = matches[0];
    if (!selected) return null;

    const next = headings.find(heading => heading.lineNumber > selected.lineNumber && heading.level <= selected.level);
    return {
        startLine: selected.lineNumber,
        endLine: next ? next.lineNumber - 1 : lines.length,
        anchor: selected.text,
    };
}

function findHeadingKeywordSections(headings: HeadingLine[], lines: string[], anchors: string[], keywords: RegExp[], maxCount = 4): Array<{ startLine: number; endLine: number; anchor: string }> {
    const results: Array<{ startLine: number; endLine: number; anchor: string }> = [];
    for (let index = 0; index < headings.length; index++) {
        const heading = headings[index];
        const headingLine = lines[heading.lineNumber - 1] || heading.text;
        const hasKeyword = keywords.some(pattern => pattern.test(headingLine));
        if (!hasKeyword) continue;
        const hasStageAnchor = anchors.length === 0 || lineMatchesAnyAnchor(headingLine, anchors);
        const nearTail = heading.lineNumber >= Math.max(1, lines.length - 300);
        if (!hasStageAnchor && !nearTail) continue;
        const next = headings.slice(index + 1).find(item => item.level <= heading.level);
        results.push({
            startLine: heading.lineNumber,
            endLine: next ? next.lineNumber - 1 : lines.length,
            anchor: heading.text,
        });
        if (results.length >= maxCount) break;
    }
    return results;
}

function findEvidenceWindows(lines: string[], anchors: string[], maxWindows = 12): Array<{ startLine: number; endLine: number; anchor: string }> {
    const patterns = [
        /npm\s+run|node\s+scripts\/|npx\s+/iu,
        /tests[\\/][\w./\\-]+|reports?[\\/][\w./\\-]+/iu,
        /runs[\\/]run_|run_[0-9a-f-]{8,}/iu,
        /obs_[0-9a-f-]{8,}/iu,
        /stage_guard_report_|Guard 检查报告/iu,
        /src[\\/][\w./\\-]+\.(ts|js|mjs)|\.md|\.json/iu,
    ];
    const windows: Array<{ startLine: number; endLine: number; anchor: string }> = [];
    lines.forEach((line, index) => {
        if (!patterns.some(pattern => pattern.test(line))) return;
        const hasStageAnchor = anchors.length === 0 || lineMatchesAnyAnchor(line, anchors);
        const nearTail = index + 1 >= Math.max(1, lines.length - 300);
        if (!hasStageAnchor && !nearTail) return;
        windows.push({
            startLine: clamp(index + 1 - 2, 1, lines.length),
            endLine: clamp(index + 1 + 2, 1, lines.length),
            anchor: line.trim().slice(0, 120),
        });
    });
    return windows.slice(0, maxWindows);
}

function mergeRanges(ranges: Array<{ startLine: number; endLine: number; label: string; reason: string }>): Array<{ startLine: number; endLine: number; labels: string[]; reasons: string[] }> {
    const normalized = ranges
        .filter(range => range.startLine <= range.endLine)
        .sort((a, b) => a.startLine - b.startLine || a.endLine - b.endLine);
    const merged: Array<{ startLine: number; endLine: number; labels: string[]; reasons: string[] }> = [];
    for (const range of normalized) {
        const last = merged[merged.length - 1];
        if (last && range.startLine <= last.endLine + 1) {
            last.endLine = Math.max(last.endLine, range.endLine);
            last.labels.push(range.label);
            last.reasons.push(range.reason);
        } else {
            merged.push({
                startLine: range.startLine,
                endLine: range.endLine,
                labels: [range.label],
                reasons: [range.reason],
            });
        }
    }
    return merged;
}

function unreadRanges(totalLines: number, segments: GuardExtractionSegment[]): string[] {
    const sorted = segments.slice().sort((a, b) => a.startLine - b.startLine);
    const gaps: string[] = [];
    let cursor = 1;
    for (const segment of sorted) {
        if (segment.startLine > cursor) gaps.push(lineRangeLabel(cursor, segment.startLine - 1));
        cursor = Math.max(cursor, segment.endLine + 1);
    }
    if (cursor <= totalLines) gaps.push(lineRangeLabel(cursor, totalLines));
    return gaps;
}

function formatDocumentSegment(basename: string, label: string, startLine: number, endLine: number, reason: string, lines: string[], maxChars: number): { text: string; segment: GuardExtractionSegment; truncated: boolean } {
    const body = lines.slice(startLine - 1, endLine).join("\n");
    const truncated = body.length > maxChars;
    const content = truncateText(body, maxChars);
    const header = `#### ${basename} [${label} ${lineRangeLabel(startLine, endLine)}]\n原因: ${reason}\n`;
    return {
        text: `${header}\n${content}`,
        segment: { label, startLine, endLine, reason, chars: content.length },
        truncated,
    };
}

function extractGuardDocument(filePath: string, mode: "task" | "plan", stageId?: string, maxChars = mode === "task" ? TASK_DOC_BUDGET : PLAN_DOC_BUDGET): { text: string; coverage: GuardSourceCoverage; evidenceSourceText: string } | null {
    if (!fs.existsSync(filePath)) return null;
    const content = fs.readFileSync(filePath, "utf-8");
    const lines = content.split(/\r?\n/u);
    const basename = path.basename(filePath);
    const headings = parseHeadings(lines);
    const anchors = deriveGuardAnchors(stageId);
    const ranges: Array<{ startLine: number; endLine: number; label: string; reason: string }> = [];

    const isShortDensePlan = mode === "plan" && lines.length <= 260 && content.length <= 60_000;
    if (isShortDensePlan) {
        ranges.push({ startLine: 1, endLine: lines.length, label: "full-map", reason: "short dense plan/map file" });
    } else {
        const headEnd = mode === "task" ? Math.min(120, lines.length) : Math.min(60, lines.length);
        ranges.push({ startLine: 1, endLine: headEnd, label: "head-rules", reason: mode === "task" ? "task rules and guard contract" : "plan goals and acceptance anchors" });

        const stageSection = findHeadingSection(headings, lines, anchors);
        if (stageSection) {
            ranges.push({
                startLine: stageSection.startLine,
                endLine: stageSection.endLine,
                label: "stage-section",
                reason: `stage anchor: ${stageSection.anchor}`,
            });
        } else if (anchors.length > 0) {
            const matchIndex = lines.findIndex(line => lineMatchesAnyAnchor(line, anchors));
            if (matchIndex >= 0) {
                ranges.push({
                    startLine: clamp(matchIndex + 1 - 80, 1, lines.length),
                    endLine: clamp(matchIndex + 1 + 160, 1, lines.length),
                    label: "anchor-window",
                    reason: `fallback anchor window: ${anchors.slice(0, 3).join(", ")}`,
                });
            }
        }

        const guardSections = findHeadingKeywordSections(headings, lines, anchors, [/待复核|小本本|追加|Guard|证据可见性/iu]);
        for (const section of guardSections) {
            ranges.push({
                startLine: section.startLine,
                endLine: section.endLine,
                label: "guard-note",
                reason: `guard/addendum anchor: ${section.anchor}`,
            });
        }

        const tailLines = mode === "task" ? 180 : 100;
        ranges.push({
            startLine: Math.max(1, lines.length - tailLines + 1),
            endLine: lines.length,
            label: "tail",
            reason: "latest tail and handoff notes",
        });

        for (const window of findEvidenceWindows(lines, anchors)) {
            ranges.push({
                startLine: window.startLine,
                endLine: window.endLine,
                label: "evidence-window",
                reason: `evidence pattern: ${window.anchor}`,
            });
        }
    }

    const merged = mergeRanges(ranges);
    const parts: string[] = [];
    const segments: GuardExtractionSegment[] = [];
    let usedChars = 0;
    let truncated = false;
    for (const range of merged) {
        const remain = Math.max(0, maxChars - usedChars);
        if (remain <= 500) {
            truncated = true;
            break;
        }
        const label = [...new Set(range.labels)].join("+");
        const reason = [...new Set(range.reasons)].join("; ");
        const perSegmentMax = Math.max(500, remain);
        const formatted = formatDocumentSegment(basename, label, range.startLine, range.endLine, reason, lines, perSegmentMax);
        usedChars += formatted.text.length + 20;
        if (formatted.truncated) truncated = true;
        parts.push(formatted.text);
        segments.push(formatted.segment);
    }

    const coverage: GuardSourceCoverage = {
        file: filePath,
        basename,
        totalLines: lines.length,
        totalChars: content.length,
        mode,
        segments,
        unreadRanges: unreadRanges(lines.length, segments),
        anchors,
        fallbackUsed: segments.some(segment => segment.label.includes("anchor-window")) || segments.length === 0,
        truncated,
    };

    return {
        text: `### ${basename}（${mode === "task" ? "Task 局部片段" : "Plan 局部片段"}）\n\n${parts.join("\n\n---\n\n")}`,
        coverage,
        evidenceSourceText: parts.join("\n\n"),
    };
}

function uniqueLimited(values: string[], max = 30): string[] {
    const seen = new Set<string>();
    const result: string[] = [];
    for (const value of values) {
        const cleaned = value.trim();
        if (!cleaned || seen.has(cleaned)) continue;
        seen.add(cleaned);
        result.push(cleaned);
        if (result.length >= max) break;
    }
    return result;
}

function extractEvidenceManifest(text: string): GuardEvidenceManifest {
    const commands = uniqueLimited([...text.matchAll(/\b(?:npm\s+run|npx|node\s+(?:scripts[\\/])?)[^\n`。，；;)]{0,160}/giu)].map(match => match[0]));
    const reports = uniqueLimited([...text.matchAll(/(?:[A-Za-z]:[\\/][^\s`，。；;)]*tests[\\/][^\s`，。；;)]+|tests[\\/][^\s`，。；;)]+)/giu)].map(match => match[0]));
    const runIds = uniqueLimited([...text.matchAll(/\brun_[0-9a-f-]{8,}/giu)].map(match => match[0]));
    const observationIds = uniqueLimited([...text.matchAll(/\bobs_[0-9a-f-]{8,}/giu)].map(match => match[0]));
    const stageGuardReports = uniqueLimited([...text.matchAll(/stage_guard_report_[^\s`，。；;)]+/giu)].map(match => match[0]));
    const files = uniqueLimited([...text.matchAll(/(?:src|tests|scripts|docs|Plan|mcp-server|windows-helper|plugin)[\\/][\w./\\\-\u4e00-\u9fff]+(?:\.(?:ts|js|mjs|json|md|cs))?/giu)].map(match => match[0]));
    return { commands, reports, runIds, observationIds, files, stageGuardReports };
}

function formatEvidenceManifest(manifest: GuardEvidenceManifest, maxChars = EVIDENCE_CONTEXT_BUDGET): string {
    const lines = [
        `### 命令 (${manifest.commands.length})`,
        ...manifest.commands.map(item => `- ${item}`),
        ``,
        `### 报告/测试路径 (${manifest.reports.length})`,
        ...manifest.reports.map(item => `- ${item}`),
        ``,
        `### run id (${manifest.runIds.length})`,
        ...manifest.runIds.map(item => `- ${item}`),
        ``,
        `### observation id (${manifest.observationIds.length})`,
        ...manifest.observationIds.map(item => `- ${item}`),
        ``,
        `### 文件路径 (${manifest.files.length})`,
        ...manifest.files.map(item => `- ${item}`),
        ``,
        `### Guard 报告 (${manifest.stageGuardReports.length})`,
        ...manifest.stageGuardReports.map(item => `- ${item}`),
    ];
    return truncateText(lines.join("\n"), maxChars);
}

function mergeEvidenceManifest(items: GuardEvidenceManifest[]): GuardEvidenceManifest {
    return {
        commands: uniqueLimited(items.flatMap(item => item.commands), 60),
        reports: uniqueLimited(items.flatMap(item => item.reports), 60),
        runIds: uniqueLimited(items.flatMap(item => item.runIds), 60),
        observationIds: uniqueLimited(items.flatMap(item => item.observationIds), 60),
        files: uniqueLimited(items.flatMap(item => item.files), 80),
        stageGuardReports: uniqueLimited(items.flatMap(item => item.stageGuardReports), 40),
    };
}

function formatCoverageText(bundle: Pick<GuardInputBundle, "coverage">): string {
    const { coverage } = bundle;
    const lines = [
        `confidence: ${coverage.confidence}`,
        `truncationRisk: ${coverage.truncationRisk}`,
        `coverageGaps: ${coverage.coverageGaps.length ? coverage.coverageGaps.join("; ") : "none"}`,
        ``,
    ];
    for (const source of coverage.sources) {
        lines.push(`### ${source.basename} (${source.totalLines} 行, ${source.totalChars} 字, ${source.mode})`);
        for (const segment of source.segments) {
            lines.push(`- ${segment.label} ${lineRangeLabel(segment.startLine, segment.endLine)}: ${segment.reason}`);
        }
        if (source.fallbackUsed) lines.push(`- fallbackUsed: true`);
        if (source.truncated) lines.push(`- truncated: true`);
        if (source.unreadRanges.length > 0) lines.push(`- unreadRanges: ${source.unreadRanges.slice(0, 8).join(", ")}${source.unreadRanges.length > 8 ? ", ..." : ""}`);
        lines.push("");
    }
    return lines.join("\n");
}

function computeCoverage(sources: GuardSourceCoverage[]) {
    const coverageGaps: string[] = [];
    for (const source of sources) {
        if (source.segments.length === 0) coverageGaps.push(`${source.basename}: no segments extracted`);
        if (source.fallbackUsed) coverageGaps.push(`${source.basename}: fallback extraction used`);
        if (source.truncated) coverageGaps.push(`${source.basename}: segment truncated`);
        if (!source.segments.some(segment => segment.label.includes("stage-section") || segment.label.includes("full-map"))) {
            coverageGaps.push(`${source.basename}: no stage section/full map`);
        }
    }
    const truncationRisk = sources.some(source => source.truncated)
        ? "medium"
        : coverageGaps.length > 0
            ? "low"
            : "none";
    const confidence = coverageGaps.length === 0
        ? "high"
        : sources.some(source => source.segments.length === 0 || source.fallbackUsed)
            ? "low"
            : "medium";
    return { confidence, truncationRisk, sources, coverageGaps } as GuardInputBundle["coverage"];
}

export function buildGuardInputBundle(planFiles: string[], taskFiles: string[], stageId?: string): GuardInputBundle {
    const taskResults = taskFiles
        .map(file => extractGuardDocument(file, "task", stageId, TASK_DOC_BUDGET))
        .filter((item): item is NonNullable<typeof item> => Boolean(item));
    const planResults = planFiles
        .map(file => extractGuardDocument(file, "plan", stageId, PLAN_DOC_BUDGET))
        .filter((item): item is NonNullable<typeof item> => Boolean(item));

    const sources = [...taskResults, ...planResults].map(item => item.coverage);
    const coverage = computeCoverage(sources);
    const evidenceSourceText = [...taskResults, ...planResults].map(item => item.evidenceSourceText).join("\n\n");
    const evidenceManifest = extractEvidenceManifest(evidenceSourceText);
    const partialBundle = { coverage } as GuardInputBundle;
    const coverageText = formatCoverageText(partialBundle);
    const evidenceText = formatEvidenceManifest(evidenceManifest);

    return {
        planContent: planResults.map(item => item.text).join("\n\n---\n\n"),
        taskContent: taskResults.map(item => item.text).join("\n\n---\n\n"),
        coverageText,
        coverage,
        evidenceText,
        evidenceManifest,
        evidenceSourceText,
    };
}

function buildLocatorFileMap(filePaths: string[], stageId?: string): string {
    const anchors = deriveGuardAnchors(stageId);
    const sections: string[] = [];
    for (const filePath of filePaths) {
        if (!fs.existsSync(filePath)) continue;
        const content = fs.readFileSync(filePath, "utf-8");
        const lines = content.split(/\r?\n/u);
        const headings = parseHeadings(lines).slice(0, 120).map(heading =>
            `L${heading.lineNumber} ${"#".repeat(heading.level)} ${heading.text}`
        );
        const anchorLines = lines
            .map((line, index) => ({ line, lineNumber: index + 1 }))
            .filter(item => lineMatchesAnyAnchor(item.line, anchors) || /npm run|tests[\\/]|runs?[\\/]|obs_|run_|报告|验证|Guard|Stage/iu.test(item.line))
            .slice(0, 80)
            .map(item => `L${item.lineNumber}: ${item.line.slice(0, 220)}`);
        sections.push([
            `### ${filePath}`,
            `totalLines=${lines.length}, totalChars=${content.length}`,
            `#### headings`,
            headings.join("\n") || "(none)",
            `#### candidate evidence lines`,
            anchorLines.join("\n") || "(none)",
        ].join("\n"));
    }
    return truncateText(sections.join("\n\n"), 60_000);
}

export function parseLocatorSuggestions(raw: string): GuardLocatorSuggestion[] {
    const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/iu)?.[1];
    const jsonText = fenced || raw.slice(raw.indexOf("{") >= 0 ? raw.indexOf("{") : 0, raw.lastIndexOf("}") + 1 || raw.length);
    try {
        const parsed = JSON.parse(jsonText);
        const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed.suggestions) ? parsed.suggestions : [];
        return list
            .map((item: any) => ({
                file: String(item.file || item.path || "").trim(),
                startLine: Number(item.startLine ?? item.start ?? item.lineStart),
                endLine: Number(item.endLine ?? item.end ?? item.lineEnd),
                reason: item.reason ? String(item.reason) : undefined,
            }))
            .filter((item: GuardLocatorSuggestion) => item.file && Number.isFinite(item.startLine) && Number.isFinite(item.endLine));
    } catch {
        return [];
    }
}

export function materializeLocatorSuggestions(
    suggestions: GuardLocatorSuggestion[],
    planFiles: string[],
    taskFiles: string[],
): GuardMaterializedLocatorSuggestions {
    const allFiles = [...taskFiles, ...planFiles].filter(file => fs.existsSync(file));
    const allowed = new Map(allFiles.map(file => [path.resolve(file).toLowerCase(), file]));
    const taskSegments: string[] = [];
    const planSegments: string[] = [];
    const evidenceSources: string[] = [];

    for (const suggestion of suggestions.slice(0, 6)) {
        const resolved = path.resolve(suggestion.file).toLowerCase();
        const filePath = allowed.get(resolved);
        if (!filePath || !fs.existsSync(filePath)) continue;
        const lines = fs.readFileSync(filePath, "utf-8").split(/\r?\n/u);
        const startLine = clamp(Math.min(suggestion.startLine, suggestion.endLine), 1, lines.length);
        const endLine = clamp(Math.max(suggestion.startLine, suggestion.endLine), startLine, Math.min(lines.length, startLine + 119));
        const formatted = formatDocumentSegment(path.basename(filePath), "model-locator", startLine, endLine, suggestion.reason || "model assisted locator validated locally", lines, 16_000);
        if (taskFiles.some(file => path.resolve(file).toLowerCase() === resolved)) {
            taskSegments.push(formatted.text);
        } else {
            planSegments.push(formatted.text);
        }
        evidenceSources.push(formatted.text);
    }

    return { taskSegments, planSegments, evidenceSources };
}

async function enhanceGuardInputBundleWithModelLocator(
    bundle: GuardInputBundle,
    planFiles: string[],
    taskFiles: string[],
    stageId: string | undefined,
    modelChain: Chain,
): Promise<GuardInputBundle> {
    const shouldLocate = stageId && (
        bundle.coverage.confidence !== "high" ||
        bundle.coverage.truncationRisk !== "none" ||
        bundle.coverage.coverageGaps.some(gap => /no stage section|fallback extraction|no segments/iu.test(gap))
    );
    if (!shouldLocate) return bundle;

    const allFiles = [...taskFiles, ...planFiles].filter(file => fs.existsSync(file));
    const fileMap = buildLocatorFileMap(allFiles, stageId);
    if (!fileMap.trim()) return bundle;

    const prompt = [
        `你是 Stage Guard 的受限定位器，只能根据下面的文件标题索引和候选证据行建议需要补读的行号。`,
        `目标 Stage: ${stageId}`,
        `当前 coverage gaps: ${bundle.coverage.coverageGaps.join("; ") || "none"}`,
        ``,
        `规则：`,
        `1. 只输出 JSON，不要解释。`,
        `2. suggestions 最多 6 项。每项必须是 {"file":"完整路径","startLine":数字,"endLine":数字,"reason":"简短原因"}。`,
        `3. 每个范围最多 120 行；如果不确定，返回空数组。`,
        `4. 只能选择下方出现过的文件路径和行号附近，不能发明文件或行号。`,
        ``,
        `文件索引：`,
        fileMap,
        ``,
        `输出格式：{"suggestions":[]}`,
    ].join("\n");

    const response = await callModelResponse(FLASH_MODEL, prompt, modelChain || "auto", Math.min(FLASH_TIMEOUT, 45_000));
    if (!response.text) {
        return {
            ...bundle,
            coverageText: `${bundle.coverageText}\n\nmodelLocator: unavailable (${response.error || "no response"})`,
        };
    }

    const suggestions = parseLocatorSuggestions(response.text);
    const { taskSegments, planSegments, evidenceSources } = materializeLocatorSuggestions(suggestions, planFiles, taskFiles);

    if (taskSegments.length === 0 && planSegments.length === 0) {
        return {
            ...bundle,
            coverageText: `${bundle.coverageText}\n\nmodelLocator: no valid local ranges returned`,
        };
    }

    const locatorEvidence = extractEvidenceManifest(evidenceSources.join("\n\n"));
    const evidenceManifest = mergeEvidenceManifest([bundle.evidenceManifest, locatorEvidence]);
    const locatorNote = [
        `modelLocator: applied`,
        `modelLocatorRanges: ${[...taskSegments, ...planSegments].length}`,
        `modelLocatorRule: model suggested line ranges, then local file/path/range validation and bounded reread`,
    ].join("\n");

    return {
        ...bundle,
        taskContent: [bundle.taskContent, taskSegments.length ? `### 模型辅助定位补读（Task）\n\n${taskSegments.join("\n\n---\n\n")}` : ""].filter(Boolean).join("\n\n---\n\n"),
        planContent: [bundle.planContent, planSegments.length ? `### 模型辅助定位补读（Plan）\n\n${planSegments.join("\n\n---\n\n")}` : ""].filter(Boolean).join("\n\n---\n\n"),
        coverageText: `${bundle.coverageText}\n\n${locatorNote}`,
        evidenceSourceText: [bundle.evidenceSourceText, ...evidenceSources].filter(Boolean).join("\n\n"),
        evidenceManifest,
        evidenceText: formatEvidenceManifest(evidenceManifest),
    };
}

// ============= 执行记录获取（分层注入） =============

/**
 * 获取执行记录：Record + 对话原文双源注入
 * 
 * @param conversationId 对话 ID
 * @param startRound 起始轮次（0=全部）
 * @param charBudget 剩余字符预算
 */
function getRecordContext(conversationId: string, stageId: string | undefined, charBudget: number): string {
    const hash = findRecordHash(conversationId) || resolveWorkspaceHashForRecord();
    const record = readRecord(hash, conversationId);
    if (!record) return "";
    const anchors = deriveGuardAnchors(stageId);
    const lines = record.split(/\r?\n/u);
    const ranges: Array<{ startLine: number; endLine: number; label: string; reason: string }> = [];
    const parts: string[] = [];
    let used = 0;

    try {
        const index = buildRecordReaderIndex(conversationId, record);
        const readerViews = [
            { label: "state", maxChars: Math.max(4_000, Math.floor(charBudget * 0.25)) },
            { label: "outputs", maxChars: Math.max(4_000, Math.floor(charBudget * 0.20)) },
            { label: "verification", maxChars: Math.max(4_000, Math.floor(charBudget * 0.20)) },
            { label: "risks", maxChars: Math.max(3_000, Math.floor(charBudget * 0.10)) },
        ] as const;
        for (const view of readerViews) {
            const remain = charBudget - used;
            if (remain <= 1_000) break;
            const rendered = formatReaderView(index, {
                view: view.label,
                maxChars: Math.min(view.maxChars, remain),
                withCitations: true,
            });
            if (!rendered.text.trim()) continue;
            const text = `#### Record Reader view=${view.label}\n${rendered.text.trim()}` +
                (rendered.truncated && rendered.nextReadHint ? `\n\n[truncated] ${rendered.nextReadHint}` : "");
            parts.push(text);
            used += text.length + 20;
        }
    } catch (err) {
        console.error(`[guard-engine] Record Reader view failed, fallback to local slices: ${err instanceof Error ? err.message : String(err)}`);
    }

    ranges.push({ startLine: 1, endLine: Math.min(80, lines.length), label: "record-head", reason: "record metadata and opening context" });
    ranges.push({ startLine: Math.max(1, lines.length - 220), endLine: lines.length, label: "record-tail", reason: "latest state / risks / outputs" });
    for (const window of findEvidenceWindows(lines, anchors, 12)) {
        ranges.push({ startLine: window.startLine, endLine: window.endLine, label: "record-evidence", reason: window.anchor });
    }
    const merged = mergeRanges(ranges);
    for (const range of merged) {
        const remain = charBudget - used;
        if (remain <= 500) break;
        const formatted = formatDocumentSegment("Record", range.labels.join("+"), range.startLine, range.endLine, range.reasons.join("; "), lines, remain);
        parts.push(formatted.text);
        used += formatted.text.length + 20;
    }
    return parts.length ? `### Record（局部 Guard 视图）\n\n${parts.join("\n\n---\n\n")}` : "";
}

async function getConversationExecutionRecord(
    conversationId: string,
    startRound: number,
    charBudget: number,
    dataChain: DataChain = "auto",
): Promise<string> {
    const parts: string[] = [];
    if (charBudget > 10_000) {
        try {
            const loaded = await loadConversationData(dataChain || "auto", conversationId, { link: "summary" });
            if (loaded) {
                const targetRounds = startRound > 0
                    ? loaded.rounds.filter(r => r.roundIndex >= startRound)
                    : loaded.rounds;

                const convParts: string[] = [];
                let convChars = 0;
                for (const round of targetRounds) {
                    const formatted = formatRound(round, "normal", ["tool_results", "code_actions", "code_diffs", "file_views"]);
                    if (convChars + formatted.length > charBudget) {
                        const brief = formatRound(round, "brief");
                        if (convChars + brief.length > charBudget) break;
                        convParts.push(brief);
                        convChars += brief.length;
                    } else {
                        convParts.push(formatted);
                        convChars += formatted.length;
                    }
                }

                if (convParts.length > 0) {
                    parts.push(
                        `### 对话原文（chain=${loaded.chainUsed}，轮次 ${targetRounds[0]?.roundIndex || "?"}-${targetRounds[targetRounds.length - 1]?.roundIndex || "?"}，共 ${convParts.length} 轮）\n\n` +
                        convParts.join("\n\n---\n\n")
                    );
                }
            }
        } catch (err) {
            console.error(`[guard-engine] 对话原文获取失败: ${err}`);
        }
    }

    return parts.join("\n\n===\n\n");
}

// ============= Prompt 构建 =============

function buildGuardPrompt(
    planContent: string,
    taskContent: string,
    executionRecord: string,
    coverageText: string,
    evidenceText: string,
    evidenceIndexText: string,
    stageId?: string,
    appealNote?: string,
    evidence?: string,
): string {
    let prompt = `你是一个独立的任务完整性审核助手。你的职责是防止 AI 虚标任务完成。

## 任务
对比【计划要求】和【实际执行记录】，检查任务是否被真正完成。
${stageId ? `\n**重点关注范围**：${stageId}。只检查与 ${stageId} 直接相关的任务项，忽略其他 Stage 的历史遗留未完成项。\n` : ""}
## 核心原则（最重要）
**执行记录是 ground truth，Task.md 标记不可信。**
- AI 可能标记了 [x] 但实际没做——你必须在执行记录中找到对应的代码修改、工具调用或文件操作作为证据
- AI 可能说"差不多了"就标完成——"差不多"不算完成，必须与 Plan 要求精确匹配
- AI 的实际操作可能偏离用户原始要求——对照用户消息检查是否存在方向性偏差

## 评判标准
- [x] 标记的项目：必须在执行记录（对话原文、代码 diff、工具调用）中找到对应证据，无证据则视为虚标
- [ ] 或 [/] 标记的项目：视为未完成
- 用户明确说"跳过"或"不做了"的项目：视为已完成
- Plan 中的关键要求：检查是否被实际落实，而非仅在 Task 中标记
- 用户在对话中提出的修改要求：检查 AI 是否真正执行了，还是口头应承但未操作
- 不要把"本次 Guard check 的返回结果、PASS 记录、完成标记、Guard 通过后才能写入的收口记录"作为前置完成证据；如果实质产物/测试/用户裁定证据已充分，唯一缺口是这种后置自指记录，应输出 PASS，并在总结中说明存在 Guard 自指收口项

## 输出格式（严格遵循）
第一行: PASS 或 FAIL 或 EVIDENCE_INSUFFICIENT
第二行: 总结（1句话）
后续行（仅 FAIL 时）: 每个遗漏项一行，格式:
- MISSING: [任务标识] 具体描述（标注是"未做"还是"虚标"还是"偏离用户要求"）

如果 coverage 明确显示关键证据可能未进入输入，或强制执行证据区为空但未读区间可能包含证据，请输出 EVIDENCE_INSUFFICIENT，而不是 FAIL。
`;

    if (coverageText) {
        prompt += `\n## 取证覆盖范围（coverage）\n\n${coverageText}\n`;
    }

    if (evidenceText) {
        prompt += `\n## 强制执行证据区（优先于 Task/Record 自述）\n\n${evidenceText}\n`;
    }

    if (evidenceIndexText) {
        prompt += `\n## 外部证据文件索引（由 Guard 预处理生成，必须结合 artifactPath / warnings 判断覆盖）\n\n${evidenceIndexText}\n`;
    }

    if (planContent) {
        prompt += `\n## 计划文件内容\n\n${planContent}\n`;
    }

    prompt += `\n## 任务文件内容\n\n${taskContent}\n`;

    if (executionRecord) {
        prompt += `\n## 执行记录\n\n${executionRecord}\n`;
    }

    if (appealNote) {
        prompt += `\n## AI 申诉说明\n以下是 AI 的补充说明，请参考但独立判断：\n${appealNote}\n`;
    }

    if (evidence) {
        prompt += `\n## 补录证据\n以下是 AI 提供的补充证据（可能在 Guard 启动前完成，不在执行记录中），请验证其合理性后采纳：\n${evidence}\n`;
    }

    return prompt;
}

// ============= 结果解析 =============

function parseGuardResult(rawResponse: string): GuardCheckResult {
    const lines = rawResponse.trim().split("\n");
    const firstLine = (lines[0] || "").trim().toUpperCase();
    const passed = firstLine === "PASS";
    const evidenceInsufficient = /^(EVIDENCE_INSUFFICIENT|INCONCLUSIVE|TRUNCATION_RISK|ANCHOR_NOT_FOUND)/u.test(firstLine);
    const summary = (lines[1] || "").trim();
    const missingItems: string[] = [];

    for (let i = 2; i < lines.length; i++) {
        const line = lines[i].trim();
        if (line.startsWith("- MISSING:") || line.startsWith("MISSING:")) {
            missingItems.push(line.replace(/^-?\s*MISSING:\s*/i, "").trim());
        }
    }

    return {
        passed,
        summary,
        missingItems,
        rawResponse,
        evidenceInsufficient,
        evidenceInsufficientReason: evidenceInsufficient ? firstLine : undefined,
    };
}

function hasAnyPattern(text: string, patterns: RegExp[]): boolean {
    return patterns.some(pattern => pattern.test(text));
}

function summaryAcknowledgesEvidence(summary: string): boolean {
    return hasAnyPattern(summary, [
        /证据.{0,12}(已|已经|看起来|基本|较为|充分|补齐|齐|落盘|存在)/u,
        /(功能|实现|产物|测试|验证|既有).{0,12}证据.{0,12}(充分|补齐|落盘|存在|可见|已)/u,
        /实质.{0,8}(完成|通过|充分|无缺口)/u,
    ]);
}

function isGuardSelfReferenceMissingItem(item: string): boolean {
    const hasGuardClosureSubject = hasAnyPattern(item, [
        /Guard|stage_guard|check|PASS|appeal|cancel/i,
        /复检|复核|收口|自指|咬尾巴|锁/u,
        /通过结论|通过记录|完成标记|完成记录|本轮.*结果|检查结果|处理结果|返回结果/u,
    ]);
    const hasOnlyAfterCheckEvidence = hasAnyPattern(item, [
        /本轮|此次|当前|这次/u,
        /只显示|停在|没有看到|未看到|未见|缺少|未提供|未出现/u,
        /Guard.*结果|check.*结果|appeal.*结果|cancel.*结果/i,
        /通过|PASS|完成标记|完成记录|收口|落盘|报告/u,
    ]);
    const hasSubstantiveMissingSignal = hasAnyPattern(item, [
        /未新增|未修改|未实现|未补齐|缺少代码|缺少测试|没有测试|没有实现|没有文件|未运行测试/u,
        /npm|build|单元测试|集成测试|截图|源码|配置|schema|fixture|README|Plan|Task\.md/i,
    ]);

    return hasGuardClosureSubject && hasOnlyAfterCheckEvidence && !hasSubstantiveMissingSignal;
}

export function resolveGuardSelfReferenceResult(result: GuardCheckResult): GuardCheckResult {
    if (result.passed || result.missingItems.length === 0) return result;
    const selfReferenceItems = result.missingItems.filter(isGuardSelfReferenceMissingItem);
    if (selfReferenceItems.length !== result.missingItems.length) return result;
    if (!summaryAcknowledgesEvidence(result.summary)) return result;

    return {
        ...result,
        passed: true,
        summary: `${result.summary}（已识别为 Guard 自指收口项，按实质证据通过）`,
        missingItems: [],
        selfReferenceResolved: true,
        selfReferenceItems,
    };
}

// ============= 核心检查入口 =============

function resolveGuardModelChain(state: GuardState): Chain {
    if (state.modelChain) return state.modelChain;
    if (state.chain === "windsurf") return "auto";
    return state.chain || "auto";
}

export async function runGuardCheck(
    state: GuardState,
    appealNote?: string,
    evidence?: string,
    options: { background?: boolean; evidenceAssets?: GuardEvidenceAssetInput[]; evidenceIndexMode?: GuardEvidenceIndexMode } = {},
): Promise<GuardCheckResult> {
    // 1. 分段读取 Plan + Task 文件并生成 coverage / evidence manifest
    const inputBundle = await enhanceGuardInputBundleWithModelLocator(
        buildGuardInputBundle(state.planFiles, state.taskFiles, state.stageId),
        state.planFiles,
        state.taskFiles,
        state.stageId,
        resolveGuardModelChain(state),
    );
    const planContent = inputBundle.planContent;
    const taskContent = inputBundle.taskContent;

    if (!taskContent) {
        return {
            passed: false,
            summary: "无法读取 Task 文件",
            missingItems: ["Task 文件不存在或为空"],
            rawResponse: "",
        };
    }

    // 2. 获取 Record 局部视图 + 对话原文执行记录（固定预算，不被 Plan/Task 挤掉）
    const recordContext = state.conversationId
        ? getRecordContext(state.conversationId, state.stageId, RECORD_CONTEXT_BUDGET)
        : "";
    const conversationExecution = state.conversationId
        ? await getConversationExecutionRecord(state.conversationId, state.startRound, EXECUTION_RECORD_BUDGET, state.chain || "auto")
        : "";

    const externalEvidence = await buildGuardEvidenceIndexes(options.evidenceAssets, {
        indexMode: options.evidenceIndexMode || "auto",
    });

    const executionRecord = [recordContext, conversationExecution].filter(Boolean).join("\n\n===\n\n");
    const runtimeEvidence = extractEvidenceManifest(executionRecord);
    const externalEvidenceManifest = extractEvidenceManifest(externalEvidence.text);
    const mergedEvidenceManifest = mergeEvidenceManifest([inputBundle.evidenceManifest, runtimeEvidence, externalEvidenceManifest]);
    const evidenceText = formatEvidenceManifest(mergedEvidenceManifest);
    const evidenceManifestPath = saveTempFile(
        `stage_guard_evidence_manifest_${Date.now()}`,
        "Stage Guard Evidence Manifest",
        JSON.stringify(mergedEvidenceManifest, null, 2)
    );
    const extractionManifestPath = saveTempFile(
        `stage_guard_extraction_manifest_${Date.now()}`,
        "Stage Guard Extraction Manifest",
        JSON.stringify(inputBundle.coverage, null, 2)
    );

    let finalRecord = truncateText(executionRecord, RECORD_CONTEXT_BUDGET + EXECUTION_RECORD_BUDGET);
    const promptBudget = MAX_PROMPT_CHARS - PROMPT_TEMPLATE_OVERHEAD;
    const projectedChars = planContent.length + taskContent.length + inputBundle.coverageText.length + evidenceText.length + externalEvidence.text.length + finalRecord.length;
    if (projectedChars > promptBudget) {
        const overflow = projectedChars - promptBudget;
        finalRecord = truncateText(finalRecord, Math.max(20_000, finalRecord.length - overflow));
        console.error(`[guard-engine] Guard prompt projected overflow ${overflow}, trimmed execution record to ${finalRecord.length}`);
    }

    // 4. 构建 Prompt → 调用 Flash
    const prompt = buildGuardPrompt(planContent, taskContent, finalRecord, inputBundle.coverageText, evidenceText, externalEvidence.text, state.stageId, appealNote, evidence);
    const response = await callFlashWithChain(prompt, resolveGuardModelChain(state), { ...options, dataChain: state.chain });

    if (!response.text) {
        return {
            passed: false,
            summary: response.error || "Flash 模型调用失败",
            missingItems: [],
            rawResponse: "",
            infrastructureError: response.infrastructureError ?? true,
        };
    }

    // 5. 解析结果
    const result = resolveGuardSelfReferenceResult(parseGuardResult(response.text));
    result.evidenceIndexManifestPath = externalEvidence.manifestPath;

    // 6. 生成详细报告写入临时文件
    const reportLines = [
        `# Stage Guard 检查报告`,
        ``,
        `- **时间**: ${new Date().toISOString()}`,
        `- **Stage**: ${state.stageId || "未指定"}`,
        `- **结果**: ${result.passed ? "✅ PASS" : "❌ FAIL"}`,
        `- **总结**: ${result.summary}`,
        `- **Coverage**: confidence=${inputBundle.coverage.confidence}, truncationRisk=${inputBundle.coverage.truncationRisk}`,
        `- **Extraction Manifest**: ${extractionManifestPath}`,
        `- **Evidence Manifest**: ${evidenceManifestPath}`,
        externalEvidence.items.length > 0 ? `- **External Evidence Index Manifest**: ${externalEvidence.manifestPath}` : "",
        ``,
    ].filter(Boolean);

    reportLines.push(`## 取证覆盖范围`, ``, "```", inputBundle.coverageText, "```", ``);
    reportLines.push(`## 强制执行证据`, ``, "```", evidenceText, "```", ``);
    if (externalEvidence.text) {
        reportLines.push(`## 外部证据文件索引`, ``, "```", externalEvidence.text, "```", ``);
    }

    if (result.missingItems.length > 0) {
        reportLines.push(`## 遗漏项`, ``);
        result.missingItems.forEach((item, i) => {
            reportLines.push(`${i + 1}. ${item}`);
        });
        reportLines.push(``);
    }

    if (result.selfReferenceResolved && result.selfReferenceItems?.length) {
        reportLines.push(`## Guard 自指收口项`, ``);
        result.selfReferenceItems.forEach((item, i) => {
            reportLines.push(`${i + 1}. ${item}`);
        });
        reportLines.push(``);
    }

    reportLines.push(`## Flash 原始返回`, ``, "```", result.rawResponse, "```");

    const reportContent = reportLines.join("\n");
    result.reportPath = saveTempFile(
        `stage_guard_report_${Date.now()}`,
        "Stage Guard 检查报告",
        reportContent
    );

    return result;
}
