/** Closed standards-check registry shared by every supported harness. */
import { createHash } from "node:crypto";
import { realpath } from "node:fs/promises";
import { projectPathKind, readProjectText } from "../health/read-only-fs.js";
import { isJsonObject } from "../sync/json-path.js";
import { inferTestEvidenceFormat, } from "./test-evidence.js";
const STATIC_ANALYSIS = "static-analysis";
const TYPESCRIPT_SCRIPT_CHECKS = [
    {
        id: "typescript.lint",
        script: "lint",
        category: "lint",
        timeoutMs: 180_000,
    },
    {
        id: "typescript.lint-slow",
        script: "lint:slow",
        category: "lint",
        timeoutMs: 180_000,
    },
    {
        id: "typescript.typecheck",
        script: "typecheck",
        category: STATIC_ANALYSIS,
        timeoutMs: 180_000,
    },
    {
        id: "typescript.build",
        script: "build",
        category: "guardrail",
        timeoutMs: 600_000,
    },
    {
        id: "typescript.test",
        script: "test",
        category: "test",
        timeoutMs: 600_000,
        requiresTestEvidence: true,
    },
    {
        id: "typescript.test-unit",
        script: "test:unit",
        category: "test",
        timeoutMs: 600_000,
        requiresTestEvidence: true,
    },
    {
        id: "typescript.test-coverage",
        script: "test:cov",
        category: "threshold",
        timeoutMs: 900_000,
        requiresTestEvidence: true,
    },
    {
        id: "typescript.test-integration",
        script: "test:integration",
        category: "test",
        timeoutMs: 600_000,
        requiresTestEvidence: true,
    },
    {
        id: "typescript.test-e2e",
        script: "test:e2e",
        category: "test",
        timeoutMs: 900_000,
        requiresTestEvidence: true,
        optional: true,
    },
    {
        id: "typescript.test-contract",
        script: "test:contract",
        category: "test",
        timeoutMs: 600_000,
        requiresTestEvidence: true,
        optional: true,
    },
    {
        id: "typescript.format",
        script: "format:check",
        category: "guardrail",
        timeoutMs: 180_000,
    },
    {
        id: "typescript.dead-code",
        script: "knip:check",
        category: STATIC_ANALYSIS,
        timeoutMs: 300_000,
    },
    {
        id: "typescript.ast-grep",
        script: "sg:scan",
        category: "guardrail",
        timeoutMs: 180_000,
    },
    {
        id: "typescript.mutation",
        script: "test:mutation",
        category: "guardrail",
        timeoutMs: 1_800_000,
        mutation: true,
    },
];
const RAILS_CHECKS = [
    direct("rails.rubocop", "lint", 300_000, "bundle", "exec", "rubocop"),
    directTest("rails.rspec", 900_000, "rspec", "bundle", "exec", "rspec"),
    direct("rails.reek", STATIC_ANALYSIS, 300_000, "bundle", "exec", "reek", "app/", "lib/"),
    direct("rails.flog", STATIC_ANALYSIS, 300_000, "bundle", "exec", "flog", "--all", "--group", "app/", "lib/"),
    direct("rails.flay", STATIC_ANALYSIS, 300_000, "bundle", "exec", "flay", "app/", "lib/"),
    direct("rails.brakeman", "guardrail", 300_000, "bundle", "exec", "brakeman", "--no-pager", "--quiet"),
    direct("rails.bundler-audit", "guardrail", 300_000, "bundle", "exec", "bundler-audit", "check", "--update"),
    direct("rails.ast-grep", "guardrail", 180_000, "sg", "scan"),
];
/**
 * Resolve the exact ordered union of checks for current detected stacks.
 * Missing managed scripts and threshold tooling fail before any command runs.
 * @param projectRoot - Canonical project root
 * @param projectTypes - Expanded, canonical detected types
 * @param config - Current bounded merged Lisa config
 * @returns Closed current check plan
 */
