Skip to content

Commit cbcf1e8

Browse files
committed
harden: impact homonym parity, plan retirement, and inPath edge case
Align consumer surfaces, delete shipped plan, fix skipped_scope on unknown symbols with inPath, and add homonym/MCP regression tests.
1 parent 35c40a7 commit cbcf1e8

13 files changed

Lines changed: 69 additions & 92 deletions

File tree

.agents/skills/harden-pr/LEDGER.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ By-design or false-positive findings — do not re-raise.
1010
- **[category]** `file:line` — label: reason
1111
```
1212

13+
- **[correctness]** `src/application/impact-engine.ts:147` — explicit inPath on single-definition symbol enables first-hop scopeFiles: by-design — matches show `--in` disambiguation (plan P2.1).
14+
- **[correctness]** `src/application/impact-engine.ts:162` — per-file walk LIMIT before global dedup: by-design v1 — plan architecture per-defining-file walks; global limit still applies at slice.
15+
1316
<!-- Example:
1417
- **[security]** `src/cli/proxy.ts:42` — https_proxy env: by-design — standard CLI proxy convention.
1518
-->

.changeset/impact-inpath-homonyms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@stainless-code/codemap": patch
33
---
44

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`.
5+
Scope `codemap impact` and MCP/HTTP `impact` homonym symbols: `--in` / `in` disambiguates by defining file (same prefix/exact rules as `codemap show --in`); 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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,8 @@ codemap impact handleAudit --depth 1 --via calls # direct callers
210210
codemap impact runWatchLoop --json --summary | jq '.summary.nodes' # CI-gate fan-in score
211211
# Replaces hand-composed `WITH RECURSIVE` queries. Cycle-detected, depth-bounded
212212
# (default 3, --depth 0 = unbounded), limit-capped (default 500). Result envelope:
213-
# {target, matches: [{depth, edge, kind, name?, file_path}], summary: {nodes, terminated_by}}.
213+
# {target, matches: [...], summary: {nodes, terminated_by}, skipped_scope?}.
214+
# Homonym symbols: unscoped unions per-defining-file graphs; --in scopes one file.
214215

215216
# Affected tests — reverse dependency walk from changed sources → test files to run
216217
codemap affected --json # working-tree changes vs HEAD (git status + diff)

docs/plans/impact-inpath-homonyms.md

Lines changed: 0 additions & 79 deletions
This file was deleted.

