import type { Dirent } from "node:fs";
import { type IgnoreFilter } from "./ignore.js";
/**
 * The shape version of a persisted {@link GraphInputRecord}.
 *
 * A record carrying any other version is treated exactly as a missing one: one
 * full rebuild, which writes the current shape back. Nothing migrates, nothing
 * is re-indexed, and no operator action is involved.
 */
export declare const GRAPH_INPUTS_VERSION = 1;
/**
 * Every input the graph build actually consumed, persisted beside the graph.
 *
 * The point of recording it from the build rather than describing it in a
 * predicate is that there is then no second answer to "does this file
 * contribute to the code graph?" to drift from the first. A file reaches this
 * record because the builder read it; a file the builder stops reading stops
 * being recorded in the same commit.
 *
 * The buckets mirror the *kinds of read* the build makes, because each kind
 * can only be watched the way it was made:
 *
 * | the build | recorded as | watched by |
 * |---|---|---|
 * | read a file whole | content hash | re-hashing it |
 * | read a file's head to classify it | head hash | re-reading that head |
 * | used a file without opening it | its size | a stat |
 * | resolved an installed module lookup path | target hash or absence | resolving it again |
 * | tried to open one and could not | its path | trying again |
 * | listed a directory | its entry names | listing it again |
 * | tried to list one and could not | its path | listing it again |
 */
export interface GraphInputRecord {
    version: number;
    /**
     * The SocratiCode version that produced this record. Compared on its own
     * rather than folded into {@link GraphInputRecord.settings} so that an
     * upgrade's rebuild is legible in the log and reviewable as its own rule.
     */
    builtByVersion: string;
    /**
     * Project-relative path → {@link hashContent} of the bytes the build used.
     * Paths outside the project root (a `tsconfig` reached through `extends`,
     * say) keep their `../` prefix and are resolved against the root again.
     */
    files: Record<string, string>;
    /**
     * Extensionless files the build head-read to decide whether they were code,
     * and turned away — path → hash of that head.
     *
     * Their bytes decided the shape of the file set, so they are inputs; but
     * only the head was read, and a whole-file hash would not describe the
     * decision. Size will not do either: `# hello!!` becoming `#!/bin/sh` flips
     * the answer without moving a byte count.
     */
    heads: Record<string, string>;
    /**
     * Inputs the build used without reading their bytes: a leaf node made from
     * its path alone, a file too large for the graph to read, a file that could
     * not be read at all. Recorded as the size the build saw, or `-1` where even
     * that was unknown.
     */
    presence: Record<string, number>;
    /** Module lookup path → canonical target hash, or null when absent. */
    realpaths?: Record<string, string | null>;
    /**
     * Files the build tried to open and could not — a manifest it found and
     * failed to read, a `tsconfig.json` that exists but is unreadable, a source
     * file that vanished mid-walk, an extensionless file whose head-read threw.
     *
     * Distinct from {@link GraphInputRecord.presence}, which is a file the build
     * deliberately did not open: that one is watched by size, and an unreadable
     * file has no size the build ever saw. The only question that matters for
     * these is whether they can be read now, because a file that starts being
     * readable starts contributing.
     */
    unreadable: string[];
    /**
     * Directories the walk could not list. The subtree under one contributed
     * nothing, so a directory that starts being listable can add nodes with its
     * own entry in the parent unchanged — and there is no listing to compare,
     * which is why these are separate from {@link GraphInputRecord.directories}
     * and re-checked with a `readdir` rather than a readability test.
     */
    unreadableDirectories: string[];
    /**
     * Every directory the discovery walk listed → what it saw there. The root is
     * `"."`.
     *
     * This is what makes an addition detectable at all. The other buckets
     * describe files that existed when the build ran, so nothing in them can
     * speak for a path that did not — and the index cannot speak for one it does
     * not hold, which covers `go.mod`, `project.godot`, `.uid` sidecars and,
     * unless `INCLUDE_DOT_FILES` is set, every ignore file. A directory listing
     * is the read the walk actually made, and it changes the moment any of them
     * appears.
     */
    directories: Record<string, DirectoryListing>;
    /** Hash of every graph input that is not a file — see {@link graphSettingsHash}. */
    settings: string;
    /**
     * Which parsers the build actually had — see {@link graphCapabilitiesHash}.
     *
     * Separate from {@link GraphInputRecord.builtByVersion} because it varies
     * *within* a version: the optional `@ast-grep/lang-*` grammars and the
     * GDScript native addon ship platform-specific prebuilds, so the same
     * SocratiCode on a different host, runtime or architecture can extract
     * different imports and symbols from byte-identical sources.
     */
    capabilities: string;
}
/**
 * What one directory held, hashed two ways.
 *
 * `all` is every entry name, ignored ones included; `kept` is only those the
 * ignore filter admitted. Both, because they answer the same question at very
 * different prices: comparing `all` needs nothing but a `readdir`, while
 * `kept` needs the filter, and building one costs a synchronous walk of the
 * tree plus an `ignore` test per entry — 2.1s of the 2.2s a check took on a
 * 3,941-file repository, against 5ms for the listings themselves.
 *
 * So `all` is asked first and settles the common case. It cannot answer on its
 * own: a `.DS_Store` appearing, or a coverage directory written and cleaned by
 * a test run, moves it without moving anything the graph was built from, and a
 * rebuild for each of those would leave the skip theoretical. `kept` is what
 * decides, and it is only ever computed for a directory whose `all` moved.
 */
