UNPKG

pi-lens

Version:

Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo

601 lines (473 loc) • 32.1 kB
# 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