/** * @packageDocumentation * CFG-70 — **the path glob matcher for `prompts.paths`**, and the only one in this repo that * matches a *repository-relative POSIX path* with a `/` boundary. * * ## Why a third matcher and not a reuse * * Neither existing `globToRegExp` means what this one has to mean, and reusing either would * mis-scope silently rather than fail: * * - `core/approvals/matcher.ts` matches **shell command strings** — there is no `/` boundary * there, so `packages/x/*` would happily span directories. * - `agent/tools/gthGrepTool.ts` matches **basenames only**, and documents its own divergence * from ripgrep. * * A scoped prompt entry that matched more of the tree than the user wrote would attach one * module's guidelines to another module's review, which is precisely the failure the feature * exists to prevent. * * ## The vocabulary * * | written | means | * |---|---| * | `**` as a WHOLE segment | zero or more path segments — the only construct that crosses `/` | * | `*` | zero or more characters **within one segment** | * | `?` | exactly one character within one segment | * | `{a,b}` | alternation; nests (`{a,{b,c}}`) | * | leading `!` | marks the pattern NEGATIVE (see {@link CompiledGlob.negated}) | * * Everything else is a literal, including `\` — there is **no escape character**. A path that * genuinely begins with `!`, or contains a literal `*`, `?` or `{`, cannot be written as a * pattern. That is a deliberate floor: an escape grammar is a second thing to get wrong, and * neither shape occurs in a source tree worth scoping guidelines to. * * Matching is **case-sensitive**, and both patterns and paths are POSIX — `/` separated, always. * * ## Three hard constraints, each with its own spec cell * * **1. This module must never import `node:path`.** The paths it matches come from a unified diff, * where they are POSIX by construction. Routing one through `join`/`resolve` would normalise it to * the host separator: identical on Linux and macOS, backslash-separated on win32, where every * pattern then stops matching. That is this repository's recurring five-cell CI failure (OPS-27, * EXT-38, GS2-42, EXT-16), and a spec can only red on win32 if it *runs* there — so the durable * guard is the import ban itself, asserted by reading this file's source rather than by behaviour. * * **2. No catastrophic backtracking — which is why nothing here compiles to a regular expression * at all.** The obvious implementation turns a pattern into one regex, and it is measurably * unusable: `pkg/a*a*a*a*a*a*a*a*b/x.ts` against a 255-character non-matching segment took **163 * seconds** in this repo before this module was rewritten. Nesting is not required for that — a * run of sibling `[^/]*` separated by literals backtracks over every way to distribute the * characters between them, which is polynomial in the segment length with the star count as the * exponent. Escaping and `**`-avoidance do not help; the engine is the problem. * * So a pattern compiles to **tokens**, and matching is a forward sweep over a *set of reachable * positions* rather than a search with backtracking: * * - within a segment, `advanceTokens` carries a set of positions through the token list — a star * widens the set to every remaining offset, a literal or `?` advances the positions that fit, * an alternation unions its options. A position reached twice is stored once, which is exactly * what a backtracking engine fails to do and why it re-explores; * - across segments, `matchSegments` resolves `**` with the classic single-backtrack-point * wildcard walk. * * Both are O(pattern × subject) with no exponential case, so the bounded-time spec cell holds for * any pattern a user can write, not merely for the shapes it happens to name. * * **3. A character outside the vocabulary above is a literal, by construction.** Because no regex * is emitted there is nothing to escape: a `.` in a pattern is compared with string equality to a * `.` in the path, and a `(`, `[`, `+` or `$` cannot mean anything at all. This is the same * guarantee an escaping implementation aims at, reached by removing the mechanism that needs it. */ /** One compiled pattern: whether it negates, and the test it applies to a path. */ export interface CompiledGlob { /** `true` when the pattern was written with a leading `!`. */ readonly negated: boolean; /** The pattern as written, minus any leading `!`. */ readonly pattern: string; /** Does this pattern match `path`? Negation is NOT applied here — see `negated` above. */ test(path: string): boolean; } /** * A pattern list compiled once, answering the two questions a caller actually has. Keeping both * on one object is what stops a caller re-deriving the meaning of a leading `!` for itself: a * second reading of the negation rule is a second place for it to drift. */ export interface CompiledGlobList { /** Does any pattern written WITHOUT a leading `!` match `path`? */ matchesPositive(path: string): boolean; /** Does any pattern written WITH a leading `!` match `path`? */ matchesNegative(path: string): boolean; } /** * Compile one glob pattern. A leading `!` is consumed here and reported as * {@link CompiledGlob.negated}; `test` answers only whether the remaining pattern matches, so the * caller never re-parses the `!`. */ export declare function compileGlob(pattern: string): CompiledGlob; /** Compile a whole pattern list once, for repeated questions about many paths. */ export declare function compileGlobList(patterns: readonly string[]): CompiledGlobList;