Skip to content

Commit b5679a6

Browse files
feat(coverage): static coverage ingestion (Istanbul + LCOV) — impl of PR #56 plan (#57)
* feat(coverage): add `coverage` table + SCHEMA_VERSION 5→6 (Tracer 1) First tracer of the static coverage ingestion plan (docs/plans/coverage-ingestion.md). Pure schema change, no engine yet. What lands: - New `coverage` table with natural-key PK (file_path, name, line_start) per D6. Deliberately NOT a FK to symbols.id: dropAll() drops symbols on every --full reindex and the recreated rows get fresh AUTOINCREMENT ids; CASCADE would wipe coverage on every full rebuild. Natural key sidesteps the entire FK/CASCADE hazard. Orphan cleanup (file deleted from project) lives at the end of every ingest in tracer 2a — exercised by the new test using a raw DELETE so the contract is locked in before the engine code lands. - `idx_coverage_file_name` mirrors the typical join shape (the killer recipe joins symbols ↔ coverage on file_path/name/line_start) and the (file_path, name) prefix also covers the GROUP BY file_path scan used by the bundled files-by-coverage recipe (D2 + D13). - `coverage` table is intentionally absent from `dropAll()` (joins the query_baselines precedent), so user rows survive --full and SCHEMA_VERSION-mismatch rebuilds. - SCHEMA_VERSION bumped 5→6. Existing installs auto-rebuild on next `codemap` run via the schema-mismatch path in createSchema(); the new table is empty until first `codemap ingest-coverage` invocation (tracer 3), which is the correct initial state per D12. - New db.test.ts case exercises the full contract: round-trip of a partial-coverage row set including the total_statements=0 → NULL edge (D5), survival across dropAll() + createTables(), and the orphan- cleanup DELETE that ships in tracer 2a's engine. * feat(coverage): shared upsert core + Istanbul parser (Tracer 2a) Pure engine + format-agnostic write path. No CLI / FS side effects beyond db.run on the caller-supplied SQLite handle. `upsertCoverageRows({db, projectRoot, rows, format, sourcePath})` — shared core consumed by both Istanbul (this commit) and LCOV (next): - Normalises absolute paths via toProjectRelative (D8); files outside the project root land in skipped.unmatched_files. - Loads symbols per-file once and projects each statement onto the innermost enclosing symbol via JS-side (line_end - line_start) ASC tie-break (D7) — avoids the per-statement SQL round-trip the plan flagged as the hot path. - Aggregates per (file_path, name, line_start) bucket; total = 0 → coverage_pct NULL (D5 edge — "untested" ≠ "no testable code"). - Single transaction: per-file DELETE + bulk INSERT for idempotent re-ingest, then orphan-cleanup DELETE (D6 — no FK / CASCADE means deleted-file rows accumulate without this sweep), then writes the three coverage_last_ingested_* meta keys. `ingestIstanbul({db, projectRoot, payload, sourcePath})` — Istanbul parser front-end: - Subset-typed `IstanbulPayload` reads only statementMap + s; ignores fnMap / branchMap / inputSourceMap so the format can grow without churning the signature. - Inner `path` field takes precedence over the absolute-path key (handles webpack-style symlinked paths). - Tolerates malformed file entries (missing statementMap or s) — skips silently rather than throwing, so one corrupt file doesn't poison the whole ingest. 10 unit tests cover both pieces: - Shared core: innermost-wins aggregation, statement-outside-symbol observability, zero-statement-symbol → no row, UPSERT idempotence, orphan cleanup after file deletion, project-root normalisation, unmatched-file skipping, meta-key writes. - Istanbul parser: real-shape payload end-to-end, malformed-entry tolerance. * feat(coverage): LCOV parser front-end (Tracer 2b) Closes the second format axis from the plan — LCOV (`lcov.info`) ingester that shares the entire write path with the Istanbul ingester from Tracer 2a. `ingestLcov({db, projectRoot, payload, sourcePath})` is a pure regex tokenizer over the LCOV record format. Recognised lines: - `SF:<path>` — start of a file record (sets the "current file") - `DA:<line>,<exec_count>[,<checksum>]` — one statement per record - `end_of_record` — closes the current file record - Comments (`# …`), blank lines, and CRLF endings tolerated Ignored lines (everything else: `TN:`, `FN:`, `FNDA:`, `FNF:`, `FNH:`, `BRDA:`, `BRF:`, `BRH:`, `LF:`, `LH:`) — D5 scope is statement coverage only; the unused records are skipped without churning the parser. Throws on `DA:` outside an `SF:` block (malformed LCOV — the file would have nowhere to attach to). Missing `end_of_record` is tolerated by construction (next `SF:` resets the current file). Five unit tests cover both the LCOV parser in isolation and the cross-format equivalence contract: - `ingestLcov`: well-formed LCOV with multiple SF records → identical-shape coverage rows; CRLF + comments + blank-line tolerance; DA-outside-SF throws; optional `DA:` checksum field parsed. - Cross-format: identical Istanbul + LCOV inputs produce byte-identical rows in the `coverage` table — the contract that lets Tracer 4 ship one set of golden snapshots that asserts both formats land on the same answer. 15 tests total in coverage-engine.test.ts (10 existing + 5 LCOV). Zero new write-side code — the LCOV parser is pure parse-and-normalise into the same `CoverageRow[]` the Istanbul parser produces, then both hand off to `upsertCoverageRows`. * feat(coverage): codemap ingest-coverage CLI verb (Tracer 3) Wires the engine (Tracer 2a/2b) into the CLI surface end-to-end. Smoke- tested against a real Istanbul payload pointed at src/db.ts: ingest writes 2 symbols (openDb 100%, closeDb 0%); query --json reads them back with the expected coverage_pct shape. cli/cmd-ingest-coverage.ts: - Single positional arg + --json. No --source flag (per plan: format is auto-detectable from extension; flag noise dropped). - resolveArtifact() probes the path: .json → istanbul, .info → lcov, directory → looks for both filenames, errors if both or neither present (no precedence guessing — explicit is better than implicit). - File reads use the canonical Node-vs-Bun split: Bun.file().json() / .text() on Bun (native parser perf for multi-MB Istanbul payloads), readFile + JSON.parse on Node. Mirrors config.ts. See packaging.md. - Plain-text terminal output by default; --json emits the IngestResult envelope. Errors emit {"error":"…"} on stdout under --json, plain message on stderr otherwise. Sets process.exitCode (no process.exit) so piped stdout isn't truncated. cli/main.ts: dispatch new verb between snippet and query (lazy import matches every other cmd). cli/bootstrap.ts: validateIndexModeArgs() recognises ingest-coverage as a known subcommand; printCliUsage() lists it in a new "Coverage ingest" section. 7 parser tests cover all paths: requires the subcommand position, help on --help/-h, single-path default, --json flag, missing-path error, unknown-option error, multiple-paths error. Engine + CLI smoke verified locally: $ bun src/index.ts ingest-coverage /tmp/cov.json --json {"ingested":{"symbols":2,"files":1},"skipped":...,"format":"istanbul"} * feat(coverage): bundled recipes + fixture coverage data + goldens (Tracer 4) Closes the agent-surface contract from D13: every common coverage question gets a `--recipe` verb. Three v1 recipes ship under templates/recipes/, auto-discovered by the existing recipes-loader (`bun src/index.ts query --recipes-json` lists 15 recipes total — was 12). Bundled recipes: - `untested-and-dead.{sql,md}` — the killer recipe. Exported functions with no callers AND zero coverage. Recipe MD documents the v1 name-collision lossiness (D11) and three concrete narrowing patterns agents can apply: scope by file_path prefix, exclude default exports (Next.js entry points), restrict to public visibility. - `files-by-coverage.{sql,md}` — files ranked ascending by statement coverage. Replaces the deferred `file_coverage` rollup table (D2): GROUP BY on the symbol-level table is index-bounded by `idx_coverage_file_name`. NULL coverage_pct (zero-statement files) sorted last to avoid drowning out actual zero-coverage files. - `worst-covered-exports.{sql,md}` — top-20 worst-covered exported functions (LIMIT in the SQL, not configurable in v1 — config-driven LIMIT defers until a real consumer asks). Each recipe ships a frontmatter `actions` block (per PR #26) so agents get a per-row follow-up hint in `--json` output. Fixture data: - fixtures/minimal/coverage/coverage-final.json (Istanbul) and fixtures/minimal/coverage/lcov.info (LCOV) — equivalent partial coverage shape covering 10 fixture symbols across 6 files. Both formats use project-relative paths so they work without sed. Golden runner extension (scripts/query-golden.ts + schema.ts): - New `setup` array in scenarios.json runs one-time setup steps after `cm.index()` and before scenarios. Today: `{kind: "ingest-coverage", path: "..."}` — engine auto-detects format by extension. - Schema is backward-compatible: parser accepts either the legacy flat array OR the new `{setup?, scenarios}` object via z.union. - Setup dispatch reuses the engine functions directly (ingestIstanbul, ingestLcov) — same write path the CLI verb uses. 5 new golden snapshots prove the surface end-to-end on the fixture corpus: - coverage-rows-after-ingest.json: raw `coverage` table contents - untested-and-dead.json: 6 dead+untested symbols (incl. legacyClient + epochMs, both @deprecated) - files-by-coverage.json: 6 files ranked from 0% (api/client.ts) to 100% (ProductCard, usePermissions) - worst-covered-exports.json: top exported functions with coverage_pct - (LCOV cross-format equivalence proven by engine unit test, not duplicated in goldens — single setup step per scenarios.json keeps the runner simple) fixtures/minimal/README.md: new "What's exercised" row + Use section with `bun src/index.ts ingest-coverage` worked examples. All 24 golden scenarios pass. Engine unit tests still 15 pass. * docs(coverage): architecture + glossary + agent rule/skill lockstep + changeset (Tracer 5) Closes the plan execution per docs/README.md Rule 10 (agent rule + skill lockstep) and Rule 9 (new domain nouns → glossary same-PR). docs/architecture.md: - application/ list: add coverage-engine.ts (upsertCoverageRows core + ingestIstanbul / ingestLcov parsers). - New § coverage table under Schema with the natural-key PK rationale cross-referencing query_baselines as the dropAll() exclusion precedent. docs/glossary.md: - New `coverage` table entry: PK shape, NULL semantics, orphan cleanup. - New `codemap ingest-coverage` / Istanbul JSON / LCOV / static coverage ingestion entry: format auto-detection, innermost-wins projection rationale (D7), three bundled recipes, no-half-way principle. .agents/rules/codemap.md + templates/agents/rules/codemap.md (lockstep): - Index intro mentions coverage as part of indexed structure. - New CLI table row for `ingest-coverage`. - Four new trigger-pattern rows: "Is symbol X tested?" → coverage table; "What's structurally dead AND untested?" → --recipe untested-and-dead; "Rank files by test coverage" → --recipe files-by-coverage; "Worst-covered exported functions" → --recipe worst-covered-exports. - Two new quick-reference query rows: symbol coverage + bundled recipe. - Drift between .agents/ and templates/agents/ stays CLI-prefix-only (`bun src/index.ts` vs `codemap`). .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/SKILL.md (lockstep): new `coverage` table block with full column schema + the three meta keys. docs/research/fallow.md: - C.11 row marked Shipped (link to plan PR + this commit). - Open question "column on symbols vs separate table?" resolved to "separate table" with D1 + D6 cross-reference. docs/roadmap.md: drop the "Static coverage ingestion" backlog row (per Rule 2 — when a backlog item ships, it leaves the roadmap). docs/plans/coverage-ingestion.md: deleted per Rule 3 (plan files have the lifetime of the work; absorbed into architecture / glossary / agent rule on ship). .changeset/coverage-ingestion.md: minor changeset (per .agents/lessons.md "changesets bump policy" — new tables + SCHEMA_VERSION bump = minor). All 24 golden scenarios pass (5 new — coverage-rows-after-ingest, untested-and-dead, files-by-coverage, worst-covered-exports — plus the existing 19). 737 tests across 43 files, 0 fail. * fix(db): drop semicolon inside `--` line comment in coverage index DDL CI Build (Node, better-sqlite3) failed with "RangeError: The supplied SQL string contains no statements" on `node dist/index.mjs --full`. Root cause is the .agents/lessons.md lesson on naive `;` splitting: the new coverage index comment contained `symbols.{file_path,name,line_start};`, and runSql() splits multi-statement strings on `;` for better-sqlite3 (one statement per prepare). The trailing semicolon inside the `--` comment created an empty fragment that better-sqlite3 rejects. Reworded the comment to use parentheses + a period — same intent, no semicolon. Verified locally via `bun run build && node dist/index.mjs --full` against /tmp project: full index now completes on Node. * fix(coverage): apply CodeRabbit review on PR #57 — 3 valid threads All three CodeRabbit comments fact-check as ✅ Correct. Apply each: 1. **architecture.md:25 — broken anchor** `#static-coverage-ingestion` doesn't exist (the section heading is `### coverage — Statement coverage…`, not "Static coverage ingestion"). Fix: drop the parenthetical anchor link, replace with "schema in § Schema → coverage" pointing at the existing § Schema anchor that does exist. 2. **fixtures/minimal/README.md:24 — overstated deprecation focus.** Said "exercise the join axis against `@deprecated` symbols" but only 1 of 6 golden rows is `@deprecated` (legacyClient); the others (`FormatPrice`, `run`, `_epochSeconds`, `nanoseconds`, `_hiResEpoch`) carry `@internal`/`@alpha`/`@private`/no tag. Reword to "across exported functions of every visibility tag" — accurate and tag-neutral. 3. **untested-and-dead.md:21 — SQL precedence bug in narrowing pattern.** `AND s.visibility IS NULL OR s.visibility = 'public'` parses as `(... AND s.visibility IS NULL) OR (s.visibility = 'public')` per SQL precedence (AND binds tighter than OR). Every row with `visibility = 'public'` would bypass every other WHERE predicate — the agent following this pattern would get a much larger result set than intended. Fix: wrap in parentheses + explicit comment that the parens are load-bearing so future edits don't drop them. files-hashes.json refreshed for the README touch. * fix(docs): bump architecture.md schema version mention 5 → 6 CodeRabbit outside-diff comment on PR #57 (architecture.md:182) — caught that I bumped SCHEMA_VERSION 5 → 6 in db.ts (Tracer 1) but left the human-readable callout in architecture.md unchanged. Now in sync. Per `docs/README.md` Rule 6, "schema version" is explicitly listed as a decision value (not inventory) so the hardcoded number is fine — it just needs to track the constant. * docs(lessons): always construct gh body args via temp file, never heredoc PR #57's body shipped with literal backslash-backtick artifacts everywhere because the heredoc-into-`gh pr create --body` path shell-escaped every backtick. Add it to .agents/lessons.md alongside the existing template- literal backtick lesson — same root cause family (backticks + nested quoting), different surface (gh CLI vs TS template strings). Pattern: Write body → temp file → `gh pr <verb> --body-file <path>` → delete. One extra tool call, zero rendering surprises.
1 parent e100d2c commit b5679a6

