import type { ExecutionEnv } from "../../internal/harness-types.js";
import { type HomeViewsCarrier } from "./read-deny-home.js";
/** The JS scanner's own output ceiling, in bytes of emitted content rows. Pinned to the
 *  ripgrep leg's figure ({@link MAX_EXEC_OUTPUT_BYTES}, the exec pipe's rolling-tail bound) so the two
 *  legs cannot disagree about how much one search may return; see the collection site in {@link jsGrep}
 *  for why this leg bounds the HEAD where the pipe bounds the tail. */
export declare const JS_GREP_OUTPUT_MAX_BYTES: number;
/** Directories never worth crawling — dependency/build/cache trees, PLUS the six VCS metadata
 *  directories CC names explicitly (git/svn/mercurial/bazaar/jujutsu/sapling). See {@link buildIgnore}'s
 *  own comment for why this list is the ENTIRE directory-skip story: there is deliberately no blanket
 *  "any dot-prefixed name is invisible" rule beside it. */
export declare const DEFAULT_IGNORE_DIRS: ReadonlySet<string>;
export interface GrepParams {
    /** Regular expression (ripgrep semantics) — CC 2.1.209 parity: the pattern is ALWAYS a regex; the
     *  old `regex?: boolean` literal-by-default switch is retired (a legacy `regex` argument sent by an
     *  old caller is tolerated and ignored at the tool layer, never a failure). */
    pattern: string;
    /** Restrict the search to this sub-path (relative to root, or absolute within root). */
    path?: string;
    /** Only search files matching this glob (e.g. `*.ts`, `*.{ts,tsx}`). CC parity: a
     *  comma/whitespace-separated list (`"*.ts,*.tsx"`) is split into multiple globs (brace globs kept whole). */
    glob?: string;
    /** CC `-o`: print only the matched (non-empty) parts of each matching line, one match per output
     *  line (rg -o / --only-matching). content mode only; ignored otherwise (CC parity). With
     *  `multiline` the JS fallback emits the matched SEGMENT's lines (rg -U -o shape). */
    only_matching?: boolean;
    /** content: `path:line:text`. files_with_matches (default, CC 2.1.209): `path`. count: `path:count`. */
    output_mode?: "content" | "files_with_matches" | "count";
    /** Lines of context around a match (content mode only) — CC's `-C`. */
    context?: number;
    /** Lines of context BEFORE each match (content mode) — CC's `-B`. Wins over `context` for the before-window. */
    context_before?: number;
    /** Lines of context AFTER each match (content mode) — CC's `-A`. Wins over `context` for the after-window. */
    context_after?: number;
    /** File type filter — CC's `type` (rg `--type`): "js", "py", "rust", "go", "ts", … The rg path uses rg's
     *  built-in type table; the JS fallback maps common types to globs (unknown types are ignored with a note). */
    type?: string;
    /** Case-insensitive match — CC's `-i`. */
    ignore_case?: boolean;
    /** Match across line boundaries — CC's multiline `-U`. JS fallback approximates per-file. */
    multiline?: boolean;
    /** Cap the number of output items (default 250) — CC's `head_limit`. */
    head_limit?: number;
    /** Skip the first N output lines — CC's `offset` (pagination partner of head_limit). */
    offset?: number;
}
/** CC 2.1.198 Grep parity: split the `glob` param into individual glob
 *  tokens — whitespace-separated first; a token WITHOUT braces is further split on commas
 *  (`"*.ts,*.tsx"` → two globs), while a brace token (`*.{ts,tsx}`) is kept whole (its commas are
 *  alternation syntax, not separators). CC-verbatim splitting rules. */
export declare function splitGlobParam(glob: string): string[];
/**
 * POSIX-safe single-quote so an arbitrary model-supplied string can't break out of the shell command that
 * runs ripgrep. Wraps in single quotes (inside which the shell expands nothing — no `$()`, backtick, `;`, or
 * glob) and escapes any embedded single quote via the `'\''` idiom (close, escaped-quote, reopen).
 * Exported for direct injection testing.
 */
