This file is the home of the command-line reference: every lci subcommand, its options and its output.
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/readyanswers HTTP 200 (HTTP 503 names the missing dependency).searchreturnsopenapi_documentinsrc/rest.rsas its top hit (the function building the document served at/openapi.json).definitionstarts from the call in the/openapi.jsonroute at the end ofsrc/rest.rsand resolves to that function's declaration; the firstreferencesstarts from the declaration and returns it and the wiring; the second starts fromWorkspace::resolveand crosses crates (callers insrc/app/andtests/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.rsgives the current ones. The secondindexshould report zero new embeddings when source and model configuration are unchanged.cargo xtask acceptance coreruns the same index/search/repeat flow and checks that production implementation chunks, includingopenapi_document, dominate the top eight. Another repository is just a different first argument. configshows how the configuration resolves without opening the application:"$lci" --config config.toml config [--json].initwrites the overlay:init --dry-run(print only),init(writeconfig.local.tomlbesideconfig.toml; exit 3 if it exists),init --force --no-probe(replace it, send no request).config --jsonprints one object:validanderror;layers(filewithpathandnamed_by(--configorLCI_CONFIG);localwithpath,stateofpresent,absentordisabled, andnamed_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) withvalue,source(default,file,local,env,flag) andfrom;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(whetherLCI_CANDIDATE_DUMPis set). Exit status: 0 valid; 2 resolves but fails validation (for example a reservedport; the report is still printed withvalidfalse); 1 when a layer cannot be read or parsed (a missing file, a non-machine key in the overlay, a malformedLCI_*value; onlyvalidanderrorare printed with--json).searchtakes repeatable--language,--include-path,--exclude-path,--source-role, plus--top-kand--search-profile <name>(the profile's settings applied over[channels]andrerank_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 indexedLib.RS, while--language rustdoes):*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 setsandroidTest/integrationTest/testFixtures, and per-language file names: Pythontest_*.py/*_test.py; JS/TS*.test.*/*.spec.*; C#*Test(s).cs; Go*_test.go; Rusttests.rs/test_*.rs/*_test.rs/*_tests.rs(a baretest.rsstays source, being a legitimate production module name; inline#[cfg(test)] mod testscannot 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 (undersrc/<set>/java/, orkotlin/scala/groovy, the remaining directories are package names, socom/example/...is not an example), and aconfig/configs/configurationdirectory marks only non-source files, sointernal/config/loader.gostays source. Production source receives a small transparent0.004fusion 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 asseed_source) and prints one JSON object (reached nodes, totals, handler decisions, and when it stopped to ask or ran out of budget ahandleandcheckpoint; continue with--handleand--steer). It ignores[traverse] enabled(which gates only the MCP mode) and is limited by the[traverse]caps. With--root, the graph also has animplementsedge from each trait-impl member to its trait method declaration (read from theimpl .. for ..headers; rust-analyzer writes no SCIP relationships), and--maps scip.implementsadds 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. Withscip.implementson, a trait path orTrait::methodin a code span of the question (`tauri::Manager`,`Runtime::run`) starts the walk at that trait or trait method declaration (the response then hastrait_seeds, how many start nodes they are, andseed_sourcetraits, ortraits+hybrid,traits+hybrid+bm25,traits+bm25ortraits+name_tokenswhen 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;--seedsstill wins;--trait-seeds replace|prepend|auto(defaultauto, as described) picks the mode), and--seedstakes 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] enabledand[compose] enabled.config_readers,config_neighboursand anlci_composeconfig.keystep needrgonPATH. - Map freshness (
[run_profile.traverse] map_freshness = true, defaultfalse): a traversal answer (MCPmode = "traverse",lci traverse) then hasmaps, 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 nomapskey 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) haveobserved = "snapshot";tests.tested_byreads the same impact index but states no freshness yet, so it has nomapsentry. The maps that read the tree or git when they answer (grep.live,grep.structural,lsp.live,git.history,exact.pickaxe,probe.compile) haveobserved = "live",stale = falseandlag = 0;livenames the channel, not a guarantee about every byte.grep.live,grep.structural,git.history,exact.pickaxeandprobe.compileread the tree or git when they answer (current bytes, or the current git HEAD).lsp.liveasks 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 notelsp.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_cachekeeps 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 itslsp.livecards areunverified, unless the walk itself started the session).grep.live,grep.structuralandlsp.livealso place what they read by the index's positions, so a line shift since the index is not covered by theirlive. 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 carryrole:"authority"forlsp.liveand"cache"for each SCIP-built map it judged (scip.impact,scip.implements,structure.contains); without an arbitration no entry has arolekey.lagis 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.staleistruewhenlagis above 0, so an index rewritten with a new stamp starts again from the files newer than it.lagisnullandstalefalse(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_gateis on (see "Stat gate" below). The mapsseeds.hybrid,search.lexicalandsearch.vectorstate nothing and are absent frommaps, unless[run_profile.freshness] verify_on_readis 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 nomaps) 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, defaultfalse): every traverse answer (a walk, a continuation, an answer with no start node) gainscompleteness,{complete, why, estimate}:completeis true exactly whenstatusiscomplete(the walk endedcompleteorbound, the top-k set is final) and no map the call asked for was lost; a map that could not be set up (for examplelsp.livewith no server) or that had failed calls makes itfalse, withwhydegraded(a cut or capped walk keeps its ownwhy) anddegradedlisting the maps, and a note naming them;whyis otherwise the answer'sstop_reason(no_seedswithout a walk) andestimateis the fraction of thelikely_completehint (the mark-recapture estimate over what the maps found;nulluntil two maps share a node; maps over one graph read high, so it is a hint, nevercomplete); a partial answer also gets onenotesline that names the reason and the handle. Every card ofresultsand of the checkpoint'sanswer,visited_cardsandfrontiergainsfreshness, one offresh,changed,stale,unverified:changedwhenfile_changedistrue,freshwhen it isfalse(the card's own file is as its maps read it, so its text is current even under a stale snapshot), else frommaps: any map of the card (mapsandvia) that isstalegivesstale, every one found with a lag givesfresh, anything else isunverified, which is what every card says withmap_freshnessandverify_on_readoff. One exception, forlsp.live: when the walk found a language server running (a session kept across calls under[run_profile.lsp] sessions = "shared", the case thelsp.live: <server> kept sessionnote reports) every card that citeslsp.liveisunverified, neverfresh, because nothing checks what a kept server read (achangedorstalefinding stands); a card from a server this walk started keepsfresh. The rule is per walk, not per card: with several servers, any reused one makes alllsp.livecardsunverified. 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 citeslsp.livestaysunverified; the cards of nodes the continuation's own walk reaches follow the rule above. Nothing is withheld with either value (resultsare the same), and the trace, the checkpoint hash, the handle andtextdo 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, defaultfalse): every card of a traversal answer'sresultsand of the checkpoint'sanswerthat the walk reached then haskind, the kind of the edge that discovered the node (nullfor a start node), andpath, the route from its start node: one{node, map, kind, witness}entry per node (depth + 1entries, the first is the start node withkindandwitnessnull, the last is the card itself and repeats itsviaandwitness). The route is the walk's final one (a stronger route found later replaces a weaker one). The cards ofvisited_cardsandfrontier, the cards of the first stage (originexactorseed), 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'skind(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 showkind: 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 traversewithgraph.scipover 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'sanswerrepeats the cards; theresultslist 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 runslsp.live, which a call with a tree undersharedand the service's manager (MCP and RESTmode = "traverse") runs by default,[run_profile.lsp] live = true, and which the CLI one-shotlci traverseandper_callrun only when--mapsor 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 = falsemakes the map opt-in everywhere, and a call that names its maps is never changed by the key): underper_callevery 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. Undershared(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 noworkspace.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 = trueprimes the ones of the index's languages at the first touch of a workspace. Withsync = "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 underper_call; jdtls lists no configuration files and is never kept, whateversyncsays. 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 unindexedsrc/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 runslsp.liveon 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 freshper_callone. 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 thanper_callfor 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, whichper_callhas 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 everywatch_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: ...andlsp: <server> kept session; ...in thenotesof 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 sayslsp: 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 aCargo.tomlorCargo.lockedit while the server reloads its workspace. Off switch:sessions = "per_call"(it moves the profile hash);lsp_idle_timeout_secondsonly 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,0never 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 traverseis one call per process, so thereshareddiffers fromper_callonly 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 forlsp_idle_timeout_seconds, onrelease_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 undershared([run_profile.lsp] breakeristrueby 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/readystill probes. Withbreaker = falsea failed probe is run again by the next call and a dead server is started again). The recipes (flow_fromand the otherlci_*tools) walk onlygraph.scipandstructure.contains, neverlsp.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(defaultfalse) adds a third request to each symbol node of anlsp.liveexpansion,textDocument/implementation, behind the samemax_external_callsgate as the other two: its answers becomeimplementsedges from the node to the symbols (or files) at the places the server names, the directionscip.implementsuses (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(defaultfalse) adds up to three more requests to each symbol node, each behind the same gate and counted:textDocument/prepareCallHierarchyat the node's name, thencallHierarchy/incomingCallsandcallHierarchy/outgoingCallsof the item that answers (an empty prepare ends it, so one more call at least). Callers and callees becomecalls_or_refsedges (no new edge kind), one per destination, whichever way the call runs, seen at the call itself; a symbol or file thatdefinitionorreferencesalready 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 complementsgraph.scip. The recipes ask no call hierarchy either way. - Verify on read (
[run_profile.freshness] verify_on_read = true, defaultfalse): withmap_freshnessalso 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(undermanualthe snapshot stays as old as it was). It reuses the one read of the tree thatmap_freshnessmakes (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 amapsentry{id, observed: "snapshot", stale, lag}whoselagcounts 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 underon-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.liveandlsp.livekeepobserved: "live"(grep.livereads current bytes;lsp.livequeries 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 thekept sessionnote) and gainplaces: "index": where they put what they read comes from the SCIP index, so theirstaleandlagare 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 withplaces. (3) Every card (results, and the checkpoint'sanswer,visited_cardsandfrontier) getsfile_changed:truewhen the content hash of the file it cites (itssite.path, else its node's path) now differs from the hash its maps read,falsewhen it does not,nullwhen 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 arenull).truealso 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, forgrep.liveandlsp.live, and for a symbol card (its site is the index's, whichever map reached it), the snapshot manifest forseeds.hybrid,search.lexicalandsearch.vectorand for a chunk card; a card that names both istruewhen either differs. Asearch_code/searchreport (JSON; the compact text output is unchanged) gets afreshnessblock with the key on, with or withoutmap_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 bystat_gatewhen 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 anote(failed; see warning) and no lag, because it answered nothing:vec.chunk.codeandlexicalareobserved: "snapshot"with the snapshot's lag;lsp.symbolsisobserved: "session"withstaleandlagnulland anote, because the language server's view of the tree is not compared), andhits(one{path, file_changed}per result in order;file_changedisnullfor 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 coststimings.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. Undermanualorwatcha search never refreshes the snapshot, so the block is how a caller sees that it is behind; underon-searchthe search has just refreshed it and the lag is 0. A search with the key off has nofreshnesskey. The recipes take the key through the run profile but their answers carry none of this. - Stat gate (
[run_profile.freshness] stat_gate = true, defaultfalse: every read of the tree formap_freshnessorverify_on_readreads and hashes every source file). Withtruethe 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 andfile_changedequal 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 deliberateset_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, defaultfalse: every file outline the map needs, atextDocument/documentSymbolrequest, goes to the server). Withtruethe 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: atextDocument/didOpenthe first time, then atextDocument/didChangecarrying the whole text when the server advertised change sync in itsinitializeresult (full or incremental; LSP 3.17 reads a change without a range as the whole text), and a seconddidOpenwhen 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 withtextDocument/didClosefirst, 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'spublishDiagnosticsfor 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: undersessions = "per_call", and for a server that a"shared"walk starts on its own manager (every one of these withsync = "off", jdtls and marksman always), it lasts one walk; in the service's manager under"shared"withsync = "watch"(the default) it serves later calls too. The recipes never runlsp.liveand 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 answerssearch_symbols, the search channel and a sharedlsp.livefrom 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 andsessions = "shared"traversals use; not the one-shotlci 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 oneworkspace/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 everywatch_poll_millisecondswhen 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 astarget/, 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 oflsp.live") and restarts a kept server after an edit whatever the sync did, so forlsp.livethe 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 everywatch_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 runlsp.liveand 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 getslsp.livecards. 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 nolsp.livecards 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.liveasks nothing about it, and underscip.mode = "off"it asks nothing about any symbol; a file seed still gets itstextDocument/documentSymboloutline, 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.liveasks the server for the file'stextDocument/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 underscip.mode = "off"the functions, methods, types (class asclass, struct asstruct, interface asinterface, except that rust-analyzer's trait staystraitin 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 asContainsdestinations too, and a reference on a field's line now resolves to the field;lsp.liveasks a field or variable like any other symbol (a server may answerimplementationfor an interface property or a call hierarchy for aconst f = () => {}it reports as a variable). A handle saved underoutlineby an earlier build holdsstructandtraitids 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. EverydocumentSymbolrange now ends on the item's last line (the nestedDocumentSymbolshape used to give the name's line), whichContainsedges read from the name's own line first. The recipes never runlsp.liveand 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.sumandgo.workfor gopls;pyrightconfig.json,pyproject.tomlandsetup.cfgfor pyright;tsconfig.json,jsconfig.jsonandpackage.jsonfor typescript-language-server; solution and project files for csharp-ls; and so on), so a module underservices/api/is served fromservices/api/and itsrootUri,workspaceFoldersand 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, andindex_statussays why inservers[].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 reportsroots_workspace). A session fixes its root when it starts and keeps it. The one-shotlci traverseundersessions = "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 runlsp.liveand 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 asextraPathsin itspyproject.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 nearesttsconfig.jsonfrom the workspace root,pathsaliases 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, defaultfalse): withtrue,lsp.livein the call,scip.modenot"authoritative"and at least one ofscip.impact,scip.implements(and only with[run_profile.lsp] implementations = true) orstructure.containsregistered 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 (Refersforscip.impact,Implementsforscip.implements,Structureforstructure.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 eitherplacesarm) leave the index edges standing and uncounted.graph.scipis 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 thescip.impact(etc.) call of eachexpandevent, intotals.arbitration, intotals.per_map[<map>].arbitrationand in one notescip.arbitration: lsp.live judged <maps> (...) in this call: <m> edges confirmed, <k> overruled; a map that was not judged prints none. Thestartevent'sarbitrationobject 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 underplaces = "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, withscip.mode = "authoritative"or withoutlsp.live, nothing changes in an answer. - Entity seeds (
[run_profile.seeds] bm25, defaultfalse: the mapseeds.bm25does not run, naming it inmapsgives a note and no seeds, and every answer is as without the key). Withtruethe 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 (thedocumentationthe index holds for the symbol, up to 1024 bytes; empty when the indexer writes none, and it is not verified thatrust-analyzer scipwrites 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_knodes, those the hybrid search did not already give (so possibly fewer) go after its nodes (seed_sourcehybrid+bm25, each with aseed_hitsrecordhow: "bm25";seed_sourcehybridstays 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, andseed_fallbacksays why the search did not answer). It reads the SCIP graph only (no--rootneeded), 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, defaultfalse: the mapseeds.repo_mapdoes not run, naming it inmapsgives a note and no seeds, and every answer is as without the key). Withtruethe 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 (implementsedges are left out, each caller and callee pair counts once), with the random jump on the question's best 64seeds.bm25matches: 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_knodes, those the earlier sources did not give go after theirs (seed_sourcehybrid+repo_map,hybrid+bm25+repo_map,bm25+repo_map, each with aseed_hitsrecordhow: "repo_map",score_kind: "pagerank",personalisedanditerations), or are the start nodes when none gave any (repo_map). It reads the SCIP graph only (no--rootneeded), builds its edge lists and theseeds.bm25index 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, defaultfalse: the mapseeds.literaldoes not run, naming it inmapsgives a note and no seeds, ripgrep is not started for it, and every answer is as without the key). Withtruethe map is in the default set of a call that names no maps, and runs when named. It searches the tree withrg --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 eacha::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,classand the like right before it, orimpl .. forit), the file's node (file: a type or trait the graph lacks, whichstructure.containsexpands 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 andseeds.bm25's nodes (seed_sourcehybrid+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 innotesand 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),traverseandrecipecarries the same top-levelprofile_hashas the MCP and REST results (derived.run_profile_hashofconfig --json;traversehashes the configuration after its command-line overrides).config --jsonreports the hash underderived;compare,replayandinit --jsonrun offline without a resolved configuration and carry none. An error prints no result.