import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { TextDecoder } from 'node:util';
import { z } from 'zod';
import { runGit, runGitRaw } from '../git/client.js';
import { readGitObjectHeaders, readGitObjects } from '../git/objects.js';
import { assertValidGitPath } from '../git/parse.js';
import { abortIfNeeded } from '../utils.js';
import { looksBinary } from './entity/snapshot.js';
const COORDINATE_SCHEMA_VERSION = 1;
const FULL_OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const FATAL_UTF8 = new TextDecoder('utf-8', { fatal: true });
/** Canonical digest used by atoms and coordinate verification. */
export const GIT_SPAN_DIGEST_ALGORITHM = 'sha256-lf-span-v1';
export const DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS = Object.freeze({
    maxRequests: 10_000,
    maxBlobBytes: 2_097_152,
    maxBlobLines: 25_000,
    maxTotalBlobBytes: 268_435_456,
    maxBatchBytes: 33_554_432,
});
export const GitCoordinateCoverageV1Schema = z.enum(['complete', 'partial', 'unavailable']);
export const GitCoordinateSourceSpanV1Schema = z
    .object({
    start_line: z.number().int().positive(),
    end_line: z.number().int().positive(),
    exact: z.boolean(),
})
    .strict()
    .superRefine((span, context) => {
    if (span.end_line < span.start_line) {
        context.addIssue({ code: 'custom', message: 'end_line must not precede start_line.' });
    }
});
const GitRelativePathV1Schema = z
    .string()
    .min(1)
    .max(4_096)
    .superRefine((value, context) => {
    try {
        assertValidGitPath(value);
    }
    catch (error) {
        context.addIssue({ code: 'custom', message: error instanceof Error ? error.message : String(error) });
    }
    if (value.includes('\\'))
        context.addIssue({ code: 'custom', message: 'path must use POSIX separators.' });
    if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) {
        context.addIssue({ code: 'custom', message: 'path must be relative.' });
    }
    if (value.split('/').some(segment => segment === '' || segment === '.' || segment === '..')) {
        context.addIssue({ code: 'custom', message: 'path must be normalized.' });
    }
});
export const GitNativeCoordinateV1Schema = z
    .object({
    oid: z.string().regex(FULL_OID),
    path: GitRelativePathV1Schema,
    span: GitCoordinateSourceSpanV1Schema,
    span_digest: z.string().regex(/^[0-9a-f]{64}$/),
})
    .strict();
export const GitCoordinateVerificationRequestV1Schema = z
    .object({
    request_id: z.string().min(1).max(512),
    coordinate: GitNativeCoordinateV1Schema,
})
    .strict();
export const GitCoordinateVerificationDiagnosticV1Schema = z
    .object({
    code: z.enum([
        'oid-format-mismatch',
        'missing-object',
        'not-a-blob',
        'blob-too-large',
        'total-blob-budget-exhausted',
        'content-unavailable',
        'binary-blob',
        'invalid-utf8',
        'blob-too-many-lines',
        'span-out-of-range',
        'span-digest-mismatch',
        'ref-path-missing',
        'ref-object-mismatch',
    ]),
    message: z.string(),
    limit: z.number().int().nonnegative().optional(),
    actual: z.number().int().nonnegative().optional(),
})
    .strict();
const GitCoordinateVerificationLimitsV1Schema = z
    .object({
    max_requests: z.number().int().positive(),
    max_blob_bytes: z.number().int().positive(),
    max_blob_lines: z.number().int().positive(),
    max_total_blob_bytes: z.number().int().positive(),
    max_batch_bytes: z.number().int().positive(),
})
    .strict();
export const GitCoordinateVerificationHeaderV1Schema = z
    .object({
    schema_version: z.literal(COORDINATE_SCHEMA_VERSION),
    type: z.literal('header'),
    requested_ref: z.string().nullable(),
    resolved_ref: z.string().regex(FULL_OID).nullable(),
    object_format: z.enum(['sha1', 'sha256']),
    request_count: z.number().int().nonnegative(),
    unique_object_count: z.number().int().nonnegative(),
    limits: GitCoordinateVerificationLimitsV1Schema,
    provenance: z
        .object({
        instrument: z.literal('occ.code.coordinate-verify'),
        instrument_version: z.literal(COORDINATE_SCHEMA_VERSION),
        occ_version: z.string().min(1),
        node_version: z.string().min(1),
        acquisition: z.literal('git-cat-file'),
        source_bodies: z.literal('omitted'),
        span_digest: z.literal(GIT_SPAN_DIGEST_ALGORITHM),
    })
        .strict(),
})
    .strict();
