From 332063a48d6590b85d15f7e7a55d2fe57fb32638 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Mon, 4 May 2026 15:50:37 +0300 Subject: [PATCH] feat(recipes): ship components-touching-deprecated + refactor-risk-ranking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two new bundled recipes from research note § 1 — both XS effort, both ship against existing substrate (no schema bump, no new infra). § 1.1 components-touching-deprecated - UNION of hook path (components.hooks_used JSON overlap) + call path (calls.caller_name = component, callee_name = @deprecated symbol). - Hook-only variant would ship false-negatives — recipe spells out the explicit UNION per research note § 1.1 grill. - Output: {component, component_file, deprecated_symbol, deprecated_file, via} where via ∈ {'hook', 'call'}. - Action template: review-deprecation-impact. § 1.4 refactor-risk-ranking - Formula: (fan_in + 1) × (100 - avg_coverage_pct), per-file. - Three correctness fixes over the naïve "fan_in × (100 - coverage_pct)": - Orphans (fan_in=0) score on coverage alone via +1 - NULL coverage treated as 0% via COALESCE (otherwise row drops) - Files with no exports excluded (no public-API surface to refactor) - Empirical divergence from research note § 1.4: spec said per-symbol; testing on codemap's own index produced 30 rows from src/db.ts all tied at the same score (file-level fan_in inherited). Ships per-file ranking; per-symbol via calls is one of the documented tuning axes in the .md. - v1 trade-off: linear-in-fan_in. Tune via project-local recipe override (suggested axes spelled out in .md: log-scale, visibility weight, LOC weight, per-symbol via calls). - Action template: review-refactor-impact. Verification: - bun src/index.ts query --recipe components-touching-deprecated → [] on codemap's own index (no @deprecated symbols hit components; expected). SQL valid. - bun src/index.ts query --recipe refactor-risk-ranking → 30 distinct files ranked by score, src/db.ts top with risk_score=4500 (44 fan_in × 100 - 0% coverage). Sensible output. - bun run check passes (format, lint, typecheck, tests, all 23 golden queries). Rule 10 lockstep updates: - templates/agents/rules/codemap.md — added trigger-pattern + quick- reference table rows for both recipes. - templates/agents/skills/codemap/SKILL.md — recipe-id list extended. - .agents/rules/codemap.md + .agents/skills/codemap/SKILL.md — same mirrored updates per agents-first-convention. Patch changeset: pre-v1 lesson — additive bundled recipes don't break existing .codemap/index.db; no schema change. Patch suffices. Files: 5 new (2 .sql + 2 .md + 1 changeset), 4 lockstep edits. --- .agents/rules/codemap.md | 60 ++++++++++--------- .agents/skills/codemap/SKILL.md | 2 +- .changeset/recipes-from-research-note.md | 12 ++++ templates/agents/rules/codemap.md | 60 ++++++++++--------- templates/agents/skills/codemap/SKILL.md | 2 +- .../recipes/components-touching-deprecated.md | 17 ++++++ .../components-touching-deprecated.sql | 27 +++++++++ templates/recipes/refactor-risk-ranking.md | 31 ++++++++++ templates/recipes/refactor-risk-ranking.sql | 43 +++++++++++++ 9 files changed, 196 insertions(+), 58 deletions(-) create mode 100644 .changeset/recipes-from-research-note.md create mode 100644 templates/recipes/components-touching-deprecated.md create mode 100644 templates/recipes/components-touching-deprecated.sql create mode 100644 templates/recipes/refactor-risk-ranking.md create mode 100644 templates/recipes/refactor-risk-ranking.sql diff --git a/.agents/rules/codemap.md b/.agents/rules/codemap.md index 34970af..a3c4a70 100644 --- a/.agents/rules/codemap.md +++ b/.agents/rules/codemap.md @@ -114,6 +114,8 @@ If the question looks like any of these → use the index: | "What's structurally dead AND untested?" | `--recipe untested-and-dead` | | "Rank files by test coverage" | `--recipe files-by-coverage` | | "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` | ## When Grep / Read IS appropriate @@ -135,34 +137,36 @@ bun src/index.ts query --json "" ## Quick reference queries -| I need to... | Query | -| ------------------------- | ------------------------------------------------------------------------------------------------------------ | -| Find a symbol | `SELECT name, kind, file_path, line_start, line_end, signature FROM symbols WHERE name = '...'` | -| Find a symbol (fuzzy) | `SELECT name, kind, file_path, line_start FROM symbols WHERE name LIKE '%...%'` | -| See file exports | `SELECT name, kind, is_default FROM exports WHERE file_path LIKE '%...'` | -| See file imports | `SELECT source, specifiers, is_type_only FROM imports WHERE file_path LIKE '%...'` | -| Who imports this module? | `SELECT DISTINCT file_path FROM imports WHERE source LIKE '~/some/module%'` | -| Who imports this file? | `SELECT DISTINCT from_path FROM dependencies WHERE to_path LIKE '%...'` | -| What does this depend on? | `SELECT DISTINCT to_path FROM dependencies WHERE from_path LIKE '%...'` | -| Component info | `SELECT name, props_type, hooks_used FROM components WHERE name = '...'` | -| All components | `SELECT name, file_path, props_type, hooks_used FROM components ORDER BY name` | -| TODOs in a file | `SELECT line_number, content FROM markers WHERE file_path LIKE '%...' AND kind = 'TODO'` | -| Most complex files | `SELECT from_path, COUNT(*) as deps FROM dependencies GROUP BY from_path ORDER BY deps DESC LIMIT 10` | -| CSS design tokens | `SELECT name, value, scope FROM css_variables WHERE name LIKE '--%...'` | -| CSS module classes | `SELECT name, file_path FROM css_classes WHERE is_module = 1` | -| CSS keyframes | `SELECT name, file_path FROM css_keyframes` | -| Type/interface shape | `SELECT name, type, is_optional, is_readonly FROM type_members WHERE symbol_name = '...'` | -| Deprecated symbols | `SELECT name, kind, file_path, doc_comment FROM symbols WHERE doc_comment LIKE '%@deprecated%'` | -| Visibility-tagged symbols | `SELECT name, kind, visibility, file_path FROM symbols WHERE visibility IS NOT NULL` (or `= 'beta'`, etc.) | -| Symbol docs | `SELECT name, signature, doc_comment FROM symbols WHERE name = '...' AND doc_comment IS NOT NULL` | -| Const values | `SELECT name, value, file_path FROM symbols WHERE kind = 'const' AND value IS NOT NULL AND name LIKE '%...'` | -| Class members | `SELECT name, kind, signature FROM symbols WHERE parent_name = '...'` | -| Top-level only | `SELECT name, kind, signature FROM symbols WHERE parent_name IS NULL AND file_path LIKE '%...'` | -| Who calls X? | `SELECT DISTINCT caller_name, file_path FROM calls WHERE callee_name = '...'` | -| What does X call? | `SELECT DISTINCT callee_name FROM calls WHERE caller_name = '...'` | -| Call hotspots | `SELECT callee_name, COUNT(*) as fan_in FROM calls GROUP BY callee_name ORDER BY fan_in DESC LIMIT 10` | -| Symbol coverage | `SELECT name, hit_statements, total_statements, coverage_pct FROM coverage WHERE file_path = '...'` | -| Untested + dead exports | `bun src/index.ts query --json --recipe untested-and-dead` | +| I need to... | Query | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Find a symbol | `SELECT name, kind, file_path, line_start, line_end, signature FROM symbols WHERE name = '...'` | +| Find a symbol (fuzzy) | `SELECT name, kind, file_path, line_start FROM symbols WHERE name LIKE '%...%'` | +| See file exports | `SELECT name, kind, is_default FROM exports WHERE file_path LIKE '%...'` | +| See file imports | `SELECT source, specifiers, is_type_only FROM imports WHERE file_path LIKE '%...'` | +| Who imports this module? | `SELECT DISTINCT file_path FROM imports WHERE source LIKE '~/some/module%'` | +| Who imports this file? | `SELECT DISTINCT from_path FROM dependencies WHERE to_path LIKE '%...'` | +| What does this depend on? | `SELECT DISTINCT to_path FROM dependencies WHERE from_path LIKE '%...'` | +| Component info | `SELECT name, props_type, hooks_used FROM components WHERE name = '...'` | +| All components | `SELECT name, file_path, props_type, hooks_used FROM components ORDER BY name` | +| TODOs in a file | `SELECT line_number, content FROM markers WHERE file_path LIKE '%...' AND kind = 'TODO'` | +| Most complex files | `SELECT from_path, COUNT(*) as deps FROM dependencies GROUP BY from_path ORDER BY deps DESC LIMIT 10` | +| CSS design tokens | `SELECT name, value, scope FROM css_variables WHERE name LIKE '--%...'` | +| CSS module classes | `SELECT name, file_path FROM css_classes WHERE is_module = 1` | +| CSS keyframes | `SELECT name, file_path FROM css_keyframes` | +| Type/interface shape | `SELECT name, type, is_optional, is_readonly FROM type_members WHERE symbol_name = '...'` | +| Deprecated symbols | `SELECT name, kind, file_path, doc_comment FROM symbols WHERE doc_comment LIKE '%@deprecated%'` | +| Visibility-tagged symbols | `SELECT name, kind, visibility, file_path FROM symbols WHERE visibility IS NOT NULL` (or `= 'beta'`, etc.) | +| Symbol docs | `SELECT name, signature, doc_comment FROM symbols WHERE name = '...' AND doc_comment IS NOT NULL` | +| Const values | `SELECT name, value, file_path FROM symbols WHERE kind = 'const' AND value IS NOT NULL AND name LIKE '%...'` | +| Class members | `SELECT name, kind, signature FROM symbols WHERE parent_name = '...'` | +| Top-level only | `SELECT name, kind, signature FROM symbols WHERE parent_name IS NULL AND file_path LIKE '%...'` | +| Who calls X? | `SELECT DISTINCT caller_name, file_path FROM calls WHERE callee_name = '...'` | +| What does X call? | `SELECT DISTINCT callee_name FROM calls WHERE caller_name = '...'` | +| Call hotspots | `SELECT callee_name, COUNT(*) as fan_in FROM calls GROUP BY callee_name ORDER BY fan_in DESC LIMIT 10` | +| Symbol coverage | `SELECT name, hit_statements, total_statements, coverage_pct FROM coverage WHERE file_path = '...'` | +| 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` | **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 fff77c5..1fa976c 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`**, **`markers-by-kind`**, **`deprecated-symbols`**, **`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)`), **`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/recipes-from-research-note.md b/.changeset/recipes-from-research-note.md new file mode 100644 index 0000000..7dc264b --- /dev/null +++ b/.changeset/recipes-from-research-note.md @@ -0,0 +1,12 @@ +--- +"@stainless-code/codemap": patch +--- + +feat(recipes): ship two new bundled recipes from research note § 1 + +- **`components-touching-deprecated`** (research note § 1.1) — UNION of two paths surfacing components that touch `@deprecated` symbols: hook path (`components.hooks_used` JSON overlap) + call path (`calls.caller_name = component`, `callee_name` is `@deprecated`). Hook-only variants ship false negatives — recipe spells out the explicit UNION. Action template `review-deprecation-impact`. +- **`refactor-risk-ranking`** (research note § 1.4) — per-file ranking by `(fan_in + 1) × (100 - avg_coverage_pct)`. Three correctness fixes vs the naïve formula: orphans (`fan_in = 0`) score on coverage alone via `+1`; NULL `coverage_pct` treated as 0% via `COALESCE` (otherwise the row drops from `ORDER BY`); files with no exports excluded (no public-API surface to refactor externally). Output is per-file (not per-symbol) — empirical test showed per-symbol ranking ties on file-level fan_in. Per-symbol via `calls` is a documented tuning axis for project-local override. Action template `review-refactor-impact`. + +Both recipes use only existing substrate (`components`, `calls`, `symbols`, `dependencies`, `coverage`, `files`) — no schema bump. Bundled recipe content follows the existing recipe-as-content registry pattern (PR #37); project-local overrides live at `/.codemap/recipes/.{sql,md}`. + +Agent rule + skill lockstep updated per `docs/README.md` Rule 10 — both `templates/agents/` (ships to npm via `codemap agents init`) and `.agents/` (this clone's mirror) gain trigger-pattern entries, quick-reference rows, and recipe-id list updates. diff --git a/templates/agents/rules/codemap.md b/templates/agents/rules/codemap.md index ca0aa60..8c71f0f 100644 --- a/templates/agents/rules/codemap.md +++ b/templates/agents/rules/codemap.md @@ -123,6 +123,8 @@ If the question looks like any of these → use the index: | "What's structurally dead AND untested?" | `--recipe untested-and-dead` | | "Rank files by test coverage" | `--recipe files-by-coverage` | | "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` | ## When Grep / Read IS appropriate @@ -144,34 +146,36 @@ codemap query --json "" ## Quick reference queries -| I need to... | Query | -| ------------------------- | ------------------------------------------------------------------------------------------------------------ | -| Find a symbol | `SELECT name, kind, file_path, line_start, line_end, signature FROM symbols WHERE name = '...'` | -| Find a symbol (fuzzy) | `SELECT name, kind, file_path, line_start FROM symbols WHERE name LIKE '%...%'` | -| See file exports | `SELECT name, kind, is_default FROM exports WHERE file_path LIKE '%...'` | -| See file imports | `SELECT source, specifiers, is_type_only FROM imports WHERE file_path LIKE '%...'` | -| Who imports this module? | `SELECT DISTINCT file_path FROM imports WHERE source LIKE '~/some/module%'` | -| Who imports this file? | `SELECT DISTINCT from_path FROM dependencies WHERE to_path LIKE '%...'` | -| What does this depend on? | `SELECT DISTINCT to_path FROM dependencies WHERE from_path LIKE '%...'` | -| Component info | `SELECT name, props_type, hooks_used FROM components WHERE name = '...'` | -| All components | `SELECT name, file_path, props_type, hooks_used FROM components ORDER BY name` | -| TODOs in a file | `SELECT line_number, content FROM markers WHERE file_path LIKE '%...' AND kind = 'TODO'` | -| Most complex files | `SELECT from_path, COUNT(*) as deps FROM dependencies GROUP BY from_path ORDER BY deps DESC LIMIT 10` | -| CSS design tokens | `SELECT name, value, scope FROM css_variables WHERE name LIKE '--%...'` | -| CSS module classes | `SELECT name, file_path FROM css_classes WHERE is_module = 1` | -| CSS keyframes | `SELECT name, file_path FROM css_keyframes` | -| Type/interface shape | `SELECT name, type, is_optional, is_readonly FROM type_members WHERE symbol_name = '...'` | -| Deprecated symbols | `SELECT name, kind, file_path, doc_comment FROM symbols WHERE doc_comment LIKE '%@deprecated%'` | -| Visibility-tagged symbols | `SELECT name, kind, visibility, file_path FROM symbols WHERE visibility IS NOT NULL` (or `= 'beta'`, etc.) | -| Symbol docs | `SELECT name, signature, doc_comment FROM symbols WHERE name = '...' AND doc_comment IS NOT NULL` | -| Const values | `SELECT name, value, file_path FROM symbols WHERE kind = 'const' AND value IS NOT NULL AND name LIKE '%...'` | -| Class members | `SELECT name, kind, signature FROM symbols WHERE parent_name = '...'` | -| Top-level only | `SELECT name, kind, signature FROM symbols WHERE parent_name IS NULL AND file_path LIKE '%...'` | -| Who calls X? | `SELECT DISTINCT caller_name, file_path FROM calls WHERE callee_name = '...'` | -| What does X call? | `SELECT DISTINCT callee_name FROM calls WHERE caller_name = '...'` | -| Call hotspots | `SELECT callee_name, COUNT(*) as fan_in FROM calls GROUP BY callee_name ORDER BY fan_in DESC LIMIT 10` | -| Symbol coverage | `SELECT name, hit_statements, total_statements, coverage_pct FROM coverage WHERE file_path = '...'` | -| Untested + dead exports | `codemap query --json --recipe untested-and-dead` | +| I need to... | Query | +| --------------------------------- | ------------------------------------------------------------------------------------------------------------ | +| Find a symbol | `SELECT name, kind, file_path, line_start, line_end, signature FROM symbols WHERE name = '...'` | +| Find a symbol (fuzzy) | `SELECT name, kind, file_path, line_start FROM symbols WHERE name LIKE '%...%'` | +| See file exports | `SELECT name, kind, is_default FROM exports WHERE file_path LIKE '%...'` | +| See file imports | `SELECT source, specifiers, is_type_only FROM imports WHERE file_path LIKE '%...'` | +| Who imports this module? | `SELECT DISTINCT file_path FROM imports WHERE source LIKE '~/some/module%'` | +| Who imports this file? | `SELECT DISTINCT from_path FROM dependencies WHERE to_path LIKE '%...'` | +| What does this depend on? | `SELECT DISTINCT to_path FROM dependencies WHERE from_path LIKE '%...'` | +| Component info | `SELECT name, props_type, hooks_used FROM components WHERE name = '...'` | +| All components | `SELECT name, file_path, props_type, hooks_used FROM components ORDER BY name` | +| TODOs in a file | `SELECT line_number, content FROM markers WHERE file_path LIKE '%...' AND kind = 'TODO'` | +| Most complex files | `SELECT from_path, COUNT(*) as deps FROM dependencies GROUP BY from_path ORDER BY deps DESC LIMIT 10` | +| CSS design tokens | `SELECT name, value, scope FROM css_variables WHERE name LIKE '--%...'` | +| CSS module classes | `SELECT name, file_path FROM css_classes WHERE is_module = 1` | +| CSS keyframes | `SELECT name, file_path FROM css_keyframes` | +| Type/interface shape | `SELECT name, type, is_optional, is_readonly FROM type_members WHERE symbol_name = '...'` | +| Deprecated symbols | `SELECT name, kind, file_path, doc_comment FROM symbols WHERE doc_comment LIKE '%@deprecated%'` | +| Visibility-tagged symbols | `SELECT name, kind, visibility, file_path FROM symbols WHERE visibility IS NOT NULL` (or `= 'beta'`, etc.) | +| Symbol docs | `SELECT name, signature, doc_comment FROM symbols WHERE name = '...' AND doc_comment IS NOT NULL` | +| Const values | `SELECT name, value, file_path FROM symbols WHERE kind = 'const' AND value IS NOT NULL AND name LIKE '%...'` | +| Class members | `SELECT name, kind, signature FROM symbols WHERE parent_name = '...'` | +| Top-level only | `SELECT name, kind, signature FROM symbols WHERE parent_name IS NULL AND file_path LIKE '%...'` | +| Who calls X? | `SELECT DISTINCT caller_name, file_path FROM calls WHERE callee_name = '...'` | +| What does X call? | `SELECT DISTINCT callee_name FROM calls WHERE caller_name = '...'` | +| Call hotspots | `SELECT callee_name, COUNT(*) as fan_in FROM calls GROUP BY callee_name ORDER BY fan_in DESC LIMIT 10` | +| Symbol coverage | `SELECT name, hit_statements, total_statements, coverage_pct FROM coverage WHERE file_path = '...'` | +| 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` | **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 98692dc..acfd40b 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`**, **`markers-by-kind`**, **`deprecated-symbols`**, **`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)`), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`codemap query --help`**. **Output flags** (compose with **`--recipe`** or ad-hoc SQL): diff --git a/templates/recipes/components-touching-deprecated.md b/templates/recipes/components-touching-deprecated.md new file mode 100644 index 0000000..be872b1 --- /dev/null +++ b/templates/recipes/components-touching-deprecated.md @@ -0,0 +1,17 @@ +--- +actions: + - type: review-deprecation-impact + auto_fixable: false + description: "Component depends on a @deprecated symbol. Either migrate the component to the replacement API, or coordinate with the deprecation removal." +--- + +Components that touch `@deprecated` symbols, via either hook usage or direct calls. + +Two evidence axes UNIONed: + +1. **Hook path** — `components.hooks_used` JSON contains the `@deprecated` symbol's name. Catches deprecated _hooks_ (e.g. `useDeprecatedThing`). +2. **Call path** — `calls.caller_name = components.name` AND `calls.callee_name` is a `@deprecated` symbol. Catches deprecated regular functions called inside components. + +The `via` column distinguishes which path matched. A single component can appear multiple times if it touches multiple deprecated symbols across both paths. + +**Caveat:** matching is by symbol name alone; cross-file collisions can produce false positives when two unrelated symbols share a name and one is `@deprecated`. Inspect the `deprecated_file` column to confirm. diff --git a/templates/recipes/components-touching-deprecated.sql b/templates/recipes/components-touching-deprecated.sql new file mode 100644 index 0000000..113820b --- /dev/null +++ b/templates/recipes/components-touching-deprecated.sql @@ -0,0 +1,27 @@ +-- Components touching @deprecated symbols (UNION of hook + call paths). +-- Hook path: components.hooks_used JSON contains a @deprecated symbol name. +-- Call path: calls.caller_name = components.name AND callee is @deprecated. +WITH touched AS ( + SELECT + c.name AS component, + c.file_path AS component_file, + s.name AS deprecated_symbol, + s.file_path AS deprecated_file, + 'hook' AS via + FROM components c + JOIN symbols s ON s.doc_comment LIKE '%@deprecated%' + WHERE c.hooks_used LIKE '%"' || s.name || '"%' + UNION + SELECT + c.name, + c.file_path, + s.name, + s.file_path, + 'call' + FROM components c + JOIN calls ca ON ca.caller_name = c.name AND ca.file_path = c.file_path + JOIN symbols s ON s.name = ca.callee_name AND s.doc_comment LIKE '%@deprecated%' +) +SELECT * FROM touched +ORDER BY component_file, component, deprecated_symbol +LIMIT 50 diff --git a/templates/recipes/refactor-risk-ranking.md b/templates/recipes/refactor-risk-ranking.md new file mode 100644 index 0000000..d325ddc --- /dev/null +++ b/templates/recipes/refactor-risk-ranking.md @@ -0,0 +1,31 @@ +--- +actions: + - type: review-refactor-impact + auto_fixable: false + description: "High refactor-risk file — many callers AND/OR low test coverage. Read all callers and add tests before changing any exported symbol's signature." +--- + +Files ranked by refactor-risk score `(fan_in + 1) × (100 - avg_coverage_pct)`. + +Higher score = higher refactor risk. Output is **per-file** (one row per high-risk file) — symbols within a file inherit the file's structural risk. + +**Why per-file (not per-symbol):** `dependencies` is tracked at the file level — every symbol in a popular file ties at the same score under per-symbol ranking, producing 30 noisy rows from one file. File-level aggregation gives 30 actionable rows ranking the riskiest files. Drill into a specific file's symbols via `query --recipe ad-hoc "SELECT name, kind, signature FROM symbols WHERE file_path = '' AND is_exported = 1"`. + +Three correctness fixes baked into the formula: + +1. **Files with zero callers (`fan_in = 0`)** score on coverage alone via the `+1`. Untested zero-fan-in files = high risk for surprising remote callers we don't track (hidden imports, dynamic loads). Tested zero-fan-in files rank low — candidates for deletion review, not refactor. +2. **NULL `coverage_pct`** (no coverage measurement) treated as `0%` via `COALESCE(coverage_pct, 0)` per-row. Without this, `100 - NULL = NULL` would silently drop rows from `ORDER BY` — files without measured coverage would vanish from the ranking. Treat unmeasured as untested = high risk. +3. **Files with no exports excluded** (`WHERE exported_count > 0`) — they have no public-API surface to refactor externally; the recipe is about cross-file refactor cost. + +The output columns `exported_count`, `fan_in`, `avg_coverage_pct`, `measured_symbols`, `risk_score` give agents enough context to triage without re-running queries: count of exports + how many files import this one + average coverage of measured symbols + how many symbols had measurements. + +**v1 trade-off (linear-in-fan_in, accepted):** `fan_in = 100, avg_coverage = 99%` and `fan_in = 1, avg_coverage = 0%` both score `100` — equivalent by formula but obviously not equivalent in practice. v1 ships the simple formula; tune via project-local recipe override at `/.codemap/recipes/refactor-risk-ranking.sql`. + +Suggested tuning axes for project-local overrides: + +- **Log-scale `fan_in`** for hub-heavy codebases — diminishing returns above ~20 callers: `LOG(fan_in + 1) * 30`. +- **Per-symbol fan_in via `calls`** if you'd rather rank symbols than files: replace the `fan_in_per_file` CTE with `SELECT callee_name, COUNT(DISTINCT caller_name || '|' || file_path) AS fan_in FROM calls GROUP BY callee_name`. Caveat: name-collision false positives across files. +- **Visibility weight** if `@public` / `@internal` / `@beta` JSDoc tags are used consistently: `+ CASE visibility WHEN 'public' THEN 20 WHEN 'beta' THEN 10 ELSE 0 END`. +- **LOC weight** scale by file `line_count` (already on the `files` table). + +**Divergence from research note (`docs/research/non-goals-reassessment-2026-05.md` § 1.4):** the research note specified per-symbol ranking. Empirical test against codemap's own index showed per-symbol output was 30 rows from `src/db.ts` all tied at the same score (file-level fan_in inherited). v1 ships file-level aggregation as the more useful default; per-symbol via `calls` is one of the documented tuning axes above. diff --git a/templates/recipes/refactor-risk-ranking.sql b/templates/recipes/refactor-risk-ranking.sql new file mode 100644 index 0000000..3c6f09a --- /dev/null +++ b/templates/recipes/refactor-risk-ranking.sql @@ -0,0 +1,43 @@ +-- Refactor risk per file: (fan_in + 1) × (100 - avg_coverage_pct). +-- Output is per-file (one row per high-risk file) — symbols within a file inherit +-- the file's structural risk. Per-symbol ranking would be misleading because +-- dependencies are tracked at the file level (every symbol in a popular file +-- ties at the same score) — file-level aggregation gives actionable rows. +-- See refactor-risk-ranking.md for tuning axes. +WITH fan_in_per_file AS ( + SELECT to_path, COUNT(*) AS fan_in + FROM dependencies + GROUP BY to_path +), +file_coverage AS ( + SELECT + file_path, + AVG(COALESCE(coverage_pct, 0)) AS avg_coverage_pct, + COUNT(*) AS measured_symbols + FROM coverage + GROUP BY file_path +), +file_export_count AS ( + SELECT file_path, COUNT(*) AS exported_count + FROM symbols + WHERE is_exported = 1 + GROUP BY file_path +) +SELECT + f.path AS file_path, + COALESCE(ec.exported_count, 0) AS exported_count, + COALESCE(fp.fan_in, 0) AS fan_in, + ROUND(COALESCE(fc.avg_coverage_pct, 0), 1) AS avg_coverage_pct, + COALESCE(fc.measured_symbols, 0) AS measured_symbols, + ROUND( + (COALESCE(fp.fan_in, 0) + 1) + * (100 - COALESCE(fc.avg_coverage_pct, 0)), + 1 + ) AS risk_score +FROM files f +LEFT JOIN fan_in_per_file fp ON fp.to_path = f.path +LEFT JOIN file_coverage fc ON fc.file_path = f.path +LEFT JOIN file_export_count ec ON ec.file_path = f.path +WHERE COALESCE(ec.exported_count, 0) > 0 +ORDER BY risk_score DESC, f.path +LIMIT 30