// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only
/**
 * Gate Runner Functions
 *
 * WU-1647: Extracted gate runner functions that execute individual gates.
 * Each runner takes a GateLogContext and returns a result with ok/duration.
 *
 * @module gates-runners
 */
import { spawnSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, writeFileSync, writeSync } from 'node:fs';
import { access } from 'node:fs/promises';
import { createRequire } from 'node:module';
import path from 'node:path';
import { getChangedLintableFiles } from '@lumenflow/core/incremental-lint';
import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/incremental-test';
import { createGitForPath } from '@lumenflow/core/git-adapter';
import { resolveGatesCommands, resolveTestRunner, getGatesSection, resolveScopedUnitTestExecutionPlan, formatScopedUnitTestFallbackMessage, } from '@lumenflow/core/gates-config';
import { CoChangeRuleConfigSchema, ConditionalCommandConfigSchema, } from '@lumenflow/core/config-schema';
import { validateBacklogSync } from '@lumenflow/core/validators/backlog-sync';
import { DELIVERY_REVIEW_ARTIFACT_DIR, DeliveryReviewResultSchema, calculateDeliveryReviewVerdict, createWuPaths, flattenDeliveryReviewAcceptanceCriteria, isCodePathCoveredByChangedFiles, isDeliveryReviewSkippedForType, isPathLikeTestEntry, readWU, resolveDeliveryReviewRuntimeConfig, validateClaimValidation, } from '@lumenflow/core';
import { runSupabaseDocsLinter } from '@lumenflow/core/validators/supabase-docs-linter';
import { BRANCHES, PACKAGES, PKG_MANAGER, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, GIT_REFS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, EXIT_CODES, FILE_SYSTEM, } from '@lumenflow/core/wu-constants';
import { runLaneHealthCheck } from './lane-health.js';
import { pnpmCmd, pnpmRun, run, makeGateLogger, filterExistingFiles, getChangedFilesForIncremental, detectCurrentWUForCwd, buildPrettierCheckCommand, resolveFilterableDocsOnlyPackages, } from './gates-utils.js';
import { resolveFormatCheckPlan, resolveLintPlan, resolveTestPlan, resolveSpecLinterPlan, isTestConfigFile, } from './gates-plan-resolvers.js';
const require = createRequire(import.meta.url);
const micromatch = require('micromatch');
const CO_CHANGE_SEVERITY = {
    WARN: 'warn',
    ERROR: 'error',
    OFF: 'off',
};
const DEFAULT_MIGRATION_VERIFY_PATTERNS = [
    '**/schema*.sql',
    '**/schema*.prisma',
    '**/db/schema*.ts',
    '**/db/schema*.js',
    '**/db/schema/**/*.ts',
    '**/db/schema/**/*.js',
    '**/drizzle/schema*.ts',
    '**/migrations/**',
    '**/migration/**',
    'supabase/schema.sql',
    'supabase/migrations/**',
    'prisma/schema.prisma',
    'prisma/migrations/**',
    'drizzle/migrations/**',
];
const DELIVERY_REVIEW_HIGH_RISK_PATH_PATTERN = /(auth|permission|secret|token|credential|billing|payment|migration|schema|policy|sandbox|security|hook)/i;
const DELIVERY_REVIEW_TEST_PATH_PATTERN = /(^|\/)(__tests__\/|.*\.(test|spec)\.[A-Za-z0-9]+$)/i;
function asStringArray(value) {
    if (!Array.isArray(value)) {
        return [];
    }
    return value
        .filter((entry) => typeof entry === 'string')
        .map((entry) => entry.trim())
        .filter(Boolean);
}
function getDeclaredAutomatedTestPaths(tests) {
    const testsRecord = tests;
    return ['unit', 'e2e', 'integration']
        .flatMap((bucket) => asStringArray(testsRecord?.[bucket]))
        .filter(isPathLikeTestEntry);
}
function hasAutomatedTestEvidence(options) {
    if (options.changedFiles.some((filePath) => DELIVERY_REVIEW_TEST_PATH_PATTERN.test(filePath))) {
        return true;
    }
    return options.declaredTestPaths.some((testPath) => isCodePathCoveredByChangedFiles({
        codePath: testPath,
        changedFiles: options.changedFiles,
    }));
}
function collectDeliveryReviewFindings(options) {
    const findings = [];
    if (options.missingCodePaths.length > 0) {
        findings.push({
            severity: 'high',
            title: 'Declared code_paths are not fully covered by changed files',
            detail: `Missing delivery evidence for: ${options.missingCodePaths.join(', ')}`,
            fileRefs: options.missingCodePaths,
        });
    }
    const riskyPaths = options.changedFiles.filter((filePath) => DELIVERY_REVIEW_HIGH_RISK_PATH_PATTERN.test(filePath));
    if (riskyPaths.length > 0 && !options.hasAutomatedEvidence) {
        findings.push({
            severity: 'high',
            title: 'High-risk paths changed without automated test evidence',
            detail: 'Changed files include high-risk delivery surfaces, but the working diff does not include ' +
                'matching automated test evidence.',
            fileRefs: riskyPaths,
        });
    }
    if (options.declaredTestPaths.length > 0 &&
        !options.hasAutomatedEvidence &&
        options.missingCodePaths.length === 0) {
        findings.push({
            severity: 'medium',
            title: 'Automated delivery evidence is unclear',
            detail: 'The WU declares automated test paths, but the changed files do not provide direct test ' +
                'evidence for this review.',
            fileRefs: options.declaredTestPaths,
        });
    }
    return findings;
}
function buildDeliveryReviewAcceptanceCriteria(options) {
    return options.criteria.map((criterion) => {
        const evidence = [];
        if (options.missingCodePaths.length === 0 && options.changedFiles.length > 0) {
            evidence.push(`Changed files cover declared delivery scope (${options.changedFiles.length} file${options.changedFiles.length === 1 ? '' : 's'}).`);
        }
        if (options.hasAutomatedEvidence) {
            evidence.push('Automated test evidence is present in the changed files.');
        }
        if (options.hasManualEvidence) {
            evidence.push('Manual verification steps are declared in the WU spec.');
        }
        let status = 'unclear';
        if (options.missingCodePaths.length > 0) {
            status = 'not_satisfied';
        }
        else if (options.hasAutomatedEvidence || options.hasManualEvidence) {
            status = 'satisfied';
        }
        return {
            criterion,
            status,
            evidence: evidence.length > 0 ? evidence : undefined,
        };
    });
}
function buildDeliveryReviewSummary(input) {
    const satisfiedCount = input.acceptanceCriteria.filter((criterion) => criterion.status === 'satisfied').length;
    const unclearCount = input.acceptanceCriteria.filter((criterion) => criterion.status === 'unclear').length;
    const blockedCount = input.acceptanceCriteria.filter((criterion) => criterion.status === 'not_satisfied').length;
    return (`verdict=${input.verdict}; findings=${input.findings.length}; ` +
        `acceptance:satisfied=${satisfiedCount},unclear=${unclearCount},not_satisfied=${blockedCount}`);
}
function writeDeliveryReviewArtifact(options) {
    const artifactPath = path.join(options.projectRoot, DELIVERY_REVIEW_ARTIFACT_DIR, `${options.wuId}.json`);
    mkdirSync(path.dirname(artifactPath), { recursive: true });
    writeFileSync(artifactPath, `${JSON.stringify(options.result, null, 2)}\n`, 'utf-8');
    return artifactPath;
}
function createDeliveryReviewFailureResult(options) {
    const completedAt = new Date().toISOString();
    return DeliveryReviewResultSchema.parse({
        wuId: options.wuId,
        verdict: 'FAIL',
        summary: options.summary,
        findings: [options.finding],
        acceptanceCriteria: [],
        metadata: {
            runtimeClient: options.runtimeClient,
            startedAt: options.startedAt,
            completedAt,
        },
    });
}
/**
 * WU-2368: Built-in default co-change rules for database-affecting work.
 *
 * These rules enforce that database schema changes are accompanied by
 * migration files. They are merged with user-configured co_change rules
 * unless `include_builtin_co_change_defaults` is set to false.
 */
