The-Billy-Company/gist

GitHub: The-Billy-Company/gist

gist 是一款基于索引化三元组搜索的极速命令行代码搜索工具,兼容 ripgrep 用法并专为 AI 编程代理场景做了常驻会话、候选索引和结果排序等增强。

Stars: 1 | Forks: 0

# gist: Indexed Regex Search for a Live Working Tree - [Overview](#overview) - [Should I Be Using This?](#should-i-be-using-this) - [Support](#support) - [Quickstart](#quickstart) - [Keeping the Reflex](#keeping-the-reflex) - [The Default Move](#the-default-move) - [Docs, Code, and Data](#docs-code-and-data) - [Choices That Prevent a Wrong Search](#choices-that-prevent-a-wrong-search) - [The Search Contract](#the-search-contract) - [Improvements](#improvements) - [Binary Files](#binary-files) - [Indexed PCRE](#indexed-pcre) - [Compressed Input](#compressed-input) - [Sorted Output](#sorted-output) - [The Type Registry](#the-type-registry) - [Hyperlinks](#hyperlinks) - [Line Buffering](#line-buffering) - [Block Buffering](#block-buffering) - [Adjacent Product Choices](#adjacent-product-choices) - [Three Execution Paths](#three-execution-paths) - [The Cold Subprocess](#the-cold-subprocess) - [The Resident Session](#the-resident-session) - [The In-Process ABI](#the-in-process-abi) - [The Two Indexes](#the-two-indexes) - [Ranked Search](#ranked-search) - [Evidence](#evidence) - [Prior Art](#prior-art) - [Indexed Neighbors](#indexed-neighbors) - [Matching Engines](#matching-engines) - [Ranking](#ranking) - [The Codex Subcommand](#the-codex-subcommand) - [Outside the Claim](#outside-the-claim) - [Package Map](#package-map) - [Build and Test](#build-and-test) - [Running One Test](#running-one-test) - [Provenance](#provenance) ## Overview gist is where our bit-level idea became a tool. Text is bits, and a required trigram is a small proof that most files cannot match. Instead of reading them all, gist rules those files out, then checks every survivor against current bytes. We kept ripgrep's useful mental model because that muscle memory is already embedded in how agents search: pattern, paths, familiar flags, stdout results, and 0/1/2 exit codes. Preserving that familiarity is why we treated parity as a product constraint rather than a loose resemblance. Then we added three things for the agent loop: 1. a persisted candidate index that can prove most files are irrelevant, from required trigrams plus a crest sidecar for the class-repetition patterns trigrams cannot see; 2. a fail-open resident session that avoids cold startup when the request is eligible; and 3. a bounded, definition-biased ranked view for questions where the best hit matters more than every hit. gist is powered by [`irregex`][irregex], the engine we built this tool on. The regex engines, the trigram index and the crest sieve beside it, the corpus walk, the freshness law, and the ranking math all come from there. What this repository adds is everything with an opinion about a product: the argv grammar, the resident daemon, distribution, and the parity contract. Read the engine for why it is shaped the way it is; read this for what the binary promises. That product thesis, the competitive ancestry behind it, and the gates that try to falsify it are separated into [`CLAIM.md`](research/gist/CLAIM.md), [`PRIOR_ART.md`](research/gist/PRIOR_ART.md), and [`TESTING.md`](research/gist/TESTING.md). This README explains the shipped instrument; the dossier explains why its claims deserve belief. ## Should I Be Using This? - **To search a repository from a terminal, with the flags you already know** – here. Type `gist` where you typed `rg`, and start at [Quickstart](#quickstart). - **For similarity, repetition, or "what is this file like"** – `relate`, a separate package. Compression kinship is not a pattern question and this binary does not answer it. - **For the blast radius of a symbol, or where a pasted snippet came from** – `blast`, also a separate package. Both of those need current bytes from two engines at once. - **For a linear-time regex to call from Python, Rust, Go, or C** – [`irregex`](https://github.com/The-Billy-Company/irregex), which ships the bindings and the header. You want the engine, not a command-line tool wrapped around one. - **To embed the search engine itself in a host process** – the [C ABI](#the-in-process-abi) here, which streams match records from the same resident engine the daemon holds. - **For structural, semantic, or hosted multi-repository search** – not here at all. See [Outside the Claim](#outside-the-claim) for who does answer that. The dividing line is whether your question is an *exact* one. gist finds the bytes you can name, quickly, over a tree that is changing underneath it. Everything it does to go fast is an accelerator that is allowed to decline, and nothing it returns was decided by anything other than the file's current bytes. ## Support - Bugs and feature requests go through the [issue templates](.github/ISSUE_TEMPLATE), which ask for the pattern, the tree, the exact command line, and whether an index or a resident session was warm. A search bug without its corpus is a bug nobody can reproduce. - A place where gist and ripgrep disagree has its own template, `parity_gap`. Divergence outside the [improvements](#improvements) bucket is a defect by definition, so report it as one rather than as a feature request. - Security vulnerabilities never go in a public issue. See [`SECURITY.md`](SECURITY.md), which also explains why the threat model here treats the corpus as the attacker. - `irregex`, `relate`, and `blast` are separate repositories with their own trackers. A wrong match or a wrong file set usually belongs to the engine; the argv grammar, the daemon, and the parity contract belong here. - [`CONTRIBUTING.md`](CONTRIBUTING.md) is the entry point for a change, and [`CODE_OF_CONDUCT.md`](CODE_OF_CONDUCT.md) governs the conversation around it. ## Install The CLI is the product, and it is built from source — one command produces the binaries, links them onto `PATH`, and writes the trigram index: zig build # ReleaseFast binaries, PATH link, trigram index The language bindings are published, and each drives that same binary rather than reimplementing it, so the CLI is a prerequisite for all three: | | Install | You write | |---|---|---| | Python | `pip install gist-search` | `import gist` | | Rust | `cargo add gist-search` | `use gist::…` | | Go | `go get github.com/The-Billy-Company/gist/bindings/go` | `import ".../bindings/go/exact"` | The bare name `gist` was taken on both PyPI and crates.io and names there are permanent, so the distribution carries the `-search` suffix while the identifier you type stays `gist` — the bs4 / PIL split. Per-language detail is in [`bindings/python`](bindings/python/README.md), [`bindings/rust`](bindings/rust/README.md), and [`bindings/go`](bindings/go/README.md). ## Quickstart The canonical form is the one you already type, with no verb and no setup: gist 'SearchRequest' # search from the current directory gist 'SearchRequest' services -n # explicit scope, line numbers gist 'SearchRequest' -l # matching paths only gist 'SearchRequest' --rank # best definitions and uses, default top 20 gist 'foo(?=bar)' -P # vendored PCRE2: lookaround/backreferences gist 'foo(?=bar)' --engine auto # linear first, PCRE2 only if required gist 'begin.*end' -U # multiline mode gist 'needle' --no-index # force a pure live walk Three lifecycle verbs inspect and refresh what the accelerators hold: gist status --json # versioned index/freshness snapshot gist index # rebuild the persisted candidate index gist serve [ROOT...] # run the resident UDS service explicitly The codex is a separate shelf, for exact literal questions answered without opening source files: gist codex build gist codex count 'literal' # exact corpus-wide occurrence count gist codex tally 'literal' --top 20 # per-file counts, heaviest first gist codex status Everything the binary supports can be asked of the binary rather than of this document: gist --help gist --schema # machine-readable flags and compatibility gist --generate man # gist(1); also complete-{bash,zsh,fish,powershell} No index is required. Without one, `gist` scans the live tree; with a covering index, it automatically skips files that cannot contain the query's required trigrams and verifies every candidate against current bytes. `gist rg …` and `gist search …` are aliases for the same search engine. The canonical form is intentionally verbless. `zig build` also links the [Vim/Neovim plugin](editor/vim/README.md) into any editor already installed, so `:grep` becomes gist and `--vimgrep` output streams into the quickfix list while the search is still running. The plugin is a client of this CLI and nothing more. It discovers flags from `--schema`, file types from `--type-list`, and index state from `gist status --json`, so a binary upgrade reaches the editor without a plugin release. The same install places [`gist(1)` and the shell completions](shell/README.md), rendered by [`cli/primer/`](src/surface/cli/primer/README.md) from this face's own flag catalog. `man gist` answers, and `gist -` offers a menu captioned by what each flag changes. Every closed value set is baked in, so no tab ever forks a process: 241 file types with their globs, 233 encodings, the engines, sort keys, color postures, and hyperlink aliases. ## Keeping the Reflex Gist has two ergonomic lanes, and the first one is the reflex you already have. The **muscle-memory lane** lets a person or agent replace `rg` with `gist` without stopping to translate the search. The **native lane** is for an intent ripgrep does not name: rank the best code hit, force the differential oracle, reuse a warm corpus, or query the compressed codex. Start in the first lane, and cross over only when the question changes. - **Find matching lines** – `gist PATTERN [PATH...]`, exactly where you would have typed `rg PATTERN [PATH...]`. - **Narrow the corpus** – the same `-t`, `-T`, `-g`, `--iglob`, and explicit positional paths, spelled the same way. - **Read only the paper trail, or only the source** – `--docs`, `--code`, and `--data`, plus their `--no-` complements, where ripgrep leaves you hand-assembling a dozen `-t` names. - **Shape familiar output** – `-n`, `-l`, `-c`, `-o`, `-A/-B/-C`, and `--json`, on the same output contract. - **Find the best definition or use** – `--rank[=N]`, rather than inspecting ordinary grep output and deciding for yourself. - **Use lookaround or backreferences** – `-P`, or `--engine auto` to escalate only when the pattern needs it. - **Prove acceleration changed nothing** – `--no-index`, whose answer is the oracle for the indexed path, rather than running another scanner. - **Avoid repeated startup** – nothing at all, since eligible searches transparently use the resident session where rg needs an external wrapper or a server. - **Count an exact literal without source-file I/O** – `gist codex count LITERAL` on a clean shelf, rather than scanning the tree. - **Ask what this binary supports** – `gist --schema`, generated from the live flag catalog, rather than prose or remembered flags. - **Read the manual, or tab-complete a flag** – `gist --generate …`, rendered from that same catalog, where rg offers `man rg` and hand-written completions. ### The Default Move For both humans and coding agents, the shortest correct sequence is six steps. 1. Type the search you already know: `gist PATTERN [PATH...]`. 2. Scope early when you know the neighborhood, since a positional path, `-t TYPE`, or `-g GLOB` saves output as well as work. 3. Choose the smallest answer that serves the next step: `-q` for existence, `-l` for files, `-c` for per-file counts, ordinary lines for reading, and `--rank` when one strong code location matters more than completeness. 4. Stay on the linear engine by default. Use `--engine auto` when a pattern may need PCRE2, and `-P` when PCRE2 semantics are the requirement. 5. Let Gist choose acceleration. Reach for `--no-index` only to debug or prove parity, `gist status` to inspect freshness, and `gist index` after a large tree change when you want to re-anchor performance. 6. Read stderr after a miss. Suggestions and budget notices never contaminate stdout, so a person can learn from them while a pipeline keeps rg-shaped bytes. The aliases `gist rg` and `gist search` exist for callers that require a verb, not because they unlock a different engine. Agents should emit the bare form. It is shorter, canonical, and leaves the pattern in the same argv position as ripgrep. ### Docs, Code, and Data `-t` answers "which language is this?", and that is the wrong grain for the question anyone actually asks. Nobody wonders whether a file is reStructuredText; they wonder **"am I reading the paper trail, or am I reading the implementation?"** So that is its own corpus axis: gist 'SessionStore' --docs # only prose: what was written ABOUT it gist 'SessionStore' --no-docs # only the implementation and its payload gist 'retry_budget' --data # only config: json, yaml, toml, lockfiles gist 'TODO' --code --no-index # implementation only, no acceleration Three genera (`docs`, `code`, `data`) are total and disjoint over every path, so `--docs` and `--no-docs` are exact complements and no file can fall through the partition. Repeats union, so `--docs --data` is either. Each name is also a type name, so `-t docs` and `-T code` mean the same thing and compose with `--type-add`. The aliases `prose`, `doc`, and `source` resolve too, because a name you guess correctly beats one you have to learn. `code` is the leftover, never a recognized set. An unfamiliar extension, a generated blob, or a file with no extension at all lands in `code`, so the worst a gap in the table can do is show `--code` one line too many. A fourth `unknown` genus excluded from `--code` would turn that same gap into a *silent miss* instead. Classification is spelling first and location second, which is why `docs/conf.py` stays code and `CMakeLists.txt` is a build recipe rather than prose. That decision is powered by irregex's [genus classifier][ir-genus] and the totality argument beneath it. What this repository adds is the flag family and the gates below. A genus **narrows** what the walk produced and never un-hides. Unlike `-t` and `-g` it will not pull a dotfile or a gitignored leaf back in, because `code` is the default and an un-hiding genus would surface all of `.git/`. The whole thing is daemon-eligible. The selection rides the `query_ext` frame as a two-byte trailer, so a `--docs` query answers from the resident session at warm speed, byte-identical to the cold run. Extend it with `--type-add 'docs:notes/**'` for one run, or `types = ["docs:notes/**"]` in `.irregex.toml` for the whole tree. No grep-class tool ships this axis. ripgrep has prose-adjacent types and no aggregate over them, and its type globs are basename-only, so a `docs/` rule is not expressible there even by hand ([ripgrep#3339][rg3339], open). The rival is therefore what a person types instead: one `-t` per prose type, hand-assembled, every time. Against that union, derived at run time from `gist --type-list --docs ∩ rg --type-list` so it can be neither strawmanned nor left to drift, `--docs` runs **2.9× faster cold and 21× warm** (geomean over the needle slate, with the warm arm running with the answer keep disabled, so it is a search and not a memoized recall). Speed is the smaller half. A basename glob and a genus **disagree about what is prose**, and the disagreement is proven on a hermetic tree rather than on this repository, so the numbers are the same on your machine. The union calls three `CMakeLists.txt` build recipes prose, because `*.txt` has no way to say "except this one", and it cannot name two extensionless documents that gist promotes by location and by name. Over this repository's tracked corpus the two rosters land within one file of each other. That is expected, since the rival is derived from gist's own docs types, and it is why the mechanism is measured where it cannot drift. Both halves are gated permanently. [`partition_parity.sh`](bench/conformance/gates/parity/partition_parity.sh) proves the set identities over the live tree on every `zig build test`, and [`bench/dominance/partition/`](bench/dominance/partition/README.md) holds the speed floors and the classification contract. The taxonomy is [GitHub Linguist's][linguist]; the [classifier][ir-genus] names the two deliberate divergences. ### Choices That Prevent a Wrong Search This section teaches selection, not a second flag registry. The checked-in `flag_catalog` and `gist --schema` remain the exhaustive, versioned answer. - **Case and character semantics** – `-i`, `-s`, and `-S` are last-wins. Unicode folding, classes, properties, and word boundaries are the default, and `--no-unicode` or a leading `(?-u)` deliberately selects byte/ASCII semantics. The fold is **simple** (`C+S`), matching ripgrep exactly, so `ß` is not `SS` on either tool. Under PCRE2, `--pcre2-unicode` and `--no-pcre2-unicode` control that backend separately. - **Literal, word, line, and inverse matching** – use `-F` when punctuation should not become regex, `-w` for a whole Unicode word, `-x` for a whole line, and `-v` for non-matching lines. Multiple intents use repeated `-e` or a pattern file with `-f`. - **Hidden and ignored files** – `-u`, `-uu`, and `-uuu` progressively disable ignores, add hidden paths, and include binary data. An explicit `-g` or `--iglob` include can whitelist an ignored path; a type filter can unhide a matching dotfile but does not override gitignore. - **The `-rn` trap** – recursion is already the default, and ripgrep semantics parse bundled `-r` as replacement. `gist -rn PATTERN` therefore means `--replace=n`, not "recursive with line numbers". Spell `-n` alone; Gist preserves the behavior for parity but emits a diagnostic. - **Zero is sometimes a real value** – explicit `-m0` means match nothing and exits 1, where omitting `-m` means unlimited. Likewise `-M0` explicitly disables the long-line cap. - **Stable ordering** – the default streams in fast parallel discovery order. Use `--sort path|modified|accessed|created` or `--sortr` only when stable global order is part of the consumer's contract. - **Unusual input** – `-z` searches compressed files, and `--pre CMD` searches a preprocessor's stdout and takes precedence over `-z`, with the command receiving the path as `argv[1]` and the file's bytes on stdin, ripgrep's exact contract. `-E/--encoding` accepts `auto`, `none`, or the checked-in WHATWG label set. Unknown labels and failing preprocessors exit 2 rather than looking like empty searches. - **Binary intent** – `-a` treats input as text, while `--binary` and `-uuu` search a binary file in full and print every matching line, an improvement over ripgrep's one-line binary summary for a code locator (see [Binary Files](#binary-files)). - **Machine output** – use `--json` for typed records, `-0` for NUL-delimited paths, `--null-data` for NUL-delimited input records, and explicit sorting when downstream comparison requires deterministic file order. - **Who is reading** – most of the human posture is already the default a terminal gets, with matches grouped under a filename title and the rows numbered beneath it, exactly as ripgrep lays them out, while a pipe keeps the `path:line:` prefix and ripgrep's bytes. `-p`/`--pretty` adds the remaining piece (color, unconditionally) and `--plain` is the opposite pole, the piped posture forced onto a terminal, so an interactive run reproduces the bytes a script would see. Decline either half on its own with `--no-heading` / `-N`, or request it into a pipe with `--heading` / `-n`. - **How fast it arrives** – delivery cadence is separate from all of that. `--line-buffered` is for a consumer that reacts per line, `--block-buffered` (with `--buffer-size`) for one that only wants the bytes cheaply, and `--buffer-size=0` when nothing may be held at all. Left alone, a pipe blocks and a terminal streams by line, which is almost always right. - **Agent budgets** – prefer `--rank`, `-l`, `-c`, a narrower path, or `-m N` before lifting the soft output guard. `--uncap` or `GIST_UNCAP=1` is the deliberate escape hatch, and `GIST_HINTS=0` mutes guidance without changing results. - **Warm and codex paths** – the resident session is an invisible, fail-open accelerator, and unsupported shapes simply stay cold. The codex is different: use it only for exact literal `count`/`tally` questions, and treat absence as proven only when `gist codex status` reports a clean shelf. - **Persisted defaults** – a committed `.irregex.toml` at the tree root declares the corpus (`roots`, `skip`, `types`), while a machine-local `$XDG_CONFIG_HOME/gist/preferences` (on Windows, `%LOCALAPPDATA%\gist\preferences`, never the roaming `%APPDATA%`) holds flag lines and applies **only when stdout is an interactive terminal**. A pipe, a script, `--json`, and the daemon never inherit them, nor do they open the file, so a typo in one person's preferences cannot fail anybody else's run. `gist config` reports the resolved stack, `gist config check` validates both layers without searching, and `gist config init` writes a charter prefilled from this machine's `GIST_ROOTS` / `skips.list`. `--no-config` / `GIST_NO_CONFIG=1` ignores both. ## The Search Contract The cold runtime's [`flag_catalog`][ir-catalog] is the source of truth for both argv handling and `gist --schema`. It separates the public surface into four buckets: exact support, **improvements** (identical-or-superset results that are strictly better, whether faster, more robust, or better for code search, and never a regression), accepted no-ops, and unknown flags that fail with exit 2. Where gist differs from ripgrep it is an improvement, or it is a bug; there is no third category. We do **not** claim every option ripgrep ever shipped. That claim is measured rather than asserted, against a denominator ripgrep owns. [`surface.py`](bench/conformance/rgsuite/surface.py) reads rg's documented flag surface at run time (longs from `rg --generate complete-bash`, shorts and value grammar from its man page) and compares both binaries byte-for-byte on stdout and exit code. Measured that way, **186 of 186 documented flags conform**: 176 are byte-identical and 10 differ only at a declared boundary whose residual check is re-verified on every run, with 0 rejected and 0 undeclared divergences. Alongside that sit **411/411** of ripgrep's mined integration cases and 27/27 adverse undo pairs, where a negation must actually undo, on a fixture where the two answers differ. Both of those denominators are ripgrep's own, which is their ceiling as well as their authority. So a third lane, [`fuzz.py`](bench/conformance/rgsuite/fuzz.py), generates what nobody curated: a random pattern × flag set × a hostile corpus (invalid UTF-8, NUL bytes, a 4 MiB line, a symlink cycle, an unreadable file, catastrophic-backtracking patterns), demanding byte-identical agreement while measuring crash, hang, and peak RSS. It is the only lane that still finds anything, and it does: a low-single-digit tail per 6,000 iterations, in corners where ripgrep's own three printers do not agree with each other. An empty match at the end of a file with no final newline is counted by `--count-matches`, dropped by `-o --json`, and rendered as the whole line by `-o`. That tail is **published, not excluded**. It is classified by root cause in `fuzz_baseline.json` and in Layer I of the certificate, ratcheted shrink-only, and a missing fuzz record refuses the mint outright. The implemented surface includes: - regular, fixed (`-F`), smart-case (`-S`), case-insensitive (`-i`), whole-word (`-w`), inverted (`-v`), and multiple (`-e`/`-f`) patterns; - Unicode-by-default case folding, character classes, properties, and word boundaries, with `(?-u)` or `--no-unicode` for byte/ASCII semantics. The fold is **simple** (Unicode `C+S`), which is ripgrep's posture rather than a shortfall against it: `café` ⇄ `CAFÉ` matches on both and `ß` ⇄ `SS` on neither, because full (`F`) folding is one-to-many and neither engine performs it; - the linear RE2/Pike engine, vendored PCRE2 10.47 with JIT (`-P`), and `--engine auto` escalation; - native multiline search (`-U`, `--multiline-dotall`); - path, type, glob, hidden-file, symlink, depth, size, filesystem, and the full `.gitignore`/`.ignore`/`.rgignore` control family; - context, only-match, count, replacement, heading, column, byte-offset, vimgrep, JSON Lines, null-delimited, sorted, and stats output; - stdin, UTF BOM detection, the WHATWG encoding label set, preprocessing, and compressed-file search. Normal results go to stdout. Diagnostics, timing, output-budget notices, and no-match suggestions go to stderr, and `GIST_HINTS=0` disables suggestions without touching results. Search exit codes follow ripgrep: - `0` – at least one match; - `1` – a clean search with no match; - `2` – invalid argv, unsupported syntax, an unreadable path, or another search error. An unknown flag or a pattern rejected by the selected engine is therefore an error, never a convincing empty result. ## Improvements Eight flag groups are not bit-identical to ripgrep, and every one of them is an **improvement**: identical-or-superset results that are strictly better in behavior, performance, or robustness, never a regression. This is the *only* category of divergence. If gist ever disagrees with ripgrep outside this list it is a bug, not a design choice, and `gist --schema` reports the whole set under the `improvements` bucket. For an exact, versioned answer about a flag, inspect `gist --schema` rather than relying on a prose list. ### Binary Files `--binary` (and `-uuu`) searches a NUL-bearing file in full. ripgrep prints one opaque line, `binary file matches (found "\0" byte around offset N)`, and stops. A code locator wants the matches, not a shrug, so gist searches past the NUL and prints every matching line, exactly as `-a/--text` does. For the source artifacts that carry a stray NUL, such as minified bundles, checked-in fixtures, and mixed-content files, this is strictly more information. ### Indexed PCRE `-P` / `--pcre2` is the only *indexed* PCRE search. The vendored PCRE2 JIT backend returns ripgrep's exact `-P` match set, including lookaround, backreferences, and Unicode properties. It rides the same trigram prefilter as the linear engine, so PCRE queries skip provable non-candidate files instead of scanning the whole tree. Same answers, fewer bytes read. The gist-native `--rank` is linear-only. ### Compressed Input `-z` / `--search-zip` decompresses in-process. Results are identical to ripgrep across every codec, verified byte-for-byte in [`bench/conformance/rgsuite`](bench/conformance/rgsuite), but gzip, zlib, zstd, and xz decode *in-process* via `std.compress`. That means no `gzip -dc` fork per file, the single biggest speed edge on compressed corpora. bzip2, lz4, Brotli, lzma, and `.Z` shell the standard external tool exactly as ripgrep does. ### Sorted Output `--sort` / `--sortr` reads in parallel and orders after. The final `path`/`modified`/`accessed`/`created` order is identical to ripgrep's, which single-threads a sorted run. `created` additionally falls back to ctime where the platform has no birth time, so a sort ripgrep cannot perform still succeeds. ### The Type Registry `--type-list` is a strict superset of ripgrep's. It is sorted and framed exactly like ripgrep's, with ripgrep's rows byte-identical, plus richer definitions and gist-only types. A caller parsing ripgrep's format parses gist's; it just sees more. ### Hyperlinks `--hyperlink` / `--hyperlink-format` makes results clickable when they should be. One axis has three spellings: `--hyperlink[=auto|always|never||]`, `--no-hyperlink`, and ripgrep's `--hyperlink-format`. The default is `auto`, so links appear when a person is reading in a terminal known to render OSC-8 and vanish the moment the bytes are going somewhere else, where ripgrep defaults to none. The deeper difference is that **ripgrep's links are a property of its color layer**. By its own help, "hyperlinks are only written when a path is also in the output and colors are enabled". So a link into a pipe costs `--color=always`, which also forces color into that pipe, and rg's documented escape hatch (`--colors path:none --colors line:none …`) still wraps every field in `ESC[0m` resets. There is no rg invocation that yields clean text plus links, and `gist --hyperlink=always` is that invocation. Nor does gist need a path in the output to have something to click. Where rg drops the link entirely when the filename is not printed, as with one explicit file argument, gist anchors the line number instead. A link is navigation, not paint, and `NO_COLOR` has no opinion about it. Naming a destination on the command line turns links on, because typing `--hyperlink=vscode` and getting silence is the mystery this flag exists to prevent. The standing-preference spelling is `GIST_HYPERLINK`, which may carry a destination alone, leaving the probe to decide, or a `WHEN,WHERE` pair like `always,vscode`. The format grammar is ripgrep's, so a format rg accepts gist accepts and one it rejects gist rejects with the same reason, plus aliases rg lacks (zed, windsurf, vscode-remote, cursor-remote). A `link` trace lens says on one line why a run linked or didn't, and always says it, naming the posture (`turned off`), the reader (`output is a byte protocol`, `machine-shaped output`), or the terminal (`stdout is not a terminal`, `terminal does not advertise OSC-8`), because a diagnostic that goes quiet reads as "nothing to report". Lighting the lens is enough to keep a run off the warm path, which has no beacon to explain. Paths fold lexically rather than through a `realpath(2)` per file, so a click lands in the tree you searched. From `/tmp/x` gist emits `file:///tmp/x` where rg emits `/private/tmp/x`, which resolves outside the workspace folder your editor has open. Every shape that prints a filename is clickable: match rows, headings, `-l`/`--files` lists sorted or not, `-c` counts, the binary notice, and the `--rank` view, whose whole point is that its top row is the one to open. Two shapes refuse every posture, including `always`: `--json` records and NUL-framed `-0` lists, where the filename's bytes *are* the payload. So does a filename carrying a control byte, where you cannot see where the click target starts and stops, since a newline in a name splits the anchor across two terminal lines outright. The URL stays exact either way; it is the text between the escapes that is refused, where rg frames those and emits the two-line link. Linking 93k matches costs ~5 ms (≈60 ns each), because the URL is split once per file into a prebuilt `Waypoint` and a row only writes the digits. The output cap counts results rather than escapes, so turning links on never costs you a row. ### Line Buffering `--line-buffered` keeps the same promise for a fraction of the syscalls. Neither implementation ever holds a finished line, and ripgrep's `LineWriter` also never writes more than one at a time, while gist emits every finished line already in hand in a single `write(2)`. `-n std src/` here is 1.04 MB of results and the same bytes either way: `rg -j1 --line-buffered` makes 15,782 writes, and gist makes 342. The boundary is the run's real terminator, so `--null-data` records flush on NUL, where rg's line writer only knows `\n` and holds NUL-delimited output until its buffer fills. ### Block Buffering `--block-buffered` ramps the block, and the ceiling is one you can name. The first fragment leaves immediately and the threshold then doubles to the ceiling, so `| head -1` answers instantly and a closed pipe is discovered within a kilobyte, while a full dump settles into whole-buffer writes. On the same run ripgrep makes 342 writes, from its 8 KiB `BufWriter` that holds the first byte as long as the last, and gist makes 23, or 11 at `--buffer-size=1M`, a knob ripgrep does not have. This is gist's default posture into a pipe, and it reaches the reader sooner as well as less often: 5 ms to first byte against ripgrep's 9. ### Adjacent Product Choices Three more choices are *not* rg-flag divergences, and it is worth saying so. `--mmap`, `--no-mmap`, `--dfa-size-limit`, and `--regex-size-limit` are accepted compatibility no-ops. Agent-facing output has a soft budget of roughly 25k tokens / 100 KiB and a hard 256 MiB ceiling that `--uncap` or `GIST_UNCAP=1` lifts. `--colors` restyles one element at a time, in ripgrep's own spec grammar (`{type}:none` or `{type}:{fg|bg|style}:{value}`, over path/line/column/match, with named colors, 0-255, and `r,g,b`). A spec merges into gist's palette the way rg's merge into its own, so naming a hue keeps the default's bold, and `match:none` unstyles matches while leaving path color alone, the thing `--color=never` cannot say since it is all-or-nothing. gist renders one SGR sequence per element where rg emits a separate escape per attribute, and paints column numbers only when a spec asks it to; a malformed spec exits 2, as it does under rg. ## Three Execution Paths There are three ways into the matcher, and they are required to give one answer. ### The Cold Subprocess We keep the normal process as the path that can answer every request: argv → parse → compile → walk → index read-elision → verify → emit The walk chooses the files, and the index only removes provable non-candidates. Files changed since the index anchor are read live, and missing coverage simply reduces acceleration. `--no-index` is the differential oracle for this invariant. ### The Resident Session To stop paying startup costs, `gist serve` holds corpus bytes and a trigram index behind a per-repository Unix socket. The CLI may auto-spawn it after an eligible cold miss. The request classifier deliberately keeps the warm surface small. [`client.zig`](src/exec/session/daemon/client/client.zig) remains the executable authority; what follows is a readable snapshot. Warm-eligible shapes are rootless line output (`-n` / `-N` allowed), rootless `-l` / `--files-with-matches`, rootless `--rank[=N]`, the `-F`, `-i` / `-s` / `-S`, and `-w` modifiers, and existence or caps via `-q` and `-m N`. Authoritative-cold shapes are any explicit path including `.`, stdin or TTY stdout, context, JSON, replace, multiline, PCRE2, globs, invert, and any malformed or unrepresentable flag value. The wire contract also defines a count mode, but CLI `-c` keeps ripgrep's per-file layout and stays cold. Warm I/O has a two-second deadline, and `GIST_NO_AUTOSERVE=1` disables automatic session startup. Eligibility is an optimization decision, never a support boundary. Freshness is fail-closed. macOS kqueue or Linux inotify can narrow the work, but a reconcile barrier decides whether resident bytes are safe. Doubt, overflow, an index generation change, or a walk error declines the warm answer and returns to the subprocess. The sockets and the lifecycle are this repository's; the engine they hold hot is the [`ResidentSession`][ir-session] invariant. ### The In-Process ABI For embedders, the in-process FFI path is the C ABI (`gist_open` / `gist_search` / `gist_close`), which streams match records from the same error-returning resident engine. Python uses it when the shared library and optional cffi are available, then falls open to UDS or subprocess. It is another route to the same matcher, not a second implementation. The request options are the engine's, in [`contract/engine.toml`][ir-engine], while transports and session rules are this repository's, in [`contract/surface.toml`](contract/surface.toml). ## The Two Indexes We use two indexes because they answer different questions. The ordinary trigram index is a **candidate filter**: small, mmap-backed, fast, and never authoritative, because every candidate it admits still gets its current bytes checked. That is the index behind normal regex search. Riding beside it is the **crest sidecar**, which prunes the literal-free class repetitions (`[0-9a-f]{12}`, `[0-9]{6}`) that extract no trigram at all and that every index in this family therefore concedes whole. Both filters only ever skip reads, and caseless queries, changed files, and a missing sidecar all fall back to reading. Both are powered by irregex: the [persisted index family][ir-index], and the [forced-run theorem][ir-crest] the sidecar is sound by. The codex shelf is a **compressed self-index** for exact literal questions. It can count in O(pattern length), locate occurrences, recover the indexed corpus, and answer without opening source files. `gist codex count` is a proof of absence only when the shelf's freshness report is clean, and the command reports files changed since the shelf was built rather than hiding that qualification. See `relate/src/kernel/codex` (math) and `relate/src/corpus/index/shelf` (persisted SHLF). ## Ranked Search Sometimes we need the best hit, not every hit. `--rank[=N]` keeps the same pattern and path semantics and changes only the shape of the answer: the definition outranks its two hundred call sites, and generated files sink below authored ones. The view is powered by irregex's [rank fusion][ir-rank] and the signals it fuses. What this repository adds is the flag, a default top K of 20, and the row: 1. path:line [def|use|gen|mirror] ×count source line This is heuristic text ranking, not name resolution or semantic code intelligence. It works from the persisted index when possible and has a live walk fallback, and `--rank` is limited to the linear engine. ## Evidence The idea is ours; the expected answers are not. The ripgrep muscle-memory promise is why we compare gist with a live `rg` oracle instead of writing expectations by hand. The gates cover parallel and serial walks, indexed versus `--no-index`, freshness, line framing, Unicode, multiline and PCRE2 modes, ordering and ignore flags, encodings, preprocessing, compressed input, binary handling, streams, and resident-versus-cold answers. The tracked ripgrep 15.2.0 snapshot contains 446 invocations per walk engine: - **Mined upstream suite** – 411 PASS, 0 ORDER, 0 FAIL, 14 NA, and 21 SKIP. Supported-surface parity is **411/411 = 100%**, so every supported-surface case matches ripgrep, with zero deferred divergences. - **Multiline** – 30/30 adversarial cases pass for stdout, exit code, and indexed-versus-`--no-index` equality. - **PCRE2** – 30/30 adversarial cases pass the same three-way oracle, including lookaround, backreferences, Unicode toggles, and resource-limit failures. - **Walk, ignore, and message flags** – 39/39 cases pass on each engine. The fixtures make path/time ordering, last-wins negations, worker counts, device boundaries, and global git-ignore state observable. The `--no-messages` / `--no-ignore-messages` cases live here rather than in the mined suite because rg's own `--no-messages` tests assert on the exit code, which a gist that merely *rejected* the flag would also satisfy; these assert the real property, that stderr goes empty while stdout and the exit class do not move, and pin the nesting asymmetry with both lanes firing at once. - **Content transforms** – 22/22 cases pass on each engine across preprocessing, binary input, legacy encodings, and the available gzip, bzip2, xz, zstd, lz4, and Brotli decoders. Every count above shares one denominator ripgrep chose, the tests it wrote and the flags it documents, so each of those 100%s is scoped to cases someone already thought of. The differential fuzzer is the lane with no such ceiling. It generates invocations nobody wrote down, over corpora built to be hostile, and it is the only one that still finds anything. It does, a handful per 6,000 iterations, and that tail is published per root-cause class in `fuzz_baseline.json` and in Layer I of the certificate rather than left out of the scoreboard. A missing fuzz record refuses the mint outright, and the tail is ratcheted shrink-only, so it can fall but never quietly grow. Parallel and serial results are reported separately because they share a contract but not an implementation path; they are not added together to inflate the case count. NA is a deliberate product boundary. SKIP is an accounted companion, boundary, or irreplayable obligation. Neither is called a pass, and with zero FAIL rows the strict `check_results.py` gate is green without `--allow-fail`. Reproduce the cited results from [`bench/conformance/rgsuite`](bench/conformance/rgsuite): python3 run.py python3 modes.py run --mode multiline python3 modes.py run --mode pcre python3 flags.py run python3 transforms.py run python3 fuzz.py --iterations 6000 --seed 20260727 # the residual lane The permanent integration order is documented in [`bench/conformance/gates`](bench/conformance/gates): correctness gates run before performance gates, so a faster wrong answer cannot earn a benchmark win. Performance claims come from the committed fail-closed certificate: fresh processes, 20 measured runs after three warmups, bootstrap 95% confidence intervals on medians, and a Mann–Whitney test. A win requires both a lower median and p < 0.05. On its recorded 20,492-file / 195.8 MiB corpus, gist beat ripgrep in all 12 query classes by 2.10×–7.76×. Those are measurements from the **macroscopic end-to-end layer**, not universal constants. The separately minted lower-bound layer covers a 20,696-file / 199.6 MiB corpus, and those single-thread kernel numbers must not be attached to the end-to-end speedups. ![gist fail-closed statistical certificate forest plot](https://static.pigsec.cn/wp-content/uploads/repos/cas/82/82230155824b70e5ec30405a442405ebaf0dfb6f37397544771ad67d24fcb63e.png) The full data, machine description, losses against other indexed tools, and rerun procedure live with the published receipts in [`bench/certificate/artifact/`](bench/certificate/artifact/README.md). The layers that bound the *engine* rather than the product, meaning the µarch budget, the memory roof, the candidate-byte floor, and the crest rung, are minted in [irregex's own harness][ir-bench]. ## Prior Art Most of the pieces are borrowed and cited. We joined them for one specific job: searching a local, constantly changing tree over and over for coding agents. The positive product case and precise composition claim live in [`CLAIM.md`](research/gist/CLAIM.md). The contribution is that measured composition and the contract around it. The ancestry of the machinery is documented where the machinery lives. Kleene and Thompson through the Pike VM and RE2, PCRE2 for what the linear lane cannot express, Cox's trigram index and the crest sieve that closes its one blind spot, and the Reciprocal Rank Fusion the ranked view is built from are the lineage of the engine gist is powered by, and that lineage is argued in [`irregex`][irregex]. What follows is the ancestry of the *product*: the tools somebody would reach for instead of this one. ### Indexed Neighbors [Zoekt](https://github.com/sourcegraph/zoekt) is the closest production indexed code-search comparison, with positional trigrams, regex planning, ranking, mmapable shards, and a serving layer. GitHub's [Blackbird](https://github.blog/engineering/architecture-optimization/the-technology-behind-githubs-new-code-search/) extends the same family with sparse variable-length n-grams and global-scale sharding. Gist claims neither distributed search nor organization-wide repository synchronization. [Microsoft tgrep](https://github.com/microsoft/tgrep) is the nearest public local-agent shape: a persistent trigram index, file watching, client/server operation, and a grep-like CLI. Gist's distinguishing contract is narrower. Accelerators may decline, while a current-tree subprocess remains capable of answering every supported request. ### Matching Engines The linear lane descends from Thompson's [Regular Expression Search Algorithm](https://doi.org/10.1145/363347.363387) (CACM 1968), the Pike VM, Cox's [Regular Expression Matching Can Be Simple And Fast](https://swtch.com/~rsc/regexp/regexp1.html), and [RE2](https://github.com/google/re2). Unicode range compilation follows the Thompson/Cox UTF-8 decomposition used by RE2 and rust-regex. Complex constructs use the vendored [PCRE2](https://www.pcre.org/current/doc/html/) 10.47 engine with JIT and resource caps. Gist does not claim to make backtracking expressions linear. `-P` deliberately selects PCRE2 semantics, while `--engine auto` keeps the linear engine whenever it can express the pattern. ### Ranking The bounded result view uses weighted Reciprocal Rank Fusion from Cormack, Clarke, and Büttcher, [Reciprocal Rank Fusion Outperforms Condorcet and Individual Rank Learning Methods](https://doi.org/10.1145/1571941.1572114) (SIGIR 2009). Its inputs are language-agnostic text and path signals. A declaration-shaped boost is not a symbol table. gist does not resolve types, references, overloads, or call graphs, and it is not an LSP, SCIP, or semantic-retrieval engine. ### The Codex Subcommand `gist codex` is a thin lifecycle face over the shared compressed self-index (`count` / `find` / shelf status). The Shannon–Manzini / FM-index bibliography and novelty framing live with `relate`, in `relate/research/relate/PRIOR_ART.md` § Corpus quotation, and in `relate/src/kernel/codex` / `relate/src/corpus/index/shelf`. ### Outside the Claim We keep the boundary sharp. Gist is not structural search (Semgrep, ast-grep, Comby), a format-preserving transformation system (OpenRewrite), semantic code intelligence (LSP/SCIP), or a hosted multi-repository platform (Sourcegraph/GitHub Code Search). It is the exact/regex leg those systems and agents can compose with. The full landscape, covering unindexed peers, indexed neighbors, matcher/ranking ancestry, and semantic/structural systems, lives in [`PRIOR_ART.md`](research/gist/PRIOR_ART.md). The positive product thesis lives in [`CLAIM.md`](research/gist/CLAIM.md), and the exact evidence inventory and known losses live in [`TESTING.md`](research/gist/TESTING.md). Codex / Shannon–Manzini literature stays with Relate in `relate/research/relate/PRIOR_ART.md`. Where prose lags implementation, `gist --schema`, the live differential harness, and the committed certificate are authoritative. ## Package Map This repository is the product chassis, and it ships the binary. `gist` is the indexed, rg-parity search, powered by [`irregex`][irregex], where the engines, the index, the corpus walk, the flag grammar, and the warm resident core live. What is here is everything with an opinion about the product: - [`src/exec/session/conduit/`](src/exec/session/conduit/) – the daemon wire: protocol, spawn, vigil. - [`src/exec/session/daemon/`](src/exec/session/daemon/) – the resident session proper: the socket server, request routing, and the client. The answer keep it serves lives in the library's warm core. - [`src/exec/session/warden/`](src/exec/session/warden/) – rationing and standdown, so the daemon never taxes the machine it serves. - [`src/surface/cli/`](src/surface/cli/) – the product vocabulary: flag surfaces, grades, the `--schema`/`--generate` manifest driver, the primer, and reprise. - [`src/surface/face/gist/`](src/surface/face/gist/) – the binary face itself. - [`src/surface/ffi/`](src/surface/ffi/) + [`include/`](include/) – the session C ABI (`libirgx.{a,dylib,so}`, `irgx.h`). - [`bindings/`](bindings/) – Go (cgo), Python (cffi), and Rust consumers of that ABI. - [`editor/vim/`](editor/vim/) – the Vim/Neovim plugin (`:grep`-as-gist, streamed quickfix, `:GistRank`, `:GistBlast`). - [`shell/`](shell/) – the generated man page and bash/zsh/fish/pwsh completions, minted from the same flag table argv is parsed with. - [`bench/`](bench/) – the vs-ripgrep dominance certificate and the ratio gates that keep it honest. ## Build and Test Four steps cover everything the package builds: zig build # gist binary + libirgx → zig-out/ zig build test # the unit suite zig build check # compile-only zig build coverage # per-function coverage The binaries default to ReleaseFast regardless of the build's own optimize mode, which `-Dcli-optimize` overrides. The test binary stays ReleaseSafe, so the suite that tries to break the checks keeps them. Dev model is sibling checkouts. `build.zig.zon` path-deps on `../irregex` and `../relate`, and releases pin url + hash; a consuming monorepo may wrap `zig build` to symlink the binaries onto PATH. ### Running One Test `-Dtest-filter=` narrows the suite and `-Dtest-shards=1` puts it back into one process. The harness is `brigade.zig`, which this package takes from the irregex dependency rather than owning, so the trap below is the same one that repository documents at more length. It is restated here because you will hit it here, running these tests. The trap is that `zig build test` caches the test run, and the environment is part of the cache key. The filter reaches the harness as `BRIGADE_FILTER`, an environment variable set on the run step, and Zig hashes a run step's environment along with its argv. First run under a given environment executes. Every later run under an environment already used is served from cache, so the step is skipped, nothing executes, and it exits 0 in about the time a no-op build takes (~0.3 s here). A cache hit still reports a test count, which is what makes it dangerous. `--summary all` prints `1/1 tests passed` either way, and the only token that distinguishes them is `cached` against `success ms`: +- test shard 0/1 success 3ms # ran +- test shard 0/1 cached # did NOT run, still "1/1 tests passed" So `zig build test` cannot answer whether the tree is sensitive to an environment variable. The natural probe runs with the variable, then without it to confirm, and the confirming leg revisits an environment it has already seen, making it a replay that is green by construction. To probe an environment variable, drive the compiled binary directly, since it has no build-cache layer and executes every time: env FORCE=$RANDOM zig build test -Dtest-filter='' -Dtest-shards=1 --verbose # ... BRIGADE_SHARD=0/1 BRIGADE_FILTER= ./.zig-cache/o//test BRIGADE_SHARD=0/1 BRIGADE_FILTER='' BRIGADE_TIMES=1 \ ./.zig-cache/o//test `BRIGADE_TIMES=1` prints one line per test, which is the evidence a run happened. A filter matching nothing fails loudly rather than passing empty, so a stale filter cannot read as a clean run. ## Provenance Extracted from a private monorepo kernel package, cut at ce430bbaab. The cut line is ripgrep's. What `rg`-the-binary owns, meaning the daemon, the product vocabulary, distribution, and the certificate, lives here; what the `grep-*` crates own, meaning engines, walker, index, and argv, lives in the library. Architecture is machine-checked by [`contract/gist.ward`](contract/gist.ward). Apache-2.0; nothing third-party is bundled here, and the certificate measures competitors by invoking installed binaries.
标签:SOC Prime, 代码搜索, 可视化界面, 开发工具, 日志审计, 索引搜索, 逆向工具