@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.
Files changed (112) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +62 -0
  3. package/package.json +36 -0
  4. package/src/README.md +4 -0
  5. package/src/analyzer/__tests__/__snapshots__/referenceSnapshot.test.ts.snap +145 -0
  6. package/src/analyzer/__tests__/_paths.ts +8 -0
  7. package/src/analyzer/__tests__/analyze.test.ts +522 -0
  8. package/src/analyzer/__tests__/archDebt.test.ts +111 -0
  9. package/src/analyzer/__tests__/asyncLifecycleRisk.test.ts +122 -0
  10. package/src/analyzer/__tests__/browserFsAccessFileSource.test.ts +97 -0
  11. package/src/analyzer/__tests__/bundle.test.ts +191 -0
  12. package/src/analyzer/__tests__/classify.test.ts +99 -0
  13. package/src/analyzer/__tests__/contracts.test.ts +372 -0
  14. package/src/analyzer/__tests__/crossSourceConsistency.test.ts +317 -0
  15. package/src/analyzer/__tests__/cyclePatterns.test.ts +132 -0
  16. package/src/analyzer/__tests__/cycles.test.ts +74 -0
  17. package/src/analyzer/__tests__/detect.test.ts +62 -0
  18. package/src/analyzer/__tests__/discover.test.ts +68 -0
  19. package/src/analyzer/__tests__/displayId.test.ts +30 -0
  20. package/src/analyzer/__tests__/feedbackArcSet.test.ts +168 -0
  21. package/src/analyzer/__tests__/inMemoryFileSource.test.ts +34 -0
  22. package/src/analyzer/__tests__/incremental.test.ts +154 -0
  23. package/src/analyzer/__tests__/layers.test.ts +87 -0
  24. package/src/analyzer/__tests__/layersOverrides.test.ts +120 -0
  25. package/src/analyzer/__tests__/memoryRisk.test.ts +132 -0
  26. package/src/analyzer/__tests__/metrics.test.ts +59 -0
  27. package/src/analyzer/__tests__/parserRegistry.test.ts +54 -0
  28. package/src/analyzer/__tests__/parsers.test.ts +187 -0
  29. package/src/analyzer/__tests__/reactParser.test.ts +93 -0
  30. package/src/analyzer/__tests__/recommendations.test.ts +171 -0
  31. package/src/analyzer/__tests__/referenceSnapshot.test.ts +63 -0
  32. package/src/analyzer/__tests__/resolve.test.ts +294 -0
  33. package/src/analyzer/__tests__/rsc.test.ts +130 -0
  34. package/src/analyzer/__tests__/signals.test.ts +316 -0
  35. package/src/analyzer/__tests__/suggestContracts.test.ts +108 -0
  36. package/src/analyzer/__tests__/svelteParser.test.ts +108 -0
  37. package/src/analyzer/__tests__/typeOnlyCandidates.test.ts +163 -0
  38. package/src/analyzer/__tests__/vueAutoImport.test.ts +177 -0
  39. package/src/analyzer/archDebt.ts +68 -0
  40. package/src/analyzer/asyncLifecycleRisk.ts +234 -0
  41. package/src/analyzer/buildGraph.ts +683 -0
  42. package/src/analyzer/bundle/analyzeBundle.ts +147 -0
  43. package/src/analyzer/bundle/index.ts +12 -0
  44. package/src/analyzer/bundle/parseStats.ts +152 -0
  45. package/src/analyzer/bundle/types.ts +85 -0
  46. package/src/analyzer/classify.ts +54 -0
  47. package/src/analyzer/contracts.ts +265 -0
  48. package/src/analyzer/cyclePatterns.ts +138 -0
  49. package/src/analyzer/cycles.ts +98 -0
  50. package/src/analyzer/detect.ts +34 -0
  51. package/src/analyzer/discover.ts +131 -0
  52. package/src/analyzer/displayId.ts +21 -0
  53. package/src/analyzer/entryPoints.ts +136 -0
  54. package/src/analyzer/feedbackArcSet.ts +332 -0
  55. package/src/analyzer/fileSource.ts +8 -0
  56. package/src/analyzer/hotZones.ts +17 -0
  57. package/src/analyzer/incremental.ts +455 -0
  58. package/src/analyzer/index.ts +444 -0
  59. package/src/analyzer/layers.ts +183 -0
  60. package/src/analyzer/loadAliases.ts +288 -0
  61. package/src/analyzer/memoryRisk.ts +345 -0
  62. package/src/analyzer/metrics.ts +156 -0
  63. package/src/analyzer/parsers/index.ts +62 -0
  64. package/src/analyzer/parsers/reactParser.ts +24 -0
  65. package/src/analyzer/parsers/svelteParser.ts +46 -0
  66. package/src/analyzer/parsers/tsParser.ts +364 -0
  67. package/src/analyzer/parsers/vueParser.ts +109 -0
  68. package/src/analyzer/recommendations.ts +432 -0
  69. package/src/analyzer/resolve.ts +315 -0
  70. package/src/analyzer/rsc.ts +120 -0
  71. package/src/analyzer/signals.ts +684 -0
  72. package/src/analyzer/sources/browserFsAccessFileSource.ts +132 -0
  73. package/src/analyzer/sources/inMemoryFileSource.ts +24 -0
  74. package/src/analyzer/sources/nodeFsFileSource.ts +93 -0
  75. package/src/analyzer/sources/tauriFileSource.ts +68 -0
  76. package/src/analyzer/suggestContracts.ts +214 -0
  77. package/src/analyzer/typeOnlyCandidates.ts +233 -0
  78. package/src/analyzer/types.ts +537 -0
  79. package/src/cache/__tests__/cache.test.ts +316 -0
  80. package/src/cache/index.ts +432 -0
  81. package/src/codegen/__tests__/applyTypeOnlyFix.integration.test.ts +62 -0
  82. package/src/codegen/__tests__/applyTypeOnlyFix.test.ts +176 -0
  83. package/src/codegen/__tests__/configSnippets.test.ts +230 -0
  84. package/src/codegen/applyTypeOnlyFix.ts +344 -0
  85. package/src/codegen/configSnippets.ts +172 -0
  86. package/src/codegen/initConfig.ts +223 -0
  87. package/src/config/__tests__/frontScopeConfig.test.ts +187 -0
  88. package/src/config/frontScopeConfig.ts +830 -0
  89. package/src/diff/__tests__/diffScans.test.ts +103 -0
  90. package/src/diff/diffScans.ts +61 -0
  91. package/src/diff/index.ts +2 -0
  92. package/src/diff/types.ts +39 -0
  93. package/src/git/__tests__/computeChurn.test.ts +113 -0
  94. package/src/git/__tests__/computeTemporalCoupling.test.ts +125 -0
  95. package/src/git/__tests__/parseGitLog.test.ts +120 -0
  96. package/src/git/computeChurn.ts +111 -0
  97. package/src/git/computeTemporalCoupling.ts +114 -0
  98. package/src/git/index.ts +24 -0
  99. package/src/git/parseGitLog.ts +124 -0
  100. package/src/git/readGitHistory.ts +130 -0
  101. package/src/git/types.ts +119 -0
  102. package/src/index.ts +137 -0
  103. package/src/report/__tests__/buildFixPlan.test.ts +357 -0
  104. package/src/report/__tests__/buildJsonReport.test.ts +34 -0
  105. package/src/report/buildFixPlan.ts +481 -0
  106. package/src/report/buildJsonReport.ts +27 -0
  107. package/src/search/__tests__/parseQuery.test.ts +67 -0
  108. package/src/search/__tests__/search.test.ts +172 -0
  109. package/src/search/index.ts +281 -0
  110. package/src/search/parseQuery.ts +75 -0
  111. package/src/views/__tests__/analyzerViews.test.ts +558 -0
  112. package/src/views/analyzerViews.ts +1294 -0