export interface DirectoryListing {
    all: string;
    kept: string;
}
/**
 * Hash of the inputs the build reads from configuration rather than from the
 * tree: which extra extensions count as source, which extensions are mapped to
 * another language's grammar, which configured Python roots are searched and
 * in what order, whether dotfiles and `.gitignore` are honoured, and how large
 * a file the graph will still read.
 */
/**
 * Which parsers were available to the build, as one hash.
 *
 * Names only, never the error text of a failed load: the text carries paths
 * and messages that differ between hosts for reasons that have nothing to do
 * with what the build could parse, and would make the fingerprint unstable in
 * exactly the situation it exists to detect.
 */
export declare function graphCapabilitiesHash(input: {
    loadedGrammars: readonly string[];
    gdscript: boolean;
    elixirTemplates: boolean;
}): string;
export declare function graphSettingsHash(extraExtensions: ReadonlySet<string>, pythonRoots?: readonly string[]): string;
/** What the ignore filter made of one directory's entries. */
export interface DirectoryScan {
    /** The entries the filter admitted, in the order they were read. */
    kept: Dirent[];
    listing: DirectoryListing;
}
/**
 * Apply the ignore filter to one directory's entries, and hash what it saw.
 *
 * The single place the filter is applied to a listing: the discovery walk
 * iterates the `kept` entries this returns rather than filtering again, and
 * the rebuild check re-reads the directory through this same function. Neither
 * can drift from the other about which entries counted, because neither has
 * its own opinion.
 */
export declare function scanDirectory(ig: IgnoreFilter, projectPath: string, dir: string, entries: Dirent[]): DirectoryScan;
/**
 * Raised by {@link GraphInputRecorder.finish} when the build observed one file
 * two irreconcilable ways.
 *
 * The build reads some sources twice — resolver setup builds its namespace,
 * FQCN, module and `class_name` maps before the main loop parses the same
 * files — so a file edited in between leaves resolver output describing one
 * version while nodes, imports and symbols describe another. That graph is
 * internally mixed, and a record of either version would let the next update
 * call it current. Neither half is worth persisting, so the build is abandoned
 * and retried from a fresh scan instead.
 */
export declare class GraphInputsMovedDuringBuild extends Error {
    /** Project-relative path → what disagreed about it. */
    readonly conflicts: ReadonlyMap<string, string>;
    constructor(conflicts: ReadonlyMap<string, string>);
}
/**
 * Collects what a single graph build read. One per build, handed to every part
 * of the build that opens a file.
 *
 * Repeated observations of one file are consistency-checked, not overwritten:
 * the first stands, and a later one that contradicts it is a conflict rather
 * than a replacement, because the build has by then already used the first.
 */
