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 } from '../git/client.js';
import { readGitObjectHeaders, readGitObjects } from '../git/objects.js';
import { assertValidGitPath } from '../git/parse.js';
import { abortIfNeeded } from '../utils.js';
import { extractFormatEntities, formatForPath, FormatEntityKindSchema } from './formats/index.js';
import { getLanguageSpec } from './languages.js';
import { CODE_PARSER_VERSION, parseCodeContent } from './parsers.js';
import { loadTreeSitterLanguage } from './tree-sitter.js';
import { CodeSourceSpanV1Schema, NormalizedSymbolKindSchema, ParserCapabilitiesSchema, ParserDiagnosticReasonSchema, toNormalizedSymbolKind, } from './types.js';
import { looksBinary } from './entity/snapshot.js';
const GIT_BLOB_STRUCTURE_SCHEMA_VERSION = 2;
/** Stable defaults for the immutable Git-blob structure instrument. */
export const DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS = Object.freeze({
    maxRequests: 10_000,
    maxBlobBytes: 2_097_152,
    maxBlobLines: 25_000,
    maxTotalBlobBytes: 268_435_456,
    maxBatchBytes: 33_554_432,
    maxSpans: 2_000,
    formats: true,
    skipMinified: true,
});
const FullGitObjectIdSchema = z
    .string()
    .regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/, 'Expected a full lowercase Git object id (40 or 64 hex characters).');
const RelativePosixPathSchema = 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: 'relativePath must use POSIX separators.' });
    }
    if (value.includes('\0')) {
        context.addIssue({ code: 'custom', message: 'relativePath must not contain NUL.' });
    }
    if (value.startsWith('/') || /^[A-Za-z]:/.test(value)) {
        context.addIssue({ code: 'custom', message: 'relativePath must be relative.' });
    }
    const segments = value.split('/');
    if (segments.some(segment => segment.length === 0 || segment === '.' || segment === '..')) {
        context.addIssue({
            code: 'custom',
            message: 'relativePath must be normalized and must not contain empty, . or .. segments.',
        });
    }
});
/** Whether a committed tree path can be expressed as a blob-structure request. */
export function isRepresentableBlobPath(relativePath) {
    return RelativePosixPathSchema.safeParse(relativePath).success;
}
export const GitBlobStructureRequestV1Schema = z
    .object({
    requestId: z.string().min(1).max(512),
    oid: FullGitObjectIdSchema,
    relativePath: RelativePosixPathSchema,
})
    .strict();
export const GitBlobStructureOptionsSchema = z
    .object({
    repoPath: z.string().min(1),
    maxRequests: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxRequests),
    maxBlobBytes: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxBlobBytes),
    maxBlobLines: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxBlobLines),
    maxTotalBlobBytes: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxTotalBlobBytes),
    maxBatchBytes: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxBatchBytes),
    maxSpans: z.number().int().positive().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.maxSpans),
    formats: z.boolean().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.formats),
    skipMinified: z.boolean().default(DEFAULT_GIT_BLOB_STRUCTURE_OPTIONS.skipMinified),
})
    .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.',
        });
    }
});
export const GitBlobSourceSpanV1Schema = z
    .object({
    startLine: z.number().int().positive(),
    startColumn: z.number().int().positive().optional(),
    endLine: z.number().int().positive(),
    endColumn: z.number().int().positive().optional(),
    exact: z.boolean(),
})
    .strict()
    .superRefine((span, context) => {
    if (span.endLine < span.startLine) {
        context.addIssue({ code: 'custom', message: 'endLine must not precede startLine.' });
    }
});
const GitBlobDeclarationKindV1Schema = z.union([
    NormalizedSymbolKindSchema,
    FormatEntityKindSchema,
    z.enum(['test', 'test_suite']),
]);
export const GitBlobDeclarationV1Schema = z
    .object({
    kind: GitBlobDeclarationKindV1Schema,
    name: z.string(),
    qualifiedName: z.string(),
    containerName: z.string().optional(),
    signature: z.string().optional(),
    isExported: z.boolean(),
    ordinal: z.number().int().nonnegative(),
    declarationIndex: z.number().int().nonnegative(),
    span: GitBlobSourceSpanV1Schema,
})
    .strict();
const GitBlobImportBindingV1Schema = z
    .object({
    localName: z.string(),
    importedName: z.string().optional(),
    isNamespace: z.boolean().optional(),
    isDefault: z.boolean().optional(),
})
    .strict();
const GitBlobImportFactV1Schema = z
    .object({
    factKind: z.literal('import'),
    specifier: z.string(),
    importKind: z.enum(['local', 'external', 'unresolved']),
    bindings: z.array(GitBlobImportBindingV1Schema),
    isReExport: z.boolean().optional(),
    span: GitBlobSourceSpanV1Schema,
})
    .strict();
const GitBlobCallFactV1Schema = z
    .object({
    factKind: z.literal('call'),
    callerName: z.string(),
    callerContainerName: z.string().optional(),
    calleeName: z.string(),
    qualifier: z.string().optional(),
    span: GitBlobSourceSpanV1Schema,
})
    .strict();
const GitBlobInheritanceFactV1Schema = z
    .object({
    factKind: z.literal('inheritance'),
    className: z.string(),
    baseName: z.string(),
    inheritanceKind: z.enum(['extends', 'implements']).optional(),
    span: GitBlobSourceSpanV1Schema,
})
    .strict();
export const GitBlobSyntacticFactV1Schema = z.discriminatedUnion('factKind', [
    GitBlobImportFactV1Schema,
    GitBlobCallFactV1Schema,
    GitBlobInheritanceFactV1Schema,
]);
export const GitBlobStructureDiagnosticV1Schema = z
    .object({
    code: z.enum([
        'git-read-failed',
        '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',
        'unsupported-path',
        'parser-skipped',
        'parser-failed',
        'span-limit-exhausted',
    ]),
    message: z.string(),
    limit: z.number().int().nonnegative().optional(),
    actual: z.number().int().nonnegative().optional(),
})
    .strict();
const GitBlobParserTierV1Schema = z.enum([
    'typescript-compiler',
    'vue-sfc-typescript',
    'tree-sitter',
    'regex-fallback',
    'format',
    'none',
]);
export const GitBlobStructureProvenanceV1Schema = z
    .object({
    instrumentName: z.literal('occ.git-blob-structure'),
    instrumentVersion: z.literal(1),
    parserName: z.literal('occ.code.parsers'),
    parserVersion: z.literal(1),
    occVersion: z.string().min(1),
    nodeVersion: z.string().min(1),
    sourceSystem: z.literal('git'),
    acquisition: z.literal('git-cat-file'),
    immutableObject: z.literal(true),
    objectFormat: z.enum(['sha1', 'sha256', 'unknown']),
    parserTier: GitBlobParserTierV1Schema,
    language: z.string().optional(),
    grammar: z.string().optional(),
    importResolution: z.literal(false),
    bodyIncluded: z.literal(false),
})
    .strict();
