import path from 'node:path';
import { z } from 'zod';
import { abortIfNeeded, compareCodeUnits } from '../utils.js';
import { inspectGitBlobStructures, isRepresentableBlobPath, } from './blob-structure.js';
import { AtomCoordinateV1Schema, AtomCoverageSchema, AtomEntityBoundsV1Schema, observeAtomsForEntityGraph, } from './atoms.js';
import { listTreeBlobs } from './entity/git.js';
import { DEFAULT_ENTITY_BOUNDS } from './entity/types.js';
import { CODE_EXTENSIONS } from './languages.js';
import { moduleForRelativePath } from './query.js';
import { seamKeyFor } from './seams.js';
const GRAPH_SCHEMA_VERSION_V1 = 1;
const GRAPH_SCHEMA_VERSION = 2;
const FULL_OID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
const DEFAULT_MAX_ENTITIES_PER_TRANSITION = 500;
const DEFAULT_MAX_STRUCTURE_FILES = 10_000;
export const EntityGraphDiagnosticV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('diagnostic'),
    severity: z.enum(['warning', 'error']),
    code: z.string().min(1),
    message: z.string(),
    parent_ref: z.string().regex(FULL_OID).nullable().optional(),
    child_ref: z.string().regex(FULL_OID).optional(),
    path: z.string().optional(),
})
    .strict();
const EntityGraphBoundsV1Schema = z
    .object({
    max_commits: z.number().int().positive(),
    max_nodes: z.number().int().positive(),
    max_edges: z.number().int().positive(),
    max_seams: z.number().int().positive(),
    max_entities_per_transition: z.number().int().positive(),
    max_structure_files: z.number().int().positive(),
})
    .strict();
export const EntityGraphHeaderV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('header'),
    requested_ref: z.string(),
    resolved_ref: z.string().regex(FULL_OID),
    object_format: z.enum(['sha1', 'sha256']),
    shallow: z.boolean(),
    lineage_run_id: z.string().min(1),
    formats: z.boolean(),
    entity_bounds: AtomEntityBoundsV1Schema,
    bounds: EntityGraphBoundsV1Schema,
    provenance: z
        .object({
        instrument: z.literal('occ.code.entity-graph'),
        instrument_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
        occ_version: z.string().min(1),
        node_version: z.string().min(1),
    })
        .strict(),
    policy: z
        .object({
        topology: z.literal('all-parent-edges'),
        node_basis: z.literal('all-observed-lineages'),
        edge_basis: z.literal('unified-undirected-entity-pairs'),
        cochange_basis: z.literal('logic-entity-presence-per-parent-edge'),
        cochange_denominator: z.literal('parent-child-edges'),
        structure_basis: z.literal('pinned-git-blob-structure'),
        structure_kinds: z.tuple([z.literal('imports'), z.literal('calls'), z.literal('inherits')]),
        seam_basis: z.literal('pinned-import-call-module-pairs'),
        historical_structure: z.literal('unavailable'),
        source_bodies: z.literal('omitted'),
        native_identity: z.literal('git-oid-path-span-digest'),
        lineage_identity: z.literal('derived-run-scoped-correlation'),
        checkout_node_ids: z.literal('omitted'),
    })
        .strict(),
})
    .strict();
const EntityGraphDeclarationV1Schema = z
    .object({
    kind: z.string(),
    name: z.string(),
    qualified_name: z.string(),
    is_exported: z.boolean(),
})
    .strict();
const EntityGraphStructureStateV1Schema = z
    .object({
    coverage: AtomCoverageSchema,
    basis: z.enum(['pinned-git-blob-structure', 'historical-unavailable', 'coordinate-unavailable']),
    reason: z.string().optional(),
})
    .strict();
export const EntityGraphNodeV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('node'),
    lineage_id: z.string().min(1),
    entity_lineage_id: z
        .string()
        .regex(/^ent[0-9]+:[0-9a-f]{32}$/)
        .nullable(),
    state: z.enum(['live', 'historical', 'unknown']),
    coordinate: AtomCoordinateV1Schema.nullable(),
    module: z.string().nullable(),
    declaration: EntityGraphDeclarationV1Schema.nullable(),
    observations: z
        .object({
        transitions: z.number().int().nonnegative(),
        logic_transitions: z.number().int().nonnegative(),
        coordinates: z.number().int().nonnegative(),
        first_ref: z.string().regex(FULL_OID),
        last_ref: z.string().regex(FULL_OID),
    })
        .strict(),
    structure: EntityGraphStructureStateV1Schema,
})
    .strict();
const DirectedIncidenceV1Schema = z
    .object({
    calls: z.number().int().nonnegative(),
    inherits: z.number().int().nonnegative(),
})
    .strict();
export const EntityGraphEdgeV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('edge'),
    source_lineage_id: z.string().min(1),
    target_lineage_id: z.string().min(1),
    cochange: z
        .object({
        count: z.number().int().nonnegative(),
        source_change_count: z.number().int().nonnegative(),
        target_change_count: z.number().int().nonnegative(),
        lift: z.number().nonnegative().nullable(),
        denominator: z.number().int().nonnegative(),
        last_ref: z.string().regex(FULL_OID).nullable(),
        coverage: AtomCoverageSchema,
    })
        .strict(),
    incidence: z
        .object({
        source_to_target: DirectedIncidenceV1Schema,
        target_to_source: DirectedIncidenceV1Schema,
        total: z.number().int().nonnegative(),
        coverage: AtomCoverageSchema,
    })
        .strict(),
    seam_membership: z
        .object({
        member: z.boolean(),
        seam_keys: z.array(z.string()),
        coverage: AtomCoverageSchema,
    })
        .strict(),
})
    .strict();
export const EntityGraphSeamV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('seam'),
    seam_key: z.string().regex(/^seam:/),
    module_a: z.string(),
    module_b: z.string(),
    import_count: z.number().int().nonnegative(),
    call_count: z.number().int().nonnegative(),
    coverage: AtomCoverageSchema,
})
    .strict();
