/** * Per-file EXTRACTION budget (changes: fix-analyze-native-abort-and-file-cost-budget, * bound-large-literal-extraction). * * The bound covers one file's whole extraction — the parse AND everything the per-language * extractor then does with the tree. It began as a parse-only bound, and that was not the bound * the documentation claimed: a generated `demo.pb.go` parsed in milliseconds and then spent 82 s * in a post-parse tree walk, so a documented 20 s per-file bound let an 82 s file through with no * disclosure at all. A bound that does not hold is an honesty defect as much as a performance one, * so the clock now starts at {@link runWithExtractionBudget} and covers every phase. * * Extraction of one file used to be unbounded in wall-clock time. Measured on a 300 KB file of * a repeated unterminated block-comment opener: 84 s inside `parser.parse()` alone, yielding a * 100,002-deep tree. A minified bundle or a generated client in an ordinary repository reaches * the same path, so this is not only a hostile-input problem. * * ## Why the bound is in-band, not a timer * * Nothing here is a timer: an extraction is synchronous JS over a native tree, so the event loop * does not run until it returns and a `setTimeout` could never fire in time to stop it. The bound * is instead checked at COOPERATIVE CHECKPOINTS the extraction already passes through: * * 1. inside `parser.parse()`, via tree-sitter's own deadline (see below), armed with the time * REMAINING on the file's budget rather than the whole of it; * 2. at each shared post-parse phase boundary (the style / parse-health / dynamic-boundary / * content-hash / CFG / class-relationship tallies every language extractor funnels through); * 3. once per node inside the shared tree-walk children boundary (`readNamedChildren`); * 4. conclusively when the file's extraction RETURNS — see {@link runWithExtractionBudget}. * * Checkpoint 4 is what makes the bound hold absolutely for the GRAPH: no file whose extraction * exceeded the budget is admitted, whether or not a checkpoint interrupted it. Checkpoints 1–3 are * what usually make it hold in WALL CLOCK too. A phase carrying none of them can still overshoot * until it finishes — that is a real, disclosed limit of a cooperative bound, not a covered case. * * `parser.parse()` is one synchronous call into a native binding. A `setTimeout` cannot preempt * it — the event loop does not run until it returns — and terminating the worker that holds it * is precisely what turns a slow parse into a process-level `abort()` (V8 can only interrupt a * thread at a JS boundary; an interrupt delivered inside a native frame surfaces as a C++ * exception with nowhere to go). tree-sitter's own deadline is the only bound that can actually * stop the work: it is checked inside the parse loop, and on expiry `parse()` simply returns no * tree. Nothing is killed, and the parser stays reusable for the next file. * * ## Honesty * * A budgeted result is a LOWER BOUND and reads as one. An abandoned file is never silently * dropped: {@link ExtractionBudgetExceededError} carries the elapsed time, the caller records the * file with reason `budget-exceeded`, and conclusions built over it disclose the boundary. The * bound is disabled by setting `OPENLORE_PARSE_BUDGET_MS=0`, which reproduces the previous * behavior exactly — the escape hatch for anyone who would rather wait than be told. * * A binding whose `setTimeoutMicros` is unavailable or unusable (older native builds, and the * web-tree-sitter WASM lane) simply parses unbounded, as it did before — {@link parseBudgetSupported} * reports which. That is a real limit of CHECKPOINT 1, not a covered case: on those languages a * runaway PARSE still runs to completion. Checkpoints 2–4 are language-independent, so such a file * is still abandoned and disclosed once the parse returns — it is the wall clock that is unbounded * there, not the graph. */ /** * Stable prefix on {@link ExtractionBudgetExceededError}'s message. * * The message is the ONLY channel that survives an extraction worker: a throw crossing the * `worker_threads` boundary is structured-cloned, so its class, prototype and properties are * gone by the time the parent sees it. Keying the reason off a message prefix is what makes the * pooled and serial lanes classify the same failure identically — an `instanceof` check would * behave differently on the two lanes, which is the bug this prefix exists to avoid. */ export declare const EXTRACTION_BUDGET_MESSAGE_PREFIX = "openlore:extraction-budget-exceeded"; /** Thrown when one file's extraction was abandoned at the deadline. Carries the bound and the spend. */ export declare class ExtractionBudgetExceededError extends Error { readonly elapsedMs: number; readonly budgetMs: number; constructor(elapsedMs: number, budgetMs: number); } /** * Read `message` as a budget-exceeded signal, returning the BUDGET it names. * * Message-based on purpose — see {@link EXTRACTION_BUDGET_MESSAGE_PREFIX}. Returns `undefined` for * any other failure, so an ordinary parse failure is never re-labelled as a budget overrun. * * The budget rather than the elapsed time, because the caller records it in a persisted artifact * that must be byte-identical across re-analyses of a fixed repository state — see * `FileParseHealth.budgetMs`. */ export declare function extractionBudgetOverrunMs(message: string | undefined): number | undefined; /** * The active budget in milliseconds, or `0` when disabled. * * Read per call rather than cached at module load: extraction workers inherit `process.env`, and * tests set the override around a single build. A value that is not a finite, non-negative number * is ignored in favour of the default — a typo must not silently disable the bound. */ export declare function extractionBudgetMs(): number; /** The minimum shape {@link parseWithBudget} needs. Narrow so tests can supply a plain object. */ export interface BudgetableParser { parse(content: string): TTree | null | undefined; setTimeoutMicros?(micros: number): void; /** * Discard partial parse state. Load-bearing after a deadline fires — see * {@link parseWithBudget}. Optional so a test double need not supply it. */ reset?(): void; } /** Can this parser actually enforce a deadline? `false` means it parses unbounded, as before. */ export declare function parseBudgetSupported(parser: object): boolean; /** * Parse `content` under the active per-file budget. * * Returns the tree on success. Throws {@link ExtractionBudgetExceededError} when the deadline fired, * so the existing per-file `try`/`catch` in the builder — which already records a file that * contributed nothing — classifies and discloses it without every extractor growing a new return * shape. * * The deadline is re-applied before each parse rather than once at parser construction: these * parsers are per-language singletons shared across a whole build (and reused by the watcher * across builds), so a budget set once could be left stale by any other caller. Setting it is a * single native field write. */ export declare function parseWithBudget(parser: BudgetableParser, content: string): TTree; /** * Run ONE file's extraction under the per-file budget. * * On return, the elapsed time is checked a final time and an overrun THROWS rather than returning * the facts. That final check is the one that makes the documented bound true of the graph: a phase * with no cooperative checkpoint of its own cannot be interrupted, but its file is still abandoned * and disclosed as `budget-exceeded` instead of being quietly admitted over budget. * * Discarding facts that were in fact computed is deliberate. The alternative — admit them because * they happen to exist — makes the bound unpredictable (a file is in the graph or not depending on * where the clock ran out), and leaves a file that costs minutes costing minutes on every re-analysis * with nothing in `parse-health.json` naming it. * * A budget of `0` (`OPENLORE_PARSE_BUDGET_MS=0`) opens no scope at all, so every checkpoint below * is inert and the behaviour is the pre-bound one byte for byte. A nested call does NOT restart the * clock: the outermost scope owns the file. */ export declare function runWithExtractionBudget(run: () => Promise): Promise; /** * The active deadline as an absolute epoch-ms value, or `undefined` when no budget is in force. * * For a hot loop that must not pay an async-local lookup per iteration: read this ONCE before the * loop, then compare `Date.now()` against it periodically and call {@link checkExtractionBudget} to * throw. Returning the raw number rather than a callback is what keeps the per-node cost to one * numeric comparison. */ export declare function extractionDeadline(): number | undefined; /** * A cooperative checkpoint: throw if this file has already spent its budget. * * Inert — one async-local read and one subtraction — when no budget is in force, which is the case * for every query-time caller of the shared walks. */ export declare function checkExtractionBudget(): void; //# sourceMappingURL=extraction-budget.d.ts.map