Status: open · ships last in the impact-vs-cadence sequence after boundary / parametrised-recipe / recency / type-member picks — orthogonal to those items (none query reachability). Closed-dead-subgraph caveat: N-file packs with sibling imports but no real entry point;
fan-in/fan-outrank hotspots, they do not detect unreachable files.Motivator: closed-dead-subgraph case — N-file packs where every file imports a sibling (non-zero
dependenciesfan-in for all) but none is reachable from a real entry point. Today'suntested-and-deadrecipe false-positives Next.jsapp/**/page.tsxfiles for the same reason: framework entry points aren't recognized as live without per-framework awareness. This plan proposes the smallest plugin contract that closes the gap.Tier: XL effort. Ships last in the impact-vs-cadence sequence — parallel iteration unblocks impl before the slot opens.
Roadmap: § Core substrate & platform
Start with Slice 1: files.is_entry column + hard-coded Next.js-style glob in a test fixture only — no plugin discovery yet. Prove reachability predicate via SQL before loader/contract work. Ships last in cadence but parallel design iteration is fine.
| File | What to read |
|---|---|
src/db.ts |
files DDL — add is_entry (Q3) |
src/application/index-engine.ts |
Post-files insert hook for annotations |
src/config.ts |
Future plugins: config surface (Q2) |
templates/recipes/untested-and-dead.sql |
Reachability false-positive class to fix |
templates/recipes/unimported-exports.sql |
Second live-predicate recipe |
docs/plans/lsp-diagnostic-push.md |
Complementary consumer — same FP class on squigglies |
plugin loader (Slice 3) → (glob, is_entry) annotations
index hook → files.is_entry = 1 for matched paths
reachability sweep (Slice 2) → dead-files-by-reachability recipe
live-predicate recipes JOIN is_entry / reachable CTE
is_entry column + migration; fixture sets one app/page.tsx entry; SELECT path FROM files WHERE is_entry = 1 returns expected row. No plugin JSON contract in slice 1.
Plugin npm packages; edge injection; verdict CLI; changes to non-reachability recipes.
bun run typecheck # db.ts + index hook
bun src/index.ts --files fixtures/golden/c9-fixture/… # after fixture lands
bun src/index.ts query "SELECT path, is_entry FROM files" --json
bun test scripts/query-golden-coverage-matrix.test.mjs # after Slice 2 recipeC.9 ships last because it only sharpens reachability-predicate recipes: untested-and-dead, unimported-exports, and (Slice 2) dead-files-by-reachability. Boundary, parametrised-recipe, recency, type-member, complexity, hotspot, call, marker, and CSS recipes do not ask "is this file/symbol live?" — they do not inherit C.9's false-positive class.
Orthogonal picks ship first: boundary violations (own boundary_rules table), rename-preview / parametrised recipes (query calls, not reachability), recipe-recency (own table), unused type members (type_members × import specifiers).
C.9 before LSP diagnostic-push: entry-point hints reduce false-positive squigglies on framework files for the two live-predicate recipes. Not a hard block — lsp-diagnostic-push.md can ship without C.9; diagnostics carry the same caveats those recipes document today.
If this plan is abandoned: close as Status: Rejected (YYYY-MM-DD) — <reason>. Design surface captured either way. The two live-predicate recipes keep framework-file caveats permanently.
These are committed to v1. Questions opened against them must justify against the linked decisions.
| # | Decision | Source |
|---|---|---|
| L.1 | Entry-point hints only (Shape A — glob → is_entry: true annotations on files). No arbitrary dependencies edge injection. |
This plan § Motivator; § What C.9 sharpens |
| L.2 | Static config only — plugins describe rules in static config (globs, glob → annotation mappings). No JS evaluation at index time. | Floor "No JS execution at index time" |
| L.3 | Moat-A clean — recipes consume the new substrate via SQL; no new verdict-shaped CLI verbs. | Moat A |
| L.4 | Moat-B aligned — is_entry annotation IS substrate growth (richer extracted structure on files). New schema column or table. |
Moat B |
| L.5 | No edge injection in v1 — defer to v2 if a real recipe demands it; backwards-compat preserved (additive). Mirrors query_baselines deferral discipline. |
Q4 resolved (lifted) |
These are the design questions the plan-PR resolves before impl starts (per the parallel-plan-PR shape). Each gets a section below as it crystallises.
- Q1 — Plugin contract shape. JSON schema (declarative)? Zod-validated TS module (typed)? Markdown-with-frontmatter (mirrors recipe-as-content registry per PR #37)? What fields beyond
entry_globs? - Q2 — Plugin discovery mechanism. npm peerDep registration (mirrors community-adapter pattern from
roadmap.md § Strategy)? Path-glob auto-discovery from<projectRoot>/.codemap/plugins/? Config-listed (.codemap/config.{ts,js,json}plugins: [...])? Some combination? - Q3 — Schema delta.
is_entry INTEGER DEFAULT 0column onfiles(single boolean) vs separateentry_annotations(file_path, plugin_id, reason)table (multiple plugins can co-annotate; preserves provenance). The latter is moat-B-aligned but slightly more storage. - Q4 — Reachability sweep algorithm. BFS from
is_entryfiles overdependencies? Materialisedis_reachablecolumn (cheap reads, expensive write)? On-demand recursive CTE (no materialisation; might be slow on big graphs)? Cache invalidation strategy? - Q5 — Bundled starter plugins for v1. Next.js (
app/**/page.tsx,pages/**/*.{ts,tsx},app/**/layout.tsx, etc.)? Vite (vite.config.{ts,js}, HTML<script src>)? Storybook (*.stories.{ts,tsx})? Vitest config? TanStack Router (__root.tsx, route files)? Bundle 1, 2, or 3 starters? Repo-structure interaction: if starter plugins ship as separate packages (e.g.codemap-plugin-nextjs), monorepo conversion enters scope perlsp-diagnostic-push.md § Repo-structure tradeoffs. - Q6 — Plugin loading order / conflict resolution. What if two plugins match the same file (e.g. both Next.js + Vite plugins claim
vite.config.tsis an entry)? Union of annotations? Conflict error? First-match-wins by load order? Likely "union" for entry hints (annotations compose; multiple-marking is fine). - Q7 — Composition with project-local recipes. Project-local recipes can already JOIN
files— they getis_entryfor free. Anything else needed? - Q8 — Minimal API surface for community plugins. What's the smallest contract that lets someone publish a
codemap-plugin-tanstack-routerpackage without forking core? Mirrors community-adapter discipline; same lever. Repo-structure interaction: community plugins as separate npm packages is one of the four triggers for converting codemap-itself to a monorepo — seelsp-diagnostic-push.md § When to revisit. - Q9 — Backwards-compat for projects without plugins. Reachability sweep +
is_entryshould default to "all files reachable" (current behaviour) when no plugins are loaded, so existing recipes don't change semantics. Verify the SQL wording handles this cleanly.
Each open decision will get a "Resolution" subsection below as it crystallises (mirrors the research-note's § 6 pattern).
Three new pieces; each composes with existing infrastructure:
- Plugin loader (Q1, Q2) — scans for plugin files at index startup; validates against the contract schema; produces a list of
(glob, is_entry)annotations. - Indexer hook (Q3) — at index time, after
filesrows are inserted, the loader's annotations are matched against file paths and the relevant rows getis_entry = 1(or rows are inserted intoentry_annotations, depending on Q3). - Reachability sweep (Q4) — either materialised at index time (column on
files) or computed on demand by recipes (recursive CTE). Slice-1 likely starts on-demand; promote to materialised if perf demands.
No CLI changes. No new verb. Recipes consume the new substrate.
Per the 2026-05 errata pass: the original framing claimed C.9 "sharpens every shipped recipe" and that the LSP plan blocked on C.9's entry-point awareness. Both were wrong. (d) was reframed across three revisions (v1 "thin shim, agent UX" → v2 "orthogonal, ship before (b)" → v2.5 "dropped" → v3 "diagnostic-push server + VSCode extension, ships after (b)"). Accurate scope:
C.9 sharpens (recipe layer):
untested-and-dead— currently false-positives Next.jsapp/**/page.tsxand similar framework files. Reachability sweep fromis_entry = 1files closes the gap.unimported-exports— false-positives barrel-only consumption + framework-only-imported exports. Same reachability path closes a subset of the gap (re-export chain following is a separate axis tracked in the recipe's own.md).- One hypothetical future recipe —
dead-files-by-reachability(Slice 2 below) — closes the closed-dead-subgraph case directly.
C.9 does NOT sharpen:
- (d) LSP diagnostic-push server + VSCode extension (§ 2.5 — ships after C.9 per § 5 v3 cadence) — complementary, not orthogonal. Standard LSP request handlers (
textDocument/references/definition) are NOT what (d) ships, sois_entrydoesn't ride those response shapes. But (d)'sDiagnostic[]push foruntested-and-deadandunimported-exports(recipes that ask "is this live?") inherits C.9's false-positive class — without entry-point awareness, those diagnostics squiggle on Next.jspage.tsxfiles. C.9 landing first means (d) ships with cleaner diagnostic precision from day one. (d) can ship without C.9 (diagnostics carry the same caveats the recipes already document); landing C.9 first is just better UX. - Boundary recipes (item 1.5) — query "who imports whom"; reachability is irrelevant.
- Hotspot recipes (
fan-in,fan-out) — rank by structural fan; not "is this live." - Complexity recipes (item 1.3) — query
symbols.complexity; orthogonal. - Call recipes (
calls) — query who-calls-what; orthogonal. - Marker / CSS recipes — orthogonal substrates.
This narrowing is why C.9 ships last: every other planned item in the cadence does not depend on entry-point hints.
Per tracer-bullets — ship one vertical slice end-to-end before expanding.
- Slice 1: schema delta + manual
is_entryannotation. Resolve Q3 minimally (probablyis_entry INTEGER DEFAULT 0onfilesfor v1; promote to a separate table in v2 if multi-plugin provenance becomes valuable). Hard-code one Next.js-style glob in the test fixture for now. Verify viacodemap query "SELECT path FROM files WHERE is_entry = 1". No plugin discovery yet. - Slice 2: reachability sweep recipe. New bundled
dead-files-by-reachability.sqlwalksdependenciesfromis_entry = 1files; flags any unreachable file. Test against the closed-dead-subgraph fixture (N-file pack with self-imports + zero entry-marked files → all N flagged). - Slice 3: plugin contract + discovery. Resolve Q1 + Q2; ship the contract; load one starter plugin from the chosen discovery surface. End-to-end: real plugin file → annotations applied → reachability recipe returns correct results.
- Slice 4: bundled starter plugin(s). Resolve Q5 — ship 1-3 starter plugins (Next.js minimum). Cross-fixture tested.
- Slice 5: docs + agent rule update. Per
docs/README.mdRule 10, update bundled agent rule + skill in lockstep — agents need to know aboutis_entryand the reachability recipe.
- Unit: plugin loader, glob matcher, reachability sweep —
*.test.tsper touched file (perverify-after-each-step). - Golden queries: add
dead-files-by-reachabilitygolden expectations tofixtures/golden/scenarios.jsonperdocs/golden-queries.md. - Integration fixture:
fixtures/golden/c9-fixture/containing:- 1 Next.js-style
app/page.tsxentry - N-file widget pack (closed-dead-subgraph reproducer)
- Expected output: N unreachable files flagged;
app/page.tsxreachable.
- 1 Next.js-style
| Item | Mitigation |
|---|---|
| Non-goal: edge injection via plugins (v2). | Per L.5; deferred. If demand emerges, additive in v2. |
| Non-goal: verdict-shaped CLI verb. | Per L.3; recipe + --format sarif already covers CI gating. |
| Risk: plugin contract over-engineered. | Slice 3 ships the smallest contract; iterate based on real plugin-author feedback (community plugins are the leverage signal). |
| Risk: bundled starter-plugin maintenance. | Limit v1 to 1-2 plugins; document community-plugin path clearly so contributors take over framework-specific knowledge. Mirrors community-adapter discipline. |
| Risk: false-positives from misdeclared entry points. | Plugins are opt-in (project lists them in config or installs as peerDep); reachability recipe output is advisory until users verify. Same advisory pattern as unused-type-members. |
| Risk: plan abandoned mid-iteration. | Per docs/README.md Rule 8, close as Status: Rejected (YYYY-MM-DD) — <reason>. Design surface captured either way. |
docs/architecture.md— schema reference (whereis_entrylands)docs/golden-queries.md— golden-query test patterndocs/roadmap.md § Strategy— community-adapter precedent (Q2 / Q8 use the same lever)docs/README.mdRule 3 — plan-file convention (this file's location)docs/README.mdRule 10 — agent rule lockstep update (Slice 5).agents/rules/tracer-bullets.md— slice cadence.agents/rules/verify-after-each-step.md— per-slice check discipline