export const EntityGraphFooterV1Schema = z
    .object({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION_V1),
    type: z.literal('footer'),
    complete: z.boolean(),
    coverage: z
        .object({
        history: AtomCoverageSchema,
        lineages: AtomCoverageSchema,
        head_mapping: AtomCoverageSchema,
        cochange: AtomCoverageSchema,
        structure: AtomCoverageSchema,
        incidence: AtomCoverageSchema,
        seams: AtomCoverageSchema,
    })
        .strict(),
    counts: z
        .object({
        transitions: z.number().int().nonnegative(),
        transitions_skipped_as_broad: z.number().int().nonnegative(),
        nodes: z.number().int().nonnegative(),
        live_nodes: z.number().int().nonnegative(),
        historical_nodes: z.number().int().nonnegative(),
        unknown_nodes: z.number().int().nonnegative(),
        edges: z.number().int().nonnegative(),
        seams: z.number().int().nonnegative(),
        cochange_edges: z.number().int().nonnegative(),
        incidence_edges: z.number().int().nonnegative(),
        structure_files: z.number().int().nonnegative(),
        declarations_resolved: z.number().int().nonnegative(),
        facts_resolved: z.number().int().nonnegative(),
        facts_unresolved: z.number().int().nonnegative(),
        diagnostics: z.number().int().nonnegative(),
    })
        .strict(),
})
    .strict();
export const OccEntityGraphEventV1Schema = z.discriminatedUnion('type', [
    EntityGraphHeaderV1Schema,
    EntityGraphDiagnosticV1Schema,
    EntityGraphNodeV1Schema,
    EntityGraphSeamV1Schema,
    EntityGraphEdgeV1Schema,
    EntityGraphFooterV1Schema,
]);
export const EntityGraphDiagnosticV2Schema = EntityGraphDiagnosticV1Schema.omit({ schema_version: true })
    .extend({ schema_version: z.literal(GRAPH_SCHEMA_VERSION) })
    .strict();
export const EntityGraphHeaderV2Schema = EntityGraphHeaderV1Schema.omit({
    schema_version: true,
    provenance: true,
    policy: true,
})
    .extend({
    schema_version: z.literal(GRAPH_SCHEMA_VERSION),
    provenance: EntityGraphHeaderV1Schema.shape.provenance
        .omit({ instrument_version: true })
        .extend({ instrument_version: z.literal(GRAPH_SCHEMA_VERSION) })
        .strict(),
    policy: EntityGraphHeaderV1Schema.shape.policy
        .extend({
        atoms_contract: z.literal('occ.atoms@2'),
        blob_structure_contract: z.literal('occ.git-blob-structure@2'),
    })
        .strict(),
})
    .strict();
export const EntityGraphNodeV2Schema = EntityGraphNodeV1Schema.omit({ schema_version: true })
    .extend({ schema_version: z.literal(GRAPH_SCHEMA_VERSION) })
    .strict();
export const EntityGraphEdgeV2Schema = EntityGraphEdgeV1Schema.omit({ schema_version: true })
    .extend({ schema_version: z.literal(GRAPH_SCHEMA_VERSION) })
    .strict();
export const EntityGraphSeamV2Schema = EntityGraphSeamV1Schema.omit({ schema_version: true })
    .extend({ schema_version: z.literal(GRAPH_SCHEMA_VERSION) })
    .strict();
export const EntityGraphFooterV2Schema = EntityGraphFooterV1Schema.omit({ schema_version: true })
    .extend({ schema_version: z.literal(GRAPH_SCHEMA_VERSION) })
    .strict();
export const OccEntityGraphEventV2Schema = z.discriminatedUnion('type', [
    EntityGraphHeaderV2Schema,
    EntityGraphDiagnosticV2Schema,
    EntityGraphNodeV2Schema,
    EntityGraphSeamV2Schema,
    EntityGraphEdgeV2Schema,
    EntityGraphFooterV2Schema,
]);
function positive(value, label) {
    if (!Number.isSafeInteger(value) || value <= 0)
        throw new Error(`${label} must be a positive safe integer.`);
    return value;
}
function resolveOptions(options) {
    if (!options.ref)
        throw new Error('Entity graph export requires a pinned ref.');
    const bounds = options.bounds ?? DEFAULT_ENTITY_BOUNDS;
    return {
        repoRoot: path.resolve(options.repoPath),
        ref: options.ref,
        bounds,
        formats: options.formats ?? true,
        maxNodes: positive(options.maxNodes ?? bounds.maxLiveEntities, 'maxNodes'),
        maxEdges: positive(options.maxEdges ?? bounds.maxLineageEvents, 'maxEdges'),
        maxSeams: positive(options.maxSeams ?? bounds.maxLineageEvents, 'maxSeams'),
        maxEntitiesPerTransition: positive(options.maxEntitiesPerTransition ?? DEFAULT_MAX_ENTITIES_PER_TRANSITION, 'maxEntitiesPerTransition'),
        maxStructureFiles: positive(options.maxStructureFiles ?? DEFAULT_MAX_STRUCTURE_FILES, 'maxStructureFiles'),
        ...(options.signal ? { signal: options.signal } : {}),
    };
}
function coordinateKey(coordinate) {
    return `${coordinate.oid}\0${coordinate.path}\0${coordinate.span.start_line}\0${coordinate.span.end_line}`;
}
function pairKey(left, right) {
    return left <= right ? `${left}\0${right}` : `${right}\0${left}`;
}
function orderedPair(left, right) {
    return left <= right ? [left, right] : [right, left];
}
function graphDiagnostic(code, message, options = {}) {
    return {
        schema_version: GRAPH_SCHEMA_VERSION,
        type: 'diagnostic',
        severity: options.severity ?? 'warning',
        code,
        message,
        ...(options.parentRef !== undefined ? { parent_ref: options.parentRef } : {}),
        ...(options.childRef ? { child_ref: options.childRef } : {}),
        ...(options.path ? { path: options.path } : {}),
    };
}
function rememberCoordinate(node, coordinate) {
    if (!coordinate)
        return;
    node.coordinates.set(coordinateKey(coordinate), coordinate);
    node.latestCoordinate = coordinate;
}
function structureCoverage(node) {
    if (!node.currentCoordinate)
        return 'unavailable';
    if (!node.declaration || !node.structureResult)
        return 'unavailable';
    return node.structureResult.status;
}
function declarationMatchesName(declaration, name) {
    return (declaration.name === name ||
        declaration.qualifiedName === name ||
        declaration.qualifiedName.endsWith(`.${name}`) ||
        declaration.qualifiedName.endsWith(`::${name}`));
}
function containsSpan(declaration, span) {
    if (declaration.span.exact &&
        span.exact &&
        declaration.span.startByte !== undefined &&
        declaration.span.endByte !== undefined &&
        span.startByte !== undefined &&
        span.endByte !== undefined) {
        return declaration.span.startByte <= span.startByte && declaration.span.endByte >= span.endByte;
    }
    return declaration.span.startLine <= span.startLine && declaration.span.endLine >= span.endLine;
}
function spanSize(declaration) {
    if (declaration.span.startByte !== undefined && declaration.span.endByte !== undefined) {
        return declaration.span.endByte - declaration.span.startByte;
    }
    return declaration.span.endLine - declaration.span.startLine;
}
function uniqueSmallest(candidates) {
    if (candidates.length === 0)
        return undefined;
    const sorted = [...candidates].sort((a, b) => spanSize(a.declaration) - spanSize(b.declaration) ||
        compareCodeUnits(a.declaration.qualifiedName, b.declaration.qualifiedName) ||
        compareCodeUnits(a.lineageId, b.lineageId));
    const first = sorted[0];
    if (!first)
        return undefined;
    const smallest = spanSize(first.declaration);
    const tied = sorted.filter(candidate => spanSize(candidate.declaration) === smallest);
    return tied.length === 1 ? tied[0] : undefined;
}
function pathCandidates(specifier, importer, knownPaths) {
    if (!specifier.startsWith('.'))
        return [];
    const base = path.posix.normalize(path.posix.join(path.posix.dirname(importer), specifier));
    if (base === '..' || base.startsWith('../') || base.startsWith('/'))
        return [];
    const extensions = CODE_EXTENSIONS.map(extension => `.${extension}`);
    const candidates = new Set([base]);
    for (const extension of extensions) {
        candidates.add(`${base}${extension}`);
        candidates.add(`${base}/index${extension}`);
    }
    const existingExtension = path.posix.extname(base);
    if (existingExtension) {
        const withoutExtension = base.slice(0, -existingExtension.length);
        for (const extension of extensions)
            candidates.add(`${withoutExtension}${extension}`);
    }
    return [...candidates].filter(candidate => knownPaths.has(candidate)).sort();
}
function mergeCoverage(left, right) {
    if (left === 'unavailable' || right === 'unavailable')
        return 'unavailable';
    if (left === 'partial' || right === 'partial')
        return 'partial';
    return 'complete';
}
/**
 * Stream a bounded entity graph at one immutable Git pin.
 *
 * Co-change is learned from every parent-child edge. Static incidence is only
 * joined when the body-free pinned declaration and both endpoint lineages are
 * unique; historical endpoints deliberately remain unavailable.
 */
