import type { EvidenceReadResult } from '../evidence/readEvidence'; import { readEvidenceResult } from '../evidence/readEvidence'; import { captureRepoState } from '../evidence/repoState'; import type { RepoState } from '../evidence/schema'; import { extractEvidenceBlockingCauses, type EvidenceBlockingCause, formatEvidenceBlockingCause, } from '../evidence/blockingCauses'; import { resolvePolicyForStage } from './stagePolicies'; import { execFileSync } from 'node:child_process'; import { existsSync, realpathSync } from 'node:fs'; import { resolve } from 'node:path'; import type { SkillsLockV1, SkillsStage } from '../config/skillsLock'; import { loadEffectiveSkillsLock, loadRequiredSkillsLock, } from '../config/skillsEffectiveLock'; import { resolveSkillsEnforcement, type SkillsEnforcementResolution, } from '../policy/skillsEnforcement'; import { readMcpAiGateReceipt, resolveMcpAiGateReceiptPath, type McpAiGateReceiptReadResult, } from '../mcp/aiGateReceipt'; export type AiGateStage = 'PRE_WRITE' | 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; export type AiGateViolation = { code: string; message: string; severity: 'ERROR' | 'WARN'; ruleId?: string; file?: string; line?: number; lines?: readonly number[]; remediation?: string; }; type PreWriteWorktreeHygienePolicy = { enabled: boolean; warnThreshold: number; blockThreshold: number; }; export type AiGateSkillsContractPlatformRequirement = { platform: PreWriteSkillsPlatform; required_rule_prefix: string; required_bundles: ReadonlyArray; required_critical_rule_ids: ReadonlyArray; required_any_transversal_critical_rule_ids: ReadonlyArray; active_prefix_covered: boolean; evaluated_prefix_covered: boolean; missing_bundles: ReadonlyArray; missing_critical_rule_ids: ReadonlyArray; transversal_critical_covered: boolean; missing_any_transversal_critical_rule_ids: ReadonlyArray; }; export type AiGateSkillsContractAssessment = { stage: AiGateStage; enforced: boolean; status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE'; detected_platforms: ReadonlyArray; requirements: ReadonlyArray; violations: ReadonlyArray; }; export type AiGateCheckResult = { stage: AiGateStage; status: 'ALLOWED' | 'BLOCKED'; allowed: boolean; policy: { stage: AiGateStage; resolved_stage: SkillsStage; block_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL'; warn_on_or_above: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL'; trace: { source: 'default' | 'skills.policy' | 'hard-mode'; bundle: string; hash: string; }; }; evidence: { kind: EvidenceReadResult['kind']; max_age_seconds: number; age_seconds: number | null; source: { source: string; path: string; digest: string | null; generated_at: string | null; }; }; mcp_receipt: { required: boolean; kind: 'disabled' | McpAiGateReceiptReadResult['kind']; path: string; max_age_seconds: number | null; age_seconds: number | null; }; skills_contract: AiGateSkillsContractAssessment; repo_state: RepoState; violations: AiGateViolation[]; }; type AiGateDependencies = { now: () => number; readEvidenceResult: (repoRoot: string) => EvidenceReadResult; readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult; captureRepoState: (repoRoot: string) => RepoState; resolvePolicyForStage: (stage: SkillsStage, repoRoot: string) => ReturnType; loadEffectiveSkillsLock: (repoRoot: string) => SkillsLockV1 | undefined; loadRequiredSkillsLock: (repoRoot: string) => SkillsLockV1 | undefined; }; const defaultDependencies: AiGateDependencies = { now: () => Date.now(), readEvidenceResult, readMcpAiGateReceipt, captureRepoState, resolvePolicyForStage, loadEffectiveSkillsLock, loadRequiredSkillsLock, }; const DEFAULT_MAX_AGE_SECONDS: Readonly> = { PRE_WRITE: 300, PRE_COMMIT: 900, PRE_PUSH: 1800, CI: 7200, }; const DEFAULT_PREWRITE_WORKTREE_HYGIENE: PreWriteWorktreeHygienePolicy = { enabled: true, warnThreshold: 12, blockThreshold: 24, }; const PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV = 'PUMUKI_PREWRITE_WORKTREE_HYGIENE_ENABLED'; const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_WARN_THRESHOLD'; const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD'; const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']); const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const; type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number]; const PLATFORM_SKILLS_RULE_PREFIXES: Readonly> = { ios: 'skills.ios.', android: 'skills.android.', backend: 'skills.backend.', frontend: 'skills.frontend.', }; const PLATFORM_REQUIRED_SKILLS_BUNDLES: Readonly>> = { ios: [ 'ios-guidelines', 'ios-concurrency-guidelines', 'ios-swiftui-expert-guidelines', ], android: ['android-guidelines'], backend: ['backend-guidelines'], frontend: ['frontend-guidelines'], }; const PREWRITE_CRITICAL_SKILLS_RULES: Readonly>> = { ios: ['skills.ios.critical-test-quality'], android: [], backend: [], frontend: [], }; const PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES: Readonly>> = { ios: [], android: ['skills.android.no-runblocking', 'skills.android.no-thread-sleep'], backend: ['skills.backend.no-empty-catch', 'skills.backend.avoid-explicit-any'], frontend: ['skills.frontend.no-empty-catch', 'skills.frontend.avoid-explicit-any'], }; const PREWRITE_PLATFORM_REPO_TREE_PREFIXES: Readonly>> = { ios: ['apps/ios/', 'ios/'], android: ['apps/android/', 'android/'], backend: ['apps/backend/'], frontend: ['apps/frontend/', 'apps/web/'], }; const MCP_RECEIPT_STAGE_ORDER: Readonly> = { PRE_WRITE: 0, PRE_COMMIT: 1, PRE_PUSH: 2, CI: 3, }; const SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES = new Set([ 'EVIDENCE_MISSING', 'EVIDENCE_INVALID', 'EVIDENCE_CHAIN_INVALID', 'EVIDENCE_TIMESTAMP_INVALID', 'EVIDENCE_STALE', ]); const toErrorViolation = (code: string, message: string): AiGateViolation => ({ code, severity: 'ERROR', message, }); const toWarnViolation = (code: string, message: string): AiGateViolation => ({ code, severity: 'WARN', message, }); const normalizeEvidenceCauseLines = ( lines: EvidenceBlockingCause['lines'] ): Pick => { if (typeof lines === 'number' && Number.isFinite(lines) && lines > 0) { return { line: Math.floor(lines) }; } if (Array.isArray(lines)) { const normalized = Array.from( new Set( lines .filter((line): line is number => typeof line === 'number' && Number.isFinite(line) && line > 0) .map((line) => Math.floor(line)) ) ).sort((left, right) => left - right); return normalized.length > 0 ? { lines: normalized } : {}; } if (typeof lines === 'string') { const normalized = Array.from( new Set( lines .split(',') .map((line) => Number.parseInt(line.trim(), 10)) .filter((line) => Number.isFinite(line) && line > 0) ) ).sort((left, right) => left - right); return normalized.length > 0 ? { lines: normalized } : {}; } return {}; }; const toEvidenceGateBlockedViolation = ( severity: AiGateViolation['severity'], message: string, cause?: EvidenceBlockingCause ): AiGateViolation => ({ code: 'EVIDENCE_GATE_BLOCKED', severity, message, ruleId: cause?.ruleId, file: cause?.file, remediation: cause?.remediation, ...normalizeEvidenceCauseLines(cause?.lines), }); const toSkillsViolation = ( resolution: SkillsEnforcementResolution, code: string, message: string ): AiGateViolation => ( resolution.blocking ? toErrorViolation(code, message) : toWarnViolation(code, message) ); const toCriticalSkillsViolation = (code: string, message: string): AiGateViolation => toErrorViolation(code, message); const buildEvidenceGateBlockedMessage = (result: Extract): string => { const causes = extractEvidenceBlockingCauses(result.evidence); if (causes.length === 0) { return 'Evidence AI gate status is BLOCKED. No blocking causes were found in evidence; rerun PRE_WRITE audit to regenerate actionable evidence.'; } const formattedCauses = causes .slice(0, 8) .map((cause, index) => `${index + 1}) ${formatEvidenceBlockingCause(cause)}`); const suffix = causes.length > formattedCauses.length ? ` ... and ${causes.length - formattedCauses.length} more blocking cause(s).` : ''; return `Evidence AI gate status is BLOCKED. Blocking causes: ${formattedCauses.join(' || ')}${suffix}`; }; const hasEvidenceBlockingCause = (result: Extract): boolean => extractEvidenceBlockingCauses(result.evidence).length > 0; const normalizeRepoStateLifecycleVersions = (repoState: RepoState): RepoState => { const packageVersion = repoState.lifecycle.package_version; const lifecycleVersion = repoState.lifecycle.lifecycle_version; if (packageVersion === lifecycleVersion) { return repoState; } const canonicalVersion = packageVersion ?? lifecycleVersion ?? null; return { ...repoState, lifecycle: { ...repoState.lifecycle, package_version: canonicalVersion, lifecycle_version: canonicalVersion, }, }; }; const toPositiveInteger = (value: unknown, fallback: number): number => { if (typeof value !== 'number' || !Number.isFinite(value)) { return fallback; } const normalized = Math.trunc(value); return normalized > 0 ? normalized : fallback; }; const toBooleanFromEnv = (value: string | undefined, fallback: boolean): boolean => { if (typeof value !== 'string') { return fallback; } const normalized = value.trim().toLowerCase(); if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') { return true; } if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') { return false; } return fallback; }; const resolvePreWriteWorktreeHygienePolicy = ( input?: Partial ): PreWriteWorktreeHygienePolicy => { const enabled = input?.enabled ?? toBooleanFromEnv( process.env[PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV], DEFAULT_PREWRITE_WORKTREE_HYGIENE.enabled ); const warnThreshold = toPositiveInteger( input?.warnThreshold ?? (process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV] ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV]) : undefined), DEFAULT_PREWRITE_WORKTREE_HYGIENE.warnThreshold ); const requestedBlockThreshold = toPositiveInteger( input?.blockThreshold ?? (process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV] ? Number(process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV]) : undefined), DEFAULT_PREWRITE_WORKTREE_HYGIENE.blockThreshold ); const blockThreshold = requestedBlockThreshold >= warnThreshold ? requestedBlockThreshold : warnThreshold; return { enabled, warnThreshold, blockThreshold, }; }; const toTimestampAgeSeconds = ( timestamp: string, nowMs: number ): number | null => { const parsed = Date.parse(timestamp); if (!Number.isFinite(parsed)) { return null; } const raw = Math.floor((nowMs - parsed) / 1000); return raw >= 0 ? raw : 0; }; const isTimestampFuture = (timestamp: string, nowMs: number): boolean => { const parsed = Date.parse(timestamp); if (!Number.isFinite(parsed)) { return false; } return parsed > nowMs; }; const toCanonicalPath = (value: string): string => { const normalized = value.replace(/\\/g, '/').toLowerCase(); try { return realpathSync(value).replace(/\\/g, '/').toLowerCase(); } catch { return normalized; } }; const toNormalizedSkillsBundleName = (bundle: string): string => { const separatorIndex = bundle.lastIndexOf('@'); if (separatorIndex <= 0) { return bundle.trim(); } return bundle.slice(0, separatorIndex).trim(); }; const toDetectedSkillsPlatforms = ( platforms: Extract['evidence']['platforms'] | undefined ): ReadonlyArray => { const platformsState = platforms ?? {}; const detected: PreWriteSkillsPlatform[] = []; for (const platform of PREWRITE_SKILLS_PLATFORMS) { if (platformsState[platform]?.detected === true) { detected.push(platform); } } return detected; }; const toCoverageInferredPlatforms = ( coverage: NonNullable['evidence']['snapshot']['rules_coverage']> | undefined ): ReadonlyArray => { if (!coverage) { return []; } const ruleIds = [...coverage.active_rule_ids, ...coverage.evaluated_rule_ids]; const inferred = new Set(); for (const ruleId of ruleIds) { for (const platform of PREWRITE_SKILLS_PLATFORMS) { const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform]; if (ruleId.startsWith(prefix)) { inferred.add(platform); } } } return PREWRITE_SKILLS_PLATFORMS.filter((platform) => inferred.has(platform)); }; const toRepoTreeDetectedPlatforms = (params: { repoRoot: string; platforms: ReadonlyArray; }): ReadonlyArray => { return params.platforms.filter((platform) => { const prefixes = PREWRITE_PLATFORM_REPO_TREE_PREFIXES[platform] ?? []; return prefixes.some((prefix) => existsSync(resolve(params.repoRoot, prefix))); }); }; const normalizeChangedPath = (value: string): string => value.replace(/\\/g, '/').replace(/^"+|"+$/g, '').trim(); const parseChangedPath = (line: string): string | null => { if (line.length < 4) { return null; } const raw = line.slice(3).trim(); if (raw.length === 0) { return null; } if (raw.includes(' -> ')) { const renamed = raw.split(' -> ').pop(); if (!renamed) { return null; } const normalizedRenamed = normalizeChangedPath(renamed); return normalizedRenamed.length > 0 ? normalizedRenamed : null; } const normalized = normalizeChangedPath(raw); return normalized.length > 0 ? normalized : null; }; const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray => { try { const output = execFileSync( 'git', ['status', '--short', '--untracked-files=all'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], } ); const files = output .split('\n') .map((line) => parseChangedPath(line)) .filter((line): line is string => typeof line === 'string' && line.length > 0); return [...new Set(files)]; } catch { return []; } }; const collectStagedChangedPaths = (repoRoot: string): ReadonlyArray => { try { const output = execFileSync( 'git', ['diff', '--cached', '--name-only'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], } ); const files = output .split('\n') .map((line) => normalizeChangedPath(line)) .filter((line) => line.length > 0); return [...new Set(files)]; } catch { return []; } }; const collectPreWriteEffectiveChangedPaths = (repoRoot: string): ReadonlyArray => { const stagedPaths = collectStagedChangedPaths(repoRoot); if (stagedPaths.length > 0) { return stagedPaths; } return collectWorktreeChangedPaths(repoRoot); }; const isIosTestQualityPath = (filePath: string): boolean => { const normalized = normalizeChangedPath(filePath).toLowerCase(); if (!normalized.endsWith('.swift')) { return false; } return normalized.includes('/tests/') || normalized.includes('/uitests/') || normalized.endsWith('test.swift') || normalized.endsWith('tests.swift') || normalized.endsWith('.spec.swift'); }; const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean => collectPreWriteEffectiveChangedPaths(repoRoot).some((filePath) => isIosTestQualityPath(filePath) ); const resolvePreWriteCriticalSkillsRules = (params: { platform: PreWriteSkillsPlatform; stage: AiGateStage; repoRoot: string; }): ReadonlyArray => { const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform]; if ( params.platform === 'ios' && params.stage === 'PRE_WRITE' && ruleIds.includes('skills.ios.critical-test-quality') && !hasPreWriteIosTestQualityScope(params.repoRoot) ) { return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality'); } return ruleIds; }; const hasRuleCoverage = ( coverage: NonNullable['evidence']['snapshot']['rules_coverage']>, ruleId: string ): boolean => coverage.active_rule_ids.includes(ruleId) && coverage.evaluated_rule_ids.includes(ruleId); const hasEquivalentCriticalRuleCoverage = ( coverage: NonNullable['evidence']['snapshot']['rules_coverage']>, ruleId: string ): boolean => { if (ruleId !== 'skills.ios.critical-test-quality') { return false; } return hasRuleCoverage(coverage, 'skills.ios.prefer-swift-testing'); }; const hasRequiredCriticalRuleCoverage = ( coverage: NonNullable['evidence']['snapshot']['rules_coverage']>, ruleId: string ): boolean => hasRuleCoverage(coverage, ruleId) || hasEquivalentCriticalRuleCoverage(coverage, ruleId); const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => { const normalized = normalizeChangedPath(filePath).toLowerCase(); if (platform === 'ios') { return normalized.endsWith('.swift') || normalized.endsWith('.m') || normalized.endsWith('.mm') || normalized.endsWith('.h') || normalized.endsWith('.xib') || normalized.endsWith('.storyboard') || normalized.endsWith('.xcstrings') || normalized.endsWith('.plist'); } if (platform === 'android') { return normalized.endsWith('.kt') || normalized.endsWith('.kts') || normalized.endsWith('.java') || normalized.endsWith('.xml') || normalized.endsWith('.gradle') || normalized.endsWith('.gradle.kts'); } if (platform === 'backend') { const isTypeScriptOrJavaScript = normalized.endsWith('.ts') || normalized.endsWith('.js') || normalized.endsWith('.mts') || normalized.endsWith('.cts') || normalized.endsWith('.mjs') || normalized.endsWith('.cjs'); if (!isTypeScriptOrJavaScript) { return false; } return normalized.startsWith('apps/backend/') || /(^|\/)(backend|server|api)(\/|$)/.test(normalized); } const isReactExtension = normalized.endsWith('.tsx') || normalized.endsWith('.jsx'); if (isReactExtension) { return true; } const isTypeScriptOrJavaScript = normalized.endsWith('.ts') || normalized.endsWith('.js') || normalized.endsWith('.mts') || normalized.endsWith('.cts') || normalized.endsWith('.mjs') || normalized.endsWith('.cjs'); if (!isTypeScriptOrJavaScript) { return false; } return normalized.startsWith('apps/frontend/') || normalized.startsWith('apps/web/') || /(^|\/)(frontend|web|client)(\/|$)/.test(normalized); }; const hasWorktreeCodePlatforms = (params: { repoRoot: string; requiredPlatforms: ReadonlyArray; }): boolean => { const changedPaths = collectWorktreeChangedPaths(params.repoRoot); if (changedPaths.length === 0) { return false; } return changedPaths.some((filePath) => params.requiredPlatforms.some((platform) => isPlatformPath(platform, filePath)) ); }; const hasEffectiveChangedCodePlatforms = (params: { changedPaths: ReadonlyArray; requiredPlatforms: ReadonlyArray; }): boolean => params.changedPaths.some((filePath) => params.requiredPlatforms.some((platform) => isPlatformPath(platform, filePath)) ); const toChangedPathDetectedPlatforms = (params: { changedPaths: ReadonlyArray; requiredPlatforms: ReadonlyArray; }): ReadonlyArray => { if (params.changedPaths.length === 0 || params.requiredPlatforms.length === 0) { return []; } return params.requiredPlatforms.filter((platform) => params.changedPaths.some((filePath) => isPlatformPath(platform, filePath)) ); }; const toLockRequiredPlatforms = ( requiredLock: SkillsLockV1 | undefined ): ReadonlyArray => { if (!requiredLock) { return []; } const requiredBundles = new Set( requiredLock.bundles.map((bundle) => toNormalizedSkillsBundleName(bundle.name)) ); const required = new Set(); for (const platform of PREWRITE_SKILLS_PLATFORMS) { const requiredByBundle = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform].some((bundleName) => requiredBundles.has(bundleName) ); const requiredByRules = requiredLock.bundles.some((bundle) => bundle.rules.some( (rule) => PREWRITE_SKILLS_PLATFORMS.includes(rule.platform as PreWriteSkillsPlatform) && rule.platform === platform ) ); if (requiredByBundle || requiredByRules) { required.add(platform); } } return PREWRITE_SKILLS_PLATFORMS.filter((platform) => required.has(platform)); }; const toEffectiveSkillsPlatforms = (params: { platforms: Extract['evidence']['platforms'] | undefined; coverage: NonNullable['evidence']['snapshot']['rules_coverage']> | undefined; }): ReadonlyArray => { const detectedPlatforms = toDetectedSkillsPlatforms(params.platforms); const inferredFromCoverage = toCoverageInferredPlatforms(params.coverage); if (detectedPlatforms.length === 0) { return inferredFromCoverage; } if (inferredFromCoverage.length === 0) { return detectedPlatforms; } const inferredSet = new Set(inferredFromCoverage); const intersection = detectedPlatforms.filter((platform) => inferredSet.has(platform)); if (intersection.length > 0) { return intersection; } return detectedPlatforms; }; const toPreWriteDetectedSkillsPlatforms = (params: { platforms: Extract['evidence']['platforms'] | undefined; coverage: NonNullable['evidence']['snapshot']['rules_coverage']> | undefined; }): ReadonlyArray => { const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.platforms); if (explicitlyDetectedPlatforms.length === 0) { return []; } return toEffectiveSkillsPlatforms(params); }; const collectActiveRuleIdsCoverageViolations = (params: { stage: AiGateStage; evidence: Extract['evidence']; coverage: NonNullable['evidence']['snapshot']['rules_coverage']>; }): AiGateViolation[] => { if (params.coverage.active_rule_ids.length > 0) { return []; } const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms); const effectivePlatforms = toEffectiveSkillsPlatforms({ platforms: params.evidence.platforms, coverage: params.coverage, }); const inferredPlatforms = toCoverageInferredPlatforms(params.coverage); const blockedPlatforms = effectivePlatforms.length > 0 ? effectivePlatforms : inferredPlatforms; if (blockedPlatforms.length === 0) { return []; } const detectionMode = explicitlyDetectedPlatforms.length > 0 ? 'detected' : 'inferred'; return [ toErrorViolation( 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES', `Active rules coverage is empty at ${params.stage} with ${detectionMode} code platforms=[${blockedPlatforms.join(', ')}].` ), ]; }; const collectPreWritePlatformSkillsViolations = (params: { repoRoot: string; evidence: Extract['evidence']; coverage: NonNullable['evidence']['snapshot']['rules_coverage']>; skillsEnforcement: SkillsEnforcementResolution; }): AiGateViolation[] => { const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({ platforms: params.evidence.platforms, coverage: params.coverage, }); if (detectedPlatforms.length === 0) { return []; } const effectiveChangedPaths = collectPreWriteEffectiveChangedPaths(params.repoRoot); const isNoCodeEffectiveScope = effectiveChangedPaths.length > 0 && !hasEffectiveChangedCodePlatforms({ changedPaths: effectiveChangedPaths, requiredPlatforms: detectedPlatforms, }); if (isNoCodeEffectiveScope) { return []; } const violations: AiGateViolation[] = []; const missingScopeCoverage: string[] = []; const missingBundlesByPlatform: string[] = []; for (const platform of detectedPlatforms) { const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform]; const hasActivePrefix = params.coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(prefix)); const hasEvaluatedPrefix = params.coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(prefix) ); if (!hasActivePrefix || !hasEvaluatedPrefix) { const reasons: string[] = []; if (!hasActivePrefix) { reasons.push(`active_rules_prefix=${prefix} missing`); } if (!hasEvaluatedPrefix) { reasons.push(`evaluated_rules_prefix=${prefix} missing`); } missingScopeCoverage.push(`${platform}{${reasons.join('; ')}}`); } } if (missingScopeCoverage.length > 0) { violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE', `Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.` ) ); } const activeSkillsBundles = new Set( (params.evidence.rulesets ?? []) .filter((ruleset) => ruleset.platform === 'skills') .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle)) .filter((bundle) => bundle.length > 0) ); for (const platform of detectedPlatforms) { const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]; const missingBundles = requiredBundles.filter((bundleName) => !activeSkillsBundles.has(bundleName)); if (missingBundles.length === 0) { continue; } missingBundlesByPlatform.push(`${platform}{missing_bundles=[${missingBundles.join(', ')}]}`); } if (missingBundlesByPlatform.length > 0) { violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING', `Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.` ) ); } const missingCriticalRulesByPlatform: string[] = []; for (const platform of detectedPlatforms) { const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({ platform, stage: 'PRE_WRITE', repoRoot: params.repoRoot, }); if (requiredCriticalRuleIds.length === 0) { continue; } const missingCriticalRuleIds = requiredCriticalRuleIds.filter((ruleId) => { return !hasRequiredCriticalRuleCoverage(params.coverage, ruleId); }); if (missingCriticalRuleIds.length === 0) { continue; } missingCriticalRulesByPlatform.push( `${platform}{missing_critical_rule_ids=[${missingCriticalRuleIds.join(', ')}]}` ); } if (missingCriticalRulesByPlatform.length > 0) { violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING', `Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.` ) ); } return violations; }; const collectPreWriteCrossPlatformCriticalViolations = (params: { evidence: Extract['evidence']; coverage: NonNullable['evidence']['snapshot']['rules_coverage']>; skillsEnforcement: SkillsEnforcementResolution; }): AiGateViolation[] => { const detectedPlatforms = toPreWriteDetectedSkillsPlatforms({ platforms: params.evidence.platforms, coverage: params.coverage, }); if (detectedPlatforms.length === 0) { return []; } const missingCriticalCoverage: string[] = []; for (const platform of detectedPlatforms) { const requiredRuleIds = PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform]; if (requiredRuleIds.length === 0) { continue; } const hasCoverage = requiredRuleIds.some((ruleId) => params.coverage.active_rule_ids.includes(ruleId) && params.coverage.evaluated_rule_ids.includes(ruleId) ); if (hasCoverage) { continue; } missingCriticalCoverage.push( `${platform}{required_any=[${requiredRuleIds.join(', ')}]}` ); } if (missingCriticalCoverage.length === 0) { return []; } return []; }; const toSkillsContractAssessment = (params: { stage: AiGateStage; repoRoot: string; repoState: RepoState; evidenceResult: EvidenceReadResult; requiredLock?: SkillsLockV1; skillsEnforcement: SkillsEnforcementResolution; }): AiGateSkillsContractAssessment => { const requiredPlatforms = toLockRequiredPlatforms(params.requiredLock); const effectiveChangedPaths = collectPreWriteEffectiveChangedPaths(params.repoRoot); const hasEffectiveChangedPaths = effectiveChangedPaths.length > 0; const hasEffectiveCodePlatforms = hasEffectiveChangedCodePlatforms({ changedPaths: effectiveChangedPaths, requiredPlatforms, }); const isNoCodeEffectiveScope = requiredPlatforms.length > 0 && hasEffectiveChangedPaths && !hasEffectiveCodePlatforms; if (isNoCodeEffectiveScope) { return { stage: params.stage, enforced: false, status: 'NOT_APPLICABLE', detected_platforms: [], requirements: [], violations: [], }; } if (params.evidenceResult.kind !== 'valid') { return { stage: params.stage, enforced: requiredPlatforms.length > 0, status: requiredPlatforms.length > 0 ? 'FAIL' : 'NOT_APPLICABLE', detected_platforms: [], requirements: requiredPlatforms.map((platform) => ({ platform, required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform], required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]], required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({ platform, stage: params.stage, repoRoot: params.repoRoot, }), required_any_transversal_critical_rule_ids: [ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform], ], active_prefix_covered: false, evaluated_prefix_covered: false, missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]], missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({ platform, stage: params.stage, repoRoot: params.repoRoot, }), transversal_critical_covered: false, missing_any_transversal_critical_rule_ids: [ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform], ], })), violations: requiredPlatforms.length > 0 ? [ toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED', `Required repo skills exist, but active platforms could not be detected for ${params.stage}.` ), ] : [], }; } const coverage = params.evidenceResult.evidence.snapshot.rules_coverage; const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms); const inferredPlatforms = toCoverageInferredPlatforms(coverage); const changedPathDetectedPlatforms = toChangedPathDetectedPlatforms({ changedPaths: effectiveChangedPaths, requiredPlatforms, }); const repoTreeDetectedPlatforms = params.stage !== 'PRE_WRITE' && requiredPlatforms.length > 0 && !hasEffectiveChangedPaths ? toRepoTreeDetectedPlatforms({ repoRoot: params.repoRoot, platforms: requiredPlatforms, }) : []; const explicitlyDetectedEffectivePlatforms = explicitlyDetectedPlatforms.length > 0 ? toEffectiveSkillsPlatforms({ platforms: params.evidenceResult.evidence.platforms, coverage, }) : []; const detectedPlatforms = explicitlyDetectedEffectivePlatforms.length > 0 ? explicitlyDetectedEffectivePlatforms : inferredPlatforms.length > 0 ? inferredPlatforms : changedPathDetectedPlatforms.length > 0 ? changedPathDetectedPlatforms : repoTreeDetectedPlatforms; const pendingChanges = resolvePendingChanges(params.repoState); const detectedPlatformSet = new Set(detectedPlatforms); const assessmentPlatforms = requiredPlatforms.length > 0 ? params.stage === 'PRE_WRITE' && detectedPlatforms.length > 0 ? requiredPlatforms.filter((platform) => detectedPlatformSet.has(platform)) : requiredPlatforms : detectedPlatforms; if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) { if ( ( pendingChanges === 0 || !hasEffectiveCodePlatforms || ( params.stage === 'PRE_WRITE' && !hasWorktreeCodePlatforms({ repoRoot: params.repoRoot, requiredPlatforms, }) ) ) ) { return { stage: params.stage, enforced: false, status: 'NOT_APPLICABLE', detected_platforms: [], requirements: [], violations: [], }; } const requirements: AiGateSkillsContractPlatformRequirement[] = requiredPlatforms.map((platform) => ({ platform, required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform], required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]], required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({ platform, stage: params.stage, repoRoot: params.repoRoot, }), required_any_transversal_critical_rule_ids: [ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform], ], active_prefix_covered: false, evaluated_prefix_covered: false, missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]], missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({ platform, stage: params.stage, repoRoot: params.repoRoot, }), transversal_critical_covered: false, missing_any_transversal_critical_rule_ids: [ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform], ], })); return { stage: params.stage, enforced: true, status: 'FAIL', detected_platforms: [], requirements, violations: [ toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED', `Required repo skills exist, but active platforms could not be detected for ${params.stage}.` ), ], }; } if (assessmentPlatforms.length === 0) { return { stage: params.stage, enforced: false, status: 'NOT_APPLICABLE', detected_platforms: [], requirements: [], violations: [], }; } const activeSkillsBundles = new Set( (params.evidenceResult.evidence.rulesets ?? []) .filter((ruleset) => ruleset.platform === 'skills') .map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle)) .filter((bundle) => bundle.length > 0) ); const requirements: AiGateSkillsContractPlatformRequirement[] = []; const violations: AiGateViolation[] = []; if (requiredPlatforms.length > 0 && detectedPlatforms.length === 0) { violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_SKILLS_PLATFORMS_UNDETECTED', `Required repo skills exist, but active platforms could not be detected for ${params.stage}.` ) ); } for (const platform of assessmentPlatforms) { const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform]; const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]]; const requiredCriticalRuleIds = [ ...resolvePreWriteCriticalSkillsRules({ platform, stage: params.stage, repoRoot: params.repoRoot, }), ]; const requiredAnyTransversalCriticalRuleIds = [ ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform], ]; const activePrefixCovered = coverage ? coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix)) : false; const evaluatedPrefixCovered = coverage ? coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix)) : false; const missingBundles = requiredBundles.filter( (bundleName) => !activeSkillsBundles.has(bundleName) ); const missingCriticalRuleIds = coverage ? requiredCriticalRuleIds.filter((ruleId) => { return !hasRequiredCriticalRuleCoverage(coverage, ruleId); }) : [...requiredCriticalRuleIds]; const transversalCriticalCovered = requiredAnyTransversalCriticalRuleIds.length === 0 ? true : coverage ? requiredAnyTransversalCriticalRuleIds.some((ruleId) => coverage.active_rule_ids.includes(ruleId) && coverage.evaluated_rule_ids.includes(ruleId) ) : false; const missingAnyTransversalCriticalRuleIds = transversalCriticalCovered ? [] : [...requiredAnyTransversalCriticalRuleIds]; requirements.push({ platform, required_rule_prefix: requiredRulePrefix, required_bundles: requiredBundles, required_critical_rule_ids: requiredCriticalRuleIds, required_any_transversal_critical_rule_ids: requiredAnyTransversalCriticalRuleIds, active_prefix_covered: activePrefixCovered, evaluated_prefix_covered: evaluatedPrefixCovered, missing_bundles: missingBundles, missing_critical_rule_ids: missingCriticalRuleIds, transversal_critical_covered: transversalCriticalCovered, missing_any_transversal_critical_rule_ids: missingAnyTransversalCriticalRuleIds, }); if (!activePrefixCovered || !evaluatedPrefixCovered) { const missingParts: string[] = []; if (!activePrefixCovered) { missingParts.push('active_prefix'); } if (!evaluatedPrefixCovered) { missingParts.push('evaluated_prefix'); } violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE', `Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).` ) ); } if (missingBundles.length > 0) { violations.push( toSkillsViolation( params.skillsEnforcement, 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING', `Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].` ) ); } if (missingCriticalRuleIds.length > 0) { violations.push( toCriticalSkillsViolation( 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING', `Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].` ) ); } } return { stage: params.stage, enforced: true, status: violations.length === 0 ? 'PASS' : 'FAIL', detected_platforms: detectedPlatforms, requirements, violations, }; }; const collectPreWriteCoherenceViolations = (params: { evidence: Extract['evidence']; repoRoot: string; repoState: RepoState; nowMs: number; preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy; }): AiGateViolation[] => { const violations: AiGateViolation[] = []; const evidenceRepoRoot = params.evidence.repo_state?.repo_root; if ( typeof evidenceRepoRoot === 'string' && evidenceRepoRoot.trim().length > 0 && toCanonicalPath(evidenceRepoRoot) !== toCanonicalPath(params.repoRoot) ) { violations.push( toErrorViolation( 'EVIDENCE_REPO_ROOT_MISMATCH', `Evidence repo root mismatch (${evidenceRepoRoot} != ${params.repoRoot}).` ) ); } const evidenceBranch = params.evidence.repo_state?.git?.branch; const currentBranch = params.repoState.git.branch; if ( typeof evidenceBranch === 'string' && evidenceBranch.trim().length > 0 && typeof currentBranch === 'string' && currentBranch.trim().length > 0 && evidenceBranch !== currentBranch ) { violations.push( toErrorViolation( 'EVIDENCE_BRANCH_MISMATCH', `Evidence branch mismatch (${evidenceBranch} != ${currentBranch}).` ) ); } if (params.evidence.severity_metrics.gate_status !== params.evidence.ai_gate.status) { violations.push( toErrorViolation( 'EVIDENCE_GATE_STATUS_INCOHERENT', `Evidence gate status mismatch (severity_metrics=${params.evidence.severity_metrics.gate_status} ai_gate=${params.evidence.ai_gate.status}).` ) ); } if (params.evidence.ai_gate.status === 'BLOCKED' && params.evidence.snapshot.outcome !== 'BLOCK') { violations.push( toErrorViolation( 'EVIDENCE_OUTCOME_INCOHERENT', `Evidence outcome mismatch (ai_gate=BLOCKED snapshot.outcome=${params.evidence.snapshot.outcome}).` ) ); } const coverage = params.evidence.snapshot.rules_coverage; if (!coverage) { violations.push( toErrorViolation( 'EVIDENCE_RULES_COVERAGE_MISSING', 'Evidence rules_coverage is missing for PRE_WRITE validation.' ) ); } else { const skillsEnforcement = resolveSkillsEnforcement(); if (coverage.stage !== params.evidence.snapshot.stage) { violations.push( toErrorViolation( 'EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH', `rules_coverage stage mismatch (${coverage.stage} != ${params.evidence.snapshot.stage}).` ) ); } if (coverage.counts.unevaluated > 0 || coverage.coverage_ratio < 1) { violations.push( toErrorViolation( 'EVIDENCE_RULES_COVERAGE_INCOMPLETE', `rules_coverage incomplete (unevaluated=${coverage.counts.unevaluated}, ratio=${coverage.coverage_ratio}).` ) ); } const unsupportedAutoCount = coverage.counts.unsupported_auto ?? coverage.unsupported_auto_rule_ids?.length ?? 0; if (unsupportedAutoCount > 0) { violations.push( toErrorViolation( 'EVIDENCE_UNSUPPORTED_AUTO_RULES', `rules_coverage has unsupported auto rules (${unsupportedAutoCount}).` ) ); } violations.push( ...collectActiveRuleIdsCoverageViolations({ stage: 'PRE_WRITE', evidence: params.evidence, coverage, }) ); violations.push( ...collectPreWritePlatformSkillsViolations({ repoRoot: params.repoRoot, evidence: params.evidence, coverage, skillsEnforcement, }) ); violations.push( ...collectPreWriteCrossPlatformCriticalViolations({ evidence: params.evidence, coverage, skillsEnforcement, }) ); } if (isTimestampFuture(params.evidence.timestamp, params.nowMs)) { violations.push( toErrorViolation( 'EVIDENCE_TIMESTAMP_FUTURE', 'Evidence timestamp is in the future.' ) ); } return violations; }; const collectEvidenceViolations = ( result: EvidenceReadResult, repoRoot: string, repoState: RepoState, stage: AiGateStage, nowMs: number, maxAgeSecondsByStage: Readonly>, preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy ): { violations: AiGateViolation[]; ageSeconds: number | null } => { const violations: AiGateViolation[] = []; const maxAgeSeconds = maxAgeSecondsByStage[stage]; if (result.kind === 'missing') { violations.push(toErrorViolation('EVIDENCE_MISSING', '.ai_evidence.json is missing.')); return { violations, ageSeconds: null }; } if (result.kind === 'invalid') { const invalidCode = result.reason === 'evidence-chain-invalid' ? 'EVIDENCE_CHAIN_INVALID' : 'EVIDENCE_INVALID'; const detailSuffix = result.detail ? ` ${result.detail}` : ''; violations.push( toErrorViolation( invalidCode, `.ai_evidence.json is invalid${result.version ? ` (version=${result.version})` : ''}.${detailSuffix}` ) ); return { violations, ageSeconds: null }; } const ageSeconds = toTimestampAgeSeconds(result.evidence.timestamp, nowMs); if (ageSeconds === null) { violations.push(toErrorViolation('EVIDENCE_TIMESTAMP_INVALID', 'Evidence timestamp is invalid.')); return { violations, ageSeconds: null }; } if (ageSeconds > maxAgeSeconds) { violations.push( toErrorViolation( 'EVIDENCE_STALE', `Evidence is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${stage}).` ) ); } if (result.evidence.ai_gate.status === 'BLOCKED') { const blockingCause = extractEvidenceBlockingCauses(result.evidence)[0]; const gateBlockedMessage = buildEvidenceGateBlockedMessage(result); violations.push( !hasEvidenceBlockingCause(result) && ageSeconds <= maxAgeSeconds ? toWarnViolation( 'EVIDENCE_GATE_BLOCKED', `${gateBlockedMessage} Advisory because the evidence snapshot did not expose any blocking cause.` ) : isAdvisoryDocumentationRenderSlice(repoRoot, ageSeconds, maxAgeSeconds) ? toWarnViolation( 'EVIDENCE_GATE_BLOCKED', `${gateBlockedMessage} Advisory because the current slice only contains documentation/render/tooling artifacts and evidence is fresh.` ) : toEvidenceGateBlockedViolation('ERROR', gateBlockedMessage, blockingCause) ); } if (stage === 'PRE_WRITE') { violations.push( ...collectPreWriteCoherenceViolations({ evidence: result.evidence, repoRoot, repoState, nowMs, preWriteWorktreeHygiene, }) ); } appendWorktreeHygieneViolations(violations, repoState, preWriteWorktreeHygiene, stage); return { violations, ageSeconds }; }; const SUPPORTED_FUNCTIONAL_EXTENSIONS = new Set([ '.swift', '.ts', '.tsx', '.js', '.jsx', '.kt', '.kts', ]); const DOCUMENTATION_RENDER_TOOLING_EXTENSIONS = new Set([ '.css', '.html', '.json', '.md', '.mdx', '.txt', ]); const normalizeGitPath = (value: string): string => value.replace(/\\/g, '/').replace(/^"|"$/g, ''); const extractGitStatusPath = (line: string): string | null => { const payload = line.slice(3).trim(); if (payload.length === 0) { return null; } const renameSeparator = ' -> '; if (payload.includes(renameSeparator)) { return normalizeGitPath(payload.slice(payload.lastIndexOf(renameSeparator) + renameSeparator.length)); } return normalizeGitPath(payload); }; const collectGitStatusPaths = (repoRoot: string): readonly string[] => { if (!existsSync(resolve(repoRoot, '.git'))) { return []; } try { return execFileSync('git', ['status', '--porcelain', '--untracked-files=all'], { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], }) .split(/\r?\n/) .map(extractGitStatusPath) .filter((path): path is string => path !== null); } catch { return []; } }; const pathExtension = (path: string): string => { const basename = path.split('/').pop() ?? ''; const dotIndex = basename.lastIndexOf('.'); return dotIndex >= 0 ? basename.slice(dotIndex).toLowerCase() : ''; }; const isSupportedFunctionalPath = (path: string): boolean => SUPPORTED_FUNCTIONAL_EXTENSIONS.has(pathExtension(path)); const isDocumentationRenderToolingPath = (path: string): boolean => { const normalized = path.toLowerCase(); if (normalized.startsWith('docs/') && DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized))) { return true; } if (normalized.endsWith('.md') || normalized.endsWith('.mdx')) { return true; } if ( normalized.startsWith('stack-my-architecture-pumuki/dist/') || normalized.startsWith('stack-my-architecture-hub/pumuki/') ) { return DOCUMENTATION_RENDER_TOOLING_EXTENSIONS.has(pathExtension(normalized)); } if (normalized === 'stack-my-architecture-pumuki/scripts/build-html.py') { return true; } if (normalized === 'package.json' || normalized === 'package-lock.json') { return true; } return false; }; const isAdvisoryDocumentationRenderSlice = ( repoRoot: string, ageSeconds: number, maxAgeSeconds: number ): boolean => { if (ageSeconds > maxAgeSeconds) { return false; } const paths = collectGitStatusPaths(repoRoot); if (paths.length === 0) { return false; } return ( paths.every(isDocumentationRenderToolingPath) && !paths.some(isSupportedFunctionalPath) ); }; const toEvidenceSourceDescriptor = ( result: EvidenceReadResult, repoRoot: string ): AiGateCheckResult['evidence']['source'] => { if ('source_descriptor' in result) { return { source: result.source_descriptor.source, path: result.source_descriptor.path, digest: result.source_descriptor.digest, generated_at: result.source_descriptor.generated_at, }; } return { source: 'local-file', path: resolve(repoRoot, '.ai_evidence.json'), digest: null, generated_at: null, }; }; const collectGitflowViolations = ( repoState: RepoState, protectedBranches: ReadonlySet ): AiGateViolation[] => { const violations: AiGateViolation[] = []; if (!repoState.git.available) { return violations; } if (repoState.git.branch && protectedBranches.has(repoState.git.branch)) { violations.push( toErrorViolation( 'GITFLOW_PROTECTED_BRANCH', `Direct work on protected branch "${repoState.git.branch}" is not allowed.` ) ); } return violations; }; const resolvePendingChanges = (repoState: RepoState): number | null => { if (!repoState.git.available) { return null; } return repoState.git.pending_changes ?? (repoState.git.staged + repoState.git.unstaged); }; const appendWorktreeHygieneViolations = ( violations: AiGateViolation[], repoState: RepoState, policy: PreWriteWorktreeHygienePolicy, stageLabel: AiGateStage ): void => { if (!policy.enabled || !repoState.git.available) { return; } const pendingChanges = resolvePendingChanges(repoState) ?? 0; if (pendingChanges >= policy.blockThreshold) { violations.push( toErrorViolation( 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT', `${stageLabel} worktree hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${policy.blockThreshold}). Split worktree into atomic slices.` ) ); } else if (pendingChanges >= policy.warnThreshold) { violations.push( toWarnViolation( 'EVIDENCE_PREWRITE_WORKTREE_WARN', `${stageLabel} worktree hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${policy.warnThreshold}). Consider smaller staged/unstaged batches.` ) ); } }; const toPolicyStage = (stage: AiGateStage): SkillsStage => { return stage; }; const isMcpReceiptStageCompatible = (params: { receiptStage: AiGateStage; requestedStage: AiGateStage; }): boolean => { return MCP_RECEIPT_STAGE_ORDER[params.receiptStage] >= MCP_RECEIPT_STAGE_ORDER[params.requestedStage]; }; const collectMcpReceiptViolations = (params: { required: boolean; stage: AiGateStage; repoRoot: string; nowMs: number; maxAgeSecondsByStage: Readonly>; readMcpAiGateReceipt: (repoRoot: string) => McpAiGateReceiptReadResult; }): { required: boolean; kind: 'disabled' | McpAiGateReceiptReadResult['kind']; path: string; maxAgeSeconds: number | null; ageSeconds: number | null; violations: AiGateViolation[]; } => { const path = resolveMcpAiGateReceiptPath(params.repoRoot); if (!params.required || params.stage !== 'PRE_WRITE') { return { required: params.required, kind: 'disabled', path, maxAgeSeconds: null, ageSeconds: null, violations: [], }; } const maxAgeSeconds = params.maxAgeSecondsByStage[params.stage]; const receiptRead = params.readMcpAiGateReceipt(params.repoRoot); if (receiptRead.kind === 'missing') { return { required: true, kind: 'missing', path: receiptRead.path, maxAgeSeconds, ageSeconds: null, violations: [ toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_MISSING', 'MCP receipt is missing. Call tool ai_gate_check in pumuki-enterprise MCP before PRE_WRITE.' ), ], }; } if (receiptRead.kind === 'invalid') { return { required: true, kind: 'invalid', path: receiptRead.path, maxAgeSeconds, ageSeconds: null, violations: [ toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_INVALID', `MCP receipt is invalid: ${receiptRead.reason}` ), ], }; } const violations: AiGateViolation[] = []; if (toCanonicalPath(receiptRead.receipt.repo_root) !== toCanonicalPath(params.repoRoot)) { violations.push( toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_REPO_ROOT_MISMATCH', `MCP receipt repo root mismatch (${receiptRead.receipt.repo_root} != ${params.repoRoot}).` ) ); } if ( !isMcpReceiptStageCompatible({ receiptStage: receiptRead.receipt.stage, requestedStage: params.stage, }) ) { violations.push( toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_STAGE_MISMATCH', `MCP receipt stage mismatch (${receiptRead.receipt.stage} incompatible with ${params.stage}).` ) ); } const ageSeconds = toTimestampAgeSeconds(receiptRead.receipt.issued_at, params.nowMs); if (ageSeconds === null) { violations.push( toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_INVALID', 'MCP receipt issued_at timestamp is invalid.' ) ); } else if (ageSeconds > maxAgeSeconds) { violations.push( toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_STALE', `MCP receipt is stale (${ageSeconds}s > ${maxAgeSeconds}s for ${params.stage}).` ) ); } if (isTimestampFuture(receiptRead.receipt.issued_at, params.nowMs)) { violations.push( toErrorViolation( 'MCP_ENTERPRISE_RECEIPT_TIMESTAMP_FUTURE', 'MCP receipt timestamp is in the future.' ) ); } return { required: true, kind: 'valid', path: receiptRead.path, maxAgeSeconds, ageSeconds, violations, }; }; export const evaluateAiGate = ( params: { repoRoot: string; stage: AiGateStage; maxAgeSecondsByStage?: Readonly>; protectedBranches?: ReadonlyArray; requireMcpReceipt?: boolean; preWriteWorktreeHygiene?: Partial; }, dependencies: Partial = {} ): AiGateCheckResult => { const activeDependencies: AiGateDependencies = { ...defaultDependencies, ...dependencies, }; const maxAgeSecondsByStage = params.maxAgeSecondsByStage ?? DEFAULT_MAX_AGE_SECONDS; const preWriteWorktreeHygiene = resolvePreWriteWorktreeHygienePolicy( params.preWriteWorktreeHygiene ); const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES)); const nowMs = activeDependencies.now(); const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot); const repoState = normalizeRepoStateLifecycleVersions( activeDependencies.captureRepoState(params.repoRoot) ); const effectiveSkillsLock = activeDependencies.loadEffectiveSkillsLock(params.repoRoot); const requiredSkillsLock = activeDependencies.loadRequiredSkillsLock(params.repoRoot); const requiredSkillsPlatforms = toLockRequiredPlatforms(requiredSkillsLock); const policyStage = toPolicyStage(params.stage); const resolvedPolicy = activeDependencies.resolvePolicyForStage( policyStage, params.repoRoot ); const skillsEnforcement = resolveSkillsEnforcement(); const evidenceAssessment = collectEvidenceViolations( evidenceResult, params.repoRoot, repoState, params.stage, nowMs, maxAgeSecondsByStage, preWriteWorktreeHygiene ); const mcpReceiptAssessment = collectMcpReceiptViolations({ required: params.requireMcpReceipt ?? false, stage: params.stage, repoRoot: params.repoRoot, nowMs, maxAgeSecondsByStage, readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt, }); const gitflowViolations = collectGitflowViolations(repoState, protectedBranches); const skillsContract = toSkillsContractAssessment({ stage: params.stage, repoRoot: params.repoRoot, repoState, evidenceResult, requiredLock: requiredSkillsLock, skillsEnforcement, }); const suppressSkillsContractViolation = evidenceAssessment.violations.some((violation) => SKILLS_CONTRACT_SUPPRESSED_EVIDENCE_CODES.has(violation.code) ); const stageSkillsContractViolations = suppressSkillsContractViolation || skillsContract.status !== 'FAIL' || (params.stage === 'PRE_WRITE' && requiredSkillsPlatforms.length === 0) || skillsContract.violations.some((violation) => violation.code === 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING' ) ? [] : [ toSkillsViolation( skillsEnforcement, 'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE', `Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.` ), ]; const violations = [ ...evidenceAssessment.violations, ...stageSkillsContractViolations, ...gitflowViolations, ...mcpReceiptAssessment.violations, ]; const blocked = violations.some((violation) => violation.severity === 'ERROR'); return { stage: params.stage, status: blocked ? 'BLOCKED' : 'ALLOWED', allowed: !blocked, policy: { stage: params.stage, resolved_stage: policyStage, block_on_or_above: resolvedPolicy.policy.blockOnOrAbove, warn_on_or_above: resolvedPolicy.policy.warnOnOrAbove, trace: resolvedPolicy.trace, }, evidence: { kind: evidenceResult.kind, max_age_seconds: maxAgeSecondsByStage[params.stage], age_seconds: evidenceAssessment.ageSeconds, source: toEvidenceSourceDescriptor(evidenceResult, params.repoRoot), }, mcp_receipt: { required: mcpReceiptAssessment.required, kind: mcpReceiptAssessment.kind, path: mcpReceiptAssessment.path, max_age_seconds: mcpReceiptAssessment.maxAgeSeconds, age_seconds: mcpReceiptAssessment.ageSeconds, }, skills_contract: skillsContract, repo_state: repoState, violations, }; };