const GitBlobStructureCoverageV1Schema = z
    .object({
    blob: z.enum(['acquired', 'missing', 'not-a-blob', 'omitted', 'error']),
    structure: z.enum(['complete', 'partial', 'unavailable']),
    expectedBytes: z.number().int().nonnegative().optional(),
    acquiredBytes: z.number().int().nonnegative(),
    lineCount: z.number().int().positive().optional(),
    spansDiscovered: z.number().int().nonnegative(),
    spansEmitted: z.number().int().nonnegative(),
    declarationsDiscovered: z.number().int().nonnegative(),
    declarationsEmitted: z.number().int().nonnegative(),
    factsDiscovered: z.number().int().nonnegative(),
    factsEmitted: z.number().int().nonnegative(),
})
    .strict();
const GitBlobStructureLimitsV1Schema = z
    .object({
    maxRequests: z.number().int().positive(),
    maxBlobBytes: z.number().int().positive(),
    maxBlobLines: z.number().int().positive(),
    maxTotalBlobBytes: z.number().int().positive(),
    maxBatchBytes: z.number().int().positive(),
    maxSpans: z.number().int().positive(),
    formats: z.boolean(),
    skipMinified: z.boolean(),
})
    .strict();
export const GitBlobStructureBatchHeaderV1Schema = z
    .object({
    schemaVersion: z.literal(1),
    type: z.literal('header'),
    requestCount: z.number().int().nonnegative(),
    uniqueObjectCount: z.number().int().nonnegative(),
    limits: GitBlobStructureLimitsV1Schema,
    provenance: z
        .object({
        sourceSystem: z.literal('git'),
        acquisition: z.literal('git-cat-file'),
        immutableObjects: z.literal(true),
        objectFormat: z.enum(['sha1', 'sha256', 'unknown']),
        instrumentName: z.literal('occ.git-blob-structure'),
        instrumentVersion: z.literal(1),
        parserName: z.literal('occ.code.parsers'),
        parserVersion: z.literal(1),
        occVersion: z.string().min(1),
        nodeVersion: z.string().min(1),
        importResolution: z.literal(false),
        bodiesIncluded: z.literal(false),
        hashesIncluded: z.literal(false),
        lineageIncluded: z.literal(false),
    })
        .strict(),
})
    .strict();
export const GitBlobStructureResultV1Schema = z
    .object({
    schemaVersion: z.literal(1),
    type: z.literal('result'),
    requestIndex: z.number().int().nonnegative(),
    requestId: z.string().min(1),
    oid: FullGitObjectIdSchema,
    relativePath: RelativePosixPathSchema,
    status: z.enum(['complete', 'partial', 'unavailable']),
    declarations: z.array(GitBlobDeclarationV1Schema),
    facts: z.array(GitBlobSyntacticFactV1Schema),
    provenance: GitBlobStructureProvenanceV1Schema,
    coverage: GitBlobStructureCoverageV1Schema,
    diagnostics: z.array(GitBlobStructureDiagnosticV1Schema),
})
    .strict();
export const GitBlobStructureBatchFooterV1Schema = z
    .object({
    schemaVersion: z.literal(1),
    type: z.literal('footer'),
    coverage: z
        .object({
        requests: z.number().int().nonnegative(),
        results: z.number().int().nonnegative(),
        uniqueObjectsRequested: z.number().int().nonnegative(),
        uniqueObjectsRead: z.number().int().nonnegative(),
        blobBytesRead: z.number().int().nonnegative(),
        completeResults: z.number().int().nonnegative(),
        partialResults: z.number().int().nonnegative(),
        unavailableResults: z.number().int().nonnegative(),
    })
        .strict(),
})
    .strict();
export const GitBlobStructureBatchEventV1Schema = z.discriminatedUnion('type', [
    GitBlobStructureBatchHeaderV1Schema,
    GitBlobStructureResultV1Schema,
    GitBlobStructureBatchFooterV1Schema,
]);
/**
 * V2 keeps the request envelope stable while replacing line-only structural
 * coordinates with the code index's canonical, half-open UTF-8 byte spans.
 * V1 schemas remain exported for decoding persisted receipts.
 */
export const GitBlobStructureRequestV2Schema = GitBlobStructureRequestV1Schema;
export const GitBlobSourceSpanV2Schema = CodeSourceSpanV1Schema;
export const GitBlobDeclarationV2Schema = GitBlobDeclarationV1Schema.omit({ span: true })
    .extend({ span: GitBlobSourceSpanV2Schema })
    .strict();
const GitBlobImportBindingV2Schema = GitBlobImportBindingV1Schema.extend({
    typeOnly: z.boolean().optional(),
}).strict();
const GitBlobImportFactV2Schema = GitBlobImportFactV1Schema.omit({ span: true, bindings: true })
    .extend({
    bindings: z.array(GitBlobImportBindingV2Schema),
    span: GitBlobSourceSpanV2Schema,
})
    .strict();
const GitBlobCallFactV2Schema = GitBlobCallFactV1Schema.omit({ span: true })
    .extend({ span: GitBlobSourceSpanV2Schema })
    .strict();
const GitBlobInheritanceFactV2Schema = GitBlobInheritanceFactV1Schema.omit({ span: true })
    .extend({ span: GitBlobSourceSpanV2Schema })
    .strict();
export const GitBlobSyntacticFactV2Schema = z.discriminatedUnion('factKind', [
    GitBlobImportFactV2Schema,
    GitBlobCallFactV2Schema,
    GitBlobInheritanceFactV2Schema,
]);
export const GitBlobStructureDiagnosticV2Schema = GitBlobStructureDiagnosticV1Schema.omit({ code: true })
    .extend({
    code: z.enum([
        ...GitBlobStructureDiagnosticV1Schema.shape.code.options,
        'syntax-recovered',
        'fact-coverage-partial',
        'exact-span-unavailable',
    ]),
    reason: ParserDiagnosticReasonSchema.optional(),
})
    .strict();
const UnavailableParserCapabilities = Object.freeze({
    definitions: 'unavailable',
    imports: 'unavailable',
    calls: 'unavailable',
    inheritance: 'unavailable',
    exports: 'unavailable',
    exactSpans: 'unavailable',
});
export const GitBlobStructureProvenanceV2Schema = GitBlobStructureProvenanceV1Schema.omit({
    instrumentVersion: true,
    parserVersion: true,
})
    .extend({
    instrumentVersion: z.literal(GIT_BLOB_STRUCTURE_SCHEMA_VERSION),
    parserVersion: z.union([z.literal(2), z.literal(3), z.literal(CODE_PARSER_VERSION)]),
    spanEncoding: z.literal('utf8-bytes'),
    spanEnd: z.literal('exclusive'),
    syntaxStatus: z.enum(['valid', 'recovered', 'unchecked']),
    syntaxErrorCount: z.number().int().nonnegative(),
    capabilities: ParserCapabilitiesSchema,
})
    .strict();