export async function* exportEntityGraph(input) {
    const options = resolveOptions(input);
    const nodes = new Map();
    const edges = new Map();
    const changeCounts = new Map();
    const diagnostics = [];
    let atomHeader;
    let atomFooter;
    let transitions = 0;
    let broadTransitions = 0;
    let nodesTruncated = false;
    let edgesTruncated = false;
    const getNode = (lineageId, ref) => {
        const existing = nodes.get(lineageId);
        if (existing)
            return existing;
        if (nodes.size >= options.maxNodes) {
            nodesTruncated = true;
            return undefined;
        }
        const created = {
            lineageId,
            entityLineageId: null,
            coordinates: new Map(),
            latestCoordinate: null,
            currentCoordinate: null,
            transitions: 0,
            logicTransitions: 0,
            firstRef: ref,
            lastRef: ref,
        };
        nodes.set(lineageId, created);
        return created;
    };
    const getEdge = (left, right) => {
        const key = pairKey(left, right);
        const existing = edges.get(key);
        if (existing)
            return existing;
        if (edges.size >= options.maxEdges) {
            edgesTruncated = true;
            return undefined;
        }
        const [source, target] = orderedPair(left, right);
        const created = {
            source,
            target,
            cochangeCount: 0,
            lastCochangeRef: null,
            sourceToTargetCalls: 0,
            sourceToTargetInherits: 0,
            targetToSourceCalls: 0,
            targetToSourceInherits: 0,
        };
        edges.set(key, created);
        return created;
    };
    for await (const event of observeAtomsForEntityGraph({
        repoPath: options.repoRoot,
        ref: options.ref,
        tokenizer: 'o200k_base',
        bounds: options.bounds,
        formats: options.formats,
        ...(options.signal ? { signal: options.signal } : {}),
    })) {
        abortIfNeeded(options.signal);
        if (event.type === 'header') {
            atomHeader = event;
            continue;
        }
        if (event.type === 'footer') {
            atomFooter = event;
            continue;
        }
        if (event.type === 'diagnostic') {
            diagnostics.push(graphDiagnostic(`atoms.${event.code}`, event.message, {
                severity: event.severity,
                ...(event.parent_ref !== undefined ? { parentRef: event.parent_ref } : {}),
                ...(event.child_ref ? { childRef: event.child_ref } : {}),
                ...(event.path ? { path: event.path } : {}),
            }));
            continue;
        }
        if (event.type === 'lineage-head') {
            const node = getNode(event.lineage_id, atomHeader?.resolved_ref ?? options.ref);
            if (node) {
                node.entityLineageId = event.entity_lineage_id;
                rememberCoordinate(node, event.coordinate);
                node.currentCoordinate = event.coordinate;
            }
            for (const item of event.diagnostics) {
                diagnostics.push(graphDiagnostic(`atoms.${item.code}`, item.message, {
                    severity: item.severity,
                    ...(item.path ? { path: item.path } : {}),
                }));
            }
            continue;
        }
        const episode = event;
        transitions++;
        const observed = new Set();
        const logic = new Set();
        for (const entity of episode.entities) {
            const node = getNode(entity.lineage_id, episode.child_ref);
            if (!node)
                continue;
            rememberCoordinate(node, entity.before);
            rememberCoordinate(node, entity.after);
            observed.add(entity.lineage_id);
            if (entity.semantic_class === 'logic')
                logic.add(entity.lineage_id);
        }
        for (const lineageId of observed) {
            const node = nodes.get(lineageId);
            if (!node)
                continue;
            node.transitions++;
            node.lastRef = episode.child_ref;
        }
        for (const lineageId of logic) {
            const node = nodes.get(lineageId);
            if (!node)
                continue;
            node.logicTransitions++;
            changeCounts.set(lineageId, (changeCounts.get(lineageId) ?? 0) + 1);
        }
        for (const item of episode.diagnostics) {
            diagnostics.push(graphDiagnostic(`atoms.${item.code}`, item.message, {
                severity: item.severity,
                ...(item.parent_ref !== undefined ? { parentRef: item.parent_ref } : {}),
                ...(item.child_ref ? { childRef: item.child_ref } : {}),
                ...(item.path ? { path: item.path } : {}),
            }));
        }
        if (logic.size > options.maxEntitiesPerTransition) {
            broadTransitions++;
            continue;
        }
        const changed = [...logic].sort();
        for (let left = 0; left < changed.length; left++) {
            for (let right = left + 1; right < changed.length; right++) {
                const lineageA = changed[left];
                const lineageB = changed[right];
                if (!lineageA || !lineageB)
                    continue;
                const edge = getEdge(lineageA, lineageB);
                if (!edge)
                    continue;
                edge.cochangeCount++;
                edge.lastCochangeRef = episode.child_ref;
            }
        }
    }
    if (!atomHeader || !atomFooter)
        throw new Error('Atoms did not produce a complete observation envelope.');
    if (nodesTruncated) {
        diagnostics.push(graphDiagnostic('lineage-limit-exhausted', `The graph retained at most ${options.maxNodes} observed lineages.`, {
            severity: 'error',
        }));
    }
    if (broadTransitions > 0) {
        diagnostics.push(graphDiagnostic('broad-transitions-skipped', `${broadTransitions} parent-child edge(s) exceeded ${options.maxEntitiesPerTransition} logic entities; their all-to-all co-change pairs were not materialized.`, { severity: 'error' }));
    }
    const tree = (await listTreeBlobs(atomHeader.resolved_ref, {
        repoRoot: options.repoRoot,
        signal: options.signal,
    })).sort((a, b) => compareCodeUnits(a.path, b.path));
    const treeOidByPath = new Map(tree.map(entry => [entry.path, entry.blobSha]));
    for (const node of nodes.values()) {
        // Only the lifetime-aware lineage-head projection can establish liveness.
        // A historical coordinate may bind to the pinned tree again after an
        // identical delete/re-add, but that does not revive the old lineage.
        if (node.currentCoordinate && treeOidByPath.get(node.currentCoordinate.path) !== node.currentCoordinate.oid) {
            node.currentCoordinate = null;
        }
    }
    const livePaths = new Set([...nodes.values()]
        .map(node => node.currentCoordinate?.path)
        .filter((value) => value !== undefined));
    const selectedTree = [...tree]
        .sort((a, b) => Number(livePaths.has(b.path)) - Number(livePaths.has(a.path)) || compareCodeUnits(a.path, b.path))
        .slice(0, options.maxStructureFiles);
    const structureFilesTruncated = selectedTree.length < tree.length;
    if (structureFilesTruncated) {
        diagnostics.push(graphDiagnostic('structure-file-limit-exhausted', `The pinned structure projection retained ${selectedTree.length} of ${tree.length} Git tree entries.`, { severity: 'error' }));
    }
    const structureByPath = new Map();
    // A committed path the request schema cannot represent (backslash, NUL,
    // over-long) must degrade coverage for that file, not abort the stream.
    let structurePathsRejected = 0;
    const representableTree = selectedTree.filter(entry => {
        if (isRepresentableBlobPath(entry.path))
            return true;
        structurePathsRejected++;
        diagnostics.push(graphDiagnostic('structure-path-unrepresentable', `Pinned tree path ${JSON.stringify(entry.path)} cannot be projected through blob structure.`, { severity: 'warning' }));
        return false;
    });
    const structureRequests = representableTree.map((entry, index) => ({
        requestId: `graph:${index}`,
        oid: entry.blobSha,
        relativePath: entry.path,
    }));
    for await (const event of inspectGitBlobStructures(structureRequests, {
        repoPath: options.repoRoot,
        maxRequests: Math.max(1, structureRequests.length),
        maxBlobBytes: options.bounds.maxBlobBytes,
        maxBlobLines: options.bounds.maxBlobLines,
        maxTotalBlobBytes: options.bounds.maxTotalBlobBytes,
        maxBatchBytes: Math.max(options.bounds.maxBatchBytes, options.bounds.maxBlobBytes),
        maxSpans: options.bounds.maxEntitiesPerFile,
        formats: options.formats,
        skipMinified: true,
        ...(options.signal ? { signal: options.signal } : {}),
    })) {
        if (event.type === 'result')
            structureByPath.set(event.relativePath, event);
    }
    // The receipt must name why structure is incomplete, not merely say that it
    // is: aggregate incomplete files by their leading cause so the stream stays
    // bounded on trees full of unparseable paths.
    const incompleteStructure = new Map();
    for (const [relativePath, result] of structureByPath) {
        if (result.status === 'complete')
            continue;
        const cause = result.diagnostics[0]?.code ?? result.status;
        let bucket = incompleteStructure.get(cause);
        if (!bucket) {
            bucket = { count: 0, examples: [] };
            incompleteStructure.set(cause, bucket);
        }
        bucket.count++;
        if (bucket.examples.length < 3)
            bucket.examples.push(relativePath);
    }
    for (const [cause, bucket] of incompleteStructure) {
        const suffix = bucket.count > bucket.examples.length ? ', …' : '';
        diagnostics.push(graphDiagnostic('structure-files-incomplete', `${bucket.count} pinned file(s) yielded incomplete structure (${cause}): ${bucket.examples.join(', ')}${suffix}.`, { severity: 'warning' }));
    }
    const lineagesByCoordinate = new Map();
    for (const node of nodes.values()) {
        const coordinate = node.currentCoordinate;
        if (!coordinate)
            continue;
        const key = coordinateKey(coordinate);
        const lineages = lineagesByCoordinate.get(key);
        if (lineages)
            lineages.add(node.lineageId);
        else
            lineagesByCoordinate.set(key, new Set([node.lineageId]));
    }
    const declarations = [];
    for (const [relativePath, result] of structureByPath) {
        for (const declaration of result.declarations) {
            const key = `${result.oid}\0${relativePath}\0${declaration.span.startLine}\0${declaration.span.endLine}`;
            const candidates = lineagesByCoordinate.get(key);
            if (!candidates || candidates.size !== 1)
                continue;
            const lineageId = candidates.values().next().value;
            if (!lineageId)
                continue;
            const node = nodes.get(lineageId);
            if (!node?.currentCoordinate || coordinateKey(node.currentCoordinate) !== key)
                continue;
            if (!node.declaration) {
                node.declaration = declaration;
                node.structureResult = result;
            }
            declarations.push({ lineageId, relativePath, declaration });
        }
    }
    const declarationsByPath = new Map();
    for (const declaration of declarations) {
        const byPath = declarationsByPath.get(declaration.relativePath);
        if (byPath)
            byPath.push(declaration);
        else
            declarationsByPath.set(declaration.relativePath, [declaration]);
    }
    const knownPaths = new Set(tree.map(entry => entry.path));
    const localImports = new Map();
    for (const [relativePath, result] of structureByPath) {
        localImports.set(relativePath, result.facts.filter((fact) => fact.factKind === 'import' && fact.importKind === 'local'));
    }
    const seams = new Map();
    const addSeam = (fromPath, toPath, kind) => {
        const fromModule = moduleForRelativePath(fromPath);
        const toModule = moduleForRelativePath(toPath);
        if (fromModule === toModule)
            return;
        const key = seamKeyFor(fromModule, toModule);
        let seam = seams.get(key);
        if (!seam) {
            const [moduleA, moduleB] = [fromModule, toModule].sort();
            seam = { moduleA, moduleB, importCount: 0, callCount: 0 };
            seams.set(key, seam);
        }
        if (kind === 'imports')
            seam.importCount++;
        else
            seam.callCount++;
    };
    let factsResolved = 0;
    let factsUnresolved = 0;
    for (const [relativePath, imports] of localImports) {
        for (const fact of imports) {
            const targets = pathCandidates(fact.specifier, relativePath, knownPaths);
            if (targets.length === 1 && targets[0]) {
                addSeam(relativePath, targets[0], 'imports');
                factsResolved++;
            }
            else {
                factsUnresolved++;
            }
        }
    }
    const resolveCaller = (relativePath, name, span, container) => {
        const candidates = (declarationsByPath.get(relativePath) ?? []).filter(candidate => declarationMatchesName(candidate.declaration, name) &&
            containsSpan(candidate.declaration, span) &&
            (!container ||
                candidate.declaration.containerName === container ||
                candidate.declaration.qualifiedName.includes(container)));
        return uniqueSmallest(candidates);
    };
    const namedCandidates = (name, relativePath) => {
        const source = declarationsByPath.get(relativePath) ?? [];
        return source.filter(candidate => declarationMatchesName(candidate.declaration, name));
    };
    // Resolution requires positional evidence: a same-file declaration or an
    // import binding naming the callee. A repo-wide unique-name match is not
    // evidence — it ignores the receiver, so `console.log` would fabricate an
    // edge to any lone `log` declaration. Unresolvable facts stay unresolved.
    const resolveTarget = (relativePath, localName, qualifier) => {
        const qualifierMatches = (declaration) => qualifier === undefined ||
            declaration.qualifiedName === `${qualifier}.${localName}` ||
            declaration.qualifiedName.endsWith(`.${qualifier}.${localName}`) ||
            declaration.qualifiedName === `${qualifier}::${localName}` ||
            declaration.qualifiedName.endsWith(`::${qualifier}::${localName}`);
        const sameFile = namedCandidates(localName, relativePath).filter(candidate => qualifierMatches(candidate.declaration));
        if (sameFile.length === 1)
            return sameFile[0];
        for (const fact of localImports.get(relativePath) ?? []) {
            const binding = fact.bindings.find(item => qualifier ? item.localName === qualifier && item.isNamespace : item.localName === localName);
            if (!binding)
                continue;
            const targets = pathCandidates(fact.specifier, relativePath, knownPaths);
            if (targets.length !== 1)
                continue;
            const targetName = qualifier ? localName : (binding.importedName ?? localName);
            const imported = namedCandidates(targetName, targets[0]);
            if (imported.length === 1)
                return imported[0];
        }
        return undefined;
    };
    const addIncidence = (from, to, kind) => {
        const edge = getEdge(from, to);
        if (!edge)
            return false;
        const forward = from === edge.source;
        if (kind === 'calls') {
            if (forward)
                edge.sourceToTargetCalls++;
            else
                edge.targetToSourceCalls++;
        }
        else if (forward)
            edge.sourceToTargetInherits++;
        else
            edge.targetToSourceInherits++;
        return true;
    };
    for (const [relativePath, result] of structureByPath) {
        for (const fact of result.facts) {
            if (fact.factKind === 'import')
                continue;
            // A fact whose endpoints resolved is resolved even when the edge budget
            // has no slot left for its incidence; `edgesTruncated` already reports
            // that loss, and the seam and resolution counters must agree.
            if (fact.factKind === 'call') {
                const caller = resolveCaller(relativePath, fact.callerName, fact.span, fact.callerContainerName);
                const target = resolveTarget(relativePath, fact.calleeName, fact.qualifier);
                if (caller && target) {
                    addSeam(caller.relativePath, target.relativePath, 'calls');
                    addIncidence(caller.lineageId, target.lineageId, 'calls');
                    factsResolved++;
                }
                else
                    factsUnresolved++;
                continue;
            }
            const child = resolveCaller(relativePath, fact.className, fact.span);
            const parent = resolveTarget(relativePath, fact.baseName);
            if (child && parent) {
                addIncidence(child.lineageId, parent.lineageId, 'inherits');
                factsResolved++;
            }
            else
                factsUnresolved++;
        }
    }
    if (factsUnresolved > 0) {
        diagnostics.push(graphDiagnostic('static-facts-unresolved', `${factsUnresolved} pinned import/call/inheritance fact(s) could not be joined to unique live lineage endpoints.`, { severity: 'warning' }));
    }
    if (edgesTruncated) {
        diagnostics.push(graphDiagnostic('edge-limit-exhausted', `The graph retained at most ${options.maxEdges} unified edges.`, {
            severity: 'error',
        }));
    }
    const orderedSeams = [...seams.entries()].sort(([a], [b]) => compareCodeUnits(a, b));
    const retainedSeams = orderedSeams.slice(0, options.maxSeams);
    const retainedSeamKeys = new Set(retainedSeams.map(([key]) => key));
    const seamsTruncated = retainedSeams.length < orderedSeams.length;
    if (seamsTruncated) {
        diagnostics.push(graphDiagnostic('seam-limit-exhausted', `The graph retained ${retainedSeams.length} of ${orderedSeams.length} pinned module seams.`, { severity: 'error' }));
    }
    const historyCoverage = atomFooter.coverage.history;
    const atomLineageCoverage = mergeCoverage(atomFooter.coverage.entities, atomFooter.coverage.lineages);
    const lineageCoverage = nodesTruncated ? 'partial' : atomLineageCoverage;
    const headMappingCoverage = atomFooter.coverage.head_mapping;
    const cochangeCoverage = broadTransitions > 0 || edgesTruncated || lineageCoverage !== 'complete' ? 'partial' : 'complete';
    const liveNodes = [...nodes.values()].filter(node => node.currentCoordinate);
    const structureCoverage = structureFilesTruncated ||
        structurePathsRejected > 0 ||
        [...structureByPath.values()].some(result => result.status !== 'complete') ||
        liveNodes.some(node => structureCoverageForNode(node) !== 'complete')
        ? 'partial'
        : 'complete';
    const incidenceCoverage = edgesTruncated || factsUnresolved > 0 || structureCoverage !== 'complete' ? 'partial' : 'complete';
    const seamsCoverage = seamsTruncated || factsUnresolved > 0 || structureCoverage !== 'complete' ? 'partial' : 'complete';
    const header = {
        schema_version: GRAPH_SCHEMA_VERSION,
        type: 'header',
        requested_ref: atomHeader.requested_ref,
        resolved_ref: atomHeader.resolved_ref,
        object_format: atomHeader.object_format,
        shallow: atomHeader.shallow,
        lineage_run_id: atomHeader.lineage_run_id,
        formats: atomHeader.formats,
        entity_bounds: atomHeader.bounds,
        bounds: {
            max_commits: options.bounds.maxCommits,
            max_nodes: options.maxNodes,
            max_edges: options.maxEdges,
            max_seams: options.maxSeams,
            max_entities_per_transition: options.maxEntitiesPerTransition,
            max_structure_files: options.maxStructureFiles,
        },
        provenance: {
            instrument: 'occ.code.entity-graph',
            instrument_version: GRAPH_SCHEMA_VERSION,
            occ_version: atomHeader.provenance.occ_version,
            node_version: process.versions.node,
        },
        policy: {
            topology: 'all-parent-edges',
            node_basis: 'all-observed-lineages',
            edge_basis: 'unified-undirected-entity-pairs',
            cochange_basis: 'logic-entity-presence-per-parent-edge',
            cochange_denominator: 'parent-child-edges',
            structure_basis: 'pinned-git-blob-structure',
            structure_kinds: ['imports', 'calls', 'inherits'],
            seam_basis: 'pinned-import-call-module-pairs',
            historical_structure: 'unavailable',
            source_bodies: 'omitted',
            native_identity: 'git-oid-path-span-digest',
            lineage_identity: 'derived-run-scoped-correlation',
            checkout_node_ids: 'omitted',
            atoms_contract: 'occ.atoms@2',
            blob_structure_contract: 'occ.git-blob-structure@2',
        },
    };
    yield header;
    for (const item of diagnostics.sort((a, b) => compareCodeUnits(a.code, b.code) || compareCodeUnits(a.message, b.message))) {
        yield item;
    }
    let historicalNodeCount = 0;
    let unknownNodeCount = 0;
    for (const node of [...nodes.values()].sort((a, b) => compareCodeUnits(a.lineageId, b.lineageId))) {
        const coordinate = node.currentCoordinate ?? node.latestCoordinate;
        const state = node.currentCoordinate ? 'live' : coordinate ? 'historical' : 'unknown';
        if (state === 'historical')
            historicalNodeCount++;
        if (state === 'unknown')
            unknownNodeCount++;
        const coverage = structureCoverageForNode(node);
        const structure = node.currentCoordinate
            ? {
                coverage,
                basis: 'pinned-git-blob-structure',
                ...(coverage !== 'complete'
                    ? {
                        reason: node.declaration && node.structureResult
                            ? 'structure-file-incomplete-at-pin'
                            : 'declaration-not-uniquely-resolved-at-pin',
                    }
                    : {}),
            }
            : coordinate
                ? {
                    coverage: 'unavailable',
                    basis: 'historical-unavailable',
                    reason: 'historical endpoints are not projected through the current pinned structure graph',
                }
                : {
                    coverage: 'unavailable',
                    basis: 'coordinate-unavailable',
                    reason: 'no Git-native coordinate was available for this observed lineage',
                };
        const event = {
            schema_version: GRAPH_SCHEMA_VERSION,
            type: 'node',
            lineage_id: node.lineageId,
            entity_lineage_id: node.entityLineageId,
            state,
            coordinate,
            module: coordinate ? moduleForRelativePath(coordinate.path) : null,
            declaration: node.declaration
                ? {
                    kind: node.declaration.kind,
                    name: node.declaration.name,
                    qualified_name: node.declaration.qualifiedName,
                    is_exported: node.declaration.isExported,
                }
                : null,
            observations: {
                transitions: node.transitions,
                logic_transitions: node.logicTransitions,
                coordinates: node.coordinates.size,
                first_ref: node.firstRef,
                last_ref: node.lastRef,
            },
            structure,
        };
        yield event;
    }
    for (const [seamKey, seam] of retainedSeams) {
        const event = {
            schema_version: GRAPH_SCHEMA_VERSION,
            type: 'seam',
            seam_key: seamKey,
            module_a: seam.moduleA,
            module_b: seam.moduleB,
            import_count: seam.importCount,
            call_count: seam.callCount,
            coverage: seamsCoverage,
        };
        yield event;
    }
    let cochangeEdgeCount = 0;
    let incidenceEdgeCount = 0;
    for (const edge of [...edges.values()].sort((a, b) => compareCodeUnits(a.source, b.source) || compareCodeUnits(a.target, b.target))) {
        const sourceNode = nodes.get(edge.source);
        const targetNode = nodes.get(edge.target);
        if (!sourceNode || !targetNode)
            continue;
        const sourceCount = changeCounts.get(edge.source) ?? 0;
        const targetCount = changeCounts.get(edge.target) ?? 0;
        const lift = edge.cochangeCount > 0 && sourceCount > 0 && targetCount > 0 && transitions > 0
            ? (edge.cochangeCount * transitions) / (sourceCount * targetCount)
            : null;
        const incidenceTotal = edge.sourceToTargetCalls + edge.sourceToTargetInherits + edge.targetToSourceCalls + edge.targetToSourceInherits;
        if (edge.cochangeCount > 0)
            cochangeEdgeCount++;
        if (incidenceTotal > 0)
            incidenceEdgeCount++;
        const endpointCoverage = mergeCoverage(structureCoverageForNode(sourceNode), structureCoverageForNode(targetNode));
        const incidenceEdgeCoverage = !sourceNode.currentCoordinate || !targetNode.currentCoordinate
            ? 'unavailable'
            : mergeCoverage(endpointCoverage, incidenceCoverage);
        const sourceModule = sourceNode.currentCoordinate
            ? moduleForRelativePath(sourceNode.currentCoordinate.path)
            : undefined;
        const targetModule = targetNode.currentCoordinate
            ? moduleForRelativePath(targetNode.currentCoordinate.path)
            : undefined;
        const endpointSeamKey = sourceModule !== undefined && targetModule !== undefined && sourceModule !== targetModule
            ? seamKeyFor(sourceModule, targetModule)
            : undefined;
        const seamMember = endpointSeamKey !== undefined && retainedSeamKeys.has(endpointSeamKey);
        const event = {
            schema_version: GRAPH_SCHEMA_VERSION,
            type: 'edge',
            source_lineage_id: edge.source,
            target_lineage_id: edge.target,
            cochange: {
                count: edge.cochangeCount,
                source_change_count: sourceCount,
                target_change_count: targetCount,
                lift,
                denominator: transitions,
                last_ref: edge.lastCochangeRef,
                coverage: cochangeCoverage,
            },
            incidence: {
                source_to_target: {
                    calls: edge.sourceToTargetCalls,
                    inherits: edge.sourceToTargetInherits,
                },
                target_to_source: {
                    calls: edge.targetToSourceCalls,
                    inherits: edge.targetToSourceInherits,
                },
                total: incidenceTotal,
                coverage: incidenceEdgeCoverage,
            },
            seam_membership: {
                member: seamMember,
                seam_keys: seamMember && endpointSeamKey ? [endpointSeamKey] : [],
                coverage: !sourceNode.currentCoordinate || !targetNode.currentCoordinate ? 'unavailable' : seamsCoverage,
            },
        };
        yield event;
    }
    const complete = historyCoverage === 'complete' &&
        lineageCoverage === 'complete' &&
        headMappingCoverage === 'complete' &&
        cochangeCoverage === 'complete' &&
        structureCoverage === 'complete' &&
        incidenceCoverage === 'complete' &&
        seamsCoverage === 'complete';
    yield {
        schema_version: GRAPH_SCHEMA_VERSION,
        type: 'footer',
        complete,
        coverage: {
            history: historyCoverage,
            lineages: lineageCoverage,
            head_mapping: headMappingCoverage,
            cochange: cochangeCoverage,
            structure: structureCoverage,
            incidence: incidenceCoverage,
            seams: seamsCoverage,
        },
        counts: {
            transitions,
            transitions_skipped_as_broad: broadTransitions,
            nodes: nodes.size,
            live_nodes: liveNodes.length,
            historical_nodes: historicalNodeCount,
            unknown_nodes: unknownNodeCount,
            edges: edges.size,
            seams: retainedSeams.length,
            cochange_edges: cochangeEdgeCount,
            incidence_edges: incidenceEdgeCount,
            structure_files: structureByPath.size,
            declarations_resolved: declarations.length,
            facts_resolved: factsResolved,
            facts_unresolved: factsUnresolved,
            diagnostics: diagnostics.length,
        },
    };
}
function structureCoverageForNode(node) {
    return structureCoverage(node);
}
export const OccEntityGraphRunV1Schema = z
    .object({
    header: EntityGraphHeaderV1Schema,
    diagnostics: z.array(EntityGraphDiagnosticV1Schema),
    nodes: z.array(EntityGraphNodeV1Schema),
    seams: z.array(EntityGraphSeamV1Schema),
    edges: z.array(EntityGraphEdgeV1Schema),
    footer: EntityGraphFooterV1Schema,
})
    .strict()
    .superRefine((run, context) => {
    const nodeIds = new Set();
    for (const [index, node] of run.nodes.entries()) {
        if (nodeIds.has(node.lineage_id)) {
            context.addIssue({ code: 'custom', path: ['nodes', index], message: 'Graph node ids must be unique.' });
        }
        nodeIds.add(node.lineage_id);
        if (index > 0 && compareCodeUnits(run.nodes[index - 1]?.lineage_id ?? '', node.lineage_id) >= 0) {
            context.addIssue({ code: 'custom', path: ['nodes', index], message: 'Graph nodes must be strictly ordered.' });
        }
        if ((node.state === 'unknown') !== (node.coordinate === null)) {
            context.addIssue({
                code: 'custom',
                path: ['nodes', index, 'coordinate'],
                message: 'Only unknown graph nodes may omit a coordinate.',
            });
        }
        if (node.entity_lineage_id !== null && node.state !== 'live') {
            context.addIssue({
                code: 'custom',
                path: ['nodes', index, 'entity_lineage_id'],
                message: 'Only live graph nodes may carry a pinned entity-lineage mapping.',
            });
        }
    }
    const seamKeys = new Set();
    for (const [index, seam] of run.seams.entries()) {
        if (seamKeys.has(seam.seam_key)) {
            context.addIssue({ code: 'custom', path: ['seams', index], message: 'Graph seam keys must be unique.' });
        }
        seamKeys.add(seam.seam_key);
        if (index > 0 && compareCodeUnits(run.seams[index - 1]?.seam_key ?? '', seam.seam_key) >= 0) {
            context.addIssue({ code: 'custom', path: ['seams', index], message: 'Graph seams must be strictly ordered.' });
        }
    }
    const edgeKeys = new Set();
    for (const [index, edge] of run.edges.entries()) {
        const key = pairKey(edge.source_lineage_id, edge.target_lineage_id);
        if (edgeKeys.has(key)) {
            context.addIssue({
                code: 'custom',
                path: ['edges', index],
                message: 'Graph entity-pair edges must be unique.',
            });
        }
        edgeKeys.add(key);
        if (edge.source_lineage_id > edge.target_lineage_id) {
            context.addIssue({ code: 'custom', path: ['edges', index], message: 'Graph edge endpoints are not ordered.' });
        }
        const previous = run.edges[index - 1];
        if (previous &&
            (compareCodeUnits(previous.source_lineage_id, edge.source_lineage_id) > 0 ||
                (previous.source_lineage_id === edge.source_lineage_id &&
                    compareCodeUnits(previous.target_lineage_id, edge.target_lineage_id) >= 0))) {
            context.addIssue({ code: 'custom', path: ['edges', index], message: 'Graph edges must be strictly ordered.' });
        }
        if (!nodeIds.has(edge.source_lineage_id) || !nodeIds.has(edge.target_lineage_id)) {
            context.addIssue({ code: 'custom', path: ['edges', index], message: 'Graph edge endpoint is undeclared.' });
        }
        if (edge.seam_membership.member !== edge.seam_membership.seam_keys.length > 0) {
            context.addIssue({
                code: 'custom',
                path: ['edges', index, 'seam_membership'],
                message: 'Graph seam membership flag and keys disagree.',
            });
        }
        for (const seamKey of edge.seam_membership.seam_keys) {
            if (!seamKeys.has(seamKey)) {
                context.addIssue({
                    code: 'custom',
                    path: ['edges', index, 'seam_membership', 'seam_keys'],
                    message: 'Graph edge references an undeclared seam.',
                });
            }
        }
        if (edge.cochange.denominator !== run.footer.counts.transitions) {
            context.addIssue({
                code: 'custom',
                path: ['edges', index, 'cochange', 'denominator'],
                message: 'Graph edge denominator does not match the observed transition count.',
            });
        }
    }
    const stateCounts = {
        live: run.nodes.filter(node => node.state === 'live').length,
        historical: run.nodes.filter(node => node.state === 'historical').length,
        unknown: run.nodes.filter(node => node.state === 'unknown').length,
    };
    const cochangeEdges = run.edges.filter(edge => edge.cochange.count > 0).length;
    const incidenceEdges = run.edges.filter(edge => edge.incidence.total > 0).length;
    if (run.footer.counts.nodes !== run.nodes.length ||
        run.footer.counts.live_nodes !== stateCounts.live ||
        run.footer.counts.historical_nodes !== stateCounts.historical ||
        run.footer.counts.unknown_nodes !== stateCounts.unknown ||
        run.footer.counts.seams !== run.seams.length ||
        run.footer.counts.edges !== run.edges.length ||
        run.footer.counts.cochange_edges !== cochangeEdges ||
        run.footer.counts.incidence_edges !== incidenceEdges ||
        run.footer.counts.diagnostics !== run.diagnostics.length) {
        context.addIssue({ code: 'custom', path: ['footer', 'counts'], message: 'Graph footer counts do not match.' });
    }
    const complete = Object.values(run.footer.coverage).every(value => value === 'complete');
    if (run.footer.complete !== complete) {
        context.addIssue({
            code: 'custom',
            path: ['footer', 'complete'],
            message: 'Graph completeness is inconsistent.',
        });
    }
});
function graphV2AsV1(run) {
    const { atoms_contract: _atoms, blob_structure_contract: _blob, ...policy } = run.header.policy;
    return {
        header: {
            ...run.header,
            schema_version: GRAPH_SCHEMA_VERSION_V1,
            lineage_run_id: run.header.lineage_run_id.replace(/^atomrun2:/, 'atomrun1:'),
            provenance: { ...run.header.provenance, instrument_version: GRAPH_SCHEMA_VERSION_V1 },
            policy,
        },
        diagnostics: run.diagnostics.map(item => ({ ...item, schema_version: GRAPH_SCHEMA_VERSION_V1 })),
        nodes: run.nodes.map(item => ({ ...item, schema_version: GRAPH_SCHEMA_VERSION_V1 })),
        seams: run.seams.map(item => ({ ...item, schema_version: GRAPH_SCHEMA_VERSION_V1 })),
        edges: run.edges.map(item => ({ ...item, schema_version: GRAPH_SCHEMA_VERSION_V1 })),
        footer: { ...run.footer, schema_version: GRAPH_SCHEMA_VERSION_V1 },
    };
}
export const OccEntityGraphRunV2Schema = z
    .object({
    header: EntityGraphHeaderV2Schema,
    diagnostics: z.array(EntityGraphDiagnosticV2Schema),
    nodes: z.array(EntityGraphNodeV2Schema),
    seams: z.array(EntityGraphSeamV2Schema),
    edges: z.array(EntityGraphEdgeV2Schema),
    footer: EntityGraphFooterV2Schema,
})
    .strict()
    .superRefine((run, context) => {
    const checked = OccEntityGraphRunV1Schema.safeParse(graphV2AsV1(run));
    if (!checked.success) {
        context.addIssue({
            code: 'custom',
            message: `Entity-graph V2 receipt is internally inconsistent: ${z.prettifyError(checked.error)}`,
        });
    }
});
/** Consume through EOF and cross-check a whole entity-graph stream. */
export async function collectEntityGraphV1(events) {
    let header;
    let footer;
    let phase = 'diagnostic';
    const diagnostics = [];
    const nodes = [];
    const seams = [];
    const edges = [];
    for await (const raw of events) {
        const event = OccEntityGraphEventV1Schema.parse(raw);
        if (footer)
            throw new Error('Entity-graph event encountered after the footer.');
        if (!header) {
            if (event.type !== 'header')
                throw new Error('Entity graph must begin with exactly one header.');
            header = event;
            continue;
        }
        if (event.type === 'header')
            throw new Error('Entity graph contains a duplicate header.');
        if (event.type === 'footer') {
            footer = event;
            continue;
        }
        if (event.type === 'diagnostic') {
            if (phase !== 'diagnostic')
                throw new Error('Entity-graph diagnostic encountered after graph records.');
            diagnostics.push(event);
            continue;
        }
        if (event.type === 'node') {
            if (phase === 'seam' || phase === 'edge')
                throw new Error('Entity-graph node encountered out of order.');
            phase = 'node';
            nodes.push(event);
            continue;
        }
        if (event.type === 'seam') {
            if (phase === 'edge')
                throw new Error('Entity-graph seam encountered after entity edges.');
            phase = 'seam';
            seams.push(event);
            continue;
        }
        phase = 'edge';
        edges.push(event);
    }
    if (!header)
        throw new Error('Entity graph is missing its header.');
    if (!footer)
        throw new Error('Entity graph is missing its footer.');
    return OccEntityGraphRunV1Schema.parse({ header, diagnostics, nodes, seams, edges, footer });
}
/** Consume through EOF and cross-check the V2 graph stream emitted by this build. */
export async function collectEntityGraph(events) {
    let header;
    let footer;
    let phase = 'diagnostic';
    const diagnostics = [];
    const nodes = [];
    const seams = [];
    const edges = [];
    for await (const raw of events) {
        const event = OccEntityGraphEventV2Schema.parse(raw);
        if (footer)
            throw new Error('Entity-graph event encountered after the footer.');
        if (!header) {
            if (event.type !== 'header')
                throw new Error('Entity graph must begin with exactly one header.');
            header = event;
            continue;
        }
        if (event.type === 'header')
            throw new Error('Entity graph contains a duplicate header.');
        if (event.type === 'footer') {
            footer = event;
            continue;
        }
        if (event.type === 'diagnostic') {
            if (phase !== 'diagnostic')
                throw new Error('Entity-graph diagnostic encountered after graph records.');
            diagnostics.push(event);
            continue;
        }
        if (event.type === 'node') {
            if (phase === 'seam' || phase === 'edge')
                throw new Error('Entity-graph node encountered out of order.');
            phase = 'node';
            nodes.push(event);
            continue;
        }
        if (event.type === 'seam') {
            if (phase === 'edge')
                throw new Error('Entity-graph seam encountered after entity edges.');
            phase = 'seam';
            seams.push(event);
            continue;
        }
        phase = 'edge';
        edges.push(event);
    }
    if (!header)
        throw new Error('Entity graph is missing its header.');
    if (!footer)
        throw new Error('Entity graph is missing its footer.');
    return OccEntityGraphRunV2Schema.parse({ header, diagnostics, nodes, seams, edges, footer });
}
//# sourceMappingURL=entity-graph.js.map