export declare function shellQuote(s: string): string;
/**
 * The tokens of a `glob` argument that this engine CANNOT compile as glob syntax, i.e. the ones whose
 * metacharacter would silently degrade to a literal match.
 *
 * The harm this closes is specific and asymmetric. An unknown `type` degrades to searching EVERY file —
 * a SUPERSET of what was asked, wrong but self-evidently over-broad, and sema already discloses it with
 * a `[note: unknown type …]` line. An unparseable `glob` degrades the other way: `src/[ab.ts` stops
 * being a character class and becomes a request for a file literally NAMED `src/[ab.ts`, so the filter
 * silently narrows to (almost always) nothing and the search answers a clean "No matches." — a false
 * negative wearing the exact shape of a successful exhaustive search. This repo's standing verdict on
 * that class is: no real data ⇒ isError, never a degraded answer.
 *
 * CC hard-errors on the same input class rather than searching at all — its ripgrep stderr matcher
 * covers `error parsing glob` (alongside regex-parse / unrecognized-file-type / flag-parse errors) and
 * raises `Search failed — ripgrep rejected the pattern, glob, or file type without searching`. sema
 * rejects at the INPUT level instead of waiting for a stderr string, because sema has
 * two search engines (rg + the JS fallback) and only the rg one would ever produce that stderr: a
 * rejection that fires only when ripgrep happens to be installed is not a contract. The `type` leg
 * deliberately does NOT get this treatment (see the `typeNote` site) — over-broad-with-a-note is an
 * honest answer, silently-narrowed is not.
 */
export declare function invalidGlobTokens(glob: string): string[];
/** Lightweight `.gitignore` matcher (handles comments, negation, dir-only `/`, anchoring, `*`/`?`). */
export declare function gitignoreMatcher(content: string): (rel: string, isDir: boolean) => boolean;
/** Build the combined ignore predicate (ignore-set ∪ .gitignore) for a JS-fallback walk.
 *  Exported so repo-map walks with the SAME ignore semantics as grep/glob.
 *
 * CC's "keep it quiet" posture is NOT "every dot-prefixed name is invisible" — it is `--hidden` (show
 * everything dot-prefixed) plus an explicit VCS-directory exclusion list ({@link DEFAULT_IGNORE_DIRS}).
 * A blanket `base.startsWith(".")` rule here would make EVERY hidden file/dir invisible with no caveat
 * — `.github/`, `.claude/`, `.env.example` would not exist as far as Grep/Glob/repo-map are concerned,
 * not even as an honest "search skipped this" note, and an unflagged gap reads as exhaustive. So
 * {@link DEFAULT_IGNORE_DIRS} (dependency/build/cache trees + the 6 VCS metadata dirs) and `.gitignore`
 * are the entire directory-skip story, matching CC. */
export declare function buildIgnore(env: ExecutionEnv, root: string, signal?: AbortSignal): Promise<(rel: string, isDir: boolean) => boolean>;
/** What a bounded walk actually saw — so consumers can be HONEST about incompleteness: silently
 *  bounded results must never read as exhaustive. */
export interface WalkResult {
    /** Greppable text files (binary-extension and oversize files excluded). */
    files: string[];
    /** Name-only entries: binary-extension + over-{@link FILE_MAX_BYTES} files. Content search skips them
     *  (grep semantics / memory bound), but globbing matches NAMES — these must still be visible to it
     *  (previously `glob *.png` could never match anything). */
    nameOnly: string[];
    /** True when the walk hit its file-count or depth ceiling — results MAY be missing files. (Ceiling
     *  ONLY; unreadable-dir and abort incompleteness live on their own fields below — use
     *  {@link walkIsPartial} for the any-cause check.) */
    incomplete: boolean;
    /** Non-binary files skipped by content search for being over {@link FILE_MAX_BYTES} (also in
     *  `nameOnly`). Surfaced by grep so "No matches." can't silently mean "didn't look". */
    skippedLarge: number;
    /** Directories whose listing FAILED (EACCES, transient I/O) — whole subtrees the walk never saw. */
    skippedUnreadable: number;
    /** The walk was cut short by an abort — the result is a prefix, not a survey. */
    aborted: boolean;
    /** CC 2.1.209 Glob parity: modification time (ms epoch) per walked path (files AND
     *  name-only entries), captured from the listing that discovered them — so glob can sort by mtime
     *  without a second stat pass. An env whose listings carry no numeric `mtimeMs` simply leaves
     *  entries out of this map (consumers fall back to name order). */
    mtimes: Map<string, number>;
    /** Entries withheld by the sensitive-path read deny list: PRUNE-ROOT count only
     *  (a denied directory's descendants are not enumerated, so they cannot be counted — the note says
     *  so). 0 when no deny judge was supplied or nothing matched. */
    denyPruned: number;
    /** The deny patterns that pruned something, first-seen order (for the disclosure note). */
    denyPatterns: string[];
}
/** Any-cause incompleteness — a result built on this walk must not present itself as exhaustive. */
export declare function walkIsPartial(w: WalkResult): boolean;
/** Bounded, ignore-aware recursive walk. Returns absolute file paths under `start`. Exported for
 *  repo-map — same bounded/ignore-aware traversal as grep/glob, one source of truth. */