@@ -0,0 +1,357 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { buildFixPlan } from '../buildFixPlan';
3
+ import type { ScanResult } from '../../analyzer/types';
4
+
5
+ describe('buildFixPlan generated policy', () => {
6
+ it('flags findings whose every target is generated and downweights them below user-code findings', () => {
7
+ const plan = buildFixPlan(scanFixture(), { exportedAt: '2026-05-12T00:00:00.000Z' });
8
+
9
+ expect(plan.summary.generatedModules).toBe(2);
10
+ expect(plan.evidence.generatedModules).toEqual([
11
+ 'src/recruit/openapi/api.ts',
12
+ 'src/recruit/openapi/client.ts',
13
+ ]);
14
+
15
+ // Generated-only cycle must not be first - the mixed (user-touching) cycle
16
+ // and the user-code layer violation outrank it.
17
+ const ids = plan.priorityFindings.map((f) => f.id);
18
+ expect(ids.indexOf('cycle:generated-only')).toBeGreaterThan(ids.indexOf('cycle:mixed'));
19
+ expect(ids.indexOf('cycle:generated-only')).toBeGreaterThan(ids.indexOf('shared->app'));
20
+
21
+ const generatedFinding = plan.priorityFindings.find((f) => f.id === 'cycle:generated-only');
22
+ expect(generatedFinding?.generated).toBe(true);
23
+ // 100 (direct cycle) * 0.1 = 10
24
+ expect(generatedFinding?.weight).toBeCloseTo(10, 5);
25
+
26
+ const mixedFinding = plan.priorityFindings.find((f) => f.id === 'cycle:mixed');
27
+ expect(mixedFinding?.generated).toBeUndefined();
28
+ });
29
+
30
+ it('adds deterministic repair findings beyond type-only imports', () => {
31
+ const plan = buildFixPlan(
32
+ {
33
+ ...scanFixture(),
34
+ modules: [
35
+ mod('src/app/main.ts', false, 'entry'),
36
+ mod('src/components/index.ts', false, 'module'),
37
+ mod('src/components/Button.ts', false, 'component'),
38
+ mod('src/legacy/dead.ts', false, 'module'),
39
+ ],
40
+ cycles: [
41
+ {
42
+ id: 'cycle:barrel',
43
+ modules: ['src/components/index.ts', 'src/components/Button.ts'],
44
+ length: 2,
45
+ severity: 'direct',
46
+ },
47
+ ],
48
+ edges: [
49
+ edge('src/components/index.ts', 'src/components/Button.ts', './Button'),
50
+ edge('src/components/Button.ts', 'src/components/index.ts', '.'),
51
+ ],
52
+ layerViolations: [
53
+ {
54
+ edgeId: 'components->app',
55
+ from: 'src/components/index.ts',
56
+ to: 'src/app/main.ts',
57
+ fromLayer: 'components',
58
+ toLayer: 'app',
59
+ severity: 'error',
60
+ },
61
+ ],
62
+ metrics: {
63
+ 'src/legacy/dead.ts': {
64
+ fanIn: 0,
65
+ fanOut: 0,
66
+ instability: 0,
67
+ depth: 0,
68
+ inCycle: false,
69
+ couplingScore: 0,
70
+ hotnessScore: 0,
71
+ },
72
+ 'src/components/index.ts': {
73
+ fanIn: 12,
74
+ fanOut: 3,
75
+ instability: 0.2,
76
+ depth: 2,
77
+ inCycle: true,
78
+ couplingScore: 6,
79
+ hotnessScore: 9,
80
+ },
81
+ },
82
+ hotZones: ['src/components/index.ts'],
83
+ contractViolations: [
84
+ {
85
+ id: 'contract:public-api',
86
+ kind: 'api-stability',
87
+ ruleName: 'Public API boundary',
88
+ severity: 'error',
89
+ message: 'src/components/Button.ts exposes unstable implementation surface.',
90
+ modules: ['src/components/Button.ts'],
91
+ },
92
+ ],
93
+ },
94
+ { exportedAt: '2026-05-12T00:00:00.000Z' },
95
+ );
96
+
97
+ expect(plan.priorityFindings.map((finding) => finding.type)).toEqual(
98
+ expect.arrayContaining(['barrel-cycle', 'move-module', 'unreachable-from-entries']),
99
+ );
100
+ expect(plan.repairGroups.map((group) => group.id)).toEqual(
101
+ expect.arrayContaining(['safe-first', 'high-impact', 'review-before-change']),
102
+ );
103
+ expect(plan.repairGroups.find((group) => group.id === 'safe-first')?.findings).toContain(
104
+ 'src/legacy/dead.ts:unreachable',
105
+ );
106
+ const barrel = plan.priorityFindings.find((finding) => finding.type === 'barrel-cycle');
107
+ expect(barrel?.action).toBe(
108
+ 'Replace imports through src/components/index.ts in src/components/Button.ts with concrete module paths, then keep src/components/index.ts as export-only.',
109
+ );
110
+ expect(barrel?.verify).toContain('archora check . --fail-on new-cycles:0');
111
+ expect(barrel?.params).toMatchObject({
112
+ cycleId: 'cycle:barrel',
113
+ barrel: 'src/components/index.ts',
114
+ importer: 'src/components/Button.ts',
115
+ importSpecifier: '.',
116
+ });
117
+ const move = plan.priorityFindings.find((finding) => finding.type === 'move-module');
118
+ expect(move?.action).toBe(
119
+ 'Remove the forbidden import src/components/index.ts -> src/app/main.ts. Move the dependency behind a components-facing adapter or invert the dependency through an allowed app entry.',
120
+ );
121
+ expect(move?.verify).toContain('archora check . --fail-on layer-violations:0');
122
+ expect(move?.params).toMatchObject({
123
+ from: 'src/components/index.ts',
124
+ to: 'src/app/main.ts',
125
+ fromLayer: 'components',
126
+ toLayer: 'app',
127
+ });
128
+ const hotspot = plan.priorityFindings.find((finding) => finding.type === 'hot-zone');
129
+ expect(hotspot?.action).toBe(
130
+ 'Freeze the public surface of src/components/index.ts first, then inspect its 12 consumers before editing internals.',
131
+ );
132
+ expect(hotspot?.verify).toContain('archora impact --module src/components/index.ts');
133
+ const contract = plan.priorityFindings.find((finding) => finding.type === 'contract-violation');
134
+ expect(contract?.action).toBe(
135
+ 'Fix Public API boundary in src/components/Button.ts: narrow the exported surface or add an explicit policy exception with owner approval.',
136
+ );
137
+ expect(contract?.verify).toContain('archora check . --fail-on contract-errors:0');
138
+ });
139
+
140
+ it('downweights generated-path findings even when the module is not tagged', () => {
141
+ const plan = buildFixPlan(
142
+ {
143
+ ...scanFixture(),
144
+ modules: [
145
+ mod('src/features/orders/model/order.ts', false, 'model'),
146
+ mod('src/shared/assets/demo/generated/src/api/client.ts', false, 'api'),
147
+ ],
148
+ metrics: {
149
+ 'src/features/orders/model/order.ts': {
150
+ fanIn: 1,
151
+ fanOut: 1,
152
+ instability: 0.5,
153
+ depth: 1,
154
+ inCycle: false,
155
+ couplingScore: 2,
156
+ hotnessScore: 4,
157
+ },
158
+ 'src/shared/assets/demo/generated/src/api/client.ts': {
159
+ fanIn: 20,
160
+ fanOut: 20,
161
+ instability: 0.5,
162
+ depth: 1,
163
+ inCycle: false,
164
+ couplingScore: 40,
165
+ hotnessScore: 10,
166
+ },
167
+ },
168
+ hotZones: [
169
+ 'src/shared/assets/demo/generated/src/api/client.ts',
170
+ 'src/features/orders/model/order.ts',
171
+ ],
172
+ cycles: [],
173
+ layerViolations: [],
174
+ },
175
+ { exportedAt: '2026-05-12T00:00:00.000Z' },
176
+ );
177
+
178
+ const ids = plan.priorityFindings.map((finding) => finding.id);
179
+ expect(plan.evidence.generatedModules).toContain(
180
+ 'src/shared/assets/demo/generated/src/api/client.ts',
181
+ );
182
+ expect(ids.indexOf('src/shared/assets/demo/generated/src/api/client.ts')).toBeGreaterThan(
183
+ ids.indexOf('src/features/orders/model/order.ts'),
184
+ );
185
+ expect(
186
+ plan.priorityFindings.find(
187
+ (finding) => finding.id === 'src/shared/assets/demo/generated/src/api/client.ts',
188
+ )?.generated,
189
+ ).toBe(true);
190
+ });
191
+
192
+ it('treats unreachable scripts as entry configuration candidates', () => {
193
+ const plan = buildFixPlan(
194
+ {
195
+ ...scanFixture(),
196
+ modules: [
197
+ mod('scripts/refresh-reports.mjs', false, 'module'),
198
+ mod('packages/ui/scripts/clean-dist.mjs', false, 'module'),
199
+ ],
200
+ metrics: {
201
+ 'scripts/refresh-reports.mjs': {
202
+ fanIn: 0,
203
+ fanOut: 0,
204
+ instability: 0,
205
+ depth: 0,
206
+ inCycle: false,
207
+ couplingScore: 0,
208
+ hotnessScore: 0,
209
+ },
210
+ 'packages/ui/scripts/clean-dist.mjs': {
211
+ fanIn: 0,
212
+ fanOut: 0,
213
+ instability: 0,
214
+ depth: 0,
215
+ inCycle: false,
216
+ couplingScore: 0,
217
+ hotnessScore: 0,
218
+ },
219
+ },
220
+ cycles: [],
221
+ layerViolations: [],
222
+ },
223
+ { exportedAt: '2026-05-12T00:00:00.000Z' },
224
+ );
225
+
226
+ const script = plan.priorityFindings.find(
227
+ (finding) => finding.id === 'scripts/refresh-reports.mjs:unreachable',
228
+ );
229
+ expect(script?.action).toBe(
230
+ 'Treat scripts/refresh-reports.mjs 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.',
231
+ );
232
+ expect(script?.verify).toContain('archora report . --format fix-plan');
233
+ expect(script?.params).toMatchObject({ entryCandidate: 'script' });
234
+ expect(
235
+ plan.priorityFindings.find(
236
+ (finding) => finding.id === 'packages/ui/scripts/clean-dist.mjs:unreachable',
237
+ )?.params,
238
+ ).toMatchObject({ entryCandidate: 'script' });
239
+ });
240
+
241
+ it('adds churn context to hotspot repair actions when git history is present', () => {
242
+ const plan = buildFixPlan(
243
+ {
244
+ ...scanFixture(),
245
+ modules: [mod('src/features/orders/model/order.ts', false, 'model')],
246
+ metrics: {
247
+ 'src/features/orders/model/order.ts': {
248
+ fanIn: 10,
249
+ fanOut: 2,
250
+ instability: 0.2,
251
+ depth: 1,
252
+ inCycle: false,
253
+ couplingScore: 12,
254
+ hotnessScore: 9,
255
+ },
256
+ },
257
+ churn: {
258
+ 'src/features/orders/model/order.ts': {
259
+ moduleId: 'src/features/orders/model/order.ts',
260
+ commits: 14,
261
+ linesChanged: 320,
262
+ authorCount: 3,
263
+ lastTouchedAt: '2026-05-20T10:00:00Z',
264
+ authors: [{ author: 'dev@example.com', commits: 8 }],
265
+ },
266
+ },
267
+ hotZones: ['src/features/orders/model/order.ts'],
268
+ cycles: [],
269
+ layerViolations: [],
270
+ },
271
+ { exportedAt: '2026-05-12T00:00:00.000Z' },
272
+ );
273
+
274
+ const hotspot = plan.priorityFindings.find(
275
+ (finding) => finding.id === 'src/features/orders/model/order.ts',
276
+ );
277
+ expect(hotspot?.action).toContain('Coordinate the change with recent owners first');
278
+ expect(hotspot?.reason).toContain('14 commits');
279
+ expect(hotspot?.params).toMatchObject({ commits: 14, authorCount: 3 });
280
+ });
281
+ });
282
+
283
+ function scanFixture(): ScanResult {
284
+ return {
285
+ project: { id: 'demo', name: 'demo', rootPath: '/repo', detectedFramework: 'vue' },
286
+ modules: [
287
+ mod('src/app/main.ts', false, 'entry'),
288
+ mod('src/shared/api.ts', false, 'api'),
289
+ mod('src/recruit/openapi/api.ts', true, 'api'),
290
+ mod('src/recruit/openapi/client.ts', true, 'api'),
291
+ ],
292
+ edges: [],
293
+ cycles: [
294
+ {
295
+ id: 'cycle:generated-only',
296
+ modules: ['src/recruit/openapi/api.ts', 'src/recruit/openapi/client.ts'],
297
+ length: 2,
298
+ severity: 'direct',
299
+ },
300
+ {
301
+ id: 'cycle:mixed',
302
+ modules: ['src/shared/api.ts', 'src/recruit/openapi/api.ts'],
303
+ length: 2,
304
+ severity: 'direct',
305
+ },
306
+ ],
307
+ metrics: {},
308
+ hotZones: [],
309
+ layerViolations: [
310
+ {
311
+ edgeId: 'shared->app',
312
+ from: 'src/shared/api.ts',
313
+ to: 'src/app/main.ts',
314
+ fromLayer: 'shared',
315
+ toLayer: 'app',
316
+ severity: 'error',
317
+ },
318
+ ],
319
+ archDebt: {
320
+ score: 50,
321
+ grade: 'C',
322
+ breakdown: { cycles: 20, layerViolations: 20, hotZones: 0, coupling: 10 },
323
+ },
324
+ recommendations: [],
325
+ contractViolations: [],
326
+ scannedAt: '2026-05-12T00:00:00.000Z',
327
+ durationMs: 1,
328
+ warnings: [],
329
+ };
330
+ }
331
+
332
+ function mod(
333
+ id: string,
334
+ isGenerated = false,
335
+ kind: ScanResult['modules'][number]['kind'] = 'module',
336
+ ): ScanResult['modules'][number] {
337
+ return {
338
+ id,
339
+ absPath: `/repo/${id}`,
340
+ kind,
341
+ language: 'ts',
342
+ loc: 10,
343
+ exports: [],
344
+ isInfra: false,
345
+ ...(isGenerated ? { isGenerated: true } : {}),
346
+ };
347
+ }
348
+
349
+ function edge(from: string, to: string, specifier: string): ScanResult['edges'][number] {
350
+ return {
351
+ from,
352
+ to,
353
+ specifier,
354
+ kind: 'static',
355
+ resolved: true,
356
+ };
357
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { analyze } from '../../analyzer';
3
+ import { createNodeFsFileSource } from '../../analyzer/sources/nodeFsFileSource';
4
+ import { fixturePath } from '../../analyzer/__tests__/_paths';
5
+ import { buildJsonReport, type ReportEnvelope } from '../buildJsonReport';
6
+
7
+ describe('buildJsonReport', () => {
8
+ it('wraps the scan result in a versioned envelope', async () => {
9
+ const source = await createNodeFsFileSource({ rootPath: fixturePath('sample-cycles') });
10
+ const scan = await analyze(source);
11
+
12
+ const json = buildJsonReport(scan, {
13
+ appVersion: 'archora@test',
14
+ exportedAt: '2026-01-01T00:00:00.000Z',
15
+ });
16
+
17
+ const parsed = JSON.parse(json) as ReportEnvelope;
18
+ expect(parsed.schema).toBe(1);
19
+ expect(parsed.app).toBe('archora@test');
20
+ expect(parsed.exportedAt).toBe('2026-01-01T00:00:00.000Z');
21
+ expect(parsed.scan.project.name).toBe(scan.project.name);
22
+ expect(parsed.scan.modules.length).toBe(scan.modules.length);
23
+ expect(parsed.scan.cycles).toHaveLength(2);
24
+ });
25
+
26
+ it('produces minified output when pretty is false', async () => {
27
+ const source = await createNodeFsFileSource({ rootPath: fixturePath('sample-cycles') });
28
+ const scan = await analyze(source);
29
+ const compact = buildJsonReport(scan, { pretty: false });
30
+ const pretty = buildJsonReport(scan);
31
+ expect(compact.length).toBeLessThan(pretty.length);
32
+ expect(compact.includes('\n')).toBe(false);
33
+ });
34
+ });