export const DEFAULT_DB_CO_CHANGE_RULES = [
    {
        name: 'schema-requires-migration',
        trigger_patterns: [
            '**/schema*.sql',
            '**/schema*.prisma',
            '**/db/schema*.ts',
            '**/db/schema*.js',
            '**/drizzle/schema*.ts',
            'supabase/schema.sql',
            'prisma/schema.prisma',
        ],
        require_patterns: [
            '**/migrations/**',
            '**/migration/**',
            'supabase/migrations/**',
            'prisma/migrations/**',
            'drizzle/migrations/**',
        ],
        severity: 'error',
        guidance: 'Database schema changes require a companion migration file. ' +
            'Create a migration in the appropriate migrations directory ' +
            '(e.g., supabase/migrations/, prisma/migrations/, or drizzle/migrations/). ' +
            'If this is a documentation-only schema change, set the co-change rule ' +
            'severity to "off" in workspace.yaml gates.co_change.',
    },
];
// ── Format check gate ──────────────────────────────────────────────────
export async function runFormatCheckGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const effectiveCwd = cwd ?? process.cwd();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    let git;
    let isMainBranch;
    try {
        git = createGitForPath(effectiveCwd);
        const currentBranch = await git.getCurrentBranch();
        isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
    }
    catch (error) {
        logLine(`\u26A0\uFE0F  Failed to determine branch for format check: ${error.message}`);
        const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
        return { ...result, duration: Date.now() - start, fileCount: -1 };
    }
    if (isMainBranch) {
        logLine('\uD83D\uDCCB On main branch - running full format check');
        const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
        return { ...result, duration: Date.now() - start, fileCount: -1 };
    }
    let changedFiles = [];
    let fileListError = false;
    try {
        changedFiles = await getChangedFilesForIncremental({ git });
    }
    catch (error) {
        fileListError = true;
        logLine(`\u26A0\uFE0F  Failed to determine changed files for format check: ${error.message}`);
    }
    const plan = resolveFormatCheckPlan({ changedFiles, fileListError, cwd: effectiveCwd });
    if (plan.mode === 'skip') {
        logLine('\n> format:check (incremental)\n');
        logLine('\u2705 No files changed - skipping format check');
        return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
    }
    if (plan.mode === 'full') {
        const reason = plan.reason === 'prettier-config'
            ? ' (prettier config changed)'
            : plan.reason === 'file-list-error'
                ? ' (file list unavailable)'
                : '';
        logLine(`\uD83D\uDCCB Running full format check${reason}`);
        const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd: effectiveCwd });
        return { ...result, duration: Date.now() - start, fileCount: -1 };
    }
    const existingFiles = await filterExistingFiles(plan.files, effectiveCwd);
    if (existingFiles.length === 0) {
        logLine('\n> format:check (incremental)\n');
        logLine('\u2705 All changed files were deleted - skipping format check');
        return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
    }
    logLine(`\n> format:check (incremental: ${existingFiles.length} files)\n`);
    const result = run(buildPrettierCheckCommand(existingFiles), { agentLog, cwd: effectiveCwd });
    return {
        ...result,
        duration: Date.now() - start,
        fileCount: existingFiles.length,
        filesChecked: existingFiles,
    };
}
// ── Lint gate ──────────────────────────────────────────────────────────
/**
 * Run incremental ESLint on changed files only
 * Falls back to full lint if on main branch or if incremental fails
 */
