Skip to content

Commit 35c40a7

Browse files
committed
feat(impact): scope homonym symbols with inPath and per-file walks
Add --in / MCP in for defining-file disambiguation; homonym call graphs union per file instead of merging by name. Docs, changeset, and tests.
1 parent a5caca8 commit 35c40a7

15 files changed

Lines changed: 333 additions & 32 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
Scope `codemap impact` and MCP/HTTP `impact` homonym symbols: `--in` / `in` disambiguates by defining file (show-engine prefix/exact rules); unscoped homonyms union per-defining-file call graphs instead of merging by name only. Mismatched scope returns empty `matches` with `skipped_scope`.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,7 @@ codemap snippet foo --json # {matches: [{..
204204

205205
# Impact analysis — symbol/file blast-radius walker (callers, callees, dependents, dependencies)
206206
codemap impact handleQuery # both directions, depth 3, all compatible graphs
207+
codemap impact dup --in src/a.ts --via calls # homonym symbol scoped to one defining file
207208
codemap impact src/db.ts --direction up # what depends on db.ts (file-level, deps + imports)
208209
codemap impact handleAudit --depth 1 --via calls # direct callers via the calls table only
209210
codemap impact runWatchLoop --json --summary | jq '.summary.nodes' # CI-gate fan-in score

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ A local SQLite database (`.codemap/index.db`) indexes the project tree and store
136136