docs/plans/security-hardening-orchestrator.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
| PR | Plan | Status | Blocks |
2525
| ----- | --------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ----------------------------------- |
2626
| **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`) ||
27+
| **2** | lifted → [`architecture.md`](../architecture.md) (plan retired) | **PR open** (`fix/impact-inpath-homonyms`) ||
2828
| **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 |
@@ -71,7 +71,8 @@ Evaluated 2026-06 against [roadmap § Floors](../roadmap.md#floors-v1-product-sh
7171
| 2026-06-05 | PR 1 harden | `/harden-pr full` — plan retired; contracts in architecture/glossary. |
7272
| 2026-06-05 | PR 1 merge | [#180](https://github.com/stainless-code/codemap/pull/180)`a5caca8`. |
7373
| 2026-06-05 | PR 2 start | `fix/impact-inpath-homonyms``inPath` + homonym walks in impact-engine. |
74-
|| PR 2 merge | _fill_ |
74+
| 2026-06-05 | PR 2 harden | `/harden-pr full` — plan retired; CLI/MCP/docs parity. |
75+
|| PR 2 merge | _PR URL · merge SHA_ |
7576
|| PR 3 start | _from `main`_ |
7677
|| PR 3 merge | _fill · close orchestrator_ |
7778

docs/roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Predicate-as-API only — enrich row shape and audit deltas; no standalone pass/
108108
- [ ] **`organize-imports` diff-shape recipe** — deterministic single-file import sort/group; `imports.line_number` + `source` substrate sufficient. Review-first (`auto_fixable: false`). Effort: S.
109109
- [ ] **`codemap-to-tsmorph` Path B adapter** — separate package experiment: `query_recipe` discovery → `ts-morph` / `jscodeshift` transforms for AST-shape edits codemap's substring executor defers (see [architecture § Rejected apply-path alternatives](./architecture.md#apply--input-modes-transport-and-policy)). Not an in-tree AST writer (Path A rejected). Effort: M.
110110
- [ ] **Apply write-safety hardening** — close apply TOCTOU: SHA-256 `hashContent` at phase-1 read, recheck disk hash immediately before phase-2 write (`file content changed` conflict); `fsync` temp file before `rename`; skip files with mixed CRLF/LF (`mixed line endings`). Preserves all-or-nothing on any conflict. Plan: [`plans/apply-write-safety.md`](./plans/apply-write-safety.md). Effort: L.
111-
- [ ] **Read-surface hardening (3 PRs)** — query/HTTP/validate safety, `impact` `inPath` homonyms, runtime guards + test teardown. **Orchestrator:** [`plans/security-hardening-orchestrator.md`](./plans/security-hardening-orchestrator.md). PR1 ([#180](https://github.com/stainless-code/codemap/pull/180), lifted to [architecture](./architecture.md)) · Plans: [PR2](./plans/impact-inpath-homonyms.md) · [PR3](./plans/runtime-test-isolation.md). Effort: S–M.
111+
- [ ] **Read-surface hardening (3 PRs)** — query/HTTP/validate safety, `impact` `inPath` homonyms, runtime guards + test teardown. **Orchestrator:** [`plans/security-hardening-orchestrator.md`](./plans/security-hardening-orchestrator.md). PR1 ([#180](https://github.com/stainless-code/codemap/pull/180), lifted to [architecture](./architecture.md)) · PR2 (`impact` homonyms, lifted to [architecture](./architecture.md)) · Plans: [PR3](./plans/runtime-test-isolation.md). Effort: S–M.
112112
- [ ] **`history` table** (deferred — revisit-triggered) — temporal queries: "when did symbol X get `@deprecated`?", "coverage trend over last 50 commits", "files that became dead this week". `audit --base <ref>` covers the most-common temporal question (PR-scoped diff) without schema growth, so the table earns its place only when bigger questions emerge. Two shapes (per-commit snapshots ~N × DB size; append-only event log heavier CTE walks); both pay an N-reindexes backfill cost (~30s per reindex). **Revisit triggers:** two consumers ship `jq`-based "audit-runs-over-time" workflows, OR `query_baselines` evolution becomes a recurring agent need.
113113
- [ ] **`codemap audit` verdict + thresholds** (v1.x) — `verdict: "pass" | "warn" | "fail"` driven by an `audit.deltas[<key>].{added_max, action}` field on the config object (`.codemap/config.{ts,js,json}`). Triggers: two consumers ship `jq`-based threshold scripts with similar shapes, OR one consumer asks with a concrete config sketch. Until then, raw deltas + consumer-side `jq` is the CI exit-code idiom. **Likely accelerant:** the Marketplace Action (next item) shipping is the most plausible path to firing the trigger — once `- uses: stainless-code/codemap@v1` is the dominant CI path, real `jq` threshold scripts will surface.
114114
- [ ] **GitHub Marketplace Action — publish + listing finish** — core Action implementation is in-tree: root `action.yml`, `query --ci`, `audit --format sarif` / `--ci`, package-manager detection, dogfood smoke, and opt-in `pr-comment` summary renderer have shipped. Remaining work is the release/listing slice: `MARKETPLACE.md`, `v1.0.0` / floating `v1` tags, Marketplace setup, sacrificial-repo smoke, and making `action-smoke` blocking once the Action tag exists. Action version stream is independent of CLI version (`package.json` currently drives CLI/npm version; Action publishes at its own `v1.0.0`). Plan: [`plans/github-marketplace-action.md`](./plans/github-marketplace-action.md). Effort: S.

src/application/impact-engine.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,17 @@ describe("findImpact — inPath + homonym scoping", () => {
417417
expect(r.matches.map((m) => m.name)).toEqual(["onlyA"]);
418418
});
419419

420+
it("inPath on unknown symbol omits skipped_scope", () => {
421+
const r = findImpact(db, {
422+
target: "missing",
423+
direction: "down",
424+
via: "calls",
425+
inPath: "src/a.ts",
426+
});
427+
expect(r.target.matched_in).toEqual([]);
428+
expect(r.skipped_scope).toBeUndefined();
429+
});
430+
420431
it("inPath outside matched_in returns empty matches with skipped_scope", () => {
421432
const r = findImpact(db, {
422433
target: "dup",
@@ -428,6 +439,18 @@ describe("findImpact — inPath + homonym scoping", () => {
428439
expect(r.skipped_scope?.reason).toContain("src/z.ts");
429440
});
430441

442+
it("unscoped homonym unions per-file graphs for direction=up", () => {
443+
seedCall("src/a.ts", "callerA", "dup");
444+
seedCall("src/b.ts", "callerB", "dup");
445+
446+
const r = findImpact(db, {
447+
target: "dup",
448+
direction: "up",
449+
via: "calls",
450+
});
451+
expect(r.matches.map((m) => m.name).sort()).toEqual(["callerA", "callerB"]);
452+
});
453+
431454
it("inPath directory prefix scopes homonym definitions", () => {
432455
seedFile("src/pkg/b.ts");
433456
seedSymbol("dup", "src/pkg/b.ts");

src/application/impact-engine.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,13 @@ export function findImpact(
118118
if (
119119
opts.inPath !== undefined &&
120120
opts.inPath.length > 0 &&
121-
target.kind === "symbol"
121+
target.kind === "symbol" &&
122+
target.matched_in.length > 0
122123
) {
123124
const scoped = filterMatchedInByInPath(target.matched_in, opts.inPath);
124-
target = { ...target, matched_in: scoped };
125125
if (scoped.length === 0) {
126126
return emptyImpactResult({
127-
target,
127+
target: { ...target, matched_in: scoped },
128128
direction,
129129
backends,
130130
depthRaw,
@@ -136,6 +136,7 @@ export function findImpact(
136136
},
137137
});
138138
}
139+
target = { ...target, matched_in: scoped };
139140
}
140141
const directions: Array<"up" | "down"> =
141142
direction === "both" ? ["up", "down"] : [direction];

src/application/mcp-server.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,32 @@ describe("MCP server — impact tool", () => {
18911891
}
18921892
});
18931893

1894+
it("impact in mismatch returns skipped_scope", async () => {
1895+
const db = openDb();
1896+
try {
1897+
db.run(
1898+
`INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export)
1899+
VALUES ('src/a.ts', 'dup', 'function', 1, 1, 'function dup()', 0, 0),
1900+
('src/b.ts', 'dup', 'function', 1, 1, 'function dup()', 0, 0)`,
1901+
);
1902+
} finally {
1903+
closeDb(db);
1904+
}
1905+
1906+
const { client, server } = await makeClient();
1907+
try {
1908+
const r = await client.callTool({
1909+
name: "impact",
1910+
arguments: { target: "dup", in: "src/z.ts" },
1911+
});
1912+
const json = readJson(r);
1913+
expect(json.matches).toEqual([]);
1914+
expect(json.skipped_scope?.reason).toContain("src/z.ts");
1915+
} finally {
1916+
await server.close();
1917+
}
1918+
});
1919+
18941920
it("impact returns isError on non-integer depth (Zod rejects)", async () => {
18951921
const { client, server } = await makeClient();
18961922
try {

src/cli/bootstrap.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Graph composers (MCP trace / explore / node twins):
6565
codemap node <name> [--kind <k>] [--in <path>] [--include-snippets] [--budget-chars N] [--compact]
6666
6767
Impact analysis (graph walk for refactor blast-radius):
68-
codemap impact <target> [--direction up|down|both] [--depth N] [--via <b>] [--limit N] [--summary] [--json]
68+
codemap impact <target> [--in <path>] [--direction up|down|both] [--depth N] [--via <b>] [--limit N] [--summary] [--json]
6969
7070
Affected tests (reverse dep walk → test files to run):
7171
codemap affected [--stdin] [--changed-since <ref>] [--json] [<path>...]

0 commit comments

Comments
 (0)