Files
callstack__agent-device/scripts/depgraph/model.test.ts
Claude 2c519cd562 experiment: analysis-only depgraph, to test whether it clears the gate unexempted
NOT a finished change — pushed so the comparison behind a pending decision is
reproducible rather than a claim in a chat log. PR #1409 still carries the viewer.

Question: the viewer's productive output turned out to be the JSON, not the
render. Every finding this session came from numeric queries; the render was never
opened to make a decision. So: does an analysis-only version meet the repo's bar
WITHOUT the Fallow exemption that PR #1409 needs?

Method: delete viewer.{js,css,html} and the geometry (clusterLayout,
layeredLayout), keep computeLevels (it is analysis, not layout), emit JSON plus the
text summary, and REMOVE scripts/depgraph/** from ignorePatterns.

Result, with zero exemptions:

                        with viewer   analysis only
  complexity findings   23 (1 CRIT)   2
  unused files          2             0
  unused exports        3             0
  clone groups          1             0
  lines                 2811          ~590

Identical output: 898 files, 4627 edges, 1338 redundant value edges, 8 non-gated
cycles, R6 42 (matching the gate's baseline). So the numeric part can meet the
repo's bar unexempted; viewer.js — 920 lines with a CRITICAL-complexity `draw` —
never could.

Fixed along the way rather than suppressed: extracted `valueSuccessors` (the
value-edge adjacency was built identically in markRedundantEdges and
computeLevels — a real clone), extracted `edgeKindCode`/`edgeFlags` from a nested
ternary with CRAP 42, un-exported buildPayload/main, and deleted `fileGroup` and
the `group` node field, both dead once the cluster layout went.

Still open if this direction is chosen: split `buildGraph` (81 lines, 20
cyclomatic) and `markRedundantEdges` — the last 2 complexity findings, ordinary
functions rather than a canvas renderer. README is rewritten to match the
report-only shape; the when-to-use guidance carries over unchanged, since it was
already about numeric queries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bfu8HofkhybiAm5LECfqur
2026-07-27 08:34:23 +00:00

147 lines
4.6 KiB
TypeScript

import assert from 'node:assert/strict';
import { test } from 'node:test';
import { resolveImportEdges } from '../layering/model.ts';
import { buildGraph, collapseEdges, collectCycles, markRedundantEdges } from './model.ts';
function sources(entries: Record<string, string>): Map<string, string> {
return new Map(Object.entries(entries));
}
test('collapseEdges keeps one edge per pair at the strongest kind', () => {
const edges = resolveImportEdges(
sources({
'src/core/a.ts': [
"import type { Shape } from './b.ts';",
"import { run } from './b.ts';",
"import type { Other } from './c.ts';",
"void import('./d.ts');",
].join('\n'),
'src/core/b.ts': 'export const run = 1;',
'src/core/c.ts': 'export type Other = string;',
'src/core/d.ts': 'export const lazy = 1;',
}),
);
assert.deepEqual(
collapseEdges(edges).map((edge) => ({ to: edge.to, kind: edge.kind })),
[
{ to: 'src/core/b.ts', kind: 'value' },
{ to: 'src/core/c.ts', kind: 'type' },
{ to: 'src/core/d.ts', kind: 'dynamic' },
],
);
});
test('redundant marks only value edges whose target is already reachable at distance >= 2', () => {
const edges = collapseEdges(
resolveImportEdges(
sources({
// a -> b -> c makes the direct a -> c edge removable; a -> d is the only route to d.
'src/core/a.ts': [
"import { b } from './b.ts';",
"import { c } from './c.ts';",
"import { d } from './d.ts';",
].join('\n'),
'src/core/b.ts': "export { c as b } from './c.ts';",
'src/core/c.ts': 'export const c = 1;',
'src/core/d.ts': 'export const d = 1;',
}),
),
);
markRedundantEdges(edges);
const flagged = edges
.filter((edge) => edge.redundant)
.map((edge) => `${edge.from} -> ${edge.to}`);
assert.deepEqual(flagged, ['src/core/a.ts -> src/core/c.ts']);
});
test('a type-only shortcut is never treated as redundant against a value path', () => {
const edges = collapseEdges(
resolveImportEdges(
sources({
'src/core/a.ts': ["import { b } from './b.ts';", "import type { C } from './c.ts';"].join(
'\n',
),
'src/core/b.ts': "export { c as b } from './c.ts';",
'src/core/c.ts': 'export type C = string;\nexport const c = 1;',
}),
),
);
markRedundantEdges(edges);
assert.deepEqual(
edges.filter((edge) => edge.redundant),
[],
);
});
test('collectCycles separates gate-rejected value cycles from type-only and dynamic loops', () => {
const valueCycle = collectCycles(
resolveImportEdges(
sources({
'src/core/a.ts': "import { b } from './b.ts';\nexport const a = 1;",
'src/core/b.ts': "import { a } from './a.ts';\nexport const b = 1;",
}),
),
);
assert.deepEqual(
valueCycle.map((cycle) => cycle.kind),
['value'],
);
const typeCycle = collectCycles(
resolveImportEdges(
sources({
'src/core/a.ts': "import type { B } from './b.ts';\nexport type A = B;",
'src/core/b.ts': "import type { A } from './a.ts';\nexport type B = A | null;",
}),
),
);
assert.deepEqual(
typeCycle.map((cycle) => cycle.kind),
['type'],
);
const dynamicCycle = collectCycles(
resolveImportEdges(
sources({
'src/core/a.ts': "export const a = () => import('./b.ts');",
'src/core/b.ts': "export const b = () => import('./a.ts');",
}),
),
);
assert.deepEqual(
dynamicCycle.map((cycle) => cycle.kind),
['dynamic'],
);
});
test('buildGraph reports zone membership, degrees, and cross-zone edge counts', () => {
const files = sources({
'src/kernel/errors.ts': 'export const fail = 1;\n',
'src/core/interactors/tap.ts': "import { fail } from '../../kernel/errors.ts';\n",
'src/commands/tap.ts': [
"import { fail } from '../kernel/errors.ts';",
"import '../core/interactors/tap.ts';",
].join('\n'),
});
const graph = buildGraph(files, resolveImportEdges(files));
const kernel = graph.nodes.find((node) => node.id === 'src/kernel/errors.ts')!;
assert.equal(kernel.zone, 'kernel');
assert.equal(kernel.fanIn, 2);
assert.equal(kernel.fanOut, 0);
const interactor = graph.nodes.find((node) => node.id === 'src/core/interactors/tap.ts')!;
assert.deepEqual(
graph.zoneEdges.map((edge) => `${edge.from} -> ${edge.to} (${edge.count})`),
['commands -> core (1)', 'commands -> kernel (1)', 'core -> kernel (1)'],
);
assert.deepEqual(
graph.zones.map((zone) => zone.classification),
['ranked', 'ranked', 'ranked'],
);
});