Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 26 additions & 24 deletions .agents/rules/codemap.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,30 +96,32 @@ Violating this order is wrong even if you get the right answer — it wastes tim

If the question looks like any of these → use the index:

| Question shape | Table(s) |
| ------------------------------------------------------------- | -------------------------------------------------------- |
| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) |
| "Where is X defined?" | `symbols` |
| "What does file X export?" | `exports` |
| "What hooks does component X use?" / "List React components" | `components` |
| "What are the CSS variables/tokens for X?" | `css_variables` |
| "Find all TODOs/FIXMEs" | `markers` |
| "Who depends on file X?" / "What does file X depend on?" | `dependencies` |
| "How many files/symbols/components are there?" | any table with `COUNT(*)` |
| "What are the CSS classes in X?" | `css_classes` |
| "What keyframe animations exist?" | `css_keyframes` |
| "What fields does interface/type X have?" | `type_members` |
| "Is symbol X deprecated?" / "What does X do?" | `symbols` (`doc_comment`) |
| "What's `@internal` / `@beta` / `@alpha` / `@private`?" | `symbols.visibility` (parsed JSDoc tag — not regex) |
| "Who calls X?" / "What does X call?" | `calls` |
| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `ingest-coverage`) |
| "What's structurally dead AND untested?" | `--recipe untested-and-dead` |
| "Rank files by test coverage" | `--recipe files-by-coverage` |
| "Worst-covered exported functions" | `--recipe worst-covered-exports` |
| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` |
| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` |
| "Which exports has nobody imported?" | `--recipe unimported-exports` |
| "Find @deprecated functions with TODO/FIXME and low coverage" | `--recipe text-in-deprecated-functions` (needs FTS5 on) |
| Question shape | Table(s) |
| ------------------------------------------------------------- | --------------------------------------------------------- |
| "What/which files import X?" | `imports` (by `source`) or `dependencies` (by `to_path`) |
| "Where is X defined?" | `symbols` |
| "What does file X export?" | `exports` |
| "What hooks does component X use?" / "List React components" | `components` |
| "What are the CSS variables/tokens for X?" | `css_variables` |
| "Find all TODOs/FIXMEs" | `markers` |
| "Who depends on file X?" / "What does file X depend on?" | `dependencies` |
| "How many files/symbols/components are there?" | any table with `COUNT(*)` |
| "What are the CSS classes in X?" | `css_classes` |
| "What keyframe animations exist?" | `css_keyframes` |
| "What fields does interface/type X have?" | `type_members` |
| "Is symbol X deprecated?" / "What does X do?" | `symbols` (`doc_comment`) |
| "What's `@internal` / `@beta` / `@alpha` / `@private`?" | `symbols.visibility` (parsed JSDoc tag — not regex) |
| "Who calls X?" / "What does X call?" | `calls` |
| "Is symbol X tested?" / "What's the coverage of file Y?" | `coverage` (after `ingest-coverage`) |
| "What's structurally dead AND untested?" | `--recipe untested-and-dead` |
| "Rank files by test coverage" | `--recipe files-by-coverage` |
| "Worst-covered exported functions" | `--recipe worst-covered-exports` |
| "Which components touch deprecated APIs?" | `--recipe components-touching-deprecated` |
| "What's risky to refactor right now?" | `--recipe refactor-risk-ranking` |
| "Which exports has nobody imported?" | `--recipe unimported-exports` |
| "Find @deprecated functions with TODO/FIXME and low coverage" | `--recipe text-in-deprecated-functions` (needs FTS5 on) |
| "What's high-complexity AND undertested?" | `--recipe high-complexity-untested` |
| "What's the cyclomatic complexity of symbol X?" | `SELECT name, complexity FROM symbols WHERE name = '...'` |

## When Grep / Read IS appropriate