const CoordinateCheckV1Schema = z.enum(['verified', 'mismatch', 'unavailable']);
const RefCoordinateCheckV1Schema = z.enum(['verified', 'not-requested', 'mismatch', 'unavailable']);
export const GitCoordinateVerificationResultV1Schema = z
    .object({
    schema_version: z.literal(COORDINATE_SCHEMA_VERSION),
    type: z.literal('result'),
    request_index: z.number().int().nonnegative(),
    request_id: z.string().min(1),
    coordinate: GitNativeCoordinateV1Schema,
    status: z.enum(['verified', 'mismatch', 'unavailable']),
    actual_span_digest: z
        .string()
        .regex(/^[0-9a-f]{64}$/)
        .nullable(),
    ref_oid: z.string().regex(FULL_OID).nullable(),
    checks: z
        .object({
        object: CoordinateCheckV1Schema,
        span: CoordinateCheckV1Schema,
        ref_binding: RefCoordinateCheckV1Schema,
    })
        .strict(),
    diagnostics: z.array(GitCoordinateVerificationDiagnosticV1Schema),
})
    .strict();
export const GitCoordinateVerificationFooterV1Schema = z
    .object({
    schema_version: z.literal(COORDINATE_SCHEMA_VERSION),
    type: z.literal('footer'),
    complete: z.boolean(),
    coverage: z
        .object({
        requests: GitCoordinateCoverageV1Schema,
        objects: GitCoordinateCoverageV1Schema,
        spans: GitCoordinateCoverageV1Schema,
        ref_bindings: GitCoordinateCoverageV1Schema,
    })
        .strict(),
    counts: z
        .object({
        requests: z.number().int().nonnegative(),
        verified: z.number().int().nonnegative(),
        mismatched: z.number().int().nonnegative(),
        unavailable: z.number().int().nonnegative(),
        unique_objects_read: z.number().int().nonnegative(),
        blob_bytes_read: z.number().int().nonnegative(),
    })
        .strict(),
})
    .strict();