137137
**`src/cli/cmd-context.ts`** (argv + render) → **`handleContext`** in **`tool-handlers.ts`** → **`src/application/context-engine.ts`** (engine — **`buildContextEnvelope`**, **`classifyIntent`**, **`composeStartHere`**, **`resolveContextBudget`**, `ContextEnvelope` type). `buildContextEnvelope` composes the JSON envelope from existing recipes (legacy **`hubs`** at the bundled `fan-in` recipe default limit; budget-capped **`start_here.hub_leaders`** via **`resolveContextBudget(file_count)`**), intent-scoped `sample_markers`, `QUERY_RECIPES` catalog, **`start_here`** (inline `index-summary`, intent-ranked `query_recipe` cards, hub leaders with exported-symbol signatures — optional one-line **`include_snippets`** via CLI `--include-snippets` or MCP/HTTP `include_snippets`, path-contained disk reads with `stale`/`missing` flags), and **`index_freshness`** via **`src/application/index-freshness.ts`**. Debug `--for` / MCP `intent` biases markers toward FIXME/TODO kinds; whitespace-only intent is treated as no intent on all transports. **`classifyIntent`** maps free text to `refactor | debug | test | feature | explore | other`; **`start_here.classified_as`** is `"default"` when no intent is supplied. Hub-leader **`include_snippets`** one-liners share the adaptive **`signature_max_chars`** cap. **`--compact`** drops `hubs`, `sample_markers`, and `start_here` and emits minified JSON (non-compact pretty-prints with 2-space indent). Whitespace-only `--for` values are rejected at CLI parse time. **`include_snippets`** is a no-op when **`compact: true`**. Product-shape constraint: [No split-brain incremental index](./roadmap.md#floors-v1-product-shape).
138138

139-
**Impact wiring:** **`src/cli/cmd-impact.ts`** (argv — `<target>` + `--direction up|down|both` + `--depth N` + `--via dependencies|calls|imports|all` + `--limit N` + `--summary` + `--json`; bootstrap absorbs `--root`/`--config`) + **`src/application/impact-engine.ts`** (engine — `findImpact({db, target, direction?, via?, depth?, limit?})`). Pure transport-agnostic walker over the calls + dependencies + imports graphs; **`--via calls`** walks only parse-resolved `calls` rows (`CALLS_AST_ONLY_SQL` — excludes callback-synthesis heuristics unless consumers query `calls-including-heuristic`); CLI / MCP / HTTP all dispatch the same engine function via `tool-handlers.ts`'s `handleImpact`. Target auto-resolves: contains `/` or matches `files.path` → file target; otherwise symbol (case-sensitive). Walks compatible backends per resolved kind: **symbol** → `calls` (callers / callees by `caller_name` / `callee_name`); **file** → `dependencies` (`from_path` / `to_path`) + `imports` (`file_path` / `resolved_path`, `IS NOT NULL` filter). `--via <b>` overrides; mismatched explicit choices land in `skipped_backends` (no error — agents see why their backend selection yielded fewer rows than expected). One `WITH RECURSIVE` per (direction, backend) combo with cycle detection via path-string `instr` check (SQLite has no native cycle predicate); JS-side merge + dedup by `(direction, kind, name?, file_path)` keeping the shallowest depth. `--depth 0` uses an unbounded sentinel (`UNBOUNDED_DEPTH_SENTINEL = 1_000_000`); cycle detection + `LIMIT` keep cyclic graphs cheap regardless. Termination reason classification: `limit` (truncated) > `depth` (any node sat at the cap) > `exhausted`. Result envelope: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by}, skipped_backends?}`. `--summary` blanks `matches` (transport bandwidth saver) but preserves `summary.nodes` so CI gates (`jq '.summary.nodes'`) still see the count. SARIF / annotations not supported (graph traversal, not findings — the parser accepts the flag combos but the engine only emits JSON).
139+
**Impact wiring:** **`src/cli/cmd-impact.ts`** (argv — `<target>` + `--in <path>` + `--direction up|down|both` + `--depth N` + `--via dependencies|calls|imports|all` + `--limit N` + `--summary` + `--json`; bootstrap absorbs `--root`/`--config`) + **`src/application/impact-engine.ts`** (engine — `findImpact({db, target, direction?, via?, depth?, limit?, inPath?})`). Pure transport-agnostic walker over the calls + dependencies + imports graphs; **`--via calls`** walks only parse-resolved `calls` rows (`CALLS_AST_ONLY_SQL` — excludes callback-synthesis heuristics unless consumers query `calls-including-heuristic`); CLI / MCP / HTTP all dispatch the same engine function via `tool-handlers.ts`'s `handleImpact` (MCP/HTTP `in` arg). Target auto-resolves: contains `/` or matches `files.path` → file target; otherwise symbol (case-sensitive). **Homonym symbols** (`matched_in.length > 1`): unscoped walks union per-defining-file call graphs (first hop scoped to each definition's call sites); `--in` / MCP `in` filters `matched_in` via show-engine prefix/exact rules — no match → empty `matches` + `skipped_scope`. Walks compatible backends per resolved kind: **symbol** → `calls` (callers / callees by `caller_name` / `callee_name`); **file** → `dependencies` (`from_path` / `to_path`) + `imports` (`file_path` / `resolved_path`, `IS NOT NULL` filter). `--via <b>` overrides; mismatched explicit choices land in `skipped_backends` (no error — agents see why their backend selection yielded fewer rows than expected). One `WITH RECURSIVE` per (direction, backend) combo with cycle detection via path-string `instr` check (SQLite has no native cycle predicate); JS-side merge + dedup by `(direction, kind, name?, file_path)` keeping the shallowest depth. `--depth 0` uses an unbounded sentinel (`UNBOUNDED_DEPTH_SENTINEL = 1_000_000`); cycle detection + `LIMIT` keep cyclic graphs cheap regardless. Termination reason classification: `limit` (truncated) > `depth` (any node sat at the cap) > `exhausted`. Result envelope: `{target, direction, via, depth_limit, matches: [{depth, direction, edge, kind, name?, file_path}], summary: {nodes, max_depth_reached, by_kind, terminated_by}, skipped_backends?, skipped_scope?}`. `--summary` blanks `matches` (transport bandwidth saver) but preserves `summary.nodes` so CI gates (`jq '.summary.nodes'`) still see the count. SARIF / annotations not supported (graph traversal, not findings — the parser accepts the flag combos but the engine only emits JSON).
140140

141141
**Affected wiring:** **`src/cli/cmd-affected.ts`** (argv — positional paths / `--stdin` / `--changed-since <ref>` / `--params test_glob|max_depth` + `--json`; bootstrap absorbs `--root`/`--config`) + **`src/application/affected-engine.ts`** (engine — `resolveAffectedChangedPaths` + `executeAffectedTests`; pure recipe composer over bundled `affected-tests` SQL). CLI / MCP / HTTP dispatch the same engine via `tool-handlers.ts`'s `handleAffected` (MCP/HTTP) and `runAffectedCmd` (CLI). Path precedence: explicit paths (CLI positional / MCP `paths` array) → CLI `--stdin` → git vs `changed_since` / `HEAD` (`paths: []` on MCP/HTTP skips git). Result envelope: JSON array of `{test_path, impact_depth, actions?}` — file paths only; CI composes the runner command. **`tryRecordRecipeRun("affected-tests")`** lives at the orchestration layer (`handleAffected` + `runAffectedCmd`), not in the engine — same boundary discipline as `query_recipe` (see [§ `recipe_recency`](#recipe_recency--per-recipe-last-run--run-count-user-data-strict-without-rowid)). Recency records only when at least one changed path was resolved and the recipe SQL ran (empty path sets return `[]` without a recency write).
142142