Expand Down
35 changes: 18 additions & 17 deletions .agents/skills/codemap/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ After **`bun run build`**, **`node dist/index.mjs query …`** or a linked **`co

Replace placeholders (`'...'`) with your module path, file glob, or symbol name.

**CLI shortcuts:** **`bun src/index.ts query --json --recipe <id>`** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe <id>`** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`bun src/index.ts query --print-sql <id>`** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`text-in-deprecated-functions`** (FTS5 ⨯ symbols ⨯ coverage demo — needs `--with-fts` enabled), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`** — see **`bun src/index.ts query --help`**.
**CLI shortcuts:** **`bun src/index.ts query --json --recipe <id>`** runs bundled SQL (preferred for agents). **`bun src/index.ts query --recipe <id>`** without **`--json`** prints a table. **`bun src/index.ts query --recipes-json`** prints every bundled recipe (**`id`**, **`description`**, **`sql`**, optional **`actions`**) as JSON (no index / DB required). **`bun src/index.ts query --print-sql <id>`** prints one recipe’s SQL only. Ids include **`fan-out`**, **`fan-out-sample`** (**`GROUP_CONCAT`** samples), **`fan-out-sample-json`** (same, but **`json_group_array`** — needs SQLite JSON1), **`fan-in`**, **`index-summary`**, **`files-largest`**, **`components-by-hooks`**, **`components-touching-deprecated`** (UNION of hook + call paths to `@deprecated` symbols), **`markers-by-kind`**, **`deprecated-symbols`**, **`refactor-risk-ranking`** (per-file `(fan_in + 1) × (100 - avg_coverage_pct)`), **`high-complexity-untested`** (cyclomatic complexity ≥ 10 + coverage < 50%; per-function), **`text-in-deprecated-functions`** (FTS5 ⨯ symbols ⨯ coverage demo — needs `--with-fts` enabled), **`unimported-exports`** (exports with no detectable importer; v1 doesn't follow re-export chains — see recipe `.md` for caveats), **`visibility-tags`**, **`barrel-files`**, **`files-hashes`**, **`untested-and-dead`** (exported AND uncalled AND uncovered), **`files-by-coverage`** (per-file rollup of statement coverage), **`worst-covered-exports`** (lowest-covered exported symbols) — see **`bun src/index.ts query --help`**.

**Output flags** (compose with **`--recipe`** or ad-hoc SQL):

Expand Down Expand Up @@ -132,22 +132,23 @@ LIMIT 10

### `symbols` — Functions, types, interfaces, enums, constants, classes

| Column | Type | Description |
| ----------------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| id | INTEGER PK | Auto-increment ID |
| file_path | TEXT FK | References `files(path)` |
| name | TEXT | Symbol name |
| kind | TEXT | `function`, `class`, `type`, `interface`, `enum`, `const` |
| line_start | INTEGER | Start line (1-based) |
| line_end | INTEGER | End line (1-based) |
| signature | TEXT | Reconstructed signature with generics and return types |
| is_exported | INTEGER | 1 if exported |
| is_default_export | INTEGER | 1 if default export |
| members | TEXT | JSON enum members (NULL for non-enums) |
| doc_comment | TEXT | Leading JSDoc text (cleaned), NULL when absent |
| value | TEXT | Literal value for consts (`"ok"`, `42`, `true`, `null`) |
| parent_name | TEXT | Enclosing symbol name (class/function), NULL = top-level |
| visibility | TEXT | Line-leading JSDoc tag: `public` / `private` / `internal` / `alpha` / `beta`; NULL when absent. First match in document order wins |
| Column | Type | Description |
| ----------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --- | -------------------- |
| id | INTEGER PK | Auto-increment ID |
| file_path | TEXT FK | References `files(path)` |
| name | TEXT | Symbol name |
| kind | TEXT | `function`, `class`, `type`, `interface`, `enum`, `const` |
| line_start | INTEGER | Start line (1-based) |
| line_end | INTEGER | End line (1-based) |
| signature | TEXT | Reconstructed signature with generics and return types |
| is_exported | INTEGER | 1 if exported |
| is_default_export | INTEGER | 1 if default export |
| members | TEXT | JSON enum members (NULL for non-enums) |
| doc_comment | TEXT | Leading JSDoc text (cleaned), NULL when absent |
| value | TEXT | Literal value for consts (`"ok"`, `42`, `true`, `null`) |
| parent_name | TEXT | Enclosing symbol name (class/function), NULL = top-level |
| visibility | TEXT | Line-leading JSDoc tag: `public` / `private` / `internal` / `alpha` / `beta`; NULL when absent. First match in document order wins |
| complexity | REAL | Cyclomatic complexity (`1 + decision points`) for function-shaped symbols. NULL for non-functions and class methods (v1). Powers `--recipe high-complexity-untested`. Decision points: `if`, `while`, `do…while`, `for`/`for-in`/`for-of`, `case X:` (not `default:`), `&&`/` | | `/`??`/`?:`, `catch` |

### `calls` — Function-scoped call edges (deduped per file)

Expand Down
27 changes: 27 additions & 0 deletions .changeset/cyclomatic-complexity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@stainless-code/codemap": patch
---

feat(complexity): cyclomatic complexity column on `symbols` + bundled recipe (research note § 1.4 ship-pick (c))

Adds per-function cyclomatic complexity computed during AST walking. Schema bump `SCHEMA_VERSION` 7 → 8 — first reindex after upgrade triggers a full rebuild via the existing version-mismatch path.

**What lands:**

- New `complexity REAL` column on `symbols`. Computed via McCabe formula (`1 + decision points`) for function-shaped symbols (top-level `function` declarations + arrow-function consts). `NULL` for non-functions (interfaces, types, enums, plain consts) and class methods (v1 limitation; documented in the recipe `.md`).
- Decision points counted: `if`, `while`, `do…while`, `for`, `for…in`, `for…of`, `case X:` arms (not `default:` fall-through), `&&` / `||` / `??` short-circuit operators, `?:` ternary, `catch` clauses.
- New bundled recipe `high-complexity-untested` — function-shaped symbols with complexity ≥ 10 AND measured coverage < 50%. Combines structural + runtime evidence axes; surfaces refactor-priority candidates that single-axis recipes (`untested-and-dead`, `worst-covered-exports`) miss because they're "called but undertested."

**Implementation:**

- Parser visitor (`src/parser.ts`) maintains a `complexityStack` keyed by symbol index. On function entry, pushes counter at 1 + symbol index. Branching-node visitors increment the top counter. On function exit, pops and writes complexity into the symbol row already pushed during entry.
- Nested function declarations get their own stack entries — inner branches don't count toward the outer function. (Standard McCabe — each function counted independently.)

**Pre-v1 patch** per `.agents/lessons.md` "changesets bump policy": schema-bumping changes are minor in semver but pre-v1 we default to patch unless the bump forces a `.codemap.db` rebuild. This one does (column added; auto-detected by `createSchema()` mismatch path) — every consumer's first run after upgrade re-indexes from scratch.

Agent rule + skill lockstep updated per `docs/README.md` Rule 10 — both `templates/agents/` and `.agents/` codemap rule + skill mention the `complexity` column, the new recipe, and the cyclomatic-complexity definition.

**Out of scope:**

- **Class method complexity** — `MethodDefinition` visitor currently doesn't push to the complexity stack. Documented in `high-complexity-untested.md` v1 limitation; refactor opportunity for class-heavy projects.
- **Per-class / per-file rollups** — `complexity` is per-symbol; project-local recipes can `SUM` / `AVG` it as needed.
Loading
Loading