Commit db2f27a
authored
feat(complexity): cyclomatic complexity column + high-complexity-untested recipe (research note § 1.4) (#70)
* feat(complexity): cyclomatic complexity column + high-complexity-untested recipe
Research note § 1.4 ship-pick (c) per § 5 cadence. Schema bump
SCHEMA_VERSION 7 → 8.
Schema:
- symbols.complexity REAL column. NULL for non-function kinds and
class methods (v1 limitation documented in recipe .md).
Parser:
- complexityStack maintained alongside scopeStack. Function entry
pushes {symbolIndex, count: 1}; branching-node visitors increment
top.count; function exit pops + writes count into the already-
pushed symbol row's complexity field.
- McCabe decision points counted: if, while, do-while, for, for-in,
for-of, case X (not default:), &&/||/??, ?:, catch.
Bundled recipe high-complexity-untested:
- Joins symbols (complexity >= 10) with coverage (< 50%).
- Combines structural + runtime evidence axes — surfaces refactor-
priority candidates that untested-and-dead and worst-covered-exports
miss (they catch dead-or-uncalled, this catches called-but-undertested-
AND-branchy).
Empirical sanity check on codemap's own index after reindex:
- extractFileData (parser.ts main visitor) → complexity 108 ✓
- stringifyTypeNode → 42 ✓
- All non-function kinds have NULL complexity ✓
- high-complexity-untested recipe returns 7 functions all from
src/parser.ts (which has 0% coverage; complexity ≥ 10) ✓
Lockstep updates per Rule 10 (templates/agents + .agents):
- Trigger pattern row "What's high-complexity AND undertested?"
- Quick reference row for SELECT name, complexity FROM symbols
- Recipe-id list extended in SKILL.md
Plus architecture.md (schema version 8, complexity column docs),
glossary.md (cyclomatic complexity entry), patch changeset.
Files changed:
- src/db.ts (SCHEMA_VERSION + symbols.complexity column + insertSymbols
bind + SymbolRow optional complexity field)
- src/parser.ts (complexityStack + branching node visitors + push/pop
in FunctionDeclaration / VariableDeclaration arrow-fn paths)
- templates/recipes/high-complexity-untested.{sql,md}
- docs/architecture.md (schema version + symbols column doc)
- docs/glossary.md (new entry)
- templates/agents/rules/codemap.md + .agents/rules/codemap.md
(trigger + quick-ref rows)
- templates/agents/skills/codemap/SKILL.md + .agents/skills/codemap/
SKILL.md (recipe-id list)
- .changeset/cyclomatic-complexity.md (patch)
Verification:
- bun test: 754 pass
- bun run check passes (format, lint, typecheck, 23/23 golden queries)
- Live re-index against codemap source produces sensible complexity
values (parser visitor itself is the highest at 108, which tracks)
* docs(skill): add complexity column to symbols schema in skill files
CodeRabbit catch on PR #70: the high-complexity-untested recipe row was
added to .agents/skills/codemap/SKILL.md but the symbols table schema
section (under "### `symbols` — Functions, types, ...") still listed
columns through `visibility` only, missing the new `complexity REAL`
column. Verified by reading the file — claim was correct.
Both lockstep mirrors (.agents/ + templates/agents/) updated with the
same row:
| complexity | REAL | Cyclomatic complexity (`1 + decision points`)
for function-shaped symbols. NULL for non-functions and class methods
(v1). Powers --recipe high-complexity-untested. Decision points: if,
while, do…while, for/for-in/for-of, case X: (not default:),
&&/||/??/?:, catch |
Per docs/README.md Rule 10 — agent rule + skill schema docs must stay
in lockstep with code-side schema changes. The trigger-pattern row +
recipe-id list were already updated; the schema-table row was the gap.
* fix(complexity): per-function visitors fix multi-declarator misattribution + cleanups
CodeRabbit raised three valid findings on PR #70. All fact-checked
against the code; all correct.
A) docs/architecture.md symbols schema table was malformed:
- Markdown table separator row had extra `| --- | ---` segments because
oxfmt mis-counted columns when the description contained `|` chars
inside `&&`/`||`/`??` backtick spans.
- The complexity row's description was split across THREE cells with
broken backtick fences.
- Fix: restored single-row layout (3 cells: Column | Type | Description)
and rephrased the decision-point list to avoid `|` inside backticks
("short-circuit `&&` / `||` / `??`" instead of "`&&`/`||`/`??`").
B) src/parser.ts complexity misattribution on multi-declarator
VariableDeclaration (e.g. `const a = () => {…}, b = () => {…};`):
Pre-fix: VariableDeclaration enter pushed all declarators' complexity
entries up front. Then visitor traversed `a`'s body — branches
incremented top (= b's entry). Then `b`'s body. Exit pops in reverse
→ symbols[1].complexity = 3 (wrong), symbols[0].complexity = 1
(wrong). Real bug.
Fix: push/pop complexity on the FUNCTION-shaped node visitors
(ArrowFunctionExpression / FunctionExpression) — not on
VariableDeclaration. The VariableDeclaration handler still creates
the symbol row but only RECORDS the symbol → init-node mapping in a
WeakMap. The ArrowFunctionExpression / FunctionExpression enter
handler reads the WeakMap to know which symbol to write back to;
anonymous arrow fns (callbacks, IIFEs) get -1 and just track count
without persistence.
Verified against fixture:
const a = () => { if (1===1) {…} },
b = () => { if (2===2) {…} },
c = () => 5;
→ a=2, b=2, c=1 (correct; pre-fix was a=1, b=3, c=1)
C) popComplexityInto guard was a no-op (callers passed top.symbolIndex,
so the equality check was always true). Simplified to parameterless
popComplexityTop() that always pops + writes back if symbolIndex >= 0.
Folds naturally into the B refactor — every push/pop pair now lives
in a function-shaped visitor.
Also re-ran codemap query against codemap source post-fix:
extractFileData=108, stringifyTypeNode=42, extractClassMembers=18,
extractLiteralValue=15, extractObjectMembers=14
Same scores as pre-fix on these (no FunctionExpression / arrow nesting
in those particular functions, so the bug didn't surface) — confirms
the refactor is a strict improvement, not a regression.
* docs: audit + lift remaining stale references; concise-comments sweep on parser.ts
Fact-checking against codebase post-PR-#69-and-#70 surfaced four stale
spots; concise-comments rule re-applied to recently-authored parser.ts
comments.
DOCS LIFTED (post-FTS5 / Mermaid / complexity merge):
- README.md (root) line 113 — --format enum was missing `mermaid`.
Updated to <text|json|sarif|annotations|mermaid> + added the
bounded-input contract one-liner + 50-edge ceiling note.
Added --with-fts example block alongside (was missing entirely;
README is the canonical CLI surface per docs/README.md Single source
of truth table).
- docs/architecture.md output-formatters paragraph — described only
formatSarif + formatAnnotations; missing formatMermaid + bounded-
input contract. Added formatMermaid description + MERMAID_MAX_EDGES
reference + the no-auto-truncation reasoning (would be a verdict
masquerading as output mode). Updated the --format CLI enum to
include mermaid; same for the MCP tools format union.
- .agents/skills/codemap/SKILL.md + templates/agents/skills/codemap/
SKILL.md — recipe-id list missed three coverage recipes
(untested-and-dead, files-by-coverage, worst-covered-exports)
shipped earlier in PR #65/#56 era. Lockstep update per Rule 10.
Skill now lists 20 of 20 bundled recipe ids.
CONCISE-COMMENTS SWEEP on parser.ts (recently authored):
- Trimmed the 14-line complexityStack JSDoc block to 6 lines. Kept:
the -1 sentinel rationale (non-obvious), the WeakMap rationale (the
bug fix from PR #70 review). Cut: re-stating push/pop semantics
obvious from method names + step-by-step "this then that" prose.
- Removed the "Defer complexity push to..." comment in the
VariableDeclaration handler. The 4-line block restated the design
decision documented one screen up in the complexityStack jsdoc;
cross-ref makes it redundant. Per concise-comments § "Cut" rule:
"Cross-references that save grep time" — keep when they actually do;
cut when they restate.
Verification:
- bun run check: format + lint + typecheck + 23/23 golden ✓
- Recipe count: SQL files = 20, skill mentions = 20 (1:1 match) ✓
- SCHEMA_VERSION = 8 in db.ts; docs/architecture.md says 8 ✓
- complexity column documented in architecture.md + glossary.md ✓
- --with-fts in README.md + architecture.md + glossary.md +
roadmap.md (consumer-facing surfaces all aligned) ✓
- --format mermaid in README.md + architecture.md + glossary.md +
agent rule/skill ✓1 parent 560390b commit db2f27a
12 files changed
Lines changed: 281 additions & 93 deletions
File tree
- .agents
- rules
- skills/codemap
- .changeset
- docs
- src
- templates
- agents
- rules
- skills/codemap
- recipes
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
100 | | - | |
101 | | - | |
102 | | - | |
103 | | - | |
104 | | - | |
105 | | - | |
106 | | - | |
107 | | - | |
108 | | - | |
109 | | - | |
110 | | - | |
111 | | - | |
112 | | - | |
113 | | - | |
114 | | - | |
115 | | - | |
116 | | - | |
117 | | - | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
123 | 125 | | |
124 | 126 | | |
125 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
37 | | - | |
| 37 | + | |
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
| |||
132 | 132 | | |
133 | 133 | | |
134 | 134 | | |
135 | | - | |
136 | | - | |
137 | | - | |
138 | | - | |
139 | | - | |
140 | | - | |
141 | | - | |
142 | | - | |
143 | | - | |
144 | | - | |
145 | | - | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
150 | | - | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
151 | 152 | | |
152 | 153 | | |
153 | 154 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
0 commit comments