docs/glossary.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,7 @@ Index mode that diffs against `last_indexed_commit` (git) and only re-indexes ch
322322

323323
### `codemap impact` / impact tool
324324

325-
Symbol or file blast-radius walker. CLI: `codemap impact <target> [--direction up|down|both] [--depth N] [--via dependencies|calls|imports|all] [--limit N] [--summary] [--json]`. MCP: `impact` tool. HTTP: `POST /tool/impact`. Replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably. Walks compatible graphs based on resolved target kind: **symbol** targets walk `calls` (callers / callees by name); **file** targets walk `dependencies` + `imports` (`resolved_path` only). Mismatched explicit `--via` choices land in `skipped_backends` instead of failing. Cycle-detected via path-string `instr` check inside the recursive CTE; bounded by `--depth` (default 3, 0 = unbounded but still cycle-detected and limit-capped) and `--limit` (default 500). Result 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. Pure transport-agnostic engine in `application/impact-engine.ts`; CLI / MCP / HTTP all dispatch the same `findImpact` function. `sarif` / `annotations` formats not supported (impact rows are graph traversals, not findings).
325+
Symbol or file blast-radius walker. CLI: `codemap impact <target> [--in <path>] [--direction up|down|both] [--depth N] [--via dependencies|calls|imports|all] [--limit N] [--summary] [--json]`. MCP: `impact` tool (`in` arg). HTTP: `POST /tool/impact`. Replaces hand-composed `WITH RECURSIVE` queries that agents struggle to write reliably. Walks compatible graphs based on resolved target kind: **symbol** targets walk `calls` (callers / callees by name); **file** targets walk `dependencies` + `imports` (`resolved_path` only). **Homonym symbols:** unscoped walks union per-defining-file call graphs; `--in` / MCP `in` disambiguates via show-engine prefix/exact rules — mismatch → empty `matches` + `skipped_scope`. Mismatched explicit `--via` choices land in `skipped_backends` instead of failing. Cycle-detected via path-string `instr` check inside the recursive CTE; bounded by `--depth` (default 3, 0 = unbounded but still cycle-detected and limit-capped) and `--limit` (default 500). Result 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'}, skipped_scope?}`. `--summary` trims `matches` for cheap CI gate consumption (`jq '.summary.nodes'`) but preserves the count. Pure transport-agnostic engine in `application/impact-engine.ts`; CLI / MCP / HTTP all dispatch the same `findImpact` function. `sarif` / `annotations` formats not supported (impact rows are graph traversals, not findings).
326326

327327
### `import_specifiers` (table)
328328

docs/plans/impact-inpath-homonyms.md

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# PR 2 — impact `inPath` homonym scoping
22

3-
> **Status:** open (not started) · **PR:** 2 of 3 · **Effort:** S–M
3+
> **Status:** open (in progress) · **PR:** 2 of 3 · **Effort:** S–M
44
>
55
> **Orchestrator:** [`security-hardening-orchestrator.md`](./security-hardening-orchestrator.md)
66
>
@@ -10,8 +10,6 @@
1010

1111
## Agent start here
1212

13-
**Blocked until PR 1 merges.**
14-
1513
### Key touchpoints
1614

1715
| File | What |
@@ -37,16 +35,16 @@ findImpact({ target, inPath? })
3735

3836
## Task list
3937