36 files changed

Lines changed: 2225 additions & 324 deletions

.agents/lessons.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ Each entry is a single bullet: `- **<topic>** — <lesson>`. Newest entries at t
1212
- **agent rule + skill maintenance** — when shipping a CLI flag, recipe id, recipe `actions` template, schema column, or any agent-queryable surface, update **both** copies of the codemap rule + skill in the **same PR** per [docs/README.md Rule 10](../docs/README.md): `templates/agents/rules/codemap.md` + `templates/agents/skills/codemap/SKILL.md` (ships to npm) **and** `.agents/rules/codemap.md` + `.agents/skills/codemap/SKILL.md` (this clone's mirror). Drift between the two pairs should be CLI-prefix-only (`codemap` vs `bun src/index.ts`). Forgetting this leaves installed agents with a stale view of the CLI — that's how `--summary` / `--changed-since` / `--group-by` / `actions` / `symbols.visibility` shipped without any `templates/agents/` mention until PR #29 retro-fixed it.
1313
- **backticks inside SQL or help-text template literals** — never put a literal backtick inside a `` `...` `` template-literal string. `db.ts` SQL DDL strings (multi-line CREATE TABLE templates) and `printQueryCmdHelp()` (multi-line help text) are both `` `...` `` template literals; an inner backtick — typically a Markdown-style code-fence around a flag like `` `--full` `` — terminates the literal early and the parser blows up several lines later with cryptic "expected `,` or `)`" errors. **Use plain prose in those strings** (`--full` not `` `--full` ``), or escape (`` \` ``) if you really need the character. Hit twice (B.7 + B.6 PR #30); the lesson is general — applies to any TS template literal that gets pasted prose later, not just SQL / help text.
1414
- **STOP-before-Grep applies to symbol lookups too**`Grep` for symbol names like `printQueryResult`, `getCurrentCommit`, `dropAll` violates the [`codemap` rule](rules/codemap.md). The codemap query `SELECT file_path, line_start FROM symbols WHERE name = '<X>'` answers it faster and without scanning. Reach for `Grep` only when the question is content-shaped (regex over file bodies, finding pattern usages inside function bodies, etc.) — not when it's "where is X defined / who calls X / what does file Y export." This was a PR #30 self-correction.
15+
- **PR / issue / comment bodies always go through a temp file** — never pass markdown bodies via shell heredoc to `gh pr create --body "$(cat <<'EOF'…)"` / `gh pr edit --body …` / `gh pr comment --body …` / `gh issue create --body …` / `gh api` `--field body=…`. Backticks inside the heredoc (every code span and code fence) get shell-escaped to `\`` and render literally on GitHub — every recipe id, file path, flag, SQL fragment, and code fence in the rendered body comes out as `\`coverage\``instead of`coverage`. Pattern: write the body to a temp file (`Write`to`/tmp/pr-<n>-body.md`), pass `--body-file /tmp/pr-<n>-body.md`, then delete the temp file. Cost is one extra tool call; saves redoing every PR body that has more than a few backticks. Hit on PR #57 — final body was a wall of `\`` artifacts until rewritten via temp file.

.agents/rules/codemap.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ alwaysApply: true
66

77
> **STOP.** Before you call Grep, Glob, SemanticSearch, or Read to answer a **structural** question about this repository — query the Codemap SQLite index first. This is not optional when the question matches a trigger pattern below.
88
9-
A local database (default **`.codemap/index.db`**) indexes structure: symbols, imports, exports, components, dependencies, markers, CSS variables, CSS classes, CSS keyframes. The `.codemap/` directory holds every codemap-managed file (`index.db` + WAL/SHM, `audit-cache/`, project `recipes/`, `config.{ts,js,json}`, self-managed `.gitignore`); override the dir with `--state-dir <path>` or `CODEMAP_STATE_DIR`. The `.codemap/.gitignore` is **codemap-managed and reconciled on every boot** (`ensureStateGitignore`) — bumping its canonical body in a PR auto-applies on every consumer's next run.
9+
A local database (default **`.codemap/index.db`**) indexes structure: symbols, imports, exports, components, dependencies, markers, CSS variables, CSS classes, CSS keyframes, and (after `bun src/index.ts ingest-coverage <path>`) static coverage from Istanbul JSON or LCOV. The `.codemap/` directory holds every codemap-managed file (`index.db` + WAL/SHM, `audit-cache/`, project `recipes/`, `config.{ts,js,json}`, self-managed `.gitignore`); override the dir with `--state-dir <path>` or `CODEMAP_STATE_DIR`. The `.codemap/.gitignore` is **codemap-managed and reconciled on every boot** (`ensureStateGitignore`) — bumping its canonical body in a PR auto-applies on every consumer's next run.
1010

1111
**This file** is for **developing Codemap** in this clone. **End users** of the published package get the agent rule from **`templates/agents/`** (via **`codemap agents init`**). **Generic defaults:** SQL and triggers stay project-agnostic — **edit** this rule for repo-specific paths and queries.
1212

@@ -32,6 +32,7 @@ A local database (default **`.codemap/index.db`**) indexes structure: symbols, i
3232
| Targeted read (metadata) || `bun src/index.ts show <name> [--kind <k>] [--in <path>] [--json]` — file:line + signature |
3333
| Targeted read (source text) || `bun src/index.ts snippet <name> [--kind <k>] [--in <path>] [--json]` — same lookup + source from disk + stale flag |
3434
| Impact (blast-radius walker) || `bun src/index.ts impact <target> [--direction up\|down\|both] [--depth N] [--via <b>] [--limit N] [--summary] [--json]` — replaces hand-composed `WITH RECURSIVE` queries |
35+
| Coverage ingest || `bun src/index.ts ingest-coverage <path> [--json]` — Istanbul (`coverage-final.json`) or LCOV (`lcov.info`); format auto-detected. Joinable to `symbols` for "untested AND dead" queries. |
3536
| SARIF / GH annotations || `bun src/index.ts query --recipe deprecated-symbols --format sarif` · `… --format annotations` |
3637

3738
**Recipe `actions`:** with **`--json`**, recipes that define an `actions` template append it to every row (kebab-case verb + description — e.g. `fan-out``review-coupling`). Under `--baseline`, actions attach to the **`added`** rows only. Inspect via **`--recipes-json`**. Ad-hoc SQL never carries actions.
@@ -109,6 +110,10 @@ If the question looks like any of these → use the index:
109110
| "Is symbol X deprecated?" / "What does X do?" | `symbols` (`doc_comment`) |
110111
| "What's `@internal` / `@beta` / `@alpha` / `@private`?" | `symbols.visibility` (parsed JSDoc tag — not regex) |
111112
| "Who calls X?" / "What does X call?" | `calls` |
113+
| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `ingest-coverage`) |
114+
| "What's structurally dead AND untested?" | `--recipe untested-and-dead` |
115+
| "Rank files by test coverage" | `--recipe files-by-coverage` |
116+
| "Worst-covered exported functions" | `--recipe worst-covered-exports` |
112117

113118
## When Grep / Read IS appropriate
114119

@@ -156,6 +161,8 @@ bun src/index.ts query --json "<SQL>"
156161
| Who calls X? | `SELECT DISTINCT caller_name, file_path FROM calls WHERE callee_name = '...'` |
157162
| What does X call? | `SELECT DISTINCT callee_name FROM calls WHERE caller_name = '...'` |
158163
| Call hotspots | `SELECT callee_name, COUNT(*) as fan_in FROM calls GROUP BY callee_name ORDER BY fan_in DESC LIMIT 10` |
164+
| Symbol coverage | `SELECT name, hit_statements, total_statements, coverage_pct FROM coverage WHERE file_path = '...'` |
165+
| Untested + dead exports | `bun src/index.ts query --json --recipe untested-and-dead` |
159166

160167
**Use `DISTINCT`** on dependency and import queries — a file importing multiple specifiers from the same module produces duplicate rows.
161168

.agents/skills/codemap/SKILL.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,21 @@ User-facing baselines saved by `codemap query --save-baseline`, replayed by `cod
267267
| git_ref | TEXT | `git rev-parse HEAD` at save time, or NULL when not a git working tree |
268268
| created_at | INTEGER | `Date.now()` at save time (epoch ms) |
269269

270+
### `coverage` — Statement coverage (user data, ingested via `codemap ingest-coverage`)
271+
272+
Static coverage from Istanbul JSON or LCOV. Joinable to `symbols` for "what's untested?" queries. **Survives `--full` and SCHEMA bumps** — intentionally absent from `dropAll()`. Empty until first ingest.
273+
274+
| Column | Type | Description |
275+
| ---------------- | ------- | -------------------------------------------------------------------------------------------------------- |
276+
| file_path | TEXT PK | Project-relative path; matches `symbols.file_path`. Forward-slashed (Windows paths normalised on ingest) |
277+
| name | TEXT PK | Symbol name (matches `symbols.name`). Same `(file_path, name, line_start)` is unique by construction |
278+
| line_start | INT PK | Symbol's starting line (matches `symbols.line_start`). Disambiguates re-declared names |
279+
| coverage_pct | REAL | Percentage 0.0–100.0; `NULL` when `total_statements = 0` (zero-statement scope; not the same as 0%) |
280+
| hit_statements | INTEGER | Count of statements with non-zero hit count after innermost-wins projection |
281+
| total_statements | INTEGER | Count of statements that projected onto this symbol |
282+
283+
Three meta keys (`coverage_last_ingested_at` / `_path` / `_format`) record freshness — single ingest at a time, format is meta-level.
284+
270285
## Query patterns
271286

272287
### Basic lookups

.changeset/coverage-ingestion.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
"@stainless-code/codemap": minor
3+
---
4+
5+
`codemap ingest-coverage <path>` — static coverage ingestion. Reads Istanbul JSON (`coverage-final.json`) or LCOV (`lcov.info`) into a new `coverage` table joinable to `symbols`, so structural queries can compose coverage filters in pure SQL — no runtime tracer, no paid coverage stack.
6+
7+
**Both formats land in v1** (Istanbul + LCOV) so every test runner is a first-class consumer on day one — `vitest --coverage`, `jest --coverage`, `c8`, `nyc` (Istanbul JSON), and `bun test --coverage` (LCOV) all work without waiting on a follow-up release.
8+
9+
**Bundled recipes (auto-discovered, no opt-in needed):**
10+
11+
- `untested-and-dead` — exported functions with no callers AND zero coverage; the killer recipe combining structural and runtime evidence axes.
12+
- `files-by-coverage` — files ranked ascending by statement coverage.
13+
- `worst-covered-exports` — top-20 worst-covered exported functions.
14+
15+
Each recipe ships a frontmatter `actions` block so agents see per-row follow-up hints in `--json` output.
16+
17+
**Schema:**
18+
19+
- New `coverage` table with natural-key PK `(file_path, name, line_start)` — intentionally not a FK to `symbols.id` so coverage rows survive the `symbols` drop-recreate cycle on every `--full` reindex.
20+
- `idx_coverage_file_name` covers the typical join shape and the `GROUP BY file_path` scan used by the `files-by-coverage` recipe.
21+
- Three new `meta` keys (`coverage_last_ingested_at` / `_path` / `_format`) record ingest freshness.
22+
- `SCHEMA_VERSION` 5 → 6 — auto-rebuilds on next `codemap` run; the new table is empty until first `ingest-coverage` invocation. Subsequent bumps preserve coverage data via the `dropAll()` exclusion.
23+
24+
**CLI:**
25+
26+
```bash
27+
codemap ingest-coverage coverage/coverage-final.json # Istanbul (auto-detected)
28+
codemap ingest-coverage coverage/lcov.info # LCOV (auto-detected)
29+
codemap ingest-coverage coverage --json # directory probe (errors if both files present)
30+
31+
codemap query --json --recipe untested-and-dead # the killer query
32+
```
33+
34+
No `--source` flag — format is auto-detected from extension. No MCP / HTTP transport in v1 — coverage exposes as a SQL column, composable with every existing recipe and ad-hoc query through the existing `query` / `query_recipe` tools (no parallel surface).
35+
36+
Plan: PR #56 (merged). Implementation: this PR.

0 commit comments

Comments
 (0)