/** * Background index repair service (changes: add-zero-interaction-onboarding → * make-index-self-healing). * * Originally the cold-start self-bootstrap: if an agent wired the OpenLore MCP * server but never ran `openlore install`, the very first session had no index * and every tool returned "run analyze first." That warmed an ABSENT index once, * in the background. * * `make-index-self-healing` generalizes it: every read-path staleness signal that * today only produces a warning (integrity `mismatched`, an over-threshold stale * region, a schema reset, an aged analysis) now triggers the SAME at-most-once, * non-blocking background rebuild — so detection finally closes the loop into * repair instead of stopping at disclosure. * * Guarantees (unchanged from the bootstrap it grew out of): * - AT MOST ONCE per process per repo. A completed repair that still observes * its trigger discloses and stops — it never loops or thrashes. The guard is * cleared only on FAILURE, so a transient build error can retry. * - NEVER blocks the caller: the build runs detached from the call path; reads * during it are served from the stale index with an honest "refresh started" * disclosure, never held. * - NEVER throws: a build failure leaves the graceful guidance in place. * - Opt-out via `OPENLORE_NO_AUTO_ANALYZE` or `.openlore/config.json` `autoInit:false`. * * Deterministic, no LLM, no new dependency. */ import { spawn, type ChildProcess } from 'node:child_process'; /** * Why a background repair was started. `index-absent` is the original cold-start * case (no artifact at all); the rest are the self-healing triggers layered on by * make-index-self-healing. */ export type RepairReason = 'index-absent' | 'integrity-mismatched' | 'stale-region' | 'schema-reset' | 'analysis-age'; /** Human-facing label for each reason, used in the "refresh started" disclosure. */ export declare const REPAIR_REASON_DETAIL: Record; /** * How much work an auto-init build does. `full` is the ordinary lane; `degraded` * sheds the semantic-embedding pass on a tree above * {@link AUTO_INIT_DEGRADED_FILE_CEILING} files, leaving signatures + the keyword * (BM25) corpus — enough for `orient` to answer, without pinning a laptop on a * monorepo (change: unify-onboarding-entrypoint). */ export type RepairBuildMode = 'full' | 'degraded'; /** The index builder a host registers. `mode` is advisory: an older builder ignores it. */ export type RepairBuilder = (directory: string, opts?: { mode?: RepairBuildMode; }) => Promise; /** * A long-lived host's non-blocking handoff for files found stale by a cited-file * read check. Returning true means the host accepted the repair request (it may * coalesce it with work already queued); false means disclosure must remain * repair-agnostic. The callback must not await the repair itself. */ export type RepairHost = (staleFiles: readonly string[]) => boolean; /** * Register the repair path owned by one watcher/serve host. Registrations are * exact-root scoped: hosting repo A never authorizes repair work in repo B. * * The disposer is identity-safe. If a replacement host registers for the same * root before the old host tears down, the old disposer cannot remove the new * registration. */ export declare function registerRepairHost(directory: string, callback: RepairHost): () => void; /** * Offer cited stale files to the host for this exact repository. Returns true * only when a registered host accepted the request. Missing or throwing hosts * fail soft so a read can still serve its factual staleness disclosure. */ export declare function requestRepairFromHost(directory: string, staleFiles: readonly string[]): boolean; /** Register the process-wide repair builder (the MCP server injects install's forced buildIndex). */ export declare function registerRepairBuilder(fn: RepairBuilder): void; /** True once an `openlore analyze` artifact exists for the directory. */ export declare function hasAnalysis(directory: string): boolean; /** * Is `directory` inside a git work tree? * * A filesystem walk-up for a `.git` entry (a directory at a repository root, a * FILE in a linked worktree or submodule), not a `git rev-parse` shell-out: this * guard runs on the read path, must be synchronous, and must not spawn a process * per tool call. It is deliberately CONSERVATIVE — a path inside a `.git` * directory is rejected outright — because the cost of a false positive (indexing * a directory the user never asked about) is the exact harm the guard exists to * prevent (change: unify-onboarding-entrypoint). */ export declare function isInsideGitWorkTree(directory: string, home?: string): boolean; /** * Count files under `directory`, stopping as soon as `limit` is exceeded. * * Bounded by construction (both the count and a directory budget), so sizing a * 400,000-file monorepo costs the same as sizing a small one. The answer is only * ever used as "at or above the ceiling?", so an approximate count is sufficient * and an unreadable directory simply contributes nothing. */ export declare function countFilesBounded(directory: string, limit: number): number; /** * Why background auto-init is suppressed for `directory`, or undefined when it is * allowed to run. * * Read by the not-ready path so a repo that opted out is told WHY nothing is * building — an opted-out repo that merely says "no analysis found" reads as a * broken install (change: unify-onboarding-entrypoint). */ export declare function autoInitSuppression(directory: string): { reason: 'config' | 'env' | 'not-a-git-work-tree'; detail: string; } | undefined; export interface RepairOptions { /** * The index builder to run. Optional: when omitted, the process-wide builder * registered via {@link registerRepairBuilder} is used. Production registers * install's forced buildIndex (init + structural analyze + BM25 search corpus, * no API key) so `orient` heals to FULL parity, not just the structural graph. */ analyze?: RepairBuilder; /** Opt out entirely (env OPENLORE_NO_AUTO_ANALYZE, or a caller flag). */ disabled?: boolean; /** Status sink (defaults to process.stderr). Never stdout — that is protocol. */ log?: (msg: string) => void; /** Injected at-most-once guard set (tests). */ seen?: Set; /** Injectable clock (tests). Defaults to Date.now. */ now?: () => number; /** * File-count ceiling above which an auto-init build sheds the embedding pass. * Defaults to {@link AUTO_INIT_DEGRADED_FILE_CEILING}; a test seam, not a knob. */ degradeAboveFiles?: number; /** Injected tree sizer (tests). Defaults to the bounded filesystem count. */ countFiles?: (directory: string, limit: number) => number; } export interface ChildProcessBuildOptions { /** Rebuild even when the source fingerprint is unchanged. */ repair?: boolean; /** * `degraded` sheds the semantic-embedding pass (`analyze --no-embed`), leaving * signatures + the keyword (BM25) corpus. Defaults to `full`. */ mode?: RepairBuildMode; /** Test seam; production uses the current OpenLore CLI entry point. */ cliPath?: string; /** Test seam for observing the child-process boundary. */ spawnProcess?: typeof spawn; } /** Open a fresh MCP transport lifetime for child-process builds. */ export declare function enableChildProcessBuilds(): void; /** Test seam for the platform-specific process-tree termination contract. */ export declare function _terminateBuildChildForTesting(child: ChildProcess, signal: NodeJS.Signals, platform: NodeJS.Platform, spawnTreeKiller: typeof spawn): void; /** Terminate analyzer children when their owning MCP transport closes. */ export declare function stopChildProcessBuilds(graceMs?: number): Promise; /** * Build the complete first-use index outside the MCP server's event loop. * Initialization is also delegated when the repository has no config yet, so * the parent process performs no analyzer or install work. */ export declare function buildIndexInChildProcess(directory: string, opts?: ChildProcessBuildOptions): Promise; /** * Kick a one-time background repair for `directory` using the caller-supplied or * process-registered builder. Returns the in-flight build promise (so tests can * await it), or null when nothing was started (already repaired this process, * disabled, no builder available, or empty directory). NEVER throws, NEVER blocks. */ export declare function repairInBackground(directory: string, reason: RepairReason, opts?: RepairOptions): Promise | null; /** * The sentence that discloses an in-flight background repair to a caller. * * `index-absent` is the one reason with NO stale index behind it — there was * nothing to serve from — so it gets its own wording. Every other reason served * an existing (stale) index and says so. One helper, so the three response paths * that disclose a repair cannot drift apart (change: unify-onboarding-entrypoint). */ export declare function repairDisclosureText(reason: RepairReason): string; /** * Take this repository's undelivered first-touch notice, if any. * * Destructive by design: the notice is disclosed on exactly ONE response per repo * per process. Returns undefined when the repo has already been disclosed, or when * no auto-init ever started for it. */ export declare function takeFirstTouchNotice(directory: string): string | undefined; /** * The in-progress repair for `directory`, or undefined when none is running. The * read path threads this into the response so a stale answer is served with an * honest "background refresh started" marker — never presented as fresh. */ export declare function repairStatusFor(directory: string): { inProgress: true; reason: RepairReason; } | undefined; /** * Cold-start self-bootstrap for an ABSENT index — the original entry point, kept * as a thin wrapper over {@link repairInBackground} so existing callers/tests are * unchanged. Only fires when no analysis artifact exists yet. */ export declare function bootstrapAnalysisInBackground(directory: string, opts: RepairOptions & { analyze: RepairBuilder; }): Promise | null; /** Test-only: clear the process-wide repair guards and registered builder. */ export declare function _resetRepairServiceForTesting(): void; //# sourceMappingURL=cold-start-bootstrap.d.ts.map