Skip to content

Commit 90092ae

Browse files
feat(impact): codemap impact — symbol/file blast-radius walker (impl of PR #49 plan) (#50)
* feat(impact): pure transport-agnostic walker engine (Tracer 1) application/impact-engine.ts — single WITH RECURSIVE per (direction, backend), JS-side merge + dedup + summary. Backends: - calls (symbol-level, up=callers / down=callees) - dependencies (file-level) - imports (file-level, resolved_path only) Plan §D2 backend split, §D6 cycle detection (path-string + instr), §D8 edge tags. 27 unit tests cover: walks (up/down/both), depth caps, limit caps, self-loop + 3-cycle, file-vs-symbol resolution, --via skipped_backends. * feat(impact): cmd-impact.ts CLI verb wired into main + bootstrap (Tracer 2) - src/cli/cmd-impact.ts — parser + runner mirroring cmd-show.ts shape - main.ts dispatch, bootstrap.ts validateIndexModeArgs + printCliUsage - 14 parser tests cover happy paths + 9 error cases (--depth/limit/direction/via validation) Smoke: `bun src/index.ts impact src/db.ts --direction up --depth 1` lists the real fan-in on this repo (every file in src/ that depends on db.ts). `bun src/index.ts impact handleQuery --json --summary` returns the summary shape ready for jq pipelines. * feat(impact): MCP impact tool wired through tool-handlers (Tracer 3) - impactArgsSchema + handleImpact in tool-handlers.ts (Zod + ToolResult shape) - registerImpactTool in mcp-server.ts (one-line wrapper, like show/snippet) - 5 MCP integration tests (tools/list, symbol target, file target, summary trims matches but keeps summary.nodes count, Zod rejects float depth) HTTP POST /tool/impact lights up automatically next tracer via the existing http-server.ts dispatcher (one switch arm). * feat(impact): HTTP POST /tool/impact dispatcher (Tracer 4) - TOOL_NAMES gains 'impact' (auto-listed in /tools) - dispatchTool switch arm with Zod validation (400 on bad body) - 4 integration tests: happy path, missing target, non-int depth, bad direction enum Both transports (MCP + HTTP) now expose impact via the same handleImpact pure function. * docs(impact): sync README + architecture + glossary + agent rule/skill + changeset (Tracer 5) - README.md: new 'Impact analysis' code block + impact in MCP tools list - docs/architecture.md: 'Impact wiring' section + impact-engine in application/ inventory - docs/glossary.md: 'codemap impact / impact tool' entry under I (full envelope spec) - docs/roadmap.md: drop the impact backlog item (it shipped) - docs/research/fallow.md: Adjacent-shipped bullet referencing PR #49 (plan) + #50 (impl) - .agents/rules/codemap.md + templates/agents/rules/codemap.md: new table row + 'Impact' paragraph + impact in MCP tools list (Rule 10 lockstep) - .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/SKILL.md: impact tool description (Rule 10 lockstep) - .changeset/codemap-impact.md: minor — full design rationale + decisions - docs/plans/impact.md: deleted per docs-governance Rule 3 (canonical descriptions now live in architecture / glossary / README) * chore(impact): slim self-authored comments per concise-comments rule Re-read every comment authored in this PR; cut/slim ones a teammate could re-derive in <30s. Targets: - impact-engine.ts: drop trivial type doc, slim findImpact JSDoc, slim termination heuristic from 6 lines → 2, drop redundant inline notes - cmd-impact.ts + tool-handlers.ts: shorten the --summary trim explanation No behaviour change; checks green (89 tests + lint + typecheck). * chore(impact): apply CodeRabbit nitpicks 1+4 - impact-engine.ts:333 — one-line comment explaining empty-string file_path fallback (external/dynamic call sites) - impact-engine.test.ts:209 — strengthen exact-fit limit test from not.toBe('limit') to toBe('exhausted') Pushed back on nitpicks 2+3 (hallucinated — see PR comment) and 5 (style; deterministic order is fine to lock).
1 parent c76f09a commit 90092ae

22 files changed

Lines changed: 1528 additions & 182 deletions

.agents/rules/codemap.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ A local database (default **`.codemap.db`**) indexes structure: symbols, imports
3030
| Watch mode (live reindex) || `bun src/index.ts watch [--debounce 250] [--quiet]` — long-running; debounced reindex on file changes. Combine with `mcp --watch` / `serve --watch` (or `CODEMAP_WATCH=1`) so every tool reads a live index without per-request prelude. |
3131
| Targeted read (metadata) || `bun src/index.ts show <name> [--kind <k>] [--in <path>] [--json]` — file:line + signature |
3232
| Targeted read (source text) || `bun src/index.ts snippet <name> [--kind <k>] [--in <path>] [--json]` — same lookup + source from disk + stale flag |
33+
| Impact (blast-radius walker) || `bun src/index.ts impact <target> [--direction up\|down\|both] [--depth N] [--via <b>] [--limit N] [--summary] [--json]` — replaces hand-composed `WITH RECURSIVE` queries |
3334
| SARIF / GH annotations || `bun src/index.ts query --recipe deprecated-symbols --format sarif` · `… --format annotations` |
3435

3536
**Recipe `actions`:** with **`--json`**, recipes that define an `actions` template append it to every row (kebab-case verb + description — e.g. `fan-out``review-coupling`). Under `--baseline`, actions attach to the **`added`** rows only. Inspect via **`--recipes-json`**. Ad-hoc SQL never carries actions.
@@ -55,9 +56,11 @@ Validation: SQL is rejected at load time if it starts with DML/DDL (DELETE/DROP/
5556

5657
**Targeted reads (`show` / `snippet`)**: precise lookup by exact symbol name without composing SQL. `show` returns metadata (`file_path:line_start-line_end` + `signature`); `snippet` returns the source text from disk plus `stale` / `missing` flags. Both share the same flag set (`--kind <k>` to filter by `symbols.kind`, `--in <path>` for file-scope filter — directory prefix or exact file). Output envelope is `{matches, disambiguation?}` — single match → `{matches: [{...}]}`; multi-match adds `disambiguation: {n, by_kind, files, hint}` so agents narrow without re-scanning. Name match is exact / case-sensitive — for fuzzy use `query` with `LIKE '%name%'`. Snippet stale-file behavior: `source` is always returned when the file exists; `stale: true` means the line range may have shifted (re-index with `bun src/index.ts` or `--files <path>` before acting on the source).
5758

59+
**Impact (`bun src/index.ts impact <target>`)**: symbol/file blast-radius walker — replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write. Target auto-resolves: contains `/` or matches `files.path` → file target; otherwise symbol (case-sensitive). Walks compatible graphs by target kind: **symbol** → `calls` (callers / callees by name); **file** → `dependencies` + `imports` (`resolved_path` only). `--via <b>` overrides; mismatched explicit choices land in `skipped_backends` (no error). Cycle-detected via `WITH RECURSIVE` path-string + `instr` check; bounded by `--depth N` (default 3, `0` = unbounded but still cycle-detected and limit-capped) and `--limit N` (default 500). Output envelope: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by: 'depth'|'limit'|'exhausted'}}`. `--summary` trims `matches` for cheap CI-gate consumption (`jq '.summary.nodes'`) but preserves the count. SARIF / annotations not supported (graph traversal, not findings). Pure transport-agnostic engine in `application/impact-engine.ts`; CLI / MCP / HTTP all dispatch the same `findImpact` function.
60+
5861
**MCP server (`bun src/index.ts mcp`)**: stdio MCP (Model Context Protocol) server — agents call codemap as JSON-RPC tools instead of shelling out to the CLI on every read. v1 ships one tool per CLI verb plus four lazy-cached resources:
5962

60-
- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate` / `show` / `snippet`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab).
63+
- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate` / `show` / `snippet` / `impact`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab).
6164
- **`query_batch` (MCP-only):** N statements in one round-trip. Items are `string | {sql, summary?, changed_since?, group_by?}` — string form inherits batch-wide flag defaults, object form overrides on a per-key basis. Per-statement errors are isolated.
6265
- **`save_baseline` (polymorphic):** one tool, `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=<name>` verb).
6366
- **Resources:** `codemap://recipes` (catalog), `codemap://recipes/{id}` (one recipe), `codemap://schema` (live DDL from `sqlite_schema`), `codemap://skill` (bundled SKILL.md text). Lazy-cached on first `read_resource`.

