// engine/hooks/handlers/on-commit.ts — on-commit hook handler (SPEC-130, SPEC-138)
// Git post-commit integration: integrity check + auto-reconcile + drift detection.
import { execFile } from 'node:child_process';
import { readFile, writeFile, chmod, unlink } from 'node:fs/promises';
import { join } from 'node:path';
import { promisify } from 'node:util';
import { extractSpecAnnotations } from './on-save.js';
const execFileAsync = promisify(execFile);
// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------
const POST_COMMIT_HOOK_PATH = '.git/hooks/post-commit';
const PLANU_HOOK_MARKER = '# PLANU-HOOK-MARKER';
/** Default drift threshold: 10% */
const DEFAULT_DRIFT_THRESHOLD = 0.1;
const SPEC_PATH_PREFIX = 'planu/specs/';
const SPEC_FILE_PATTERNS = [
    /spec\.md$/,
    /technical\.md$/,
    /progress\.md$/,
    /SPEC-\d+/,
    /\.spec\.(ts|js|tsx|jsx)$/,
];
// ---------------------------------------------------------------------------
// Spec file classification (SPEC-138 — drift detection)
// ---------------------------------------------------------------------------
/**
 * Returns true if the given path is related to a spec file.
 */
export function isSpecRelatedFile(path) {
    if (path.startsWith(SPEC_PATH_PREFIX)) {
        return true;
    }
    return SPEC_FILE_PATTERNS.some((pattern) => pattern.test(path));
}
/**
 * Compute drift score: ratio of spec-related changed files to total criteria count.
 * Returns a value between 0 and 1.
 */
function computeDriftScore(specCriteria, specFileCount) {
    /* v8 ignore start -- specCriteria is always > 0 when called from detectCommitDrift */
    if (specCriteria <= 0) {
        return 0;
    }
    /* v8 ignore stop */
    return Math.min(1, specFileCount / specCriteria);
}
/**
 * Detect drift in a commit by checking spec-related changed files.
 * Returns an array of warning messages if drift exceeds threshold, otherwise empty array.
 */
export function detectCommitDrift(specFiles, threshold = DEFAULT_DRIFT_THRESHOLD) {
    if (specFiles.length === 0) {
        return [];
    }
    // Estimate total spec surface as 10x the changed file count.
    // This avoids the always-0.2 result from using 5x (1/5 = 0.2).
    // In production, specCriteria would come from the spec store.
    const driftScore = computeDriftScore(specFiles.length * 10, specFiles.length);
    if (driftScore < threshold) {
        return [];
    }
    const driftPct = Math.round(driftScore * 100);
    return specFiles.map((f) => `Drift detected in ${f}: ${String(driftPct)}% of spec criteria affected`);
}
// ---------------------------------------------------------------------------
// Git hook install/uninstall
// ---------------------------------------------------------------------------
/**
 * Install a Planu post-commit git hook.
 * If a hook already exists, appends the Planu section.
 */
export async function installGitHook(projectPath) {
    const hookPath = join(projectPath, POST_COMMIT_HOOK_PATH);
    const hookScript = buildPostCommitScript();
    let existingContent = '';
    try {
        existingContent = await readFile(hookPath, 'utf-8');
    }
    catch {
        // No existing hook
    }
    // Check if already installed (use START marker for consistency with uninstall)
    if (existingContent.includes(`${PLANU_HOOK_MARKER}-START`)) {
        return { installed: false, path: hookPath };
    }
    const newContent = existingContent
        ? `${existingContent}\n${hookScript}`
        : `#!/usr/bin/env sh\n${hookScript}`;
    await writeFile(hookPath, newContent, 'utf-8');
    await chmod(hookPath, 0o755);
    return { installed: true, path: hookPath };
}
/**
 * Uninstall the Planu post-commit hook section.
 * Removes only the Planu-marked section, preserving other hooks.
 */
