@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.
Files changed (52) hide show
  1. package/README.md +5 -3
  2. package/package.json +1 -1
  3. package/src/README.md +2 -2
  4. package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +1 -1
  5. package/src/analyzer/__tests__/analyze.test.ts +41 -0
  6. package/src/analyzer/__tests__/bundle.test.ts +99 -0
  7. package/src/analyzer/__tests__/hotZones.test.ts +128 -0
  8. package/src/analyzer/__tests__/incremental.test.ts +61 -13
  9. package/src/analyzer/__tests__/layerViolationAccuracy.test.ts +77 -0
  10. package/src/analyzer/__tests__/memoryRisk.test.ts +94 -0
  11. package/src/analyzer/__tests__/metrics.test.ts +39 -0
  12. package/src/analyzer/__tests__/nuxtComposableAutoImport.test.ts +109 -0
  13. package/src/analyzer/__tests__/reactParser.test.ts +22 -0
  14. package/src/analyzer/__tests__/recommendations.test.ts +67 -0
  15. package/src/analyzer/__tests__/resolve.test.ts +54 -0
  16. package/src/analyzer/__tests__/rsc.test.ts +133 -3
  17. package/src/analyzer/archDebt.ts +32 -9
  18. package/src/analyzer/buildGraph.ts +75 -3
  19. package/src/analyzer/bundle/analyzeBundle.ts +84 -1
  20. package/src/analyzer/bundle/types.ts +9 -1
  21. package/src/analyzer/hotZones.ts +94 -2
  22. package/src/analyzer/incremental.ts +28 -10
  23. package/src/analyzer/index.ts +3 -1
  24. package/src/analyzer/loadAliases.ts +4 -4
  25. package/src/analyzer/memoryRisk.ts +33 -2
  26. package/src/analyzer/metrics.ts +10 -1
  27. package/src/analyzer/parsers/svelteParser.ts +5 -0
  28. package/src/analyzer/parsers/tsParser.ts +11 -1
  29. package/src/analyzer/recommendations.ts +28 -14
  30. package/src/analyzer/resolve.ts +51 -18
  31. package/src/analyzer/rsc.ts +90 -9
  32. package/src/analyzer/sources/browserFsAccessFileSource.ts +1 -1
  33. package/src/analyzer/sources/nodeFsFileSource.ts +1 -1
  34. package/src/analyzer/sources/tauriFileSource.ts +2 -2
  35. package/src/analyzer/types.ts +22 -0
  36. package/src/cache/index.ts +18 -3
  37. package/src/diff/__tests__/diffScans.test.ts +64 -1
  38. package/src/diff/diffScans.ts +31 -1
  39. package/src/diff/types.ts +19 -1
  40. package/src/git/__tests__/computeTemporalCoupling.test.ts +24 -0
  41. package/src/git/computeTemporalCoupling.ts +35 -4
  42. package/src/git/types.ts +14 -1
  43. package/src/index.ts +5 -0
  44. package/src/report/__tests__/buildDeadCodeReport.test.ts +108 -0
  45. package/src/report/buildDeadCodeReport.ts +110 -0
  46. package/src/report/buildFixPlan.ts +14 -69
  47. package/src/search/__tests__/parseQuery.test.ts +13 -13
  48. package/src/search/__tests__/search.test.ts +19 -19
  49. package/src/search/index.ts +39 -39
  50. package/src/search/parseQuery.ts +13 -13
  51. package/src/views/__tests__/analyzerViews.test.ts +6 -0
  52. package/src/views/analyzerViews.ts +1 -6
@@ -1,18 +1,18 @@
1
- // Глобальный поиск по `ScanResult` — без чтения файловой системы, только
2
- // по структурированным метаданным (modules + edges). Стратегия:
1
+ // Global search over `ScanResult` — no filesystem reads, only structured
2
+ // metadata (modules + edges). Strategy:
3
3
  //
4
- // 1. Парсим query (`parseQuery`).
5
- // 2. Для каждого префикса набираем set'ы ID-кандидатов; в финальный набор
6
- // попадают только модули, прошедшие пересечение всех префиксов (AND).
7
- // 3. Поверх отфильтрованных модулей применяем free-text ранкинг
8
- // (substring-match по path и exports) и сортируем по итоговому скору.
9
- // 4. Возвращаем разноимённые результаты с типом матча — UI рендерит pill-чипы
10
- // «Path / Export / Import / Kind», поэтому каждому SearchResult нужно знать,
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
- // Не делаем: ripgrep по содержимому файлов, fuzzy-substring (типа
14
- // fzf-style scoring) — для path → modules.length обычно сотни, обычный
15
- // substring достаточен. Усложним, если появится сигнал «слишком шумно».
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
- /** Какие критерии сработали — UI использует для подсветки чипов. */
27
+ /** Which criteria matchedthe UI uses this to highlight chips. */
28
28
  matched: MatchKind[];
29
- /** Score 0..1; 1 = идеальное соответствие, 0 = только prefix-фильтр прошёл. */
29
+ /** Score 0..1; 1 = perfect match, 0 = only the prefix filter passed. */
30
30
  score: number;
31
31
  /**
32
- * Подсветочный hint для каждого совпавшего критерияконкретные значения
33
- * (имя экспорта, specifier импорта, kind). Используется панелью результатов
34
- * для пояснения «почему этот модуль вылез на этот запрос».
32
+ * Highlight hint for each matched criterionthe 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
- /** Ограничить размер выдачи. По умолчанию 50 — типовой UI-overlay. */
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. Префиксные фильтры выдают `Map<id, MatchKind[]> + highlights` —
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 ранкинг. Если free пуст и префиксы естьотдаём eligible
65
- // как есть со скором 0.5 (нейтральный). Если free естьсчитаем скор и
66
- // отбрасываем модули, у которых ни один free-токен не сматчился, КОГДА
67
- // префиксов нет; иначе free снижает скор, но не отсекает.
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
- // Нет ни префикс-матча, ни free-матча: модуль вообще не попал.
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. Сортировка: больший score → раньше; tiebreak по короче-путьраньше
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 — оригинальная строка импорта; non-resolved тоже учитываем
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
- * Простой substring-ранкер. Не fuzzy: для типичной задачи «найди useAuth»
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
- * - Точное совпадение exports[i] === term: +0.5
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 (но в score раз чтобы три совпадения не
224
- * делали скор > 1)
225
- * Затем нормализуем в 0..1 по числу term'ов.
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
- // Нормализуем: максимум 0.5 за term — top score 1.0 при N term'ов.
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,
@@ -1,23 +1,23 @@
1
- // Парсер query-строки для глобального поиска. Поддерживает префиксы
2
- // `path:`, `export:`, `import:`, `kind:`, разделяемые пробелами; всё, что
3
- // без префикса free-text токены, которые ранкер использует одновременно
4
- // по path и exports.
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
- // Несколько префиксов в одной строке = AND-пересечение
7
- // (`kind:component path:auth` → component-модули, у которых path содержит auth).
8
- // Несколько одинаковых префиксов = OR внутри ключа
9
- // (`kind:component kind:composable` → component ИЛИ composable).
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-строка, а не shell-парсер.
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 objectno 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
  }