Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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