/** Strict executable contract for Health v1 results and scheduling. */
import { hasUnsafeTextCharacter, readStrictDenseArray, readStrictProperty as read, requireBoundedMachineId, requireCanonicalUtcTimestamp, requireClosedString as requireEnum, requireStrictRecord as requireRecord, } from "./strict-validation.js";
export const HEALTH_SCHEDULE_VALUES = ["off", "daily", "weekly"];
const RESULT_FIELDS = [
    "schemaVersion",
    "runId",
    "mode",
    "startedAt",
    "completedAt",
    "findings",
    "summary",
];
const FINDING_FIELDS = ["check", "layer", "status", "reason"];
const SUMMARY_FIELDS = ["verdict", "counts"];
const COUNT_FIELDS = ["pass", "warn", "fail"];
const MAX_FINDINGS = 200;
const MAX_ID_BYTES = 128;
const MAX_REASON_BYTES = 2_000;
/**
 * Validate a health schedule value.
 * @param value - Untrusted schedule candidate
 * @returns Validated closed schedule value
 */
export function validateHealthSchedule(value) {
    if (!HEALTH_SCHEDULE_VALUES.includes(value)) {
        throw new Error("Invalid health.schedule: expected off, daily, or weekly");
    }
    return value;
}
/**
 * Derive zero-filled counts and the canonical verdict.
 * @param findings - Untrusted finding collection
 * @returns Frozen canonical summary
 */
export function summarizeHealthFindings(findings) {
    const validated = requireFindings(findings);
    const counts = validated.reduce((current, finding) => ({
        ...current,
        [finding.status]: current[finding.status] + 1,
    }), { pass: 0, warn: 0, fail: 0 });
    return Object.freeze({
        verdict: counts.fail > 0 ? "drift detected" : "in band",
        counts: Object.freeze(counts),
    });
}
/**
 * Validate untrusted data into a detached, deeply frozen Health v1 result.
 * @param candidate - Untrusted result envelope
 * @returns Frozen validated Health v1 result
 */
export function validateHealthResult(candidate) {
    const input = requireRecord(candidate, RESULT_FIELDS, "HealthResult");
    if (read(input, "schemaVersion") !== 1) {
        throw new Error("Invalid schemaVersion: expected 1");
    }
    const runId = requireBoundedMachineId(read(input, "runId"), "runId", MAX_ID_BYTES);
    const mode = requireEnum(read(input, "mode"), ["deterministic", "full"], "mode");
    const startedAt = requireCanonicalUtcTimestamp(read(input, "startedAt"), "startedAt");
    const completedAt = requireCanonicalUtcTimestamp(read(input, "completedAt"), "completedAt");
    if (completedAt < startedAt) {
        throw new Error("Invalid completedAt: precedes startedAt");
    }
    const findings = requireFindings(read(input, "findings"));
    const supplied = requireSummary(read(input, "summary"));
    const derived = summarizeHealthFindings(findings);
    assertModeLayers(mode, findings);
    if (supplied.verdict !== derived.verdict ||
        COUNT_FIELDS.some(key => supplied.counts[key] !== derived.counts[key])) {
        throw new Error("Invalid summary: verdict or counts do not match findings");
    }
    return Object.freeze({
        schemaVersion: 1,
        runId,
        mode,
        startedAt,
        completedAt,
        findings: Object.freeze(findings),
        summary: derived,
    });
}
/**
 * Validate and detach a non-empty finding list.
 * @param value - Untrusted finding collection
 * @returns Detached validated findings
 */
function requireFindings(value) {
    const findings = readStrictDenseArray(value, 1, MAX_FINDINGS, "findings").map((candidate, index) => requireFinding(candidate, index));
    const checks = findings.map(finding => finding.check);
    if (new Set(checks).size !== checks.length) {
        throw new Error("Invalid findings: check identifiers must be unique");
    }
    return findings;
}
/**
 * Validate one finding.
 * @param candidate - Untrusted finding candidate
 * @param index - Finding index used in validation messages
 * @returns Frozen validated finding
 */
function requireFinding(candidate, index) {
    const input = requireRecord(candidate, FINDING_FIELDS, `findings[${index}]`);
    const reason = read(input, "reason");
    if (typeof reason !== "string" ||
        reason.trim() !== reason ||
        reason.length === 0 ||
        Buffer.byteLength(reason, "utf8") > MAX_REASON_BYTES ||
        hasUnsafeTextCharacter(reason)) {
        throw new Error(`Invalid findings[${index}].reason: expected readable text`);
    }
    return Object.freeze({
        check: requireBoundedMachineId(read(input, "check"), `findings[${index}].check`, MAX_ID_BYTES),
        layer: requireEnum(read(input, "layer"), ["deterministic", "agentic"], `findings[${index}].layer`),
        status: requireEnum(read(input, "status"), ["pass", "warn", "fail"], `findings[${index}].status`),
        reason,
    });
}
/**
 * Validate supplied summary shape and scalar values before comparison.
 * @param value - Untrusted summary candidate
 * @returns Frozen validated supplied summary
 */
function requireSummary(value) {
    const summary = requireRecord(value, SUMMARY_FIELDS, "summary");
    const counts = requireRecord(read(summary, "counts"), COUNT_FIELDS, "summary.counts");
    const normalizedCounts = Object.fromEntries(COUNT_FIELDS.map(field => {
        const count = read(counts, field);
        if (!Number.isSafeInteger(count) || count < 0) {
            throw new Error(`Invalid summary.counts.${field}: expected a non-negative integer`);
        }
        return [field, count];
    }));
    return Object.freeze({
        verdict: requireEnum(read(summary, "verdict"), ["in band", "drift detected"], "summary.verdict"),
        counts: Object.freeze(normalizedCounts),
    });
}
/**
 * Enforce mode meaning rather than treating it as decorative metadata.
 * @param mode - Validated run mode
 * @param findings - Validated run findings
 */
function assertModeLayers(mode, findings) {
    const layers = new Set(findings.map(finding => finding.layer));
    if (mode === "deterministic" &&
        (layers.size !== 1 || !layers.has("deterministic"))) {
        throw new Error("Invalid findings: deterministic mode permits deterministic findings only");
    }
    if (mode === "full" && !layers.has("deterministic")) {
        throw new Error("Invalid findings: full mode requires deterministic findings");
    }
}
//# sourceMappingURL=contract.js.map