@archora/core 1.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/LICENSE +201 -0
- package/README.md +62 -0
- package/package.json +36 -0
- package/src/README.md +4 -0
- package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +145 -0
- package/src/analyzer/__tests__/_paths.ts +8 -0
- package/src/analyzer/__tests__/analyze.test.ts +522 -0
- package/src/analyzer/__tests__/archDebt.test.ts +111 -0
- package/src/analyzer/__tests__/asyncLifecycleRisk.test.ts +122 -0
- package/src/analyzer/__tests__/browserFsAccessFileSource.test.ts +97 -0
- package/src/analyzer/__tests__/bundle.test.ts +191 -0
- package/src/analyzer/__tests__/classify.test.ts +99 -0
- package/src/analyzer/__tests__/contracts.test.ts +372 -0
- package/src/analyzer/__tests__/crossSourceConsistency.test.ts +317 -0
- package/src/analyzer/__tests__/cyclePatterns.test.ts +132 -0
- package/src/analyzer/__tests__/cycles.test.ts +74 -0
- package/src/analyzer/__tests__/detect.test.ts +62 -0
- package/src/analyzer/__tests__/discover.test.ts +68 -0
- package/src/analyzer/__tests__/displayId.test.ts +30 -0
- package/src/analyzer/__tests__/feedbackArcSet.test.ts +168 -0
- package/src/analyzer/__tests__/inMemoryFileSource.test.ts +34 -0
- package/src/analyzer/__tests__/incremental.test.ts +154 -0
- package/src/analyzer/__tests__/layers.test.ts +87 -0
- package/src/analyzer/__tests__/layersOverrides.test.ts +120 -0
- package/src/analyzer/__tests__/memoryRisk.test.ts +132 -0
- package/src/analyzer/__tests__/metrics.test.ts +59 -0
- package/src/analyzer/__tests__/parserRegistry.test.ts +54 -0
- package/src/analyzer/__tests__/parsers.test.ts +187 -0
- package/src/analyzer/__tests__/reactParser.test.ts +93 -0
- package/src/analyzer/__tests__/recommendations.test.ts +171 -0
- package/src/analyzer/__tests__/referenceSnapshot.test.ts +63 -0
- package/src/analyzer/__tests__/resolve.test.ts +294 -0
- package/src/analyzer/__tests__/rsc.test.ts +130 -0
- package/src/analyzer/__tests__/signals.test.ts +316 -0
- package/src/analyzer/__tests__/suggestContracts.test.ts +108 -0
- package/src/analyzer/__tests__/svelteParser.test.ts +108 -0
- package/src/analyzer/__tests__/typeOnlyCandidates.test.ts +163 -0
- package/src/analyzer/__tests__/vueAutoImport.test.ts +177 -0
- package/src/analyzer/archDebt.ts +68 -0
- package/src/analyzer/asyncLifecycleRisk.ts +234 -0
- package/src/analyzer/buildGraph.ts +683 -0
- package/src/analyzer/bundle/analyzeBundle.ts +147 -0
- package/src/analyzer/bundle/index.ts +12 -0
- package/src/analyzer/bundle/parseStats.ts +152 -0
- package/src/analyzer/bundle/types.ts +85 -0
- package/src/analyzer/classify.ts +54 -0
- package/src/analyzer/contracts.ts +265 -0
- package/src/analyzer/cyclePatterns.ts +138 -0
- package/src/analyzer/cycles.ts +98 -0
- package/src/analyzer/detect.ts +34 -0
- package/src/analyzer/discover.ts +131 -0
- package/src/analyzer/displayId.ts +21 -0
- package/src/analyzer/entryPoints.ts +136 -0
- package/src/analyzer/feedbackArcSet.ts +332 -0
- package/src/analyzer/fileSource.ts +8 -0
- package/src/analyzer/hotZones.ts +17 -0
- package/src/analyzer/incremental.ts +455 -0
- package/src/analyzer/index.ts +444 -0
- package/src/analyzer/layers.ts +183 -0
- package/src/analyzer/loadAliases.ts +288 -0
- package/src/analyzer/memoryRisk.ts +345 -0
- package/src/analyzer/metrics.ts +156 -0
- package/src/analyzer/parsers/index.ts +62 -0
- package/src/analyzer/parsers/reactParser.ts +24 -0
- package/src/analyzer/parsers/svelteParser.ts +46 -0
- package/src/analyzer/parsers/tsParser.ts +364 -0
- package/src/analyzer/parsers/vueParser.ts +109 -0
- package/src/analyzer/recommendations.ts +432 -0
- package/src/analyzer/resolve.ts +315 -0
- package/src/analyzer/rsc.ts +120 -0
- package/src/analyzer/signals.ts +684 -0
- package/src/analyzer/sources/browserFsAccessFileSource.ts +132 -0
- package/src/analyzer/sources/inMemoryFileSource.ts +24 -0
- package/src/analyzer/sources/nodeFsFileSource.ts +93 -0
- package/src/analyzer/sources/tauriFileSource.ts +68 -0
- package/src/analyzer/suggestContracts.ts +214 -0
- package/src/analyzer/typeOnlyCandidates.ts +233 -0
- package/src/analyzer/types.ts +537 -0
- package/src/cache/__tests__/cache.test.ts +316 -0
- package/src/cache/index.ts +432 -0
- package/src/codegen/__tests__/applyTypeOnlyFix.integration.test.ts +62 -0
- package/src/codegen/__tests__/applyTypeOnlyFix.test.ts +176 -0
- package/src/codegen/__tests__/configSnippets.test.ts +230 -0
- package/src/codegen/applyTypeOnlyFix.ts +344 -0
- package/src/codegen/configSnippets.ts +172 -0
- package/src/codegen/initConfig.ts +223 -0
- package/src/config/__tests__/frontScopeConfig.test.ts +187 -0
- package/src/config/frontScopeConfig.ts +830 -0
- package/src/diff/__tests__/diffScans.test.ts +103 -0
- package/src/diff/diffScans.ts +61 -0
- package/src/diff/index.ts +2 -0
- package/src/diff/types.ts +39 -0
- package/src/git/__tests__/computeChurn.test.ts +113 -0
- package/src/git/__tests__/computeTemporalCoupling.test.ts +125 -0
- package/src/git/__tests__/parseGitLog.test.ts +120 -0
- package/src/git/computeChurn.ts +111 -0
- package/src/git/computeTemporalCoupling.ts +114 -0
- package/src/git/index.ts +24 -0
- package/src/git/parseGitLog.ts +124 -0
- package/src/git/readGitHistory.ts +130 -0
- package/src/git/types.ts +119 -0
- package/src/index.ts +137 -0
- package/src/report/__tests__/buildFixPlan.test.ts +357 -0
- package/src/report/__tests__/buildJsonReport.test.ts +34 -0
- package/src/report/buildFixPlan.ts +481 -0
- package/src/report/buildJsonReport.ts +27 -0
- package/src/search/__tests__/parseQuery.test.ts +67 -0
- package/src/search/__tests__/search.test.ts +172 -0
- package/src/search/index.ts +281 -0
- package/src/search/parseQuery.ts +75 -0
- package/src/views/__tests__/analyzerViews.test.ts +558 -0
- package/src/views/analyzerViews.ts +1294 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
// Инкрементальный пере-скан: на вход — предыдущий ScanResult и список
|
|
2
|
+
// изменённых относительных путей. Если все изменения — это «pure modify»
|
|
3
|
+
// существующих модулей со static-импортами, переразрешаем только их
|
|
4
|
+
// outgoing-рёбра и пересчитываем downstream-метрики/циклы. Иначе fallback
|
|
5
|
+
// на полный `analyze()`.
|
|
6
|
+
//
|
|
7
|
+
// Подход «простой вариант» из плана 15.2: оптимизация только для типичного
|
|
8
|
+
// IDE-save сценария. Глоб/префикс-импорты, auto-import шаблонов, изменения
|
|
9
|
+
// конфигов и добавление/удаление файлов перенаправляются в полный пайплайн —
|
|
10
|
+
// корректность важнее скорости, а эти случаи редки на сохранении одного файла.
|
|
11
|
+
|
|
12
|
+
import type { FileSource } from './fileSource';
|
|
13
|
+
import type {
|
|
14
|
+
AnalyzerWarning,
|
|
15
|
+
DependencyEdge,
|
|
16
|
+
FactConfidence,
|
|
17
|
+
ImportResolutionKind,
|
|
18
|
+
ModuleNode,
|
|
19
|
+
ParsedFile,
|
|
20
|
+
ParsedFileSummary,
|
|
21
|
+
RawImport,
|
|
22
|
+
ScanResult,
|
|
23
|
+
} from './types';
|
|
24
|
+
import { analyze, type AnalyzeOptions } from './index';
|
|
25
|
+
import { loadAliases } from './loadAliases';
|
|
26
|
+
import { createResolver } from './resolve';
|
|
27
|
+
import { createParserRegistry, isParseFailure } from './parsers';
|
|
28
|
+
import { classifyKind, isInfra } from './classify';
|
|
29
|
+
import { classifyModuleRuntime, detectRscLeaks } from './rsc';
|
|
30
|
+
import { detectCycles } from './cycles';
|
|
31
|
+
import { computeMetrics } from './metrics';
|
|
32
|
+
import { rankHotZones } from './hotZones';
|
|
33
|
+
import { detectLayerViolations } from './layers';
|
|
34
|
+
import { computeArchDebt } from './archDebt';
|
|
35
|
+
import { computeRecommendations } from './recommendations';
|
|
36
|
+
import { applySignalSuppressions, buildArchitectureSignals } from './signals';
|
|
37
|
+
import { discoverEntryPoints } from './entryPoints';
|
|
38
|
+
import { checkContracts } from './contracts';
|
|
39
|
+
import { loadArchoraConfig } from '../config/frontScopeConfig';
|
|
40
|
+
import type { Framework } from './detect';
|
|
41
|
+
|
|
42
|
+
export interface IncrementalAnalyzeInput {
|
|
43
|
+
prev: ScanResult;
|
|
44
|
+
source: FileSource;
|
|
45
|
+
/** Relative paths reported by the watcher (already debounced & deduped). */
|
|
46
|
+
changedFiles: string[];
|
|
47
|
+
/**
|
|
48
|
+
* Files that exist now but were not in the previous scan. Currently any
|
|
49
|
+
* non-empty value forces a full rescan: re-resolving prior `resolve-failed`
|
|
50
|
+
* warnings against the new module ids is not (yet) implemented and would
|
|
51
|
+
* otherwise leak stale warnings. Optional - watchers built on
|
|
52
|
+
* `incrementalAnalyze` directly without persistent cache pass undefined.
|
|
53
|
+
*/
|
|
54
|
+
addedFiles?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* Files that were in the previous scan but are missing now. Handled on the
|
|
57
|
+
* fast path: matching modules and incident edges are dropped and downstream
|
|
58
|
+
* metrics are recomputed. Resolution warnings keyed to the removed file are
|
|
59
|
+
* stripped. Edges that previously pointed *at* a removed module become
|
|
60
|
+
* `resolve-failed` warnings against their `from` files - matching what a
|
|
61
|
+
* cold scan would emit.
|
|
62
|
+
*/
|
|
63
|
+
removedFiles?: string[];
|
|
64
|
+
options?: AnalyzeOptions;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const CONFIG_PATTERNS: readonly RegExp[] = [
|
|
68
|
+
/^tsconfig.*\.json$/u,
|
|
69
|
+
/^vite\.config\.(?:ts|js|mts|mjs|cts|cjs)$/u,
|
|
70
|
+
/^\.archora\.json$/u,
|
|
71
|
+
/^package\.json$/u,
|
|
72
|
+
/^nuxt\.config\.(?:ts|js|mts|mjs)$/u,
|
|
73
|
+
/^svelte\.config\.(?:ts|js|mts|mjs)$/u,
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
function isConfigFile(rel: string): boolean {
|
|
77
|
+
return CONFIG_PATTERNS.some((re) => re.test(rel));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Re-run the analysis using the previous scan as a baseline. The function may
|
|
82
|
+
* either fast-path (re-parsing only the changed modules and reusing existing
|
|
83
|
+
* graph data) or fall back to a full {@link analyze} call when fast-path
|
|
84
|
+
* preconditions don't hold. Either way, the returned `ScanResult` has the same
|
|
85
|
+
* structure and semantics as a full scan.
|
|
86
|
+
*/
|
|
87
|
+
export async function incrementalAnalyze(input: IncrementalAnalyzeInput): Promise<ScanResult> {
|
|
88
|
+
const { prev, source, changedFiles, options } = input;
|
|
89
|
+
const addedFiles = input.addedFiles ?? [];
|
|
90
|
+
const removedFiles = input.removedFiles ?? [];
|
|
91
|
+
const start = Date.now();
|
|
92
|
+
|
|
93
|
+
if (changedFiles.length === 0 && addedFiles.length === 0 && removedFiles.length === 0) {
|
|
94
|
+
return prev;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Adds force a full rescan: previously `resolve-failed` imports could now
|
|
98
|
+
// resolve to the new files, and we don't track which warnings to retract.
|
|
99
|
+
if (addedFiles.length > 0) return analyze(source, options);
|
|
100
|
+
|
|
101
|
+
const anyConfig = [...changedFiles, ...removedFiles].some(isConfigFile);
|
|
102
|
+
if (anyConfig) return analyze(source, options);
|
|
103
|
+
|
|
104
|
+
const moduleIds = new Set(prev.modules.map((m) => m.id));
|
|
105
|
+
const allChangedKnown = changedFiles.every((f) => moduleIds.has(f));
|
|
106
|
+
const allRemovedKnown = removedFiles.every((f) => moduleIds.has(f));
|
|
107
|
+
if (!allChangedKnown || !allRemovedKnown) return analyze(source, options);
|
|
108
|
+
|
|
109
|
+
// Verify each changed file still exists (a delete masquerading as a modify
|
|
110
|
+
// from a stale watcher batch would otherwise produce a stale module entry).
|
|
111
|
+
for (const rel of changedFiles) {
|
|
112
|
+
if (!(await source.exists(rel))) return analyze(source, options);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const config = await loadArchoraConfig(source);
|
|
116
|
+
const aliases = await loadAliases(source, prev.project);
|
|
117
|
+
const resolver = createResolver(source, { aliases });
|
|
118
|
+
const registry = createParserRegistry({
|
|
119
|
+
...(config.dynamicLoaders ? { dynamicLoaders: config.dynamicLoaders } : {}),
|
|
120
|
+
...(prev.project.detectedFramework !== 'unknown'
|
|
121
|
+
? { framework: prev.project.detectedFramework as Framework }
|
|
122
|
+
: {}),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Re-parse changed files. Any glob/prefix/template-auto-import involvement
|
|
126
|
+
// pushes us off the fast-path: those edge kinds are rebuilt inside
|
|
127
|
+
// buildGraph from the full file list and are not safe to recompute per-file.
|
|
128
|
+
const reparsed = new Map<string, ParsedFile>();
|
|
129
|
+
const newWarnings: AnalyzerWarning[] = [];
|
|
130
|
+
for (const rel of changedFiles) {
|
|
131
|
+
let content: string;
|
|
132
|
+
try {
|
|
133
|
+
content = await source.read(rel);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
newWarnings.push({
|
|
136
|
+
code: 'parse-failed',
|
|
137
|
+
message: 'Read failed during incremental rescan',
|
|
138
|
+
file: rel,
|
|
139
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
140
|
+
});
|
|
141
|
+
return analyze(source, options);
|
|
142
|
+
}
|
|
143
|
+
const result = registry.parse({ relPath: rel, content });
|
|
144
|
+
if (isParseFailure(result)) return analyze(source, options);
|
|
145
|
+
if (result.imports.some((i) => i.pattern === 'glob' || i.pattern === 'prefix')) {
|
|
146
|
+
return analyze(source, options);
|
|
147
|
+
}
|
|
148
|
+
if (result.templateRefs && result.templateRefs.length > 0) {
|
|
149
|
+
return analyze(source, options);
|
|
150
|
+
}
|
|
151
|
+
reparsed.set(rel, result);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Build new modules array: drop removed entries, replace changed ones.
|
|
155
|
+
const removedSet = new Set(removedFiles);
|
|
156
|
+
const updatedModules: ModuleNode[] = [];
|
|
157
|
+
for (const m of prev.modules) {
|
|
158
|
+
if (removedSet.has(m.id)) continue;
|
|
159
|
+
const p = reparsed.get(m.id);
|
|
160
|
+
if (!p) {
|
|
161
|
+
updatedModules.push(m);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
updatedModules.push({
|
|
165
|
+
id: p.relPath,
|
|
166
|
+
absPath: p.relPath,
|
|
167
|
+
kind: classifyKind(p, p.relPath),
|
|
168
|
+
language: p.language,
|
|
169
|
+
loc: p.loc,
|
|
170
|
+
exports: p.exports,
|
|
171
|
+
isInfra: isInfra(p.relPath),
|
|
172
|
+
runtime: classifyModuleRuntime({
|
|
173
|
+
relPath: p.relPath,
|
|
174
|
+
framework: prev.project.detectedFramework as Framework,
|
|
175
|
+
...(p.directives ? { directives: p.directives } : {}),
|
|
176
|
+
}),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const updatedModuleIds = new Set(updatedModules.map((m) => m.id));
|
|
180
|
+
|
|
181
|
+
// Replace outgoing edges for changed modules and drop incident edges of
|
|
182
|
+
// removed ones. Edges that previously pointed *at* a removed module become
|
|
183
|
+
// unresolved imports - record them as `resolve-failed` warnings on the
|
|
184
|
+
// surviving `from` file (matches what a cold scan would emit).
|
|
185
|
+
const changedSet = new Set(changedFiles);
|
|
186
|
+
const preservedEdges: DependencyEdge[] = [];
|
|
187
|
+
for (const e of prev.edges) {
|
|
188
|
+
if (changedSet.has(e.from)) continue;
|
|
189
|
+
if (removedSet.has(e.from)) continue;
|
|
190
|
+
if (removedSet.has(e.to)) {
|
|
191
|
+
newWarnings.push({
|
|
192
|
+
code: 'resolve-failed',
|
|
193
|
+
message: `Cannot resolve "${e.specifier}"`,
|
|
194
|
+
file: e.from,
|
|
195
|
+
detail: `previously resolved to "${e.to}" which was deleted`,
|
|
196
|
+
});
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
preservedEdges.push(e);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const newEdges: DependencyEdge[] = [];
|
|
203
|
+
for (const [rel, parsed] of reparsed) {
|
|
204
|
+
for (const imp of parsed.imports) {
|
|
205
|
+
// we already bailed on glob/prefix above
|
|
206
|
+
if (imp.pattern === 'glob' || imp.pattern === 'prefix') continue;
|
|
207
|
+
if (isAssetSpecifier(imp.specifier)) continue;
|
|
208
|
+
if (isExternal(imp.specifier)) continue;
|
|
209
|
+
const resolved = await resolver.resolve(imp.specifier, rel);
|
|
210
|
+
if (resolved && updatedModuleIds.has(resolved)) {
|
|
211
|
+
if (resolved === rel) {
|
|
212
|
+
newWarnings.push({
|
|
213
|
+
code: 'self-import',
|
|
214
|
+
message: 'Module imports itself',
|
|
215
|
+
file: rel,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
newEdges.push({
|
|
219
|
+
from: rel,
|
|
220
|
+
to: resolved,
|
|
221
|
+
kind: imp.kind,
|
|
222
|
+
specifier: imp.specifier,
|
|
223
|
+
resolved: true,
|
|
224
|
+
...edgeMeta(imp, imp.kind === 'dynamic' ? 'dynamic-literal' : 'literal', 'high', false),
|
|
225
|
+
});
|
|
226
|
+
} else {
|
|
227
|
+
newWarnings.push({
|
|
228
|
+
code: 'resolve-failed',
|
|
229
|
+
message: `Cannot resolve "${imp.specifier}"`,
|
|
230
|
+
file: rel,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const allEdges = preservedEdges.concat(newEdges);
|
|
237
|
+
|
|
238
|
+
// Downstream pure recomputation. These are O(V+E) and dominated by graph
|
|
239
|
+
// size, not file IO, so they're cheap to re-run wholesale.
|
|
240
|
+
const cycles = detectCycles(updatedModules, allEdges);
|
|
241
|
+
const entries = await discoverEntryPoints({
|
|
242
|
+
source,
|
|
243
|
+
moduleIds: updatedModules.map((m) => m.id),
|
|
244
|
+
framework: prev.project.detectedFramework as Framework,
|
|
245
|
+
...(config.entryPoints ? { configEntryPoints: config.entryPoints } : {}),
|
|
246
|
+
});
|
|
247
|
+
const metrics = computeMetrics({ modules: updatedModules, edges: allEdges, cycles, entries });
|
|
248
|
+
for (const m of updatedModules) {
|
|
249
|
+
if (entries.includes(m.id) && m.kind === 'unknown') m.kind = 'entry';
|
|
250
|
+
}
|
|
251
|
+
const hotZones = rankHotZones({
|
|
252
|
+
modules: updatedModules,
|
|
253
|
+
metrics,
|
|
254
|
+
topN: options?.topHotZones ?? 10,
|
|
255
|
+
});
|
|
256
|
+
const layerViolations = detectLayerViolations(updatedModules, allEdges, config.layerOverrides);
|
|
257
|
+
const archDebt = computeArchDebt({
|
|
258
|
+
modules: updatedModules,
|
|
259
|
+
cycles,
|
|
260
|
+
layerViolations,
|
|
261
|
+
metrics,
|
|
262
|
+
hotZoneCount: hotZones.length,
|
|
263
|
+
});
|
|
264
|
+
const contractViolations = checkContracts({
|
|
265
|
+
modules: updatedModules,
|
|
266
|
+
edges: allEdges,
|
|
267
|
+
metrics,
|
|
268
|
+
cycles,
|
|
269
|
+
...(config.contracts ? { contracts: config.contracts } : {}),
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
const rscLeaks = detectRscLeaks({ modules: updatedModules, edges: allEdges });
|
|
273
|
+
if (rscLeaks.length > 0) contractViolations.unshift(...rscLeaks);
|
|
274
|
+
|
|
275
|
+
const recommendations = computeRecommendations({
|
|
276
|
+
modules: updatedModules,
|
|
277
|
+
edges: allEdges,
|
|
278
|
+
metrics,
|
|
279
|
+
cycles,
|
|
280
|
+
layerViolations,
|
|
281
|
+
hotZones,
|
|
282
|
+
entries,
|
|
283
|
+
typeOnlyCandidates: [],
|
|
284
|
+
contractViolations,
|
|
285
|
+
});
|
|
286
|
+
const parserFacts = mergeParserFacts(prev.parserFacts ?? [], reparsed);
|
|
287
|
+
|
|
288
|
+
// Carry over original analyzer warnings, drop any keyed to the changed or
|
|
289
|
+
// removed files (they're being re-emitted, or no longer relevant), and
|
|
290
|
+
// append our own.
|
|
291
|
+
const carriedWarnings = prev.warnings.filter(
|
|
292
|
+
(w) => !w.file || (!changedSet.has(w.file) && !removedSet.has(w.file)),
|
|
293
|
+
);
|
|
294
|
+
const warnings = [...carriedWarnings, ...newWarnings];
|
|
295
|
+
const builtSignals = buildArchitectureSignals({
|
|
296
|
+
recommendations,
|
|
297
|
+
warnings,
|
|
298
|
+
parserFacts,
|
|
299
|
+
...(config.signals?.insightLimit !== undefined
|
|
300
|
+
? { insightLimit: config.signals.insightLimit }
|
|
301
|
+
: {}),
|
|
302
|
+
...(config.signals?.minInsightSeverity
|
|
303
|
+
? { minInsightSeverity: config.signals.minInsightSeverity }
|
|
304
|
+
: {}),
|
|
305
|
+
...(config.signals?.minInsightConfidence
|
|
306
|
+
? { minInsightConfidence: config.signals.minInsightConfidence }
|
|
307
|
+
: {}),
|
|
308
|
+
});
|
|
309
|
+
const signals =
|
|
310
|
+
config.signals?.suppressions && config.signals.suppressions.length > 0
|
|
311
|
+
? applySignalSuppressions(builtSignals.signals, config.signals.suppressions).signals
|
|
312
|
+
: builtSignals.signals;
|
|
313
|
+
const { insights } = builtSignals;
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
project: prev.project,
|
|
317
|
+
modules: updatedModules,
|
|
318
|
+
edges: allEdges,
|
|
319
|
+
cycles,
|
|
320
|
+
metrics,
|
|
321
|
+
hotZones,
|
|
322
|
+
layerViolations,
|
|
323
|
+
archDebt,
|
|
324
|
+
recommendations,
|
|
325
|
+
signals,
|
|
326
|
+
insights,
|
|
327
|
+
parserFacts,
|
|
328
|
+
contractViolations,
|
|
329
|
+
scannedAt: new Date().toISOString(),
|
|
330
|
+
durationMs: Date.now() - start,
|
|
331
|
+
warnings,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function edgeMeta(
|
|
336
|
+
imp: RawImport,
|
|
337
|
+
resolutionKind: ImportResolutionKind,
|
|
338
|
+
confidence: FactConfidence,
|
|
339
|
+
approximate: boolean,
|
|
340
|
+
): Pick<DependencyEdge, 'confidence' | 'resolutionKind' | 'approximate'> {
|
|
341
|
+
return {
|
|
342
|
+
confidence: imp.confidence ?? confidence,
|
|
343
|
+
resolutionKind: imp.resolutionKind ?? resolutionKind,
|
|
344
|
+
approximate: imp.approximate ?? approximate,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function mergeParserFacts(
|
|
349
|
+
prevFacts: readonly ParsedFileSummary[],
|
|
350
|
+
reparsed: ReadonlyMap<string, ParsedFile>,
|
|
351
|
+
): ParsedFileSummary[] {
|
|
352
|
+
const changed = new Set(reparsed.keys());
|
|
353
|
+
const next = prevFacts.filter((f) => !changed.has(f.relPath));
|
|
354
|
+
for (const parsed of reparsed.values()) next.push(toParsedFileSummary(parsed));
|
|
355
|
+
return next.sort((a, b) => a.relPath.localeCompare(b.relPath));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function toParsedFileSummary(file: ParsedFile): ParsedFileSummary {
|
|
359
|
+
return {
|
|
360
|
+
relPath: file.relPath,
|
|
361
|
+
language: file.language,
|
|
362
|
+
loc: file.loc,
|
|
363
|
+
imports: file.imports.map((imp) => ({
|
|
364
|
+
specifier: imp.specifier,
|
|
365
|
+
kind: imp.kind,
|
|
366
|
+
resolutionKind:
|
|
367
|
+
imp.resolutionKind ??
|
|
368
|
+
(isAssetSpecifier(imp.specifier)
|
|
369
|
+
? 'asset'
|
|
370
|
+
: imp.kind === 'dynamic'
|
|
371
|
+
? 'dynamic-literal'
|
|
372
|
+
: 'literal'),
|
|
373
|
+
confidence: imp.confidence ?? 'high',
|
|
374
|
+
approximate: imp.approximate ?? false,
|
|
375
|
+
...(isAssetSpecifier(imp.specifier) ? { isAsset: true } : {}),
|
|
376
|
+
})),
|
|
377
|
+
exports: file.exports.map((name) => ({
|
|
378
|
+
name,
|
|
379
|
+
kind: name === 'default' ? 'default' : 'named',
|
|
380
|
+
confidence: 'medium',
|
|
381
|
+
})),
|
|
382
|
+
runtimeFacts: [
|
|
383
|
+
{
|
|
384
|
+
runtime: file.directives?.includes('use server')
|
|
385
|
+
? 'server'
|
|
386
|
+
: file.directives?.includes('use client')
|
|
387
|
+
? 'client'
|
|
388
|
+
: 'shared',
|
|
389
|
+
source: file.directives && file.directives.length > 0 ? 'directive' : 'default',
|
|
390
|
+
confidence: file.directives && file.directives.length > 0 ? 'high' : 'low',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
frameworkFacts: (file.templateRefs ?? []).map((value) => ({
|
|
394
|
+
kind: 'vue-template-ref',
|
|
395
|
+
value,
|
|
396
|
+
confidence: 'medium',
|
|
397
|
+
})),
|
|
398
|
+
routeFacts: [],
|
|
399
|
+
stateFacts: file.hasDefineStore ? [{ kind: 'pinia-store', confidence: 'high' }] : [],
|
|
400
|
+
assetFacts: file.imports
|
|
401
|
+
.filter((imp) => isAssetSpecifier(imp.specifier))
|
|
402
|
+
.map((imp) => ({
|
|
403
|
+
specifier: imp.specifier,
|
|
404
|
+
assetKind: assetKindOf(imp.specifier),
|
|
405
|
+
confidence: 'high',
|
|
406
|
+
})),
|
|
407
|
+
limitations: file.exports.length > 0 ? ['export facts are name-only'] : [],
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function isExternal(spec: string): boolean {
|
|
412
|
+
if (spec.startsWith('.') || spec.startsWith('/')) return false;
|
|
413
|
+
if (spec.startsWith('@/') || spec.startsWith('~/')) return false;
|
|
414
|
+
return /^[a-z@]/u.test(spec) && !spec.includes('://');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const ASSET_EXTS = new Set([
|
|
418
|
+
'.css',
|
|
419
|
+
'.scss',
|
|
420
|
+
'.sass',
|
|
421
|
+
'.less',
|
|
422
|
+
'.module.css',
|
|
423
|
+
'.module.scss',
|
|
424
|
+
'.json',
|
|
425
|
+
'.svg',
|
|
426
|
+
'.png',
|
|
427
|
+
'.jpg',
|
|
428
|
+
'.jpeg',
|
|
429
|
+
'.gif',
|
|
430
|
+
'.webp',
|
|
431
|
+
'.avif',
|
|
432
|
+
'.ico',
|
|
433
|
+
'.woff',
|
|
434
|
+
'.woff2',
|
|
435
|
+
'.ttf',
|
|
436
|
+
'.otf',
|
|
437
|
+
]);
|
|
438
|
+
|
|
439
|
+
function isAssetSpecifier(specifier: string): boolean {
|
|
440
|
+
const clean = specifier.split('?')[0] ?? specifier;
|
|
441
|
+
const lower = clean.toLowerCase();
|
|
442
|
+
for (const ext of ASSET_EXTS) {
|
|
443
|
+
if (lower.endsWith(ext)) return true;
|
|
444
|
+
}
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function assetKindOf(specifier: string): 'style' | 'json' | 'image' | 'font' | 'other' {
|
|
449
|
+
const clean = (specifier.split('?')[0] ?? specifier).toLowerCase();
|
|
450
|
+
if (/\.(css|scss|sass|less)$/u.test(clean)) return 'style';
|
|
451
|
+
if (clean.endsWith('.json')) return 'json';
|
|
452
|
+
if (/\.(svg|png|jpg|jpeg|gif|webp|avif|ico)$/u.test(clean)) return 'image';
|
|
453
|
+
if (/\.(woff2?|ttf|otf)$/u.test(clean)) return 'font';
|
|
454
|
+
return 'other';
|
|
455
|
+
}
|