export const GitCoordinateVerificationEventV1Schema = z.discriminatedUnion('type', [
    GitCoordinateVerificationHeaderV1Schema,
    GitCoordinateVerificationResultV1Schema,
    GitCoordinateVerificationFooterV1Schema,
]);
export const GitCoordinateVerificationOptionsSchema = z
    .object({
    repoPath: z.string().min(1),
    ref: z.string().min(1).optional(),
    maxRequests: z.number().int().positive().default(DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS.maxRequests),
    maxBlobBytes: z.number().int().positive().default(DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS.maxBlobBytes),
    maxBlobLines: z.number().int().positive().default(DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS.maxBlobLines),
    maxTotalBlobBytes: z
        .number()
        .int()
        .positive()
        .default(DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS.maxTotalBlobBytes),
    maxBatchBytes: z.number().int().positive().default(DEFAULT_GIT_COORDINATE_VERIFICATION_OPTIONS.maxBatchBytes),
})
    .strict()
    .superRefine((options, context) => {
    if (options.maxBatchBytes < options.maxBlobBytes) {
        context.addIssue({
            code: 'custom',
            path: ['maxBatchBytes'],
            message: 'maxBatchBytes must be greater than or equal to maxBlobBytes.',
        });
    }
    if (options.ref?.startsWith('-') || options.ref?.includes('\0')) {
        context.addIssue({ code: 'custom', path: ['ref'], message: 'ref is not a safe Git revision.' });
    }
});
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
let cachedOccVersion;
async function readOccVersion() {
    if (cachedOccVersion)
        return cachedOccVersion;
    for (const relative of ['../..', '../../..']) {
        try {
            const parsed = JSON.parse(await readFile(path.resolve(moduleDirectory, relative, 'package.json'), 'utf8'));
            if (typeof parsed.version === 'string' && parsed.version)
                return (cachedOccVersion = parsed.version);
        }
        catch {
            // Source and compiled layouts have different depths.
        }
    }
    return (cachedOccVersion = '0.0.0');
}
function resolveOptions(options) {
    const signal = options.signal;
    const parsed = GitCoordinateVerificationOptionsSchema.parse({
        repoPath: options.repoPath,
        ...(options.ref ? { ref: options.ref } : {}),
        ...(options.maxRequests !== undefined ? { maxRequests: options.maxRequests } : {}),
        ...(options.maxBlobBytes !== undefined ? { maxBlobBytes: options.maxBlobBytes } : {}),
        ...(options.maxBlobLines !== undefined ? { maxBlobLines: options.maxBlobLines } : {}),
        ...(options.maxTotalBlobBytes !== undefined ? { maxTotalBlobBytes: options.maxTotalBlobBytes } : {}),
        ...(options.maxBatchBytes !== undefined ? { maxBatchBytes: options.maxBatchBytes } : {}),
    });
    return {
        repoRoot: path.resolve(parsed.repoPath),
        ...(parsed.ref ? { ref: parsed.ref } : {}),
        maxRequests: parsed.maxRequests,
        maxBlobBytes: parsed.maxBlobBytes,
        maxBlobLines: parsed.maxBlobLines,
        maxTotalBlobBytes: parsed.maxTotalBlobBytes,
        maxBatchBytes: parsed.maxBatchBytes,
        ...(signal ? { signal } : {}),
    };
}
/** Hash one already LF-normalized span without retaining it in a receipt. */
export function computeGitSpanDigest(spanSource) {
    return createHash('sha256').update(spanSource, 'utf8').digest('hex');
}
/** Select a one-based inclusive line span and normalize line separators to LF. */
export function selectGitCoordinateSpan(source, span) {
    const lines = source.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
    // A trailing newline terminates the last line; it must not mint a phantom
    // line after EOF, or a span one past the end would verify against sha256('').
    if (lines.length > 1 && lines[lines.length - 1] === '')
        lines.pop();
    if (span.start_line < 1 || span.end_line < span.start_line || span.end_line > lines.length)
        return undefined;
    return { source: lines.slice(span.start_line - 1, span.end_line).join('\n'), lineCount: lines.length };
}
async function resolveCommit(ref, options) {
    const result = await runGit(['rev-parse', '--verify', '--quiet', `${ref}^{commit}`], {
        repoRoot: options.repoRoot,
        signal: options.signal,
        allowExitCodes: [1],
        physicalObjects: true,
    });
    const oid = result.stdout.trim();
    if (result.exitCode !== 0 || !FULL_OID.test(oid))
        throw new Error(`The requested ref "${ref}" is not a commit.`);
    return oid;
}
async function objectFormat(options, fallbackOid) {
    const result = await runGit(['rev-parse', '--show-object-format=storage'], {
        repoRoot: options.repoRoot,
        signal: options.signal,
        physicalObjects: true,
    }).catch(() => ({ stdout: fallbackOid?.length === 64 ? 'sha256\n' : 'sha1\n' }));
    return result.stdout.trim() === 'sha256' ? 'sha256' : 'sha1';
}
function splitNul(buffer) {
    const values = [];
    let start = 0;
    for (let index = 0; index < buffer.length; index++) {
        if (buffer[index] !== 0)
            continue;
        values.push(buffer.subarray(start, index));
        start = index + 1;
    }
    if (start !== buffer.length)
        throw new Error('Git ls-tree output was not NUL-terminated.');
    return values;
}
async function readTreeObjects(resolvedRef, paths, options) {
    const found = new Map();
    const uniquePaths = [...new Set(paths)].sort();
    for (let start = 0; start < uniquePaths.length; start += 128) {
        const batch = uniquePaths.slice(start, start + 128);
        const result = await runGitRaw(['ls-tree', '-z', '--full-tree', resolvedRef, '--', ...batch.map(value => `:(literal)${value}`)], {
            repoRoot: options.repoRoot,
            signal: options.signal,
            physicalObjects: true,
            maxBuffer: Math.max(1_048_576, batch.length * 8_192),
        });
        for (const field of splitNul(result.stdout)) {
            const tab = field.indexOf(0x09);
            if (tab <= 0)
                throw new Error('Git ls-tree emitted a malformed record.');
            const header = field.subarray(0, tab).toString('ascii').split(' ');
            if (header.length !== 3 || !header[1] || !header[2] || !FULL_OID.test(header[2])) {
                throw new Error('Git ls-tree emitted a malformed object header.');
            }
            const relativePath = FATAL_UTF8.decode(field.subarray(tab + 1));
            found.set(relativePath, { type: header[1], oid: header[2] });
        }
    }
    return found;
}
function unavailableDiagnostic(code, message, detail = {}) {
    return { code, message, ...detail };
}
function coverageForChecks(results, pick) {
    if (results.length === 0)
        return 'complete';
    const values = results.map(pick);
    if (values.every(value => value === 'verified' || value === 'not-requested' || value === 'mismatch')) {
        return 'complete';
    }
    return values.every(value => value === 'unavailable') ? 'unavailable' : 'partial';
}
/**
 * Verify immutable Git coordinates without checking out a revision or emitting
 * source. A mismatch is a complete negative observation; unavailable means OCC
 * could not perform the requested check.
 */
