Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload

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

**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 (`<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)).

Expand Down
54 changes: 54 additions & 0 deletions docs/plans/scoped-rename-define-in.md
Original file line number Diff line number Diff line change
@@ -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 <old> <new> [--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).
2 changes: 1 addition & 1 deletion docs/roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)**

Expand Down
2 changes: 1 addition & 1 deletion docs/testing-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Every `templates/recipes/<id>.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`.

---

Expand Down
3 changes: 2 additions & 1 deletion src/cli/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ Affected tests (reverse dep walk → test files to run):
Apply (substrate-shaped fix executor; diff-json row contract):
codemap apply <recipe-id> [--params k=v[,k=v]] [--dry-run] [--yes] [--json]
codemap apply --rows -|<file.json> codemap apply --diff-input <file>
(see codemap apply --help for --force, --until-empty, --commit)
codemap rename <old> <new> [--define-in <file_path>] [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 <path> [--json] # path = file or dir; format auto-detected
Expand Down
51 changes: 51 additions & 0 deletions src/cli/cmd-apply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
11 changes: 11 additions & 0 deletions src/cli/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ export async function main(): Promise<void> {
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 <ids>] [--link-mode symlink|copy] [--git-hooks] [--no-git-hooks]
Expand Down
95 changes: 95 additions & 0 deletions src/cli/rename-alias.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
124 changes: 124 additions & 0 deletions src/cli/rename-alias.ts
Original file line number Diff line number Diff line change
@@ -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", ""];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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 <old> <new> [--define-in <file_path>] [--in-file <prefix>] [--kind <k>] [apply flags...]
codemap rename --params old=<old>,new=<new>[,define_in=<file_path>] [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 "<msg>".

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.`);
}
Loading