Skip to content

Commit e1480ee

Browse files
committed
feat(recipes): add stale-imports diff-shape; reject organize-imports in plan
Sole-specifier unused import lines via import_specifiers × references; review-first apply (auto_fixable false). Golden on ProductCard unused now import. Defer organize-imports to formatter domain in utilization plan.
1 parent 5ac85ec commit e1480ee

9 files changed

Lines changed: 176 additions & 26 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ codemap apply replace-marker-kind --params old_kind=TODO,new_kind=FIXME --yes --
221221
# External unified diff: codemap apply --diff-input /tmp/patch.diff --dry-run
222222
# Fixpoint (recipe mode): codemap apply rename-preview --params old=a,new=b --until-empty --yes
223223
# Optional git commit after clean apply: codemap apply ... --yes --commit "chore: rename a→b"
224-
# Bundled diff-shape recipes: rename-preview, migrate-import-source, replace-marker-kind, add-jsdoc-deprecated
224+
# Bundled diff-shape recipes: rename-preview, migrate-import-source, replace-marker-kind, stale-imports, add-jsdoc-deprecated
225225
# All-or-nothing: any conflict aborts before any file is written. MCP: apply + apply_rows (yes: true for writes).
226226

227227
# Live agent content (pointer protocol — full body served from installed package version)

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,7 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload
154154

155155
**Recipe-only CLI flags** (not on MCP/HTTP today): `--until-empty` + `--max-passes N` (default 10) run `runApplyUntilEmpty` — dry-run probe → apply → targeted reindex of touched files → repeat until zero rows, conflicts, or cap; envelope adds `passes` and `terminated_by``{empty, cap, conflicts, complete}`. `--commit "<msg>"` runs `git add -- <touched>` + `git commit` after a clean apply (stderr error string on git failure; apply still succeeded).
156156

157-
**Bundled diff-shape recipes** (emit the row contract; inspect with `codemap query --recipe <id> --format diff-json`): `rename-preview`, `migrate-import-source`, `replace-marker-kind` (`auto_fixable: true` in frontmatter); `add-jsdoc-deprecated` (`auto_fixable: false` — writes need `--force` unless allowlisted). Wave-2 candidates (`organize-imports`, `stale-imports`, …) tracked in [`plans/substrate-apply-utilization.md`](./plans/substrate-apply-utilization.md).
157+
**Bundled diff-shape recipes** (emit the row contract; inspect with `codemap query --recipe <id> --format diff-json`): `rename-preview`, `migrate-import-source`, `replace-marker-kind`, `stale-imports` (first three `auto_fixable: true` except `stale-imports` — review-first); `add-jsdoc-deprecated` (`auto_fixable: false` — writes need `--force` unless allowlisted). Wave-2 remainder (`migrate-deprecated`, …) in [`plans/substrate-apply-utilization.md`](./plans/substrate-apply-utilization.md).
158158

159159
**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.
160160

docs/plans/substrate-apply-utilization.md

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
| ------------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
1111
| **Substrate (tiers 1–6)** | **Partially** | Rich tables exist (`references`, `bindings`, `jsx_*`, `import_specifiers`, `jsdoc_tags`, …) but **only a thin slice** is JOIN'd by shipped diff-shape recipes. Tiers **7–13** are intentionally unshipped — not a utilization failure. |
1212
| **Apply executor** | **Mostly yes** | Phase-1/2 engine, three CLI input modes, MCP `apply` + `apply_rows`, policy gates, fixpoint loop — **shipped and engine-heavy in tests**. |
13-
| **Apply recipes** | **No** | **4 of 62** bundled SQL recipes emit the diff row contract; **3** are `auto_fixable: true`. The product bottleneck is **recipe surface**, not executor plumbing. |
13+
| **Apply recipes** | **Partially** | **5 of 62** bundled SQL recipes emit the diff row contract; **3** `auto_fixable: true`, **`stale-imports`** review-first. Bottleneck remains wave-2 **semantic** recipes (`migrate-deprecated`, …). |
1414
| **Agent/consumer docs** | **Mostly yes** | Architecture + glossary + README apply subsection + skill/MCP apply workflow. Wave-2 recipe catalog still thin until Phase C. |
1515
| **Verification** | **Mostly yes** | CLI E2E: three input modes (recipe, `--rows`, recipes on disk); `rename-preview-product-card` golden (barrel + re-export). **Open:** `--diff-input` CLI e2e. |
1616

17-
**Conclusion:** Rename substrate utilization improved on `rename-preview`. **Next:** wave-2 diff-shape recipes (C.2–C.4) — still the highest user-visible ROI.
17+
**Conclusion:** Five diff-shape recipes shipped (`rename-preview` extended, `stale-imports`). **Next:** `migrate-deprecated` (C.4) — pairs with `deprecated-symbols` audit → apply.
1818

1919
---
2020

