Content
# Lurp: a Roslyn semantic map for .NET agents
[](https://github.com/t-macabee/lurp/actions/workflows/ci.yml)
[](https://www.nuget.org/packages/lurp)

> **Windows only, today.** Lurp is developed and CI-tested on Windows
> (`windows-latest`); cross-platform support is on the roadmap but not there yet.
Lurp indexes a .NET solution into SQLite with Roslyn, so an agent gets a small,
sufficient code neighborhood instead of re-grepping and re-parsing the whole
thing for every question. It loads the solution through the compiler and stores
symbols, typed relationships, source spans, and provenance in one database, then
serves retrieval, semantic diffs, impact reachability, and token-bounded context
capsules. Index once, query as many times as you want, and get back exact
source with evidence levels each time.
## [What the model sees](https://t-macabee.github.io/lurp/MODEL_VIEW.html)
A point-in-time map of one Lurp 2.0.0 index of
[eNoteV2](https://github.com/t-macabee/eNoteV2) (`eNote/eNote.sln`, 7 projects):
the evidence level of every edge, the DI, routing and EF Core surfaces, and the
full edge ledgers behind them. [`scripts/model-view/`](scripts/model-view/)
regenerates it.
## Why this exists
An agent working a C# codebase today loops search → read → guess, reopening and
re-parsing the whole solution for every question. Context windows fill up with
irrelevant source, and the agent ends up rediscovering the same call graph on
every turn.
Lurp flips that cost around. A one-time index builds a snapshot-bound semantic
graph. After that, every query just reads persisted facts: no Roslyn reload, no
re-grep. Capsules return the smallest neighborhood that's actually sufficient
for the task, each fact states its provenance (`compiler_proved` →
`runtime_unknown`), and any omitted tier is named so you can fetch it
separately.
## The mental model
| Command | What it does |
|---|---|
| `context` | Assemble a bounded capsule of relevant code from a symbol or source location. |
| `impact` | Follow typed relationships outward and list the affected symbols with a witness path. |
| `diff` | Show semantic changes between two snapshots. |
| `search` | Full-text search over source and symbols. |
| `grep` | Literal/exact-text search over source content with line numbers. |
| `find-symbol` | Resolve a symbol by its fully-qualified name. |
| `navigate` | Resolve an indexed declaration by file and line. |
| `get-symbol` | Look up symbol metadata (signature, provenance, declaration spans). |
| `get-source` | Retrieve source text for a document by relative path. |
| `index` | Build or update the snapshot-bound map in `index.db`. |
| `pin-snapshot` | Pin which snapshot reads default to when `--snapshot=` is omitted, without deleting or rewriting any snapshot. |
| `annotate` / `get-annotations` / `retract-annotation` | Attach and retrieve user-authored annotations on symbols; retract (by `annotation_id`) any annotation regardless of provenance. |
| `status` | Report whether the indexed snapshot still matches the workspace. |
| `timings` | Per-step performance data for a snapshot. |
| `outline` | List declarations in a document with line spans. |
| `diagnostics` | List compiler diagnostics captured at index time. |
| `dead-candidates` | List dead-code candidates: no incoming LIVE edge, after a suppression ladder that keeps public surface, EF/serialization conventions, process entry points, and generated/test code out of the default view. |
## Worked example
Index a solution, then build a context capsule from a source location:
```bash
lurp --mode=index --solution=MySolution.slnx --output-dir=./out
lurp --mode=context --file=src/Services/OrderService.cs --line=42 --output-dir=./out --output=summary
```
`--output=summary` prints the handoff facts instead of the full JSON capsule:
```
capsule OrderService.CreateAsync (Method)
snapshot: f3bff523b103462be239655c9b753be3 intent: inspect maxHops: 3
content tokens: 283/8000 (estimatedTokens: the budget basis)
delivery tokens: ~571 (estimatedArtifactTokens: whole emitted file; size the context window from this)
truncated: true
omitted: direct_callers (budget_exhausted) : fetch with --tier=direct_callers
omitted: relevant_tests (budget_exhausted) : fetch with --tier=relevant_tests
```
When a tier is `budget_exhausted`, fetch it on its own with no budget applied:
```bash
lurp --mode=context --file=src/Services/OrderService.cs --line=42 --output-dir=./out --tier=direct_callers
```
## Install
```bash
dotnet tool install --global lurp --version 2.0.0
lurp --mode=index --solution=path/to/Your.slnx --output-dir=./out
```
<details>
<summary>Build from source & environment variables</summary>
```bash
dotnet build Lurp.slnx
dotnet run --project src -- --mode=index --solution=path/to/Your.slnx --output-dir=./out
```
Environment variables `LURP_SOLUTION_PATH` and `LURP_OUTPUT_DIR` are equivalent to
`--solution=` and `--output-dir=`. Requires .NET 10 SDK 10.0.301 (pinned via
`src/global.json` `rollForward=latestMajor`; Roslyn 5.6 requires `net10.0`).
**Installing a local build as the global tool:** if you pack a local build and its
version number matches the version already on nuget.org, `dotnet tool install
--global --add-source <path> lurp` can silently
install the nuget.org copy instead of yours. `--add-source` only appends a source;
it does not remove nuget.org, and when two sources offer the same version, NuGet is
free to pick either one. Use `--source <path>` instead — it replaces every other
source, so only your local build is visible. Confirm afterward with
`lurp --version`, which prints the schema/extractor/CLI-MCP-contract versions the
installed binary was built with — compare that against the docs you're reading; a
mismatch means `dotnet tool install` pulled an older published build than the repo
this doc came from. This will bite again on every future local build that reuses
an already-published version number; bump the local package version before packing, or
always install with `--source`, never `--add-source`.
</details>
## Common recipes
Task-first lookup for things you already know Lurp can do but not which mode does it:
| Task | Command |
|---|---|
| Find unused `using` directives | `lurp --mode=diagnostics --severity=hidden --id=CS8019 --output-dir=./out` |
| Confirm the installed build matches these docs | `lurp --version` |
| Find dead code, excluding public API and tests | `lurp --mode=dead-candidates --output-dir=./out` |
| Search source text literally (not symbol search) | `lurp --mode=grep --query=<text> --output-dir=./out` |
| See what changed between two indexing runs | `lurp --mode=diff --from-snapshot=<id> --to-snapshot=<id> --output-dir=./out` |
| Pull just the code relevant to a change | `lurp --mode=context --file=<path> --line=<n> --output-dir=./out` |
| Find every caller of a symbol | `lurp --mode=impact --symbol=<id> --direction=upstream --output-dir=./out` |
## Framework adapters
Lurp models framework-mediated relationships through six adapters that emit
ordinary typed facts into the shared model. Each fact retains its evidence level;
see [ARCHITECTURE.md](docs/ARCHITECTURE.md) for the ladder.
| Adapter | Facts emitted |
|---|---|
| ASP.NET Core | `RoutesTo` (route → controller action) |
| Dependency Injection | `Registers` (service → implementation) |
| MediatR | request/notification → handler chains |
| EF Core | entity, `DbSet`, and configuration relationships |
| Serialization | DTO/property contract participation |
| Test | `TestedBy` (production symbol → covering test) |
## Limitations
- **Single active TFM/configuration**: one snapshot per index run.
- **Source generators not executed**: `GeneratedTreesIncluded=false`; generated files under `obj/` are path-filtered out.
- **Reflection string-literal candidates are `name_candidate`**, not `compiler_proved`.
- **3-snapshot retention**: older snapshots and their document versions are pruned automatically, except a snapshot pinned via `pin-snapshot`, which pruning always skips.
- **Workspace must be restorable**: `MSBuildWorkspace` requires a successful restore; no index without it.
## Performance
| Measurement | Value |
|---|---|
| eNoteV2 (402 docs) full index | ~48 s |
| eNoteV2 incremental (no changes) | ~11 s |
| Capsule token estimates | `estimated_tokens` (content) vs `estimated_artifact_tokens` (delivery) |
| Incremental↔full convergence | 5 cycles; 0 changed docs after cycle 1 |
## Status & roadmap
Shipped as a global tool (`dotnet tool install lurp`); the published version is
2.0.0. Schema v30, extractor
1.6.0, CLI/MCP contract v2, output schema v5. 2.0.0 is a breaking release for
`impact` and the capsule topology, and an index built by 1.4.0 needs one
`--mode=index` run before read commands accept it: see
[RELEASE_NOTES_2.0.0.md](docs/RELEASE_NOTES_2.0.0.md). `windows-latest` CI plus a self-hosted real-parity gate on FIT-RS2-2026 +
eNoteV2 (opt-in via `real-parity` PR label). Roadmap: multi-TFM and richer DI
parameter-type matching are postponed by design (see
[DeclaredBoundaries](notes/TRUST_KERNEL.md#declared-boundaries-registry-capsule-audit-task-7)).
## Documentation & license
- [ARCHITECTURE.md](docs/ARCHITECTURE.md): product model, storage, pipeline, rules.
- [CLI_REFERENCE.md](docs/CLI_REFERENCE.md): commands, options, output shapes, snapshot lifecycle, MCP.
- [RELEASE_NOTES_2.0.0.md](docs/RELEASE_NOTES_2.0.0.md): breaking changes from 1.x and how to migrate.
- [VERSIONING.md](VERSIONING.md): what counts as a breaking CLI/MCP change.
- MIT license, see [LICENSE](LICENSE).
Also MCP: `--mode=serve` exposes 18 tools (`lurp_context`, `lurp_get_source`,
`lurp_outline`, `lurp_navigate`, `lurp_find_symbol`, `lurp_search`, `lurp_grep`,
`lurp_impact`, `lurp_diff`, `lurp_get_symbol`, `lurp_get_annotations`, `lurp_retract_annotation`,
`lurp_diagnostics`, `lurp_status`, `lurp_timings`, `lurp_refresh`, `lurp_index`,
`lurp_dead_candidates`) over stdio; all are read-only except `lurp_index` and `lurp_retract_annotation`
(background re-index). Index first, then serve. See
[CLI_REFERENCE.md#mcp](docs/CLI_REFERENCE.md).
Connection Info
You Might Also Like
No recommendations available