export async function runIncrementalLint({ agentLog, cwd, }) {
    const start = Date.now();
    const logLine = (line) => {
        if (!agentLog) {
            console.log(line);
            return;
        }
        writeSync(agentLog.logFd, `${line}\n`);
    };
    try {
        // Check if we're on main branch
        const git = createGitForPath(cwd);
        const currentBranch = await git.getCurrentBranch();
        const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
        if (isMainBranch) {
            logLine('\uD83D\uDCCB On main branch - running full lint');
            const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
            return { ...result, fileCount: -1 };
        }
        const changedFiles = await getChangedLintableFiles({ git });
        const plan = resolveLintPlan({ isMainBranch, changedFiles });
        if (plan.mode === 'skip') {
            logLine('\n> ESLint (incremental)\n');
            logLine('\u2705 No lintable files changed - skipping lint');
            return { ok: true, duration: Date.now() - start, fileCount: 0 };
        }
        if (plan.mode === 'full') {
            logLine('\uD83D\uDCCB Running full lint (incremental plan forced full)');
            const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
            return { ...result, fileCount: -1 };
        }
        const existingFiles = await filterExistingFiles(plan.files, cwd);
        if (existingFiles.length === 0) {
            logLine('\n> ESLint (incremental)\n');
            logLine('\u2705 All changed files were deleted - skipping lint');
            return { ok: true, duration: Date.now() - start, fileCount: 0 };
        }
        logLine(`\n> ESLint (incremental: ${existingFiles.length} files)\n`);
        logLine(`Files to lint:\n  ${existingFiles.join('\n  ')}\n`);
        const result = spawnSync(PKG_MANAGER, [
            ESLINT_COMMANDS.ESLINT,
            ESLINT_FLAGS.MAX_WARNINGS,
            ESLINT_DEFAULTS.MAX_WARNINGS,
            ESLINT_FLAGS.NO_WARN_IGNORED,
            ESLINT_FLAGS.CACHE,
            ESLINT_FLAGS.CACHE_STRATEGY,
            CACHE_STRATEGIES.CONTENT,
            ESLINT_FLAGS.CACHE_LOCATION,
            '.eslintcache',
            ESLINT_FLAGS.PASS_ON_UNPRUNED,
            ...existingFiles,
        ], agentLog
            ? {
                stdio: ['ignore', agentLog.logFd, agentLog.logFd],
                encoding: FILE_SYSTEM.ENCODING,
                cwd,
            }
            : {
                stdio: 'inherit',
                encoding: FILE_SYSTEM.ENCODING,
                cwd,
            });
        const duration = Date.now() - start;
        return {
            ok: result.status === EXIT_CODES.SUCCESS,
            duration,
            fileCount: existingFiles.length,
        };
    }
    catch (error) {
        console.error('\u26A0\uFE0F  Incremental lint failed, falling back to full lint:', error.message);
        const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
        return { ...result, fileCount: -1 };
    }
}
// ── Test gates ─────────────────────────────────────────────────────────
const DEFAULT_INCREMENTAL_BASE_BRANCH = GIT_REFS.ORIGIN_MAIN;
export function buildStableVitestIncrementalCommand(baseBranch = DEFAULT_INCREMENTAL_BASE_BRANCH) {
    return pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch }));
}
export function resolveIncrementalTestCommand({ testRunner, configuredIncrementalCommand, baseBranch = DEFAULT_INCREMENTAL_BASE_BRANCH, }) {
    const normalizedConfiguredCommand = configuredIncrementalCommand?.trim();
    if (testRunner === 'vitest') {
        if (!normalizedConfiguredCommand) {
            return buildStableVitestIncrementalCommand(baseBranch);
        }
        const isVitestChangedCommand = normalizedConfiguredCommand.includes('vitest') &&
            normalizedConfiguredCommand.includes('--changed');
        if (isVitestChangedCommand) {
            return buildStableVitestIncrementalCommand(baseBranch);
        }
    }
    return normalizedConfiguredCommand ?? null;
}
/**
 * Run changed tests using configured test runner's incremental mode.
 * WU-1356: Updated to use configured commands from gates-config.
 */
