/**
 * 控制面可信契约。
 *
 * MCP 工具是 SoloForge 规则、契约、状态与证据的主控制面。
 * 本模块把可用性、可调用性、旁路、状态可信、证据真实性和恢复路径统一成可测试契约。
 */
import fs from "node:fs";
import path from "node:path";
import { safeJsonParse } from "../core/helpers.js";
const WRITE_SIDE_EFFECTS = [
    "file_write",
    "knowledge_write",
    "config_write",
    "command_execution",
    "git_commit",
    "git_push",
    "pr_create",
    "external_write",
];
/** read_only_bypass 工具不得声明任何非 none 的副作用（含 task_context_write） */
const READ_ONLY_FORBIDDEN_EFFECTS = new Set([
    ...WRITE_SIDE_EFFECTS,
    "task_context_write",
]);
export const CONTROL_PLANE_MATRIX = [
    { id: "mcp_availability", title: "MCP 可用性", invariant: "MCP 服务、工具注册、依赖、PATH 和版本必须可用且可诊断。", must_fail: "工具未注册或服务不可启动仍允许进入实现。", recovery: "运行 soloforge doctor 并按诊断修复 MCP 配置或版本。" },
    { id: "tool_callability", title: "工具可调用性", invariant: "工具 schema 必须能被适配器合法调用，参数不能形成永远不可达的死链路。", must_fail: "工具需要 task_id 但入口无法产生 task_id。", recovery: "修复 schema 或入口链路，先 sf_classify 再进入受控工具。" },
    { id: "contract_mislabel", title: "契约错标", invariant: "会写状态、文件、知识或外部系统的工具不得标成 read_only_bypass。", must_fail: "read_only_bypass 声明非 none 副作用。", recovery: "重标为 normal_controlled 或 strict_controlled 并补授权。" },
    { id: "dead_chain", title: "死链路", invariant: "工具前置条件必须能由上游工具或显式授权满足。", must_fail: "strict 工具既要求 workflow 又无 task_id 路径。", recovery: "补 task_id、confirm 或专用恢复 action。" },
    { id: "bypass", title: "旁路", invariant: "Bash/Edit/Write/脚本不得直接改 .soloforge/state 或绕过受控工具改业务代码后自证。", must_fail: "Bash 写 .soloforge/state。", recovery: "使用 sf_status archive_stale 或相应 MCP 工具恢复。" },
    { id: "provenance", title: "来源归因", invariant: "代码变更必须能说明来自哪个任务、工具链和工作包。", must_fail: "非 MCP 计划内变更被当成完整验证通过。", recovery: "重新分类并建立 task_id，或把验证标记为 limited。" },
    { id: "state_trust", title: "状态可信", invariant: "current-task、任务终态和状态文件必须可校验，陈旧指针不得阻塞正常任务。", must_fail: "陈旧 current-task 无合法恢复动作。", recovery: "sf_status action=archive_stale confirm=true。" },
    { id: "evidence_truth", title: "证据真实性", invariant: "验证计划不得冒充验证结果，命令必须有真实执行证据。", must_fail: "validation_plan 被标记为 passed。", recovery: "执行真实命令并记录 command_execution 证据。" },
    { id: "scope_completeness", title: "范围完整性", invariant: "changed_files 必须覆盖实际改动，验证不得只看子集。", must_fail: "未验证文件仍允许 deliver。", recovery: "补齐 changed_files 或声明 limited_verification 并阻断交付。" },
    { id: "invisible_degradation", title: "降级可见", invariant: "工具失败、跳过或降级必须用户可见，不得静默 advisory 化。", must_fail: "degraded trace 仍 delivered 且无说明。", recovery: "输出降级原因、影响范围和恢复命令。" },
    { id: "fake_delivery_closure", title: "交付假闭环", invariant: "交付必须闭合设计、验证、本地验收、用户确认和 PR 前检查的适用条件。", must_fail: "仅 build/test 通过即 delivered。", recovery: "补齐适用验收或进入 manual_required。" },
    { id: "adapter_drift", title: "适配器漂移", invariant: "Claude/Codex/Trae 的 hook、prompt、MCP 配置必须共享同一控制面边界。", must_fail: "某适配器缺少 check-bash 或 MCP 配置。", recovery: "重新 soloforge init 或 sync-adapters。" },
    { id: "version_drift", title: "版本漂移", invariant: "用户项目调用的全局 soloforge 必须与发布能力兼容。", must_fail: "repo 已修复但全局命令仍旧版。", recovery: "npm update -g soloforge 后运行 soloforge doctor。" },
    { id: "concurrency_race", title: "并发竞态", invariant: "多会话写状态和代码必须有租约或冲突检测。", must_fail: "两个会话同时覆盖 current-task。", recovery: "检查 active lease，必要时人工确认归档。" },
    { id: "recovery_path", title: "恢复路径", invariant: "每个 hard_fail 都必须给合法恢复动作，不得引导手动清理状态。", must_fail: "错误只提示手动删除 .soloforge/state。", recovery: "返回 MCP/CLI 恢复命令和确认条件。" },
];
export const CONTROL_PLANE_BRANCH_DETAILS = [
    { id: "mcp-process-health", category: "mcp_availability", detail: "MCP 进程启动失败必须可诊断。", gate: "CONTROL_PLANE_MATRIX" },
    { id: "tool-registry-complete", category: "mcp_availability", detail: "注册工具必须与契约一一对应。", gate: "CONTROL_PLANE_TOOL_CONTRACT" },
    { id: "path-and-bin", category: "mcp_availability", detail: "PATH 与 bin 指向错误必须暴露版本漂移。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "dependency-load", category: "mcp_availability", detail: "依赖缺失不得降级成普通建议。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "schema-required-fields", category: "tool_callability", detail: "schema required 字段必须可由上游满足。", gate: "CONTROL_PLANE_SCHEMA_REACHABILITY" },
    { id: "sdk-compatible-shape", category: "tool_callability", detail: "工具参数结构必须兼容 MCP SDK。", gate: "CONTROL_PLANE_SCHEMA_REACHABILITY" },
    { id: "confirm-field", category: "tool_callability", detail: "需要确认的工具必须有 confirm 或 authorized。", gate: "CONTROL_PLANE_TOOL_CONTRACT" },
    { id: "read-only-side-effect", category: "contract_mislabel", detail: "read_only_bypass 不得声明写副作用。", gate: "CONTROL_PLANE_TOOL_CONTRACT" },
    { id: "state-write-label", category: "contract_mislabel", detail: "写 task_context 的工具不得伪装只读。", gate: "CONTROL_PLANE_TOOL_CONTRACT" },
    { id: "workflow-taskid", category: "dead_chain", detail: "requires_workflow 工具必须有 task_id 路径。", gate: "CONTROL_PLANE_SCHEMA_REACHABILITY" },
    { id: "strict-standalone-auth", category: "dead_chain", detail: "独立 strict 工具必须显式授权。", gate: "CONTROL_PLANE_TOOL_CONTRACT" },
    { id: "bash-state-write", category: "bypass", detail: "Bash 不得写 .soloforge/state。", gate: "CONTROL_PLANE_BYPASS_GUARD" },
    { id: "bash-implementation-write", category: "bypass", detail: "Bash 不得绕过 sf_classify/sf_expand 直接写业务实现文件。", gate: "CONTROL_PLANE_BYPASS_GUARD" },
    { id: "edit-state-write", category: "bypass", detail: "Edit/Write 不得直接篡改状态。", gate: "CONTROL_PLANE_BYPASS_GUARD" },
    { id: "business-code-without-task", category: "provenance", detail: "业务代码变更缺少任务来源只能 limited。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "tool-trace-taskid", category: "provenance", detail: "写入 trace 必须带 task_id 或显式授权。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "stale-current-task", category: "state_trust", detail: "陈旧 current-task 必须可受控归档。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "terminal-task-blocking", category: "state_trust", detail: "终态任务不得继续阻塞新任务。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "manual-state-tamper", category: "state_trust", detail: "手工改状态必须降级可信度。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "plan-vs-result", category: "evidence_truth", detail: "计划证据不得等同执行结果。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "command-execution-proof", category: "evidence_truth", detail: "测试通过必须有真实命令执行证据。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "changed-files-subset", category: "scope_completeness", detail: "未覆盖 changed_files 的验证不得交付。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "git-diff-crosscheck", category: "scope_completeness", detail: "验证范围需可与实际变更交叉检查。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "degraded-user-visible", category: "invisible_degradation", detail: "degraded 必须进入用户可见输出。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "advisory-not-pass", category: "invisible_degradation", detail: "advisory 不得伪装 enforced pass。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "local-acceptance", category: "fake_delivery_closure", detail: "适用场景必须有本地验收闭环。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "user-confirmation", category: "fake_delivery_closure", detail: "需要用户确认的交付不得自动完成。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "claude-hook", category: "adapter_drift", detail: "Claude Code hook 必须包含状态旁路拦截。", gate: "CONTROL_PLANE_ADAPTER_DRIFT" },
    { id: "codex-hook", category: "adapter_drift", detail: "Codex hook 必须包含状态旁路拦截。", gate: "CONTROL_PLANE_ADAPTER_DRIFT" },
    { id: "trae-config", category: "adapter_drift", detail: "Trae 配置不得绕过 MCP 主链路。", gate: "CONTROL_PLANE_ADAPTER_DRIFT" },
    { id: "global-version", category: "version_drift", detail: "全局安装旧版必须提示升级。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "package-dist-version", category: "version_drift", detail: "dist 与 package 版本能力必须一致。", gate: "CONTROL_PLANE_DOCTOR" },
    { id: "workspace-lease", category: "concurrency_race", detail: "写操作必须尊重工作区租约。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "parallel-session", category: "concurrency_race", detail: "并发会话冲突必须人工确认。", gate: "CONTROLLED_GENERATION_CHAIN" },
    { id: "legal-recovery-command", category: "recovery_path", detail: "每类阻断必须提供合法恢复命令。", gate: "CONTROL_PLANE_MATRIX" },
    { id: "no-manual-rm-state", category: "recovery_path", detail: "恢复建议不得要求手动 rm 状态文件。", gate: "CONTROL_PLANE_BYPASS_GUARD" },
];
export function isSoloforgeStateBypassCommand(commandText) {
    const command = commandText.replace(/\s+/g, " ").trim();
    if (!/\.soloforge[/\\]state(?:[/\\]|\b)/.test(command))
        return false;
    const writeCommandPattern = /(^|[\s;&|()])(?:rm|mv|cp|mkdir|touch|tee|chmod|chown|python3?|node|ruby|perl)\b/i;
    const inPlaceEditPattern = /(^|[\s;&|()])(?:sed|gsed)\s+[^;&|]*\s-i\b/i;
    const redirectionPattern = /(^|[^<])>>?\s*(?:"[^"]*\.soloforge[/\\]state|'[^']*\.soloforge[/\\]state|[^\s;&|]*\.soloforge[/\\]state)/i;
    return writeCommandPattern.test(command) || inPlaceEditPattern.test(command) || redirectionPattern.test(command);
}
export function validateControlPlaneToolContracts(contracts) {
    const findings = [];
    const names = new Set();
    for (const contract of contracts) {
        if (names.has(contract.tool_name)) {
            findings.push({
                severity: "hard_fail",
                category: "mcp_availability",
                code: "CONTROL_PLANE_DUPLICATE_TOOL_CONTRACT",
                message: `工具契约重复: ${contract.tool_name}`,
                recovery: "删除重复契约并保持工具注册一一对应。",
            });
        }
        names.add(contract.tool_name);
        const hasWrite = contract.side_effects.some((effect) => READ_ONLY_FORBIDDEN_EFFECTS.has(effect));
        if (contract.category === "read_only_bypass" && hasWrite) {
            findings.push({
                severity: "hard_fail",
                category: "contract_mislabel",
                code: "CONTROL_PLANE_READ_ONLY_HAS_SIDE_EFFECT",
                message: `${contract.tool_name} 标记为 read_only_bypass 但声明写副作用: ${contract.side_effects.join(", ")}`,
                recovery: "把工具重标为 normal_controlled/strict_controlled，或移除写副作用。",
            });
        }
        if (contract.category === "strict_controlled" && !contract.requires_workflow && !contract.requires_authorization) {
            findings.push({
                severity: "hard_fail",
                category: "dead_chain",
                code: "CONTROL_PLANE_STRICT_WITHOUT_AUTH_OR_WORKFLOW",
                message: `${contract.tool_name} 是 strict_controlled，但既无 workflow 约束也无显式授权。`,
                recovery: "补 requires_workflow 或 requires_authorization，并在 schema 中暴露 task_id/confirm。",
            });
        }
        if (hasWrite && contract.requires_authorization && !/确认|授权|confirm|authorized/i.test(contract.description)) {
            findings.push({
                severity: "warning",
                category: "tool_callability",
                code: "CONTROL_PLANE_AUTH_DESCRIPTION_WEAK",
                message: `${contract.tool_name} 需要授权，但描述未说明确认语义。`,
                recovery: "在工具说明和 schema 中明确 confirm/authorized 的含义。",
            });
        }
    }
    return findings;
}
export function assessControlledGenerationChain(input) {
    const findings = [];
    const toolNames = new Set(input.traces.map((trace) => trace.tool_name));
    const changed = new Set(input.changed_files);
    const verified = new Set(input.verified_files);
    if (input.changed_files.length > 0 && (!toolNames.has("sf_classify") || !toolNames.has("sf_expand"))) {
        findings.push({
            severity: "hard_fail",
            category: "provenance",
            code: "CONTROLLED_GENERATION_MISSING_MCP_PROVENANCE",
            message: "存在代码变更，但缺少 sf_classify/sf_expand 受控生成来源。",
            recovery: "重新通过 SoloForge 分类和膨胀任务，或把后续验证标记为 limited 并阻断交付。",
        });
    }
    const unverified = [...changed].filter((file) => !verified.has(file));
    if (unverified.length > 0) {
        findings.push({
            severity: "hard_fail",
            category: "scope_completeness",
            code: "CONTROLLED_GENERATION_CHANGED_FILE_UNVERIFIED",
            message: `存在未验证变更文件: ${unverified.join(", ")}`,
            recovery: "把所有实际变更加入 changed_files 并重新 sf_verify。",
        });
    }
    const fakeEvidence = input.traces.find((trace) => trace.tool_name === "sf_deliver" && trace.evidence_kind === "plan" && trace.status === "passed");
    if (fakeEvidence) {
        findings.push({
            severity: "hard_fail",
            category: "evidence_truth",
            code: "CONTROLLED_GENERATION_PLAN_AS_RESULT",
            message: "sf_verify 把验证计划当成验证结果。",
            recovery: "执行真实测试命令并记录 command_execution 证据。",
        });
    }
    const degraded = input.traces.find((trace) => trace.status === "degraded");
    if (degraded && input.delivery_status === "delivered") {
        findings.push({
            severity: "hard_fail",
            category: "invisible_degradation",
            code: "CONTROLLED_GENERATION_DEGRADED_DELIVERED",
            message: "存在 degraded 工具 trace，但交付仍标记 delivered。",
            recovery: "向用户暴露降级原因，恢复后重新验证，或进入 manual_required。",
        });
    }
    const hasHardFail = findings.some((finding) => finding.severity === "hard_fail");
    return {
        accepted: !hasHardFail,
        confidence: hasHardFail ? "blocked" : "full",
        findings,
    };
}
export function auditControlPlaneProject(projectPath) {
    const findings = [];
    const mcpPath = path.join(projectPath, ".mcp.json");
    const claudeSettings = path.join(projectPath, ".claude", "settings.json");
    const codexHooks = path.join(projectPath, ".codex", "hooks.json");
    if (!fs.existsSync(mcpPath)) {
        findings.push({
            severity: "warning",
            category: "mcp_availability",
            code: "CONTROL_PLANE_MCP_CONFIG_MISSING",
            message: ".mcp.json 不存在，AI 可能无法看到 SoloForge MCP 工具。",
            recovery: "运行 soloforge init 或 soloforge sync-adapters。",
        });
    }
    const claudeText = fs.existsSync(claudeSettings) ? fs.readFileSync(claudeSettings, "utf-8") : "";
    if (claudeText && !claudeText.includes("check-bash")) {
        findings.push({
            severity: "hard_fail",
            category: "adapter_drift",
            code: "CONTROL_PLANE_CLAUDE_HOOK_MISSING_BYPASS_GUARD",
            message: "Claude Code hooks 未包含 check-bash，可能绕过状态保护。",
            recovery: "运行 soloforge generate-hooks 或 soloforge init。",
        });
    }
    const codexText = fs.existsSync(codexHooks) ? fs.readFileSync(codexHooks, "utf-8") : "";
    if (codexText && !codexText.includes("check-bash")) {
        findings.push({
            severity: "hard_fail",
            category: "adapter_drift",
            code: "CONTROL_PLANE_CODEX_HOOK_MISSING_BYPASS_GUARD",
            message: "Codex hooks 未包含 check-bash，可能绕过状态保护。",
            recovery: "运行 soloforge generate-codex。",
        });
    }
    const stateDir = path.join(projectPath, ".soloforge", "state");
    const currentPath = path.join(stateDir, "current-task.json");
    if (fs.existsSync(currentPath)) {
        try {
            const current = safeJsonParse(fs.readFileSync(currentPath, "utf-8"));
            const taskId = current?.task_id ?? current?.current_task_id;
            if (taskId && !fs.existsSync(path.join(stateDir, `${taskId}.json`))) {
                findings.push({
                    severity: "warning",
                    category: "state_trust",
                    code: "CONTROL_PLANE_STALE_CURRENT_TASK",
                    message: `current-task 指向不存在任务: ${taskId}`,
                    recovery: "运行 sf_status action=archive_stale confirm=true，或 soloforge cleanup --stale。",
                });
            }
        }
        catch {
            findings.push({
                severity: "hard_fail",
                category: "state_trust",
                code: "CONTROL_PLANE_STATE_JSON_INVALID",
                message: "current-task.json 不是合法 JSON。",
                recovery: "使用 sf_status 受控恢复，不要手动删除状态目录。",
            });
        }
    }
    return {
        passed: findings.every((finding) => finding.severity !== "hard_fail"),
        matrix_count: CONTROL_PLANE_MATRIX.length,
        branch_detail_count: CONTROL_PLANE_BRANCH_DETAILS.length,
        findings,
    };
}
//# sourceMappingURL=control_plane_contract.js.map