// eslint-disable-next-line sonarjs/cognitive-complexity -- explicit stack gates preserve fail-closed requirements and command order
export async function resolveStandardsCheckPlan(// eslint-disable-line max-lines-per-function -- the closed multi-stack registry is one auditable ordered plan
projectRoot, projectTypes, config) {
    const root = await realpath(projectRoot);
    if (projectTypes.length === 0) {
        throw new Error("No supported Lisa project type was detected.");
    }
    const checks = [];
    if (projectTypes.includes("typescript")) {
        const manifest = await readPackageManifest(root);
        const scripts = isJsonObject(manifest.scripts) ? manifest.scripts : {};
        const packageManager = await resolvePackageManager(root);
        for (const check of TYPESCRIPT_SCRIPT_CHECKS) {
            if (check.mutation === true && !mutationGateEnabled(config))
                continue;
            const script = scripts[check.script];
            if (typeof script !== "string" || script.trim().length === 0) {
                if (check.optional === true)
                    continue;
                throw new Error(`Required package script is missing: ${check.script}.`);
            }
            // eslint-disable-next-line functional/immutable-data -- the private ordered plan is frozen before it leaves this resolver
            checks.push(Object.freeze({
                id: check.id,
                category: check.category,
                argv: Object.freeze([
                    packageManager,
                    "run",
                    check.script,
                ]),
                timeoutMs: check.timeoutMs,
                ...(check.requiresTestEvidence === true
                    ? { testEvidence: inferTestEvidenceFormat(script) }
                    : {}),
            }));
        }
    }
    if (projectTypes.includes("rails")) {
        // eslint-disable-next-line functional/immutable-data -- the private ordered plan is frozen before it leaves this resolver
        checks.push(...RAILS_CHECKS);
        if (mutationGateEnabled(config)) {
            if ((await projectPathKind(root, "scripts/lisa-mutation.sh")) !== "file") {
                throw new Error("Required Rails mutation command is missing: scripts/lisa-mutation.sh.");
            }
            // eslint-disable-next-line functional/immutable-data -- the private ordered plan is frozen before it leaves this resolver
            checks.push(direct("rails.mutation", "guardrail", 1_800_000, "bash", "scripts/lisa-mutation.sh"));
        }
    }
    if (projectTypes.includes("phaser")) {
        if ((await projectPathKind(root, "scripts/check-verification-coverage.mjs")) !== "file") {
            throw new Error("Required verification-coverage command is missing: scripts/check-verification-coverage.mjs.");
        }
        // eslint-disable-next-line functional/immutable-data -- the private ordered plan is frozen before it leaves this resolver
        checks.push(Object.freeze({
            ...direct("phaser.verification-coverage", "guardrail", 180_000, "node", "scripts/check-verification-coverage.mjs"),
            environment: Object.freeze({
                VERIFY_BASE_SHA: "HEAD^",
                VERIFY_HEAD_SHA: "HEAD",
            }),
        }));
    }
    if ((await projectPathKind(root, "scripts/check-threshold-ratchet.mjs")) !==
        "file") {
        throw new Error("Required threshold command is missing: scripts/check-threshold-ratchet.mjs.");
    }
    // eslint-disable-next-line functional/immutable-data -- the private ordered plan is frozen before it leaves this resolver
    checks.push(direct("shared.threshold-ratchet", "threshold", 180_000, "node", "scripts/check-threshold-ratchet.mjs", "--base", "HEAD^"));
    if (new Set(checks.map(check => check.id)).size !== checks.length) {
        throw new Error("Standards registry produced duplicate check identifiers.");
    }
    return freezePlan(checks, config);
}
/**
 * Freeze the resolved checks and bind their registry and configuration digests.
 * @param checks - Private ordered check accumulator
 * @param config - Current bounded merged Lisa config
 * @returns Frozen current check plan
 */
function freezePlan(checks, config) {
    const frozen = Object.freeze(checks.map(check => Object.freeze(check)));
    // eslint-disable-next-line max-lines -- this single closed registry keeps every supported stack command reviewable together
    return Object.freeze({
        checks: frozen,
        registryDigest: digest({ version: 1, checks: frozen }),
        configDigest: digestStandardsConfig(config),
    });
}
/**
 * Digest only configuration that changes standards applicability or policy.
 * @param config - Current merged Lisa config
 * @returns Canonical standards configuration digest
 */
