# Inspiration — gograph (ozgurcd/gograph)

> A read of `github.com/ozgurcd/gograph` (v1.4.88, 186⭐, Go static analysis
> engine + MCP server) and what its design choices imply for the pi-lens
> review graph and FactStore.

**Status:** inspiration / opportunity analysis. Not a plan of record —
[`reviewgraphenhancement.md`](./reviewgraphenhancement.md) is the internal
roadmap for review-graph work and covers most of the same surface from a
different angle (Phase 1–7 there already lists `lens_context`, `tests` edges,
and per-tool routing). This doc is the **gograph-as-inspiration** lens:
concrete code patterns from that repo, asserted against the current
pi-lens codebase, with priorities for what to adopt and what to skip.

## 1. What gograph actually is

A **single-language Go-only** local static analysis engine:

- Parses Go source via `go/ast` (fast) and optionally re-runs the type-checker
  (`--precise` mode) to resolve `CalleeSymbolID` instead of just `CalleeRaw`.
- Persists the entire graph as JSON in `.gograph/graph.json` (schema `Version = "2"`,
  see `internal/graph/graph.go:14-16`).
- Exposes ~50 query functions in `internal/search/*.go`, each a **pure
  function over `*graph.Graph`** — no I/O at query time, no daemon, no DB.
- Mirrors every query through an MCP server (`internal/mcp/server.go`) over
  stdio JSON-RPC — 50+ tools, one per query.
- Sells itself on **token-saving composites**: a `context` call bundles
  node + source + callers + callees + tests + role, replacing 4–5
  separate calls (README claims ~80% fewer tool calls).

