/** * Parse-health disclosure (change: add-parse-health-boundary-disclosure). * * The language-support registry can't over-claim a *language* — but it says nothing about failed * extraction *inside* a supported language. A file the pinned grammar rejects, a tree tree-sitter * recovered with a large `ERROR` region, or a source that decoded lossily today yields a silently * smaller graph indistinguishable from "there is genuinely nothing there." This module records * per-file parse health so downstream conclusions can disclose *unknown* instead of implying * *absent* — the exact failure mode the `NoFalseCompleteness` requirement exists to prevent. * * Two honesty rules, mirroring the style-fingerprint and IaC extractors: * 1. Clean files pay zero: `tallyParseHealth` returns `undefined` unless the parsed tree actually * carries an error, so a healthy repo produces no records and no boundary is ever emitted. * 2. The signal is a LOWER BOUND: an ERROR region can swallow well-formed neighbors, so a * disclosed count is "at least this degraded," never "exactly this and no more." * * Tallied in the SAME per-file AST walk that extracts nodes/edges (no second parse), exactly like * the style fingerprint. Deterministic: integer tallies over a deterministic walk, sorted keys, no * clock — byte-identical across re-analyses of a fixed repository state. */ import type { MemoryDegradation } from './memory-strategy.js'; /** Bump when the persisted artifact shape changes incompatibly. */ export declare const PARSE_HEALTH_SCHEMA_VERSION = 1; /** * Cap on the number of error-region start lines retained per file. A bound on the persisted record * (a pathological file could otherwise carry thousands of ERROR nodes); the counts stay exact, only * the line LIST is truncated, and truncation is disclosed (`truncated: true`). */ export declare const PARSE_HEALTH_LINE_CAP = 25; /** * Machine-readable cause for a file the analyzer declined to include (change: * fix-analyze-native-abort-and-file-cost-budget). * * Before this existed, an exclusion reached the user as a bare count — "Files skipped: 3" — with * no cause, while `doctor` judged the same repository independently and could report a clean bill * of health for it. Recording the cause on the file, in the one artifact every health surface * reads, is what makes the two surfaces unable to disagree. * * Each member is a path that actually exists in the code; the set is deliberately not aspirational: * - `parse-failure` the extractor threw, or produced no usable tree * - `budget-exceeded` the parse hit {@link PER_FILE_PARSE_BUDGET_MS} and was abandoned * - `size-cap` the file exceeded a size bound before extraction was attempted * * A **worker fault is deliberately absent**. The proposal listed one, but a worker fault no longer * excludes a file: the pool hands the file back to the main thread — the reference implementation — * which extracts it normally, so the facts stay whole and the fault is disclosed on the extraction * LANE instead. If that main-thread attempt also fails, the reason recorded here is the one the * main thread actually produced. Recording `worker-fault` on the file would have blamed the source * for a defect in the thread reading it. * * `encoding` is likewise absent: a lossy decode does not exclude a file (it still parses, over * replacement characters), so it stays the separate `encodingFallback` signal it has always been. */ export type FileExclusionReason = 'parse-failure' | 'budget-exceeded' | 'size-cap'; /** * Per-file parse health. Present ONLY for a file with at least one signal (error region, parse * failure, or encoding fallback) — a clean file has no record. Absent fields mean "not observed." */ export interface FileParseHealth { filePath: string; language: string; /** tree-sitter `ERROR` nodes (unparseable spans the recovery inserted). */ errorCount: number; /** tree-sitter `MISSING` nodes (tokens the grammar expected but the source omitted). */ missingCount: number; /** 1-based start lines of the error/missing regions, sorted + deduped, bounded by the cap. */ errorLines: number[]; /** `errorLines` hit the cap — more regions exist than are listed. */ truncated?: boolean; /** The extractor threw or produced no usable tree — the whole file contributed nothing. */ parseFailed?: boolean; /** The source decoded lossily (contained U+FFFD) — parse output may be garbage. */ encodingFallback?: boolean; /** * Why this file contributed nothing, when it contributed nothing. Absent on a file that WAS * extracted and merely parsed with error regions — a degraded file is not an excluded one. */ exclusion?: FileExclusionReason; /** * The budget, in ms, that this file exceeded. Present only for `budget-exceeded`. * * The BOUND, not the measured elapsed time — deliberately. This artifact must be byte-identical * across re-analyses of a fixed repository state (change: fix-artifact-output-determinism), and * a wall-clock measurement never is. Nothing is lost: a file is only recorded here because it * ran past the bound, so "exceeded 20000ms" says everything the measurement would, and says it * the same way every run. The measured time still reaches the operator live, on the CLI's * extraction-lane disclosure, which is not persisted. */ budgetMs?: number; } /** * Minimal structural view of a tree-sitter node. Kept dependency-light (no `tree-sitter` import) so * this module stays a leaf, and defensive across binding versions: `ERROR` is detected by `type` * (stable across bindings) and `MISSING` by `isMissing` as either a boolean property (node-tree- * sitter) or a method (web-tree-sitter). A plain test object supplying only `type`/`children` * still works. */ export interface ParseHealthNode { type: string; startPosition: { row: number; }; isMissing?: boolean | (() => boolean); hasError?: boolean | (() => boolean); children: ParseHealthNode[]; childCount?: number; child?(i: number): ParseHealthNode | null; } /** * Record parse health for one file from its already-parsed tree. Returns `undefined` for a clean * tree (the fast path — no walk, zero cost, so a healthy repo produces no records). When the tree * carries an error it walks ALL children (not just named — an unnamed ERROR token still counts), * tallying `ERROR` and `MISSING` nodes and collecting their start lines up to the cap. * * The walk fires only on the rare error tree, so its `children` allocation is not on the hot path. * * ## The walk is iterative, and that is load-bearing * * This walk used to recurse (change: fix-analyze-native-abort-and-file-cost-budget). Tree depth is * not bounded by anything the analyzer controls: a 300 KB file of a repeated unterminated * block-comment opener parses into a right-leaning chain 100,002 nodes deep, and error recovery is * exactly the condition that produces such trees — which is also exactly when this walk runs. The * recursion overflowed the stack there, and a `RangeError` raised while executing inside the * native binding's node accessor is what turns a slow file into * `libc++abi: terminating due to uncaught exception of type Napi::Error` and an exit-134 abort, * with no JavaScript error anywhere. An explicit stack costs a heap array and cannot overflow, so * depth stops being a correctness cliff. Order is unchanged: children are pushed in reverse so * they pop in source order, which keeps `errorLines` (capped by insertion order) byte-identical * to the recursive version. */ export declare function tallyParseHealth(language: string, rootNode: ParseHealthNode, filePath: string): FileParseHealth | undefined; /** * True if decoding these bytes as UTF-8 is LOSSY — the source contains byte sequences that are not * valid UTF-8 and would be replaced by U+FFFD. Detected at the BYTE level (a strict decode that * throws), NOT by scanning the decoded string for U+FFFD: a file may legitimately CONTAIN U+FFFD * (as valid UTF-8 bytes `EF BF BD`), and flagging that would be a false positive. Only genuinely * undecodable bytes count. */ export declare function isLossyUtf8(bytes: Uint8Array): boolean; /** A file is "degraded" if it carries any parse-health signal at all. */ export declare function isDegraded(h: FileParseHealth): boolean; /** Human phrasing for one exclusion reason, used by every surface that renders one. */ export declare const EXCLUSION_REASON_LABEL: Record; /** One language's rolled-up degradation, for the compact summary. */ export interface ParseHealthLanguageSummary { language: string; degradedFiles: number; errorRegions: number; parseFailures: number; encodingFallbacks: number; } /** The persisted, rolled-up parse-health report (its own `parse-health.json` artifact). */ export interface ParseHealthReport { version: number; /** Files carrying at least one signal. */ totalDegradedFiles: number; /** Sum of ERROR + MISSING regions across all degraded files. */ totalErrorRegions: number; /** Per-language rollup, sorted by degraded-file count desc then name. */ byLanguage: ParseHealthLanguageSummary[]; /** The worst offenders, sorted by region count desc then path, bounded. */ topFiles: FileParseHealth[]; /** Every per-file record (the source of truth the watcher splices and consumers scan). */ files: FileParseHealth[]; /** * How many files were EXCLUDED, per reason (change: * fix-analyze-native-abort-and-file-cost-budget). Omitted entirely when nothing was excluded, * so a repository whose only signal is error regions carries no empty tally. This is the single * record every health surface reads, which is what stops `analyze` and `doctor` disagreeing. */ excludedByReason?: Partial>; /** * What the graceful-degradation ladder shed under memory pressure, if anything (change: * make-analyze-scale-to-any-repo). Present ONLY when a tier was shed — a full-fidelity run * carries none, so its artifact stays byte-identical to a run that never had this feature. Rides * the SAME artifact every health surface already reads, so a reduced overlay/deep-analysis is * disclosed exactly like an excluded file: a downstream conclusion reads it as reduced coverage, * never as genuine structural absence. */ memoryDegradation?: MemoryDegradation; } /** Total files excluded (any reason). `0` when nothing was excluded. */ export declare function totalExcluded(report: ParseHealthReport | null | undefined): number; /** * One-line "3 excluded (2 budget-exceeded, 1 parse-failure)" phrasing, or `undefined` when nothing * was excluded. Shared so `analyze`, `doctor` and the boundary text cannot word it differently. * Reason order is fixed (alphabetical) so the line is deterministic across runs. */ export declare function describeExclusions(report: ParseHealthReport | null | undefined): string | undefined; /** * Roll the raw per-file records up into the persisted report. Returns `undefined` when there are no * records at all — a clean repo persists no artifact and every consumer treats "no artifact" as * "nothing degraded," so clean repos pay zero (no boundary, no payload growth). */ export declare function buildParseHealthReport(records: FileParseHealth[], topN?: number, memoryDegradation?: MemoryDegradation): ParseHealthReport | undefined; /** A compact, one-line-per-language string list for the `orient` summary (bounded upstream). */ export declare function compactParseHealthSummary(report: ParseHealthReport): string[]; //# sourceMappingURL=parse-health.d.ts.map