const GitBlobStructureBatchHeaderV2BaseSchema = GitBlobStructureBatchHeaderV1Schema.omit({
    schemaVersion: true,
    provenance: true,
});
export const GitBlobStructureBatchHeaderV2Schema = GitBlobStructureBatchHeaderV2BaseSchema.extend({
    schemaVersion: z.literal(GIT_BLOB_STRUCTURE_SCHEMA_VERSION),
    provenance: GitBlobStructureBatchHeaderV1Schema.shape.provenance
        .omit({ instrumentVersion: true, parserVersion: true })
        .extend({
        instrumentVersion: z.literal(GIT_BLOB_STRUCTURE_SCHEMA_VERSION),
        parserVersion: z.union([z.literal(2), z.literal(3), z.literal(CODE_PARSER_VERSION)]),
        spanEncoding: z.literal('utf8-bytes'),
        spanEnd: z.literal('exclusive'),
        parserEvidence: z.literal('per-result'),
    })
        .strict(),
}).strict();
export const GitBlobStructureResultV2Schema = GitBlobStructureResultV1Schema.omit({
    schemaVersion: true,
    declarations: true,
    facts: true,
    provenance: true,
    diagnostics: true,
})
    .extend({
    schemaVersion: z.literal(GIT_BLOB_STRUCTURE_SCHEMA_VERSION),
    declarations: z.array(GitBlobDeclarationV2Schema),
    facts: z.array(GitBlobSyntacticFactV2Schema),
    provenance: GitBlobStructureProvenanceV2Schema,
    diagnostics: z.array(GitBlobStructureDiagnosticV2Schema),
})
    .strict();
export const GitBlobStructureBatchFooterV2Schema = GitBlobStructureBatchFooterV1Schema.omit({
    schemaVersion: true,
})
    .extend({ schemaVersion: z.literal(GIT_BLOB_STRUCTURE_SCHEMA_VERSION) })
    .strict();