@@ -69,7 +69,7 @@ Canonical apply homes: [`architecture.md § Apply`](../architecture.md#apply--in
6969
| `markers` | Yes | `replace-marker-kind` | `markers-by-kind` |
7070
| `references` / `bindings` / `scopes` | Yes | `rename-preview` (`reference_rows`; non-import/call/definition) | `find-symbol-references`, `find-re-exported-bindings` |
7171
| `jsx_elements` / `jsx_attributes` | Yes | **No** | `find-jsx-usages` |
72-
| `import_specifiers` (child table) | Yes | **No** (stale-imports backlog) ||
72+
| `import_specifiers` (child table) | Yes | **`stale-imports`** (sole-specifier lines) ||
7373
| `jsdoc_tags` | Yes (partial tier 4/5) | **No** (`add-jsdoc` is text template, not tag-aware) | `find-throws-jsdoc` |
7474
| Tiers **7–13** | Open | N/A | Future recipes |
7575

@@ -98,12 +98,12 @@ Canonical apply homes: [`architecture.md § Apply`](../architecture.md#apply--in
9898

9999
## Open work (gap matrix)
100100

101-
| Gap | Type | Track here |
102-
| ---------------------------------------------------------------------------------------- | -------------- | -------------------- |
103-
| Wave-2 diff-shape recipes (`organize-imports`, `stale-imports`, `migrate-deprecated`, …) | Implementation | § Phase C |
104-
| `--diff-input` CLI e2e | Testing | § Phase B (optional) |
105-
| MCP `until_empty` / `--commit` | Deferred | § Phase D |
106-
| Substrate tiers 7–13 | Separate plan | substrate-extraction |
101+
| Gap | Type | Track here |
102+
| ---------------------------------------------------------------------------------- | -------------- | -------------------- |
103+
| Wave-2 diff-shape recipes (`migrate-deprecated`, multi-specifier stale-imports, …) | Implementation | § Phase C |
104+
| `--diff-input` CLI e2e | Testing | § Phase B (optional) |
105+
| MCP `until_empty` / `--commit` | Deferred | § Phase D |
106+
| Substrate tiers 7–13 | Separate plan | substrate-extraction |
107107

108108
---
109109

@@ -125,13 +125,13 @@ Canonical apply homes: [`architecture.md § Apply`](../architecture.md#apply--in
125125
| Priority | Recipe | Status |
126126
| -------- | ------------------------------------------------ | --------------------------------------------------------------------------- |
127127
| C.1 | Fix **`replace-marker-kind`** | **✓ Shipped** |
128-
| C.2 | **`organize-imports`** | **Open** |
129-
| C.3 | **`stale-imports`** | **Open** |
128+
| C.2 | **`organize-imports`** | **Rejected** — formatter/ESLint domain; see § Rejected |
129+
| C.3 | **`stale-imports`** | **✓ Shipped** — sole-specifier line delete (v1) |
130130
| C.4 | **`migrate-deprecated` / `deprecated-usages`** | **Open** |
131131
| C.5 | **`rename-app-wide` or barrel import rows** | **✓ Shipped**`barrel_import_rows` + `reference_rows` on `rename-preview` |
132132
| C.6 | `actions[].command` on audit recipes w/ diff SQL | **Open** |
133133

134-
**Phase C exit criteria:** ≥2 new diff-shape recipes beyond the original four; `re_export` golden or documented gap removed.
134+
**Phase C exit criteria (partial):** `stale-imports` + rename extensions shipped; **`migrate-deprecated`** still open for “≥2 beyond original four” if counting only new ids.
135135

136136
### Phase D — Optional transport parity (trigger-gated)
137137

@@ -160,18 +160,19 @@ The "no fix engine" floor was about **product class** (no ESLint-style verdict e
160160

161161
## Rejected items (grep-able; revisit only on trigger)
162162

163-
| Item | Why rejected | Revisit when |
164-
| ------------------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------- |
165-
| Curated CLI write verbs (`codemap rename`, …) | Moat A — premature verb sprawl | ≥3 diff-shape recipes + clear agent-host demand beyond `actions[].command` |
166-
| Parallel AST apply engine (Path A) | Competes with ts-morph/jscodeshift; maintenance burden | ≥2 external teams hit substring wall + concrete AST-shape demand |
167-
| Trust tiers (`safe`/`review`/`risky`) | Taxonomy debt; `auto_fixable` + allowlist suffice | Allowlist insufficient + ≥2 consumers ship trust filters in CI |
168-
| Per-row confidence scores | No consensus on computation | Recipe needs per-site ranking when `before_pattern` is ambiguous |
169-
| Verifier as product surface (typecheck/lint gate) | Consumer CI owns orchestration | Consumer plan with concrete examples |
170-
| Reliability loop telemetry | No upload floor | Self-hosted observability request |
171-
| `--branch` / `--output-patch` flags | `--commit` priority | `--commit` insufficient in practice |
172-
| Multi-line kind-tagged row contract | After single-line path stable | Multi-line edits required and workarounds fail |
173-
| Cross-file moves in one apply | Higher risk | Alternative two-step ops insufficient |
174-
| Cross-file atomic apply (50+ files) | Per-file atomicity sufficient today | Real 50+ file apply + partial failure leak |
163+
| Item | Why rejected | Revisit when |
164+
| ------------------------------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------ |
165+
| Curated CLI write verbs (`codemap rename`, …) | Moat A — premature verb sprawl | ≥3 diff-shape recipes + clear agent-host demand beyond `actions[].command` |
166+
| Parallel AST apply engine (Path A) | Competes with ts-morph/jscodeshift; maintenance burden | ≥2 external teams hit substring wall + concrete AST-shape demand |
167+
| Trust tiers (`safe`/`review`/`risky`) | Taxonomy debt; `auto_fixable` + allowlist suffice | Allowlist insufficient + ≥2 consumers ship trust filters in CI |
168+
| Per-row confidence scores | No consensus on computation | Recipe needs per-site ranking when `before_pattern` is ambiguous |
169+
| Verifier as product surface (typecheck/lint gate) | Consumer CI owns orchestration | Consumer plan with concrete examples |
170+
| Reliability loop telemetry | No upload floor | Self-hosted observability request |
171+
| `--branch` / `--output-patch` flags | `--commit` priority | `--commit` insufficient in practice |
172+
| Multi-line kind-tagged row contract | After single-line path stable | Multi-line edits required and workarounds fail |
173+
| Cross-file moves in one apply | Higher risk | Alternative two-step ops insufficient |
174+
| Cross-file atomic apply (50+ files) | Per-file atomicity sufficient today | Real 50+ file apply + partial failure leak |
175+
| **`organize-imports`** (sort/group import lines) | Prettier / Biome / ESLint `import/order` territory | Index-driven import order + substring apply proves insufficient vs `actions[].command` pointing at formatter |
175176

176177
Full trigger wording: [`research/codemap-richer-index-synthesis-2026-05.md` § 7](../research/codemap-richer-index-synthesis-2026-05.md#7-rejected-items-with-trigger-conditions).
177178

docs/testing-coverage.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Every `templates/recipes/<id>.sql` has **≥1** scenario in `fixtures/golden/sce
3939
| `migrate-import-source` | `migrate-import-source` | dry-run |
4040
| `replace-marker-kind` | `replace-marker-kind` | `--yes` disk apply (temp project) |
4141
| `add-jsdoc-deprecated` | `add-jsdoc-deprecated` | — (query golden only; writes need `--force`) |
42+
| `stale-imports` | `stale-imports` | dry-run + `--force --yes` disk apply |
4243

4344
**Input modes:** recipe id (above); `--rows` JSON file (e2e); `--diff-input` (`apply-diff-input.test.ts` unit). MCP `apply_rows` shares the rows path — no separate e2e yet.
4445

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[
2+
{
3+
"file_path": "src/components/shop/ProductCard.tsx",
4+
"line_start": 2,
5+
"line_end": 2,
6+
"before_pattern": "import { now } from \"../../utils/date\"",
7+
"after_pattern": "",
8+
"location_kind": "import_line",
9+
"chain_depth": 0
10+
}
11+
]

fixtures/golden/scenarios.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,11 @@
744744
"in_file": "src/notes"
745745
}
746746
},
747+
{
748+
"id": "stale-imports",
749+
"prompt": "Unused sole-specifier import lines (import_specifiers vs references)",
750+
"recipe": "stale-imports"
751+
},
747752
{
748753
"id": "migrate-import-source",
749754
"prompt": "Rewrite consumer import source path",

src/cli/cmd-apply.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,42 @@ describe("codemap apply <recipe-id> — CLI integration", () => {
330330
});
331331
});
332332

333+
describe("stale-imports", () => {
334+
beforeEach(async () => {
335+
writeFileSync(
336+
join(projectRoot, "src", "widget.ts"),
337+
'import { unusedBinding } from "./helper";\n\nexport const widget = 1;\n',
338+
"utf8",
339+
);
340+
const idx = await runCli(["--full"], { CODEMAP_ROOT: projectRoot });
341+
expect(idx.exitCode).toBe(0);
342+
});
343+
344+
it("dry-run finds the sole-specifier import line", async () => {
345+
const r = await runCli(
346+
["apply", "stale-imports", "--dry-run", "--json"],
347+
{ CODEMAP_ROOT: projectRoot },
348+
);
349+
expect(r.exitCode).toBe(0);
350+
const env = JSON.parse(r.out);
351+
expect(env.summary.rows).toBe(1);
352+
expect(readFile("src/widget.ts")).toContain("unusedBinding");
353+
});
354+
355+
it("removes the unused import line with --force --yes", async () => {
356+
const r = await runCli(
357+
["apply", "stale-imports", "--force", "--yes", "--json"],
358+
{ CODEMAP_ROOT: projectRoot },
359+
);
360+
expect(r.exitCode).toBe(0);
361+
const env = JSON.parse(r.out);
362+
expect(env.applied).toBe(true);
363+
const body = readFile("src/widget.ts");
364+
expect(body).not.toContain("unusedBinding");
365+
expect(body).toContain("export const widget");
366+
});
367+
});
368+
333369
describe("migrate-import-source", () => {
334370
beforeEach(async () => {
335371
mkdirSync(join(projectRoot, "src", "api"), { recursive: true });

0 commit comments

Comments
 (0)