export interface GraphInputRecorder {
    /** An input the build read whole, recorded by the bytes it actually used. */
    read(absolutePath: string, content: string): void;
    /** An input the build head-read to classify and did not take. */
    head(absolutePath: string, head: string): void;
    /**
     * An input the build used without reading it. `size` is what the build saw,
     * where it got that far.
     */
    present(absolutePath: string, size?: number): void;
    /** A module lookup's native binding. Optional for existing recorder callers. */
    realpath?(absolutePath: string, canonicalPath: string | null): void;
    /** An input the build tried to open and could not. */
    unreadable(absolutePath: string): void;
    /** A directory the walk tried to list and could not. */
    unreadableDirectory(absolutePath: string): void;
    /** A directory the walk listed, by what it saw there. */
    directory(absolutePath: string, listing: DirectoryListing): void;
    /**
     * Seal the record. Safe to call once the build has finished reading.
     *
     * @throws {GraphInputsMovedDuringBuild} if any file was observed two ways.
     */
    finish(extraExtensions: ReadonlySet<string>, capabilities: string, pythonRoots?: readonly string[]): GraphInputRecord;
}
export declare function createGraphInputRecorder(projectRoot: string): GraphInputRecorder;
/**
 * A record whose shape this build understands, or null.
 *
 * Persisted as a JSON string beside the graph, so the string form is what
 * comes back and is accepted here; anything that fails to parse, or parses to
 * something that is not this shape, is null — which the caller reads the same
 * way as no record at all.
 */
export declare function parseGraphInputRecord(stored: unknown): GraphInputRecord | null;
/**
 * What an incremental update did to the index, in the terms this decision
 * needs. The index has already hashed everything it holds, so most recorded
 * inputs are answered from that work rather than read again.
 */
export interface GraphChangeSummary {
    /**
     * Whether the update indexed a file it had never seen. Cheaper than the
     * directory check below and answers the common case, so it is asked first —
     * but it speaks only for files the index holds, which is why it is not the
     * whole of the addition story.
     */
    hasAdditions: boolean;
    /** Paths whose content changed, mapped to the hash the index now holds. */
    changed: ReadonlyMap<string, string>;
    /** Paths the index no longer holds. */
    removed: ReadonlySet<string>;
    /**
     * Paths the index tried to read this run and could not. The index keeps the
     * hash it already had for those, so without this they would read here as
     * unchanged — while a rebuild would drop them from the graph.
     */
    unreadable: ReadonlySet<string>;
    /**
     * The hash the index currently holds for a path it did not report as
     * changed, or undefined for a path it does not index at all (a `.gitignore`,
     * a `go.mod`). Those are read from disk here.
     */
    knownHash(relativePath: string): string | undefined;
}
export interface GraphRebuildDecision {
    rebuild: boolean;
    /** Why, in a few words, for the log line and the progress message. */
    reason: string;
    /**
     * Whether there is a graph to rebuild at all.
     *
     * False only when the project has no persisted graph: rebuilding then is
     * *building* one, which an update that indexed nothing has no business
     * doing. Every other answer — including a graph whose record is missing,
     * malformed, or could not be read — is true, because those are the cases
     * that must rebuild once and populate however quiet the update was.
     */
    graphExists: boolean;
}
/**
 * Whether the graph has to be rebuilt for this change.
 *
 * Conservative in every direction that is not proven: an unreadable or
 * unrecognised record, a version or settings change, any addition, and any
 * recorded input that changed, vanished, or cannot be read all rebuild. The
 * skip is reached only by checking every recorded input and finding all of
 * them unchanged.
 *
 * Ordered cheapest first — the checks that need no I/O, then stats, then head
 * reads, then whole-file hashes, and last the directory listings, which are
 * the only check that walks anything.
 */
export declare function decideGraphRebuild(projectRoot: string, stored: unknown, change: GraphChangeSummary, extraExtensions: ReadonlySet<string>, capabilities: string, pythonRoots?: readonly string[]): Promise<GraphRebuildDecision>;
//# sourceMappingURL=graph-inputs.d.ts.map