Skip to content

docs(plan): static coverage ingestion (coverage-final.jsoncoverage table)#56

Merged
SutuSebastian merged 6 commits intomainfrom
docs/coverage-ingestion-plan
May 4, 2026
Merged

docs(plan): static coverage ingestion (coverage-final.jsoncoverage table)#56
SutuSebastian merged 6 commits intomainfrom
docs/coverage-ingestion-plan

Conversation

@SutuSebastian
Copy link
Copy Markdown
Contributor

@SutuSebastian SutuSebastian commented May 4, 2026

Summary

Plans the C.11 candidate from research/fallow.mdcodemap ingest-coverage <path> reads Istanbul coverage-final.json into two new tables (coverage symbol-level + file_coverage rollup), joinable to symbols for the killer "what's structurally dead AND untested?" recipe in one query.

Resolves the open question from fallow.md § 6 ("symbols column vs separate table?") in favour of a separate table with ON DELETE CASCADE (D1).

Killer recipe this unlocks

-- "What's structurally dead AND untested?" — single query, two evidence axes.
SELECT s.name, s.file_path, c.coverage_pct
FROM symbols s
LEFT JOIN coverage c ON c.symbol_id = s.id
WHERE s.is_exported = 1
  AND s.id NOT IN (SELECT callee_id FROM calls WHERE callee_id IS NOT NULL)
  AND COALESCE(c.coverage_pct, 0) = 0
ORDER BY s.file_path, s.line_start;

Today an agent runs two tools and joins in JS. After this lands it's one `query` + one `JOIN`.

