import { mkdirSync, unlinkSync, writeFileSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { chdir, cwd } from 'node:process'; import { execFileSync as runBinarySync } from 'node:child_process'; import { runRepoAndStagedPrePushGateSilent, runRepoGateSilent, runStagedGateSilent, runUnstagedGateSilent, runWorkingTreeGateSilent, runWorkingTreePrePushGateSilent, } from './framework-menu-gate-lib'; import { readMatrixOptionReport, type MatrixOptionId } from './framework-menu-matrix-evidence-lib'; import { extractRuleIdsFromEvidence, extractRuleMatchesFromEvidence, type CanaryEvidenceRuleMatch, } from './framework-menu-matrix-canary-evidence'; import { resolveConsumerMenuCanaryScenario } from './framework-menu-matrix-canary-scenario'; import type { ConsumerMenuCanaryPlatform, ConsumerMenuCanaryResult, ConsumerMenuCanaryStage, } from './framework-menu-matrix-canary-types'; export type { ConsumerMenuCanaryPlatform, ConsumerMenuCanaryResult, ConsumerMenuCanaryScenario, ConsumerMenuCanaryStage, } from './framework-menu-matrix-canary-types'; export { resolveConsumerMenuCanaryScenario } from './framework-menu-matrix-canary-scenario'; const stageCanaryPath = (repoRoot: string, relativePath: string): void => { runBinarySync('git', ['add', '--', relativePath], { cwd: repoRoot, encoding: 'utf8', }); }; const cleanupCanaryPathFromIndex = (repoRoot: string, relativePath: string): void => { try { runBinarySync('git', ['restore', '--staged', '--', relativePath], { cwd: repoRoot, encoding: 'utf8', stdio: 'ignore', }); return; } catch (restoreError) { void restoreError; } try { runBinarySync('git', ['rm', '--cached', '--ignore-unmatch', '--', relativePath], { cwd: repoRoot, encoding: 'utf8', stdio: 'ignore', }); } catch (removeError) { void removeError; } }; const runGateByOption = async (option: MatrixOptionId): Promise => { if (option === '1') { await runRepoGateSilent(); return; } if (option === '2') { await runRepoAndStagedPrePushGateSilent(); return; } if (option === '3') { await runStagedGateSilent(); return; } if (option === '4') { await runWorkingTreePrePushGateSilent(); return; } if (option === '9') { return; } if (option === '11') { await runStagedGateSilent(); return; } if (option === '12') { await runUnstagedGateSilent(); return; } if (option === '13') { await runWorkingTreeGateSilent(); return; } if (option === '14') { await runRepoGateSilent(); } }; export const runConsumerMenuCanary = async (params?: { repoRoot?: string; stage?: ConsumerMenuCanaryStage; platform?: ConsumerMenuCanaryPlatform; dependencies?: { runGate?: (option: MatrixOptionId) => Promise; readOptionReport?: typeof readMatrixOptionReport; extractRuleIds?: (repoRoot: string) => string[]; extractRuleMatches?: ( repoRoot: string, scenario: ReturnType ) => CanaryEvidenceRuleMatch[]; }; }): Promise => { const previousCwd = cwd(); const repoRoot = params?.repoRoot ?? previousCwd; const scenario = resolveConsumerMenuCanaryScenario({ stage: params?.stage, platform: params?.platform, }); const canaryRelativePath = scenario.canaryRelativePath; const canaryAbsolutePath = join(repoRoot, canaryRelativePath); const dependencies = params?.dependencies; const runGate = dependencies?.runGate ?? runGateByOption; const readOptionReport = dependencies?.readOptionReport ?? readMatrixOptionReport; const extractRuleIds = dependencies?.extractRuleIds ?? extractRuleIdsFromEvidence; const extractRuleMatches = dependencies?.extractRuleMatches ?? extractRuleMatchesFromEvidence; chdir(repoRoot); try { mkdirSync(dirname(canaryAbsolutePath), { recursive: true }); writeFileSync( canaryAbsolutePath, scenario.canarySource, 'utf8' ); if (scenario.option === '1' || scenario.option === '2' || scenario.option === '3' || scenario.option === '11') { stageCanaryPath(repoRoot, canaryRelativePath); } await runGate(scenario.option); const optionReport = readOptionReport(repoRoot, scenario.option); const ruleIds = extractRuleIds(repoRoot); const expectedRuleIds = [scenario.expectedRuleId, ...(scenario.equivalentRuleIds ?? [])]; const ruleMatches = extractRuleMatches(repoRoot, scenario); const canaryRuleMatched = ruleMatches.some((match) => expectedRuleIds.includes(match.ruleId) && match.filePath.replaceAll('\\', '/').endsWith(canaryRelativePath) ); const blocked = optionReport.outcome === 'BLOCK' && optionReport.diagnosis === 'violations-detected'; return { option: scenario.option, detected: blocked && optionReport.totalViolations > 0 && expectedRuleIds.some((ruleId) => ruleIds.includes(ruleId)) && canaryRuleMatched, outcome: optionReport.outcome, diagnosis: optionReport.diagnosis, canaryRelativePath, canaryRuleMatched, totalViolations: optionReport.totalViolations, filesScanned: optionReport.filesScanned, ruleIds, }; } finally { try { cleanupCanaryPathFromIndex(repoRoot, canaryRelativePath); } catch (cleanupError) { void cleanupError; } try { unlinkSync(canaryAbsolutePath); } catch (unlinkError) { void unlinkError; } chdir(previousCwd); } };