export const GitBlobStructureBatchEventV2Schema = z.discriminatedUnion('type', [
    GitBlobStructureBatchHeaderV2Schema,
    GitBlobStructureResultV2Schema,
    GitBlobStructureBatchFooterV2Schema,
]);
function addBatchIssue(context, path, message) {
    context.addIssue({ code: 'custom', path, message });
}
function sameResultProvenance(header, result) {
    const batch = header.provenance;
    const item = result.provenance;
    return (item.sourceSystem === batch.sourceSystem &&
        item.acquisition === batch.acquisition &&
        item.immutableObject === batch.immutableObjects &&
        item.objectFormat === batch.objectFormat &&
        item.instrumentName === batch.instrumentName &&
        item.instrumentVersion === batch.instrumentVersion &&
        item.parserName === batch.parserName &&
        item.parserVersion === batch.parserVersion &&
        item.occVersion === batch.occVersion &&
        item.nodeVersion === batch.nodeVersion &&
        item.importResolution === batch.importResolution &&
        item.bodyIncluded === batch.bodiesIncluded);
}
function validateBatchReceipt(batch, context) {
    const { header, blobs, footer } = batch;
    const footerCoverage = footer.coverage;
    const uniqueOids = new Set(blobs.map(blob => blob.oid));
    if (header.requestCount !== blobs.length) {
        addBatchIssue(context, ['header', 'requestCount'], 'Header requestCount must equal the number of results.');
    }
    if (header.uniqueObjectCount !== uniqueOids.size) {
        addBatchIssue(context, ['header', 'uniqueObjectCount'], 'Header uniqueObjectCount must equal the distinct result object count.');
    }
    if (header.requestCount > header.limits.maxRequests) {
        addBatchIssue(context, ['header', 'requestCount'], 'Header requestCount exceeds the advertised request limit.');
    }
    if (footerCoverage.requests !== header.requestCount || footerCoverage.requests !== blobs.length) {
        addBatchIssue(context, ['footer', 'coverage', 'requests'], 'Footer request count does not match the batch.');
    }
    if (footerCoverage.results !== blobs.length) {
        addBatchIssue(context, ['footer', 'coverage', 'results'], 'Footer result count does not match the batch.');
    }
    if (footerCoverage.uniqueObjectsRequested !== header.uniqueObjectCount ||
        footerCoverage.uniqueObjectsRequested !== uniqueOids.size) {
        addBatchIssue(context, ['footer', 'coverage', 'uniqueObjectsRequested'], 'Footer unique-object request count does not match the batch.');
    }
    const statusCounts = { complete: 0, partial: 0, unavailable: 0 };
    const acquiredObjects = new Map();
    const objectAcquisition = new Map();
    for (const [position, blob] of blobs.entries()) {
        statusCounts[blob.status] += 1;
        if (blob.requestIndex !== position) {
            addBatchIssue(context, ['blobs', position, 'requestIndex'], 'Result requestIndex values must be unique, ordered, and contiguous from zero.');
        }
        if (!sameResultProvenance(header, blob)) {
            addBatchIssue(context, ['blobs', position, 'provenance'], 'Result provenance does not match the batch header provenance.');
        }
        if (header.provenance.objectFormat !== 'unknown' &&
            blob.oid.length !== (header.provenance.objectFormat === 'sha1' ? 40 : 64)) {
            addBatchIssue(context, ['blobs', position, 'oid'], 'Result object id length does not match the advertised object format.');
        }
        const coverage = blob.coverage;
        const priorAcquisition = objectAcquisition.get(blob.oid);
        if (priorAcquisition &&
            (priorAcquisition.blob !== coverage.blob ||
                priorAcquisition.acquiredBytes !== coverage.acquiredBytes ||
                priorAcquisition.expectedBytes !== coverage.expectedBytes)) {
            addBatchIssue(context, ['blobs', position, 'coverage'], 'Repeated results for one immutable object disagree about blob acquisition.');
        }
        else if (!priorAcquisition) {
            objectAcquisition.set(blob.oid, {
                blob: coverage.blob,
                acquiredBytes: coverage.acquiredBytes,
                expectedBytes: coverage.expectedBytes,
            });
        }
        const expectedStructure = blob.status === 'complete' ? 'complete' : blob.status === 'partial' ? 'partial' : 'unavailable';
        if (coverage.structure !== expectedStructure) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'structure'], 'Result status and structure coverage must describe the same state.');
        }
        if (coverage.declarationsEmitted !== blob.declarations.length) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'declarationsEmitted'], 'Emitted declaration count does not match the declaration payload.');
        }
        if (coverage.factsEmitted !== blob.facts.length) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'factsEmitted'], 'Emitted fact count does not match the fact payload.');
        }
        if (coverage.declarationsDiscovered < coverage.declarationsEmitted) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'declarationsDiscovered'], 'Discovered declarations cannot be fewer than emitted declarations.');
        }
        if (coverage.factsDiscovered < coverage.factsEmitted) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'factsDiscovered'], 'Discovered facts cannot be fewer than emitted facts.');
        }
        if (coverage.spansEmitted !== coverage.declarationsEmitted + coverage.factsEmitted) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'spansEmitted'], 'Emitted span count must equal emitted declarations plus emitted facts.');
        }
        if (coverage.spansDiscovered !== coverage.declarationsDiscovered + coverage.factsDiscovered) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'spansDiscovered'], 'Discovered span count must equal discovered declarations plus discovered facts.');
        }
        if (coverage.spansEmitted > header.limits.maxSpans) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'spansEmitted'], 'Emitted span count exceeds the advertised per-result span limit.');
        }
        const declarationIndexes = new Set();
        for (const [declarationPosition, declaration] of blob.declarations.entries()) {
            if (declarationIndexes.has(declaration.declarationIndex)) {
                addBatchIssue(context, ['blobs', position, 'declarations', declarationPosition, 'declarationIndex'], 'Declaration indexes must be unique within a result.');
            }
            declarationIndexes.add(declaration.declarationIndex);
            if (declaration.declarationIndex >= coverage.declarationsDiscovered) {
                addBatchIssue(context, ['blobs', position, 'declarations', declarationPosition, 'declarationIndex'], 'Declaration index falls outside the discovered declaration range.');
            }
            if (coverage.lineCount !== undefined && declaration.span.endLine > coverage.lineCount) {
                addBatchIssue(context, ['blobs', position, 'declarations', declarationPosition, 'span'], 'Declaration span falls outside the acquired source line range.');
            }
        }
        for (const [factPosition, fact] of blob.facts.entries()) {
            if (coverage.lineCount !== undefined && fact.span.endLine > coverage.lineCount) {
                addBatchIssue(context, ['blobs', position, 'facts', factPosition, 'span'], 'Fact span falls outside the acquired source line range.');
            }
        }
        if (blob.schemaVersion === GIT_BLOB_STRUCTURE_SCHEMA_VERSION) {
            const structuralSpans = [
                ...blob.declarations.map(declaration => declaration.span),
                ...blob.facts.map(fact => fact.span),
            ];
            for (const [spanPosition, span] of structuralSpans.entries()) {
                if (span.startByte !== undefined &&
                    span.endByte !== undefined &&
                    (span.startByte > coverage.acquiredBytes || span.endByte > coverage.acquiredBytes)) {
                    addBatchIssue(context, ['blobs', position, 'spans', spanPosition], 'Canonical UTF-8 span falls outside the acquired blob bytes.');
                }
            }
            if (blob.status === 'complete') {
                const requiredCapabilities = Object.values(blob.provenance.capabilities);
                if (blob.provenance.syntaxStatus !== 'valid' ||
                    requiredCapabilities.some(capability => capability !== 'full') ||
                    structuralSpans.some(span => !span.exact)) {
                    addBatchIssue(context, ['blobs', position, 'provenance'], 'A complete V2 result requires valid syntax, full fact coverage, and exact UTF-8 spans.');
                }
            }
        }
        if (coverage.blob === 'acquired') {
            if (coverage.expectedBytes === undefined || coverage.acquiredBytes !== coverage.expectedBytes) {
                addBatchIssue(context, ['blobs', position, 'coverage', 'acquiredBytes'], 'An acquired blob must include all expected bytes.');
            }
            const prior = acquiredObjects.get(blob.oid);
            if (prior && (prior.acquiredBytes !== coverage.acquiredBytes || prior.expectedBytes !== coverage.expectedBytes)) {
                addBatchIssue(context, ['blobs', position, 'coverage'], 'Repeated results for one object disagree about acquired bytes.');
            }
            else if (!prior) {
                acquiredObjects.set(blob.oid, {
                    acquiredBytes: coverage.acquiredBytes,
                    expectedBytes: coverage.expectedBytes,
                });
            }
            if (coverage.acquiredBytes > header.limits.maxBlobBytes) {
                addBatchIssue(context, ['blobs', position, 'coverage', 'acquiredBytes'], 'Acquired blob bytes exceed the advertised per-blob bound.');
            }
        }
        else if (coverage.acquiredBytes !== 0) {
            addBatchIssue(context, ['blobs', position, 'coverage', 'acquiredBytes'], 'A blob that was not acquired cannot report acquired bytes.');
        }
        if (blob.status === 'complete') {
            if (coverage.blob !== 'acquired' ||
                blob.diagnostics.length !== 0 ||
                coverage.declarationsDiscovered !== coverage.declarationsEmitted ||
                coverage.factsDiscovered !== coverage.factsEmitted ||
                coverage.spansDiscovered !== coverage.spansEmitted) {
                addBatchIssue(context, ['blobs', position], 'A complete result requires a fully acquired blob, exhaustive emitted structure, and no evidence-gap diagnostics.');
            }
        }
        else if (blob.status === 'partial') {
            if (coverage.blob !== 'acquired' || blob.diagnostics.length === 0) {
                addBatchIssue(context, ['blobs', position], 'A partial result requires a fully acquired blob and an explicit evidence-gap diagnostic.');
            }
        }
        else if (blob.declarations.length !== 0 ||
            blob.facts.length !== 0 ||
            coverage.declarationsDiscovered !== 0 ||
            coverage.declarationsEmitted !== 0 ||
            coverage.factsDiscovered !== 0 ||
            coverage.factsEmitted !== 0 ||
            coverage.spansDiscovered !== 0 ||
            coverage.spansEmitted !== 0 ||
            blob.diagnostics.length === 0) {
            addBatchIssue(context, ['blobs', position], 'An unavailable result cannot contain structure and must explain the evidence gap.');
        }
    }
    if (footerCoverage.completeResults !== statusCounts.complete) {
        addBatchIssue(context, ['footer', 'coverage', 'completeResults'], 'Footer complete-result tally is incorrect.');
    }
    if (footerCoverage.partialResults !== statusCounts.partial) {
        addBatchIssue(context, ['footer', 'coverage', 'partialResults'], 'Footer partial-result tally is incorrect.');
    }
    if (footerCoverage.unavailableResults !== statusCounts.unavailable) {
        addBatchIssue(context, ['footer', 'coverage', 'unavailableResults'], 'Footer unavailable-result tally is incorrect.');
    }
    if (footerCoverage.uniqueObjectsRead !== acquiredObjects.size) {
        addBatchIssue(context, ['footer', 'coverage', 'uniqueObjectsRead'], 'Footer unique-object read count is not supported by result acquisition evidence.');
    }
    const bytesRead = [...acquiredObjects.values()].reduce((total, object) => total + object.acquiredBytes, 0);
    if (footerCoverage.blobBytesRead !== bytesRead) {
        addBatchIssue(context, ['footer', 'coverage', 'blobBytesRead'], 'Footer blob-byte total is not supported by result acquisition evidence.');
    }
    if (footerCoverage.blobBytesRead > header.limits.maxTotalBlobBytes) {
        addBatchIssue(context, ['footer', 'coverage', 'blobBytesRead'], 'Footer blob-byte total exceeds the advertised total-byte bound.');
    }
    if (header.limits.maxBatchBytes < header.limits.maxBlobBytes) {
        addBatchIssue(context, ['header', 'limits', 'maxBatchBytes'], 'Advertised batch-byte bound must be at least the per-blob bound.');
    }
}
/**
 * A complete, internally cross-checked V1 receipt for one blob-structure run.
 *
 * This establishes completeness within one trusted local producer process. It
 * is not authentication of a transported payload; hostile-payload validation
 * would require independently rereading and reparsing every physical blob.
 */