export function digestStandardsConfig(config) {
    return digest({
        quality: config.quality ?? null,
        thresholdRatchet: config.thresholdRatchet ?? null,
    });
}
/**
 * Return a canonical SHA-256 digest without retaining config values.
 * @param value - JSON-like value to digest
 * @returns Canonical SHA-256 digest
 */
export function digestStandardsValue(value) {
    return digest(value);
}
/**
 * Construct one fixed direct-command check.
 * @param id - Stable check identifier
 * @param category - Standards category
 * @param timeoutMs - Command deadline in milliseconds
 * @param command - Fixed no-shell argv
 * @returns Frozen standards check
 */
function direct(id, category, timeoutMs, ...command) {
    return Object.freeze({
        id,
        category,
        argv: Object.freeze(command),
        timeoutMs,
    });
}
/**
 * Construct one direct test check with positive runner-evidence semantics.
 * @param id - Stable check identifier
 * @param timeoutMs - Command deadline in milliseconds
 * @param testEvidence - Positive test-evidence parser
 * @param command - Fixed no-shell argv
 * @returns Frozen standards test check
 */
function directTest(id, timeoutMs, testEvidence, ...command) {
    return Object.freeze({
        ...direct(id, "test", timeoutMs, ...command),
        testEvidence,
    });
}
/**
 * Read a bounded package manifest and require its object shape.
 * @param projectRoot - Canonical project repository
 * @returns Parsed package manifest
 */
async function readPackageManifest(projectRoot) {
    const payload = await readProjectText(projectRoot, "package.json");
    if (payload === undefined) {
        throw new Error("Required package manifest is missing: package.json.");
    }
    const parsed = JSON.parse(payload);
    if (!isJsonObject(parsed)) {
        throw new Error("package.json must contain a JSON object.");
    }
    return parsed;
}
/**
 * Resolve one managed package runner in stable precedence order.
 * @param projectRoot - Canonical project repository
 * @returns Package runner selected by lockfile
 */
async function resolvePackageManager(projectRoot) {
    const candidates = [
        ["bun.lock", "bun"],
        ["bun.lockb", "bun"],
        ["pnpm-lock.yaml", "pnpm"],
        ["yarn.lock", "yarn"],
        ["package-lock.json", "npm"],
    ];
    for (const [lockfile, manager] of candidates) {
        if ((await projectPathKind(projectRoot, lockfile)) === "file")
            return manager;
    }
    return "npm";
}
/**
 * Read the shared mutation-gate switch without treating absence as enabled.
 * @param config - Current merged Lisa config
 * @returns Whether mutation proof is required
 */
function mutationGateEnabled(config) {
    return (isJsonObject(config.quality) &&
        isJsonObject(config.quality.mutation) &&
        isJsonObject(config.quality.mutation.gate) &&
        config.quality.mutation.gate.enabled === true);
}
/**
 * Stable recursive JSON serialization for registry/config digests.
 * @param value - JSON-like value to digest
 * @returns Canonical SHA-256 digest
 */
function digest(value) {
    return `sha256:${createHash("sha256")
        .update(canonicalJson(value))
        .digest("hex")}`;
}
/**
 * Serialize JSON-like input with recursively sorted object keys.
 * @param value - JSON-like value to serialize
 * @returns Canonical JSON text
 */
function canonicalJson(value) {
    if (Array.isArray(value)) {
        return `[${value.map(item => canonicalJson(item)).join(",")}]`;
    }
    if (isJsonObject(value)) {
        return `{${Object.keys(value)
            .sort((left, right) => left.localeCompare(right))
            .map(key => `${JSON.stringify(key)}:${canonicalJson(value[key])}`)
            .join(",")}}`;
    }
    return JSON.stringify(value) ?? "null";
}
//# sourceMappingURL=registry.js.map