@archora/core 1.3.0 → 2.1.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/package.json +1 -1
- package/src/README.md +2 -2
- package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +1 -1
- package/src/analyzer/__tests__/hotZones.test.ts +128 -0
- package/src/analyzer/__tests__/memoryRisk.test.ts +94 -0
- package/src/analyzer/__tests__/recommendations.test.ts +67 -0
- package/src/analyzer/__tests__/resolve.test.ts +27 -0
- package/src/analyzer/__tests__/rsc.test.ts +62 -3
- package/src/analyzer/buildGraph.ts +2 -1
- package/src/analyzer/hotZones.ts +94 -2
- package/src/analyzer/incremental.ts +2 -1
- package/src/analyzer/index.ts +2 -1
- package/src/analyzer/memoryRisk.ts +33 -2
- package/src/analyzer/recommendations.ts +15 -11
- package/src/analyzer/resolve.ts +29 -4
- package/src/analyzer/rsc.ts +18 -1
- package/src/analyzer/types.ts +17 -0
- package/src/cache/index.ts +18 -3
- package/src/codegen/initConfig.ts +4 -0
- package/src/config/frontScopeConfig.ts +4 -0
- package/src/diff/__tests__/diffScans.test.ts +64 -1
- package/src/diff/diffScans.ts +31 -1
- package/src/diff/types.ts +19 -1
- package/src/git/computeTemporalCoupling.ts +5 -1
- package/src/index.ts +6 -0
- package/src/report/__tests__/buildDeadCodeReport.test.ts +108 -0
- package/src/report/buildDeadCodeReport.ts +110 -0
- package/src/report/buildFixPlan.ts +14 -69
- package/src/views/__tests__/analyzerViews.test.ts +6 -0
- package/src/views/analyzerViews.ts +1 -6
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ModuleId, ModuleNode, ScanResult } from '../analyzer/types';
|
|
2
|
+
|
|
3
|
+
export interface DeadCodeCandidate {
|
|
4
|
+
id: ModuleId;
|
|
5
|
+
loc: number;
|
|
6
|
+
kind: ModuleNode['kind'];
|
|
7
|
+
group: 'isolated' | 'script-entry';
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DeadCodeReport {
|
|
12
|
+
candidates: DeadCodeCandidate[];
|
|
13
|
+
/** Sum of candidate loc — the reclaimable LOC estimate. */
|
|
14
|
+
totalLoc: number;
|
|
15
|
+
isolatedCount: number;
|
|
16
|
+
scriptEntryCount: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const DEAD_MODULE_REASON =
|
|
20
|
+
'No resolved imports connect this module to the analyzed dependency model.';
|
|
21
|
+
|
|
22
|
+
export function buildDeadCodeReport(scan: ScanResult): DeadCodeReport {
|
|
23
|
+
const candidates: DeadCodeCandidate[] = [];
|
|
24
|
+
|
|
25
|
+
for (const module of scan.modules) {
|
|
26
|
+
if (!isReviewModule(module.id)) continue;
|
|
27
|
+
const metrics = scan.metrics[module.id];
|
|
28
|
+
const fanIn = metrics?.fanIn ?? 0;
|
|
29
|
+
const fanOut = metrics?.fanOut ?? 0;
|
|
30
|
+
if (
|
|
31
|
+
fanIn === 0 &&
|
|
32
|
+
fanOut === 0 &&
|
|
33
|
+
module.exports.length === 0 &&
|
|
34
|
+
module.kind !== 'entry' &&
|
|
35
|
+
module.kind !== 'test' &&
|
|
36
|
+
!module.isGenerated &&
|
|
37
|
+
!module.isInfra
|
|
38
|
+
) {
|
|
39
|
+
candidates.push({
|
|
40
|
+
id: module.id,
|
|
41
|
+
loc: module.loc,
|
|
42
|
+
kind: module.kind,
|
|
43
|
+
group: isScriptEntryCandidate(module.id) ? 'script-entry' : 'isolated',
|
|
44
|
+
reason: DEAD_MODULE_REASON,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
candidates.sort((a, b) => b.loc - a.loc || a.id.localeCompare(b.id));
|
|
50
|
+
|
|
51
|
+
const totalLoc = candidates.reduce((sum, c) => sum + c.loc, 0);
|
|
52
|
+
const isolatedCount = candidates.filter((c) => c.group === 'isolated').length;
|
|
53
|
+
const scriptEntryCount = candidates.filter((c) => c.group === 'script-entry').length;
|
|
54
|
+
|
|
55
|
+
return { candidates, totalLoc, isolatedCount, scriptEntryCount };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Repair action and verify text for an unreachable module finding.
|
|
60
|
+
* Shared with buildFixPlan so the two never diverge.
|
|
61
|
+
*/
|
|
62
|
+
export function unreachableRepair(id: ModuleId): {
|
|
63
|
+
action: string;
|
|
64
|
+
verify: string;
|
|
65
|
+
params: Record<string, unknown>;
|
|
66
|
+
} {
|
|
67
|
+
if (isScriptEntryCandidate(id)) {
|
|
68
|
+
return {
|
|
69
|
+
action: `Treat ${id} as a script entry: add it to architecture entry configuration or exclude it from review scope; delete only after confirming no package script or CI job calls it.`,
|
|
70
|
+
verify:
|
|
71
|
+
'Run archora report . --format fix-plan after entry/exclude config and confirm this script is no longer a priority finding.',
|
|
72
|
+
params: { entryCandidate: 'script' },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
action:
|
|
78
|
+
'Check whether this file is dead code, dynamically loaded outside analyzer reach, or should be declared as an entry point.',
|
|
79
|
+
verify:
|
|
80
|
+
'Re-scan after deletion, ignore, or entry-point configuration and confirm the candidate is gone.',
|
|
81
|
+
params: {},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Matches modules that should go through architecture review.
|
|
87
|
+
* Excludes fixture directories and test files — same predicate as buildFixPlan.
|
|
88
|
+
*/
|
|
89
|
+
export function isReviewModule(id: ModuleId): boolean {
|
|
90
|
+
return !/(^|\/)(fixtures|test\/fixtures|__fixtures__|__tests__|__mocks__)(\/|$)|\.(test|spec)\./u.test(
|
|
91
|
+
id,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Heuristic: a file directly under a `scripts/` directory with a JS extension
|
|
97
|
+
* is likely a standalone Node script, not a dead module.
|
|
98
|
+
*/
|
|
99
|
+
export function isScriptEntryCandidate(id: ModuleId): boolean {
|
|
100
|
+
const normalized = id.replace(/\\/gu, '/');
|
|
101
|
+
let scriptsIndex = 0;
|
|
102
|
+
if (!normalized.startsWith('scripts/')) {
|
|
103
|
+
const nestedIndex = normalized.indexOf('/scripts/');
|
|
104
|
+
if (nestedIndex < 0) return false;
|
|
105
|
+
scriptsIndex = nestedIndex + 1;
|
|
106
|
+
}
|
|
107
|
+
const file = normalized.slice(scriptsIndex + 'scripts/'.length);
|
|
108
|
+
if (!file) return false;
|
|
109
|
+
return file.endsWith('.js') || file.endsWith('.cjs') || file.endsWith('.mjs');
|
|
110
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ModuleId, ModuleNode, Recommendation, ScanResult } from '../analyzer/types';
|
|
2
|
+
import { buildDeadCodeReport, isReviewModule, unreachableRepair } from './buildDeadCodeReport';
|
|
2
3
|
|
|
3
4
|
export interface FixPlanFinding {
|
|
4
5
|
type:
|
|
@@ -223,33 +224,19 @@ function buildPriorityFindings(scan: ScanResult, generated: Set<ModuleId>): FixP
|
|
|
223
224
|
});
|
|
224
225
|
}
|
|
225
226
|
|
|
226
|
-
for (const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
) {
|
|
240
|
-
const repair = unreachableRepair(module.id);
|
|
241
|
-
findings.push({
|
|
242
|
-
type: 'unreachable-from-entries',
|
|
243
|
-
id: `${module.id}:unreachable`,
|
|
244
|
-
title: 'Unreachable module candidate',
|
|
245
|
-
weight: Math.min(70, 35 + module.loc / 10),
|
|
246
|
-
targets: [module.id],
|
|
247
|
-
reason: 'No resolved imports connect this module to the analyzed dependency model.',
|
|
248
|
-
action: repair.action,
|
|
249
|
-
verify: repair.verify,
|
|
250
|
-
params: { loc: module.loc, ...repair.params },
|
|
251
|
-
});
|
|
252
|
-
}
|
|
227
|
+
for (const candidate of buildDeadCodeReport(scan).candidates) {
|
|
228
|
+
const repair = unreachableRepair(candidate.id);
|
|
229
|
+
findings.push({
|
|
230
|
+
type: 'unreachable-from-entries',
|
|
231
|
+
id: `${candidate.id}:unreachable`,
|
|
232
|
+
title: 'Unreachable module candidate',
|
|
233
|
+
weight: Math.min(70, 35 + candidate.loc / 10),
|
|
234
|
+
targets: [candidate.id],
|
|
235
|
+
reason: candidate.reason,
|
|
236
|
+
action: repair.action,
|
|
237
|
+
verify: repair.verify,
|
|
238
|
+
params: { loc: candidate.loc, ...repair.params },
|
|
239
|
+
});
|
|
253
240
|
}
|
|
254
241
|
|
|
255
242
|
for (const violation of scan.contractViolations.slice(0, 50)) {
|
|
@@ -318,52 +305,10 @@ function buildPriorityFindings(scan: ScanResult, generated: Set<ModuleId>): FixP
|
|
|
318
305
|
.slice(0, 100);
|
|
319
306
|
}
|
|
320
307
|
|
|
321
|
-
function isReviewModule(id: ModuleId): boolean {
|
|
322
|
-
return !/(^|\/)(fixtures|test\/fixtures|__fixtures__|__tests__|__mocks__)(\/|$)|\.(test|spec)\./u.test(
|
|
323
|
-
id,
|
|
324
|
-
);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
308
|
function isLikelyGeneratedPath(id: ModuleId): boolean {
|
|
328
309
|
return /(^|\/)(generated|__generated__|openapi|swagger|graphql-codegen)(\/|$)/iu.test(id);
|
|
329
310
|
}
|
|
330
311
|
|
|
331
|
-
function unreachableRepair(id: ModuleId): {
|
|
332
|
-
action: string;
|
|
333
|
-
verify: string;
|
|
334
|
-
params: Record<string, unknown>;
|
|
335
|
-
} {
|
|
336
|
-
if (isScriptEntryCandidate(id)) {
|
|
337
|
-
return {
|
|
338
|
-
action: `Treat ${id} as a script entry: add it to architecture entry configuration or exclude it from review scope; delete only after confirming no package script or CI job calls it.`,
|
|
339
|
-
verify:
|
|
340
|
-
'Run archora report . --format fix-plan after entry/exclude config and confirm this script is no longer a priority finding.',
|
|
341
|
-
params: { entryCandidate: 'script' },
|
|
342
|
-
};
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
action:
|
|
347
|
-
'Check whether this file is dead code, dynamically loaded outside analyzer reach, or should be declared as an entry point.',
|
|
348
|
-
verify:
|
|
349
|
-
'Re-scan after deletion, ignore, or entry-point configuration and confirm the candidate is gone.',
|
|
350
|
-
params: {},
|
|
351
|
-
};
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function isScriptEntryCandidate(id: ModuleId): boolean {
|
|
355
|
-
const normalized = id.replace(/\\/gu, '/');
|
|
356
|
-
let scriptsIndex = 0;
|
|
357
|
-
if (!normalized.startsWith('scripts/')) {
|
|
358
|
-
const nestedIndex = normalized.indexOf('/scripts/');
|
|
359
|
-
if (nestedIndex < 0) return false;
|
|
360
|
-
scriptsIndex = nestedIndex + 1;
|
|
361
|
-
}
|
|
362
|
-
const file = normalized.slice(scriptsIndex + 'scripts/'.length);
|
|
363
|
-
if (!file) return false;
|
|
364
|
-
return file.endsWith('.js') || file.endsWith('.cjs') || file.endsWith('.mjs');
|
|
365
|
-
}
|
|
366
|
-
|
|
367
312
|
function barrelCycleRepair(
|
|
368
313
|
scan: ScanResult,
|
|
369
314
|
modules: readonly ModuleId[],
|
|
@@ -168,12 +168,18 @@ describe('analyzer view helpers', () => {
|
|
|
168
168
|
changedModules: [],
|
|
169
169
|
newCycles: current.cycles,
|
|
170
170
|
resolvedCycles: [],
|
|
171
|
+
newLayerViolations: [],
|
|
172
|
+
resolvedLayerViolations: [],
|
|
173
|
+
newContractViolations: [],
|
|
174
|
+
resolvedContractViolations: [],
|
|
171
175
|
summary: {
|
|
172
176
|
addedModules: 0,
|
|
173
177
|
removedModules: 0,
|
|
174
178
|
changedModules: 0,
|
|
175
179
|
newCycles: current.cycles.length,
|
|
176
180
|
resolvedCycles: 0,
|
|
181
|
+
newLayerViolations: 0,
|
|
182
|
+
newContractViolations: 0,
|
|
177
183
|
},
|
|
178
184
|
},
|
|
179
185
|
});
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
import { detectLayer } from '../analyzer/layers';
|
|
11
11
|
import { diffScans } from '../diff';
|
|
12
12
|
import type { ScanDiff } from '../diff/types';
|
|
13
|
+
import { isReviewModule } from '../report/buildDeadCodeReport';
|
|
13
14
|
|
|
14
15
|
export type MatrixGrouping = 'area' | 'layer' | 'folder' | 'package';
|
|
15
16
|
|
|
@@ -1188,12 +1189,6 @@ function isSourceFile(part: string): boolean {
|
|
|
1188
1189
|
return /\.[cm]?[jt]sx?$/u.test(part) || /\.(vue|svelte)$/u.test(part);
|
|
1189
1190
|
}
|
|
1190
1191
|
|
|
1191
|
-
function isReviewModule(id: ModuleId): boolean {
|
|
1192
|
-
return !/(^|\/)(fixtures|test\/fixtures|__fixtures__|__tests__|__mocks__)(\/|$)|\.(test|spec)\./u.test(
|
|
1193
|
-
id,
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
1192
|
function stripExtension(part: string): string {
|
|
1198
1193
|
return part.replace(/\.[^.]+$/u, '');
|
|
1199
1194
|
}
|