Skip to content

Latest commit

 

History

History
43 lines (38 loc) · 53.2 KB

cli.md

File metadata and controls

43 lines (38 loc) · 53.2 KB

CLI reference

This file is the home of the command-line reference: every lci subcommand, its options and its output.

CLI

The CLI calls the same application logic and needs no editor. Its commits and a running server on the same data directory are serialized by the index lock; a separate data_dir (--config with its own) keeps CLI experiments from waiting on, or refreshing, the server's indexes. Subcommands: serve, config, init, index, status, list-files, search, symbols, definition, references, evaluate, compare, replay, traverse, recipe.

lci=./target/debug/local-code-intelligence   # local-code-intelligence.exe on Windows
repo=$PWD                                    # this repository: the walkthrough indexes LCI's own source
"$lci" index "$repo"
"$lci" status "$repo"
"$lci" list-files "$repo"
"$lci" search "$repo" 'plain REST JSON-over-HTTP mirror of the MCP tools'
"$lci" symbols "$repo" 'openapi_document#'
"$lci" definition "$repo" 'src/rest.rs' 377 21
"$lci" references "$repo" 'src/rest.rs' 188 3 --include-declaration
"$lci" references "$repo" 'lci-core/src/workspace.rs' 17 11 --include-declaration
"$lci" index "$repo"
  • The walkthrough needs only a clone, a build and the model services (see Model services in the README); start it once curl http://127.0.0.1:8768/ready answers HTTP 200 (HTTP 503 names the missing dependency). search returns openapi_document in src/rest.rs as its top hit (the function building the document served at /openapi.json). definition starts from the call in the /openapi.json route at the end of src/rest.rs and resolves to that function's declaration; the first references starts from the declaration and returns it and the wiring; the second starts from Workspace::resolve and crosses crates (callers in src/app/ and tests/cases/). Lines are one-based and characters zero-based UTF-16; the numbers are this repository's own, so if a file shifted, grep -n openapi_document src/rest.rs gives the current ones. The second index should report zero new embeddings when source and model configuration are unchanged. cargo xtask acceptance core runs the same index/search/repeat flow and checks that production implementation chunks, including openapi_document, dominate the top eight. Another repository is just a different first argument.
  • config shows how the configuration resolves without opening the application: "$lci" --config config.toml config [--json]. init writes the overlay: init --dry-run (print only), init (write config.local.toml beside config.toml; exit 3 if it exists), init --force --no-probe (replace it, send no request).
  • config --json prints one object: valid and error; layers (file with path and named_by (--config or LCI_CONFIG); local with path, state of present, absent or disabled, and named_by; env, the machine-set variables that were set; flags); values, every setting keyed by its dotted name (port, typescript.path, index.freshness; the [search_profiles.<name>] tables are one entry, search_profiles, when configured) with value, source (default, file, local, env, flag) and from; config, the whole resolved configuration; derived (mcp_url, mcp_profile_urls, rest_base, health_url, ready_url, openapi_url, embedding_identity, run_profile_hash); environment.other (whether LCI_CANDIDATE_DUMP is set). Exit status: 0 valid; 2 resolves but fails validation (for example a reserved port; the report is still printed with valid false); 1 when a layer cannot be read or parsed (a missing file, a non-machine key in the overlay, a malformed LCI_* value; only valid and error are printed with --json).
  • search takes repeatable --language, --include-path, --exclude-path, --source-role, plus --top-k and --search-profile <name> (the profile's settings applied over [channels] and rerank_candidate_count; the response then carries "search_profile"; an unknown name exits with an error listing the configured names, before any index work). Path filters are case-sensitive repository-relative globs (so --include-path '*.rs' does not match an indexed Lib.RS, while --language rust does): * and ? stay within one path component, ** crosses directories. Absolute paths, backslashes, traversal components, empty patterns and malformed globs are rejected. An explicitly empty include list is a valid filter returning no results. Filters apply to every retrieval channel and are reported back as effective filters; no result may bypass them.
  • Source roles are deterministic. Precedence: generated, test, benchmark, example, documentation, configuration, source. Tests are recognized by test/tests/__tests__ directories, the Gradle/Android source sets androidTest/integrationTest/testFixtures, and per-language file names: Python test_*.py/*_test.py; JS/TS *.test.*/*.spec.*; C# *Test(s).cs; Go *_test.go; Rust tests.rs/test_*.rs/*_test.rs/*_tests.rs (a bare test.rs stays source, being a legitimate production module name; inline #[cfg(test)] mod tests cannot be told from the path); Java *Test/*Tests/*TestCase/*IT.java; C/C++ *_test/*_tests/*_unittest(s)/test_* on the C and C++ extensions. Layout directories (examples, docs, config, scripts, tools, build, ...) count only above a Maven/Gradle source root (under src/<set>/java/, or kotlin/scala/groovy, the remaining directories are package names, so com/example/... is not an example), and a config/configs/configuration directory marks only non-source files, so internal/config/loader.go stays source. Production source receives a small transparent 0.004 fusion prior; all other roles receive zero. Role and prior appear in result metadata; explicit role filters stay authoritative.
  • Traversal and recipes (SCIP-backed, see engine.md): "$lci" traverse --scip <index> [--root <tree>] --question '<text>' walks the code graph of a SCIP index (rust-analyzer scip) from the start nodes the question finds (with --root, the top hits of the hybrid search over that tree, mapped to the index's symbols; otherwise a stand-in matching the question's words against symbol names, reported as seed_source) and prints one JSON object (reached nodes, totals, handler decisions, and when it stopped to ask or ran out of budget a handle and checkpoint; continue with --handle and --steer). It ignores [traverse] enabled (which gates only the MCP mode) and is limited by the [traverse] caps. With --root, the graph also has an implements edge from each trait-impl member to its trait method declaration (read from the impl .. for .. headers; rust-analyzer writes no SCIP relationships), and --maps scip.implements adds the traits a type implements, the types that implement a trait and the members that implement a trait method; derives, macro_rules! impls and traits the index does not define give no edge. With scip.implements on, a trait path or Trait::method in a code span of the question (`tauri::Manager`, `Runtime::run`) starts the walk at that trait or trait method declaration (the response then has trait_seeds, how many start nodes they are, and seed_source traits, or traits+hybrid, traits+hybrid+bm25, traits+bm25 or traits+name_tokens when the other start nodes stay behind them: they are replaced when the path matched with its qualifiers, kept when it matched by the last segment alone; --seeds still wins; --trait-seeds replace|prepend|auto (default auto, as described) picks the mode), and --seeds takes the node ids the map prints; without the map the start nodes are unchanged. Selectors include --maps, --policy (a preset: explore, impact, flow, where-to, history; or JSON; the policy is the router: it sets the walk or first-stage answer and the maps of the question type, see engine.md Presets), --first-stage, --pops, --max-depth, --top-k, --out. "$lci" recipe <name> --root <tree> [--scip <index>] [--args <json>] runs one recipe (config_readers, impact_rings, flow_path, flow_from, config_neighbours, lci_compose); it ignores [recipes] enabled and [compose] enabled. config_readers, config_neighbours and an lci_compose config.key step need rg on PATH.
  • Map freshness ([run_profile.traverse] map_freshness = true, default false): a traversal answer (MCP mode = "traverse", lci traverse) then has maps, one entry {id, observed, stale, lag} for each map of the call that states its freshness, in registration order; with the key off the answer has no maps key and is unchanged. The SCIP-built maps (graph.scip, structure.contains, seeds.scip_names, seeds.bm25, seeds.repo_map, exact.closure, config.key, scip.impact, scip.implements) have observed = "snapshot"; tests.tested_by reads the same impact index but states no freshness yet, so it has no maps entry. The maps that read the tree or git when they answer (grep.live, grep.structural, lsp.live, git.history, exact.pickaxe, probe.compile) have observed = "live", stale = false and lag = 0; live names the channel, not a guarantee about every byte. grep.live, grep.structural, git.history, exact.pickaxe and probe.compile read the tree or git when they answer (current bytes, or the current git HEAD). lsp.live asks a running language server instead of the index: a fresh server (one this call started) reads the disk as it is, but a server kept across calls ([run_profile.lsp] sessions = "shared") is only as fresh as its tree stamp's coverage (see the language-server sessions item below), and its answer carries a note lsp.live: <server> kept session; tree stamp unchanged since <n> s ago; when that note is absent the walk used no server it found running and was served no cached outline from one ([run_profile.lsp] outline_cache keeps a file's outline per server session, and an outline served from that cache counts as a use of that session: the walk gets the note and its lsp.live cards are unverified, unless the walk itself started the session). grep.live, grep.structural and lsp.live also place what they read by the index's positions, so a line shift since the index is not covered by their live. The emitted fields are the same either way. While an arbitration ran in the call ([run_profile.scip] arbitration, see "Arbitration of SCIP edges" below) the entries of the maps it judged also carry role: "authority" for lsp.live and "cache" for each SCIP-built map it judged (scip.impact, scip.implements, structure.contains); without an arbitration no entry has a role key. lag is a count of files, never wall time: the source files the language adapters index (prose, such as Markdown, is left out) that are newer than the index file (modified time after the index file's), plus those that changed since this process first saw the index's current stamp (modified time and length): a file whose content hash changed, added or removed, each file counted once however often it is edited. A touch without a content change does not count. stale is true when lag is above 0, so an index rewritten with a new stamp starts again from the files newer than it. lag is null and stale false (meaning not compared, never fresh) when the call has no tree (no --root), the tree could not be read, or the index file has no modified time; a note in the answer says which. Not seen: a change that left a file older than the index file before this process first saw the stamp, and a change that kept the file's content hash. Every call reads and hashes the tree's source files (no chunk is made), unless [run_profile.freshness] stat_gate is on (see "Stat gate" below). The maps seeds.hybrid, search.lexical and search.vector state nothing and are absent from maps, unless [run_profile.freshness] verify_on_read is also on (see "Verify on read" below). The key is read from the run profile the call is given, so the recipes (whose answers carry no maps) take it too: flow_from's inner walk, with the key on, reads the tree and discards the comparison.
  • Answer labels ([run_profile.traverse] answer_labels = true, default false): every traverse answer (a walk, a continuation, an answer with no start node) gains completeness, {complete, why, estimate}: complete is true exactly when status is complete (the walk ended complete or bound, the top-k set is final) and no map the call asked for was lost; a map that could not be set up (for example lsp.live with no server) or that had failed calls makes it false, with why degraded (a cut or capped walk keeps its own why) and degraded listing the maps, and a note naming them; why is otherwise the answer's stop_reason (no_seeds without a walk) and estimate is the fraction of the likely_complete hint (the mark-recapture estimate over what the maps found; null until two maps share a node; maps over one graph read high, so it is a hint, never complete); a partial answer also gets one notes line that names the reason and the handle. Every card of results and of the checkpoint's answer, visited_cards and frontier gains freshness, one of fresh, changed, stale, unverified: changed when file_changed is true, fresh when it is false (the card's own file is as its maps read it, so its text is current even under a stale snapshot), else from maps: any map of the card (maps and via) that is stale gives stale, every one found with a lag gives fresh, anything else is unverified, which is what every card says with map_freshness and verify_on_read off. One exception, for lsp.live: when the walk found a language server running (a session kept across calls under [run_profile.lsp] sessions = "shared", the case the lsp.live: <server> kept session note reports) every card that cites lsp.live is unverified, never fresh, because nothing checks what a kept server read (a changed or stale finding stands); a card from a server this walk started keeps fresh. The rule is per walk, not per card: with several servers, any reused one makes all lsp.live cards unverified. A continuation (handle) cannot tell whether the earlier call used a kept server, and the checkpoint format carries no such flag, so every card of a node restored from the checkpoint that cites lsp.live stays unverified; the cards of nodes the continuation's own walk reaches follow the rule above. Nothing is withheld with either value (results are the same), and the trace, the checkpoint hash, the handle and text do not change; with the key off the answer is byte-for-byte as without it. In the profile hash; recipe answers have no labels.
  • Paths on cards ([run_profile.traverse] card_paths = true, default false): every card of a traversal answer's results and of the checkpoint's answer that the walk reached then has kind, the kind of the edge that discovered the node (null for a start node), and path, the route from its start node: one {node, map, kind, witness} entry per node (depth + 1 entries, the first is the start node with kind and witness null, the last is the card itself and repeats its via and witness). The route is the walk's final one (a stronger route found later replaces a weaker one). The cards of visited_cards and frontier, the cards of the first stage (origin exact or seed), the trace and the checkpoint hash are unchanged; with the key off nothing in the answer, the trace, the checkpoint or the handle changes. A handle made with the key on also keeps each state's kind (an additive key; the handle format version is unchanged), so a continuation shows whole chains; a handle made with it off has no kinds for its earlier hops, which show kind: null, and a note says so. Size, measured as the answer's characters divided by 4: on the unit-test fixture (4 cards, depth up to 2, a complete walk) 616 tokens off, 856 on; on one real walk (lci traverse with graph.scip over an older index of this repository, one start node, 8 cards at depth up to 3, ended by the pop cap with a checkpoint) 3807 tokens off, 5160 on, 35.6 percent more, because the checkpoint's answer repeats the cards; the results list alone grows 87.5 percent (3095 to 5802 characters), about 85 tokens per card per list. Cards at greater depth cost more, one entry of about 25 tokens per hop.
  • Language-server sessions of lsp.live ([run_profile.lsp] sessions, "shared" default or "per_call"; it matters only when a call runs lsp.live, which a call with a tree under shared and the service's manager (MCP and REST mode = "traverse") runs by default, [run_profile.lsp] live = true, and which the CLI one-shot lci traverse and per_call run only when --maps or a [traverse.policies] entry names it, because a server that dies with the call cannot amortise its start; with no language server installed, or one that will not start or is refused, the walk says so in a note and goes on without the map; live = false makes the map opt-in everywhere, and a call that names its maps is never changed by the key): under per_call every traversal call starts its own language servers (a cold start and a version probe of each enabled server) and stops them when its walk ends. Under shared (the default) the calls of one process may keep a server across calls, but only a server that declares the configuration files its tree stamp covers (LspAdapter::stamp_manifests; every adapter but jdtls and marksman does) and either sees an edit on its own, today rust-analyzer alone (LspAdapter::watches_workspace; its client capabilities here declare no workspace.didChangeWatchedFiles), or is told by [run_profile.lsp] sync = "watch", which is the default since K11b-4. So by default the service's manager keeps every server but jdtls and marksman (rust-analyzer, csharp-ls, typescript-language-server, pyright, gopls, clangd), and a service with [traverse] enabled = true primes the ones of the index's languages at the first touch of a workspace. With sync = "off" rust-analyzer alone is kept, and every other server (csharp-ls and typescript-language-server are told nothing about an edit; pyright, gopls and clangd are resent only the file a request names) is started by each call on that call's own manager and stopped when its walk ends, exactly as under per_call; jdtls lists no configuration files and is never kept, whatever sync says. The reason for the default: a server started per walk cost 266 to 524 ms per expansion on python, typescript, go and csharp trees against 2 to 5 ms for a kept one, with identical cards (a measurement on those trees, not a bound). A kept server does not rely on its own file watcher or on the sync for correctness: each walk that uses it first passes a tree barrier. The barrier takes a stamp of the tree from the index's file list and one directory read per stamped directory, with no recursive walk: the server's indexed files (modification time and length); every directory that holds one and its ancestors up to the workspace root (modification time and child names); one level of child directories of those (modification time), except version-control and build-output directories (.git, target, node_modules, obj, bin, build, dist, out, virtual environments), so a file created in an existing unindexed src/generated/ that a #[path] attribute names is seen; and the server's configuration files among those children, matched by name (rust-analyzer: Cargo.toml, Cargo.lock, rust-analyzer.toml; typescript-language-server: tsconfig.json, tsconfig.*.json, jsconfig.json, package.json; pyright: pyrightconfig.json, pyproject.toml, setup.cfg; gopls: go.mod, go.sum, go.work; clangd: compile_commands.json, compile_flags.txt, .clangd, CMakeLists.txt; csharp-ls: solution and project files, Directory.Build.*, Directory.Packages.props, global.json, nuget.config). Only "not found" counts as absent: any other failure to stat or read makes the stamp fail, and the server is then restarted and nothing is recorded. Each walk passes the barrier before the server is used (a walk that runs lsp.live on the shared manager passes it twice, once before its seed search and once in the backend, so it restarts a stale server once: the second restarts it again only if the stamp changed between the two; one at a time per workspace, so a slower barrier cannot record an older stamp over a newer one) and compares the stamp with the one recorded when the server last started or passed; when it differs, or the server was started by a call that passed no barrier, the server is stopped and the walk starts a fresh one that reads the disk, and the walk says so in a note (lsp.live: <server> restarted: the tree changed since its last call). An unchanged tree keeps the server, and the walk says that too (lsp.live: <server> kept session; tree stamp unchanged since <n> s ago), so an answer from a reused server is distinguishable from a fresh per_call one. The note comes from the session the walk's requests actually acquired (found running, or started by that walk), not from the barrier, so it is also there when the server was started by an overlapping walk that passed its barrier at the same time. The rule: a reused server is never staler than per_call for any change inside the stamp's coverage. It is not an absolute guarantee (that would need a full content walk, which cost 1.5 s on deno): the residuals are a new file two or more levels below a stamped directory or inside a skipped build-output directory; a configuration file that is not on the server's list; an edit that keeps both modification time and length; and an edit made during the call, which per_call has too. sync = "watch" does not avoid a restart after an edit; it tells a running server about the edit, which is what makes a non-watching server worth keeping, and it narrows the residuals above for navigation and the search channel. Two residuals follow from the default: a workspace whose platform watcher and poll fallback both failed, or whose Windows watcher silently stopped, keeps a non-watching server on the strength of the tree stamp alone, so it is told nothing but still passes the barrier (the poll fallback itself scans the tree every watch_poll_milliseconds, which is the expensive case on a very large or remote tree); and the search channel restarts a synced non-watching server after an edit, a loss of the channel for a few seconds that rust-analyzer's exception below avoids. A workspace with no index snapshot has no file list and so no sharing: the server is restarted by every walk. On a large repository the stamp costs tens of milliseconds (decision d454), plus one read of the workspace manifest after each index change; navigation and the search channel pass the same barrier, once per call, and carry the same notes (lsp: <server> restarted: ... and lsp: <server> kept session; ... in the notes of a navigation answer and of a search report; a server the call started adds none). The one exception is the search channel and a server that watches its workspace (rust-analyzer): an edit would restart it and the next searches would lose the channel while it loads, so the search keeps its session through a changed tree and says lsp: rust-analyzer kept session; <reason> (kept: it watches its workspace), with no tree stamp recorded for it, so the next navigation call or traversal still restarts it; that answer is as fresh as the server's own watcher, not as the stamp, so it can miss an edit inside the watcher's latency and a Cargo.toml or Cargo.lock edit while the server reloads its workspace. Off switch: sessions = "per_call" (it moves the profile hash); lsp_idle_timeout_seconds only limits how long an unused server stays, and a kept server holds memory while it runs. Peak resident size measured during walks (a measurement on the benchmark trees, neither a limit nor an idle figure): rust-analyzer about 4 GB, typescript-language-server about 460 MB, csharp-ls about 190 MB, pyright and gopls about 120 MB each; a workspace in all of those languages adds about 0.9 GB (the sum of the four non-rust-analyzer figures) to rust-analyzer's 4 GB. The idle timeout (default 1800 s, 0 never releases) releases a session that has started and primed, or whose start failed, once nobody has used it for that long; a start or priming still in progress is not released, so it bounds the residency of servers that finish starting, not of one that is slow or stuck starting. The kept servers are the service's own manager's, the one that navigation (find_definition, find_references, search_symbols) and the search channel use, so a server started by any of them serves the others; lci traverse is one call per process, so there shared differs from per_call only in that the kept servers stop cleanly when the walk ends; its manager is built without the sync, so there only rust-analyzer is kept and every other server starts per call. A shared server stops like any other of the service: when it has been idle for lsp_idle_timeout_seconds, on release_workspace, when its session is replaced (stuck or failed) and at service shutdown (Ctrl+C, SIGTERM); the end of a walk does not stop it. The version probe of a server binary runs once per manager under shared ([run_profile.lsp] breaker is true by default: a provider whose version probe failed (a binary that is missing, or a probe that timed out, opens no window: that is no evidence of a dead server), or whose server failed to start without timing out, is skipped for 30 seconds, with a note that says so, by the service's manager and each call's alike; a server already running is still asked, a start that timed out opens no window, and /ready still probes. With breaker = false a failed probe is run again by the next call and a dead server is started again). The recipes (flow_from and the other lci_* tools) walk only graph.scip and structure.contains, never lsp.live, so no language server starts under a recipe and they run the same under both values; the key still reaches them in their run profile, so the profile hash they carry is the one that ran. [run_profile.lsp] implementations (default false) adds a third request to each symbol node of an lsp.live expansion, textDocument/implementation, behind the same max_external_calls gate as the other two: its answers become implements edges from the node to the symbols (or files) at the places the server names, the direction scip.implements uses (an interface or trait member to the members that implement it). The place table is the SCIP index's, whose nodes are functions and methods, so the request is asked at an interface or trait member, not yet at the interface or trait itself. An error reply from the implementation request is noted and costs the other two requests' edges nothing (a hang trips the call's deadline and clears them all). The recipes ask no implementations either way. [run_profile.lsp] call_hierarchy (default false) adds up to three more requests to each symbol node, each behind the same gate and counted: textDocument/prepareCallHierarchy at the node's name, then callHierarchy/incomingCalls and callHierarchy/outgoingCalls of the item that answers (an empty prepare ends it, so one more call at least). Callers and callees become calls_or_refs edges (no new edge kind), one per destination, whichever way the call runs, seen at the call itself; a symbol or file that definition or references already named keeps that edge. An error reply (a server without call hierarchy answers -32601) is noted and costs the other edges nothing; a failed prepare ends the block and a failed incoming request still asks outgoing. Servers miss some calls (rust-analyzer's call hierarchy missed 16% of the call pairs the SCIP index has, in macro arguments), so this complements graph.scip. The recipes ask no call hierarchy either way.
  • Verify on read ([run_profile.freshness] verify_on_read = true, default false): with map_freshness also on, a traversal answer checks what it cites against the working tree by content hash. It reads and labels; it never refreshes the index and never changes [index].freshness (under manual the snapshot stays as old as it was). It reuses the one read of the tree that map_freshness makes (no second scan), and with the key off every answer is byte-identical to before. What it adds: (1) the maps that answer from the index snapshot (seeds.hybrid, search.lexical, search.vector) get a maps entry {id, observed: "snapshot", stale, lag} whose lag counts the source files that differ between the snapshot's manifest and the tree (changed, added, or removed; the manifest's prose entries are not counted, as the tree scan leaves prose out). It is read after the walk, because the walk's first search may refresh the snapshot under on-search (a refresh that lands between a map's read and this manifest read, as under [index].freshness = "watch", makes the answer look as current as the new snapshot); null (not compared) when the call has no tree or no manifest. (2) grep.live and lsp.live keep observed: "live" (grep.live reads current bytes; lsp.live queries a language server, which reads the disk when this call started it and is bounded by its tree stamp's coverage when kept, see the language-server sessions item and the kept session note) and gain places: "index": where they put what they read comes from the SCIP index, so their stale and lag are the SCIP index's (the same count as the SCIP-built maps), and a line shift since the index shows as stale. They are the only entries with places. (3) Every card (results, and the checkpoint's answer, visited_cards and frontier) gets file_changed: true when the content hash of the file it cites (its site.path, else its node's path) now differs from the hash its maps read, false when it does not, null when no reference applies (a commit or other card with no file, a file that is not a source file, or no tree was read; one note says how many cards are null). true also covers a file that was newer than the index file when this process first saw the index's stamp: its hash at index time is unknown, so it counts as changed whatever its content is. The reference is the SCIP baseline for the maps built from the index, for grep.live and lsp.live, and for a symbol card (its site is the index's, whichever map reached it), the snapshot manifest for seeds.hybrid, search.lexical and search.vector and for a chunk card; a card that names both is true when either differs. A search_code / search report (JSON; the compact text output is unchanged) gets a freshness block with the key on, with or without map_freshness: snapshot ({stale, lag}: the source files that differ between the snapshot's manifest, read after the search, and the tree, which one read of the tree's content hashes gives, gated by stat_gate when it is on), channels ({id, observed, stale, lag} for the channels that ran, so a channel that was off or not reached (no language server applies, no vectors) is not listed, and one that failed is listed with a note (failed; see warning) and no lag, because it answered nothing: vec.chunk.code and lexical are observed: "snapshot" with the snapshot's lag; lsp.symbols is observed: "session" with stale and lag null and a note, because the language server's view of the tree is not compared), and hits (one {path, file_changed} per result in order; file_changed is null for a hit no snapshot channel returned (only the language server, or no channel named), or when the tree was not compared, which a note says). The block is read after the search, so a refresh of the snapshot that lands between the search and that read ([index].freshness = "watch") is not seen: the answer then looks as current as the new snapshot. The scan costs timings.verify_ms (absent when the key is off), and only a search a caller asks for pays it: the searches that find a traversal's start nodes and the evaluation runner do not scan. Under manual or watch a search never refreshes the snapshot, so the block is how a caller sees that it is behind; under on-search the search has just refreshed it and the lag is 0. A search with the key off has no freshness key. The recipes take the key through the run profile but their answers carry none of this.
  • Stat gate ([run_profile.freshness] stat_gate = true, default false: every read of the tree for map_freshness or verify_on_read reads and hashes every source file). With true the process keeps each source file's modified time, length and content hash per root, lists the tree without reading it, and reads and hashes only the files whose modified time or length is new or changed (a file with no modified time is always read; a file skipped by its content, such as a Qt Linguist catalog saved as .ts, stays skipped while its stamp stays). The labels and file_changed equal the full-hash path's, through the same hash function, with one known gap: a same-length edit that keeps a modified time older than 3 seconds before the previous read of the file (a deliberate set_modified, or a clock that went back) is not seen until that file's next edit. A file modified within 3 seconds of a read is not trusted and is read again on the next call, which covers a write in the same clock tick and the coarse stamps of FAT, exFAT and SMB shares. The table lives in the process and is rebuilt on the first call; the index scan is not gated. The key is in the profile hash.
  • Outline cache of lsp.live ([run_profile.lsp] outline_cache, default false: every file outline the map needs, a textDocument/documentSymbol request, goes to the server). With true the cache works only for the servers that hold a file's current text at every request: pyright, gopls, jdtls, clangd and typescript-language-server, whose adapters send the file's text from disk before each request when it differs from what the session last sent for that file: a textDocument/didOpen the first time, then a textDocument/didChange carrying the whole text when the server advertised change sync in its initialize result (full or incremental; LSP 3.17 reads a change without a range as the whole text), and a second didOpen when it advertised none (pyright logs the repeated open and applies it; gopls overwrites its overlay; jdtls replaces the working copy's buffer; clangd's open replaces the contents; typescript-language-server refuses a second open, which is why the change matters for it). A session holds at most 64 open documents per server (a size, not a key): a new file past that closes the least recently used one with textDocument/didClose first, and a closed file is opened again, at version 1, when it is next navigated. A text is sent once, and a request that follows a send waits for the server's publishDiagnostics for the file (at most the request timeout and 3 s) before it goes out. For those the language-server manager reads and hashes the file (the content hash the index uses, sha-256 over up to 16 MiB, on every outline request) before it asks, and keeps each non-empty outline under that hash and the server session that answered; the same file is then answered from memory while its bytes and that session are unchanged. An edited file is asked again and gets the outline of the new text, and so is any file after the server was restarted, released, replaced or stopped idle. An empty outline or an error is never kept (a server still loading answers []), and neither is an answer for a file whose bytes changed during the request, an answer from a session that is no longer the live one, a file that is not UTF-8 or one over the 16 MiB open limit. The other servers are asked every time, as without the key: rust-analyzer watches the disk itself and can answer from the text before an edit for a moment after it, and csharp-ls is sent no document text. A manager keeps at most 4096 outlines; a new one past that clears them. The cache belongs to the manager: under sessions = "per_call", and for a server that a "shared" walk starts on its own manager (every one of these with sync = "off", jdtls and marksman always), it lasts one walk; in the service's manager under "shared" with sync = "watch" (the default) it serves later calls too. The recipes never run lsp.live and run the same under both values.
  • Language-server sync ([run_profile.lsp] sync, default "watch" since K11b-4; "off": a language server walks the tree when it starts and is told nothing after, so a long-lived session answers search_symbols, the search channel and a shared lsp.live from the tree it started on, and a server that does not see an edit on its own is not kept across traverse calls). With "watch" the service's language-server manager (the MCP and REST server's, which navigation, the search channel and sessions = "shared" traversals use; not the one-shot lci traverse's, which is built without the sync because its process ends with the walk) watches each workspace that has a running session and sends every change of a source file to that workspace's servers as one workspace/didChangeWatchedFiles (created, changed or deleted, as the file is on disk), and drops the changed files' kept outlines. The watcher is the platform's (ReadDirectoryChangesW, inotify, FSEvents), or a poll every watch_poll_milliseconds when it cannot start (the poll compares modification times in whole seconds, so a second write within the same second is sent only with a later change). It starts with the workspace's first session, walks the tree once to know its files, walks again only when the platform reports lost events (inotify and FSEvents do; on Windows a watcher overflow during a large burst, such as a big checkout, is not reported and silently ends the watching until the workspace's sessions stop, roadmap N102), and stops when the workspace's last session stops. Single changes are not filtered by .gitignore (a source file written under an ignored directory, such as target/, is sent), and a renamed or moved directory is seen as the directory alone, so the files inside it are not sent. Before each request the manager sends what the watcher has already reported: the change notification goes out before the request, but the manager does not wait for the server to apply it, so the answer to that request may still be from the old text; an event the watcher reports after that send goes out before the first request that drains after the report, and an edit the platform has not reported yet is in a later answer. It never waits for a walk. Prose files are not sent, nor anything under .git. It is independent of index watching (watch): a workspace can be watched for the index, synced, both or neither. A per-call manager (sessions = "per_call", and the one a "shared" walk uses for a server that does not see an edit on its own) never syncs: its servers start on the tree as it is and stop with the walk, so there the key changes nothing but the profile hash. A shared traverse walk passes the tree barrier (see "Language-server sessions of lsp.live") and restarts a kept server after an edit whatever the sync did, so for lsp.live the sync does not avoid a restart: it makes a non-watching server worth keeping, and it covers what the barrier's stamp does not (a new file two or more levels below a stamped directory; an edit that keeps modification time and length) for the answers of a kept server. Navigation and the search channel pass the same barrier, so the sync does not decide whether they answer from the tree as it is either. The cost of the default: one watcher thread and one task per workspace with a running session, one table of (modification time, length) from one walk of the tree, and under the poll fallback a scan of the tree every watch_poll_milliseconds. When both the platform watcher and the poll fail to start, the servers are told nothing and a warning is logged. The recipes never run lsp.live and run the same under both values.
  • Place table of lsp.live ([run_profile.lsp] places, default "auto": per walk, "scip" when the SCIP graph read for the workspace has at least one node (the walk is then the "scip" walk), else "outline", so a tree with no index read, scip.mode = "off" or "cache" with no file, still gets lsp.live cards. The choice is per workspace, not per language: an index with nodes that lacks a language of the tree (a Rust index in a tree with Python files) still takes "scip" places, and that language gets no lsp.live cards while its server still starts; set "outline" for such a tree; "scip": the SCIP index's table alone, so a symbol the index has no node for has no place, lsp.live asks nothing about it, and under scip.mode = "off" it asks nothing about any symbol; a file seed still gets its textDocument/documentSymbol outline, but its members are named only when the index defines them). With "outline" the table is the index's plus the language server's own outline of a file: the first time a walk expands a symbol the index places in a file, or expands the file, lsp.live asks the server for the file's textDocument/documentSymbol (one more external call per distinct file and walk; a failed request keeps the index's place and is not repeated) and from then on the outline's places win in that file, so lines inserted since the SCIP file was written no longer make it ask at the wrong line. An outline member takes the index's id for the same symbol when the index has one (same file and name, a function or method of either kind, nearest line; one symbol keeps one id across the layers), else an id of the file's real language with no container (sym:csharp:Shapes.cs::Area/method, equal names told apart in document order: ~1, ~2), so under scip.mode = "off" the functions, methods, types (class as class, struct as struct, interface as interface, except that rust-analyzer's trait stays trait in a Rust file), enums, modules, constants, fields (a property is a field) and variables of an outlined file have ids (sym:csharp:Shapes.cs::Circle/class). Enum members, impl blocks and type parameters have none. A file expansion lists the fields as Contains destinations too, and a reference on a field's line now resolves to the field; lsp.live asks a field or variable like any other symbol (a server may answer implementation for an interface property or a call hierarchy for a const f = () => {} it reports as a variable). A handle saved under outline by an earlier build holds struct and trait ids for class-language members, which no longer match a learned place. A seed that is a symbol id of a file this walk has not outlined still has no place. Every documentSymbol range now ends on the item's last line (the nested DocumentSymbol shape used to give the name's line), which Contains edges read from the name's own line first. The recipes never run lsp.live and run the same under all three values. The place table also bounds what an arbitration can confirm (next item): the index's table holds functions and methods only.
  • Language-server roots ([run_profile.lsp] roots, default "workspace": every server starts at the workspace path, as always). With "manifest" a server starts at the directory of the topmost project file its adapter names (the list of the tree stamp above: go.mod, go.sum and go.work for gopls; pyrightconfig.json, pyproject.toml and setup.cfg for pyright; tsconfig.json, jsconfig.json and package.json for typescript-language-server; solution and project files for csharp-ls; and so on), so a module under services/api/ is served from services/api/ and its rootUri, workspaceFolders and working directory say so. The project files are looked for in the directories of the index's files of the server's languages and in their ancestors up to the workspace, one directory read each (the index's file list holds source files only, so it names no project file), with no recursive walk. The shallowest one decides. The workspace is kept, and index_status says why in servers[].root_reason, when no project file is found (no_manifest), when sibling directories at the same depth each hold one (several_roots:<n>: one session per module is not built, so a monorepo of sibling modules keeps today's root), when the chosen directory would leave an indexed file of the server's languages outside it (files_outside_root:<n>), when the index has no file list (no_file_list), when a directory cannot be read (unreadable) and for a server that is disabled, unconfigured or serves no language of the index (not_served, reported under "manifest" only; under the default every server reports roots_workspace). A session fixes its root when it starts and keeps it. The one-shot lci traverse under sessions = "per_call" has no index file list, so under "manifest" its servers start at the workspace (no_file_list) and its output does not say so; the service always has the list. There is still one session per server per workspace, answers still name paths relative to the workspace, and the tree stamp and the barrier still read the whole workspace. The recipes never run lsp.live and run the same under both values. What was measured (a premise check on small fixtures, pyright 1.1.414 and typescript-language-server 6.0.2, not a general bound): "manifest" helps pyright when a name resolves only through a nested project's own settings, such as extraPaths in its pyproject.toml (the definition is empty from the workspace root and found from the project directory); plain imports in a src layout resolve from either root. typescript-language-server already finds the nearest tsconfig.json from the workspace root, paths aliases included, so "manifest" gains nothing for it. Any indexed file of the server's languages outside the project directory, even one script at the top of the repository, sends "manifest" back to the workspace root (files_outside_root:<n> in the roster), so the gain appears only when every such file lies under the project. gopls and csharp-ls, other pyright effects of the root and cost were not measured. The default stays "workspace".
  • Arbitration of SCIP edges ([run_profile.scip] arbitration, default false): with true, lsp.live in the call, scip.mode not "authoritative" and at least one of scip.impact, scip.implements (and only with [run_profile.lsp] implementations = true) or structure.contains registered and built, the server is the authority over those maps before fusion. Per expansion and claim family where the server offered at least one edge to a function or method (Refers for scip.impact, Implements for scip.implements, Structure for structure.contains), an index edge to a function or method that the server also names is confirmed and one it does not name is removed and counted as overruled. A family the server offered nothing in, an answer cut at the per-node edge limit, an answer that named only a file, and destinations that are not functions or methods (under either places arm) leave the index edges standing and uncounted. graph.scip is never arbitrated: its edges point from a caller to its callees, a direction the server's references do not answer. An edge only the server has is not a disagreement. The counts (confirmed, overruled) are per call: in the trace on the scip.impact (etc.) call of each expand event, in totals.arbitration, in totals.per_map[<map>].arbitration and in one note scip.arbitration: lsp.live judged <maps> (...) in this call: <m> edges confirmed, <k> overruled; a map that was not judged prints none. The start event's arbitration object names the arbitration (authority, judged [map, family] pairs, kinds) and carries no counts. An overruled edge can be a false one: the server's answer at a node may be partial (a file it has not opened, a kept session behind an edit), and under places = "outline" an index member and its outline twin may carry different ids. A handle records the arbitration (its authority, judged pairs and kinds) and refuses a resume under another or under none. With the key off, with scip.mode = "authoritative" or without lsp.live, nothing changes in an answer.
  • Entity seeds ([run_profile.seeds] bm25, default false: the map seeds.bm25 does not run, naming it in maps gives a note and no seeds, and every answer is as without the key). With true the map is in the default set of a call that names no maps, and runs when named. It ranks the SCIP graph's functions and methods by BM25 (the scorer of the ripgrep channel, lexical::score_chunks) over three fields of each node: the tokens of its name, of its qualified path (the symbol's package, modules, types and member, and the file's path) and of its documentation (the documentation the index holds for the symbol, up to 1024 bytes; empty when the indexer writes none, and it is not verified that rust-analyzer scip writes any; it is read from the index file, a second parse, only when the map first runs for an index stamp, and never with the key off). A question word counts when it is a whole token (up to 4 characters) or inside one (longer); words under 3 characters and stop words are not terms, and a word in more than a quarter of the nodes is dropped unless no word would remain. Of the top [traverse] seed_k nodes, those the hybrid search did not already give (so possibly fewer) go after its nodes (seed_source hybrid+bm25, each with a seed_hits record how: "bm25"; seed_source hybrid stays when it adds none), or are the start nodes when the hybrid search gave none (bm25; the name-token stand-in then does not run, and seed_fallback says why the search did not answer). It reads the SCIP graph only (no --root needed), builds its index on the first call that wants it and keeps it with the graph for the index stamp. The recipes' seeds are given, so they run the same under both values.
  • Repo-map seeds ([run_profile.seeds] repo_map, default false: the map seeds.repo_map does not run, naming it in maps gives a note and no seeds, and every answer is as without the key). With true the map is in the default set of a call that names no maps, and runs when named. It ranks the SCIP graph's functions and methods by personalized PageRank (damping 0.85, at most 30 power iterations) over the call and containment edges (implements edges are left out, each caller and callee pair counts once), with the random jump on the question's best 64 seeds.bm25 matches: a function that many of the matched functions call becomes a start node even when its own name holds none of the question's words. A question with no entity match gives no seeds and a note (no global hub is ever a seed), and so does an empty graph. Of the top [traverse] seed_k nodes, those the earlier sources did not give go after theirs (seed_source hybrid+repo_map, hybrid+bm25+repo_map, bm25+repo_map, each with a seed_hits record how: "repo_map", score_kind: "pagerank", personalised and iterations), or are the start nodes when none gave any (repo_map). It reads the SCIP graph only (no --root needed), builds its edge lists and the seeds.bm25 index on the first call that wants it and keeps them with the graph for the index stamp. The recipes' seeds are given, so they run the same under both values.
  • Literal seeds ([run_profile.seeds] literal, default false: the map seeds.literal does not run, naming it in maps gives a note and no seeds, ripgrep is not started for it, and every answer is as without the key). With true the map is in the default set of a call that names no maps, and runs when named. It searches the tree with rg --fixed-strings --word-regexp (case-sensitive, _ counts as part of a word, the source files of the language adapters, at most 20 lines per file) for the names the question puts in code spans (the last segment of each a::Name, at least 2 characters, at most 4 names), and places each line in the SCIP graph: the symbol defined on that line (how: "definition"), else the innermost symbol around it (occurrence, counted per symbol), else, when the line declares the name (trait, struct, fn, class and the like right before it, or impl .. for it), the file's node (file: a type or trait the graph lacks, which structure.contains expands to the symbols it holds), else the line is dropped. Definitions (3 per name at most) come first, then occurrences (most lines first), then files; nodes the other sources gave are left out and the list is cut at [traverse] seed_k. They go after the hybrid search's and seeds.bm25's nodes (seed_source hybrid+literal, hybrid+bm25+literal, bm25+literal), or are the start nodes when those gave none (literal). It is not [traverse] exact_seeds, which turns the first stage's exact cards into start nodes. It needs the application's ripgrep (a question with --root); a call without says so in notes and goes on. The recipes' seeds are given, so they run the same under both values.
  • Every JSON result of index, status, list-files, search, symbols, definition, references, evaluate (the report, also when written with --output), traverse and recipe carries the same top-level profile_hash as the MCP and REST results (derived.run_profile_hash of config --json; traverse hashes the configuration after its command-line overrides). config --json reports the hash under derived; compare, replay and init --json run offline without a resolved configuration and carry none. An error prints no result.