// engine/supply-chain/build-checker.ts — SPEC-046
// Detects reproducible build signals: pinned Docker images, nix, bazel, frozen lockfile flags.
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { glob } from 'glob';
// Reproducible build tools
const HERMETIC_BUILD_TOOLS = ['nix', 'guix', 'bazel'];
// CI flags that indicate frozen/reproducible installs
const FROZEN_LOCKFILE_FLAGS = [
    '--frozen-lockfile',
    '--ci',
    'npm ci',
    'pip install --require-hashes',
];
// Docker digest pattern (pinned base image)
const DIGEST_PATTERN = /FROM\s+\S+@sha256:[a-f0-9]{64}/i;
// Non-pinned FROM patterns
const UNPINNED_FROM_PATTERN = /FROM\s+\S+:latest\b/i;
const BUILD_REFS = [
    'https://reproducible-builds.org/',
    'https://docs.docker.com/build/building/base-images/',
    'https://nixos.org/',
];
/** Find all Dockerfiles in the project. */
async function findDockerfiles(projectPath) {
    try {
        const files = await glob(['Dockerfile', '**/Dockerfile', '**/*.dockerfile'], {
            cwd: projectPath,
            nodir: true,
            maxDepth: 5,
            ignore: ['node_modules/**', 'vendor/**', '.git/**'],
        });
        return files.map((f) => join(projectPath, f));
    }
    catch {
        return [];
    }
}
/** Check if all Dockerfiles use pinned base image digests. */
async function checkDockerfilePinning(projectPath) {
    const dockerfiles = await findDockerfiles(projectPath);
    if (dockerfiles.length === 0) {
        return { hasPinned: false, hasUnpinned: false };
    }
    let hasPinned = false;
    let hasUnpinned = false;
    for (const file of dockerfiles) {
        try {
            const content = await readFile(file, 'utf-8');
            if (DIGEST_PATTERN.test(content)) {
                hasPinned = true;
            }
            if (UNPINNED_FROM_PATTERN.test(content)) {
                hasUnpinned = true;
            }
        }
        catch {
            // Skip unreadable files
        }
    }
    return { hasPinned, hasUnpinned };
}
/** Detect hermetic build tool (nix, bazel, guix). */
async function detectHermeticBuildTool(projectPath) {
    for (const tool of HERMETIC_BUILD_TOOLS) {
        const candidates = tool === 'nix'
            ? ['flake.nix', 'default.nix', 'shell.nix']
            : tool === 'bazel'
                ? ['BUILD', 'WORKSPACE', 'BUILD.bazel']
                : ['guix.scm'];
        for (const file of candidates) {
            try {
                await readFile(join(projectPath, file), 'utf-8');
                return tool;
            }
            catch {
                // File not found
            }
        }
    }
    return null;
}
/** Check if CI workflows use frozen lockfile flags. */
async function hasFrozenLockfileInCi(projectPath) {
    try {
        const workflowFiles = await glob(['.github/workflows/*.yml', '.github/workflows/*.yaml'], {
            cwd: projectPath,
            nodir: true,
            absolute: false,
        });
        for (const f of workflowFiles) {
            try {
                const content = await readFile(join(projectPath, f), 'utf-8');
                if (FROZEN_LOCKFILE_FLAGS.some((flag) => content.includes(flag))) {
                    return true;
                }
            }
            catch {
                // Skip
            }
        }
    }
    catch {
        // No workflows directory
    }
    return false;
}
/**
 * Detect reproducible build signals in the project.
 */
export async function checkBuild(projectPath) {
    const [dockerfileInfo, hermeticTool, frozenLockfileInCi] = await Promise.all([
        checkDockerfilePinning(projectPath),
        detectHermeticBuildTool(projectPath),
        hasFrozenLockfileInCi(projectPath),
    ]);
    const reproducibleBuild = dockerfileInfo.hasPinned ||
        hermeticTool !== null ||
        (frozenLockfileInCi && !dockerfileInfo.hasUnpinned);
    return {
        reproducibleBuild,
        dockerfilesPinned: dockerfileInfo.hasPinned,
        dockerfilesHaveUnpinned: dockerfileInfo.hasUnpinned,
        hermeticTool,
        frozenLockfileInCi,
    };
}
/**
 * Build reproducible-build gaps from check results.
 */
export function buildBuildGaps(result) {
    const gaps = [];
    if (result.dockerfilesHaveUnpinned) {
        gaps.push({
            category: 'reproducible',
            severity: 'warning',
            message: 'Dockerfile uses `FROM ...latest` — base image is not pinned to a digest',
            fix: 'Pin base images to a digest: `FROM node:20-alpine@sha256:<digest>`',
            references: [BUILD_REFS[1] ?? ''],
        });
    }
    if (!result.reproducibleBuild && !result.dockerfilesPinned && result.hermeticTool === null) {
        gaps.push({
            category: 'reproducible',
            severity: 'info',
            message: 'No reproducible build signals detected — builds may not be deterministic',
            fix: 'Use `npm ci` / `--frozen-lockfile` in CI, pin Dockerfile base images, or adopt Nix/Bazel for hermetic builds',
            references: BUILD_REFS,
        });
    }
    return gaps;
}
//# sourceMappingURL=build-checker.js.map