@archora/core 1.1.0 → 2.0.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 +5 -3
- 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__/analyze.test.ts +41 -0
- package/src/analyzer/__tests__/bundle.test.ts +99 -0
- package/src/analyzer/__tests__/hotZones.test.ts +128 -0
- package/src/analyzer/__tests__/incremental.test.ts +61 -13
- package/src/analyzer/__tests__/layerViolationAccuracy.test.ts +77 -0
- package/src/analyzer/__tests__/memoryRisk.test.ts +94 -0
- package/src/analyzer/__tests__/metrics.test.ts +39 -0
- package/src/analyzer/__tests__/nuxtComposableAutoImport.test.ts +109 -0
- package/src/analyzer/__tests__/reactParser.test.ts +22 -0
- package/src/analyzer/__tests__/recommendations.test.ts +67 -0
- package/src/analyzer/__tests__/resolve.test.ts +54 -0
- package/src/analyzer/__tests__/rsc.test.ts +133 -3
- package/src/analyzer/archDebt.ts +32 -9
- package/src/analyzer/buildGraph.ts +75 -3
- package/src/analyzer/bundle/analyzeBundle.ts +84 -1
- package/src/analyzer/bundle/types.ts +9 -1
- package/src/analyzer/hotZones.ts +94 -2
- package/src/analyzer/incremental.ts +28 -10
- package/src/analyzer/index.ts +3 -1
- package/src/analyzer/loadAliases.ts +4 -4
- package/src/analyzer/memoryRisk.ts +33 -2
- package/src/analyzer/metrics.ts +10 -1
- package/src/analyzer/parsers/svelteParser.ts +5 -0
- package/src/analyzer/parsers/tsParser.ts +11 -1
- package/src/analyzer/recommendations.ts +28 -14
- package/src/analyzer/resolve.ts +51 -18
- package/src/analyzer/rsc.ts +90 -9
- package/src/analyzer/sources/browserFsAccessFileSource.ts +1 -1
- package/src/analyzer/sources/nodeFsFileSource.ts +1 -1
- package/src/analyzer/sources/tauriFileSource.ts +2 -2
- package/src/analyzer/types.ts +22 -0
- package/src/cache/index.ts +18 -3
- 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/__tests__/computeTemporalCoupling.test.ts +24 -0
- package/src/git/computeTemporalCoupling.ts +35 -4
- package/src/git/types.ts +14 -1
- package/src/index.ts +5 -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/search/__tests__/parseQuery.test.ts +13 -13
- package/src/search/__tests__/search.test.ts +19 -19
- package/src/search/index.ts +39 -39
- package/src/search/parseQuery.ts +13 -13
- package/src/views/__tests__/analyzerViews.test.ts +6 -0
- package/src/views/analyzerViews.ts +1 -6
package/src/search/index.ts
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Global search over `ScanResult` — no filesystem reads, only structured
|
|
2
|
+
// metadata (modules + edges). Strategy:
|
|
3
3
|
//
|
|
4
|
-
// 1.
|
|
5
|
-
// 2.
|
|
6
|
-
//
|
|
7
|
-
// 3.
|
|
8
|
-
//
|
|
9
|
-
// 4.
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// 1. Parse the query (`parseQuery`).
|
|
5
|
+
// 2. For each prefix collect sets of candidate IDs; only modules that pass
|
|
6
|
+
// the intersection of all prefixes (AND) make it into the final set.
|
|
7
|
+
// 3. On top of the filtered modules apply free-text ranking (substring match
|
|
8
|
+
// over path and exports) and sort by the resulting score.
|
|
9
|
+
// 4. Return results tagged with the match kind — the UI renders pill chips
|
|
10
|
+
// "Path / Export / Import / Kind", so each SearchResult must know why it
|
|
11
|
+
// ended up in the output.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
13
|
+
// We do not: ripgrep over file contents, fuzzy substring (fzf-style scoring) —
|
|
14
|
+
// for path → modules.length is usually in the hundreds, plain substring is
|
|
15
|
+
// enough. We'll add complexity if a "too noisy" signal shows up.
|
|
16
16
|
|
|
17
17
|
import type { ScanResult, ModuleId, ModuleKind } from '../analyzer/types';
|
|
18
18
|
import { parseQuery, isEmpty, type ParsedQuery, type SearchPrefix } from './parseQuery';
|
|
@@ -24,20 +24,20 @@ export type MatchKind = 'path' | 'export' | 'import' | 'kind';
|
|
|
24
24
|
export interface SearchResult {
|
|
25
25
|
/** Module id (relative path). */
|
|
26
26
|
id: ModuleId;
|
|
27
|
-
/**
|
|
27
|
+
/** Which criteria matched — the UI uses this to highlight chips. */
|
|
28
28
|
matched: MatchKind[];
|
|
29
|
-
/** Score 0..1; 1 =
|
|
29
|
+
/** Score 0..1; 1 = perfect match, 0 = only the prefix filter passed. */
|
|
30
30
|
score: number;
|
|
31
31
|
/**
|
|
32
|
-
*
|
|
33
|
-
* (
|
|
34
|
-
*
|
|
32
|
+
* Highlight hint for each matched criterion — the concrete values
|
|
33
|
+
* (export name, import specifier, kind). Used by the results panel to
|
|
34
|
+
* explain "why this module surfaced for this query".
|
|
35
35
|
*/
|
|
36
36
|
highlights: Partial<Record<MatchKind, string[]>>;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
export interface SearchOptions {
|
|
40
|
-
/**
|
|
40
|
+
/** Cap the output size. Defaults to 50 — a typical UI overlay. */
|
|
41
41
|
limit?: number;
|
|
42
42
|
}
|
|
43
43
|
|
|
@@ -54,17 +54,17 @@ export function search(
|
|
|
54
54
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
55
55
|
const moduleIds = new Set(scan.modules.map((m) => m.id));
|
|
56
56
|
|
|
57
|
-
// 1.
|
|
58
|
-
//
|
|
57
|
+
// 1. Prefix filters yield `Map<id, MatchKind[]> + highlights` — if a prefix
|
|
58
|
+
// is present, a module can only reach the output by passing it.
|
|
59
59
|
const prefixHits = collectPrefixHits(scan, parsed);
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// If there are prefixes — keep only modules that hit all of the given ones.
|
|
62
62
|
const eligibleIds = applyPrefixIntersection(moduleIds, prefixHits, parsed.prefixes);
|
|
63
63
|
|
|
64
|
-
// 2. Free-text
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
64
|
+
// 2. Free-text ranking. If free is empty and prefixes exist — return eligible
|
|
65
|
+
// as is with score 0.5 (neutral). If free is present — compute the score and
|
|
66
|
+
// drop modules where no free token matched WHEN there are no prefixes;
|
|
67
|
+
// otherwise free lowers the score but does not cut a module out.
|
|
68
68
|
const results: SearchResult[] = [];
|
|
69
69
|
for (const id of eligibleIds) {
|
|
70
70
|
const module = scan.modules.find((m) => m.id === id);
|
|
@@ -72,7 +72,7 @@ export function search(
|
|
|
72
72
|
const matched: MatchKind[] = [];
|
|
73
73
|
const highlights: Partial<Record<MatchKind, string[]>> = {};
|
|
74
74
|
|
|
75
|
-
//
|
|
75
|
+
// collect confirmations from prefix hits
|
|
76
76
|
for (const kind of ['path', 'export', 'import', 'kind'] as const) {
|
|
77
77
|
const hit = prefixHits[kind].get(id);
|
|
78
78
|
if (hit) {
|
|
@@ -95,7 +95,7 @@ export function search(
|
|
|
95
95
|
(highlights.export ??= []).push(...f.matchedExports);
|
|
96
96
|
}
|
|
97
97
|
} else if (matched.length === 0) {
|
|
98
|
-
//
|
|
98
|
+
// Neither a prefix match nor a free match: the module is out entirely.
|
|
99
99
|
continue;
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -104,8 +104,8 @@ export function search(
|
|
|
104
104
|
results.push({ id, matched, score, highlights });
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
// 3.
|
|
108
|
-
// (
|
|
107
|
+
// 3. Sort: higher score → first; tiebreak by shorter path → first
|
|
108
|
+
// (short paths usually sit closer to a feature root than deep implementations).
|
|
109
109
|
results.sort((a, b) => {
|
|
110
110
|
if (b.score !== a.score) return b.score - a.score;
|
|
111
111
|
return a.id.length - b.id.length;
|
|
@@ -157,7 +157,7 @@ function collectPrefixHits(scan: ScanResult, q: ParsedQuery): PrefixHits {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
if (q.prefixes.import) {
|
|
160
|
-
// edges.specifier —
|
|
160
|
+
// edges.specifier — the original import string; non-resolved are counted too
|
|
161
161
|
const needles = q.prefixes.import;
|
|
162
162
|
const matchedSpecifiersByModule = new Map<ModuleId, Set<string>>();
|
|
163
163
|
for (const e of scan.edges) {
|
|
@@ -213,16 +213,16 @@ interface FreeTextMatch {
|
|
|
213
213
|
}
|
|
214
214
|
|
|
215
215
|
/**
|
|
216
|
-
*
|
|
217
|
-
*
|
|
216
|
+
* Plain substring ranker. Not fuzzy: for the typical "find useAuth" task a
|
|
217
|
+
* substring gives stable results without noisy matches.
|
|
218
218
|
*
|
|
219
|
-
*
|
|
220
|
-
* -
|
|
219
|
+
* Scoring:
|
|
220
|
+
* - Exact match exports[i] === term: +0.5
|
|
221
221
|
* - exports[i] contains term: +0.25
|
|
222
222
|
* - basename(path) === term: +0.4
|
|
223
|
-
* - path contains term: +0.2 (
|
|
224
|
-
*
|
|
225
|
-
*
|
|
223
|
+
* - path contains term: +0.2 (but counted once in the score — so that three
|
|
224
|
+
* matches don't push the score > 1)
|
|
225
|
+
* Then normalize to 0..1 by the number of terms.
|
|
226
226
|
*/
|
|
227
227
|
function scoreFreeText(id: ModuleId, exports: readonly string[], terms: string[]): FreeTextMatch {
|
|
228
228
|
let score = 0;
|
|
@@ -260,7 +260,7 @@ function scoreFreeText(id: ModuleId, exports: readonly string[], terms: string[]
|
|
|
260
260
|
score += bestForTerm;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
//
|
|
263
|
+
// Normalize: max 0.5 per term — top score 1.0 with N terms.
|
|
264
264
|
const normalized = terms.length > 0 ? Math.min(1, score / (terms.length * 0.5)) : 0;
|
|
265
265
|
return {
|
|
266
266
|
score: normalized,
|
package/src/search/parseQuery.ts
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
//
|
|
2
|
-
// `
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Parser for the global-search query string. Supports the prefixes `path:`,
|
|
2
|
+
// `export:`, `import:`, `kind:`, separated by spaces; anything without a
|
|
3
|
+
// prefix is a free-text token that the ranker uses against both path and
|
|
4
|
+
// exports at once.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
// (`kind:component path:auth` → component
|
|
8
|
-
//
|
|
9
|
-
// (`kind:component kind:composable` → component
|
|
6
|
+
// Several prefixes in one string = AND intersection
|
|
7
|
+
// (`kind:component path:auth` → component modules whose path contains auth).
|
|
8
|
+
// Several identical prefixes = OR within a key
|
|
9
|
+
// (`kind:component kind:composable` → component OR composable).
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
// `path:"src/feature x"`.
|
|
13
|
-
// query
|
|
11
|
+
// Prefix values may use quotes so spaces don't split the token:
|
|
12
|
+
// `path:"src/feature x"`. Quotes inside a value are not supported — this is a
|
|
13
|
+
// query string, not a shell parser.
|
|
14
14
|
|
|
15
15
|
export type SearchPrefix = 'path' | 'export' | 'import' | 'kind';
|
|
16
16
|
|
|
17
17
|
export interface ParsedQuery {
|
|
18
|
-
/**
|
|
18
|
+
/** Free tokens without a prefix. Empty array if all are prefixed. */
|
|
19
19
|
free: string[];
|
|
20
|
-
/**
|
|
20
|
+
/** Values grouped by key. Empty object → no prefixes. */
|
|
21
21
|
prefixes: Partial<Record<SearchPrefix, string[]>>;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -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
|
}
|