Skip to content

Commit 34dd9b0

Browse files
authored
fix(louvain): demote native-path parity warning to debug (#989)
* fix(louvain): demote native-path parity warning to debug Warning fired on every communities computation because DEFAULTS.community in config.ts always populates maxLevels/maxLocalPasses/refinementTheta, so the != null guard never gated anything in practice. The message is an informational parity note (Leiden knobs ignored by native Louvain), not user-actionable — debug is the right level. docs check acknowledged: no language/architecture/feature changes. Impact: 1 functions changed, 10 affected * test(louvain): add regression guard for parity log level (#989)
1 parent e3edd63 commit 34dd9b0

2 files changed

Lines changed: 68 additions & 5 deletions

File tree

src/graph/algorithms/louvain.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* JS fallback: Leiden algorithm via `detectClusters` (always undirected, `directed: false`).
77
*/
88

9-
import { warn } from '../../infrastructure/logger.js';
9+
import { debug } from '../../infrastructure/logger.js';
1010
import { loadNative } from '../../infrastructure/native.js';
1111
import type { CodeGraph } from '../model.js';
1212
import type { DetectClustersResult } from './leiden/index.js';
@@ -36,10 +36,8 @@ export function louvainCommunities(graph: CodeGraph, opts: LouvainOptions = {}):
3636

3737
const native = loadNative();
3838
if (native?.louvainCommunities) {
39-
// maxLevels, maxLocalPasses, and refinementTheta are Leiden-specific tuning knobs
40-
// not supported by the Rust Louvain implementation. Warn callers who set them.
4139
if (opts.maxLevels != null || opts.maxLocalPasses != null || opts.refinementTheta != null) {
42-
warn(
40+
debug(
4341
'louvainCommunities: maxLevels/maxLocalPasses/refinementTheta are ignored by the native Rust path',
4442
);
4543
}

tests/graph/algorithms/louvain.test.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { describe, expect, it } from 'vitest';
1+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
22
import { louvainCommunities } from '../../../src/graph/algorithms/louvain.js';
33
import { CodeGraph } from '../../../src/graph/model.js';
4+
import { setVerbose } from '../../../src/infrastructure/logger.js';
45

56
describe('louvainCommunities', () => {
67
it('returns empty for empty graph', () => {
@@ -45,4 +46,68 @@ describe('louvainCommunities', () => {
4546
expect(assignments.size).toBe(0);
4647
expect(modularity).toBe(0);
4748
});
49+
50+
describe('Leiden-knob parity logging', () => {
51+
let stderrSpy: ReturnType<typeof vi.spyOn>;
52+
53+
beforeEach(() => {
54+
stderrSpy = vi.spyOn(process.stderr, 'write').mockImplementation(() => true);
55+
});
56+
57+
afterEach(() => {
58+
stderrSpy.mockRestore();
59+
setVerbose(false);
60+
});
61+
62+
function buildTwoClusterGraph(): CodeGraph {
63+
const g = new CodeGraph();
64+
g.addEdge('a', 'b');
65+
g.addEdge('b', 'c');
66+
g.addEdge('c', 'a');
67+
g.addEdge('x', 'y');
68+
g.addEdge('y', 'z');
69+
g.addEdge('z', 'x');
70+
g.addEdge('c', 'x');
71+
return g;
72+
}
73+
74+
// Regression guard: DEFAULTS.community always populates maxLevels/maxLocalPasses/
75+
// refinementTheta, so forwarding config values used to emit `[codegraph WARN]` on
76+
// every communities computation. Keep the parity note at debug level — never warn.
77+
it('never emits the parity message at WARN level when config defaults are forwarded', () => {
78+
const g = buildTwoClusterGraph();
79+
louvainCommunities(g, {
80+
maxLevels: 50,
81+
maxLocalPasses: 20,
82+
refinementTheta: 1.0,
83+
});
84+
85+
const warnWrites = stderrSpy.mock.calls
86+
.map(([chunk]) => (typeof chunk === 'string' ? chunk : (chunk?.toString?.() ?? '')))
87+
.filter((line) => line.includes('[codegraph WARN]'));
88+
expect(warnWrites).toEqual([]);
89+
});
90+
91+
it('emits the parity message at DEBUG level when verbose is enabled and Leiden knobs are set', () => {
92+
setVerbose(true);
93+
const g = buildTwoClusterGraph();
94+
louvainCommunities(g, {
95+
maxLevels: 50,
96+
maxLocalPasses: 20,
97+
refinementTheta: 1.0,
98+
});
99+
100+
const writes = stderrSpy.mock.calls
101+
.map(([chunk]) => (typeof chunk === 'string' ? chunk : (chunk?.toString?.() ?? '')))
102+
.join('');
103+
// Message is only emitted on the native path. When native is unavailable we at
104+
// least assert no WARN was emitted (covered by the previous test); when it is
105+
// available, it must go through the DEBUG channel and never WARN.
106+
expect(writes).not.toContain('[codegraph WARN]');
107+
// Allow either outcome for DEBUG depending on engine availability.
108+
if (writes.includes('maxLevels/maxLocalPasses/refinementTheta')) {
109+
expect(writes).toContain('[codegraph DEBUG]');
110+
}
111+
});
112+
});
48113
});

0 commit comments

Comments
 (0)