Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
e0677b9
feat(apply): slice 1 — apply-engine phase-1 validation + dry-run
SutuSebastian May 6, 2026
564ba32
feat(apply): slice 2 — phase 2 writes via temp + rename
SutuSebastian May 6, 2026
a24f715
feat(apply): slice 3 — CLI verb + recipe execution + TTY/--yes gate
SutuSebastian May 6, 2026
1fd85ce
feat(apply): slice 4 — MCP/HTTP `apply` tool
SutuSebastian May 6, 2026
30080f6
docs(apply): slice 5 — lockstep + plan retire
SutuSebastian May 6, 2026
bdf7ef3
fix(apply): path-containment + overlap detection (triangulated review)
SutuSebastian May 6, 2026
aaabc13
chore(apply): slim comments + sync docs to five conflict reasons
SutuSebastian May 6, 2026
1c43bac
fix(apply): address CodeRabbit review (7 of 9; 2 already fixed)
SutuSebastian May 6, 2026
b9448bd
refactor(mergeParams): simplify parameter merging logic
SutuSebastian May 6, 2026
bbb6b24
feat(tier1): R.17 extractor architecture + position-precision substrate
SutuSebastian May 14, 2026
6b32fb6
feat(tier2): scopes + references substrate per R.11/R.13
SutuSebastian May 15, 2026
ad69d7b
feat(tier2.1): bindings substrate + find-symbol-references recipe
SutuSebastian May 15, 2026
4dfabce
feat(tier2.2): function/type params + re-export chain walking
SutuSebastian May 15, 2026
bb1ceb9
feat(tier2.3): member-kind refs + destructuring + type globals (Tier …
SutuSebastian May 15, 2026
fca1d64
feat(tier2.4): arrow + catch scoping (Tier 2 truly closed at 1.3%)
SutuSebastian May 15, 2026
887d2d6
feat(tier2.5): interface/type-alias + for-of/in body scoping
SutuSebastian May 15, 2026
6d8b500
feat(tier11): per-symbol metrics + file_metrics aggregate
SutuSebastian May 15, 2026
99197a1
feat(tier12): module_cycles table via Tarjan's SCC
SutuSebastian May 15, 2026
e5b0938
feat(tier6): re_export_chains materialised table
SutuSebastian May 15, 2026
00150f3
feat(tier4): function_params first-class table
SutuSebastian May 15, 2026
7230f29
feat(tier11.5): nesting_depth tracker (Tier 11 close)
SutuSebastian May 15, 2026
16e6090
feat(refs): JSX intrinsics + DOM/React globals + TS qualified names
SutuSebastian May 15, 2026
9b5f9a2
fix(indexer): exclude .codemap/audit-cache from default glob
SutuSebastian May 15, 2026
b09220f
feat(tier5): runtime_markers + find-leftover-console + env-var-audit
SutuSebastian May 15, 2026
b5191f1
feat(tier9): test_suites + find-skipped-tests + tests-by-file
SutuSebastian May 15, 2026
759a294
fix(golden): add ORDER BY line_number to markers-notes-todo query
SutuSebastian May 15, 2026
6323818
Merge remote-tracking branch 'origin/main' into feat/codemap-richer-i…
SutuSebastian May 15, 2026
a1a17ba
fix(refs+calls+scopes+symbols): address CodeRabbit review on #79
SutuSebastian May 15, 2026
aa18b13
fix(bindings+stats+markers): address CodeRabbit nitpicks on #79
SutuSebastian May 15, 2026
f1aee1c
docs: sync architecture / glossary / roadmap / golden-queries with su…
SutuSebastian May 15, 2026
3387a43
templates(agents): sync user-facing rule + skill with substrate
SutuSebastian May 15, 2026
2a73c9d
chore(deps): bump deps to latest + dedupe zod across MCP SDK
SutuSebastian May 15, 2026
f960316
chore(deps): drop ip-address override (no longer load-bearing)
SutuSebastian May 15, 2026
d757272
fix(build): declare unrun as devDependency for tsdown 0.22
SutuSebastian May 15, 2026
7c7509a
chore: add changeset for substrate extraction (minor)
SutuSebastian May 15, 2026
d00cabe
Merge branch 'main' into feat/codemap-richer-index
SutuSebastian May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .agents/rules/codemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ A local database (default **`.codemap/index.db`**) indexes structure: symbols, i
| Targeted read (metadata) | — | `bun src/index.ts show <name> [--kind <k>] [--in <path>] [--json]` — file:line + signature |
| Targeted read (source text) | — | `bun src/index.ts snippet <name> [--kind <k>] [--in <path>] [--json]` — same lookup + source from disk + stale flag |
| 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 |
| Apply (substrate fix executor) | — | `bun src/index.ts apply <recipe-id> [--params k=v[,k=v]] [--dry-run] [--yes] [--json]` — executes the diff hunks a recipe describes (one per `{file_path, line_start, before_pattern, after_pattern}` row). Q6 gate: TTY prompt; non-TTY needs `--yes` (or `--dry-run`). Q2 (c) all-or-nothing — any conflict aborts before any file is touched. |
| Coverage ingest | — | `bun src/index.ts ingest-coverage <path> [--runtime] [--json]` — Istanbul (`coverage-final.json`) and LCOV (`lcov.info`) auto-detect from path; V8 is **opt-in** via `--runtime` (treats `<path>` as a `NODE_V8_COVERAGE=...`-style directory of `coverage-*.json` dumps). Joinable to `symbols` for "untested AND dead" queries. Local-only — no SaaS aggregation. |
| SARIF / GH annotations | — | `bun src/index.ts query --recipe deprecated-symbols --format sarif` · `… --format annotations` |
| `--ci` aggregate flag | — | `bun src/index.ts query -r deprecated-symbols --ci` (or `audit --base origin/main --ci`) — aliases `--format sarif` + non-zero exit when findings/additions surfaced + suppresses the no-locatable-rows stderr warning. Mutually exclusive with `--json` / `--format <other>`. |
Expand Down Expand Up @@ -75,9 +76,11 @@ Validation: SQL is rejected at load time if it starts with DML/DDL (DELETE/DROP/

**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.

**Apply (`bun src/index.ts apply <recipe-id>`)**: substrate-shaped fix executor over the existing `--format diff-json` row contract — recipe SQL is the synthesis surface, codemap executes. Phase 1 validates every `{file_path, line_start, before_pattern, after_pattern}` row against current disk via `actual.includes(before_pattern)` (substring match — same contract `buildDiffJson` uses); collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). The `path escapes project root` guard rejects absolute `file_path` inputs and any candidate whose resolved form lands outside `projectRoot`; the `duplicate edit on same line` guard rejects two-or-more rows targeting the same `(file_path, line_start)` so phase 2 doesn't split mid-loop and leak Q2 (c). Phase 2 (gated on `!dryRun && conflicts.length === 0`) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing** — any conflict aborts the whole run before any file is touched. **Q6 gate** — TTY prompts `Proceed? [y/N]` (default-N) on stderr; non-TTY (CI / agents / MCP / HTTP) requires `--yes` (or `yes: true`) explicitly; `--dry-run` + `--yes` mutually exclusive. Q7 idempotency: re-running on already-applied code reports `line content drifted` with `actual_at_line` showing the post-rename content — re-run `bun src/index.ts` to refresh, then re-run apply (vacuous clean pass). Output envelope (identical across modes): `{mode: 'dry-run'|'apply', applied: bool, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Pure transport-agnostic engine in `application/apply-engine.ts`; CLI / MCP / HTTP all dispatch the same `applyDiffPayload` function.

**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 six resources (`codemap://recipes` + `codemap://recipes/{id}` are live read every call so inline `last_run_at` / `run_count` recency stays fresh; `codemap://schema` + `codemap://skill` lazy-cache; `codemap://files/{path}` + `codemap://symbols/{name}` always live):

- **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).
- **Tools:** `query` / `query_batch` / `query_recipe` / `audit` / `save_baseline` / `list_baselines` / `drop_baseline` / `context` / `validate` / `show` / `snippet` / `impact` / `apply`. Snake_case keys (Codemap convention matching MCP spec examples + reference servers — spec is convention-agnostic; CLI stays kebab).
- **`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.
- **`save_baseline` (polymorphic):** one tool, `{name, sql? | recipe?}` with runtime exclusivity check (mirrors the CLI's single `--save-baseline=<name>` verb).
- **Resources:** `codemap://recipes` (catalog — live), `codemap://recipes/{id}` (one recipe — live), `codemap://schema` (live DDL from `sqlite_schema`; lazy-cached), `codemap://skill` (bundled SKILL.md text; lazy-cached), `codemap://files/{path}` (per-file roll-up: symbols, imports, exports, coverage — live), `codemap://symbols/{name}` (symbol lookup with `{matches, disambiguation?}` envelope; `?in=<path-prefix>` filter mirrors `show --in` — live). Recipe catalogs read live every call so inline `last_run_at` / `run_count` recency reflects mutations during the server lifetime; `schema` / `skill` cache because their inputs don't change mid-session.
Expand Down
1 change: 1 addition & 0 deletions .agents/skills/codemap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are
- **`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`.
- **`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.
- **`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).
- **`apply`** — `{recipe, params?, dry_run?, yes?}`. Substrate-shaped fix executor — runs the same recipe `query_recipe` runs, then applies the diff hunks each row describes (`{file_path, line_start, before_pattern, after_pattern}`) to disk. Recipe SQL is the synthesis surface; codemap is the executor. Phase 1 validates every row against current disk via substring-match (`actual.includes(before_pattern)`) — the same contract `--format diff-json` uses; collects five conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `duplicate edit on same line`). Phase 2 (gated on no conflicts) writes via sibling temp + `rename` for POSIX-atomic per-file writes. **Q2 (c) all-or-nothing** — any conflict in any file aborts phase 2 entirely; partial writes never ship. Over MCP/HTTP `yes: true` is required for the write path (no TTY prompt to fall back on); `dry_run: true` previews without writing; the two are mutually exclusive. Re-running on already-applied code reports a `line content drifted` conflict whose `actual_at_line` shows the post-rename content — re-run `bun src/index.ts` to refresh the index, then re-run `apply` for a clean vacuous pass. Result envelope (identical across modes): `{mode: 'dry-run'|'apply', applied, files: [{file_path, rows_applied, warnings?}], conflicts: [{file_path, line_start, before_pattern, actual_at_line, reason}], summary: {files, files_modified, rows, rows_applied, conflicts, files_with_conflicts}}`. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.

