From 741633832afab5c9d32d89988cb29ca7cf892351 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:27:18 +0300 Subject: [PATCH 01/13] feat(cli): add codemap rename alias for homonym-safe renames MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `define_in` on rename-preview shipped in #165; this adds the thin `codemap rename` → `apply rename-preview` alias with positional ergonomics and plan doc for the scoped-rename slice. --- README.md | 1 + docs/architecture.md | 2 +- docs/plans/scoped-rename-define-in.md | 54 +++++++++++ docs/roadmap.md | 2 +- docs/testing-coverage.md | 2 +- src/cli/bootstrap.ts | 3 +- src/cli/cmd-apply.test.ts | 51 +++++++++++ src/cli/main.ts | 11 +++ src/cli/rename-alias.test.ts | 95 ++++++++++++++++++++ src/cli/rename-alias.ts | 124 ++++++++++++++++++++++++++ 10 files changed, 341 insertions(+), 4 deletions(-) create mode 100644 docs/plans/scoped-rename-define-in.md create mode 100644 src/cli/rename-alias.test.ts create mode 100644 src/cli/rename-alias.ts diff --git a/README.md b/README.md index 0f7b570b..e14204dd 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,7 @@ codemap affected --changed-since origin/main --json # committed de codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --dry-run codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --yes # TTY prompts without --yes # Homonym-safe: add define_in=src/path/to/definition.ts (scopes target; in_file only filters output rows) +# Alias: codemap rename usePermissions useAccess --define-in src/hooks/usePermissions.ts --dry-run codemap apply migrate-import-source --params old_source=legacy,new_source=@app/core --dry-run codemap apply stale-imports --params in_file=src/widget --dry-run # preview; writes need --force --yes codemap apply migrate-jsx-prop --params old_name=data-id,new_name=data-testid,component_name=ProductCard --dry-run --force diff --git a/docs/architecture.md b/docs/architecture.md index ce073741..5ff5bd79 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -166,7 +166,7 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload **Discover → preview → apply** (agent loop): `query_recipe` / `query --recipe --format diff-json` (or audit baseline `added` rows) → `apply` with `dry_run: true` → `apply` with `yes: true` (+ `force: true` when required). Per-row `actions[].command` on `--json` query output renders a copy-paste shell line (`renderRecipeActionCommands`). -**Non-goals on the apply path** (Moat A preserved): no curated write verbs (`codemap rename`, …); no severity / verdict engine on rows; no JS execution at apply time; no Path A AST apply engine; no cross-file transactional rollback. Rejected alternatives + revisit triggers: [synthesis §7](./research/codemap-richer-index-synthesis-2026-05.md#7-rejected-items-with-trigger-conditions) (`organize-imports`, Path A AST apply, trust tiers, …). +**Non-goals on the apply path** (Moat A preserved): no curated write verbs with new semantics (`codemap fix deprecated`, …); **`codemap rename`** is a thin alias to `apply rename-preview` (same recipe + policy gates as outcome aliases → `query --recipe`). No severity / verdict engine on rows; no JS execution at apply time; no Path A AST apply engine; no cross-file transactional rollback. Rejected alternatives + revisit triggers: [synthesis §7](./research/codemap-richer-index-synthesis-2026-05.md#7-rejected-items-with-trigger-conditions) (`organize-imports`, Path A AST apply, trust tiers, …). **Show / snippet wiring:** **`src/cli/show-snippet-args.ts`** (shared argv parser) + **`src/cli/show-snippet-render.ts`** (shared terminal/JSON error helpers) + **`src/cli/cmd-show.ts`** + **`src/cli/cmd-snippet.ts`** — sibling CLI verbs sharing the same parser shape (`` or **`--query ''`** + **`--with-fts`** + `--kind` + `--in ` + `--json`; show adds **`--print-sql`**) and the pure engines **`src/application/show-engine.ts`** (exact lookup + envelope builders), **`src/application/search-query-parser.ts`** + **`src/application/search-engine.ts`** (field-qualified search → parameterized SQL on `symbols`, optional `source_fts` join), and **`src/application/show-search-mode.ts`** (shared parse/normalize + FTS resolution + **`executeShowLookup`** + **`formatShowSearchSqlForQuery`** for CLI/MCP/HTTP). Exact lookup: `findSymbolsByName({db, name, kind?, inPath?})`. Query lookup: `searchSymbols({db, parsed, withFts?})`. Snippet FS read: `readSymbolSource({match, projectRoot, indexedContentHash?})` + `getIndexedContentHash(db, filePath)`. **`buildShowResult`** + **`buildSnippetResult`** envelope builders — same engines the MCP show/snippet tools call. Both verbs return the same `{matches, disambiguation?, warning?}` envelope — single match → `{matches: [{...}]}`; multi-match adds `{n, by_kind, files, hint}`; optional **`warning`** when FTS was requested but `source_fts` is empty. Snippet matches add `source` / `stale` / `missing` fields (additive — no shape divergence). **`--in `** and **`path:`** inside **`--query`** normalize through `toProjectRelative(projectRoot, p)` (from **`src/application/validate-engine.ts`**). Stale-file behavior on `snippet`: `hashContent` (from **`src/hash.ts`**) compares on-disk content against `files.content_hash`; mismatch sets `stale: true` but source IS still returned. MCP tools `show` and `snippet` register parallel to the CLI surface (see [§ MCP wiring](#cli-usage)). diff --git a/docs/plans/scoped-rename-define-in.md b/docs/plans/scoped-rename-define-in.md new file mode 100644 index 00000000..baa4494a --- /dev/null +++ b/docs/plans/scoped-rename-define-in.md @@ -0,0 +1,54 @@ +# Scoped rename (`define_in`) — plan + +> **Status:** ready to delete · **Priority:** P1 · **Effort:** S (alias slice) + M (shipped in [#165](https://github.com/stainless-code/codemap/pull/165)) +> +> **Motivator:** Homonymous symbol names make bare `rename-preview` union every `symbols.name` match. Agents need the same definition anchor `find-symbol-references` uses (`file_path` of the definition). +> +> **Roadmap:** [§ Backlog — Agent & indexing ops](../roadmap.md#agent--indexing-ops) + +--- + +## Pre-locked decisions + +| # | Decision | Source | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| R.1 | **`define_in` scopes `target_symbols`** to `symbols.file_path = define_in`; binding-resolved `call_rows` / JSX CTEs filter when set. | Moat B — substrate | +| R.2 | **`in_file` stays output-only** — prefix filter on result row paths; does not pick which homonym is the rename target. | [rename-preview.md](../../templates/recipes/rename-preview.md) | +| R.3 | **Moat A preserved** — no new verdict engine; optional `codemap rename` is a **thin alias** → `apply rename-preview` (same as outcome aliases → query). | [architecture § Apply](../architecture.md#apply--input-modes-transport-and-policy) | +| R.4 | **Golden + apply e2e** — `rename-preview-homonym-scoped` / `rename-preview-homonym-unscoped` in `fixtures/golden/minimal/`. | [testing-coverage.md](../testing-coverage.md) | + +--- + +## Shipped ([#165](https://github.com/stainless-code/codemap/pull/165)) + +- [x] `define_in` recipe param on `rename-preview` (`templates/recipes/rename-preview.{sql,md}`) +- [x] Binding-scoped `call_rows` + `jsx_element_rows` / `jsx_closing_rows` when `define_in` set +- [x] Golden `rename-preview-homonym-scoped` + homonym fixtures under `fixtures/minimal/src/bench/` +- [x] `cmd-apply.test.ts` homonym apply e2e +- [x] Agent surfaces: README, skill, MCP instructions, `find-symbol-references` action template + +--- + +## Remaining slice (this PR) + +- [x] **`codemap rename` CLI alias** — rewrite to `codemap apply rename-preview` with full apply flag pass-through +- [x] Positional ergonomics: `codemap rename [--define-in path] [--in-file prefix] [--kind k]` +- [x] `rename-alias.test.ts` + integration via `cmd-apply.test.ts` +- [x] `bootstrap.ts` help line; roadmap checkbox for alias + +**Non-goals (unchanged):** global rename verb with new semantics; MCP `rename` tool; AST apply engine. + +--- + +## Acceptance + +- [x] Scoped golden matches binding-resolved call sites only for anchored homonym +- [x] Unscoped golden unions both homonyms +- [x] `codemap rename helper worker --define-in src/bench/homonym-helper-a.ts --yes` ≡ apply recipe path +- [x] `codemap rename --help` documents alias + `define_in` vs `in_file` + +--- + +## Delete trigger + +Delete this plan when the alias slice merges and roadmap item is fully checked — lift one-liner to [architecture § Homonym-safe rename](../architecture.md#apply--input-modes-transport-and-policy). diff --git a/docs/roadmap.md b/docs/roadmap.md index 291744b3..be33f769 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -61,7 +61,7 @@ Wave 1–2 shipped in [#126](https://github.com/stainless-code/codemap/pull/126) **Open (P1)** -- [x] **Scoped rename (`define_in`)** — homonym-safe `rename-preview` (binding-scoped calls/JSX). Shipped [#165](https://github.com/stainless-code/codemap/pull/165). Optional alias still open: `codemap rename`. +- [x] **Scoped rename (`define_in`)** — homonym-safe `rename-preview` (binding-scoped calls/JSX) + thin `codemap rename` alias → `apply rename-preview`. Shipped [#165](https://github.com/stainless-code/codemap/pull/165); alias in scoped-rename plan PR. **P2 — strategic (trigger-gated where noted)** diff --git a/docs/testing-coverage.md b/docs/testing-coverage.md index 94588b0f..b2db53ae 100644 --- a/docs/testing-coverage.md +++ b/docs/testing-coverage.md @@ -46,7 +46,7 @@ Every `templates/recipes/.sql` has **≥1** scenario in `fixtures/golden/sce **Read→apply (C.6):** `deprecated-symbols`, `find-symbol-references`, `find-symbol-definitions`, `find-jsx-usages`, `find-import-sites`, `markers-by-kind` frontmatter `actions[].command` → apply twins; `cmd-query.test.ts` rendered-command cases. -**Input modes:** recipe id (above); `--rows` JSON file; `--diff-input` / `--until-empty` / `--commit` — all e2e in `cmd-apply.test.ts`. **`define_in`** homonym scope: golden `rename-preview-homonym-scoped` + `cmd-apply.test.ts`. MCP/HTTP: `apply` / `apply_rows` / `apply_diff_input` e2e in `mcp-server.test.ts` and `http-server.test.ts`; transport writes/consent/fixpoint in `tool-handlers.test.ts`. +**Input modes:** recipe id (above); `--rows` JSON file; `--diff-input` / `--until-empty` / `--commit` — all e2e in `cmd-apply.test.ts`. **`define_in`** homonym scope: golden `rename-preview-homonym-scoped` + `cmd-apply.test.ts`; **`codemap rename`** alias: `rename-alias.test.ts` + homonym e2e in `cmd-apply.test.ts`. MCP/HTTP: `apply` / `apply_rows` / `apply_diff_input` e2e in `mcp-server.test.ts` and `http-server.test.ts`; transport writes/consent/fixpoint in `tool-handlers.test.ts`. --- diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index 995a448b..e34463e6 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -73,7 +73,8 @@ Affected tests (reverse dep walk → test files to run): Apply (substrate-shaped fix executor; diff-json row contract): codemap apply [--params k=v[,k=v]] [--dry-run] [--yes] [--json] codemap apply --rows -| codemap apply --diff-input - (see codemap apply --help for --force, --until-empty, --commit) + codemap rename [--define-in ] [apply flags...] # alias → apply rename-preview + (see codemap apply --help for --force, --until-empty, --commit; codemap rename --help) Coverage ingest (Istanbul JSON or LCOV from any test runner): codemap ingest-coverage [--json] # path = file or dir; format auto-detected diff --git a/src/cli/cmd-apply.test.ts b/src/cli/cmd-apply.test.ts index a46e29a5..01bb1d71 100644 --- a/src/cli/cmd-apply.test.ts +++ b/src/cli/cmd-apply.test.ts @@ -558,6 +558,57 @@ export function staleOne(): number { return 2; } }); }); + describe("codemap rename alias", () => { + beforeEach(async () => { + mkdirSync(join(projectRoot, "src", "bench"), { recursive: true }); + writeFileSync( + join(projectRoot, "src", "bench", "homonym-helper-a.ts"), + 'export function helper(): string {\n return "a";\n}\n', + "utf8", + ); + writeFileSync( + join(projectRoot, "src", "bench", "homonym-helper-b.ts"), + 'export function helper(): string {\n return "b";\n}\n', + "utf8", + ); + writeFileSync( + join(projectRoot, "src", "bench", "homonym-consumer-a.ts"), + 'import { helper } from "./homonym-helper-a";\n\nexport function useHelperA(): string {\n return helper();\n}\n', + "utf8", + ); + writeFileSync( + join(projectRoot, "src", "bench", "homonym-consumer-b.ts"), + 'import { helper } from "./homonym-helper-b";\n\nexport function useHelperB(): string {\n return helper();\n}\n', + "utf8", + ); + const idx = await runCli(["--full"], { CODEMAP_ROOT: projectRoot }); + expect(idx.exitCode).toBe(0); + }); + + it("rewrites to apply rename-preview with homonym scope", async () => { + const r = await runCli( + [ + "rename", + "helper", + "worker", + "--define-in", + "src/bench/homonym-helper-a.ts", + "--yes", + "--json", + ], + { CODEMAP_ROOT: projectRoot }, + ); + expect(r.exitCode).toBe(0); + const env = JSON.parse(r.out); + expect(env.applied).toBe(true); + expect(readFile("src/bench/homonym-helper-a.ts")).toContain("worker"); + expect(readFile("src/bench/homonym-consumer-a.ts")).toContain("worker"); + expect(readFile("src/bench/homonym-helper-b.ts")).toMatch( + /function helper\(\)/, + ); + }); + }); + describe("rename-preview member JSX", () => { beforeEach(async () => { mkdirSync(join(projectRoot, "src", "bench"), { recursive: true }); diff --git a/src/cli/main.ts b/src/cli/main.ts index 69803e24..a008b375 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -58,6 +58,17 @@ export async function main(): Promise { if (rewritten) rest.splice(0, rest.length, ...rewritten); } + if (rest[0] === "rename") { + const { printRenameAliasHelp, resolveRenameAlias } = + await import("./rename-alias.js"); + if (rest.includes("--help") || rest.includes("-h")) { + printRenameAliasHelp(); + return; + } + const rewritten = resolveRenameAlias(rest); + if (rewritten) rest.splice(0, rest.length, ...rewritten); + } + if (rest[0] === "agents" && rest[1] === "init") { if (rest.includes("--help") || rest.includes("-h")) { console.log(`Usage: codemap agents init [--force] [--interactive|-i] [--mcp] [--targets ] [--link-mode symlink|copy] [--git-hooks] [--no-git-hooks] diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts new file mode 100644 index 00000000..b8ac7948 --- /dev/null +++ b/src/cli/rename-alias.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "bun:test"; + +import { formatParamsCli, resolveRenameAlias } from "./rename-alias.js"; + +describe("resolveRenameAlias", () => { + it("rewrites positional old/new with scoped flags", () => { + expect( + resolveRenameAlias([ + "rename", + "helper", + "worker", + "--define-in", + "src/a.ts", + "--yes", + ]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "define_in=src/a.ts,new=worker,old=helper", + "--yes", + ]); + }); + + it("rewrites --params form", () => { + expect( + resolveRenameAlias([ + "rename", + "--params", + "old=foo,new=bar,define_in=src/x.ts", + "--dry-run", + ]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "define_in=src/x.ts,new=bar,old=foo", + "--dry-run", + ]); + }); + + it("merges --params with positional when both present", () => { + expect( + resolveRenameAlias([ + "rename", + "a", + "b", + "--params", + "kind=function", + "--dry-run", + ]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "kind=function,new=b,old=a", + "--dry-run", + ]); + }); + + it("maps --in-file and --kind to recipe params", () => { + expect( + resolveRenameAlias([ + "rename", + "Foo", + "Bar", + "--in-file", + "src/lib/", + "--kind", + "function", + ]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "in_file=src/lib/,kind=function,new=Bar,old=Foo", + ]); + }); + + it("returns null for non-rename commands", () => { + expect(resolveRenameAlias(["apply", "rename-preview"])).toBeNull(); + }); + + it("returns null when help is requested", () => { + expect(resolveRenameAlias(["rename", "--help"])).toBeNull(); + }); +}); + +describe("formatParamsCli", () => { + it("serializes key=value pairs", () => { + expect(formatParamsCli({ old: "a", new: "b", define_in: "src/x.ts" })).toBe( + "define_in=src/x.ts,new=b,old=a", + ); + }); +}); diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts new file mode 100644 index 00000000..496da2ff --- /dev/null +++ b/src/cli/rename-alias.ts @@ -0,0 +1,124 @@ +import { mergeParams, parseParamsCli } from "../application/recipe-params.js"; +import type { RecipeParamValues } from "../application/recipe-params.js"; + +const RENAME_RECIPE_ID = "rename-preview"; + +/** Serialize a param map for `codemap apply --params` (stable key order). */ +export function formatParamsCli(params: RecipeParamValues): string { + return Object.keys(params) + .sort() + .map((key) => `${key}=${String(params[key])}`) + .join(","); +} + +/** + * Thin alias: `codemap rename` → `codemap apply rename-preview`. + * Moat A — no new write verb semantics; same recipe + policy gates as `apply`. + */ +export function resolveRenameAlias(rest: string[]): string[] | null { + if (rest[0] !== "rename") return null; + + const tail = rest.slice(1); + if (tail.includes("--help") || tail.includes("-h")) { + return null; + } + + let params: RecipeParamValues | undefined; + const passthrough: string[] = []; + let i = 0; + + while (i < tail.length) { + const a = tail[i]!; + if (a === "--params") { + const next = tail[i + 1]; + if (next === undefined) { + return ["apply", RENAME_RECIPE_ID, "--params", ""]; + } + params = mergeParams(params, parseParamsCli(next)); + i += 2; + continue; + } + if (a === "--define-in") { + const next = tail[i + 1]; + if (next === undefined) { + return ["apply", RENAME_RECIPE_ID, "--define-in"]; + } + params = mergeParams(params, { define_in: next }); + i += 2; + continue; + } + if (a === "--in-file") { + const next = tail[i + 1]; + if (next === undefined) { + return ["apply", RENAME_RECIPE_ID, "--in-file"]; + } + params = mergeParams(params, { in_file: next }); + i += 2; + continue; + } + if (a === "--kind") { + const next = tail[i + 1]; + if (next === undefined) { + return ["apply", RENAME_RECIPE_ID, "--kind"]; + } + params = mergeParams(params, { kind: next }); + i += 2; + continue; + } + passthrough.push(a); + i++; + } + + const head = passthrough[0]; + const second = passthrough[1]; + if ( + head !== undefined && + second !== undefined && + !head.startsWith("-") && + !second.startsWith("-") + ) { + params = mergeParams(params, { old: head, new: second }); + const applyTail = passthrough.slice(2); + if (params && Object.keys(params).length > 0) { + return [ + "apply", + RENAME_RECIPE_ID, + "--params", + formatParamsCli(params), + ...applyTail, + ]; + } + return ["apply", RENAME_RECIPE_ID, ...applyTail]; + } + + if (params && Object.keys(params).length > 0) { + return [ + "apply", + RENAME_RECIPE_ID, + "--params", + formatParamsCli(params), + ...passthrough, + ]; + } + + return ["apply", RENAME_RECIPE_ID, ...passthrough]; +} + +export function printRenameAliasHelp(): void { + console.log(`Usage: + codemap rename [--define-in ] [--in-file ] [--kind ] [apply flags...] + codemap rename --params old=,new=[,define_in=] [apply flags...] + +Alias for \`codemap apply rename-preview\` — homonym-safe renames pass \`--define-in\` +(definition \`symbols.file_path\` anchor). \`--in-file\` only narrows output row paths. + +Apply flags pass through: --dry-run, --yes, --force, --json, --until-empty, +--max-passes N, --commit "". + +Examples: + codemap rename usePermissions useAccess --kind function --dry-run + codemap rename helper worker --define-in src/bench/homonym-helper-a.ts --yes + codemap rename --params old=foo,new=bar,define_in=src/a.ts --dry-run + +Run \`codemap apply --help\` for executor details.`); +} From 609b140811da607a670f9a5240cb604f0032f1c2 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:44:13 +0300 Subject: [PATCH 02/13] fix(cli): preserve missing --params operand in rename alias Align with --define-in/--in-file/--kind: pass bare --params through so apply reports the real missing-argument error instead of accepting {}. --- src/cli/rename-alias.test.ts | 8 ++++++++ src/cli/rename-alias.ts | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index b8ac7948..6759e974 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -84,6 +84,14 @@ describe("resolveRenameAlias", () => { it("returns null when help is requested", () => { expect(resolveRenameAlias(["rename", "--help"])).toBeNull(); }); + + it("preserves missing --params operand for downstream apply parser", () => { + expect(resolveRenameAlias(["rename", "--params"])).toEqual([ + "apply", + "rename-preview", + "--params", + ]); + }); }); describe("formatParamsCli", () => { diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index 496da2ff..dc453013 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -32,7 +32,7 @@ export function resolveRenameAlias(rest: string[]): string[] | null { if (a === "--params") { const next = tail[i + 1]; if (next === undefined) { - return ["apply", RENAME_RECIPE_ID, "--params", ""]; + return ["apply", RENAME_RECIPE_ID, "--params"]; } params = mergeParams(params, parseParamsCli(next)); i += 2; From 172278a34b31a3c13d1fb59add4598819323791a Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:48:16 +0300 Subject: [PATCH 03/13] fix(cli): harden rename alias parsing and docs Return rename-local errors for missing scoped-flag operands, extract old/new regardless of apply-flag order, reject stray positionals, and align README/roadmap/architecture/skill with the shipped alias. --- README.md | 2 +- docs/architecture.md | 2 +- docs/roadmap.md | 6 +- src/cli/bootstrap.ts | 2 +- src/cli/cmd-apply.test.ts | 3 + src/cli/main.ts | 11 +- src/cli/rename-alias.test.ts | 69 +++++++-- src/cli/rename-alias.ts | 132 +++++++++++++----- .../agent-content/skill/10-recipes-context.md | 2 +- 9 files changed, 171 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index e14204dd..48f464dd 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ codemap affected --changed-since origin/main --json # committed de codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --dry-run codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --yes # TTY prompts without --yes # Homonym-safe: add define_in=src/path/to/definition.ts (scopes target; in_file only filters output rows) -# Alias: codemap rename usePermissions useAccess --define-in src/hooks/usePermissions.ts --dry-run +# Alias: codemap rename helper worker --define-in src/bench/homonym-helper-a.ts --dry-run codemap apply migrate-import-source --params old_source=legacy,new_source=@app/core --dry-run codemap apply stale-imports --params in_file=src/widget --dry-run # preview; writes need --force --yes codemap apply migrate-jsx-prop --params old_name=data-id,new_name=data-testid,component_name=ProductCard --dry-run --force diff --git a/docs/architecture.md b/docs/architecture.md index 5ff5bd79..3d0fb508 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -160,7 +160,7 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload **Bundled diff-shape recipes** (emit the row contract; inspect with `codemap query --recipe --format diff-json`): `rename-preview` (includes member/namespaced JSX via `jsx_elements`), `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (`auto_fixable: false` — writes need `--force` unless allowlisted). Pair read `deprecated-symbols` with `migrate-deprecated` + `deprecated-usages`; `find-jsx-usages` with `migrate-jsx-prop`. Golden map: [`testing-coverage.md`](./testing-coverage.md). -**Homonym-safe rename:** optional `define_in=` on `rename-preview` anchors `target_symbols` and binding-resolved call/JSX sites (distinct from `in_file`, which only filters output row paths). Bare `old`/`new` still unions every same-named symbol. +**Homonym-safe rename:** optional `define_in=` on `rename-preview` anchors `target_symbols` and binding-resolved call/JSX sites (distinct from `in_file`, which only filters output row paths). Bare `old`/`new` still unions every same-named symbol. CLI shorthand: `codemap rename [--define-in ]` → `apply rename-preview` (thin alias — same recipe + policy gates). **Policy** (`src/application/apply-policy.ts`, recipe mode only): non-`auto_fixable` recipes reject writes unless `--force` / MCP `force: true`. `apply.autoApplyRecipes` in user config is an allowlist of recipe ids that may run without TTY `--yes` on non-interactive CLI (MCP/HTTP still require `yes: true` for writes). `--rows` / `apply_rows` / `--diff-input` bypass both gates — separate trust boundary for agent-supplied hunks. diff --git a/docs/roadmap.md b/docs/roadmap.md index be33f769..782df5a6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -59,10 +59,6 @@ Prioritized agent & indexing ops queue (2026-05). Reference: [agents.md](./agent Wave 1–2 shipped in [#126](https://github.com/stainless-code/codemap/pull/126)–[#138](https://github.com/stainless-code/codemap/pull/138) (MCP instructions, allowlist, WSL watch, git hooks, trace/explore/node, `agents init --mcp`, affected tests, index lock/`unlock`, parse-worker hardening, field-qualified search). Agent eval (PR 9) shipped in [#139](https://github.com/stainless-code/codemap/pull/139) (probe) + [#144](https://github.com/stainless-code/codemap/pull/144) (live MCP arms + log comparison). -**Open (P1)** - -- [x] **Scoped rename (`define_in`)** — homonym-safe `rename-preview` (binding-scoped calls/JSX) + thin `codemap rename` alias → `apply rename-preview`. Shipped [#165](https://github.com/stainless-code/codemap/pull/165); alias in scoped-rename plan PR. - **P2 — strategic (trigger-gated where noted)** - [ ] **Framework route extraction** — Express / React Router / NestJS `http_routes` substrate. Plan: [`plans/framework-route-extraction.md`](./plans/framework-route-extraction.md). Blocked on C.9 contract. Effort: L. @@ -105,7 +101,7 @@ Predicate-as-API only — enrich row shape and audit deltas; no standalone pass/ - [ ] **C.9 framework plugin layer** — static entry-point hints on `files` to sharpen reachability-predicate recipes (`untested-and-dead`, `unimported-exports`, future `dead-files-by-reachability`). Plan: [`plans/c9-plugin-layer.md`](./plans/c9-plugin-layer.md). Effort: XL; ships last in the impact-vs-cadence sequence (see plan § Shipping cadence). - [ ] **LSP diagnostic-push + VSCode extension** — recipes-as-`Diagnostic[]` server + paired extension; explicitly **not** a go-to-def / references shim (`tsserver` covers those). Plan: [`plans/lsp-diagnostic-push.md`](./plans/lsp-diagnostic-push.md). Effort: XL; soft ordering after C.9 for cleaner squigglies on framework files. -- [x] **Apply-engine direction** — diff-shape recipes (8 bundled ids), `actions[].command` on apply + read→apply pairs, `auto_fixable`/`--force`, `rename-preview` (calls, re-exports, barrel, JSX), `apply --rows` / `apply_rows`, `--diff-input`, `--commit`, `--until-empty`, `apply.autoApplyRecipes`. Shipped [#165](https://github.com/stainless-code/codemap/pull/165). Executor + transport: [`architecture.md` § Apply](./architecture.md#apply--input-modes-transport-and-policy), [`glossary.md` § codemap apply](./glossary.md#codemap-apply--apply-tool). +- [x] **Apply-engine direction** — diff-shape recipes (8 bundled ids), `actions[].command` on apply + read→apply pairs, `auto_fixable`/`--force`, `rename-preview` (calls, re-exports, barrel, JSX; homonym `define_in` [#165](https://github.com/stainless-code/codemap/pull/165); CLI `codemap rename` alias [#166](https://github.com/stainless-code/codemap/pull/166)), `apply --rows` / `apply_rows`, `--diff-input`, `--commit`, `--until-empty`, `apply.autoApplyRecipes`. Shipped [#165](https://github.com/stainless-code/codemap/pull/165) + [#166](https://github.com/stainless-code/codemap/pull/166). Executor + transport: [`architecture.md` § Apply](./architecture.md#apply--input-modes-transport-and-policy), [`glossary.md` § codemap apply](./glossary.md#codemap-apply--apply-tool). - [ ] **`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 ` 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. - [ ] **`codemap audit` verdict + thresholds** (v1.x) — `verdict: "pass" | "warn" | "fail"` driven by an `audit.deltas[].{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. - [ ] **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. diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index e34463e6..495a1e0e 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -73,7 +73,7 @@ Affected tests (reverse dep walk → test files to run): Apply (substrate-shaped fix executor; diff-json row contract): codemap apply [--params k=v[,k=v]] [--dry-run] [--yes] [--json] codemap apply --rows -| codemap apply --diff-input - codemap rename [--define-in ] [apply flags...] # alias → apply rename-preview + codemap rename [--define-in ] [--in-file ] [--kind ] [apply flags...] # alias → apply rename-preview (see codemap apply --help for --force, --until-empty, --commit; codemap rename --help) Coverage ingest (Istanbul JSON or LCOV from any test runner): diff --git a/src/cli/cmd-apply.test.ts b/src/cli/cmd-apply.test.ts index 01bb1d71..ac2b16af 100644 --- a/src/cli/cmd-apply.test.ts +++ b/src/cli/cmd-apply.test.ts @@ -606,6 +606,9 @@ export function staleOne(): number { return 2; } expect(readFile("src/bench/homonym-helper-b.ts")).toMatch( /function helper\(\)/, ); + const consumerB = readFile("src/bench/homonym-consumer-b.ts"); + expect(consumerB).toContain("helper()"); + expect(consumerB).not.toContain("worker"); }); }); diff --git a/src/cli/main.ts b/src/cli/main.ts index a008b375..d5246e66 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -65,8 +65,15 @@ export async function main(): Promise { printRenameAliasHelp(); return; } - const rewritten = resolveRenameAlias(rest); - if (rewritten) rest.splice(0, rest.length, ...rewritten); + const renameResult = resolveRenameAlias(rest); + if (renameResult?.kind === "error") { + console.error(renameResult.message); + process.exitCode = 1; + return; + } + if (renameResult?.kind === "rewrite") { + rest.splice(0, rest.length, ...renameResult.argv); + } } if (rest[0] === "agents" && rest[1] === "init") { diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 6759e974..cfc61578 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -2,10 +2,22 @@ import { describe, expect, it } from "bun:test"; import { formatParamsCli, resolveRenameAlias } from "./rename-alias.js"; +function rewrite(rest: string[]): string[] | undefined { + const r = resolveRenameAlias(rest); + if (r?.kind === "rewrite") return r.argv; + return undefined; +} + +function renameError(rest: string[]): string | undefined { + const r = resolveRenameAlias(rest); + if (r?.kind === "error") return r.message; + return undefined; +} + describe("resolveRenameAlias", () => { it("rewrites positional old/new with scoped flags", () => { expect( - resolveRenameAlias([ + rewrite([ "rename", "helper", "worker", @@ -24,7 +36,7 @@ describe("resolveRenameAlias", () => { it("rewrites --params form", () => { expect( - resolveRenameAlias([ + rewrite([ "rename", "--params", "old=foo,new=bar,define_in=src/x.ts", @@ -41,14 +53,7 @@ describe("resolveRenameAlias", () => { it("merges --params with positional when both present", () => { expect( - resolveRenameAlias([ - "rename", - "a", - "b", - "--params", - "kind=function", - "--dry-run", - ]), + rewrite(["rename", "a", "b", "--params", "kind=function", "--dry-run"]), ).toEqual([ "apply", "rename-preview", @@ -60,7 +65,7 @@ describe("resolveRenameAlias", () => { it("maps --in-file and --kind to recipe params", () => { expect( - resolveRenameAlias([ + rewrite([ "rename", "Foo", "Bar", @@ -77,6 +82,16 @@ describe("resolveRenameAlias", () => { ]); }); + it("allows apply flags before positional old/new", () => { + expect(rewrite(["rename", "--dry-run", "helper", "worker"])).toEqual([ + "apply", + "rename-preview", + "--params", + "new=worker,old=helper", + "--dry-run", + ]); + }); + it("returns null for non-rename commands", () => { expect(resolveRenameAlias(["apply", "rename-preview"])).toBeNull(); }); @@ -86,12 +101,42 @@ describe("resolveRenameAlias", () => { }); it("preserves missing --params operand for downstream apply parser", () => { - expect(resolveRenameAlias(["rename", "--params"])).toEqual([ + expect(rewrite(["rename", "--params"])).toEqual([ "apply", "rename-preview", "--params", ]); }); + + it("errors on missing --define-in operand", () => { + expect(renameError(["rename", "a", "b", "--define-in"])).toContain( + '"--define-in" requires a file path', + ); + }); + + it("errors on missing --in-file operand", () => { + expect(renameError(["rename", "a", "b", "--in-file"])).toContain( + '"--in-file" requires a path prefix', + ); + }); + + it("errors on missing --kind operand", () => { + expect(renameError(["rename", "a", "b", "--kind"])).toContain( + '"--kind" requires a symbol kind', + ); + }); + + it("errors on a single positional", () => { + expect(renameError(["rename", "helper"])).toContain( + "requires and ", + ); + }); + + it("errors on a third positional", () => { + expect(renameError(["rename", "a", "b", "c"])).toMatch( + /unexpected argument "c"/, + ); + }); }); describe("formatParamsCli", () => { diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index dc453013..3dc7075c 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -11,11 +11,67 @@ export function formatParamsCli(params: RecipeParamValues): string { .join(","); } +export type RenameAliasResult = + | { kind: "rewrite"; argv: string[] } + | { kind: "error"; message: string }; + +function renameError(message: string): RenameAliasResult { + return { kind: "error", message }; +} + +/** Split passthrough tail into bare positionals vs apply flags (value-taking flags kept paired). */ +function splitPassthrough(tokens: string[]): { + positionals: string[]; + applyTail: string[]; +} { + const positionals: string[] = []; + const applyTail: string[] = []; + let i = 0; + while (i < tokens.length) { + const a = tokens[i]!; + if (a === "--max-passes" || a === "--commit") { + const next = tokens[i + 1]; + if (next === undefined) { + applyTail.push(a); + i++; + continue; + } + applyTail.push(a, next); + i += 2; + continue; + } + if (a.startsWith("-")) { + applyTail.push(a); + i++; + continue; + } + positionals.push(a); + i++; + } + return { positionals, applyTail }; +} + +function buildApplyArgv( + params: RecipeParamValues | undefined, + applyTail: string[], +): string[] { + if (params && Object.keys(params).length > 0) { + return [ + "apply", + RENAME_RECIPE_ID, + "--params", + formatParamsCli(params), + ...applyTail, + ]; + } + return ["apply", RENAME_RECIPE_ID, ...applyTail]; +} + /** * Thin alias: `codemap rename` → `codemap apply rename-preview`. * Moat A — no new write verb semantics; same recipe + policy gates as `apply`. */ -export function resolveRenameAlias(rest: string[]): string[] | null { +export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { if (rest[0] !== "rename") return null; const tail = rest.slice(1); @@ -32,7 +88,10 @@ export function resolveRenameAlias(rest: string[]): string[] | null { if (a === "--params") { const next = tail[i + 1]; if (next === undefined) { - return ["apply", RENAME_RECIPE_ID, "--params"]; + return { + kind: "rewrite", + argv: ["apply", RENAME_RECIPE_ID, "--params"], + }; } params = mergeParams(params, parseParamsCli(next)); i += 2; @@ -41,7 +100,9 @@ export function resolveRenameAlias(rest: string[]): string[] | null { if (a === "--define-in") { const next = tail[i + 1]; if (next === undefined) { - return ["apply", RENAME_RECIPE_ID, "--define-in"]; + return renameError( + 'codemap rename: "--define-in" requires a file path.', + ); } params = mergeParams(params, { define_in: next }); i += 2; @@ -50,7 +111,9 @@ export function resolveRenameAlias(rest: string[]): string[] | null { if (a === "--in-file") { const next = tail[i + 1]; if (next === undefined) { - return ["apply", RENAME_RECIPE_ID, "--in-file"]; + return renameError( + 'codemap rename: "--in-file" requires a path prefix.', + ); } params = mergeParams(params, { in_file: next }); i += 2; @@ -59,7 +122,7 @@ export function resolveRenameAlias(rest: string[]): string[] | null { if (a === "--kind") { const next = tail[i + 1]; if (next === undefined) { - return ["apply", RENAME_RECIPE_ID, "--kind"]; + return renameError('codemap rename: "--kind" requires a symbol kind.'); } params = mergeParams(params, { kind: next }); i += 2; @@ -69,39 +132,38 @@ export function resolveRenameAlias(rest: string[]): string[] | null { i++; } - const head = passthrough[0]; - const second = passthrough[1]; - if ( - head !== undefined && - second !== undefined && - !head.startsWith("-") && - !second.startsWith("-") - ) { - params = mergeParams(params, { old: head, new: second }); - const applyTail = passthrough.slice(2); - if (params && Object.keys(params).length > 0) { - return [ - "apply", - RENAME_RECIPE_ID, - "--params", - formatParamsCli(params), - ...applyTail, - ]; - } - return ["apply", RENAME_RECIPE_ID, ...applyTail]; + const { positionals, applyTail } = splitPassthrough(passthrough); + + if (positionals.length === 1) { + return renameError( + "codemap rename: requires and (or pass old=/new= via --params).", + ); + } + if (positionals.length > 2) { + return renameError( + `codemap rename: unexpected argument "${positionals[2]}".`, + ); } - if (params && Object.keys(params).length > 0) { - return [ - "apply", - RENAME_RECIPE_ID, - "--params", - formatParamsCli(params), - ...passthrough, - ]; + if (positionals.length === 2) { + params = mergeParams(params, { + old: positionals[0]!, + new: positionals[1]!, + }); + } + + const hasOldNew = + params !== undefined && + params.old !== undefined && + params.new !== undefined; + + if (!hasOldNew && positionals.length === 0 && params === undefined) { + return renameError( + "codemap rename: requires and (or pass old=/new= via --params).", + ); } - return ["apply", RENAME_RECIPE_ID, ...passthrough]; + return { kind: "rewrite", argv: buildApplyArgv(params, applyTail) }; } export function printRenameAliasHelp(): void { @@ -110,7 +172,7 @@ export function printRenameAliasHelp(): void { codemap rename --params old=,new=[,define_in=] [apply flags...] Alias for \`codemap apply rename-preview\` — homonym-safe renames pass \`--define-in\` -(definition \`symbols.file_path\` anchor). \`--in-file\` only narrows output row paths. +to anchor the file where the symbol is defined. \`--in-file\` only narrows output row paths. Apply flags pass through: --dry-run, --yes, --force, --json, --until-empty, --max-passes N, --commit "". diff --git a/templates/agent-content/skill/10-recipes-context.md b/templates/agent-content/skill/10-recipes-context.md index 653fbb55..8659994d 100644 --- a/templates/agent-content/skill/10-recipes-context.md +++ b/templates/agent-content/skill/10-recipes-context.md @@ -63,7 +63,7 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are 4. **Commit (optional)** — CLI `--commit ""` or MCP `commit_message` on recipe `apply` / `apply_diff_input` after a clean apply; with `until_empty`, only when `terminated_by` is `empty`. 5. **Agent rows / diffs** — when you already have hunks (codemod output), skip recipe policy: `apply_rows` / `apply_diff_input` or CLI `codemap apply --rows -` / `--diff-input` (both need `--yes` / `yes: true`). -**Homonym renames:** `rename-preview` unions every `symbols.name` match unless you pass `define_in=` (same anchor as `find-symbol-references`). `in_file` only narrows output row paths — it does not scope the target symbol. +**Homonym renames:** `rename-preview` unions every `symbols.name` match unless you pass `define_in=` (same anchor as `find-symbol-references`). `in_file` only narrows output row paths — it does not scope the target symbol. CLI shorthand: `codemap rename [--define-in ]` (alias → `apply rename-preview`). **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line); apply rewrites the first match only. From ffc6568c41c03408d5b46bbfa44c201f0ee561a5 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:51:23 +0300 Subject: [PATCH 04/13] fix(cli): polish rename alias edge cases and docs Support equals-form scoped flags, reject partial old/new at the alias layer, add changeset and glossary/rule coverage, delete shipped plan. --- .changeset/rename-alias-cli.md | 5 ++ docs/architecture.md | 2 +- docs/glossary.md | 4 +- docs/plans/scoped-rename-define-in.md | 54 --------------- docs/testing-coverage.md | 22 +++--- src/cli/cmd-apply.test.ts | 8 +++ src/cli/rename-alias.test.ts | 24 +++++++ src/cli/rename-alias.ts | 49 +++++++++----- templates/agent-content/rule/00-full.md | 67 ++++++++++--------- .../agent-content/skill/10-recipes-context.md | 2 +- 10 files changed, 119 insertions(+), 118 deletions(-) create mode 100644 .changeset/rename-alias-cli.md delete mode 100644 docs/plans/scoped-rename-define-in.md diff --git a/.changeset/rename-alias-cli.md b/.changeset/rename-alias-cli.md new file mode 100644 index 00000000..5c016624 --- /dev/null +++ b/.changeset/rename-alias-cli.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +Add `codemap rename` CLI alias for homonym-safe renames via `apply rename-preview` (`--define-in`, `--in-file`, `--kind`). diff --git a/docs/architecture.md b/docs/architecture.md index 3d0fb508..bba74ddb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -160,7 +160,7 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload **Bundled diff-shape recipes** (emit the row contract; inspect with `codemap query --recipe --format diff-json`): `rename-preview` (includes member/namespaced JSX via `jsx_elements`), `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (`auto_fixable: false` — writes need `--force` unless allowlisted). Pair read `deprecated-symbols` with `migrate-deprecated` + `deprecated-usages`; `find-jsx-usages` with `migrate-jsx-prop`. Golden map: [`testing-coverage.md`](./testing-coverage.md). -**Homonym-safe rename:** optional `define_in=` on `rename-preview` anchors `target_symbols` and binding-resolved call/JSX sites (distinct from `in_file`, which only filters output row paths). Bare `old`/`new` still unions every same-named symbol. CLI shorthand: `codemap rename [--define-in ]` → `apply rename-preview` (thin alias — same recipe + policy gates). +**Homonym-safe rename:** optional `define_in=` on `rename-preview` anchors `target_symbols` and binding-resolved call/JSX sites (distinct from `in_file`, which only filters output row paths). Bare `old`/`new` still unions every same-named symbol. CLI shorthand: `codemap rename [--define-in ] [--in-file ] [--kind ]` → `apply rename-preview` (thin alias — same recipe + policy gates; see `codemap rename --help`). **Policy** (`src/application/apply-policy.ts`, recipe mode only): non-`auto_fixable` recipes reject writes unless `--force` / MCP `force: true`. `apply.autoApplyRecipes` in user config is an allowlist of recipe ids that may run without TTY `--yes` on non-interactive CLI (MCP/HTTP still require `yes: true` for writes). `--rows` / `apply_rows` / `--diff-input` bypass both gates — separate trust boundary for agent-supplied hunks. diff --git a/docs/glossary.md b/docs/glossary.md index dc457506..d25f9e13 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -41,7 +41,7 @@ Third apply transport for **caller-supplied unified diff text** — parses git-s ### `codemap apply` / apply tool -Substrate-shaped fix executor — reads the same row contract `--format diff-json` emits and applies hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A — verdict-shape "should we fix this?" stays on the recipe author). **Three CLI input modes** (mutually exclusive): (1) **recipe** — `codemap apply [--params k=v[,k=v]]`; MCP/HTTP `apply` `{recipe, params?, dry_run?, yes?, force?, until_empty?, max_passes?, commit_message?}`. (2) **rows** — `codemap apply --rows -|`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input `; MCP/HTTP [`apply_diff_input`](#apply_diff_input-mcp-tool--cli-mode). Shared flags: `--dry-run` (phase-1 only), `--yes` (skip TTY prompt; required for non-TTY writes), `--json`. **Recipe-only flags:** `--force` / MCP `force` (bypass `auto_fixable` + allowlist); `--until-empty` / `until_empty` + `--max-passes` / `max_passes` (fixpoint on recipe `apply` only). **Git commit:** `--commit` / `commit_message` on recipe `apply` and `apply_diff_input`. **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line; apply rewrites first match only). **Policy (recipe mode only):** recipes with `auto_fixable: false` in `.md` frontmatter reject writes unless `--force` / MCP `force: true`; `apply.autoApplyRecipes` in [user config](./architecture.md#user-config) allowlists recipe ids for non-interactive CLI without `--yes` (MCP/HTTP writes still need `yes: true`). Bundled diff-shape recipe ids: `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (force-gated unless allowlisted). **`rename-preview` homonyms:** pass `define_in=` to scope the rename to one definition; omit to union all homonyms (use `find-symbol-references` to pick the anchor first). **`--commit` / `commit_message`:** recipe `apply` and `apply_diff_input` only (not `apply_rows`); with `--until-empty`, commit only when `terminated_by` is `empty`. **Phase 1** validates every row via `actual.includes(before_pattern)` (substring match); seven conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `path is a symlink` / `duplicate edit on same line`). **Phase 2** (gated on `!dryRun && zero conflicts`) writes via sibling temp + `renameSync` per file; **all-or-nothing** across files on conflicts (no cross-file rollback on crash mid-phase-2). **Q6 gate** — TTY without `--yes` prompts `Proceed? [y/N]`; MCP/HTTP have no prompt path. Result envelope: `{mode, applied, files, conflicts, summary}` (+ optional `passes`, `terminated_by`). Re-apply on stale disk → `line content drifted`; re-index then vacuous zero-row pass (Q7). Engine: `application/apply-engine.ts` (`applyDiffPayload`); orchestration: `application/apply-run.ts`. Full transport matrix: [`architecture.md` § Apply — input modes](./architecture.md#apply--input-modes-transport-and-policy). Boundary kit: [§ Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path). +Substrate-shaped fix executor — reads the same row contract `--format diff-json` emits and applies hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A — verdict-shape "should we fix this?" stays on the recipe author). **Three CLI input modes** (mutually exclusive): (1) **recipe** — `codemap apply [--params k=v[,k=v]]`; MCP/HTTP `apply` `{recipe, params?, dry_run?, yes?, force?, until_empty?, max_passes?, commit_message?}`. (2) **rows** — `codemap apply --rows -|`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input `; MCP/HTTP [`apply_diff_input`](#apply_diff_input-mcp-tool--cli-mode). Shared flags: `--dry-run` (phase-1 only), `--yes` (skip TTY prompt; required for non-TTY writes), `--json`. **Recipe-only flags:** `--force` / MCP `force` (bypass `auto_fixable` + allowlist); `--until-empty` / `until_empty` + `--max-passes` / `max_passes` (fixpoint on recipe `apply` only). **Git commit:** `--commit` / `commit_message` on recipe `apply` and `apply_diff_input`. **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line; apply rewrites first match only). **Policy (recipe mode only):** recipes with `auto_fixable: false` in `.md` frontmatter reject writes unless `--force` / MCP `force: true`; `apply.autoApplyRecipes` in [user config](./architecture.md#user-config) allowlists recipe ids for non-interactive CLI without `--yes` (MCP/HTTP writes still need `yes: true`). Bundled diff-shape recipe ids: `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (force-gated unless allowlisted). **`rename-preview` homonyms:** pass `define_in=` to scope the rename to one definition; omit to union all homonyms (use `find-symbol-references` to pick the anchor first). CLI shorthand: `codemap rename [--define-in ]` → `apply rename-preview` (thin alias — same recipe + policy gates). **`--commit` / `commit_message`:** recipe `apply` and `apply_diff_input` only (not `apply_rows`); with `--until-empty`, commit only when `terminated_by` is `empty`. **Phase 1** validates every row via `actual.includes(before_pattern)` (substring match); seven conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `path is a symlink` / `duplicate edit on same line`). **Phase 2** (gated on `!dryRun && zero conflicts`) writes via sibling temp + `renameSync` per file; **all-or-nothing** across files on conflicts (no cross-file rollback on crash mid-phase-2). **Q6 gate** — TTY without `--yes` prompts `Proceed? [y/N]`; MCP/HTTP have no prompt path. Result envelope: `{mode, applied, files, conflicts, summary}` (+ optional `passes`, `terminated_by`). Re-apply on stale disk → `line content drifted`; re-index then vacuous zero-row pass (Q7). Engine: `application/apply-engine.ts` (`applyDiffPayload`); orchestration: `application/apply-run.ts`. Full transport matrix: [`architecture.md` § Apply — input modes](./architecture.md#apply--input-modes-transport-and-policy). Boundary kit: [§ Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path). ### audit @@ -393,7 +393,7 @@ Key-value metadata table. Holds `schema_version`, `last_indexed_commit`, `indexe ### outcome aliases (`dead-code` / `deprecated` / `boundaries` / `hotspots` / `coverage-gaps`) -Top-level CLI verbs that thin-wrap `query --recipe `: `dead-code` → `untested-and-dead`, `deprecated` → `deprecated-symbols`, `boundaries` → `boundary-violations`, `hotspots` → `fan-in`, `coverage-gaps` → `worst-covered-exports`. Every `query` flag passes through (`--json`, `--format`, `--ci`, `--summary`, `--changed-since`, `--group-by`, `--params`, `--save-baseline`, `--baseline`). Mapping lives in `src/cli/aliases.ts` (`OUTCOME_ALIASES`). Capped at 5 to avoid alias-sprawl — promote a sixth only when the recipe becomes a headline outcome. Moat-A clean: the alias is a one-line rewrite, not a new primitive; the recipe IS the SQL. +Top-level CLI verbs that thin-wrap `query --recipe `: `dead-code` → `untested-and-dead`, `deprecated` → `deprecated-symbols`, `boundaries` → `boundary-violations`, `hotspots` → `fan-in`, `coverage-gaps` → `worst-covered-exports`. Every `query` flag passes through (`--json`, `--format`, `--ci`, `--summary`, `--changed-since`, `--group-by`, `--params`, `--save-baseline`, `--baseline`). Mapping lives in `src/cli/aliases.ts` (`OUTCOME_ALIASES`). Capped at 5 to avoid alias-sprawl — promote a sixth only when the recipe becomes a headline outcome. Moat-A clean: the alias is a one-line rewrite, not a new primitive; the recipe IS the SQL. **Write alias (distinct):** `codemap rename` thin-wraps `apply rename-preview` (not `query --recipe`) — mapping in `src/cli/rename-alias.ts`; same Moat-A rule (no new write semantics). ### oxc-parser diff --git a/docs/plans/scoped-rename-define-in.md b/docs/plans/scoped-rename-define-in.md deleted file mode 100644 index baa4494a..00000000 --- a/docs/plans/scoped-rename-define-in.md +++ /dev/null @@ -1,54 +0,0 @@ -# Scoped rename (`define_in`) — plan - -> **Status:** ready to delete · **Priority:** P1 · **Effort:** S (alias slice) + M (shipped in [#165](https://github.com/stainless-code/codemap/pull/165)) -> -> **Motivator:** Homonymous symbol names make bare `rename-preview` union every `symbols.name` match. Agents need the same definition anchor `find-symbol-references` uses (`file_path` of the definition). -> -> **Roadmap:** [§ Backlog — Agent & indexing ops](../roadmap.md#agent--indexing-ops) - ---- - -## Pre-locked decisions - -| # | Decision | Source | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | -| R.1 | **`define_in` scopes `target_symbols`** to `symbols.file_path = define_in`; binding-resolved `call_rows` / JSX CTEs filter when set. | Moat B — substrate | -| R.2 | **`in_file` stays output-only** — prefix filter on result row paths; does not pick which homonym is the rename target. | [rename-preview.md](../../templates/recipes/rename-preview.md) | -| R.3 | **Moat A preserved** — no new verdict engine; optional `codemap rename` is a **thin alias** → `apply rename-preview` (same as outcome aliases → query). | [architecture § Apply](../architecture.md#apply--input-modes-transport-and-policy) | -| R.4 | **Golden + apply e2e** — `rename-preview-homonym-scoped` / `rename-preview-homonym-unscoped` in `fixtures/golden/minimal/`. | [testing-coverage.md](../testing-coverage.md) | - ---- - -## Shipped ([#165](https://github.com/stainless-code/codemap/pull/165)) - -- [x] `define_in` recipe param on `rename-preview` (`templates/recipes/rename-preview.{sql,md}`) -- [x] Binding-scoped `call_rows` + `jsx_element_rows` / `jsx_closing_rows` when `define_in` set -- [x] Golden `rename-preview-homonym-scoped` + homonym fixtures under `fixtures/minimal/src/bench/` -- [x] `cmd-apply.test.ts` homonym apply e2e -- [x] Agent surfaces: README, skill, MCP instructions, `find-symbol-references` action template - ---- - -## Remaining slice (this PR) - -- [x] **`codemap rename` CLI alias** — rewrite to `codemap apply rename-preview` with full apply flag pass-through -- [x] Positional ergonomics: `codemap rename [--define-in path] [--in-file prefix] [--kind k]` -- [x] `rename-alias.test.ts` + integration via `cmd-apply.test.ts` -- [x] `bootstrap.ts` help line; roadmap checkbox for alias - -**Non-goals (unchanged):** global rename verb with new semantics; MCP `rename` tool; AST apply engine. - ---- - -## Acceptance - -- [x] Scoped golden matches binding-resolved call sites only for anchored homonym -- [x] Unscoped golden unions both homonyms -- [x] `codemap rename helper worker --define-in src/bench/homonym-helper-a.ts --yes` ≡ apply recipe path -- [x] `codemap rename --help` documents alias + `define_in` vs `in_file` - ---- - -## Delete trigger - -Delete this plan when the alias slice merges and roadmap item is fully checked — lift one-liner to [architecture § Homonym-safe rename](../architecture.md#apply--input-modes-transport-and-policy). diff --git a/docs/testing-coverage.md b/docs/testing-coverage.md index b2db53ae..64e43a7f 100644 --- a/docs/testing-coverage.md +++ b/docs/testing-coverage.md @@ -33,20 +33,20 @@ Every `templates/recipes/.sql` has **≥1** scenario in `fixtures/golden/sce ### Apply-shaped recipes (diff row contract) -| Recipe id | Golden scenario(s) | CLI e2e (`cmd-apply.test.ts`) | -| ----------------------- | ---------------------------------------------------------------------------- | -------------------------------------------- | -| `rename-preview` | `rename-preview`, `rename-preview-product-card`, `rename-preview-jsx-member` | dry-run, `--yes` disk apply, member JSX tag | -| `migrate-import-source` | `migrate-import-source` | dry-run | -| `replace-marker-kind` | `replace-marker-kind` | `--yes` disk apply (temp project) | -| `add-jsdoc-deprecated` | `add-jsdoc-deprecated` | — (query golden only; writes need `--force`) | -| `stale-imports` | `stale-imports`, `stale-imports-multi-specifier` | dry-run + sole/multi `--force --yes` apply | -| `migrate-jsx-prop` | `migrate-jsx-prop-product-card` | `--force --yes` attribute rename on disk | -| `migrate-deprecated` | `migrate-deprecated` | dry-run + `--force --yes` disk apply | -| `deprecated-usages` | `deprecated-usages` | `--force --yes` disk apply (JSDoc line) | +| Recipe id | Golden scenario(s) | CLI e2e (`cmd-apply.test.ts`) | +| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| `rename-preview` | `rename-preview`, `rename-preview-product-card`, `rename-preview-jsx-member`, `rename-preview-homonym-scoped`, `rename-preview-homonym-unscoped` | dry-run, `--yes` disk apply, member JSX tag, homonym `define_in` scoped/unscoped | +| `migrate-import-source` | `migrate-import-source` | dry-run | +| `replace-marker-kind` | `replace-marker-kind` | `--yes` disk apply (temp project) | +| `add-jsdoc-deprecated` | `add-jsdoc-deprecated` | — (query golden only; writes need `--force`) | +| `stale-imports` | `stale-imports`, `stale-imports-multi-specifier` | dry-run + sole/multi `--force --yes` apply | +| `migrate-jsx-prop` | `migrate-jsx-prop-product-card` | `--force --yes` attribute rename on disk | +| `migrate-deprecated` | `migrate-deprecated` | dry-run + `--force --yes` disk apply | +| `deprecated-usages` | `deprecated-usages` | `--force --yes` disk apply (JSDoc line) | **Read→apply (C.6):** `deprecated-symbols`, `find-symbol-references`, `find-symbol-definitions`, `find-jsx-usages`, `find-import-sites`, `markers-by-kind` frontmatter `actions[].command` → apply twins; `cmd-query.test.ts` rendered-command cases. -**Input modes:** recipe id (above); `--rows` JSON file; `--diff-input` / `--until-empty` / `--commit` — all e2e in `cmd-apply.test.ts`. **`define_in`** homonym scope: golden `rename-preview-homonym-scoped` + `cmd-apply.test.ts`; **`codemap rename`** alias: `rename-alias.test.ts` + homonym e2e in `cmd-apply.test.ts`. MCP/HTTP: `apply` / `apply_rows` / `apply_diff_input` e2e in `mcp-server.test.ts` and `http-server.test.ts`; transport writes/consent/fixpoint in `tool-handlers.test.ts`. +**Input modes:** recipe id (above); `--rows` JSON file; `--diff-input` / `--until-empty` / `--commit` — all e2e in `cmd-apply.test.ts`. **`define_in`** homonym scope: goldens `rename-preview-homonym-scoped` / `rename-preview-homonym-unscoped` + `cmd-apply.test.ts`; **`codemap rename`** alias: `rename-alias.test.ts` + homonym e2e in `cmd-apply.test.ts`. MCP/HTTP: `apply` / `apply_rows` / `apply_diff_input` e2e in `mcp-server.test.ts` and `http-server.test.ts`; transport writes/consent/fixpoint in `tool-handlers.test.ts`. --- diff --git a/src/cli/cmd-apply.test.ts b/src/cli/cmd-apply.test.ts index ac2b16af..06821018 100644 --- a/src/cli/cmd-apply.test.ts +++ b/src/cli/cmd-apply.test.ts @@ -610,6 +610,14 @@ export function staleOne(): number { return 2; } expect(consumerB).toContain("helper()"); expect(consumerB).not.toContain("worker"); }); + + it("rejects a single positional with rename-local error", async () => { + const r = await runCli(["rename", "helper"], { + CODEMAP_ROOT: projectRoot, + }); + expect(r.exitCode).toBe(1); + expect(r.err).toContain("requires and "); + }); }); describe("rename-preview member JSX", () => { diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index cfc61578..5d1280b2 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -137,6 +137,30 @@ describe("resolveRenameAlias", () => { /unexpected argument "c"/, ); }); + + it("accepts equals-form scoped flags", () => { + expect( + rewrite([ + "rename", + "helper", + "worker", + "--define-in=src/a.ts", + "--dry-run", + ]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "define_in=src/a.ts,new=worker,old=helper", + "--dry-run", + ]); + }); + + it("errors on partial old/new via --params", () => { + expect( + renameError(["rename", "--params", "old=foo", "--dry-run"]), + ).toContain("requires and "); + }); }); describe("formatParamsCli", () => { diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index 3dc7075c..0a2e6d73 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -51,6 +51,23 @@ function splitPassthrough(tokens: string[]): { return { positionals, applyTail }; } +function readFlagOperand( + flag: string, + token: string, + tail: string[], + index: number, +): { value: string | undefined; nextIndex: number } | null { + if (token === flag) { + return { value: tail[index + 1], nextIndex: index + 2 }; + } + const prefix = `${flag}=`; + if (token.startsWith(prefix)) { + const value = token.slice(prefix.length); + return { value: value === "" ? undefined : value, nextIndex: index + 1 }; + } + return null; +} + function buildApplyArgv( params: RecipeParamValues | undefined, applyTail: string[], @@ -97,35 +114,35 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { i += 2; continue; } - if (a === "--define-in") { - const next = tail[i + 1]; - if (next === undefined) { + const defineIn = readFlagOperand("--define-in", a, tail, i); + if (defineIn !== null) { + if (defineIn.value === undefined) { return renameError( 'codemap rename: "--define-in" requires a file path.', ); } - params = mergeParams(params, { define_in: next }); - i += 2; + params = mergeParams(params, { define_in: defineIn.value }); + i = defineIn.nextIndex; continue; } - if (a === "--in-file") { - const next = tail[i + 1]; - if (next === undefined) { + const inFile = readFlagOperand("--in-file", a, tail, i); + if (inFile !== null) { + if (inFile.value === undefined) { return renameError( 'codemap rename: "--in-file" requires a path prefix.', ); } - params = mergeParams(params, { in_file: next }); - i += 2; + params = mergeParams(params, { in_file: inFile.value }); + i = inFile.nextIndex; continue; } - if (a === "--kind") { - const next = tail[i + 1]; - if (next === undefined) { + const kind = readFlagOperand("--kind", a, tail, i); + if (kind !== null) { + if (kind.value === undefined) { return renameError('codemap rename: "--kind" requires a symbol kind.'); } - params = mergeParams(params, { kind: next }); - i += 2; + params = mergeParams(params, { kind: kind.value }); + i = kind.nextIndex; continue; } passthrough.push(a); @@ -157,7 +174,7 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { params.old !== undefined && params.new !== undefined; - if (!hasOldNew && positionals.length === 0 && params === undefined) { + if (!hasOldNew) { return renameError( "codemap rename: requires and (or pass old=/new= via --params).", ); diff --git a/templates/agent-content/rule/00-full.md b/templates/agent-content/rule/00-full.md index db87931f..1fb8c98b 100644 --- a/templates/agent-content/rule/00-full.md +++ b/templates/agent-content/rule/00-full.md @@ -24,39 +24,40 @@ codemap query --recipes-json # canonical list of every bundled + p If the question matches any of these, use the index instead of grepping: -| Question shape | Table(s) / Recipe | -| ------------------------------------------------------------ | ------------------------------------------------------------------------- | -| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) | -| "Where is X defined?" | `symbols` | -| "What does file X export?" | `exports` | -| "Who depends on file X?" / "What does file X depend on?" | `dependencies` | -| "Who calls X?" / "What does X call?" | `calls` | -| "Where is X used?" / "Every reference to X" | `--recipe find-references` (name-keyed) | -| "Every reference to X defined in file Y" (precise rename) | `--recipe find-symbol-references` (bindings-precise) | -| "Every write to X" | `--recipe find-write-sites` | -| "Every fn taking a `User` param" | `--recipe find-by-param-type` (params `type_text=...`) | -| "What hooks does component X use?" / "List React components" | `components` | -| "What are the CSS variables/tokens for X?" | `css_variables` | -| "What CSS classes / keyframes are in X?" | `css_classes` / `css_keyframes` | -| "Find all TODOs / FIXMEs / HACKs / NOTEs" | `markers` | -| "What fields does interface/type X have?" | `type_members` | -| "What does X extend / implement?" / type hierarchy | `type_heritage` / `--recipe type-ancestors` / `--recipe type-descendants` | -| "Is X deprecated?" / "What's `@beta` / `@internal`?" | `symbols.doc_comment` / `symbols.visibility` | -| "Leftover `console.log` calls" | `--recipe find-leftover-console` (or `runtime_markers`) | -| "What `process.env.X` vars does this app read?" | `--recipe env-var-audit` | -| "Find `.skip` / `.only` / `.todo` tests" | `--recipe find-skipped-tests` | -| "Tests per file (counts + framework)" | `--recipe tests-by-file` | -| "Are there import cycles?" / "Files in cycles" | `--recipe circular-imports` / `module_cycles` | -| "Where do barrel files re-export from?" | `--recipe barrel-chains` / `re_export_chains` | -| "Functions over 50 lines / deeply nested" | `--recipe large-functions` / `deeply-nested-functions` | -| "What's the cyclomatic complexity / nesting depth of X?" | `symbols.complexity` / `symbols.nesting_depth` | -| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `codemap ingest-coverage`) | -| "What's structurally dead AND untested?" | `--recipe untested-and-dead` | -| "Worst-covered exported functions" | `--recipe worst-covered-exports` | -| "Which exports has nobody imported?" | `--recipe unimported-exports` | -| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | -| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | -| "What's high-complexity AND undertested?" | `--recipe high-complexity-untested` | +| Question shape | Table(s) / Recipe | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | +| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) | +| "Where is X defined?" | `symbols` | +| "What does file X export?" | `exports` | +| "Who depends on file X?" / "What does file X depend on?" | `dependencies` | +| "Who calls X?" / "What does X call?" | `calls` | +| "Where is X used?" / "Every reference to X" | `--recipe find-references` (name-keyed) | +| "Every reference to X defined in file Y" (precise rename) | `--recipe find-symbol-references` (bindings-precise) | +| Homonym-safe rename (scoped definition anchor) | `--recipe rename-preview` with `define_in=`; CLI `codemap rename --define-in ` | +| "Every write to X" | `--recipe find-write-sites` | +| "Every fn taking a `User` param" | `--recipe find-by-param-type` (params `type_text=...`) | +| "What hooks does component X use?" / "List React components" | `components` | +| "What are the CSS variables/tokens for X?" | `css_variables` | +| "What CSS classes / keyframes are in X?" | `css_classes` / `css_keyframes` | +| "Find all TODOs / FIXMEs / HACKs / NOTEs" | `markers` | +| "What fields does interface/type X have?" | `type_members` | +| "What does X extend / implement?" / type hierarchy | `type_heritage` / `--recipe type-ancestors` / `--recipe type-descendants` | +| "Is X deprecated?" / "What's `@beta` / `@internal`?" | `symbols.doc_comment` / `symbols.visibility` | +| "Leftover `console.log` calls" | `--recipe find-leftover-console` (or `runtime_markers`) | +| "What `process.env.X` vars does this app read?" | `--recipe env-var-audit` | +| "Find `.skip` / `.only` / `.todo` tests" | `--recipe find-skipped-tests` | +| "Tests per file (counts + framework)" | `--recipe tests-by-file` | +| "Are there import cycles?" / "Files in cycles" | `--recipe circular-imports` / `module_cycles` | +| "Where do barrel files re-export from?" | `--recipe barrel-chains` / `re_export_chains` | +| "Functions over 50 lines / deeply nested" | `--recipe large-functions` / `deeply-nested-functions` | +| "What's the cyclomatic complexity / nesting depth of X?" | `symbols.complexity` / `symbols.nesting_depth` | +| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `codemap ingest-coverage`) | +| "What's structurally dead AND untested?" | `--recipe untested-and-dead` | +| "Worst-covered exported functions" | `--recipe worst-covered-exports` | +| "Which exports has nobody imported?" | `--recipe unimported-exports` | +| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | +| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | +| "What's high-complexity AND undertested?" | `--recipe high-complexity-untested` | ## Quick reference queries diff --git a/templates/agent-content/skill/10-recipes-context.md b/templates/agent-content/skill/10-recipes-context.md index 8659994d..672b3b77 100644 --- a/templates/agent-content/skill/10-recipes-context.md +++ b/templates/agent-content/skill/10-recipes-context.md @@ -63,7 +63,7 @@ Each emitted delta carries its own `base` metadata so mixed-baseline audits are 4. **Commit (optional)** — CLI `--commit ""` or MCP `commit_message` on recipe `apply` / `apply_diff_input` after a clean apply; with `until_empty`, only when `terminated_by` is `empty`. 5. **Agent rows / diffs** — when you already have hunks (codemod output), skip recipe policy: `apply_rows` / `apply_diff_input` or CLI `codemap apply --rows -` / `--diff-input` (both need `--yes` / `yes: true`). -**Homonym renames:** `rename-preview` unions every `symbols.name` match unless you pass `define_in=` (same anchor as `find-symbol-references`). `in_file` only narrows output row paths — it does not scope the target symbol. CLI shorthand: `codemap rename [--define-in ]` (alias → `apply rename-preview`). +**Homonym renames:** `rename-preview` unions every `symbols.name` match unless you pass `define_in=` (same anchor as `find-symbol-references`). `in_file` only narrows output row paths — it does not scope the target symbol. CLI shorthand: `codemap rename [--define-in ] [--in-file ] [--kind ]` (alias → `apply rename-preview`). **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line); apply rewrites the first match only. From 36718fb5f6dba4c7de441d87eb670a4c8d427732 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:53:23 +0300 Subject: [PATCH 05/13] fix(cli): improve stray-arg error after complete rename params Report unexpected positionals when old/new are already bound via --params; align glossary shorthand with full rename flag surface. --- docs/glossary.md | 2 +- src/cli/rename-alias.ts | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index d25f9e13..1aada1ac 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -41,7 +41,7 @@ Third apply transport for **caller-supplied unified diff text** — parses git-s ### `codemap apply` / apply tool -Substrate-shaped fix executor — reads the same row contract `--format diff-json` emits and applies hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A — verdict-shape "should we fix this?" stays on the recipe author). **Three CLI input modes** (mutually exclusive): (1) **recipe** — `codemap apply [--params k=v[,k=v]]`; MCP/HTTP `apply` `{recipe, params?, dry_run?, yes?, force?, until_empty?, max_passes?, commit_message?}`. (2) **rows** — `codemap apply --rows -|`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input `; MCP/HTTP [`apply_diff_input`](#apply_diff_input-mcp-tool--cli-mode). Shared flags: `--dry-run` (phase-1 only), `--yes` (skip TTY prompt; required for non-TTY writes), `--json`. **Recipe-only flags:** `--force` / MCP `force` (bypass `auto_fixable` + allowlist); `--until-empty` / `until_empty` + `--max-passes` / `max_passes` (fixpoint on recipe `apply` only). **Git commit:** `--commit` / `commit_message` on recipe `apply` and `apply_diff_input`. **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line; apply rewrites first match only). **Policy (recipe mode only):** recipes with `auto_fixable: false` in `.md` frontmatter reject writes unless `--force` / MCP `force: true`; `apply.autoApplyRecipes` in [user config](./architecture.md#user-config) allowlists recipe ids for non-interactive CLI without `--yes` (MCP/HTTP writes still need `yes: true`). Bundled diff-shape recipe ids: `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (force-gated unless allowlisted). **`rename-preview` homonyms:** pass `define_in=` to scope the rename to one definition; omit to union all homonyms (use `find-symbol-references` to pick the anchor first). CLI shorthand: `codemap rename [--define-in ]` → `apply rename-preview` (thin alias — same recipe + policy gates). **`--commit` / `commit_message`:** recipe `apply` and `apply_diff_input` only (not `apply_rows`); with `--until-empty`, commit only when `terminated_by` is `empty`. **Phase 1** validates every row via `actual.includes(before_pattern)` (substring match); seven conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `path is a symlink` / `duplicate edit on same line`). **Phase 2** (gated on `!dryRun && zero conflicts`) writes via sibling temp + `renameSync` per file; **all-or-nothing** across files on conflicts (no cross-file rollback on crash mid-phase-2). **Q6 gate** — TTY without `--yes` prompts `Proceed? [y/N]`; MCP/HTTP have no prompt path. Result envelope: `{mode, applied, files, conflicts, summary}` (+ optional `passes`, `terminated_by`). Re-apply on stale disk → `line content drifted`; re-index then vacuous zero-row pass (Q7). Engine: `application/apply-engine.ts` (`applyDiffPayload`); orchestration: `application/apply-run.ts`. Full transport matrix: [`architecture.md` § Apply — input modes](./architecture.md#apply--input-modes-transport-and-policy). Boundary kit: [§ Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path). +Substrate-shaped fix executor — reads the same row contract `--format diff-json` emits and applies hunks to disk. Recipe SQL is the synthesis surface; codemap is the executor (Moat-A — verdict-shape "should we fix this?" stays on the recipe author). **Three CLI input modes** (mutually exclusive): (1) **recipe** — `codemap apply [--params k=v[,k=v]]`; MCP/HTTP `apply` `{recipe, params?, dry_run?, yes?, force?, until_empty?, max_passes?, commit_message?}`. (2) **rows** — `codemap apply --rows -|`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input `; MCP/HTTP [`apply_diff_input`](#apply_diff_input-mcp-tool--cli-mode). Shared flags: `--dry-run` (phase-1 only), `--yes` (skip TTY prompt; required for non-TTY writes), `--json`. **Recipe-only flags:** `--force` / MCP `force` (bypass `auto_fixable` + allowlist); `--until-empty` / `until_empty` + `--max-passes` / `max_passes` (fixpoint on recipe `apply` only). **Git commit:** `--commit` / `commit_message` on recipe `apply` and `apply_diff_input`. **diff-json preview:** each hunk includes `ambiguity_count` (extra `before_pattern` matches on the line; apply rewrites first match only). **Policy (recipe mode only):** recipes with `auto_fixable: false` in `.md` frontmatter reject writes unless `--force` / MCP `force: true`; `apply.autoApplyRecipes` in [user config](./architecture.md#user-config) allowlists recipe ids for non-interactive CLI without `--yes` (MCP/HTTP writes still need `yes: true`). Bundled diff-shape recipe ids: `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true`); `stale-imports`, `migrate-deprecated`, `deprecated-usages`, `add-jsdoc-deprecated`, `migrate-jsx-prop` (force-gated unless allowlisted). **`rename-preview` homonyms:** pass `define_in=` to scope the rename to one definition; omit to union all homonyms (use `find-symbol-references` to pick the anchor first). CLI shorthand: `codemap rename [--define-in ] [--in-file ] [--kind ]` → `apply rename-preview` (thin alias — same recipe + policy gates). **`--commit` / `commit_message`:** recipe `apply` and `apply_diff_input` only (not `apply_rows`); with `--until-empty`, commit only when `terminated_by` is `empty`. **Phase 1** validates every row via `actual.includes(before_pattern)` (substring match); seven conflict reasons (`file missing` / `line out of range` / `line content drifted` / `path escapes project root` / `path is a symlink` / `duplicate edit on same line`). **Phase 2** (gated on `!dryRun && zero conflicts`) writes via sibling temp + `renameSync` per file; **all-or-nothing** across files on conflicts (no cross-file rollback on crash mid-phase-2). **Q6 gate** — TTY without `--yes` prompts `Proceed? [y/N]`; MCP/HTTP have no prompt path. Result envelope: `{mode, applied, files, conflicts, summary}` (+ optional `passes`, `terminated_by`). Re-apply on stale disk → `line content drifted`; re-index then vacuous zero-row pass (Q7). Engine: `application/apply-engine.ts` (`applyDiffPayload`); orchestration: `application/apply-run.ts`. Full transport matrix: [`architecture.md` § Apply — input modes](./architecture.md#apply--input-modes-transport-and-policy). Boundary kit: [§ Boundary verification — apply write path](./architecture.md#boundary-verification--apply-write-path). ### audit diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index 0a2e6d73..60388591 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -151,11 +151,6 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { const { positionals, applyTail } = splitPassthrough(passthrough); - if (positionals.length === 1) { - return renameError( - "codemap rename: requires and (or pass old=/new= via --params).", - ); - } if (positionals.length > 2) { return renameError( `codemap rename: unexpected argument "${positionals[2]}".`, @@ -174,6 +169,17 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { params.old !== undefined && params.new !== undefined; + if (positionals.length === 1) { + if (hasOldNew) { + return renameError( + `codemap rename: unexpected argument "${positionals[0]}".`, + ); + } + return renameError( + "codemap rename: requires and (or pass old=/new= via --params).", + ); + } + if (!hasOldNew) { return renameError( "codemap rename: requires and (or pass old=/new= via --params).", From bba068bcd4fc2ea33ad041c951e8af915d9cf54a Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 09:53:35 +0300 Subject: [PATCH 06/13] test(cli): cover stray positional after rename --params --- src/cli/rename-alias.test.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 5d1280b2..04b16592 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -161,6 +161,18 @@ describe("resolveRenameAlias", () => { renameError(["rename", "--params", "old=foo", "--dry-run"]), ).toContain("requires and "); }); + + it("errors on stray positional after complete --params", () => { + expect( + renameError([ + "rename", + "--params", + "old=foo,new=bar", + "--dry-run", + "stray", + ]), + ).toMatch(/unexpected argument "stray"/); + }); }); describe("formatParamsCli", () => { From 97ade6b2e73e0b16aa15cc10e5fe9c05091cc5ef Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 10:15:13 +0300 Subject: [PATCH 07/13] fix(cli): preserve state on trailing bare --params in rename Only early-return bare --params when it is the sole rename arg; otherwise keep merged params, positionals, and apply flags. Delegate incomplete old/new to apply when applyTail still carries --params. --- .../codemap-richer-index-synthesis-2026-05.md | 32 ++++----- src/cli/rename-alias.test.ts | 28 ++++++++ src/cli/rename-alias.ts | 19 ++++-- templates/agent-content/rule/00-full.md | 68 +++++++++---------- 4 files changed, 93 insertions(+), 54 deletions(-) diff --git a/docs/research/codemap-richer-index-synthesis-2026-05.md b/docs/research/codemap-richer-index-synthesis-2026-05.md index ec8d644e..0eeb1ecd 100644 --- a/docs/research/codemap-richer-index-synthesis-2026-05.md +++ b/docs/research/codemap-richer-index-synthesis-2026-05.md @@ -534,7 +534,7 @@ The minimum synthesis preserving every consensus claim (§ 2) and resolving ever | 11 | `--until-empty` / `max_passes` / fixpoint envelope | ✓ Shipped 2026-06 | | 12 | `apply.autoApplyRecipes` allowlist | ✓ Shipped 2026-06 | -**Follow-on (not § 6 steps):** `define_in` on `rename-preview` for homonym-safe renames (shipped [#165](https://github.com/stainless-code/codemap/pull/165)); optional `codemap rename` CLI alias — [roadmap](../roadmap.md). Multi-line row contract and global rename verb remain backlog per architecture § Apply. +**Follow-on (not § 6 steps):** `define_in` on `rename-preview` for homonym-safe renames (shipped [#165](https://github.com/stainless-code/codemap/pull/165)); thin `codemap rename` CLI alias → `apply rename-preview` (shipped [#166](https://github.com/stainless-code/codemap/pull/166) — not a new write primitive per [architecture § Apply](../architecture.md#apply--input-modes-transport-and-policy)). Multi-line row contract remains backlog. --- @@ -542,21 +542,21 @@ The minimum synthesis preserving every consensus claim (§ 2) and resolving ever Items rejected on architectural grounds (not on time/demand). Listing here so the rejection is grep-able from the synthesis and a future contributor doesn't re-litigate without seeing the prior verdict. -| Item | Source | Why rejected | Trigger to revisit | -| ------------------------------------------------------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Curated CLI write verbs (`codemap rename`, `codemap fix deprecated`, etc.) | A2, A3 | § 3.1 verdict — premature; pro-verb sources disagree on cap (8–12 vs 3–5); read-side outcome-alias pattern requires recipe layer to land first. | ≥3 diff-shape recipes shipping AND clear agent-host UX demand for verb-level discovery beyond `actions[].command` template. | -| Parallel `applyAstPayload()` AST engine (Path A) | A5, A3 (long-term) | § 3.2 verdict — competes with `ts-morph` / `jscodeshift` on their home turf; AST printer maintenance burden in perpetuity; positioning blur; floor-disappearance makes product surface unbounded. | ≥2 of: (a) ≥3 external project teams hit substring-substitution wall on real recipes; (b) specific AST-shape transformation class requested with concrete consumer demand; (c) agent ecosystem moves toward AST-template-shaped patches AND substring contract becomes bottleneck; (d) Path B handoff seam friction motivates integrated AST writer. | -| Trust tiers (`safe` / `review` / `risky` taxonomy on recipes) | A2 | § 3.4 verdict — adds taxonomy debt; the binary `auto_fixable` flag (Step 4) plus the `apply.autoApplyRecipes` allowlist (Step 12) covers the same use cases. | Allowlist proves insufficient AND ≥2 consumers ship `jq`-style trust filters in CI. | -| Per-row confidence scores in `diff-json` | A2 | § 3.4 verdict — speculative; no consensus on computation method (heuristic per recipe? graph-derived? LLM-tagged?). | A recipe ships where `before_pattern` matches multiple sites and the desired UX is per-site ranking. | -| Verifier as product surface (typecheck / lint / tests + expected structural delta) | A3 | § 3.4 verdict — scope creep into orchestration; consumer-side CI / pre-commit owns this; watch + reindex covers codemap-side structural verification. | A consumer-driven plan PR articulates the verifier shape with concrete examples. | -| Reliability loop (collect conflict-rate / apply-success metrics) | A2 | § 3.4 verdict — needs telemetry surface; codemap doesn't ship telemetry upload (Floors row). | A consumer requests the shape with an offline / self-hosted observability target. | -| Generalised `references` + `bindings` + `scopes` + `symbol_namespace` substrate | A3, A4 | § 3.5 verdict — incremental position tables first; consolidate when ≥3 land AND a recipe wants UNION. | Third position-table lands AND a recipe wants to UNION across all three. | -| `--branch` / `--output-patch` workflow flags | A1 | § 4.5 — nice-to-have; `--commit` (Step 10) is the priority workflow flag. | User reports of `--commit` being insufficient. | -| Multi-line + kind-tagged row contract (`before_lines`, `kind: insert/delete/replace`) | A1 | Postponed; the synthesis path covers single-line cases first. Multi-line is a contract extension after Step 11 ships. | A recipe needs multi-line edits AND single-line workarounds prove insufficient. | -| C.9 plugin layer entry-point integration with apply | A5 | Tracked in [`docs/plans/c9-plugin-layer.md`](../plans/c9-plugin-layer.md); already its own plan PR. Synthesis path doesn't depend on it; recipes that need entry-point awareness JOIN to `files.is_entry` once C.9 ships. | C.9 lands. | -| Cross-file moves (`move_to: { file_path, line_start }`) | A1 mentions | Higher risk than single-file edits; defer until single-file multi-line proves out. | A recipe needs cross-file moves AND the alternative (delete-source + insert-dest as two operations) proves insufficient. | -| Cross-file atomic apply (pre-write backups + restore-on-throw) | A1 mentions | Current per-file atomicity is fine for ≤10 files; defer until apply scales to 50+ files in real recipes. | A real `apply` invocation crosses 50 files AND a phase-2 I/O failure leaks partial state. | -| `codemap-to-tsmorph` adapter (Path B partner shim) | A4 | Not rejected — separable; ships independently of the main path. Codemap-side surface is `apply --rows -` (Step 8) — adapter lives in user-side glue. | Independent; ship anytime as a separate package experiment after Step 8. | +| Item | Source | Why rejected | Trigger to revisit | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Curated CLI write verbs (`codemap fix deprecated`, etc.) — **excludes** thin alias `codemap rename` → `apply rename-preview` ([#166](https://github.com/stainless-code/codemap/pull/166)) | A2, A3 | § 3.1 verdict — premature; pro-verb sources disagree on cap (8–12 vs 3–5); read-side outcome-alias pattern requires recipe layer to land first. Thin rename alias is Moat-A argv rewrite only (see architecture § Apply non-goals). | ≥3 diff-shape recipes shipping AND clear agent-host UX demand for verb-level discovery beyond `actions[].command` template. | +| Parallel `applyAstPayload()` AST engine (Path A) | A5, A3 (long-term) | § 3.2 verdict — competes with `ts-morph` / `jscodeshift` on their home turf; AST printer maintenance burden in perpetuity; positioning blur; floor-disappearance makes product surface unbounded. | ≥2 of: (a) ≥3 external project teams hit substring-substitution wall on real recipes; (b) specific AST-shape transformation class requested with concrete consumer demand; (c) agent ecosystem moves toward AST-template-shaped patches AND substring contract becomes bottleneck; (d) Path B handoff seam friction motivates integrated AST writer. | +| Trust tiers (`safe` / `review` / `risky` taxonomy on recipes) | A2 | § 3.4 verdict — adds taxonomy debt; the binary `auto_fixable` flag (Step 4) plus the `apply.autoApplyRecipes` allowlist (Step 12) covers the same use cases. | Allowlist proves insufficient AND ≥2 consumers ship `jq`-style trust filters in CI. | +| Per-row confidence scores in `diff-json` | A2 | § 3.4 verdict — speculative; no consensus on computation method (heuristic per recipe? graph-derived? LLM-tagged?). | A recipe ships where `before_pattern` matches multiple sites and the desired UX is per-site ranking. | +| Verifier as product surface (typecheck / lint / tests + expected structural delta) | A3 | § 3.4 verdict — scope creep into orchestration; consumer-side CI / pre-commit owns this; watch + reindex covers codemap-side structural verification. | A consumer-driven plan PR articulates the verifier shape with concrete examples. | +| Reliability loop (collect conflict-rate / apply-success metrics) | A2 | § 3.4 verdict — needs telemetry surface; codemap doesn't ship telemetry upload (Floors row). | A consumer requests the shape with an offline / self-hosted observability target. | +| Generalised `references` + `bindings` + `scopes` + `symbol_namespace` substrate | A3, A4 | § 3.5 verdict — incremental position tables first; consolidate when ≥3 land AND a recipe wants UNION. | Third position-table lands AND a recipe wants to UNION across all three. | +| `--branch` / `--output-patch` workflow flags | A1 | § 4.5 — nice-to-have; `--commit` (Step 10) is the priority workflow flag. | User reports of `--commit` being insufficient. | +| Multi-line + kind-tagged row contract (`before_lines`, `kind: insert/delete/replace`) | A1 | Postponed; the synthesis path covers single-line cases first. Multi-line is a contract extension after Step 11 ships. | A recipe needs multi-line edits AND single-line workarounds prove insufficient. | +| C.9 plugin layer entry-point integration with apply | A5 | Tracked in [`docs/plans/c9-plugin-layer.md`](../plans/c9-plugin-layer.md); already its own plan PR. Synthesis path doesn't depend on it; recipes that need entry-point awareness JOIN to `files.is_entry` once C.9 ships. | C.9 lands. | +| Cross-file moves (`move_to: { file_path, line_start }`) | A1 mentions | Higher risk than single-file edits; defer until single-file multi-line proves out. | A recipe needs cross-file moves AND the alternative (delete-source + insert-dest as two operations) proves insufficient. | +| Cross-file atomic apply (pre-write backups + restore-on-throw) | A1 mentions | Current per-file atomicity is fine for ≤10 files; defer until apply scales to 50+ files in real recipes. | A real `apply` invocation crosses 50 files AND a phase-2 I/O failure leaks partial state. | +| `codemap-to-tsmorph` adapter (Path B partner shim) | A4 | Not rejected — separable; ships independently of the main path. Codemap-side surface is `apply --rows -` (Step 8) — adapter lives in user-side glue. | Independent; ship anytime as a separate package experiment after Step 8. | --- diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 04b16592..014b71fa 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -108,6 +108,34 @@ describe("resolveRenameAlias", () => { ]); }); + it("preserves prior state when trailing bare --params follows values", () => { + expect( + rewrite(["rename", "--params", "old=foo,new=bar", "--params"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=bar,old=foo", + "--params", + ]); + }); + + it("preserves positionals and apply flags when trailing bare --params", () => { + expect(rewrite(["rename", "helper", "worker", "--params"])).toEqual([ + "apply", + "rename-preview", + "--params", + "new=worker,old=helper", + "--params", + ]); + expect(rewrite(["rename", "--dry-run", "--params"])).toEqual([ + "apply", + "rename-preview", + "--dry-run", + "--params", + ]); + }); + it("errors on missing --define-in operand", () => { expect(renameError(["rename", "a", "b", "--define-in"])).toContain( '"--define-in" requires a file path', diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index 60388591..d18531f1 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -105,10 +105,18 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { if (a === "--params") { const next = tail[i + 1]; if (next === undefined) { - return { - kind: "rewrite", - argv: ["apply", RENAME_RECIPE_ID, "--params"], - }; + const bareOnly = + passthrough.length === 0 && + (params === undefined || Object.keys(params).length === 0); + if (bareOnly) { + return { + kind: "rewrite", + argv: ["apply", RENAME_RECIPE_ID, "--params"], + }; + } + passthrough.push(a); + i++; + continue; } params = mergeParams(params, parseParamsCli(next)); i += 2; @@ -181,6 +189,9 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { } if (!hasOldNew) { + if (applyTail.includes("--params")) { + return { kind: "rewrite", argv: buildApplyArgv(params, applyTail) }; + } return renameError( "codemap rename: requires and (or pass old=/new= via --params).", ); diff --git a/templates/agent-content/rule/00-full.md b/templates/agent-content/rule/00-full.md index 1fb8c98b..785b3239 100644 --- a/templates/agent-content/rule/00-full.md +++ b/templates/agent-content/rule/00-full.md @@ -24,40 +24,40 @@ codemap query --recipes-json # canonical list of every bundled + p If the question matches any of these, use the index instead of grepping: -| Question shape | Table(s) / Recipe | -| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- | -| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) | -| "Where is X defined?" | `symbols` | -| "What does file X export?" | `exports` | -| "Who depends on file X?" / "What does file X depend on?" | `dependencies` | -| "Who calls X?" / "What does X call?" | `calls` | -| "Where is X used?" / "Every reference to X" | `--recipe find-references` (name-keyed) | -| "Every reference to X defined in file Y" (precise rename) | `--recipe find-symbol-references` (bindings-precise) | -| Homonym-safe rename (scoped definition anchor) | `--recipe rename-preview` with `define_in=`; CLI `codemap rename --define-in ` | -| "Every write to X" | `--recipe find-write-sites` | -| "Every fn taking a `User` param" | `--recipe find-by-param-type` (params `type_text=...`) | -| "What hooks does component X use?" / "List React components" | `components` | -| "What are the CSS variables/tokens for X?" | `css_variables` | -| "What CSS classes / keyframes are in X?" | `css_classes` / `css_keyframes` | -| "Find all TODOs / FIXMEs / HACKs / NOTEs" | `markers` | -| "What fields does interface/type X have?" | `type_members` | -| "What does X extend / implement?" / type hierarchy | `type_heritage` / `--recipe type-ancestors` / `--recipe type-descendants` | -| "Is X deprecated?" / "What's `@beta` / `@internal`?" | `symbols.doc_comment` / `symbols.visibility` | -| "Leftover `console.log` calls" | `--recipe find-leftover-console` (or `runtime_markers`) | -| "What `process.env.X` vars does this app read?" | `--recipe env-var-audit` | -| "Find `.skip` / `.only` / `.todo` tests" | `--recipe find-skipped-tests` | -| "Tests per file (counts + framework)" | `--recipe tests-by-file` | -| "Are there import cycles?" / "Files in cycles" | `--recipe circular-imports` / `module_cycles` | -| "Where do barrel files re-export from?" | `--recipe barrel-chains` / `re_export_chains` | -| "Functions over 50 lines / deeply nested" | `--recipe large-functions` / `deeply-nested-functions` | -| "What's the cyclomatic complexity / nesting depth of X?" | `symbols.complexity` / `symbols.nesting_depth` | -| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `codemap ingest-coverage`) | -| "What's structurally dead AND untested?" | `--recipe untested-and-dead` | -| "Worst-covered exported functions" | `--recipe worst-covered-exports` | -| "Which exports has nobody imported?" | `--recipe unimported-exports` | -| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | -| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | -| "What's high-complexity AND undertested?" | `--recipe high-complexity-untested` | +| Question shape | Table(s) / Recipe | +| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) | +| "Where is X defined?" | `symbols` | +| "What does file X export?" | `exports` | +| "Who depends on file X?" / "What does file X depend on?" | `dependencies` | +| "Who calls X?" / "What does X call?" | `calls` | +| "Where is X used?" / "Every reference to X" | `--recipe find-references` (name-keyed) | +| "Every reference to X defined in file Y" (precise rename) | `--recipe find-symbol-references` (bindings-precise) | +| Homonym-safe rename (scoped definition anchor) | `--recipe rename-preview` with `define_in=`; CLI `codemap rename [--define-in ] [--in-file ] [--kind ]` | +| "Every write to X" | `--recipe find-write-sites` | +| "Every fn taking a `User` param" | `--recipe find-by-param-type` (params `type_text=...`) | +| "What hooks does component X use?" / "List React components" | `components` | +| "What are the CSS variables/tokens for X?" | `css_variables` | +| "What CSS classes / keyframes are in X?" | `css_classes` / `css_keyframes` | +| "Find all TODOs / FIXMEs / HACKs / NOTEs" | `markers` | +| "What fields does interface/type X have?" | `type_members` | +| "What does X extend / implement?" / type hierarchy | `type_heritage` / `--recipe type-ancestors` / `--recipe type-descendants` | +| "Is X deprecated?" / "What's `@beta` / `@internal`?" | `symbols.doc_comment` / `symbols.visibility` | +| "Leftover `console.log` calls" | `--recipe find-leftover-console` (or `runtime_markers`) | +| "What `process.env.X` vars does this app read?" | `--recipe env-var-audit` | +| "Find `.skip` / `.only` / `.todo` tests" | `--recipe find-skipped-tests` | +| "Tests per file (counts + framework)" | `--recipe tests-by-file` | +| "Are there import cycles?" / "Files in cycles" | `--recipe circular-imports` / `module_cycles` | +| "Where do barrel files re-export from?" | `--recipe barrel-chains` / `re_export_chains` | +| "Functions over 50 lines / deeply nested" | `--recipe large-functions` / `deeply-nested-functions` | +| "What's the cyclomatic complexity / nesting depth of X?" | `symbols.complexity` / `symbols.nesting_depth` | +| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `codemap ingest-coverage`) | +| "What's structurally dead AND untested?" | `--recipe untested-and-dead` | +| "Worst-covered exported functions" | `--recipe worst-covered-exports` | +| "Which exports has nobody imported?" | `--recipe unimported-exports` | +| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | +| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | +| "What's high-complexity AND undertested?" | `--recipe high-complexity-untested` | ## Quick reference queries From 7ff7d8fa5e063aa64c4fc92c8917cc5a035bc4d8 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 10:17:51 +0300 Subject: [PATCH 08/13] fix(cli): reject flag tokens as --params operands in rename Mirror query/apply parser guard: when --params is not followed by a value token, treat it as bare trailing --params instead of parsing the next flag as k=v input. --- src/cli/rename-alias.test.ts | 13 +++++++++++++ src/cli/rename-alias.ts | 3 ++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 014b71fa..1de0fe10 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -136,6 +136,19 @@ describe("resolveRenameAlias", () => { ]); }); + it("does not treat a following flag as --params operand", () => { + expect( + rewrite(["rename", "helper", "worker", "--params", "--dry-run"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=worker,old=helper", + "--params", + "--dry-run", + ]); + }); + it("errors on missing --define-in operand", () => { expect(renameError(["rename", "a", "b", "--define-in"])).toContain( '"--define-in" requires a file path', diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index d18531f1..884cfaac 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -104,8 +104,9 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { const a = tail[i]!; if (a === "--params") { const next = tail[i + 1]; - if (next === undefined) { + if (next === undefined || next.startsWith("-")) { const bareOnly = + next === undefined && passthrough.length === 0 && (params === undefined || Object.keys(params).length === 0); if (bareOnly) { From a0ef0de994a60b0aff5dd505d6ded4f70b5b65f6 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 10:20:40 +0300 Subject: [PATCH 09/13] fix(cli): strip redundant bare --params from rename rewrite When old/new are already serialized, drop no-op trailing --params tokens so apply argv stays parseable (e.g. before --dry-run). --- src/cli/bootstrap.ts | 2 +- src/cli/rename-alias.test.ts | 21 +++++++++++---------- src/cli/rename-alias.ts | 27 ++++++++++++++++++++++++++- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/cli/bootstrap.ts b/src/cli/bootstrap.ts index 495a1e0e..946b72b8 100644 --- a/src/cli/bootstrap.ts +++ b/src/cli/bootstrap.ts @@ -73,7 +73,7 @@ Affected tests (reverse dep walk → test files to run): Apply (substrate-shaped fix executor; diff-json row contract): codemap apply [--params k=v[,k=v]] [--dry-run] [--yes] [--json] codemap apply --rows -| codemap apply --diff-input - codemap rename [--define-in ] [--in-file ] [--kind ] [apply flags...] # alias → apply rename-preview + codemap rename [--define-in ] [--in-file ] [--kind ] [apply flags...] # or: rename --params old=…,new=… — alias → apply rename-preview (see codemap apply --help for --force, --until-empty, --commit; codemap rename --help) Coverage ingest (Istanbul JSON or LCOV from any test runner): diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 1de0fe10..72566788 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -108,26 +108,28 @@ describe("resolveRenameAlias", () => { ]); }); - it("preserves prior state when trailing bare --params follows values", () => { + it("drops redundant bare --params when old/new are already bound", () => { expect( rewrite(["rename", "--params", "old=foo,new=bar", "--params"]), - ).toEqual([ + ).toEqual(["apply", "rename-preview", "--params", "new=bar,old=foo"]); + expect(rewrite(["rename", "helper", "worker", "--params"])).toEqual([ "apply", "rename-preview", "--params", - "new=bar,old=foo", - "--params", + "new=worker,old=helper", ]); - }); - - it("preserves positionals and apply flags when trailing bare --params", () => { - expect(rewrite(["rename", "helper", "worker", "--params"])).toEqual([ + expect( + rewrite(["rename", "helper", "worker", "--params", "--dry-run"]), + ).toEqual([ "apply", "rename-preview", "--params", "new=worker,old=helper", - "--params", + "--dry-run", ]); + }); + + it("delegates incomplete old/new with bare --params in apply tail", () => { expect(rewrite(["rename", "--dry-run", "--params"])).toEqual([ "apply", "rename-preview", @@ -144,7 +146,6 @@ describe("resolveRenameAlias", () => { "rename-preview", "--params", "new=worker,old=helper", - "--params", "--dry-run", ]); }); diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index 884cfaac..ffae1d62 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -68,6 +68,28 @@ function readFlagOperand( return null; } +/** Drop bare `--params` tokens when recipe params are already serialized. */ +function stripRedundantBareParams(applyTail: string[]): string[] { + const out: string[] = []; + let i = 0; + while (i < applyTail.length) { + const a = applyTail[i]!; + if (a === "--params") { + const next = applyTail[i + 1]; + if (next === undefined || next.startsWith("-")) { + i++; + continue; + } + out.push(a, next); + i += 2; + continue; + } + out.push(a); + i++; + } + return out; +} + function buildApplyArgv( params: RecipeParamValues | undefined, applyTail: string[], @@ -198,7 +220,10 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { ); } - return { kind: "rewrite", argv: buildApplyArgv(params, applyTail) }; + return { + kind: "rewrite", + argv: buildApplyArgv(params, stripRedundantBareParams(applyTail)), + }; } export function printRenameAliasHelp(): void { From 6f70740c4994e99e0b0de9f43d8e4dd4c12341a4 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 10:22:01 +0300 Subject: [PATCH 10/13] fix(cli): reject flag tokens as scoped-flag operands in rename Align --define-in, --in-file, and --kind with --params: a following flag is not a valid operand value. --- src/cli/rename-alias.test.ts | 10 ++++++++++ src/cli/rename-alias.ts | 6 +++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 72566788..4b5c127c 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -168,6 +168,16 @@ describe("resolveRenameAlias", () => { ); }); + it("errors when a scoped flag is followed by another flag", () => { + expect( + renameError(["rename", "a", "b", "--define-in", "--dry-run"]), + ).toContain('"--define-in" requires a file path'); + }); + + it("errors on bare rename with no old/new", () => { + expect(renameError(["rename"])).toContain("requires and "); + }); + it("errors on a single positional", () => { expect(renameError(["rename", "helper"])).toContain( "requires and ", diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index ffae1d62..e350c2eb 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -147,7 +147,7 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { } const defineIn = readFlagOperand("--define-in", a, tail, i); if (defineIn !== null) { - if (defineIn.value === undefined) { + if (defineIn.value === undefined || defineIn.value.startsWith("-")) { return renameError( 'codemap rename: "--define-in" requires a file path.', ); @@ -158,7 +158,7 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { } const inFile = readFlagOperand("--in-file", a, tail, i); if (inFile !== null) { - if (inFile.value === undefined) { + if (inFile.value === undefined || inFile.value.startsWith("-")) { return renameError( 'codemap rename: "--in-file" requires a path prefix.', ); @@ -169,7 +169,7 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { } const kind = readFlagOperand("--kind", a, tail, i); if (kind !== null) { - if (kind.value === undefined) { + if (kind.value === undefined || kind.value.startsWith("-")) { return renameError('codemap rename: "--kind" requires a symbol kind.'); } params = mergeParams(params, { kind: kind.value }); From d427c3a356e2731e01a5c23f4ddd6c565085764a Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Fri, 5 Jun 2026 10:30:54 +0300 Subject: [PATCH 11/13] fix(cli): harden rename alias flag parsing and help guards Treat --params followed by apply flags as bare passthrough, whitelist apply flags in splitPassthrough so symbol names like --help stay positional, and only show rename help when it is the sole argument. --- src/cli/main.ts | 2 +- src/cli/rename-alias.test.ts | 54 ++++++++++++ src/cli/rename-alias.ts | 85 ++++++++++++++++--- .../agent-content/skill/10-recipes-context.md | 2 +- 4 files changed, 131 insertions(+), 12 deletions(-) diff --git a/src/cli/main.ts b/src/cli/main.ts index d5246e66..c5b3925e 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -61,7 +61,7 @@ export async function main(): Promise { if (rest[0] === "rename") { const { printRenameAliasHelp, resolveRenameAlias } = await import("./rename-alias.js"); - if (rest.includes("--help") || rest.includes("-h")) { + if ((rest[1] === "--help" || rest[1] === "-h") && rest.length === 2) { printRenameAliasHelp(); return; } diff --git a/src/cli/rename-alias.test.ts b/src/cli/rename-alias.test.ts index 4b5c127c..afb3830b 100644 --- a/src/cli/rename-alias.test.ts +++ b/src/cli/rename-alias.test.ts @@ -100,6 +100,60 @@ describe("resolveRenameAlias", () => { expect(resolveRenameAlias(["rename", "--help"])).toBeNull(); }); + it("does not treat a symbol named --help as help when it is not first", () => { + expect(rewrite(["rename", "--help", "Bar", "--dry-run"])).toEqual([ + "apply", + "rename-preview", + "--params", + "new=Bar,old=--help", + "--dry-run", + ]); + }); + + it("accepts equals-form --params", () => { + expect( + rewrite(["rename", "--params=old=foo,new=bar", "--dry-run"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=bar,old=foo", + "--dry-run", + ]); + }); + + it("errors when positional old/new conflicts with --params old/new", () => { + expect( + renameError([ + "rename", + "--params", + "old=foo,new=bar", + "helper", + "worker", + ]), + ).toContain("cannot mix --params old=/new= with positional"); + }); + + it("passes equals-form --commit through to apply", () => { + expect( + rewrite(["rename", "a", "b", "--yes", "--commit=chore: rename a→b"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=b,old=a", + "--yes", + "--commit", + "chore: rename a→b", + ]); + }); + + it("rejects empty old/new in --params", () => { + expect(renameError(["rename", "--params", "old=,new=bar"])).toContain( + "must be non-empty", + ); + }); + it("preserves missing --params operand for downstream apply parser", () => { expect(rewrite(["rename", "--params"])).toEqual([ "apply", diff --git a/src/cli/rename-alias.ts b/src/cli/rename-alias.ts index e350c2eb..53087b52 100644 --- a/src/cli/rename-alias.ts +++ b/src/cli/rename-alias.ts @@ -3,6 +3,23 @@ import type { RecipeParamValues } from "../application/recipe-params.js"; const RENAME_RECIPE_ID = "rename-preview"; +const APPLY_BOOLEAN_FLAGS = new Set([ + "--dry-run", + "--yes", + "--force", + "--json", + "--until-empty", +]); + +function isApplyPassthroughFlag(token: string): boolean { + if (APPLY_BOOLEAN_FLAGS.has(token)) return true; + if (token === "--params" || token.startsWith("--params=")) return true; + if (token === "--max-passes" || token.startsWith("--max-passes=")) + return true; + if (token === "--commit" || token.startsWith("--commit=")) return true; + return false; +} + /** Serialize a param map for `codemap apply --params` (stable key order). */ export function formatParamsCli(params: RecipeParamValues): string { return Object.keys(params) @@ -29,18 +46,27 @@ function splitPassthrough(tokens: string[]): { let i = 0; while (i < tokens.length) { const a = tokens[i]!; - if (a === "--max-passes" || a === "--commit") { - const next = tokens[i + 1]; - if (next === undefined) { - applyTail.push(a); - i++; - continue; + const maxPasses = readFlagOperand("--max-passes", a, tokens, i); + if (maxPasses !== null) { + if (maxPasses.value !== undefined) { + applyTail.push("--max-passes", maxPasses.value); + } else { + applyTail.push("--max-passes"); } - applyTail.push(a, next); - i += 2; + i = maxPasses.nextIndex; + continue; + } + const commit = readFlagOperand("--commit", a, tokens, i); + if (commit !== null) { + if (commit.value !== undefined) { + applyTail.push("--commit", commit.value); + } else { + applyTail.push("--commit"); + } + i = commit.nextIndex; continue; } - if (a.startsWith("-")) { + if (isApplyPassthroughFlag(a)) { applyTail.push(a); i++; continue; @@ -114,7 +140,7 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { if (rest[0] !== "rename") return null; const tail = rest.slice(1); - if (tail.includes("--help") || tail.includes("-h")) { + if ((tail[0] === "--help" || tail[0] === "-h") && tail.length === 1) { return null; } @@ -124,6 +150,17 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { while (i < tail.length) { const a = tail[i]!; + if (a.startsWith("--params=")) { + const value = a.slice("--params=".length); + if (value === "" || value.startsWith("-")) { + return renameError( + 'codemap rename: "--params" requires a value (old=…,new=…).', + ); + } + params = mergeParams(params, parseParamsCli(value)); + i++; + continue; + } if (a === "--params") { const next = tail[i + 1]; if (next === undefined || next.startsWith("-")) { @@ -188,7 +225,25 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { ); } + for (const p of positionals) { + if (p.startsWith("old=") || p.startsWith("new=")) { + return renameError( + "codemap rename: use --params old=…,new=… instead of positional key=value tokens.", + ); + } + } + + const paramsHadOldNew = + params !== undefined && + params.old !== undefined && + params.new !== undefined; + if (positionals.length === 2) { + if (paramsHadOldNew) { + return renameError( + "codemap rename: cannot mix --params old=/new= with positional .", + ); + } params = mergeParams(params, { old: positionals[0]!, new: positionals[1]!, @@ -200,6 +255,16 @@ export function resolveRenameAlias(rest: string[]): RenameAliasResult | null { params.old !== undefined && params.new !== undefined; + if (hasOldNew && params !== undefined) { + const oldStr = String(params.old); + const newStr = String(params.new); + if (oldStr === "" || newStr === "") { + return renameError( + "codemap rename: old and new must be non-empty strings.", + ); + } + } + if (positionals.length === 1) { if (hasOldNew) { return renameError( diff --git a/templates/agent-content/skill/10-recipes-context.md b/templates/agent-content/skill/10-recipes-context.md index 672b3b77..5abf8b02 100644 --- a/templates/agent-content/skill/10-recipes-context.md +++ b/templates/agent-content/skill/10-recipes-context.md @@ -2,7 +2,7 @@ Replace placeholders (`'...'`) with your module path, file glob, or symbol name. -**Outcome aliases:** **`codemap dead-code`** · **`deprecated`** · **`boundaries`** · **`hotspots`** · **`coverage-gaps`** — thin wrappers over `query --recipe `. Every `query` flag passes through (`--json`, `--format sarif`, `--ci`, `--summary`, `--changed-since`, `--group-by`, `--params`, `--save-baseline`, `--baseline`). Run **`codemap --help`** for the wrapped recipe id. Capped at 5 to avoid sprawl. +**Outcome aliases:** **`codemap dead-code`** · **`deprecated`** · **`boundaries`** · **`hotspots`** · **`coverage-gaps`** — thin wrappers over `query --recipe `. Every `query` flag passes through (`--json`, `--format sarif`, `--ci`, `--summary`, `--changed-since`, `--group-by`, `--params`, `--save-baseline`, `--baseline`). Run **`codemap --help`** for the wrapped recipe id. Capped at 5 to avoid sprawl. **Write alias (outside the cap):** **`codemap rename`** → **`apply rename-preview`** (homonym-safe via `--define-in` / `define_in` in params). **Suppressions (opt-in):** `// codemap-ignore-next-line ` and `// codemap-ignore-file ` (also `#`, `--`, `