Skip to content

Commit 96c504c

Browse files
committed
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.
1 parent 0900140 commit 96c504c

11 files changed

Lines changed: 69 additions & 32 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stainless-code/codemap": patch
3+
---
4+
5+
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.

docs/architecture.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ Three **mutually exclusive** CLI entry shapes; all converge on `applyDiffPayload
194194

195195
**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).
196196

197+
**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).
198+
197199
**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: **`<id>.sql`** + optional **`<id>.md`**. The loader reads `templates/recipes/` (bundled, ships in npm package next to `templates/agents/`) and `<state-dir>/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 `<id>.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). `<state-dir>/index.db` is gitignored; `<state-dir>/recipes/` is NOT (verified via `git check-ignore`) — recipes are git-tracked source code authored for human review.
198200

199201
**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.

docs/glossary.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ Statement coverage ingested from Istanbul JSON, LCOV, or V8 runtime (`NODE_V8_CO
170170
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):
171171

172172
- `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`).
173+
- `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`.
173174
- `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`).
174175
- `worst-covered-exports` — top-20 worst-covered exported functions.
175176

docs/golden-queries.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ We **do not** commit another product’s source tree, paths, business strings, o
6262

6363
## Scenario shape (implemented)
6464

65-
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": "<relative-to-fixture>" }`** (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`**.
65+
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`**.
6666

6767
**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).
6868

@@ -74,6 +74,10 @@ Some bundled recipes add optional **`reason`** (TEXT) and **`evidence_json`** (T
7474

7575
`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`.
7676

77+
### Confidence columns (deletion-confidence recipes)
78+
79+
`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`).
80+
7781
---
7882

7983
## Status

docs/plans/agent-enrichment-wave.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,4 @@ Each PR: `harden-pr full` (includes plan retirement) → merge. Do not batch pla
6565

6666
## Current slice
6767

68-
**Active:** Plan 3 slice **3.4** (optional classifier) or PR harden — agent surfaces + changeset on `feat/coverage-confirmed-dead`.
68+
**Active:** Plan 3 complete on `feat/coverage-confirmed-dead` — run **`/harden-pr full`** → PR **#D** → retire `coverage-deletion-confidence.md` on merge.

src/application/context-engine.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,21 @@ export function classifyIntent(intent: string): {
168168
hint: "Markers (TODO/FIXME) and deprecated-symbols often hint at known gotchas; fan-in shows the blast radius of a change.",
169169
};
170170
}
171+
if (
172+
/delete dead|dead code|coverage confirmed|confirmed dead|remove unused/.test(
173+
t,
174+
)
175+
) {
176+
return {
177+
classified_as: "cleanup",
178+
matched_recipes: [
179+
"coverage-confirmed-dead",
180+
"untested-and-dead",
181+
"unimported-exports",
182+
],
183+
hint: "coverage-confirmed-dead splits high (ingested 0%) vs medium (unmeasured); run ingest-coverage before treating rows as measurement-confirmed.",
184+
};
185+
}
171186
if (/test|coverage|spec|mock/.test(t)) {
172187
return {
173188
classified_as: "test",

src/application/mcp-server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ function registerIngestCoverageTool(server: McpServer, opts: ServerOpts): void {
300300
"ingest_coverage",
301301
withToolAnnotations("ingest_coverage", {
302302
description:
303-
"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).",
303+
"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).",
304304
inputSchema: ingestCoverageArgsSchema,
305305
}),
306306
async (args) => wrapToolResult(await handleIngestCoverage(args, opts.root)),

src/cli/cmd-context.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ describe("classifyIntent", () => {
9494
expect(classifyIntent("debug regression").classified_as).toBe("debug");
9595
});
9696

97+
it("classifies cleanup / dead-code intent", () => {
98+
const r = classifyIntent("delete dead code with coverage confirmed");
99+
expect(r.classified_as).toBe("cleanup");
100+
expect(r.matched_recipes).toContain("coverage-confirmed-dead");
101+
});
102+
97103
it("classifies test intent", () => {
98104
expect(classifyIntent("add coverage for parser").classified_as).toBe(
99105
"test",

0 commit comments

Comments
 (0)