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/README.md b/README.md index 0f7b570b..d3b5d56e 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 helper worker --define-in src/path/to/definition.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..bba74ddb 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -160,13 +160,13 @@ 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 ] [--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. **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/glossary.md b/docs/glossary.md index dc457506..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). **`--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 @@ -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/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/docs/roadmap.md b/docs/roadmap.md index 291744b3..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). Shipped [#165](https://github.com/stainless-code/codemap/pull/165). Optional alias still open: `codemap rename`. - **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/docs/testing-coverage.md b/docs/testing-coverage.md index 94588b0f..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`. 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/bootstrap.ts b/src/cli/bootstrap.ts index 995a448b..946b72b8 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 ] [--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): 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..06821018 100644 --- a/src/cli/cmd-apply.test.ts +++ b/src/cli/cmd-apply.test.ts @@ -558,6 +558,68 @@ 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\(\)/, + ); + const consumerB = readFile("src/bench/homonym-consumer-b.ts"); + 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", () => { beforeEach(async () => { mkdirSync(join(projectRoot, "src", "bench"), { recursive: true }); diff --git a/src/cli/cmd-apply.ts b/src/cli/cmd-apply.ts index 4de4dd85..74948d42 100644 --- a/src/cli/cmd-apply.ts +++ b/src/cli/cmd-apply.ts @@ -150,6 +150,9 @@ export function parseApplyRest(rest: string[]): } if (a === "--max-passes") { const next = rest[i + 1]; + if (next !== undefined && next.startsWith("-")) { + continue; + } if (next === undefined || !/^\d+$/.test(next)) { return { kind: "error", @@ -174,8 +177,10 @@ export function parseApplyRest(rest: string[]): message: `codemap apply: "--commit" requires a message string.`, }; } - commitMessage = next; - i++; + if (!next.startsWith("-")) { + commitMessage = next; + i++; + } continue; } if (a === "--params") { diff --git a/src/cli/main.ts b/src/cli/main.ts index 69803e24..c5b3925e 100644 --- a/src/cli/main.ts +++ b/src/cli/main.ts @@ -58,6 +58,24 @@ 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[1] === "--help" || rest[1] === "-h") && rest.length === 2) { + printRenameAliasHelp(); + return; + } + 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") { 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..011a5bcc --- /dev/null +++ b/src/cli/rename-alias.test.ts @@ -0,0 +1,324 @@ +import { describe, expect, it } from "bun:test"; + +import { parseApplyRest } from "./cmd-apply.js"; +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( + rewrite([ + "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( + rewrite([ + "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( + rewrite(["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( + rewrite([ + "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("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(); + }); + + it("returns null when help is requested", () => { + expect(resolveRenameAlias(["rename", "--help"])).toBeNull(); + expect(resolveRenameAlias(["rename", "-h"])).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("passes space-separated --commit through to apply", () => { + expect( + rewrite(["rename", "a", "b", "--commit", "chore: rename a→b", "--yes"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=b,old=a", + "--commit", + "chore: rename a→b", + "--yes", + ]); + }); + + it("does not treat a following flag as --commit operand", () => { + const argv = rewrite(["rename", "a", "b", "--commit", "--dry-run"]); + expect(argv).toEqual([ + "apply", + "rename-preview", + "--params", + "new=b,old=a", + "--commit", + "--dry-run", + ]); + const parsed = parseApplyRest(argv!); + expect(parsed.kind).toBe("run"); + if (parsed.kind === "run") { + expect(parsed.dryRun).toBe(true); + expect(parsed.commitMessage).toBeUndefined(); + } + }); + + 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", + "rename-preview", + "--params", + ]); + }); + + it("drops redundant bare --params when old/new are already bound", () => { + expect( + rewrite(["rename", "--params", "old=foo,new=bar", "--params"]), + ).toEqual(["apply", "rename-preview", "--params", "new=bar,old=foo"]); + expect(rewrite(["rename", "helper", "worker", "--params"])).toEqual([ + "apply", + "rename-preview", + "--params", + "new=worker,old=helper", + ]); + expect( + rewrite(["rename", "helper", "worker", "--params", "--dry-run"]), + ).toEqual([ + "apply", + "rename-preview", + "--params", + "new=worker,old=helper", + "--dry-run", + ]); + }); + + it("delegates incomplete old/new with bare --params in apply tail", () => { + expect(rewrite(["rename", "--dry-run", "--params"])).toEqual([ + "apply", + "rename-preview", + "--dry-run", + "--params", + ]); + }); + + 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", + "--dry-run", + ]); + }); + + 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 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 ", + ); + }); + + it("errors on a third positional", () => { + expect(renameError(["rename", "a", "b", "c"])).toMatch( + /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 "); + }); + + 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", () => { + 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..4a84c296 --- /dev/null +++ b/src/cli/rename-alias.ts @@ -0,0 +1,321 @@ +import { mergeParams, parseParamsCli } from "../application/recipe-params.js"; +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) + .sort() + .map((key) => `${key}=${String(params[key])}`) + .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]!; + const maxPasses = readFlagOperand("--max-passes", a, tokens, i); + if (maxPasses !== null) { + const operand = + maxPasses.value !== undefined && !maxPasses.value.startsWith("-") + ? maxPasses.value + : undefined; + if (operand !== undefined) { + applyTail.push("--max-passes", operand); + i = maxPasses.nextIndex; + } else { + applyTail.push("--max-passes"); + i++; + } + continue; + } + const commit = readFlagOperand("--commit", a, tokens, i); + if (commit !== null) { + const operand = + commit.value !== undefined && !commit.value.startsWith("-") + ? commit.value + : undefined; + if (operand !== undefined) { + applyTail.push("--commit", operand); + i = commit.nextIndex; + } else { + applyTail.push("--commit"); + i++; + } + continue; + } + if (isApplyPassthroughFlag(a)) { + applyTail.push(a); + i++; + continue; + } + positionals.push(a); + i++; + } + 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; +} + +/** 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[], +): 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[]): RenameAliasResult | null { + if (rest[0] !== "rename") return null; + + const tail = rest.slice(1); + if ((tail[0] === "--help" || tail[0] === "-h") && tail.length === 1) { + return null; + } + + let params: RecipeParamValues | undefined; + const passthrough: string[] = []; + let i = 0; + + 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("-")) { + const bareOnly = + next === undefined && + 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; + continue; + } + const defineIn = readFlagOperand("--define-in", a, tail, i); + if (defineIn !== null) { + if (defineIn.value === undefined || defineIn.value.startsWith("-")) { + return renameError( + 'codemap rename: "--define-in" requires a file path.', + ); + } + params = mergeParams(params, { define_in: defineIn.value }); + i = defineIn.nextIndex; + continue; + } + const inFile = readFlagOperand("--in-file", a, tail, i); + if (inFile !== null) { + if (inFile.value === undefined || inFile.value.startsWith("-")) { + return renameError( + 'codemap rename: "--in-file" requires a path prefix.', + ); + } + params = mergeParams(params, { in_file: inFile.value }); + i = inFile.nextIndex; + continue; + } + const kind = readFlagOperand("--kind", a, tail, i); + if (kind !== null) { + if (kind.value === undefined || kind.value.startsWith("-")) { + return renameError('codemap rename: "--kind" requires a symbol kind.'); + } + params = mergeParams(params, { kind: kind.value }); + i = kind.nextIndex; + continue; + } + passthrough.push(a); + i++; + } + + const { positionals, applyTail } = splitPassthrough(passthrough); + + if (positionals.length > 2) { + return renameError( + `codemap rename: unexpected argument "${positionals[2]}".`, + ); + } + + 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]!, + }); + } + + const hasOldNew = + params !== undefined && + 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( + `codemap rename: unexpected argument "${positionals[0]}".`, + ); + } + return renameError( + "codemap rename: requires and (or pass old=/new= via --params).", + ); + } + + 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).", + ); + } + + return { + kind: "rewrite", + argv: buildApplyArgv(params, stripRedundantBareParams(applyTail)), + }; +} + +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\` +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 "". + +Examples: + codemap rename usePermissions useAccess --kind function --dry-run + codemap rename helper worker --define-in src/lib/helper-module.ts --yes + codemap rename --params old=foo,new=bar,define_in=src/a.ts --dry-run + +Run \`codemap apply --help\` for executor details.`); +} diff --git a/templates/agent-content/rule/00-full.md b/templates/agent-content/rule/00-full.md index db87931f..785b3239 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 ] [--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 diff --git a/templates/agent-content/skill/10-recipes-context.md b/templates/agent-content/skill/10-recipes-context.md index 653fbb55..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 `#`, `--`, `