export async function* verifyGitCoordinates(requests, input) {
    const options = resolveOptions(input);
    const collected = [];
    for await (const raw of requests) {
        abortIfNeeded(options.signal);
        if (collected.length >= options.maxRequests) {
            throw new Error(`Coordinate verification exceeds the ${options.maxRequests}-request limit.`);
        }
        collected.push(GitCoordinateVerificationRequestV1Schema.parse(raw));
    }
    const [format, resolvedRef, occVersion] = await Promise.all([
        objectFormat(options, collected[0]?.coordinate.oid),
        options.ref ? resolveCommit(options.ref, options) : Promise.resolve(undefined),
        readOccVersion(),
    ]);
    const expectedOidLength = format === 'sha256' ? 64 : 40;
    const uniqueOids = [...new Set(collected.map(request => request.coordinate.oid))];
    const treeObjects = resolvedRef
        ? await readTreeObjects(resolvedRef, collected.map(request => request.coordinate.path), options)
        : new Map();
    const header = {
        schema_version: COORDINATE_SCHEMA_VERSION,
        type: 'header',
        requested_ref: options.ref ?? null,
        resolved_ref: resolvedRef ?? null,
        object_format: format,
        request_count: collected.length,
        unique_object_count: uniqueOids.length,
        limits: {
            max_requests: options.maxRequests,
            max_blob_bytes: options.maxBlobBytes,
            max_blob_lines: options.maxBlobLines,
            max_total_blob_bytes: options.maxTotalBlobBytes,
            max_batch_bytes: options.maxBatchBytes,
        },
        provenance: {
            instrument: 'occ.code.coordinate-verify',
            instrument_version: COORDINATE_SCHEMA_VERSION,
            occ_version: occVersion,
            node_version: process.versions.node,
            acquisition: 'git-cat-file',
            source_bodies: 'omitted',
            span_digest: GIT_SPAN_DIGEST_ALGORITHM,
        },
    };
    yield header;
    const eligibleOids = uniqueOids.filter(oid => oid.length === expectedOidLength);
    const headers = await readGitObjectHeaders(eligibleOids, {
        repoRoot: options.repoRoot,
        signal: options.signal,
        physicalObjects: true,
    });
    const materialized = new Map();
    const wanted = [];
    let bytesReserved = 0;
    for (const oid of uniqueOids) {
        if (oid.length !== expectedOidLength) {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('oid-format-mismatch', `Object ${oid} does not match the repository's ${format} object format.`),
            });
            continue;
        }
        const object = headers.get(oid);
        if (!object || object.missing) {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('missing-object', `Object ${oid} is not physically available.`),
            });
            continue;
        }
        if (object.type !== 'blob') {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('not-a-blob', `Object ${oid} is ${object.type}, not a blob.`),
            });
            continue;
        }
        if (object.size > options.maxBlobBytes) {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('blob-too-large', `Blob ${oid} exceeds the per-blob byte limit.`, {
                    actual: object.size,
                    limit: options.maxBlobBytes,
                }),
            });
            continue;
        }
        if (bytesReserved + object.size > options.maxTotalBlobBytes) {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('total-blob-budget-exhausted', `Reading blob ${oid} would exceed the total byte limit.`, { actual: bytesReserved + object.size, limit: options.maxTotalBlobBytes }),
            });
            continue;
        }
        bytesReserved += object.size;
        wanted.push(oid);
    }
    const read = await readGitObjects(wanted, headers, {
        repoRoot: options.repoRoot,
        signal: options.signal,
        physicalObjects: true,
        maxBatchBytes: options.maxBatchBytes,
    });
    let bytesRead = 0;
    for (const oid of wanted) {
        const buffer = read.get(oid);
        if (!buffer) {
            materialized.set(oid, {
                diagnostic: unavailableDiagnostic('content-unavailable', `Git did not return blob ${oid}.`),
            });
            continue;
        }
        bytesRead += buffer.length;
        materialized.set(oid, { buffer });
    }
    const results = [];
    for (const [requestIndex, request] of collected.entries()) {
        const { coordinate } = request;
        const diagnostics = [];
        const object = materialized.get(coordinate.oid);
        let objectCheck = 'unavailable';
        let spanCheck = 'unavailable';
        let actualSpanDigest = null;
        if (object?.buffer) {
            objectCheck = 'verified';
            if (looksBinary(object.buffer)) {
                diagnostics.push(unavailableDiagnostic('binary-blob', `Blob ${coordinate.oid} contains binary data.`));
            }
            else {
                let source;
                try {
                    source = FATAL_UTF8.decode(object.buffer);
                }
                catch {
                    diagnostics.push(unavailableDiagnostic('invalid-utf8', `Blob ${coordinate.oid} is not valid UTF-8.`));
                }
                if (source !== undefined) {
                    const lines = source.split(/\r\n|\r|\n/);
                    if (lines.length > options.maxBlobLines) {
                        diagnostics.push(unavailableDiagnostic('blob-too-many-lines', `Blob ${coordinate.oid} exceeds the line limit.`, {
                            actual: lines.length,
                            limit: options.maxBlobLines,
                        }));
                    }
                    else {
                        const selected = selectGitCoordinateSpan(source, coordinate.span);
                        if (!selected) {
                            // The blob is fully materialized and decoded, so a span outside
                            // it is a proven negative, not an unperformed check.
                            spanCheck = 'mismatch';
                            diagnostics.push(unavailableDiagnostic('span-out-of-range', `The requested span is outside blob ${coordinate.oid}.`));
                        }
                        else {
                            actualSpanDigest = computeGitSpanDigest(selected.source);
                            spanCheck = actualSpanDigest === coordinate.span_digest ? 'verified' : 'mismatch';
                            if (spanCheck === 'mismatch') {
                                diagnostics.push(unavailableDiagnostic('span-digest-mismatch', `The stored span digest does not match blob ${coordinate.oid}.`));
                            }
                        }
                    }
                }
            }
        }
        else if (object?.diagnostic) {
            diagnostics.push(object.diagnostic);
            if (object.diagnostic.code === 'oid-format-mismatch' || object.diagnostic.code === 'not-a-blob') {
                objectCheck = 'mismatch';
            }
        }
        else {
            diagnostics.push(unavailableDiagnostic('content-unavailable', `Blob ${coordinate.oid} was not materialized.`));
        }
        let refCheck = 'not-requested';
        let refOid = null;
        if (resolvedRef) {
            const treeObject = treeObjects.get(coordinate.path);
            if (!treeObject || treeObject.type !== 'blob') {
                refCheck = 'mismatch';
                diagnostics.push(unavailableDiagnostic('ref-path-missing', `The pinned tree has no blob at ${coordinate.path}.`));
            }
            else {
                refOid = treeObject.oid;
                refCheck = treeObject.oid === coordinate.oid ? 'verified' : 'mismatch';
                if (refCheck === 'mismatch') {
                    diagnostics.push(unavailableDiagnostic('ref-object-mismatch', `The pinned tree maps ${coordinate.path} to ${treeObject.oid}, not ${coordinate.oid}.`));
                }
            }
        }
        const checks = { object: objectCheck, span: spanCheck, ref_binding: refCheck };
        const checkValues = [objectCheck, spanCheck, refCheck];
        // A proven mismatch is a complete negative observation: it must not be
        // downgraded because a dependent check could not run on the refuted claim.
        const status = checkValues.includes('mismatch')
            ? 'mismatch'
            : checkValues.includes('unavailable')
                ? 'unavailable'
                : 'verified';
        const result = {
            schema_version: COORDINATE_SCHEMA_VERSION,
            type: 'result',
            request_index: requestIndex,
            request_id: request.request_id,
            coordinate,
            status,
            actual_span_digest: actualSpanDigest,
            ref_oid: refOid,
            checks,
            diagnostics,
        };
        results.push(result);
        yield result;
    }
    const verified = results.filter(result => result.status === 'verified').length;
    const mismatched = results.filter(result => result.status === 'mismatch').length;
    const unavailable = results.filter(result => result.status === 'unavailable').length;
    const objectCoverage = coverageForChecks(results, result => result.checks.object);
    const spanCoverage = coverageForChecks(results, result => result.checks.span);
    const refCoverage = coverageForChecks(results, result => result.checks.ref_binding);
    yield {
        schema_version: COORDINATE_SCHEMA_VERSION,
        type: 'footer',
        complete: unavailable === 0,
        coverage: {
            requests: 'complete',
            objects: objectCoverage,
            spans: spanCoverage,
            ref_bindings: refCoverage,
        },
        counts: {
            requests: results.length,
            verified,
            mismatched,
            unavailable,
            unique_objects_read: read.size,
            blob_bytes_read: bytesRead,
        },
    };
}
export const GitCoordinateVerificationBatchV1Schema = z
    .object({
    header: GitCoordinateVerificationHeaderV1Schema,
    results: z.array(GitCoordinateVerificationResultV1Schema),
    footer: GitCoordinateVerificationFooterV1Schema,
})
    .strict()
    .superRefine((batch, context) => {
    const { header, results, footer } = batch;
    if (header.request_count !== results.length || footer.counts.requests !== results.length) {
        context.addIssue({ code: 'custom', message: 'Coordinate receipt request counts do not match.' });
    }
    if (header.unique_object_count !== new Set(results.map(result => result.coordinate.oid)).size) {
        context.addIssue({ code: 'custom', message: 'Coordinate receipt unique-object count does not match.' });
    }
    if (results.some((result, index) => result.request_index !== index)) {
        context.addIssue({ code: 'custom', message: 'Coordinate results must be ordered and contiguous.' });
    }
    const counts = {
        verified: results.filter(result => result.status === 'verified').length,
        mismatched: results.filter(result => result.status === 'mismatch').length,
        unavailable: results.filter(result => result.status === 'unavailable').length,
    };
    for (const [index, result] of results.entries()) {
        const values = [result.checks.object, result.checks.span, result.checks.ref_binding];
        // Mirror the emitter's precedence: a proven mismatch is a complete
        // negative observation even when a dependent check could not run.
        const expectedStatus = values.includes('mismatch')
            ? 'mismatch'
            : values.includes('unavailable')
                ? 'unavailable'
                : 'verified';
        if (result.status !== expectedStatus) {
            context.addIssue({
                code: 'custom',
                path: ['results', index, 'status'],
                message: 'Coordinate result status does not match its checks.',
            });
        }
    }
    const expectedCoverage = {
        requests: 'complete',
        objects: coverageForChecks(results, result => result.checks.object),
        spans: coverageForChecks(results, result => result.checks.span),
        ref_bindings: coverageForChecks(results, result => result.checks.ref_binding),
    };
    if (footer.coverage.requests !== expectedCoverage.requests ||
        footer.coverage.objects !== expectedCoverage.objects ||
        footer.coverage.spans !== expectedCoverage.spans ||
        footer.coverage.ref_bindings !== expectedCoverage.ref_bindings) {
        context.addIssue({
            code: 'custom',
            path: ['footer', 'coverage'],
            message: 'Coordinate coverage is inconsistent.',
        });
    }
    if (footer.counts.verified !== counts.verified ||
        footer.counts.mismatched !== counts.mismatched ||
        footer.counts.unavailable !== counts.unavailable ||
        footer.complete !== (counts.unavailable === 0)) {
        context.addIssue({ code: 'custom', message: 'Coordinate footer status counts are inconsistent.' });
    }
});
/** Collect through EOF and cross-check a whole coordinate-verification run. */
export async function collectGitCoordinateVerificationBatch(events) {
    let header;
    let footer;
    const results = [];
    for await (const raw of events) {
        const event = GitCoordinateVerificationEventV1Schema.parse(raw);
        if (footer)
            throw new Error('Coordinate-verification event encountered after the footer.');
        if (!header) {
            if (event.type !== 'header')
                throw new Error('Coordinate verification must begin with exactly one header.');
            header = event;
            continue;
        }
        if (event.type === 'header')
            throw new Error('Coordinate verification contains a duplicate header.');
        if (event.type === 'footer')
            footer = event;
        else
            results.push(event);
    }
    if (!header)
        throw new Error('Coordinate verification is missing its header.');
    if (!footer)
        throw new Error('Coordinate verification is missing its footer.');
    return GitCoordinateVerificationBatchV1Schema.parse({ header, results, footer });
}
//# sourceMappingURL=coordinates.js.map