Key decisions (full list in plan)

  • D1 Separate `coverage` table, not columns on `symbols` — coverage shape evolves independently; LEFT JOIN keeps NULL semantics explicit; rows survive `--full` reindex via the `query_baselines` precedent.
  • D3 Istanbul JSON in v1; LCOV in v1.x; raw V8 traces never (Fallow's paid moat per § D.16).
  • D4 One-shot `codemap ingest-coverage` verb, not auto-detected during `codemap` runs — coverage cadence (per `bun test --coverage`) ≠ index cadence (per file edit).
  • D5 Statement coverage only in v1; branch / function deferred until a consumer asks.
  • D7 Symbol mapping by line-range projection (`line_start ≤ stmt_line ≤ line_end`) — same shape `markers` already uses.
  • D9 MCP / HTTP exposure as a query column, not a separate `coverage` tool — composes with every existing recipe.
  • D10 `codemap audit --delta coverage` deferred to v1.x — raw schema first.

Tracer-bullet plan

  1. Schema + `SCHEMA_VERSION` bump in `db.ts`
  2. Istanbul ingester engine in `application/coverage-engine.ts`
  3. CLI verb `cli/cmd-ingest-coverage.ts`
  4. Fixture `coverage-final.json` + golden recipe (joins to PR chore(fixture): enrich fixtures/minimal to exercise every codemap surface #55's `@deprecated` symbols)
  5. Doc + agent rule + skill + changeset + plan deletion

Workflow

Plan only — implementation follows after CodeRabbit review per the established workflow (PRs #46 / #47, #49 / #50, #51 / #52, #53 / #54).

Test plan

  • CodeRabbit review of plan structure / decisions
  • Implementation PR follows after merge

Summary by CodeRabbit

  • Documentation
    • Added a comprehensive design doc specifying static coverage ingestion: supported input formats, ingestion behavior, output shape, tracking/meta rules, cleanup, and design constraints.
    • Updated the roadmap to list static coverage ingestion as a planned feature with links to the new design and intended CLI surface.

Plans the C.11 candidate from `research/fallow.md` — `codemap ingest-coverage <path>`
reads Istanbul `coverage-final.json` into two new tables (`coverage` symbol-level +
`file_coverage` rollup), joinable to `symbols` for the killer "what's structurally
dead AND untested?" recipe in one query.

Resolves the open question from `fallow.md § 6` ("symbols column vs separate table?")
in favour of a separate table with `ON DELETE CASCADE` (D1) — coverage shape evolves
independently of structural columns; LEFT JOIN keeps NULL semantics explicit; rows
survive `--full` reindex via the `query_baselines` precedent (D6).

Key decisions:
- Istanbul JSON in v1; LCOV in v1.x; raw V8 traces never (D3, fallow's paid moat).
- One-shot `ingest-coverage` verb decoupled from `codemap` index runs (D4) — coverage
  cadence (per `bun test --coverage`) ≠ index cadence (per file edit).
- Statement coverage only in v1 (D5); branch/function deferred until a consumer asks.
- MCP/HTTP exposure as a query column, not a separate `coverage` tool (D9) — composes
  with every existing recipe + ad-hoc SQL.
- `codemap audit --delta coverage` deferred to v1.x (D10) — raw schema first.

Five-tracer plan: schema bump → engine → CLI verb → fixture + golden recipe → docs.
Plan only — implementation follows after CodeRabbit review per the established
workflow (PRs #46/47, #49/50, #51/52, #53/54).
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

Warning

Rate limit exceeded

@SutuSebastian has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 52 minutes and 21 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ad3622c4-e82e-4666-952a-caf55454e400

📥 Commits

Reviewing files that changed from the base of the PR and between 9e52ffa and 83bf2e6.

📒 Files selected for processing (1)
  • docs/plans/coverage-ingestion.md
📝 Walkthrough

Walkthrough

This PR adds planning documentation for "static coverage ingestion": a design doc defining a new coverage table schema, ingest semantics, CLI surface codemap ingest-coverage <path> [--json], ingest/outlier cleanup rules, design decisions, and a tracer-bullet implementation roadmap; and it adds a backlog entry linking the plan from the roadmap.

Changes

Coverage ingestion plan & roadmap

Layer / File(s) Summary
Design / Goals
docs/plans/coverage-ingestion.md
Introduces "static coverage ingestion" and the primary SQL join recipe linking symbols to coverage for queries like "structurally dead AND untested".
Schema & Semantics
docs/plans/coverage-ingestion.md
Proposes coverage table DDL with natural-key PK (file_path, name, line_start), coverage_pct semantics (NULL when total_statements = 0), WITHOUT ROWID, and index on (file_path, name). Specifies ingest meta keys and unconditional orphan-prune sweep.
CLI Surface
docs/plans/coverage-ingestion.md
Defines CLI: codemap ingest-coverage <path> [--json] with format auto-detection (Istanbul coverage-final.json and LCOV lcov.info) and JSON envelope stdout shape; states pruning runs after each ingest and ingestion is decoupled from general index runs.
Design Decisions
docs/plans/coverage-ingestion.md
Records decisions (D1–D12): statement-only v1, innermost-wins symbol mapping from Istanbul statementMap, project-relative path normalization, natural-key durability across --full rebuilds, LCOV/Istanbul support, and placement of ingester engine at src/application/coverage-engine.ts.
Implementation Plan
docs/plans/coverage-ingestion.md
Tracer-bullet roadmap: DB schema bump and tests, shared coverage ingester engine, LCOV parser, CLI wiring and tests, fixtures/golden assertions, docs/rules/changeset updates, and plan-file deletion step.
Alternatives / Perf / OOS
docs/plans/coverage-ingestion.md
Discusses performance/storage/read-costs, enumerates alternatives considered, and lists explicit out-of-scope items for v1 (rollups, branch/function breakdowns, cross-file disambiguation, audit-delta integration, source-map handling, auto-reingest via watch, etc.).
Roadmap Update
docs/roadmap.md
Adds backlog entry for static coverage ingestion referencing the plan document and research source; describes codemap ingest-coverage <path> and join capability with symbols.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes

Poem

🐰
I hopped through plans with eager paws,
Mapping coverage, tracing flaws.
A table born, a CLI in sight,
Symbols and coverage join tonight.
Tiny tracer-bullet, off we go — hooray!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and specifically describes the main change: adding a design plan document for static coverage ingestion that converts Istanbul coverage files into a coverage table.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/coverage-ingestion-plan

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 52 minutes and 21 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 4, 2026

⚠️ No Changeset found

Latest commit: 83bf2e6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
docs/plans/coverage-ingestion.md (1)

58-60: 💤 Low value

Clarify meta value types.

The schema comments show <unix-ms> for coverage_last_ingested_at, which suggests a numeric timestamp, but the meta table likely stores values as TEXT. Consider clarifying whether these are stored as stringified numbers or if the meta table schema supports typed values.

This is a minor documentation clarity point — the implementation will resolve it, but being explicit in the plan prevents ambiguity during tracer #1 (schema bump).

Suggested clarification
-INSERT INTO meta (key, value) VALUES ('coverage_last_ingested_at', '<unix-ms>');
-INSERT INTO meta (key, value) VALUES ('coverage_last_ingested_path', '<abs-path>');
+-- Meta table stores TEXT values; numeric timestamp stored as string
+INSERT INTO meta (key, value) VALUES ('coverage_last_ingested_at', '1714809600000');  -- unix-ms as TEXT
+INSERT INTO meta (key, value) VALUES ('coverage_last_ingested_path', '/absolute/path/to/coverage-final.json');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/coverage-ingestion.md` around lines 58 - 60, The INSERT examples
for meta keys (coverage_last_ingested_at, coverage_last_ingested_path,
coverage_last_ingested_source) are ambiguous about value types — clarify whether
meta.value is TEXT and thus expects stringified numbers/paths/sources or if the
schema uses typed columns; update the docs to state the meta table's value type
explicitly (e.g., "meta.value is TEXT, store unix-ms as stringified integer" or
"schema uses INTEGER/JSON for typed values"), and if you decide to change the
schema, note the required schema change and migration step so implementers know
to add/alter columns rather than rely on implicit typing.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plans/coverage-ingestion.md`:
- Around line 11-20: The SQL uses non-existent columns/tables: replace the
callee_id/symbol_id joins with text-name joins against calls.callee_name and
symbols.name (e.g., LEFT JOIN calls ON calls.callee_name = s.name) and remove or
replace the non-existent coverage table; either join to the actual coverage
source (use the real coverage table name/column that holds coverage_pct) or
compute coverage_pct from existing data (or add coverage_pct to the schema)
before using COALESCE(c.coverage_pct, 0). Ensure the final query references
symbols.name, calls.callee_name/caller_name, and the real coverage column/table
instead of callee_id/symbol_id or a missing coverage table.

---

Nitpick comments:
In `@docs/plans/coverage-ingestion.md`:
- Around line 58-60: The INSERT examples for meta keys
(coverage_last_ingested_at, coverage_last_ingested_path,
coverage_last_ingested_source) are ambiguous about value types — clarify whether
meta.value is TEXT and thus expects stringified numbers/paths/sources or if the
schema uses typed columns; update the docs to state the meta table's value type
explicitly (e.g., "meta.value is TEXT, store unix-ms as stringified integer" or
"schema uses INTEGER/JSON for typed values"), and if you decide to change the
schema, note the required schema change and migration step so implementers know
to add/alter columns rather than rely on implicit typing.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 77028727-a2bc-497e-87f8-930409f6f689

📥 Commits

Reviewing files that changed from the base of the PR and between 499f661 and cc2ab66.

📒 Files selected for processing (2)
  • docs/plans/coverage-ingestion.md
  • docs/roadmap.md

Comment thread docs/plans/coverage-ingestion.md
…r claims

Self-audit against the actual codebase surfaced four claims that didn't hold:

1. Killer recipe SQL referenced `callee_id` — `calls` is name-keyed
   (`callee_name TEXT`, no symbol-id FK; see `db.ts` `CallRow`). Rewrote
   the "no callers" predicate as `NOT EXISTS (… WHERE callee_name = s.name)`.
2. D7 claimed line-range projection is "the same `markers` already uses" —
   `markers` is line-pinned (`line_number INTEGER`), no projection.
   Reworded as "novel for this plan" with the actual mechanic spelled out.
3. D3 listed `bun test --coverage` as an Istanbul JSON emitter — `bun test
   --help` shows only `text` / `lcov` reporters today. Removed bun from the
   Istanbul-emitters list; left vitest/jest/c8/nyc with the explicit reporter
   flags they need.
4. D12 contradicted D6 ("rows absent until re-ingest" vs "rows survive
   `--full`"). Reconciled: empty is the correct initial state on first bump;
   subsequent bumps preserve via the `dropAll()` exclusion. Quoted the
   `lessons.md` policy verbatim instead of paraphrasing.
Self-grilling found two real schema design holes that would block execution:

1. **D6 CASCADE hazard.** Original draft keyed `coverage` on
   `symbol_id REFERENCES symbols(id) ON DELETE CASCADE`. Every `--full`
   reindex calls `dropAll()` → drops `symbols` → CASCADE wipes coverage,
   regardless of whether `coverage` itself was excluded from `dropAll()`.
   Recreated `symbols` get fresh auto-increment IDs anyway → coverage
   permanently lost without re-ingest. Fix: natural-key PK
   `(file_path, name, line_start)` — no FK to `symbols.id`. Survives the
   `symbols` drop-recreate cycle. Trade-off: orphan rows when files are
   deleted; cleaned by one explicit `DELETE FROM coverage WHERE file_path
   NOT IN (SELECT path FROM files)` after every ingest.

2. **D7 overlapping symbols.** Original draft: `line_start ≤ stmt_line ≤
   line_end` matches every enclosing scope. With nested symbols (class
   methods inside classes, closures inside functions), one Istanbul
   statement projects onto 3+ symbols, inflating `total_statements` 2-3×.
   Fix: innermost-wins via `(line_end - line_start) ASC LIMIT 1`. New
   `skipped.statements_no_symbol` counter for statements that fall outside
   every symbol range (top-level expressions, side-effect imports).

Nits cleared in the same pass:

- D2: drop `file_coverage` rollup table from v1 (aggregateable via
  GROUP BY on the symbol-level table; doubling sources of truth without
  a benchmark is premature). Promote to v1.x with a real query.
- D11: spec the `total_statements = 0 → coverage_pct IS NULL` edge case
  + document the cross-file name-collision lossiness in the killer recipe.
- Drop `--prune` flag (orphan cleanup is unconditional, no flag needed).
- Drop per-row `source` column (single meta key sufficient; one ingest
  at a time).
- Update killer recipe SQL to use the natural-key 3-column join.
- Drop made-up "~50 LoC LCOV ingester" estimate and "<50 ms / <1 ms /
  ~500 KB" performance numbers (no benchmark backed them).
- Tracer 1 / 2 / 3 acceptance criteria updated to match the new schema.

Plan is now ready for tracer-1 implementation. CodeRabbit pass deferred
(rate-limited 57m).
Plan correctly inherits the established Node vs Bun runtime split, but the
single tracer-3 reference understated it. Now:

- Tracer 3 cites `packaging.md § Node vs Bun` as the canonical pattern
  source instead of pointing at config.ts in passing.
- Performance section calls out the actual lever — `Bun.file(path).json()`
  uses Bun's native JSON parser, materially faster than V8 `JSON.parse`
  on multi-MB Istanbul payloads (real coverage files for medium codebases
  routinely hit several MB).

No new Bun-native API surfaces are added — the feature doesn't need
globbing, file writes, spawn, or hashing beyond what the existing engines
already use through their abstractions.
… recipe

The "fully capable, no half-way APIs" principle reshapes three things:

1. **LCOV ingester ships in v1** alongside Istanbul. Original draft deferred
   LCOV to v1.x, which would exclude `bun test --coverage` users — i.e.
   codemap's own primary runtime. That's the textbook half-baked surface
   the principle bans. Two parser front-ends share one `upsertCoverageRows`
   core; LCOV is regex tokenizing over `SF:` / `DA:` / `end_of_record`.
   Tracer 2 splits into 2a (shared core + Istanbul parser) and 2b (LCOV
   parser), both writing identical normalised CoverageRow[] into the same
   upsert path.

2. **`--source istanbul|lcov` flag dropped.** Auto-detection from extension
   (`.json` → istanbul, `.info` → lcov, directory → probe both, error on
   ambiguous) is unambiguous; a flag for "tell codemap what it can already
   see" is API noise. Misnamed files can be renamed (one-liner) cheaper
   than codemap can grow a flag.

3. **Killer recipe ships as bundled `untested-and-dead.{sql,md}`** in
   `templates/recipes/`. Per the recipes-as-content registry (PR #37), the
   high-value queries become first-class agent surface. A buried doc
   snippet would be invisible to agents at session start; the bundled
   recipe shows up in `--recipes-json` and gets a `codemap query --recipe
   untested-and-dead` direct invocation.

Tracer 4 also fans out: Istanbul + LCOV fixtures cover the same partial
coverage shape; three golden recipes (`coverage-istanbul.json`,
`coverage-lcov.json`, `untested-and-dead.json`) prove format equivalence.
Out-of-scope, alternatives, performance section, title, and goal
statement all updated to match.
Walked every D / OOS / tracer item against "fully capable + agent
first-class + no half-baked APIs". Found three half-baked surfaces:

1. **D2 deferral leaks "compose GROUP BY yourself" onto the agent.**
   Deferring the `file_coverage` table is correct (no benchmark proves
   it's needed) — but the agent-facing answer for "rank files by
   coverage" was missing. Fix: keep table deferral, ship a bundled
   `files-by-coverage.{sql,md}` recipe so the GROUP BY view IS
   first-class.

2. **D11 name-collision lossiness was acknowledged but unmitigated.**
   The killer recipe's `callee_name = s.name` cross-file lossiness
   was documented in the recipe SQL comment, but the recipe `.md`
   didn't give the agent any narrowing pattern. Now D11 ships three
   concrete narrowing patterns in the `.md` (file_path scope, default-
   export filter, exported-only restriction) so the agent has
   workable mitigations on day one.

3. **Missing recipe shelf for common agent questions.** Walking the
   journey: only "What's structurally dead AND untested?" had a recipe;
   "Rank files by coverage" and "Worst-covered exported symbols" forced
   ad-hoc SQL. Three recipes fully cover the agent journey end-to-end.

New D13 codifies the bundled-recipe principle: every common agent
question gets a `--recipe` verb. Three v1 recipes:
- `untested-and-dead.{sql,md}` (killer, with name-collision mitigations)
- `files-by-coverage.{sql,md}` (replaces D2's table deferral)
- `worst-covered-exports.{sql,md}` (top-N agent ask)

Each `.md` carries a frontmatter `actions` block (per PR #26) so agents
get per-row follow-up hints. All three appear in `--recipes-json`
automatically — agents discover them at session start.

New "Agent journey" section makes the principle visible: a table mapping
every common agent question to the v1 verb that answers it. If a row
ever shows "compose SQL yourself" without a recipe, the surface is
half-baked and needs a recipe before tracer 1 ships.

Tracer 4 expanded: ships all three recipes + five golden snapshots
(adds files-by-coverage.json + worst-covered-exports.json on top of the
three existing). Tracer 5 expanded: glossary + agent rule trigger
table gain three new rows.

Plan now passes the principle audit end-to-end.
@SutuSebastian
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@SutuSebastian SutuSebastian merged commit e100d2c into main May 4, 2026
10 checks passed
@SutuSebastian SutuSebastian deleted the docs/coverage-ingestion-plan branch May 4, 2026 07:56
SutuSebastian added a commit that referenced this pull request May 4, 2026
… 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.
SutuSebastian added a commit that referenced this pull request May 4, 2026
User reframe: codemap is the only SQL-based code index in the market;
inspiration comes from the free and open internet (LSP spec, SQLite
docs, AST tooling), not code-by-code cloning of any peer tool. Drop
fallow as a yardstick throughout.

Vital information preserved (per "don't lose any vital information
that is used to execute the plan"):

- Closed-dead-subgraph motivator for C.9 — kept as an abstract pattern
  description in § 2.3 caveat (N-file packs with self-imports, non-
  zero fan-in, none reachable from real entry). Was previously cited
  to fallow.md § 0; now stands on its own merit.
- LSP read-side capabilities (show / impact / watch) — kept; LSP spec
  upstream is now the protocol authority instead of fallow's
  crates/lsp/.
- Runtime-tracing scope distinction — § 3 floor reframed to anchor on
  "different product class entirely" (live process data vs static
  analysis) instead of "fallow's paid moat."
- Predicate-as-API moat (A) — kept; justification now anchors on
  intrinsic merit (SQL is durable, agents compose any predicate)
  rather than "fallow ships verdicts; we don't."
- Schema-breadth moat (B) — kept; justification now "codemap-specific
  extractions; their richness directly determines what JOINs are
  expressible" rather than "fallow has none of these."

Section-by-section changes:

- HEADER — "Companion docs / Source for deep-dives" replaced with
  "Companion doc" (competitive-scan only) + "Positioning" paragraph
  declaring structural uniqueness.
- § 2.3 original-framing quote — paraphrased to drop the "(e.g.
  fallow, knip, jscpd)" parenthetical; pointers to roadmap.md for the
  full original wording. (roadmap.md itself still has the parenthetical;
  separate-PR scope.)
- § 2.3 caveat — closed-dead-subgraph case described abstractly; no
  source citation needed.
- § 2.5 LSP shim — "fallow has crates/lsp/" → "LSP spec upstream is
  the protocol authority."
- § 3 intro — mission framing rewritten; "equal/surpass fallow"
  language replaced with "extract maximum value from the SQL-index
  architecture; grow the ecosystem" + "only SQL-based code index in
  the market" positioning.
- § 3 Moat A — anchored on intrinsic merit (SQL durable + agent
  composability) instead of fallow comparison.
- § 3 Moat B — anchored on "substrate every recipe layers on; richness
  determines JOIN expressivity" instead of "fallow has none of these."
- § 3 ergonomic floors — dropped all "fallow is also fast" /
  "Convergent with fallow" annotations; reframed runtime-tracing as
  "different product class entirely (live process data, not static
  analysis)" + reframed telemetry-upload as standalone safety promise.
- § 4 — DELETED ENTIRELY ("What to inspect in the fallow source
  tree"). Replaced with "Inspiration sources for plan-PR authoring"
  table listing open specs / primitive sources only (LSP spec, SQLite
  docs, oxc node reference, Lightning CSS, JSON-RPC + MCP spec, TC39
  proposals, existing codemap surface, internal third-party graph
  audits). Discipline statement preserved: every plan PR cites the
  spec / primitive source it took inspiration from.
- § 5 (d) row + T-table T+5w → +7w cell — dropped fallow crates/lsp/
  refs; LSP spec is now the named authority.
- § 6 Q1 — dropped fallow.md § 6 citation; stale-index frequency now
  anchored on PR #46 + PR #56 internal evidence.
- § 6 Q4 — dropped fallow.md § 0 + § 6 citations; closed-dead-subgraph
  case cross-refs § 2.3 caveat instead.
- § 7 cross-references — removed research/fallow.md and fallow
  upstream entries. Added § 4 inspection list as a self-reference.
- § 8 errata § 2.3 row — dropped fallow.md citation; pattern described
  inline.

Net effect: the doc stands on codemap's intrinsic structural
properties. No peer-tool framing remains. The mission is now
self-coherent: extract max value from the SQL-index architecture +
grow the ecosystem, anchored on the unique-in-market positioning.
SutuSebastian added a commit that referenced this pull request May 4, 2026
…quence (2026-05) (#58)

* docs(research): non-goals reassessment + fallow clone deep-dive map (2026-05)

Companion to research/fallow.md (capability tracker — what to adopt FROM
fallow). This new doc inventories what THIS codebase already unlocks
that the current Non-goals (v1) list forbids, post-C.11.

User observation: many non-goals were defensive choices made when the
project was 1/10th its current size, then carried forward unchallenged
as the surface grew (15+ recipes, 12+ tables, 3 engines, watch mode,
coverage, audit, impact). The reframe: stop asking "what should we not
do?" and start asking "what does the SQL-index-with-three-transports
actually unlock that no other tool does?"

Findings:

§1 — 10 first-class agent capabilities sitting in unwritten JOINs /
formatters / verbs (components-touching-deprecated, unimported-exports,
complexity per symbol, refactor-risk-ranking, boundary violations,
unused type members, Mermaid output, MCP file/symbol resources, recipe
usage telemetry, rename --dry-run preview).

§2 — Five non-goals worth challenging:
- "No FTS5 / use ripgrep" — SQLite ships FTS5; ripgrep loses JOIN
  composition (TODOs inside @deprecated functions in <50% covered files
  is one query, vs three tools today).
- "No visualisation" — conflates rendering pixels with shaping render-
  ready data; Mermaid / D2 are JSON-shaped formatters (sibling of SARIF).
- "No static analysis" — we already ship deprecated-symbols, untested-
  and-dead, barrel-files, fan-in/out; the line was rhetorical. Real
  boundary is "no opinionated rule engine, no fix mutation".
- "No persistent daemon" — we have one (mcp --watch, serve --watch,
  watch); non-goal preserves a constraint that no longer exists.
- "No LSP replacement" — show + impact + watch is 80% of LSP read-side;
  ship a thin shim consuming existing engines, don't write an LSP.

§3 — Real architectural limits worth keeping (sub-100ms cold-start CLI,
no LLM in box, no fix engine, no runtime tracing, no JS exec at index
time).

§4 — Map of /Users/sutusebastian/Developer/OSS/fallow clone deep-dive
points: which crates / docs / configs to inspect before each shipped
feature so we adopt patterns rather than reinvent. Cite-the-source-path
discipline mirrors the existing research/fallow.md cite-the-PR habit.

§5 — Recommended sequence: (a) FTS5 + Mermaid one-PR non-goal flip →
(c) complexity column → (b) C.9 plugin layer (multi-tracer big surface)
→ (d) LSP shim. (a) is the cheapest non-goal flip; ships a confidence
move before the bigger surfaces.

§6 — 5 open questions (daemon-by-default for MCP/HTTP, FTS5 opt-in,
LSP shim vs standalone, plugin contract scope, history table shape).

Doc-governance compliance:
- Goes in docs/research/ per Rule 3 (research-class doc).
- Cross-references roadmap, why-codemap, fallow.md, competitive-scan
  per Rule 5.
- Doesn't duplicate non-goals (Rule 1) — proposes amendments to be
  applied when § 2 items ship, in lockstep with why-codemap per the
  Single source of truth table.
- No inventory counts in narrative (Rule 6) — uses qualitative "15+
  recipes / 12+ tables" only.

* docs(research): triangulate non-goals reassessment vs descriptive baseline

User cross-checked my prescriptive doc (non-goals-reassessment-2026-05.md)
against composer-2-fast's descriptive baseline (codemap-capability-
surface-2026-05.md) plus the codebase as source of truth. Found three
factual errors in mine; baseline doc held up clean.

Corrections applied:

1. § 1.2 (Exports never imported): codebase has `exports.re_export_source`
   column — original doc missed it. Re-exports require a JOIN through
   that column to avoid false positives on barrel-only exports. Effort
   bumped XS → S.

2. § 1.3 (Cyclomatic complexity): claimed "AST walker already counts
   nodes during parse" — false. `rg 'complexity|node_count|nodeCount'
   src/` returns zero matches. Node-counting is NOT in place; needs an
   extension to the AST walker in src/parser.ts. Effort bumped S → M.

3. § 2.3 ("no static analysis" non-goal): listed `fan-in` and `fan-out`
   as "static analysis we already ship" — too loose. Per `fan-in.sql`
   (`ORDER BY fan_in DESC LIMIT 15`) they're hotspot rankers, not
   orphan / dead-code detectors. They don't cover the closed-dead-
   subgraph case from research/fallow.md § 0 (8-file pack with non-
   zero fan-in via self-import). That gap motivates C.9 framework
   plugin layer, not the "no static analysis" flip. Caveat now spelled
   out in the doc.

Header updated: this doc is the **prescriptive** lens; the **descriptive
baseline** lives in codemap-capability-surface-2026-05.md (read first).
Cross-references list and § 8 errata block document the diff between v1
and v2 so future reviewers can see what changed and why.

Process lesson encoded in § 8: every prescriptive research note should
triangulate against a descriptive baseline (own doc or peer model) before
recommending a ship sequence. Caught all three errors before they
propagated into a plan PR.

* docs(research): scrub local user paths from non-goals doc + new lesson

User caught absolute-path leaks in the research note pointing at the
fallow clone on the maintainer's machine. Three references replaced with
the public upstream URL (https://github.com/fallow-rs/fallow):

- Header "Local clone for deep-dives" → "Source for deep-dives"
- § 4 heading "What to inspect in the local fallow clone" → "...in the
  fallow source tree"
- § 7 cross-references "Local fallow clone — /Users/..." → "fallow
  upstream"

Also adds a new general-purpose lesson to .agents/lessons.md:

  Never commit absolute local user paths — no /Users/<name>/…,
  /home/<name>/…, ~/…, or file:/// URIs in any tracked doc, code,
  comment, or PR body. Pattern: cite https://github.com/<org>/<repo>
  for upstream sources; repo-relative paths for in-tree references.

Sibling to the existing "PR bodies via temp file" lesson — same family
(committed strings need to be portable + non-leaking), different surface.

* docs(lessons): add 'never commit local user paths' lesson (PR #58 catch)

* docs(research): delete codemap-capability-surface-2026-05.md (existence test)

Per docs/README.md existence test, this doc fails 3 of 4 criteria:
- ❌ Doesn't document durable policy unavailable elsewhere — every fact
  reproducible from db.ts / builtin.ts / audit-engine.ts / --recipes-json
- ❌ Doesn't track open work — pure snapshot
- ❌ No unique historical context git log + architecture.md can't
  reconstruct
- ✅ Cited by another doc (only because non-goals-reassessment cited it)

Plus Rule 1 violation (duplicates architecture.md § Schema) and Rule 6
violation (hardcodes "15 recipes" / "9 of 15 ship actions" inventory
counts in narrative).

The real value the doc delivered was the **triangulation discipline** —
catching 3 errors in non-goals-reassessment v1. That discipline is the
durable artifact, not the doc. Codified in two places:

1. non-goals-reassessment § 8 errata + process lesson (kept)
2. .agents/lessons.md — new lesson explicitly bans the "dual descriptive
   + prescriptive doc" pattern as a Rule 1 violation. Right discipline:
   pin every concrete claim in the prescriptive doc itself, or self-audit
   against the canonical home before committing. Don't ship a parallel
   descriptive doc.

non-goals-reassessment header + § 7 + § 8 updated to drop the now-deleted
companion-doc references and point at canonical sources directly
(architecture.md § Schema, db.ts, builtin.ts, audit-engine.ts V1_DELTAS).

* docs(research): align § 5 (c) effort with § 1.3 / § 8 (M, not S)

CodeRabbit caught § 5 row (c) "Cyclomatic complexity column" listing
effort S, while § 1.3 + § 8 errata both list M (the v1→v2 bump after
`rg 'complexity|node_count|nodeCount' src/` returned zero — node-
counting isn't already in place; the AST walker in src/parser.ts has
to be extended). Effort propagation gap from the v2 errata pass.

§ 5 row (c) updated to M; "Why" cell now spells out the AST-walker
dependency inline so future readers don't re-litigate the figure.

* docs(research): split § 3 into moat (load-bearing) vs ergonomic limits

Grill-me Q1 outcome (under "extract max from SQL-index + equal/surpass
fallow" mission): the original § 3 list conflated ergonomic floors
(sub-100ms cold-start, no LLM, no JS at index time) with the actual
moats. Most of the original entries are floors fallow also follows;
they're not differentiators.

The two real moats that needed naming as load-bearing limits:

  A. SQL is the API — every capability is a recipe (saved query) or a
     primitive recipes can compose. Verdicts are an OUTPUT mode
     (--format sarif, audit deltas), never a primitive. Reviewer test:
     "is this verb also expressible as query --recipe <id>?"

  B. Extracted structure ≥ verdicts — schema breadth (CSS, markers,
     type_members, calls.caller_scope, components.hooks_used) is what
     equals/surpasses fallow on agent-facing capability per
     fallow.md § 5. Reviewer test for any "drop column X" PR:
     "what recipe (bundled or hypothetical) does this kill?"

Both are now load-bearing rows above the ergonomic ones. The original
five preferences are kept verbatim but annotated with their relation
to the moat (floor / convergent / adjacent / rivalrous / safety).

Eroding either A or B is the most likely path from "codemap" to
"fallow with extra steps" — § 3 now equips a reviewer to spot it.

* docs(research): § 5 ship sequence — parallel plan-PR for (b) at T+0

Grill-me Q2 outcome (under "equal/surpass fallow" mission): the
"cheapest non-goal flip first" ordering was a small-team confidence
move, but the § 3 moat rewrite already paid that confidence cost. The
real risk under the actual mission is the deferral trap — XL items
become "next quarter" while every new recipe inherits the noisy
substrate (untested-and-dead's Next.js page.tsx false-positive class).

Hybrid resolved:
- Shipping cadence stays (a) → (c) → (b) impl → (d).
- (b) plan PR opens at T+0, iterates in parallel during (a)+(c).
- Plan opens with ~30% of decisions pre-locked: entry-point hints only
  per Grill Q4, static config only per § 3 "no JS exec at index time"
  ergonomic limit. Not a blank-slate plan — structured from day 1.

Added a 5-row T-table in § 5 spelling out the parallel tracks. (b)'s
"Why" cell now names the deferral trap explicitly; (d)'s "Why" pins
its dep on (b) impl (not just (b)). Rationale list updated to flag
that the moat rewrite paid the confidence move so (a) doesn't pay it
again.

Cost-if-abandoned escape hatch: plan PR can close as
"Status: Rejected (YYYY-MM-DD)" per docs/README.md Rule 8. Design
surface captured either way.

* docs(research): § 2 reframed via § 3 moats (taxonomy + verdict cross-refs)

Grill-me Q3 outcome: § 2's five flips inherited their shape from
"original non-goals worth challenging" — but after § 3 locked in the
moats, that shape conflated three different categories:

- Moat-extending flips (2.1 FTS5, 2.3 static analysis) — substrate
  growth inside moat B
- Moat-aligned flip (2.2 output formatters) — verdicts as output mode
  per moat A
- Moat-orthogonal transport flips (2.4 daemon, 2.5 LSP shim) — neither
  moat is touched; flipping just re-exposes existing substrate

Anchors preserved (2.1-2.5 stay) — anchor-preservation discipline per
docs-governance § 3 / docs/README.md Rule 7. No cascading link updates
needed in § 3 / § 4 / § 5 / § 8.

Changes per section:

- § 2 header — added a reading note naming the three categories and
  pointing each flip at the moat row it relates to.
- § 2.3 — verdict no longer restates "no opinionated rule engine + no
  fix engine" (now canonical in § 3 moat A + ergonomic row); instead
  cross-references and names the static-analysis category as in-scope.
  Closed-dead-subgraph caveat preserved (it's the C.9 motivator).
- § 2.4 — added "Moat relation: orthogonal" subsection naming the
  transport / process-model framing. AST-caching capability claim
  preserved + cross-linked to § 6 Q1. Verdict points the daemon-default
  question at § 6 Q1 explicitly (single canonical home).
- § 2.5 — replaced the unmeasured "80% of LSP read-side" claim with a
  structural argument: shim wraps shipped engines (show / impact /
  watch) via stdio without re-extracting structure; an LSP *engine*
  would duplicate moat B substrate (the actual reason not to build
  one). Cited application/show-engine.ts + application/impact-engine.ts
  as the substrate the shim wraps.

- § 6 Q1 — enriched with the AST-caching downstream measurement note
  lifted from § 2.4 (single canonical home for the daemon-default
  decision; § 2.4 cross-refs here).

Vital-info preservation audit:
- ✅ Closed-dead-subgraph caveat (8-file widget pack via fallow.md § 0)
  — kept verbatim in § 2.3 caveat block.
- ✅ AST-caching capability claim — kept in § 2.4 "Capability unlocked"
  + cross-linked from § 6 Q1.
- ✅ Watch-mode receipts (codemap watch / mcp --watch / serve --watch)
  — kept verbatim in § 2.4 "What's actually true".
- ✅ Fan-in/fan-out hotspot-rankers framing — kept verbatim in § 2.3
  caveat (with errata cross-ref to § 8).
- ✅ Fallow `crates/lsp/` cross-ref — kept in § 2.5.

Dropped (intentional):
- "80% of LSP read-side" — unmeasured; replaced with structural
  argument that doesn't need a measurement.

* docs(research): § 1.7 Mermaid — bounded-input contract (moat A)

Grill-me Q4 outcome: § 1.7's "What's needed" cell was loose ("new
--format mermaid formatter") — true but underspecified. Real-project
edge counts on dependencies / calls are 1k-10k+; rendering them is
either Mermaid-choking or a hairball, and silently auto-truncating
(or "best-effort") would be a verdict-shaped affordance masquerading
as an output mode — violates moat A.

Locked in:

- Allow on: impact engine output (depth-bounded), LIMIT N-shipped
  recipes (fan-in / fan-out), ad-hoc SQL with explicit LIMIT ≤ 50.
- Reject (with scope-suggestion message) on unbounded inputs.
- No auto-truncation — that's a verdict (recipe author's job to scope).

Threshold (50 edges) is configurable; chosen as a default-readable
upper bound for chat-client rendering. Calibrate during (a) impl PR
against fixtures/golden / external corpus.

DX framing: hairballed Mermaid in MCP / Cursor / Slack chat clients
renders as garbage; a clear error naming knobs (LIMIT / --via / WHERE
from_path LIKE) is the better consumer signal.

This keeps Mermaid an output mode (moat A clean) and forces recipe
authors to scope graphs — correct because they own the structural
meaning of the result set.

* docs(research): § 1.10 rename — recipe-shape (moat A) + parametrised recipes

Grill-me Q5 outcome: § 1.10's verb-shape ("codemap rename <old> <new>
--dry-run") was downstream of the OLD § 3 ("no fix engine" as a top-
level non-goal). After the moat reframe, the actual test is moat A:
verdict-shape vs recipe-shape. Verb hides every implicit rename
choice (visibility filter, type-only re-exports, test files, aliases)
inside argv parsing — not auditable. Recipe-shape puts those choices
in reviewable SQL.

Locked in:

- Bundled recipe rename-preview.sql with --params key=value
  substitution (?-placeholder binding via db.ts prepared statements).
- --format diff output mode (sibling of --format mermaid per item 1.7;
  same "rows in, renderable text out" pattern).
- No new verb / engine / MCP tool / HTTP route. SQL stays the API.
- Effort drops M → S.

Cross-cutting infrastructure unlocked: parametrised recipes is net-new
plumbing but pays for itself on the first downstream use. Already-
visible follow-ons captured in the new "Cross-cutting infrastructure
unlocked by item 1.10" paragraph at the end of § 1:

- delete-symbol-preview, extract-function-preview, inline-symbol-
  preview — same recipe-shape pattern; all gated on the same plumbing.
- Parametrising existing static recipes (untested-and-dead
  --params min_coverage=80 instead of hardcoded < 80) — cleanup
  opportunity the same plumbing enables.

This is the second moat-A demonstration in two adjacent grill rounds
(after § 1.7's bounded-input contract on Mermaid). Both prove the
"verdicts are output mode, recipes are the API" framing on real
capabilities — exactly what the (a) plan-PR will need to point at
when reviewers ask "what changed?".

* docs(research): § 6 — close Q1 (daemon-default), Q3 (LSP shape), Q4 (plugin scope)

Grill-me Q6 outcome (and accounting cleanup): three of five § 6 open
questions are now resolved by prior grill outcomes — § 6 needs to
reflect that, not pretend they're still open.

Resolutions captured:

- Q1 (daemon-default for mcp/serve) — RESOLVED THIS GRILL TURN.
  Default --watch ON for both modes; opt-out via --no-watch /
  CODEMAP_WATCH=0. One-shot CLI defaults preserved (no watcher on
  query/show/snippet). Receipts: stale-index = #1 agent UX complaint
  (fallow.md § 6); chokidar lazy startup validated tiny by PR #46
  6-watcher audit. Flip is a small follow-up PR (flag default + test
  + patch changeset + agent rule update per docs/README.md Rule 10).
  AST-caching measurement parked downstream of the flip.

- Q3 (LSP shim vs standalone) — RESOLVED in § 2.5 reframe earlier
  this grill (commit 0b9d878). Thin shim wrapping shipped engines;
  no engine (would duplicate moat B substrate). Standalone deferred
  to "if VSCode-extension demand emerges."

- Q4 (C.9 plugin contract scope) — RESOLVED via § 5 (b) plan-PR
  pre-locked decisions (commit 6f845ba). Entry-point hints only for
  v1; arbitrary edge injection deferred to v2. Static config only
  per § 3 ergonomic "no JS exec at index time" floor.

§ 6 restructured: "Resolved (2026-05)" subsection at top with full
rationale + receipts; "Still open" subsection below with Q2 (FTS5
default) and Q5 (history table) — the only two genuinely-open
questions left.

§ 2.4 verdict updated to point at the resolved § 6 Q1 anchor instead
of the open-question wording.

Anchor preservation: external links (#6-open-questions) still resolve
to the section heading. New internal anchor (#resolved-2026-05) used
by § 2.4 verdict — single inbound link, no external citations to
break.

* docs(research): § 6 Q2 closed — FTS5 default-OFF, both config + CLI

Grill-me Q7 outcome: § 6 Q2 (FTS5 opt-in vs default-on) resolved.

Locked in:

- Toggle: BOTH codemap.config.ts `fts5: true` AND --with-fts CLI flag
  at index time. Config-only forces CI / ephemeral workflows to commit
  fts5: true to a config file; CLI-only forces long-term users to
  remember the flag on every --full. Cheap to support both.
- Default: OFF. Backwards-compat — existing users wouldn't see
  .codemap/index.db grow ~30-50% silently on next --full.
- Re-evaluate default in v2 once external-corpus size measurements
  land (bun run benchmark:query shape).

Bug fix in § 2.1: the "off by default to keep cold-start sub-100ms"
framing was a WRONG REASON. FTS5 is index-time cost only; cold-start
reads existing DB and the virtual table doesn't slow startup. Real
reason for default-OFF is index size growth. § 2.1 verdict updated to
reflect this; § 6 Q2 resolution explicitly calls out the wrong-reason
correction so future readers see the diff.

Principle pinned: default-ON is reserved for capabilities without
disk-size tax (Mermaid output, parametrised recipes, complexity
column). FTS5 is the disk-tax exception.

Tree state after this commit:

- § 6 Q1 (daemon-default) — resolved
- § 6 Q2 (FTS5 default) — resolved
- § 6 Q3 (LSP shape) — resolved
- § 6 Q4 (plugin scope) — resolved
- § 6 Q5 (history table) — STILL OPEN (defer-bias confirmed by doc)

* docs(research): § 6 Q5 closed — history table deferred + full grill findings

Grill-me Q8 outcome: § 6 Q5 (history table) resolved as DEFERRED, with
the full grill analysis preserved inline so the next reviewer doesn't
have to re-derive why we said no.

Findings captured:

- WHAT it would do — point-in-time index gains a temporal dimension
  ("when did symbol X get @deprecated?", "coverage trend over 50
  commits", "files that became dead this week").
- WHAT audit --base <ref> already covers — pairwise diff serves the
  most-common temporal question (PR-scoped delta) with no schema
  growth. Longitudinal "evolved over commits 1..N" is the unfilled gap.
- TWO SHAPES table — per-commit snapshots (~25 GB on 500-commit
  retention; trivial query cost) vs append-only event log (~5-25 MB
  deltas; heavy recursive-CTE query cost).
- BACKFILL COST — N reindexes (~30s each = ~4 hrs first-run for 500
  commits) is the same for both shapes; deal-breaker today.
- ARCHITECTURE IMPACT — schema bump (minor per pre-v1 lesson), db.ts
  + indexer hooks, retention policy config, deeper git integration.
- WHY DEFER — anti-bloat meta-rule (no recipe demands it); audit
  --base covers common case; backfill prohibitive without paying use
  case; shape-decision wasted without empirical access patterns.
- REVISIT TRIGGERS — TWO consumers shipping jq-based "audit runs over
  time" workflows (mirrors B.5 verdict-threshold deferral pattern), OR
  query_baselines evolution becoming a recurring agent need.

The full analysis is now inline in § 6 Q5 (~30 lines + cost table).
Per user request: don't lose vital information; document grilling
findings for fuller context. Future reviewers see the full reasoning,
not just "deferred" — same posture as § 8 errata's "future readers
can see the diff between v1 and v2."

§ 6 status after this commit: ALL FIVE OPEN QUESTIONS RESOLVED. Q1
(daemon-default), Q2 (FTS5 default), Q3 (LSP shape), Q4 (plugin
scope), Q5 (history table) — every decision the doc was authored to
force is now pinned with rationale and revisit triggers (where
applicable).

* docs(research): § 1.9 reframe + § 3 "No telemetry upload" floor

Grill-me Q9 outcome: § 1.9's "Recipe usage telemetry" framing was a
gotcha. The word "telemetry" carries upload / aggregation /
surveillance connotations that don't match the actual capability
(purely local recency tracking) — and would either get the feature
rejected sight-unseen by privacy-conscious users / corp installations
OR silently set up substrate for a future "phone home" PR without an
explicit non-goal saying we won't.

Renamed + tightened § 1.9:

- "Recipe usage telemetry" → "Local recipe-recency tracking".
- Table renamed recipe_usage → recipe_recency (named after the value,
  not the act).
- Added 90-day retention bound (caps unbounded growth via per-reindex
  pruning).
- Added opt-out config (`recipe_recency: false` skips the reconciler).
- --recipes-json surface spec'd: {recipe_id, last_run_at,
  run_count_90d}.
- Naming-note paragraph explains why "telemetry" was rejected.

New § 3 ergonomic floor row "No telemetry upload":

- Locks in the privacy posture explicitly. No HTTP-out primitive in
  codebase today (grep-able), but the floor exists to resist
  accumulation pressure — a future "anonymous opt-in usage stats to
  help prioritize recipes" PR would look reasonable without an
  explicit floor.
- Convergent with fallow (probably also doesn't upload) — floor, not
  moat.
- Cross-references item 1.9 as the only usage-data feature; consumers
  can audit the .codemap/index.db location + retention bound.

Lockstep update needed when item 1.9 ships: docs/why-codemap.md
"What Codemap is not" gains "Codemap never uploads usage data" per
docs/README.md Rule 10. Already cross-referenced in § 7 of this doc.

* docs(research): drop all fallow framing — codemap is structurally unique

User reframe: codemap is the only SQL-based code index in the market;
inspiration comes from the free and open internet (LSP spec, SQLite
docs, AST tooling), not code-by-code cloning of any peer tool. Drop
fallow as a yardstick throughout.

Vital information preserved (per "don't lose any vital information
that is used to execute the plan"):

- Closed-dead-subgraph motivator for C.9 — kept as an abstract pattern
  description in § 2.3 caveat (N-file packs with self-imports, non-
  zero fan-in, none reachable from real entry). Was previously cited
  to fallow.md § 0; now stands on its own merit.
- LSP read-side capabilities (show / impact / watch) — kept; LSP spec
  upstream is now the protocol authority instead of fallow's
  crates/lsp/.
- Runtime-tracing scope distinction — § 3 floor reframed to anchor on
  "different product class entirely" (live process data vs static
  analysis) instead of "fallow's paid moat."
- Predicate-as-API moat (A) — kept; justification now anchors on
  intrinsic merit (SQL is durable, agents compose any predicate)
  rather than "fallow ships verdicts; we don't."
- Schema-breadth moat (B) — kept; justification now "codemap-specific
  extractions; their richness directly determines what JOINs are
  expressible" rather than "fallow has none of these."

Section-by-section changes:

- HEADER — "Companion docs / Source for deep-dives" replaced with
  "Companion doc" (competitive-scan only) + "Positioning" paragraph
  declaring structural uniqueness.
- § 2.3 original-framing quote — paraphrased to drop the "(e.g.
  fallow, knip, jscpd)" parenthetical; pointers to roadmap.md for the
  full original wording. (roadmap.md itself still has the parenthetical;
  separate-PR scope.)
- § 2.3 caveat — closed-dead-subgraph case described abstractly; no
  source citation needed.
- § 2.5 LSP shim — "fallow has crates/lsp/" → "LSP spec upstream is
  the protocol authority."
- § 3 intro — mission framing rewritten; "equal/surpass fallow"
  language replaced with "extract maximum value from the SQL-index
  architecture; grow the ecosystem" + "only SQL-based code index in
  the market" positioning.
- § 3 Moat A — anchored on intrinsic merit (SQL durable + agent
  composability) instead of fallow comparison.
- § 3 Moat B — anchored on "substrate every recipe layers on; richness
  determines JOIN expressivity" instead of "fallow has none of these."
- § 3 ergonomic floors — dropped all "fallow is also fast" /
  "Convergent with fallow" annotations; reframed runtime-tracing as
  "different product class entirely (live process data, not static
  analysis)" + reframed telemetry-upload as standalone safety promise.
- § 4 — DELETED ENTIRELY ("What to inspect in the fallow source
  tree"). Replaced with "Inspiration sources for plan-PR authoring"
  table listing open specs / primitive sources only (LSP spec, SQLite
  docs, oxc node reference, Lightning CSS, JSON-RPC + MCP spec, TC39
  proposals, existing codemap surface, internal third-party graph
  audits). Discipline statement preserved: every plan PR cites the
  spec / primitive source it took inspiration from.
- § 5 (d) row + T-table T+5w → +7w cell — dropped fallow crates/lsp/
  refs; LSP spec is now the named authority.
- § 6 Q1 — dropped fallow.md § 6 citation; stale-index frequency now
  anchored on PR #46 + PR #56 internal evidence.
- § 6 Q4 — dropped fallow.md § 0 + § 6 citations; closed-dead-subgraph
  case cross-refs § 2.3 caveat instead.
- § 7 cross-references — removed research/fallow.md and fallow
  upstream entries. Added § 4 inspection list as a self-reference.
- § 8 errata § 2.3 row — dropped fallow.md citation; pattern described
  inline.

Net effect: the doc stands on codemap's intrinsic structural
properties. No peer-tool framing remains. The mission is now
self-coherent: extract max value from the SQL-index architecture +
grow the ecosystem, anchored on the unique-in-market positioning.

* docs(research): retract uniqueness claim — honest cohort positioning

Fact-check finding: the "structurally unique — only SQL-based code
index in the market" claim doesn't hold. Web search + verification
surfaced a real cohort of SQLite-backed code indexers for AI agents:

- srclight (29 stars) — SQLite FTS5 + tree-sitter + embeddings + MCP,
  42 tools, 11 langs. Pitch identical to codemap's ("AI agents spend
  40-60% tokens on orientation; we eliminate this").
- Sverklo (30 stars) — local-first MCP, symbol graph, blast-radius,
  open-source alternative to Greptile/Sourcegraph.
- ctxpp / ctx++ (17 stars) — Go MCP, tree-sitter, SQLite + FTS +
  vector, blast-radius analysis (= codemap's impact).
- KotaDB (99 stars) — TS + Bun + SQLite — IDENTICAL stack to codemap.
- codemogger (2026) — MCP, tree-sitter, SQLite + FTS + vector,
  semantic search.
- @squirrelsoft/code-index, QuickAST, code-scale-mcp, CodeAgent
  Indexing Engine, Polyglot Indexer MCP, Continue's CodeSnippetsIndex
  — all SQLite-backed code indexers with overlapping surface.

Codemap is one of ~10+, NOT unique. Retracting the claim.

Honest differentiation (after verification):

1. Predicate-as-API — peers ship pre-baked verbs / MCP tools; codemap
   exposes raw SQL + recipes. Genuinely rare in the cohort.
2. Pure structural — no embeddings, no LLM in box. Most peers add
   vector search by default. Genuine differentiation.
3. JS/TS/CSS-ecosystem-deep extraction — CSS variables/classes/
   keyframes, React components.hooks_used, type_members, markers.
   Peers focus on cross-language symbol+call surface via tree-sitter.

The depth axis (3) is structurally enabled by parser choice — oxc
(JS/TS) and lightningcss (CSS) are Rust-based and ecosystem-
specialized; peers using tree-sitter trade depth for breadth.

Where codemap is BEHIND the cohort (not hidden): multi-language
support (codemap = TS/JS/CSS only; peers = 10-15 langs), star count,
embeddings/semantic search, market traction.

Edits applied:

- HEADER positioning paragraph — retracted "structurally unique";
  named the cohort explicitly (srclight, Sverklo, ctxpp, KotaDB,
  codemogger, etc.); spelled out the three differentiation axes;
  added the parser-choice rationale (oxc + lightningcss as the
  structural enabler of axis 3).
- § 3 moat-intro line — replaced "the only SQL-based code index in
  the market" with "specific niche in the SQLite-backed-code-index
  cohort" + the three axes. Reviewer test reframed: eroding either
  moat turns codemap into "yet-another-tool-in-the-cohort instead of
  the predicate-shaped specialist."

Moats A and B themselves required no rewrite — their justifications
(predicate-as-API durability + extracted-structure substrate) hold
under the corrected positioning. The peer cohort discovery actually
sharpens both moats: A is the specialty (raw SQL surface) and B is
the depth axis (richer extraction than tree-sitter cohort).

* docs(research): § 1.4 refactor-risk formula — orphan + NULL fixes + caveat

Grill-me Q12 outcome: § 1.4's "fan_in × (100 - coverage_pct)" formula
had two correctness bugs and one accepted modeling limitation:

CORRECTNESS FIXES (must ship):
- Orphans (fan_in=0) scored 0 → "no risk" → wrong (orphans are
  high-risk: dead code or hidden-import targets we don't track).
  Fix: `fan_in + 1` so orphans score on coverage alone.
- NULL coverage_pct propagated through the formula → 100 - NULL = NULL
  → row dropped from ORDER BY → unmeasured-coverage symbols silently
  vanished from the ranking. Fix: COALESCE(coverage_pct, 0) treats
  unmeasured as 0% (high risk).

ACCEPTED v1 TRADE-OFF:
- Linear-in-fan_in (fan_in 100 with 99% coverage = fan_in 1 with 0%
  coverage in the score). Real, but not worth fixing in the bundled
  recipe — users tune via project-local override.

Caveat block in refactor-risk-ranking.md (will accompany the recipe
when (a) ships) names tuning axes for project-local overrides:
- Log-scale fan_in (LOG(fan_in + 1) * 30) for hub-heavy codebases
- Visibility weight (if @public / @internal / @beta JSDoc tags are
  used consistently)
- LOC weight (if test-density varies across files)

Why ship-with-caveat instead of multi-axis composite (Option B):
- Moat A says recipes are saved queries (starting points), not
  authoritative verdicts. Bundled formula gets 80% right; users iterate.
- Anti-bloat meta-rule — every additional axis encodes more opinions;
  shipping minimal forces explicit thought during tuning.
- Ecosystem-specific axes (visibility weight, LOC weight) shouldn't
  be in the bundled default.

Effort stays XS. The .md caveat block lands in the (a) plan PR / impl
PR alongside the .sql; not part of THIS research-note PR's scope.

* docs(research): § 1.5 boundary violations — Shape A directional rules

Grill-me Q13 outcome: § 1.5 was underspecified ("--boundaries <config>
flag on audit OR recipe consuming the config"). Three real questions
needed answering: where the config lives, what shape, recipe-or-flag.

Shape A (directional rules) locked in for v1:

  boundaries: [
    {
      name: "no-cross-feature",
      from_glob: "src/features/*/**",
      to_glob:   "src/features/*/**",
      action: "deny",
      except_self: true,
    },
    ...
  ]

Why A over B (element-types) over C (layers) — honest discriminator:

A and B have IDENTICAL expressiveness (B compiles to A at index time).
The real question is ergonomics-at-scale vs forward-compat / smallest-
viable-config:

- A wins 5 of 6 dimensions: smallest-viable-config (one entry); Zod
  schema simplest; mental-model load (one concept); forward-compat (B
  layers on top later as sugar); backwards-compat (never paint into a
  corner; primitives are durable).
- B wins only "ergonomics at scale" (5+ rules with element reuse) —
  exactly the dimension that can be added later as a sugar layer
  without breaking A.
- C (layer ordering) is most opinionated; only fits layered
  architectures. Not a v1 default.

Decision rule (ship the smallest primitive that doesn't paint into a
corner; layer ergonomics on top later) mirrors § 6 Q5 history-table
defer logic.

Implementation reuses every shipped or in-flight piece of plumbing:

- Zod config slot (existing src/config.ts substrate)
- Index-time reconciler (mirrors recipe_recency from item 1.9)
- New boundary_rules table (moat-B-aligned schema growth)
- Bundled recipe boundary-violations.sql via SQLite GLOB operator
- SARIF output formatter (already shipped) for CI gate

NO new CLI flag — moat-A clean. The verb is query --recipe
boundary-violations --format sarif. Recipe consumes config-as-data;
SARIF output mode handles verdict-shaped CI consumers.

Effort stays S. Element-types / layer sugar deferred to v1.x with
explicit "demand-driven" trigger (mirrors fallow.md B.5 verdict-
threshold deferral pattern, kept in this doc as the recurring
deferral idiom).

* docs(research): § 1.1, 1.6, 1.8 sanity sharpening (gotchas + envelopes)

Grill-me Q14 outcome: three remaining § 1 rows had implicit gotchas
the recipe author would otherwise have to discover during impl. Each
row gets a small clarification — substrate unchanged, effort unchanged.

§ 1.1 components-touching-deprecated:
- Was: "One bundled recipe (components-touching-deprecated)"
- Now: explicit two-path UNION
  - HOOK PATH: components.hooks_used JSON overlap with @deprecated
    symbols (catches deprecated hooks like useDeprecatedThing)
  - CALL PATH: calls.caller_name IN (SELECT name FROM components) ×
    @deprecated symbols by callee_name (catches regular deprecated
    functions called inside components)
- Hook-only variants would ship false-negatives — recipe author needs
  the explicit UNION to avoid the trap.

§ 1.6 unused-type-members:
- Was: "Recipe (unused-type-members) — needs JSON-extraction predicate"
- Now: ADVISORY recipe with explicit caveat block in .md. Output is
  "review these" candidates, NEVER "safe to delete" — TS has multiple
  indirect-usage classes codemap's substrate doesn't track:
    - Indexed access: T['fieldName']
    - keyof T
    - Type spreads: type X = T & {...}
    - Mapped types: {[K in keyof T]: ...}
  These produce false-positives. Recipe is useful as a candidate
  surfacer; agents must verify before deletion.

§ 1.8 more MCP resources:
- Was: hand-wave "add codemap://files/{path} and codemap://symbols/
  {name}"
- Now: spell out disambiguation envelope (reuses {matches,
  disambiguation?} pattern from PR #39 show/snippet) — symbols with
  duplicate names across files (Component, index, default, util-name
  collisions) return all matches with by_kind / files / hint metadata.
  Plus ?in=<path-prefix> query parameter mirroring show --in <path>.
- Without spelling this out, the implementation would have to invent
  disambiguation OR ship a "first match wins" gotcha.

Net: each row's What's-needed cell now contains enough detail that
the recipe / resource author can implement without re-deriving the
JOIN structure or envelope shape. Tactical clarity layered on top of
the structural decisions made in earlier grills.
SutuSebastian added a commit that referenced this pull request May 4, 2026
… on parser.ts

Fact-checking against codebase post-PR-#69-and-#70 surfaced four stale
spots; concise-comments rule re-applied to recently-authored parser.ts
comments.

DOCS LIFTED (post-FTS5 / Mermaid / complexity merge):

- README.md (root) line 113 — --format enum was missing `mermaid`.
  Updated to <text|json|sarif|annotations|mermaid> + added the
  bounded-input contract one-liner + 50-edge ceiling note.
  Added --with-fts example block alongside (was missing entirely;
  README is the canonical CLI surface per docs/README.md Single source
  of truth table).

- docs/architecture.md output-formatters paragraph — described only
  formatSarif + formatAnnotations; missing formatMermaid + bounded-
  input contract. Added formatMermaid description + MERMAID_MAX_EDGES
  reference + the no-auto-truncation reasoning (would be a verdict
  masquerading as output mode). Updated the --format CLI enum to
  include mermaid; same for the MCP tools format union.

- .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/
  SKILL.md — recipe-id list missed three coverage recipes
  (untested-and-dead, files-by-coverage, worst-covered-exports)
  shipped earlier in PR #65/#56 era. Lockstep update per Rule 10.
  Skill now lists 20 of 20 bundled recipe ids.

CONCISE-COMMENTS SWEEP on parser.ts (recently authored):

- Trimmed the 14-line complexityStack JSDoc block to 6 lines. Kept:
  the -1 sentinel rationale (non-obvious), the WeakMap rationale (the
  bug fix from PR #70 review). Cut: re-stating push/pop semantics
  obvious from method names + step-by-step "this then that" prose.

- Removed the "Defer complexity push to..." comment in the
  VariableDeclaration handler. The 4-line block restated the design
  decision documented one screen up in the complexityStack jsdoc;
  cross-ref makes it redundant. Per concise-comments § "Cut" rule:
  "Cross-references that save grep time" — keep when they actually do;
  cut when they restate.

Verification:
- bun run check: format + lint + typecheck + 23/23 golden ✓
- Recipe count: SQL files = 20, skill mentions = 20 (1:1 match) ✓
- SCHEMA_VERSION = 8 in db.ts; docs/architecture.md says 8 ✓
- complexity column documented in architecture.md + glossary.md ✓
- --with-fts in README.md + architecture.md + glossary.md +
  roadmap.md (consumer-facing surfaces all aligned) ✓
- --format mermaid in README.md + architecture.md + glossary.md +
  agent rule/skill ✓
SutuSebastian added a commit that referenced this pull request May 4, 2026
…sted recipe (research note § 1.4) (#70)

* feat(complexity): cyclomatic complexity column + high-complexity-untested recipe

Research note § 1.4 ship-pick (c) per § 5 cadence. Schema bump
SCHEMA_VERSION 7 → 8.

Schema:
- symbols.complexity REAL column. NULL for non-function kinds and
  class methods (v1 limitation documented in recipe .md).

Parser:
- complexityStack maintained alongside scopeStack. Function entry
  pushes {symbolIndex, count: 1}; branching-node visitors increment
  top.count; function exit pops + writes count into the already-
  pushed symbol row's complexity field.
- McCabe decision points counted: if, while, do-while, for, for-in,
  for-of, case X (not default:), &&/||/??, ?:, catch.

Bundled recipe high-complexity-untested:
- Joins symbols (complexity >= 10) with coverage (< 50%).
- Combines structural + runtime evidence axes — surfaces refactor-
  priority candidates that untested-and-dead and worst-covered-exports
  miss (they catch dead-or-uncalled, this catches called-but-undertested-
  AND-branchy).

Empirical sanity check on codemap's own index after reindex:
- extractFileData (parser.ts main visitor) → complexity 108 ✓
- stringifyTypeNode → 42 ✓
- All non-function kinds have NULL complexity ✓
- high-complexity-untested recipe returns 7 functions all from
  src/parser.ts (which has 0% coverage; complexity ≥ 10) ✓

Lockstep updates per Rule 10 (templates/agents + .agents):
- Trigger pattern row "What's high-complexity AND undertested?"
- Quick reference row for SELECT name, complexity FROM symbols
- Recipe-id list extended in SKILL.md

Plus architecture.md (schema version 8, complexity column docs),
glossary.md (cyclomatic complexity entry), patch changeset.

Files changed:
- src/db.ts (SCHEMA_VERSION + symbols.complexity column + insertSymbols
  bind + SymbolRow optional complexity field)
- src/parser.ts (complexityStack + branching node visitors + push/pop
  in FunctionDeclaration / VariableDeclaration arrow-fn paths)
- templates/recipes/high-complexity-untested.{sql,md}
- docs/architecture.md (schema version + symbols column doc)
- docs/glossary.md (new entry)
- templates/agents/rules/codemap.md + .agents/rules/codemap.md
  (trigger + quick-ref rows)
- templates/agents/skills/codemap/SKILL.md + .agents/skills/codemap/
  SKILL.md (recipe-id list)
- .changeset/cyclomatic-complexity.md (patch)

Verification:
- bun test: 754 pass
- bun run check passes (format, lint, typecheck, 23/23 golden queries)
- Live re-index against codemap source produces sensible complexity
  values (parser visitor itself is the highest at 108, which tracks)

* docs(skill): add complexity column to symbols schema in skill files

CodeRabbit catch on PR #70: the high-complexity-untested recipe row was
added to .agents/skills/codemap/SKILL.md but the symbols table schema
section (under "### `symbols` — Functions, types, ...") still listed
columns through `visibility` only, missing the new `complexity REAL`
column. Verified by reading the file — claim was correct.

Both lockstep mirrors (.agents/ + templates/agents/) updated with the
same row:

  | complexity | REAL | Cyclomatic complexity (`1 + decision points`)
  for function-shaped symbols. NULL for non-functions and class methods
  (v1). Powers --recipe high-complexity-untested. Decision points: if,
  while, do…while, for/for-in/for-of, case X: (not default:),
  &&/||/??/?:, catch |

Per docs/README.md Rule 10 — agent rule + skill schema docs must stay
in lockstep with code-side schema changes. The trigger-pattern row +
recipe-id list were already updated; the schema-table row was the gap.

* fix(complexity): per-function visitors fix multi-declarator misattribution + cleanups

CodeRabbit raised three valid findings on PR #70. All fact-checked
against the code; all correct.

A) docs/architecture.md symbols schema table was malformed:
- Markdown table separator row had extra `| --- | ---` segments because
  oxfmt mis-counted columns when the description contained `|` chars
  inside `&&`/`||`/`??` backtick spans.
- The complexity row's description was split across THREE cells with
  broken backtick fences.
- Fix: restored single-row layout (3 cells: Column | Type | Description)
  and rephrased the decision-point list to avoid `|` inside backticks
  ("short-circuit `&&` / `||` / `??`" instead of "`&&`/`||`/`??`").

B) src/parser.ts complexity misattribution on multi-declarator
   VariableDeclaration (e.g. `const a = () => {…}, b = () => {…};`):

   Pre-fix: VariableDeclaration enter pushed all declarators' complexity
   entries up front. Then visitor traversed `a`'s body — branches
   incremented top (= b's entry). Then `b`'s body. Exit pops in reverse
   →  symbols[1].complexity = 3 (wrong), symbols[0].complexity = 1
   (wrong). Real bug.

   Fix: push/pop complexity on the FUNCTION-shaped node visitors
   (ArrowFunctionExpression / FunctionExpression) — not on
   VariableDeclaration. The VariableDeclaration handler still creates
   the symbol row but only RECORDS the symbol → init-node mapping in a
   WeakMap. The ArrowFunctionExpression / FunctionExpression enter
   handler reads the WeakMap to know which symbol to write back to;
   anonymous arrow fns (callbacks, IIFEs) get -1 and just track count
   without persistence.

   Verified against fixture:
     const a = () => { if (1===1) {…} },
           b = () => { if (2===2) {…} },
           c = () => 5;
   → a=2, b=2, c=1 (correct; pre-fix was a=1, b=3, c=1)

C) popComplexityInto guard was a no-op (callers passed top.symbolIndex,
   so the equality check was always true). Simplified to parameterless
   popComplexityTop() that always pops + writes back if symbolIndex >= 0.
   Folds naturally into the B refactor — every push/pop pair now lives
   in a function-shaped visitor.

Also re-ran codemap query against codemap source post-fix:
  extractFileData=108, stringifyTypeNode=42, extractClassMembers=18,
  extractLiteralValue=15, extractObjectMembers=14
Same scores as pre-fix on these (no FunctionExpression / arrow nesting
in those particular functions, so the bug didn't surface) — confirms
the refactor is a strict improvement, not a regression.

* docs: audit + lift remaining stale references; concise-comments sweep on parser.ts

Fact-checking against codebase post-PR-#69-and-#70 surfaced four stale
spots; concise-comments rule re-applied to recently-authored parser.ts
comments.

DOCS LIFTED (post-FTS5 / Mermaid / complexity merge):

- README.md (root) line 113 — --format enum was missing `mermaid`.
  Updated to <text|json|sarif|annotations|mermaid> + added the
  bounded-input contract one-liner + 50-edge ceiling note.
  Added --with-fts example block alongside (was missing entirely;
  README is the canonical CLI surface per docs/README.md Single source
  of truth table).

- docs/architecture.md output-formatters paragraph — described only
  formatSarif + formatAnnotations; missing formatMermaid + bounded-
  input contract. Added formatMermaid description + MERMAID_MAX_EDGES
  reference + the no-auto-truncation reasoning (would be a verdict
  masquerading as output mode). Updated the --format CLI enum to
  include mermaid; same for the MCP tools format union.

- .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/
  SKILL.md — recipe-id list missed three coverage recipes
  (untested-and-dead, files-by-coverage, worst-covered-exports)
  shipped earlier in PR #65/#56 era. Lockstep update per Rule 10.
  Skill now lists 20 of 20 bundled recipe ids.

CONCISE-COMMENTS SWEEP on parser.ts (recently authored):

- Trimmed the 14-line complexityStack JSDoc block to 6 lines. Kept:
  the -1 sentinel rationale (non-obvious), the WeakMap rationale (the
  bug fix from PR #70 review). Cut: re-stating push/pop semantics
  obvious from method names + step-by-step "this then that" prose.

- Removed the "Defer complexity push to..." comment in the
  VariableDeclaration handler. The 4-line block restated the design
  decision documented one screen up in the complexityStack jsdoc;
  cross-ref makes it redundant. Per concise-comments § "Cut" rule:
  "Cross-references that save grep time" — keep when they actually do;
  cut when they restate.

Verification:
- bun run check: format + lint + typecheck + 23/23 golden ✓
- Recipe count: SQL files = 20, skill mentions = 20 (1:1 match) ✓
- SCHEMA_VERSION = 8 in db.ts; docs/architecture.md says 8 ✓
- complexity column documented in architecture.md + glossary.md ✓
- --with-fts in README.md + architecture.md + glossary.md +
  roadmap.md (consumer-facing surfaces all aligned) ✓
- --format mermaid in README.md + architecture.md + glossary.md +
  agent rule/skill ✓
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant