import { spawn, type ChildProcessByStdio } from "node:child_process"; import { access, realpath, stat } from "node:fs/promises"; import { constants as fsConstants } from "node:fs"; import type { Readable } from "node:stream"; import * as path from "node:path"; import type { FileDiscoveryExecutionContextV1, FileDiscoveryFilterDecisionV1, FileDiscoveryFilterRootV1, FileDiscoveryFilterV1, FileDiscoveryQueryV1, FileDiscoveryRequestV1 } from "../contracts/v1/index.js"; import { assertFileDiscoveryExecutionContextV1, assertFileDiscoveryFilterResultV1, assertFileDiscoveryRequestV1 } from "../contracts/v1/index.js"; export type FileDiscoveryCellStatusV1 = "matched" | "no_matches" | "partial_limit" | "invalid_regex" | "timeout" | "error" | "not_run_global_limit" | "root_unavailable"; export interface FileDiscoveryMatchV1 { readonly path: string; readonly line: number; readonly column?: number; readonly text: string; } export interface FileDiscoveryCellV1 { readonly queryId: string; readonly root: string; readonly status: FileDiscoveryCellStatusV1; readonly matches: readonly FileDiscoveryMatchV1[]; readonly appliedIgnoreFiles: readonly string[]; readonly filterExclusions: readonly string[]; readonly filterDecision: FileDiscoveryFilterDecisionV1; readonly disclosures: readonly string[]; readonly stderr?: string; readonly exitCode?: number | null; } export interface EffectiveRootFilterV1 { readonly path: string; readonly displayPath: string; readonly ignoreFiles: readonly string[]; readonly exclusions: readonly string[]; readonly disclosures: readonly string[]; readonly explicitRoot: boolean; readonly filterBypassed: boolean; readonly filterDecision: FileDiscoveryFilterDecisionV1; readonly filterDecisionCodes: readonly string[]; } export interface FileDiscoveryProviderOutcomeV1 { readonly providerId: string; readonly outcome: string; readonly decision: FileDiscoveryFilterDecisionV1; readonly code?: string; } export interface FileDiscoveryFilterDecisionRecordV1 { readonly scope: "provider" | "root"; readonly decision: FileDiscoveryFilterDecisionV1; readonly target: string; readonly code?: string; readonly disclosures: readonly string[]; } export interface FileDiscoveryCandidateV1 { readonly path: string; readonly queryIds: readonly string[]; readonly roots: readonly string[]; readonly matchCount: number; readonly score: number; readonly excerpts: readonly FileDiscoveryMatchV1[]; } export interface FileDiscoveryCoverageV1 { readonly ranCellCount: number; readonly completeCellCount: number; readonly incompleteCellCount: number; readonly negativeEvidenceCellCount: number; } /** A requested in-workspace root that no longer exists; it is an incomplete scoped diagnostic, not a request-wide failure. */ export interface FileDiscoveryRootDiagnosticV1 { readonly requestedRoot: string; readonly path: string; readonly displayPath: string; readonly status: "root_unavailable"; readonly message: string; } export interface CoreFileDiscoveryResultV1 { readonly workspaceRoot: string; readonly requestedRoots: readonly string[]; readonly rootDiagnostics: readonly FileDiscoveryRootDiagnosticV1[]; readonly queries: readonly NormalizedQueryV1[]; readonly roots: readonly EffectiveRootFilterV1[]; readonly cells: readonly FileDiscoveryCellV1[]; readonly candidates: readonly FileDiscoveryCandidateV1[]; readonly coverage: FileDiscoveryCoverageV1; readonly completeness: "complete" | "partial" | "blocked"; readonly filters: Readonly>; readonly providerOutcomes: readonly FileDiscoveryProviderOutcomeV1[]; readonly filterDecisions: readonly FileDiscoveryFilterDecisionRecordV1[]; readonly retrySuggestions: readonly string[]; readonly outputMode: "compact" | "detailed"; } export type NormalizedQueryV1 = Readonly<{ id: string; pattern: string; mode: "literal" | "regex"; caseSensitive?: boolean }>; const DEFAULTS = Object.freeze({ maxMatches: 80, maxMatchesPerFile: 5, maxSnippetChars: 240, maxCandidates: 8, maxExcerptsPerCandidate: 2, timeoutSecondsPerSearch: 30 }); const HARD = Object.freeze({ maxMatches: 100, maxMatchesPerFile: 20, maxSnippetChars: 300 }); const MAX_STDERR_CHARS = 8_000; const MAX_PENDING_JSON_CHARS = 1_000_000; const MAX_IGNORE_FILE_BYTES = 1_000_000; const RIPGREP_OVERRIDE_ENV = "PI_FILE_DISCOVERY_RG_PATH"; export type ResolvedRipgrepExecutableV1 = Readonly<{ executable: string; env: NodeJS.ProcessEnv }>; export function stripLeadingAt(value: string): string { return value.trim().replace(/^@+/, ""); } export function normalizeDisplayPath(value: string): string { return value.replaceAll("\\", "/"); } export function isPathWithin(root: string, target: string, pathApi: Pick = path): boolean { const relative = pathApi.relative(root, target); return relative === "" || (!relative.startsWith(`..${pathApi.sep}`) && relative !== ".." && !pathApi.isAbsolute(relative)); } export function displayPathFromRoot(root: string, target: string): string { return normalizeDisplayPath(path.relative(root, target) || "."); } /** Resolve a deterministic absolute ripgrep executable for this bounded discovery call. */ export async function resolveRipgrepExecutableV1(cwd: string, environment: NodeJS.ProcessEnv = process.env): Promise { return await resolveConfiguredRipgrepExecutableV1(environment, await canonicalExisting(path.resolve(cwd), "discovery cwd")); } async function resolveConfiguredRipgrepExecutableV1(environment: NodeJS.ProcessEnv, selectedPhysicalScope: string): Promise { const override = environment.PI_FILE_DISCOVERY_RG_PATH?.trim(); if (override) { if (!path.isAbsolute(override)) throw new Error(`${RIPGREP_OVERRIDE_ENV} must be an absolute path to a regular executable.`); try { return Object.freeze({ executable: await validateExecutable(override), env: ripgrepSubprocessEnvV1(environment) }); } catch { throw new Error(`${RIPGREP_OVERRIDE_ENV} must resolve to a readable regular executable.`); } } for (const entry of environmentPath(environment).split(path.delimiter)) { const directory = entry.trim().replace(/^"|"$/g, ""); if (!directory || !path.isAbsolute(directory)) continue; const candidate = path.resolve(directory, process.platform === "win32" ? "rg.exe" : "rg"); try { const executable = await validateExecutable(candidate); if (isPathWithin(selectedPhysicalScope, executable)) continue; return Object.freeze({ executable, env: ripgrepSubprocessEnvV1(environment) }); } catch { /* Ignore unusable PATH entries and continue deterministic PATH order. */ } } throw new Error(`No usable ripgrep executable was found on PATH outside the selected discovery scope. Set ${RIPGREP_OVERRIDE_ENV} to an absolute executable path.`); } export function ripgrepSubprocessEnvV1(environment: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { const output: NodeJS.ProcessEnv = {}; if (process.platform === "win32") for (const key of ["SystemRoot", "WINDIR"]) { const value = environment[key] ?? environment[Object.keys(environment).find((name) => name.toLowerCase() === key.toLowerCase()) ?? ""]; if (value !== undefined) output[key] = value; } return Object.freeze(output); } async function validateExecutable(candidate: string): Promise { const physical = await realpath(candidate); const details = await stat(physical); await access(physical, process.platform === "win32" ? fsConstants.R_OK : fsConstants.R_OK | fsConstants.X_OK); if (!details.isFile()) throw new Error("not a regular executable"); return physical; } function environmentPath(environment: NodeJS.ProcessEnv): string { return environment.PATH ?? environment.Path ?? ""; } export function buildRipgrepArgs(input: { readonly root: string; readonly query: NormalizedQueryV1; readonly globs: readonly string[]; readonly ignoreFiles: readonly string[]; readonly exclusions: readonly string[]; readonly includeHidden: boolean; readonly maxMatchesPerFile: number }): string[] { const args = ["--json", "--color", "never", "--max-count", String(input.maxMatchesPerFile)]; if (input.query.mode === "literal") args.push("--fixed-strings"); if (input.query.caseSensitive === true) args.push("--case-sensitive"); else if (input.query.caseSensitive === false) args.push("--ignore-case"); else args.push("--smart-case"); if (input.includeHidden) args.push("--hidden"); for (const file of input.ignoreFiles) args.push("--ignore-file", file); for (const glob of [...input.globs, ...input.exclusions]) args.push("--glob", glob); args.push("-e", input.query.pattern, "--", input.root); return args; } export async function executeFileDiscoveryV1(context: FileDiscoveryExecutionContextV1, request: FileDiscoveryRequestV1, providers: readonly Readonly[]): Promise { assertFileDiscoveryExecutionContextV1(context); assertFileDiscoveryRequestV1(request); abortIfNeeded(context.signal); const selectedScopeLexical = path.resolve(context.cwd); const selectedScopePhysical = await canonicalExisting(selectedScopeLexical, "discovery cwd"); const ripgrep = await resolveConfiguredRipgrepExecutableV1(process.env, selectedScopePhysical); const requestedWorkspaceValue = stripLeadingAt(request.workspaceRoot ?? selectedScopeLexical); const workspaceRoot = await canonicalExisting(path.resolve(selectedScopeLexical, requestedWorkspaceValue), "workspaceRoot"); if (!isPathWithin(selectedScopePhysical, workspaceRoot)) throw new Error(`workspaceRoot is outside the selected discovery scope: ${request.workspaceRoot}`); const explicitRoots = request.roots !== undefined; const roots: string[] = []; const requestedRoots: string[] = []; const rootDiagnostics: FileDiscoveryRootDiagnosticV1[] = []; for (const raw of request.roots ?? [workspaceRoot]) { const requested = stripLeadingAt(raw); const lexicalRoot = path.resolve(workspaceRoot, requested); if (!isPathWithin(workspaceRoot, lexicalRoot)) throw new Error(`File discovery root is outside the selected workspace: ${raw}`); if (requestedRoots.some((candidate) => equalPath(candidate, lexicalRoot))) continue; requestedRoots.push(lexicalRoot); let root: string; try { root = await canonicalExisting(lexicalRoot, "root"); } catch (error) { if (!isNotFound(error)) throw error; rootDiagnostics.push(Object.freeze({ requestedRoot: raw, path: lexicalRoot, displayPath: displayPathFromRoot(workspaceRoot, lexicalRoot), status: "root_unavailable", message: "Requested root does not exist." })); continue; } if (!isPathWithin(workspaceRoot, root)) throw new Error(`File discovery root is outside the selected workspace after path resolution: ${raw}`); if (!roots.some((candidate) => equalPath(candidate, root))) roots.push(root); } const queries = normalizeQueries(request.queries); const timeoutMs = clamp(request.timeoutSecondsPerSearch, 1, 120, DEFAULTS.timeoutSecondsPerSearch) * 1000; const providerOutcomes: FileDiscoveryProviderOutcomeV1[] = []; const filterRoots = new Map(); if (request.filterMode !== "native-only") for (const provider of providers) { try { const result = await evaluateFilter(provider, context, { workspaceRoot, roots, includeHidden: Boolean(request.includeHidden) }, timeoutMs); assertFileDiscoveryFilterResultV1(result); providerOutcomes.push({ providerId: provider.id, outcome: result.outcome, decision: filterOutcomeDecision(result.outcome), ...("code" in result ? { code: result.code } : {}) }); if (result.outcome === "applied") for (const root of await canonicalizeFilterRoots(result.roots, roots)) { const key = comparable(root.root); filterRoots.set(key, [...(filterRoots.get(key) ?? []), root]); } } catch (error) { if (isAbort(error)) throw error; providerOutcomes.push({ providerId: provider.id, outcome: "error", decision: "degraded", code: error instanceof FilterTimeoutError ? "filter_timeout" : "filter_malformed_or_threw" }); } } else for (const provider of providers) providerOutcomes.push({ providerId: provider.id, outcome: "skipped", decision: "skipped", code: "native_only" }); const effectiveRoots = roots.map((root) => composeRootFilter(workspaceRoot, root, filterRoots.get(comparable(root)) ?? [], explicitRoots, request.filterMode === "native-only", providerOutcomes.some((outcome) => outcome.decision === "degraded"))); const maxMatches = clamp(request.maxMatches, 1, HARD.maxMatches, DEFAULTS.maxMatches); const maxPerFile = clamp(request.maxMatchesPerFile, 1, HARD.maxMatchesPerFile, DEFAULTS.maxMatchesPerFile); const maxSnippetChars = clamp(request.maxSnippetChars, 80, HARD.maxSnippetChars, DEFAULTS.maxSnippetChars); const globs = request.globs ?? []; const cells: FileDiscoveryCellV1[] = []; let remaining = maxMatches; const totalCells = queries.length * effectiveRoots.length; let nextCell = 0; for (const query of queries) for (const root of effectiveRoots) { abortIfNeeded(context.signal); const cellsLeft = totalCells - nextCell++; if (!remaining) { cells.push(emptyCell(query.id, root, "not_run_global_limit")); continue; } const allowance = Math.max(1, Math.min(remaining, Math.ceil(remaining / cellsLeft))); const cell = await runRipgrepCellV1({ executable: ripgrep.executable, env: ripgrep.env, cwd: workspaceRoot, root, query, globs, includeHidden: Boolean(request.includeHidden), maxMatches: allowance, maxMatchesPerFile: maxPerFile, maxSnippetChars, timeoutMs, signal: context.signal }); cells.push(cell); remaining -= cell.matches.length; } for (const diagnostic of rootDiagnostics) for (const query of queries) cells.push(unavailableRootCell(query.id, diagnostic)); const { coverage, completeness } = summarizeFileDiscoveryCompletenessV1(cells); const candidates = synthesizeCandidates(cells, request.maxCandidates ?? DEFAULTS.maxCandidates, request.maxExcerptsPerCandidate ?? DEFAULTS.maxExcerptsPerCandidate); const filterDecisions = Object.freeze([... providerOutcomes.map((outcome) => Object.freeze({ scope: "provider" as const, decision: outcome.decision, target: outcome.providerId, ...(outcome.code === undefined ? {} : { code: outcome.code }), disclosures: Object.freeze([]) })), ...effectiveRoots.map((root) => Object.freeze({ scope: "root" as const, decision: root.filterDecision, target: root.displayPath, disclosures: root.disclosures })), ]); const degraded = providerOutcomes.filter((entry) => entry.decision === "degraded"); const retrySuggestions: string[] = []; if (rootDiagnostics.length) retrySuggestions.push("Remove or correct unavailable requested roots, then rerun those scoped cells."); if (cells.some((cell) => cell.status === "partial_limit" || cell.status === "not_run_global_limit")) retrySuggestions.push("Narrow roots or hypotheses and rerun the incomplete cells."); if (cells.some((cell) => cell.status === "invalid_regex")) retrySuggestions.push("Correct the regex pattern or use mode: 'literal'."); if (!candidates.length && coverage.negativeEvidenceCellCount === 0) retrySuggestions.push("No completed absence claim is available; resolve incomplete cells first."); return Object.freeze({ workspaceRoot, requestedRoots: Object.freeze(requestedRoots), rootDiagnostics: Object.freeze(rootDiagnostics), queries, roots: effectiveRoots, cells: Object.freeze(cells), candidates, coverage, completeness, filters: Object.freeze({ mode: request.filterMode ?? "recommended", filteringDegraded: degraded.length > 0, explicitRootOverride: effectiveRoots.some((root) => root.filterBypassed), decisions: filterDecisions, nativeIgnore: true, followsSymlinks: false, binaryFiles: "skipped", requested: { maxMatches: request.maxMatches, maxMatchesPerFile: request.maxMatchesPerFile, maxSnippetChars: request.maxSnippetChars }, effective: { maxMatches, maxMatchesPerFile: maxPerFile, maxSnippetChars } }), providerOutcomes: Object.freeze(providerOutcomes), filterDecisions, retrySuggestions: Object.freeze(retrySuggestions), outputMode: request.outputMode ?? "compact" }); } function normalizeQueries(values: readonly FileDiscoveryQueryV1[]): NormalizedQueryV1[] { const ids = new Set(); return values.map((query, index) => { const pattern = query.pattern; if (!pattern.trim() || /[\0\r\n]/.test(pattern)) throw new Error(`queries[${index}].pattern must be non-empty and contain no newline/NUL`); const id = (query.id?.trim() || `q${index + 1}`).replace(/[^A-Za-z0-9_.-]+/g, "-"); if (!id || ids.has(id)) throw new Error(`Query ids must be unique: ${id || "(empty)"}`); ids.add(id); return Object.freeze({ id, pattern, mode: query.mode, ...(query.caseSensitive === undefined ? {} : { caseSensitive: query.caseSensitive }) }); }); } function composeRootFilter(workspaceRoot: string, root: string, entries: readonly FileDiscoveryFilterRootV1[], explicitRoot: boolean, nativeOnly: boolean, providerDegraded: boolean): EffectiveRootFilterV1 { const files = new Set(); const exclusions = new Set(); const disclosures = new Set(); const decisionCodes = new Set(); for (const entry of entries) { for (const file of entry.ignoreFiles ?? []) files.add(file); for (const glob of entry.excludeGlobs ?? []) exclusions.add(glob); for (const disclosure of entry.disclosures) disclosures.add(disclosure); if (entry.decisionCode) decisionCodes.add(entry.decisionCode); } const declaredBypass = entries.some((entry) => entry.filterDecision === "bypassed"); const bypass = explicitRoot && declaredBypass; if (bypass) disclosures.add("Exact requested generated/cache root bypassed recommended filtering."); const decision: FileDiscoveryFilterDecisionV1 = bypass ? "bypassed" : nativeOnly ? "skipped" : entries.length ? "applied" : providerDegraded ? "degraded" : "skipped"; return Object.freeze({ path: root, displayPath: displayPathFromRoot(workspaceRoot, root), ignoreFiles: Object.freeze([...files]), exclusions: Object.freeze(bypass ? [] : [...exclusions]), disclosures: Object.freeze([...disclosures]), explicitRoot, filterBypassed: bypass, filterDecision: decision, filterDecisionCodes: Object.freeze([...decisionCodes]) }); } function filterOutcomeDecision(outcome: import("../contracts/v1/index.js").FileDiscoveryFilterResultV1["outcome"]): FileDiscoveryFilterDecisionV1 { return outcome === "applied" ? "applied" : outcome === "not_applicable" ? "skipped" : "degraded"; } function synthesizeCandidates(cells: readonly FileDiscoveryCellV1[], maxCandidates: number, excerptsPerCandidate: number): readonly FileDiscoveryCandidateV1[] { const records = new Map; roots: Set; matches: FileDiscoveryMatchV1[] }>(); for (const cell of cells) for (const match of cell.matches) { const item = records.get(match.path) ?? { queries: new Set(), roots: new Set(), matches: [] }; item.queries.add(cell.queryId); item.roots.add(normalizeDisplayPath(cell.root)); item.matches.push(match); records.set(match.path, item); } return Object.freeze([...records.entries()].map(([candidatePath, item]) => { const queries = [...item.queries].sort(); const roots = [...item.roots].sort(); const matchCount = item.matches.length; const score = queries.length * 1000 + roots.length * 100 + Math.min(matchCount, 99); return Object.freeze({ path: candidatePath, queryIds: Object.freeze(queries), roots: Object.freeze(roots), matchCount, score, excerpts: Object.freeze(item.matches.sort(compareMatch).slice(0, excerptsPerCandidate)) }); }).sort((a, b) => b.score - a.score || a.path.localeCompare(b.path)).slice(0, Math.min(maxCandidates, 20))); } function compareMatch(a: FileDiscoveryMatchV1, b: FileDiscoveryMatchV1): number { return a.path.localeCompare(b.path) || a.line - b.line || (a.column ?? 0) - (b.column ?? 0); } /** Summarize which cells ran and whether their evidence is complete. */ export function summarizeFileDiscoveryCompletenessV1(cells: readonly FileDiscoveryCellV1[]): Readonly<{ coverage: FileDiscoveryCoverageV1; completeness: "complete" | "partial" | "blocked" }> { const complete = cells.filter((cell) => cell.status === "matched" || cell.status === "no_matches"); const coverage = Object.freeze({ ranCellCount: cells.filter((cell) => cell.status !== "not_run_global_limit" && cell.status !== "root_unavailable").length, completeCellCount: complete.length, incompleteCellCount: cells.length - complete.length, negativeEvidenceCellCount: cells.filter((cell) => cell.status === "no_matches").length }); return Object.freeze({ coverage, completeness: coverage.ranCellCount === 0 ? "blocked" : coverage.incompleteCellCount ? "partial" : "complete" }); } function emptyCell(queryId: string, root: EffectiveRootFilterV1, status: FileDiscoveryCellStatusV1): FileDiscoveryCellV1 { return Object.freeze({ queryId, root: root.path, status, matches: Object.freeze([]), appliedIgnoreFiles: root.ignoreFiles, filterExclusions: root.exclusions, filterDecision: root.filterDecision, disclosures: root.disclosures }); } function unavailableRootCell(queryId: string, diagnostic: FileDiscoveryRootDiagnosticV1): FileDiscoveryCellV1 { return Object.freeze({ queryId, root: diagnostic.path, status: "root_unavailable", matches: Object.freeze([]), appliedIgnoreFiles: Object.freeze([]), filterExclusions: Object.freeze([]), filterDecision: "skipped", disclosures: Object.freeze([diagnostic.message]), stderr: diagnostic.message }); } function clamp(value: number | undefined, min: number, max: number, fallback: number): number { return Math.min(max, Math.max(min, value ?? fallback)); } function comparable(value: string): string { return process.platform === "win32" ? value.toLowerCase() : value; } function equalPath(a: string, b: string): boolean { return comparable(a) === comparable(b); } async function canonicalExisting(candidate: string, label: string): Promise { try { return await realpath(candidate); } catch (error) { const wrapped = new Error(`${label} does not exist: ${candidate}`, { cause: error }); Object.assign(wrapped, { code: (error as NodeJS.ErrnoException).code }); throw wrapped; } } function isNotFound(error: unknown): boolean { return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT"; } class FilterTimeoutError extends Error {} async function evaluateFilter(provider: Readonly, context: FileDiscoveryExecutionContextV1, request: Omit, timeoutMs: number): Promise { abortIfNeeded(context.signal); const controller = new AbortController(); let timer: ReturnType | undefined; let rejectAbort!: (error: Error) => void; const onAbort = () => { controller.abort(); rejectAbort(abortError()); }; const timed = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new FilterTimeoutError()); }, timeoutMs); }); const aborted = new Promise((_, reject) => { rejectAbort = reject; context.signal.addEventListener("abort", onAbort, { once: true }); }); try { return await Promise.race([provider.evaluate(Object.freeze({ ...context, signal: controller.signal }), Object.freeze({ ...request, signal: controller.signal })), timed, aborted]); } finally { if (timer !== undefined) clearTimeout(timer); context.signal.removeEventListener("abort", onAbort); controller.abort(); } } async function canonicalizeFilterRoots(entries: readonly FileDiscoveryFilterRootV1[], requestedRoots: readonly string[]): Promise { const output: FileDiscoveryFilterRootV1[] = []; for (const entry of entries) { const root = await canonicalExisting(entry.root, "filter root"); if (!requestedRoots.some((requested) => equalPath(requested, root))) throw new Error("filter root does not match a selected root"); let boundary: string | undefined; if (entry.filterBoundary !== undefined) boundary = await canonicalExisting(entry.filterBoundary, "filter boundary"); const files: string[] = []; for (const input of entry.ignoreFiles ?? []) { if (!boundary) throw new Error("filter boundary missing"); const file = await canonicalExisting(input, "filter ignore file"); const info = await stat(file); await access(file, fsConstants.R_OK); if (!isPathWithin(boundary, file) || !info.isFile() || info.size > MAX_IGNORE_FILE_BYTES) throw new Error("filter ignore file must be a readable bounded regular file within its filter boundary"); files.push(file); } output.push(Object.freeze({ ...entry, root, ...(boundary === undefined ? {} : { filterBoundary: boundary }), ...(entry.ignoreFiles === undefined ? {} : { ignoreFiles: Object.freeze(files) }) })); } return output; } function abortIfNeeded(signal: AbortSignal): void { if (signal.aborted) throw abortError(); } function abortError(): Error { const error = new Error("File discovery cancelled."); error.name = "AbortError"; return error; } function isAbort(error: unknown): boolean { return error instanceof Error && error.name === "AbortError"; } function truncate(value: string, max: number): string { const line = value.replace(/\r?\n/g, " ").trim(); return line.length <= max ? line : `${line.slice(0, Math.max(0, max - 1))}…`; } function classify(stderr: string): FileDiscoveryCellStatusV1 { return /regex parse error|invalid regex|error parsing regex/i.test(stderr) ? "invalid_regex" : "error"; } async function runRipgrepCellV1(input: { readonly executable: string; readonly env: NodeJS.ProcessEnv; readonly cwd: string; readonly root: EffectiveRootFilterV1; readonly query: NormalizedQueryV1; readonly globs: readonly string[]; readonly includeHidden: boolean; readonly maxMatches: number; readonly maxMatchesPerFile: number; readonly maxSnippetChars: number; readonly timeoutMs: number; readonly signal: AbortSignal }): Promise { const args = buildRipgrepArgs({ root: input.root.path, query: input.query, globs: input.globs, ignoreFiles: input.root.ignoreFiles, exclusions: input.root.exclusions, includeHidden: input.includeHidden, maxMatchesPerFile: input.maxMatchesPerFile }); return await new Promise((resolve, reject) => { let child: ChildProcessByStdio; try { child = spawn(input.executable, args, { cwd: input.cwd, env: input.env, shell: false, windowsHide: true, stdio: ["ignore", "pipe", "pipe"] }); } catch (error) { reject(error); return; } const matches: FileDiscoveryMatchV1[] = []; let pending = "", stderr = "", limited = false, timedOut = false, aborted = false, settled = false; const kill = () => { if (!child.killed) child.kill(); }; const timer = setTimeout(() => { timedOut = true; kill(); }, input.timeoutMs); const onAbort = () => { aborted = true; kill(); }; input.signal.addEventListener("abort", onAbort, { once: true }); const clean = () => { clearTimeout(timer); input.signal.removeEventListener("abort", onAbort); }; const consume = (line: string) => { if (!line.trim() || limited) return; try { const event = JSON.parse(line); if (event?.type !== "match") return; const raw = event.data?.path?.text, lineNumber = event.data?.line_number, text = event.data?.lines?.text; if (typeof raw !== "string" || typeof lineNumber !== "number" || typeof text !== "string") return; const absolute = path.isAbsolute(raw) ? raw : path.resolve(input.cwd, raw); const submatch = event.data?.submatches?.[0]; matches.push(Object.freeze({ path: displayPathFromRoot(input.cwd, absolute), line: lineNumber, ...(typeof submatch?.start === "number" ? { column: submatch.start + 1 } : {}), text: truncate(text, input.maxSnippetChars) })); if (matches.length >= input.maxMatches) { limited = true; kill(); } } catch { /* malformed JSON is bounded process noise */ } }; child.stdout.setEncoding("utf8"); child.stdout.on("data", (chunk: string) => { pending += chunk; if (pending.length > MAX_PENDING_JSON_CHARS && !pending.includes("\n")) { stderr = "ripgrep emitted oversized JSON"; kill(); return; } const lines = pending.split(/\r?\n/); pending = lines.pop() ?? ""; lines.forEach(consume); }); child.stderr.setEncoding("utf8"); child.stderr.on("data", (chunk: string) => { if (stderr.length < MAX_STDERR_CHARS) stderr += chunk.slice(0, MAX_STDERR_CHARS - stderr.length); }); child.on("error", reject); child.on("close", (exitCode) => { if (settled) return; settled = true; clean(); if (pending) consume(pending); if (aborted) return reject(abortError()); const status = timedOut ? "timeout" : limited ? "partial_limit" : exitCode === 0 ? (matches.length ? "matched" : "no_matches") : exitCode === 1 ? "no_matches" : classify(stderr); resolve(Object.freeze({ queryId: input.query.id, root: input.root.path, status, matches: Object.freeze(matches), appliedIgnoreFiles: input.root.ignoreFiles, filterExclusions: input.root.exclusions, filterDecision: input.root.filterDecision, disclosures: input.root.disclosures, ...(stderr.trim() ? { stderr: stderr.trim() } : {}), exitCode })); }); }); } /** Compact reports prioritize ranked candidates; detailed mode adds bounded cell diagnostics. */ export function formatFileDiscoveryReportV1(result: CoreFileDiscoveryResultV1): string { const lines = ["# Candidate File Discovery", `- Workspace: ${result.workspaceRoot}`, `- Roots: ${result.requestedRoots.map((root) => displayPathFromRoot(result.workspaceRoot, root)).join(", ") || "(none available)"}`, `- Status: ${result.completeness} (${result.coverage.completeCellCount}/${result.cells.length} complete cells)`, `- Query modes: ${result.queries.map((query) => `${query.id}=${query.mode}`).join(", ")}`]; const filters = result.filters as { filteringDegraded?: boolean; mode?: string; explicitRootOverride?: boolean; decisions?: readonly FileDiscoveryFilterDecisionRecordV1[]; registrationDegradation?: { outcome: string; code: string } }; const filterDecisions = filters.decisions ?? result.filterDecisions; if (filters.filteringDegraded || filters.mode === "native-only" || filters.explicitRootOverride || filters.registrationDegradation || filterDecisions.some((entry) => entry.decision !== "skipped")) { const summary = filterDecisions.filter((entry) => entry.scope === "root" && entry.decision !== "skipped").map((entry) => `${entry.target}: ${entry.decision}`).join(", "); lines.push(`- Filtering: ${filters.mode}${filters.filteringDegraded ? "; degraded provider filtering" : ""}${filters.registrationDegradation ? "; degraded filter registration" : ""}${summary ? `; ${summary}` : ""}`); for (const entry of filterDecisions.filter((item) => item.scope === "root" && item.disclosures.length)) for (const disclosure of entry.disclosures) lines.push(` - ${disclosure}`); } lines.push("", "## Candidate files"); if (result.candidates.length) for (const candidate of result.candidates) { lines.push(`- ${candidate.path} — hypotheses: ${candidate.queryIds.join(", ")}; matches: ${candidate.matchCount}`); for (const excerpt of candidate.excerpts) lines.push(` - ${excerpt.line}${excerpt.column === undefined ? "" : `:${excerpt.column}`} — ${excerpt.text}`); } else lines.push("- (none)"); if (result.rootDiagnostics.length) lines.push("", "## Unavailable requested roots", ...result.rootDiagnostics.map((diagnostic) => `- ${diagnostic.displayPath}: ${diagnostic.message}`)); const incomplete = result.cells.filter((cell) => cell.status !== "matched" && cell.status !== "no_matches"); if (incomplete.length) lines.push("", "## Incomplete hypotheses", ...incomplete.map((cell) => `- ${cell.queryId} / ${displayPathFromRoot(result.workspaceRoot, cell.root)}: ${cell.status}`)); if (result.candidates[0]) lines.push("", `Next: read ${result.candidates[0].path} around line ${result.candidates[0].excerpts[0]?.line ?? 1}.`); else if (result.coverage.negativeEvidenceCellCount) lines.push("", "No matches were found only in the completed searched cells; filters may have excluded descendants."); if (result.retrySuggestions.length) lines.push("", ...result.retrySuggestions.map((suggestion) => `Retry: ${suggestion}`)); if (result.outputMode === "detailed") { lines.push("", "## Bounded status matrix", ...result.cells.map((cell) => `- ${cell.queryId} / ${displayPathFromRoot(result.workspaceRoot, cell.root)}: ${cell.status} (${cell.matches.length} matches)`)); } const output = lines.join("\n"); if (output.length <= 8_000) return output; const suffix = "\n[Compact report truncated at 8,000 characters; use narrower roots.]"; return `${output.slice(0, 8_000 - suffix.length)}${suffix}`; }