export async function runChangedTests({ agentLog, cwd, scopedTestPaths = [], }) {
    const start = Date.now();
    // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
    const logLine = (line) => {
        if (!agentLog) {
            console.log(line);
            return;
        }
        writeSync(agentLog.logFd, `${line}\n`);
    };
    // WU-1356: Get configured commands
    const gatesCommands = resolveGatesCommands(cwd);
    const testRunner = resolveTestRunner(cwd);
    const normalizedScopedTestPaths = scopedTestPaths
        .filter((testPath) => typeof testPath === 'string')
        .map((testPath) => testPath.trim())
        .filter(Boolean);
    try {
        const scopedExecutionPlan = resolveScopedUnitTestExecutionPlan({
            projectRoot: cwd,
            scopedUnitTests: normalizedScopedTestPaths,
        });
        if (scopedExecutionPlan?.mode === 'direct') {
            logLine(`\n> Running scoped tests from WU tests.unit (${normalizedScopedTestPaths.length})\n`);
            const result = spawnSync(PKG_MANAGER, scopedExecutionPlan.args, agentLog
                ? {
                    stdio: ['ignore', agentLog.logFd, agentLog.logFd],
                    encoding: FILE_SYSTEM.ENCODING,
                    cwd,
                }
                : {
                    stdio: 'inherit',
                    encoding: FILE_SYSTEM.ENCODING,
                    cwd,
                });
            return {
                ok: result.status === EXIT_CODES.SUCCESS,
                duration: Date.now() - start,
                isIncremental: true,
            };
        }
        if (scopedExecutionPlan?.mode === 'default-flow') {
            logLine(`⚠️  ${formatScopedUnitTestFallbackMessage(scopedExecutionPlan)}`);
        }
        const git = createGitForPath(cwd);
        const currentBranch = await git.getCurrentBranch();
        const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
        if (isMainBranch) {
            logLine('\uD83D\uDCCB On main branch - running full test suite');
            const result = run(gatesCommands.test_full, { agentLog, cwd });
            return { ...result, isIncremental: false };
        }
        let changedFiles = [];
        let fileListError = false;
        try {
            changedFiles = await getChangedFilesForIncremental({ git });
        }
        catch (error) {
            fileListError = true;
            logLine(`\u26A0\uFE0F  Failed to determine changed files for tests: ${error.message}`);
        }
        const hasConfigChange = !fileListError && changedFiles.some(isTestConfigFile);
        const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
        const untrackedFiles = untrackedOutput
            .split(/\r?\n/)
            .map((f) => f.trim())
            .filter(Boolean);
        const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
        const hasUntrackedCode = untrackedCodeFiles.length > 0;
        const plan = resolveTestPlan({
            isMainBranch,
            hasUntrackedCode,
            hasConfigChange,
            fileListError,
        });
        if (plan.mode === 'full') {
            if (plan.reason === 'untracked-code') {
                const preview = untrackedCodeFiles.slice(0, 5).join(', ');
                logLine(`\u26A0\uFE0F  Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? '...' : ''}`);
            }
            else if (plan.reason === 'test-config') {
                logLine('\u26A0\uFE0F  Test config changes detected - running full test suite');
            }
            else if (plan.reason === 'file-list-error') {
                logLine('\u26A0\uFE0F  Changed file list unavailable - running full test suite');
            }
            logLine('\uD83D\uDCCB Running full test suite to avoid missing coverage');
            const result = run(gatesCommands.test_full, { agentLog, cwd });
            return { ...result, duration: Date.now() - start, isIncremental: false };
        }
        // WU-1356: Use configured incremental test command
        logLine(`\n> Running tests (${testRunner} --changed)\n`);
        const incrementalCommand = resolveIncrementalTestCommand({
            testRunner,
            configuredIncrementalCommand: gatesCommands.test_incremental,
        });
        if (incrementalCommand) {
            if (testRunner === 'vitest' &&
                incrementalCommand !== gatesCommands.test_incremental?.trim()) {
                logLine('ℹ️  Using hardened vitest incremental command for worker stability');
            }
            const result = run(incrementalCommand, { agentLog, cwd });
            return { ...result, duration: Date.now() - start, isIncremental: true };
        }
        // For other runners without configured incremental, fall back to full
        logLine('\u26A0\uFE0F  No incremental test command configured, running full suite');
        const result = run(gatesCommands.test_full, { agentLog, cwd });
        return { ...result, duration: Date.now() - start, isIncremental: false };
    }
    catch (error) {
        console.error('\u26A0\uFE0F  Changed tests failed, falling back to full suite:', error.message);
        const result = run(gatesCommands.test_full, { agentLog, cwd });
        return { ...result, isIncremental: false };
    }
}
/**
 * Safety-critical test file patterns (relative to apps/web).
 */
export const SAFETY_CRITICAL_TEST_FILES = [
    // Privacy detection tests
    'src/lib/llm/__tests__/privacyDetector.test.ts',
    // Escalation trigger tests
    'src/lib/llm/__tests__/escalationTrigger.test.ts',
    'src/components/escalation/__tests__/EscalationHistory.test.tsx',
    // Constitutional enforcer tests
    'src/lib/llm/__tests__/constitutionalEnforcer.test.ts',
    // Safe prompt wrapper tests
    'src/lib/llm/__tests__/safePromptWrapper.test.ts',
    // Crisis/emergency handling tests
    'src/lib/prompts/__tests__/golden-crisis.test.ts',
];
/**
 * WU-2062: Run safety-critical tests
 * These tests ALWAYS run regardless of which files changed.
 */
export async function runSafetyCriticalTests({ agentLog, cwd, }) {
    const start = Date.now();
    // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
    const logLine = (line) => {
        if (!agentLog) {
            console.log(line);
            return;
        }
        writeSync(agentLog.logFd, `${line}\n`);
    };
    // WU-1006: Skip safety-critical tests if apps/web doesn't exist (repo-agnostic)
    const webDir = path.join(cwd, DIRECTORIES.APPS_WEB);
    try {
        await access(webDir);
    }
    catch {
        logLine('\n> Safety-critical tests skipped (apps/web not present)\n');
        return { ok: true, duration: Date.now() - start, testCount: 0 };
    }
    try {
        logLine('\n> Safety-critical tests (always run)\n');
        logLine(`Test files: ${SAFETY_CRITICAL_TEST_FILES.length} files\n`);
        const result = spawnSync(PKG_MANAGER, [
            'vitest',
            'run',
            '--project',
            PACKAGES.WEB,
            '--reporter=verbose',
            ...SAFETY_CRITICAL_TEST_FILES,
            '--passWithNoTests',
        ], agentLog
            ? {
                stdio: ['ignore', agentLog.logFd, agentLog.logFd],
                encoding: FILE_SYSTEM.ENCODING,
                cwd,
            }
            : {
                stdio: 'inherit',
                encoding: FILE_SYSTEM.ENCODING,
                cwd,
            });
        const duration = Date.now() - start;
        return {
            ok: result.status === EXIT_CODES.SUCCESS,
            duration,
            testCount: SAFETY_CRITICAL_TEST_FILES.length,
        };
    }
    catch (error) {
        console.error('\u26A0\uFE0F  Safety-critical tests failed:', error.message);
        return { ok: false, duration: Date.now() - start, testCount: 0 };
    }
}
/**
 * WU-2062: Run integration tests for high-risk changes
 */
