/** * 旧门禁模块 —— 阶段进入检查和证据校验。 * * 保留原因:inspectGate / canEnterPhase 同步门禁检查, * 用于 /kd check 和阶段推进的快速预检。 * * V2 替代:异步策略校验由 V2 GateRunner + PolicyRegistry 处理。 * 已移除的旧规则(旗舰路径、TDD 红绿)现由 V2 对应策略接管。 */ import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import type { ActiveRun, GateResult, KdPhase } from "./types.ts"; import { PHASE_ARTIFACTS, PHASE_ORDER, nextPhaseForRun, phaseOrderForRun } from "./types.ts"; import { artifactExists, readArtifact } from "./artifacts.ts"; import { isKnownProduct } from "../product/profile.ts"; import { executionStepsBlockReason, planStepsBlockReason } from "./plan-steps.ts"; import { SDK_SIGNATURE_EVIDENCE, hasValidSdkSignatureEvidence, requiresSdkSignatureEvidence } from "./sdk-policy.ts"; import { COSMIC_METADATA_EVIDENCE, dataSourceContextBlockReason, dataSourceEvidenceForRun, hasValidDataSourceEvidence, requiresDataSourceEvidence, } from "./data-source-policy.ts"; import { runRoot, runArtifactPath } from "./paths.ts"; import { EVIDENCE_INDEX, readEvidenceIndex } from "./evidence.ts"; import { missingArtifactsReason, missingEvidenceReason, missingForTargetReason, missingMarkerReason, openQuestionsReason, repairBlockedReason, unknownProductReason, unknownRiskReason, } from "./messages.ts"; const REQUIRED_MARKERS: Partial> = { plan: ["## 验证命令"], verify: ["## 证据"], ship: ["## 验证证据"], }; const COSMIC_CONFIG_EVIDENCE = "evidence/cosmic-config.txt"; const COSMIC_API_EVIDENCE = "evidence/cosmic-api.txt"; const KSQL_LINT_EVIDENCE = "evidence/ksql-lint.txt"; const VERIFY_PASS_EVIDENCE = "evidence/verify-pass.md"; type GateMode = "inspect" | "enter"; export function inspectGate(cwd: string, run: ActiveRun): GateResult { const currentProblems = collectGateProblems(cwd, run, run.phase, "inspect"); const nextPhase = nextPhaseForRun(run); if (!nextPhase) return gateResult(currentProblems); const lookaheadProblems = collectLookaheadProblems(cwd, run, nextPhase); return gateResult([...currentProblems, ...lookaheadProblems]); } export function canEnterPhase(cwd: string, run: ActiveRun, target: KdPhase): GateResult { return gateResult(collectGateProblems(cwd, run, target, "enter")); } /** * Collect lookahead problems that would block advancement to the next phase. * These are checks that canEnterPhase runs but inspectGate historically did not, * causing "/kd check passed" but "/kd phase blocked" discrepancies. * * Only includes problems that are actionable in the current phase — * i.e. things the user can fix NOW before attempting to advance. */ function collectLookaheadProblems(cwd: string, run: ActiveRun, nextPhase: KdPhase): string[] { const nextLifecycleIndex = PHASE_ORDER.indexOf(nextPhase); const problems: string[] = []; // Product confirmation required for execute+ if (nextLifecycleIndex >= PHASE_ORDER.indexOf("execute") && !isKnownProduct(run.profile?.product ?? run.product)) { const declaration = productImplementationDeclaration(cwd, run); if (declaration !== false) problems.push(unknownProductReason(declaration)); } // Required facts contract (implementation, integration, data-source) if (nextLifecycleIndex >= PHASE_ORDER.indexOf("execute")) { const dsProblem = dataSourceContextBlockReason(cwd, run); if (dsProblem) problems.push(dsProblem); } // Missing evidence required for entering the next phase const missingEvidence = missingEvidenceForPhase(cwd, run, nextPhase); if (missingEvidence.length > 0) { problems.push(missingForTargetReason(nextPhase, missingEvidence)); } return problems; } function collectGateProblems(cwd: string, run: ActiveRun, phase: KdPhase, mode: GateMode): string[] { if (run.mode === "quick") { return []; } const phaseOrder = phaseOrderForRun(run); const phaseIndex = phaseOrder.indexOf(phase); const lifecycleIndex = PHASE_ORDER.indexOf(phase); const missing: string[] = []; const reasonParts: string[] = []; if (phaseIndex < 0) { return [`阶段 ${phase} 不属于当前 Harness 模式 ${run.mode}。可选阶段:${phaseOrder.join(", ")}`]; } if (lifecycleIndex >= PHASE_ORDER.indexOf("execute") && !isKnownProduct(run.profile?.product ?? run.product)) { const declaration = productImplementationDeclaration(cwd, run); if (declaration !== false) reasonParts.push(unknownProductReason(declaration)); } if (lifecycleIndex >= PHASE_ORDER.indexOf("ship") && phaseOrder.includes("ship") && !hasRiskAssessment(cwd, run)) { reasonParts.push(unknownRiskReason()); } const lastRequiredArtifactIndex = mode === "inspect" ? phaseIndex : phaseIndex - 1; for (let i = 0; i <= lastRequiredArtifactIndex; i++) { const requiredPhase = phaseOrder[i]; const artifact = PHASE_ARTIFACTS[requiredPhase]; if (!artifactExists(cwd, run, artifact)) missing.push(artifact); } if (mode === "enter" && phase === "execute" && !artifactExists(cwd, run, PHASE_ARTIFACTS.plan)) { missing.push(PHASE_ARTIFACTS.plan); } const markerProblem = mode === "inspect" ? inspectMarkers(cwd, run, phase) : undefined; const dataSourceContextProblem = lifecycleIndex >= PHASE_ORDER.indexOf("execute") ? dataSourceContextBlockReason(cwd, run) : undefined; const stepProblem = inspectStepState(cwd, run, phase); const evidenceProblem = mode === "inspect" ? inspectEvidence(cwd, run, phase) : undefined; const questionProblem = inspectOpenQuestions(run); const repairProblem = inspectRepairState(run); if (dataSourceContextProblem) { reasonParts.push(dataSourceContextProblem); } if (markerProblem) { reasonParts.push(markerProblem); } if (stepProblem) { reasonParts.push(stepProblem); } if (evidenceProblem) { reasonParts.push(evidenceProblem); } if (questionProblem) { reasonParts.push(questionProblem); } if (repairProblem) { reasonParts.push(repairProblem); } if (mode === "enter") { missing.push(...missingEvidenceForPhase(cwd, run, phase)); if (phase === "ship" && !evidenceArtifactSatisfied(cwd, run, VERIFY_PASS_EVIDENCE)) { missing.push(VERIFY_PASS_EVIDENCE); } if (phase === "ship" && !artifactExists(cwd, run, PHASE_ARTIFACTS.verify)) { missing.push(PHASE_ARTIFACTS.verify); } } if (missing.length > 0) { const uniqueMissing = [...new Set(missing)]; reasonParts.push(mode === "inspect" ? missingArtifactsReason(uniqueMissing) : missingForTargetReason(phase, uniqueMissing)); } return reasonParts; } function gateResult(reasonParts: string[]): GateResult { const reason = reasonParts.length > 0 ? reasonParts.join("; ") : undefined; return { passed: !reason, reason, checkedAt: new Date().toISOString(), }; } function productImplementationDeclaration(cwd: string, run: ActiveRun): boolean | undefined { const plan = readArtifact(cwd, run, "plan") ?? ""; const line = plan .split(/\r?\n/) .map((item) => item.trim()) .find((item) => item.includes("是否涉及产品实现、构建、元数据或 SDK 查证")); if (!line) return undefined; const value = line.replace(/^[-*]\s*/, "").split(/[::]/).slice(1).join(":").trim(); if (/^(是|涉及|需要|yes|true)(\s|。|,|,|;|;|$)/i.test(value)) return true; if (/^(否|不涉及|不需要|无|no|false)(\s|。|,|,|;|;|$)/i.test(value)) return false; return undefined; } function hasRiskAssessment(cwd: string, run: ActiveRun): boolean { const level = run.riskAssessment?.level; if (!level) return false; if (typeof run.riskAssessment?.reason === "string" && run.riskAssessment.reason.trim()) return true; return riskSectionHasContent(readArtifact(cwd, run, "verify") ?? "") || riskSectionHasContent(readArtifact(cwd, run, "ship") ?? ""); } function riskSectionHasContent(content: string): boolean { const match = content.match(/##\s*(残余风险|风险)\s*\r?\n([\s\S]*?)(?=\r?\n##\s+|$)/); if (!match) return false; return match[2] .split(/\r?\n/) .map((line) => line.trim()) .some((line) => line && !/^[-*]?\s*(无|未知|待确认|待补充|none|unknown)$/i.test(line)); } function inspectOpenQuestions(run: ActiveRun): string | undefined { const open = Array.isArray(run.questions) ? run.questions.filter((question) => question.status === "open" && question.blocking) : []; if (open.length === 0) return undefined; return openQuestionsReason(open); } function inspectRepairState(run: ActiveRun): string | undefined { if (run.repair?.status !== "blocked") return undefined; return repairBlockedReason(run.repair.attempts, run.repair.maxAttempts, run.repair.lastFailureEvidence); } function inspectStepState(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined { if (phase === "execute") { const plan = readArtifact(cwd, run, "plan") ?? ""; const problems: string[] = []; const stepReason = planStepsBlockReason(plan); if (stepReason) problems.push(stepReason); const flagshipReason = flagshipPlanBlockReason(cwd, run, plan); if (flagshipReason) problems.push(flagshipReason); if (run.mode === "normal") { const tddReason = tddPlanBlockReason(plan); if (tddReason) problems.push(tddReason); } return problems.length > 0 ? problems.join("; ") : undefined; } if (phase === "verify" || phase === "ship") { return ( executionStepsBlockReason(cwd, run, readArtifact(cwd, run, "plan") ?? "", readArtifact(cwd, run, "execute") ?? "") ?? (run.mode === "normal" ? tddVerifyBlockReason(cwd, run) : undefined) ); } return undefined; } function inspectMarkers(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined { const markers = REQUIRED_MARKERS[phase]; if (!markers?.length) return undefined; const content = readArtifact(cwd, run, phase); if (!content) return undefined; const missing = markers.filter((marker) => !content.includes(marker)); if (missing.length === 0) return undefined; return missingMarkerReason(phase, missing); } function inspectEvidence(cwd: string, run: ActiveRun, phase: KdPhase): string | undefined { const missing = missingEvidenceForPhase(cwd, run, phase); if (PHASE_ORDER.indexOf(phase) >= PHASE_ORDER.indexOf("verify") && !missing.includes(VERIFY_PASS_EVIDENCE) && !evidenceArtifactSatisfied(cwd, run, VERIFY_PASS_EVIDENCE)) { missing.push(VERIFY_PASS_EVIDENCE); } if (missing.length === 0) return undefined; return missingEvidenceReason(missing); } function isCosmicRun(run: ActiveRun): boolean { return run.profile?.platform === "cosmic"; } function missingEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] { return requiredEvidenceForPhase(cwd, run, phase).filter((artifact) => !evidenceArtifactSatisfied(cwd, run, artifact)); } function requiredEvidenceForPhase(cwd: string, run: ActiveRun, phase: KdPhase): string[] { const required = new Set(); const phaseIndex = PHASE_ORDER.indexOf(phase); if (phaseIndex >= PHASE_ORDER.indexOf("execute")) { if (requiresSdkSignatureEvidence(run)) required.add(SDK_SIGNATURE_EVIDENCE); if (requiresDataSourceEvidence(cwd, run)) { const evidence = dataSourceEvidenceForRun(run); if (evidence) required.add(evidence); } if (isCosmicRun(run) && run.mode === "normal") { required.add(COSMIC_CONFIG_EVIDENCE); if (planHasMetadataRequirement(cwd, run)) required.add(COSMIC_METADATA_EVIDENCE); if (planHasApiRequirement(cwd, run)) required.add(COSMIC_API_EVIDENCE); } } if (isCosmicRun(run) && phaseIndex >= PHASE_ORDER.indexOf("ship") && runHasKsqlDelivery(cwd, run)) { required.add(COSMIC_METADATA_EVIDENCE); required.add(KSQL_LINT_EVIDENCE); } return [...required]; } function evidenceArtifactSatisfied(cwd: string, run: ActiveRun, artifact: string): boolean { if (artifact === SDK_SIGNATURE_EVIDENCE) return hasValidSdkSignatureEvidence(cwd, run); if (artifact === dataSourceEvidenceForRun(run)) return hasValidDataSourceEvidence(cwd, run); if (artifact === EVIDENCE_INDEX) return existsSync(join(runRoot(cwd, run), artifact)); return existsSync(join(runRoot(cwd, run), artifact)) && hasSuccessfulEvidenceEntry(cwd, run, artifact); } function hasSuccessfulEvidenceEntry(cwd: string, run: ActiveRun, artifact: string): boolean { const normalized = artifact.replace(/\\/g, "/").replace(/^\/+/, "").replace(/^\.\//, ""); const entry = readEvidenceIndex(cwd, run).entries.find((item) => typeof item.path === "string" && item.path.replace(/\\/g, "/") === normalized); return Boolean(entry && entry.exitCode === 0); } function planHasMetadataRequirement(cwd: string, run: ActiveRun): boolean { const plan = readArtifact(cwd, run, "plan") ?? ""; return /kd_cosmic_metadata|cosmic-metadata|cosmic-metadata\.json|metadata evidence|字段元数据证据|元数据证据/i.test(plan); } // ── 以下部分包含从 V2 移植回来的旗舰路径同步预检与 TDD 同步校验 ─────────────────── function planHasApiRequirement(cwd: string, run: ActiveRun): boolean { const plan = readArtifact(cwd, run, "plan") ?? ""; return /kd_cosmic_api|cosmic-api|cosmic-api\.txt/i.test(plan); } function runHasKsqlDelivery(cwd: string, run: ActiveRun): boolean { const text = ["spec", "plan", "verify"] .map((phase) => readArtifact(cwd, run, phase as KdPhase) ?? "") .join("\n"); return /kd_ksql_lint|kd-ksql|\bksql\b|数据修复|批量更新|字段回填|备份语句|回滚语句/i.test(text); } function flagshipPlanBlockReason(cwd: string, run: ActiveRun | undefined, plan: string): string | undefined { if ((run?.profile?.product ?? run?.product) !== "flagship") return undefined; if (hasWorkspaceCodeDir(cwd)) { if (/(?:^|[\s\`"'(])code[\\/][a-zA-Z0-9_\-\.\/]+/i.test(plan)) return undefined; return "不能进入 execute:星空旗舰版 PLAN.md 必须先记录当前项目 code/ 下的实际目标路径;不要按固定模块规则猜路径。"; } if (planMentionsDiscoveredSourcePath(plan)) return undefined; return "不能进入 execute:PLAN.md 必须先记录已检查当前项目结构,并写明实际源码根或目标文件路径;当前项目没有 code/ 时更不能猜路径。"; } function hasWorkspaceCodeDir(cwd: string): boolean { return existsSync(join(cwd, "code")); } function planMentionsDiscoveredSourcePath(plan: string): boolean { return /(?:^|[\s\`"'(])(?:src[\\/]main[\\/]java|src[\\/]main[\\/]resources|[^`\n]*[\\/](?:src[\\/]main[\\/]java|src[\\/]main[\\/]resources)|[^`\n]*\.(?:java|xml|properties|yml|yaml|sql|ksql|py))(?:[\s`"')]|$)/i.test( plan, ); } function tddPlanBlockReason(plan: string): string | undefined { if (/##\s*TDD\s*\/\s*(?:Red-Green Checks|红绿检查)/i.test(plan)) return undefined; return "PLAN.md 缺少 ## TDD / Red-Green Checks。必须声明红灯验证、绿灯验证和无法自动化时的产品验证替代方案。"; } function tddVerifyBlockReason(cwd: string, run: ActiveRun): string | undefined { const problems: string[] = []; const redResult = validateTddEvidence(cwd, run, "red"); const greenResult = validateTddEvidence(cwd, run, "green"); if (!redResult.valid) problems.push(redResult.reason ?? "evidence/tdd-red.md 校验失败"); if (!greenResult.valid) problems.push(greenResult.reason ?? "evidence/tdd-green.md 校验失败"); if (problems.length === 0) return undefined; return `不能进入 verify:${problems.join(";")}。`; } function validateTddEvidence(cwd: string, run: ActiveRun, kind: "red" | "green"): { valid: boolean; reason?: string } { const evidenceName = kind === "red" ? "evidence/tdd-red.md" : "evidence/tdd-green.md"; const path = runArtifactPath(cwd, run, evidenceName); const content = existsSync(path) ? readFileSync(path, "utf8") : undefined; if (!content) return { valid: false, reason: `缺少 ${evidenceName}` }; // Check for uncertainty const uncertainty = uncertaintyReason(content); if (uncertainty) { return { valid: false, reason: `${evidenceName} 内容无效:${uncertainty}` }; } if (kind === "red") { const hasFailure = /red|fail|failed|failure|error|失败|未通过|Exit\s*[::]\s*[1-9]/i.test(content); return hasFailure ? { valid: true } : { valid: false, reason: `${evidenceName} 未包含失败证据` }; } const hasGreenExit = /Exit\s*[::]\s*0|退出码\s*[::]\s*0/i.test(content); const hasSuccess = /green|pass|passed|success|成功|通过/i.test(content); if (hasGreenExit && hasSuccess) return { valid: true }; return { valid: false, reason: `${evidenceName} 未包含成功证据`, }; } function uncertaintyReason(content: string): string | undefined { const patterns: Array<[RegExp, string]> = [ [/需.*验证|需要.*验证|待.*验证|后续.*验证/i, "包含待验证措辞"], [/未验证|未执行|没有执行|无法执行|无法验证/i, "声明未完成验证"], [/TODO|待补充|待完善|假设|预计|理论上|应该可以/i, "包含占位或推测性结论"], [/编译验证通过[^。\n]*(需|需要|待).*验证/i, "同时声明通过和仍需验证,结论矛盾"], ]; for (const [pattern, reason] of patterns) { if (pattern.test(content)) return reason; } return undefined; }