export const GitBlobStructureBatchV1Schema = z
    .object({
    header: GitBlobStructureBatchHeaderV1Schema,
    blobs: z.array(GitBlobStructureResultV1Schema),
    footer: GitBlobStructureBatchFooterV1Schema,
})
    .strict()
    .superRefine(validateBatchReceipt);
export const GitBlobStructureBatchV2Schema = z
    .object({
    header: GitBlobStructureBatchHeaderV2Schema,
    blobs: z.array(GitBlobStructureResultV2Schema),
    footer: GitBlobStructureBatchFooterV2Schema,
})
    .strict()
    .superRefine(validateBatchReceipt);
/**
 * Consume an entire event stream and return its verified receipt.
 *
 * The collector deliberately reads through EOF so a valid-looking prefix
 * cannot hide a duplicate footer or any event appended after the footer.
 */
export async function collectGitBlobStructureBatchV1(events) {
    let header;
    let footer;
    const blobs = [];
    for await (const rawEvent of events) {
        const event = GitBlobStructureBatchEventV1Schema.parse(rawEvent);
        if (footer)
            throw new Error('Git blob structure event encountered after the batch footer.');
        if (!header) {
            if (event.type !== 'header')
                throw new Error('Git blob structure batch must begin with exactly one header.');
            header = event;
            continue;
        }
        if (event.type === 'header')
            throw new Error('Git blob structure batch contains a duplicate header.');
        if (event.type === 'footer') {
            footer = event;
            continue;
        }
        blobs.push(event);
    }
    if (!header)
        throw new Error('Git blob structure batch is missing its header.');
    if (!footer)
        throw new Error('Git blob structure batch is missing its footer.');
    return GitBlobStructureBatchV1Schema.parse({ header, blobs, footer });
}
/** Consume and verify the V2 stream emitted by this build. */
export async function collectGitBlobStructureBatch(events) {
    let header;
    let footer;
    const blobs = [];
    for await (const rawEvent of events) {
        const event = GitBlobStructureBatchEventV2Schema.parse(rawEvent);
        if (footer)
            throw new Error('Git blob structure event encountered after the batch footer.');
        if (!header) {
            if (event.type !== 'header')
                throw new Error('Git blob structure batch must begin with exactly one header.');
            header = event;
            continue;
        }
        if (event.type === 'header')
            throw new Error('Git blob structure batch contains a duplicate header.');
        if (event.type === 'footer') {
            footer = event;
            continue;
        }
        blobs.push(event);
    }
    if (!header)
        throw new Error('Git blob structure batch is missing its header.');
    if (!footer)
        throw new Error('Git blob structure batch is missing its footer.');
    return GitBlobStructureBatchV2Schema.parse({ header, blobs, footer });
}
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
const moduleDirectory = path.dirname(fileURLToPath(import.meta.url));
let cachedOccVersion;
async function readOccVersion() {
    if (cachedOccVersion)
        return cachedOccVersion;
    for (const relative of ['../..', '../../..']) {
        try {
            const raw = await readFile(path.resolve(moduleDirectory, relative, 'package.json'), 'utf8');
            const parsed = JSON.parse(raw);
            if (typeof parsed.version === 'string' && parsed.version.length > 0) {
                cachedOccVersion = parsed.version;
                return parsed.version;
            }
        }
        catch {
            // Source and compiled layouts have different depths; try both.
        }
    }
    cachedOccVersion = '0.0.0';
    return cachedOccVersion;
}
function parseOptions(options) {
    const { signal, ...serializable } = options;
    const parsed = GitBlobStructureOptionsSchema.parse(serializable);
    return { ...parsed, signal };
}
async function collectRequests(requests, options) {
    const collected = [];
    for await (const request of requests) {
        abortIfNeeded(options.signal);
        if (collected.length >= options.maxRequests) {
            throw new Error(`Git blob structure request count exceeds the ${options.maxRequests}-request limit.`);
        }
        collected.push(GitBlobStructureRequestV1Schema.parse(request));
    }
    return collected;
}
function safeMessage(error, repoPath) {
    const raw = error instanceof Error ? error.message : String(error);
    return raw.split(path.resolve(repoPath)).join('<repository>');
}
function noParserProvenance(objectFormat, producer) {
    return {
        instrumentName: 'occ.git-blob-structure',
        instrumentVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
        parserName: 'occ.code.parsers',
        parserVersion: CODE_PARSER_VERSION,
        ...producer,
        sourceSystem: 'git',
        acquisition: 'git-cat-file',
        immutableObject: true,
        objectFormat,
        parserTier: 'none',
        spanEncoding: 'utf8-bytes',
        spanEnd: 'exclusive',
        syntaxStatus: 'unchecked',
        syntaxErrorCount: 0,
        capabilities: { ...UnavailableParserCapabilities },
        importResolution: false,
        bodyIncluded: false,
    };
}
function emptyCoverage(blob, expectedBytes, acquiredBytes = 0) {
    return {
        blob,
        structure: 'unavailable',
        ...(expectedBytes === undefined ? {} : { expectedBytes }),
        acquiredBytes,
        spansDiscovered: 0,
        spansEmitted: 0,
        declarationsDiscovered: 0,
        declarationsEmitted: 0,
        factsDiscovered: 0,
        factsEmitted: 0,
    };
}
function unavailableResult(request, requestIndex, coverage, diagnostic, objectFormat, producer, parserProvenance) {
    return {
        schemaVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
        type: 'result',
        requestIndex,
        ...request,
        status: 'unavailable',
        declarations: [],
        facts: [],
        provenance: parserProvenance ?? noParserProvenance(objectFormat, producer),
        coverage,
        diagnostics: [diagnostic],
    };
}
function declarationFromSymbol(symbol, declarationIndex, ordinals) {
    const kind = symbol.testRole ?? toNormalizedSymbolKind(symbol.type, symbol.containerName);
    const qualifiedName = symbol.containerName ? `${symbol.containerName}.${symbol.name}` : symbol.name;
    const ordinalKey = `${kind}\0${qualifiedName}`;
    const ordinal = ordinals.get(ordinalKey) ?? 0;
    ordinals.set(ordinalKey, ordinal + 1);
    const declaration = {
        kind,
        name: symbol.name,
        qualifiedName,
        isExported: symbol.exported === true,
        ordinal,
        declarationIndex,
        span: symbol.span ?? inexactSpan(symbol.line, symbol.endLine),
    };
    if (symbol.containerName)
        declaration.containerName = symbol.containerName;
    if (symbol.signature)
        declaration.signature = symbol.signature;
    return declaration;
}
function inexactSpan(startLine, endLine = startLine) {
    return {
        version: 1,
        offsetEncoding: 'utf8-bytes',
        endExclusive: true,
        startLine,
        endLine,
        exact: false,
    };
}
function projectParsedFile(file) {
    const orderedSymbols = [...file.symbols].sort((left, right) => left.line - right.line || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
    const ordinals = new Map();
    const declarations = orderedSymbols.map((symbol, index) => declarationFromSymbol(symbol, index, ordinals));
    const facts = [];
    for (const imported of file.imports) {
        facts.push({
            factKind: 'import',
            specifier: imported.specifier,
            importKind: imported.kind,
            bindings: imported.bindings.map(binding => ({ ...binding })),
            ...(imported.isReExport === undefined ? {} : { isReExport: imported.isReExport }),
            span: imported.span ?? inexactSpan(imported.line),
        });
    }
    for (const call of file.calls) {
        facts.push({
            factKind: 'call',
            callerName: call.callerName,
            ...(call.callerContainerName ? { callerContainerName: call.callerContainerName } : {}),
            calleeName: call.calleeName,
            ...(call.qualifier ? { qualifier: call.qualifier } : {}),
            span: call.span ?? inexactSpan(call.line),
        });
    }
    for (const inheritance of file.inheritances) {
        facts.push({
            factKind: 'inheritance',
            className: inheritance.className,
            baseName: inheritance.baseName,
            ...(inheritance.kind ? { inheritanceKind: inheritance.kind } : {}),
            span: inheritance.span ?? inexactSpan(inheritance.line),
        });
    }
    return { declarations, facts };
}
function limitProjection(declarations, facts, maxSpans) {
    let discoveryOrder = 0;
    const items = [
        ...declarations.map(declaration => ({
            line: declaration.span.startLine,
            kindOrder: 0,
            discoveryOrder: discoveryOrder++,
            declaration,
        })),
        ...facts.map(fact => ({
            line: fact.span.startLine,
            kindOrder: fact.factKind === 'import' ? 1 : fact.factKind === 'inheritance' ? 2 : 3,
            discoveryOrder: discoveryOrder++,
            fact,
        })),
    ];
    items.sort((left, right) => left.line - right.line || left.kindOrder - right.kindOrder || left.discoveryOrder - right.discoveryOrder);
    const emitted = items.slice(0, maxSpans);
    return {
        declarations: emitted.flatMap(item => (item.declaration ? [item.declaration] : [])),
        facts: emitted.flatMap(item => (item.fact ? [item.fact] : [])),
        spansDiscovered: items.length,
        declarationsDiscovered: declarations.length,
        factsDiscovered: facts.length,
        truncated: emitted.length < items.length,
    };
}
async function actualParserProvenance(relativePath, objectFormat, producer) {
    const spec = getLanguageSpec(relativePath);
    if (!spec) {
        const format = formatForPath(relativePath);
        return {
            ...noParserProvenance(objectFormat, producer),
            parserTier: format ? 'format' : 'none',
            ...(format ? { language: format } : {}),
        };
    }
    if (spec.parser === 'typescript') {
        return { ...noParserProvenance(objectFormat, producer), parserTier: 'typescript-compiler', language: spec.name };
    }
    if (spec.parser === 'vue') {
        return { ...noParserProvenance(objectFormat, producer), parserTier: 'vue-sfc-typescript', language: spec.name };
    }
    if (spec.treeSitter) {
        let loaded;
        try {
            loaded = (await loadTreeSitterLanguage(spec.treeSitter.grammar)) !== null;
        }
        catch {
            // A loader failure means tree-sitter was the recognized and attempted
            // tier. The parse below will retain this provenance with parser-failed.
            loaded = true;
        }
        return {
            ...noParserProvenance(objectFormat, producer),
            parserTier: loaded ? 'tree-sitter' : 'regex-fallback',
            language: spec.name,
            grammar: spec.treeSitter.grammar,
        };
    }
    return { ...noParserProvenance(objectFormat, producer), parserTier: 'regex-fallback', language: spec.name };
}
function parsedProvenance(base, parsed) {
    return {
        ...base,
        parserTier: parsed.tier,
        ...(parsed.grammar ? { grammar: parsed.grammar } : {}),
        syntaxStatus: parsed.syntax.status,
        syntaxErrorCount: parsed.syntax.errorCount,
        capabilities: { ...parsed.capabilities },
    };
}
function parserGapDiagnostics(provenance, declarations, facts, reason) {
    const diagnostics = [];
    if (provenance.syntaxStatus !== 'valid') {
        diagnostics.push({
            code: 'syntax-recovered',
            message: provenance.syntaxStatus === 'recovered'
                ? `The parser recovered from ${provenance.syntaxErrorCount} syntax error(s).`
                : 'The parser could not prove that the syntax was valid.',
            actual: provenance.syntaxErrorCount,
            ...(reason ? { reason } : {}),
        });
    }
    const incompleteFacts = ['definitions', 'imports', 'calls', 'inheritance', 'exports'].filter(capability => provenance.capabilities[capability] !== 'full');
    if (incompleteFacts.length > 0) {
        diagnostics.push({
            code: 'fact-coverage-partial',
            message: `Parser coverage is not exhaustive for: ${incompleteFacts.join(', ')}.`,
            ...(reason ? { reason } : {}),
        });
    }
    if (provenance.capabilities.exactSpans !== 'full' ||
        declarations.some(declaration => !declaration.span.exact) ||
        facts.some(fact => !fact.span.exact)) {
        diagnostics.push({
            code: 'exact-span-unavailable',
            message: 'At least one emitted or discoverable structural item lacks a canonical exact UTF-8 byte span.',
        });
    }
    return diagnostics;
}
async function parseStructure(request, buffer, options, objectFormat, producer) {
    let content;
    try {
        content = utf8Decoder.decode(buffer);
    }
    catch {
        return { diagnostic: { code: 'invalid-utf8', message: 'Git blob is not valid UTF-8.' } };
    }
    const lineCount = content.split(/\r?\n/).length;
    if (lineCount > options.maxBlobLines) {
        return {
            lineCount,
            diagnostic: {
                code: 'blob-too-many-lines',
                message: `Git blob has ${lineCount} lines; the limit is ${options.maxBlobLines}.`,
                limit: options.maxBlobLines,
                actual: lineCount,
            },
        };
    }
    let provenance = await actualParserProvenance(request.relativePath, objectFormat, producer);
    if (provenance.parserTier === 'none') {
        return {
            lineCount,
            provenance,
            diagnostic: { code: 'unsupported-path', message: 'No structural parser is registered for this path.' },
        };
    }
    try {
        let declarations = [];
        let facts = [];
        let gapReason;
        if (provenance.parserTier === 'format') {
            if (!options.formats) {
                return {
                    lineCount,
                    provenance,
                    diagnostic: { code: 'unsupported-path', message: 'Data-format structure extraction is disabled.' },
                };
            }
            const extracted = extractFormatEntities(request.relativePath, content);
            if (!extracted) {
                return {
                    lineCount,
                    provenance,
                    diagnostic: { code: 'unsupported-path', message: 'No structural parser is registered for this path.' },
                };
            }
            gapReason = extracted.parseFailed ? 'parse-failed' : undefined;
            provenance = {
                ...provenance,
                syntaxStatus: extracted.parseFailed ? 'recovered' : 'valid',
                syntaxErrorCount: extracted.parseFailed ? 1 : 0,
                capabilities: {
                    definitions: extracted.parseFailed ? 'partial' : 'full',
                    imports: extracted.parseFailed ? 'unavailable' : 'full',
                    calls: extracted.parseFailed ? 'unavailable' : 'full',
                    inheritance: extracted.parseFailed ? 'unavailable' : 'full',
                    exports: extracted.parseFailed ? 'unavailable' : 'full',
                    exactSpans: 'unavailable',
                },
            };
            const ordinals = new Map();
            declarations = extracted.entities.map((entity, declarationIndex) => {
                const ordinalKey = `${entity.kind}\0${entity.qualifiedName}`;
                const ordinal = ordinals.get(ordinalKey) ?? 0;
                ordinals.set(ordinalKey, ordinal + 1);
                return {
                    kind: entity.kind,
                    name: entity.name,
                    qualifiedName: entity.qualifiedName,
                    ...(entity.containerName ? { containerName: entity.containerName } : {}),
                    isExported: false,
                    ordinal,
                    declarationIndex,
                    span: inexactSpan(entity.startLine, entity.endLine),
                };
            });
        }
        else {
            const virtualPath = path.resolve(options.repoPath, ...request.relativePath.split('/'));
            const parsed = await parseCodeContent(virtualPath, content, { repoRoot: path.resolve(options.repoPath), resolveImports: false }, {
                maxFileSizeBytes: options.maxBlobBytes,
                maxLines: options.maxBlobLines,
                skipMinified: options.skipMinified,
                byteSize: buffer.length,
            });
            if (!parsed) {
                return {
                    lineCount,
                    provenance,
                    diagnostic: { code: 'unsupported-path', message: 'No structural parser is registered for this path.' },
                };
            }
            if (parsed.kind === 'skipped') {
                return { lineCount, provenance, diagnostic: { code: 'parser-skipped', message: parsed.reason } };
            }
            if (parsed.file.provenance)
                provenance = parsedProvenance(provenance, parsed.file.provenance);
            ({ declarations, facts } = projectParsedFile(parsed.file));
        }
        return {
            ...limitProjection(declarations, facts, options.maxSpans),
            provenance,
            diagnostics: parserGapDiagnostics(provenance, declarations, facts, gapReason),
            lineCount,
        };
    }
    catch (error) {
        return {
            lineCount,
            provenance,
            diagnostic: { code: 'parser-failed', message: safeMessage(error, options.repoPath) },
        };
    }
}
function headerFor(requests, uniqueObjectCount, options, objectFormat, producer) {
    return {
        schemaVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
        type: 'header',
        requestCount: requests.length,
        uniqueObjectCount,
        limits: {
            maxRequests: options.maxRequests,
            maxBlobBytes: options.maxBlobBytes,
            maxBlobLines: options.maxBlobLines,
            maxTotalBlobBytes: options.maxTotalBlobBytes,
            maxBatchBytes: options.maxBatchBytes,
            maxSpans: options.maxSpans,
            formats: options.formats,
            skipMinified: options.skipMinified,
        },
        provenance: {
            sourceSystem: 'git',
            acquisition: 'git-cat-file',
            immutableObjects: true,
            objectFormat,
            instrumentName: 'occ.git-blob-structure',
            instrumentVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
            parserName: 'occ.code.parsers',
            parserVersion: CODE_PARSER_VERSION,
            ...producer,
            importResolution: false,
            bodiesIncluded: false,
            hashesIncluded: false,
            lineageIncluded: false,
            spanEncoding: 'utf8-bytes',
            spanEnd: 'exclusive',
            parserEvidence: 'per-result',
        },
    };
}
function footerFor(requestCount, uniqueObjectCount, contents, resultCounts) {
    return {
        schemaVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
        type: 'footer',
        coverage: {
            requests: requestCount,
            results: resultCounts.results,
            uniqueObjectsRequested: uniqueObjectCount,
            uniqueObjectsRead: contents.size,
            blobBytesRead: [...contents.values()].reduce((total, content) => total + content.length, 0),
            completeResults: resultCounts.completeResults,
            partialResults: resultCounts.partialResults,
            unavailableResults: resultCounts.unavailableResults,
        },
    };
}
function preflightDiagnostic(header, options, omittedForTotal, oidFormatMismatch) {
    if (oidFormatMismatch) {
        return {
            code: 'oid-format-mismatch',
            message: 'Object id length does not match the repository object format.',
        };
    }
    if (!header || header.missing)
        return { code: 'missing-object', message: 'Git object does not exist.' };
    if (header.type !== 'blob') {
        return { code: 'not-a-blob', message: `Git object has type ${header.type}; expected blob.` };
    }
    if (header.size > options.maxBlobBytes) {
        return {
            code: 'blob-too-large',
            message: `Git blob has ${header.size} bytes; the per-blob limit is ${options.maxBlobBytes}.`,
            limit: options.maxBlobBytes,
            actual: header.size,
        };
    }
    if (omittedForTotal.has(header.oid)) {
        return {
            code: 'total-blob-budget-exhausted',
            message: `Reading this blob would exceed the ${options.maxTotalBlobBytes}-byte total budget.`,
            limit: options.maxTotalBlobBytes,
            actual: header.size,
        };
    }
    return undefined;
}
async function readObjectFormat(options) {
    try {
        const result = await runGit(['rev-parse', '--show-object-format'], {
            repoRoot: options.repoPath,
            signal: options.signal,
        });
        const format = result.stdout.trim();
        if (format === 'sha1' || format === 'sha256')
            return format;
        throw new Error(`Git reported unsupported object format "${format}".`);
    }
    catch (firstError) {
        // `--show-object-format` was added after Git learned SHA-256 repositories.
        // On older Git, an absent extension is therefore unambiguously SHA-1.
        const fallback = await runGit(['config', '--get', 'extensions.objectFormat'], {
            repoRoot: options.repoPath,
            signal: options.signal,
            allowExitCodes: [1],
        }).catch(() => undefined);
        if (fallback?.exitCode === 1 || fallback?.stdout.trim() === '')
            return 'sha1';
        if (fallback?.stdout.trim() === 'sha256')
            return 'sha256';
        throw firstError;
    }
}
function oidMatchesObjectFormat(oid, objectFormat) {
    return objectFormat === 'unknown' || oid.length === (objectFormat === 'sha1' ? 40 : 64);
}
/**
 * Inspect immutable Git blobs without consulting their checkout contents.
 *
 * Reads are deduplicated by object id. Parsing is keyed by `(oid, relativePath)`
 * because identical bytes can denote different languages at different paths.
 * A result is still emitted for every request occurrence, in input order.
 */
export async function* inspectGitBlobStructures(requestsInput, rawOptions) {
    const options = parseOptions(rawOptions);
    const requests = await collectRequests(requestsInput, options);
    const uniqueOids = [...new Set(requests.map(request => request.oid))];
    const producer = {
        occVersion: await readOccVersion(),
        nodeVersion: process.versions.node,
    };
    let headers = new Map();
    let headerFailure;
    let objectFormat = 'unknown';
    try {
        objectFormat = await readObjectFormat(options);
    }
    catch (error) {
        headerFailure = { code: 'git-read-failed', message: safeMessage(error, options.repoPath) };
    }
    yield headerFor(requests, uniqueOids.length, options, objectFormat, producer);
    const formatCompatibleOids = uniqueOids.filter(oid => oidMatchesObjectFormat(oid, objectFormat));
    if (!headerFailure) {
        try {
            headers = await readGitObjectHeaders(formatCompatibleOids, {
                repoRoot: options.repoPath,
                signal: options.signal,
                physicalObjects: true,
            });
        }
        catch (error) {
            headerFailure = { code: 'git-read-failed', message: safeMessage(error, options.repoPath) };
        }
    }
    const omittedForTotal = new Set();
    const readableOids = [];
    let selectedBytes = 0;
    if (!headerFailure) {
        for (const oid of formatCompatibleOids) {
            abortIfNeeded(options.signal);
            const header = headers.get(oid);
            if (!header || header.missing || header.type !== 'blob' || header.size > options.maxBlobBytes)
                continue;
            if (selectedBytes + header.size > options.maxTotalBlobBytes) {
                omittedForTotal.add(oid);
                continue;
            }
            selectedBytes += header.size;
            readableOids.push(oid);
        }
    }
    let contents = new Map();
    let contentFailure;
    if (readableOids.length > 0) {
        try {
            contents = await readGitObjects(readableOids, headers, {
                repoRoot: options.repoPath,
                signal: options.signal,
                maxBatchBytes: options.maxBatchBytes,
                physicalObjects: true,
            });
        }
        catch (error) {
            contentFailure = { code: 'git-read-failed', message: safeMessage(error, options.repoPath) };
        }
    }
    const parseCache = new Map();
    const maxParseCacheEntries = 64;
    const resultCounts = {
        results: 0,
        completeResults: 0,
        partialResults: 0,
        unavailableResults: 0,
    };
    for (const [requestIndex, request] of requests.entries()) {
        abortIfNeeded(options.signal);
        const header = headers.get(request.oid);
        const expectedBytes = header && !header.missing ? header.size : undefined;
        let result;
        if (headerFailure) {
            result = unavailableResult(request, requestIndex, emptyCoverage('error'), headerFailure, objectFormat, producer);
        }
        else {
            const oidFormatMismatch = !oidMatchesObjectFormat(request.oid, objectFormat);
            const preflight = preflightDiagnostic(header, options, omittedForTotal, oidFormatMismatch);
            if (preflight) {
                const blobStatus = preflight.code === 'missing-object' ? 'missing' : preflight.code === 'not-a-blob' ? 'not-a-blob' : 'omitted';
                result = unavailableResult(request, requestIndex, emptyCoverage(blobStatus, expectedBytes), preflight, objectFormat, producer);
            }
            else if (contentFailure) {
                result = unavailableResult(request, requestIndex, emptyCoverage('error', expectedBytes), contentFailure, objectFormat, producer);
            }
            else {
                const content = contents.get(request.oid);
                if (!content) {
                    result = unavailableResult(request, requestIndex, emptyCoverage('error', expectedBytes), { code: 'content-unavailable', message: 'Git did not return the requested blob content.' }, objectFormat, producer);
                }
                else if (looksBinary(content)) {
                    result = unavailableResult(request, requestIndex, emptyCoverage('acquired', expectedBytes, content.length), { code: 'binary-blob', message: 'Git blob contains NUL bytes and is treated as binary.' }, objectFormat, producer);
                }
                else {
                    const parseKey = `${request.oid}\0${request.relativePath}`;
                    let parsedPromise = parseCache.get(parseKey);
                    if (!parsedPromise) {
                        parsedPromise = parseStructure(request, content, options, objectFormat, producer).catch((error) => ({
                            diagnostic: { code: 'parser-failed', message: safeMessage(error, options.repoPath) },
                        }));
                        parseCache.set(parseKey, parsedPromise);
                        if (parseCache.size > maxParseCacheEntries) {
                            const oldestKey = parseCache.keys().next().value;
                            if (oldestKey !== undefined)
                                parseCache.delete(oldestKey);
                        }
                    }
                    else {
                        // Refresh insertion order so frequently repeated `(oid, path)`
                        // projections remain cached without making the cache unbounded.
                        parseCache.delete(parseKey);
                        parseCache.set(parseKey, parsedPromise);
                    }
                    const parsed = await parsedPromise;
                    if ('diagnostic' in parsed) {
                        const coverage = emptyCoverage('acquired', expectedBytes, content.length);
                        if (parsed.lineCount !== undefined)
                            coverage.lineCount = parsed.lineCount;
                        result = unavailableResult(request, requestIndex, coverage, parsed.diagnostic, objectFormat, producer, parsed.provenance);
                    }
                    else {
                        const diagnostics = [
                            ...parsed.diagnostics,
                            ...(parsed.truncated
                                ? [
                                    {
                                        code: 'span-limit-exhausted',
                                        message: `Returned ${options.maxSpans} of ${parsed.spansDiscovered} structural spans.`,
                                        limit: options.maxSpans,
                                        actual: parsed.spansDiscovered,
                                    },
                                ]
                                : []),
                        ];
                        const partial = parsed.truncated || diagnostics.length > 0;
                        result = {
                            schemaVersion: GIT_BLOB_STRUCTURE_SCHEMA_VERSION,
                            type: 'result',
                            requestIndex,
                            ...request,
                            status: partial ? 'partial' : 'complete',
                            declarations: parsed.declarations,
                            facts: parsed.facts,
                            provenance: parsed.provenance,
                            coverage: {
                                blob: 'acquired',
                                structure: partial ? 'partial' : 'complete',
                                expectedBytes,
                                acquiredBytes: content.length,
                                lineCount: parsed.lineCount,
                                spansDiscovered: parsed.spansDiscovered,
                                spansEmitted: parsed.declarations.length + parsed.facts.length,
                                declarationsDiscovered: parsed.declarationsDiscovered,
                                declarationsEmitted: parsed.declarations.length,
                                factsDiscovered: parsed.factsDiscovered,
                                factsEmitted: parsed.facts.length,
                            },
                            diagnostics,
                        };
                    }
                }
            }
        }
        resultCounts.results += 1;
        if (result.status === 'complete')
            resultCounts.completeResults += 1;
        else if (result.status === 'partial')
            resultCounts.partialResults += 1;
        else
            resultCounts.unavailableResults += 1;
        yield result;
    }
    yield footerFor(requests.length, uniqueOids.length, contents, resultCounts);
}
//# sourceMappingURL=blob-structure.js.map