export async function runIntegrationTests({ agentLog, cwd, }) {
    const start = Date.now();
    // eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
    const logLine = (line) => {
        if (!agentLog) {
            console.log(line);
            return;
        }
        writeSync(agentLog.logFd, `${line}\n`);
    };
    try {
        logLine('\n> Integration tests (high-risk changes detected)\n');
        // WU-1415: vitest doesn't support --include flag
        const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "'**/*.integration.*'", "'**/golden-*.test.*'")}`, { agentLog, cwd });
        const duration = Date.now() - start;
        return {
            ok: result.ok,
            duration,
        };
    }
    catch (error) {
        console.error('\u26A0\uFE0F  Integration tests failed:', error.message);
        return { ok: false, duration: Date.now() - start };
    }
}
// ── Co-change gate ────────────────────────────────────────────────────
const CoChangeRulesConfigSchema = CoChangeRuleConfigSchema.array();
const ConditionalCommandsConfigSchema = ConditionalCommandConfigSchema.array();
function hasPatternMatch(changedFiles, patterns) {
    return changedFiles.some((filePath) => micromatch.isMatch(filePath, patterns));
}
export function getResolvedCoChangeRules(projectRoot, logWarning) {
    const gatesSection = getGatesSection(projectRoot);
    const parsedRules = CoChangeRulesConfigSchema.safeParse(gatesSection?.co_change ?? []);
    if (!parsedRules.success) {
        logWarning('Invalid gates.co_change config; skipping co-change rules.');
        return [];
    }
    const includeDefaults = gatesSection?.include_builtin_co_change_defaults !== false;
    const userRules = parsedRules.data;
    const mergedRules = includeDefaults ? [...DEFAULT_DB_CO_CHANGE_RULES, ...userRules] : userRules;
    return resolveGuidanceRefs(mergedRules, projectRoot, logWarning);
}
export function getMatchingCoChangeRulesForPaths(options) {
    return options.rules.filter((rule) => rule.severity !== CO_CHANGE_SEVERITY.OFF &&
        hasPatternMatch(options.filePaths, rule.trigger_patterns));
}
export function shouldRunMigrationVerifyForChanges(options) {
    return hasPatternMatch(options.changedFiles, [...DEFAULT_MIGRATION_VERIFY_PATTERNS]);
}
function resolveConditionalCommandGuidanceRefs(commands, projectRoot, logWarning) {
    return commands.map((command) => {
        if (!command.guidance_ref) {
            return command;
        }
        const refPath = path.resolve(projectRoot, command.guidance_ref);
        if (!existsSync(refPath)) {
            logWarning(`conditional command "${command.command}": guidance_ref "${command.guidance_ref}" not found; skipping file content`);
            return command;
        }
        const fileContent = readFileSync(refPath, 'utf-8').trim();
        const combined = command.guidance ? `${command.guidance}\n${fileContent}` : fileContent;
        return { ...command, guidance: combined };
    });
}
export function getResolvedConditionalCommands(projectRoot, logWarning) {
    const gatesSection = getGatesSection(projectRoot);
    const parsedCommands = ConditionalCommandsConfigSchema.safeParse(gatesSection?.conditional_commands ?? []);
    if (!parsedCommands.success) {
        logWarning('Invalid gates.conditional_commands config; skipping conditional commands.');
        return [];
    }
    return resolveConditionalCommandGuidanceRefs(parsedCommands.data, projectRoot, logWarning);
}
export function getMatchingConditionalCommandsForPaths(options) {
    return options.commands.filter((command) => command.severity !== CO_CHANGE_SEVERITY.OFF &&
        hasPatternMatch(options.filePaths, command.trigger_patterns));
}
function appendConditionalCommandGuidance(message, command) {
    if (!command.guidance) {
        return message;
    }
    return `${message}\n  Guidance: ${command.guidance}`;
}
export function evaluateConditionalCommandFailure(command) {
    const message = appendConditionalCommandGuidance(`conditional command failed: ${command.command}`, command);
    if (command.severity === CO_CHANGE_SEVERITY.WARN) {
        return {
            errors: [],
            warnings: [`warn-level ${message}`],
        };
    }
    return {
        errors: [message],
        warnings: [],
    };
}
export function evaluateCoChangeRules({ changedFiles, rules, }) {
    const errors = [];
    const warnings = [];
    for (const rule of rules) {
        if (rule.severity === CO_CHANGE_SEVERITY.OFF) {
            continue;
        }
        const triggerMatched = hasPatternMatch(changedFiles, rule.trigger_patterns);
        if (!triggerMatched) {
            continue;
        }
        const requireMatched = hasPatternMatch(changedFiles, rule.require_patterns);
        if (requireMatched) {
            continue;
        }
        let message = `co-change "${rule.name}" violated: ` +
            `trigger matched (${rule.trigger_patterns.join(', ')}) but required patterns missing ` +
            `(${rule.require_patterns.join(', ')})`;
        // WU-2368: Append actionable guidance when present
        if (rule.guidance) {
            message += `\n  Guidance: ${rule.guidance}`;
        }
        if (rule.severity === CO_CHANGE_SEVERITY.WARN) {
            warnings.push(message);
            continue;
        }
        errors.push(message);
    }
    return { errors, warnings };
}
/**
 * WU-2446: Resolve guidance_ref file references into inline guidance content.
 * For each rule with a guidance_ref, reads the file and appends its content
 * to the rule's guidance field. Returns a new array (does not mutate input).
 * Logs a warning for missing files but does not fail.
 */
export function resolveGuidanceRefs(rules, projectRoot, logWarning) {
    return rules.map((rule) => {
        if (!rule.guidance_ref)
            return rule;
        const refPath = path.resolve(projectRoot, rule.guidance_ref);
        if (!existsSync(refPath)) {
            logWarning(`co-change "${rule.name}": guidance_ref "${rule.guidance_ref}" not found; skipping file content`);
            return rule;
        }
        const fileContent = readFileSync(refPath, 'utf-8').trim();
        const combined = rule.guidance ? `${rule.guidance}\n${fileContent}` : fileContent;
        return { ...rule, guidance: combined };
    });
}
export async function runCoChangeGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const effectiveCwd = cwd ?? process.cwd();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    logLine('\n> Co-change check\n');
    const rules = getResolvedCoChangeRules(effectiveCwd, (msg) => logLine(`⚠️  ${msg}`));
    if (rules.length === 0) {
        logLine('ℹ️  No co-change rules configured; skipping.');
        return { ok: true, duration: Date.now() - start };
    }
    const git = createGitForPath(effectiveCwd);
    const changedFiles = await getChangedFilesForIncremental({ git });
    if (changedFiles.length === 0) {
        logLine('ℹ️  No changed files detected; skipping co-change checks.');
        return { ok: true, duration: Date.now() - start };
    }
    const evaluation = evaluateCoChangeRules({ changedFiles, rules });
    for (const warning of evaluation.warnings) {
        logLine(`⚠️  ${warning}`);
    }
    for (const error of evaluation.errors) {
        logLine(`❌ ${error}`);
    }
    if (evaluation.errors.length > 0) {
        logLine('co-change check failed.');
        return { ok: false, duration: Date.now() - start };
    }
    logLine('co-change check passed.');
    return { ok: true, duration: Date.now() - start };
}
export async function runMigrationVerifyGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const effectiveCwd = cwd ?? process.cwd();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    logLine('\n> Migration verification\n');
    const configuredCommand = resolveGatesCommands(effectiveCwd).migration_verify.trim();
    if (configuredCommand.length === 0) {
        logLine('ℹ️  No gates.commands.migration_verify configured; skipping.');
        return { ok: true, duration: Date.now() - start };
    }
    const git = createGitForPath(effectiveCwd);
    const changedFiles = await getChangedFilesForIncremental({ git });
    if (changedFiles.length === 0) {
        logLine('ℹ️  No changed files detected; skipping migration verification.');
        return { ok: true, duration: Date.now() - start };
    }
    const conditionalCommands = getResolvedConditionalCommands(effectiveCwd, (msg) => logLine(`⚠️  ${msg}`));
    const matchingConditionalCommands = getMatchingConditionalCommandsForPaths({
        filePaths: changedFiles,
        commands: conditionalCommands,
    });
    for (const conditionalCommand of matchingConditionalCommands) {
        const result = run(conditionalCommand.command, { agentLog, cwd: effectiveCwd });
        if (result.ok) {
            continue;
        }
        const evaluation = evaluateConditionalCommandFailure(conditionalCommand);
        for (const warning of evaluation.warnings) {
            logLine(`⚠️  ${warning}`);
        }
        for (const error of evaluation.errors) {
            logLine(`❌ ${error}`);
        }
        if (evaluation.errors.length > 0) {
            return { ok: false, duration: Date.now() - start };
        }
    }
    if (!shouldRunMigrationVerifyForChanges({ changedFiles })) {
        logLine('ℹ️  No schema or migration paths changed; skipping migration verification.');
        return { ok: true, duration: Date.now() - start };
    }
    const result = run(configuredCommand, { agentLog, cwd: effectiveCwd });
    if (!result.ok) {
        logLine('❌ Migration verification failed.');
        logLine('Run the configured migration verification command after applying pending migrations, then retry wu:prep.');
        return { ok: false, duration: Date.now() - start };
    }
    logLine('Migration verification passed.');
    return { ok: true, duration: Date.now() - start };
}
export async function runDeliveryReviewGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const effectiveCwd = cwd ?? process.cwd();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    const startedAt = new Date().toISOString();
    logLine('\n> Delivery review\n');
    const runtime = resolveDeliveryReviewRuntimeConfig({
        projectRoot: effectiveCwd,
        env: process.env,
    });
    if (!runtime.gate.enabled) {
        logLine('ℹ️  delivery_review gate is not enabled in software_delivery.gates; skipping.');
        return { ok: true, duration: Date.now() - start };
    }
    if (!runtime.feature.enabled) {
        logLine(`ℹ️  delivery_review is disabled for runtime client ${runtime.clientName}; skipping.`);
        return { ok: true, duration: Date.now() - start };
    }
    const wuId = await detectCurrentWUForCwd(effectiveCwd);
    if (!wuId) {
        logLine('⚠️  Unable to detect current WU; skipping delivery review.');
        return { ok: true, duration: Date.now() - start };
    }
    let doc;
    try {
        const wuPath = createWuPaths({ projectRoot: effectiveCwd }).WU(wuId);
        doc = readWU(path.isAbsolute(wuPath) ? wuPath : path.join(effectiveCwd, wuPath), wuId);
    }
    catch (error) {
        const failure = createDeliveryReviewFailureResult({
            wuId,
            runtimeClient: runtime.clientName,
            startedAt,
            summary: 'delivery_review could not load the current WU spec.',
            finding: {
                severity: 'critical',
                title: 'WU spec could not be loaded for delivery review',
                detail: error instanceof Error ? error.message : String(error),
            },
        });
        const artifactPath = writeDeliveryReviewArtifact({
            projectRoot: effectiveCwd,
            wuId,
            result: failure,
        });
        logLine(`❌ ${failure.summary}`);
        logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
        return { ok: false, duration: Date.now() - start };
    }
    if (isDeliveryReviewSkippedForType({ gate: runtime.gate, wuType: doc.type })) {
        const result = DeliveryReviewResultSchema.parse({
            wuId,
            verdict: 'PASS',
            summary: `Skipped delivery review because WU type "${doc.type}" matched ` +
                'software_delivery.gates.delivery_review.skip_types.',
            findings: [],
            acceptanceCriteria: [],
            metadata: {
                runtimeClient: runtime.clientName,
                startedAt,
                completedAt: new Date().toISOString(),
            },
        });
        const artifactPath = writeDeliveryReviewArtifact({
            projectRoot: effectiveCwd,
            wuId,
            result,
        });
        logLine(`ℹ️  ${result.summary}`);
        logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
        return { ok: true, duration: Date.now() - start };
    }
    let changedFiles;
    try {
        changedFiles = await getChangedFilesForIncremental({ git: createGitForPath(effectiveCwd) });
    }
    catch (error) {
        const failure = createDeliveryReviewFailureResult({
            wuId,
            runtimeClient: runtime.clientName,
            startedAt,
            summary: 'delivery_review could not resolve changed files for this WU.',
            finding: {
                severity: 'critical',
                title: 'Changed files could not be resolved',
                detail: error instanceof Error ? error.message : String(error),
            },
        });
        const artifactPath = writeDeliveryReviewArtifact({
            projectRoot: effectiveCwd,
            wuId,
            result: failure,
        });
        logLine(`❌ ${failure.summary}`);
        logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
        return { ok: false, duration: Date.now() - start };
    }
    const codePaths = asStringArray(doc.code_paths);
    const missingCodePaths = codePaths.filter((codePath) => !isCodePathCoveredByChangedFiles({ codePath, changedFiles }));
    const declaredTestPaths = getDeclaredAutomatedTestPaths(doc.tests);
    const manualEvidence = asStringArray(doc.tests?.manual);
    const criteria = flattenDeliveryReviewAcceptanceCriteria(doc.acceptance);
    const hasAutomatedEvidence = hasAutomatedTestEvidence({
        changedFiles,
        declaredTestPaths,
    });
    const findings = collectDeliveryReviewFindings({
        changedFiles,
        missingCodePaths,
        hasAutomatedEvidence,
        declaredTestPaths,
    });
    const acceptanceCriteria = buildDeliveryReviewAcceptanceCriteria({
        criteria,
        missingCodePaths,
        hasAutomatedEvidence,
        hasManualEvidence: manualEvidence.length > 0,
        changedFiles,
    });
    const verdict = calculateDeliveryReviewVerdict({ findings, acceptanceCriteria });
    const summary = buildDeliveryReviewSummary({
        verdict,
        findings,
        acceptanceCriteria,
    });
    const result = DeliveryReviewResultSchema.parse({
        wuId,
        verdict,
        summary,
        findings,
        acceptanceCriteria,
        metadata: {
            runtimeClient: runtime.clientName,
            startedAt,
            completedAt: new Date().toISOString(),
        },
    });
    const artifactPath = writeDeliveryReviewArtifact({
        projectRoot: effectiveCwd,
        wuId,
        result,
    });
    if (result.verdict === 'FAIL') {
        logLine(`❌ Delivery review ${result.summary}`);
        logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
        return { ok: false, duration: Date.now() - start };
    }
    if (result.verdict === 'PARTIAL') {
        logLine(`⚠️  Delivery review ${result.summary}`);
        logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
        return { ok: true, duration: Date.now() - start };
    }
    logLine(`Delivery review ${result.summary}`);
    logLine(`Artifact: ${path.relative(effectiveCwd, artifactPath).replaceAll(path.sep, '/')}`);
    return { ok: true, duration: Date.now() - start };
}
// ── Spec linter gate ───────────────────────────────────────────────────
export async function runSpecLinterGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const wuId = await detectCurrentWUForCwd(cwd);
    const plan = resolveSpecLinterPlan(wuId);
    if (plan.scopedWuId) {
        const scopedCmd = pnpmCmd('wu:validate', '--id', plan.scopedWuId);
        const scopedResult = run(scopedCmd, { agentLog, cwd });
        if (!scopedResult.ok) {
            return { ok: false, duration: Date.now() - start };
        }
        return { ok: true, duration: Date.now() - start };
    }
    if (!useAgentMode) {
        console.log('\u26A0\uFE0F  Unable to detect current WU; skipping scoped validation.');
    }
    else if (agentLog) {
        writeSync(agentLog.logFd, '\u26A0\uFE0F  Unable to detect current WU; skipping scoped validation.\n');
    }
    if (!plan.runGlobal) {
        return { ok: true, duration: Date.now() - start };
    }
    const fallbackResult = run(pnpmRun(SCRIPTS.SPEC_LINTER), { agentLog, cwd });
    return { ok: fallbackResult.ok, duration: Date.now() - start };
}
// ── Claim validation gate ───────────────────────────────────────────────
export async function runClaimValidationGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const effectiveCwd = cwd ?? process.cwd();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    logLine('\n> Claim validation\n');
    const wuId = await detectCurrentWUForCwd(effectiveCwd);
    if (!wuId) {
        logLine('\u26A0\uFE0F  Unable to detect current WU; skipping claim validation.');
        return { ok: true, duration: Date.now() - start };
    }
    const result = await validateClaimValidation({ cwd: effectiveCwd, wuId });
    if (result.warnings.length > 0) {
        for (const warning of result.warnings) {
            logLine(`\u26A0\uFE0F  ${warning}`);
        }
    }
    if (result.ok) {
        logLine(`Claim validation passed (${result.checkedClaims} checked claim${result.checkedClaims === 1 ? '' : 's'}).`);
        return { ok: true, duration: Date.now() - start };
    }
    logLine('\u274C Claim validation mismatches detected:');
    for (const mismatch of result.mismatches) {
        const specPath = path
            .relative(effectiveCwd, mismatch.specReference.filePath)
            .replaceAll(path.sep, '/');
        logLine(`  - Claim (${mismatch.claimId}): ${mismatch.claimText}`);
        logLine(`    Spec: ${specPath}:${mismatch.specReference.line} [${mismatch.specReference.id} ${mismatch.specReference.section}]`);
        for (const evidence of mismatch.evidence) {
            logLine(`    Evidence: ${evidence.filePath}:${evidence.line} ${evidence.lineText}`);
        }
        logLine(`    Hint: ${mismatch.remediationHint}`);
    }
    return { ok: false, duration: Date.now() - start };
}
// ── Backlog sync gate ──────────────────────────────────────────────────
export async function runBacklogSyncGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    logLine('\n> Backlog sync\n');
    const result = await validateBacklogSync({ cwd });
    if (result.errors.length > 0) {
        logLine('\u274C Backlog sync errors:');
        result.errors.forEach((error) => logLine(`  - ${error}`));
    }
    if (result.warnings.length > 0) {
        logLine('\u26A0\uFE0F  Backlog sync warnings:');
        result.warnings.forEach((warning) => logLine(`  - ${warning}`));
    }
    logLine(`Backlog sync summary: WU files=${result.wuCount}, Backlog refs=${result.backlogCount}`);
    return { ok: result.valid, duration: Date.now() - start };
}
// ── Supabase docs gate ─────────────────────────────────────────────────
export async function runSupabaseDocsGate({ agentLog, useAgentMode, cwd }) {
    const start = Date.now();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    logLine('\n> Supabase docs linter\n');
    const result = await runSupabaseDocsLinter({ cwd, logger: { log: logLine } });
    if (result.skipped) {
        logLine(`\u26A0\uFE0F  ${result.message ?? 'Supabase docs linter skipped.'}`);
    }
    else if (!result.ok) {
        logLine('\u274C Supabase docs linter failed.');
        (result.errors ?? []).forEach((error) => logLine(`  - ${error}`));
    }
    else {
        logLine(result.message ?? 'Supabase docs linter passed.');
    }
    return { ok: result.ok, duration: Date.now() - start };
}
// ── Lane health gate ───────────────────────────────────────────────────
/**
 * WU-1191: Run lane health check gate
 */
export async function runLaneHealthGate({ agentLog, useAgentMode, mode, cwd, }) {
    const start = Date.now();
    const logLine = makeGateLogger({ agentLog, useAgentMode });
    if (mode === 'off') {
        logLine('\n> Lane health check (skipped - mode: off)\n');
        return { ok: true, duration: Date.now() - start };
    }
    logLine(`\n> Lane health check (mode: ${mode})\n`);
    const report = runLaneHealthCheck({ projectRoot: cwd });
    if (!report.healthy) {
        logLine('\u26A0\uFE0F  Lane health issues detected:');
        if (report.overlaps.hasOverlaps) {
            logLine(`  - ${report.overlaps.overlaps.length} overlapping code_paths`);
        }
        if (report.gaps.hasGaps) {
            logLine(`  - ${report.gaps.uncoveredFiles.length} uncovered files`);
        }
        logLine(`  Run 'pnpm lane:health' for full report.`);
        if (mode === 'error') {
            return { ok: false, duration: Date.now() - start };
        }
        // mode === 'warn': report but don't fail
        logLine('  (mode: warn - not blocking)');
    }
    else {
        logLine('Lane health check passed.');
    }
    return { ok: true, duration: Date.now() - start };
}
// ── Docs-only filtered tests ───────────────────────────────────────────
/**
 * WU-1299: Run filtered tests for docs-only mode
 * WU-1356: Updated to use configured test command
 */
export async function runDocsOnlyFilteredTests({ packages, agentLog, cwd = process.cwd(), }) {
    const start = Date.now();
    const logLine = makeGateLogger({ agentLog, useAgentMode: !!agentLog, cwd });
    const filterablePackages = resolveFilterableDocsOnlyPackages(packages, { cwd });
    if (filterablePackages.length === 0) {
        logLine('\uD83D\uDCDD docs-only mode: no packages to test, skipping');
        return { ok: true, duration: Date.now() - start };
    }
    logLine(`\n> Tests (docs-only filtered: ${filterablePackages.join(', ')})\n`);
    // WU-1356: Use configured test command with filter
    const gatesCommands = resolveGatesCommands(cwd);
    // If there's a configured test_docs_only command, use it
    if (gatesCommands.test_docs_only) {
        const result = run(gatesCommands.test_docs_only, { agentLog, cwd });
        return { ok: result.ok, duration: Date.now() - start };
    }
    // Otherwise, use the full test command with filter args
    const filterArgs = filterablePackages.map((pkg) => `--filter=${pkg}`);
    const baseCmd = gatesCommands.test_full;
    const filteredCmd = `${baseCmd} ${filterArgs.join(' ')}`;
    const result = run(filteredCmd, { agentLog });
    return { ok: result.ok, duration: Date.now() - start };
}
//# sourceMappingURL=gates-runners.js.map