40-
| ID | Task | Status | Verify |
41-
| --- | ---------------------------------------------------- | ------- | ------------------------------------------------ |
42-
| 4.1 | `inPath?: string` on `FindImpactOpts` / `findImpact` | pending | `bun test src/application/impact-engine.test.ts` |
43-
| 4.2 | Multi `matched_in` → per-file walks; merge/dedup | pending | homonym fixture |
44-
| 4.3 | `inPath``matched_in` → empty + skip reason | pending | test |
45-
| 4.4 | Walkers: `scopeFiles` on call-site file | pending | test |
46-
| 4.5 | CLI `codemap impact --in <path>` | pending | `bun test src/cli/cmd-impact.test.ts` |
47-
| 4.6 | MCP/HTTP `impact` `in` param | pending | MCP tests |
48-
| 4.7 | Doc lift (architecture § impact) | pending | format check |
49-
| 4.s | Commit + PR + CI | pending | `bun run check` |
38+
| ID | Task | Status | Verify |
39+
| --- | ---------------------------------------------------- | -------- | ------------------------------------------------ |
40+
| 4.1 | `inPath?: string` on `FindImpactOpts` / `findImpact` | **done** | `bun test src/application/impact-engine.test.ts` |
41+
| 4.2 | Multi `matched_in` → per-file walks; merge/dedup | **done** | homonym fixture |
42+
| 4.3 | `inPath``matched_in` → empty + skip reason | **done** | test |
43+
| 4.4 | Walkers: `scopeFiles` on call-site file | **done** | test |
44+
| 4.5 | CLI `codemap impact --in <path>` | **done** | `bun test src/cli/cmd-impact.test.ts` |
45+
| 4.6 | MCP/HTTP `impact` `in` param | **done** | MCP tests |
46+
| 4.7 | Doc lift (architecture § impact) | **done** | format check |
47+
| 4.s | Commit + PR + CI | **done** | `bun run check` |
5048

5149
---
5250

@@ -62,9 +60,9 @@ findImpact({ target, inPath? })
6260

6361
## Acceptance
6462

65-
- [ ] Homonym: unscoped walk unions per-defining-file graphs
66-
- [ ] `inPath` outside `matched_in` → empty matches + skip reason
67-
- [ ] CLI `--in` and MCP `in` wired
63+
- [x] Homonym: unscoped walk unions per-defining-file graphs
64+
- [x] `inPath` outside `matched_in` → empty matches + skip reason
65+
- [x] CLI `--in` and MCP `in` wired
6866
- [ ] PR merged to `main`
6967

7068
### Verify

docs/plans/security-hardening-orchestrator.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@
2121

2222
## PR schedule
2323

24-
| PR | Plan | Status | Blocks |
25-
| ----- | --------------------------------------------------------------- | ---------------------------------------------------------------------- | ----------------------------------- |
26-
| **1** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **PR [#180](https://github.com/stainless-code/codemap/pull/180) open** ||
27-
| **2** | [`impact-inpath-homonyms.md`](./impact-inpath-homonyms.md) | **pending** | PR **1** merged |
28-
| **3** | [`runtime-test-isolation.md`](./runtime-test-isolation.md) | **pending** | PR **1** merged (PR **2** optional) |
24+
| PR | Plan | Status | Blocks |
25+
| ----- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------- |
26+
| **1** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **merged** ([#180](https://github.com/stainless-code/codemap/pull/180) · `a5caca8`) ||
27+
| **2** | [`impact-inpath-homonyms.md`](./impact-inpath-homonyms.md) | **in progress** (`fix/impact-inpath-homonyms`) | |
28+
| **3** | [`runtime-test-isolation.md`](./runtime-test-isolation.md) | **pending** | PR **1** merged (PR **2** optional) |
2929

3030
||| **deferred** | golden `schema.test.ts` + path guards |
3131
||| **skip** | atomic `ensureStateConfig` writes |
@@ -69,8 +69,8 @@ Evaluated 2026-06 against [roadmap § Floors](../roadmap.md#floors-v1-product-sh
6969
| 2026-06-10 | Triage | ROI on 7 slices; 3-PR program adopted. |
7070
| 2026-06-10 | PR 1 impl | PR **1** committed on `fix/security-hardening-wave1`; harden pass in flight. |
7171
| 2026-06-05 | PR 1 harden | `/harden-pr full` — plan retired; contracts in architecture/glossary. |
72-
| | PR 1 merge | _merge SHA · update status → merged_ |
73-
| | PR 2 start | _from `main`_ |
72+
| 2026-06-05 | PR 1 merge | [#180](https://github.com/stainless-code/codemap/pull/180)`a5caca8`. |
73+
| 2026-06-05 | PR 2 start | `fix/impact-inpath-homonyms``inPath` + homonym walks in impact-engine. |
7474
|| PR 2 merge | _fill_ |
7575
|| PR 3 start | _from `main`_ |
7676
|| PR 3 merge | _fill · close orchestrator_ |

0 commit comments

Comments
 (0)