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 90df7ccf..f70c9419 100644 --- a/docs/plans/agent-enrichment-wave.md +++ b/docs/plans/agent-enrichment-wave.md @@ -1,42 +1,29 @@ -# Agent enrichment wave — tracer workflow (plans 3–4) +# Agent enrichment wave — tracer workflow (plan 4) -> **Status:** in-flight · **Scope:** remaining P2 plans ranked by consumer/agent ROI +> **Status:** in-flight · **Scope:** remaining P2 plan ranked by consumer/agent ROI > > **Goal:** Ship tracer bullets that cut agent round-trips, improve answer trust, and sharpen PR/CI deltas — all Moat-A (predicate columns, no verdict primitives). > -> **Shipped (plans retired):** Evidence chains ([#174](https://github.com/stainless-code/codemap/pull/174)) · Graph-estimated CRAP ([#175](https://github.com/stainless-code/codemap/pull/175)) — durable contract in `golden-queries.md` + `architecture.md`; plan files deleted per [docs-governance](../../.agents/skills/docs-governance/SKILL.md) § Closing a plan. +> **Shipped (plans retired):** Evidence chains ([#174](https://github.com/stainless-code/codemap/pull/174)) · Graph-estimated CRAP ([#175](https://github.com/stainless-code/codemap/pull/175)) · Coverage deletion confidence (PR **#D**) — durable contracts in `golden-queries.md` + `architecture.md`; plan files deleted per [docs-governance](../../.agents/skills/docs-governance/SKILL.md) § Closing a plan. > -> **Remaining:** [coverage-deletion-confidence](./coverage-deletion-confidence.md) → [audit-delta-attribution](./audit-delta-attribution.md) +> **Remaining:** [audit-delta-attribution](./audit-delta-attribution.md) --- ## Shared conventions (locked) -| Convention | Applies to | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | -| **Moat A** — no `pass`/`fail` engine verdict; extra columns only | All | -| **`reason` TEXT** — machine code + short clause where useful | #3 | -| **`evidence_json` TEXT** — bounded JSON array (≤3 hops) | shipped #1 | -| **`confidence` / `coverage_source` / `attribution`** — recipe-specific enums | #3, #4 | -| **Golden update per slice** — `fixtures/golden/minimal/*.json` + `scenarios.json` | All | -| **`/harden-pr lite`** after each tracer commit; **`/harden-pr full`** before PR merge | All | -| **Retire plan on merge** — delete `docs/plans/.md` + lift to reference docs/roadmap in the **same PR** (never leave shipped plans as leftovers) | All | -| **No deferring complements** — agent surfaces (rule/skill/MCP), glossary, golden/script tests, and plan acceptance items ship **in the same PR** unless explicitly listed under plan **Out of scope** | All | +| Convention | Applies to | +| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | +| **Moat A** — no `pass`/`fail` engine verdict; extra columns only | All | +| **`reason` TEXT** — machine code + short clause where useful | shipped | +| **`evidence_json` TEXT** — bounded JSON array (≤3 hops) | shipped | +| **`confidence` / `coverage_source` / `attribution`** — recipe-specific enums | shipped #3, #4 | +| **Golden update per slice** — `fixtures/golden/minimal/*.json` + `scenarios.json` | All | +| **`/harden-pr lite`** after each tracer commit; **`/harden-pr full`** before PR merge | All | +| **Retire plan on merge** — delete `docs/plans/.md` + lift to reference docs/roadmap in the **same PR** (never leave shipped plans as leftovers) | All | +| **No deferring complements** — agent surfaces (rule/skill/MCP), glossary, golden/script tests, and plan acceptance items ship in the **same PR** unless explicitly listed under plan **Out of scope** | All | -**Cross-plan synergy:** shipped evidence `reason` complements #4 `attribution` on audit `added` rows. CRAP `coverage_source` (#175) ships before #3 so deletion-confidence can narrow rows with coverage semantics. - ---- - -## Plan 3 — Coverage deletion confidence (`coverage-deletion-confidence.md`) - -| Slice | Deliverable | Verify | -| -------------------------- | -------------------------------------------------------------- | ------------- | -| **3.1 recipe fork** | `coverage-confirmed-dead.sql` + `.md` from `untested-and-dead` | query CLI | -| **3.2 golden no-ingest** | `confidence: medium` policy (per D.4) | `test:golden` | -| **3.3 golden with ingest** | fixture coverage → `confidence: high` | `test:golden` | -| **3.4 classifier** | intent keywords if needed | optional | - -**Grill before 3.1:** Q3 without ingest — `medium` rows vs empty + stderr (plan D.4 leans medium rows). +**Cross-plan synergy:** shipped evidence `reason` complements #4 `attribution` on audit `added` rows. Shipped `confidence` (#D) narrows deletion triage after `ingest-coverage`. --- @@ -54,15 +41,14 @@ ## PR cadence -| PR | Contents | Changeset | Retire plan on merge | -| -------------------------- | --------------- | --------- | --------------------------------- | -| **#D Deletion confidence** | Plan 3 complete | patch | `coverage-deletion-confidence.md` | -| **#E Audit attribution** | Plan 4 complete | patch | `audit-delta-attribution.md` | +| PR | Contents | Changeset | Retire plan on merge | +| ------------------------ | --------------- | --------- | ---------------------------- | +| **#E Audit attribution** | Plan 4 complete | patch | `audit-delta-attribution.md` | -Each PR: `harden-pr full` (includes plan retirement) → merge. Do not batch plans 3–4 into one PR. +Each PR: `harden-pr full` (includes plan retirement) → merge. --- ## 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 4 slice **4.1** on fresh branch from `main` after **#D** merges — `findingKey()` helper + unit tests. diff --git a/docs/plans/coverage-deletion-confidence.md b/docs/plans/coverage-deletion-confidence.md deleted file mode 100644 index 04ae7a02..00000000 --- a/docs/plans/coverage-deletion-confidence.md +++ /dev/null @@ -1,116 +0,0 @@ -# Coverage × static deletion-confidence recipe — plan - -> **Status:** open · **Priority:** P2 · **Effort:** L–M (~2–3 weeks) -> -> **Motivator:** `untested-and-dead` surfaces statically uncalled exports with zero coverage — strong candidates but still "verify before delete." When **both** static dead-code signals **and** ingested coverage show zero (or below-threshold) execution, agents need a tighter predicate row set for cleanup triage — without a `pass`/`fail` verdict primitive. -> -> **Roadmap:** [§ Recipe & audit enrichment](../roadmap.md#recipe--audit-enrichment) - ---- - -## Agent start here - -**Fork `untested-and-dead.sql`** — same static dead predicate + coverage JOIN — then add `confidence` column logic. No schema change in v1. Read [`templates/recipes/untested-and-dead.md`](../../templates/recipes/untested-and-dead.md) for C.9 caveat text to reuse. - -### Key touchpoints - -| File | What to read | -| ------------------------------------------------------------------------------------------ | ------------------------------------------ | -| [`templates/recipes/untested-and-dead.sql`](../../templates/recipes/untested-and-dead.sql) | Core dead + coverage predicate | -| [`templates/recipes/untested-and-dead.md`](../../templates/recipes/untested-and-dead.md) | Framework false-positive disclaimer | -| [`src/db.ts`](../../src/db.ts) | `coverage`, `calls`, `suppressions` tables | -| [`docs/golden-queries.md`](../golden-queries.md) | Golden with + without coverage ingest | -| [`src/cli/cmd-ingest-coverage.ts`](../../src/cli/cmd-ingest-coverage.ts) or ingest path | Prerequisite for `high` confidence rows | - -### Architecture - -```text -recipe coverage-confirmed-dead (SQL fork) - → static dead core (untested-and-dead) - → JOIN coverage: pct = 0 or absent - → confidence: high (ingested zero) | medium (no ingest) - → reason column (Moat A — not engine verdict) -``` - -### Tracer bullet (slice 1) - -New `.sql` + `.md` + golden fixture with minimal Istanbul/LCOV ingest → one `high` row. Second scenario without ingest → `medium` only (or documented empty policy per D.4). - -### Out of scope (v1) - -`max_coverage_pct` param (v2); inverse "live + zero coverage" recipe; schema migration. - ---- - -## Pre-locked decisions - -| # | Decision | Source | -| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | -| D.1 | **Recipe-only** — bundled id `coverage-confirmed-dead` (name TBD in impl PR); expressible as `query --recipe`; no standalone `codemap check-dead` verb. | [Moat A](../roadmap.md#moats-load-bearing) | -| D.2 | **Cross-product rows** — JOIN static dead predicate (same core as `untested-and-dead`: no incoming `calls`, export visibility, suppressions honored) WITH `coverage` where `coverage_pct = 0` OR row absent treated as 0. | Existing `untested-and-dead.sql` + `coverage` table | -| D.3 | **Explicit `confidence` column** — string enum on each row: `high` (static dead + zero coverage ingested), `medium` (static dead, no coverage ingest — same as untested-and-dead today), not a top-level verdict. | Moat A — predicate columns, not engine verdict | -| D.4 | **Requires ingest** — recipe frontmatter documents `codemap ingest-coverage` prerequisite; empty `coverage` table → only `medium` rows (or stderr hint when all rows medium). | [ingest-coverage](../architecture.md) | -| D.5 | **Complements C.9** — framework false-positives remain until `files.is_entry` ships; recipe description cites C.9 caveat (same as `untested-and-dead.md`). | [c9-plugin-layer](./c9-plugin-layer.md) | -| D.6 | **Inverse recipe deferred** — "statically live + zero coverage" (risky untested hot path) is a separate recipe (`high-complexity-untested` partial overlap); out of scope for v1. | Tracer bullet | - ---- - -## Row shape (sketch) - -| Column | Meaning | -| ----------------------------------------- | ----------------------------------------------- | -| `name`, `file_path`, `line_start`, `kind` | Symbol identity | -| `coverage_pct` | From `coverage` JOIN (0 or NULL→0) | -| `caller_count` | Fan-in from `calls` (0 for dead) | -| `confidence` | `high` \| `medium` | -| `reason` | Short text: e.g. `no_callers_and_zero_coverage` | - -Optional v2: param `max_coverage_pct` (default 0) for "cold but not literally zero." - ---- - -## Implementation steps - -1. Author `templates/recipes/coverage-confirmed-dead.sql` + `.md` (suppressions, C.9 caveat, ingest prerequisite). -2. Golden query scenario in `fixtures/golden/` with minimal coverage fixture. -3. Register in recipe catalog; intent keywords in `context --for` classifier ("delete dead code", "coverage confirmed"). -4. SARIF / annotations compatible via existing `--format` (location columns present). -5. No schema change required if v1 is pure SQL over existing tables. - ---- - -### Verification - -```bash -bun src/index.ts query --recipe coverage-confirmed-dead --json # without ingest → medium -bun src/index.ts ingest-coverage && bun src/index.ts query --recipe coverage-confirmed-dead --json -bun test scripts/query-golden-coverage-matrix.test.mjs -``` - -Compare row counts with `untested-and-dead` on same index — subset should be narrower when coverage ingested. - ---- - -## Acceptance - -- [ ] With coverage ingest: uncalled export at 0% → `confidence: high` -- [ ] Without coverage ingest: same symbol → `confidence: medium` (or documented empty result policy) -- [ ] Suppressions for `untested-and-dead` honored (shared recipe-id or explicit join — decide in impl PR) -- [ ] Expressible as `codemap query --recipe coverage-confirmed-dead --json` - ---- - -## Open decisions (impl PR) - -| # | Question | -| --- | ------------------------------------------------------------------------------------------- | -| Q1 | Recipe id: `coverage-confirmed-dead` vs `deletion-confidence`? | -| Q2 | Share suppressions recipe-id with `untested-and-dead` or explicit duplicate in frontmatter? | -| Q3 | Without coverage ingest: emit `medium` rows or empty set + stderr hint? | - ---- - -## Dependencies - -- Shipped: `coverage` table, `ingest-coverage`, `untested-and-dead`, `calls`, `suppressions` -- Optional synergy: [audit-delta-attribution](./audit-delta-attribution.md) for PR-scoped runs with `--changed-since` diff --git a/docs/roadmap.md b/docs/roadmap.md index 9e041100..06d8a00a 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -90,7 +90,7 @@ Predicate-as-API only — enrich row shape and audit deltas; no standalone pass/ - [x] **Evidence chains on recipe rows** — shipped on `boundary-violations`, `deprecated-symbols`, `unimported-exports` (`reason` + `evidence_json`; includes `reexport_chain_possible` and `unresolved_import_blind_spot`). Contract: [golden-queries.md § Evidence columns](./golden-queries.md#evidence-columns-high-judgment-recipes). v2 optional: audit `added` attribution merge ([Moat A](./roadmap.md#moats-load-bearing)). - [ ] **Tiered lookup fast paths** — `show` / exact-name recipe paths hit covering indexes first; document latency expectations in MCP tool descriptions. FTS and broad scans remain explicit fallbacks. Effort: S–M. - [x] **Graph-estimated CRAP recipe** — bundled `high-crap-score`: CRAP = `CC² × (1 - coverage/100)³ + CC` using `symbols.complexity`; **measured** `coverage` when ingested, else **graph-estimated** tiers (85% / 40% / 0% from test-file reachability over `dependencies` / `calls` / `test_suites`). Rows expose `coverage_source: measured | estimated`. Contract: [golden-queries.md § Coverage columns](./golden-queries.md#coverage-columns-crap--enrichment-recipes). Complements `high-complexity-untested` when no coverage file exists. -- [ ] **Coverage-confirmed dead recipe** — bundled `coverage-confirmed-dead`: JOIN static dead-code predicate (uncalled exports, suppression-aware) with ingested `coverage` — rows carry `confidence: high` when callers = 0 and `coverage_pct = 0`, `medium` when coverage not ingested. Predicate columns only, no verdict primitive ([Moat A](./roadmap.md#moats-load-bearing)). Plan: [`plans/coverage-deletion-confidence.md`](./plans/coverage-deletion-confidence.md). Effort: L–M. +- [x] **Coverage-confirmed dead recipe** — bundled `coverage-confirmed-dead`: static dead predicate (uncalled exports, suppression-aware) with **`confidence: high`** when ingested `coverage_pct = 0`, **`medium`** when unmeasured. Predicate columns only, no verdict primitive ([Moat A](./roadmap.md#moats-load-bearing)). Contract: [golden-queries.md § Confidence columns](./golden-queries.md#confidence-columns-deletion-confidence-recipes). ### Distribution & evaluation depth 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/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index 899c3f14..8436d04f 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -13,9 +13,9 @@ }, { "path": "README.md", - "content_hash": "2ddba8d7d07352340f7d76a8bbe542335cc84ef3a32681afb01ad3734929391c", + "content_hash": "9f557281e7bed140f9ab0d8e792c7b22abcbbd6a718f01eeb5207e19f1f38259", "language": "md", - "line_count": 59 + "line_count": 60 }, { "path": "package.json", diff --git a/fixtures/golden/minimal/files-largest.json b/fixtures/golden/minimal/files-largest.json index a1ef33ed..2797e814 100644 --- a/fixtures/golden/minimal/files-largest.json +++ b/fixtures/golden/minimal/files-largest.json @@ -7,8 +7,8 @@ }, { "path": "README.md", - "line_count": 59, - "size": 9819, + "line_count": 60, + "size": 9922, "language": "md" }, { 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/fixtures/minimal/README.md b/fixtures/minimal/README.md index 5681d3ce..f14ec763 100644 --- a/fixtures/minimal/README.md +++ b/fixtures/minimal/README.md @@ -50,9 +50,10 @@ bun run test:golden -- --update # Benchmark CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun run benchmark -# Coverage ingest + killer recipe +# Coverage ingest + killer recipes CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts ingest-coverage coverage/coverage-final.json CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts query --recipe untested-and-dead --json +CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts query --recipe coverage-confirmed-dead --json ``` **Editor / `tsc`:** run `bun install` here so `react` + `@types/react` resolve `react/jsx-runtime` for `.tsx` (`jsx: "react-jsx"` in `tsconfig.json`). 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(), 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..ae2001ea 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` | @@ -84,6 +87,7 @@ If the question matches any of these, use the index instead of grepping: | Deprecated symbols | `SELECT name, kind, file_path FROM symbols WHERE doc_comment LIKE '%@deprecated%'` | | 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` | +| Coverage-confirmed dead | `codemap query --json --recipe coverage-confirmed-dead` (sort by `confidence`) | ## When Grep / Read IS appropriate 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 `#`, `--`, `