mirror of
https://github.com/callstack/agent-device.git
synced 2026-09-14 20:06:34 +08:00
2c519cd562
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
147 lines
4.6 KiB
TypeScript
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'],
|
|
);
|
|
});
|