"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.TaskValidationEnvironmentExecutionError = void 0;
exports.resolveCommandOwnedExecutionPlan = resolveCommandOwnedExecutionPlan;
exports.executeCommandOwnedValidationSync = executeCommandOwnedValidationSync;
exports.parseJsonPointerTokens = parseJsonPointerTokens;
exports.verifyReferencedPassedValidationEvidence = verifyReferencedPassedValidationEvidence;
exports.isBoundedOutputEvidence = isBoundedOutputEvidence;
exports.isSemanticAssertionEvidence = isSemanticAssertionEvidence;
const node_child_process_1 = require("node:child_process");
const node_crypto_1 = require("node:crypto");
const node_fs_1 = __importDefault(require("node:fs"));
const node_path_1 = __importDefault(require("node:path"));
const errors_1 = require("../../errors");
const paths_1 = require("../../paths");
const IDENTITY_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{2,127}$/;
const MAX_STATUS_BYTES = 16 * 1024 * 1024;
const MAX_VALIDATION_PROCESS_OUTPUT_BYTES = 16 * 1024 * 1024;
const MAX_VALIDATION_CAPTURE_BYTES = 32 * 1024;
const MAX_VALIDATION_EVIDENCE_CONTRACT_BYTES = 256 * 1024;
const MAX_VALIDATION_ARGV_BYTES = 64 * 1024;
const MAX_VALIDATION_ARG_COUNT = 256;
const MAX_VALIDATION_SEMANTIC_ASSERTION_COUNT = 64;
const MAX_VALIDATION_SEMANTIC_ASSERTION_BYTES = 32 * 1024;
const MAX_VALIDATION_SEMANTIC_VALUE_CAPTURE_BYTES = 4 * 1024;
class TaskValidationEnvironmentExecutionError extends Error {
    failure;
    constructor(failure) {
        super(`Command-owned validation execution failed: ${failure.message}`);
        this.name = 'TaskValidationEnvironmentExecutionError';
        this.failure = failure;
    }
}
exports.TaskValidationEnvironmentExecutionError = TaskValidationEnvironmentExecutionError;
function resolveCommandOwnedExecutionPlan(options) {
    const semanticAssertions = parseSemanticAssertions(options.expectJson ?? []);
    const timeoutMs = normalizeExecutionTimeout(options.executionTimeoutMs);
    const directArgv = options.executionArgv === undefined ? null : requireCommandArgv(options.executionArgv, 'Execution argv');
    if (options.source !== 'manual') {
        if (directArgv !== null ||
            options.executionContractPath !== null ||
            timeoutMs !== null ||
            semanticAssertions.length > 0) {
            throw new TypeError('Command-owned execution is supported only for manual validation evidence.');
        }
        return null;
    }
    if (directArgv !== null && options.executionContractPath !== null) {
        throw new TypeError('Select exact argv or one execution-contract file, not both.');
    }
    let commandArgv = directArgv;
    let supersedesValidationId = options.supersedesValidationId;
    let command = options.command;
    if (options.executionContractPath !== null) {
        const evidence = readReferencedValidationContract(options.repositoryRoot, options.executionContractPath, 'Execution contract');
        const contract = evidence.contract;
        if (contract.schemaVersion !== 1 || contract.mode !== 'execute') {
            throw new TypeError("Execution-contract files require schemaVersion 1 with mode 'execute'.");
        }
        requireMatchingContractIdentity(contract, options.validationId, options.attemptId);
        commandArgv = requireCommandArgv(contract.argv, 'Execution contract argv');
        supersedesValidationId = reconcileSupersessionIdentity(supersedesValidationId, optionalContractIdentity(contract.supersedesValidationId, 'Superseded validation identity'));
        command = optionalText(contract.command);
    }
    else if (commandArgv === null && options.command?.trim().startsWith('{') === true) {
        const contract = parseObjectJson(options.command, 'Inline legacy execution contracts must contain one JSON object.');
        if (contract.schemaVersion !== 1 || contract.mode !== 'execute') {
            throw new TypeError("Inline legacy execution contracts require schemaVersion 1 with mode 'execute'.");
        }
        requireMatchingContractIdentity(contract, options.validationId, options.attemptId);
        commandArgv = requireCommandArgv(contract.argv, 'Execution contract argv');
        supersedesValidationId = reconcileSupersessionIdentity(supersedesValidationId, optionalContractIdentity(contract.supersedesValidationId, 'Superseded validation identity'));
    }
    if (commandArgv === null) {
        if (semanticAssertions.length > 0) {
            throw new TypeError('--expect-json requires exact command-owned execution argv or an execution-contract file.');
        }
        return null;
    }
    if (directArgv !== null && command !== null) {
        throw new TypeError('Exact execution argv cannot be combined with --command text.');
    }
    return {
        mode: 'command-owned-execution',
        validationId: options.validationId,
        attemptId: options.attemptId,
        commandArgv,
        command: command ?? formatCommandArgv(commandArgv),
        supersedesValidationId,
        semanticAssertions,
        timeoutMs,
        shell: false,
    };
}
function reconcileSupersessionIdentity(selected, contracted) {
    if (selected !== null && contracted !== null && selected !== contracted) {
        throw new TypeError('Execution contract supersession does not match the selected superseded identity.');
    }
    return selected ?? contracted;
}
function executeCommandOwnedValidationSync(repositoryRoot, executionPlan, options = {}) {
    const startedAt = Date.now();
    const executed = (options.spawnSync ?? node_child_process_1.spawnSync)(executionPlan.commandArgv[0], executionPlan.commandArgv.slice(1), {
        cwd: repositoryRoot,
        shell: false,
        encoding: 'buffer',
        maxBuffer: MAX_VALIDATION_PROCESS_OUTPUT_BYTES,
        windowsHide: true,
        ...(executionPlan.timeoutMs === null ? {} : { timeout: executionPlan.timeoutMs }),
        env: commandOwnedExecutionEnvironment(),
    });
    const durationMs = Math.max(0, Date.now() - startedAt);
    const recordedAt = new Date(Date.now()).toISOString();
    const timedOut = executed.error !== undefined && executed.error.code === 'ETIMEDOUT';
    if (executed.error !== undefined && !timedOut) {
        throw new TaskValidationEnvironmentExecutionError((0, errors_1.classifyEnvironmentExecutionFailure)(executed.error, executionPlan.commandArgv));
    }
    if (executed.status === null && !timedOut && executed.signal === null) {
        throw new TypeError(`Command-owned validation execution did not produce an exit status${executed.signal === null ? '.' : ` (signal ${executed.signal}).`}`);
    }
    const observedExitCode = timedOut ? 124 : (executed.status ?? 128);
    const stdout = asBuffer(executed.stdout);
    const semanticAssertions = executionPlan.semanticAssertions.length === 0
        ? null
        : evaluateSemanticAssertions(stdout, executionPlan.semanticAssertions);
    const observedStatus = observedExitCode === 0 && semanticAssertions?.passed !== false ? 'passed' : 'failed';
    return {
        executionPlan,
        verification: {
            mode: 'command-owned-execution',
            observedStatus,
            observedExitCode,
            durationMs,
            termination: timedOut ? 'timed_out' : executed.signal === null ? 'exit' : 'signal',
            signal: executed.signal,
            commandArgv: executionPlan.commandArgv,
            stdout: boundOutputEvidence(stdout),
            stderr: boundOutputEvidence(asBuffer(executed.stderr)),
            evidencePath: null,
            evidenceSha256: null,
            evidenceByteLength: null,
            ...(semanticAssertions === null ? {} : { semanticAssertions }),
        },
        recordedAt,
    };
}
function parseSemanticAssertions(values) {
    if (values.length > MAX_VALIDATION_SEMANTIC_ASSERTION_COUNT) {
        throw new TypeError(`--expect-json accepts at most ${MAX_VALIDATION_SEMANTIC_ASSERTION_COUNT} assertions.`);
    }
    const assertions = [];
    const pointers = new Set();
    for (const value of values) {
        if (typeof value !== 'string') {
            throw new TypeError('--expect-json must use <json-pointer>=<JSON-literal>.');
        }
        const separator = value.indexOf('=');
        if (separator < 0) {
            throw new TypeError(`Semantic assertion '${value}' must use <json-pointer>=<JSON-literal>.`);
        }
        const jsonPointer = value.slice(0, separator);
        parseJsonPointerTokens(jsonPointer);
        if (pointers.has(jsonPointer)) {
            throw new TypeError(`Semantic assertion JSON Pointer '${jsonPointer}' is duplicated.`);
        }
        pointers.add(jsonPointer);
        const expectedLiteral = value.slice(separator + 1);
        let expected;
        try {
            expected = JSON.parse(expectedLiteral);
        }
        catch {
            throw new TypeError(`Semantic assertion '${jsonPointer}' expected value must be one JSON literal.`);
        }
        assertions.push({ jsonPointer, expectedJson: canonicalJson(expected) });
    }
    if (Buffer.byteLength(JSON.stringify(assertions), 'utf8') > MAX_VALIDATION_SEMANTIC_ASSERTION_BYTES) {
        throw new TypeError(`Semantic assertion plan exceeds ${MAX_VALIDATION_SEMANTIC_ASSERTION_BYTES} bytes.`);
    }
    return assertions;
}
function normalizeExecutionTimeout(value) {
    if (value === undefined || value === null)
        return null;
    if (!Number.isInteger(value) || value < 1 || value > 86_400_000) {
        throw new TypeError('Command-owned execution timeout must be an integer from 1 through 86400000 milliseconds.');
    }
    return value;
}
function evaluateSemanticAssertions(stdout, assertions) {
    if (stdout.length > MAX_VALIDATION_CAPTURE_BYTES) {
        return unavailableSemanticAssertions('stdout-truncated', assertions);
    }
    let document;
    try {
        document = JSON.parse(stdout.toString('utf8'));
    }
    catch {
        return unavailableSemanticAssertions('invalid-json', assertions);
    }
    const results = assertions.map((assertion) => {
        const resolution = resolveJsonPointer(document, assertion.jsonPointer);
        if (!resolution.resolved) {
            return {
                ...assertion,
                resolved: false,
                observed: null,
                passed: false,
                failureReason: 'missing-pointer',
            };
        }
        const observedJson = canonicalJson(resolution.value);
        const passed = observedJson === assertion.expectedJson;
        return {
            ...assertion,
            resolved: true,
            observed: boundSemanticValueEvidence(observedJson),
            passed,
            failureReason: passed ? null : 'mismatch',
        };
    });
    return {
        documentStatus: 'parsed',
        failureReason: null,
        passed: results.every((assertion) => assertion.passed),
        assertions: results,
    };
}
function unavailableSemanticAssertions(failureReason, assertions) {
    return {
        documentStatus: failureReason,
        failureReason,
        passed: false,
        assertions: assertions.map((assertion) => ({
            ...assertion,
            resolved: false,
            observed: null,
            passed: false,
            failureReason: 'document-unavailable',
        })),
    };
}
function resolveJsonPointer(document, jsonPointer) {
    let value = document;
    for (const token of parseJsonPointerTokens(jsonPointer)) {
        if (Array.isArray(value)) {
            if (!/^(?:0|[1-9][0-9]*)$/u.test(token))
                return { resolved: false };
            const index = Number(token);
            if (!Number.isSafeInteger(index) || index >= value.length)
                return { resolved: false };
            value = value[index];
            continue;
        }
        if (value === null || typeof value !== 'object' || !Object.prototype.hasOwnProperty.call(value, token)) {
            return { resolved: false };
        }
        value = value[token];
    }
    return { resolved: true, value };
}
function parseJsonPointerTokens(jsonPointer) {
    if (jsonPointer === '')
        return [];
    if (!jsonPointer.startsWith('/')) {
        throw new TypeError(`Semantic assertion JSON Pointer '${jsonPointer}' must be empty or start with '/'.`);
    }
    return jsonPointer
        .slice(1)
        .split('/')
        .map((token) => {
        if (/~(?:[^01]|$)/u.test(token)) {
            throw new TypeError(`Semantic assertion JSON Pointer '${jsonPointer}' contains an invalid '~' escape.`);
        }
        return token.replace(/~1/gu, '/').replace(/~0/gu, '~');
    });
}
function boundSemanticValueEvidence(value) {
    const bytes = Buffer.from(value, 'utf8');
    return {
        text: bytes.subarray(0, MAX_VALIDATION_SEMANTIC_VALUE_CAPTURE_BYTES).toString('utf8'),
        byteLength: bytes.length,
        contentSha256: sha256(bytes),
        truncated: bytes.length > MAX_VALIDATION_SEMANTIC_VALUE_CAPTURE_BYTES,
    };
}
function verifyReferencedPassedValidationEvidence(options) {
    if (options.status !== 'passed')
        return null;
    if (options.source === 'manual') {
        throw new TypeError('Manual passed validation requires exact argv, an execution-contract file, or a compatible inline execution contract.');
    }
    if (options.outputReference === null) {
        throw new TypeError(`${options.source} passed validation requires --output-reference to select a verifiable repository-local result contract.`);
    }
    const evidence = readReferencedValidationContract(options.repositoryRoot, options.outputReference, 'Verified-result contract');
    if (evidence.contract.schemaVersion !== 1 || evidence.contract.mode !== 'verified-result') {
        throw new TypeError(`${options.source} passed validation requires a schemaVersion 1 contract with mode 'verified-result'.`);
    }
    requireMatchingContractIdentity(evidence.contract, options.validationId, options.attemptId);
    if (evidence.contract.command !== options.command) {
        throw new TypeError('Referenced validation command does not match --command.');
    }
    const commandArgv = requireCommandArgv(evidence.contract.commandArgv, 'Referenced result commandArgv');
    if (evidence.contract.status !== 'passed' || evidence.contract.exitCode !== 0) {
        throw new TypeError("Referenced passed validation must verifiably record status 'passed' and exitCode 0.");
    }
    if (typeof evidence.contract.output !== 'string') {
        throw new TypeError('Referenced passed validation must contain bounded string output.');
    }
    const currentSourceRevision = runGit(options.repositoryRoot, ['rev-parse', '--verify', 'HEAD']);
    if (currentSourceRevision === null || evidence.contract.sourceRevision !== currentSourceRevision) {
        throw new TypeError('Referenced passed validation sourceRevision does not match the current repository revision.');
    }
    const output = Buffer.from(evidence.contract.output, 'utf8');
    return {
        verification: {
            mode: 'verified-result-reference',
            observedStatus: 'passed',
            observedExitCode: 0,
            commandArgv,
            stdout: boundOutputEvidence(output),
            stderr: boundOutputEvidence(Buffer.alloc(0)),
            evidencePath: evidence.relativePath,
            evidenceSha256: sha256(evidence.contents),
            evidenceByteLength: evidence.contents.length,
        },
        supersedesValidationId: optionalContractIdentity(evidence.contract.supersedesValidationId, 'Superseded validation identity'),
    };
}
function commandOwnedExecutionEnvironment() {
    const environment = { ...process.env };
    delete environment.NODE_TEST_CONTEXT;
    return environment;
}
function readReferencedValidationContract(repositoryRoot, reference, label) {
    const relativePath = (0, paths_1.parseRepositoryRelativePath)(reference).normalizedPath;
    const candidate = node_path_1.default.join(repositoryRoot, relativePath);
    const stats = node_fs_1.default.lstatSync(candidate, { throwIfNoEntry: false });
    if (stats === undefined || !stats.isFile() || stats.isSymbolicLink()) {
        throw new TypeError(`${label} '${relativePath}' must be an existing regular file.`);
    }
    if (stats.size > MAX_VALIDATION_EVIDENCE_CONTRACT_BYTES) {
        throw new TypeError(`${label} '${relativePath}' exceeds ${MAX_VALIDATION_EVIDENCE_CONTRACT_BYTES} bytes.`);
    }
    const realCandidate = node_fs_1.default.realpathSync(candidate);
    const relativeRealPath = node_path_1.default.relative(repositoryRoot, realCandidate);
    if (relativeRealPath === '..' || relativeRealPath.startsWith(`..${node_path_1.default.sep}`) || node_path_1.default.isAbsolute(relativeRealPath)) {
        throw new TypeError('Referenced validation evidence must resolve inside the repository.');
    }
    const contents = node_fs_1.default.readFileSync(realCandidate);
    return {
        relativePath,
        contents,
        contract: parseObjectJson(contents.toString('utf8'), `${label} '${relativePath}' must contain one JSON object.`),
    };
}
function formatCommandArgv(argv) {
    return argv.map((entry) => (/^[A-Za-z0-9_./:@%+=,-]+$/u.test(entry) ? entry : JSON.stringify(entry))).join(' ');
}
function requireMatchingContractIdentity(contract, validationId, attemptId) {
    if (contract.validationId !== validationId || contract.attemptId !== attemptId) {
        throw new TypeError('Validation evidence contract identities must match the selected validation and attempt.');
    }
}
function requireCommandArgv(value, label) {
    if (!Array.isArray(value) ||
        value.length === 0 ||
        value.length > MAX_VALIDATION_ARG_COUNT ||
        value.some((entry) => typeof entry !== 'string' || entry.includes('\0') || entry.includes('\n') || entry.includes('\r'))) {
        throw new TypeError(`${label} must contain 1-${MAX_VALIDATION_ARG_COUNT} newline-free string arguments.`);
    }
    const argv = value.map((entry) => (entry.trim() === '' ? entry : entry));
    if (argv[0].trim() === '')
        throw new TypeError(`${label} requires a non-empty executable.`);
    if (Buffer.byteLength(JSON.stringify(argv), 'utf8') > MAX_VALIDATION_ARGV_BYTES) {
        throw new TypeError(`${label} exceeds ${MAX_VALIDATION_ARGV_BYTES} bytes.`);
    }
    return argv;
}
function parseObjectJson(value, message) {
    let parsed;
    try {
        parsed = JSON.parse(value);
    }
    catch {
        throw new TypeError(message);
    }
    if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
        throw new TypeError(message);
    }
    return parsed;
}
function optionalContractIdentity(value, label) {
    if (value === undefined || value === null || value === '')
        return null;
    return requireIdentity(value, label);
}
function isBoundedOutputEvidence(value) {
    if (value === null || typeof value !== 'object' || Array.isArray(value))
        return false;
    const evidence = value;
    return (typeof evidence.text === 'string' &&
        Number.isInteger(evidence.byteLength) &&
        evidence.byteLength >= 0 &&
        /^sha256:[0-9a-f]{64}$/.test(evidence.contentSha256) &&
        typeof evidence.truncated === 'boolean');
}
function isSemanticAssertionEvidence(value) {
    if (value === null || typeof value !== 'object' || Array.isArray(value))
        return false;
    const evidence = value;
    if (!['parsed', 'invalid-json', 'stdout-truncated'].includes(evidence.documentStatus) ||
        typeof evidence.passed !== 'boolean' ||
        !Array.isArray(evidence.assertions)) {
        return false;
    }
    if (evidence.documentStatus === 'parsed') {
        if (evidence.failureReason !== null)
            return false;
    }
    else if (evidence.failureReason !== evidence.documentStatus || evidence.passed) {
        return false;
    }
    return (evidence.assertions.every((assertion) => {
        try {
            parseJsonPointerTokens(assertion.jsonPointer);
            JSON.parse(assertion.expectedJson);
        }
        catch {
            return false;
        }
        return (typeof assertion.resolved === 'boolean' &&
            typeof assertion.passed === 'boolean' &&
            (assertion.observed === null || isBoundedOutputEvidence(assertion.observed)) &&
            [null, 'document-unavailable', 'missing-pointer', 'mismatch'].includes(assertion.failureReason));
    }) && evidence.passed === evidence.assertions.every((assertion) => assertion.passed));
}
function boundOutputEvidence(value) {
    return {
        text: value.subarray(0, MAX_VALIDATION_CAPTURE_BYTES).toString('utf8'),
        byteLength: value.length,
        contentSha256: sha256(value),
        truncated: value.length > MAX_VALIDATION_CAPTURE_BYTES,
    };
}
function asBuffer(value) {
    return Buffer.isBuffer(value) ? value : Buffer.alloc(0);
}
function requireIdentity(value, label) {
    const identity = requireText(value, label);
    if (!IDENTITY_PATTERN.test(identity))
        throw new TypeError(`${label} must use 3-128 machine-safe characters.`);
    return identity;
}
function requireText(value, label) {
    if (typeof value !== 'string' || value.trim() === '')
        throw new TypeError(`${label} must be non-empty.`);
    return value.trim();
}
function optionalText(value) {
    return typeof value === 'string' && value.trim() !== '' ? value.trim() : null;
}
function runGit(repositoryRoot, args) {
    const result = (0, node_child_process_1.spawnSync)('git', [...args], { cwd: repositoryRoot, encoding: 'utf8', maxBuffer: MAX_STATUS_BYTES });
    return result.status === 0 ? result.stdout.trim() : null;
}
function canonicalJson(value) {
    if (Array.isArray(value))
        return `[${value.map(canonicalJson).join(',')}]`;
    if (value !== null && typeof value === 'object') {
        const record = value;
        return `{${Object.keys(record)
            .sort()
            .map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`)
            .join(',')}}`;
    }
    return JSON.stringify(value);
}
function sha256(value) {
    return `sha256:${(0, node_crypto_1.createHash)('sha256').update(value).digest('hex')}`;
}
