@titan-design/code-graph 0.10.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +135 -5
- package/dist/analysis/browser.d.ts +2 -0
- package/dist/analysis/browser.js +84 -0
- package/dist/analysis/browser.js.map +1 -0
- package/dist/browser-BBzShneN.d.ts +936 -0
- package/dist/{change-coupling-CyqHgRsm.d.ts → change-coupling-CT3bcZMD.d.ts} +5 -1
- package/dist/{chunk-PFI5XMUG.js → chunk-GX563F6R.js} +79 -89
- package/dist/chunk-GX563F6R.js.map +1 -0
- package/dist/chunk-HIVVDGWE.js +83 -0
- package/dist/chunk-HIVVDGWE.js.map +1 -0
- package/dist/chunk-L5KU543A.js +1395 -0
- package/dist/chunk-L5KU543A.js.map +1 -0
- package/dist/history/index.d.ts +25 -4
- package/dist/history/index.js +11 -3
- package/dist/index.d.ts +406 -353
- package/dist/index.js +1753 -889
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
- package/dist/chunk-PFI5XMUG.js.map +0 -1
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
import { a as ChurnWindow, c as CoEditPair } from './change-coupling-CT3bcZMD.js';
|
|
2
|
+
|
|
3
|
+
type NodeKind = "package" | "module" | "file" | "symbol" | "external";
|
|
4
|
+
type EdgeKind = "imports" | "re-exports" | "calls" | "extends" | "implements" | "references" | "depends-on";
|
|
5
|
+
/** `requalify` maps a bare-name symbol id from before index version 0.14.0 to its scope-qualified successor. */
|
|
6
|
+
type IdAliasReason = "rename" | "move" | "merge" | "requalify";
|
|
7
|
+
type NodeRole = "test" | "fixture" | "story" | "lab" | "barrel" | "types" | "config" | "script" | "entry" | "generated" | "source";
|
|
8
|
+
interface GraphNode {
|
|
9
|
+
id: string;
|
|
10
|
+
kind: NodeKind;
|
|
11
|
+
name: string;
|
|
12
|
+
parentId?: string;
|
|
13
|
+
language?: string;
|
|
14
|
+
role?: NodeRole;
|
|
15
|
+
attrs?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
interface GraphEdge {
|
|
18
|
+
srcId: string;
|
|
19
|
+
dstId: string;
|
|
20
|
+
kind: EdgeKind;
|
|
21
|
+
attrs?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
interface GraphMetric {
|
|
24
|
+
nodeId: string;
|
|
25
|
+
name: string;
|
|
26
|
+
value: number | null;
|
|
27
|
+
unit?: string;
|
|
28
|
+
}
|
|
29
|
+
interface GraphFragment {
|
|
30
|
+
nodes: GraphNode[];
|
|
31
|
+
edges: GraphEdge[];
|
|
32
|
+
}
|
|
33
|
+
interface SnapshotRow {
|
|
34
|
+
id: number;
|
|
35
|
+
ref: string;
|
|
36
|
+
commitHash: string | null;
|
|
37
|
+
takenAt: string;
|
|
38
|
+
indexVersion: string;
|
|
39
|
+
attrs: Record<string, unknown>;
|
|
40
|
+
}
|
|
41
|
+
interface IdAlias {
|
|
42
|
+
oldId: string;
|
|
43
|
+
newId: string;
|
|
44
|
+
reason: IdAliasReason;
|
|
45
|
+
}
|
|
46
|
+
interface FileFingerprint {
|
|
47
|
+
fileId: string;
|
|
48
|
+
contentHash: string;
|
|
49
|
+
/**
|
|
50
|
+
* Comment/whitespace-insensitive parse-structure hash (C-18). Absent on
|
|
51
|
+
* snapshots written before C-18 (they can only reuse whole unchanged files).
|
|
52
|
+
*/
|
|
53
|
+
structuralHash?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface HotspotRow {
|
|
57
|
+
nodeId: string;
|
|
58
|
+
churn: number;
|
|
59
|
+
complexity: number;
|
|
60
|
+
score: number;
|
|
61
|
+
/** Age-recency factor applied to the score (1 = no discount); see recency_{window}d. */
|
|
62
|
+
recency: number;
|
|
63
|
+
}
|
|
64
|
+
interface NewHotspot extends HotspotRow {
|
|
65
|
+
/**
|
|
66
|
+
* Baseline hotspot score if the file existed in the baseline snapshot;
|
|
67
|
+
* `undefined` ⇒ a newborn file (didn't exist at baseline). Lets the UI split
|
|
68
|
+
* "new file" (neutral) from "existing file that climbed into the ranking".
|
|
69
|
+
*/
|
|
70
|
+
before?: number;
|
|
71
|
+
}
|
|
72
|
+
interface BusFactorRow {
|
|
73
|
+
nodeId: string;
|
|
74
|
+
busFactor: number;
|
|
75
|
+
topAuthorShare: number;
|
|
76
|
+
churn: number;
|
|
77
|
+
}
|
|
78
|
+
interface CouplingRow {
|
|
79
|
+
fileA: string;
|
|
80
|
+
fileB: string;
|
|
81
|
+
count: number;
|
|
82
|
+
}
|
|
83
|
+
interface CentralRow {
|
|
84
|
+
nodeId: string;
|
|
85
|
+
score: number;
|
|
86
|
+
}
|
|
87
|
+
interface UnusedExportRow {
|
|
88
|
+
/** The `symbol` node id (`<fileId>#<name>`). */
|
|
89
|
+
nodeId: string;
|
|
90
|
+
/** The exported name with no inbound reference. */
|
|
91
|
+
name: string;
|
|
92
|
+
/** The file that declares it. */
|
|
93
|
+
fileId: string;
|
|
94
|
+
/** The export's own cognitive complexity (C-58); 0 for a class/type/re-export. */
|
|
95
|
+
cognitive: number;
|
|
96
|
+
/**
|
|
97
|
+
* The declaring file is re-exported by a `barrel` — so this export is part of a
|
|
98
|
+
* package's public surface and may be consumed *externally* (lower confidence
|
|
99
|
+
* that it's removable). `false` ⇒ internal, no reference found anywhere
|
|
100
|
+
* (higher confidence).
|
|
101
|
+
*/
|
|
102
|
+
publicApi: boolean;
|
|
103
|
+
}
|
|
104
|
+
interface DeadModuleRow {
|
|
105
|
+
/** The unreferenced file's node id. */
|
|
106
|
+
nodeId: string;
|
|
107
|
+
/** Its size, for ranking (a large dead file is the most worth removing). */
|
|
108
|
+
loc: number;
|
|
109
|
+
/** Its role (usually "source" or "types"). */
|
|
110
|
+
role: string;
|
|
111
|
+
}
|
|
112
|
+
interface GrowthRiskRow {
|
|
113
|
+
nodeId: string;
|
|
114
|
+
/** Max lexical loop-nesting depth (C-66); 0 when the file's smell is not nesting. */
|
|
115
|
+
loopDepth: number;
|
|
116
|
+
/** Human-readable scaling smells (deep loops, recursion, linear-search-in-loop). */
|
|
117
|
+
smells: string[];
|
|
118
|
+
}
|
|
119
|
+
interface UntestedRiskRow {
|
|
120
|
+
nodeId: string;
|
|
121
|
+
/** Coverage % from an ingested Istanbul report (C-63). */
|
|
122
|
+
coverage: number;
|
|
123
|
+
/** Hotspot score (churn × complexity × recency) for context. */
|
|
124
|
+
hotspot: number;
|
|
125
|
+
/** hotspot × (1 − coverage/100): load-bearing, complex, churning, AND untested. */
|
|
126
|
+
score: number;
|
|
127
|
+
}
|
|
128
|
+
interface TestCoverageRow {
|
|
129
|
+
/** Source (non-test) file whose test coverage is owner-concentrated. */
|
|
130
|
+
nodeId: string;
|
|
131
|
+
/** Bus factor of the linked test files' authorship (1 = single owner). */
|
|
132
|
+
testBusFactor: number;
|
|
133
|
+
/** Share of test churn from the single largest test author (0..1). */
|
|
134
|
+
testTopAuthorShare: number;
|
|
135
|
+
/** How many distinct test files link to this source. */
|
|
136
|
+
linkedTests: number;
|
|
137
|
+
}
|
|
138
|
+
interface GraphReportResult {
|
|
139
|
+
snapshot: SnapshotRow;
|
|
140
|
+
/** The resolved churn window these sections were computed for (`"lifetime"` = all-time). */
|
|
141
|
+
windowDays: ChurnWindow;
|
|
142
|
+
hotspots: HotspotRow[];
|
|
143
|
+
busFactorRisks: BusFactorRow[];
|
|
144
|
+
testCoverageRisks: TestCoverageRow[];
|
|
145
|
+
couplingClusters: CouplingRow[];
|
|
146
|
+
centralFiles: CentralRow[];
|
|
147
|
+
/** Exported symbols with zero inbound references — "no reference found" (C-65). */
|
|
148
|
+
unusedExports: UnusedExportRow[];
|
|
149
|
+
/** Files unreachable from entry roots (barrels/tests/scripts) — "no importer found" (C-65). */
|
|
150
|
+
deadModules: DeadModuleRow[];
|
|
151
|
+
/** Files with structural scaling smells (deep loop nesting) — heuristic, not Big-O (C-66). */
|
|
152
|
+
growthRisks: GrowthRiskRow[];
|
|
153
|
+
/** Load-bearing + complex + churning + under-tested files (C-63); empty if no coverage ingested. */
|
|
154
|
+
untestedRisks: UntestedRiskRow[];
|
|
155
|
+
/** True when no file has churn > 0 in the window (churn sections all empty). */
|
|
156
|
+
emptyWindow?: boolean;
|
|
157
|
+
/** User-facing guidance shown when emptyWindow is true. */
|
|
158
|
+
hint?: string;
|
|
159
|
+
drift?: ReportDrift;
|
|
160
|
+
}
|
|
161
|
+
interface HotspotDelta {
|
|
162
|
+
nodeId: string;
|
|
163
|
+
before: number;
|
|
164
|
+
after: number;
|
|
165
|
+
delta: number;
|
|
166
|
+
}
|
|
167
|
+
interface BusFactorChange {
|
|
168
|
+
nodeId: string;
|
|
169
|
+
churn: number;
|
|
170
|
+
}
|
|
171
|
+
interface CouplingDelta {
|
|
172
|
+
fileA: string;
|
|
173
|
+
fileB: string;
|
|
174
|
+
before: number;
|
|
175
|
+
after: number;
|
|
176
|
+
}
|
|
177
|
+
interface ReportDrift {
|
|
178
|
+
baselineSnapshot: SnapshotRow;
|
|
179
|
+
newHotspots: NewHotspot[];
|
|
180
|
+
/** Was in baseline top-N, score actually went down (or file gone). */
|
|
181
|
+
resolvedHotspots: HotspotDelta[];
|
|
182
|
+
/** Was in baseline top-N, score didn't improve — newer hotspots displaced it. */
|
|
183
|
+
displacedHotspots: HotspotDelta[];
|
|
184
|
+
worsenedHotspots: HotspotDelta[];
|
|
185
|
+
improvedHotspots: HotspotDelta[];
|
|
186
|
+
newSilos: BusFactorChange[];
|
|
187
|
+
/** Was silo (bus_factor=1) in baseline, now bus_factor>1 or no churn in window. */
|
|
188
|
+
resolvedSilos: BusFactorChange[];
|
|
189
|
+
/** Still bus_factor=1, but churn dropped below top-N (still single-owner). */
|
|
190
|
+
displacedSilos: BusFactorChange[];
|
|
191
|
+
newCoupling: CouplingRow[];
|
|
192
|
+
intensifiedCoupling: CouplingDelta[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
interface ReportContext {
|
|
196
|
+
nodes: readonly GraphNode[];
|
|
197
|
+
nodeById: Map<string, GraphNode>;
|
|
198
|
+
metricsByName: Map<string, Map<string, number>>;
|
|
199
|
+
excluders: readonly RegExp[];
|
|
200
|
+
excludedRoles: ReadonlySet<string>;
|
|
201
|
+
windowDays: ChurnWindow;
|
|
202
|
+
/** Metric-name suffix for {@link windowDays} (`30d` … or `lifetime`). */
|
|
203
|
+
windowSuffix: string;
|
|
204
|
+
}
|
|
205
|
+
interface ReportContextInput {
|
|
206
|
+
nodes: readonly GraphNode[];
|
|
207
|
+
metrics: readonly GraphMetric[];
|
|
208
|
+
excluders: readonly RegExp[];
|
|
209
|
+
excludedRoles: ReadonlySet<string>;
|
|
210
|
+
windowDays: ChurnWindow;
|
|
211
|
+
}
|
|
212
|
+
declare function buildReportContext(input: ReportContextInput): ReportContext;
|
|
213
|
+
declare function keepNode(ctx: ReportContext, nodeId: string): boolean;
|
|
214
|
+
declare function lookupMetric(ctx: ReportContext, name: string, nodeId: string): number | undefined;
|
|
215
|
+
declare function topHotspots(ctx: ReportContext, limit: number): HotspotRow[];
|
|
216
|
+
declare function hotspotScoreOf(ctx: ReportContext, nodeId: string): number;
|
|
217
|
+
/** The complexity factor a file's hotspot score multiplies, read even when the file has no churn; undefined when unmeasured. */
|
|
218
|
+
declare function hotspotComplexityOf(ctx: ReportContext, nodeId: string): number | undefined;
|
|
219
|
+
declare function busFactorOf(ctx: ReportContext, nodeId: string): number | undefined;
|
|
220
|
+
declare function topBusFactorRisks(ctx: ReportContext, limit: number): BusFactorRow[];
|
|
221
|
+
/**
|
|
222
|
+
* Sources whose *test coverage* is a single-author silo (test bus factor = 1)
|
|
223
|
+
* — the honest, role-split view: production code can be well-spread while the
|
|
224
|
+
* tests that guard it are owned by one person (or vice versa).
|
|
225
|
+
*/
|
|
226
|
+
declare function topTestCoverageRisks(ctx: ReportContext, limit: number): TestCoverageRow[];
|
|
227
|
+
declare function topCentralFiles(nodes: readonly GraphNode[], edges: readonly GraphEdge[], ctx: ReportContext, limit: number): CentralRow[];
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The set of files re-exported by a `barrel`-role node (1-hop `re-exports`
|
|
231
|
+
* edges) — i.e. files whose exports form a package's public surface. An unused
|
|
232
|
+
* export declared in one of these may still be consumed *externally* (by an npm
|
|
233
|
+
* consumer of a published package), so it's flagged lower-confidence rather than
|
|
234
|
+
* excluded. Transitive barrel chains are not followed (v1); a symbol behind two
|
|
235
|
+
* barrels reads as internal.
|
|
236
|
+
*/
|
|
237
|
+
declare function publicApiFiles(nodes: readonly GraphNode[], edges: readonly GraphEdge[]): Set<string>;
|
|
238
|
+
/**
|
|
239
|
+
* Exported symbols (C-64 `attrs.exported`) with zero inbound `references` — an
|
|
240
|
+
* export that nothing imports by name (utilization is barrel-resolved, C-53, so
|
|
241
|
+
* an export used *through* a barrel reads > 0). Framed as "no reference found",
|
|
242
|
+
* not "dead": it may be used only internally within its own file, or consumed
|
|
243
|
+
* externally if the repo is a published library — hence the `publicApi` split.
|
|
244
|
+
* Ranked by the export's own cognitive complexity (a complex unused export is
|
|
245
|
+
* the most worth removing), scoped to kept (non-excluded) files.
|
|
246
|
+
*/
|
|
247
|
+
declare function topUnusedExports(symbolNodes: readonly GraphNode[], publicApi: ReadonlySet<string>, ctx: ReportContext, limit: number): UnusedExportRow[];
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Files unreachable from the entry roots by a forward BFS over `imports` /
|
|
251
|
+
* `re-exports` edges — "no importer found given configured entry points" (C-65),
|
|
252
|
+
* NOT proven dead. Catches transitively-dead chains, not just fan-in-0 files.
|
|
253
|
+
* Blind spots (disclosed): a computed dynamic `import(variable)`, DI/registry
|
|
254
|
+
* strings, and any package entry that isn't an index barrel escape the roots and
|
|
255
|
+
* could make a live file look dead — so treat it as a lead, not a verdict.
|
|
256
|
+
* Ranked by LOC (a large unreferenced file is the most worth removing).
|
|
257
|
+
*/
|
|
258
|
+
declare function topDeadModules(nodes: readonly GraphNode[], edges: readonly GraphEdge[], ctx: ReportContext, limit: number): DeadModuleRow[];
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Files carrying a structural scaling smell (C-66): deep loop nesting, direct
|
|
262
|
+
* recursion, or a linear-scan method call inside a loop. A HEURISTIC, not Big-O:
|
|
263
|
+
* depth-2 loops over two different collections are linear, `.includes` on a `Set`
|
|
264
|
+
* is O(1), and recursion may be well-bounded. Ranked by loop depth, then smell
|
|
265
|
+
* count.
|
|
266
|
+
*/
|
|
267
|
+
declare function topGrowthRisks(ctx: ReportContext, limit: number): GrowthRiskRow[];
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Files that are load-bearing + complex + churning AND under-tested (C-63) —
|
|
271
|
+
* the sharpest single risk signal: `hotspot × (1 − coverage/100)`. Requires an
|
|
272
|
+
* ingested coverage overlay (`graph coverage`); with no coverage, the section is
|
|
273
|
+
* empty (never a stale or assumed number — coverage is an overlay, not inferred).
|
|
274
|
+
* A fully-covered hotspot (coverage 100) scores 0 and drops out.
|
|
275
|
+
*/
|
|
276
|
+
declare function topUntestedRisks(ctx: ReportContext, limit: number): UntestedRiskRow[];
|
|
277
|
+
|
|
278
|
+
interface ComputeDriftInput {
|
|
279
|
+
baselineSnapshot: SnapshotRow;
|
|
280
|
+
currentHotspots: readonly HotspotRow[];
|
|
281
|
+
baselineHotspots: readonly HotspotRow[];
|
|
282
|
+
/** Current churn × complexity for any nodeId. 0 when file is gone or no longer has churn/complexity. */
|
|
283
|
+
currentHotspotScore: (nodeId: string) => number;
|
|
284
|
+
/**
|
|
285
|
+
* Baseline churn × complexity for a file that existed at baseline, or
|
|
286
|
+
* `undefined` when the file is newborn (absent from the baseline snapshot).
|
|
287
|
+
* Distinguishes a brand-new hotspot from an existing file that climbed in.
|
|
288
|
+
*/
|
|
289
|
+
baselineHotspotScore?: (nodeId: string) => number | undefined;
|
|
290
|
+
currentSilos: readonly BusFactorRow[];
|
|
291
|
+
baselineSilos: readonly BusFactorRow[];
|
|
292
|
+
/** Current bus_factor for any nodeId. undefined when file has no churn in window. */
|
|
293
|
+
currentBusFactor: (nodeId: string) => number | undefined;
|
|
294
|
+
currentCoupling: readonly CouplingRow[];
|
|
295
|
+
baselineCoupling: readonly CouplingRow[];
|
|
296
|
+
}
|
|
297
|
+
declare function computeReportDrift(input: ComputeDriftInput): ReportDrift;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Composite dashboard health score. Extracted from dashboard-payload.ts (which
|
|
301
|
+
* sits over the max-file-loc budget) so that file stays lean as new payload
|
|
302
|
+
* slices land.
|
|
303
|
+
*/
|
|
304
|
+
type HealthComponentKey = "hotspots" | "findings" | "complexity" | "hidden-coupling";
|
|
305
|
+
interface HealthComponent {
|
|
306
|
+
key: HealthComponentKey;
|
|
307
|
+
label: string;
|
|
308
|
+
penalty: number;
|
|
309
|
+
/** The most this component can take off the score. */
|
|
310
|
+
cap: number;
|
|
311
|
+
detail: string;
|
|
312
|
+
}
|
|
313
|
+
/** Points per counted item, and the most one component can take. */
|
|
314
|
+
interface PenaltyWeight {
|
|
315
|
+
each: number;
|
|
316
|
+
cap: number;
|
|
317
|
+
}
|
|
318
|
+
interface HealthWeights {
|
|
319
|
+
hotspots: PenaltyWeight;
|
|
320
|
+
findings: {
|
|
321
|
+
eachNew: number;
|
|
322
|
+
eachCarry: number;
|
|
323
|
+
cap: number;
|
|
324
|
+
};
|
|
325
|
+
/** `each` point per unit of max complexity over `budget`. */
|
|
326
|
+
complexity: PenaltyWeight & {
|
|
327
|
+
budget: number;
|
|
328
|
+
};
|
|
329
|
+
hiddenCoupling: PenaltyWeight;
|
|
330
|
+
}
|
|
331
|
+
declare const DEFAULT_HEALTH_WEIGHTS: HealthWeights;
|
|
332
|
+
interface HealthInput {
|
|
333
|
+
scary: number;
|
|
334
|
+
newViolations: number;
|
|
335
|
+
carryViolations: number;
|
|
336
|
+
maxComplexity: number;
|
|
337
|
+
hiddenCoupling: number;
|
|
338
|
+
/** The hotspot score `scary` counted files at; only the detail text reads it. */
|
|
339
|
+
scaryCutoff?: number;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Composite health as a transparent sum of independent penalty components, so
|
|
343
|
+
* the UI can show *why* the score is what it is instead of a black-box number.
|
|
344
|
+
* Each component is capped and drawn from a distinct dimension — the hotspots
|
|
345
|
+
* component owns scary files, so the violations component excludes the
|
|
346
|
+
* scary-hotspots rule (no double-count). Ownership (knowledge-silo / bus-factor)
|
|
347
|
+
* signal is deliberately NOT a health component: it saturates on single-author
|
|
348
|
+
* repos and lives on the Ownership tab, not in the cross-cutting score.
|
|
349
|
+
* `weights` defaults to the dashboard's; a caller may weigh the components its own way.
|
|
350
|
+
*/
|
|
351
|
+
declare function computeHealth(x: HealthInput, weights?: HealthWeights): {
|
|
352
|
+
health: number;
|
|
353
|
+
healthBreakdown: HealthComponent[];
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Per-file structural metrics for the dashboard Dossier heat readout. Extracted
|
|
358
|
+
* from dashboard-payload so that file stays under the max-file-loc budget.
|
|
359
|
+
*/
|
|
360
|
+
interface NodeMetrics {
|
|
361
|
+
loc?: number;
|
|
362
|
+
cognitiveMax?: number;
|
|
363
|
+
cyclomaticMax?: number;
|
|
364
|
+
maxNesting?: number;
|
|
365
|
+
fanIn?: number;
|
|
366
|
+
fanOut?: number;
|
|
367
|
+
utilization?: number;
|
|
368
|
+
/** Distinct test files linking to this source (C-4); shown in the Dossier (C-59). */
|
|
369
|
+
linkedTests?: number;
|
|
370
|
+
/** Node role (e.g. "barrel") — lets the Dossier explain barrel-resolved utilization. */
|
|
371
|
+
role?: string;
|
|
372
|
+
}
|
|
373
|
+
/** Fold the flat metric rows into a per-node structural-metrics map. */
|
|
374
|
+
declare function collectNodeMetrics(rows: {
|
|
375
|
+
nodeId: string;
|
|
376
|
+
name: string;
|
|
377
|
+
value: number | null;
|
|
378
|
+
}[]): Map<string, NodeMetrics>;
|
|
379
|
+
/** Every file the Dossier can open on (referenced in hotspots / silos / coupling / coverage / central / drift). */
|
|
380
|
+
declare function referencedNodes(report: GraphReportResult): Set<string>;
|
|
381
|
+
/**
|
|
382
|
+
* Structural metrics for every file the Dossier can open on. Scoped to
|
|
383
|
+
* referenced nodes rather than the whole graph to keep the payload tight — the
|
|
384
|
+
* Dossier never opens on an unreferenced file.
|
|
385
|
+
*/
|
|
386
|
+
declare function buildNodeMetrics(report: GraphReportResult, metrics: ReadonlyMap<string, NodeMetrics>): Record<string, NodeMetrics>;
|
|
387
|
+
/**
|
|
388
|
+
* Reading-order centrality (top-N central files) PLUS the centrality of every
|
|
389
|
+
* node referenced elsewhere in the payload (hotspots, silos, coupling), so the
|
|
390
|
+
* Dossier can always show a PageRank score instead of "—" for a hotspot that
|
|
391
|
+
* falls outside the top-N central list. Sorted descending, so the Overview
|
|
392
|
+
* "reading order" (top-6 slice) is unaffected.
|
|
393
|
+
*/
|
|
394
|
+
declare function buildCentralFiles(report: GraphReportResult, centrality: ReadonlyMap<string, number>): {
|
|
395
|
+
nodeId: string;
|
|
396
|
+
score: number;
|
|
397
|
+
}[];
|
|
398
|
+
/** One declared symbol's utilization (C-53), tagged with its declaring file. */
|
|
399
|
+
interface SymbolUtil {
|
|
400
|
+
symbolId: string;
|
|
401
|
+
name: string;
|
|
402
|
+
fileId: string;
|
|
403
|
+
utilization: number;
|
|
404
|
+
/** Whether the symbol is exported (model B, C-64); internal helpers are false. */
|
|
405
|
+
exported: boolean;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Pair each `symbol` node with its utilization metric and declaring file (C-53).
|
|
409
|
+
* Model B (C-64) adds non-exported function/class nodes; the `exported` attr
|
|
410
|
+
* (defaulting true for pre-C-64 nodes that lack it) lets the Dossier separate a
|
|
411
|
+
* file's public surface from its internal helpers.
|
|
412
|
+
*/
|
|
413
|
+
declare function collectSymbolUtil(nodes: readonly {
|
|
414
|
+
id: string;
|
|
415
|
+
kind: string;
|
|
416
|
+
name: string;
|
|
417
|
+
parentId?: string;
|
|
418
|
+
attrs?: Record<string, unknown>;
|
|
419
|
+
}[], metrics: ReadonlyMap<string, NodeMetrics>): SymbolUtil[];
|
|
420
|
+
interface HotExport {
|
|
421
|
+
name: string;
|
|
422
|
+
utilization: number;
|
|
423
|
+
/** The symbol's OWN cognitive complexity (C-58); undefined for a class/type/re-export. */
|
|
424
|
+
cognitive?: number;
|
|
425
|
+
/** Distinct files that reference this export (inbound `references` edges, C-59). */
|
|
426
|
+
consumers: number;
|
|
427
|
+
/** Exported (public surface) vs internal helper (model B, C-64). */
|
|
428
|
+
exported: boolean;
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Per-file symbol detail for the Dossier (C-59, C-64): each of a file's declared
|
|
432
|
+
* symbols with its per-symbol complexity (C-58), utilization (C-53), and
|
|
433
|
+
* consumer count (inbound references). Keeps zero-utilization symbols — a complex
|
|
434
|
+
* but unused symbol is worth seeing. Model B (C-64) adds internal helpers, so the
|
|
435
|
+
* two surfaces are ranked and capped *separately* (the Dossier renders them in
|
|
436
|
+
* their own sections): exports by utilization-then-complexity, internal functions
|
|
437
|
+
* by complexity — otherwise a large public surface would slice every internal
|
|
438
|
+
* helper out before it could show. Scoped to Dossier-openable files; top 8 each.
|
|
439
|
+
*/
|
|
440
|
+
declare function buildHotExports(symbols: readonly SymbolUtil[], referenced: ReadonlySet<string>, metrics: ReadonlyMap<string, NodeMetrics>, consumersBySymbol: ReadonlyMap<string, number>): Record<string, HotExport[]>;
|
|
441
|
+
interface BlastRadiusEntry {
|
|
442
|
+
symbolId: string;
|
|
443
|
+
name: string;
|
|
444
|
+
fileId: string;
|
|
445
|
+
utilization: number;
|
|
446
|
+
complexity: number;
|
|
447
|
+
churn: number;
|
|
448
|
+
score: number;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Rank exports by blast radius = utilization × cognitive complexity × file
|
|
452
|
+
* churn (C-53, "idea d"). Surfaces the single riskiest thing to touch: a
|
|
453
|
+
* heavily-used export that is both hard to reason about and actively changing.
|
|
454
|
+
* Complexity is the export's OWN cognitive complexity (C-58, per-symbol) when
|
|
455
|
+
* known, falling back to the file's max for exports without a computed symbol
|
|
456
|
+
* complexity (e.g. class or re-exported symbols). This is what lets two exports
|
|
457
|
+
* of one hot file separate instead of tying on the file-broadcast value.
|
|
458
|
+
* Stable files (churn 0) score 0 and drop out — a load-bearing export in a calm
|
|
459
|
+
* file isn't a change hazard.
|
|
460
|
+
*/
|
|
461
|
+
declare function buildBlastRadius(symbols: readonly SymbolUtil[], metrics: ReadonlyMap<string, NodeMetrics>, churnByFile: ReadonlyMap<string, number>, limit?: number): BlastRadiusEntry[];
|
|
462
|
+
|
|
463
|
+
type Severity = "error" | "warning";
|
|
464
|
+
interface MetricMaxRule {
|
|
465
|
+
type: "metric-max";
|
|
466
|
+
id: string;
|
|
467
|
+
metric: string;
|
|
468
|
+
max: number;
|
|
469
|
+
kind?: NodeKind;
|
|
470
|
+
severity?: Severity;
|
|
471
|
+
exclude?: string[];
|
|
472
|
+
excludeRoles?: NodeRole[];
|
|
473
|
+
}
|
|
474
|
+
interface MetricMinRule {
|
|
475
|
+
type: "metric-min";
|
|
476
|
+
id: string;
|
|
477
|
+
metric: string;
|
|
478
|
+
min: number;
|
|
479
|
+
kind?: NodeKind;
|
|
480
|
+
severity?: Severity;
|
|
481
|
+
exclude?: string[];
|
|
482
|
+
excludeRoles?: NodeRole[];
|
|
483
|
+
}
|
|
484
|
+
interface MetricProductMaxRule {
|
|
485
|
+
type: "metric-product-max";
|
|
486
|
+
id: string;
|
|
487
|
+
metrics: string[];
|
|
488
|
+
max: number;
|
|
489
|
+
kind?: NodeKind;
|
|
490
|
+
severity?: Severity;
|
|
491
|
+
exclude?: string[];
|
|
492
|
+
excludeRoles?: NodeRole[];
|
|
493
|
+
}
|
|
494
|
+
/** Flags nodes whose value sits strictly above the given percentile of the metric over every node of the kind. */
|
|
495
|
+
interface MetricOutlierRule {
|
|
496
|
+
type: "metric-outlier";
|
|
497
|
+
id: string;
|
|
498
|
+
metric: string;
|
|
499
|
+
kind: NodeKind;
|
|
500
|
+
/** 50 to 100; the threshold interpolates linearly between the two nearest ranked values. */
|
|
501
|
+
percentile: number;
|
|
502
|
+
/** Fewest nodes that must carry the metric before any is judged; defaults to 20. */
|
|
503
|
+
minSample?: number;
|
|
504
|
+
/** A node is flagged only if its value also exceeds this absolute floor, guarding sparse metrics whose percentile sits at or near zero. */
|
|
505
|
+
floor?: number;
|
|
506
|
+
/** When true, rank and gate on the pool of carriers with a non-zero value only; zero-valued nodes are never flagged. */
|
|
507
|
+
rankNonZero?: boolean;
|
|
508
|
+
severity?: Severity;
|
|
509
|
+
}
|
|
510
|
+
interface ForbidImportRule {
|
|
511
|
+
type: "forbid-import";
|
|
512
|
+
id: string;
|
|
513
|
+
from: string;
|
|
514
|
+
to: string;
|
|
515
|
+
/** Destination patterns `to` matches but the rule allows, such as one sanctioned entry file. */
|
|
516
|
+
except?: string[];
|
|
517
|
+
severity?: Severity;
|
|
518
|
+
}
|
|
519
|
+
interface LayeredDepsRule {
|
|
520
|
+
type: "layered-deps";
|
|
521
|
+
id: string;
|
|
522
|
+
layers: string[][];
|
|
523
|
+
severity?: Severity;
|
|
524
|
+
/** An import is dropped when its source or destination file has one of these roles. */
|
|
525
|
+
excludeRoles?: NodeRole[];
|
|
526
|
+
}
|
|
527
|
+
interface NoInternalOnlyBarrelsRule {
|
|
528
|
+
type: "no-internal-only-barrels";
|
|
529
|
+
id: string;
|
|
530
|
+
/** Path prefixes marking package roots; node ids carry no intrinsic package membership. */
|
|
531
|
+
packageRoots: string[];
|
|
532
|
+
severity?: Severity;
|
|
533
|
+
/** Globs or substrings to skip, such as CLI bin entries the role classifier calls barrels. */
|
|
534
|
+
exclude?: string[];
|
|
535
|
+
}
|
|
536
|
+
type CheckRule = MetricMaxRule | MetricMinRule | MetricProductMaxRule | MetricOutlierRule | ForbidImportRule | LayeredDepsRule | NoInternalOnlyBarrelsRule;
|
|
537
|
+
interface CheckRulesFile {
|
|
538
|
+
rules: CheckRule[];
|
|
539
|
+
}
|
|
540
|
+
interface CheckViolation {
|
|
541
|
+
ruleId: string;
|
|
542
|
+
severity: Severity;
|
|
543
|
+
nodeId: string;
|
|
544
|
+
message: string;
|
|
545
|
+
metric?: string;
|
|
546
|
+
value?: number;
|
|
547
|
+
threshold?: number;
|
|
548
|
+
destinationId?: string;
|
|
549
|
+
isCarryover?: boolean;
|
|
550
|
+
/** Repo-relative file the violation sits in; a symbol's parent file. */
|
|
551
|
+
path?: string;
|
|
552
|
+
lineStart?: number;
|
|
553
|
+
lineEnd?: number;
|
|
554
|
+
symbol?: string;
|
|
555
|
+
/** One line a reader can check without re-running the rule, such as `loc=412 (max 350)`. */
|
|
556
|
+
evidence?: string;
|
|
557
|
+
tool?: string;
|
|
558
|
+
}
|
|
559
|
+
interface CheckResult {
|
|
560
|
+
snapshotId: number;
|
|
561
|
+
baselineSnapshotId?: number;
|
|
562
|
+
rulesEvaluated: number;
|
|
563
|
+
nodesEvaluated: number;
|
|
564
|
+
violations: CheckViolation[];
|
|
565
|
+
newErrors: number;
|
|
566
|
+
newWarnings: number;
|
|
567
|
+
carryoverErrors: number;
|
|
568
|
+
carryoverWarnings: number;
|
|
569
|
+
passed: boolean;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/** The fields that identify a violation; a finding read back from an export carries them too. */
|
|
573
|
+
type ViolationIdentity = Pick<CheckViolation, "ruleId" | "nodeId" | "destinationId">;
|
|
574
|
+
/** Identity of a violation across snapshots: severity, value and message may change, the key does not. */
|
|
575
|
+
declare function violationKey(v: ViolationIdentity): string;
|
|
576
|
+
/** {@link violationKey} with both node ids carried into another snapshot's id space; unmoved ids key as before. */
|
|
577
|
+
declare function rebasedViolationKey(v: ViolationIdentity, resolve: (id: string) => string): string;
|
|
578
|
+
|
|
579
|
+
/** What bucketing reads from a violation: its identity and, when the rule measures one, its value. */
|
|
580
|
+
type BucketableViolation = ViolationIdentity & Pick<CheckViolation, "value">;
|
|
581
|
+
interface UnchangedViolation<V extends BucketableViolation = CheckViolation> {
|
|
582
|
+
from: V;
|
|
583
|
+
to: V;
|
|
584
|
+
delta: number | null;
|
|
585
|
+
}
|
|
586
|
+
interface ViolationBuckets<V extends BucketableViolation = CheckViolation> {
|
|
587
|
+
newViolations: V[];
|
|
588
|
+
resolvedViolations: V[];
|
|
589
|
+
unchanged: UnchangedViolation<V>[];
|
|
590
|
+
worsened: UnchangedViolation<V>[];
|
|
591
|
+
improved: UnchangedViolation<V>[];
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Bucket two snapshots' violations as new, resolved, or unchanged (worsened or improved
|
|
595
|
+
* by value). `resolve` carries from-side ids into the to-snapshot, so a moved file's
|
|
596
|
+
* violations stay unchanged; omitted, ids match as they are. Needs no store.
|
|
597
|
+
*/
|
|
598
|
+
declare function bucketViolations<V extends BucketableViolation>(from: readonly V[], to: readonly V[], resolve?: (id: string) => string): ViolationBuckets<V>;
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Symbol-level change coupling (C-60). Decomposes a god-file the file-level
|
|
602
|
+
* coupling view rolls up as one blob (e.g. `types.ts`) into *which symbol* is
|
|
603
|
+
* used where. Built on the C-53 `references` edge substrate: `src` is the
|
|
604
|
+
* importing file, `dst` is the imported symbol node id (`<fileId>#<name>`).
|
|
605
|
+
*
|
|
606
|
+
* Two slices, both pure functions of the assembled reference edge set (a
|
|
607
|
+
* whole-graph rollup like utilization/PageRank, sound under incremental reuse
|
|
608
|
+
* since it reads the reassembled edges, not a per-file cache):
|
|
609
|
+
*
|
|
610
|
+
* - **Slice C — per-symbol consumers**: invert the edges by `dst`, giving the
|
|
611
|
+
* set of files that import each symbol. Directly answers "what IN this file is
|
|
612
|
+
* used where"; covers span-less types/consts that a git-hunk approach cannot.
|
|
613
|
+
* - **Slice B — co-import coupling**: group edges by `src`, and every pair of
|
|
614
|
+
* symbols co-imported by the same file is a coupling pair. Two symbols that
|
|
615
|
+
* are always imported together travel together — structural (used-together),
|
|
616
|
+
* drift-free coupling, as opposed to the temporal (changed-together) git
|
|
617
|
+
* co-edit signal.
|
|
618
|
+
*/
|
|
619
|
+
/** The minimal shape of a `references` edge this module consumes. */
|
|
620
|
+
interface ReferenceEdgeLite {
|
|
621
|
+
/** Importing file id. */
|
|
622
|
+
srcId: string;
|
|
623
|
+
/** Imported symbol node id (`<fileId>#<name>`). */
|
|
624
|
+
dstId: string;
|
|
625
|
+
}
|
|
626
|
+
/** One symbol's consumer set (Slice C). */
|
|
627
|
+
interface SymbolConsumers {
|
|
628
|
+
symbolId: string;
|
|
629
|
+
/** Declaring file, parsed from the symbol id. */
|
|
630
|
+
fileId: string;
|
|
631
|
+
/** Export name. */
|
|
632
|
+
name: string;
|
|
633
|
+
/** Distinct importing file ids, sorted. */
|
|
634
|
+
consumers: string[];
|
|
635
|
+
}
|
|
636
|
+
/** A pair of symbols co-imported by the same file (Slice B). */
|
|
637
|
+
interface SymbolCouplingPair {
|
|
638
|
+
aId: string;
|
|
639
|
+
aFile: string;
|
|
640
|
+
aName: string;
|
|
641
|
+
bId: string;
|
|
642
|
+
bFile: string;
|
|
643
|
+
bName: string;
|
|
644
|
+
/** Distinct files that import both symbols. */
|
|
645
|
+
coImports: number;
|
|
646
|
+
/** True when the two symbols are declared in different files. */
|
|
647
|
+
crossFile: boolean;
|
|
648
|
+
}
|
|
649
|
+
interface SymbolCouplingOptions {
|
|
650
|
+
/** Skip pairs co-imported by fewer than this many files. Default 2. */
|
|
651
|
+
minCoImports?: number;
|
|
652
|
+
/**
|
|
653
|
+
* Skip importing files that reference more than this many distinct symbols —
|
|
654
|
+
* a wide barrel-style importer would otherwise explode into O(n²) noise
|
|
655
|
+
* pairs, mirroring change-coupling's large-commit guard. Default 40.
|
|
656
|
+
*/
|
|
657
|
+
largeImporterThreshold?: number;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Group reference edges by imported symbol, yielding each symbol's distinct
|
|
661
|
+
* consuming files (Slice C). Sorted by consumer count desc, then symbol id, so
|
|
662
|
+
* the most broadly-depended-on exports lead.
|
|
663
|
+
*/
|
|
664
|
+
declare function computeSymbolConsumers(edges: readonly ReferenceEdgeLite[]): SymbolConsumers[];
|
|
665
|
+
/**
|
|
666
|
+
* Every pair of symbols co-imported by the same file becomes a coupling pair,
|
|
667
|
+
* counted by how many distinct files co-import them (Slice B). Wide importers
|
|
668
|
+
* are dropped to keep the pairing near-linear. Sorted by co-import count desc,
|
|
669
|
+
* cross-file pairs preferred at a tie (they are the actionable ones — a
|
|
670
|
+
* same-file pair is just cohesion within one module).
|
|
671
|
+
*/
|
|
672
|
+
declare function computeSymbolCoupling(edges: readonly ReferenceEdgeLite[], options?: SymbolCouplingOptions): SymbolCouplingPair[];
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Dashboard payload assembly for symbol-level coupling (C-60). Turns the raw
|
|
676
|
+
* `references` edge set into two lean, capped slices the Coupling view renders,
|
|
677
|
+
* so a god-file like `types.ts` decomposes into *which symbol goes where*
|
|
678
|
+
* instead of one aggregate file node:
|
|
679
|
+
*
|
|
680
|
+
* - **Slice B — `symbolCoupling`**: symbol pairs consistently co-imported by the
|
|
681
|
+
* same files (structural, used-together coupling; drift-free).
|
|
682
|
+
* - **Slice C — `symbolConsumers`**: per-file groups of exported symbols with
|
|
683
|
+
* the files that consume each — the literal "what IN this file is used where".
|
|
684
|
+
*
|
|
685
|
+
* Kept out of dashboard-payload.ts (at its LOC ceiling) so that file stays lean.
|
|
686
|
+
*/
|
|
687
|
+
/** One co-imported symbol pair, dashboard-lean (Slice B). */
|
|
688
|
+
interface SymbolCouplingRow {
|
|
689
|
+
aName: string;
|
|
690
|
+
aFile: string;
|
|
691
|
+
bName: string;
|
|
692
|
+
bFile: string;
|
|
693
|
+
coImports: number;
|
|
694
|
+
crossFile: boolean;
|
|
695
|
+
}
|
|
696
|
+
/** One symbol and the files that consume it (Slice C). */
|
|
697
|
+
interface SymbolConsumerRow {
|
|
698
|
+
name: string;
|
|
699
|
+
/** Up to CONSUMER_SAMPLE consuming file ids. */
|
|
700
|
+
consumers: string[];
|
|
701
|
+
/** Full distinct-consumer count (consumers may be truncated). */
|
|
702
|
+
consumerCount: number;
|
|
703
|
+
}
|
|
704
|
+
/** A file's shared exports and where each goes (Slice C, grouped for the view). */
|
|
705
|
+
interface SymbolConsumerGroup {
|
|
706
|
+
fileId: string;
|
|
707
|
+
symbols: SymbolConsumerRow[];
|
|
708
|
+
/** Sum of consumer counts across kept symbols — the group's ordering weight. */
|
|
709
|
+
totalConsumers: number;
|
|
710
|
+
}
|
|
711
|
+
interface SymbolCouplingPayload {
|
|
712
|
+
symbolCoupling: SymbolCouplingRow[];
|
|
713
|
+
symbolConsumers: SymbolConsumerGroup[];
|
|
714
|
+
}
|
|
715
|
+
declare function buildSymbolCouplingPayload(edges: readonly ReferenceEdgeLite[]): SymbolCouplingPayload;
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Snapshot-level (window-independent) derived data: which file pairs are joined
|
|
719
|
+
* by a static import edge, and each node's PageRank centrality. Computed once
|
|
720
|
+
* per snapshot from a single db open, shared across all window payloads.
|
|
721
|
+
*/
|
|
722
|
+
interface SnapshotContext {
|
|
723
|
+
linkedPairs: ReadonlySet<string>;
|
|
724
|
+
centrality: ReadonlyMap<string, number>;
|
|
725
|
+
/**
|
|
726
|
+
* Ids of files that participate in at least one *internal* (repo-to-repo)
|
|
727
|
+
* import/re-export edge. A file missing here has no resolved imports in the
|
|
728
|
+
* graph — either it isn't indexed, or its imports couldn't be resolved (e.g. a
|
|
729
|
+
* dir outside the tsconfig project, whose relative specifiers resolve to junk).
|
|
730
|
+
* Either way the import evidence is absent, so a co-change touching it can't be
|
|
731
|
+
* called hidden-vs-import-backed; it's "unverifiable", not "hidden".
|
|
732
|
+
*/
|
|
733
|
+
connectedNodes: ReadonlySet<string>;
|
|
734
|
+
/** Per-node structural metrics (loc, cognitive/cyclomatic max, nesting, fan) for the Dossier. */
|
|
735
|
+
metrics: ReadonlyMap<string, NodeMetrics>;
|
|
736
|
+
/** Per-export utilization (C-53), for the Dossier "hot exports" list and the blast-radius section. */
|
|
737
|
+
symbols: readonly SymbolUtil[];
|
|
738
|
+
/** Inbound `references` count per symbol id (C-59): how many files consume each export. */
|
|
739
|
+
consumersBySymbol: ReadonlyMap<string, number>;
|
|
740
|
+
/** Symbol-level coupling slices (C-60): co-imported pairs + per-symbol consumers. */
|
|
741
|
+
symbolCoupling?: SymbolCouplingPayload;
|
|
742
|
+
}
|
|
743
|
+
type CouplingClass = {
|
|
744
|
+
hidden: boolean;
|
|
745
|
+
unindexed: boolean;
|
|
746
|
+
};
|
|
747
|
+
/**
|
|
748
|
+
* Classify a co-changed pair against the static import graph:
|
|
749
|
+
* - unindexed: an endpoint has no resolved internal imports → can't tell (not hidden).
|
|
750
|
+
* - hidden: both connected, but no import/re-export edge joins them → the signal.
|
|
751
|
+
* - expected: both connected and import-backed → usually fine.
|
|
752
|
+
*/
|
|
753
|
+
declare function classifyCoupling(a: string, b: string, ctx: SnapshotContext): CouplingClass;
|
|
754
|
+
/** Order-independent key for an undirected file pair (JSON tuple; no in-band separator). */
|
|
755
|
+
declare function pairKey(a: string, b: string): string;
|
|
756
|
+
|
|
757
|
+
interface PackageRoot {
|
|
758
|
+
/** Path relative to repo root, posix-separated (e.g., "packages/cli"). Empty for repo-root package. */
|
|
759
|
+
id: string;
|
|
760
|
+
/** Display name from package.json or directory basename. */
|
|
761
|
+
name: string;
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* For each file id, find the longest matching package prefix.
|
|
765
|
+
* Returns a Map from package id → file ids assigned to it.
|
|
766
|
+
* Files matching no package are returned under the empty-string key.
|
|
767
|
+
*/
|
|
768
|
+
declare function bucketFilesByPackage(fileIds: readonly string[], packages: readonly PackageRoot[]): Map<string, string[]>;
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Per-package and per-pair structural quality metrics, plus an overall
|
|
772
|
+
* Newman-Girvan modularity Q for the package partition.
|
|
773
|
+
*
|
|
774
|
+
* Operates on a barrel-resolved edge set by default: edges that land on a
|
|
775
|
+
* file with role="barrel" are rewritten to land on the underlying files
|
|
776
|
+
* the barrel re-exports from (transitively). The intent is to measure the
|
|
777
|
+
* real dependency surface, not the re-export plumbing.
|
|
778
|
+
*
|
|
779
|
+
* See C-8 task notes for the design rationale and empirical calibration
|
|
780
|
+
* data from the 2026-05-21 codewatch dogfood.
|
|
781
|
+
*/
|
|
782
|
+
interface PartitionQualityInput {
|
|
783
|
+
/** Logical packages — at minimum needs an id. */
|
|
784
|
+
packages: ReadonlyArray<{
|
|
785
|
+
id: string;
|
|
786
|
+
}>;
|
|
787
|
+
/** Package id → list of file ids assigned to that package. */
|
|
788
|
+
fileByPackage: ReadonlyMap<string, ReadonlyArray<string>>;
|
|
789
|
+
/** All file/module/external nodes for the snapshot. Used to identify role="barrel". */
|
|
790
|
+
nodes: readonly GraphNode[];
|
|
791
|
+
/** All edges for the snapshot. */
|
|
792
|
+
edges: readonly GraphEdge[];
|
|
793
|
+
/**
|
|
794
|
+
* When true, edges landing on a barrel file are resolved through its
|
|
795
|
+
* re-export chain to the underlying source files. The cheap implementation
|
|
796
|
+
* fans each barrel import into N synthetic edges (one per re-export
|
|
797
|
+
* target), which over-attributes — a single `import { x } from "./pkg"`
|
|
798
|
+
* becomes N edges as if every re-export were used. Default false until
|
|
799
|
+
* a weighted or symbol-tracking version is available.
|
|
800
|
+
*/
|
|
801
|
+
resolveBarrels?: boolean;
|
|
802
|
+
}
|
|
803
|
+
type PackageFlag = "weak-boundary";
|
|
804
|
+
type PairFlag = "tight" | "moderate" | "none";
|
|
805
|
+
type PackageLayer = "top" | "middle" | "foundation";
|
|
806
|
+
interface PackageStats {
|
|
807
|
+
pkgId: string;
|
|
808
|
+
fileCount: number;
|
|
809
|
+
internalEdges: number;
|
|
810
|
+
outgoingEdges: number;
|
|
811
|
+
incomingEdges: number;
|
|
812
|
+
/** internal / (internal + outgoing) — higher = more self-contained. */
|
|
813
|
+
cohesion: number;
|
|
814
|
+
/** outgoing / (outgoing + incoming) — Martin's I metric at the package level. */
|
|
815
|
+
instability: number;
|
|
816
|
+
/**
|
|
817
|
+
* Abstractness proxy A ∈ [0,1]: share of the package's files with role
|
|
818
|
+
* "types" (dedicated type/interface definitions). codewatch has no
|
|
819
|
+
* symbol-level abstract/concrete counts, so this file-role ratio stands in
|
|
820
|
+
* for Martin's A. Enables the instability×abstractness main-sequence plot.
|
|
821
|
+
*/
|
|
822
|
+
abstractness: number;
|
|
823
|
+
layer: PackageLayer;
|
|
824
|
+
flags: PackageFlag[];
|
|
825
|
+
}
|
|
826
|
+
interface PairCoupling {
|
|
827
|
+
from: string;
|
|
828
|
+
to: string;
|
|
829
|
+
edges: number;
|
|
830
|
+
/** edges / files(from) — fraction of from-side files contributing dependencies into `to`. */
|
|
831
|
+
intensity: number;
|
|
832
|
+
flag: PairFlag;
|
|
833
|
+
}
|
|
834
|
+
interface PartitionQualityResult {
|
|
835
|
+
modularityQ: number;
|
|
836
|
+
totalEdges: number;
|
|
837
|
+
perPackage: PackageStats[];
|
|
838
|
+
pairCoupling: PairCoupling[];
|
|
839
|
+
/** Total raised flags across packages + pairs (excluding "moderate"). */
|
|
840
|
+
flagsCount: number;
|
|
841
|
+
}
|
|
842
|
+
declare function computePartitionQuality(input: PartitionQualityInput): PartitionQualityResult;
|
|
843
|
+
/**
|
|
844
|
+
* Invert a package→files bucket map into a file→package lookup, skipping the
|
|
845
|
+
* empty-string "unassigned" bucket. Shared by partition-quality and the CLI's
|
|
846
|
+
* arch/wiki package rollups, which all need the same file→package direction.
|
|
847
|
+
*/
|
|
848
|
+
declare function invertBuckets(fileByPackage: ReadonlyMap<string, ReadonlyArray<string>>): Map<string, string>;
|
|
849
|
+
|
|
850
|
+
/** A top-level sub-directory of a drilled package, rendered inside its cluster. */
|
|
851
|
+
interface ArchSubNode {
|
|
852
|
+
/** Full path id, e.g. "packages/cli/src/commands". */
|
|
853
|
+
id: string;
|
|
854
|
+
/** Directory name shown as the node label, e.g. "commands". */
|
|
855
|
+
label: string;
|
|
856
|
+
files: number;
|
|
857
|
+
}
|
|
858
|
+
interface ArchPackage {
|
|
859
|
+
id: string;
|
|
860
|
+
name: string;
|
|
861
|
+
files: number;
|
|
862
|
+
/** Present when the package was drilled (--depth modules); renders as a subgraph. */
|
|
863
|
+
subNodes?: ArchSubNode[];
|
|
864
|
+
}
|
|
865
|
+
interface ArchEdge {
|
|
866
|
+
from: string;
|
|
867
|
+
to: string;
|
|
868
|
+
count: number;
|
|
869
|
+
}
|
|
870
|
+
interface ArchResult {
|
|
871
|
+
snapshot: SnapshotRow;
|
|
872
|
+
packages: ArchPackage[];
|
|
873
|
+
edges: ArchEdge[];
|
|
874
|
+
includesExternal: boolean;
|
|
875
|
+
/** Present when options.health=true. */
|
|
876
|
+
quality?: PartitionQualityResult;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
declare const EXTERNAL_BUCKET = "(external)";
|
|
880
|
+
/** Default file-count threshold above which a package is drilled (C-10). */
|
|
881
|
+
declare const DEFAULT_MAX_PACKAGE_SIZE = 30;
|
|
882
|
+
interface ComputeArchInput {
|
|
883
|
+
snapshot: SnapshotRow;
|
|
884
|
+
nodes: readonly GraphNode[];
|
|
885
|
+
edges: readonly GraphEdge[];
|
|
886
|
+
packages: readonly PackageRoot[];
|
|
887
|
+
exclude?: string[];
|
|
888
|
+
excludeRole?: string[];
|
|
889
|
+
includeExternal?: boolean;
|
|
890
|
+
minEdges?: number;
|
|
891
|
+
depth?: "modules";
|
|
892
|
+
maxPackageSize?: number;
|
|
893
|
+
}
|
|
894
|
+
declare function computeArch(input: ComputeArchInput): ArchResult;
|
|
895
|
+
declare function filteredFileIds(nodes: readonly GraphNode[], options: {
|
|
896
|
+
exclude?: string[];
|
|
897
|
+
excludeRole?: string[];
|
|
898
|
+
}): string[];
|
|
899
|
+
declare function aggregateEdges(edges: ReadonlyArray<{
|
|
900
|
+
srcId: string;
|
|
901
|
+
dstId: string;
|
|
902
|
+
}>, pkgByFile: ReadonlyMap<string, string>, nodeByFile: ReadonlyMap<string, string>, externalIds: ReadonlySet<string>, includeExternal: boolean): Map<string, Map<string, number>>;
|
|
903
|
+
declare function toSortedEdges(counts: ReadonlyMap<string, ReadonlyMap<string, number>>, minEdges: number): ArchEdge[];
|
|
904
|
+
declare function packagesReferencedByEdges(counts: ReadonlyMap<string, ReadonlyMap<string, number>>): Set<string>;
|
|
905
|
+
|
|
906
|
+
/** How a test↔source pairing was inferred. */
|
|
907
|
+
type LinkMethod = "path" | "coedit";
|
|
908
|
+
interface TestSourceLink {
|
|
909
|
+
/** Node id of the test file. */
|
|
910
|
+
testId: string;
|
|
911
|
+
/** Node id of the (non-test) file it covers. */
|
|
912
|
+
sourceId: string;
|
|
913
|
+
method: LinkMethod;
|
|
914
|
+
}
|
|
915
|
+
interface LinkTestsOptions {
|
|
916
|
+
/** Minimum co-edit count for a pass-2 (coedit) link. Default 2. */
|
|
917
|
+
minCoEditCount?: number;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* Two-pass test↔source linker. Pass 1 pairs each test file with non-test files
|
|
921
|
+
* matching its path conventions (high confidence). Pass 2 supplements tests
|
|
922
|
+
* left unpaired by pass 1 with their strongest co-edited non-test partner from
|
|
923
|
+
* change-coupling. Handles orphan tests (no pairing), orphan/untested sources
|
|
924
|
+
* (no incoming link), and one-to-many pairings (a test matching several
|
|
925
|
+
* sources, or a source covered by several tests).
|
|
926
|
+
*/
|
|
927
|
+
declare function linkTestsToSources(nodes: readonly GraphNode[], coEditPairs: readonly CoEditPair[], options?: LinkTestsOptions): TestSourceLink[];
|
|
928
|
+
/**
|
|
929
|
+
* Per-source coverage breadth: how many distinct test files link to each
|
|
930
|
+
* covered source. Emitted only for sources with at least one linked test.
|
|
931
|
+
*/
|
|
932
|
+
declare function testCoverageCountMetrics(links: readonly TestSourceLink[]): GraphMetric[];
|
|
933
|
+
/** Map each covered source to the set of test files that link to it. */
|
|
934
|
+
declare function groupTestsBySource(links: readonly TestSourceLink[]): Map<string, Set<string>>;
|
|
935
|
+
|
|
936
|
+
export { type LinkTestsOptions as $, type ArchEdge as A, type BlastRadiusEntry as B, type CheckRule as C, DEFAULT_HEALTH_WEIGHTS as D, type EdgeKind as E, type FileFingerprint as F, type GraphNode as G, type DeadModuleRow as H, type IdAlias as I, EXTERNAL_BUCKET as J, type ForbidImportRule as K, type GraphReportResult as L, type GrowthRiskRow as M, type NodeKind as N, type HealthComponent as O, type HealthComponentKey as P, type HealthInput as Q, type ReferenceEdgeLite as R, type SnapshotRow as S, type TestSourceLink as T, type HealthWeights as U, type ViolationBuckets as V, type HotExport as W, type HotspotDelta as X, type HotspotRow as Y, type LayeredDepsRule as Z, type LinkMethod as _, type GraphEdge as a, topCentralFiles as a$, type MetricMaxRule as a0, type MetricMinRule as a1, type MetricOutlierRule as a2, type MetricProductMaxRule as a3, type NewHotspot as a4, type NoInternalOnlyBarrelsRule as a5, type PackageFlag as a6, type PackageLayer as a7, type PackageRoot as a8, type PackageStats as a9, buildReportContext as aA, buildSymbolCouplingPayload as aB, busFactorOf as aC, classifyCoupling as aD, collectNodeMetrics as aE, collectSymbolUtil as aF, computeArch as aG, computeHealth as aH, computePartitionQuality as aI, computeReportDrift as aJ, computeSymbolConsumers as aK, computeSymbolCoupling as aL, filteredFileIds as aM, groupTestsBySource as aN, hotspotScoreOf as aO, invertBuckets as aP, keepNode as aQ, linkTestsToSources as aR, lookupMetric as aS, packagesReferencedByEdges as aT, pairKey as aU, publicApiFiles as aV, rebasedViolationKey as aW, referencedNodes as aX, testCoverageCountMetrics as aY, toSortedEdges as aZ, topBusFactorRisks as a_, type PairCoupling as aa, type PairFlag as ab, type PartitionQualityInput as ac, type PartitionQualityResult as ad, type PenaltyWeight as ae, type ReportContext as af, type ReportContextInput as ag, type ReportDrift as ah, type SnapshotContext as ai, type SymbolConsumerGroup as aj, type SymbolConsumerRow as ak, type SymbolCouplingPayload as al, type SymbolCouplingRow as am, type SymbolUtil as an, type TestCoverageRow as ao, type UnchangedViolation as ap, type UntestedRiskRow as aq, type UnusedExportRow as ar, type ViolationIdentity as as, aggregateEdges as at, bucketFilesByPackage as au, bucketViolations as av, buildBlastRadius as aw, buildCentralFiles as ax, buildHotExports as ay, buildNodeMetrics as az, type GraphMetric as b, topDeadModules as b0, topGrowthRisks as b1, topHotspots as b2, topTestCoverageRisks as b3, topUntestedRisks as b4, topUnusedExports as b5, violationKey as b6, hotspotComplexityOf as b7, type GraphFragment as c, type NodeRole as d, type IdAliasReason as e, type NodeMetrics as f, type SymbolConsumers as g, type SymbolCouplingOptions as h, type SymbolCouplingPair as i, type CheckResult as j, type CheckViolation as k, type Severity as l, type ArchPackage as m, type ArchResult as n, type ArchSubNode as o, type BucketableViolation as p, type BusFactorChange as q, type BusFactorRow as r, type CentralRow as s, type CheckRulesFile as t, type ComputeArchInput as u, type ComputeDriftInput as v, type CouplingClass as w, type CouplingDelta as x, type CouplingRow as y, DEFAULT_MAX_PACKAGE_SIZE as z };
|