From 61c77483eb7aaf39e7e0682fd0d9b831b3daf486 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 19 May 2026 23:02:39 +0300 Subject: [PATCH 01/23] =?UTF-8?q?docs:=20add=20substrate=20tiers=201?= =?UTF-8?q?=E2=80=936=20rollout=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codemap-validated baseline and tracer-bullet execution order for tiers 1–6 remainder, explicitly excluding C.9 / files.is_entry. --- docs/plans/substrate-tiers-1-6-rollout.md | 136 ++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/plans/substrate-tiers-1-6-rollout.md diff --git a/docs/plans/substrate-tiers-1-6-rollout.md b/docs/plans/substrate-tiers-1-6-rollout.md new file mode 100644 index 00000000..9032aa8f --- /dev/null +++ b/docs/plans/substrate-tiers-1-6-rollout.md @@ -0,0 +1,136 @@ +# Substrate tiers 1–6 rollout (without C.9) + +> **Status:** In flight — branch `feat/substrate-tiers-1-6`. +> **Parent plan:** [`substrate-extraction.md`](./substrate-extraction.md) (tiers 7–13 out of scope here). +> **Explicit exclusion:** C.9 plugin layer — no `files.is_entry`, no reachability-from-entry, no framework entry hints. See [`c9-plugin-layer.md`](./c9-plugin-layer.md). + +## Baseline (codemap-validated 2026-05-19) + +Reindexed self (`bun src/index.ts --full`); `validate --json` → `[]`. + +| Fact | Value | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SCHEMA_VERSION` | **27** ([`src/db.ts`](../../src/db.ts)) | +| Extractor orchestration | [`src/parser.ts`](../../src/parser.ts) `EXTRACTORS[]` — `symbolsExtractor` → `scopesExtractor` → `complexityExtractor` → `callsExtractor` → `componentsExtractor` → `referencesExtractor` → `runtimeMarkersExtractor` → `testsExtractor` → `markersExtractor` | +| Post-passes (index-engine) | bindings ([`resolveBindings`](../../src/application/bindings-engine.ts)), re-export chains + module cycles (full index only) | +| Live substrate tables | `calls`, `exports`, `import_specifiers`, `references`, `scopes`, `bindings`, `function_params`, `re_export_chains`, `module_cycles`, … | +| Absent (in-scope gaps) | `jsx_*`, `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`, `dynamic_imports`; `calls` call-shape flags; `symbols.{return_type,is_async,is_generator}`; `files.{is_barrel,has_side_effects}`; `bindings.resolution_kind='re-exported'` | + +Codemap impact on call extraction touch chain: `src/extractors/calls.ts` → `src/parser.ts` → `src/application/index-engine.ts` → `src/db.ts`. + +## Scope summary + +| Tier | Shipped today | This rollout closes | +| ----- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | +| **1** | Positions on `calls`/`exports`/`symbols`/`markers`; `import_specifiers` (no `import_id`; side-effect imports skipped) | `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`; optional slice 1.B: side-effect import rows + `import_id` FK | +| **2** | `references`/`scopes`/`bindings`/`function_params`/`re_export_chains`; narrowed kinds | `resolution_kind='re-exported'` in bindings pass; optional reference-kind expansion deferred | +| **3** | Component heuristic only (`components` table) | `jsx_elements` + `jsx_attributes` (new extractor module) | +| **4** | `function_params` (owner-keyed, not `symbol_id` FK) | `symbols.{return_type,is_async,is_generator}`; defer `generic_params`/`type_predicates`/`throws_clauses` tables | +| **5** | Nothing | Tracer bullets: `async_calls` → `try_catch` → `decorators` → `jsdoc_tags` | +| **6** | `re_export_chains`, `module_cycles` | `dynamic_imports`; `files.is_barrel`; `files.has_side_effects` (heuristic — no Tier 8 `package.json` yet). **Not** `files.is_entry`. | + +## Pre-locked decisions (inherited) + +- [R.16](./substrate-extraction.md#pre-locked-decisions): rebuild-forcing DDL → bump `SCHEMA_VERSION`; no in-place migrations. +- [R.18](./substrate-extraction.md#pre-locked-decisions): each slice ships ≥1 recipe + golden fixture update. +- [Tracer bullets](../../.agents/rules/tracer-bullets.md): vertical slice per commit; verify before next slice. + +## Execution order + +Priority = dependency order + reviewability. Each row is one commit (or commit pair: schema + tests). + +### Phase A — Low-risk column extensions (Tier 1 + 4) + +| Slice | Work | SCHEMA | Flagship recipe / golden | +| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **A.1** | `calls` metadata in [`calls.ts`](../../src/extractors/calls.ts): `args_count` (NULL if spread), `is_method_call`, `is_constructor_call` (`NewExpression`), `is_optional_chain` (`node.optional` / callee.optional); dedup key includes call vs `new` | 27→**28** | Extend [`find-call-sites`](../../templates/recipes/find-call-sites.sql) SELECT; update [`fixtures/golden/minimal/find-call-sites.json`](../../fixtures/golden/minimal/find-call-sites.json) | +| **A.2** | `symbols.{return_type,is_async,is_generator}` in [`symbols.ts`](../../src/extractors/symbols.ts) + [`type-stringify.ts`](../../src/extractors/type-stringify.ts); function-shaped kinds only | **29** | New recipe `find-async-functions` + golden | +| **A.3** _(optional)_ | Side-effect import specifier row (`kind='side-effect'`) + `import_specifiers.import_id` FK | **30** | Extend `find-import-sites` or new `find-side-effect-imports` | + +**A.1 validation queries (post-index):** + +```sql +SELECT callee_name, args_count, is_method_call, is_constructor_call, is_optional_chain +FROM calls WHERE file_path = 'src/extractors/calls.ts' LIMIT 5; + +SELECT COUNT(*) FROM calls WHERE is_method_call = 1; +SELECT COUNT(*) FROM calls WHERE is_constructor_call = 1; +``` + +### Phase B — Module graph remainder (Tier 6, no C.9) + +| Slice | Work | SCHEMA | Flagship | +| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- | +| **B.1** | `dynamic_imports` table + visitor on `ImportExpression` in new [`src/extractors/dynamic-imports.ts`](../../src/extractors/dynamic-imports.ts) | **31** | `find-dynamic-imports` + golden | +| **B.2** | Post-pass `files.is_barrel` (100% re-exports, no value defs) in [`index-engine.ts`](../../src/application/index-engine.ts) | **32** | Extend [`barrel-files`](../../fixtures/golden/minimal/barrel-files.json) scenario or recipe | +| **B.3** | `files.has_side_effects` — top-level call/assign heuristic (Tier 8 `package.json` deferred) | **32** or same commit as B.2 | `find-side-effect-files` | + +### Phase C — Bindings polish (Tier 2) + +| Slice | Work | SCHEMA | Flagship | +| ------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------- | +| **C.1** | Add `'re-exported'` to `bindings.resolution_kind` CHECK + walk in [`bindings-engine.ts`](../../src/application/bindings-engine.ts) | **33** | Extend rename-preview / find-references binding filter in golden | + +### Phase D — JSX substrate (Tier 3) + +| Slice | Work | SCHEMA | Flagship | +| ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------- | +| **D.1** | `jsx_elements` table + tracer: self-closing + simple opening tags; `parent_element_id` post-link pass per [Q4](./substrate-extraction.md) | **34** | `find-jsx-usages` + golden on `fixtures/minimal` TSX | +| **D.2** | `jsx_attributes` + fragment rows (`is_fragment=1`) | **35** | Extend recipe with attribute JOIN | + +New extractor registers after `referencesExtractor` (needs scope + refs). Does not replace [`componentsExtractor`](../../src/extractors/components.ts) heuristic. + +### Phase E — Behavioral (Tier 5) + +| Slice | Work | SCHEMA | Flagship | +| ------- | ----------------------------------------------------------------- | ------ | -------------------------------------------------------------- | +| **E.1** | `async_calls` + context stack (`in_loop`, `in_try`) | **36** | `find-unawaited-async-calls` (stretch) or `find-await-in-loop` | +| **E.2** | `try_catch` | **37** | `find-swallowed-errors` | +| **E.3** | `decorators` | **38** | `find-decorator-usage` | +| **E.4** | `jsdoc_tags` — extend [`jsdoc.ts`](../../src/extractors/jsdoc.ts) | **39** | `find-throws-jsdoc` / param tag queries | + +Tier 5 slices depend on Tier 2 scopes (already shipped). `jsdoc_tags` can reuse existing `@deprecated` / visibility parsing. + +## Per-slice Definition of Done + +1. DDL + row types + `insert*` in [`src/db.ts`](../../src/db.ts). +2. Extractor or post-pass wired through [`parser.ts`](../../src/parser.ts) / [`index-engine.ts`](../../src/application/index-engine.ts). +3. Unit tests in [`src/parser.test.ts`](../../src/parser.test.ts) or dedicated `*.test.ts`. +4. Recipe + golden update when user-visible query shape changes. +5. [`docs/architecture.md`](../architecture.md) schema table row(s) for new columns/tables. +6. Checks: `bun run format:check`, `lint`, `typecheck`, affected `bun test`, `bun run test:golden` when golden touched. +7. Re-index before codemap validation queries. + +## Draft PR (push when ready) + +```markdown +## Summary + +- Rollout plan for substrate tiers 1–6 remainder (excludes C.9 / `files.is_entry`). +- Tracer-bullet implementation: call-shape metadata on `calls`, then symbol async/return-type columns, module-graph enrichment, JSX, behavioral tables. + +## Test plan + +- [ ] `bun run check` +- [ ] `bun run test:golden` +- [ ] `bun src/index.ts --full` on self + `validate --json` +- [ ] Spot-check flagship recipes per shipped slice + +## Out of scope + +- C.9 plugin layer / `files.is_entry` / reachability pruning +- Tiers 7–13 (CSS rich, project meta, ORM, …) +``` + +Push: `git push -u origin feat/substrate-tiers-1-6 && gh pr create --draft --title "feat: substrate tiers 1–6 (no C.9)" --body-file …` + +## Risk notes + +- **Dedup semantics:** `calls` dedupes per `(caller_scope, callee)` today; constructor vs call with same name needs distinct dedup keys (A.1). +- **Spread args:** `args_count = NULL` when any argument is `SpreadElement` (substrate Tier 1 open question — bias adopted). +- **Barrel detection:** must not flag files with mixed re-exports and local value symbols. +- **JSX scope:** largest slice; keep D.1 minimal (no attribute values, no conditional render analysis). + +## Closing + +When all scoped slices ship: update [`substrate-extraction.md`](./substrate-extraction.md) per-tier ship status; lift completed items from [`roadmap.md`](../roadmap.md); close this plan (delete + lift per docs-governance). From 79c69fcea1138b1e0d2d4689826aa04647ab43d4 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 19 May 2026 23:02:43 +0300 Subject: [PATCH 02/23] feat: add call-shape metadata to calls table (schema 28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Record args_count, method/constructor/optional-chain flags on call edges; extend find-call-sites recipe and golden fixture. First tracer bullet from the tiers 1–6 rollout plan. --- docs/architecture.md | 28 ++-- fixtures/golden/minimal/find-call-sites.json | 12 +- src/db.ts | 21 ++- src/extractors/calls.ts | 159 ++++++++++++------- src/parser.test.ts | 46 ++++++ templates/recipes/find-call-sites.sql | 3 +- 6 files changed, 190 insertions(+), 79 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index e90ed86f..17ea5285 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -231,18 +231,22 @@ All tables use `STRICT` mode. Tables marked with `WITHOUT ROWID` store data dire ### `calls` — Function-scoped call edges, deduped per file (`STRICT`) -| Column | Type | Description | -| ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | -| id | INTEGER PK | Auto-increment row id | -| file_path | TEXT FK | References `files(path)` ON DELETE CASCADE | -| caller_name | TEXT | Name of the calling function/method | -| caller_scope | TEXT | Dot-joined scope path (e.g. `UserService.run`). Anonymous scopes encode as `$anon_` to avoid sibling-callback collisions | -| callee_name | TEXT | Name of the called function, `obj.method` / `obj.foo.bar` for member chains (recursive flatten), `this.method` for self | -| line_start | INTEGER | 1-based line of the callee identifier token (per [R.6]) | -| column_start | INTEGER | 0-based byte column of the callee token | -| column_end | INTEGER | One-past-last column | - -Edges are deduped per (caller_scope, callee) per file: if `foo` calls `bar` three times in the same file, only one row is stored. Same-named methods in different classes get distinct `caller_scope` values. Module-level calls (outside any function) are excluded — only function-scoped calls are tracked. +| Column | Type | Description | +| ------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------- | +| id | INTEGER PK | Auto-increment row id | +| file_path | TEXT FK | References `files(path)` ON DELETE CASCADE | +| caller_name | TEXT | Name of the calling function/method | +| caller_scope | TEXT | Dot-joined scope path (e.g. `UserService.run`). Anonymous scopes encode as `$anon_` to avoid sibling-callback collisions | +| callee_name | TEXT | Name of the called function, `obj.method` / `obj.foo.bar` for member chains (recursive flatten), `this.method` for self | +| line_start | INTEGER | 1-based line of the callee identifier token (per [R.6]) | +| column_start | INTEGER | 0-based byte column of the callee token | +| column_end | INTEGER | One-past-last column | +| args_count | INTEGER | Argument count; NULL when a spread argument is present | +| is_method_call | INTEGER | 1 when callee is a member expression (`obj.method()`) | +| is_constructor_call | INTEGER | 1 for `new Foo()` (`NewExpression`) | +| is_optional_chain | INTEGER | 1 when the call uses optional chaining (`?.`) | + +Edges are deduped per (caller_scope, callee, call vs constructor) per file: if `foo` calls `bar` three times in the same file, only one row is stored. `foo()` and `new Foo()` with the same callee name remain distinct rows. Same-named methods in different classes get distinct `caller_scope` values. Module-level calls (outside any function) are excluded — only function-scoped calls are tracked. ### `type_members` — Properties and methods of interfaces and object-literal types (`STRICT`) diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index ec42a8a9..be727189 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -5,7 +5,11 @@ "caller_scope": "legacyClient", "line_start": 42, "column_start": 9, - "column_end": 21 + "column_end": 21, + "args_count": 0, + "is_method_call": 0, + "is_constructor_call": 0, + "is_optional_chain": 0 }, { "file_path": "src/consumer.ts", @@ -13,6 +17,10 @@ "caller_scope": "run", "line_start": 12, "column_start": 2, - "column_end": 14 + "column_end": 14, + "args_count": 1, + "is_method_call": 0, + "is_constructor_call": 0, + "is_optional_chain": 0 } ] diff --git a/src/db.ts b/src/db.ts index f7d47d99..d384fe83 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 27; +export const SCHEMA_VERSION = 28; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -164,7 +164,11 @@ export function createTables(db: CodemapDatabase) { callee_name TEXT NOT NULL, line_start INTEGER NOT NULL, column_start INTEGER NOT NULL, - column_end INTEGER NOT NULL + column_end INTEGER NOT NULL, + args_count INTEGER, + is_method_call INTEGER NOT NULL DEFAULT 0, + is_constructor_call INTEGER NOT NULL DEFAULT 0, + is_optional_chain INTEGER NOT NULL DEFAULT 0 ) STRICT; CREATE TABLE IF NOT EXISTS type_members ( @@ -1101,14 +1105,19 @@ export interface CallRow { column_start: number; /** 0-based byte column one past the callee identifier end. */ column_end: number; + /** NULL when the call includes a spread argument. */ + args_count?: number | null; + is_method_call?: number; + is_constructor_call?: number; + is_optional_chain?: number; } export function insertCalls(db: CodemapDatabase, calls: CallRow[]) { batchInsert( db, calls, - "INSERT INTO calls (file_path, caller_name, caller_scope, callee_name, line_start, column_start, column_end)", - "(?,?,?,?,?,?,?)", + "INSERT INTO calls (file_path, caller_name, caller_scope, callee_name, line_start, column_start, column_end, args_count, is_method_call, is_constructor_call, is_optional_chain)", + "(?,?,?,?,?,?,?,?,?,?,?)", (c, v) => v.push( c.file_path, @@ -1118,6 +1127,10 @@ export function insertCalls(db: CodemapDatabase, calls: CallRow[]) { c.line_start, c.column_start, c.column_end, + c.args_count ?? null, + c.is_method_call ?? 0, + c.is_constructor_call ?? 0, + c.is_optional_chain ?? 0, ), ); } diff --git a/src/extractors/calls.ts b/src/extractors/calls.ts index f89a1a54..d3247e7b 100644 --- a/src/extractors/calls.ts +++ b/src/extractors/calls.ts @@ -1,7 +1,7 @@ /** * Calls — one row per (caller, callee) edge, deduped per - * `(caller_scope, callee_name)` per file. Module-level calls skipped - * (`caller` must be non-null). Chained with `componentsExtractor`'s + * `(caller_scope, callee_name, call_kind)` per file. Module-level calls + * skipped (`caller` must be non-null). Chained with `componentsExtractor`'s * CallExpression handler (hook detection). * * Per [R.6]: `line_start` / `column_*` record the callee **identifier @@ -12,72 +12,111 @@ import { offsetToLine } from "./offsets"; import type { TierExtractor } from "./types"; +function argsCount( + args: readonly { type?: string }[] | undefined, +): number | null { + if (!args?.length) return 0; + if (args.some((a) => a.type === "SpreadElement")) return null; + return args.length; +} + +function calleeFromNode(callee: any): { + calleeName: string | null; + tokenStart: number | undefined; + tokenEnd: number | undefined; + isMethodCall: boolean; +} { + let calleeName: string | null = null; + let tokenStart: number | undefined; + let tokenEnd: number | undefined; + let isMethodCall = false; + + if (callee?.type === "Identifier") { + calleeName = callee.name; + tokenStart = callee.start; + tokenEnd = callee.end; + } else if ( + callee?.type === "MemberExpression" && + !callee.computed && + callee.property?.name + ) { + isMethodCall = true; + const segments: string[] = [callee.property.name]; + let cursor: any = callee.object; + while ( + cursor?.type === "MemberExpression" && + !cursor.computed && + cursor.property?.name + ) { + segments.unshift(cursor.property.name); + cursor = cursor.object; + } + if (cursor?.type === "Identifier") { + segments.unshift(cursor.name); + calleeName = segments.join("."); + } else if (cursor?.type === "ThisExpression") { + segments.unshift("this"); + calleeName = segments.join("."); + } + tokenStart = callee.property.start; + tokenEnd = callee.property.end; + } + + return { calleeName, tokenStart, tokenEnd, isMethodCall }; +} + +function isOptionalChain(node: any, callee: any): boolean { + return Boolean(node.optional || callee?.optional); +} + export const callsExtractor: TierExtractor = { tierId: "calls", register(visitor, ctx) { const { scopes, calls, relPath, lineMap } = ctx; const seenCalls = new Set(); + const recordCall = ( + node: any, + callee: any, + args: readonly { type?: string }[] | undefined, + isConstructorCall: boolean, + ) => { + const caller = scopes.currentParent(); + if (!caller) return; + + const { calleeName, tokenStart, tokenEnd, isMethodCall } = + calleeFromNode(callee); + if (calleeName && tokenStart !== undefined && tokenEnd !== undefined) { + const scope = scopes.currentScope(); + const callKind = isConstructorCall ? "new" : "call"; + const key = `${scope}>>${calleeName}>>${callKind}`; + if (!seenCalls.has(key)) { + seenCalls.add(key); + const lineStart = offsetToLine(lineMap, tokenStart); + const lineStartOffset = lineMap[lineStart - 1]!; + calls.push({ + file_path: relPath, + caller_name: caller, + caller_scope: scope, + callee_name: calleeName, + line_start: lineStart, + column_start: tokenStart - lineStartOffset, + column_end: tokenEnd - lineStartOffset, + args_count: argsCount(args), + is_method_call: isMethodCall ? 1 : 0, + is_constructor_call: isConstructorCall ? 1 : 0, + is_optional_chain: isOptionalChain(node, callee) ? 1 : 0, + }); + } + } + }; + Object.assign(visitor, { CallExpression(node: any) { - const caller = scopes.currentParent(); - if (!caller) return; - const callee = node.callee; - let calleeName: string | null = null; - // `tokenStart` / `tokenEnd` track the identifier token whose - // position we record (per R.6) — for `obj.foo()` that's the - // `foo` property, not the whole `obj.foo` member expression. - let tokenStart: number | undefined; - let tokenEnd: number | undefined; - if (callee?.type === "Identifier") { - calleeName = callee.name; - tokenStart = callee.start; - tokenEnd = callee.end; - } else if ( - callee?.type === "MemberExpression" && - !callee.computed && - callee.property?.name - ) { - // Computed segments (`a[i].b()`) drop the chain — recipes filter - // by dot-joined identifier shape, computed breaks the shape. - const segments: string[] = [callee.property.name]; - let cursor: any = callee.object; - while ( - cursor?.type === "MemberExpression" && - !cursor.computed && - cursor.property?.name - ) { - segments.unshift(cursor.property.name); - cursor = cursor.object; - } - if (cursor?.type === "Identifier") { - segments.unshift(cursor.name); - calleeName = segments.join("."); - } else if (cursor?.type === "ThisExpression") { - segments.unshift("this"); - calleeName = segments.join("."); - } - tokenStart = callee.property.start; - tokenEnd = callee.property.end; - } - if (calleeName && tokenStart !== undefined && tokenEnd !== undefined) { - const scope = scopes.currentScope(); - const key = `${scope}>>${calleeName}`; - if (!seenCalls.has(key)) { - seenCalls.add(key); - const lineStart = offsetToLine(lineMap, tokenStart); - const lineStartOffset = lineMap[lineStart - 1]!; - calls.push({ - file_path: relPath, - caller_name: caller, - caller_scope: scope, - callee_name: calleeName, - line_start: lineStart, - column_start: tokenStart - lineStartOffset, - column_end: tokenEnd - lineStartOffset, - }); - } - } + recordCall(node, node.callee, node.arguments, false); + }, + NewExpression(node: any) { + recordCall(node, node.callee, node.arguments, true); }, }); }, diff --git a/src/parser.test.ts b/src/parser.test.ts index b1c18547..0d77a407 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -591,6 +591,52 @@ describe("extractFileData", () => { expect(scopes).toContain("A.run"); expect(scopes).toContain("B.run"); }); + + it("records call-shape metadata", () => { + const src = [ + "function f() {", + " foo(1, 2);", + " obj.bar();", + " obj?.baz();", + " qux?.();", + " new Date();", + " new Map();", + " spread(...args);", + "}", + ].join("\n"); + const d = extractFileData("/proj/x.ts", src, "x.ts"); + const byCallee = Object.fromEntries( + d.calls.map((c) => [c.callee_name, c]), + ); + + expect(byCallee.foo).toMatchObject({ + args_count: 2, + is_method_call: 0, + is_constructor_call: 0, + is_optional_chain: 0, + }); + expect(byCallee["obj.bar"]).toMatchObject({ + args_count: 0, + is_method_call: 1, + is_constructor_call: 0, + is_optional_chain: 0, + }); + expect(byCallee["obj.baz"]).toMatchObject({ + is_method_call: 1, + is_optional_chain: 1, + }); + expect(byCallee.qux).toMatchObject({ + is_optional_chain: 1, + }); + expect(byCallee.Date).toMatchObject({ + is_constructor_call: 1, + is_method_call: 0, + }); + expect(byCallee.Map).toMatchObject({ is_constructor_call: 1 }); + expect(byCallee.spread).toMatchObject({ args_count: null }); + expect(d.calls.filter((c) => c.callee_name === "Date")).toHaveLength(1); + expect(d.calls.filter((c) => c.callee_name === "Map")).toHaveLength(1); + }); }); describe("component detection heuristic", () => { diff --git a/templates/recipes/find-call-sites.sql b/templates/recipes/find-call-sites.sql index a13daeeb..ac243075 100644 --- a/templates/recipes/find-call-sites.sql +++ b/templates/recipes/find-call-sites.sql @@ -1,4 +1,5 @@ -SELECT file_path, caller_name, caller_scope, line_start, column_start, column_end +SELECT file_path, caller_name, caller_scope, line_start, column_start, column_end, + args_count, is_method_call, is_constructor_call, is_optional_chain FROM calls WHERE callee_name = ? ORDER BY file_path, line_start, column_start; From 8fb98b2a7a86506893f8133d1e8a2d5fd42cee2f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 19 May 2026 23:04:22 +0300 Subject: [PATCH 03/23] feat: add async/return-type columns on symbols (schema 29) Structured function shape on symbols (return_type, is_async, is_generator) with find-async-functions recipe; minimal fixture prefetch export for golden. --- docs/architecture.md | 3 +++ fixtures/golden/minimal/barrel-files.json | 8 +++---- fixtures/golden/minimal/files-hashes.json | 4 ++-- .../golden/minimal/find-async-functions.json | 9 +++++++ fixtures/golden/minimal/find-call-sites.json | 2 +- fixtures/golden/minimal/find-references.json | 2 +- .../minimal/find-symbol-references.json | 2 +- fixtures/golden/minimal/index-summary.json | 2 +- .../golden/minimal/untested-and-dead.json | 8 ++++++- .../golden/minimal/worst-covered-exports.json | 14 +++++------ fixtures/golden/scenarios.json | 5 ++++ fixtures/minimal/src/consumer.ts | 4 ++++ src/db.ts | 22 +++++++++++++---- src/extractors/symbols.ts | 4 ++++ src/extractors/type-stringify.ts | 15 ++++++++++++ src/parser.test.ts | 24 +++++++++++++++++-- templates/recipes/find-async-functions.md | 20 ++++++++++++++++ templates/recipes/find-async-functions.sql | 4 ++++ 18 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 fixtures/golden/minimal/find-async-functions.json create mode 100644 templates/recipes/find-async-functions.md create mode 100644 templates/recipes/find-async-functions.sql diff --git a/docs/architecture.md b/docs/architecture.md index 17ea5285..5608b173 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -228,6 +228,9 @@ All tables use `STRICT` mode. Tables marked with `WITHOUT ROWID` store data dire | body_line_count | INTEGER | `line_end - line_start + 1` for function-shaped symbols; NULL for non-functions | | param_count | INTEGER | Parameter count for function-shaped symbols; NULL otherwise | | nesting_depth | INTEGER | Max conditional/loop/ternary nesting inside the body; NULL for non-functions | +| return_type | TEXT | Stringified return type for function-shaped symbols; NULL when unannotated or N/A | +| is_async | INTEGER | 1 for async function-shaped symbols (`function`, `method`, arrow-assigned `function` kind) | +| is_generator | INTEGER | 1 for generator function-shaped symbols | ### `calls` — Function-scoped call edges, deduped per file (`STRICT`) diff --git a/fixtures/golden/minimal/barrel-files.json b/fixtures/golden/minimal/barrel-files.json index 64d6f363..a47c79f2 100644 --- a/fixtures/golden/minimal/barrel-files.json +++ b/fixtures/golden/minimal/barrel-files.json @@ -15,6 +15,10 @@ "file_path": "src/components/shop/ShopButton.tsx", "exports": 2 }, + { + "file_path": "src/consumer.ts", + "exports": 2 + }, { "file_path": "src/lib/cache.ts", "exports": 2 @@ -35,10 +39,6 @@ "file_path": "src/components/shop/ShopButton.default.ts", "exports": 1 }, - { - "file_path": "src/consumer.ts", - "exports": 1 - }, { "file_path": "src/usePermissions.ts", "exports": 1 diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index 5e7d373d..f4f57de3 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -49,9 +49,9 @@ }, { "path": "src/consumer.ts", - "content_hash": "ab868f04a0f22a8b132bc7a4ae42b403dad9c20185497f8513c06dd26c848091", + "content_hash": "c3a9b5d818c52584925072550641e7c559048b4952bc8d328df45e3c3cc14b79", "language": "ts", - "line_count": 19 + "line_count": 23 }, { "path": "src/lib/cache.ts", diff --git a/fixtures/golden/minimal/find-async-functions.json b/fixtures/golden/minimal/find-async-functions.json new file mode 100644 index 00000000..28cbe8d6 --- /dev/null +++ b/fixtures/golden/minimal/find-async-functions.json @@ -0,0 +1,9 @@ +[ + { + "file_path": "src/consumer.ts", + "name": "prefetch", + "kind": "function", + "return_type": "Promise", + "is_generator": 0 + } +] diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index be727189..01dc42df 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -15,7 +15,7 @@ "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 12, + "line_start": 16, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index 99d2e884..c8dfa4d6 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -21,7 +21,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 16, + "line_start": 20, "column_start": 35, "column_end": 46, "kind": "value", diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index c3b82bb5..f2e511a5 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -23,7 +23,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 12, + "line_start": 16, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/index-summary.json b/fixtures/golden/minimal/index-summary.json index 75fb3e08..999c0044 100644 --- a/fixtures/golden/minimal/index-summary.json +++ b/fixtures/golden/minimal/index-summary.json @@ -1,7 +1,7 @@ [ { "files": 18, - "symbols": 44, + "symbols": 45, "imports": 11, "components": 2, "dependencies": 10 diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index e6f870ac..585cce82 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -12,11 +12,17 @@ "coverage_pct": 0 }, { - "name": "run", + "name": "prefetch", "file_path": "src/consumer.ts", "line_start": 10, "coverage_pct": 0 }, + { + "name": "run", + "file_path": "src/consumer.ts", + "line_start": 14, + "coverage_pct": 0 + }, { "name": "_epochSeconds", "file_path": "src/utils/date.ts", diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index 541eab6d..0fb58bfa 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -36,11 +36,17 @@ "coverage_pct": 0 }, { - "name": "run", + "name": "prefetch", "file_path": "src/consumer.ts", "line_start": 10, "coverage_pct": 0 }, + { + "name": "run", + "file_path": "src/consumer.ts", + "line_start": 14, + "coverage_pct": 0 + }, { "name": "get", "file_path": "src/lib/cache.ts", @@ -112,11 +118,5 @@ "file_path": "src/utils/date.ts", "line_start": 5, "coverage_pct": 100 - }, - { - "name": "nowIso", - "file_path": "src/utils/format.ts", - "line_start": 13, - "coverage_pct": 100 } ] diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index e7a5d58d..437774e9 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -100,6 +100,11 @@ "recipe": "find-call-sites", "params": { "callee": "createClient" } }, + { + "id": "find-async-functions", + "prompt": "Parametrised recipe: list async function-shaped symbols with return_type", + "recipe": "find-async-functions" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/fixtures/minimal/src/consumer.ts b/fixtures/minimal/src/consumer.ts index 693a30d1..f37d2484 100644 --- a/fixtures/minimal/src/consumer.ts +++ b/fixtures/minimal/src/consumer.ts @@ -7,6 +7,10 @@ import { epochMs } from "./utils/format"; // FIXME: handle errors // HACK: short-circuit shouldn't ship to prod +export async function prefetch(): Promise { + get("warm"); +} + export function run() { const config: ClientConfig = { baseUrl: "https://api.example.com" }; createClient(config); diff --git a/src/db.ts b/src/db.ts index d384fe83..d17a0476 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 28; +export const SCHEMA_VERSION = 29; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -62,7 +62,10 @@ export function createTables(db: CodemapDatabase) { scope_local_id INTEGER NOT NULL DEFAULT 0, body_line_count INTEGER, param_count INTEGER, - nesting_depth INTEGER + nesting_depth INTEGER, + return_type TEXT, + is_async INTEGER NOT NULL DEFAULT 0, + is_generator INTEGER NOT NULL DEFAULT 0 ) STRICT; -- One row per indexed file. Pure counters from the AST walk. @@ -465,6 +468,8 @@ export function createIndexes(db: CodemapDatabase) { WHERE kind = 'function'; CREATE INDEX IF NOT EXISTS idx_symbols_visibility ON symbols(visibility, file_path, name, line_start) WHERE visibility IS NOT NULL; + CREATE INDEX IF NOT EXISTS idx_symbols_async ON symbols(file_path, name, return_type) + WHERE is_async = 1; CREATE INDEX IF NOT EXISTS idx_imports_source ON imports(source, file_path); CREATE INDEX IF NOT EXISTS idx_imports_resolved ON imports(resolved_path, file_path); @@ -738,6 +743,12 @@ export interface SymbolRow { param_count?: number | null; /** Max nesting depth (conditionals/loops/ternaries) for function-shaped symbols. NULL otherwise. */ nesting_depth?: number | null; + /** Stringified return type for function-shaped symbols; NULL when unannotated or N/A. */ + return_type?: string | null; + /** 1 for async function-shaped symbols. */ + is_async?: number; + /** 1 for generator function-shaped symbols. */ + is_generator?: number; } // SQLite 3.32+ (2020+) default; bun:sqlite + better-sqlite3 12.x both ship @@ -812,8 +823,8 @@ export function insertSymbols(db: CodemapDatabase, symbols: SymbolRow[]) { batchInsert( db, symbols, - "INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export, members, doc_comment, value, parent_name, visibility, complexity, name_column_start, name_column_end, scope_local_id, body_line_count, param_count, nesting_depth)", - "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + "INSERT INTO symbols (file_path, name, kind, line_start, line_end, signature, is_exported, is_default_export, members, doc_comment, value, parent_name, visibility, complexity, name_column_start, name_column_end, scope_local_id, body_line_count, param_count, nesting_depth, return_type, is_async, is_generator)", + "(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", (s, v) => v.push( s.file_path, @@ -836,6 +847,9 @@ export function insertSymbols(db: CodemapDatabase, symbols: SymbolRow[]) { s.body_line_count ?? null, s.param_count ?? null, s.nesting_depth ?? null, + s.return_type ?? null, + s.is_async ?? 0, + s.is_generator ?? 0, ), ); } diff --git a/src/extractors/symbols.ts b/src/extractors/symbols.ts index e63503ca..c1a8377a 100644 --- a/src/extractors/symbols.ts +++ b/src/extractors/symbols.ts @@ -19,6 +19,7 @@ import { offsetToLine } from "./offsets"; import { pushDestructuredVars, pushParams, pushTypeParams } from "./params"; import { buildFunctionSignature, + functionShapeColumns, extractLiteralValue, stringifyTypeNode, stringifyTypeParams, @@ -101,6 +102,7 @@ function registerSymbolHandlers( scope_local_id: scopes.currentLocalId(), body_line_count: lineEnd - lineStart + 1, param_count: node.params?.length ?? 0, + ...functionShapeColumns(node), }); complexity.pushFor(symbolIndex); @@ -183,6 +185,7 @@ function registerSymbolHandlers( scope_local_id: scopes.currentLocalId(), body_line_count: isArrowOrFn ? lineEnd - lineStart + 1 : null, param_count: isArrowOrFn ? (init.params?.length ?? 0) : null, + ...(isArrowOrFn ? functionShapeColumns(init) : {}), }); if (isArrowOrFn) { @@ -486,6 +489,7 @@ function extractClassMembers( scope_local_id: classScopeLocalId, body_line_count: methodLineEnd - methodLineStart + 1, param_count: fn?.params?.length ?? 0, + ...functionShapeColumns(fn), }); } else if (m.type === "PropertyDefinition") { let prefix = ""; diff --git a/src/extractors/type-stringify.ts b/src/extractors/type-stringify.ts index 2df09f10..e8c0832c 100644 --- a/src/extractors/type-stringify.ts +++ b/src/extractors/type-stringify.ts @@ -150,6 +150,21 @@ export function buildFunctionSignature(name: string, node: any): string { return sig; } +/** Structured function-shape columns for Tier 4 (`symbols.return_type`, etc.). */ +export function functionShapeColumns(node: any): { + return_type: string | null; + is_async: number; + is_generator: number; +} { + const returnType = node?.returnType?.typeAnnotation; + const rt = returnType ? stringifyTypeNode(returnType) : null; + return { + return_type: rt, + is_async: node?.async ? 1 : 0, + is_generator: node?.generator ? 1 : 0, + }; +} + /** * Literal initialiser → string for `symbols.value`. Unwraps * `TSAsExpression` / `TSSatisfiesExpression`; handles unary `-N` and diff --git a/src/parser.test.ts b/src/parser.test.ts index 0d77a407..a9558f95 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -66,8 +66,28 @@ describe("extractFileData", () => { it("includes return type on arrow functions", () => { const src = `export const add = (a: number, b: number): number => a + b;\n`; const d = extractFileData("/proj/x.ts", src, "x.ts"); - const sig = d.symbols.find((s) => s.name === "add")?.signature; - expect(sig).toBe("add(a, b): number"); + const sym = d.symbols.find((s) => s.name === "add"); + expect(sym?.signature).toBe("add(a, b): number"); + expect(sym?.return_type).toBe("number"); + expect(sym?.is_async).toBe(0); + }); + + it("records async and generator shape columns", () => { + const src = [ + "async function load(): Promise {}", + "function* gen(): Generator {}", + ].join("\n"); + const d = extractFileData("/proj/x.ts", src, "x.ts"); + expect(d.symbols.find((s) => s.name === "load")).toMatchObject({ + return_type: "Promise", + is_async: 1, + is_generator: 0, + }); + expect(d.symbols.find((s) => s.name === "gen")).toMatchObject({ + return_type: "Generator", + is_async: 0, + is_generator: 1, + }); }); it("includes generics on arrow functions", () => { diff --git a/templates/recipes/find-async-functions.md b/templates/recipes/find-async-functions.md new file mode 100644 index 00000000..d17c5f36 --- /dev/null +++ b/templates/recipes/find-async-functions.md @@ -0,0 +1,20 @@ +--- +params: [] +actions: + - type: navigate-to-definition + description: "Each row is an async function-shaped symbol with structured return_type." +--- + +# find-async-functions + +List every async function with its stringified return type. Tier 4 substrate — complements signature text search with queryable `is_async` / `return_type` columns. + +```bash +codemap query --recipe find-async-functions +``` + +Filter to Promise-returning async functions: + +```bash +codemap query --json "SELECT * FROM symbols WHERE is_async = 1 AND return_type LIKE 'Promise%'" +``` diff --git a/templates/recipes/find-async-functions.sql b/templates/recipes/find-async-functions.sql new file mode 100644 index 00000000..c6056f97 --- /dev/null +++ b/templates/recipes/find-async-functions.sql @@ -0,0 +1,4 @@ +SELECT file_path, name, kind, return_type, is_generator +FROM symbols +WHERE is_async = 1 +ORDER BY file_path, name; From 5be22609d3644cb55afda07dc701f95903065a7f Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 19 May 2026 23:06:18 +0300 Subject: [PATCH 04/23] feat: add dynamic_imports table and extractor (schema 30) Record import() sites with specifier kind, async-fn context, and literal resolution; ship find-dynamic-imports recipe + minimal fixture golden. --- docs/architecture.md | 14 ++++ fixtures/golden/minimal/files-hashes.json | 4 +- fixtures/golden/minimal/find-call-sites.json | 2 +- .../golden/minimal/find-dynamic-imports.json | 11 +++ fixtures/golden/minimal/find-references.json | 2 +- .../minimal/find-symbol-references.json | 2 +- .../golden/minimal/untested-and-dead.json | 2 +- .../golden/minimal/worst-covered-exports.json | 2 +- fixtures/golden/scenarios.json | 5 ++ fixtures/minimal/src/consumer.ts | 1 + src/adapters/builtin.ts | 1 + src/adapters/types.ts | 1 + src/application/index-engine.ts | 24 +++++- src/application/run-index.ts | 1 + src/application/types.ts | 1 + src/db.ts | 53 +++++++++++- src/extractors/dynamic-imports.ts | 83 +++++++++++++++++++ src/extractors/types.ts | 2 + src/parsed-types.ts | 2 + src/parser.test.ts | 35 ++++++++ src/parser.ts | 7 ++ src/resolver.ts | 18 ++++ templates/recipes/find-dynamic-imports.md | 20 +++++ templates/recipes/find-dynamic-imports.sql | 3 + 24 files changed, 285 insertions(+), 11 deletions(-) create mode 100644 fixtures/golden/minimal/find-dynamic-imports.json create mode 100644 src/extractors/dynamic-imports.ts create mode 100644 templates/recipes/find-dynamic-imports.md create mode 100644 templates/recipes/find-dynamic-imports.sql diff --git a/docs/architecture.md b/docs/architecture.md index 5608b173..92cf366d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -428,6 +428,20 @@ SCCs of size ≥ 2 from `dependencies`, plus size-1 SCCs with a self-edge. Compu | cycle_id | INTEGER | Per-PR auto-numbered cycle id (shared across cycle members) | | cycle_size | INTEGER | Number of files in the cycle | +### `dynamic_imports` — Dynamic `import()` sites (`STRICT`) + +| Column | Type | Description | +| -------------- | ---------- | --------------------------------------------------------------------- | +| id | INTEGER PK | Auto-increment row id | +| file_path | TEXT FK | Containing file | +| line_start | INTEGER | 1-based line of the module specifier token | +| column_start | INTEGER | 0-based column of the specifier start | +| source_kind | TEXT | `literal` / `template` / `expression` | +| source_text | TEXT | Specifier text (literal value, template source, or expression source) | +| resolved_path | TEXT | Project-relative path when `source_kind = 'literal'` and resolvable | +| in_async_fn | INTEGER | 1 when the import sits inside an async function body | +| scope_local_id | INTEGER | Enclosing scope (joins `scopes.local_id`; `0` = module) | + ### `runtime_markers` — Operational signals (`STRICT`) Every `console.*` call, `debugger` statement, `throw` statement, and `process.env.X` access. Powers `find-leftover-console` + `env-var-audit`. diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index f4f57de3..5bc3cb5a 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -49,9 +49,9 @@ }, { "path": "src/consumer.ts", - "content_hash": "c3a9b5d818c52584925072550641e7c559048b4952bc8d328df45e3c3cc14b79", + "content_hash": "382aa555ecc0857d53dc71235731e5f37b3d4fea210e228e3d3c2154a74ee0e3", "language": "ts", - "line_count": 23 + "line_count": 24 }, { "path": "src/lib/cache.ts", diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index 01dc42df..733b8b55 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -15,7 +15,7 @@ "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 16, + "line_start": 17, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-dynamic-imports.json b/fixtures/golden/minimal/find-dynamic-imports.json new file mode 100644 index 00000000..83313fcf --- /dev/null +++ b/fixtures/golden/minimal/find-dynamic-imports.json @@ -0,0 +1,11 @@ +[ + { + "file_path": "src/consumer.ts", + "line_start": 11, + "column_start": 15, + "source_kind": "literal", + "source_text": "./lib/cache", + "resolved_path": "src/lib/cache.ts", + "in_async_fn": 1 + } +] diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index c8dfa4d6..ed094259 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -21,7 +21,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 20, + "line_start": 21, "column_start": 35, "column_end": 46, "kind": "value", diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index f2e511a5..02c0a205 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -23,7 +23,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 16, + "line_start": 17, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index 585cce82..42e8145d 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -20,7 +20,7 @@ { "name": "run", "file_path": "src/consumer.ts", - "line_start": 14, + "line_start": 15, "coverage_pct": 0 }, { diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index 0fb58bfa..7a0091c6 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -44,7 +44,7 @@ { "name": "run", "file_path": "src/consumer.ts", - "line_start": 14, + "line_start": 15, "coverage_pct": 0 }, { diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index 437774e9..ed6de448 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -105,6 +105,11 @@ "prompt": "Parametrised recipe: list async function-shaped symbols with return_type", "recipe": "find-async-functions" }, + { + "id": "find-dynamic-imports", + "prompt": "Recipe: every dynamic import() site with specifier kind and async-fn context", + "recipe": "find-dynamic-imports" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/fixtures/minimal/src/consumer.ts b/fixtures/minimal/src/consumer.ts index f37d2484..11b36332 100644 --- a/fixtures/minimal/src/consumer.ts +++ b/fixtures/minimal/src/consumer.ts @@ -8,6 +8,7 @@ import { epochMs } from "./utils/format"; // FIXME: handle errors // HACK: short-circuit shouldn't ship to prod export async function prefetch(): Promise { + await import("./lib/cache"); get("warm"); } diff --git a/src/adapters/builtin.ts b/src/adapters/builtin.ts index 7577c093..48e562cd 100644 --- a/src/adapters/builtin.ts +++ b/src/adapters/builtin.ts @@ -33,6 +33,7 @@ function parseTsJs(ctx: ParseContext): ParsedFilePayload { functionParams: data.functionParams, runtimeMarkers: data.runtimeMarkers, testSuites: data.testSuites, + dynamicImports: data.dynamicImports, }; } diff --git a/src/adapters/types.ts b/src/adapters/types.ts index 8e3ce6de..cec142f6 100644 --- a/src/adapters/types.ts +++ b/src/adapters/types.ts @@ -37,6 +37,7 @@ export type ParsedFilePayload = Pick< | "functionParams" | "runtimeMarkers" | "testSuites" + | "dynamicImports" | "cssVariables" | "cssClasses" | "cssKeyframes" diff --git a/src/application/index-engine.ts b/src/application/index-engine.ts index dae794a3..dad516fa 100644 --- a/src/application/index-engine.ts +++ b/src/application/index-engine.ts @@ -35,12 +35,13 @@ import { insertCssKeyframes, insertTypeMembers, insertCalls, + insertDynamicImports, getAllFileHashes, upsertSourceFts, META_FTS5_ENABLED_KEY, SCHEMA_VERSION, } from "../db"; -import type { CodemapDatabase, FileRow } from "../db"; +import type { CodemapDatabase, DynamicImportRow, FileRow } from "../db"; import { countLines } from "../extractors/offsets"; import { filterRowsByChangedFiles } from "../git-changed"; import { globSync } from "../glob-sync"; @@ -48,7 +49,7 @@ import { hashContent } from "../hash"; import { extractMarkers, extractSuppressions } from "../markers"; import type { ParsedFile } from "../parse-worker"; import { extractFileData } from "../parser"; -import { resolveImports } from "../resolver"; +import { resolveImports, resolveModuleSpecifier } from "../resolver"; import { getExcludeDirNames, getFts5Enabled, @@ -115,6 +116,20 @@ export function collectFiles(): string[] { /** Reused between {@link getChangedFiles} and {@link indexFiles} so the incremental path reads + hashes each file once, not twice. */ export type ChangedSourceCache = Map; +function persistDynamicImports( + db: CodemapDatabase, + absPath: string, + rows: DynamicImportRow[] | undefined, +): void { + if (!rows?.length) return; + for (const row of rows) { + if (row.source_kind === "literal" && row.source_text) { + row.resolved_path = resolveModuleSpecifier(absPath, row.source_text); + } + } + insertDynamicImports(db, rows); +} + // Incremental indexing: `last_indexed_commit` must still be an ancestor of HEAD (otherwise // history was rewritten — caller does a full rebuild). Union `git diff` (committed deltas // since that commit) with `git status --porcelain` (staged + unstaged not in the diff alone). @@ -264,10 +279,10 @@ function insertParsedResults( ); } } else { + const absPath = join(root, parsed.relPath); if (parsed.symbols?.length) insertSymbols(db, parsed.symbols); if (parsed.imports?.length) { - const absPath = join(root, parsed.relPath); const deps = resolveImports(absPath, parsed.imports, indexedPaths); insertImports(db, parsed.imports); if (deps.length) insertDependencies(db, deps); @@ -295,6 +310,7 @@ function insertParsedResults( insertTypeMembers(db, parsed.typeMembers); } if (parsed.calls?.length) insertCalls(db, parsed.calls); + persistDynamicImports(db, absPath, parsed.dynamicImports); } if (parsed.suppressions?.length) insertSuppressions(db, parsed.suppressions); @@ -337,6 +353,7 @@ export function fetchTableStats(db: CodemapDatabase): IndexTableStats { (SELECT COUNT(*) FROM test_suites) as test_suites, (SELECT COUNT(*) FROM re_export_chains) as re_export_chains, (SELECT COUNT(*) FROM module_cycles) as module_cycles, + (SELECT COUNT(*) FROM dynamic_imports) as dynamic_imports, (SELECT COUNT(*) FROM file_metrics) as file_metrics`, ) .get()!; @@ -514,6 +531,7 @@ export async function indexFiles( if (data.typeMembers.length) insertTypeMembers(db, data.typeMembers); if (data.calls.length) insertCalls(db, data.calls); + persistDynamicImports(db, absPath, data.dynamicImports); } // Category-agnostic: one regex pass over raw source, no AST needed. const suppressions = extractSuppressions(source, relPath); diff --git a/src/application/run-index.ts b/src/application/run-index.ts index 44e43d75..1f1228ab 100644 --- a/src/application/run-index.ts +++ b/src/application/run-index.ts @@ -66,6 +66,7 @@ function emptyStats(): IndexTableStats { test_suites: 0, re_export_chains: 0, module_cycles: 0, + dynamic_imports: 0, file_metrics: 0, }; } diff --git a/src/application/types.ts b/src/application/types.ts index 13e3d63c..b8fa1ced 100644 --- a/src/application/types.ts +++ b/src/application/types.ts @@ -26,6 +26,7 @@ export interface IndexTableStats extends Record { test_suites: number; re_export_chains: number; module_cycles: number; + dynamic_imports: number; file_metrics: number; } diff --git a/src/db.ts b/src/db.ts index d17a0476..f1773ba8 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 29; +export const SCHEMA_VERSION = 30; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -304,6 +304,18 @@ export function createTables(db: CodemapDatabase) { cycle_size INTEGER NOT NULL ) STRICT; + CREATE TABLE IF NOT EXISTS dynamic_imports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + line_start INTEGER NOT NULL, + column_start INTEGER NOT NULL, + source_kind TEXT NOT NULL CHECK (source_kind IN ('literal','template','expression')), + source_text TEXT, + resolved_path TEXT, + in_async_fn INTEGER NOT NULL DEFAULT 0, + scope_local_id INTEGER NOT NULL DEFAULT 0 + ) STRICT; + -- Per-specifier breakdown of imports.specifiers JSON blob. Recipes that -- want specifier-precise rewrites (rename specifier, dedupe, type-only -- migrate) JOIN this table. The original imports.specifiers JSON stays @@ -517,6 +529,10 @@ export function createIndexes(db: CodemapDatabase) { CREATE INDEX IF NOT EXISTS idx_module_cycles_cid ON module_cycles(cycle_id); CREATE INDEX IF NOT EXISTS idx_module_cycles_size ON module_cycles(cycle_size); + CREATE INDEX IF NOT EXISTS idx_dynamic_imports_file ON dynamic_imports(file_path, line_start); + CREATE INDEX IF NOT EXISTS idx_dynamic_imports_resolved ON dynamic_imports(resolved_path, file_path) + WHERE resolved_path IS NOT NULL; + CREATE INDEX IF NOT EXISTS idx_re_export_chains_to ON re_export_chains(to_file, to_name); CREATE INDEX IF NOT EXISTS idx_re_export_chains_truncated ON re_export_chains(truncated) WHERE truncated = 1; @@ -582,6 +598,7 @@ export function createSchema(db: CodemapDatabase) { export function dropAll(db: CodemapDatabase) { db.run(` DROP TABLE IF EXISTS module_cycles; + DROP TABLE IF EXISTS dynamic_imports; DROP TABLE IF EXISTS re_export_chains; DROP TABLE IF EXISTS function_params; DROP TABLE IF EXISTS runtime_markers; @@ -1149,6 +1166,40 @@ export function insertCalls(db: CodemapDatabase, calls: CallRow[]) { ); } +export interface DynamicImportRow { + file_path: string; + line_start: number; + column_start: number; + source_kind: "literal" | "template" | "expression"; + source_text: string | null; + resolved_path: string | null; + in_async_fn: number; + scope_local_id: number; +} + +export function insertDynamicImports( + db: CodemapDatabase, + rows: DynamicImportRow[], +) { + batchInsert( + db, + rows, + "INSERT INTO dynamic_imports (file_path, line_start, column_start, source_kind, source_text, resolved_path, in_async_fn, scope_local_id)", + "(?,?,?,?,?,?,?,?)", + (r, v) => + v.push( + r.file_path, + r.line_start, + r.column_start, + r.source_kind, + r.source_text, + r.resolved_path, + r.in_async_fn, + r.scope_local_id, + ), + ); +} + /** * Lexical scope row per [R.11]. `parent_local_id` is `null` for the * module scope; `owner_symbol_name` is `null` for module + arrow scopes. diff --git a/src/extractors/dynamic-imports.ts b/src/extractors/dynamic-imports.ts new file mode 100644 index 00000000..3c0fe66c --- /dev/null +++ b/src/extractors/dynamic-imports.ts @@ -0,0 +1,83 @@ +/** + * Dynamic `import()` sites — module specifier kind, text, and async-fn context. + */ + +import { offsetToLine } from "./offsets"; +import type { TierExtractor } from "./types"; + +function classifySource( + source: string, + node: any, +): { + source_kind: "literal" | "template" | "expression"; + source_text: string; +} { + const src = node.source; + if (src?.type === "Literal" && typeof src.value === "string") { + return { source_kind: "literal", source_text: src.value }; + } + if (src?.type === "TemplateLiteral") { + return { + source_kind: "template", + source_text: source.slice(src.start, src.end), + }; + } + if (src) { + return { + source_kind: "expression", + source_text: source.slice(src.start, src.end), + }; + } + return { source_kind: "expression", source_text: "" }; +} + +function isAsyncFnNode(node: any): boolean { + return Boolean(node?.async); +} + +export const dynamicImportsExtractor: TierExtractor = { + tierId: "dynamic-imports", + register(visitor, ctx) { + const { dynamicImports, relPath, lineMap, source, scopes } = ctx; + let asyncDepth = 0; + + const enterAsync = (node: any) => { + if (isAsyncFnNode(node)) asyncDepth++; + }; + const exitAsync = (node: any) => { + if (isAsyncFnNode(node)) asyncDepth--; + }; + + Object.assign(visitor, { + FunctionDeclaration: enterAsync, + "FunctionDeclaration:exit": exitAsync, + FunctionExpression: enterAsync, + "FunctionExpression:exit": exitAsync, + ArrowFunctionExpression: enterAsync, + "ArrowFunctionExpression:exit": exitAsync, + MethodDefinition(node: any) { + enterAsync(node.value); + }, + "MethodDefinition:exit"(node: any) { + exitAsync(node.value); + }, + + ImportExpression(node: any) { + const tokenStart = node.source?.start ?? node.start; + const lineStart = offsetToLine(lineMap, tokenStart); + const lineStartOffset = lineMap[lineStart - 1] ?? 0; + const { source_kind, source_text } = classifySource(source, node); + dynamicImports.push({ + file_path: relPath, + line_start: lineStart, + column_start: tokenStart - lineStartOffset, + source_kind, + source_text, + resolved_path: null, + in_async_fn: asyncDepth > 0 ? 1 : 0, + scope_local_id: scopes.currentLocalId(), + }); + }, + }); + }, +}; diff --git a/src/extractors/types.ts b/src/extractors/types.ts index 5a2c568f..95e4f254 100644 --- a/src/extractors/types.ts +++ b/src/extractors/types.ts @@ -3,6 +3,7 @@ import type { Comment, VisitorObject } from "oxc-parser"; import type { CallRow, ComponentRow, + DynamicImportRow, ExportRow, FunctionParamRow, ImportRow, @@ -125,6 +126,7 @@ export interface ExtractContext { readonly functionParams: FunctionParamRow[]; readonly runtimeMarkers: RuntimeMarkerRow[]; readonly testSuites: TestSuiteRow[]; + readonly dynamicImports: DynamicImportRow[]; readonly scopes: ScopeTracker; readonly complexity: ComplexityTracker; diff --git a/src/parsed-types.ts b/src/parsed-types.ts index fc1a31bf..64236368 100644 --- a/src/parsed-types.ts +++ b/src/parsed-types.ts @@ -18,6 +18,7 @@ import type { FunctionParamRow, RuntimeMarkerRow, TestSuiteRow, + DynamicImportRow, } from "./db"; /** @@ -51,6 +52,7 @@ export interface ParsedFile { functionParams?: FunctionParamRow[]; runtimeMarkers?: RuntimeMarkerRow[]; testSuites?: TestSuiteRow[]; + dynamicImports?: DynamicImportRow[]; /** CSS-only fields (populated when `category === "css"`). */ cssVariables?: CssVariableRow[]; cssClasses?: CssClassRow[]; diff --git a/src/parser.test.ts b/src/parser.test.ts index a9558f95..a0980009 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -540,6 +540,41 @@ describe("extractFileData", () => { }); }); + describe("dynamic import extraction", () => { + it("records literal, template, and expression specifiers with async context", () => { + const src = [ + "async function load() {", + " await import('./foo');", + " import('./bar');", + "}", + "const x = import(`./${n}.js`);", + "const y = import(getPath());", + ].join("\n"); + const d = extractFileData("/proj/x.ts", src, "x.ts"); + expect(d.dynamicImports).toHaveLength(4); + + const foo = d.dynamicImports.find((r) => r.source_text === "./foo"); + expect(foo).toMatchObject({ + source_kind: "literal", + in_async_fn: 1, + }); + + const bar = d.dynamicImports.find((r) => r.source_text === "./bar"); + expect(bar).toMatchObject({ + source_kind: "literal", + in_async_fn: 1, + }); + + const tmpl = d.dynamicImports.find((r) => r.source_kind === "template"); + expect(tmpl?.source_text).toContain("${n}"); + expect(tmpl?.in_async_fn).toBe(0); + + const expr = d.dynamicImports.find((r) => r.source_kind === "expression"); + expect(expr?.source_text).toBe("getPath()"); + expect(expr?.in_async_fn).toBe(0); + }); + }); + describe("call graph extraction", () => { it("extracts function-to-function calls", () => { const src = `function foo() { bar(); baz(); }\nfunction bar() {}\nfunction baz() {}\n`; diff --git a/src/parser.ts b/src/parser.ts index 56854ad7..73655ff9 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -24,6 +24,7 @@ import type { FunctionParamRow, RuntimeMarkerRow, TestSuiteRow, + DynamicImportRow, } from "./db"; import { callsExtractor } from "./extractors/calls"; import { @@ -34,6 +35,7 @@ import { componentsExtractor, createComponentDetector, } from "./extractors/components"; +import { dynamicImportsExtractor } from "./extractors/dynamic-imports"; import { extractVisibility } from "./extractors/jsdoc"; import { markersExtractor } from "./extractors/markers"; import { buildLineMap, offsetToLine } from "./extractors/offsets"; @@ -62,6 +64,7 @@ interface ExtractedData { functionParams: FunctionParamRow[]; runtimeMarkers: RuntimeMarkerRow[]; testSuites: TestSuiteRow[]; + dynamicImports: DynamicImportRow[]; } /** @@ -105,6 +108,7 @@ const EXTRACTORS: readonly TierExtractor[] = [ callsExtractor, componentsExtractor, referencesExtractor, + dynamicImportsExtractor, runtimeMarkersExtractor, testsExtractor, markersExtractor, @@ -143,6 +147,7 @@ export function extractFileData( const functionParams: FunctionParamRow[] = []; const runtimeMarkers: RuntimeMarkerRow[] = []; const testSuites: TestSuiteRow[] = []; + const dynamicImports: DynamicImportRow[] = []; const exportedNames = new Set(); const defaultExportedNames = new Set(); @@ -194,6 +199,7 @@ export function extractFileData( functionParams, runtimeMarkers, testSuites, + dynamicImports, scopes: createScopeTracker(relPath), complexity: createComplexityTracker(symbols), componentDetector: createComponentDetector(), @@ -228,6 +234,7 @@ export function extractFileData( functionParams, runtimeMarkers, testSuites, + dynamicImports, }; } diff --git a/src/resolver.ts b/src/resolver.ts index 059804d3..f7e4cf7b 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -86,3 +86,21 @@ export function resolveImports( return deps; } + +/** Resolve a string module specifier from `absoluteFilePath`; null when external/unresolvable. */ +export function resolveModuleSpecifier( + absoluteFilePath: string, + source: string, +): string | null { + const root = _projectRoot!; + const resolver = getResolver(); + try { + const result = resolver.resolveFileSync(absoluteFilePath, source); + if (!result.path) return null; + return result.path.startsWith(root) + ? result.path.slice(root.length + 1) + : result.path; + } catch { + return null; + } +} diff --git a/templates/recipes/find-dynamic-imports.md b/templates/recipes/find-dynamic-imports.md new file mode 100644 index 00000000..d3fc150e --- /dev/null +++ b/templates/recipes/find-dynamic-imports.md @@ -0,0 +1,20 @@ +--- +params: [] +actions: + - type: navigate-to-reference + description: "Each row is a dynamic `import()` site with specifier kind and async-fn context." +--- + +# find-dynamic-imports + +List every dynamic `import()` in the indexed project. Tier 6 substrate — literal specifiers get `resolved_path` when oxc-resolver can resolve them. + +```bash +codemap query --recipe find-dynamic-imports +``` + +Filter to lazy imports outside async functions: + +```bash +codemap query --json "SELECT * FROM dynamic_imports WHERE source_kind = 'literal' AND in_async_fn = 0" +``` diff --git a/templates/recipes/find-dynamic-imports.sql b/templates/recipes/find-dynamic-imports.sql new file mode 100644 index 00000000..d8b0498c --- /dev/null +++ b/templates/recipes/find-dynamic-imports.sql @@ -0,0 +1,3 @@ +SELECT file_path, line_start, column_start, source_kind, source_text, resolved_path, in_async_fn +FROM dynamic_imports +ORDER BY file_path, line_start, column_start; From 026889bb2accb8395e467000e49312fff29c87fd Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Tue, 19 May 2026 23:07:50 +0300 Subject: [PATCH 05/23] feat: add files.is_barrel and has_side_effects flags (schema 31) Post-pass barrel detection from exports/symbols; parse-time module side- effect tracking; find-barrel-files and find-side-effect-files recipes. --- docs/architecture.md | 20 ++++++------ fixtures/golden/minimal/files-hashes.json | 4 +-- .../golden/minimal/find-barrel-files.json | 8 +++++ fixtures/golden/minimal/find-call-sites.json | 2 +- .../golden/minimal/find-dynamic-imports.json | 2 +- fixtures/golden/minimal/find-references.json | 2 +- .../minimal/find-side-effect-files.json | 8 +++++ .../minimal/find-symbol-references.json | 2 +- .../golden/minimal/untested-and-dead.json | 4 +-- .../golden/minimal/worst-covered-exports.json | 4 +-- fixtures/golden/scenarios.json | 10 ++++++ fixtures/minimal/src/consumer.ts | 2 ++ src/adapters/builtin.ts | 1 + src/adapters/types.ts | 1 + src/application/file-graph-flags.ts | 32 +++++++++++++++++++ src/application/index-engine.ts | 12 +++++++ src/db.ts | 14 +++++--- src/extractors/module-side-effects.ts | 21 ++++++++++++ src/extractors/types.ts | 3 ++ src/parsed-types.ts | 1 + src/parser.test.ts | 6 ++++ src/parser.ts | 5 +++ templates/recipes/find-barrel-files.md | 14 ++++++++ templates/recipes/find-barrel-files.sql | 4 +++ templates/recipes/find-side-effect-files.md | 14 ++++++++ templates/recipes/find-side-effect-files.sql | 4 +++ 26 files changed, 177 insertions(+), 23 deletions(-) create mode 100644 fixtures/golden/minimal/find-barrel-files.json create mode 100644 fixtures/golden/minimal/find-side-effect-files.json create mode 100644 src/application/file-graph-flags.ts create mode 100644 src/extractors/module-side-effects.ts create mode 100644 templates/recipes/find-barrel-files.md create mode 100644 templates/recipes/find-barrel-files.sql create mode 100644 templates/recipes/find-side-effect-files.md create mode 100644 templates/recipes/find-side-effect-files.sql diff --git a/docs/architecture.md b/docs/architecture.md index 92cf366d..48753b1e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -193,15 +193,17 @@ All tables use `STRICT` mode. Tables marked with `WITHOUT ROWID` store data dire ### `files` — Every indexed file (`STRICT`) -| Column | Type | Description | -| ------------- | ------- | ---------------------------------------------- | -| path | TEXT PK | Relative path from project root | -| content_hash | TEXT | SHA-256 hex — see **Fingerprints** at § Schema | -| size | INTEGER | File size in bytes | -| line_count | INTEGER | Total lines | -| language | TEXT | `ts`, `tsx`, `css`, `md`, etc. | -| last_modified | INTEGER | File mtime (epoch ms) | -| indexed_at | INTEGER | When this row was written | +| Column | Type | Description | +| ---------------- | ------- | -------------------------------------------------------------------- | +| path | TEXT PK | Relative path from project root | +| content_hash | TEXT | SHA-256 hex — see **Fingerprints** at § Schema | +| size | INTEGER | File size in bytes | +| line_count | INTEGER | Total lines | +| language | TEXT | `ts`, `tsx`, `css`, `md`, etc. | +| last_modified | INTEGER | File mtime (epoch ms) | +| indexed_at | INTEGER | When this row was written | +| is_barrel | INTEGER | 1 when every export is a re-export and no local value symbols exist | +| has_side_effects | INTEGER | 1 when module-level calls or assignments were detected at parse time | ### `symbols` — Functions, constants, classes, interfaces, types, enums (`STRICT`) diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index 5bc3cb5a..e11f6711 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -49,9 +49,9 @@ }, { "path": "src/consumer.ts", - "content_hash": "382aa555ecc0857d53dc71235731e5f37b3d4fea210e228e3d3c2154a74ee0e3", + "content_hash": "34693df2ea065083f8492f65d715e56b4473eee8205e90c85a1eff2ce63ba64b", "language": "ts", - "line_count": 24 + "line_count": 26 }, { "path": "src/lib/cache.ts", diff --git a/fixtures/golden/minimal/find-barrel-files.json b/fixtures/golden/minimal/find-barrel-files.json new file mode 100644 index 00000000..4a9aed00 --- /dev/null +++ b/fixtures/golden/minimal/find-barrel-files.json @@ -0,0 +1,8 @@ +[ + { + "path": "src/components/shop/index.ts", + "language": "ts", + "is_barrel": 1, + "has_side_effects": 0 + } +] diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index 733b8b55..54f05a96 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -15,7 +15,7 @@ "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 17, + "line_start": 19, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-dynamic-imports.json b/fixtures/golden/minimal/find-dynamic-imports.json index 83313fcf..faa3c7ee 100644 --- a/fixtures/golden/minimal/find-dynamic-imports.json +++ b/fixtures/golden/minimal/find-dynamic-imports.json @@ -1,7 +1,7 @@ [ { "file_path": "src/consumer.ts", - "line_start": 11, + "line_start": 13, "column_start": 15, "source_kind": "literal", "source_text": "./lib/cache", diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index ed094259..3463e3b8 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -21,7 +21,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 21, + "line_start": 23, "column_start": 35, "column_end": 46, "kind": "value", diff --git a/fixtures/golden/minimal/find-side-effect-files.json b/fixtures/golden/minimal/find-side-effect-files.json new file mode 100644 index 00000000..103d76f8 --- /dev/null +++ b/fixtures/golden/minimal/find-side-effect-files.json @@ -0,0 +1,8 @@ +[ + { + "path": "src/consumer.ts", + "language": "ts", + "is_barrel": 0, + "has_side_effects": 1 + } +] diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index 02c0a205..e4a9fefe 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -23,7 +23,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 17, + "line_start": 19, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index 42e8145d..d725db87 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -14,13 +14,13 @@ { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 10, + "line_start": 12, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 15, + "line_start": 17, "coverage_pct": 0 }, { diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index 7a0091c6..7570b3d4 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -38,13 +38,13 @@ { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 10, + "line_start": 12, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 15, + "line_start": 17, "coverage_pct": 0 }, { diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index ed6de448..b10419e2 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -110,6 +110,16 @@ "prompt": "Recipe: every dynamic import() site with specifier kind and async-fn context", "recipe": "find-dynamic-imports" }, + { + "id": "find-barrel-files", + "prompt": "Recipe: barrel files (re-exports only, no local value symbols)", + "recipe": "find-barrel-files" + }, + { + "id": "find-side-effect-files", + "prompt": "Recipe: files with module-level side effects", + "recipe": "find-side-effect-files" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/fixtures/minimal/src/consumer.ts b/fixtures/minimal/src/consumer.ts index 11b36332..5dc00500 100644 --- a/fixtures/minimal/src/consumer.ts +++ b/fixtures/minimal/src/consumer.ts @@ -5,6 +5,8 @@ import { get } from "./lib/cache"; import { now } from "./utils/date"; import { epochMs } from "./utils/format"; +get("bootstrap"); + // FIXME: handle errors // HACK: short-circuit shouldn't ship to prod export async function prefetch(): Promise { diff --git a/src/adapters/builtin.ts b/src/adapters/builtin.ts index 48e562cd..ca6d434c 100644 --- a/src/adapters/builtin.ts +++ b/src/adapters/builtin.ts @@ -34,6 +34,7 @@ function parseTsJs(ctx: ParseContext): ParsedFilePayload { runtimeMarkers: data.runtimeMarkers, testSuites: data.testSuites, dynamicImports: data.dynamicImports, + hasSideEffects: data.hasSideEffects, }; } diff --git a/src/adapters/types.ts b/src/adapters/types.ts index cec142f6..17900da5 100644 --- a/src/adapters/types.ts +++ b/src/adapters/types.ts @@ -38,6 +38,7 @@ export type ParsedFilePayload = Pick< | "runtimeMarkers" | "testSuites" | "dynamicImports" + | "hasSideEffects" | "cssVariables" | "cssClasses" | "cssKeyframes" diff --git a/src/application/file-graph-flags.ts b/src/application/file-graph-flags.ts new file mode 100644 index 00000000..550ae9a0 --- /dev/null +++ b/src/application/file-graph-flags.ts @@ -0,0 +1,32 @@ +import type { CodemapDatabase } from "../db"; + +const TS_LANGUAGES = "('ts','tsx','mts','cts','js','jsx','mjs','cjs')"; + +const VALUE_SYMBOL_KINDS = + "('function','const','let','var','class','enum','method','getter','setter','property')"; + +/** Recompute `files.is_barrel` from exports + value-symbol rows (Tier 6 post-pass). */ +export function persistFileBarrelFlags(db: CodemapDatabase): void { + db.run(` + UPDATE files + SET is_barrel = 0 + WHERE language IN ${TS_LANGUAGES} + `); + db.run(` + UPDATE files + SET is_barrel = 1 + WHERE language IN ${TS_LANGUAGES} + AND EXISTS ( + SELECT 1 FROM exports e WHERE e.file_path = files.path + ) + AND NOT EXISTS ( + SELECT 1 FROM exports e + WHERE e.file_path = files.path AND e.is_re_export = 0 + ) + AND NOT EXISTS ( + SELECT 1 FROM symbols s + WHERE s.file_path = files.path + AND s.kind IN ${VALUE_SYMBOL_KINDS} + ) + `); +} diff --git a/src/application/index-engine.ts b/src/application/index-engine.ts index dad516fa..25518745 100644 --- a/src/application/index-engine.ts +++ b/src/application/index-engine.ts @@ -64,6 +64,7 @@ import { resolveBindings, } from "./bindings-engine"; import { persistModuleCycles } from "./cycles-engine"; +import { persistFileBarrelFlags } from "./file-graph-flags"; import type { QueryBindValue } from "./query-engine"; import type { IndexPerformanceReport, @@ -244,6 +245,10 @@ function insertParsedResults( for (const parsed of results) { if (parsed.error) continue; + if (parsed.hasSideEffects) { + parsed.fileRow.has_side_effects = parsed.hasSideEffects; + } + insertFile(db, parsed.fileRow); if (parsed.content !== undefined) { @@ -532,6 +537,11 @@ export async function indexFiles( insertTypeMembers(db, data.typeMembers); if (data.calls.length) insertCalls(db, data.calls); persistDynamicImports(db, absPath, data.dynamicImports); + if (data.hasSideEffects) { + db.run("UPDATE files SET has_side_effects = 1 WHERE path = ?", [ + relPath, + ]); + } } // Category-agnostic: one regex pass over raw source, no AST needed. const suppressions = extractSuppressions(source, relPath); @@ -568,6 +578,8 @@ export async function indexFiles( setMeta(db, "file_count", String(fileCount)); setMeta(db, "project_root", getProjectRoot()); + persistFileBarrelFlags(db); + // Pass-2 binding resolution per R.12 — full-rebuild only to honor // R.10's <100ms targeted contract. Orphan-cleared until next full. if (fullRebuild) { diff --git a/src/db.ts b/src/db.ts index f1773ba8..623a3a0d 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 30; +export const SCHEMA_VERSION = 31; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -38,7 +38,9 @@ export function createTables(db: CodemapDatabase) { line_count INTEGER NOT NULL, language TEXT NOT NULL, last_modified INTEGER NOT NULL, - indexed_at INTEGER NOT NULL + indexed_at INTEGER NOT NULL, + is_barrel INTEGER NOT NULL DEFAULT 0, + has_side_effects INTEGER NOT NULL DEFAULT 0 ) STRICT; CREATE TABLE IF NOT EXISTS symbols ( @@ -697,12 +699,14 @@ export interface FileRow { language: string; last_modified: number; indexed_at: number; + is_barrel?: number; + has_side_effects?: number; } export function insertFile(db: CodemapDatabase, file: FileRow) { db.run( - `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at) - VALUES (?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO files (path, content_hash, size, line_count, language, last_modified, indexed_at, is_barrel, has_side_effects) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ file.path, file.content_hash, @@ -711,6 +715,8 @@ export function insertFile(db: CodemapDatabase, file: FileRow) { file.language, file.last_modified, file.indexed_at, + file.is_barrel ?? 0, + file.has_side_effects ?? 0, ], ); } diff --git a/src/extractors/module-side-effects.ts b/src/extractors/module-side-effects.ts new file mode 100644 index 00000000..891ce325 --- /dev/null +++ b/src/extractors/module-side-effects.ts @@ -0,0 +1,21 @@ +/** + * Module-level side effects — top-level calls and assignments per Tier 6. + */ + +import type { TierExtractor } from "./types"; + +export const moduleSideEffectsExtractor: TierExtractor = { + tierId: "module-side-effects", + register(visitor, ctx) { + const { scopes } = ctx; + + Object.assign(visitor, { + CallExpression() { + if (!scopes.currentParent()) ctx.moduleHasSideEffects = true; + }, + AssignmentExpression() { + if (!scopes.currentParent()) ctx.moduleHasSideEffects = true; + }, + }); + }, +}; diff --git a/src/extractors/types.ts b/src/extractors/types.ts index 95e4f254..ddfe296e 100644 --- a/src/extractors/types.ts +++ b/src/extractors/types.ts @@ -128,6 +128,9 @@ export interface ExtractContext { readonly testSuites: TestSuiteRow[]; readonly dynamicImports: DynamicImportRow[]; + /** When true, module-level CallExpression / AssignmentExpression seen. */ + moduleHasSideEffects: boolean; + readonly scopes: ScopeTracker; readonly complexity: ComplexityTracker; // Named `componentDetector` (not `components`) to avoid clashing with diff --git a/src/parsed-types.ts b/src/parsed-types.ts index 64236368..652cb470 100644 --- a/src/parsed-types.ts +++ b/src/parsed-types.ts @@ -53,6 +53,7 @@ export interface ParsedFile { runtimeMarkers?: RuntimeMarkerRow[]; testSuites?: TestSuiteRow[]; dynamicImports?: DynamicImportRow[]; + hasSideEffects?: number; /** CSS-only fields (populated when `category === "css"`). */ cssVariables?: CssVariableRow[]; cssClasses?: CssClassRow[]; diff --git a/src/parser.test.ts b/src/parser.test.ts index a0980009..f08a1717 100644 --- a/src/parser.test.ts +++ b/src/parser.test.ts @@ -573,6 +573,12 @@ describe("extractFileData", () => { expect(expr?.source_text).toBe("getPath()"); expect(expr?.in_async_fn).toBe(0); }); + + it("flags module-level side effects", () => { + const src = `import { x } from "./m";\nx();\nfunction f() { y(); }\n`; + const d = extractFileData("/proj/x.ts", src, "x.ts"); + expect(d.hasSideEffects).toBe(1); + }); }); describe("call graph extraction", () => { diff --git a/src/parser.ts b/src/parser.ts index 73655ff9..fb3d36a8 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -38,6 +38,7 @@ import { import { dynamicImportsExtractor } from "./extractors/dynamic-imports"; import { extractVisibility } from "./extractors/jsdoc"; import { markersExtractor } from "./extractors/markers"; +import { moduleSideEffectsExtractor } from "./extractors/module-side-effects"; import { buildLineMap, offsetToLine } from "./extractors/offsets"; import { referencesExtractor } from "./extractors/references"; import { runtimeMarkersExtractor } from "./extractors/runtime-markers"; @@ -65,6 +66,7 @@ interface ExtractedData { runtimeMarkers: RuntimeMarkerRow[]; testSuites: TestSuiteRow[]; dynamicImports: DynamicImportRow[]; + hasSideEffects: number; } /** @@ -109,6 +111,7 @@ const EXTRACTORS: readonly TierExtractor[] = [ componentsExtractor, referencesExtractor, dynamicImportsExtractor, + moduleSideEffectsExtractor, runtimeMarkersExtractor, testsExtractor, markersExtractor, @@ -204,6 +207,7 @@ export function extractFileData( complexity: createComplexityTracker(symbols), componentDetector: createComponentDetector(), claimedScopeNodes: new WeakSet(), + moduleHasSideEffects: false, }; const multiplexedVisitor = new Visitor( @@ -235,6 +239,7 @@ export function extractFileData( runtimeMarkers, testSuites, dynamicImports, + hasSideEffects: ctx.moduleHasSideEffects ? 1 : 0, }; } diff --git a/templates/recipes/find-barrel-files.md b/templates/recipes/find-barrel-files.md new file mode 100644 index 00000000..2b21de60 --- /dev/null +++ b/templates/recipes/find-barrel-files.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-definition + description: "Each row is a barrel file (re-exports only, no local value symbols)." +--- + +# find-barrel-files + +List files flagged `is_barrel = 1` — every export is a re-export and the file defines no local value symbols. + +```bash +codemap query --recipe find-barrel-files +``` diff --git a/templates/recipes/find-barrel-files.sql b/templates/recipes/find-barrel-files.sql new file mode 100644 index 00000000..c5d15bad --- /dev/null +++ b/templates/recipes/find-barrel-files.sql @@ -0,0 +1,4 @@ +SELECT path, language, is_barrel, has_side_effects +FROM files +WHERE is_barrel = 1 +ORDER BY path; diff --git a/templates/recipes/find-side-effect-files.md b/templates/recipes/find-side-effect-files.md new file mode 100644 index 00000000..bf292844 --- /dev/null +++ b/templates/recipes/find-side-effect-files.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-definition + description: "Each row is a file with module-level calls or assignments." +--- + +# find-side-effect-files + +List files with `has_side_effects = 1` — module-level `CallExpression` or `AssignmentExpression` detected at parse time. + +```bash +codemap query --recipe find-side-effect-files +``` diff --git a/templates/recipes/find-side-effect-files.sql b/templates/recipes/find-side-effect-files.sql new file mode 100644 index 00000000..75c9b3aa --- /dev/null +++ b/templates/recipes/find-side-effect-files.sql @@ -0,0 +1,4 @@ +SELECT path, language, is_barrel, has_side_effects +FROM files +WHERE has_side_effects = 1 +ORDER BY path; From c086bdbb7f3522e1c9793e8add56282985f2cb7b Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:21:14 +0300 Subject: [PATCH 06/23] feat: add re-exported resolution_kind to bindings (schema 32) Mark bindings that reach their symbol through a re-export chain; ship find-re-exported-bindings recipe + golden fixture. --- .../minimal/find-re-exported-bindings.json | 30 +++++++++++++++++++ fixtures/golden/scenarios.json | 5 ++++ src/application/bindings-engine.ts | 4 ++- src/db.ts | 11 +++++-- .../recipes/find-re-exported-bindings.md | 14 +++++++++ .../recipes/find-re-exported-bindings.sql | 5 ++++ 6 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 fixtures/golden/minimal/find-re-exported-bindings.json create mode 100644 templates/recipes/find-re-exported-bindings.md create mode 100644 templates/recipes/find-re-exported-bindings.sql diff --git a/fixtures/golden/minimal/find-re-exported-bindings.json b/fixtures/golden/minimal/find-re-exported-bindings.json new file mode 100644 index 00000000..589d538c --- /dev/null +++ b/fixtures/golden/minimal/find-re-exported-bindings.json @@ -0,0 +1,30 @@ +[ + { + "file_path": "src/consumer.ts", + "name": "ProductCard", + "line_start": 2, + "column_start": 9, + "resolution_kind": "re-exported" + }, + { + "file_path": "src/consumer.ts", + "name": "ShopButton", + "line_start": 2, + "column_start": 22, + "resolution_kind": "re-exported" + }, + { + "file_path": "src/consumer.ts", + "name": "ShopButton", + "line_start": 23, + "column_start": 23, + "resolution_kind": "re-exported" + }, + { + "file_path": "src/consumer.ts", + "name": "ProductCard", + "line_start": 23, + "column_start": 35, + "resolution_kind": "re-exported" + } +] diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index b10419e2..438a6c05 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -120,6 +120,11 @@ "prompt": "Recipe: files with module-level side effects", "recipe": "find-side-effect-files" }, + { + "id": "find-re-exported-bindings", + "prompt": "Recipe: bindings resolved via re-export chains", + "recipe": "find-re-exported-bindings" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/src/application/bindings-engine.ts b/src/application/bindings-engine.ts index e6419530..635cd497 100644 --- a/src/application/bindings-engine.ts +++ b/src/application/bindings-engine.ts @@ -668,13 +668,15 @@ function resolveOne( reExportsByFile, indexedPaths, ) ?? { file: targetFile, name: exportName }; + const viaReExport = + resolved.file !== targetFile || resolved.name !== exportName; const targetSymbols = symbolsByFile.get(resolved.file); const symList = targetSymbols?.get(resolved.name); const targetSym = symList?.find((s) => s.scope_local_id === 0); return { reference_id: ref.id, resolved_symbol_id: targetSym?.id ?? null, - resolution_kind: "imported", + resolution_kind: viaReExport ? "re-exported" : "imported", is_external: 0, }; } diff --git a/src/db.ts b/src/db.ts index 623a3a0d..9e24c1be 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 31; +export const SCHEMA_VERSION = 32; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -222,7 +222,7 @@ export function createTables(db: CodemapDatabase) { reference_id INTEGER PRIMARY KEY REFERENCES "references"(id) ON DELETE CASCADE, resolved_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL, resolution_kind TEXT NOT NULL CHECK (resolution_kind IN ( - 'same-file','imported','global','unresolved' + 'same-file','imported','re-exported','global','unresolved' )), is_external INTEGER NOT NULL DEFAULT 0 ) STRICT, WITHOUT ROWID; @@ -1267,7 +1267,12 @@ export function insertReferences(db: CodemapDatabase, rows: ReferenceRow[]) { export interface BindingRow { reference_id: number; resolved_symbol_id: number | null; - resolution_kind: "same-file" | "imported" | "global" | "unresolved"; + resolution_kind: + | "same-file" + | "imported" + | "re-exported" + | "global" + | "unresolved"; is_external: number; } diff --git a/templates/recipes/find-re-exported-bindings.md b/templates/recipes/find-re-exported-bindings.md new file mode 100644 index 00000000..49bfb933 --- /dev/null +++ b/templates/recipes/find-re-exported-bindings.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-reference + description: "Each row is a binding resolved through a re-export chain (Tier 2.1 / Tier 6)." +--- + +# find-re-exported-bindings + +List identifier references whose binding resolved via a re-export chain (`resolution_kind = 're-exported'`). + +```bash +codemap query --recipe find-re-exported-bindings +``` diff --git a/templates/recipes/find-re-exported-bindings.sql b/templates/recipes/find-re-exported-bindings.sql new file mode 100644 index 00000000..007084b0 --- /dev/null +++ b/templates/recipes/find-re-exported-bindings.sql @@ -0,0 +1,5 @@ +SELECT r.file_path, r.name, r.line_start, r.column_start, b.resolution_kind +FROM bindings b +JOIN "references" r ON r.id = b.reference_id +WHERE b.resolution_kind = 're-exported' +ORDER BY r.file_path, r.line_start, r.column_start; From 3b3349cf1bc9712b3203fec32756dbd660c45625 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:22:30 +0300 Subject: [PATCH 07/23] feat: side-effect import_specifiers rows + import_id FK (schema 33) Emit kind='side-effect' for bare imports; link specifiers to parent imports.id at insert time. Recipe + golden on fixtures/minimal polyfill. --- docs/architecture.md | 5 +- .../minimal/dependencies-from-consumer.json | 4 ++ fixtures/golden/minimal/files-count.json | 2 +- fixtures/golden/minimal/files-hashes.json | 10 ++- fixtures/golden/minimal/find-call-sites.json | 2 +- .../golden/minimal/find-dynamic-imports.json | 2 +- .../minimal/find-re-exported-bindings.json | 4 +- fixtures/golden/minimal/find-references.json | 2 +- .../minimal/find-side-effect-files.json | 6 ++ .../minimal/find-side-effect-imports.json | 10 +++ .../minimal/find-symbol-references.json | 2 +- fixtures/golden/minimal/index-summary.json | 6 +- .../golden/minimal/untested-and-dead.json | 4 +- .../golden/minimal/worst-covered-exports.json | 4 +- fixtures/golden/scenarios.json | 5 ++ fixtures/minimal/src/consumer.ts | 1 + fixtures/minimal/src/polyfill.ts | 2 + src/application/index-engine.ts | 25 +++++--- src/db.ts | 61 +++++++++++++++++-- src/parser.ts | 29 ++++++++- templates/recipes/find-side-effect-imports.md | 9 +++ .../recipes/find-side-effect-imports.sql | 4 ++ 22 files changed, 162 insertions(+), 37 deletions(-) create mode 100644 fixtures/golden/minimal/find-side-effect-imports.json create mode 100644 fixtures/minimal/src/polyfill.ts create mode 100644 templates/recipes/find-side-effect-imports.md create mode 100644 templates/recipes/find-side-effect-imports.sql diff --git a/docs/architecture.md b/docs/architecture.md index 48753b1e..d6986608 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -323,8 +323,9 @@ Edges are deduped per (caller_scope, callee, call vs constructor) per file: if ` | column_end | INTEGER | One-past-last column | | imported_name | TEXT | Original exported name (or `default` / `*`) | | local_name | TEXT | Local binding name (different from `imported_name` for `import { foo as bar }`) | -| kind | TEXT | `named` / `default` / `namespace` | +| kind | TEXT | `named` / `default` / `namespace` / `side-effect` | | is_type_only | INTEGER | 1 if this specifier is `type`-only | +| import_id | INTEGER FK | Parent `imports.id`; populated for all specifier rows including side-effect | ### `scopes` — Lexical scope graph (`STRICT, WITHOUT ROWID`) @@ -364,7 +365,7 @@ Per [R.12]. One row per non-`member`-kind `references` row. Resolved in a single | ------------------ | ------- | ------------------------------------------------------------------------------- | | reference_id | INTEGER | PK + FK → `references(id)` CASCADE | | resolved_symbol_id | INTEGER | FK → `symbols(id)` SET NULL. NULL for `is_external=1` / `global` / `unresolved` | -| resolution_kind | TEXT | `same-file` / `imported` / `global` / `unresolved` | +| resolution_kind | TEXT | `same-file` / `imported` / `re-exported` / `global` / `unresolved` | | is_external | INTEGER | 1 when the import target isn't in the indexed set (e.g. `react`, `lodash`) | ### `function_params` — Typed parameters per function/method (`STRICT`) diff --git a/fixtures/golden/minimal/dependencies-from-consumer.json b/fixtures/golden/minimal/dependencies-from-consumer.json index dba1d723..3157a61f 100644 --- a/fixtures/golden/minimal/dependencies-from-consumer.json +++ b/fixtures/golden/minimal/dependencies-from-consumer.json @@ -11,6 +11,10 @@ "from_path": "src/consumer.ts", "to_path": "src/lib/cache.ts" }, + { + "from_path": "src/consumer.ts", + "to_path": "src/polyfill.ts" + }, { "from_path": "src/consumer.ts", "to_path": "src/utils/date.ts" diff --git a/fixtures/golden/minimal/files-count.json b/fixtures/golden/minimal/files-count.json index f5f07db5..7dd51363 100644 --- a/fixtures/golden/minimal/files-count.json +++ b/fixtures/golden/minimal/files-count.json @@ -1,5 +1,5 @@ [ { - "n": 18 + "n": 19 } ] diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index e11f6711..ef2cdbeb 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -49,9 +49,9 @@ }, { "path": "src/consumer.ts", - "content_hash": "34693df2ea065083f8492f65d715e56b4473eee8205e90c85a1eff2ce63ba64b", + "content_hash": "8fa6942382a4a9980bd7f0835cfcdc820db68559660097f559d839efe60a8446", "language": "ts", - "line_count": 26 + "line_count": 27 }, { "path": "src/lib/cache.ts", @@ -71,6 +71,12 @@ "language": "md", "line_count": 11 }, + { + "path": "src/polyfill.ts", + "content_hash": "f9c2f0d0224aef67b737ce225be1c3797d50fa0ab05fcd8c2fd129dc5bad6914", + "language": "ts", + "line_count": 3 + }, { "path": "src/styles/button.module.css", "content_hash": "1c385277a506b2cb1c70c2d2128bbd280bf080796c83ab37f6f3434655c17348", diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index 54f05a96..ae5a9394 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -15,7 +15,7 @@ "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 19, + "line_start": 20, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-dynamic-imports.json b/fixtures/golden/minimal/find-dynamic-imports.json index faa3c7ee..100285d2 100644 --- a/fixtures/golden/minimal/find-dynamic-imports.json +++ b/fixtures/golden/minimal/find-dynamic-imports.json @@ -1,7 +1,7 @@ [ { "file_path": "src/consumer.ts", - "line_start": 13, + "line_start": 14, "column_start": 15, "source_kind": "literal", "source_text": "./lib/cache", diff --git a/fixtures/golden/minimal/find-re-exported-bindings.json b/fixtures/golden/minimal/find-re-exported-bindings.json index 589d538c..e486f2dd 100644 --- a/fixtures/golden/minimal/find-re-exported-bindings.json +++ b/fixtures/golden/minimal/find-re-exported-bindings.json @@ -16,14 +16,14 @@ { "file_path": "src/consumer.ts", "name": "ShopButton", - "line_start": 23, + "line_start": 24, "column_start": 23, "resolution_kind": "re-exported" }, { "file_path": "src/consumer.ts", "name": "ProductCard", - "line_start": 23, + "line_start": 24, "column_start": 35, "resolution_kind": "re-exported" } diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index 3463e3b8..de736351 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -21,7 +21,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 23, + "line_start": 24, "column_start": 35, "column_end": 46, "kind": "value", diff --git a/fixtures/golden/minimal/find-side-effect-files.json b/fixtures/golden/minimal/find-side-effect-files.json index 103d76f8..880bd3a3 100644 --- a/fixtures/golden/minimal/find-side-effect-files.json +++ b/fixtures/golden/minimal/find-side-effect-files.json @@ -4,5 +4,11 @@ "language": "ts", "is_barrel": 0, "has_side_effects": 1 + }, + { + "path": "src/polyfill.ts", + "language": "ts", + "is_barrel": 0, + "has_side_effects": 1 } ] diff --git a/fixtures/golden/minimal/find-side-effect-imports.json b/fixtures/golden/minimal/find-side-effect-imports.json new file mode 100644 index 00000000..b7e03e28 --- /dev/null +++ b/fixtures/golden/minimal/find-side-effect-imports.json @@ -0,0 +1,10 @@ +[ + { + "file_path": "src/consumer.ts", + "source": "./polyfill", + "line": 4, + "column_start": 7, + "column_end": 19, + "import_id": 6 + } +] diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index e4a9fefe..c3e30f40 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -23,7 +23,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 19, + "line_start": 20, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/index-summary.json b/fixtures/golden/minimal/index-summary.json index 999c0044..b2b68646 100644 --- a/fixtures/golden/minimal/index-summary.json +++ b/fixtures/golden/minimal/index-summary.json @@ -1,9 +1,9 @@ [ { - "files": 18, + "files": 19, "symbols": 45, - "imports": 11, + "imports": 12, "components": 2, - "dependencies": 10 + "dependencies": 11 } ] diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index d725db87..0833c303 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -14,13 +14,13 @@ { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 12, + "line_start": 13, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 17, + "line_start": 18, "coverage_pct": 0 }, { diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index 7570b3d4..b3c1e698 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -38,13 +38,13 @@ { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 12, + "line_start": 13, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 17, + "line_start": 18, "coverage_pct": 0 }, { diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index 438a6c05..59fd62f2 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -125,6 +125,11 @@ "prompt": "Recipe: bindings resolved via re-export chains", "recipe": "find-re-exported-bindings" }, + { + "id": "find-side-effect-imports", + "prompt": "Recipe: side-effect-only import statements", + "recipe": "find-side-effect-imports" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/fixtures/minimal/src/consumer.ts b/fixtures/minimal/src/consumer.ts index 5dc00500..5de77801 100644 --- a/fixtures/minimal/src/consumer.ts +++ b/fixtures/minimal/src/consumer.ts @@ -1,6 +1,7 @@ import { createClient, type ClientConfig } from "~/api/client"; import { ProductCard, ShopButton } from "~/components/shop"; +import "./polyfill"; import { get } from "./lib/cache"; import { now } from "./utils/date"; import { epochMs } from "./utils/format"; diff --git a/fixtures/minimal/src/polyfill.ts b/fixtures/minimal/src/polyfill.ts new file mode 100644 index 00000000..d853d480 --- /dev/null +++ b/fixtures/minimal/src/polyfill.ts @@ -0,0 +1,2 @@ +// Side-effect module for import_specifiers.kind = 'side-effect' golden coverage. +(globalThis as { __codemap_polyfill?: boolean }).__codemap_polyfill = true; diff --git a/src/application/index-engine.ts b/src/application/index-engine.ts index 25518745..cf5f1c5c 100644 --- a/src/application/index-engine.ts +++ b/src/application/index-engine.ts @@ -18,7 +18,7 @@ import { insertFile, insertSymbols, insertImports, - insertImportSpecifiers, + insertImportsWithSpecifiers, insertScopes, insertReferences, insertFileMetrics, @@ -287,14 +287,17 @@ function insertParsedResults( const absPath = join(root, parsed.relPath); if (parsed.symbols?.length) insertSymbols(db, parsed.symbols); - if (parsed.imports?.length) { - const deps = resolveImports(absPath, parsed.imports, indexedPaths); - insertImports(db, parsed.imports); + if (parsed.imports?.length || parsed.importSpecifiers?.length) { + const deps = parsed.imports?.length + ? resolveImports(absPath, parsed.imports, indexedPaths) + : []; + insertImportsWithSpecifiers( + db, + parsed.imports ?? [], + parsed.importSpecifiers ?? [], + ); if (deps.length) insertDependencies(db, deps); } - if (parsed.importSpecifiers?.length) { - insertImportSpecifiers(db, parsed.importSpecifiers); - } if (parsed.scopes?.length) insertScopes(db, parsed.scopes); if (parsed.references?.length) insertReferences(db, parsed.references); @@ -518,9 +521,11 @@ export async function indexFiles( const data = extractFileData(absPath, source, relPath); if (data.symbols.length) insertSymbols(db, data.symbols); const deps = resolveImports(absPath, data.imports, indexedPaths); - if (data.imports.length) insertImports(db, data.imports); - if (data.importSpecifiers.length) - insertImportSpecifiers(db, data.importSpecifiers); + insertImportsWithSpecifiers( + db, + data.imports, + data.importSpecifiers, + ); if (data.scopes.length) insertScopes(db, data.scopes); if (data.references.length) insertReferences(db, data.references); if (data.fileMetrics) insertFileMetrics(db, [data.fileMetrics]); diff --git a/src/db.ts b/src/db.ts index 9e24c1be..83e22f44 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 32; +export const SCHEMA_VERSION = 33; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -325,13 +325,14 @@ export function createTables(db: CodemapDatabase) { CREATE TABLE IF NOT EXISTS import_specifiers ( id INTEGER PRIMARY KEY AUTOINCREMENT, file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + import_id INTEGER REFERENCES imports(id) ON DELETE CASCADE, source TEXT NOT NULL, line INTEGER NOT NULL, column_start INTEGER NOT NULL, column_end INTEGER NOT NULL, imported_name TEXT NOT NULL, local_name TEXT NOT NULL, - kind TEXT NOT NULL CHECK (kind IN ('named','default','namespace')), + kind TEXT NOT NULL CHECK (kind IN ('named','default','namespace','side-effect')), is_type_only INTEGER NOT NULL DEFAULT 0 ) STRICT; @@ -555,6 +556,8 @@ export function createIndexes(db: CodemapDatabase) { CREATE INDEX IF NOT EXISTS idx_import_specifiers_local ON import_specifiers(local_name, file_path); CREATE INDEX IF NOT EXISTS idx_import_specifiers_file ON import_specifiers(file_path, line); CREATE INDEX IF NOT EXISTS idx_import_specifiers_source ON import_specifiers(source, file_path); + CREATE INDEX IF NOT EXISTS idx_import_specifiers_import ON import_specifiers(import_id) WHERE import_id IS NOT NULL; + CREATE INDEX IF NOT EXISTS idx_import_specifiers_kind ON import_specifiers(kind, file_path); CREATE INDEX IF NOT EXISTS idx_calls_caller ON calls(caller_name, file_path); CREATE INDEX IF NOT EXISTS idx_calls_scope ON calls(caller_scope, file_path, callee_name); @@ -1566,22 +1569,68 @@ export interface ImportSpecifierRow { imported_name: string; /** Name as bound locally (`bar` in `import { foo as bar }`); equals `imported_name` when no alias. For default + namespace imports, this is the binding name. */ local_name: string; - kind: "named" | "default" | "namespace"; + kind: "named" | "default" | "namespace" | "side-effect"; is_type_only: number; + /** Index into the parallel `imports[]` array for this file; resolved to `import_id` at insert. */ + import_index: number; +} + +export function insertImportsWithSpecifiers( + db: CodemapDatabase, + imports: ImportRow[], + specifiers: ImportSpecifierRow[], +) { + if (!imports.length) { + if (specifiers.length) { + insertImportSpecifiers( + db, + specifiers.map((row) => ({ ...row, import_id: null })), + ); + } + return; + } + const importIds: number[] = []; + for (const imp of imports) { + db.run( + "INSERT INTO imports (file_path, source, resolved_path, specifiers, is_type_only, line_number) VALUES (?,?,?,?,?,?)", + [ + imp.file_path, + imp.source, + imp.resolved_path, + imp.specifiers, + imp.is_type_only, + imp.line_number, + ], + ); + importIds.push( + db.query<{ id: number }>("SELECT last_insert_rowid() AS id").get()!.id, + ); + } + if (!specifiers.length) return; + const linked = specifiers.map((row) => ({ + ...row, + import_id: importIds[row.import_index] ?? null, + })); + insertImportSpecifiers(db, linked); +} + +export interface ImportSpecifierInsertRow extends ImportSpecifierRow { + import_id: number | null; } export function insertImportSpecifiers( db: CodemapDatabase, - rows: ImportSpecifierRow[], + rows: ImportSpecifierInsertRow[], ) { batchInsert( db, rows, - "INSERT INTO import_specifiers (file_path, source, line, column_start, column_end, imported_name, local_name, kind, is_type_only)", - "(?,?,?,?,?,?,?,?,?)", + "INSERT INTO import_specifiers (file_path, import_id, source, line, column_start, column_end, imported_name, local_name, kind, is_type_only)", + "(?,?,?,?,?,?,?,?,?,?)", (r, v) => v.push( r.file_path, + r.import_id, r.source, r.line, r.column_start, diff --git a/src/parser.ts b/src/parser.ts index fb3d36a8..2391477a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -177,8 +177,11 @@ export function extractFileData( } for (const imp of mod.staticImports) { + const importIndex = imports.length; imports.push(staticImportToRow(relPath, imp, lineMap)); - importSpecifiers.push(...staticImportSpecifierRows(relPath, imp, lineMap)); + importSpecifiers.push( + ...staticImportSpecifierRows(relPath, imp, lineMap, importIndex), + ); } const ctx: ExtractContext = { @@ -348,9 +351,28 @@ function staticImportSpecifierRows( filePath: string, imp: StaticImport, lineMap: number[], + importIndex: number, ): ImportSpecifierRow[] { - // Side-effect imports (`import "mod"`) have zero entries — produce no rows. - if (imp.entries.length === 0) return []; + if (imp.entries.length === 0) { + const line = offsetToLine(lineMap, imp.start); + const lineStartOffset = lineMap[line - 1] ?? 0; + const tokenStart = imp.moduleRequest.start; + const tokenEnd = imp.moduleRequest.end; + return [ + { + file_path: filePath, + source: imp.moduleRequest.value, + line, + column_start: tokenStart - lineStartOffset, + column_end: tokenEnd - lineStartOffset, + imported_name: "", + local_name: "", + kind: "side-effect", + is_type_only: 0, + import_index: importIndex, + }, + ]; + } const rows: ImportSpecifierRow[] = []; for (const entry of imp.entries) { const importKind = entry.importName.kind; @@ -387,6 +409,7 @@ function staticImportSpecifierRows( local_name: localName, kind, is_type_only: entry.isType ? 1 : 0, + import_index: importIndex, }); } return rows; diff --git a/templates/recipes/find-side-effect-imports.md b/templates/recipes/find-side-effect-imports.md new file mode 100644 index 00000000..84d69251 --- /dev/null +++ b/templates/recipes/find-side-effect-imports.md @@ -0,0 +1,9 @@ +# find-side-effect-imports + +Side-effect-only import statements (`import "./mod"` with no bindings). + +```bash +codemap query --recipe find-side-effect-imports +``` + +`import_id` FK links each row to the parent `imports` row. diff --git a/templates/recipes/find-side-effect-imports.sql b/templates/recipes/find-side-effect-imports.sql new file mode 100644 index 00000000..17edff7c --- /dev/null +++ b/templates/recipes/find-side-effect-imports.sql @@ -0,0 +1,4 @@ +SELECT file_path, source, line, column_start, column_end, import_id +FROM import_specifiers +WHERE kind = 'side-effect' +ORDER BY file_path, line, column_start; From 52fc02f3d771a1a3438beb401bdde4f19dabf7e2 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:25:06 +0300 Subject: [PATCH 08/23] feat: JSX + behavioral substrate tables (schema 34) Add jsx_elements/jsx_attributes extractors with parent linking, async_calls/try_catch/decorators/jsdoc_tags behavioral extractors, five flagship recipes + golden fixtures on fixtures/minimal. --- docs/architecture.md | 27 ++ fixtures/golden/minimal/barrel-files.json | 4 + .../golden/minimal/deprecated-symbols.json | 2 +- fixtures/golden/minimal/files-count.json | 2 +- fixtures/golden/minimal/files-hashes.json | 22 +- .../golden/minimal/find-await-in-loop.json | 11 + .../golden/minimal/find-by-param-type.json | 2 +- fixtures/golden/minimal/find-call-sites.json | 4 +- .../golden/minimal/find-decorator-usage.json | 10 + .../golden/minimal/find-dynamic-imports.json | 4 +- fixtures/golden/minimal/find-jsx-usages.json | 12 + .../golden/minimal/find-leftover-console.json | 9 +- .../minimal/find-re-exported-bindings.json | 4 +- fixtures/golden/minimal/find-references.json | 2 +- .../golden/minimal/find-swallowed-errors.json | 10 + .../minimal/find-symbol-references.json | 4 +- .../golden/minimal/find-throws-jsdoc.json | 10 + fixtures/golden/minimal/index-summary.json | 4 +- .../golden/minimal/untested-and-dead.json | 4 +- .../golden/minimal/worst-covered-exports.json | 12 +- fixtures/golden/scenarios.json | 26 ++ fixtures/minimal/src/api/client.ts | 1 + fixtures/minimal/src/api/decorated.ts | 4 + fixtures/minimal/src/consumer.ts | 4 +- fixtures/minimal/src/lib/cache.ts | 8 +- fixtures/minimal/tsconfig.json | 1 + src/adapters/builtin.ts | 6 + src/adapters/types.ts | 6 + src/application/behavioral-persist.ts | 109 ++++++ src/application/index-engine.ts | 35 ++ src/application/jsx-persist.ts | 67 ++++ src/db.ts | 99 +++++- src/extractors/behavioral.ts | 335 ++++++++++++++++++ src/extractors/jsx.ts | 212 +++++++++++ src/extractors/types.ts | 14 + src/parsed-types.ts | 6 + src/parser.ts | 28 ++ templates/recipes/find-await-in-loop.md | 14 + templates/recipes/find-await-in-loop.sql | 4 + templates/recipes/find-decorator-usage.md | 14 + templates/recipes/find-decorator-usage.sql | 3 + templates/recipes/find-jsx-usages.md | 20 ++ templates/recipes/find-jsx-usages.sql | 4 + templates/recipes/find-swallowed-errors.md | 14 + templates/recipes/find-swallowed-errors.sql | 4 + templates/recipes/find-throws-jsdoc.md | 14 + templates/recipes/find-throws-jsdoc.sql | 5 + 47 files changed, 1182 insertions(+), 34 deletions(-) create mode 100644 fixtures/golden/minimal/find-await-in-loop.json create mode 100644 fixtures/golden/minimal/find-decorator-usage.json create mode 100644 fixtures/golden/minimal/find-jsx-usages.json create mode 100644 fixtures/golden/minimal/find-swallowed-errors.json create mode 100644 fixtures/golden/minimal/find-throws-jsdoc.json create mode 100644 fixtures/minimal/src/api/decorated.ts create mode 100644 src/application/behavioral-persist.ts create mode 100644 src/application/jsx-persist.ts create mode 100644 src/extractors/behavioral.ts create mode 100644 src/extractors/jsx.ts create mode 100644 templates/recipes/find-await-in-loop.md create mode 100644 templates/recipes/find-await-in-loop.sql create mode 100644 templates/recipes/find-decorator-usage.md create mode 100644 templates/recipes/find-decorator-usage.sql create mode 100644 templates/recipes/find-jsx-usages.md create mode 100644 templates/recipes/find-jsx-usages.sql create mode 100644 templates/recipes/find-swallowed-errors.md create mode 100644 templates/recipes/find-swallowed-errors.sql create mode 100644 templates/recipes/find-throws-jsdoc.md create mode 100644 templates/recipes/find-throws-jsdoc.sql diff --git a/docs/architecture.md b/docs/architecture.md index d6986608..c2932df4 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -445,6 +445,33 @@ SCCs of size ≥ 2 from `dependencies`, plus size-1 SCCs with a self-edge. Compu | in_async_fn | INTEGER | 1 when the import sits inside an async function body | | scope_local_id | INTEGER | Enclosing scope (joins `scopes.local_id`; `0` = module) | +### `jsx_elements` / `jsx_attributes` — JSX substrate (`STRICT`) + +Every JSX element and attribute in `.tsx`/`.jsx` files. `parent_element_id` is filled in a post-insert pass within the file. Fragments use `is_fragment = 1` and empty `component_name`. + +| Column (elements) | Type | Description | +| ----------------- | ---------- | -------------------------------------- | +| component_name | TEXT | Tag name (`ProductCard`, `article`, …) | +| is_self_closing | INTEGER | 1 for `` | +| is_fragment | INTEGER | 1 for `<>…` | +| is_lowercase | INTEGER | 1 for native HTML tags | +| parent_element_id | INTEGER FK | Parent element row | +| children_count | INTEGER | Direct JSX child element count | + +| Column (attributes) | Type | Description | +| ------------------- | ---- | ---------------------------------------------------------- | +| element_id | FK | Owning `jsx_elements.id` | +| value_kind | TEXT | `string` / `expression` / `boolean` / `spread` / `element` | + +### `async_calls` / `try_catch` / `decorators` / `jsdoc_tags` — Behavioral substrate (`STRICT`) + +| Table | Flagship signal | +| ------------- | ---------------------------------------------------------------------- | +| `async_calls` | `AwaitExpression` sites with `in_loop` / `in_try` context stack | +| `try_catch` | `TryStatement` shape + `catch_logs_only` / `catch_rethrows` heuristics | +| `decorators` | Decorator name + `target_kind`; `target_symbol_id` linked post-insert | +| `jsdoc_tags` | Structured tags (`@param`, `@throws`, …) per symbol from `doc_comment` | + ### `runtime_markers` — Operational signals (`STRICT`) Every `console.*` call, `debugger` statement, `throw` statement, and `process.env.X` access. Powers `find-leftover-console` + `env-var-audit`. diff --git a/fixtures/golden/minimal/barrel-files.json b/fixtures/golden/minimal/barrel-files.json index a47c79f2..61d8f39c 100644 --- a/fixtures/golden/minimal/barrel-files.json +++ b/fixtures/golden/minimal/barrel-files.json @@ -31,6 +31,10 @@ "file_path": "src/utils/format.ts", "exports": 2 }, + { + "file_path": "src/api/decorated.ts", + "exports": 1 + }, { "file_path": "src/components/shop/ProductCard.tsx", "exports": 1 diff --git a/fixtures/golden/minimal/deprecated-symbols.json b/fixtures/golden/minimal/deprecated-symbols.json index 3cb0802c..b854cb12 100644 --- a/fixtures/golden/minimal/deprecated-symbols.json +++ b/fixtures/golden/minimal/deprecated-symbols.json @@ -3,7 +3,7 @@ "name": "legacyClient", "kind": "function", "file_path": "src/api/client.ts", - "line_start": 41, + "line_start": 42, "signature": "legacyClient()", "doc_comment": "@deprecated Use `createClient({ baseUrl })` directly. Kept as a fixture for\n`deprecated-symbols` + `--format sarif` / `--format annotations` recipes." }, diff --git a/fixtures/golden/minimal/files-count.json b/fixtures/golden/minimal/files-count.json index 7dd51363..dd846749 100644 --- a/fixtures/golden/minimal/files-count.json +++ b/fixtures/golden/minimal/files-count.json @@ -1,5 +1,5 @@ [ { - "n": 19 + "n": 20 } ] diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index ef2cdbeb..b3dfaa50 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -19,9 +19,15 @@ }, { "path": "src/api/client.ts", - "content_hash": "625df0a0f197f2233506c028f5a4b2022567587de2ecc5e81863c2cb2654e507", + "content_hash": "2035094c31ce72ef9b723e4b47ebb1d65bc8d9a381ca7f98bd43164adcd2757f", "language": "ts", - "line_count": 44 + "line_count": 45 + }, + { + "path": "src/api/decorated.ts", + "content_hash": "96bd631cc642f270589cba89e4fbfea8555b375aba66be53fed657f4b2de8d9c", + "language": "ts", + "line_count": 5 }, { "path": "src/components/shop/ProductCard.tsx", @@ -49,15 +55,15 @@ }, { "path": "src/consumer.ts", - "content_hash": "8fa6942382a4a9980bd7f0835cfcdc820db68559660097f559d839efe60a8446", + "content_hash": "2698fbba58a1ae47e9cb3a2297b5677a21d9ecfa64a62c8a4ee072dab3ef185f", "language": "ts", - "line_count": 27 + "line_count": 29 }, { "path": "src/lib/cache.ts", - "content_hash": "a185908afc6dca9552783847c0726686378dcf60e9d5714bfdb0aa24c479a4c1", + "content_hash": "964f6d3ddb8a7782b1273a38ed0bbde8cd886eb672c1f20466f0aa88ff49205f", "language": "ts", - "line_count": 21 + "line_count": 27 }, { "path": "src/lib/store.ts", @@ -109,8 +115,8 @@ }, { "path": "tsconfig.json", - "content_hash": "6b8ead4b09e6885483c805937a35fa0df3cbc47de346abb2d0ed8b73dc7f9e92", + "content_hash": "4bbd0839e5e2edc79d5432dff495b709edc6f281cf0b09f35d1155ff38f9def0", "language": "json", - "line_count": 17 + "line_count": 18 } ] diff --git a/fixtures/golden/minimal/find-await-in-loop.json b/fixtures/golden/minimal/find-await-in-loop.json new file mode 100644 index 00000000..7a5e6e74 --- /dev/null +++ b/fixtures/golden/minimal/find-await-in-loop.json @@ -0,0 +1,11 @@ +[ + { + "file_path": "src/consumer.ts", + "caller_scope": "prefetch.$anon_2", + "awaited_expression": "await import(\"./lib/cache\")", + "awaited_callee_name": null, + "line_start": 15, + "in_loop": 1, + "in_try": 0 + } +] diff --git a/fixtures/golden/minimal/find-by-param-type.json b/fixtures/golden/minimal/find-by-param-type.json index b40f707d..aeb3cb8e 100644 --- a/fixtures/golden/minimal/find-by-param-type.json +++ b/fixtures/golden/minimal/find-by-param-type.json @@ -8,7 +8,7 @@ "default_text": null, "is_rest": 0, "is_optional": 1, - "line_start": 15, + "line_start": 16, "column_start": 29 } ] diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index ae5a9394..c5deeaca 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -3,7 +3,7 @@ "file_path": "src/api/client.ts", "caller_name": "legacyClient", "caller_scope": "legacyClient", - "line_start": 42, + "line_start": 43, "column_start": 9, "column_end": 21, "args_count": 0, @@ -15,7 +15,7 @@ "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 20, + "line_start": 22, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-decorator-usage.json b/fixtures/golden/minimal/find-decorator-usage.json new file mode 100644 index 00000000..a5e93e58 --- /dev/null +++ b/fixtures/golden/minimal/find-decorator-usage.json @@ -0,0 +1,10 @@ +[ + { + "file_path": "src/api/decorated.ts", + "target_kind": "class", + "name": "sealed", + "line": 3, + "column_start": 0, + "target_symbol_id": 17 + } +] diff --git a/fixtures/golden/minimal/find-dynamic-imports.json b/fixtures/golden/minimal/find-dynamic-imports.json index 100285d2..53c381ab 100644 --- a/fixtures/golden/minimal/find-dynamic-imports.json +++ b/fixtures/golden/minimal/find-dynamic-imports.json @@ -1,8 +1,8 @@ [ { "file_path": "src/consumer.ts", - "line_start": 14, - "column_start": 15, + "line_start": 15, + "column_start": 17, "source_kind": "literal", "source_text": "./lib/cache", "resolved_path": "src/lib/cache.ts", diff --git a/fixtures/golden/minimal/find-jsx-usages.json b/fixtures/golden/minimal/find-jsx-usages.json new file mode 100644 index 00000000..064d66f8 --- /dev/null +++ b/fixtures/golden/minimal/find-jsx-usages.json @@ -0,0 +1,12 @@ +[ + { + "file_path": "src/components/shop/ProductCard.tsx", + "component_name": "article", + "line_start": 14, + "line_end": 17, + "is_self_closing": 0, + "is_fragment": 0, + "is_lowercase": 1, + "children_count": 1 + } +] diff --git a/fixtures/golden/minimal/find-leftover-console.json b/fixtures/golden/minimal/find-leftover-console.json index fe51488c..a0fabd71 100644 --- a/fixtures/golden/minimal/find-leftover-console.json +++ b/fixtures/golden/minimal/find-leftover-console.json @@ -1 +1,8 @@ -[] +[ + { + "file_path": "src/lib/cache.ts", + "line_start": 23, + "column_start": 6, + "method": "error" + } +] diff --git a/fixtures/golden/minimal/find-re-exported-bindings.json b/fixtures/golden/minimal/find-re-exported-bindings.json index e486f2dd..5d5f62d4 100644 --- a/fixtures/golden/minimal/find-re-exported-bindings.json +++ b/fixtures/golden/minimal/find-re-exported-bindings.json @@ -16,14 +16,14 @@ { "file_path": "src/consumer.ts", "name": "ShopButton", - "line_start": 24, + "line_start": 26, "column_start": 23, "resolution_kind": "re-exported" }, { "file_path": "src/consumer.ts", "name": "ProductCard", - "line_start": 24, + "line_start": 26, "column_start": 35, "resolution_kind": "re-exported" } diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index de736351..1cc2fb56 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -21,7 +21,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 24, + "line_start": 26, "column_start": 35, "column_end": 46, "kind": "value", diff --git a/fixtures/golden/minimal/find-swallowed-errors.json b/fixtures/golden/minimal/find-swallowed-errors.json new file mode 100644 index 00000000..5dcb169e --- /dev/null +++ b/fixtures/golden/minimal/find-swallowed-errors.json @@ -0,0 +1,10 @@ +[ + { + "file_path": "src/lib/cache.ts", + "try_line_start": 20, + "try_line_end": 22, + "catch_param": "err", + "catch_logs_only": 1, + "catch_rethrows": 0 + } +] diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index c3e30f40..45e2d93d 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -1,7 +1,7 @@ [ { "file_path": "src/api/client.ts", - "line_start": 42, + "line_start": 43, "column_start": 9, "column_end": 21, "kind": "value", @@ -23,7 +23,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 20, + "line_start": 22, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/find-throws-jsdoc.json b/fixtures/golden/minimal/find-throws-jsdoc.json new file mode 100644 index 00000000..b7bc25c0 --- /dev/null +++ b/fixtures/golden/minimal/find-throws-jsdoc.json @@ -0,0 +1,10 @@ +[ + { + "file_path": "src/api/client.ts", + "name": "createClient", + "line_start": 16, + "tag": "@throws", + "type_text": "Error", + "description": "when config is invalid" + } +] diff --git a/fixtures/golden/minimal/index-summary.json b/fixtures/golden/minimal/index-summary.json index b2b68646..14fe02e0 100644 --- a/fixtures/golden/minimal/index-summary.json +++ b/fixtures/golden/minimal/index-summary.json @@ -1,7 +1,7 @@ [ { - "files": 19, - "symbols": 45, + "files": 20, + "symbols": 51, "imports": 12, "components": 2, "dependencies": 11 diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index 0833c303..6472bb47 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -2,7 +2,7 @@ { "name": "legacyClient", "file_path": "src/api/client.ts", - "line_start": 41, + "line_start": 42, "coverage_pct": 0 }, { @@ -20,7 +20,7 @@ { "name": "run", "file_path": "src/consumer.ts", - "line_start": 18, + "line_start": 20, "coverage_pct": 0 }, { diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index b3c1e698..9ebc3aa6 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -2,31 +2,31 @@ { "name": "createClient", "file_path": "src/api/client.ts", - "line_start": 15, + "line_start": 16, "coverage_pct": 0 }, { "name": "setupTransport", "file_path": "src/api/client.ts", - "line_start": 22, + "line_start": 23, "coverage_pct": 0 }, { "name": "openSocket", "file_path": "src/api/client.ts", - "line_start": 28, + "line_start": 29, "coverage_pct": 0 }, { "name": "handshake", "file_path": "src/api/client.ts", - "line_start": 32, + "line_start": 33, "coverage_pct": 0 }, { "name": "legacyClient", "file_path": "src/api/client.ts", - "line_start": 41, + "line_start": 42, "coverage_pct": 0 }, { @@ -44,7 +44,7 @@ { "name": "run", "file_path": "src/consumer.ts", - "line_start": 18, + "line_start": 20, "coverage_pct": 0 }, { diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index 59fd62f2..69813090 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -130,6 +130,32 @@ "prompt": "Recipe: side-effect-only import statements", "recipe": "find-side-effect-imports" }, + { + "id": "find-jsx-usages", + "prompt": "Recipe: JSX element usages", + "recipe": "find-jsx-usages", + "params": { "component_name": "article" } + }, + { + "id": "find-await-in-loop", + "prompt": "Recipe: await expressions inside loops", + "recipe": "find-await-in-loop" + }, + { + "id": "find-swallowed-errors", + "prompt": "Recipe: catch blocks that only log", + "recipe": "find-swallowed-errors" + }, + { + "id": "find-decorator-usage", + "prompt": "Recipe: decorator sites", + "recipe": "find-decorator-usage" + }, + { + "id": "find-throws-jsdoc", + "prompt": "Recipe: @throws JSDoc tags", + "recipe": "find-throws-jsdoc" + }, { "id": "find-export-sites", "prompt": "Parametrised recipe: where is ProductCard exported (direct + re-exports)?", diff --git a/fixtures/minimal/src/api/client.ts b/fixtures/minimal/src/api/client.ts index ec52a6c4..f868650a 100644 --- a/fixtures/minimal/src/api/client.ts +++ b/fixtures/minimal/src/api/client.ts @@ -12,6 +12,7 @@ export interface Transport { readonly handshakeMs: number; } +/** @throws {Error} when config is invalid */ export function createClient(config?: ClientConfig) { const transport = setupTransport( config?.baseUrl ?? "https://api.example.com", diff --git a/fixtures/minimal/src/api/decorated.ts b/fixtures/minimal/src/api/decorated.ts new file mode 100644 index 00000000..aab42bc1 --- /dev/null +++ b/fixtures/minimal/src/api/decorated.ts @@ -0,0 +1,4 @@ +function sealed(_target: unknown) {} + +@sealed +export class ApiCache {} diff --git a/fixtures/minimal/src/consumer.ts b/fixtures/minimal/src/consumer.ts index 5de77801..35452d3d 100644 --- a/fixtures/minimal/src/consumer.ts +++ b/fixtures/minimal/src/consumer.ts @@ -11,7 +11,9 @@ get("bootstrap"); // FIXME: handle errors // HACK: short-circuit shouldn't ship to prod export async function prefetch(): Promise { - await import("./lib/cache"); + for (const _key of ["warm"] as const) { + await import("./lib/cache"); + } get("warm"); } diff --git a/fixtures/minimal/src/lib/cache.ts b/fixtures/minimal/src/lib/cache.ts index 28fb41ae..296cdd99 100644 --- a/fixtures/minimal/src/lib/cache.ts +++ b/fixtures/minimal/src/lib/cache.ts @@ -16,5 +16,11 @@ export function get(key: string): string | undefined { export function invalidate(key: string): void { _data.delete(key); // Parse-only: AST records `invalidate → write` edge; guard prevents runtime recursion. - if (key === "__codemap_unreachable__") write(key, ""); + if (key === "__codemap_unreachable__") { + try { + write(key, ""); + } catch (err) { + console.error(err); + } + } } diff --git a/fixtures/minimal/tsconfig.json b/fixtures/minimal/tsconfig.json index c1a39805..36d32b69 100644 --- a/fixtures/minimal/tsconfig.json +++ b/fixtures/minimal/tsconfig.json @@ -4,6 +4,7 @@ "module": "ESNext", "moduleResolution": "bundler", "jsx": "react-jsx", + "experimentalDecorators": true, "strict": true, "skipLibCheck": true, "ignoreDeprecations": "6.0", diff --git a/src/adapters/builtin.ts b/src/adapters/builtin.ts index ca6d434c..e83727f2 100644 --- a/src/adapters/builtin.ts +++ b/src/adapters/builtin.ts @@ -34,6 +34,12 @@ function parseTsJs(ctx: ParseContext): ParsedFilePayload { runtimeMarkers: data.runtimeMarkers, testSuites: data.testSuites, dynamicImports: data.dynamicImports, + jsxElements: data.jsxElements, + jsxAttributes: data.jsxAttributes, + asyncCalls: data.asyncCalls, + tryCatchRows: data.tryCatchRows, + decorators: data.decorators, + jsdocTags: data.jsdocTags, hasSideEffects: data.hasSideEffects, }; } diff --git a/src/adapters/types.ts b/src/adapters/types.ts index 17900da5..db03af07 100644 --- a/src/adapters/types.ts +++ b/src/adapters/types.ts @@ -39,6 +39,12 @@ export type ParsedFilePayload = Pick< | "testSuites" | "dynamicImports" | "hasSideEffects" + | "jsxElements" + | "jsxAttributes" + | "asyncCalls" + | "tryCatchRows" + | "decorators" + | "jsdocTags" | "cssVariables" | "cssClasses" | "cssKeyframes" diff --git a/src/application/behavioral-persist.ts b/src/application/behavioral-persist.ts new file mode 100644 index 00000000..a630619b --- /dev/null +++ b/src/application/behavioral-persist.ts @@ -0,0 +1,109 @@ +import type { CodemapDatabase } from "../db"; +import type { + ParsedAsyncCall, + ParsedDecorator, + ParsedJsdocTag, + ParsedTryCatch, +} from "../extractors/behavioral"; + +export function insertAsyncCalls( + db: CodemapDatabase, + rows: ParsedAsyncCall[], +): void { + for (const r of rows) { + db.run( + `INSERT INTO async_calls ( + file_path, caller_scope, awaited_expression, awaited_callee_name, + line_start, column_start, in_loop, in_try, scope_local_id + ) VALUES (?,?,?,?,?,?,?,?,?)`, + [ + r.file_path, + r.caller_scope, + r.awaited_expression, + r.awaited_callee_name, + r.line_start, + r.column_start, + r.in_loop, + r.in_try, + r.scope_local_id, + ], + ); + } +} + +export function insertTryCatchRows( + db: CodemapDatabase, + rows: ParsedTryCatch[], +): void { + for (const r of rows) { + db.run( + `INSERT INTO try_catch ( + file_path, containing_scope_local_id, try_line_start, try_line_end, + has_catch, catch_param, catch_rethrows, catch_logs_only, has_finally + ) VALUES (?,?,?,?,?,?,?,?,?)`, + [ + r.file_path, + r.containing_scope_local_id, + r.try_line_start, + r.try_line_end, + r.has_catch, + r.catch_param, + r.catch_rethrows, + r.catch_logs_only, + r.has_finally, + ], + ); + } +} + +export function insertDecorators( + db: CodemapDatabase, + filePath: string, + rows: ParsedDecorator[], +): void { + for (const d of rows) { + const sym = db + .query<{ id: number }>( + `SELECT id FROM symbols + WHERE file_path = ? AND line_start = ? + ORDER BY id ASC LIMIT 1`, + ) + .get(filePath, d.target_line_start); + db.run( + `INSERT INTO decorators ( + file_path, target_symbol_id, target_kind, name, line, column_start, args_text + ) VALUES (?,?,?,?,?,?,?)`, + [ + d.file_path, + sym?.id ?? null, + d.target_kind, + d.name, + d.line, + d.column_start, + d.args_text, + ], + ); + } +} + +export function insertJsdocTags( + db: CodemapDatabase, + filePath: string, + rows: ParsedJsdocTag[], +): void { + for (const t of rows) { + const sym = db + .query<{ id: number }>( + `SELECT id FROM symbols + WHERE file_path = ? AND name = ? AND line_start = ? + ORDER BY id ASC LIMIT 1`, + ) + .get(filePath, t.symbol_name, t.symbol_line_start); + if (!sym) continue; + db.run( + `INSERT INTO jsdoc_tags (symbol_id, tag, name, type_text, description) + VALUES (?,?,?,?,?)`, + [sym.id, t.tag, t.name, t.type_text, t.description], + ); + } +} diff --git a/src/application/index-engine.ts b/src/application/index-engine.ts index cf5f1c5c..065a45d4 100644 --- a/src/application/index-engine.ts +++ b/src/application/index-engine.ts @@ -58,6 +58,12 @@ import { isPathExcluded, } from "../runtime"; import { parseFilesParallel } from "../worker-pool"; +import { + insertAsyncCalls, + insertDecorators, + insertJsdocTags, + insertTryCatchRows, +} from "./behavioral-persist"; import { persistBindings, persistReExportChains, @@ -65,6 +71,7 @@ import { } from "./bindings-engine"; import { persistModuleCycles } from "./cycles-engine"; import { persistFileBarrelFlags } from "./file-graph-flags"; +import { persistJsxElementsAndAttributes } from "./jsx-persist"; import type { QueryBindValue } from "./query-engine"; import type { IndexPerformanceReport, @@ -97,6 +104,32 @@ function fileCategory(path: string): "ts" | "css" | "text" { return "text"; } +function persistTierSubstrate( + db: CodemapDatabase, + relPath: string, + data: Pick< + ParsedFile, + | "jsxElements" + | "jsxAttributes" + | "asyncCalls" + | "tryCatchRows" + | "decorators" + | "jsdocTags" + >, +) { + if (data.jsxElements?.length || data.jsxAttributes?.length) { + persistJsxElementsAndAttributes( + db, + data.jsxElements ?? [], + data.jsxAttributes ?? [], + ); + } + if (data.asyncCalls?.length) insertAsyncCalls(db, data.asyncCalls); + if (data.tryCatchRows?.length) insertTryCatchRows(db, data.tryCatchRows); + if (data.decorators?.length) insertDecorators(db, relPath, data.decorators); + if (data.jsdocTags?.length) insertJsdocTags(db, relPath, data.jsdocTags); +} + export function collectFiles(): string[] { const root = getProjectRoot(); // Route excludeDirNames into the glob layer as `**//**` ignores — @@ -319,6 +352,7 @@ function insertParsedResults( } if (parsed.calls?.length) insertCalls(db, parsed.calls); persistDynamicImports(db, absPath, parsed.dynamicImports); + persistTierSubstrate(db, parsed.relPath, parsed); } if (parsed.suppressions?.length) insertSuppressions(db, parsed.suppressions); @@ -542,6 +576,7 @@ export async function indexFiles( insertTypeMembers(db, data.typeMembers); if (data.calls.length) insertCalls(db, data.calls); persistDynamicImports(db, absPath, data.dynamicImports); + persistTierSubstrate(db, relPath, data); if (data.hasSideEffects) { db.run("UPDATE files SET has_side_effects = 1 WHERE path = ?", [ relPath, diff --git a/src/application/jsx-persist.ts b/src/application/jsx-persist.ts new file mode 100644 index 00000000..038a0385 --- /dev/null +++ b/src/application/jsx-persist.ts @@ -0,0 +1,67 @@ +import type { CodemapDatabase } from "../db"; +import type { ParsedJsxAttribute, ParsedJsxElement } from "../extractors/jsx"; + +export function persistJsxElementsAndAttributes( + db: CodemapDatabase, + elements: ParsedJsxElement[], + attributes: ParsedJsxAttribute[], +): void { + if (!elements.length) return; + const idMap = new Map(); + for (const el of elements) { + db.run( + `INSERT INTO jsx_elements ( + file_path, component_name, line_start, line_end, column_start, column_end, + is_self_closing, is_fragment, namespace_prefix, parent_element_id, + children_count, is_lowercase + ) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`, + [ + el.file_path, + el.component_name, + el.line_start, + el.line_end, + el.column_start, + el.column_end, + el.is_self_closing, + el.is_fragment, + el.namespace_prefix, + null, + el.children_count, + el.is_lowercase, + ], + ); + idMap.set( + el._local_id, + db.query<{ id: number }>("SELECT last_insert_rowid() AS id").get()!.id, + ); + } + for (const el of elements) { + if (el._parent_local_id == null) continue; + const id = idMap.get(el._local_id); + const parentId = idMap.get(el._parent_local_id); + if (id != null && parentId != null) { + db.run("UPDATE jsx_elements SET parent_element_id = ? WHERE id = ?", [ + parentId, + id, + ]); + } + } + for (const attr of attributes) { + const elementId = idMap.get(attr.element_local_id); + if (elementId == null) continue; + db.run( + `INSERT INTO jsx_attributes ( + element_id, name, line, column_start, column_end, value_kind, value_text + ) VALUES (?,?,?,?,?,?,?)`, + [ + elementId, + attr.name, + attr.line, + attr.column_start, + attr.column_end, + attr.value_kind, + attr.value_text, + ], + ); + } +} diff --git a/src/db.ts b/src/db.ts index 83e22f44..23c25ba5 100644 --- a/src/db.ts +++ b/src/db.ts @@ -3,7 +3,7 @@ import type { CodemapDatabase, BindValues } from "./sqlite-db"; /** Bump only on rebuild-forcing DDL changes (NOT on additive tables/columns). * See `docs/architecture.md` § Schema Versioning. */ -export const SCHEMA_VERSION = 33; +export const SCHEMA_VERSION = 34; /** * `meta` key tracking the FTS5 state at the last reindex; mismatch with the @@ -318,6 +318,79 @@ export function createTables(db: CodemapDatabase) { scope_local_id INTEGER NOT NULL DEFAULT 0 ) STRICT; + CREATE TABLE IF NOT EXISTS jsx_elements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + component_name TEXT NOT NULL, + line_start INTEGER NOT NULL, + line_end INTEGER NOT NULL, + column_start INTEGER NOT NULL, + column_end INTEGER NOT NULL, + is_self_closing INTEGER NOT NULL DEFAULT 0, + is_fragment INTEGER NOT NULL DEFAULT 0, + namespace_prefix TEXT, + parent_element_id INTEGER REFERENCES jsx_elements(id), + children_count INTEGER NOT NULL DEFAULT 0, + is_lowercase INTEGER NOT NULL DEFAULT 0 + ) STRICT; + + CREATE TABLE IF NOT EXISTS jsx_attributes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + element_id INTEGER NOT NULL REFERENCES jsx_elements(id) ON DELETE CASCADE, + name TEXT NOT NULL, + line INTEGER NOT NULL, + column_start INTEGER NOT NULL, + column_end INTEGER NOT NULL, + value_kind TEXT NOT NULL CHECK (value_kind IN ('string','expression','boolean','spread','element')), + value_text TEXT + ) STRICT; + + CREATE TABLE IF NOT EXISTS async_calls ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + caller_scope TEXT NOT NULL, + awaited_expression TEXT, + awaited_callee_name TEXT, + line_start INTEGER NOT NULL, + column_start INTEGER NOT NULL, + in_loop INTEGER NOT NULL DEFAULT 0, + in_try INTEGER NOT NULL DEFAULT 0, + scope_local_id INTEGER NOT NULL DEFAULT 0 + ) STRICT; + + CREATE TABLE IF NOT EXISTS try_catch ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + containing_scope_local_id INTEGER NOT NULL DEFAULT 0, + try_line_start INTEGER NOT NULL, + try_line_end INTEGER NOT NULL, + has_catch INTEGER NOT NULL DEFAULT 0, + catch_param TEXT, + catch_rethrows INTEGER NOT NULL DEFAULT 0, + catch_logs_only INTEGER NOT NULL DEFAULT 0, + has_finally INTEGER NOT NULL DEFAULT 0 + ) STRICT; + + CREATE TABLE IF NOT EXISTS decorators ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_path TEXT NOT NULL REFERENCES files(path) ON DELETE CASCADE, + target_symbol_id INTEGER REFERENCES symbols(id) ON DELETE SET NULL, + target_kind TEXT NOT NULL CHECK (target_kind IN ('class','method','property','parameter','accessor')), + name TEXT NOT NULL, + line INTEGER NOT NULL, + column_start INTEGER NOT NULL, + args_text TEXT + ) STRICT; + + CREATE TABLE IF NOT EXISTS jsdoc_tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + symbol_id INTEGER NOT NULL REFERENCES symbols(id) ON DELETE CASCADE, + tag TEXT NOT NULL, + name TEXT, + type_text TEXT, + description TEXT + ) STRICT; + -- Per-specifier breakdown of imports.specifiers JSON blob. Recipes that -- want specifier-precise rewrites (rename specifier, dedupe, type-only -- migrate) JOIN this table. The original imports.specifiers JSON stays @@ -536,6 +609,24 @@ export function createIndexes(db: CodemapDatabase) { CREATE INDEX IF NOT EXISTS idx_dynamic_imports_resolved ON dynamic_imports(resolved_path, file_path) WHERE resolved_path IS NOT NULL; + CREATE INDEX IF NOT EXISTS idx_jsx_elements_name ON jsx_elements(component_name, file_path); + CREATE INDEX IF NOT EXISTS idx_jsx_elements_file ON jsx_elements(file_path, line_start); + CREATE INDEX IF NOT EXISTS idx_jsx_attrs_name ON jsx_attributes(name); + CREATE INDEX IF NOT EXISTS idx_jsx_attrs_element ON jsx_attributes(element_id); + + CREATE INDEX IF NOT EXISTS idx_async_calls_callee ON async_calls(awaited_callee_name, file_path); + CREATE INDEX IF NOT EXISTS idx_async_calls_file ON async_calls(file_path, line_start); + CREATE INDEX IF NOT EXISTS idx_async_calls_loop ON async_calls(in_loop) WHERE in_loop = 1; + + CREATE INDEX IF NOT EXISTS idx_try_catch_file ON try_catch(file_path, try_line_start); + CREATE INDEX IF NOT EXISTS idx_try_catch_logs ON try_catch(catch_logs_only) WHERE catch_logs_only = 1; + + CREATE INDEX IF NOT EXISTS idx_decorators_name ON decorators(name, file_path); + CREATE INDEX IF NOT EXISTS idx_decorators_target ON decorators(target_symbol_id); + + CREATE INDEX IF NOT EXISTS idx_jsdoc_tags_symbol ON jsdoc_tags(symbol_id); + CREATE INDEX IF NOT EXISTS idx_jsdoc_tags_tag ON jsdoc_tags(tag); + CREATE INDEX IF NOT EXISTS idx_re_export_chains_to ON re_export_chains(to_file, to_name); CREATE INDEX IF NOT EXISTS idx_re_export_chains_truncated ON re_export_chains(truncated) WHERE truncated = 1; @@ -602,6 +693,12 @@ export function createSchema(db: CodemapDatabase) { export function dropAll(db: CodemapDatabase) { db.run(` + DROP TABLE IF EXISTS jsdoc_tags; + DROP TABLE IF EXISTS decorators; + DROP TABLE IF EXISTS try_catch; + DROP TABLE IF EXISTS async_calls; + DROP TABLE IF EXISTS jsx_attributes; + DROP TABLE IF EXISTS jsx_elements; DROP TABLE IF EXISTS module_cycles; DROP TABLE IF EXISTS dynamic_imports; DROP TABLE IF EXISTS re_export_chains; diff --git a/src/extractors/behavioral.ts b/src/extractors/behavioral.ts new file mode 100644 index 00000000..1fa51220 --- /dev/null +++ b/src/extractors/behavioral.ts @@ -0,0 +1,335 @@ +/** + * Behavioral substrate — async/await sites, try/catch, decorators (Tier 5). + */ + +import { offsetToLine } from "./offsets"; +import type { TierExtractor } from "./types"; + +export interface ParsedAsyncCall { + file_path: string; + caller_scope: string; + awaited_expression: string; + awaited_callee_name: string | null; + line_start: number; + column_start: number; + in_loop: number; + in_try: number; + scope_local_id: number; +} + +export interface ParsedTryCatch { + file_path: string; + containing_scope_local_id: number; + try_line_start: number; + try_line_end: number; + has_catch: number; + catch_param: string | null; + catch_rethrows: number; + catch_logs_only: number; + has_finally: number; +} + +export interface ParsedDecorator { + file_path: string; + target_kind: "class" | "method" | "property" | "parameter" | "accessor"; + target_line_start: number; + name: string; + line: number; + column_start: number; + args_text: string | null; +} + +export interface ParsedJsdocTag { + file_path: string; + symbol_name: string; + symbol_line_start: number; + tag: string; + name: string | null; + type_text: string | null; + description: string | null; +} + +function calleeNameFromExpression(expr: any): string | null { + if (expr?.type === "CallExpression") { + const callee = expr.callee; + if (callee?.type === "Identifier") return callee.name; + if ( + callee?.type === "MemberExpression" && + !callee.computed && + callee.property?.type === "Identifier" + ) { + return callee.property.name; + } + } + return null; +} + +function decoratorName(node: any, source: string): string { + const expr = node.expression; + if (expr?.type === "Identifier") return expr.name; + if (expr?.type === "CallExpression") { + const callee = expr.callee; + if (callee?.type === "Identifier") return callee.name; + if ( + callee?.type === "MemberExpression" && + callee.property?.type === "Identifier" + ) { + return callee.property.name; + } + return source.slice(expr.callee.start, expr.callee.end); + } + return source.slice(node.start, node.end); +} + +function catchParamName(param: any): string | null { + if (!param) return null; + if (param.type === "Identifier") return param.name; + return null; +} + +function bodyHasThrow(body: any, catchParam: string | null): boolean { + if (!body?.body?.length) return false; + const stack: any[] = [...body.body]; + while (stack.length) { + const node = stack.pop(); + if (!node) continue; + if (node.type === "ThrowStatement") { + const arg = node.argument; + if (!arg) return true; + if (arg.type === "Identifier" && catchParam && arg.name === catchParam) { + return true; + } + if (arg.type === "Identifier" && !catchParam) return true; + } + for (const key of Object.keys(node)) { + const val = node[key]; + if (val && typeof val === "object") { + if (Array.isArray(val)) stack.push(...val); + else if (val.type) stack.push(val); + } + } + } + return false; +} + +function isLogsOnlyCatch(body: any): boolean { + if (!body?.body?.length) return false; + for (const stmt of body.body) { + if (stmt.type === "ThrowStatement") return false; + if (stmt.type === "ExpressionStatement") { + const expr = stmt.expression; + if (expr?.type === "CallExpression") { + const callee = expr.callee; + if ( + callee?.type === "MemberExpression" && + callee.object?.type === "Identifier" && + callee.object.name === "console" + ) { + continue; + } + } + return false; + } + return false; + } + return body.body.length > 0; +} + +function isLoopNode(type: string): boolean { + return ( + type === "ForStatement" || + type === "ForInStatement" || + type === "ForOfStatement" || + type === "WhileStatement" || + type === "DoWhileStatement" + ); +} + +export function parseJsDocTags(doc: string): Array<{ + tag: string; + name: string | null; + type_text: string | null; + description: string | null; +}> { + const out: Array<{ + tag: string; + name: string | null; + type_text: string | null; + description: string | null; + }> = []; + for (const rawLine of doc.split("\n")) { + const line = rawLine.trim(); + if (!line.startsWith("@")) continue; + let m = /^@param\s+(?:\{([^}]*)\}\s+)?(\S+)\s*(.*)$/.exec(line); + if (m) { + out.push({ + tag: "@param", + name: m[2] ?? null, + type_text: m[1] || null, + description: m[3]?.trim() || null, + }); + continue; + } + m = /^@returns?\s+(?:\{([^}]*)\}\s+)?(.*)$/.exec(line); + if (m) { + out.push({ + tag: "@returns", + name: null, + type_text: m[1] || null, + description: m[2]?.trim() || null, + }); + continue; + } + m = /^@throws\s+(?:\{([^}]*)\}\s+)?(.*)$/.exec(line); + if (m) { + out.push({ + tag: "@throws", + name: null, + type_text: m[1] || null, + description: m[2]?.trim() || null, + }); + continue; + } + m = /^@(\w+)\s+(.*)$/.exec(line); + if (m) { + out.push({ + tag: `@${m[1]}`, + name: null, + type_text: null, + description: m[2]?.trim() || null, + }); + } + } + return out; +} + +export const behavioralExtractor: TierExtractor = { + tierId: "behavioral", + register(visitor, ctx) { + const { relPath, lineMap, source, scopes } = ctx; + const asyncCalls = ctx.asyncCalls; + const tryCatchRows = ctx.tryCatchRows; + const decorators = ctx.decorators; + let loopDepth = 0; + let tryDepth = 0; + + const enterLoop = (node: any) => { + if (isLoopNode(node.type)) loopDepth++; + }; + const exitLoop = (node: any) => { + if (isLoopNode(node.type)) loopDepth--; + }; + + function recordDecorator( + dec: any, + targetKind: ParsedDecorator["target_kind"], + targetLineStart: number, + ) { + const line = offsetToLine(lineMap, dec.start); + const lineStartOffset = lineMap[line - 1] ?? 0; + const expr = dec.expression; + const argsText = + expr?.type === "CallExpression" + ? source.slice(expr.arguments[0]?.start ?? expr.start, expr.end) + : null; + decorators.push({ + file_path: relPath, + target_kind: targetKind, + target_line_start: targetLineStart, + name: decoratorName(dec, source), + line, + column_start: dec.start - lineStartOffset, + args_text: argsText, + }); + } + + Object.assign(visitor, { + ForStatement: enterLoop, + "ForStatement:exit": exitLoop, + ForInStatement: enterLoop, + "ForInStatement:exit": exitLoop, + ForOfStatement: enterLoop, + "ForOfStatement:exit": exitLoop, + WhileStatement: enterLoop, + "WhileStatement:exit": exitLoop, + DoWhileStatement: enterLoop, + "DoWhileStatement:exit": exitLoop, + + TryStatement(node: any) { + tryDepth++; + const handler = node.handler; + const catchParam = catchParamName(handler?.param); + const catchBody = handler?.body; + tryCatchRows.push({ + file_path: relPath, + containing_scope_local_id: scopes.currentLocalId(), + try_line_start: offsetToLine(lineMap, node.block.start), + try_line_end: offsetToLine(lineMap, node.block.end), + has_catch: handler ? 1 : 0, + catch_param: catchParam, + catch_rethrows: + catchBody && bodyHasThrow(catchBody, catchParam) ? 1 : 0, + catch_logs_only: catchBody && isLogsOnlyCatch(catchBody) ? 1 : 0, + has_finally: node.finalizer ? 1 : 0, + }); + }, + "TryStatement:exit"() { + tryDepth--; + }, + + AwaitExpression(node: any) { + const lineStart = offsetToLine(lineMap, node.start); + const lineStartOffset = lineMap[lineStart - 1] ?? 0; + asyncCalls.push({ + file_path: relPath, + caller_scope: scopes.currentScope(), + awaited_expression: source.slice(node.start, node.end), + awaited_callee_name: calleeNameFromExpression(node.argument), + line_start: lineStart, + column_start: node.start - lineStartOffset, + in_loop: loopDepth > 0 ? 1 : 0, + in_try: tryDepth > 0 ? 1 : 0, + scope_local_id: scopes.currentLocalId(), + }); + }, + + ClassDeclaration(node: any) { + const targetLine = offsetToLine(lineMap, node.id?.start ?? node.start); + for (const dec of node.decorators ?? []) { + recordDecorator(dec, "class", targetLine); + } + }, + + MethodDefinition(node: any) { + const targetLine = offsetToLine(lineMap, node.key.start); + const kind = + node.kind === "get" || node.kind === "set" ? "accessor" : "method"; + for (const dec of node.decorators ?? []) { + recordDecorator(dec, kind, targetLine); + } + }, + + PropertyDefinition(node: any) { + const targetLine = offsetToLine(lineMap, node.key.start); + for (const dec of node.decorators ?? []) { + recordDecorator(dec, "property", targetLine); + } + }, + }); + }, + + finalize(ctx) { + for (const s of ctx.symbols) { + if (!s.doc_comment) continue; + if (s.kind === "param" || s.kind === "type-param") continue; + for (const t of parseJsDocTags(s.doc_comment)) { + ctx.jsdocTags.push({ + file_path: s.file_path, + symbol_name: s.name, + symbol_line_start: s.line_start, + ...t, + }); + } + } + }, +}; diff --git a/src/extractors/jsx.ts b/src/extractors/jsx.ts new file mode 100644 index 00000000..63513bb7 --- /dev/null +++ b/src/extractors/jsx.ts @@ -0,0 +1,212 @@ +/** + * JSX elements + attributes substrate (Tier 3). + */ + +import { offsetToLine } from "./offsets"; +import type { TierExtractor } from "./types"; + +export interface ParsedJsxElement { + file_path: string; + component_name: string; + line_start: number; + line_end: number; + column_start: number; + column_end: number; + is_self_closing: number; + is_fragment: number; + namespace_prefix: string | null; + children_count: number; + is_lowercase: number; + _local_id: number; + _parent_local_id: number | null; +} + +export interface ParsedJsxAttribute { + element_local_id: number; + name: string; + line: number; + column_start: number; + column_end: number; + value_kind: "string" | "expression" | "boolean" | "spread" | "element"; + value_text: string | null; +} + +function jsxElementName(node: any): { + component_name: string; + namespace_prefix: string | null; + is_lowercase: number; +} { + if (!node) { + return { component_name: "", namespace_prefix: null, is_lowercase: 0 }; + } + if (node.type === "JSXIdentifier") { + const name = node.name ?? ""; + return { + component_name: name, + namespace_prefix: null, + is_lowercase: /^[a-z]/.test(name) ? 1 : 0, + }; + } + if (node.type === "JSXMemberExpression") { + const object = jsxElementName(node.object); + const prop = node.property?.name ?? ""; + return { + component_name: prop, + namespace_prefix: object.component_name || object.namespace_prefix, + is_lowercase: /^[a-z]/.test(prop) ? 1 : 0, + }; + } + if (node.type === "JSXNamespacedName") { + return { + component_name: node.name?.name ?? "", + namespace_prefix: node.namespace?.name ?? null, + is_lowercase: 0, + }; + } + return { component_name: "", namespace_prefix: null, is_lowercase: 0 }; +} + +function countJsxChildren(children: any[] | undefined): number { + if (!children?.length) return 0; + let n = 0; + for (const child of children) { + if (child?.type === "JSXElement" || child?.type === "JSXFragment") n++; + } + return n; +} + +function attributeValue( + source: string, + value: any, +): { value_kind: ParsedJsxAttribute["value_kind"]; value_text: string | null } { + if (value == null) { + return { value_kind: "boolean", value_text: null }; + } + if (value.type === "Literal" && typeof value.value === "string") { + return { value_kind: "string", value_text: value.value }; + } + if (value.type === "JSXExpressionContainer") { + const inner = value.expression; + if (inner) { + return { + value_kind: "expression", + value_text: source.slice(inner.start, inner.end), + }; + } + return { value_kind: "expression", value_text: null }; + } + if (value.type === "JSXElement" || value.type === "JSXFragment") { + return { + value_kind: "element", + value_text: source.slice(value.start, value.end), + }; + } + return { + value_kind: "expression", + value_text: source.slice(value.start, value.end), + }; +} + +export const jsxExtractor: TierExtractor = { + tierId: "jsx", + register(visitor, ctx) { + if (!ctx.isTsx) return; + + const { relPath, lineMap, source } = ctx; + const elements = ctx.jsxElements; + const attributes = ctx.jsxAttributes; + const stack: number[] = []; + let nextLocalId = 0; + + function recordAttributes(opening: any, elementLocalId: number) { + for (const attr of opening.attributes ?? []) { + if (attr.type === "JSXSpreadAttribute") { + const line = offsetToLine(lineMap, attr.start); + const lineStartOffset = lineMap[line - 1] ?? 0; + attributes.push({ + element_local_id: elementLocalId, + name: "…spread", + line, + column_start: attr.start - lineStartOffset, + column_end: attr.end - lineStartOffset, + value_kind: "spread", + value_text: source.slice(attr.argument.start, attr.argument.end), + }); + continue; + } + if (attr.type !== "JSXAttribute") continue; + const name = attr.name?.name ?? ""; + const tokenStart = attr.name?.start ?? attr.start; + const tokenEnd = attr.name?.end ?? attr.end; + const line = offsetToLine(lineMap, tokenStart); + const lineStartOffset = lineMap[line - 1] ?? 0; + const { value_kind, value_text } = attributeValue(source, attr.value); + attributes.push({ + element_local_id: elementLocalId, + name, + line, + column_start: tokenStart - lineStartOffset, + column_end: tokenEnd - lineStartOffset, + value_kind, + value_text, + }); + } + } + + function pushElement( + node: any, + nameNode: any, + opening: any, + isFragment: number, + ) { + const localId = nextLocalId++; + const parentLocalId = stack.length ? stack[stack.length - 1]! : null; + const { component_name, namespace_prefix, is_lowercase } = isFragment + ? { component_name: "", namespace_prefix: null, is_lowercase: 0 } + : jsxElementName(nameNode); + const tokenStart = isFragment + ? (node.openingFragment?.start ?? node.start) + : (opening.name?.start ?? opening.start); + const tokenEnd = isFragment + ? (node.closingFragment?.end ?? node.end) + : (opening.name?.end ?? opening.end); + const lineStart = offsetToLine(lineMap, node.start); + const lineEnd = offsetToLine(lineMap, node.end); + const lineStartOffset = lineMap[lineStart - 1] ?? 0; + elements.push({ + file_path: relPath, + component_name, + line_start: lineStart, + line_end: lineEnd, + column_start: tokenStart - lineStartOffset, + column_end: tokenEnd - lineStartOffset, + is_self_closing: opening?.selfClosing ? 1 : 0, + is_fragment: isFragment, + namespace_prefix, + children_count: countJsxChildren(node.children), + is_lowercase, + _local_id: localId, + _parent_local_id: parentLocalId, + }); + if (!isFragment && opening) { + recordAttributes(opening, localId); + } + stack.push(localId); + } + + Object.assign(visitor, { + JSXElement(node: any) { + pushElement(node, node.openingElement?.name, node.openingElement, 0); + }, + "JSXElement:exit"() { + stack.pop(); + }, + JSXFragment(node: any) { + pushElement(node, null, node.openingFragment, 1); + }, + "JSXFragment:exit"() { + stack.pop(); + }, + }); + }, +}; diff --git a/src/extractors/types.ts b/src/extractors/types.ts index ddfe296e..d89dade1 100644 --- a/src/extractors/types.ts +++ b/src/extractors/types.ts @@ -14,6 +14,13 @@ import type { TestSuiteRow, TypeMemberRow, } from "../db"; +import type { + ParsedAsyncCall, + ParsedDecorator, + ParsedJsdocTag, + ParsedTryCatch, +} from "./behavioral"; +import type { ParsedJsxAttribute, ParsedJsxElement } from "./jsx"; /** * Tier opt-out config key per [R.15](../../docs/plans/substrate-extraction.md). @@ -128,6 +135,13 @@ export interface ExtractContext { readonly testSuites: TestSuiteRow[]; readonly dynamicImports: DynamicImportRow[]; + readonly jsxElements: ParsedJsxElement[]; + readonly jsxAttributes: ParsedJsxAttribute[]; + readonly asyncCalls: ParsedAsyncCall[]; + readonly tryCatchRows: ParsedTryCatch[]; + readonly decorators: ParsedDecorator[]; + readonly jsdocTags: ParsedJsdocTag[]; + /** When true, module-level CallExpression / AssignmentExpression seen. */ moduleHasSideEffects: boolean; diff --git a/src/parsed-types.ts b/src/parsed-types.ts index 652cb470..290d681c 100644 --- a/src/parsed-types.ts +++ b/src/parsed-types.ts @@ -53,6 +53,12 @@ export interface ParsedFile { runtimeMarkers?: RuntimeMarkerRow[]; testSuites?: TestSuiteRow[]; dynamicImports?: DynamicImportRow[]; + jsxElements?: import("./extractors/jsx").ParsedJsxElement[]; + jsxAttributes?: import("./extractors/jsx").ParsedJsxAttribute[]; + asyncCalls?: import("./extractors/behavioral").ParsedAsyncCall[]; + tryCatchRows?: import("./extractors/behavioral").ParsedTryCatch[]; + decorators?: import("./extractors/behavioral").ParsedDecorator[]; + jsdocTags?: import("./extractors/behavioral").ParsedJsdocTag[]; hasSideEffects?: number; /** CSS-only fields (populated when `category === "css"`). */ cssVariables?: CssVariableRow[]; diff --git a/src/parser.ts b/src/parser.ts index 2391477a..f31e843a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -26,6 +26,7 @@ import type { TestSuiteRow, DynamicImportRow, } from "./db"; +import { behavioralExtractor } from "./extractors/behavioral"; import { callsExtractor } from "./extractors/calls"; import { complexityExtractor, @@ -37,6 +38,7 @@ import { } from "./extractors/components"; import { dynamicImportsExtractor } from "./extractors/dynamic-imports"; import { extractVisibility } from "./extractors/jsdoc"; +import { jsxExtractor } from "./extractors/jsx"; import { markersExtractor } from "./extractors/markers"; import { moduleSideEffectsExtractor } from "./extractors/module-side-effects"; import { buildLineMap, offsetToLine } from "./extractors/offsets"; @@ -66,6 +68,12 @@ interface ExtractedData { runtimeMarkers: RuntimeMarkerRow[]; testSuites: TestSuiteRow[]; dynamicImports: DynamicImportRow[]; + jsxElements: import("./extractors/jsx").ParsedJsxElement[]; + jsxAttributes: import("./extractors/jsx").ParsedJsxAttribute[]; + asyncCalls: import("./extractors/behavioral").ParsedAsyncCall[]; + tryCatchRows: import("./extractors/behavioral").ParsedTryCatch[]; + decorators: import("./extractors/behavioral").ParsedDecorator[]; + jsdocTags: import("./extractors/behavioral").ParsedJsdocTag[]; hasSideEffects: number; } @@ -110,7 +118,9 @@ const EXTRACTORS: readonly TierExtractor[] = [ callsExtractor, componentsExtractor, referencesExtractor, + jsxExtractor, dynamicImportsExtractor, + behavioralExtractor, moduleSideEffectsExtractor, runtimeMarkersExtractor, testsExtractor, @@ -151,6 +161,12 @@ export function extractFileData( const runtimeMarkers: RuntimeMarkerRow[] = []; const testSuites: TestSuiteRow[] = []; const dynamicImports: DynamicImportRow[] = []; + const jsxElements: import("./extractors/jsx").ParsedJsxElement[] = []; + const jsxAttributes: import("./extractors/jsx").ParsedJsxAttribute[] = []; + const asyncCalls: import("./extractors/behavioral").ParsedAsyncCall[] = []; + const tryCatchRows: import("./extractors/behavioral").ParsedTryCatch[] = []; + const decorators: import("./extractors/behavioral").ParsedDecorator[] = []; + const jsdocTags: import("./extractors/behavioral").ParsedJsdocTag[] = []; const exportedNames = new Set(); const defaultExportedNames = new Set(); @@ -206,6 +222,12 @@ export function extractFileData( runtimeMarkers, testSuites, dynamicImports, + jsxElements, + jsxAttributes, + asyncCalls, + tryCatchRows, + decorators, + jsdocTags, scopes: createScopeTracker(relPath), complexity: createComplexityTracker(symbols), componentDetector: createComponentDetector(), @@ -242,6 +264,12 @@ export function extractFileData( runtimeMarkers, testSuites, dynamicImports, + jsxElements, + jsxAttributes, + asyncCalls, + tryCatchRows, + decorators, + jsdocTags, hasSideEffects: ctx.moduleHasSideEffects ? 1 : 0, }; } diff --git a/templates/recipes/find-await-in-loop.md b/templates/recipes/find-await-in-loop.md new file mode 100644 index 00000000..db77582a --- /dev/null +++ b/templates/recipes/find-await-in-loop.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-await-in-loop + description: Await sites inside loop bodies — Tier 5 behavioral substrate. +--- + +# find-await-in-loop + +`AwaitExpression` sites inside loop bodies (Tier 5). + +```bash +codemap query --recipe find-await-in-loop +``` diff --git a/templates/recipes/find-await-in-loop.sql b/templates/recipes/find-await-in-loop.sql new file mode 100644 index 00000000..ddc2edb0 --- /dev/null +++ b/templates/recipes/find-await-in-loop.sql @@ -0,0 +1,4 @@ +SELECT file_path, caller_scope, awaited_expression, awaited_callee_name, line_start, in_loop, in_try +FROM async_calls +WHERE in_loop = 1 +ORDER BY file_path, line_start; diff --git a/templates/recipes/find-decorator-usage.md b/templates/recipes/find-decorator-usage.md new file mode 100644 index 00000000..3c0b8061 --- /dev/null +++ b/templates/recipes/find-decorator-usage.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-decorator-usage + description: Decorator sites with optional resolved target_symbol_id. +--- + +# find-decorator-usage + +Decorator sites linked to decorated symbols when resolvable (Tier 5). + +```bash +codemap query --recipe find-decorator-usage +``` diff --git a/templates/recipes/find-decorator-usage.sql b/templates/recipes/find-decorator-usage.sql new file mode 100644 index 00000000..d7ce5017 --- /dev/null +++ b/templates/recipes/find-decorator-usage.sql @@ -0,0 +1,3 @@ +SELECT file_path, target_kind, name, line, column_start, target_symbol_id +FROM decorators +ORDER BY file_path, line, column_start; diff --git a/templates/recipes/find-jsx-usages.md b/templates/recipes/find-jsx-usages.md new file mode 100644 index 00000000..d6747765 --- /dev/null +++ b/templates/recipes/find-jsx-usages.md @@ -0,0 +1,20 @@ +--- +params: + - name: component_name + type: string + required: true + description: JSX component or HTML tag name (`ProductCard`, `article`, …). +actions: + - type: navigate-to-jsx-usages + description: Each row is one JSX element occurrence with line/column bounds. +--- + +# find-jsx-usages + +JSX element rows from the `jsx_elements` substrate (Tier 3). + +```bash +codemap query --recipe find-jsx-usages --params component_name=article +``` + +Join `jsx_attributes` on `element_id` for attribute-level queries. diff --git a/templates/recipes/find-jsx-usages.sql b/templates/recipes/find-jsx-usages.sql new file mode 100644 index 00000000..040cae95 --- /dev/null +++ b/templates/recipes/find-jsx-usages.sql @@ -0,0 +1,4 @@ +SELECT file_path, component_name, line_start, line_end, is_self_closing, is_fragment, is_lowercase, children_count +FROM jsx_elements +WHERE component_name = ? +ORDER BY file_path, line_start, column_start; diff --git a/templates/recipes/find-swallowed-errors.md b/templates/recipes/find-swallowed-errors.md new file mode 100644 index 00000000..82b227a0 --- /dev/null +++ b/templates/recipes/find-swallowed-errors.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-swallowed-errors + description: Catch blocks that only log to console.* without rethrowing. +--- + +# find-swallowed-errors + +Try/catch blocks whose catch body only logs to `console.*` (Tier 5 heuristic). + +```bash +codemap query --recipe find-swallowed-errors +``` diff --git a/templates/recipes/find-swallowed-errors.sql b/templates/recipes/find-swallowed-errors.sql new file mode 100644 index 00000000..a56d7e6e --- /dev/null +++ b/templates/recipes/find-swallowed-errors.sql @@ -0,0 +1,4 @@ +SELECT file_path, try_line_start, try_line_end, catch_param, catch_logs_only, catch_rethrows +FROM try_catch +WHERE has_catch = 1 AND catch_logs_only = 1 +ORDER BY file_path, try_line_start; diff --git a/templates/recipes/find-throws-jsdoc.md b/templates/recipes/find-throws-jsdoc.md new file mode 100644 index 00000000..e29864a9 --- /dev/null +++ b/templates/recipes/find-throws-jsdoc.md @@ -0,0 +1,14 @@ +--- +params: [] +actions: + - type: navigate-to-throws-jsdoc + description: Symbols carrying structured @throws JSDoc tags. +--- + +# find-throws-jsdoc + +Structured `@throws` tags from the `jsdoc_tags` substrate (Tier 5). + +```bash +codemap query --recipe find-throws-jsdoc +``` diff --git a/templates/recipes/find-throws-jsdoc.sql b/templates/recipes/find-throws-jsdoc.sql new file mode 100644 index 00000000..22ca79d1 --- /dev/null +++ b/templates/recipes/find-throws-jsdoc.sql @@ -0,0 +1,5 @@ +SELECT s.file_path, s.name, s.line_start, t.tag, t.type_text, t.description +FROM symbols s +JOIN jsdoc_tags t ON t.symbol_id = s.id +WHERE t.tag = '@throws' +ORDER BY s.file_path, s.line_start; From 0a69232492c633fadb3b0418144758aaa748b0b2 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:25:35 +0300 Subject: [PATCH 09/23] =?UTF-8?q?docs:=20mark=20substrate=20tiers=201?= =?UTF-8?q?=E2=80=936=20rollout=20complete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update ship status in substrate-extraction.md and close the rollout plan baseline (SCHEMA 34, all scoped slices landed, C.9 excluded). --- docs/plans/substrate-extraction.md | 14 +++++++------- docs/plans/substrate-tiers-1-6-rollout.md | 16 ++++++++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/plans/substrate-extraction.md b/docs/plans/substrate-extraction.md index 7f4e5d9d..900b2966 100644 --- a/docs/plans/substrate-extraction.md +++ b/docs/plans/substrate-extraction.md @@ -2,7 +2,7 @@ > **Status:** open · plan iterating in parallel with the broader [`research/codemap-richer-index-synthesis-2026-05.md`](../research/codemap-richer-index-synthesis-2026-05.md) write-engine direction. > -> **Per-tier ship status (fact-checked 2026-05-18):** Tiers **1** and **2** shipped in narrowed form. Tiers **4 / 6 / 9 / 10 / 11 / 12** partially shipped — their foundation tables landed in [`src/db.ts`](../../src/db.ts) but not the full proposed shapes. Tiers **3 (JSX) / 5 (Behavioral) / 7 (CSS rich) / 8 (Project meta) / 13 (ORM/SQL)** are not shipped. Current live schema confirms rows for `calls`, `exports`, `import_specifiers`, `references`, `scopes`, `bindings`, `function_params`, `re_export_chains`, `test_suites`, `runtime_markers`, `file_metrics`, and `module_cycles`; absent tables include `jsx_elements`, `jsx_attributes`, `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`, `css_rules`, `css_at_rules`, `css_declarations`, `tsconfig_options`, `package_json_meta`, `orm_models`, `sql_strings`, and `db_migrations`. +> **Per-tier ship status (fact-checked 2026-05-19):** Tiers **1–6** (remainder without C.9) landed on branch `feat/substrate-tiers-1-6` — `SCHEMA_VERSION` **34**. Tier **1** now includes call-shape columns, side-effect `import_specifiers` + `import_id` FK. Tier **2** adds `bindings.resolution_kind='re-exported'`. Tier **3** (`jsx_elements` / `jsx_attributes`) and Tier **5** (`async_calls` / `try_catch` / `decorators` / `jsdoc_tags`) shipped. Tier **4** partial — `symbols.{return_type,is_async,is_generator}` shipped; `generic_params` / `type_predicates` deferred. Tier **6** partial — `dynamic_imports`, `files.{is_barrel,has_side_effects}` shipped; `files.is_entry` stays gated on [`c9-plugin-layer.md`](./c9-plugin-layer.md). Tiers **7–13** remain open. > > **Motivator:** Codemap's distinctive value is the SQL-against-structural-index substrate. Per [Moat B](../roadmap.md#moats-load-bearing) — _"Extracted structure ≥ verdicts. Schema breadth is the substrate every recipe layers on."_ — the load-bearing growth axis is **what oxc / Lightning CSS / config loaders give us that the index doesn't yet expose.** Today the schema captures symbols + imports + exports + calls + components + markers + type*members + css*{variables,classes,keyframes} + suppressions. The AST contains roughly 4× more queryable structure that we discard at parse time. This plan enumerates the entire extraction surface — ~13 tiers spanning identifier references, scope graph, binding resolution, JSX, type-system depth, behavioral facts, module-graph topology, CSS rule structure, test-suite metadata, runtime/dev markers, metrics expansion, and ORM/SQL tracking — and sequences them as independent tracer-bullet PRs that compound into a maximal substrate. Once landed, every recipe / write capability discussed in the synthesis doc (and many more) lights up via SQL JOINs alone, with zero engine work. > @@ -193,7 +193,7 @@ Each tier is one tracer-bullet PR: parser visitor change + schema migration + 1- **Goal:** Make `calls` / `exports` / `symbols` / `markers` column-precise; split `imports.specifiers` JSON blob into a typed child table. -**Ship status (fact-checked 2026-05-18):** 4 slices landed, but the live schema is narrower than this tier's original proposal. Present today: `calls.{line_start,column_start,column_end}`, `exports.{line_start,line_end,column_start,column_end,is_re_export}`, `symbols.{name_column_start,name_column_end}`, `markers.{column_start,column_end}`, and `import_specifiers`. Deferred from the proposal: `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`, `import_specifiers.import_id`, and side-effect import rows. +**Ship status (fact-checked 2026-05-19):** Tier 1 remainder shipped on `feat/substrate-tiers-1-6` — `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`, side-effect `import_specifiers` rows, and `import_id` FK. Position columns from 2026-05-14 remain. | Slice | Substrate | Flagship recipe | Schema bump | | ----- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------- | @@ -278,7 +278,7 @@ New recipe candidates: `dedupe-imports`, `consolidate-type-only-imports`, `stale ### Tier 2 — `references` + `scopes` + `bindings` (the load-bearing tier) — **SHIPPED 2026-05-15** -**Status (fact-checked 2026-05-18):** Tier 2 shipped in narrowed form. `references`, `scopes`, and `bindings` exist and are populated in the live self-index. Current schema uses parser-local scope IDs (`scopes.local_id`, `references.scope_local_id`) and a compact `references.kind IN ('value','type','jsx','member')`; the richer proposed kind taxonomy (`decorator`, `shorthand-*`, `computed-member`, etc.), `bindings.namespace`, and `resolution_kind='re-exported'` remain deferred. Params and re-export chains shipped as separate foundation tables (`function_params`, `re_export_chains`) rather than the exact Tier 2 DDL below. +**Status (fact-checked 2026-05-19):** Tier 2 shipped including `bindings.resolution_kind='re-exported'` (2026-05-19). `references`, `scopes`, and `bindings` populated. Richer proposed kind taxonomy (`decorator`, `shorthand-*`, …) and `bindings.namespace` remain deferred. **Goal:** Every identifier _use_ — call, type position, JSX, decorator, shorthand, member access, spread — becomes a queryable row. Plus a lexical scope graph and per-reference binding resolution to the originating symbol. @@ -526,7 +526,7 @@ Deferred to a future slice (out of Tier 2 scope): ### Tier 3 — JSX elements + attributes -**Ship status (2026-05-15):** Not shipped. `jsx_elements` / `jsx_attributes` absent from [`src/db.ts`](../../src/db.ts). Open. +**Ship status (2026-05-19):** Shipped on `feat/substrate-tiers-1-6`. `jsx_elements` / `jsx_attributes` in [`src/db.ts`](../../src/db.ts); extractor [`src/extractors/jsx.ts`](../../src/extractors/jsx.ts); recipe `find-jsx-usages`. **Goal:** Every JSX element + every JSX attribute becomes a queryable row with column-precise positions. @@ -684,7 +684,7 @@ New recipe candidates: `swap-positional-to-named-args` (extends `rename-preview` ### Tier 5 — Behavioral facts (async, try/catch, decorators, structured JSDoc) -**Ship status (2026-05-15):** Not shipped. `async_calls` / `try_catch` / `decorators` / `jsdoc_tags` absent from [`src/db.ts`](../../src/db.ts). Open. +**Ship status (2026-05-19):** Shipped on `feat/substrate-tiers-1-6`. Tables + [`src/extractors/behavioral.ts`](../../src/extractors/behavioral.ts); recipes `find-await-in-loop`, `find-swallowed-errors`, `find-decorator-usage`, `find-throws-jsdoc`. **Goal:** Capture runtime-shape behavioral facts the AST encodes but today's index discards. @@ -787,9 +787,9 @@ New recipe candidates: `find-awaits-in-loops`; `find-empty-catches`; `find-depre --- -### Tier 6 — Module-graph enrichment — **PARTIAL (2026-05-15)** +### Tier 6 — Module-graph enrichment — **PARTIAL (2026-05-19)** -**Ship status (2026-05-15):** `re_export_chains` shipped via Tier 2.2 (slimmer shape — `(from_file, from_name, to_file, to_name, hops, truncated)` `WITHOUT ROWID` PK; no separate `chain_path` text). Bounded at 10 hops with cycle detection. `bindings-engine` walks chains for cross-file resolution. **Deferred:** `dynamic_imports` table; `files.{is_barrel, is_entry, has_side_effects}` columns. `files.is_entry` stays gated on the [`c9-plugin-layer.md`](./c9-plugin-layer.md) plan. +**Ship status (2026-05-19):** `re_export_chains`, `dynamic_imports`, `files.{is_barrel,has_side_effects}` shipped on `feat/substrate-tiers-1-6`. **Deferred:** `files.is_entry` (C.9 / [`c9-plugin-layer.md`](./c9-plugin-layer.md)). **Goal:** Flatten re-export chains; record dynamic imports; mark barrel files. diff --git a/docs/plans/substrate-tiers-1-6-rollout.md b/docs/plans/substrate-tiers-1-6-rollout.md index 9032aa8f..86d2aada 100644 --- a/docs/plans/substrate-tiers-1-6-rollout.md +++ b/docs/plans/substrate-tiers-1-6-rollout.md @@ -1,6 +1,6 @@ # Substrate tiers 1–6 rollout (without C.9) -> **Status:** In flight — branch `feat/substrate-tiers-1-6`. +> **Status:** Complete — implemented on `feat/substrate-tiers-1-6` (8 commits, `SCHEMA_VERSION` 27→**34**). C.9 explicitly excluded. Push + draft PR when ready (body below). > **Parent plan:** [`substrate-extraction.md`](./substrate-extraction.md) (tiers 7–13 out of scope here). > **Explicit exclusion:** C.9 plugin layer — no `files.is_entry`, no reachability-from-entry, no framework entry hints. See [`c9-plugin-layer.md`](./c9-plugin-layer.md). @@ -8,13 +8,13 @@ Reindexed self (`bun src/index.ts --full`); `validate --json` → `[]`. -| Fact | Value | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SCHEMA_VERSION` | **27** ([`src/db.ts`](../../src/db.ts)) | -| Extractor orchestration | [`src/parser.ts`](../../src/parser.ts) `EXTRACTORS[]` — `symbolsExtractor` → `scopesExtractor` → `complexityExtractor` → `callsExtractor` → `componentsExtractor` → `referencesExtractor` → `runtimeMarkersExtractor` → `testsExtractor` → `markersExtractor` | -| Post-passes (index-engine) | bindings ([`resolveBindings`](../../src/application/bindings-engine.ts)), re-export chains + module cycles (full index only) | -| Live substrate tables | `calls`, `exports`, `import_specifiers`, `references`, `scopes`, `bindings`, `function_params`, `re_export_chains`, `module_cycles`, … | -| Absent (in-scope gaps) | `jsx_*`, `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`, `dynamic_imports`; `calls` call-shape flags; `symbols.{return_type,is_async,is_generator}`; `files.{is_barrel,has_side_effects}`; `bindings.resolution_kind='re-exported'` | +| Fact | Value | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `SCHEMA_VERSION` | **34** ([`src/db.ts`](../../src/db.ts)) | +| Extractor orchestration | [`src/parser.ts`](../../src/parser.ts) `EXTRACTORS[]` — … `referencesExtractor` → `jsxExtractor` → `dynamicImportsExtractor` → `behavioralExtractor` → `moduleSideEffectsExtractor` → … | +| Post-passes (index-engine) | bindings, re-export chains, module cycles, JSX parent links (per-file insert), decorator/jsdoc symbol linking | +| Live substrate tables | All tier 1–6 rollout tables including `jsx_*`, `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`, `dynamic_imports`, enriched `calls`/`symbols`/`import_specifiers`/`bindings`/`files` | +| Absent (in-scope gaps) | C.9 only (`files.is_entry`, reachability). Tiers 7–13 out of scope. | Codemap impact on call extraction touch chain: `src/extractors/calls.ts` → `src/parser.ts` → `src/application/index-engine.ts` → `src/db.ts`. From f9377b76a477dc073caf273e627941ddbc8103ef Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:38:12 +0300 Subject: [PATCH 10/23] =?UTF-8?q?chore:=20add=20changeset=20for=20substrat?= =?UTF-8?q?e=20tiers=201=E2=80=936=20(minor)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SCHEMA 27→34; documents new substrate tables, recipes, and C.9 exclusion. --- .changeset/substrate-tiers-1-6.md | 34 +++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .changeset/substrate-tiers-1-6.md diff --git a/.changeset/substrate-tiers-1-6.md b/.changeset/substrate-tiers-1-6.md new file mode 100644 index 00000000..46180c66 --- /dev/null +++ b/.changeset/substrate-tiers-1-6.md @@ -0,0 +1,34 @@ +--- +"@stainless-code/codemap": minor +--- + +Substrate tiers 1–6 remainder (excludes C.9 / `files.is_entry`). **Schema bump** `SCHEMA_VERSION` 27 → **34** — first run after upgrade auto-rebuilds `.codemap/index.db` via the existing version-mismatch path. + +**Tier 1 — call + import precision** + +- `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`; constructor vs call dedup key fix +- `symbols.{return_type,is_async,is_generator}` +- Side-effect `import_specifiers` rows (`kind='side-effect'`) + `import_id` FK to `imports` + +**Tier 2 — bindings** + +- `bindings.resolution_kind='re-exported'` when resolution walks a re-export chain + +**Tier 3 — JSX** + +- New tables `jsx_elements` / `jsx_attributes`; extractor with per-file parent linking post-pass + +**Tier 5 — behavioral** + +- New tables `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`; context stack for `in_loop` / `in_try` + +**Tier 6 — module graph (no entry points)** + +- `dynamic_imports` table + extractor +- Post-pass `files.is_barrel` and parse-time `files.has_side_effects` + +**Recipes + goldens:** `find-call-sites` (extended), `find-async-functions`, `find-dynamic-imports`, `find-barrel-files`, `find-side-effect-files`, `find-re-exported-bindings`, `find-side-effect-imports`, `find-jsx-usages`, `find-await-in-loop`, `find-swallowed-errors`, `find-decorator-usage`, `find-throws-jsdoc`. + +**Out of scope:** C.9 plugin layer (`files.is_entry`, reachability-from-entry); tiers 7–13. + +**Migration:** No in-place DDL — rebuild on schema mismatch preserves user-data tables (`coverage`, `query_baselines`, `recipe_recency`). Re-run `codemap --full` (or any index) after upgrade. From d65d2696e7614901e5f3062152bcb5bd85b47262 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:41:07 +0300 Subject: [PATCH 11/23] =?UTF-8?q?docs:=20retire=20substrate=20tiers=201?= =?UTF-8?q?=E2=80=936=20rollout=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Delete execution plan per docs-governance (shipped content lives in substrate-extraction.md and architecture.md). --- docs/plans/substrate-extraction.md | 10 +- docs/plans/substrate-tiers-1-6-rollout.md | 136 ---------------------- 2 files changed, 5 insertions(+), 141 deletions(-) delete mode 100644 docs/plans/substrate-tiers-1-6-rollout.md diff --git a/docs/plans/substrate-extraction.md b/docs/plans/substrate-extraction.md index 900b2966..4421b8ab 100644 --- a/docs/plans/substrate-extraction.md +++ b/docs/plans/substrate-extraction.md @@ -2,7 +2,7 @@ > **Status:** open · plan iterating in parallel with the broader [`research/codemap-richer-index-synthesis-2026-05.md`](../research/codemap-richer-index-synthesis-2026-05.md) write-engine direction. > -> **Per-tier ship status (fact-checked 2026-05-19):** Tiers **1–6** (remainder without C.9) landed on branch `feat/substrate-tiers-1-6` — `SCHEMA_VERSION` **34**. Tier **1** now includes call-shape columns, side-effect `import_specifiers` + `import_id` FK. Tier **2** adds `bindings.resolution_kind='re-exported'`. Tier **3** (`jsx_elements` / `jsx_attributes`) and Tier **5** (`async_calls` / `try_catch` / `decorators` / `jsdoc_tags`) shipped. Tier **4** partial — `symbols.{return_type,is_async,is_generator}` shipped; `generic_params` / `type_predicates` deferred. Tier **6** partial — `dynamic_imports`, `files.{is_barrel,has_side_effects}` shipped; `files.is_entry` stays gated on [`c9-plugin-layer.md`](./c9-plugin-layer.md). Tiers **7–13** remain open. +> **Per-tier ship status (fact-checked 2026-05-19):** Tiers **1–6** remainder shipped (`SCHEMA_VERSION` **34**). Tier **1**: call-shape columns, side-effect `import_specifiers` + `import_id`. Tier **2**: `bindings.resolution_kind='re-exported'`. Tier **3**: `jsx_elements` / `jsx_attributes`. Tier **5**: `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`. Tier **4** partial: `symbols.{return_type,is_async,is_generator}`; `generic_params` / `type_predicates` deferred. Tier **6** partial: `dynamic_imports`, `files.{is_barrel,has_side_effects}`; `files.is_entry` deferred to [`c9-plugin-layer.md`](./c9-plugin-layer.md). Tiers **7–13** open. > > **Motivator:** Codemap's distinctive value is the SQL-against-structural-index substrate. Per [Moat B](../roadmap.md#moats-load-bearing) — _"Extracted structure ≥ verdicts. Schema breadth is the substrate every recipe layers on."_ — the load-bearing growth axis is **what oxc / Lightning CSS / config loaders give us that the index doesn't yet expose.** Today the schema captures symbols + imports + exports + calls + components + markers + type*members + css*{variables,classes,keyframes} + suppressions. The AST contains roughly 4× more queryable structure that we discard at parse time. This plan enumerates the entire extraction surface — ~13 tiers spanning identifier references, scope graph, binding resolution, JSX, type-system depth, behavioral facts, module-graph topology, CSS rule structure, test-suite metadata, runtime/dev markers, metrics expansion, and ORM/SQL tracking — and sequences them as independent tracer-bullet PRs that compound into a maximal substrate. Once landed, every recipe / write capability discussed in the synthesis doc (and many more) lights up via SQL JOINs alone, with zero engine work. > @@ -193,7 +193,7 @@ Each tier is one tracer-bullet PR: parser visitor change + schema migration + 1- **Goal:** Make `calls` / `exports` / `symbols` / `markers` column-precise; split `imports.specifiers` JSON blob into a typed child table. -**Ship status (fact-checked 2026-05-19):** Tier 1 remainder shipped on `feat/substrate-tiers-1-6` — `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`, side-effect `import_specifiers` rows, and `import_id` FK. Position columns from 2026-05-14 remain. +**Ship status (fact-checked 2026-05-19):** Tier 1 remainder shipped — `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`, side-effect `import_specifiers` rows, and `import_id` FK. Position columns from 2026-05-14 remain. | Slice | Substrate | Flagship recipe | Schema bump | | ----- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------- | @@ -526,7 +526,7 @@ Deferred to a future slice (out of Tier 2 scope): ### Tier 3 — JSX elements + attributes -**Ship status (2026-05-19):** Shipped on `feat/substrate-tiers-1-6`. `jsx_elements` / `jsx_attributes` in [`src/db.ts`](../../src/db.ts); extractor [`src/extractors/jsx.ts`](../../src/extractors/jsx.ts); recipe `find-jsx-usages`. +**Ship status (2026-05-19):** Shipped. `jsx_elements` / `jsx_attributes` in [`src/db.ts`](../../src/db.ts); extractor [`src/extractors/jsx.ts`](../../src/extractors/jsx.ts); recipe `find-jsx-usages`. **Goal:** Every JSX element + every JSX attribute becomes a queryable row with column-precise positions. @@ -684,7 +684,7 @@ New recipe candidates: `swap-positional-to-named-args` (extends `rename-preview` ### Tier 5 — Behavioral facts (async, try/catch, decorators, structured JSDoc) -**Ship status (2026-05-19):** Shipped on `feat/substrate-tiers-1-6`. Tables + [`src/extractors/behavioral.ts`](../../src/extractors/behavioral.ts); recipes `find-await-in-loop`, `find-swallowed-errors`, `find-decorator-usage`, `find-throws-jsdoc`. +**Ship status (2026-05-19):** Shipped. Tables + [`src/extractors/behavioral.ts`](../../src/extractors/behavioral.ts); recipes `find-await-in-loop`, `find-swallowed-errors`, `find-decorator-usage`, `find-throws-jsdoc`. **Goal:** Capture runtime-shape behavioral facts the AST encodes but today's index discards. @@ -789,7 +789,7 @@ New recipe candidates: `find-awaits-in-loops`; `find-empty-catches`; `find-depre ### Tier 6 — Module-graph enrichment — **PARTIAL (2026-05-19)** -**Ship status (2026-05-19):** `re_export_chains`, `dynamic_imports`, `files.{is_barrel,has_side_effects}` shipped on `feat/substrate-tiers-1-6`. **Deferred:** `files.is_entry` (C.9 / [`c9-plugin-layer.md`](./c9-plugin-layer.md)). +**Ship status (2026-05-19):** `re_export_chains`, `dynamic_imports`, `files.{is_barrel,has_side_effects}` shipped. **Deferred:** `files.is_entry` ([`c9-plugin-layer.md`](./c9-plugin-layer.md)). **Goal:** Flatten re-export chains; record dynamic imports; mark barrel files. diff --git a/docs/plans/substrate-tiers-1-6-rollout.md b/docs/plans/substrate-tiers-1-6-rollout.md deleted file mode 100644 index 86d2aada..00000000 --- a/docs/plans/substrate-tiers-1-6-rollout.md +++ /dev/null @@ -1,136 +0,0 @@ -# Substrate tiers 1–6 rollout (without C.9) - -> **Status:** Complete — implemented on `feat/substrate-tiers-1-6` (8 commits, `SCHEMA_VERSION` 27→**34**). C.9 explicitly excluded. Push + draft PR when ready (body below). -> **Parent plan:** [`substrate-extraction.md`](./substrate-extraction.md) (tiers 7–13 out of scope here). -> **Explicit exclusion:** C.9 plugin layer — no `files.is_entry`, no reachability-from-entry, no framework entry hints. See [`c9-plugin-layer.md`](./c9-plugin-layer.md). - -## Baseline (codemap-validated 2026-05-19) - -Reindexed self (`bun src/index.ts --full`); `validate --json` → `[]`. - -| Fact | Value | -| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `SCHEMA_VERSION` | **34** ([`src/db.ts`](../../src/db.ts)) | -| Extractor orchestration | [`src/parser.ts`](../../src/parser.ts) `EXTRACTORS[]` — … `referencesExtractor` → `jsxExtractor` → `dynamicImportsExtractor` → `behavioralExtractor` → `moduleSideEffectsExtractor` → … | -| Post-passes (index-engine) | bindings, re-export chains, module cycles, JSX parent links (per-file insert), decorator/jsdoc symbol linking | -| Live substrate tables | All tier 1–6 rollout tables including `jsx_*`, `async_calls`, `try_catch`, `decorators`, `jsdoc_tags`, `dynamic_imports`, enriched `calls`/`symbols`/`import_specifiers`/`bindings`/`files` | -| Absent (in-scope gaps) | C.9 only (`files.is_entry`, reachability). Tiers 7–13 out of scope. | - -Codemap impact on call extraction touch chain: `src/extractors/calls.ts` → `src/parser.ts` → `src/application/index-engine.ts` → `src/db.ts`. - -## Scope summary - -| Tier | Shipped today | This rollout closes | -| ----- | --------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- | -| **1** | Positions on `calls`/`exports`/`symbols`/`markers`; `import_specifiers` (no `import_id`; side-effect imports skipped) | `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`; optional slice 1.B: side-effect import rows + `import_id` FK | -| **2** | `references`/`scopes`/`bindings`/`function_params`/`re_export_chains`; narrowed kinds | `resolution_kind='re-exported'` in bindings pass; optional reference-kind expansion deferred | -| **3** | Component heuristic only (`components` table) | `jsx_elements` + `jsx_attributes` (new extractor module) | -| **4** | `function_params` (owner-keyed, not `symbol_id` FK) | `symbols.{return_type,is_async,is_generator}`; defer `generic_params`/`type_predicates`/`throws_clauses` tables | -| **5** | Nothing | Tracer bullets: `async_calls` → `try_catch` → `decorators` → `jsdoc_tags` | -| **6** | `re_export_chains`, `module_cycles` | `dynamic_imports`; `files.is_barrel`; `files.has_side_effects` (heuristic — no Tier 8 `package.json` yet). **Not** `files.is_entry`. | - -## Pre-locked decisions (inherited) - -- [R.16](./substrate-extraction.md#pre-locked-decisions): rebuild-forcing DDL → bump `SCHEMA_VERSION`; no in-place migrations. -- [R.18](./substrate-extraction.md#pre-locked-decisions): each slice ships ≥1 recipe + golden fixture update. -- [Tracer bullets](../../.agents/rules/tracer-bullets.md): vertical slice per commit; verify before next slice. - -## Execution order - -Priority = dependency order + reviewability. Each row is one commit (or commit pair: schema + tests). - -### Phase A — Low-risk column extensions (Tier 1 + 4) - -| Slice | Work | SCHEMA | Flagship recipe / golden | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| **A.1** | `calls` metadata in [`calls.ts`](../../src/extractors/calls.ts): `args_count` (NULL if spread), `is_method_call`, `is_constructor_call` (`NewExpression`), `is_optional_chain` (`node.optional` / callee.optional); dedup key includes call vs `new` | 27→**28** | Extend [`find-call-sites`](../../templates/recipes/find-call-sites.sql) SELECT; update [`fixtures/golden/minimal/find-call-sites.json`](../../fixtures/golden/minimal/find-call-sites.json) | -| **A.2** | `symbols.{return_type,is_async,is_generator}` in [`symbols.ts`](../../src/extractors/symbols.ts) + [`type-stringify.ts`](../../src/extractors/type-stringify.ts); function-shaped kinds only | **29** | New recipe `find-async-functions` + golden | -| **A.3** _(optional)_ | Side-effect import specifier row (`kind='side-effect'`) + `import_specifiers.import_id` FK | **30** | Extend `find-import-sites` or new `find-side-effect-imports` | - -**A.1 validation queries (post-index):** - -```sql -SELECT callee_name, args_count, is_method_call, is_constructor_call, is_optional_chain -FROM calls WHERE file_path = 'src/extractors/calls.ts' LIMIT 5; - -SELECT COUNT(*) FROM calls WHERE is_method_call = 1; -SELECT COUNT(*) FROM calls WHERE is_constructor_call = 1; -``` - -### Phase B — Module graph remainder (Tier 6, no C.9) - -| Slice | Work | SCHEMA | Flagship | -| ------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------- | -| **B.1** | `dynamic_imports` table + visitor on `ImportExpression` in new [`src/extractors/dynamic-imports.ts`](../../src/extractors/dynamic-imports.ts) | **31** | `find-dynamic-imports` + golden | -| **B.2** | Post-pass `files.is_barrel` (100% re-exports, no value defs) in [`index-engine.ts`](../../src/application/index-engine.ts) | **32** | Extend [`barrel-files`](../../fixtures/golden/minimal/barrel-files.json) scenario or recipe | -| **B.3** | `files.has_side_effects` — top-level call/assign heuristic (Tier 8 `package.json` deferred) | **32** or same commit as B.2 | `find-side-effect-files` | - -### Phase C — Bindings polish (Tier 2) - -| Slice | Work | SCHEMA | Flagship | -| ------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------- | -| **C.1** | Add `'re-exported'` to `bindings.resolution_kind` CHECK + walk in [`bindings-engine.ts`](../../src/application/bindings-engine.ts) | **33** | Extend rename-preview / find-references binding filter in golden | - -### Phase D — JSX substrate (Tier 3) - -| Slice | Work | SCHEMA | Flagship | -| ------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------- | -| **D.1** | `jsx_elements` table + tracer: self-closing + simple opening tags; `parent_element_id` post-link pass per [Q4](./substrate-extraction.md) | **34** | `find-jsx-usages` + golden on `fixtures/minimal` TSX | -| **D.2** | `jsx_attributes` + fragment rows (`is_fragment=1`) | **35** | Extend recipe with attribute JOIN | - -New extractor registers after `referencesExtractor` (needs scope + refs). Does not replace [`componentsExtractor`](../../src/extractors/components.ts) heuristic. - -### Phase E — Behavioral (Tier 5) - -| Slice | Work | SCHEMA | Flagship | -| ------- | ----------------------------------------------------------------- | ------ | -------------------------------------------------------------- | -| **E.1** | `async_calls` + context stack (`in_loop`, `in_try`) | **36** | `find-unawaited-async-calls` (stretch) or `find-await-in-loop` | -| **E.2** | `try_catch` | **37** | `find-swallowed-errors` | -| **E.3** | `decorators` | **38** | `find-decorator-usage` | -| **E.4** | `jsdoc_tags` — extend [`jsdoc.ts`](../../src/extractors/jsdoc.ts) | **39** | `find-throws-jsdoc` / param tag queries | - -Tier 5 slices depend on Tier 2 scopes (already shipped). `jsdoc_tags` can reuse existing `@deprecated` / visibility parsing. - -## Per-slice Definition of Done - -1. DDL + row types + `insert*` in [`src/db.ts`](../../src/db.ts). -2. Extractor or post-pass wired through [`parser.ts`](../../src/parser.ts) / [`index-engine.ts`](../../src/application/index-engine.ts). -3. Unit tests in [`src/parser.test.ts`](../../src/parser.test.ts) or dedicated `*.test.ts`. -4. Recipe + golden update when user-visible query shape changes. -5. [`docs/architecture.md`](../architecture.md) schema table row(s) for new columns/tables. -6. Checks: `bun run format:check`, `lint`, `typecheck`, affected `bun test`, `bun run test:golden` when golden touched. -7. Re-index before codemap validation queries. - -## Draft PR (push when ready) - -```markdown -## Summary - -- Rollout plan for substrate tiers 1–6 remainder (excludes C.9 / `files.is_entry`). -- Tracer-bullet implementation: call-shape metadata on `calls`, then symbol async/return-type columns, module-graph enrichment, JSX, behavioral tables. - -## Test plan - -- [ ] `bun run check` -- [ ] `bun run test:golden` -- [ ] `bun src/index.ts --full` on self + `validate --json` -- [ ] Spot-check flagship recipes per shipped slice - -## Out of scope - -- C.9 plugin layer / `files.is_entry` / reachability pruning -- Tiers 7–13 (CSS rich, project meta, ORM, …) -``` - -Push: `git push -u origin feat/substrate-tiers-1-6 && gh pr create --draft --title "feat: substrate tiers 1–6 (no C.9)" --body-file …` - -## Risk notes - -- **Dedup semantics:** `calls` dedupes per `(caller_scope, callee)` today; constructor vs call with same name needs distinct dedup keys (A.1). -- **Spread args:** `args_count = NULL` when any argument is `SpreadElement` (substrate Tier 1 open question — bias adopted). -- **Barrel detection:** must not flag files with mixed re-exports and local value symbols. -- **JSX scope:** largest slice; keep D.1 minimal (no attribute values, no conditional render analysis). - -## Closing - -When all scoped slices ship: update [`substrate-extraction.md`](./substrate-extraction.md) per-tier ship status; lift completed items from [`roadmap.md`](../roadmap.md); close this plan (delete + lift per docs-governance). From 1b5c1dc190eb4e83ef951e93e314b7c01749bc67 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:46:08 +0300 Subject: [PATCH 12/23] =?UTF-8?q?docs:=20align=20glossary=20and=20ship=20s?= =?UTF-8?q?tatus=20with=20substrate=20tiers=201=E2=80=936?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rule 9 glossary entries and stale deferred text in substrate-extraction, research synthesis, and roadmap; trim restating SymbolRow JSDoc per concise-comments. --- docs/glossary.md | 44 ++++++++++++++++--- docs/plans/substrate-extraction.md | 18 ++++---- .../codemap-richer-index-synthesis-2026-05.md | 9 ++-- docs/roadmap.md | 2 +- src/db.ts | 8 ++-- src/extractors/module-side-effects.ts | 2 +- 6 files changed, 57 insertions(+), 26 deletions(-) diff --git a/docs/glossary.md b/docs/glossary.md index b8e26adf..91301ee1 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -43,13 +43,17 @@ Two-snapshot structural-drift command: `codemap audit` diffs the live `.codemap/ Ad-hoc audit snapshot from any git committish (`origin/main`, `HEAD~5`, ``, tag, …). `git archive --format=tar ` is piped through `tar -x` into `/.codemap/audit-cache//` — a plain extracted tree with no `.git` artifact and no registered git worktree, so `git clean -xdf` and `rm -rf` both sweep it without flag escalation. Codemap reindexes into the cache's `.codemap/index.db`, then per-delta canonical SQL runs on that DB vs the live one. Cache key is the **resolved sha** (`git rev-parse --verify`), so `--base origin/main` and `--base ` (when they point at the same commit) share one cache entry. **Atomic populate** — per-pid temp dir + POSIX `rename`; concurrent processes resolving the same sha race-safely without lock files. Eviction: hardcoded LRU 5 entries / 500 MiB. Per-delta `base.source` is `"ref"` (vs `"baseline"`) and the delta carries `base.ref` (user-supplied string) + `base.sha` (resolved). Mutually exclusive with `--baseline `; composes orthogonally with per-delta `---baseline ` overrides. Hard error on non-git projects (no graceful fallback — there's no meaningful "ref" without git). Both transports (MCP `audit` tool's `base?` arg, HTTP `POST /tool/audit`) call the same `runAuditFromRef` engine in `application/audit-engine.ts`. +### `async_calls` (table) + +One row per `AwaitExpression`. Captures `awaited_expression` source text, optional `awaited_callee_name`, and loop/try context flags (`in_loop`, `in_try`) from a visitor context stack. Joins `scopes.local_id` via `scope_local_id`. Powers `find-await-in-loop`. + --- ## B ### barrel file -In Codemap usage: a file with a high number of `exports` rows — typically a public-API hub like `src/index.ts`. Surfaced by the `barrel-files` recipe. Distinct from **hub** below — barrel measures _exports out_, hub measures _imports in_. +In Codemap usage: a file whose exports are entirely re-exports with no local value symbols — surfaced as `files.is_barrel = 1` (post-pass) and by the `barrel-files` / `find-barrel-files` recipes. Distinct from **hub** below — barrel measures _exports out_, hub measures _imports in_. ### batch insert @@ -57,7 +61,7 @@ The shared `batchInsert()` helper in `src/db.ts`. Splits inserts into multi-r ### `bindings` (table) / bindings resolver -Per-reference resolution to the originating symbol per [R.12]. One row per non-`member`-kind `references` row, with `resolution_kind` in `{same-file, imported, global, unresolved}` and a nullable `resolved_symbol_id` joining `symbols(id)`. Resolved in a single in-memory pass (`src/application/bindings-engine.ts`) after files/symbols/imports settle. Full-rebuild only — targeted reindex skips per [R.10]. Powers `find-symbol-references` (bindings-precise rename substrate). +Per-reference resolution to the originating symbol per [R.12]. One row per non-`member`-kind `references` row, with `resolution_kind` in `{same-file, imported, re-exported, global, unresolved}` and a nullable `resolved_symbol_id` joining `symbols(id)`. Resolved in a single in-memory pass (`src/application/bindings-engine.ts`) after files/symbols/imports settle. Full-rebuild only — targeted reindex skips per [R.10]. Powers `find-symbol-references` and `find-re-exported-bindings`. ### `boundaries` (config) / `boundary_rules` (table) / `boundary-violations` (recipe) @@ -77,7 +81,7 @@ Synchronous Node.js SQLite binding. The Node-side counterpart to `bun:sqlite`. A ### `calls` (table) -Function-scoped call edges, deduped per `(caller_scope, callee_name)` per file. **`caller_scope`** is dot-joined enclosing scope (e.g. `UserService.run`). Module-level calls are excluded. See `CallRow` for the TS shape. +Function-scoped call edges, deduped per `(caller_scope, callee_name, call_kind)` per file (`call` vs `new`). **`caller_scope`** is dot-joined enclosing scope (e.g. `UserService.run`). Module-level calls are excluded. Columns include `args_count` (NULL when a spread argument is present), `is_method_call`, `is_constructor_call`, and `is_optional_chain`. See `CallRow`. ### `CallRow` @@ -183,6 +187,10 @@ CSS custom properties (`--token: value`). `scope` is `:root`, `@theme` (Tailwind ## D +### `decorators` (table) + +One row per `@decorator` site on classes, methods, properties, or accessors. `target_symbol_id` links to `symbols(id)` post-insert when resolvable. Powers `find-decorator-usage`. + ### DDL Data Definition Language — the `CREATE TABLE` / `CREATE INDEX` strings in `src/db.ts`. Distinct from **schema** (the conceptual structure) and from **`SCHEMA_VERSION`** (the integer that triggers auto-rebuild on mismatch). @@ -195,6 +203,10 @@ Resolved file-to-file edges derived from `imports.resolved_path`. Composite prim TS shape for one row of the `dependencies` table. +### `dynamic_imports` (table) + +One row per `import()` expression. `source_kind` is `literal` / `template` / `expression`; `resolved_path` filled when literal and resolvable. `in_async_fn` flags dynamic imports inside async functions. Powers `find-dynamic-imports`. + --- ## E @@ -225,7 +237,7 @@ Number of edges _out of_ a file — `COUNT(*) FROM dependencies WHERE from_path ### `files` (table) -Header row for every indexed file. `path` is the primary key; all other tables FK to it with `ON DELETE CASCADE`. See `FileRow`. +Header row for every indexed file. `path` is the primary key; all other tables FK to it with `ON DELETE CASCADE`. Flags: `is_barrel` (100% re-exports, no local value symbols) and `has_side_effects` (module-level call/assignment seen at parse time). See `FileRow`. ### `FileRow` @@ -285,7 +297,7 @@ Symbol or file blast-radius walker. CLI: `codemap impact [--direction u ### `import_specifiers` (table) -Per-specifier breakdown of the `imports.specifiers` JSON blob. One row per imported binding — `imported_name` (original) and `local_name` (renamed via `as`), `kind` in `{named, default, namespace}`, `is_type_only`, column-precise position. Powers specifier-precise rewrites and the `find-import-sites` recipe. +Per-specifier breakdown of the `imports.specifiers` JSON blob. One row per imported binding — or one `kind='side-effect'` row for bare `import "mod"`. Columns include `imported_name`, `local_name`, `kind` in `{named, default, namespace, side-effect}`, `is_type_only`, column-precise position, and `import_id` FK to `imports`. Powers `find-import-sites` and `find-side-effect-imports`. ### `imports` (table) @@ -305,6 +317,22 @@ Public TS shape returned from `Codemap#index()` and `runCodemapIndex()`. Wall-cl --- +## J + +### `jsdoc_tags` (table) + +Structured JSDoc tags parsed from `symbols.doc_comment` — one row per tag per symbol (`@param`, `@throws`, `@returns`, …). FK `symbol_id` → `symbols(id)`. Powers `find-throws-jsdoc`. + +### `jsx_attributes` (table) + +One row per JSX attribute on a `jsx_elements` row. `value_kind` in `{string, expression, boolean, spread, element}`. Join parent via `element_id`. + +### `jsx_elements` (table) + +One row per JSX element or fragment in `.tsx`/`.jsx` files. `parent_element_id` linked in a per-file post-insert pass. `is_fragment = 1` for `<>…`. Powers `find-jsx-usages`. + +--- + ## L ### language adapter @@ -520,7 +548,7 @@ SQLite per-table option enforcing column types at insert time. Every Codemap tab ### `symbols` (table) -Functions / consts / classes / interfaces / types / enums, plus class members (`method`, `property`, `getter`, `setter`). Class members carry `parent_name`. JSDoc tags in `doc_comment` power the `deprecated-symbols` and `visibility-tags` recipes; `members` is JSON for enums. See `SymbolRow`. +Functions / consts / classes / interfaces / types / enums, plus class members (`method`, `property`, `getter`, `setter`). Class members carry `parent_name`. Function-shaped rows may carry `return_type`, `is_async`, and `is_generator`. JSDoc tags in `doc_comment` power the `deprecated-symbols` and `visibility-tags` recipes; structured tags also land in `jsdoc_tags`. `members` is JSON for enums. See `SymbolRow`. ### `SymbolRow` @@ -530,6 +558,10 @@ TS shape for one row of the `symbols` table. ## T +### `try_catch` (table) + +One row per `TryStatement`. Heuristic flags include `catch_logs_only` (catch body only calls `console.*`) and `catch_rethrows`. Powers `find-swallowed-errors`. + ### `test_suites` (table) Test metadata — describe / it / test / suite / context blocks with skip/only/todo flags and detected `framework` in `{vitest, jest, bun-test, node-test, mocha, unknown}`. Framework detection is per-file from imports (mixed-framework codebases handled automatically). `parent_suite_id` resolves nested describes. Powers `find-skipped-tests` + `tests-by-file` recipes. diff --git a/docs/plans/substrate-extraction.md b/docs/plans/substrate-extraction.md index 4421b8ab..724de77f 100644 --- a/docs/plans/substrate-extraction.md +++ b/docs/plans/substrate-extraction.md @@ -43,7 +43,7 @@ These commit before any PR opens. Questions opened against them must justify aga | R.8 | **No JS execution at extract time.** oxc parses; we walk; we record. Same floor as today's index. No `eval`, no dynamic resolution, no LLM in the box. | [Floors "No JS execution at index time"](../roadmap.md#floors-v1-product-shape) | | R.9 | **No hard size ceiling; soft warn at >5× DB growth.** Empirical measurement on four real fixtures with a minimal `references`-only probe (one of the heaviest single tiers in isolation) showed consistent ~3.6-4.5× DB growth at one tier. Projecting all 13 tiers conservatively: ~5-10× growth. SQLite handles 200-500 MB DBs trivially. Users hitting pain on large monorepos opt out of expensive tiers via R.3 — that's the safety valve, not a global ceiling. | Measured 2026-05-14, four fixtures spanning ~900-2,100 files (see § Operational considerations § Index size growth) | | R.10 | **Latency budget tied to user-visible operations, not DB size.** Soft warn when full reindex > 30s OR targeted reindex > 500ms. Measured worst-case (one tier, largest fixture ~2,100 files / 28k symbols): full ~1.9s, targeted ~15ms. Both ~10-60× under the user-stated bottleneck threshold (1 min full / sub-second targeted). Full 13-tier projection still well under budget. | Measured 2026-05-14 (see § Operational considerations § Reindex performance) | -| R.11 | **Hand-rolled scope walker in the existing oxc visitor.** No library dep. `oxc-parser` explicitly doesn't construct scopes; no NAPI binding for `oxc-semantic` yet. Existing `scopeStack` in `parser.ts` (used for cyclomatic complexity + call-site scope) extends to a full scope graph. Edge cases (TS namespace merge, declaration hoisting, TDZ) handled conservatively. **Status 2026-05-18:** the shipped `bindings.resolution_kind` enum is `same-file` / `imported` / `global` / `unresolved`; the originally proposed `ambiguous` escape valve did not ship. | oxc-parser's `showSemanticErrors` doc explicitly says "the parser does not construct symbols and scopes"; existing `scopeStack` infrastructure in `parser.ts` | +| R.11 | **Hand-rolled scope walker in the existing oxc visitor.** No library dep. `oxc-parser` explicitly doesn't construct scopes; no NAPI binding for `oxc-semantic` yet. Existing `scopeStack` in `parser.ts` (used for cyclomatic complexity + call-site scope) extends to a full scope graph. Edge cases (TS namespace merge, declaration hoisting, TDZ) handled conservatively. **Status 2026-05-19:** the shipped `bindings.resolution_kind` enum is `same-file` / `imported` / `re-exported` / `global` / `unresolved`; the originally proposed `ambiguous` escape valve did not ship. | oxc-parser's `showSemanticErrors` doc explicitly says "the parser does not construct symbols and scopes"; existing `scopeStack` infrastructure in `parser.ts` | | R.12 | **Pre-resolve `bindings` at index time (two-pass).** Pass 1 (per file, in worker): extract refs, scopes, local declarations. Pass 2 (main thread, after all files parsed): walk `references` rows; resolve via same-file scope-walk → `imports` → `exports` → re-export chain; populate `bindings`. Same architecture as today's `resolver.ts` two-pass for `dependencies`. Cost: ~25-50% on top of refs-only reindex (projected worst case ~3-4s full on the largest fixture; well under R.10 budget). Recipes get a single-JOIN `bindings → symbols` instead of recursive-CTE-per-recipe. R.4 cascade extends: single-file reindex deletes that file's `bindings` rows AND any binding referencing symbols in that file. | Existing `resolver.ts` two-pass pattern; `dependencies` table as precedent | | R.13 | **`references.is_write` distinguishes reads from writes.** Boolean column populated by parent-node-shape check during the visitor pass (`AssignmentExpression.left`, `UpdateExpression`, `delete`, `AssignmentPattern`, `VariableDeclarator.id` with initializer, `ForOfStatement.left`, `ForInStatement.left`). Compound assignment (`x += 1`) emits TWO `references` rows — one with `is_write = 0` (the read) and one with `is_write = 1` (the write) — at the same `(file_path, line_start, column_start)`. Substrate honesty: recipes that want a single-row-per-position can `SELECT DISTINCT`. Unlocks immutability audits, side-effect detection, cross-file mutation tracking. | Cost trivial (one column + ~10 lines of visitor logic); recipe-unlock substantial (no other way to express "find writes to X" without external AST walk) | | R.14 | **FTS5 stays file-content-only.** New substrate tables (`references`, `jsx_elements`, `function_params`, `decorators`, `test_suites`, …) are NOT indexed via FTS5 by default. Every `name` / identifier column gets a regular B-tree index, which covers exact match + anchored prefix (`LIKE 'use%'` / `GLOB 'use*'`) at O(log N). FTS5 only helps unanchored substring search; the row counts at every tier remain small enough (~10-500k) that an unanchored `LIKE '%foo%'` scan still completes in tens of milliseconds. Cost saved: ~25-90 MB of FTS5 storage per project across all 13 tiers. Per-tier opt-in path: a tier PR can add FTS5 on its own table when a concrete recipe requires unanchored search — schema-additive, no breaking change. | Existing `source_fts` keeps its current shape (file-content full-text); empirical row-count + B-tree-index-perf argument; substrate stays lean | @@ -195,12 +195,12 @@ Each tier is one tracer-bullet PR: parser visitor change + schema migration + 1- **Ship status (fact-checked 2026-05-19):** Tier 1 remainder shipped — `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}`, side-effect `import_specifiers` rows, and `import_id` FK. Position columns from 2026-05-14 remain. -| Slice | Substrate | Flagship recipe | Schema bump | -| ----- | -------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ----------- | -| 1.A | `calls.{line_start, column_start, column_end}` + `idx_calls_position`; proposed call flags deferred | `find-call-sites` (`--params callee=…`) | 10 → 11 | -| 1.B | `exports.{line_start, line_end, column_start, column_end, is_re_export}` + 2 indexes | `find-export-sites` (`--params name=…`) | 11 → 12 | -| 1.C | `symbols.{name_column_start, name_column_end}` + `markers.{column_start, column_end}` | `find-symbol-definitions` (`--params name=…`) | 12 → 13 | -| 1.D | `import_specifiers` child table (file_path, source, line, column_start/end, imported_name, local_name, kind, is_type_only) + 4 indexes | `find-import-sites` (`--params imported_name=…`) | 13 → 14 | +| Slice | Substrate | Flagship recipe | Schema bump | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------ | ----------- | +| 1.A | `calls.{line_start, column_start, column_end}` + `idx_calls_position`; call-shape flags `{args_count,is_method_call,is_constructor_call,is_optional_chain}` shipped 2026-05-19 | `find-call-sites` (`--params callee=…`) | 10 → 11 | +| 1.B | `exports.{line_start, line_end, column_start, column_end, is_re_export}` + 2 indexes | `find-export-sites` (`--params name=…`) | 11 → 12 | +| 1.C | `symbols.{name_column_start, name_column_end}` + `markers.{column_start, column_end}` | `find-symbol-definitions` (`--params name=…`) | 12 → 13 | +| 1.D | `import_specifiers` child table (file_path, source, line, column_start/end, imported_name, local_name, kind, is_type_only) + 4 indexes | `find-import-sites` (`--params imported_name=…`) | 13 → 14 | **Empirical post-Tier-1 cost** (clean rebuild, median of 3 runs): @@ -419,7 +419,7 @@ Both numbers are well within the plan's thresholds (full < 1 min, targeted < 100 What landed (SCHEMA_VERSION 16 → 17): -- **`bindings` table** — `(reference_id, resolved_symbol_id, resolution_kind, is_external)`. PK on `reference_id`. `resolution_kind` enum: `same-file` / `imported` / `global` / `unresolved`. `re-exported` deferred to Tier 6. +- **`bindings` table** — `(reference_id, resolved_symbol_id, resolution_kind, is_external)`. PK on `reference_id`. `resolution_kind` enum: `same-file` / `imported` / `global` / `unresolved` at initial ship; `re-exported` shipped 2026-05-19 (Tier 6 rollout). - **`symbols.scope_local_id`** column — captured BEFORE the symbol's own scope is pushed (so it points at the declaring scope, not the body). Class members anchor to their class's pushed scope. - **`resolveBindings`** engine (`src/application/bindings-engine.ts`) — two-phase: one SELECT per table into in-memory Maps, then per-reference resolution via scope-walk → imports → globals → unresolved. ~300ms for ~127k refs. - **Cross-file resolution** uses `imports.resolved_path` (not `dependencies`, which lacks the module specifier). When `imp.imported_name` matches an export and the target file has a module-scope symbol of the same name → `is_external=0` with a real symbol id. Non-indexed module → `is_external=1`. @@ -602,7 +602,7 @@ New recipe candidates: `rename-component` (alongside `rename-app-wide`); `migrat ### Tier 4 — Type / signature depth (params, generics, predicates) — **PARTIAL (2026-05-15)** -**Ship status (2026-05-15):** `function_params` table shipped via Tier 2.2 (different keying — `(file_path, owner_name, owner_kind, position)` instead of `symbol_id`-FK; columns `name` / `type_text` / `default_text` / `is_rest` / `is_optional` + position triplet). Params also emit as `symbols` rows with `kind='param'` so cross-file binding resolution works. **Deferred:** `generic_params` table (type-params currently emit as `symbols.kind='type-param'` instead — adequate for binding resolution; structured constraint/default columns deferred); `type_predicates` table; `symbols.{return_type, is_async, is_generator, throws_clauses}` columns. Recipes that need per-param JOINs work today against the shipped `function_params`; recipes needing predicates / async / return-type / generics-with-constraints stay open. +**Ship status (2026-05-19):** `function_params` table shipped via Tier 2.2 (different keying — `(file_path, owner_name, owner_kind, position)` instead of `symbol_id`-FK; columns `name` / `type_text` / `default_text` / `is_rest` / `is_optional` + position triplet). Params also emit as `symbols` rows with `kind='param'` so cross-file binding resolution works. **`symbols.{return_type,is_async,is_generator}`** shipped 2026-05-19. **Deferred:** `generic_params` table (type-params currently emit as `symbols.kind='type-param'` instead — adequate for binding resolution; structured constraint/default columns deferred); `type_predicates` table; `symbols.throws_clauses`. Recipes that need per-param JOINs work today against the shipped `function_params`; recipes needing predicates / generics-with-constraints stay open. **Goal:** Function parameters + generic parameters + type predicates + return types become structured queryable facts, not just stringified into `symbols.signature`. diff --git a/docs/research/codemap-richer-index-synthesis-2026-05.md b/docs/research/codemap-richer-index-synthesis-2026-05.md index 8f052b9e..9c3e97f4 100644 --- a/docs/research/codemap-richer-index-synthesis-2026-05.md +++ b/docs/research/codemap-richer-index-synthesis-2026-05.md @@ -16,13 +16,14 @@ The substrate-growth half of this synthesis lifted to a dedicated plan PR — [` **Shipped via the substrate plan:** -- **Synthesis Step 5** (`calls.{line_start, column_start, column_end}`) — shipped as substrate plan **Tier 1 Slice 1.A**. Proposed call metadata (`args_count`, `is_method_call`, `is_constructor_call`, `is_optional_chain`) is not in the live schema. +- **Synthesis Step 5** (`calls.{line_start, column_start, column_end}`) — shipped as substrate plan **Tier 1 Slice 1.A**. Call metadata (`args_count`, `is_method_call`, `is_constructor_call`, `is_optional_chain`) shipped 2026-05-19. - **Synthesis Step 7** (`exports.{line_start, line_end, column_start, column_end, is_re_export}`) — shipped as substrate plan **Tier 1 Slice 1.B**. - **§ 4.1 column anchoring on `symbols` / `imports` / `markers`** ("Deferred (incremental)") — shipped as substrate plan **Tier 1 Slices 1.C / 1.D**. -- **§ 4.2 `import_specifiers` child table** — shipped as substrate plan **Tier 1 Slice 1.D**, with the live schema keyed by `file_path`/`source` rather than the originally proposed `import_id` FK. -- **§ 4.2 generalised `references` + `scopes` + `bindings` + `symbol_namespace`** ("Deferred — defer until ≥3 narrower position tables prove demand") — the trigger fired with Tier 1 landing four position-precise surfaces; lifted to substrate plan **Tier 2** and shipped in narrowed form. Live schema has `references.kind IN ('value','type','jsx','member')` and `bindings.resolution_kind IN ('same-file','imported','global','unresolved')`; richer namespaces / re-export resolution kinds remain deferred. +- **§ 4.2 `import_specifiers` child table** — shipped as substrate plan **Tier 1 Slice 1.D**, including `import_id` FK and `kind='side-effect'` rows (2026-05-19). +- **§ 4.2 generalised `references` + `scopes` + `bindings` + `symbol_namespace`** ("Deferred — defer until ≥3 narrower position tables prove demand") — the trigger fired with Tier 1 landing four position-precise surfaces; lifted to substrate plan **Tier 2** and shipped in narrowed form. Live schema has `references.kind IN ('value','type','jsx','member')` and `bindings.resolution_kind IN ('same-file','imported','re-exported','global','unresolved')`; richer `bindings.namespace` remains deferred. - **§ 5.3 leverage-ranked items 1 + 3** — shipped via Tiers 1 + 2. -- **Partial ship** of substrate plan Tiers 4 / 6 / 9 / 10 / 11 / 12 — foundation tables landed (`function_params` / `re_export_chains` / `test_suites` / `runtime_markers` / `file_metrics` / `module_cycles`); deferred bits stay tracked under each tier's heading. +- **Substrate plan Tiers 1–6 (2026-05-19)** — remainder shipped: JSX (`jsx_elements` / `jsx_attributes`), behavioral (`async_calls`, `try_catch`, `decorators`, `jsdoc_tags`), `symbols.{return_type,is_async,is_generator}`, `dynamic_imports`, `files.{is_barrel,has_side_effects}`. **`files.is_entry`** deferred to [`plans/c9-plugin-layer.md`](../plans/c9-plugin-layer.md). +- **Partial ship** of substrate plan Tiers 9 / 10 / 11 / 12 — foundation tables landed (`test_suites` / `runtime_markers` / `file_metrics` / `module_cycles`); deferred bits stay tracked under each tier's heading. Tier 4 partial: `function_params` shipped; `generic_params` / `type_predicates` deferred. **Still open (the apply-engine half of this synthesis):** diff --git a/docs/roadmap.md b/docs/roadmap.md index 9841bb59..037de3db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -29,7 +29,7 @@ Codemap stays a structural-index primitive that other tools can consume. Two lay Every PR reviewer defends these. The reviewer tests embedded below are the canonical filters for any new verb / column / engine. - **A. SQL is the API.** Every capability is a recipe (saved query) or a primitive recipes can compose — never a pre-baked verdict. SQL is a durable, well-known query language; agents compose any predicate without us deciding which questions are important. The moment a CLI verb returns `pass`/`fail` _without_ a recipe form behind it, the moat erodes — the tool becomes "yet another linter with opinions baked in" instead of "the database your agent queries." **Verdicts are an OUTPUT mode** (e.g. `--format sarif`, `audit --base ` deltas), never a primitive. **Reviewer test for any new verb:** "is this also expressible as `query --recipe `?" -- **B. Extracted structure ≥ verdicts.** Schema breadth is the substrate every recipe layers on. CSS (`css_variables` / `css_classes` / `css_keyframes`), `markers`, `type_members`, `calls.caller_scope`, `components.hooks_used`, the substrate-extraction tier (`scopes` / `references` / `bindings` / `function_params` / `runtime_markers` / `test_suites` / `re_export_chains` / `module_cycles` / `file_metrics` / `import_specifiers`) — these are codemap-specific extractions; their richness directly determines what JOINs are expressible and which agent questions get clean answers. Slimming the schema for theoretical perf / simplicity is a regression unless the column is empirically unread. **Reviewer test for any "drop column X" PR:** "what recipe (bundled or hypothetical) does this kill?" +- **B. Extracted structure ≥ verdicts.** Schema breadth is the substrate every recipe layers on. CSS (`css_variables` / `css_classes` / `css_keyframes`), `markers`, `type_members`, `calls.caller_scope`, `components.hooks_used`, the substrate-extraction tier (`scopes` / `references` / `bindings` / `function_params` / `runtime_markers` / `test_suites` / `re_export_chains` / `module_cycles` / `file_metrics` / `import_specifiers` / `jsx_elements` / `jsx_attributes` / `async_calls` / `try_catch` / `decorators` / `jsdoc_tags` / `dynamic_imports`) — these are codemap-specific extractions; their richness directly determines what JOINs are expressible and which agent questions get clean answers. Slimming the schema for theoretical perf / simplicity is a regression unless the column is empirically unread. **Reviewer test for any "drop column X" PR:** "what recipe (bundled or hypothetical) does this kill?" ### Floors (v1 product-shape) diff --git a/src/db.ts b/src/db.ts index 23c25ba5..164cb88f 100644 --- a/src/db.ts +++ b/src/db.ts @@ -868,9 +868,7 @@ export interface SymbolRow { nesting_depth?: number | null; /** Stringified return type for function-shaped symbols; NULL when unannotated or N/A. */ return_type?: string | null; - /** 1 for async function-shaped symbols. */ is_async?: number; - /** 1 for generator function-shaped symbols. */ is_generator?: number; } @@ -1651,9 +1649,9 @@ export function insertScopes(db: CodemapDatabase, rows: ScopeRow[]) { /** * Per-specifier row for `import { foo, bar as baz }` / `import foo from 'mod'` - * / `import * as ns from 'mod'`. Side-effect imports (`import "mod"`) have - * no specifiers. JOIN to `imports` by (file_path, line, source) when the - * import statement's other fields are needed. + * / `import * as ns from 'mod'`. Side-effect imports (`import "mod"`) emit one + * `kind='side-effect'` row (no binding names). JOIN to `imports` by + * (file_path, line, source) when the import statement's other fields are needed. */ export interface ImportSpecifierRow { file_path: string; diff --git a/src/extractors/module-side-effects.ts b/src/extractors/module-side-effects.ts index 891ce325..7106df8b 100644 --- a/src/extractors/module-side-effects.ts +++ b/src/extractors/module-side-effects.ts @@ -1,5 +1,5 @@ /** - * Module-level side effects — top-level calls and assignments per Tier 6. + * Module-level side effects — top-level calls and assignments. */ import type { TierExtractor } from "./types"; From 7852ba2379821823b239c5baf004e98cce306cde Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 10:58:43 +0300 Subject: [PATCH 13/23] perf(insert): batch async_calls and try_catch inserts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit insert_ms median 151→145ms (7-run self-index); fewer JS↔SQLite round-trips via batchInsert. --- src/application/behavioral-persist.ts | 57 +---------------------- src/application/index-engine.ts | 9 ++-- src/db.ts | 66 +++++++++++++++++++++++++++ 3 files changed, 70 insertions(+), 62 deletions(-) diff --git a/src/application/behavioral-persist.ts b/src/application/behavioral-persist.ts index a630619b..65fb4d48 100644 --- a/src/application/behavioral-persist.ts +++ b/src/application/behavioral-persist.ts @@ -1,60 +1,5 @@ import type { CodemapDatabase } from "../db"; -import type { - ParsedAsyncCall, - ParsedDecorator, - ParsedJsdocTag, - ParsedTryCatch, -} from "../extractors/behavioral"; - -export function insertAsyncCalls( - db: CodemapDatabase, - rows: ParsedAsyncCall[], -): void { - for (const r of rows) { - db.run( - `INSERT INTO async_calls ( - file_path, caller_scope, awaited_expression, awaited_callee_name, - line_start, column_start, in_loop, in_try, scope_local_id - ) VALUES (?,?,?,?,?,?,?,?,?)`, - [ - r.file_path, - r.caller_scope, - r.awaited_expression, - r.awaited_callee_name, - r.line_start, - r.column_start, - r.in_loop, - r.in_try, - r.scope_local_id, - ], - ); - } -} - -export function insertTryCatchRows( - db: CodemapDatabase, - rows: ParsedTryCatch[], -): void { - for (const r of rows) { - db.run( - `INSERT INTO try_catch ( - file_path, containing_scope_local_id, try_line_start, try_line_end, - has_catch, catch_param, catch_rethrows, catch_logs_only, has_finally - ) VALUES (?,?,?,?,?,?,?,?,?)`, - [ - r.file_path, - r.containing_scope_local_id, - r.try_line_start, - r.try_line_end, - r.has_catch, - r.catch_param, - r.catch_rethrows, - r.catch_logs_only, - r.has_finally, - ], - ); - } -} +import type { ParsedDecorator, ParsedJsdocTag } from "../extractors/behavioral"; export function insertDecorators( db: CodemapDatabase, diff --git a/src/application/index-engine.ts b/src/application/index-engine.ts index 065a45d4..dcc24517 100644 --- a/src/application/index-engine.ts +++ b/src/application/index-engine.ts @@ -36,6 +36,8 @@ import { insertTypeMembers, insertCalls, insertDynamicImports, + insertAsyncCalls, + insertTryCatchRows, getAllFileHashes, upsertSourceFts, META_FTS5_ENABLED_KEY, @@ -58,12 +60,7 @@ import { isPathExcluded, } from "../runtime"; import { parseFilesParallel } from "../worker-pool"; -import { - insertAsyncCalls, - insertDecorators, - insertJsdocTags, - insertTryCatchRows, -} from "./behavioral-persist"; +import { insertDecorators, insertJsdocTags } from "./behavioral-persist"; import { persistBindings, persistReExportChains, diff --git a/src/db.ts b/src/db.ts index 164cb88f..b9ce1f38 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1304,6 +1304,72 @@ export function insertDynamicImports( ); } +export interface AsyncCallRow { + file_path: string; + caller_scope: string; + awaited_expression: string; + awaited_callee_name: string | null; + line_start: number; + column_start: number; + in_loop: number; + in_try: number; + scope_local_id: number; +} + +export function insertAsyncCalls(db: CodemapDatabase, rows: AsyncCallRow[]) { + batchInsert( + db, + rows, + "INSERT INTO async_calls (file_path, caller_scope, awaited_expression, awaited_callee_name, line_start, column_start, in_loop, in_try, scope_local_id)", + "(?,?,?,?,?,?,?,?,?)", + (r, v) => + v.push( + r.file_path, + r.caller_scope, + r.awaited_expression, + r.awaited_callee_name, + r.line_start, + r.column_start, + r.in_loop, + r.in_try, + r.scope_local_id, + ), + ); +} + +export interface TryCatchRow { + file_path: string; + containing_scope_local_id: number; + try_line_start: number; + try_line_end: number; + has_catch: number; + catch_param: string | null; + catch_rethrows: number; + catch_logs_only: number; + has_finally: number; +} + +export function insertTryCatchRows(db: CodemapDatabase, rows: TryCatchRow[]) { + batchInsert( + db, + rows, + "INSERT INTO try_catch (file_path, containing_scope_local_id, try_line_start, try_line_end, has_catch, catch_param, catch_rethrows, catch_logs_only, has_finally)", + "(?,?,?,?,?,?,?,?,?)", + (r, v) => + v.push( + r.file_path, + r.containing_scope_local_id, + r.try_line_start, + r.try_line_end, + r.has_catch, + r.catch_param, + r.catch_rethrows, + r.catch_logs_only, + r.has_finally, + ), + ); +} + /** * Lexical scope row per [R.11]. `parent_local_id` is `null` for the * module scope; `owner_symbol_name` is `null` for module + arrow scopes. From b9ebdb854f7d19fafd329fc49272b9dd2b171ded Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 11:00:15 +0300 Subject: [PATCH 14/23] perf(insert): batch imports in insertImportsWithSpecifiers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit insert_ms median 145→143ms (10-run self-index); batchInsert + id > maxBefore for import_id FK wiring preserves row order. --- src/db.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/db.ts b/src/db.ts index b9ce1f38..d18d8b60 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1750,23 +1750,30 @@ export function insertImportsWithSpecifiers( } return; } - const importIds: number[] = []; - for (const imp of imports) { - db.run( - "INSERT INTO imports (file_path, source, resolved_path, specifiers, is_type_only, line_number) VALUES (?,?,?,?,?,?)", - [ + const maxBefore = db + .query<{ m: number }>("SELECT COALESCE(MAX(id), 0) AS m FROM imports") + .get()!.m; + batchInsert( + db, + imports, + "INSERT INTO imports (file_path, source, resolved_path, specifiers, is_type_only, line_number)", + "(?,?,?,?,?,?)", + (imp, v) => + v.push( imp.file_path, imp.source, imp.resolved_path, imp.specifiers, imp.is_type_only, imp.line_number, - ], - ); - importIds.push( - db.query<{ id: number }>("SELECT last_insert_rowid() AS id").get()!.id, - ); - } + ), + ); + const importIds = db + .query<{ id: number }>( + "SELECT id FROM imports WHERE id > ? ORDER BY id ASC", + ) + .all(maxBefore) + .map((r) => r.id); if (!specifiers.length) return; const linked = specifiers.map((row) => ({ ...row, From 12b40c0c7a2e324e1f052394c2c91e6c6dedc7d7 Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 11:01:09 +0300 Subject: [PATCH 15/23] perf(insert): batch jsx_attributes inserts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit insert_ms median 145→143ms (10-run self-index); element inserts unchanged. --- src/application/jsx-persist.ts | 26 ++++++++++++-------------- src/db.ts | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 14 deletions(-) diff --git a/src/application/jsx-persist.ts b/src/application/jsx-persist.ts index 038a0385..fdfa0ba8 100644 --- a/src/application/jsx-persist.ts +++ b/src/application/jsx-persist.ts @@ -1,3 +1,4 @@ +import { insertJsxAttributes } from "../db"; import type { CodemapDatabase } from "../db"; import type { ParsedJsxAttribute, ParsedJsxElement } from "../extractors/jsx"; @@ -46,22 +47,19 @@ export function persistJsxElementsAndAttributes( ]); } } + const attrRows = []; for (const attr of attributes) { const elementId = idMap.get(attr.element_local_id); if (elementId == null) continue; - db.run( - `INSERT INTO jsx_attributes ( - element_id, name, line, column_start, column_end, value_kind, value_text - ) VALUES (?,?,?,?,?,?,?)`, - [ - elementId, - attr.name, - attr.line, - attr.column_start, - attr.column_end, - attr.value_kind, - attr.value_text, - ], - ); + attrRows.push({ + element_id: elementId, + name: attr.name, + line: attr.line, + column_start: attr.column_start, + column_end: attr.column_end, + value_kind: attr.value_kind, + value_text: attr.value_text, + }); } + if (attrRows.length) insertJsxAttributes(db, attrRows); } diff --git a/src/db.ts b/src/db.ts index d18d8b60..8036159a 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1370,6 +1370,38 @@ export function insertTryCatchRows(db: CodemapDatabase, rows: TryCatchRow[]) { ); } +export interface JsxAttributeRow { + element_id: number; + name: string; + line: number; + column_start: number; + column_end: number; + value_kind: "string" | "expression" | "boolean" | "spread" | "element"; + value_text: string | null; +} + +export function insertJsxAttributes( + db: CodemapDatabase, + rows: JsxAttributeRow[], +): void { + batchInsert( + db, + rows, + "INSERT INTO jsx_attributes (element_id, name, line, column_start, column_end, value_kind, value_text)", + "(?,?,?,?,?,?,?)", + (r, v) => + v.push( + r.element_id, + r.name, + r.line, + r.column_start, + r.column_end, + r.value_kind, + r.value_text, + ), + ); +} + /** * Lexical scope row per [R.11]. `parent_local_id` is `null` for the * module scope; `owner_symbol_name` is `null` for module + arrow scopes. From 93b26595ea97b5c1dfaf1012fcf5b33a1e565cdd Mon Sep 17 00:00:00 2001 From: Sutu Sebastian Date: Wed, 20 May 2026 11:06:25 +0300 Subject: [PATCH 16/23] test(fixtures): enrich minimal corpus for full substrate coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add boundary config, tests, complexity/JSX/behavioral fixtures, and 16 golden scenarios so bundled recipes and tiers 1–6 tables have non-empty CI coverage. --- fixtures/golden/minimal/barrel-files.json | 20 +++ .../golden/minimal/boundary-violations.json | 9 ++ fixtures/golden/minimal/calls-consumer.json | 12 ++ .../golden/minimal/components-by-hooks.json | 12 ++ .../components-touching-deprecated.json | 9 ++ .../minimal/coverage-rows-after-ingest.json | 22 +-- .../minimal/deeply-nested-functions.json | 21 ++- .../minimal/dependencies-from-consumer.json | 8 + .../golden/minimal/deprecated-symbols.json | 2 +- .../golden/minimal/enum-order-status.json | 7 + fixtures/golden/minimal/env-var-audit.json | 13 +- fixtures/golden/minimal/fan-in.json | 46 ++++++ fixtures/golden/minimal/fan-out.json | 34 +++++ .../golden/minimal/files-by-coverage.json | 6 - fixtures/golden/minimal/files-count.json | 2 +- fixtures/golden/minimal/files-hashes.json | 66 ++++++-- fixtures/golden/minimal/files-largest.json | 122 +++++++++++++++ .../golden/minimal/find-await-in-loop.json | 2 +- .../golden/minimal/find-by-param-type.json | 2 +- fixtures/golden/minimal/find-call-sites.json | 16 +- .../golden/minimal/find-dynamic-imports.json | 2 +- .../golden/minimal/find-export-sites.json | 4 +- .../golden/minimal/find-import-sites.json | 2 +- fixtures/golden/minimal/find-jsx-usages.json | 6 +- .../golden/minimal/find-leftover-console.json | 6 + .../minimal/find-re-exported-bindings.json | 4 +- fixtures/golden/minimal/find-references.json | 2 +- .../minimal/find-side-effect-files.json | 6 + .../minimal/find-side-effect-imports.json | 4 +- .../golden/minimal/find-skipped-tests.json | 27 +++- .../minimal/find-symbol-definitions.json | 4 +- .../minimal/find-symbol-references.json | 26 +++- .../golden/minimal/find-throws-jsdoc.json | 2 +- fixtures/golden/minimal/find-write-sites.json | 4 +- .../minimal/high-complexity-untested.json | 11 ++ fixtures/golden/minimal/index-summary.json | 8 +- .../minimal/jsdoc-tags-createClient.json | 17 +++ .../minimal/jsx-attributes-product-card.json | 26 ++++ fixtures/golden/minimal/large-functions.json | 14 +- .../golden/minimal/markers-all-kinds.json | 4 +- fixtures/golden/minimal/markers-by-kind.json | 18 +++ .../golden/minimal/refactor-risk-ranking.json | 122 +++++++++++++++ .../golden/minimal/suppressions-orphan.json | 7 + fixtures/golden/minimal/tests-by-file.json | 12 +- .../minimal/text-in-deprecated-functions.json | 10 ++ .../golden/minimal/unimported-exports.json | 142 ++++++++++++++++++ .../golden/minimal/untested-and-dead.json | 32 +++- fixtures/golden/minimal/visibility-tags.json | 2 +- .../golden/minimal/worst-covered-exports.json | 86 +++++------ fixtures/golden/scenarios.json | 75 +++++++++ fixtures/minimal/.codemap/config.json | 10 ++ fixtures/minimal/README.md | 58 +++---- fixtures/minimal/src/__tests__/smoke.test.ts | 25 +++ fixtures/minimal/src/api/client.ts | 6 +- .../minimal/src/components/shop/ApiBridge.tsx | 7 + .../src/components/shop/ProductCard.tsx | 17 ++- .../src/components/shop/ShopButton.tsx | 2 + fixtures/minimal/src/consumer.ts | 17 ++- fixtures/minimal/src/env.ts | 3 + .../minimal/src/lib/complexity-fixture.ts | 83 ++++++++++ fixtures/minimal/src/orphan.ts | 9 ++ fixtures/minimal/src/types/status.ts | 4 + fixtures/minimal/src/utils/format.ts | 1 + 63 files changed, 1206 insertions(+), 152 deletions(-) create mode 100644 fixtures/golden/minimal/boundary-violations.json create mode 100644 fixtures/golden/minimal/components-by-hooks.json create mode 100644 fixtures/golden/minimal/components-touching-deprecated.json create mode 100644 fixtures/golden/minimal/enum-order-status.json create mode 100644 fixtures/golden/minimal/fan-in.json create mode 100644 fixtures/golden/minimal/fan-out.json create mode 100644 fixtures/golden/minimal/files-largest.json create mode 100644 fixtures/golden/minimal/high-complexity-untested.json create mode 100644 fixtures/golden/minimal/jsdoc-tags-createClient.json create mode 100644 fixtures/golden/minimal/jsx-attributes-product-card.json create mode 100644 fixtures/golden/minimal/markers-by-kind.json create mode 100644 fixtures/golden/minimal/refactor-risk-ranking.json create mode 100644 fixtures/golden/minimal/suppressions-orphan.json create mode 100644 fixtures/golden/minimal/text-in-deprecated-functions.json create mode 100644 fixtures/golden/minimal/unimported-exports.json create mode 100644 fixtures/minimal/.codemap/config.json create mode 100644 fixtures/minimal/src/__tests__/smoke.test.ts create mode 100644 fixtures/minimal/src/components/shop/ApiBridge.tsx create mode 100644 fixtures/minimal/src/env.ts create mode 100644 fixtures/minimal/src/lib/complexity-fixture.ts create mode 100644 fixtures/minimal/src/orphan.ts create mode 100644 fixtures/minimal/src/types/status.ts diff --git a/fixtures/golden/minimal/barrel-files.json b/fixtures/golden/minimal/barrel-files.json index 61d8f39c..f0934966 100644 --- a/fixtures/golden/minimal/barrel-files.json +++ b/fixtures/golden/minimal/barrel-files.json @@ -19,14 +19,26 @@ "file_path": "src/consumer.ts", "exports": 2 }, + { + "file_path": "src/env.ts", + "exports": 2 + }, { "file_path": "src/lib/cache.ts", "exports": 2 }, + { + "file_path": "src/lib/complexity-fixture.ts", + "exports": 2 + }, { "file_path": "src/lib/store.ts", "exports": 2 }, + { + "file_path": "src/orphan.ts", + "exports": 2 + }, { "file_path": "src/utils/format.ts", "exports": 2 @@ -35,6 +47,10 @@ "file_path": "src/api/decorated.ts", "exports": 1 }, + { + "file_path": "src/components/shop/ApiBridge.tsx", + "exports": 1 + }, { "file_path": "src/components/shop/ProductCard.tsx", "exports": 1 @@ -43,6 +59,10 @@ "file_path": "src/components/shop/ShopButton.default.ts", "exports": 1 }, + { + "file_path": "src/types/status.ts", + "exports": 1 + }, { "file_path": "src/usePermissions.ts", "exports": 1 diff --git a/fixtures/golden/minimal/boundary-violations.json b/fixtures/golden/minimal/boundary-violations.json new file mode 100644 index 00000000..7b06f3d6 --- /dev/null +++ b/fixtures/golden/minimal/boundary-violations.json @@ -0,0 +1,9 @@ +[ + { + "file_path": "src/components/shop/ApiBridge.tsx", + "to_path": "src/api/client.ts", + "rule_name": "ui-no-api", + "rule_from_glob": "src/components/**", + "rule_to_glob": "src/api/**" + } +] diff --git a/fixtures/golden/minimal/calls-consumer.json b/fixtures/golden/minimal/calls-consumer.json index 86dd87ed..fcef6a9f 100644 --- a/fixtures/golden/minimal/calls-consumer.json +++ b/fixtures/golden/minimal/calls-consumer.json @@ -1,4 +1,7 @@ [ + { + "callee_name": "ApiCache" + }, { "callee_name": "createClient" }, @@ -8,7 +11,16 @@ { "callee_name": "get" }, + { + "callee_name": "labyrinth" + }, { "callee_name": "now" + }, + { + "callee_name": "optionalPing" + }, + { + "callee_name": "spreadLog" } ] diff --git a/fixtures/golden/minimal/components-by-hooks.json b/fixtures/golden/minimal/components-by-hooks.json new file mode 100644 index 00000000..76a0a445 --- /dev/null +++ b/fixtures/golden/minimal/components-by-hooks.json @@ -0,0 +1,12 @@ +[ + { + "name": "ProductCard", + "file_path": "src/components/shop/ProductCard.tsx", + "hook_count": 1 + }, + { + "name": "ShopButton", + "file_path": "src/components/shop/ShopButton.tsx", + "hook_count": 1 + } +] diff --git a/fixtures/golden/minimal/components-touching-deprecated.json b/fixtures/golden/minimal/components-touching-deprecated.json new file mode 100644 index 00000000..568b2425 --- /dev/null +++ b/fixtures/golden/minimal/components-touching-deprecated.json @@ -0,0 +1,9 @@ +[ + { + "component": "ShopButton", + "component_file": "src/components/shop/ShopButton.tsx", + "deprecated_symbol": "now", + "deprecated_file": "src/utils/date.ts", + "via": "call" + } +] diff --git a/fixtures/golden/minimal/coverage-rows-after-ingest.json b/fixtures/golden/minimal/coverage-rows-after-ingest.json index abd5c0e2..1f5c5f3e 100644 --- a/fixtures/golden/minimal/coverage-rows-after-ingest.json +++ b/fixtures/golden/minimal/coverage-rows-after-ingest.json @@ -1,11 +1,4 @@ [ - { - "file_path": "src/api/client.ts", - "name": "legacyClient", - "hit_statements": 0, - "total_statements": 1, - "coverage_pct": 0 - }, { "file_path": "src/components/shop/ProductCard.tsx", "name": "ProductCard", @@ -15,16 +8,23 @@ }, { "file_path": "src/components/shop/ShopButton.tsx", - "name": "FormatPrice", + "name": "cents", "hit_statements": 0, "total_statements": 1, "coverage_pct": 0 }, { "file_path": "src/components/shop/ShopButton.tsx", - "name": "ShopButton", - "hit_statements": 2, - "total_statements": 2, + "name": "perms", + "hit_statements": 1, + "total_statements": 1, + "coverage_pct": 100 + }, + { + "file_path": "src/components/shop/ShopButton.tsx", + "name": "_stamp", + "hit_statements": 1, + "total_statements": 1, "coverage_pct": 100 }, { diff --git a/fixtures/golden/minimal/deeply-nested-functions.json b/fixtures/golden/minimal/deeply-nested-functions.json index fe51488c..8ee30046 100644 --- a/fixtures/golden/minimal/deeply-nested-functions.json +++ b/fixtures/golden/minimal/deeply-nested-functions.json @@ -1 +1,20 @@ -[] +[ + { + "name": "labyrinth", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 22, + "body_line_count": 62, + "complexity": 19, + "nesting_depth": 5 + }, + { + "name": "deeplyNested", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 6, + "body_line_count": 15, + "complexity": 5, + "nesting_depth": 4 + } +] diff --git a/fixtures/golden/minimal/dependencies-from-consumer.json b/fixtures/golden/minimal/dependencies-from-consumer.json index 3157a61f..c4dd3845 100644 --- a/fixtures/golden/minimal/dependencies-from-consumer.json +++ b/fixtures/golden/minimal/dependencies-from-consumer.json @@ -3,6 +3,10 @@ "from_path": "src/consumer.ts", "to_path": "src/api/client.ts" }, + { + "from_path": "src/consumer.ts", + "to_path": "src/api/decorated.ts" + }, { "from_path": "src/consumer.ts", "to_path": "src/components/shop/index.ts" @@ -11,6 +15,10 @@ "from_path": "src/consumer.ts", "to_path": "src/lib/cache.ts" }, + { + "from_path": "src/consumer.ts", + "to_path": "src/lib/complexity-fixture.ts" + }, { "from_path": "src/consumer.ts", "to_path": "src/polyfill.ts" diff --git a/fixtures/golden/minimal/deprecated-symbols.json b/fixtures/golden/minimal/deprecated-symbols.json index b854cb12..9472f835 100644 --- a/fixtures/golden/minimal/deprecated-symbols.json +++ b/fixtures/golden/minimal/deprecated-symbols.json @@ -3,7 +3,7 @@ "name": "legacyClient", "kind": "function", "file_path": "src/api/client.ts", - "line_start": 42, + "line_start": 46, "signature": "legacyClient()", "doc_comment": "@deprecated Use `createClient({ baseUrl })` directly. Kept as a fixture for\n`deprecated-symbols` + `--format sarif` / `--format annotations` recipes." }, diff --git a/fixtures/golden/minimal/enum-order-status.json b/fixtures/golden/minimal/enum-order-status.json new file mode 100644 index 00000000..61d52a3d --- /dev/null +++ b/fixtures/golden/minimal/enum-order-status.json @@ -0,0 +1,7 @@ +[ + { + "name": "OrderStatus", + "kind": "enum", + "file_path": "src/types/status.ts" + } +] diff --git a/fixtures/golden/minimal/env-var-audit.json b/fixtures/golden/minimal/env-var-audit.json index fe51488c..c11b7740 100644 --- a/fixtures/golden/minimal/env-var-audit.json +++ b/fixtures/golden/minimal/env-var-audit.json @@ -1 +1,12 @@ -[] +[ + { + "env_var": "CODEMAP_FIXTURE_API_KEY", + "uses": 1, + "files": 1 + }, + { + "env_var": "NODE_ENV", + "uses": 1, + "files": 1 + } +] diff --git a/fixtures/golden/minimal/fan-in.json b/fixtures/golden/minimal/fan-in.json new file mode 100644 index 00000000..755eac5b --- /dev/null +++ b/fixtures/golden/minimal/fan-in.json @@ -0,0 +1,46 @@ +[ + { + "to_path": "src/utils/date.ts", + "fan_in": 3 + }, + { + "to_path": "src/api/client.ts", + "fan_in": 2 + }, + { + "to_path": "src/lib/cache.ts", + "fan_in": 2 + }, + { + "to_path": "src/lib/complexity-fixture.ts", + "fan_in": 2 + }, + { + "to_path": "src/usePermissions.ts", + "fan_in": 2 + }, + { + "to_path": "src/api/decorated.ts", + "fan_in": 1 + }, + { + "to_path": "src/components/shop/ShopButton.tsx", + "fan_in": 1 + }, + { + "to_path": "src/components/shop/index.ts", + "fan_in": 1 + }, + { + "to_path": "src/lib/store.ts", + "fan_in": 1 + }, + { + "to_path": "src/polyfill.ts", + "fan_in": 1 + }, + { + "to_path": "src/utils/format.ts", + "fan_in": 1 + } +] diff --git a/fixtures/golden/minimal/fan-out.json b/fixtures/golden/minimal/fan-out.json new file mode 100644 index 00000000..3cd7e2b9 --- /dev/null +++ b/fixtures/golden/minimal/fan-out.json @@ -0,0 +1,34 @@ +[ + { + "from_path": "src/consumer.ts", + "deps": 8 + }, + { + "from_path": "src/components/shop/ProductCard.tsx", + "deps": 2 + }, + { + "from_path": "src/components/shop/ShopButton.tsx", + "deps": 2 + }, + { + "from_path": "src/__tests__/smoke.test.ts", + "deps": 1 + }, + { + "from_path": "src/components/shop/ApiBridge.tsx", + "deps": 1 + }, + { + "from_path": "src/components/shop/ShopButton.default.ts", + "deps": 1 + }, + { + "from_path": "src/lib/cache.ts", + "deps": 1 + }, + { + "from_path": "src/lib/store.ts", + "deps": 1 + } +] diff --git a/fixtures/golden/minimal/files-by-coverage.json b/fixtures/golden/minimal/files-by-coverage.json index 7a24c45c..25892853 100644 --- a/fixtures/golden/minimal/files-by-coverage.json +++ b/fixtures/golden/minimal/files-by-coverage.json @@ -1,10 +1,4 @@ [ - { - "file_path": "src/api/client.ts", - "hit_statements": 0, - "total_statements": 1, - "coverage_pct": 0 - }, { "file_path": "src/utils/date.ts", "hit_statements": 1, diff --git a/fixtures/golden/minimal/files-count.json b/fixtures/golden/minimal/files-count.json index dd846749..9353bfed 100644 --- a/fixtures/golden/minimal/files-count.json +++ b/fixtures/golden/minimal/files-count.json @@ -1,5 +1,5 @@ [ { - "n": 20 + "n": 27 } ] diff --git a/fixtures/golden/minimal/files-hashes.json b/fixtures/golden/minimal/files-hashes.json index b3dfaa50..a8c9415d 100644 --- a/fixtures/golden/minimal/files-hashes.json +++ b/fixtures/golden/minimal/files-hashes.json @@ -1,4 +1,10 @@ [ + { + "path": ".codemap/config.json", + "content_hash": "336aef86f425c7227027d30a696382314825c37d6c7a6b65b13d354d5980f111", + "language": "json", + "line_count": 11 + }, { "path": ".codemap/recipes/shop-symbols.md", "content_hash": "0ef26e4291fd1dbfb60887d79251bfd9eb1af71675811ba40a1932368e6b112c", @@ -7,9 +13,9 @@ }, { "path": "README.md", - "content_hash": "05f60d056b6a09c6d2024be74cd45e17f40b1126c408b67a42bc098f5159d13e", + "content_hash": "d6a63cfa7cb8f176151054235c84819b9af51979b482793def5d8569c50979c2", "language": "md", - "line_count": 50 + "line_count": 52 }, { "path": "package.json", @@ -17,11 +23,17 @@ "language": "json", "line_count": 11 }, + { + "path": "src/__tests__/smoke.test.ts", + "content_hash": "2fa9fff351d4f0181db6a2edac133ab3d90ee227934a599b122ad8dcbf7ba13c", + "language": "ts", + "line_count": 26 + }, { "path": "src/api/client.ts", - "content_hash": "2035094c31ce72ef9b723e4b47ebb1d65bc8d9a381ca7f98bd43164adcd2757f", + "content_hash": "7d313fff06443ff9e7ca3d13f44a8c601ce65baa3d8aa1a8263f3e85090afe0b", "language": "ts", - "line_count": 45 + "line_count": 49 }, { "path": "src/api/decorated.ts", @@ -29,11 +41,17 @@ "language": "ts", "line_count": 5 }, + { + "path": "src/components/shop/ApiBridge.tsx", + "content_hash": "825a69353a860b7a4f0bb562b71ca12fad3f6a2b38cd7f28b91a17562ac5d8d5", + "language": "tsx", + "line_count": 8 + }, { "path": "src/components/shop/ProductCard.tsx", - "content_hash": "4cdd3d8376da664789c075bfdb3eab49adc3460fcd0810eedc1ad456e7c5be9a", + "content_hash": "27b25a905655574a3002adc140b36e91b95f8f1a5db0758ba172115595134122", "language": "tsx", - "line_count": 20 + "line_count": 23 }, { "path": "src/components/shop/ShopButton.default.ts", @@ -43,9 +61,9 @@ }, { "path": "src/components/shop/ShopButton.tsx", - "content_hash": "cbc7939a134240c0076f76f6499fa893309f131dda53eaef4d946470f42c54af", + "content_hash": "abc37ff9c611c7259b63bde0727216c8e1848f279f142d1ce276564bcb34aed8", "language": "tsx", - "line_count": 15 + "line_count": 17 }, { "path": "src/components/shop/index.ts", @@ -55,9 +73,15 @@ }, { "path": "src/consumer.ts", - "content_hash": "2698fbba58a1ae47e9cb3a2297b5677a21d9ecfa64a62c8a4ee072dab3ef185f", + "content_hash": "ce739e8bbfd30a8ef46a82f6eb9760227ca8b8369162df3b819747b81b7a57cc", "language": "ts", - "line_count": 29 + "line_count": 40 + }, + { + "path": "src/env.ts", + "content_hash": "5d5828d36203762f7aae84b4bc3672285ee9e335fbbba95e29c01393095440b6", + "language": "ts", + "line_count": 4 }, { "path": "src/lib/cache.ts", @@ -65,6 +89,12 @@ "language": "ts", "line_count": 27 }, + { + "path": "src/lib/complexity-fixture.ts", + "content_hash": "ba844b9262adbb407030708f7fc3432bde780ba88a07066ad383e66a6f3851d8", + "language": "ts", + "line_count": 84 + }, { "path": "src/lib/store.ts", "content_hash": "a23e13e22cecf8051d83659b39f792b6fc592f90d501da83d75ab5fd61dac1a8", @@ -77,6 +107,12 @@ "language": "md", "line_count": 11 }, + { + "path": "src/orphan.ts", + "content_hash": "2c1516b82d9ad1c65f3827701f6adf6c9a246bc71b9b8eab486717e40e6e0e58", + "language": "ts", + "line_count": 10 + }, { "path": "src/polyfill.ts", "content_hash": "f9c2f0d0224aef67b737ce225be1c3797d50fa0ab05fcd8c2fd129dc5bad6914", @@ -95,6 +131,12 @@ "language": "css", "line_count": 9 }, + { + "path": "src/types/status.ts", + "content_hash": "85a4d68b1bcffdd9b00c25293b5b81be1ce690a7525b444c6315dcf2b8876b7b", + "language": "ts", + "line_count": 5 + }, { "path": "src/usePermissions.ts", "content_hash": "8758cc9ba2756e08198f11fca9ff58a0612fc9219173d06784e0b7e2f2ca1e1f", @@ -109,9 +151,9 @@ }, { "path": "src/utils/format.ts", - "content_hash": "8f4e1b729d1a69f1457a57c0a6ce61740ff27f2119976d2183ca053bf50b2ad6", + "content_hash": "670e458b068c63994b027d3200cc74e1a2c1bf5472ae2dc5e5508653178c506d", "language": "ts", - "line_count": 16 + "line_count": 17 }, { "path": "tsconfig.json", diff --git a/fixtures/golden/minimal/files-largest.json b/fixtures/golden/minimal/files-largest.json new file mode 100644 index 00000000..093d476a --- /dev/null +++ b/fixtures/golden/minimal/files-largest.json @@ -0,0 +1,122 @@ +[ + { + "path": "src/lib/complexity-fixture.ts", + "line_count": 84, + "size": 1424, + "language": "ts" + }, + { + "path": "README.md", + "line_count": 52, + "size": 8499, + "language": "md" + }, + { + "path": "src/api/client.ts", + "line_count": 49, + "size": 1373, + "language": "ts" + }, + { + "path": "src/consumer.ts", + "line_count": 40, + "size": 1068, + "language": "ts" + }, + { + "path": "src/utils/date.ts", + "line_count": 29, + "size": 684, + "language": "ts" + }, + { + "path": "src/lib/cache.ts", + "line_count": 27, + "size": 733, + "language": "ts" + }, + { + "path": "src/__tests__/smoke.test.ts", + "line_count": 26, + "size": 489, + "language": "ts" + }, + { + "path": "src/components/shop/ProductCard.tsx", + "line_count": 23, + "size": 654, + "language": "tsx" + }, + { + "path": "tsconfig.json", + "line_count": 18, + "size": 363, + "language": "json" + }, + { + "path": "src/components/shop/ShopButton.tsx", + "line_count": 17, + "size": 377, + "language": "tsx" + }, + { + "path": "src/utils/format.ts", + "line_count": 17, + "size": 500, + "language": "ts" + }, + { + "path": "src/styles/button.module.css", + "line_count": 16, + "size": 167, + "language": "css" + }, + { + "path": "src/lib/store.ts", + "line_count": 15, + "size": 329, + "language": "ts" + }, + { + "path": ".codemap/config.json", + "line_count": 11, + "size": 151, + "language": "json" + }, + { + "path": "package.json", + "line_count": 11, + "size": 238, + "language": "json" + }, + { + "path": "src/notes.md", + "line_count": 11, + "size": 418, + "language": "md" + }, + { + "path": "src/orphan.ts", + "line_count": 10, + "size": 239, + "language": "ts" + }, + { + "path": ".codemap/recipes/shop-symbols.md", + "line_count": 9, + "size": 310, + "language": "md" + }, + { + "path": "src/theme.css", + "line_count": 9, + "size": 94, + "language": "css" + }, + { + "path": "src/components/shop/ApiBridge.tsx", + "line_count": 8, + "size": 184, + "language": "tsx" + } +] diff --git a/fixtures/golden/minimal/find-await-in-loop.json b/fixtures/golden/minimal/find-await-in-loop.json index 7a5e6e74..365ecfef 100644 --- a/fixtures/golden/minimal/find-await-in-loop.json +++ b/fixtures/golden/minimal/find-await-in-loop.json @@ -4,7 +4,7 @@ "caller_scope": "prefetch.$anon_2", "awaited_expression": "await import(\"./lib/cache\")", "awaited_callee_name": null, - "line_start": 15, + "line_start": 17, "in_loop": 1, "in_try": 0 } diff --git a/fixtures/golden/minimal/find-by-param-type.json b/fixtures/golden/minimal/find-by-param-type.json index aeb3cb8e..c021bf0f 100644 --- a/fixtures/golden/minimal/find-by-param-type.json +++ b/fixtures/golden/minimal/find-by-param-type.json @@ -8,7 +8,7 @@ "default_text": null, "is_rest": 0, "is_optional": 1, - "line_start": 16, + "line_start": 20, "column_start": 29 } ] diff --git a/fixtures/golden/minimal/find-call-sites.json b/fixtures/golden/minimal/find-call-sites.json index c5deeaca..fc9cbf2a 100644 --- a/fixtures/golden/minimal/find-call-sites.json +++ b/fixtures/golden/minimal/find-call-sites.json @@ -3,7 +3,7 @@ "file_path": "src/api/client.ts", "caller_name": "legacyClient", "caller_scope": "legacyClient", - "line_start": 43, + "line_start": 47, "column_start": 9, "column_end": 21, "args_count": 0, @@ -11,11 +11,23 @@ "is_constructor_call": 0, "is_optional_chain": 0 }, + { + "file_path": "src/components/shop/ApiBridge.tsx", + "caller_name": "ApiBridge", + "caller_scope": "ApiBridge", + "line_start": 5, + "column_start": 2, + "column_end": 14, + "args_count": 0, + "is_method_call": 0, + "is_constructor_call": 0, + "is_optional_chain": 0 + }, { "file_path": "src/consumer.ts", "caller_name": "run", "caller_scope": "run", - "line_start": 22, + "line_start": 24, "column_start": 2, "column_end": 14, "args_count": 1, diff --git a/fixtures/golden/minimal/find-dynamic-imports.json b/fixtures/golden/minimal/find-dynamic-imports.json index 53c381ab..f3f9ed1b 100644 --- a/fixtures/golden/minimal/find-dynamic-imports.json +++ b/fixtures/golden/minimal/find-dynamic-imports.json @@ -1,7 +1,7 @@ [ { "file_path": "src/consumer.ts", - "line_start": 15, + "line_start": 17, "column_start": 17, "source_kind": "literal", "source_text": "./lib/cache", diff --git a/fixtures/golden/minimal/find-export-sites.json b/fixtures/golden/minimal/find-export-sites.json index 020856c6..82d3099b 100644 --- a/fixtures/golden/minimal/find-export-sites.json +++ b/fixtures/golden/minimal/find-export-sites.json @@ -6,8 +6,8 @@ "is_default": 0, "is_re_export": 0, "re_export_source": null, - "line_start": 11, - "line_end": 19, + "line_start": 10, + "line_end": 22, "column_start": 16, "column_end": 27 }, diff --git a/fixtures/golden/minimal/find-import-sites.json b/fixtures/golden/minimal/find-import-sites.json index af9b3a51..0c667a71 100644 --- a/fixtures/golden/minimal/find-import-sites.json +++ b/fixtures/golden/minimal/find-import-sites.json @@ -2,7 +2,7 @@ { "file_path": "src/consumer.ts", "source": "~/components/shop", - "line": 2, + "line": 3, "column_start": 9, "column_end": 20, "imported_name": "ProductCard", diff --git a/fixtures/golden/minimal/find-jsx-usages.json b/fixtures/golden/minimal/find-jsx-usages.json index 064d66f8..c81d8216 100644 --- a/fixtures/golden/minimal/find-jsx-usages.json +++ b/fixtures/golden/minimal/find-jsx-usages.json @@ -2,11 +2,11 @@ { "file_path": "src/components/shop/ProductCard.tsx", "component_name": "article", - "line_start": 14, - "line_end": 17, + "line_start": 15, + "line_end": 19, "is_self_closing": 0, "is_fragment": 0, "is_lowercase": 1, - "children_count": 1 + "children_count": 2 } ] diff --git a/fixtures/golden/minimal/find-leftover-console.json b/fixtures/golden/minimal/find-leftover-console.json index a0fabd71..f3d3c5c3 100644 --- a/fixtures/golden/minimal/find-leftover-console.json +++ b/fixtures/golden/minimal/find-leftover-console.json @@ -1,4 +1,10 @@ [ + { + "file_path": "src/consumer.ts", + "line_start": 38, + "column_start": 2, + "method": "debug" + }, { "file_path": "src/lib/cache.ts", "line_start": 23, diff --git a/fixtures/golden/minimal/find-re-exported-bindings.json b/fixtures/golden/minimal/find-re-exported-bindings.json index 5d5f62d4..17249d13 100644 --- a/fixtures/golden/minimal/find-re-exported-bindings.json +++ b/fixtures/golden/minimal/find-re-exported-bindings.json @@ -2,14 +2,14 @@ { "file_path": "src/consumer.ts", "name": "ProductCard", - "line_start": 2, + "line_start": 3, "column_start": 9, "resolution_kind": "re-exported" }, { "file_path": "src/consumer.ts", "name": "ShopButton", - "line_start": 2, + "line_start": 3, "column_start": 22, "resolution_kind": "re-exported" }, diff --git a/fixtures/golden/minimal/find-references.json b/fixtures/golden/minimal/find-references.json index 1cc2fb56..e5a1d59f 100644 --- a/fixtures/golden/minimal/find-references.json +++ b/fixtures/golden/minimal/find-references.json @@ -11,7 +11,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 2, + "line_start": 3, "column_start": 9, "column_end": 20, "kind": "value", diff --git a/fixtures/golden/minimal/find-side-effect-files.json b/fixtures/golden/minimal/find-side-effect-files.json index 880bd3a3..3042612d 100644 --- a/fixtures/golden/minimal/find-side-effect-files.json +++ b/fixtures/golden/minimal/find-side-effect-files.json @@ -1,4 +1,10 @@ [ + { + "path": "src/__tests__/smoke.test.ts", + "language": "ts", + "is_barrel": 0, + "has_side_effects": 1 + }, { "path": "src/consumer.ts", "language": "ts", diff --git a/fixtures/golden/minimal/find-side-effect-imports.json b/fixtures/golden/minimal/find-side-effect-imports.json index b7e03e28..47f4aa2b 100644 --- a/fixtures/golden/minimal/find-side-effect-imports.json +++ b/fixtures/golden/minimal/find-side-effect-imports.json @@ -2,9 +2,9 @@ { "file_path": "src/consumer.ts", "source": "./polyfill", - "line": 4, + "line": 5, "column_start": 7, "column_end": 19, - "import_id": 6 + "import_id": 12 } ] diff --git a/fixtures/golden/minimal/find-skipped-tests.json b/fixtures/golden/minimal/find-skipped-tests.json index fe51488c..1a952360 100644 --- a/fixtures/golden/minimal/find-skipped-tests.json +++ b/fixtures/golden/minimal/find-skipped-tests.json @@ -1 +1,26 @@ -[] +[ + { + "file_path": "src/__tests__/smoke.test.ts", + "line_start": 10, + "kind": "it", + "name": "focused example", + "status": "only", + "framework": "vitest" + }, + { + "file_path": "src/__tests__/smoke.test.ts", + "line_start": 6, + "kind": "it", + "name": "skipped example", + "status": "skipped", + "framework": "vitest" + }, + { + "file_path": "src/__tests__/smoke.test.ts", + "line_start": 14, + "kind": "it", + "name": "todo example", + "status": "todo", + "framework": "vitest" + } +] diff --git a/fixtures/golden/minimal/find-symbol-definitions.json b/fixtures/golden/minimal/find-symbol-definitions.json index 300623be..a9a9b567 100644 --- a/fixtures/golden/minimal/find-symbol-definitions.json +++ b/fixtures/golden/minimal/find-symbol-definitions.json @@ -3,8 +3,8 @@ "file_path": "src/components/shop/ProductCard.tsx", "name": "ProductCard", "kind": "function", - "line_start": 11, - "line_end": 19, + "line_start": 10, + "line_end": 22, "name_column_start": 16, "name_column_end": 27, "parent_name": null, diff --git a/fixtures/golden/minimal/find-symbol-references.json b/fixtures/golden/minimal/find-symbol-references.json index 45e2d93d..174678b5 100644 --- a/fixtures/golden/minimal/find-symbol-references.json +++ b/fixtures/golden/minimal/find-symbol-references.json @@ -1,7 +1,7 @@ [ { "file_path": "src/api/client.ts", - "line_start": 43, + "line_start": 47, "column_start": 9, "column_end": 21, "kind": "value", @@ -10,6 +10,28 @@ "scope_kind": "function", "scope_owner": "legacyClient" }, + { + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 1, + "column_start": 9, + "column_end": 21, + "kind": "value", + "is_write": 0, + "resolution_kind": "imported", + "scope_kind": "module", + "scope_owner": null + }, + { + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 5, + "column_start": 2, + "column_end": 14, + "kind": "value", + "is_write": 0, + "resolution_kind": "imported", + "scope_kind": "function", + "scope_owner": "ApiBridge" + }, { "file_path": "src/consumer.ts", "line_start": 1, @@ -23,7 +45,7 @@ }, { "file_path": "src/consumer.ts", - "line_start": 22, + "line_start": 24, "column_start": 2, "column_end": 14, "kind": "value", diff --git a/fixtures/golden/minimal/find-throws-jsdoc.json b/fixtures/golden/minimal/find-throws-jsdoc.json index b7bc25c0..2fb488bd 100644 --- a/fixtures/golden/minimal/find-throws-jsdoc.json +++ b/fixtures/golden/minimal/find-throws-jsdoc.json @@ -2,7 +2,7 @@ { "file_path": "src/api/client.ts", "name": "createClient", - "line_start": 16, + "line_start": 20, "tag": "@throws", "type_text": "Error", "description": "when config is invalid" diff --git a/fixtures/golden/minimal/find-write-sites.json b/fixtures/golden/minimal/find-write-sites.json index 7e4faa18..b006a54a 100644 --- a/fixtures/golden/minimal/find-write-sites.json +++ b/fixtures/golden/minimal/find-write-sites.json @@ -2,7 +2,7 @@ { "file_path": "src/components/shop/ProductCard.tsx", "name": "perms", - "line_start": 12, + "line_start": 11, "column_start": 8, "column_end": 13, "kind": "value", @@ -12,7 +12,7 @@ { "file_path": "src/components/shop/ShopButton.tsx", "name": "perms", - "line_start": 8, + "line_start": 9, "column_start": 8, "column_end": 13, "kind": "value", diff --git a/fixtures/golden/minimal/high-complexity-untested.json b/fixtures/golden/minimal/high-complexity-untested.json new file mode 100644 index 00000000..9166acdf --- /dev/null +++ b/fixtures/golden/minimal/high-complexity-untested.json @@ -0,0 +1,11 @@ +[ + { + "name": "labyrinth", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 22, + "line_end": 83, + "complexity": 19, + "coverage_pct": 0 + } +] diff --git a/fixtures/golden/minimal/index-summary.json b/fixtures/golden/minimal/index-summary.json index 14fe02e0..b4745bbb 100644 --- a/fixtures/golden/minimal/index-summary.json +++ b/fixtures/golden/minimal/index-summary.json @@ -1,9 +1,9 @@ [ { - "files": 20, - "symbols": 51, - "imports": 12, + "files": 27, + "symbols": 70, + "imports": 19, "components": 2, - "dependencies": 11 + "dependencies": 17 } ] diff --git a/fixtures/golden/minimal/jsdoc-tags-createClient.json b/fixtures/golden/minimal/jsdoc-tags-createClient.json new file mode 100644 index 00000000..05a4b64c --- /dev/null +++ b/fixtures/golden/minimal/jsdoc-tags-createClient.json @@ -0,0 +1,17 @@ +[ + { + "tag": "@param", + "name": "config", + "type_text": null + }, + { + "tag": "@returns", + "name": null, + "type_text": null + }, + { + "tag": "@throws", + "name": null, + "type_text": "Error" + } +] diff --git a/fixtures/golden/minimal/jsx-attributes-product-card.json b/fixtures/golden/minimal/jsx-attributes-product-card.json new file mode 100644 index 00000000..1866bcde --- /dev/null +++ b/fixtures/golden/minimal/jsx-attributes-product-card.json @@ -0,0 +1,26 @@ +[ + { + "name": "alt", + "value_kind": "string" + }, + { + "name": "data-id", + "value_kind": "expression" + }, + { + "name": "hidden", + "value_kind": "expression" + }, + { + "name": "src", + "value_kind": "string" + }, + { + "name": "type", + "value_kind": "string" + }, + { + "name": "…spread", + "value_kind": "spread" + } +] diff --git a/fixtures/golden/minimal/large-functions.json b/fixtures/golden/minimal/large-functions.json index fe51488c..c5c0d337 100644 --- a/fixtures/golden/minimal/large-functions.json +++ b/fixtures/golden/minimal/large-functions.json @@ -1 +1,13 @@ -[] +[ + { + "name": "labyrinth", + "kind": "function", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 22, + "line_end": 83, + "body_line_count": 62, + "param_count": 1, + "complexity": 19, + "nesting_depth": 5 + } +] diff --git a/fixtures/golden/minimal/markers-all-kinds.json b/fixtures/golden/minimal/markers-all-kinds.json index 7daab66b..d5c659de 100644 --- a/fixtures/golden/minimal/markers-all-kinds.json +++ b/fixtures/golden/minimal/markers-all-kinds.json @@ -1,7 +1,7 @@ [ { "kind": "FIXME", - "n": 1 + "n": 2 }, { "kind": "HACK", @@ -13,6 +13,6 @@ }, { "kind": "TODO", - "n": 2 + "n": 1 } ] diff --git a/fixtures/golden/minimal/markers-by-kind.json b/fixtures/golden/minimal/markers-by-kind.json new file mode 100644 index 00000000..806f99c3 --- /dev/null +++ b/fixtures/golden/minimal/markers-by-kind.json @@ -0,0 +1,18 @@ +[ + { + "kind": "FIXME", + "count": 2 + }, + { + "kind": "HACK", + "count": 2 + }, + { + "kind": "NOTE", + "count": 2 + }, + { + "kind": "TODO", + "count": 1 + } +] diff --git a/fixtures/golden/minimal/refactor-risk-ranking.json b/fixtures/golden/minimal/refactor-risk-ranking.json new file mode 100644 index 00000000..211524b9 --- /dev/null +++ b/fixtures/golden/minimal/refactor-risk-ranking.json @@ -0,0 +1,122 @@ +[ + { + "file_path": "src/api/client.ts", + "exported_count": 7, + "fan_in": 2, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 300 + }, + { + "file_path": "src/lib/cache.ts", + "exported_count": 2, + "fan_in": 2, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 300 + }, + { + "file_path": "src/lib/complexity-fixture.ts", + "exported_count": 2, + "fan_in": 2, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 300 + }, + { + "file_path": "src/utils/date.ts", + "exported_count": 4, + "fan_in": 3, + "avg_coverage_pct": 33.3, + "measured_symbols": 3, + "risk_score": 266.7 + }, + { + "file_path": "src/api/decorated.ts", + "exported_count": 1, + "fan_in": 1, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 200 + }, + { + "file_path": "src/lib/store.ts", + "exported_count": 2, + "fan_in": 1, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 200 + }, + { + "file_path": "src/components/shop/ApiBridge.tsx", + "exported_count": 1, + "fan_in": 0, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 100 + }, + { + "file_path": "src/consumer.ts", + "exported_count": 2, + "fan_in": 0, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 100 + }, + { + "file_path": "src/env.ts", + "exported_count": 2, + "fan_in": 0, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 100 + }, + { + "file_path": "src/orphan.ts", + "exported_count": 2, + "fan_in": 0, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 100 + }, + { + "file_path": "src/types/status.ts", + "exported_count": 1, + "fan_in": 0, + "avg_coverage_pct": 0, + "measured_symbols": 0, + "risk_score": 100 + }, + { + "file_path": "src/utils/format.ts", + "exported_count": 2, + "fan_in": 1, + "avg_coverage_pct": 50, + "measured_symbols": 2, + "risk_score": 100 + }, + { + "file_path": "src/components/shop/ShopButton.tsx", + "exported_count": 2, + "fan_in": 1, + "avg_coverage_pct": 66.7, + "measured_symbols": 3, + "risk_score": 66.7 + }, + { + "file_path": "src/components/shop/ProductCard.tsx", + "exported_count": 1, + "fan_in": 0, + "avg_coverage_pct": 100, + "measured_symbols": 1, + "risk_score": 0 + }, + { + "file_path": "src/usePermissions.ts", + "exported_count": 1, + "fan_in": 2, + "avg_coverage_pct": 100, + "measured_symbols": 1, + "risk_score": 0 + } +] diff --git a/fixtures/golden/minimal/suppressions-orphan.json b/fixtures/golden/minimal/suppressions-orphan.json new file mode 100644 index 00000000..97046f3c --- /dev/null +++ b/fixtures/golden/minimal/suppressions-orphan.json @@ -0,0 +1,7 @@ +[ + { + "file_path": "src/orphan.ts", + "line_number": 2, + "recipe_id": "unimported-exports" + } +] diff --git a/fixtures/golden/minimal/tests-by-file.json b/fixtures/golden/minimal/tests-by-file.json index fe51488c..a62eb864 100644 --- a/fixtures/golden/minimal/tests-by-file.json +++ b/fixtures/golden/minimal/tests-by-file.json @@ -1 +1,11 @@ -[] +[ + { + "file_path": "src/__tests__/smoke.test.ts", + "framework": "vitest", + "describes": 2, + "tests": 5, + "skipped": 1, + "only_marks": 1, + "todos": 1 + } +] diff --git a/fixtures/golden/minimal/text-in-deprecated-functions.json b/fixtures/golden/minimal/text-in-deprecated-functions.json new file mode 100644 index 00000000..8450766f --- /dev/null +++ b/fixtures/golden/minimal/text-in-deprecated-functions.json @@ -0,0 +1,10 @@ +[ + { + "symbol": "epochMs", + "kind": "function", + "file_path": "src/utils/format.ts", + "line_start": 5, + "line_end": 8, + "coverage_pct": 0 + } +] diff --git a/fixtures/golden/minimal/unimported-exports.json b/fixtures/golden/minimal/unimported-exports.json new file mode 100644 index 00000000..8bd550f7 --- /dev/null +++ b/fixtures/golden/minimal/unimported-exports.json @@ -0,0 +1,142 @@ +[ + { + "name": "Transport", + "kind": "type", + "file_path": "src/api/client.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "handshake", + "kind": "value", + "file_path": "src/api/client.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "legacyClient", + "kind": "value", + "file_path": "src/api/client.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "openSocket", + "kind": "value", + "file_path": "src/api/client.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "setupTransport", + "kind": "value", + "file_path": "src/api/client.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "ApiBridge", + "kind": "value", + "file_path": "src/components/shop/ApiBridge.tsx", + "is_default": 0, + "re_export_source": null + }, + { + "name": "ProductCard", + "kind": "value", + "file_path": "src/components/shop/ProductCard.tsx", + "is_default": 0, + "re_export_source": null + }, + { + "name": "FormatPrice", + "kind": "value", + "file_path": "src/components/shop/ShopButton.tsx", + "is_default": 0, + "re_export_source": null + }, + { + "name": "prefetch", + "kind": "value", + "file_path": "src/consumer.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "run", + "kind": "value", + "file_path": "src/consumer.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "fixtureApiKey", + "kind": "value", + "file_path": "src/env.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "nodeEnv", + "kind": "value", + "file_path": "src/env.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "deeplyNested", + "kind": "value", + "file_path": "src/lib/complexity-fixture.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "ignoredExport", + "kind": "value", + "file_path": "src/orphan.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "orphanHelper", + "kind": "value", + "file_path": "src/orphan.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "OrderStatus", + "kind": "value", + "file_path": "src/types/status.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "_epochSeconds", + "kind": "value", + "file_path": "src/utils/date.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "_hiResEpoch", + "kind": "value", + "file_path": "src/utils/date.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "nanoseconds", + "kind": "value", + "file_path": "src/utils/date.ts", + "is_default": 0, + "re_export_source": null + }, + { + "name": "nowIso", + "kind": "value", + "file_path": "src/utils/format.ts", + "is_default": 0, + "re_export_source": null + } +] diff --git a/fixtures/golden/minimal/untested-and-dead.json b/fixtures/golden/minimal/untested-and-dead.json index 6472bb47..253b14b6 100644 --- a/fixtures/golden/minimal/untested-and-dead.json +++ b/fixtures/golden/minimal/untested-and-dead.json @@ -2,25 +2,49 @@ { "name": "legacyClient", "file_path": "src/api/client.ts", - "line_start": 42, + "line_start": 46, + "coverage_pct": 0 + }, + { + "name": "ApiBridge", + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 4, "coverage_pct": 0 }, { "name": "FormatPrice", "file_path": "src/components/shop/ShopButton.tsx", - "line_start": 3, + "line_start": 4, + "coverage_pct": 0 + }, + { + "name": "ShopButton", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 8, "coverage_pct": 0 }, { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 13, + "line_start": 15, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 20, + "line_start": 22, + "coverage_pct": 0 + }, + { + "name": "ignoredExport", + "file_path": "src/orphan.ts", + "line_start": 2, + "coverage_pct": 0 + }, + { + "name": "orphanHelper", + "file_path": "src/orphan.ts", + "line_start": 7, "coverage_pct": 0 }, { diff --git a/fixtures/golden/minimal/visibility-tags.json b/fixtures/golden/minimal/visibility-tags.json index a8786270..25245993 100644 --- a/fixtures/golden/minimal/visibility-tags.json +++ b/fixtures/golden/minimal/visibility-tags.json @@ -31,7 +31,7 @@ "kind": "function", "visibility": "beta", "file_path": "src/utils/format.ts", - "line_start": 13, + "line_start": 14, "signature": "nowIso(): string", "doc_comment": "@beta Fixture for the `visibility-tags` recipe — lock-in for the four-tag\ncoverage (alongside `@internal`, `@alpha`, `@private`)." } diff --git a/fixtures/golden/minimal/worst-covered-exports.json b/fixtures/golden/minimal/worst-covered-exports.json index 9ebc3aa6..d7742b7e 100644 --- a/fixtures/golden/minimal/worst-covered-exports.json +++ b/fixtures/golden/minimal/worst-covered-exports.json @@ -2,49 +2,61 @@ { "name": "createClient", "file_path": "src/api/client.ts", - "line_start": 16, + "line_start": 20, "coverage_pct": 0 }, { "name": "setupTransport", "file_path": "src/api/client.ts", - "line_start": 23, + "line_start": 27, "coverage_pct": 0 }, { "name": "openSocket", "file_path": "src/api/client.ts", - "line_start": 29, + "line_start": 33, "coverage_pct": 0 }, { "name": "handshake", "file_path": "src/api/client.ts", - "line_start": 33, + "line_start": 37, "coverage_pct": 0 }, { "name": "legacyClient", "file_path": "src/api/client.ts", - "line_start": 42, + "line_start": 46, + "coverage_pct": 0 + }, + { + "name": "ApiBridge", + "file_path": "src/components/shop/ApiBridge.tsx", + "line_start": 4, "coverage_pct": 0 }, { "name": "FormatPrice", "file_path": "src/components/shop/ShopButton.tsx", - "line_start": 3, + "line_start": 4, + "coverage_pct": 0 + }, + { + "name": "ShopButton", + "file_path": "src/components/shop/ShopButton.tsx", + "line_start": 8, "coverage_pct": 0 }, { "name": "prefetch", "file_path": "src/consumer.ts", - "line_start": 13, + "line_start": 15, "coverage_pct": 0 }, { "name": "run", "file_path": "src/consumer.ts", - "line_start": 20, + "line_start": 22, "coverage_pct": 0 }, { @@ -59,6 +71,18 @@ "line_start": 16, "coverage_pct": 0 }, + { + "name": "deeplyNested", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 6, + "coverage_pct": 0 + }, + { + "name": "labyrinth", + "file_path": "src/lib/complexity-fixture.ts", + "line_start": 22, + "coverage_pct": 0 + }, { "name": "read", "file_path": "src/lib/store.ts", @@ -72,51 +96,27 @@ "coverage_pct": 0 }, { - "name": "_epochSeconds", - "file_path": "src/utils/date.ts", - "line_start": 12, + "name": "ignoredExport", + "file_path": "src/orphan.ts", + "line_start": 2, "coverage_pct": 0 }, { - "name": "nanoseconds", - "file_path": "src/utils/date.ts", - "line_start": 19, + "name": "orphanHelper", + "file_path": "src/orphan.ts", + "line_start": 7, "coverage_pct": 0 }, { - "name": "_hiResEpoch", + "name": "_epochSeconds", "file_path": "src/utils/date.ts", - "line_start": 26, - "coverage_pct": 0 - }, - { - "name": "epochMs", - "file_path": "src/utils/format.ts", - "line_start": 5, + "line_start": 12, "coverage_pct": 0 }, { - "name": "ProductCard", - "file_path": "src/components/shop/ProductCard.tsx", - "line_start": 11, - "coverage_pct": 100 - }, - { - "name": "ShopButton", - "file_path": "src/components/shop/ShopButton.tsx", - "line_start": 7, - "coverage_pct": 100 - }, - { - "name": "usePermissions", - "file_path": "src/usePermissions.ts", - "line_start": 1, - "coverage_pct": 100 - }, - { - "name": "now", + "name": "nanoseconds", "file_path": "src/utils/date.ts", - "line_start": 5, - "coverage_pct": 100 + "line_start": 19, + "coverage_pct": 0 } ] diff --git a/fixtures/golden/scenarios.json b/fixtures/golden/scenarios.json index 69813090..4b9e1194 100644 --- a/fixtures/golden/scenarios.json +++ b/fixtures/golden/scenarios.json @@ -285,6 +285,81 @@ "id": "unused-type-members", "prompt": "Members of exported types whose owning type has no detectable importer (advisory; field-level enumeration of `unimported-exports`)", "recipe": "unused-type-members" + }, + { + "id": "boundary-violations", + "prompt": "Config-driven architecture boundary violations (ui-no-api)", + "recipe": "boundary-violations" + }, + { + "id": "unimported-exports", + "prompt": "Exports with no direct importer (orphanHelper)", + "recipe": "unimported-exports" + }, + { + "id": "components-by-hooks", + "prompt": "Components ranked by hooks_used count", + "recipe": "components-by-hooks" + }, + { + "id": "components-touching-deprecated", + "prompt": "Components calling or hooking @deprecated symbols", + "recipe": "components-touching-deprecated" + }, + { + "id": "files-largest", + "prompt": "Largest indexed files by line_count", + "recipe": "files-largest" + }, + { + "id": "fan-in", + "prompt": "Top files by import fan-in", + "recipe": "fan-in" + }, + { + "id": "fan-out", + "prompt": "Top files by import fan-out", + "recipe": "fan-out" + }, + { + "id": "refactor-risk-ranking", + "prompt": "Per-file refactor risk (fan-in × coverage gap)", + "recipe": "refactor-risk-ranking" + }, + { + "id": "high-complexity-untested", + "prompt": "High cyclomatic complexity + low coverage", + "recipe": "high-complexity-untested" + }, + { + "id": "text-in-deprecated-functions", + "prompt": "FTS TODO/FIXME/HACK in @deprecated functions with low coverage (requires fts5)", + "recipe": "text-in-deprecated-functions" + }, + { + "id": "markers-by-kind", + "prompt": "Marker inventory by kind (recipe form of markers-all-kinds)", + "recipe": "markers-by-kind" + }, + { + "id": "suppressions-orphan", + "prompt": "codemap-ignore suppressions registered at index time", + "sql": "SELECT file_path, line_number, recipe_id FROM suppressions ORDER BY file_path, line_number" + }, + { + "id": "enum-order-status", + "prompt": "Enum symbol extraction", + "sql": "SELECT name, kind, file_path FROM symbols WHERE name = 'OrderStatus'" + }, + { + "id": "jsdoc-tags-createClient", + "prompt": "Structured JSDoc tags on createClient", + "sql": "SELECT j.tag, j.name, j.type_text FROM jsdoc_tags j JOIN symbols s ON s.id = j.symbol_id WHERE s.name = 'createClient' AND s.file_path = 'src/api/client.ts' ORDER BY j.tag, j.name" + }, + { + "id": "jsx-attributes-product-card", + "prompt": "JSX attribute substrate on ProductCard", + "sql": "SELECT a.name, a.value_kind FROM jsx_attributes a JOIN jsx_elements e ON e.id = a.element_id WHERE e.file_path = 'src/components/shop/ProductCard.tsx' ORDER BY a.name" } ] } diff --git a/fixtures/minimal/.codemap/config.json b/fixtures/minimal/.codemap/config.json new file mode 100644 index 00000000..0858beb0 --- /dev/null +++ b/fixtures/minimal/.codemap/config.json @@ -0,0 +1,10 @@ +{ + "boundaries": [ + { + "name": "ui-no-api", + "from_glob": "src/components/**", + "to_glob": "src/api/**" + } + ], + "fts5": true +} diff --git a/fixtures/minimal/README.md b/fixtures/minimal/README.md index aea44ecf..616a3ea5 100644 --- a/fixtures/minimal/README.md +++ b/fixtures/minimal/README.md @@ -4,24 +4,32 @@ Stable tree exercising every codemap surface — used by `src/benchmark.ts`, gol ## What's exercised -| Codemap surface | Fixture coverage | -| --------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `symbols` (function / const / interface / class) | `usePermissions`, `createClient`, `setupTransport`, `openSocket`, `handshake`, `legacyClient`, `now`, `nanoseconds`, `_epochSeconds`, `_hiResEpoch`, `epochMs`, `nowIso`, `FormatPrice`, `ShopButton`, `ProductCard`, `get`, `invalidate`, `read`, `write`, `run` | -| `imports` / `exports` (named + default + re-export) | `consumer.ts` named imports; `components/shop/index.ts` barrel re-exports; `ShopButton.default.ts` default export | -| `dependencies` (resolved file→file edges) | TS imports across `api/`, `lib/`, `components/shop/`, `utils/`, `usePermissions` | -| `components` (React) | `ShopButton`, `ProductCard` (both call `usePermissions` — fan-in) | -| `calls` (caller→callee, depth >1, with cycle) | `run → createClient → setupTransport → openSocket → handshake`; non-cyclic `cache.get → store.read`; 2-node cycle `cache.invalidate ↔ store.write` | -| `markers` (TODO / FIXME / HACK / NOTE) | `notes.md` + `consumer.ts` (`XXX` is not yet a recognised kind) | -| `type_members` | `ClientConfig`, `Transport`, `ProductCardProps` | -| Visibility tags (`@internal` / `@beta` / `@alpha` / `@private`) | `_epochSeconds`, `nowIso`, `nanoseconds`, `_hiResEpoch` | -| `@deprecated` | `now`, `legacyClient`, `epochMs` (3 rows for SARIF / GH-annotations) | -| `css_variables` | `theme.css` (`--color-brand`, `--spacing-md`) | -| `css_classes` | `theme.css` (`.container`), `button.module.css` (`.primary`) | -| `css_keyframes` | `button.module.css` (`fadeIn`) | -| `--group-by owner` | `CODEOWNERS` (4 owners) | -| Project-local recipes | `.codemap/recipes/shop-symbols.{sql,md}` (with frontmatter actions) — file shape valid; loader currently runs at parse time before bootstrap, so `--recipe shop-symbols` is rejected as "unknown" until that's deferred to the runner (known limitation) | -| Self-managed `.gitignore` | `.codemap/.gitignore` (codemap-managed) | -| `coverage` (Istanbul + LCOV ingest) | `coverage/coverage-final.json` (Istanbul) + `coverage/lcov.info` (LCOV) — equivalent partial coverage shape; bundled recipes `untested-and-dead`, `files-by-coverage`, `worst-covered-exports` exercise the `coverage ↔ symbols` join across exported functions of every visibility tag (`@deprecated`, `@internal`, `@alpha`, `@private`, untagged) | +| Codemap surface | Fixture coverage | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Tier 1 — position + calls + imports** | Column-precise `symbols` / `imports` / `markers`; `calls.{args_count,is_method_call,is_constructor_call,is_optional_chain}` (`consumer.ts` spread + `new ApiCache()` + optional chain); side-effect `import_specifiers` (`polyfill.ts`); `import_id` FK | +| **Tier 2 — references / bindings** | `find-references`, `find-write-sites`, `find-symbol-references`, `find-re-exported-bindings` via shop barrel + `consumer.ts` | +| **Tier 3 — JSX** | `ProductCard.tsx` — fragments, nested elements, spread/boolean/expression attrs, self-closing ``, lowercase tags | +| **Tier 4 — types on symbols** | `return_type` / `is_async` on `prefetch`; `@param` / `@returns` / `@throws` → `jsdoc_tags` on `createClient` | +| **Tier 5 — behavioral** | `await` in loop (`prefetch`); swallowed catch (`cache.ts`); `@sealed` decorator (`decorated.ts`); `@throws` jsdoc | +| **Tier 6 — module graph** | `files.is_barrel` (`components/shop/index.ts`); `has_side_effects` (`polyfill.ts`); `dynamic_imports` (`prefetch`) | +| **`symbols`** | function / const / interface / class / enum / method; visibility + `@deprecated` tags | +| **`imports` / `exports`** | Named, default, namespace (`date` util), re-exports, side-effect import | +| **`dependencies`** | Resolved edges + 2-node cycle (`cache ↔ store`) + boundary violation (`ApiBridge → api`) | +| **`components`** | `ShopButton`, `ProductCard` — `usePermissions` fan-in; `components-touching-deprecated` via `now()` | +| **`calls`** | Depth >1 chain (`createClient → … → handshake`); cycle edges; optional chain; constructor; spread args | +| **`markers`** | `notes.md` + inline `FIXME`/`HACK` in `consumer.ts` / `format.ts` | +| **`type_members`** | `ClientConfig`, `Transport`, `ProductCardProps` | +| **`css_*`** | Variables, classes, keyframes, `@import` | +| **`test_suites`** | `src/__tests__/smoke.test.ts` — skip / only / todo / nested describe (vitest) | +| **`file_metrics` / complexity** | `lib/complexity-fixture.ts` — `large-functions`, `deeply-nested-functions`, `high-complexity-untested` | +| **`runtime_markers`** | `console.*`, `process.env` (`env.ts`), `throw` path in cache | +| **`suppressions`** | `codemap-ignore-next-line` in `orphan.ts` | +| **`boundaries`** | `.codemap/config.json` — `ui-no-api` deny rule → `boundary-violations` recipe | +| **`fts5`** | `fts5: true` in config — powers `text-in-deprecated-functions` | +| **`coverage`** | Istanbul + LCOV under `coverage/` — killer recipes + refactor-risk | +| **`orphan` exports** | `orphan.ts` — `unimported-exports` | +| **Project-local recipes** | `.codemap/recipes/shop-symbols.{sql,md}` | +| **`CODEOWNERS`** | `--group-by owner` | ## Use @@ -29,20 +37,14 @@ Stable tree exercising every codemap surface — used by `src/benchmark.ts`, gol # Index the fixture from the codemap repo CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun run dev --full +# Golden queries (updates goldens after fixture changes) +bun run test:golden -- --update + # Benchmark CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun run benchmark -# Project-local recipe (known limitation: currently rejected as "unknown" — see -# the "Project-local recipes" row above; will work once recipe loading is -# deferred past bootstrap) -CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts query --recipe shop-symbols --json - -# Static coverage ingest — Istanbul (every modern JS test runner that emits -# coverage-final.json) or LCOV (e.g. `bun test --coverage`). Format auto-detected. +# Coverage ingest + killer recipe CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts ingest-coverage coverage/coverage-final.json -CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts ingest-coverage coverage/lcov.info - -# After ingest — the killer recipe (exported + no callers + zero coverage) CODEMAP_ROOT="$(pwd)/fixtures/minimal" bun src/index.ts query --recipe untested-and-dead --json ``` diff --git a/fixtures/minimal/src/__tests__/smoke.test.ts b/fixtures/minimal/src/__tests__/smoke.test.ts new file mode 100644 index 00000000..658faf8f --- /dev/null +++ b/fixtures/minimal/src/__tests__/smoke.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; + +import { labyrinth } from "../lib/complexity-fixture"; + +describe("smoke", () => { + it.skip("skipped example", () => { + expect(labyrinth(0)).toBe(0); + }); + + it.only("focused example", () => { + expect(labyrinth(1)).toBeGreaterThanOrEqual(0); + }); + + it.todo("todo example"); + + it("passing", () => { + expect(1).toBe(1); + }); +}); + +describe("nested suite", () => { + it("inner case", () => { + expect(true).toBe(true); + }); +}); diff --git a/fixtures/minimal/src/api/client.ts b/fixtures/minimal/src/api/client.ts index f868650a..b5cc09c8 100644 --- a/fixtures/minimal/src/api/client.ts +++ b/fixtures/minimal/src/api/client.ts @@ -12,7 +12,11 @@ export interface Transport { readonly handshakeMs: number; } -/** @throws {Error} when config is invalid */ +/** + * @param config Client connection settings + * @returns A lightweight client handle + * @throws {Error} when config is invalid + */ export function createClient(config?: ClientConfig) { const transport = setupTransport( config?.baseUrl ?? "https://api.example.com", diff --git a/fixtures/minimal/src/components/shop/ApiBridge.tsx b/fixtures/minimal/src/components/shop/ApiBridge.tsx new file mode 100644 index 00000000..eace03b0 --- /dev/null +++ b/fixtures/minimal/src/components/shop/ApiBridge.tsx @@ -0,0 +1,7 @@ +import { createClient } from "~/api/client"; + +/** Boundary-violation fixture: `src/components/**` → `src/api/**`. */ +export function ApiBridge() { + createClient(); + return null; +} diff --git a/fixtures/minimal/src/components/shop/ProductCard.tsx b/fixtures/minimal/src/components/shop/ProductCard.tsx index 9b8a4107..ef571dc4 100644 --- a/fixtures/minimal/src/components/shop/ProductCard.tsx +++ b/fixtures/minimal/src/components/shop/ProductCard.tsx @@ -1,19 +1,22 @@ import { usePermissions } from "../../usePermissions"; +import { now } from "../../utils/date"; interface ProductCardProps { readonly id: number; readonly title: string; } -// React component fixture — exercises `components` table fan-in to -// `usePermissions` (also used by ShopButton.tsx); pair with the barrel -// re-export in `./index.ts` to surface fan-in via the `dependencies` graph. +// React component fixture — JSX substrate (fragments, attrs, nesting, lowercase tags). export function ProductCard(props: ProductCardProps) { const perms = usePermissions(); + const spread = { className: "card" }; return ( -
-

{props.title}

- {perms.canEdit ? : null} -
+ <> + + ); } diff --git a/fixtures/minimal/src/components/shop/ShopButton.tsx b/fixtures/minimal/src/components/shop/ShopButton.tsx index f0dd05d0..168c6b12 100644 --- a/fixtures/minimal/src/components/shop/ShopButton.tsx +++ b/fixtures/minimal/src/components/shop/ShopButton.tsx @@ -1,4 +1,5 @@ import { usePermissions } from "../../usePermissions"; +import { now } from "../../utils/date"; export function FormatPrice(cents: number): string { return `$${(cents / 100).toFixed(2)}`; @@ -6,6 +7,7 @@ export function FormatPrice(cents: number): string { export function ShopButton() { const perms = usePermissions(); + const _stamp = now(); return (