/** * Code-graph index freshness — disclosed on every graph query, and repaired * inline when the repair is small (T12316). * * A graph answer from a stale index is a confident answer about code that no * longer exists. The check compares the published generation's file manifest * against the working tree: a file whose size and mtime are unchanged keeps its * recorded hash, and only a metadata mismatch is re-hashed, so a checkout or * `touch` that changed no bytes is not reported stale. * * Code placed in `packages/core/` per Package-Boundary Check — verified against AGENTS.md. * * @task T12316 * @module nexus/freshness */ import type { GraphIndexFreshness, NexusAnswerFreshness, ParserExecutionPort } from '@cleocode/contracts'; import { type GraphRunCost } from './graph-manifest.js'; import { type NexusStalePolicy } from './index-health.js'; export { DEFAULT_MAX_COMMITS_BEHIND, measureAgainstBase, NEXUS_ALLOW_STALE_ENV, type NexusIndexHealth, type NexusStalePolicy, probeNexusIndexHealth, readNexusStalePolicy, } from './index-health.js'; /** Command that refreshes the index; incremental by default since T12315. */ export declare const NEXUS_REFRESH_COMMAND = "cleo nexus analyze"; /** * Retired file-count cap for an inline refresh (T14014): the size-scaled cost * estimate against the budget decides. Kept as the reported default of the * ignored `nexus.autoRefresh.maxFiles`. */ export declare const DEFAULT_AUTO_REFRESH_MAX_FILES = 25; /** Default wall-clock budget for an inline refresh. */ export declare const DEFAULT_AUTO_REFRESH_BUDGET_MS = 60000; /** Options for {@link assessNexusIndexFreshness}. */ export interface NexusFreshnessOptions { /** Files of the queried symbol(s); their own staleness is reported separately. */ symbolFiles?: readonly string[]; /** Maximum stale paths listed in the result. @defaultValue 10 */ sampleLimit?: number; } /** * A freshness verdict plus the complete stale set it was computed from, so the * files of a symbol identified only AFTER the query can still be judged. */ export interface NexusFreshnessAssessment { /** Disclosed freshness facts. */ freshness: GraphIndexFreshness; /** Every stale path (modified, added, deleted or unreadable). */ staleFiles: ReadonlySet; /** * What a refresh would cost (T14014): files it must parse (stale ones and * any without a reusable cache entry), files it can reuse, and the estimate. */ refreshPlan?: NexusRefreshPlan; } /** What an inline refresh would parse, reuse and cost (T14014). */ export interface NexusRefreshPlan { /** Files that must be parsed: stale, or with no exact cache entry for this build. */ toParse: number; /** Unchanged files with an exact cache entry for this build. */ reusable: number; /** Estimated milliseconds, or `null` when no previous run cost is recorded. */ estimateMs: number | null; } /** * Judge the queried symbol's own files against an assessment. * @param assessment - Assessment taken before the query ran. * @param files - Files of the queried symbol(s), e.g. from {@link querySymbolFiles}. * @returns The freshness facts with `symbolFileStale`/`symbolFile` set when files are known. */ export declare function judgeSymbolFiles(assessment: NexusFreshnessAssessment, files: readonly string[]): GraphIndexFreshness; /** Auto-refresh policy, read from `nexus.autoRefresh.*` config unless given. */ export interface NexusAutoRefreshPolicy { /** Whether queries may refresh the index inline. @defaultValue true */ enabled: boolean; /** * Retired (T14014): the size-scaled cost estimate against `budgetMs` decides. * Still read from `nexus.autoRefresh.maxFiles` for compatibility; ignored. */ maxFiles: number; /** Wall-clock budget; a refresh estimated to exceed it is not started. @defaultValue 60000 */ budgetMs: number; /** What turned refresh off, when not the config key (`--no-refresh`, `CLEO_NEXUS_AUTO_REFRESH`). */ disabledBy?: string; /** * Indexed files above which any inline refresh needs `cleo run` admission, * however few files it parses (T14019). @defaultValue {@link ADMISSION_GRAPH_FILES} */ admissionGraphFiles?: number; } /** * Assess how far the published code graph lags the working tree. * * @param projectRoot - Project whose graph database is inspected. * @param options - Symbol files to judge individually and the stale-path sample size. * @returns Freshness facts; `status: 'unknown'` carries a `reason` and is never a pass. * @example * ```ts * const freshness = await assessNexusIndexFreshness('/repo', { symbolFiles: ['src/a.ts'] }); * if (freshness.status === 'stale') console.error(freshness.refreshCommand); * ``` */ export declare function assessNexusIndexFreshness(projectRoot: string, options?: NexusFreshnessOptions): Promise; /** * The freshness block every index-sensitive answer carries as `data.freshness`. * * @param freshness - The assessment behind the answer. * @returns Revision, base, commits behind, age and verdict. */ export declare function answerFreshness(freshness: GraphIndexFreshness): NexusAnswerFreshness; /** A refused graph answer: the index is too far behind to answer from (T14008). */ export interface NexusStaleRefusal { /** Stable error code. */ code: 'E_NEXUS_INDEX_STALE'; /** Why the answer was refused. */ message: string; /** The exact reindex command, and the opt-out. */ fix: string; /** The freshness facts and the threshold that refused. */ details: NexusAnswerFreshness & { maxCommitsBehind: number; }; } /** * Whether a graph answer must be refused as stale: the index trails the * checked-out HEAD by more than the policy allows, or the target is missing * from an index that is stale for this checkout (commits behind HEAD or * changed files), so it may exist in code the index never saw. Distance to the * base branch (`commitsBehind`) is reported only: on a feature branch or * worktree it grows as the base moves, and no reindex here could clear it. * * @param freshness - The assessment behind the answer. * @param policy - The stale-answer policy. * @param options - `targetMissing` when the query found no symbol; `projectRoot` * names the checkout in the reindex command. * @returns The refusal, or `null` when the answer may stand. * @example * ```ts * const refusal = staleIndexRefusal(freshness, policy, { projectRoot: root }); * if (refusal) return refusalEnvelope(refusal); * ``` */ export declare function staleIndexRefusal(freshness: GraphIndexFreshness, policy: NexusStalePolicy, options?: { targetMissing?: boolean; projectRoot?: string; }): NexusStaleRefusal | null; /** * Read the auto-refresh policy from `nexus.autoRefresh.*`, applying defaults. * @param projectRoot - Project whose merged config is read. * @returns The effective policy. */ export declare function readNexusAutoRefreshPolicy(projectRoot: string): Promise; /** What an inline refresh is asked to do (sent to its child process). */ export interface NexusRefreshInput { /** Project whose graph is refreshed. */ projectRoot: string; /** Source root the previous generation was built from. */ repoPath: string; /** Included nested repositories carried over from the previous generation. */ includedRepositories?: readonly string[]; /** Wall-clock budget; cancels the run at its next phase boundary. */ budgetMs: number; } /** What an inline refresh reports back. */ export interface NexusRefreshSummary { /** Analysis mode the refresh ran in. */ mode: GraphRunCost['summary']['mode']; /** Why that mode was chosen. */ reason: string; } /** Runs one inline refresh; {@link runRefreshInChild} unless a caller supplies another. */ export type NexusRefreshRunner = (input: NexusRefreshInput) => Promise; /** * Run a refresh in this thread, bounded by its budget. Unbounded in memory: * {@link runRefreshInChild} runs it inside a heap-limited child process. * * @param input - The refresh to run. * @returns The refresh summary. */ export declare function runRefreshInProcess(input: NexusRefreshInput): Promise; /** What the refresh child sends back: its summary, or why it failed. */ export type NexusRefreshReply = { ok: true; summary: NexusRefreshSummary; } | { ok: false; message: string; }; /** * Run an inline refresh in a child process with its own heap limit (T13537 · * T13548). * * Even an incremental run re-resolves the whole graph (Leiden included), so * the CLI's own heap is never used: a refresh that outgrows its limit kills * only the child. The child is the runtime's owned parser process (process * group, teardown, cgroup sizing), so ending it — on its result, its budget * plus a margin, or a crash — releases every SQLite handle and lock it held. * * @param input - The refresh to run. * @param options - Child entry, heap limit and process port; default to * `refresh-child.js` beside this module, this process's own V8 heap limit (so * a refresh that fit in-process still fits) and the runtime's parser port. * @returns The refresh summary. */ export declare function runRefreshInChild(input: NexusRefreshInput, options?: { entry?: URL; heapMb?: number; port?: ParserExecutionPort; }): Promise; /** Cooldown after an inline refresh fails or is aborted (T14014). */ export declare const INLINE_REFRESH_COOLDOWN_MS: number; /** * Indexed files above which every inline refresh needs `cleo run` admission * (T14019): resolve, communities, flows and the republish run over the whole * graph, so on a large repository they dominate memory even when one file is * re-parsed. */ export declare const ADMISSION_GRAPH_FILES = 2000; /** * The memory an inline refresh asks `cleo run` admission for (T14019): scaled * to the graph it re-resolves (indexed files), at least * {@link REFRESH_MIN_FOOTPRINT_BYTES}, at most the build class's footprint. * * @param indexedFiles - Files in the published generation. * @param classBytes - Footprint of the `scoped-build` class on this machine. * @returns Footprint in bytes. * @example * ```ts * inlineRefreshFootprintBytes(10_000, footprintForClass('scoped-build')); * ``` */ export declare function inlineRefreshFootprintBytes(indexedFiles: number, classBytes: number): number; /** Fallback cooldown file under `.cleo/` when `_nexus_meta` cannot be written (T14014). */ export declare const INLINE_REFRESH_COOLDOWN_FILE = "nexus-refresh-cooldown.json"; /** Lock file under `.cleo/`: one inline refresh at a time per project. */ export declare const INLINE_REFRESH_LOCK_FILE = "nexus-inline-refresh.lock"; /** * Admits a heavy inline refresh without waiting; `{ refused }` when the machine * is busy (T14014). `indexedFiles` sizes the footprint (T14019). */ export type NexusRefreshAdmitter = (projectRoot: string, indexedFiles: number) => Promise<{ release(): Promise; } | { refused: string; }>; /** * Assess freshness and, when the refresh is affordable, refresh the index * inline before the caller answers from it. * * A refresh runs when the recorded cost, scaled to the repository and to the * files it must parse (T14014), fits `budgetMs`. It is guarded so a stale * index never turns every query into another heavy run: * - one inline refresh at a time per project (a pid lock; a concurrent query * answers from the stale index); * - a refresh beyond a small incremental set needs a non-waiting `cleo run` * admission, and is skipped when the machine is busy; * - a refresh that fails or is aborted sets a cooldown: no inline refresh for * {@link INLINE_REFRESH_COOLDOWN_MS}, or until an analysis publishes a new * generation. The cooldown is reported as `refreshCooldownUntil`. * The budget also cancels a run at its next check, and publication is atomic, * so an abandoned refresh leaves the previous graph intact. Every decision is * disclosed in `autoRefresh`. * * @param projectRoot - Project whose graph is about to be queried. * @param policy - Explicit policy; read from config when omitted. * @param runRefresh - Runs the refresh; a heap-bounded worker unless supplied (T13537). * @param admitter - Admits a heavy refresh; `cleo run` admission unless supplied. * @returns Freshness of the index as it stands after any refresh, with its stale set. */ export declare function ensureNexusIndexFresh(projectRoot: string, policy?: NexusAutoRefreshPolicy, runRefresh?: NexusRefreshRunner, admitter?: NexusRefreshAdmitter): Promise; /** * Collect the source files a graph query answer is about. * * Graph node ids are `::`, so a target id names its file; an * explicit `filePath` is used when present. Only the answer's own target (or, * for symbol lookups, each matched symbol) is considered — callers listed in * the answer are not "the queried symbol". * * @param data - A query operation's result payload. * @returns Repository-relative files of the queried symbol(s); empty when none are identifiable. */ export declare function querySymbolFiles(data: unknown): string[]; /** * Assess (and when small, repair) index freshness before a graph query. * * Never throws: a failed assessment is returned as `status: 'unknown'` with its * reason, so it neither blocks the query nor is ever mistaken for fresh. * * @param projectRoot - Project whose graph is about to be queried. * @param options - `refresh: false` (`--no-refresh`) skips any inline refresh. * @returns The assessment to judge the answer against. */ export declare function assessNexusFreshnessForQuery(projectRoot: string, options?: { refresh?: boolean; }): Promise; /** * Disclose a non-fresh index as an envelope warning * (`W_NEXUS_INDEX_STALE` / `W_NEXUS_INDEX_FRESHNESS_UNKNOWN`), and an inline * refresh that just happened as `W_NEXUS_INDEX_REFRESHED` (info). Warnings * reach `meta.warnings` through the active collector — never stderr, which * would break the one-envelope stream contract (T9775). * * @param subject - What answered, e.g. `nexus impact`. * @param freshness - Freshness of the index the answer came from. */ export declare function discloseNexusFreshness(subject: string, freshness: GraphIndexFreshness): void; /** * Merge a freshness verdict into envelope extensions' `_nexus` block. * @param extensions - Envelope extensions, possibly already carrying `_nexus`. * @param freshness - Freshness of the index the answer came from. * @returns New extensions with `_nexus.indexFreshness` and `_nexus.freshness` set. */ export declare function withNexusFreshnessMeta(extensions: Record, freshness: GraphIndexFreshness): Record; //# sourceMappingURL=freshness.d.ts.map