**Resources (mostly lazy-cached on first `read_resource`; recipes / one-recipe live-read every call so the inline recency fields stay fresh):**

Expand Down
31 changes: 31 additions & 0 deletions .changeset/codemap-apply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
"@stainless-code/codemap": minor
---

`codemap apply <recipe-id>` — substrate-shaped fix executor over the existing `--format diff-json` row contract. The recipe SQL describes the transformation (`{file_path, line_start, before_pattern, after_pattern}` rows); codemap is the executor. Floor "No fix engine" preserved — codemap doesn't synthesise edits, it only executes the hunks the recipe row described.

**Three transports, one engine:**

- **CLI:** `codemap apply <recipe-id> [--params k=v[,k=v]] [--dry-run] [--yes] [--json]`
- **MCP tool:** `apply` (registered alongside `impact` / `show` / `snippet`)
- **HTTP:** `POST /tool/apply`

All three dispatch the same pure `applyDiffPayload` engine in `application/apply-engine.ts`.

**Decisions worth knowing (Q1–Q10 locked in `docs/plans/codemap-apply.md`, lifted into `docs/architecture.md § Apply wiring` on this PR):**

- **Apply-by-default, `--dry-run` opts into preview.** Verb-name semantics + `git apply` / `terraform apply` precedent.
- **Per-recipe-run all-or-nothing (Q2 (c)).** Phase 1 validates every row first; any conflict aborts phase 2 entirely before any file is touched. Cross-file invariants matter — `rename-preview` produces a definition row + N import rows, and partial application leaves the project syntactically broken.
- **Scan-and-collect conflicts (Q3 (b)).** Phase 1 walks every row and collects all conflicts in one pass — better remediation UX than fail-fast.
- **TTY prompt + `--yes` gate (Q6 (a)).** Interactive contexts (TTY) get a `Proceed? [y/N]` prompt with default-N; non-interactive contexts (CI / agents / MCP / HTTP) require `--yes` (or `yes: true`) explicitly. `--dry-run` + `--yes` mutually exclusive.
- **Substring match per row, single-line (Q8 (a)).** Mirrors `buildDiffJson`'s contract verbatim — `actual.includes(before_pattern)` + `actual.replace(before, after)` with `$`-pre-escape per `String.prototype.replace`'s GetSubstitution rule. Exemplar: `templates/recipes/rename-preview.sql` emits `before_pattern = old_name` (the bare identifier). When `before_pattern` appears more than once on the line (e.g. `const foo = foo();`), only the leftmost is replaced — same shape `--format diff` previews; recipe authors normalise their SQL if they need a different occurrence.
- **Path-containment guard.** Every `file_path` is rejected with a `path escapes project root` conflict if it's absolute or if `path.resolve(projectRoot, file_path)` lands outside the project root. Defends the CLI + MCP + HTTP write paths against malicious or malformed recipe rows.
- **Overlap detection.** Two rows targeting the same `(file_path, line_start)` are rejected with a `duplicate edit on same line` conflict during phase 1. Without it, the second row's substring assertion would fail mid-phase-2 (after earlier files in alphabetical order had already been renamed) — that would leave the project in a partial-write state and violate Q2 (c).
- **Atomic per-file writes via temp + rename.** Sibling `<file>.codemap-apply-<rand>.tmp` then `renameSync` — POSIX-atomic so concurrent readers see either pre-rename or post-rename content, never a torn write.
- **Q7 idempotency (conflict-only path).** Re-running on already-applied code reports `line content drifted` with `actual_at_line` showing the post-rename content; user re-runs `codemap` to refresh the index → next run produces 0 rows → vacuous clean apply.
- **Single envelope shape across modes (Q5).** `{mode, applied, files, conflicts, summary}` — same shape for `dry-run` and `apply`; consumers pattern-match on `mode` + `applied`.
- **No SARIF / annotations.** Apply is a write action, not a findings list.

**Boundary discipline (Q10):** only `cli/cmd-apply.ts` + `application/tool-handlers.ts` may import the apply engine — re-runnable kit at `docs/architecture.md § Boundary verification — apply write path`.

Plan: PR #77 (merged). Implementation: this PR.
Loading
Loading