export async function uninstallGitHook(projectPath) {
    const hookPath = join(projectPath, POST_COMMIT_HOOK_PATH);
    let content;
    try {
        content = await readFile(hookPath, 'utf-8');
    }
    catch {
        return { uninstalled: false, path: hookPath };
    }
    if (!content.includes(`${PLANU_HOOK_MARKER}-START`)) {
        return { uninstalled: false, path: hookPath };
    }
    // Remove the Planu section
    const startMarker = `${PLANU_HOOK_MARKER}-START`;
    const endMarker = `${PLANU_HOOK_MARKER}-END`;
    const startIdx = content.indexOf(startMarker);
    const endIdx = content.indexOf(endMarker);
    /* v8 ignore next 3 -- defensive: markers present but START/END corrupted */
    if (startIdx === -1 || endIdx === -1) {
        return { uninstalled: false, path: hookPath };
    }
    const cleaned = content.slice(0, startIdx) + content.slice(endIdx + endMarker.length + 1);
    const trimmed = cleaned.trim();
    if (trimmed === '' || trimmed === '#!/usr/bin/env sh') {
        // Remove the hook file entirely if nothing remains
        await unlink(hookPath);
    }
    else {
        await writeFile(hookPath, trimmed + '\n', 'utf-8');
    }
    return { uninstalled: true, path: hookPath };
}
// ---------------------------------------------------------------------------
// Build hook script
// ---------------------------------------------------------------------------
function buildPostCommitScript() {
    return `
${PLANU_HOOK_MARKER}-START
# Planu on-commit hook — auto-generated by SPEC-130
if command -v node >/dev/null 2>&1; then
  COMMIT_HASH="$(git rev-parse --short HEAD 2>/dev/null || echo 'unknown')"
  BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo 'unknown')"
  echo "[Planu] Post-commit: $COMMIT_HASH on $BRANCH"
fi
${PLANU_HOOK_MARKER}-END
`;
}
// ---------------------------------------------------------------------------
// Git helpers
// ---------------------------------------------------------------------------
/**
 * Get the latest commit hash.
 */
async function getLatestCommitHash(projectRoot) {
    try {
        const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], {
            cwd: projectRoot,
        });
        return stdout.trim();
    }
    catch {
        return 'unknown';
    }
}
/**
 * Get list of files changed in the latest commit.
 */
async function getCommittedFiles(projectRoot) {
    try {
        const { stdout } = await execFileAsync('git', ['diff-tree', '--no-commit-id', '--name-only', '-r', 'HEAD'], { cwd: projectRoot });
        return stdout
            .trim()
            .split('\n')
            .filter((f) => f.length > 0);
    }
    catch {
        return [];
    }
}
// ---------------------------------------------------------------------------
// Integrity check
// ---------------------------------------------------------------------------
/**
 * Run a basic spec integrity check on committed files.
 * Validates that @spec annotations reference known specs.
 */
async function checkIntegrity(files, projectRoot, knownSpecs) {
    const errors = [];
    for (const file of files) {
        // Only check source files
        if (!file.endsWith('.ts') &&
            !file.endsWith('.js') &&
            !file.endsWith('.tsx') &&
            !file.endsWith('.jsx')) {
            continue;
        }
        try {
            const content = await readFile(join(projectRoot, file), 'utf-8');
            const annotations = extractSpecAnnotations(content);
            for (const specId of annotations) {
                if (!knownSpecs.has(specId)) {
                    errors.push(`${file}: @spec ${specId} references unknown spec`);
                }
            }
        }
        catch {
            // File might have been deleted in this commit — skip
        }
    }
    return { valid: errors.length === 0, errors };
}
// ---------------------------------------------------------------------------
// Handler
// ---------------------------------------------------------------------------
/**
 * Handle on-commit event.
 * - Gets committed files
 * - Runs spec integrity check
 * - Identifies specs that may need reconciliation
 * - Non-blocking, async
 */
export async function handleOnCommit(ctx) {
    const commitHash = await getLatestCommitHash(ctx.projectRoot);
    const committedFiles = await getCommittedFiles(ctx.projectRoot);
    // Run integrity check
    const integrityResult = await checkIntegrity(committedFiles, ctx.projectRoot, ctx.knownSpecs);
    // Collect specs referenced in committed files for auto-reconcile
    const reconciledSpecs = new Set();
    for (const file of committedFiles) {
        if (!file.endsWith('.ts') && !file.endsWith('.js')) {
            continue;
        }
        try {
            const content = await readFile(join(ctx.projectRoot, file), 'utf-8');
            const annotations = extractSpecAnnotations(content);
            for (const specId of annotations) {
                if (ctx.knownSpecs.has(specId)) {
                    reconciledSpecs.add(specId);
                }
            }
        }
        catch {
            // Skip unreadable files
        }
    }
    // Drift detection on spec-related committed files (SPEC-138)
    const specRelatedFiles = committedFiles.filter((f) => isSpecRelatedFile(f));
    const driftWarnings = detectCommitDrift(specRelatedFiles);
    return {
        hook: 'on-commit',
        commitHash,
        reconciledSpecs: [...reconciledSpecs],
        integrityResult,
        ...(driftWarnings.length > 0 ? { driftWarnings } : {}),
    };
}
//# sourceMappingURL=on-commit.js.map