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
311 lines
9.7 KiB
TypeScript
311 lines
9.7 KiB
TypeScript
// Dependency-graph analysis model — pure functions over the layering gate's edge model.
|
||
//
|
||
// The graph is deliberately derived from `scripts/layering/model.ts` rather than a
|
||
// third-party extractor: the gate's file set (production `src/**/*.ts`, tests excluded),
|
||
// zone partition, edge kinds (value/type-only/dynamic), and cycle definition are already
|
||
// the repo's source of truth. A second extractor with its own resolution rules would
|
||
// visualize a graph the gate does not enforce.
|
||
|
||
import {
|
||
backEdgePair,
|
||
classifyZone,
|
||
findValueImportCycles,
|
||
targetDagZone,
|
||
typeInversionPair,
|
||
type ResolvedImportEdge,
|
||
} from '../layering/model.ts';
|
||
|
||
export type EdgeKind = 'value' | 'type' | 'dynamic';
|
||
|
||
export type GraphEdge = {
|
||
from: string;
|
||
to: string;
|
||
kind: EdgeKind;
|
||
line: number;
|
||
/** Set when this edge is a ranked-spine back-edge (`R5`), as `from-zone -> to-zone`. */
|
||
backEdge: string | null;
|
||
/** Set when this edge is a type-only spine inversion (`R6`), as `from-zone -> to-zone`. */
|
||
typeInversion: string | null;
|
||
/** True when the same pair is also reachable through a longer path of the same weight class. */
|
||
redundant: boolean;
|
||
};
|
||
|
||
export type GraphNode = {
|
||
id: string;
|
||
zone: string;
|
||
/** First two path segments — a finer cluster than the zone, used for layout gravity. */
|
||
loc: number;
|
||
fanIn: number;
|
||
fanOut: number;
|
||
/** Index into `GraphData.cycles`, or -1. */
|
||
cycle: number;
|
||
};
|
||
|
||
export type ZoneEdge = {
|
||
from: string;
|
||
to: string;
|
||
count: number;
|
||
valueCount: number;
|
||
backEdge: boolean;
|
||
};
|
||
|
||
export type GraphCycle = {
|
||
path: string[];
|
||
/** `value` cycles are gate-rejected (R4); the others are gate-invisible by design. */
|
||
kind: EdgeKind;
|
||
};
|
||
|
||
export type GraphData = {
|
||
nodes: GraphNode[];
|
||
edges: GraphEdge[];
|
||
zones: { id: string; classification: string; files: number; loc: number }[];
|
||
zoneEdges: ZoneEdge[];
|
||
cycles: GraphCycle[];
|
||
};
|
||
|
||
|
||
function countLines(source: string): number {
|
||
let lines = 1;
|
||
for (let index = 0; index < source.length; index++) {
|
||
if (source[index] === '\n') lines++;
|
||
}
|
||
return lines;
|
||
}
|
||
|
||
function edgeKind(edge: ResolvedImportEdge): EdgeKind {
|
||
if (edge.dynamic) return 'dynamic';
|
||
if (edge.typeOnly) return 'type';
|
||
return 'value';
|
||
}
|
||
|
||
/**
|
||
* Deduplicate parsed import edges down to one edge per (from, to) pair, keeping the
|
||
* strongest kind. A file that imports both a type and a value from the same module has one
|
||
* dependency on it, and the value import is what constrains layering and cold-start.
|
||
*/
|
||
export function collapseEdges(edges: readonly ResolvedImportEdge[]): GraphEdge[] {
|
||
const strength: Record<EdgeKind, number> = { type: 0, dynamic: 1, value: 2 };
|
||
const byPair = new Map<string, GraphEdge>();
|
||
for (const edge of edges) {
|
||
if (edge.file === edge.target) continue;
|
||
const key = `${edge.file} |