export declare function walk(env: ExecutionEnv, root: string, start: string, ignore: (rel: string, isDir: boolean) => boolean, signal?: AbortSignal, deny?: {
    matchPath(path: string): {
        pattern: string;
    } | null;
} & HomeViewsCarrier): Promise<WalkResult>;
/** The JS-leg withholding disclosure: an empty result must stay distinguishable from
 *  searched-and-empty, so this note rides EVERY output shape when something was pruned. Distinct from
 *  {@link walkCaveat}: pruning is deliberate withholding, not incompleteness.
 *  The `[note:` prefix keeps it out of the tool layer's structured row extraction. */
export declare function denyWithheldNote(w: Pick<WalkResult, "denyPruned" | "denyPatterns">): string;
/** Injectable execution boundaries for the JS-fallback scan (tests shrink them; callers use defaults). */
export interface JsGrepGuards {
    /** Wall-clock budget (ms) for matching a SINGLE file before the scan stops with a diagnosable error. */
    budgetMs?: number;
    /** Longest line/content (chars) a gray-zone pattern may be matched against. */
    longLineLimit?: number;
    /** Bytes of content rows this scan may accumulate before it stops collecting and
     *  says so (default {@link JS_GREP_OUTPUT_MAX_BYTES}). A seam for the same reason the two above
     *  are: the boundary is pinnable at a testable size instead of only at an 8MB harness. */
    outputMaxBytes?: number;
}
/** What a traversal withheld under the sensitive-path read deny list, structured (the prose note is
 *  the model-facing twin). Three shapes: `pruned_count` = the JS walker
 *  counted its prune roots; `existence` = the rg leg's bounded probe CONFIRMED matching entries exist
 *  under the scope (rg's own exclusions are silent, so the count is unknowable); `probe_failed` =
 *  the probe itself failed — entries MAY have been withheld. */
export interface ReadDenyWithheld {
    kind: "pruned_count" | "existence" | "probe_failed";
    count?: number;
    patterns?: readonly string[];
}
/** The deny judge seam every search leg shares (structural subset of ReadDenyMatcher), with its placement's home views. */
export interface ReadDenyJudge extends HomeViewsCarrier {
    matchPath(path: string): {
        pattern: string;
    } | null;
    rgExclusionGlobs: ReadonlyArray<{
        flag: "--glob" | "--iglob";
        glob: string;
    }>;
    rgProbeGlobs: ReadonlyArray<{
        flag: "--glob" | "--iglob";
        glob: string;
    }>;
    /** read-deny.ts `ReadDenyMatcher.unexcludableUnder`: a scope where an entry the rg globs cannot exclude may lie. */
    unexcludableUnder?(scope: string): boolean;
}
/** JS-fallback grep: ignore-aware walk + per-line scan, honoring output_mode / context / head_limit.
 *  `deny` prunes the walk (note appended to every output shape); `denyOut` is the
 *  structured twin — a sink because this function's dozen error returns predate the facts (the sink
 *  is written the moment the walk lands, whatever the scan then returns).
 *
 *  The SERVED rows ride back next to the text. This scanner knows every result's
 *  identity natively (the walk hands it whole paths), so a row's `path` is that walked identity,
 *  never a cut of the rendered line — the mis-split family the text re-parse carries cannot exist
 *  here. The rows ARE the window `text` shows: post offset, post row cap, post byte ceiling, in the
 *  same order. The unit is the RECORD, which is the unit the cap has always counted: one record is
 *  one rendered line EXCEPT where the file's own name carries a newline, and then the one record
 *  spans two physical lines — the same relation {@link formatRgRecords} states for the rg leg, and
 *  precisely the case where a row's identity beats anything the text can be split into.
 *  Every row carries a path (this leg emits no group-separator row — the one pathless shape
 *  ripgrep's output can carry), and a no-match run's row set is `[]`, not absent. A typed
 *  `Error (grep): …` return carries no rows at all: a refusal is not a row set. */
export declare function jsGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
    withheld?: ReadDenyWithheld;
}): Promise<{
    text: string;
    rows?: readonly GrepRow[];
}>;
/** Text-only wrapper of {@link jsGrepDetailed} (the shape callers that never look at the served rows
 *  want — the same relation {@link runGrep} has to {@link runGrepDetailed}). */
