Skip to content

Commit 4bfb067

Browse files
feat(cli): codemap rename alias for homonym-safe renames (#166)
* feat(cli): add codemap rename alias for homonym-safe renames `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. * 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 {}. * 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. * 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. * 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. * test(cli): cover stray positional after rename --params * 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. * 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. * 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). * 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. * 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. * fix(cli): reject flag tokens as --commit/--max-passes operands Rename alias and apply parser no longer pair value-taking apply flags with a following flag, so `rename a b --commit --dry-run` preserves dry-run semantics end-to-end. * docs(cli): use generic paths in rename alias examples Replace dogfood bench fixture paths in README and rename --help with consumer-facing placeholder paths.
1 parent 2e1e5e0 commit 4bfb067

15 files changed

Lines changed: 808 additions & 74 deletions

.changeset/rename-alias-cli.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
Add `codemap rename` CLI alias for homonym-safe renames via `apply rename-preview` (`--define-in`, `--in-file`, `--kind`).

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ codemap affected --changed-since origin/main --json # committed de
216216
codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --dry-run
217217
codemap apply rename-preview --params old=usePermissions,new=useAccess,kind=function --yes # TTY prompts without --yes
218218
# Homonym-safe: add define_in=src/path/to/definition.ts (scopes target; in_file only filters output rows)
219+
# Alias: codemap rename helper worker --define-in src/path/to/definition.ts --dry-run
219220
codemap apply migrate-import-source --params old_source=legacy,new_source=@app/core --dry-run
220221
codemap apply stale-imports --params in_file=src/widget --dry-run # preview; writes need --force --yes
221222
codemap apply migrate-jsx-prop --params old_name=data-id,new_name=data-testid,component_name=ProductCard --dry-run --force

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,13 +160,13 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload
160160

161161
**Bundled diff-shape recipes** (emit the row contract; inspect with `codemap query --recipe <id> --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).
162162

163-
**Homonym-safe rename:** optional `define_in=<definition file_path>` 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.
163+
**Homonym-safe rename:** optional `define_in=<definition file_path>` 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 <old> <new> [--define-in <file_path>] [--in-file <prefix>] [--kind <k>]``apply rename-preview` (thin alias — same recipe + policy gates; see `codemap rename --help`).
164164

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

167167
**Discover → preview → apply** (agent loop): `query_recipe` / `query --recipe <id> --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`).
168168

169-
**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, …).
169+
**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, …).
170170

171171
**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 (`<name>` or **`--query '<field:value …>'`** + **`--with-fts`** + `--kind` + `--in <path>` + `--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 <path>`** 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)).
172172

docs/glossary.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ Third apply transport for **caller-supplied unified diff text** — parses git-s
4141

4242
### `codemap apply` / apply tool
4343

44-
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 <recipe-id> [--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 -|<file.json>`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input <unified-diff>`; 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 `<id>.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=<symbols.file_path>` 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).
44+
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 <recipe-id> [--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 -|<file.json>`; MCP/HTTP [`apply_rows`](#apply_rows-mcp-tool--cli-mode). (3) **diff** — `codemap apply --diff-input <unified-diff>`; 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 `<id>.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=<symbols.file_path>` 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 <old> <new> [--define-in <file_path>] [--in-file <prefix>] [--kind <k>]` → `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).
4545

4646
### audit
4747

@@ -393,7 +393,7 @@ Key-value metadata table. Holds `schema_version`, `last_indexed_commit`, `indexe
393393

394394
### outcome aliases (`dead-code` / `deprecated` / `boundaries` / `hotspots` / `coverage-gaps`)
395395

396-
Top-level CLI verbs that thin-wrap `query --recipe <id>`: `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.
396+
Top-level CLI verbs that thin-wrap `query --recipe <id>`: `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).
397397

398398
### oxc-parser
399399

0 commit comments

Comments
 (0)