.agents/skills/codemap/SKILL.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
7373
- **`validate`**`{paths?: string[]}`. Compares on-disk SHA-256 to indexed `files.content_hash`; empty `paths` validates everything. Returns rows with status (`ok`/`stale`/`missing`/`unindexed`).
7474
- **`show`**`{name, kind?, in?}`. Exact, case-sensitive symbol name lookup. Returns `{matches: [{name, kind, file_path, line_start, line_end, signature, ...}], disambiguation?: {n, by_kind, files, hint}}`. Single match → `{matches: [{...}]}`; multi-match adds the disambiguation envelope so you narrow without re-scanning. Fuzzy lookup belongs in `query` with `LIKE`.
7575
- **`snippet`**`{name, kind?, in?}`. Same lookup as `show` but each match also carries `source` (file lines from disk at `line_start..line_end`), `stale` (true when content_hash drifted since indexing — line range may have shifted), `missing` (true when file is gone). Per Q-6 (settled): `source` is always returned when the file exists; agent decides whether to act on stale content or run `codemap` / `codemap --files <path>` to re-index first. No auto-reindex side-effects from this read tool.
76+
- **`impact`** — `{target, direction?, via?, depth?, limit?, summary?}`. Symbol/file blast-radius walker — replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably. `target` is a symbol name (case-sensitive, exact) OR a project-relative file path (auto-detected by `/` or by matching `files.path`). `direction`: `up` (callers / dependents), `down` (callees / dependencies), `both` (default). `via`: `dependencies`, `calls`, `imports`, `all` (default — every backend compatible with the resolved target kind: symbol → `calls`; file → `dependencies` + `imports`; mismatched explicit choices land in `skipped_backends`, no error). `depth` default 3, `0` = unbounded (still cycle-detected and limit-capped). `limit` default 500. `summary: true` trims `matches` for cheap CI-gate consumption (`jq '.summary.nodes'`) but preserves the count. Result: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by: 'depth'|'limit'|'exhausted'}}`. Cycle detection is approximate-but-bounded — bounded depth + `LIMIT` keep cyclic graphs cheap; `terminated_by` reports the dominant stop reason. SARIF / annotations not supported (impact rows are graph traversals, not findings).
7677

7778
**Resources (lazy-cached on first `read_resource`; constant for server-process lifetime):**
7879

.changeset/codemap-impact.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@stainless-code/codemap": minor
3+
---
4+
5+
`codemap impact <target>` — symbol/file blast-radius walker. Replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably with a single verb that walks the calls / dependencies / imports graphs (callers, callees, dependents, dependencies). Depth- and limit-bounded, cycle-detected.
6+
7+
**Three transports, one engine:**
8+
9+
- **CLI:** `codemap impact <target> [--direction up|down|both] [--depth N] [--via dependencies|calls|imports|all] [--limit N] [--summary] [--json]`
10+
- **MCP tool:** `impact` (registered alongside `show` / `snippet`)
11+
- **HTTP:** `POST /tool/impact`
12+
13+
All three dispatch the same pure `findImpact` engine in `application/impact-engine.ts` per the post-PR #41 layering — adding tools never duplicates business logic.
14+
15+
**Decisions worth knowing:**
16+
17+
- **Target auto-resolution.** Contains `/` or matches `files.path` → file target; otherwise symbol (case-sensitive, exact). Symbol targets walk `calls`; file targets walk `dependencies` + `imports` (`resolved_path` only). Mismatched explicit `--via` choices land in `skipped_backends` (no error — agent sees why their selection yielded fewer rows than expected).
18+
- **Cycle detection.** SQLite has no native cycle predicate; we materialise a comma-bounded path string per row and `instr` it to break re-entry. Bounded depth + `--limit` (default 500) keep cyclic graphs cheap regardless. `--depth 0` walks unbounded but stays cycle-detected and limit-capped.
19+
- **Termination classification.** `summary.terminated_by`: `limit` > `depth` > `exhausted`. CI gates can branch on it.
20+
- **`--summary` shape.** Trims the `matches` array but preserves `summary.nodes` — the `jq '.summary.nodes'` consumption pattern still works.
21+
- **No SARIF / annotations.** Impact rows are graph traversals, not findings — wrong shape for those formats.
22+
23+
**Engine sketch:** one `WITH RECURSIVE` query per (direction, backend) combo, JS-side merge + dedup by `(direction, kind, name?, file_path)` keeping the shallowest depth, then `summary.by_kind` + `terminated_by` classification.
24+
25+
Plan: PR #49 (merged). Implementation: PR #50.

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,19 @@ codemap snippet foo --json # {matches: [{..
146146
# Output envelope is always {matches, disambiguation?} — single match → {matches: [{...}]};
147147
# multi-match adds disambiguation: {n, by_kind, files, hint} for agent-friendly narrowing.
148148

149+
# Impact analysis — symbol/file blast-radius walker (callers, callees, dependents, dependencies)
150+
codemap impact handleQuery # both directions, depth 3, all compatible graphs
151+
codemap impact src/db.ts --direction up # what depends on db.ts (file-level, deps + imports)
152+
codemap impact handleAudit --depth 1 --via calls # direct callers via the calls table only
153+
codemap impact runWatchLoop --json --summary | jq '.summary.nodes' # CI-gate fan-in score
154+
# Replaces hand-composed `WITH RECURSIVE` queries. Cycle-detected, depth-bounded
155+
# (default 3, --depth 0 = unbounded), limit-capped (default 500). Result envelope:
156+
# {target, matches: [{depth, edge, kind, name?, file_path}], summary: {nodes, terminated_by}}.
157+
149158
# MCP server (Model Context Protocol) — for agent hosts (Claude Code, Cursor, Codex, generic MCP clients)
150159
codemap mcp # JSON-RPC on stdio; one tool per CLI verb plus query_batch
151160
# Tools: query, query_batch (MCP-only — N statements in one round-trip), query_recipe, audit,
152-
# save_baseline, list_baselines, drop_baseline, context, validate
161+
# save_baseline, list_baselines, drop_baseline, context, validate, show, snippet, impact
153162
# Resources: codemap://recipes, codemap://recipes/{id}, codemap://schema, codemap://skill (lazy-cached)
154163
# Output shape verbatim from `--json` envelopes (no re-mapping). Snake_case throughout.
155164

0 commit comments

Comments
 (0)