diff --git a/.agents/rules/codemap.md b/.agents/rules/codemap.md index a3c4a70f..dd6c4773 100644 --- a/.agents/rules/codemap.md +++ b/.agents/rules/codemap.md @@ -116,6 +116,7 @@ If the question looks like any of these → use the index: | "Worst-covered exported functions" | `--recipe worst-covered-exports` | | "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | | "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | +| "Which exports has nobody imported?" | `--recipe unimported-exports` | ## When Grep / Read IS appropriate @@ -167,6 +168,7 @@ bun src/index.ts query --json "" | Untested + dead exports | `bun src/index.ts query --json --recipe untested-and-dead` | | Components touching `@deprecated` | `bun src/index.ts query --json --recipe components-touching-deprecated` | | Refactor-risk-ranked files | `bun src/index.ts query --json --recipe refactor-risk-ranking` | +| Exports nobody imports | `bun src/index.ts query --json --recipe unimported-exports` | **Use `DISTINCT`** on dependency and import queries — a file importing multiple specifiers from the same module produces duplicate rows. diff --git a/.agents/skills/codemap/SKILL.md b/.agents/skills/codemap/SKILL.md index 1fa976cb..8e4adb8f 100644 --- a/.agents/skills/codemap/SKILL.md +++ b/.agents/skills/codemap/SKILL.md @@ -34,7 +34,7 @@ After **`bun run build`**, **`node dist/index.mjs query …`** or a linked **`co Replace placeholders (`'...'`) with your module path, file glob, or symbol name. -**CLI shortcuts:** **`bun src/index.ts query --json --recipe `** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe `** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`bun src/index.ts query --print-sql `** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`bun src/index.ts query --help`**. +**CLI shortcuts:** **`bun src/index.ts query --json --recipe `** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe `** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`bun src/index.ts query --print-sql `** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`bun src/index.ts query --help`**. **Output flags** (compose with **`--recipe`** or ad-hoc SQL): diff --git a/.changeset/unimported-exports-recipe.md b/.changeset/unimported-exports-recipe.md new file mode 100644 index 00000000..ce2d6a00 --- /dev/null +++ b/.changeset/unimported-exports-recipe.md @@ -0,0 +1,17 @@ +--- +"@stainless-code/codemap": patch +--- + +feat(recipes): ship `unimported-exports` recipe (research note § 1.2) + +Surfaces exports that have no detectable import. Useful as a starting candidate list for "what's unused?" — explicitly **NOT** a "safe to delete" list. + +V1 limitations documented in the recipe `.md`: + +1. **Re-export chains not followed** — false positives if A re-exports `bar` from B and consumers import `bar` from A. Tracked under research note § 1.2; future recipe with recursive CTE walking `re_export_source` will close the gap. +2. **Unresolved imports ignored** — when `imports.resolved_path IS NULL` (codemap's resolver couldn't resolve a `tsconfig.json` path alias or external package), those rows don't count toward "used" matching. +3. **Default exports skipped** — common framework entry points (Next.js `page.tsx`, Storybook stories, `vite.config.ts`) skipped to reduce noise. Override in project-local recipe if you want to include them. + +Action template `review-for-deletion` (auto_fixable: false) — agents flag for manual verification before deletion. + +Agent rule + skill lockstep updated per `docs/README.md` Rule 10 — both `templates/agents/` and `.agents/` codemap rule + skill gain trigger-pattern row, quick-reference row, and recipe-id list update. diff --git a/templates/agents/rules/codemap.md b/templates/agents/rules/codemap.md index 8c71f0fd..b97aee6d 100644 --- a/templates/agents/rules/codemap.md +++ b/templates/agents/rules/codemap.md @@ -125,6 +125,7 @@ If the question looks like any of these → use the index: | "Worst-covered exported functions" | `--recipe worst-covered-exports` | | "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` | | "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` | +| "Which exports has nobody imported?" | `--recipe unimported-exports` | ## When Grep / Read IS appropriate @@ -176,6 +177,7 @@ codemap query --json "" | Untested + dead exports | `codemap query --json --recipe untested-and-dead` | | Components touching `@deprecated` | `codemap query --json --recipe components-touching-deprecated` | | Refactor-risk-ranked files | `codemap query --json --recipe refactor-risk-ranking` | +| Exports nobody imports | `codemap query --json --recipe unimported-exports` | **Use `DISTINCT`** on dependency and import queries — a file importing multiple specifiers from the same module produces duplicate rows. diff --git a/templates/agents/skills/codemap/SKILL.md b/templates/agents/skills/codemap/SKILL.md index acfd40b3..f2e977dc 100644 --- a/templates/agents/skills/codemap/SKILL.md +++ b/templates/agents/skills/codemap/SKILL.md @@ -34,7 +34,7 @@ Use **`codemap --root /path/to/project`** (or **`CODEMAP_ROOT`**) to index anoth Replace placeholders (`'...'`) with your module path, file glob, or symbol name. -**CLI shortcuts:** **`codemap query --json --recipe `** runs bundled SQL (preferred for agents). **`codemap query --recipe `** without **`--json`** prints a table. **`codemap query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`codemap query --print-sql `** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`codemap query --help`**. +**CLI shortcuts:** **`codemap query --json --recipe `** runs bundled SQL (preferred for agents). **`codemap query --recipe `** without **`--json`** prints a table. **`codemap query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`codemap query --print-sql `** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`codemap query --help`**. **Output flags** (compose with **`--recipe`** or ad-hoc SQL): diff --git a/templates/recipes/unimported-exports.md b/templates/recipes/unimported-exports.md new file mode 100644 index 00000000..574eb0f1 --- /dev/null +++ b/templates/recipes/unimported-exports.md @@ -0,0 +1,27 @@ +--- +actions: + - type: review-for-deletion + auto_fixable: false + description: "Export with no detectable import — candidate for deletion. VERIFY against the v1 caveats below before deleting; codemap's import-resolution doesn't follow re-export chains or `tsconfig.json` path aliases that the resolver can't resolve." +--- + +Exports that have no row in `imports` referencing their file AND name. Surfaces the **direct-use-only** subset of "unused exports" — useful as a starting candidate list, but **NEVER as a "safe to delete" list** without manual verification. + +## V1 limitations (false-positive classes) + +The recipe ships intentionally simple. Three known classes of false positive: + +1. **Re-export chains** — codemap's `exports.re_export_source` column tracks barrel-style `export { foo } from './foo'` re-exports, but this v1 recipe **does not follow the chain**. If `src/index.ts` re-exports `bar` from `src/bar.ts`, and consumers `import { bar } from '~/'` (hitting `src/index.ts`), this recipe falsely flags `bar` in `src/bar.ts` as unimported. Workaround: filter out rows with `re_export_source IS NOT NULL` in a project-local override, OR cross-check against `barrel-files` recipe output. +2. **Unresolved imports** — when `imports.resolved_path IS NULL` (e.g. `tsconfig.json` path aliases codemap's resolver can't resolve, or external-package imports), those rows are ignored. If the unresolved import actually targets the export, it's a false positive. Codemap's resolver covers most TS / JS shapes; this is a corner case for unusual config. +3. **Default exports skipped** — `is_default = 0` filter. Default exports are commonly framework entry points (Next.js `page.tsx`, Storybook stories, `vite.config.ts`) that codemap doesn't model; flagging them produces high false-positive noise. To include them, drop the `AND e.is_default = 0` clause in a project-local override. + +## What's NOT covered (orthogonal recipes) + +- **Re-export chain handling** — wait for a future recipe with recursive CTE walking `re_export_source`. Tracked under research note § 1.2 ("re-export chains need a JOIN through `re_export_source` to avoid false positives"). +- **Component-touching-deprecated** style cross-checks — not applicable here; this recipe is about EXPORTS, not symbol references inside files. + +## Tuning axes for project-local overrides + +- **Strip framework entry-point patterns** — add `AND e.file_path NOT LIKE '%/page.tsx' AND e.file_path NOT LIKE '%/layout.tsx' AND e.file_path NOT LIKE '%.stories.tsx'` to exclude common Next.js / Storybook conventions. +- **Filter to a directory** — add `AND e.file_path LIKE 'src/lib/%'` to scope the audit to a single owner / package. +- **Include re-exports** — drop `AND e.kind != 're-export'` if you want to flag stale re-exports too (e.g. a barrel that re-exports a symbol nobody imports anymore). diff --git a/templates/recipes/unimported-exports.sql b/templates/recipes/unimported-exports.sql new file mode 100644 index 00000000..7beac099 --- /dev/null +++ b/templates/recipes/unimported-exports.sql @@ -0,0 +1,33 @@ +-- Exports never directly imported (V1: resolved-path matching only). +-- An export is "directly used" if any imports row's resolved_path matches its +-- file AND the specifiers JSON contains its name (or "*" for namespace imports). +-- +-- V1 limitations (documented in unimported-exports.md): +-- 1. Re-export chains: if A re-exports `bar` from B, and consumers import `bar` +-- from A, the recipe doesn't follow the chain — false positive on B.bar. +-- Workaround: skip rows with `kind = 're-export'` or hand-check via +-- `re_export_source` column. +-- 2. Unresolved imports (`resolved_path IS NULL`, e.g. tsconfig path aliases +-- that codemap's resolver can't resolve) get IGNORED — false positives if +-- they actually reference an export. +-- 3. Default exports skipped (often framework entry points like Next.js +-- page.tsx, Storybook stories, vite.config.ts). +WITH direct_uses AS ( + SELECT DISTINCT e.id + FROM exports e + JOIN imports i ON i.resolved_path = e.file_path + CROSS JOIN json_each(i.specifiers) j + WHERE j.value = e.name OR j.value = '*' +) +SELECT + e.name, + e.kind, + e.file_path, + e.is_default, + e.re_export_source +FROM exports e +WHERE e.id NOT IN (SELECT id FROM direct_uses) + AND e.is_default = 0 + AND e.kind != 're-export' +ORDER BY e.file_path, e.name +LIMIT 50