The README positions it as a **user-invoked code-intelligence tool** ("stop
burning tokens on grep"). That's a different problem than pi-lens solves
("make every edit better via substrate-injected advisories"), but the
overlap is in the **graph primitives** and the **agent-facing API design**.

## 2. Cross-reference to the existing plan

[`docs/reviewgraphenhancement.md`](./reviewgraphenhancement.md) (Phases 1–7)
already covers:

- Symbol ID enrichment + scoped call resolution (Phases 1–2)
- `searchSymbols` / `getCallers` / `getCallees` / `getFileImpact` /
  `getSymbolImpact` query helpers (Phase 3)
- `lens_symbol_search` / `lens_impact` / `lens_context` pi tools (Phase 4)
- `tests` edge kind (Phase 5)
- Framework-aware edges (`routes_to`, `handles`, `configures`) (Phase 6)
- SQLite/FTS migration trigger conditions (Phase 7)

This doc doesn't re-propose those. Where the gograph lens **changes the
prioritization** or adds a concrete code pattern not in that plan, it's
called out explicitly with `[gograph-specific]` markers below.

## 3. gograph's distinctive design patterns

These are the patterns worth borrowing, regardless of which specific
primitives get adopted.

### 3.1 Token-saving composites — replace N calls with one structured answer

The headline insight. gograph's `context`, `plan`, and `risk` are **not
new analysis** — they're **compositions** of existing queries:

- `context` = `Node` + `Source` + `Callers` + `Callees` + `Tests` + `quickRole` heuristic
- `plan` = `FindSymbols` + `Callers` + `Tests` + `ImpactMultiple` + downstream BFS for env/SQL
- `risk` = `ImpactMultiple` + `Complexity` + `Tests` + downstream BFS for env/SQL

The win is agent round-trip cost. pi-lens already does this for one pair
(`pilens_module_report` + `pilens_read_symbol`, see
[`docs/module-report-read-symbol.md`](./module-report-read-symbol.md) —
"−75% tokens" vs `read`). The pattern is to extend it to **pre-edit
checks**: one composite call that says "before you change X, here's what
to read, what tests to update, and how risky this is" — replacing 3–5
calls with one structured response.

**gograph source pointers**:
- `internal/search/context.go` — `ContextResult` struct + `Context()` + `quickRole()`
- `internal/search/plan.go` — `PlanResult` + `Plan()` + downstream BFS loop
- `internal/search/risk.go` — `RiskReport` + per-component weighted scoring

### 3.2 Per-query file layout — discoverability and testability

Every query in gograph is its own file: `search.go`, `context.go`, `plan.go`,
`risk.go`, `deps.go`, `hotspot.go`, `orphans.go`, `untested.go`, `coupling.go`,
`complexity.go`, `errorflow.go`, `mutate.go`, `literals.go`, `implementers.go`,
`endpoint.go`, `globals.go`, `httpcalls.go`, `concurrency.go`, `interfaces.go`,
`constructors.go`, `embeds.go`, `usages.go`, `tests.go`, `fixtures.go`,
`mocks.go`, `routes.go`, `sql.go`, `envs.go`, `errors.go`, `skeleton.go`,
`mermaid.go`, `explain.go`, `summary.go`, `stats.go`, `changes.go`,
`git.go`, `review.go`, `api.go`, `check.go`, `gate.go`, `snapshot.go`,
`baseline.go`, `session.go`, `claude_plugin.go`, `hook_guard.go`,
`rootfind.go`, `scanner/ignore.go`.

Each is a pure function over `*graph.Graph`. Easy to skim, easy to test,
easy to extend.

Pi-lens currently puts every review-graph query in
[`clients/review-graph/query.ts`](../../clients/review-graph/query.ts)
(one file, 230 lines, two exports: `computeImpactCascade`,
`computeTransitiveImpact`). Splitting into
`clients/review-graph/queries/{cascade,impact,context,plan,risk,hotspot,
untested,deps,dependents}.ts` would mirror gograph's discoverability,
align with the `clients/dispatch/facts/` pattern already in use, and
make each query independently unit-testable.

### 3.3 Stable module-rooted symbol IDs

gograph's `SymbolNode.ID` is `"github.com/org/repo/pkg::Symbol"` — the
schema v2 comment (`internal/graph/graph.go:14-27`) is explicit:

> v2: symbol IDs use module-rooted import paths (e.g.
> "github.com/org/repo/pkg::Symbol") instead of relative file paths
> ("internal/pkg/file.go::Symbol"). This makes IDs stable across file
> renames within the same package.

Pi-lens's symbol IDs are file-rooted: `${normalized}:${fn.name}` in
[`clients/review-graph/builder.ts:533`](../../clients/review-graph/builder.ts#L533)
and `:572`. A rename within a package breaks every incoming edge that
points at the old ID. Switching to module-rooted IDs would future-proof
the graph for rename-tolerant impact analysis — the same property the
existing `cachedExports` map (symbol → file) on `RuntimeCoordinator`
preserves for cascade purposes.

Cost: requires per-language package detection (go.mod, package.json,
Cargo.toml, pyproject.toml, etc.) — most of which already exists in
[`clients/review-graph/workspace-modules.ts`](../../clients/review-graph/workspace-modules.ts).

### 3.4 CalleeSymbolID + CalleeRaw dual-field on call edges

gograph's `CallEdge` (`internal/graph/graph.go`) carries **both**:

```go
type CallEdge struct {
    CallerSymbolID string  // FQ ID of the enclosing function
    CallerName     string  // short name
    CalleeRaw      string  // the raw text of the call expression
    CalleeSymbolID string  // populated ONLY by --precise (type-checker)
    File           string
    Line           int
    ReturnUsage    string  // "discarded"|"assigned"|"returned"|...
}
```

`--precise` mode (the type-checker pass) is what populates `CalleeSymbolID`.
Without it, the resolver falls back to name-substring matching against
`CalleeRaw`, which has a known name-conflation footgun: `(*A).Validate` and
`(*B).Validate` share the short name `Validate`.

The traversal functions explicitly handle **both** forms in their frontier
(`internal/search/search.go:Callers()`, `ImpactMultiple()`), with the
following invariant:

> When `CalleeSymbolID != ""`, match exactly against `SymbolNode.ID`.
> Otherwise fall back to substring matching against `CalleeRaw`.
> Both forms are checked on every edge; order doesn't matter because
> callers dedup on `SymbolNode.ID`.

Pi-lens has only the unresolved form: `metadata.unresolvedName` on the
target node ([`builder.ts:486`](../../clients/review-graph/builder.ts#L486),
`:571`). The `resolveDeferredSymbolEdges` pass (`query.ts:489-516`)
handles the single-candidate fast-path (when exactly one symbol with the
unresolved name exists) but otherwise leaves edges unresolved.

A pi-lens equivalent of "precise mode" would be per-language type-checker
hooks (tsserver for JS/TS, pyright for Python, gopls for Go, etc.). This
is **explicitly out of scope** for adoption — too expensive vs. the
advisory-quality accuracy the current name-resolution gives. Worth
documenting as the deliberate non-adoption.

### 3.5 `tests` edges as a thin overlay (not a graph re-population)

gograph stores test coverage as **one edge per (test_function, production_symbol)**:

```go
type TestEdge struct {
    TestFunc string  // the test function name
    Target   string  // the production symbol it exercises
    File     string
    Line     int
}
```

Test files themselves are **not** in the graph as full symbol trees. The
parser populates `TestEdges` as a side product of `extractFuncDecl` when
it sees a function whose name starts with `Test`/`Benchmark`/`Example`.

This pattern unlocks four high-value queries:

- `Tests(symbol)` — "what tests cover this?"
- `Untested()` — single sweep: production symbols with callers > 0 but no `TestEdge` pointing at them
- `AffectedTests(changedSymbols)` — "what tests must I update?" (post-edit)
- `Plan(symbol)`'s "Update likely affected tests" section

**This is the single biggest gograph primitive pi-lens is missing**, and
the reason is documented in pi-lens history: **PR #260 excluded test
files entirely** from the graph to reduce bloat (~56% tests in test-heavy
repos). The exclusion was for **graph nodes**, but gograph shows the
same data as **edges only** — a much smaller data shape.

For a repo with 1000 test functions and 5000 production symbols, the
edge array is 1000 entries — trivial vs. the current 50k+ symbol nodes.
A `testEdges: TestEdge[]` field on `ReviewGraph` (plus a `tests` edge
kind on the existing `ReviewGraphEdgeKind` union, already proposed as
Phase 5 of `reviewgraphenhancement.md`) restores "affected tests"
capability at <1% size cost.

**Source pointers**:
- gograph: `internal/graph/graph.go:TestEdge` struct, parser populates via `ast.Inspect` over `*ast.FuncDecl` with `Test`/`Benchmark`/`Example` prefix
- pi-lens: `builder.ts:85` (`detectFileRole(file) !== "test"` exclusion), no `testEdges` field in [`types.ts`](../../clients/review-graph/types.ts)

### 3.6 `risk` as a graded score, not a flat flag list

gograph's `Risk()` (`internal/search/risk.go`) emits a per-symbol 0–100
score with **six weighted components** and a `SAFE | REVIEW | DANGER` verdict:

```go
totalScore = blastRadiusScore + complexityScore + testScore
           + publicAPIScore + sqlScore + envScore
// each component clamped, total capped 100
verdict = totalScore > 70 ? "DANGER" : totalScore > 30 ? "REVIEW" : "SAFE"
```

Component weights (from `risk.go`):
- **Blast radius**: `min(callersCount * 3, 30)` — transitive callers via `ImpactMultiple`
- **Complexity**: `(compScore - 1) * 2`, capped 25 — cyclomatic complexity via `Complexity()`
- **Test score**: `20` if 0 tests, `10` if 1 test — derived from `TestEdges`
- **Public API**: `10` if exported (capital first letter in Go)
- **SQL downstream**: `10` if any SQL call reachable via BFS from this symbol
- **Env downstream**: `5` if any env read reachable via BFS from this symbol

This produces a structured advisory:
```
SYMBOL                          VERDICT   SCORE  METRICS
(*Service).Validate             DANGER    72     callers=18 (12 untested)  complexity=14  sql=0  env=2
newUser                         SAFE      8      callers=1 (0 untested)    complexity=2   sql=0  env=0
```

The pi-lens equivalent today is a flat unordered list
([`query.ts:204-222`](../../clients/review-graph/query.ts#L204-L222)):
```ts
riskFlags: ["exported symbol changed", "high fanout", "high complexity"]
```

A graded verdict with component breakdown is more actionable in turn-end
advisories: `🟡 REVIEW (62/100): exported + high complexity, 7 untested
callers`. Mechanical to implement — the graph already exposes all the
inputs (complexity from `metadata.cyclomaticComplexity`, fanout from
`edgesByFrom.get(nodeId).filter(kind="calls").length`, exportedness
from `node.exported`).

### 3.7 Tiered build cache (in-memory → disk → full rebuild)

gograph has `gograph build .` (cold) + `gograph stale` (incremental) +
`gograph stats` (cache health). The cache is the on-disk `graph.json`;
"stale" detection is based on file mtime.

Pi-lens already does this with **three tiers** ([`builder.ts:_doBuildGraph`](../../clients/review-graph/builder.ts)),
in a more sophisticated way:

1. **In-memory cache** (`_workspaceGraphCache`) — same process, hot path
2. **Disk cache** (`loadPersistedGraph`) — cross-process, snapshot on disk
3. **Full rebuild** — last resort

Plus **content-hash confirmation** of mtime-only drift ([`builder.ts:265-285`](../../clients/review-graph/builder.ts#L265-L285),
the `#202` work) which gograph doesn't do. This is one area where
pi-lens is **strictly ahead** — no change recommended.

### 3.8 Hand-rolled CLI + MCP server (no SDKs)

gograph's MCP server (`internal/mcp/server.go`) is built on
`github.com/modelcontextprotocol/go-sdk` — a real SDK dep. Pi-lens's
mirror at [`mcp/server.ts`](../../mcp/server.ts) is hand-rolled
newline-delimited JSON-RPC, zero-dep. AGENTS.md documents why: SDKs
bloat installs under `npm install --omit=dev`.

Both approaches work; pi-lens's is the leaner one. **Keep as-is.**

## 4. Gap analysis vs. current pi-lens

| gograph primitive | Pi-lens equivalent | File | Verdict |
|---|---|---|---|
| `Graph` JSON + `Version` | `ReviewGraph` + `REVIEW_GRAPH_VERSION = "v3"` | [`types.ts`](../../clients/review-graph/types.ts), [`builder.ts:71`](../../clients/review-graph/builder.ts#L71) | ✅ Equivalent, ahead on tiering |
| Tiered build cache | 3-tier cache + content-hash confirm | [`builder.ts:_doBuildGraph`](../../clients/review-graph/builder.ts) | ✅ **Ahead** |
| Incremental updates | `confirmContentChanged` + `#202` | [`builder.ts:265-285`](../../clients/review-graph/builder.ts#L265-L285) | ✅ **Ahead** |
| `FindSymbols` (FQ-ID + name match) | `resolveDeferredSymbolEdges` + BM25 | [`query.ts:489-516`](../../clients/review-graph/query.ts#L489-L516), [`word-index.ts`](../../clients/word-index.ts) | ⚠️ Partial — no symbol-ID matching |
| `Callers` (incoming call sites) | `computeImpactCascade` (one-hop) + `computeTransitiveImpact` (BFS) | [`query.ts:96-228`](../../clients/review-graph/query.ts#L96-L228) | ✅ Equivalent (file-level) |
| `Impact` / `ImpactMultiple` (transitive blast radius) | `computeTransitiveImpact` (BFS-bounded) | [`query.ts:35-93`](../../clients/review-graph/query.ts#L35-L93) | ✅ Equivalent |
| `Tests(symbol)` | **None** | n/a | ❌ **Missing** (tests excluded #260) |
| `Untested()` (sweep) | **None** | n/a | ❌ **Missing** (depends on `tests`) |
| `Hotspot(symbol)` (fan-in ranking) | **None** | n/a | ❌ Missing — trivial to add |
| `Risk(symbol)` (graded score) | `riskFlags: string[]` (flat list) | [`query.ts:204-222`](../../clients/review-graph/query.ts#L204-L222) | ⚠️ Partial — needs grading |
| `Plan(symbol)` (pre-edit checklist) | **None** as a composite | n/a | ❌ **Missing** |
| `Context(symbol)` (bundle) | `pilens_module_report` + `pilens_read_symbol` (split) | [`tools/module-report.ts`](../../tools/module-report.ts), [`clients/module-report.ts`](../../clients/module-report.ts) | ⚠️ Split — composite would be better |
| `Orphans()` (zero callers) | **None** | n/a | ❌ Missing — trivial |
| `Deps` / `Dependents` (package closure) | `reverse-deps.ts` (per-file, one-hop) | [`clients/reverse-deps.ts`](../../clients/reverse-deps.ts) | ⚠️ Partial — no transitive, no package grouping |
| `Routes()` (HTTP routes) | **None** | n/a | ❌ Missing — language-specific |
| `SQL()` (SQL strings) | **None** | n/a | ❌ Missing — language-specific |
| `Envs()` (env reads) | **None** | n/a | ❌ Missing — language-specific |
| `HTTPCalls()` (outbound HTTP) | **None** | n/a | ❌ Missing — language-specific |
| `Concurrency()` (goroutines, channels) | **None** | n/a | ❌ Missing — language-specific |
| `APIDrift(baseline, current)` | **None** (only #202 mtime drift) | n/a | ❌ Missing |
| `Boundaries()` (layer rules) | **None** | n/a | ❌ Missing — could be ast-grep rules |
| `CalleeSymbolID` (precise resolution) | `unresolvedName` fallback only | [`builder.ts:486`](../../clients/review-graph/builder.ts#L486) | ⚠️ Deliberately skipped — too expensive |
| Module-rooted symbol IDs | File-rooted IDs | [`builder.ts:533, 572`](../../clients/review-graph/builder.ts) | ⚠️ Easy rename-tolerant upgrade |
| Hand-rolled MCP server (no SDK) | Hand-rolled MCP server | [`mcp/server.ts`](../../mcp/server.ts) | ✅ **Ahead** (zero-dep) |
| CLI for end users | `pi-lens-analyze` bin + warm IPC | [`mcp/analyze-cli.ts`](../../mcp/analyze-cli.ts) | ✅ Ahead on UX |
| Multi-language via tree-sitter | **17 languages** | [`clients/review-graph/builder.ts:42`](../../clients/review-graph/builder.ts#L42) | ✅ **Way ahead** |
| `recordSymbolRead` integration | Reading a symbol → edit coverage | [`clients/module-report.ts`](../../clients/module-report.ts) | ✅ **Unique** — no gograph equivalent |
| `pilens_module_report` dual-surface | CLI+MCP only | [`tools/module-report.ts`](../../tools/module-report.ts) | ✅ **Ahead** |
| Turn-end cascade (auto-injected) | `handleTurnEnd` + advisory injection | [`clients/runtime-turn.ts`](../../clients/runtime-turn.ts) | ✅ **Unique** — gograph is user-invoked only |
| Full linter ecosystem (50+ tools) | Biome, ruff, eslint, oxlint, etc. | [`clients/dispatch/`](../../clients/dispatch/) | ✅ **Way ahead** |
| FactStore (transient per-file facts) | Conflated with graph in gograph | [`clients/dispatch/fact-store.ts`](../../clients/dispatch/fact-store.ts) | ✅ **Ahead** (cleaner separation) |
| Read-guard (pre-edit safety) | n/a | [`clients/read-guard.ts`](../../clients/read-guard.ts) | ✅ **Unique** |

## 5. Recommendations, prioritized

### 5.1 High value — adopt

#### 5.1.1 `risk` graded scoring `[gograph-specific]`

**Why**: replace flat `riskFlags` with structured 0–100 score + verdict.
Maps to existing turn-end advisory injection; mechanical to implement.

**What**: in [`query.ts:204-222`](../../clients/review-graph/query.ts#L204-L222), replace the `Set<string>` with a weighted-score struct:

```ts
interface RiskScore {
  total: number;          // 0..100
  verdict: "SAFE" | "REVIEW" | "DANGER";
  components: {
    blastRadius: number;   // 0..30, callers * 3 clamped
    complexity: number;    // 0..25, (cyclomatic - 1) * 2 clamped
    testGap: number;       // 0..20, 20 if no tests, 10 if 1, 0 if ≥ 2 (when test_edges exist)
    publicApi: number;     // 0..10
    highFanout: number;    // 0..10 if fanout ≥ 4
    boundaryWrapper: number; // 0..5
  };
}
```

**Effort**: ~2h. No new edge types needed. Updates `format.ts` to render `🟡 REVIEW (62/100): exported + high complexity, 7 untested callers` instead of flat flag list.

#### 5.1.2 `hotspot` query — fan-in ranking `[gograph-specific]`

**Why**: orientation in unfamiliar repos. Cheap, maps to `/lens-health`
or session-start summary blocks ("your repo's 10 most-called functions
are…").

**What**: count incoming `calls` edges per symbol, rank desc, emit top N.

**Effort**: ~30 LOC aggregation in `query.ts` (or new `queries/hotspot.ts`).

#### 5.1.3 `test_edges` re-extraction (edge-only overlay) `[gograph-specific]`

**Why**: this is the **biggest single design debt** flagged by the
comparison. PR #260 excluded all test files from the graph, but gograph
shows the same data as edges-only (1 entry per test_function) — adding
<1% bloat for >5x new capability.

**What**: add `testEdges: ReviewGraphEdge[]` field to `ReviewGraph`,
where each edge is `{ from: "test:<file>:<func>", to: <productionSymbolId>,
kind: "tests", metadata: { line } }`. Bump `REVIEW_GRAPH_VERSION` from
`v3` to `v4` so existing snapshots cleanly reject and rebuild.

The test-file-symbol → production-symbol association can be derived
during test-file parse:

1. Test file's symbols are extracted normally (their nodes are NOT added
   to the graph — kept out per #260).
2. For each test-file function, find the production symbols it calls
   (using the existing `resolveDeferredSymbolEdges` traversal over the
   test-file's parsed calls).
3. Emit one `tests` edge per call target.

**Effort**: ~half-day. Schema bump is the riskiest part — must be
coordinated with the persisted cache migration logic in
[`builder.ts:loadPersistedGraph`](../../clients/review-graph/builder.ts).

#### 5.1.4 `untested` sweep — single-query advisory `[gograph-specific]`

**Why**: the cleanest "where to add tests next" primitive. Maps directly
to pi-lens's existing turn-end advisory injection.

**What**: depends on #5.1.3 (test_edges). Single sweep over all
production symbol nodes: those with non-test `calls` edges pointing at
them but no `test_edges` pointing at them. Output by caller count desc
→ "the top 10 untested-but-used functions in this repo are…".

**Effort**: trivial once `test_edges` exist. Single pass over the
graph's symbol nodes.

#### 5.1.5 `context` composite — bundle node + source + callers + callees + tests + role `[gograph-specific]`

**Why**: gograph's signature agent value-add. One tool call replaces 4–5.
Maps directly to the **existing** `pilens_module_report` +
`pilens_read_symbol` + `pilens_impact` trio, but as a single structured
call instead of three.

**What**: new `pilens_context(symbol)` MCP tool. Internally:
1. Resolve `symbol` to one or more `SymbolNode`s (name or qualified name)
2. Look up file path + line range from `ReviewGraph`
3. Read the symbol body (existing `clients/module-report.ts:readSymbol()` logic)
4. Traverse incoming `calls` edges (one-hop → `ImpactMultiple(g, sym.ID)`)
5. Traverse outgoing `calls` edges (`Callees(g, sym.ID)`)
6. Look up `test_edges` where `from` includes this symbol
7. Compute `quickRole` heuristic (HTTP handler / data access / orchestrator / utility / entry point / internal)

**Effort**: half-day. New `pilens_context` tool + new
`queries/context.ts` query function. Existing
[`clients/module-report.ts`](../../clients/module-report.ts) provides
the `readSymbol()` body-extraction logic to compose.

#### 5.1.6 `plan` composite — pre-edit checklist `[gograph-specific]`

**Why**: the **single biggest feature gap** between pi-lens and gograph.
The current `computeImpactCascade` produces a flat neighbor list; a
`plan` composite produces an actionable pre-edit checklist with risk
breakdown, affected tests, and a ranked "read first" list.

**What**: depends on #5.1.3 (`test_edges`) + #5.1.5 (`context`).
Internally:
1. Resolve target symbols (file path or symbol name)
2. Find immediate callers → ordered "read first" list
3. Find affected test files (via `test_edges`)
4. Check public API signal (exported)
5. Downstream BFS for env/SQL/route reachability (when those edge types exist; see §5.2.3)
6. Compute `risk` score for the seed symbol

**Effort**: half-day. New `pilens_plan(symbol_or_file)` MCP tool.

### 5.2 Medium value — consider

#### 5.2.1 `deps` / `dependents` package-level closure

Promote the per-file `reverse-deps.ts` index to package-level by grouping
files by package (via go.mod / package.json / Cargo.toml / pyproject.toml,
using the existing `workspace-modules.ts` ModuleGraph) and adding
`packageDeps: Record<packageName, packageName[]>` to the project snapshot.

New BFS in `clients/review-graph/queries/deps.ts` returns
`{ direct, transitive }` closure per package. New `dependents()` returns
the inverse — who imports me, package-rolled-up.

Cost: low-medium. Most infrastructure (workspace detection) already exists.

#### 5.2.2 `orphans` query — zero callers

Symbol-level version of "is this dead code". Pi-lens already excludes
tests, so this would scan production symbols with no incoming `calls` or
`references` edges. Different problem than `untested` (which is about
test coverage, not usage).

Cheap to implement (~30 LOC). Lower priority than `untested` because
turn-end advisories don't usually need it.

#### 5.2.3 `routes` / `sql` / `envs` extraction

Tree-sitter queries for HTTP framework route definitions, SQL string
literals, env reads. Multi-language cost is real — each framework has its
own DSL — but the value for full-stack repos is high. Feeds `plan`'s
downstream reachability check.

Phase 6 of [`docs/reviewgraphenhancement.md`](./reviewgraphenhancement.md)
already proposes the parallel `routes_to | handles | configures` edge
kinds. Could be combined with this work.

#### 5.2.4 API drift detection (`api --since`)

Baseline-vs-current diff over exported symbols + routes + interface
methods + struct fields. Persist a `baseline` snapshot alongside the
project snapshot (similar to gograph's `GraphBaseline`). Most valuable
in CI / structured workflows.

Lower priority than #5.1.* because #202 mtime/size drift detection
already covers the most common case ("did anything change since last
build?"). This would add *what* changed, which is a richer question.

#### 5.2.5 Boundary enforcement

`.pi-lens/boundaries.json` with layer rules (which packages may import
which), a runner that emits 🔴 on violation. Mirrors pi-lens's existing
ast-grep rule architecture cleanly. Could be an MVP "layer-aware
import-check" using the existing `moduleGraph` already extracted in
`computeImpactCascade`.

### 5.3 Low value / skip

#### 5.3.1 `CalleeSymbolID` precise resolution `[deliberately skip]`

gograph's `--precise` mode uses the Go type-checker to populate
`CalleeSymbolID`. Pi-lens already handles the name-only case via
`resolveDeferredSymbolEdges` + `unresolvedName` fallback. Adding
type-checker-backed resolution would require per-language
infrastructure (tsserver for JS, pyright for Python, gopls for Go, etc.)
and would not unlock new agent-facing capabilities beyond what
`unresolvedName` already gives.

**Recommendation**: don't adopt. Document the deliberate non-adoption
in `clients/review-graph/types.ts` so future contributors don't re-propose it.

#### 5.3.2 `Mutation` / `Literal` / `Implements` edges

gograph-specific (Go type-checker dependent). For pi-lens's 17-language
support, equivalent would need per-language rule development; unclear
ROI for the advisory-grade accuracy pi-lens targets.

#### 5.3.3 Graph baseline as primary drift mechanism

pi-lens has #202 already, different but equivalent for the "did anything
change" question. A fuller API-drift primitive (#5.2.4) is a richer
question but orthogonal to the baseline mechanism.

## 6. Concrete next steps

If adopting #5.1.1–#5.1.6, suggested order (each step is independently
mergeable):

1. **`risk` scoring** (#5.1.1) — ~2h. Replaces flat `riskFlags`. Zero new edge types. Highest ROI per LOC.
2. **`hotspot` query** (#5.1.2) — ~30 LOC. Surfaces in `/lens-health`.
3. **`test_edges` re-extraction** (#5.1.3) — ~half-day. **Schema bump `v3 → v4`**. Restores "affected tests" capability at <1% size cost.
4. **`untested` sweep** (#5.1.4) — depends on #3. Single sweep, output to turn-end advisory as "tests-to-add" block.
5. **`context` composite** (#5.1.5) — ~half-day. New `pilens_context` tool + new `queries/context.ts` query function.
6. **`plan` composite** (#5.1.6) — depends on #3 + #5. New `pilens_plan` tool. The **headline** agent value-add.

Items #5.2.* are follow-on work (likely 1+ quarter out). Items #5.3.* are
documented non-adoptions.

## 7. Reuse pattern summary

Beyond specific primitives, the **patterns** worth carrying over to
pi-lens:

- **Token-saving composites**: any time two of pi-lens's existing tools
  are commonly called together, fuse them into one composite with a
  structured output. `pilens_module_report` + `pilens_read_symbol` is the
  precedent; `pilens_context` + `pilens_plan` would be the next.
- **Per-query file layout**: split `clients/review-graph/query.ts` into
  `queries/*.ts` to match `clients/dispatch/facts/` and gograph's
  one-query-per-file convention.
- **Pure functions over the graph**: every new query is a pure function
  `(graph, inputs) → output`. No I/O, no side effects, no daemon. This
  is what makes the cache + persistence + MCP story composable.
- **Graded verdicts, not flat flags**: turn `Set<string>` into
  weighted scores with named components and `SAFE | REVIEW | DANGER`
  verdicts. Actionable in advisory injection.
- **Stable IDs**: prefer module-rooted symbol IDs over file-rooted ones
  for rename-tolerance. (`SymbolNode.ID = "pkg::Symbol"` rather than
  `"path/to/file.ts:Symbol"`.)

## 8. What pi-lens has that gograph doesn't (preserve)

These are deliberate scope differences — don't shrink pi-lens to fit
gograph's shape:

1. **Multi-language** (17 langs via tree-sitter). Gograph is Go-only.
2. **`pilens_module_report` is a pi agent tool, not just MCP** (dual-surface pattern). Gograph is CLI+MCP only.
3. **`recordSymbolRead` integration**: reading a symbol via
   `pilens_read_symbol` records edit coverage for the read-guard. No
   gograph equivalent.
4. **FactStore**: per-file transient facts populated by dispatch runners,
   separate from the persisted graph. Gograph conflates these. Keep as-is.
5. **The full dispatch ecosystem** (50+ linters, formatters, autofix).
   Gograph is purely structural.
6. **Read-guard** — pre-edit safety layer. Gograph has no equivalent.
7. **Turn-end cascade integration** — graph feeds advisory injection on
   every edit. Gograph is user-invoked only.
8. **Hand-rolled zero-dep MCP server** — gograph uses a Go SDK. Pi-lens
   avoids the dep cost under `npm install --omit=dev`.

## 9. References

### gograph source (this analysis read at v1.4.88, commit main)

- `internal/graph/graph.go` — top-level `Graph` struct + `Version` v2 comment + 14 edge types
- `internal/parser/parser.go` — single-file AST extraction (FileResult bundling)
- `internal/precise/precise.go` — `--precise` type-checker pass for `CalleeSymbolID`
- `internal/search/context.go` — `Context()` + `quickRole()`
- `internal/search/plan.go` — `Plan()` + downstream BFS for env/SQL
- `internal/search/risk.go` — `Risk()` + 6-component weighted score
- `internal/search/deps.go` — `Deps()` + `Dependents()` package closure
- `internal/search/search.go` — `Query()` + `Callers()` + `Callees()` + `CallersDepth()` + `CalleeSymbolID` frontier discipline
- `internal/search/hotspot.go` — `Hotspot()` fan-in ranking
- `internal/search/orphans.go`, `internal/search/untested.go`, `internal/search/api.go`
- `internal/mcp/server.go` — 50+ tool surface, Go SDK
- `README.md` — "80% fewer tool calls" composite positioning

### Pi-lens files referenced

- `clients/review-graph/types.ts` — `ReviewGraph`, `ReviewGraphNode`, `ReviewGraphEdge`, `ImpactCascadeResult`
- `clients/review-graph/builder.ts` — `_doBuildGraph`, `confirmContentChanged`, `getGraphSourceFiles`, `addJsTsFile`, `addTreeSitterFile`, `resolveDeferredSymbolEdges`
- `clients/review-graph/query.ts` — `computeImpactCascade`, `computeTransitiveImpact`, `TransitiveImpactResult`, `ImpactHit`
- `clients/review-graph/service.ts` — facade, `recordEntitySnapshotDiff`
- `clients/review-graph/workspace-modules.ts` — ModuleGraph (existing package detection)
- `clients/word-index.ts` — BM25 + centrality boost (existing)
- `clients/reverse-deps.ts` — per-file reverse dep index (existing)
- `clients/module-report.ts` — `readSymbol()`, `moduleReport()` (existing — composes into `context`)
- `clients/dispatch/fact-store.ts` — `FactStore` (transient, separate from graph)
- `mcp/server.ts` — 16 tool surface, hand-rolled JSON-RPC
- `tools/module-report.ts` — pi agent tool wiring (dual-surface pattern)
- `AGENTS.md` — the maintainability seam discipline this doc respects
- `docs/reviewgraphenhancement.md` — the internal plan this doc complements
- `docs/module-report-read-symbol.md` — the dual-tool precedent for composites