From 7c08fa0d99905e81cff7170a6191a33921557d7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Feb 2026 19:31:56 -0700 Subject: [PATCH 1/3] fix: native complexity upsert supplies all 18 Halstead/MI columns The native complexity shortcut was passing only 4 params to the upsert statement which now expects 18 (added by the Halstead/MI migration). This crashed at runtime for natively-parsed functions. Fill Halstead/LOC/MI columns with zeros since native engine intentionally skips WASM AST. Also adds #[napi(js_name = "maxNesting")] to ComplexityMetrics so Rust exports camelCase directly, removing the snake_case fallback in parser.js and complexity.js. Cleans up a duplicate assertion and fixes indentation in go.rs. Impact: 4 functions changed, 4 affected --- crates/codegraph-core/src/complexity.rs | 9 +-------- crates/codegraph-core/src/extractors/go.rs | 2 +- crates/codegraph-core/src/types.rs | 1 + src/complexity.js | 16 +++++++++++++++- src/parser.js | 2 +- 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/codegraph-core/src/complexity.rs b/crates/codegraph-core/src/complexity.rs index c391f8e0..609047a7 100644 --- a/crates/codegraph-core/src/complexity.rs +++ b/crates/codegraph-core/src/complexity.rs @@ -323,14 +323,7 @@ mod tests { let m = compute_js( "function f(x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }", ); - assert_eq!(m.cognitive, 4); // +1 if, +1 else-if, +1 else (from else-if's else_clause), +1 else-if cognitive - // Wait, let me recalculate: - // if: cognitive +1 (nesting 0), cyclomatic +1 - // else-if: cognitive +1, cyclomatic +1 (no nesting) - // else: cognitive +1 - // Total cognitive = 3, cyclomatic = 1 + 1 + 1 = 3 - // Hmm, the else clause wrapping the else-if doesn't add anything (it's detected as else-if wrapper) - // So: if (+1 cog, +1 cyc), else-if (+1 cog, +1 cyc), plain else (+1 cog) + // if (+1 cog, +1 cyc), else-if (+1 cog, +1 cyc), plain else (+1 cog) // cognitive = 3, cyclomatic = 3 assert_eq!(m.cognitive, 3); assert_eq!(m.cyclomatic, 3); diff --git a/crates/codegraph-core/src/extractors/go.rs b/crates/codegraph-core/src/extractors/go.rs index 4f3bd62f..edf18c5b 100644 --- a/crates/codegraph-core/src/extractors/go.rs +++ b/crates/codegraph-core/src/extractors/go.rs @@ -111,7 +111,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) { line: start_line(&member), end_line: Some(end_line(&member)), decorators: None, - complexity: None, + complexity: None, }); } } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index 9fe5bad7..fc790d5f 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub struct ComplexityMetrics { pub cognitive: u32, pub cyclomatic: u32, + #[napi(js_name = "maxNesting")] pub max_nesting: u32, } diff --git a/src/complexity.js b/src/complexity.js index 34614a49..81a0c78f 100644 --- a/src/complexity.js +++ b/src/complexity.js @@ -581,7 +581,21 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp row.id, def.complexity.cognitive, def.complexity.cyclomatic, - def.complexity.maxNesting ?? def.complexity.max_nesting ?? 0, + def.complexity.maxNesting ?? 0, + 0, + 0, + 0, // loc, sloc, commentLines + 0, + 0, + 0, + 0, // halstead n1, n2, bigN1, bigN2 + 0, + 0, + 0, // vocabulary, length, volume + 0, + 0, + 0, // difficulty, effort, bugs + 0, // maintainabilityIndex ); analyzed++; continue; diff --git a/src/parser.js b/src/parser.js index 46ad4312..1af5a527 100644 --- a/src/parser.js +++ b/src/parser.js @@ -136,7 +136,7 @@ function normalizeNativeSymbols(result) { ? { cognitive: d.complexity.cognitive, cyclomatic: d.complexity.cyclomatic, - maxNesting: d.complexity.maxNesting ?? d.complexity.max_nesting, + maxNesting: d.complexity.maxNesting, } : null, })), From c167b91893dae23ed30c7c7e2434e7a52715a385 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Feb 2026 20:16:00 -0700 Subject: [PATCH 2/3] fix: make complexity rules report-only in manifesto engine Complexity rules (cognitive, cyclomatic, maxNesting) now always report warnings but never cause process.exit(1). This keeps codegraph focused on dependency graph analysis rather than acting as a CI linter gate. - Add reportOnly flag to function-level complexity RULE_DEFS - Force fail: null in resolveRules() when reportOnly is set - Remove fail key from complexity defaults in config.js - Add integration test verifying user fail thresholds are ignored Impact: 1 functions changed, 2 affected --- src/config.js | 6 ++-- src/manifesto.js | 6 ++-- tests/integration/manifesto.test.js | 48 +++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/config.js b/src/config.js index b0bceaac..efe38ba9 100644 --- a/src/config.js +++ b/src/config.js @@ -26,9 +26,9 @@ export const DEFAULTS = { ci: { failOnCycles: false, impactThreshold: null }, manifesto: { rules: { - cognitive: { warn: 15, fail: null }, - cyclomatic: { warn: 10, fail: null }, - maxNesting: { warn: 4, fail: null }, + cognitive: { warn: 15 }, + cyclomatic: { warn: 10 }, + maxNesting: { warn: 4 }, maintainabilityIndex: { warn: 20, fail: null }, importCount: { warn: null, fail: null }, exportCount: { warn: null, fail: null }, diff --git a/src/manifesto.js b/src/manifesto.js index 54638a5d..d9cb4444 100644 --- a/src/manifesto.js +++ b/src/manifesto.js @@ -12,18 +12,20 @@ import { debug } from './logger.js'; * defaults: { warn, fail } — null means disabled */ export const RULE_DEFS = [ - { name: 'cognitive', level: 'function', metric: 'cognitive', defaults: { warn: 15, fail: null } }, + { name: 'cognitive', level: 'function', metric: 'cognitive', defaults: { warn: 15, fail: null }, reportOnly: true }, { name: 'cyclomatic', level: 'function', metric: 'cyclomatic', defaults: { warn: 10, fail: null }, + reportOnly: true, }, { name: 'maxNesting', level: 'function', metric: 'max_nesting', defaults: { warn: 4, fail: null }, + reportOnly: true, }, { name: 'importCount', @@ -67,7 +69,7 @@ function resolveRules(userRules) { const user = userRules?.[def.name]; resolved[def.name] = { warn: user?.warn !== undefined ? user.warn : def.defaults.warn, - fail: user?.fail !== undefined ? user.fail : def.defaults.fail, + fail: def.reportOnly ? null : (user?.fail !== undefined ? user.fail : def.defaults.fail), }; } return resolved; diff --git a/tests/integration/manifesto.test.js b/tests/integration/manifesto.test.js index bc876012..d3b0c0bb 100644 --- a/tests/integration/manifesto.test.js +++ b/tests/integration/manifesto.test.js @@ -291,6 +291,54 @@ describe('manifestoData', () => { } }); + test('reportOnly complexity rules ignore user-configured fail thresholds', () => { + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-manifesto-reportonly-')); + fs.mkdirSync(path.join(configDir, '.codegraph')); + const roDbPath = path.join(configDir, '.codegraph', 'graph.db'); + + fs.copyFileSync(dbPath, roDbPath); + + // User attempts to set fail thresholds on complexity rules + fs.writeFileSync( + path.join(configDir, '.codegraphrc.json'), + JSON.stringify({ + manifesto: { + rules: { + cognitive: { warn: 15, fail: 5 }, + cyclomatic: { warn: 10, fail: 3 }, + maxNesting: { warn: 4, fail: 2 }, + }, + }, + }), + ); + + const origCwd = process.cwd(); + try { + process.chdir(configDir); + const data = manifestoData(roDbPath); + + // fail thresholds should be forced to null (reportOnly) + for (const name of ['cognitive', 'cyclomatic', 'maxNesting']) { + const rule = data.rules.find((r) => r.name === name); + expect(rule.thresholds.fail).toBeNull(); + } + + // All violations should be warn-only, never fail + const complexityViolations = data.violations.filter( + (v) => v.rule === 'cognitive' || v.rule === 'cyclomatic' || v.rule === 'maxNesting', + ); + for (const v of complexityViolations) { + expect(v.level).toBe('warn'); + } + + // Overall result should pass (no fail-level violations) + expect(data.passed).toBe(true); + } finally { + process.chdir(origCwd); + fs.rmSync(configDir, { recursive: true, force: true }); + } + }); + test('noCycles rule with fail threshold sets passed=false', () => { const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-manifesto-fail-')); fs.mkdirSync(path.join(configDir, '.codegraph')); From ff69f78b67c6c2da5af9f29e4305ac0e10c1259b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 26 Feb 2026 20:16:22 -0700 Subject: [PATCH 3/3] style: format manifesto.js per biome rules Impact: 1 functions changed, 2 affected --- src/manifesto.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/manifesto.js b/src/manifesto.js index d9cb4444..8fc907ff 100644 --- a/src/manifesto.js +++ b/src/manifesto.js @@ -12,7 +12,13 @@ import { debug } from './logger.js'; * defaults: { warn, fail } — null means disabled */ export const RULE_DEFS = [ - { name: 'cognitive', level: 'function', metric: 'cognitive', defaults: { warn: 15, fail: null }, reportOnly: true }, + { + name: 'cognitive', + level: 'function', + metric: 'cognitive', + defaults: { warn: 15, fail: null }, + reportOnly: true, + }, { name: 'cyclomatic', level: 'function', @@ -69,7 +75,7 @@ function resolveRules(userRules) { const user = userRules?.[def.name]; resolved[def.name] = { warn: user?.warn !== undefined ? user.warn : def.defaults.warn, - fail: def.reportOnly ? null : (user?.fail !== undefined ? user.fail : def.defaults.fail), + fail: def.reportOnly ? null : user?.fail !== undefined ? user.fail : def.defaults.fail, }; } return resolved;