export declare function jsGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, guards?: JsGrepGuards, deny?: ReadDenyJudge, denyOut?: {
    withheld?: ReadDenyWithheld;
}): Promise<string>;
/** Detect ripgrep once per env (cached). */
export declare function detectRipgrep(env: ExecutionEnv): Promise<boolean>;
/** How a grep result was produced when the ripgrep leg could not complete normally. Either the
 *  delivered text IS ripgrep's own partial output (`partial: true`), or ripgrep produced nothing
 *  usable and the results came from the JS fallback scanner (`fallback: "js-scan"`). Absent on a
 *  clean run (and on rg-less envs, where the JS scanner is the designed primary path, not a
 *  degradation). */
export type GrepDegradation = {
    partial: true;
    reason: string;
} | {
    fallback: "js-scan";
    reason: string;
};
/** Structured grep result: the model-facing text plus the degradation facts, so the tool layer can
 *  ship them on the structured frame instead of leaving them prose-only. */
/** One SERVED result RECORD with its path taken from the engine's own knowledge, exactly the
 *  window `text` shows (post cap/offset): ripgrep's `--null` path field ({@link parseRgRecords}) on
 *  the rg legs, the walked file identity ({@link jsGrepDetailed}) on the JS-scanner legs. A record is
 *  one rendered line except where a path carries a newline (see {@link formatRgRecords}), which is
 *  also the case the text can no longer be split into records at all. The tool layer prefers these
 *  over re-parsing `text` (whose `path:line:text` split mis-cuts a path that
 *  itself contains `:digits:`).
 *
 *  KNOWN EXCEPTION to "exactly the window `text` shows", stated because it is measurable: on the two
 *  ripgrep PARTIAL legs (timeout / error-exit with output) the delivered text is fenced by
 *  `delimitUntrusted`, which NEUTRALIZES fence markers inside the body, while the rows carry the
 *  record bytes as ripgrep emitted them. A row's `text` there is therefore the un-neutralized form —
 *  identity (`path`) is unaffected. Consume `text` for anything rendered; the rows' contract is
 *  identity. Reconciling the two (defuse per record, or withhold rows where the text is fenced) is a
 *  contract choice recorded for the backlog, not settled here.
 *
 *  `path` is absent only where the record genuinely carries none —
 *  ripgrep's `--` group separator and the unaccountable shapes listed on {@link RgRecord} — and a
 *  row set carrying such a row sends the tool layer back to the text parse for identity. */
export interface GrepRow {
    path?: string;
    text: string;
}
export interface GrepRunResult {
    text: string;
    /** Served rows: present on BOTH engine legs — ripgrep's and the JS scanner's, including the
     *  re-scan legs. Absent only when the JS scanner refused the search outright (a typed
     *  `Error (grep): …` text): a refusal has no row set, truthfully, rather than an empty one. */
    rows?: readonly GrepRow[];
    degraded?: GrepDegradation;
    /** Deny-list withholding facts (see {@link ReadDenyWithheld}); absent = nothing
     *  withheld / no deny judge in play. */
    withheld?: ReadDenyWithheld;
}
/** The output modes the rg leg and {@link jsGrep} share. */
type GrepOutputMode = "content" | "files_with_matches" | "count";
/**
 * The rg leg's deny TRIPWIRE. rg's exclusion globs are spelled
 * from the pattern text and cannot express the win32 component-alias family (`.aws.` / `.aws ` —
 * trailing dots/spaces the win32 namespace strips), so a directory planted under an alias spelling
 * slips past the globs while the shared `matchPath` (two-view, read-deny.ts) correctly names it —
 * the JS leg pruned it, the rg leg returned its contents, and the same tree answered differently
 * depending on whether ripgrep was installed.
 *
 * FILTERING rg's text output line-by-line is not an option, on two counts: ripgrep prints filenames
 * verbatim, so a path component containing a NEWLINE splits one record across physical lines whose
 * fragments carry no `:line:` boundary — the filter keeps them and the deny-listed content passes (a
 * protection hole, not a precision residual); and pruning every line of a partial run fabricates a
 * `No matches.` row inside the structured card. So this function edits nothing: it JUDGES. Any deny
 * hit — or any record the format cannot account for —
 * trips, and the caller abandons the rg run for the JS scanner, whose walk prunes with the
 * authoritative judge and needs no path parsing at all. rg stays the fast path for the common case
 * (no guarded entries in the result); the moment a guarded spelling is involved, the engine that
 * cannot mis-parse it owns the answer.
 *
 * What it judges is ripgrep's OWN path field ({@link parseRgRecords}, `--null`),
 * not a prefix cut at a guessed separator. Trip conditions, all three modes alike: a record whose
 * path the deny judge names trips as `deny-hit`; a record with no path field at all (rg's binary-file
 * notice, a no-filename row, an unterminated tail, output from an env that ignored `--null`) is
 * unaccountable and trips as `ambiguous-record` — same abandonment, same reason code as before.
 * `dropIncompleteTail` mirrors {@link parseRgRecords}: on the partial legs the cut tail is dropped
 * BEFORE judging, because judging a record the caller will never receive can only cost an
 * unnecessary re-scan — every record that does reach the output is still judged, which is what the
 * rule is for. (A killed stream ending mid-record is an EXPECTED shape with a rule of its own; the
 * unaccountable arm is for records that survive that rule.)
 * `--` group separators and empty lines carry no path by construction and pass. The `:digits:` /
 * `-digits-` boundary family a guessing parser would have to reason about is gone: both of its
 * residuals (over-tripping on a cluster inside match TEXT, and the dedup key folding two denied files
 * whose own names carry a cluster) were artifacts of that decision, not of the judge.
 */
