import { evaluateGate } from '../../core/gate/evaluateGate'; import type { Fact } from '../../core/facts/Fact'; import type { Finding } from '../../core/gate/Finding'; import type { GateOutcome } from '../../core/gate/GateOutcome'; import type { GatePolicy } from '../../core/gate/GatePolicy'; import type { RuleSet } from '../../core/rules/RuleSet'; import type { SkillsRuleSetLoadResult } from '../config/skillsRuleSet'; import type { ResolvedStagePolicy } from '../gate/stagePolicies'; import type { DetectedPlatforms } from '../platform/detectPlatforms'; import { GitService, type IGitService } from './GitService'; import { EvidenceService, type IEvidenceService } from './EvidenceService'; import { resolveActiveGateWaiver, type GateWaiverResult, } from './gateWaiver'; import { evaluateAstIntelligenceDualValidation, evaluatePlatformGateFindings, type AstIntelligenceDualValidationResult, } from './runPlatformGateEvaluation'; import { countScannedFilesFromFacts, resolveFactsForGateScope, type GateScope, } from './runPlatformGateFacts'; import { evaluateBrownfieldHotspotFindings } from './brownfieldHotspots'; import { emitPlatformGateEvidence } from './runPlatformGateEvidence'; import { printGateFindings } from './runPlatformGateOutput'; import { evaluateSddPolicy, type SddDecision } from '../sdd'; import type { SnapshotEvaluationMetrics } from '../evidence/schema'; import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics'; import { createEmptySnapshotRulesCoverage } from '../evidence/rulesCoverage'; import { enforceTddBddPolicy } from '../tdd/enforcement'; import type { TddBddSnapshot } from '../tdd/types'; import { applyTddBddEnforcement } from '../policy/tddBddEnforcement'; import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings'; import { filterFactsByPathPrefixes, resolveGateScopePathPrefixesFromEnv, } from './filterFactsByPathPrefixes'; import { readPreWriteLeaseStatus, toPreWriteEnforcementGapFinding, } from '../lifecycle/preWriteLease'; import { evaluateContextGate, type ContextGateResult } from '../context/contextGate'; import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification'; export type OperationalMemoryShadowRecommendation = { recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK'; confidence: number; reasonCodes: ReadonlyArray; }; export type GateServices = { git: IGitService; evidence: IEvidenceService; }; export type GateDependencies = { evaluateGate: typeof evaluateGate; evaluatePlatformGateFindings: typeof evaluatePlatformGateFindings; evaluateBrownfieldHotspotFindings: typeof evaluateBrownfieldHotspotFindings; resolveFactsForGateScope: typeof resolveFactsForGateScope; emitPlatformGateEvidence: typeof emitPlatformGateEvidence; printGateFindings: typeof printGateFindings; enforceTddBddPolicy: typeof enforceTddBddPolicy; evaluateAstIntelligenceDualValidation: typeof evaluateAstIntelligenceDualValidation; buildMemoryShadowRecommendation: (params: { findings: ReadonlyArray; tddBddSnapshot?: TddBddSnapshot; }) => OperationalMemoryShadowRecommendation | undefined; evaluateSddForStage: ( stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI', repoRoot: string ) => Pick; resolveActiveGateWaiver: (params: { repoRoot: string; stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; branch: string | null; }) => GateWaiverResult; evaluateContextGate: typeof evaluateContextGate; notifyGateBlocked: typeof emitGateBlockedNotification; }; const defaultServices: GateServices = { git: new GitService(), evidence: new EvidenceService(), }; const hasPositiveFindingLine = (lines: Finding['lines']): boolean => { if (typeof lines === 'number') { return Number.isFinite(lines) && lines > 0; } if (typeof lines === 'string') { return lines.trim().length > 0; } if (Array.isArray(lines)) { return lines.some((line) => Number.isFinite(line) && line > 0); } return false; }; const hasActionableFindingLocation = (finding: Finding): boolean => { return ( hasPositiveFindingLine(finding.lines) || finding.primary_node !== undefined || (finding.related_nodes?.length ?? 0) > 0 ); }; const normalizeFindingLines = (lines: Finding['lines']): ReadonlyArray => { if (typeof lines === 'number') { return Number.isFinite(lines) && lines > 0 ? [Math.trunc(lines)] : []; } if (Array.isArray(lines)) { return [...new Set(lines.filter((line) => Number.isFinite(line) && line > 0).map((line) => Math.trunc(line)))].sort( (left, right) => left - right ); } if (typeof lines === 'string') { return [...new Set( lines .split(/[^0-9]+/) .map((line) => Number.parseInt(line, 10)) .filter((line) => Number.isFinite(line) && line > 0) )].sort((left, right) => left - right); } return []; }; const parseChangedLinesByPathFromUnifiedDiff = (diff: string): Map> => { const changedLinesByPath = new Map>(); let currentPath: string | undefined; for (const rawLine of diff.split('\n')) { if (rawLine.startsWith('+++ b/')) { const nextPath = rawLine.slice('+++ b/'.length).trim(); currentPath = nextPath.length > 0 && nextPath !== '/dev/null' ? toNormalizedPath(nextPath) : undefined; if (currentPath && !changedLinesByPath.has(currentPath)) { changedLinesByPath.set(currentPath, new Set()); } continue; } if (!currentPath || !rawLine.startsWith('@@')) { continue; } const hunkMatch = rawLine.match(/\+(\d+)(?:,(\d+))?/); if (!hunkMatch) { continue; } const startLine = Number.parseInt(hunkMatch[1] ?? '', 10); const lineCount = hunkMatch[2] === undefined ? 1 : Number.parseInt(hunkMatch[2], 10); if (!Number.isFinite(startLine) || startLine <= 0 || !Number.isFinite(lineCount)) { continue; } const changedLines = changedLinesByPath.get(currentPath) ?? new Set(); for (let offset = 0; offset < Math.max(0, lineCount); offset += 1) { changedLines.add(startLine + offset); } changedLinesByPath.set(currentPath, changedLines); } return changedLinesByPath; }; const collectStagedChangedLinesByPath = ( git: IGitService, repoRoot: string ): Map> => { try { return parseChangedLinesByPathFromUnifiedDiff( git.runGit(['diff', '--cached', '--unified=0', '--no-ext-diff'], repoRoot) ); } catch { return new Map>(); } }; const isFindingOutsideChangedLines = ( finding: Finding, changedLinesByPath: ReadonlyMap> ): boolean => { if (!finding.filePath) { return false; } const changedLines = changedLinesByPath.get(toNormalizedPath(finding.filePath)); if (!changedLines || changedLines.size === 0) { return false; } const findingLines = normalizeFindingLines(finding.lines); if (findingLines.length === 0) { return false; } return findingLines.every((line) => !changedLines.has(line)); }; const BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD = 20; const isBroadFileLevelFinding = (finding: Finding): boolean => { const findingLines = normalizeFindingLines(finding.lines); if (findingLines.length <= BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD) { return false; } const nodeLines = [ ...normalizeFindingLines(finding.primary_node?.lines), ...(finding.related_nodes ?? []).flatMap((node) => normalizeFindingLines(node.lines)), ]; const hasPreciseNode = nodeLines.length > 0 && nodeLines.length <= BROAD_BROWNFIELD_FINDING_LINES_THRESHOLD; return !hasPreciseNode; }; const isFindingTooBroadForStagedDiffBlock = ( finding: Finding, changedLinesByPath: ReadonlyMap> ): boolean => { if (!finding.filePath || !isBroadFileLevelFinding(finding)) { return false; } const changedLines = changedLinesByPath.get(toNormalizedPath(finding.filePath)); return typeof changedLines !== 'undefined' && changedLines.size > 0; }; const normalizeScopedRuleEngineFindings = (params: { findings: ReadonlyArray; scope: GateScope; stage: GatePolicy['stage']; changedLinesByPath: ReadonlyMap>; stagedPaths: ReadonlyArray; }): ReadonlyArray => { if ( (params.scope.kind !== 'staged' && params.scope.kind !== 'repoAndStaged') || (params.stage !== 'PRE_WRITE' && params.stage !== 'PRE_COMMIT') ) { return params.findings; } const stagedPathSet = new Set(params.stagedPaths.map(toNormalizedPath)); return params.findings.map((finding) => { if (!isAstRuleFinding(finding)) { return finding; } const outsideStagedFileSet = params.scope.kind === 'staged' && params.stagedPaths.length > 0 && typeof finding.filePath === 'string' && finding.filePath.length > 0 && !stagedPathSet.has(toNormalizedPath(finding.filePath)); const outsideChangedLines = isFindingOutsideChangedLines( finding, params.changedLinesByPath ); const tooBroadForDiffBlock = isFindingTooBroadForStagedDiffBlock( finding, params.changedLinesByPath ); if (!outsideStagedFileSet && !outsideChangedLines && !tooBroadForDiffBlock) { return finding; } return { ...finding, blocking: false, severity: 'INFO', message: `${finding.message} ` + (outsideStagedFileSet ? 'Baseline brownfield outside the staged file set; tracked as advisory for this atomic slice.' : tooBroadForDiffBlock ? 'Finding is file-level/broad and cannot prove the violation was introduced by the staged diff; tracked as advisory for this atomic slice.' : 'Baseline brownfield outside the staged diff; tracked as advisory for this atomic slice.'), expected_fix: finding.expected_fix ?? (outsideStagedFileSet ? 'Plan a dedicated brownfield remediation slice for the non-staged file, or include it explicitly in the atomic commit.' : tooBroadForDiffBlock ? 'Emit a precise AST/nodal finding with line-level evidence for the changed node, or plan a dedicated brownfield remediation slice for this file-level debt.' : 'Plan a dedicated brownfield remediation slice for this pre-existing finding.'), }; }); }; const normalizeNonBlockingSwiftTestingMigrationWarnings = ( findings: ReadonlyArray ): ReadonlyArray => findings.map((finding) => { if ( finding.ruleId !== 'skills.ios.prefer-swift-testing' || finding.severity !== 'WARN' ) { return finding; } return { ...finding, blocking: false, expected_fix: finding.expected_fix ?? 'Mantén XCTest en suites legacy, UI o brownfield. Planifica una migración a Swift Testing en una slice dedicada si el repo lo decide.', }; }); const buildDefaultMemoryShadowRecommendation = (params: { findings: ReadonlyArray; tddBddSnapshot?: TddBddSnapshot; }): OperationalMemoryShadowRecommendation | undefined => { const hasCritical = params.findings.some((finding) => finding.severity === 'CRITICAL'); const hasError = params.findings.some((finding) => finding.severity === 'ERROR'); const hasWarn = params.findings.some((finding) => finding.severity === 'WARN'); const reasonCodes: string[] = []; if (hasCritical || hasError) { reasonCodes.push('severity.error_or_critical'); } else if (hasWarn) { reasonCodes.push('severity.warn'); } else { reasonCodes.push('severity.clean'); } if (params.tddBddSnapshot?.status === 'blocked') { reasonCodes.push('tdd_bdd.blocked'); } else if (params.tddBddSnapshot?.status === 'advisory') { reasonCodes.push('tdd_bdd.advisory'); } else if (params.tddBddSnapshot?.status === 'passed') { reasonCodes.push('tdd_bdd.passed'); } if (hasCritical || hasError || params.tddBddSnapshot?.status === 'blocked') { return { recommendedOutcome: 'BLOCK', confidence: 0.9, reasonCodes, }; } if (hasWarn) { return { recommendedOutcome: 'WARN', confidence: 0.75, reasonCodes, }; } if (params.tddBddSnapshot?.status === 'advisory') { return { recommendedOutcome: 'WARN', confidence: 0.7, reasonCodes, }; } return { recommendedOutcome: 'ALLOW', confidence: 0.65, reasonCodes, }; }; const defaultDependencies: GateDependencies = { evaluateGate, evaluatePlatformGateFindings, evaluateBrownfieldHotspotFindings, resolveFactsForGateScope, emitPlatformGateEvidence, printGateFindings, enforceTddBddPolicy, evaluateAstIntelligenceDualValidation, buildMemoryShadowRecommendation: buildDefaultMemoryShadowRecommendation, evaluateSddForStage: (stage, repoRoot) => evaluateSddPolicy({ stage, repoRoot, }).decision, resolveActiveGateWaiver: ({ repoRoot, stage, branch }) => resolveActiveGateWaiver({ repoRoot, stage, branch, }), evaluateContextGate, notifyGateBlocked: emitGateBlockedNotification, }; const readSymbolicBranchRef = (git: IGitService, repoRoot: string): string | null => { try { const symbolicBranch = git.runGit(['symbolic-ref', '--short', 'HEAD'], repoRoot).trim(); return symbolicBranch.length > 0 ? symbolicBranch : null; } catch { return null; } }; const resolveCurrentBranch = (git: IGitService, repoRoot: string): string | null => { const symbolic = readSymbolicBranchRef(git, repoRoot); if (symbolic !== null) { return symbolic; } try { const branch = git.runGit(['rev-parse', '--abbrev-ref', 'HEAD'], repoRoot).trim(); if (branch.length === 0 || branch === 'HEAD') { return null; } return branch; } catch { return null; } }; const toSddBlockingFinding = (decision: Pick): Finding => ({ ruleId: 'sdd.policy.blocked', severity: 'ERROR', code: decision.code, message: decision.message, filePath: 'openspec/changes', matchedBy: 'SddPolicy', source: 'sdd-policy', }); const toRulesCoverageBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; activeRuleIds: ReadonlyArray; evaluatedRuleIds: ReadonlyArray; unevaluatedRuleIds: ReadonlyArray; }): Finding | undefined => { if (params.unevaluatedRuleIds.length === 0) { return undefined; } const active = params.activeRuleIds.length; const evaluated = params.evaluatedRuleIds.length; const coverageRatio = active === 0 ? 1 : Number((evaluated / active).toFixed(6)); const unevaluatedRuleIds = [...params.unevaluatedRuleIds].sort().join(', '); return { ruleId: 'governance.rules.coverage.incomplete', severity: 'ERROR', code: 'RULES_COVERAGE_INCOMPLETE_HIGH', message: `Coverage incomplete at ${params.stage}: unevaluated_rule_ids=[${unevaluatedRuleIds}]` + ` coverage_ratio=${coverageRatio}. Evaluate all active rules before proceeding.`, filePath: '.ai_evidence.json', matchedBy: 'RulesCoverageGuard', source: 'rules-coverage', }; }; const toSkillsUnsupportedAutoRulesBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; unsupportedAutoRuleIds: ReadonlyArray; }): Finding | undefined => { if (params.unsupportedAutoRuleIds.length === 0) { return undefined; } const unsupportedAutoRuleIds = [...params.unsupportedAutoRuleIds].sort().join(', '); return { ruleId: 'governance.skills.detector-mapping.incomplete', severity: 'ERROR', code: 'SKILLS_DETECTOR_MAPPING_INCOMPLETE_HIGH', message: `Skills detector mapping incomplete at ${params.stage}: ` + `unsupported_auto_rule_ids=[${unsupportedAutoRuleIds}]. ` + 'Map every AUTO skill rule to an AST detector before proceeding.', filePath: '.ai_evidence.json', matchedBy: 'SkillsDetectorMappingGuard', source: 'skills-detector-mapping', }; }; const toSkillsDeclarativeRulesClassificationFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; declarativeRuleIds: ReadonlyArray; facts: ReadonlyArray; }): Finding | undefined => { if (process.env.PUMUKI_ENFORCE_DECLARATIVE_RULE_CLASSIFICATION !== '1') { return undefined; } if (params.declarativeRuleIds.length === 0) { return undefined; } if (collectObservedCodePathsFromFacts(params.facts).length === 0) { return undefined; } const sampleRuleIds = [...params.declarativeRuleIds].sort().slice(0, 12).join(', '); return { ruleId: 'governance.skills.declarative-rules.classification-required', severity: 'ERROR', code: 'SKILLS_DECLARATIVE_RULES_CLASSIFICATION_REQUIRED', message: `Skills declarative rules require AST/nodal classification at ${params.stage}: ` + `total=${params.declarativeRuleIds.length} sample_rule_ids=[${sampleRuleIds}]. ` + 'Classify every rule as IMPLEMENTAR_DETECTOR, REQUIERE_ESTUDIO, or NO_ES_REGLA_DE_CODIGO; code skills cannot remain as hidden declarative/advisory rules.', filePath: 'skills.lock.json', matchedBy: 'SkillsDeclarativeRulesClassificationGuard', source: 'skills-declarative-rules-classification', }; }; const PLATFORM_SKILLS_RULE_PREFIXES: Record< 'ios' | 'android' | 'backend' | 'frontend', string > = { ios: 'skills.ios.', android: 'skills.android.', backend: 'skills.backend.', frontend: 'skills.frontend.', }; const PLATFORM_REQUIRED_SKILLS_BUNDLES: Record< 'ios' | 'android' | 'backend' | 'frontend', ReadonlyArray > = { ios: [ 'ios-guidelines', 'ios-concurrency-guidelines', 'ios-swiftui-expert-guidelines', 'ios-swift-testing-guidelines', 'ios-core-data-guidelines', ], android: ['android-guidelines'], backend: ['backend-guidelines'], frontend: ['frontend-guidelines'], }; const isCriticalProfileSeverity = (severity: string): boolean => { return severity === 'CRITICAL' || severity === 'ERROR'; }; const toNormalizedPath = (value: string): string => value.replace(/\\/g, '/').trim(); const CODE_FILE_EXTENSIONS = new Set([ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.swift', '.kt', '.kts', ]); const isObservedCodePath = (path: string): boolean => { const normalized = toNormalizedPath(path).toLowerCase(); if (normalized.length === 0) { return false; } if ( normalized.startsWith('apps/backend/') || normalized.startsWith('apps/frontend/') || normalized.startsWith('apps/web/') || normalized.startsWith('apps/admin-dashboard/') || normalized.startsWith('apps/ios/') || normalized.startsWith('apps/android/') ) { return true; } for (const extension of CODE_FILE_EXTENSIONS) { if (normalized.endsWith(extension)) { return true; } } return false; }; const collectObservedPathsFromFacts = ( facts: ReadonlyArray ): ReadonlyArray => { const observedPaths = new Set(); for (const fact of facts) { if (fact.kind === 'FileChange' || fact.kind === 'FileContent') { const normalized = toNormalizedPath(fact.path); if (normalized.length > 0) { observedPaths.add(normalized); } continue; } if (fact.kind === 'Heuristic' && typeof fact.filePath === 'string') { const normalized = toNormalizedPath(fact.filePath); if (normalized.length > 0) { observedPaths.add(normalized); } } } return [...observedPaths].sort(); }; const collectObservedCodePathsFromFacts = ( facts: ReadonlyArray ): ReadonlyArray => { const codePaths = collectObservedPathsFromFacts(facts).filter((path) => isObservedCodePath(path)); return [...new Set(codePaths)].sort(); }; const isSkillsContractCarrierPath = (path: string): boolean => { const normalized = toNormalizedPath(path).toLowerCase(); return ( normalized === 'agents.md' || normalized === 'skills.lock.json' || normalized === 'skills.sources.json' || normalized.startsWith('vendor/skills/') || normalized.startsWith('docs/codex-skills/') || normalized === '.pumuki/policy-as-code.json' ); }; const collectStagedPaths = (git: IGitService, repoRoot: string): ReadonlyArray => { try { return git.runGit(['diff', '--cached', '--name-only'], repoRoot) .split('\n') .map((line) => toNormalizedPath(line)) .filter((line) => line.length > 0); } catch { return []; } }; const shouldAugmentStagedSkillsContractFactsWithRepoFacts = (params: { scope: GateScope; facts: ReadonlyArray; stagedPaths: ReadonlyArray; }): boolean => { if (params.scope.kind !== 'staged') { return false; } if (collectObservedCodePathsFromFacts(params.facts).length > 0) { return false; } return params.stagedPaths.some((path) => isSkillsContractCarrierPath(path)); }; type IosTestFileContent = { path: string; content: string; }; const isIosSwiftTestPath = (path: string): boolean => { const normalized = toNormalizedPath(path).toLowerCase(); if (!normalized.endsWith('.swift')) { return false; } return normalized.includes('/tests/') || normalized.includes('/uitests/'); }; const collectIosTestFileContents = ( facts: ReadonlyArray ): ReadonlyArray => { const filesByPath = new Map(); for (const fact of facts) { if (fact.kind !== 'FileContent') { continue; } if (!isIosSwiftTestPath(fact.path)) { continue; } const normalizedPath = toNormalizedPath(fact.path); filesByPath.set(normalizedPath, fact.content); } return [...filesByPath.entries()] .sort(([leftPath], [rightPath]) => leftPath.localeCompare(rightPath)) .map(([path, content]) => ({ path, content })); }; const isXCTestSource = (content: string): boolean => { return /\bclass\s+\w+\s*:\s*XCTestCase\b/.test(content); }; const hasMakeSUTPattern = (content: string): boolean => /\bmakeSUT\s*\(/.test(content); const hasTrackForMemoryLeaksPattern = (content: string): boolean => /\btrackForMemoryLeaks\s*\(/.test(content); const IOS_CRITICAL_TEST_QUALITY_RULE_ID = 'skills.ios.critical-test-quality'; const hasIosTestQualityScope = (facts: ReadonlyArray): boolean => collectIosTestFileContents(facts).length > 0; const appendUniqueRuleId = ( ruleIds: ReadonlyArray, ruleId: string ): readonly string[] => { if (ruleIds.includes(ruleId)) { return [...ruleIds]; } return [...ruleIds, ruleId].sort((left, right) => left.localeCompare(right)); }; const toIosTestsQualityBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; facts: ReadonlyArray; }): Finding | undefined => { const testFiles = collectIosTestFileContents(params.facts); if (testFiles.length === 0) { return undefined; } const invalidFiles: string[] = []; for (const testFile of testFiles) { if (!isXCTestSource(testFile.content)) { continue; } const missingMarkers: string[] = []; if (!hasMakeSUTPattern(testFile.content)) { missingMarkers.push('makeSUT()'); } if (!hasTrackForMemoryLeaksPattern(testFile.content)) { missingMarkers.push('trackForMemoryLeaks()'); } if (missingMarkers.length === 0) { continue; } invalidFiles.push(`${testFile.path}{missing=[${missingMarkers.join(', ')}]}`); } if (invalidFiles.length === 0) { return undefined; } const sampleFiles = invalidFiles.slice(0, 3).join(' | '); return { ruleId: 'governance.skills.ios-test-quality.incomplete', severity: 'ERROR', code: 'IOS_TEST_QUALITY_PATTERN_MISSING_HIGH', message: `iOS test quality enforcement incomplete at ${params.stage}: ${sampleFiles}. ` + 'Use makeSUT() factory pattern and trackForMemoryLeaks() in XCTest sources.', filePath: '.ai_evidence.json', matchedBy: 'IosTestsQualityGuard', source: 'skills-ios-test-quality', }; }; const toActiveRulesEmptyForCodeChangesBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; facts: ReadonlyArray; activeRuleIds: ReadonlyArray; }): Finding | undefined => { if (params.activeRuleIds.length > 0) { return undefined; } const codePaths = collectObservedCodePathsFromFacts(params.facts); if (codePaths.length === 0) { return undefined; } const samplePaths = codePaths.slice(0, 5).join(', '); return { ruleId: 'governance.rules.active-rule-coverage.empty', severity: 'ERROR', code: 'ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH', message: `Active rules coverage is empty at ${params.stage} while code changes were detected. ` + `sample_paths=[${samplePaths}]. Ensure skill/project rules are active before allowing this stage.`, filePath: '.ai_evidence.json', matchedBy: 'ActiveRulesCoverageGuard', source: 'rules-coverage', }; }; const detectRequiredSkillsScopesFromPaths = ( observedPaths: ReadonlyArray ): Record<'ios' | 'android' | 'backend' | 'frontend', ReadonlyArray> => { const scopes: Record<'ios' | 'android' | 'backend' | 'frontend', string[]> = { ios: [], android: [], backend: [], frontend: [], }; for (const observedPath of observedPaths) { const normalized = observedPath.toLowerCase(); if (normalized.startsWith('apps/ios/') || normalized.endsWith('.swift')) { scopes.ios.push(observedPath); } if ( normalized.startsWith('apps/android/') || normalized.endsWith('.kt') || normalized.endsWith('.kts') ) { scopes.android.push(observedPath); } if (normalized.startsWith('apps/backend/')) { scopes.backend.push(observedPath); } if ( normalized.startsWith('apps/frontend/') || normalized.startsWith('apps/web/') || normalized.startsWith('apps/admin-dashboard/') ) { scopes.frontend.push(observedPath); } } return { ios: [...new Set(scopes.ios)].sort(), android: [...new Set(scopes.android)].sort(), backend: [...new Set(scopes.backend)].sort(), frontend: [...new Set(scopes.frontend)].sort(), }; }; const toSkillsScopeComplianceBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; facts: ReadonlyArray; activeRuleIds: ReadonlyArray; evaluatedRuleIds: ReadonlyArray; }): Finding | undefined => { const observedPaths = collectObservedPathsFromFacts(params.facts); const requiredScopes = detectRequiredSkillsScopesFromPaths(observedPaths); const missingScopes: string[] = []; for (const scope of ['ios', 'android', 'backend', 'frontend'] as const) { const scopePaths = requiredScopes[scope]; if (scopePaths.length === 0) { continue; } const prefix = PLATFORM_SKILLS_RULE_PREFIXES[scope]; const hasActiveRules = params.activeRuleIds.some((ruleId) => ruleId.startsWith(prefix)); const hasEvaluatedRules = params.evaluatedRuleIds.some((ruleId) => ruleId.startsWith(prefix)); if (hasActiveRules && hasEvaluatedRules) { continue; } const reasons: string[] = []; if (!hasActiveRules) { reasons.push(`active_rules_prefix=${prefix} missing`); } if (!hasEvaluatedRules) { reasons.push(`evaluated_rules_prefix=${prefix} missing`); } const samplePaths = scopePaths.slice(0, 3).join(', '); missingScopes.push(`${scope}{${reasons.join('; ')} sample_paths=[${samplePaths}]}`); } if (missingScopes.length === 0) { return undefined; } return { ruleId: 'governance.skills.scope-compliance.incomplete', severity: 'ERROR', code: 'SKILLS_SCOPE_COMPLIANCE_INCOMPLETE_HIGH', message: `Skills scope compliance incomplete at ${params.stage}: ${missingScopes.join(' | ')}. ` + 'Map changed file scopes to required skill rules and ensure those prefixes are active/evaluated.', filePath: '.ai_evidence.json', matchedBy: 'SkillsScopeComplianceGuard', source: 'skills-scope-compliance', }; }; const toPolicyAsCodeBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; policyTrace?: ResolvedStagePolicy['trace']; }): Finding | undefined => { const validation = params.policyTrace?.validation; if (!validation || validation.status === 'valid' || !validation.strict) { return undefined; } return { ruleId: 'governance.policy-as-code.invalid', severity: 'ERROR', code: validation.code, message: `Policy-as-code validation failed at ${params.stage}: ${validation.message} ` + `policy_source=${params.policyTrace?.policySource ?? 'n/a'} ` + `policy_version=${params.policyTrace?.version ?? 'n/a'}.`, filePath: '.pumuki/policy-as-code.json', matchedBy: 'PolicyAsCodeGuard', source: 'policy-as-code', }; }; const toDegradedModeFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; policyTrace?: ResolvedStagePolicy['trace']; }): Finding | undefined => { const degraded = params.policyTrace?.degraded; if (!degraded?.enabled) { return undefined; } if (degraded.action === 'block') { return { ruleId: 'governance.degraded-mode.blocked', severity: 'ERROR', code: degraded.code, message: `Degraded mode is active at ${params.stage} with fail-closed action=block. ` + `reason=${degraded.reason} source=${degraded.source}.`, filePath: '.pumuki/degraded-mode.json', matchedBy: 'DegradedModeGuard', source: 'degraded-mode', }; } return { ruleId: 'governance.degraded-mode.active', severity: 'INFO', code: degraded.code, message: `Degraded mode is active at ${params.stage} with fail-open action=allow. ` + `reason=${degraded.reason} source=${degraded.source}.`, filePath: '.pumuki/degraded-mode.json', matchedBy: 'DegradedModeGuard', source: 'degraded-mode', }; }; const toGateWaiverAppliedFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; waiver: Extract['waiver']; }): Finding => ({ ruleId: 'governance.waiver.applied', severity: 'INFO', code: 'GATE_WAIVER_APPLIED', message: `Gate waiver applied at ${params.stage}: waiver_id=${params.waiver.id} owner=${params.waiver.owner} ` + `expires_at=${params.waiver.expires_at}.`, filePath: '.pumuki/waivers/gate.json', matchedBy: 'GateWaiverGuard', source: 'gate-waiver', }); const toGateWaiverExpiredFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; waiver: Extract['waiver']; }): Finding => ({ ruleId: 'governance.waiver.expired', severity: 'ERROR', code: 'GATE_WAIVER_EXPIRED', message: `Gate waiver expired at ${params.stage}: waiver_id=${params.waiver.id} owner=${params.waiver.owner} ` + `expired_at=${params.waiver.expires_at}. Provide a valid waiver or fix blocking findings.`, filePath: '.pumuki/waivers/gate.json', matchedBy: 'GateWaiverGuard', source: 'gate-waiver', }); const toGateWaiverInvalidFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; reason: string; }): Finding => ({ ruleId: 'governance.waiver.invalid', severity: 'ERROR', code: 'GATE_WAIVER_INVALID', message: `Gate waiver file is invalid at ${params.stage}: ${params.reason}. ` + 'Fix waiver schema or remove invalid waiver before continuing.', filePath: '.pumuki/waivers/gate.json', matchedBy: 'GateWaiverGuard', source: 'gate-waiver', }); const toPlatformSkillsCoverageBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; detectedPlatforms: DetectedPlatforms; activeBundles: ReadonlyArray; activeRuleIds: ReadonlyArray; evaluatedRuleIds: ReadonlyArray; }): Finding | undefined => { const detectedPlatformKeys = ( ['ios', 'android', 'backend', 'frontend'] as const ).filter((platform) => params.detectedPlatforms[platform]?.detected === true); if (detectedPlatformKeys.length === 0) { return undefined; } const activeBundleNames = new Set(params.activeBundles.map((bundle) => bundle.name)); const gaps: string[] = []; for (const platform of detectedPlatformKeys) { const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]; const missingBundles = requiredBundles.filter((bundleName) => !activeBundleNames.has(bundleName)); const rulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform]; const hasActiveRules = params.activeRuleIds.some((ruleId) => ruleId.startsWith(rulePrefix)); const hasEvaluatedRules = params.evaluatedRuleIds.some((ruleId) => ruleId.startsWith(rulePrefix)); if (missingBundles.length === 0 && hasActiveRules && hasEvaluatedRules) { continue; } const reasons: string[] = []; if (missingBundles.length > 0) { reasons.push(`missing_bundles=[${missingBundles.join(', ')}]`); } if (!hasActiveRules) { reasons.push(`active_rules_prefix=${rulePrefix} missing`); } if (!hasEvaluatedRules) { reasons.push(`evaluated_rules_prefix=${rulePrefix} missing`); } gaps.push(`${platform}{${reasons.join('; ')}}`); } if (gaps.length === 0) { return undefined; } return { ruleId: 'governance.skills.platform-coverage.incomplete', severity: 'ERROR', code: 'SKILLS_PLATFORM_COVERAGE_INCOMPLETE_HIGH', message: `Platform skills coverage incomplete at ${params.stage}: ${gaps.join(' | ')}. ` + 'Activate required bundles and ensure platform skill rules are active/evaluated.', filePath: '.ai_evidence.json', matchedBy: 'SkillsPlatformCoverageGuard', source: 'skills-platform-coverage', }; }; const toCrossPlatformCriticalEnforcementBlockingFinding = (params: { stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'; detectedPlatforms: DetectedPlatforms; skillsRules: SkillsRuleSetLoadResult['rules']; evaluatedRuleIds: ReadonlyArray; }): Finding | undefined => { const detectedPlatformKeys = ( ['ios', 'android', 'backend', 'frontend'] as const ).filter((platform) => params.detectedPlatforms[platform]?.detected === true); if (detectedPlatformKeys.length === 0) { return undefined; } return undefined; }; const shouldBlockFromFinding = (finding: Finding | undefined): boolean => { if (!finding) { return false; } return finding.blocking !== false; }; const isAstRuleFinding = (finding: Finding): boolean => { const ruleId = finding.ruleId.toLowerCase(); const source = finding.source?.toLowerCase() ?? ''; const matchedBy = finding.matchedBy?.toLowerCase() ?? ''; if (ruleId.startsWith('governance.')) { return false; } return ( ruleId.startsWith('skills.') || ruleId.startsWith('heuristics.') || source.includes('heuristic') || matchedBy.includes('heuristic') || matchedBy.includes('ast') ); }; const resolvePrimaryBlockingFinding = ( findings: ReadonlyArray ): Finding | undefined => ( findings.find( (finding) => shouldBlockFromFinding(finding) && isAstRuleFinding(finding) && finding.severity === 'CRITICAL' ) ?? findings.find( (finding) => shouldBlockFromFinding(finding) && isAstRuleFinding(finding) && finding.severity === 'ERROR' ) ?? findings.find((finding) => shouldBlockFromFinding(finding) && isAstRuleFinding(finding)) ?? findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'CRITICAL') ?? findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'ERROR') ?? findings.find((finding) => shouldBlockFromFinding(finding)) ?? findings[0] ); const resolveBlockingNotificationFindings = ( findings: ReadonlyArray ): ReadonlyArray => { const blockingFindings = findings.filter((finding) => shouldBlockFromFinding(finding)); const astFindings = blockingFindings.filter(isAstRuleFinding); const nonAstFindings = blockingFindings.filter((finding) => !isAstRuleFinding(finding)); return [...astFindings, ...nonAstFindings]; }; const applySkillsFindingEnforcement = ( finding: Finding | undefined ): Finding | undefined => { if (!finding) { return undefined; } return finding; }; export async function runPlatformGate(params: { policy: GatePolicy; auditMode?: 'gate' | 'engine'; policyTrace?: ResolvedStagePolicy['trace']; scope: GateScope; silent?: boolean; sddShortCircuit?: boolean; sddDecisionOverride?: Pick; services?: Partial; dependencies?: Partial; }): Promise { const git = params.services?.git ?? defaultServices.git; const evidence = params.services?.evidence ?? defaultServices.evidence; const dependencies: GateDependencies = { ...defaultDependencies, ...params.dependencies, }; const repoRoot = git.resolveRepoRoot(); const auditMode = params.auditMode ?? 'gate'; const shouldShortCircuitSdd = params.sddShortCircuit ?? false; const contextGate: ContextGateResult = dependencies.evaluateContextGate({ repoRoot, stage: params.policy.stage, }); const contextBlockingFinding = contextGate.finding; let sddDecision: | Pick | undefined; let sddBlockingFinding: Finding | undefined; if ( params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ) { sddDecision = params.sddDecisionOverride ?? dependencies.evaluateSddForStage( params.policy.stage, repoRoot ); if (!sddDecision.allowed) { if (params.silent !== true) { process.stdout.write(`[pumuki][sdd] ${sddDecision.code}: ${sddDecision.message}\n`); } sddBlockingFinding = toSddBlockingFinding(sddDecision); if (shouldShortCircuitSdd) { const emptyDetectedPlatforms: DetectedPlatforms = {}; const emptySkillsRuleSet: SkillsRuleSetLoadResult = { rules: [], activeBundles: [], mappedHeuristicRuleIds: new Set(), requiresHeuristicFacts: false, }; const emptyRuleSet: RuleSet = []; dependencies.emitPlatformGateEvidence({ stage: params.policy.stage, auditMode, policyTrace: params.policyTrace, findings: [sddBlockingFinding], gateOutcome: 'BLOCK', filesScanned: 0, evaluationMetrics: createEmptyEvaluationMetrics(), rulesCoverage: createEmptySnapshotRulesCoverage(params.policy.stage), repoRoot, detectedPlatforms: emptyDetectedPlatforms, skillsRuleSet: emptySkillsRuleSet, projectRules: emptyRuleSet, heuristicRules: emptyRuleSet, evidenceService: evidence, sddDecision, }); return 1; } } } const gateScopePathPrefixes = resolveGateScopePathPrefixesFromEnv(); const facts = filterFactsByPathPrefixes( await dependencies.resolveFactsForGateScope({ scope: params.scope, git, }), gateScopePathPrefixes ); const stagedPaths = collectStagedPaths(git, repoRoot); const stagedChangedLinesByPath = collectStagedChangedLinesByPath(git, repoRoot); const factsForPlatformEvaluation = shouldAugmentStagedSkillsContractFactsWithRepoFacts({ scope: params.scope, facts, stagedPaths, }) ? [ ...facts, ...(await dependencies.resolveFactsForGateScope({ scope: { kind: 'repo', }, git, })), ] : facts; const filesScanned = countScannedFilesFromFacts(factsForPlatformEvaluation); const observedCodePaths = collectObservedCodePathsFromFacts(facts); const preWriteLeaseFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toPreWriteEnforcementGapFinding({ stage: params.policy.stage, status: readPreWriteLeaseStatus({ repoRoot, git, }), }) : undefined; const platformEvaluation = dependencies.evaluatePlatformGateFindings({ facts: factsForPlatformEvaluation, stage: params.policy.stage, repoRoot, }); const aiGateRepoPolicyFindings = collectAiGateRepoPolicyFindings({ repoRoot, stage: params.policy.stage, }); const { detectedPlatforms, skillsRuleSet, projectRules, heuristicRules, coverage, evaluationFacts = factsForPlatformEvaluation, findings: ruleEngineFindings, } = platformEvaluation; const scopedRuleEngineFindings = normalizeScopedRuleEngineFindings({ findings: ruleEngineFindings, scope: params.scope, stage: params.policy.stage, changedLinesByPath: stagedChangedLinesByPath, stagedPaths, }); const findings = normalizeNonBlockingSwiftTestingMigrationWarnings([ ...aiGateRepoPolicyFindings, ...scopedRuleEngineFindings, ]); const evaluationMetrics: SnapshotEvaluationMetrics = coverage ? { facts_total: coverage.factsTotal, rules_total: coverage.rulesTotal, baseline_rules: coverage.baselineRules, heuristic_rules: coverage.heuristicRules, skills_rules: coverage.skillsRules, project_rules: coverage.projectRules, matched_rules: coverage.matchedRules, unmatched_rules: coverage.unmatchedRules, evaluated_rule_ids: [...coverage.evaluatedRuleIds], matched_rule_ids: [...coverage.matchedRuleIds], unmatched_rule_ids: [...coverage.unmatchedRuleIds], } : createEmptyEvaluationMetrics(); const coverageBlockingFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toRulesCoverageBlockingFinding({ stage: params.policy.stage, activeRuleIds: coverage?.activeRuleIds ?? [], evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [], unevaluatedRuleIds: coverage?.unevaluatedRuleIds ?? [], }) : undefined; const unsupportedSkillsMappingFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toSkillsUnsupportedAutoRulesBlockingFinding({ stage: params.policy.stage, unsupportedAutoRuleIds: skillsRuleSet.unsupportedAutoRuleIds ?? [], }) : undefined; const declarativeRulesClassificationFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toSkillsDeclarativeRulesClassificationFinding({ stage: params.policy.stage, declarativeRuleIds: skillsRuleSet.registryCoverage?.declarativeRuleIds ?? [], facts: factsForPlatformEvaluation, }) : undefined; const effectiveUnsupportedSkillsMappingInput = applySkillsFindingEnforcement( unsupportedSkillsMappingFinding ); const platformSkillsCoverageFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toPlatformSkillsCoverageBlockingFinding({ stage: params.policy.stage, detectedPlatforms, activeBundles: skillsRuleSet.activeBundles, activeRuleIds: coverage?.activeRuleIds ?? [], evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [], }) : undefined; const effectivePlatformSkillsCoverageInput = applySkillsFindingEnforcement( platformSkillsCoverageFinding ); const crossPlatformCriticalFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toCrossPlatformCriticalEnforcementBlockingFinding({ stage: params.policy.stage, detectedPlatforms, skillsRules: skillsRuleSet.rules, evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [], }) : undefined; const effectiveCrossPlatformCriticalInput = applySkillsFindingEnforcement( crossPlatformCriticalFinding ); const skillsScopeComplianceFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toSkillsScopeComplianceBlockingFinding({ stage: params.policy.stage, facts, activeRuleIds: coverage?.activeRuleIds ?? [], evaluatedRuleIds: coverage?.evaluatedRuleIds ?? [], }) : undefined; const effectiveSkillsScopeComplianceInput = applySkillsFindingEnforcement( skillsScopeComplianceFinding ); const activeRulesEmptyForCodeChangesFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toActiveRulesEmptyForCodeChangesBlockingFinding({ stage: params.policy.stage, facts, activeRuleIds: coverage?.activeRuleIds ?? [], }) : undefined; const iosTestsQualityFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toIosTestsQualityBlockingFinding({ stage: params.policy.stage, facts, }) : undefined; const effectiveIosTestsQualityFinding = applySkillsFindingEnforcement( iosTestsQualityFinding ); const policyAsCodeBlockingFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toPolicyAsCodeBlockingFinding({ stage: params.policy.stage, policyTrace: params.policyTrace, }) : undefined; const degradedModeFinding = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? toDegradedModeFinding({ stage: params.policy.stage, policyTrace: params.policyTrace, }) : undefined; const astIntelligenceDualValidation: | AstIntelligenceDualValidationResult | undefined = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? dependencies.evaluateAstIntelligenceDualValidation({ stage: params.policy.stage, skillsRules: skillsRuleSet.rules, facts: evaluationFacts, legacyFindings: findings, }) : undefined; const astIntelligenceDualFinding = astIntelligenceDualValidation?.finding; if (astIntelligenceDualValidation && astIntelligenceDualValidation.mode !== 'off') { const summary = astIntelligenceDualValidation.summary; if (params.silent !== true) { process.stdout.write( `[pumuki][ast-intelligence] mode=${astIntelligenceDualValidation.mode}` + ` mapped_rules=${summary.mapped_rules}` + ` compared_rules=${summary.compared_rules}` + ` divergences=${summary.divergences}` + ` false_positives=${summary.false_positives}` + ` false_negatives=${summary.false_negatives}` + ` latency_ms=${summary.latency_ms}` + ` languages=[${summary.languages.join(',') || 'none'}]\n` ); } } const degradedModeBlocks = params.policyTrace?.degraded?.action === 'block'; const coverageActiveRuleIds = coverage && hasIosTestQualityScope(facts) ? appendUniqueRuleId(coverage.activeRuleIds, IOS_CRITICAL_TEST_QUALITY_RULE_ID) : coverage?.activeRuleIds; const coverageEvaluatedRuleIds = coverage && hasIosTestQualityScope(facts) ? appendUniqueRuleId(coverage.evaluatedRuleIds, IOS_CRITICAL_TEST_QUALITY_RULE_ID) : coverage?.evaluatedRuleIds; const rulesCoverage = coverage ? { stage: params.policy.stage, contract: skillsRuleSet.registryCoverage?.contract ?? 'AUTO_RUNTIME_RULES_FOR_STAGE', scope_note: 'rules_coverage reports AUTO runtime rules applicable to this stage; it does not claim full DECLARATIVE registry execution.', active_rule_ids: [...(coverageActiveRuleIds ?? coverage.activeRuleIds)], evaluated_rule_ids: [...(coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds)], matched_rule_ids: [...coverage.matchedRuleIds], unevaluated_rule_ids: [...coverage.unevaluatedRuleIds], ...(skillsRuleSet.registryCoverage ? { registry_totals: skillsRuleSet.registryCoverage.registryTotals, stage_applicable_auto_rule_ids: [ ...skillsRuleSet.registryCoverage.stageApplicableAutoRuleIds, ], declarative_rule_ids: [...skillsRuleSet.registryCoverage.declarativeRuleIds], declarative_excluded_reason: skillsRuleSet.registryCoverage.excludedDeclarativeReason, } : {}), ...((skillsRuleSet.unsupportedAutoRuleIds?.length ?? 0) > 0 ? { unsupported_auto_rule_ids: [...(skillsRuleSet.unsupportedAutoRuleIds ?? [])], } : {}), counts: { active: (coverageActiveRuleIds ?? coverage.activeRuleIds).length, evaluated: (coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds).length, matched: coverage.matchedRuleIds.length, unevaluated: coverage.unevaluatedRuleIds.length, ...(skillsRuleSet.registryCoverage ? { registry_total: skillsRuleSet.registryCoverage.registryTotals.total, registry_auto: skillsRuleSet.registryCoverage.registryTotals.auto, registry_declarative: skillsRuleSet.registryCoverage.registryTotals.declarative, stage_applicable_auto: skillsRuleSet.registryCoverage.stageApplicableAutoRuleIds.length, } : {}), ...((skillsRuleSet.unsupportedAutoRuleIds?.length ?? 0) > 0 ? { unsupported_auto: (skillsRuleSet.unsupportedAutoRuleIds ?? []).length, } : {}), }, coverage_ratio: (coverageActiveRuleIds ?? coverage.activeRuleIds).length === 0 ? 1 : Number( ( (coverageEvaluatedRuleIds ?? coverage.evaluatedRuleIds).length / (coverageActiveRuleIds ?? coverage.activeRuleIds).length ).toFixed(6) ), } : createEmptySnapshotRulesCoverage(params.policy.stage); const brownfieldHotspotFindings = dependencies.evaluateBrownfieldHotspotFindings({ repoRoot, stage: params.policy.stage, facts, }); const currentBranch = resolveCurrentBranch(git, repoRoot); const tddBddEvaluation = applyTddBddEnforcement( dependencies.enforceTddBddPolicy({ facts, repoRoot, branch: currentBranch, }) ); const tddBddSnapshot: TddBddSnapshot | undefined = tddBddEvaluation.snapshot.scope.in_scope ? tddBddEvaluation.snapshot : undefined; const hasTddBddBlockingFinding = tddBddEvaluation.findings.some( (finding) => shouldBlockFromFinding(finding) ); const hasNativeBlockingFinding = findings.some( (finding) => shouldBlockFromFinding(finding) ); const effectiveUnsupportedSkillsMappingFinding = effectiveUnsupportedSkillsMappingInput; const effectivePlatformSkillsCoverageFinding = effectivePlatformSkillsCoverageInput; const effectiveCrossPlatformCriticalFinding = effectiveCrossPlatformCriticalInput; const effectiveSkillsScopeComplianceFinding = effectiveSkillsScopeComplianceInput; const effectiveFindings = sddBlockingFinding ? [ ...(contextBlockingFinding ? [contextBlockingFinding] : []), sddBlockingFinding, ...(degradedModeFinding ? [degradedModeFinding] : []), ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []), ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []), ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []), ...(effectiveCrossPlatformCriticalFinding ? [effectiveCrossPlatformCriticalFinding] : []), ...(effectiveSkillsScopeComplianceFinding ? [effectiveSkillsScopeComplianceFinding] : []), ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []), ...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []), ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []), ...(coverageBlockingFinding ? [coverageBlockingFinding] : []), ...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []), ...brownfieldHotspotFindings, ...tddBddEvaluation.findings, ...findings, ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []), ] : effectiveUnsupportedSkillsMappingFinding || effectivePlatformSkillsCoverageFinding || effectiveCrossPlatformCriticalFinding || effectiveSkillsScopeComplianceFinding || preWriteLeaseFinding || activeRulesEmptyForCodeChangesFinding || effectiveIosTestsQualityFinding || astIntelligenceDualFinding || coverageBlockingFinding || declarativeRulesClassificationFinding || brownfieldHotspotFindings.length > 0 || policyAsCodeBlockingFinding || contextBlockingFinding || degradedModeFinding || tddBddEvaluation.findings.length > 0 ? [ ...(contextBlockingFinding ? [contextBlockingFinding] : []), ...(degradedModeFinding ? [degradedModeFinding] : []), ...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []), ...(effectiveUnsupportedSkillsMappingFinding ? [effectiveUnsupportedSkillsMappingFinding] : []), ...(effectivePlatformSkillsCoverageFinding ? [effectivePlatformSkillsCoverageFinding] : []), ...(effectiveCrossPlatformCriticalFinding ? [effectiveCrossPlatformCriticalFinding] : []), ...(effectiveSkillsScopeComplianceFinding ? [effectiveSkillsScopeComplianceFinding] : []), ...(activeRulesEmptyForCodeChangesFinding ? [activeRulesEmptyForCodeChangesFinding] : []), ...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []), ...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []), ...(coverageBlockingFinding ? [coverageBlockingFinding] : []), ...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []), ...brownfieldHotspotFindings, ...tddBddEvaluation.findings, ...findings, ...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []), ] : brownfieldHotspotFindings.length > 0 ? [...brownfieldHotspotFindings, ...findings] : findings; const hasAstIntelligenceBlockingFinding = shouldBlockFromFinding(astIntelligenceDualFinding); const gateDecisionFindings = effectiveFindings.filter((finding) => finding.blocking !== false); const decision = dependencies.evaluateGate([...gateDecisionFindings], params.policy); const hasNonBlockingAdvisoryFinding = effectiveFindings.some( (finding) => finding.blocking === false && (finding.severity === 'WARN' || finding.severity === 'ERROR' || finding.severity === 'CRITICAL') ); const baseGateOutcome = sddBlockingFinding || shouldBlockFromFinding(contextBlockingFinding) || degradedModeBlocks || shouldBlockFromFinding(preWriteLeaseFinding) || shouldBlockFromFinding(policyAsCodeBlockingFinding) || shouldBlockFromFinding(effectiveUnsupportedSkillsMappingFinding) || shouldBlockFromFinding(effectivePlatformSkillsCoverageFinding) || shouldBlockFromFinding(effectiveCrossPlatformCriticalFinding) || shouldBlockFromFinding(effectiveSkillsScopeComplianceFinding) || shouldBlockFromFinding(activeRulesEmptyForCodeChangesFinding) || shouldBlockFromFinding(effectiveIosTestsQualityFinding) || hasAstIntelligenceBlockingFinding || shouldBlockFromFinding(coverageBlockingFinding) || shouldBlockFromFinding(declarativeRulesClassificationFinding) || brownfieldHotspotFindings.some((finding) => shouldBlockFromFinding(finding)) || hasTddBddBlockingFinding ? 'BLOCK' : (decision.outcome === 'PASS' && (tddBddSnapshot?.status === 'advisory' || hasNonBlockingAdvisoryFinding) ? 'WARN' : decision.outcome); const gateWaiverStage = params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ? params.policy.stage : undefined; const gateWaiverResult = gateWaiverStage ? dependencies.resolveActiveGateWaiver({ repoRoot, stage: gateWaiverStage, branch: currentBranch, }) : ({ kind: 'none', path: '.pumuki/waivers/gate.json', } as const); let gateWaiverFinding: Finding | undefined; let gateOutcome = baseGateOutcome; if (baseGateOutcome === 'BLOCK' && gateWaiverStage) { if (gateWaiverResult.kind === 'applied') { gateWaiverFinding = toGateWaiverAppliedFinding({ stage: gateWaiverStage, waiver: gateWaiverResult.waiver, }); gateOutcome = 'PASS'; } else if (gateWaiverResult.kind === 'expired') { gateWaiverFinding = toGateWaiverExpiredFinding({ stage: gateWaiverStage, waiver: gateWaiverResult.waiver, }); } else if (gateWaiverResult.kind === 'invalid') { gateWaiverFinding = toGateWaiverInvalidFinding({ stage: gateWaiverStage, reason: gateWaiverResult.reason, }); } } const findingsWithWaiver = gateWaiverFinding ? [...effectiveFindings, gateWaiverFinding] : effectiveFindings; const shadowFlagRaw = process.env.PUMUKI_OPERATIONAL_MEMORY_SHADOW_ENABLED?.trim().toLowerCase(); const isMemoryShadowEnabled = shadowFlagRaw === '1' || shadowFlagRaw === 'true'; let memoryShadowRecommendation: OperationalMemoryShadowRecommendation | undefined; if (isMemoryShadowEnabled) { try { memoryShadowRecommendation = dependencies.buildMemoryShadowRecommendation({ findings: findingsWithWaiver, ...(tddBddSnapshot ? { tddBddSnapshot } : {}), }); } catch (error) { const rawReason = error instanceof Error ? error.message : String(error); const reason = rawReason.trim().replace(/\s+/g, ' '); if (params.silent !== true) { process.stdout.write( `[pumuki][memory-shadow] unavailable reason=${reason.length > 0 ? reason : 'unknown_error'}\n` ); } } } const memoryShadow: | { recommended_outcome: GateOutcome; actual_outcome: GateOutcome; confidence: number; reason_codes: string[]; } | undefined = memoryShadowRecommendation ? { recommended_outcome: memoryShadowRecommendation.recommendedOutcome === 'ALLOW' ? 'PASS' : memoryShadowRecommendation.recommendedOutcome, actual_outcome: gateOutcome, confidence: memoryShadowRecommendation.confidence, reason_codes: [...memoryShadowRecommendation.reasonCodes], } : undefined; if (memoryShadowRecommendation) { if (params.silent !== true) { process.stdout.write( `[pumuki][memory-shadow] recommended=${memoryShadowRecommendation.recommendedOutcome}` + ` confidence=${memoryShadowRecommendation.confidence.toFixed(2)}` + ` reasons=${memoryShadowRecommendation.reasonCodes.join(',')}\n` ); } } dependencies.emitPlatformGateEvidence({ stage: params.policy.stage, auditMode, policyTrace: params.policyTrace, findings: findingsWithWaiver, gateOutcome, filesScanned, evaluationMetrics, rulesCoverage, ...(tddBddSnapshot ? { tddBdd: tddBddSnapshot } : {}), ...(memoryShadow ? { memoryShadow } : {}), repoRoot, detectedPlatforms, skillsRuleSet, projectRules, heuristicRules, evidenceService: evidence, sddDecision, }); if (gateOutcome === 'BLOCK') { const notificationFindings = resolveBlockingNotificationFindings(findingsWithWaiver); const primaryBlockingFinding = notificationFindings[0] ?? resolvePrimaryBlockingFinding(findingsWithWaiver); if ( primaryBlockingFinding && ( params.policy.stage === 'PRE_COMMIT' || params.policy.stage === 'PRE_PUSH' || params.policy.stage === 'CI' ) ) { const notificationResult = dependencies.notifyGateBlocked({ repoRoot, stage: params.policy.stage, totalViolations: findingsWithWaiver.length, causeCode: primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId, causeMessage: primaryBlockingFinding.message, remediation: primaryBlockingFinding.expected_fix ?? 'Corrige la causa bloqueante y reejecuta el gate.', blockingCauses: notificationFindings.map((finding) => ({ code: finding.code ?? finding.ruleId, message: finding.message, ruleId: finding.ruleId, file: finding.filePath, remediation: finding.expected_fix, })), }); process.stderr.write( `[pumuki][blocked] code=${primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId} ` + `stage=${params.policy.stage} reason=${primaryBlockingFinding.message}\n` ); for (const finding of notificationFindings) { process.stderr.write( `[pumuki][blocked-cause] code=${finding.code ?? finding.ruleId}` + ` rule=${finding.ruleId}` + ` file=${finding.filePath ?? 'n/a'}` + ` reason=${finding.message}` + ` fix=${finding.expected_fix ?? 'Corrige la causa bloqueante y reejecuta el gate.'}\n` ); } process.stderr.write( `[pumuki][notification] delivered=${notificationResult.delivered ? 'yes' : 'no'} ` + `reason=${notificationResult.reason}\n` ); } if (params.silent !== true) { dependencies.printGateFindings(findingsWithWaiver); } return 1; } return 0; }