import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate'; import { resolveRemediationHintForViolationCode } from '../gate/remediationCatalog'; import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures'; import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights'; const PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']); export type EnterpriseAiGateCheckResult = { tool: 'ai_gate_check'; dryRun: true; executed: true; success: boolean; result: { allowed: ReturnType['allowed']; status: ReturnType['status']; timestamp: string | null; branch: string | null; message: string; stage: ReturnType['stage']; policy: ReturnType['policy']; violations: ReturnType['violations']; warnings: ReadonlyArray; auto_fixes: ReadonlyArray; learning_context: SddLearningContext | null; evidence: ReturnType['evidence']; mcp_receipt: ReturnType['mcp_receipt']; skills_contract: ReturnType['skills_contract']; repo_state: ReturnType['repo_state']; consistency_hint?: { comparable_with_hook_runner: boolean; reason_code: 'HOOK_RUNNER_CAN_REFRESH_EVIDENCE' | null; message: string; }; }; }; const HOOK_STAGE_SET = new Set(['PRE_COMMIT', 'PRE_PUSH', 'CI']); const isHookRefreshableEvidenceCode = (code: string): boolean => code.startsWith('EVIDENCE_'); type AiGateCheckDependencies = { evaluateAiGate: typeof evaluateAiGate; }; const defaultDependencies: AiGateCheckDependencies = { evaluateAiGate, }; const buildConsistencyHint = ( evaluation: ReturnType ): EnterpriseAiGateCheckResult['result']['consistency_hint'] => { if (!HOOK_STAGE_SET.has(evaluation.stage)) { return { comparable_with_hook_runner: true, reason_code: null, message: 'Stage is directly comparable with ai_gate_check semantics.', }; } const hasRefreshableEvidenceViolation = evaluation.violations.some((violation) => isHookRefreshableEvidenceCode(violation.code) ); if (!evaluation.allowed && hasRefreshableEvidenceViolation) { return { comparable_with_hook_runner: false, reason_code: 'HOOK_RUNNER_CAN_REFRESH_EVIDENCE', message: 'ai_gate_check is blocking on evidence integrity/freshness. ' + 'Hook stage runners may regenerate evidence before final verdict.', }; } return { comparable_with_hook_runner: true, reason_code: null, message: 'ai_gate_check verdict is directly comparable with hook stage runner output.', }; }; const buildWarnings = (evaluation: ReturnType): ReadonlyArray => { const warnings: string[] = []; const currentBranch = evaluation.repo_state.git.branch; if (typeof currentBranch === 'string' && PROTECTED_BRANCHES.has(currentBranch.toLowerCase())) { warnings.push( `ON_PROTECTED_BRANCH: Estás en '${currentBranch}'. Crea una rama feature/* antes de continuar.` ); } if (evaluation.stage === 'PRE_PUSH' && !evaluation.repo_state.git.upstream) { warnings.push('NO_UPSTREAM: Configura upstream con git push --set-upstream origin .'); } return warnings; }; const buildAutoFixes = ( evaluation: ReturnType, learningContext: SddLearningContext | null ): ReadonlyArray => { const fixes: string[] = []; const emittedCodes = new Set(); for (const violation of evaluation.violations) { if (emittedCodes.has(violation.code)) { continue; } const fix = resolveRemediationHintForViolationCode(violation.code); if (!fix) { continue; } fixes.push(fix); emittedCodes.add(violation.code); } for (const recommendation of learningContext?.recommended_actions ?? []) { if (!fixes.includes(recommendation)) { fixes.push(recommendation); } } return fixes; }; const buildMessage = (evaluation: ReturnType): string => { if (evaluation.allowed) { return `✅ Gate ${evaluation.stage} ALLOWED.`; } const firstViolation = evaluation.violations[0]; if (!firstViolation) { return `🔴 Gate ${evaluation.stage} BLOCKED.`; } return `🔴 ${firstViolation.code}: ${firstViolation.message}`; }; export const runEnterpriseAiGateCheck = (params: { repoRoot: string; stage: AiGateStage; requireMcpReceipt?: boolean; }, dependencies: Partial = {}): EnterpriseAiGateCheckResult => { const activeDependencies: AiGateCheckDependencies = { ...defaultDependencies, ...dependencies, }; const evaluation = activeDependencies.evaluateAiGate({ repoRoot: params.repoRoot, stage: params.stage, requireMcpReceipt: params.requireMcpReceipt ?? false, }); const branch = evaluation.repo_state.git.branch; const timestamp = evaluation.evidence.source.generated_at; const learningContextFeature = resolveLearningContextExperimentalFeature(); const learningContext = learningContextFeature.mode === 'off' ? null : readSddLearningContext({ repoRoot: params.repoRoot, }); const warnings = buildWarnings(evaluation); const autoFixes = buildAutoFixes(evaluation, learningContext); const message = buildMessage(evaluation); return { tool: 'ai_gate_check', dryRun: true, executed: true, success: evaluation.allowed, result: { allowed: evaluation.allowed, status: evaluation.status, timestamp, branch, message, stage: evaluation.stage, policy: evaluation.policy, violations: evaluation.violations, warnings, auto_fixes: autoFixes, learning_context: learningContext, evidence: evaluation.evidence, mcp_receipt: evaluation.mcp_receipt, skills_contract: evaluation.skills_contract, repo_state: evaluation.repo_state, consistency_hint: buildConsistencyHint(evaluation), }, }; };