export declare function rgOutputDenyTripwire(stdout: string, mode: GrepOutputMode, judge: Pick<ReadDenyJudge, "matchPath">, opts?: {
    dropIncompleteTail?: boolean;
    base?: string;
}): {
    trip: false;
} | {
    trip: true;
    reason: "deny-hit" | "ambiguous-record";
    pattern?: string;
};
/** ripgrep grep: build flags from params, run, normalize to the same output as {@link jsGrep}.
 *  Result-fidelity contract: output ripgrep DID produce is never silently replaced by a fallback
 *  re-scan — partial results ship with a caveat; only a zero-output failure degrades to
 *  {@link jsGrep}, and that degradation is declared in both text and structure. An external abort
 *  propagates as the interruption it is (no fallback rerun). */
export declare function rgGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, deny?: ReadDenyJudge): Promise<GrepRunResult>;
/** grep dispatch: ripgrep when the env has it, else the hardened JS fallback.
 *  Returns the degradation facts alongside the text so the tool layer can surface them structurally. */
export declare function runGrepDetailed(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal, deny?: ReadDenyJudge): Promise<GrepRunResult>;
/** Text-only wrapper of {@link runGrepDetailed}. */
export declare function runGrep(env: ExecutionEnv, root: string, p: GrepParams, signal?: AbortSignal): Promise<string>;
/** glob: find files by name pattern, with optional sub-path scoping; returns RELATIVE paths. */
export declare function runGlob(env: ExecutionEnv, root: string, pattern: string, opts?: {
    path?: string;
    max?: number;
}, signal?: AbortSignal): Promise<string>;
/** Structured sibling of {@link runGlob}: same walk/match, plus the CC-shaped facts a
 *  shell renders (`filenames`/`numFiles`/`truncated`/`durationMs`/`totalMatches`/`countIsComplete`)
 *  without re-parsing the text.
 *
 *  CC207 backlog P2-2 (CC 2.1.206:397761-398004 = 2.1.207:342454-342633, node-sliced verbatim):
 *   · `totalMatches` — "Total number of matching files before truncation. A lower bound when
 *     countIsComplete is false." (CC: `totalMatches: g.length`)
 *   · `countIsComplete` — "Whether totalMatches is the exact total (true) or a floor because the
 *     underlying search truncated its own output (false)." (CC: `countIsComplete: !m`, m = the rg
 *     enumeration cut its own output; sema equivalent = the bounded walk hit its file/depth ceiling)
 *   · `numFiles` — "Number of file paths returned (after any truncation)" (CC: `numFiles: u.length`
 *     post-slice; was matchedAll.length here — pre-truncation total — fixed to the CC contract)
 *   · `truncated` — CC: `y = m || g.length > n + r` (underlying-incomplete OR over the result cap). */
export declare function runGlobDetailed(env: ExecutionEnv, root: string, pattern: string, opts?: {
    path?: string;
    max?: number;
    deny?: ReadDenyJudge;
}, signal?: AbortSignal): Promise<{
    text: string;
    filenames: string[];
    numFiles: number;
    truncated: boolean;
    durationMs: number;
    totalMatches: number;
    countIsComplete: boolean;
    error?: string;
    withheld?: ReadDenyWithheld;
}>;
export {};
