From 2f3e2295b1b146e5b7048683074b823a50d9d73e Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 10 Jun 2026 13:04:16 +0300 Subject: [PATCH 1/4] feat(recipes): add coverage-confirmed-dead recipe (Plan 3 slice 3.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fork untested-and-dead with confidence/reason/caller_count columns — high when ingested coverage is 0%, medium when unmeasured; honors shared suppressions. --- docs/plans/agent-enrichment-wave.md | 2 +- templates/recipes/coverage-confirmed-dead.md | 40 +++++++++++++++++ templates/recipes/coverage-confirmed-dead.sql | 44 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 templates/recipes/coverage-confirmed-dead.md create mode 100644 templates/recipes/coverage-confirmed-dead.sql diff --git a/docs/plans/agent-enrichment-wave.md b/docs/plans/agent-enrichment-wave.md index 90df7ccf..bd07ece4 100644 --- a/docs/plans/agent-enrichment-wave.md +++ b/docs/plans/agent-enrichment-wave.md @@ -65,4 +65,4 @@ Each PR: `harden-pr full` (includes plan retirement) → merge. Do not batch pla ## Current slice -**Active:** Plan 3 slice **3.1** on `feat/high-crap-score` or fresh branch from `main` after **#175** merges — `coverage-confirmed-dead` recipe fork. +**Active:** Plan 3 slice **3.2** on `feat/coverage-confirmed-dead` — golden no-ingest (`confidence: medium`). diff --git a/templates/recipes/coverage-confirmed-dead.md b/templates/recipes/coverage-confirmed-dead.md new file mode 100644 index 00000000..95f11ee0 --- /dev/null +++ b/templates/recipes/coverage-confirmed-dead.md @@ -0,0 +1,40 @@ +--- +actions: + - type: review-for-deletion + auto_fixable: false + description: "Exported function with zero callers and zero (or unmeasured) coverage — check `confidence` before deleting. `high` = ingested 0% coverage; `medium` = static dead only (run `codemap ingest-coverage` to confirm). Verify framework entry points (Next.js page.tsx, Storybook, vite.config.ts) per C.9 caveat." +--- + +# coverage-confirmed-dead + +Cross-product of **static dead** (same core as `untested-and-dead`) and **coverage** semantics with an explicit **`confidence`** column — Moat A predicate, not an engine verdict. + +| `confidence` | When | +| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------- | +| **`high`** | No AST callers **and** ingested `coverage` row with `coverage_pct = 0` | +| **`medium`** | No AST callers **and** no ingested coverage row for the symbol (`coverage_pct` treated as 0 for filtering, but not measurement-confirmed) | + +Rows also include **`reason`** (`no_callers_and_zero_coverage` \| `no_callers_and_coverage_unmeasured`), **`caller_count`** (always 0 for rows that pass the dead predicate), and **`coverage_pct`** (`COALESCE` to 0). + +## Prerequisite + +Run `codemap ingest-coverage ` for **`high`** rows. With an empty `coverage` table, every row is **`medium`** — same static dead set as `untested-and-dead`, but agents can sort by `confidence` after ingest. + +## Shared predicate (with `untested-and-dead`) + +1. **Structural**: `is_exported = 1`, `kind = 'function'`, no incoming AST `calls` (`callee_name = s.name`). +2. **Coverage filter**: `COALESCE(c.coverage_pct, 0) = 0`. + +**Known v1 limitation:** `callee_name = s.name` is name-only — homonyms across files share the "no callers" check. Narrow with `file_path` / `is_default_export` filters in project-local overrides (see `untested-and-dead.md`). + +**C.9 caveat:** framework entry-point exports (Next.js `page.tsx`, Storybook stories, `vite.config.ts`) may appear as dead until `files.is_entry` ships — triage before deletion. + +## Suppressions + +Honors `// codemap-ignore-next-line` / `// codemap-ignore-file` for **`untested-and-dead`** or **`coverage-confirmed-dead`**. + +```bash +codemap query --recipe coverage-confirmed-dead --json +codemap ingest-coverage coverage/coverage-final.json +codemap query --recipe coverage-confirmed-dead --json # high + medium rows +``` diff --git a/templates/recipes/coverage-confirmed-dead.sql b/templates/recipes/coverage-confirmed-dead.sql new file mode 100644 index 00000000..1c8a623a --- /dev/null +++ b/templates/recipes/coverage-confirmed-dead.sql @@ -0,0 +1,44 @@ +-- Static dead exports with zero (or unmeasured) coverage — adds `confidence` over `untested-and-dead`. +-- Honors `// codemap-ignore-{next-line,file}` for `untested-and-dead` or `coverage-confirmed-dead`. +SELECT + s.name, + s.kind, + s.file_path, + s.line_start, + COALESCE(c.coverage_pct, 0) AS coverage_pct, + ( + SELECT COUNT(*) + FROM calls + WHERE callee_name = s.name + AND (provenance IS NULL OR provenance = 'ast') + ) AS caller_count, + CASE + WHEN c.coverage_pct IS NOT NULL AND c.coverage_pct = 0 THEN 'high' + ELSE 'medium' + END AS confidence, + CASE + WHEN c.coverage_pct IS NOT NULL AND c.coverage_pct = 0 + THEN 'no_callers_and_zero_coverage' + ELSE 'no_callers_and_coverage_unmeasured' + END AS reason +FROM symbols s +LEFT JOIN coverage c + ON c.file_path = s.file_path + AND c.name = s.name + AND c.line_start = s.line_start +LEFT JOIN suppressions sup + ON sup.file_path = s.file_path + AND sup.recipe_id IN ('untested-and-dead', 'coverage-confirmed-dead') + AND (sup.line_number = 0 OR sup.line_number = s.line_start) +WHERE s.kind = 'function' + AND s.is_exported = 1 + AND NOT EXISTS ( + SELECT 1 + FROM calls + WHERE callee_name = s.name + AND (provenance IS NULL OR provenance = 'ast') + ) + AND COALESCE(c.coverage_pct, 0) = 0 + AND sup.id IS NULL +ORDER BY confidence DESC, s.file_path ASC, s.line_start ASC +LIMIT 100 From 0900140fadfc9696b5a7b0b09fb82bd0442eb2a1 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 10 Jun 2026 13:06:07 +0300 Subject: [PATCH 2/4] test(golden): coverage-confirmed-dead scenarios with preSetup clear-coverage Add no-ingest (all medium) and post-ingest goldens; extend query-golden with per-scenario preSetup, clear-coverage step, and everyRowFieldEquals match. --- docs/plans/agent-enrichment-wave.md | 2 +- fixtures/CAPABILITIES.json | 2 + .../coverage-confirmed-dead-no-ingest.json | 232 ++++++++++++++++++ .../minimal/coverage-confirmed-dead.json | 212 ++++++++++++++++ fixtures/golden/scenarios.json | 16 ++ scripts/query-golden.ts | 34 +++ scripts/query-golden/run-setup.ts | 4 + scripts/query-golden/schema.ts | 39 ++- 8 files changed, 527 insertions(+), 14 deletions(-) create mode 100644 fixtures/golden/minimal/coverage-confirmed-dead-no-ingest.json create mode 100644 fixtures/golden/minimal/coverage-confirmed-dead.json diff --git a/docs/plans/agent-enrichment-wave.md b/docs/plans/agent-enrichment-wave.md index bd07ece4..d9db64ca 100644 --- a/docs/plans/agent-enrichment-wave.md +++ b/docs/plans/agent-enrichment-wave.md @@ -65,4 +65,4 @@ Each PR: `harden-pr full` (includes plan retirement) → merge. Do not batch pla ## Current slice -**Active:** Plan 3 slice **3.2** on `feat/coverage-confirmed-dead` — golden no-ingest (`confidence: medium`). +**Active:** Plan 3 slice **3.4** (optional classifier) or PR harden — agent surfaces + changeset on `feat/coverage-confirmed-dead`. diff --git a/fixtures/CAPABILITIES.json b/fixtures/CAPABILITIES.json index e19595cd..154f5c62 100644 --- a/fixtures/CAPABILITIES.json +++ b/fixtures/CAPABILITIES.json @@ -167,6 +167,8 @@ "goldenScenarios": [ "coverage-rows-after-ingest", "untested-and-dead", + "coverage-confirmed-dead", + "coverage-confirmed-dead-no-ingest", "refactor-risk-ranking", "files-by-coverage", "high-crap-score" diff --git a/fixtures/golden/minimal/coverage-confirmed-dead-no-ingest.json b/fixtures/golden/minimal/coverage-confirmed-dead-no-ingest.json new file mode 100644 index 00000000..54a3c68d --- /dev/null +++ b/fixtures/golden/minimal/coverage-confirmed-dead-no-ingest.json @@ -0,0 +1,232 @@ +[ + { + "name": "legacyClient", + "kind": "function", + "file_path": "src/api/client.ts", + "line_start": 46, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "useHelperA", + "kind": "function", + "file_path": "src/bench/homonym-consumer-a.ts", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "useHelperB", + "kind": "function", + "file_path": "src/bench/homonym-consumer-b.ts", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "MemberHost", + "kind": "function", + "file_path": "src/bench/jsx-member-gap.tsx", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ChildCard", + "kind": "function", + "file_path": "src/bench/jsx-synthesis/ChildCard.tsx", + "line_start": 5, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "PageShell", + "kind": "function", + "file_path": "src/bench/jsx-synthesis/PageShell.tsx", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "UiPanel", + "kind": "function", + "file_path": "src/bench/jsx-ui-namespace.ts", + "line_start": 1, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "keepMe", + "kind": "function", + "file_path": "src/bench/stale-multi-helpers.ts", + "line_start": 1, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "dropMe", + "kind": "function", + "file_path": "src/bench/stale-multi-helpers.ts", + "line_start": 5, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ApiBridge", + "kind": "function", + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 4, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ProductCard", + "kind": "function", + "file_path": "src/components/shop/ProductCard.tsx", + "line_start": 10, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "FormatPrice", + "kind": "function", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 4, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ShopButton", + "kind": "function", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 8, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "prefetch", + "kind": "function", + "file_path": "src/consumer.ts", + "line_start": 15, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "run", + "kind": "function", + "file_path": "src/consumer.ts", + "line_start": 22, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "catchInnerArrowRethrow", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 86, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "catchDirectRethrow", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 98, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ignoredExport", + "kind": "function", + "file_path": "src/orphan.ts", + "line_start": 2, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "orphanHelper", + "kind": "function", + "file_path": "src/orphan.ts", + "line_start": 7, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "_epochSeconds", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 12, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "nanoseconds", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 19, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "_hiResEpoch", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 26, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "nowIso", + "kind": "function", + "file_path": "src/utils/format.ts", + "line_start": 14, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + } +] diff --git a/fixtures/golden/minimal/coverage-confirmed-dead.json b/fixtures/golden/minimal/coverage-confirmed-dead.json new file mode 100644 index 00000000..b0bbe23c --- /dev/null +++ b/fixtures/golden/minimal/coverage-confirmed-dead.json @@ -0,0 +1,212 @@ +[ + { + "name": "legacyClient", + "kind": "function", + "file_path": "src/api/client.ts", + "line_start": 46, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "useHelperA", + "kind": "function", + "file_path": "src/bench/homonym-consumer-a.ts", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "useHelperB", + "kind": "function", + "file_path": "src/bench/homonym-consumer-b.ts", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "MemberHost", + "kind": "function", + "file_path": "src/bench/jsx-member-gap.tsx", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ChildCard", + "kind": "function", + "file_path": "src/bench/jsx-synthesis/ChildCard.tsx", + "line_start": 5, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "PageShell", + "kind": "function", + "file_path": "src/bench/jsx-synthesis/PageShell.tsx", + "line_start": 3, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "UiPanel", + "kind": "function", + "file_path": "src/bench/jsx-ui-namespace.ts", + "line_start": 1, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "keepMe", + "kind": "function", + "file_path": "src/bench/stale-multi-helpers.ts", + "line_start": 1, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "dropMe", + "kind": "function", + "file_path": "src/bench/stale-multi-helpers.ts", + "line_start": 5, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ApiBridge", + "kind": "function", + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 4, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "FormatPrice", + "kind": "function", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 4, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ShopButton", + "kind": "function", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 8, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "prefetch", + "kind": "function", + "file_path": "src/consumer.ts", + "line_start": 15, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "run", + "kind": "function", + "file_path": "src/consumer.ts", + "line_start": 22, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "catchInnerArrowRethrow", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 86, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "catchDirectRethrow", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 98, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "ignoredExport", + "kind": "function", + "file_path": "src/orphan.ts", + "line_start": 2, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "orphanHelper", + "kind": "function", + "file_path": "src/orphan.ts", + "line_start": 7, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "_hiResEpoch", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 26, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "medium", + "reason": "no_callers_and_coverage_unmeasured" + }, + { + "name": "_epochSeconds", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 12, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "high", + "reason": "no_callers_and_zero_coverage" + }, + { + "name": "nanoseconds", + "kind": "function", + "file_path": "src/utils/date.ts", + "line_start": 19, + "coverage_pct": 0, + "caller_count": 0, + "confidence": "high", + "reason": "no_callers_and_zero_coverage" + } +] diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index 688264e2..a92df0b4 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -523,6 +523,22 @@ "prompt": "Exported functions with no callers AND zero coverage (the killer recipe)", "recipe": "untested-and-dead" }, + { + "id": "coverage-confirmed-dead-no-ingest", + "prompt": "Static dead exports — all confidence medium when coverage table empty", + "recipe": "coverage-confirmed-dead", + "preSetup": [{ "kind": "clear-coverage" }], + "match": { + "kind": "everyRowFieldEquals", + "field": "confidence", + "value": "medium" + } + }, + { + "id": "coverage-confirmed-dead", + "prompt": "Dead exports with confidence high (ingested 0%) and medium (unmeasured)", + "recipe": "coverage-confirmed-dead" + }, { "id": "files-by-coverage", "prompt": "Files ranked ascending by statement coverage", diff --git a/scripts/query-golden.ts b/scripts/query-golden.ts index 4e7bbead..053bf123 100644 --- a/scripts/query-golden.ts +++ b/scripts/query-golden.ts @@ -121,6 +121,25 @@ function evaluateMatch( } return { ok: true, detail: "" }; } + if (match.kind === "everyRowFieldEquals") { + for (let i = 0; i < rows.length; i++) { + const r = rows[i]; + if (r === null || typeof r !== "object") { + return { + ok: false, + detail: `everyRowFieldEquals: row ${i} is not an object`, + }; + } + const o = r as Record; + if (o[match.field] !== match.value) { + return { + ok: false, + detail: `everyRowFieldEquals: row ${i} field ${JSON.stringify(match.field)} expected ${JSON.stringify(match.value)}, got ${JSON.stringify(o[match.field])}`, + }; + } + } + return { ok: true, detail: "" }; + } return { ok: false, detail: "unknown match kind" }; } @@ -174,6 +193,10 @@ async function main(): Promise { let budgetFailures = 0; for (const s of scenarios) { + const hadPreSetup = s.preSetup !== undefined && s.preSetup.length > 0; + if (hadPreSetup) { + runGoldenSetup(s.preSetup!, fixtureRoot); + } const { sql, bindValues } = resolveGoldenQuery(s); const t0 = performance.now(); const rows = queryRows(sql, bindValues) as unknown[]; @@ -195,6 +218,9 @@ async function main(): Promise { if (UPDATE) { writeFileSync(goldenPath, `${JSON.stringify(rows, null, 2)}\n`, "utf-8"); console.log(` updated ${goldenPath}`); + if (hadPreSetup && setup.length > 0) { + runGoldenSetup(setup, fixtureRoot); + } continue; } @@ -215,6 +241,9 @@ async function main(): Promise { } else { console.log(` ok ${s.id}`); } + if (hadPreSetup && setup.length > 0) { + runGoldenSetup(setup, fixtureRoot); + } continue; } @@ -226,6 +255,11 @@ async function main(): Promise { } else { console.log(` ok ${s.id} (${match.kind})`); } + + // preSetup mutations (e.g. clear-coverage) persist — restore global setup. + if (hadPreSetup && setup.length > 0) { + runGoldenSetup(setup, fixtureRoot); + } } if (UPDATE) { diff --git a/scripts/query-golden/run-setup.ts b/scripts/query-golden/run-setup.ts index 34a0a37b..fa1ee497 100644 --- a/scripts/query-golden/run-setup.ts +++ b/scripts/query-golden/run-setup.ts @@ -22,6 +22,10 @@ export function runGoldenSetup( const db = openDb(); try { for (const step of steps) { + if (step.kind === "clear-coverage") { + db.run("DELETE FROM coverage"); + continue; + } if (step.kind !== "ingest-coverage") continue; const absPath = resolve(fixtureRoot, step.path); if ( diff --git a/scripts/query-golden/schema.ts b/scripts/query-golden/schema.ts index 9e31d807..49eaeb21 100644 --- a/scripts/query-golden/schema.ts +++ b/scripts/query-golden/schema.ts @@ -13,14 +13,38 @@ const matchEveryRowContainsSchema = z.object({ includes: z.string(), }); +const matchEveryRowFieldEqualsSchema = z.object({ + kind: z.literal("everyRowFieldEquals"), + field: z.string(), + value: z.union([z.string(), z.number(), z.boolean()]), +}); + export const matchSchema = z.union([ matchExactSchema, matchMinRowsSchema, matchEveryRowContainsSchema, + matchEveryRowFieldEqualsSchema, ]); export type GoldenMatch = z.infer; +/** + * One-time or per-scenario setup step. Extend the union as more one-shot + * ingest / reset verbs land. + */ +export const setupStepSchema = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("ingest-coverage"), + /** Path relative to the fixture root (e.g. `coverage/coverage-final.json`). */ + path: z.string().min(1), + }), + z.object({ + kind: z.literal("clear-coverage"), + }), +]); + +export type GoldenSetupStep = z.infer; + export const scenarioSchema = z .object({ id: z.string().min(1), @@ -31,6 +55,8 @@ export const scenarioSchema = z .record(z.string(), z.union([z.string(), z.number(), z.boolean()])) .optional(), match: matchSchema.optional(), + /** Runs after global `setup` and before this scenario's query (e.g. clear coverage). */ + preSetup: z.array(setupStepSchema).optional(), budgetMs: z.number().positive().optional(), }) .refine( @@ -59,19 +85,6 @@ export const scenarioSchema = z export type GoldenScenario = z.infer; -/** - * One-time setup step run after `cm.index()` and before the first scenario. - * Currently only `ingest-coverage` (Istanbul / LCOV); extend the union as - * other one-shot ingest verbs land. - */ -export const setupStepSchema = z.object({ - kind: z.literal("ingest-coverage"), - /** Path relative to the fixture root (e.g. `coverage/coverage-final.json`). */ - path: z.string().min(1), -}); - -export type GoldenSetupStep = z.infer; - const legacyArraySchema = z.array(scenarioSchema); const objectShapeSchema = z.object({ setup: z.array(setupStepSchema).optional(), From 96c504c825d76558ba3f7cb41e7f83af67e62164 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 10 Jun 2026 13:06:50 +0300 Subject: [PATCH 3/4] feat(recipes): ship coverage-confirmed-dead agent surfaces and classifier Document confidence columns in golden-queries, glossary, architecture, rule/skill/MCP; add cleanup intent mapping; changeset for patch release. --- .changeset/coverage-confirmed-dead.md | 5 ++ docs/architecture.md | 2 + docs/glossary.md | 1 + docs/golden-queries.md | 6 +- docs/plans/agent-enrichment-wave.md | 2 +- src/application/context-engine.ts | 15 +++++ src/application/mcp-server.ts | 2 +- src/cli/cmd-context.test.ts | 6 ++ templates/agent-content/mcp-instructions.md | 56 +++++++++---------- templates/agent-content/rule/00-full.md | 3 + .../agent-content/skill/10-recipes-context.md | 3 +- 11 files changed, 69 insertions(+), 32 deletions(-) create mode 100644 .changeset/coverage-confirmed-dead.md diff --git a/.changeset/coverage-confirmed-dead.md b/.changeset/coverage-confirmed-dead.md new file mode 100644 index 00000000..06913d5a --- /dev/null +++ b/.changeset/coverage-confirmed-dead.md @@ -0,0 +1,5 @@ +--- +"@stainless-code/codemap": patch +--- + +Add `coverage-confirmed-dead` recipe: static dead exports with `confidence` (`high` when ingested 0% coverage, `medium` when unmeasured), plus `reason` and `caller_count`. Golden scenarios and agent rule/skill/MCP documented. diff --git a/docs/architecture.md b/docs/architecture.md index ef11ffda..49105860 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -194,6 +194,8 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload **Coverage columns (CRAP recipes):** `high-crap-score` adds **`coverage_source`** and **`effective_coverage_pct`** — measured vs graph-estimated undertest signal. Contract: [golden-queries.md § Coverage columns](./golden-queries.md#coverage-columns-crap--enrichment-recipes). +**Confidence columns (deletion recipes):** `coverage-confirmed-dead` adds **`confidence`** (`high` \| `medium`), **`reason`**, and **`caller_count`** on static dead exports — ingested zero vs unmeasured coverage. Contract: [golden-queries.md § Confidence columns](./golden-queries.md#confidence-columns-deletion-confidence-recipes). + **Recipes wiring:** **`src/application/recipes-loader.ts`** (pure transport-agnostic loader) + **`src/application/query-recipes.ts`** (cache + public API — `getQueryRecipeSql` / `getQueryRecipeActions` / `getQueryRecipeParams` / `listQueryRecipeIds` / `listQueryRecipeCatalog` / `getQueryRecipeCatalogEntry`, shared by CLI + MCP). Recipes live as file pairs: **`.sql`** + optional **`.md`**. The loader reads `templates/recipes/` (bundled, ships in npm package next to `templates/agents/`) and `/recipes/` (project-local — default `.codemap/recipes/`; honors `--state-dir` / `CODEMAP_STATE_DIR`; root-only resolution per the registry plan, no walk-up). Project recipes win on id collision; entries that override a bundled id carry **`shadows: true`** in the catalog so agents reading `codemap://recipes` at session start see when a recipe behaves differently from the documented bundled version. Per-row **`actions`** templates and recipe **`params`** declarations live in YAML frontmatter on each `.md` — uniform shape across bundled + project. Param types are `string | number | boolean`; CLI passes values via repeatable `--params key=value[,key=value]`, MCP / HTTP pass nested `params: {key: value}` to `query_recipe`. Validation runs before SQL binding; missing / unknown / malformed params return the same `{error}` envelope as query failures. Hand-rolled YAML parser is scoped to block-list `actions:` and `params:` only (no `js-yaml` dep). Load-time validation rejects empty SQL and DML / DDL keywords (`INSERT` / `UPDATE` / `DELETE` / `DROP` / `CREATE` / `ALTER` / `ATTACH` / `DETACH` / `REPLACE` / `TRUNCATE` / `VACUUM` / `PRAGMA`) with recipe-aware error messages — defence in depth alongside the runtime `PRAGMA query_only=1` backstop in `query-engine.ts` (PR #35). `/index.db` is gitignored; `/recipes/` is NOT (verified via `git check-ignore`) — recipes are git-tracked source code authored for human review. **Tool / resource handlers (transport-agnostic):** **`src/application/tool-handlers.ts`** + **`src/application/resource-handlers.ts`** — pure functions that take the args object an MCP tool / resource URI accepts and return a discriminated **`ToolResult`** (`{ok: true, format: 'json'|'sarif'|'annotations'|'mermaid'|'diff'|'diff-json'|'codeclimate'|'badge', payload}` — badge arm also carries `badgeStyle`; `{ok: false, error}`) or a **`ResourcePayload`** (`{mimeType, text}`). MCP and HTTP both wrap the same handlers — MCP translates to `{content: [{type: "text", text}]}`, HTTP translates to `(status, body)` with the right `Content-Type`. Engine layer untouched; transport changes don't ripple into the SQL. diff --git a/docs/glossary.md b/docs/glossary.md index 741d170a..f9d4f169 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -170,6 +170,7 @@ Statement coverage ingested from Istanbul JSON, LCOV, or V8 runtime (`NODE_V8_CO Format auto-detected from extension (`.json` → istanbul, `.info` → lcov, directory → probe both, error if ambiguous); `--runtime` opts into V8 directory mode. Each statement projects onto the **innermost** enclosing symbol via JS-side `(line_end - line_start) ASC` tie-break — required because nested symbols (class methods inside classes, closures inside functions) would otherwise inflate `total_statements`. Statements that fall outside every symbol range (top-level expressions, side-effect imports) increment `skipped.statements_no_symbol` for observability. Three bundled recipes consume the table at first-class agent surface (no agent ever has to hand-compose the JOIN): - `untested-and-dead` — exported functions with no callers AND zero coverage (the killer recipe; ships with a name-collision mitigation guide in the recipe `.md`). +- `coverage-confirmed-dead` — same static dead predicate as `untested-and-dead` with explicit **`confidence`** (`high` when ingested `coverage_pct = 0`, `medium` when unmeasured) plus **`reason`** and **`caller_count`** — Moat A predicate columns for deletion triage after `ingest-coverage`. - `files-by-coverage` — files ranked ascending by statement coverage (replaces a deferred `file_coverage` rollup table; aggregates the symbol-level table via index-bounded `GROUP BY`). - `worst-covered-exports` — top-20 worst-covered exported functions. diff --git a/docs/golden-queries.md b/docs/golden-queries.md index 489c8303..f8411f07 100644 --- a/docs/golden-queries.md +++ b/docs/golden-queries.md @@ -62,7 +62,7 @@ We **do not** commit another product’s source tree, paths, business strings, o ## Scenario shape (implemented) -Scenarios live in **`fixtures/golden/scenarios.json`** (Tier A) or optional **`scenarios.external.json`** / **example** (Tier B). The file may be a **bare array** of scenarios (legacy) or an object `{ "setup": [...], "scenarios": [...] }`. Optional top-level **`setup`** runs once after index, before scenarios — today only **`{ "kind": "ingest-coverage", "path": "" }`** (see [run-setup.ts](../scripts/query-golden/run-setup.ts)); missing coverage files are skipped with a warning. Each scenario has **`id`**, **`sql` or `recipe`**, optional **`match`** (`exact`, `minRows`, `everyRowContains`), optional **`budgetMs`**. Goldens: **`fixtures/golden/minimal/*.json`** etc. Refresh: **`bun scripts/query-golden.ts --update`**. +Scenarios live in **`fixtures/golden/scenarios.json`** (Tier A) or optional **`scenarios.external.json`** / **example** (Tier B). The file may be a **bare array** of scenarios (legacy) or an object `{ "setup": [...], "scenarios": [...] }`. Optional top-level **`setup`** runs once after index, before scenarios — today **`ingest-coverage`** and **`clear-coverage`** (see [run-setup.ts](../scripts/query-golden/run-setup.ts)); missing coverage files are skipped with a warning. Per-scenario **`preSetup`** runs after global setup (global setup restores after the scenario when `preSetup` mutates the index). Each scenario has **`id`**, **`sql` or `recipe`**, optional **`match`** (`exact`, `minRows`, `everyRowContains`, `everyRowFieldEquals`), optional **`budgetMs`**. Goldens: **`fixtures/golden/minimal/*.json`** etc. Refresh: **`bun scripts/query-golden.ts --update`**. **Prompts** in JSON are **intent labels**, not pasted chat logs — pair with queries whose literals come from **fixture-owned** data (see [fixtures/qa/prompts.external.template.md](../fixtures/qa/prompts.external.template.md) for optional chat QA). @@ -74,6 +74,10 @@ Some bundled recipes add optional **`reason`** (TEXT) and **`evidence_json`** (T `high-crap-score` adds **`coverage_source`** (`measured` \| `estimated`) and **`effective_coverage_pct`** on each row — measured when `coverage` has a matching symbol row after `ingest-coverage`; otherwise graph-estimated tiers from test reachability. Goldens assert `coverage_source` when the recipe ships coverage semantics (`high-crap-score`); measured override is covered by `scripts/high-crap-score-measured.test.mjs`. +### Confidence columns (deletion-confidence recipes) + +`coverage-confirmed-dead` adds **`confidence`** (`high` \| `medium`) on each row — **`high`** when static dead and ingested `coverage_pct = 0`; **`medium`** when static dead but the symbol has no ingested coverage row. Also **`reason`**, **`caller_count`**. Goldens: `coverage-confirmed-dead` (post-ingest mix) and `coverage-confirmed-dead-no-ingest` (`preSetup: clear-coverage`, `everyRowFieldEquals` on `confidence: medium`). + --- ## Status diff --git a/docs/plans/agent-enrichment-wave.md b/docs/plans/agent-enrichment-wave.md index d9db64ca..29c75320 100644 --- a/docs/plans/agent-enrichment-wave.md +++ b/docs/plans/agent-enrichment-wave.md @@ -65,4 +65,4 @@ Each PR: `harden-pr full` (includes plan retirement) → merge. Do not batch pla ## Current slice -**Active:** Plan 3 slice **3.4** (optional classifier) or PR harden — agent surfaces + changeset on `feat/coverage-confirmed-dead`. +**Active:** Plan 3 complete on `feat/coverage-confirmed-dead` — run **`/harden-pr full`** → PR **#D** → retire `coverage-deletion-confidence.md` on merge. diff --git a/src/application/context-engine.ts b/src/application/context-engine.ts index 85e21b0b..f4169290 100644 --- a/src/application/context-engine.ts +++ b/src/application/context-engine.ts @@ -168,6 +168,21 @@ export function classifyIntent(intent: string): { hint: "Markers (TODO/FIXME) and deprecated-symbols often hint at known gotchas; fan-in shows the blast radius of a change.", }; } + if ( + /delete dead|dead code|coverage confirmed|confirmed dead|remove unused/.test( + t, + ) + ) { + return { + classified_as: "cleanup", + matched_recipes: [ + "coverage-confirmed-dead", + "untested-and-dead", + "unimported-exports", + ], + hint: "coverage-confirmed-dead splits high (ingested 0%) vs medium (unmeasured); run ingest-coverage before treating rows as measurement-confirmed.", + }; + } if (/test|coverage|spec|mock/.test(t)) { return { classified_as: "test", diff --git a/src/application/mcp-server.ts b/src/application/mcp-server.ts index 76f1bdf0..b519a3d5 100644 --- a/src/application/mcp-server.ts +++ b/src/application/mcp-server.ts @@ -300,7 +300,7 @@ function registerIngestCoverageTool(server: McpServer, opts: ServerOpts): void { "ingest_coverage", withToolAnnotations("ingest_coverage", { description: - "Ingest a coverage artifact (Istanbul JSON, LCOV, or NODE_V8_COVERAGE directory with `runtime: true`) into the index `coverage` table. Same JSON envelope as `codemap ingest-coverage --json`. Enables coverage-aware recipes (`worst-covered-exports`, `files-by-coverage`, `untested-and-dead`). Args: `path` (required), `runtime` (optional).", + "Ingest a coverage artifact (Istanbul JSON, LCOV, or NODE_V8_COVERAGE directory with `runtime: true`) into the index `coverage` table. Same JSON envelope as `codemap ingest-coverage --json`. Enables coverage-aware recipes (`worst-covered-exports`, `files-by-coverage`, `untested-and-dead`, `coverage-confirmed-dead`). Args: `path` (required), `runtime` (optional).", inputSchema: ingestCoverageArgsSchema, }), async (args) => wrapToolResult(await handleIngestCoverage(args, opts.root)), diff --git a/src/cli/cmd-context.test.ts b/src/cli/cmd-context.test.ts index 6646c6d5..82d1ce3d 100644 --- a/src/cli/cmd-context.test.ts +++ b/src/cli/cmd-context.test.ts @@ -94,6 +94,12 @@ describe("classifyIntent", () => { expect(classifyIntent("debug regression").classified_as).toBe("debug"); }); + it("classifies cleanup / dead-code intent", () => { + const r = classifyIntent("delete dead code with coverage confirmed"); + expect(r.classified_as).toBe("cleanup"); + expect(r.matched_recipes).toContain("coverage-confirmed-dead"); + }); + it("classifies test intent", () => { expect(classifyIntent("add coverage for parser").classified_as).toBe( "test", diff --git a/templates/agent-content/mcp-instructions.md b/templates/agent-content/mcp-instructions.md index 68b863d7..2c1b02ad 100644 --- a/templates/agent-content/mcp-instructions.md +++ b/templates/agent-content/mcp-instructions.md @@ -29,32 +29,32 @@ Key fields: `pending_sync` (watcher debounce queue or in-flight reindex), `commi ## Common tasks -| Goal | MCP tool | Recipe twin (`query_recipe`) | -| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| Exact symbol lookup | **`show`** (`name`, optional `in`) | `find-symbol-definitions` | -| Field-qualified symbol discovery | **`show`** or **`snippet`** (`query` with `kind:` / `name:` / `path:` / `in:` + free text) | `find-symbol-by-kind` for kind-heavy patterns; CLI `codemap show --query '…' --print-sql` to inspect generated SQL (no MCP `print_sql` arg) | -| Kind / pattern lookup | **`query_recipe`** | `find-symbol-by-kind` | -| Source at symbol | **`snippet`** | same rows as `show` + disk text | -| Blast radius | **`impact`** (`target`, `direction`, `via`, `depth`) | `fan-in` for file hubs; symbol call graph via SQL or `impact` | -| Call path + snippets | **`trace`** (`from`, `to`, `via?`, `max_depth?`, `budget_chars?`) — adaptive snippet caps 15k/10k/6k when omitted | `call-path` | -| Type extends / implements chain | **`query_recipe`** | `type-ancestors`, `type-descendants` (`file_path` when homonyms; on `type-descendants` also scopes output to that file) | -| Multi-symbol survey | **`explore`** (`names`, `depth?`, `kind?`, `budget_chars?`) — row cap always adaptive (500/250/125); snippets 15k/10k/6k when `budget_chars` omitted | `symbol-neighborhood` (once per name) | -| One-hop symbol card | **`node`** (`name`, `kind?`, `in?`, `include_snippets?`, `budget_chars?`) — adaptive snippet caps when snippets enabled | `show` + `symbol-neighborhood` with `depth=1` | -| Affected tests | **`affected`** (`paths?`, `changed_since?`, `test_glob?`, `max_depth?`) | `affected-tests` (RS-delimit multiple paths in `query_recipe` params) | -| CI / SARIF | **`query_recipe`** + `format: "sarif"` | `deprecated-symbols`, `boundary-violations`, … | -| GitLab Code Quality | **`query_recipe`** + `format: "codeclimate"` | `boundary-violations`, … — locatable rows only; flat `minor` severity | -| CI badge / issue count | **`query_recipe`** + `format: "badge"` (+ `badge_style: "json"` for gates) | presentation only — triage via JSON rows / `--summary` | -| Ad-hoc SQL | **`query`** | — | -| N statements / one round-trip | **`query_batch`** | **`codemap query batch`** | -| Index freshness (index-level) | **`context`** (`index_freshness`) + tool metadata above | — | -| Per-file staleness | **`validate`** | — | -| Drift vs baseline | **`audit`** (`baseline_prefix` and/or per-delta `baselines`) or **`query`** / **`query_recipe`** + `baseline` (one-shot row diff vs `query_baselines`; incompatible with non-`json` `format` / `group_by`) | save via **`save_baseline`**; `summary: true` → count-only diff | -| Load coverage data | **`ingest_coverage`** (`path`, optional `runtime` for V8 dirs; auto-detects Istanbul `.json` / LCOV `.info`) | enables `worst-covered-exports`, `files-by-coverage`, `untested-and-dead`; **`high-crap-score`** uses measured rows (`coverage_source: measured`) over graph tiers | -| Complex + undertested (no ingest) | **`query_recipe`** `high-crap-score` | graph-estimated 85/40/0% tiers — parse `coverage_source` before CI gates; prefer after **`ingest_coverage`** when possible | -| High-judgment recipe triage | **`query_recipe`** `unimported-exports`, `boundary-violations`, `deprecated-symbols` | rows include `reason` / `evidence_json` — cite before **`apply`** or deletion | -| Apply recipe diff rows | **`apply`** (`recipe`, `params?`, `dry_run?`, `yes?`, `force?`, `until_empty?`, `max_passes?`, `commit_message?`) | recipe must emit `{file_path, line_start, before_pattern, after_pattern}` rows; `yes: true` required for writes; non-`auto_fixable` recipes need `force: true` | -| Apply agent/codemod rows | **`apply_rows`** (`rows`, `dry_run?`, `yes?`) | same row contract; bypasses recipe `auto_fixable` / allowlist gates | -| Apply unified diff text | **`apply_diff_input`** (`diff_text`, `dry_run?`, `yes?`, `commit_message?`) | parses git-style hunks; same executor as `apply_rows` | +| Goal | MCP tool | Recipe twin (`query_recipe`) | +| --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Exact symbol lookup | **`show`** (`name`, optional `in`) | `find-symbol-definitions` | +| Field-qualified symbol discovery | **`show`** or **`snippet`** (`query` with `kind:` / `name:` / `path:` / `in:` + free text) | `find-symbol-by-kind` for kind-heavy patterns; CLI `codemap show --query '…' --print-sql` to inspect generated SQL (no MCP `print_sql` arg) | +| Kind / pattern lookup | **`query_recipe`** | `find-symbol-by-kind` | +| Source at symbol | **`snippet`** | same rows as `show` + disk text | +| Blast radius | **`impact`** (`target`, `direction`, `via`, `depth`) | `fan-in` for file hubs; symbol call graph via SQL or `impact` | +| Call path + snippets | **`trace`** (`from`, `to`, `via?`, `max_depth?`, `budget_chars?`) — adaptive snippet caps 15k/10k/6k when omitted | `call-path` | +| Type extends / implements chain | **`query_recipe`** | `type-ancestors`, `type-descendants` (`file_path` when homonyms; on `type-descendants` also scopes output to that file) | +| Multi-symbol survey | **`explore`** (`names`, `depth?`, `kind?`, `budget_chars?`) — row cap always adaptive (500/250/125); snippets 15k/10k/6k when `budget_chars` omitted | `symbol-neighborhood` (once per name) | +| One-hop symbol card | **`node`** (`name`, `kind?`, `in?`, `include_snippets?`, `budget_chars?`) — adaptive snippet caps when snippets enabled | `show` + `symbol-neighborhood` with `depth=1` | +| Affected tests | **`affected`** (`paths?`, `changed_since?`, `test_glob?`, `max_depth?`) | `affected-tests` (RS-delimit multiple paths in `query_recipe` params) | +| CI / SARIF | **`query_recipe`** + `format: "sarif"` | `deprecated-symbols`, `boundary-violations`, … | +| GitLab Code Quality | **`query_recipe`** + `format: "codeclimate"` | `boundary-violations`, … — locatable rows only; flat `minor` severity | +| CI badge / issue count | **`query_recipe`** + `format: "badge"` (+ `badge_style: "json"` for gates) | presentation only — triage via JSON rows / `--summary` | +| Ad-hoc SQL | **`query`** | — | +| N statements / one round-trip | **`query_batch`** | **`codemap query batch`** | +| Index freshness (index-level) | **`context`** (`index_freshness`) + tool metadata above | — | +| Per-file staleness | **`validate`** | — | +| Drift vs baseline | **`audit`** (`baseline_prefix` and/or per-delta `baselines`) or **`query`** / **`query_recipe`** + `baseline` (one-shot row diff vs `query_baselines`; incompatible with non-`json` `format` / `group_by`) | save via **`save_baseline`**; `summary: true` → count-only diff | +| Load coverage data | **`ingest_coverage`** (`path`, optional `runtime` for V8 dirs; auto-detects Istanbul `.json` / LCOV `.info`) | enables `worst-covered-exports`, `files-by-coverage`, `untested-and-dead`, **`coverage-confirmed-dead`** (`confidence: high`); **`high-crap-score`** measured override | +| Complex + undertested (no ingest) | **`query_recipe`** `high-crap-score` | graph-estimated 85/40/0% tiers — parse `coverage_source` before CI gates; prefer after **`ingest_coverage`** when possible | +| High-judgment recipe triage | **`query_recipe`** `unimported-exports`, `boundary-violations`, `deprecated-symbols` | rows include `reason` / `evidence_json` — cite before **`apply`** or deletion | +| Apply recipe diff rows | **`apply`** (`recipe`, `params?`, `dry_run?`, `yes?`, `force?`, `until_empty?`, `max_passes?`, `commit_message?`) | recipe must emit `{file_path, line_start, before_pattern, after_pattern}` rows; `yes: true` required for writes; non-`auto_fixable` recipes need `force: true` | +| Apply agent/codemod rows | **`apply_rows`** (`rows`, `dry_run?`, `yes?`) | same row contract; bypasses recipe `auto_fixable` / allowlist gates | +| Apply unified diff text | **`apply_diff_input`** (`diff_text`, `dry_run?`, `yes?`, `commit_message?`) | parses git-style hunks; same executor as `apply_rows` | ## Chains @@ -74,6 +74,6 @@ Key fields: `pending_sync` (watcher debounce queue or in-flight reindex), `commi ## Recipe ids cited here -`find-symbol-definitions`, `find-symbol-by-kind`, `find-symbol-references`, `fan-in`, `call-path`, `symbol-neighborhood`, `type-ancestors`, `type-descendants`, `affected-tests`, `deprecated-symbols`, `boundary-violations`, `unimported-exports`, `high-crap-score`, `refactor-risk-ranking`, `calls-including-heuristic` (opt-in; requires `synthesis.heuristicCalls: true` in config). Default call-graph recipes exclude `provenance = 'heuristic'`. Others: list via **`codemap://recipes`** before **`query_recipe`**. +`find-symbol-definitions`, `find-symbol-by-kind`, `find-symbol-references`, `fan-in`, `call-path`, `symbol-neighborhood`, `type-ancestors`, `type-descendants`, `affected-tests`, `deprecated-symbols`, `boundary-violations`, `unimported-exports`, `coverage-confirmed-dead`, `high-crap-score`, `refactor-risk-ranking`, `calls-including-heuristic` (opt-in; requires `synthesis.heuristicCalls: true` in config). Default call-graph recipes exclude `provenance = 'heuristic'`. Others: list via **`codemap://recipes`** before **`query_recipe`**. - + diff --git a/templates/agent-content/rule/00-full.md b/templates/agent-content/rule/00-full.md index 43bc2ac6..eec48664 100644 --- a/templates/agent-content/rule/00-full.md +++ b/templates/agent-content/rule/00-full.md @@ -24,6 +24,8 @@ codemap query --recipes-json # canonical list of every bundled + p **Coverage columns:** `high-crap-score` rows add **`coverage_source`** (`measured` \| `estimated`) and **`effective_coverage_pct`** — measured when `ingest-coverage` has a symbol row; else graph tiers 85/40/0% from test reachability (heuristic, not execution). +**Confidence columns:** `coverage-confirmed-dead` rows add **`confidence`** (`high` \| `medium`) — `high` when static dead and ingested `coverage_pct = 0`; `medium` when dead but unmeasured. Parse before deletion. + ## Trigger patterns If the question matches any of these, use the index instead of grepping: @@ -58,6 +60,7 @@ If the question matches any of these, use the index instead of grepping: | "What's the nesting depth of X?" | `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` | +| "Dead exports with ingested zero coverage?" | `--recipe coverage-confirmed-dead` (check `confidence`: `high` vs `medium`) | | "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` | diff --git a/templates/agent-content/skill/10-recipes-context.md b/templates/agent-content/skill/10-recipes-context.md index eae47cc7..726c6a54 100644 --- a/templates/agent-content/skill/10-recipes-context.md +++ b/templates/agent-content/skill/10-recipes-context.md @@ -4,7 +4,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|codeclimate|badge`, `--badge-style markdown|json`, `--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 `#`, `--`, `