@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,558 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildExplainView,
4
+ buildImpactView,
5
+ buildLifecycleHygieneView,
6
+ buildMatrixView,
7
+ buildOwnershipView,
8
+ buildReviewRiskView,
9
+ buildSemanticSurfaceView,
10
+ buildSignalBaselineView,
11
+ buildTrendView,
12
+ resolveImpactTarget,
13
+ } from '../analyzerViews';
14
+ import type { ArchitectureSignal, ScanResult } from '../../analyzer/types';
15
+
16
+ describe('analyzer view helpers', () => {
17
+ it('builds filtered matrix cells and ranks risky relations first', () => {
18
+ const matrix = buildMatrixView(scanFixture(), {
19
+ groupBy: 'layer',
20
+ onlyViolations: true,
21
+ top: 1,
22
+ });
23
+
24
+ expect(matrix.cells).toEqual([
25
+ {
26
+ from: 'features',
27
+ to: 'pages',
28
+ imports: 1,
29
+ violations: 1,
30
+ cycleEdges: 1,
31
+ edges: [
32
+ {
33
+ from: 'src/features/auth/model/session.ts',
34
+ to: 'src/pages/login/Page.ts',
35
+ kind: 'static',
36
+ specifier: '@/pages/login/Page',
37
+ violation: true,
38
+ cycleEdge: true,
39
+ },
40
+ ],
41
+ },
42
+ ]);
43
+ expect(matrix.summary).toMatchObject({
44
+ modules: 2,
45
+ imports: 2,
46
+ groups: 2,
47
+ cells: 1,
48
+ violations: 1,
49
+ cycleEdges: 1,
50
+ });
51
+ });
52
+
53
+ it('groups non-FSD projects by readable areas by default', () => {
54
+ const matrix = buildMatrixView(
55
+ scanFixture({
56
+ modules: [
57
+ moduleNode('src/App.tsx', 'component'),
58
+ moduleNode('src/mfes/products/ProductCard.tsx', 'component'),
59
+ moduleNode('src/mfes/users/UserCard.tsx', 'component'),
60
+ moduleNode('src/utils/date.ts', 'util'),
61
+ ],
62
+ edges: [
63
+ edge('src/App.tsx', 'src/mfes/products/ProductCard.tsx'),
64
+ edge('src/mfes/products/ProductCard.tsx', 'src/mfes/products/productTypes.ts'),
65
+ edge('src/mfes/products/ProductCard.tsx', 'src/utils/date.ts'),
66
+ edge('src/mfes/users/UserCard.tsx', 'src/utils/date.ts'),
67
+ ],
68
+ cycles: [],
69
+ metrics: {},
70
+ layerViolations: [],
71
+ }),
72
+ );
73
+
74
+ expect(matrix.grouping).toBe('area');
75
+ expect(matrix.groups).toEqual(['app', 'mfes/products', 'mfes/users', 'utils']);
76
+ expect(matrix.groups).not.toContain('unknown');
77
+ expect(matrix.cells).not.toContainEqual(
78
+ expect.objectContaining({ from: 'mfes/products', to: 'mfes/products' }),
79
+ );
80
+ expect(matrix.cells).toContainEqual(
81
+ expect.objectContaining({
82
+ from: 'app',
83
+ to: 'mfes/products',
84
+ imports: 1,
85
+ violations: 0,
86
+ cycleEdges: 0,
87
+ }),
88
+ );
89
+ });
90
+
91
+ it('uses project label for non-FSD layer grouping', () => {
92
+ const matrix = buildMatrixView(
93
+ scanFixture({
94
+ modules: [
95
+ moduleNode('src/client/index.ts', 'module'),
96
+ moduleNode('src/services/api.ts', 'service'),
97
+ ],
98
+ edges: [edge('src/client/index.ts', 'src/services/api.ts')],
99
+ cycles: [],
100
+ metrics: {},
101
+ layerViolations: [],
102
+ }),
103
+ 'layer',
104
+ );
105
+
106
+ expect(matrix.groups).toEqual(['project']);
107
+ expect(matrix.groups).not.toContain('unknown');
108
+ });
109
+
110
+ it('builds review, ownership and semantic views', () => {
111
+ const scan = scanFixture({
112
+ modules: [
113
+ moduleNode('src/client/index.ts', 'module'),
114
+ moduleNode('src/services/api.ts', 'service'),
115
+ moduleNode('src/types/public.ts', 'schema', ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']),
116
+ ],
117
+ edges: [
118
+ edge('src/client/index.ts', 'src/services/api.ts'),
119
+ edge('src/services/api.ts', 'src/types/public.ts'),
120
+ ],
121
+ cycles: [],
122
+ metrics: {
123
+ 'src/client/index.ts': metric({ fanIn: 0, fanOut: 1 }),
124
+ 'src/services/api.ts': metric({ fanIn: 1, fanOut: 1, hotnessScore: 12 }),
125
+ 'src/types/public.ts': metric({ fanIn: 1, fanOut: 0 }),
126
+ },
127
+ hotZones: ['src/services/api.ts'],
128
+ layerViolations: [],
129
+ });
130
+
131
+ expect(buildReviewRiskView(scan)).toMatchObject({
132
+ level: 'medium',
133
+ checkFirst: ['src/services/api.ts'],
134
+ guidedActions: [
135
+ {
136
+ kind: 'hotspot',
137
+ title: 'Review hotspot impact',
138
+ target: 'src/services/api.ts',
139
+ action: 'Open impact before changing this module.',
140
+ verify: 'Run impact for this module and check the top importers before editing.',
141
+ },
142
+ ],
143
+ });
144
+ expect(buildOwnershipView(scan).areas[0]).toMatchObject({
145
+ area: 'services',
146
+ primaryKind: 'service',
147
+ });
148
+ expect(buildSemanticSurfaceView(scan).broadPublicModules[0]).toMatchObject({
149
+ id: 'src/types/public.ts',
150
+ role: 'schema',
151
+ });
152
+ });
153
+
154
+ it('adds baseline regressions to the review view', () => {
155
+ const baseline = scanFixture({ cycles: [], signals: [] });
156
+ const current = scanFixture({
157
+ signals: [signal({ stableKey: 'contract:new', severity: 'high' })],
158
+ });
159
+ const view = buildReviewRiskView(current, {
160
+ baseline,
161
+ diff: {
162
+ projectId: 'p',
163
+ projectName: 'project',
164
+ prevScannedAt: baseline.scannedAt,
165
+ nextScannedAt: current.scannedAt,
166
+ addedModules: [],
167
+ removedModules: [],
168
+ changedModules: [],
169
+ newCycles: current.cycles,
170
+ resolvedCycles: [],
171
+ summary: {
172
+ addedModules: 0,
173
+ removedModules: 0,
174
+ changedModules: 0,
175
+ newCycles: current.cycles.length,
176
+ resolvedCycles: 0,
177
+ },
178
+ },
179
+ });
180
+
181
+ expect(view.baseline).toMatchObject({ newCycles: 1, newSignals: 1 });
182
+ expect(view.regressions).toEqual(['1 new cycle(s)', '1 new signal(s)']);
183
+ });
184
+
185
+ it('raises review priority for unowned lifecycle side effects', () => {
186
+ const scan = scanFixture({
187
+ archDebt: {
188
+ score: 10,
189
+ grade: 'A',
190
+ breakdown: { cycles: 0, layerViolations: 0, hotZones: 0, coupling: 0 },
191
+ },
192
+ modules: [moduleNode('src/components/Dashboard.tsx', 'module')],
193
+ metrics: {
194
+ 'src/components/Dashboard.tsx': metric({ fanIn: 1, fanOut: 0 }),
195
+ },
196
+ cycles: [],
197
+ hotZones: [],
198
+ layerViolations: [],
199
+ memoryRisks: [
200
+ {
201
+ id: 'memory:event-listener-cleanup:src/components/Dashboard.tsx:8',
202
+ kind: 'event-listener-cleanup',
203
+ moduleId: 'src/components/Dashboard.tsx',
204
+ severity: 'medium',
205
+ confidence: 'high',
206
+ evidence: [
207
+ {
208
+ message: 'addEventListener has no visible removeEventListener cleanup',
209
+ line: 8,
210
+ acquire: 'addEventListener',
211
+ expectedCleanup: 'removeEventListener',
212
+ },
213
+ ],
214
+ remediation: 'Remove the listener from the matching component teardown lifecycle.',
215
+ },
216
+ ],
217
+ });
218
+
219
+ expect(buildReviewRiskView(scan)).toMatchObject({
220
+ level: 'medium',
221
+ reasons: ['1 lifecycle owner review(s)'],
222
+ checkFirst: ['src/components/Dashboard.tsx'],
223
+ guidedActions: [
224
+ {
225
+ kind: 'lifecycle',
226
+ title: 'Assign lifecycle owner',
227
+ target: 'src/components/Dashboard.tsx',
228
+ verify: 'Run hygiene and confirm the module is no longer listed as a review boundary.',
229
+ },
230
+ ],
231
+ });
232
+ });
233
+
234
+ it('builds lifecycle hygiene and trend views', () => {
235
+ const baseline = scanFixture({
236
+ cycles: [],
237
+ signals: [],
238
+ archDebt: {
239
+ score: 12,
240
+ grade: 'A',
241
+ breakdown: { cycles: 0, layerViolations: 0, hotZones: 0, coupling: 0 },
242
+ },
243
+ });
244
+ const current = scanFixture({
245
+ modules: [
246
+ moduleNode('src/app/main.ts', 'entry'),
247
+ moduleNode('src/client/bootstrap.ts', 'module'),
248
+ moduleNode('src/legacy/unused.ts', 'module'),
249
+ { ...moduleNode('src/generated/api.ts', 'api'), isGenerated: true, loc: 800 },
250
+ ],
251
+ edges: [edge('src/client/bootstrap.ts', 'src/app/main.ts')],
252
+ cycles: [],
253
+ metrics: {
254
+ 'src/client/bootstrap.ts': metric({ fanIn: 0, fanOut: 1 }),
255
+ 'src/legacy/unused.ts': metric({ fanIn: 0, fanOut: 0 }),
256
+ 'src/generated/api.ts': metric({ fanIn: 30, fanOut: 0 }),
257
+ },
258
+ signals: [signal({ stableKey: 'surface:new', severity: 'high' })],
259
+ memoryRisks: [
260
+ {
261
+ id: 'memory:event-listener-cleanup:src/client/bootstrap.ts:3',
262
+ kind: 'event-listener-cleanup',
263
+ moduleId: 'src/client/bootstrap.ts',
264
+ severity: 'medium',
265
+ confidence: 'high',
266
+ evidence: [
267
+ {
268
+ message: 'addEventListener has no visible removeEventListener cleanup',
269
+ line: 3,
270
+ acquire: 'addEventListener',
271
+ expectedCleanup: 'removeEventListener',
272
+ },
273
+ ],
274
+ remediation: 'Remove the listener from the matching component teardown lifecycle.',
275
+ },
276
+ ],
277
+ asyncLifecycleRisks: [
278
+ {
279
+ id: 'async-lifecycle:async-effect-cleanup:src/client/bootstrap.ts:4',
280
+ kind: 'async-effect-cleanup',
281
+ moduleId: 'src/client/bootstrap.ts',
282
+ severity: 'medium',
283
+ confidence: 'high',
284
+ evidence: [
285
+ {
286
+ message: 'async lifecycle work has no visible abort, stale guard, or cleanup',
287
+ line: 4,
288
+ asyncSource: 'fetch',
289
+ expectedGuard: 'AbortController or stale guard cleanup',
290
+ },
291
+ ],
292
+ remediation:
293
+ 'Add AbortController, a stale-result guard, or lifecycle cleanup before updating state.',
294
+ },
295
+ ],
296
+ archDebt: {
297
+ score: 42,
298
+ grade: 'C',
299
+ breakdown: { cycles: 0, layerViolations: 0, hotZones: 0, coupling: 1 },
300
+ },
301
+ });
302
+
303
+ expect(buildLifecycleHygieneView(current)).toMatchObject({
304
+ summary: {
305
+ removableCandidates: 1,
306
+ entryCandidates: 1,
307
+ generatedPressure: 1,
308
+ memoryRisks: 1,
309
+ asyncLifecycleRisks: 1,
310
+ lifecycleRiskModules: 1,
311
+ sideEffectOwners: 1,
312
+ },
313
+ sideEffectOwners: [
314
+ {
315
+ id: 'src/client/bootstrap.ts',
316
+ owner: 'src',
317
+ layer: 'project',
318
+ kind: 'module',
319
+ memoryRisks: 1,
320
+ asyncLifecycleRisks: 1,
321
+ totalRisks: 2,
322
+ placement: 'review',
323
+ },
324
+ ],
325
+ lifecycleRiskModules: [
326
+ {
327
+ id: 'src/client/bootstrap.ts',
328
+ memoryRisks: 1,
329
+ asyncLifecycleRisks: 1,
330
+ totalRisks: 2,
331
+ confidence: 'high',
332
+ severity: 'medium',
333
+ },
334
+ ],
335
+ });
336
+ expect(buildTrendView(baseline, current)).toMatchObject({
337
+ direction: 'regressed',
338
+ summary: { scoreDelta: 30, newSignals: 1 },
339
+ });
340
+ });
341
+
342
+ it('resolves impact targets by exact id or substring', () => {
343
+ const scan = scanFixture();
344
+
345
+ expect(resolveImpactTarget(scan, 'src/features/auth/model/session.ts')).toBe(
346
+ 'src/features/auth/model/session.ts',
347
+ );
348
+ expect(resolveImpactTarget(scan, 'session')).toBe('src/features/auth/model/session.ts');
349
+
350
+ const impact = buildImpactView(scan, 'src/features/auth/model/session.ts');
351
+ expect(impact.importers).toContain('src/pages/login/Page.ts');
352
+ expect(impact.affectedModules).not.toContain('src/features/auth/model/session.ts');
353
+ expect(impact.cyclesTouched).toEqual(['cycle:auth']);
354
+ expect(impact.metrics).toMatchObject({ fanIn: 1, fanOut: 1 });
355
+ });
356
+
357
+ it('explains cycle scope, path edges and suggested breakpoints', () => {
358
+ const view = buildExplainView(scanFixture(), { cycle: 'cycle:auth' });
359
+
360
+ expect(view).toMatchObject({
361
+ kind: 'cycle',
362
+ title: 'Cycle cycle:auth',
363
+ severity: 'direct',
364
+ cycle: {
365
+ affectedAreas: ['features/auth', 'pages/login'],
366
+ affectedFolders: ['src/features/auth/model', 'src/pages/login'],
367
+ affectedLayers: ['features', 'pages'],
368
+ suggestedBreakpoint: {
369
+ from: 'src/features/auth/model/session.ts',
370
+ to: 'src/pages/login/Page.ts',
371
+ },
372
+ },
373
+ });
374
+ expect(view.cycle?.edges).toContainEqual(
375
+ expect.objectContaining({
376
+ from: 'src/features/auth/model/session.ts',
377
+ to: 'src/pages/login/Page.ts',
378
+ fromLayer: 'features',
379
+ toLayer: 'pages',
380
+ fromFolder: 'src/features/auth/model',
381
+ toFolder: 'src/pages/login',
382
+ }),
383
+ );
384
+ expect(view.evidence).toContain(
385
+ 'Affected areas: features/auth, pages/login; layers: features, pages',
386
+ );
387
+ });
388
+
389
+ it('reconciles new, regressed and resolved signals for baseline reports', () => {
390
+ const baseline = scanFixture({
391
+ signals: [
392
+ signal({ stableKey: 'contract:a', severity: 'medium' }),
393
+ signal({ stableKey: 'contract:resolved', severity: 'high' }),
394
+ ],
395
+ });
396
+ const current = scanFixture({
397
+ signals: [
398
+ signal({ stableKey: 'contract:a', severity: 'high' }),
399
+ signal({ stableKey: 'contract:new', severity: 'high' }),
400
+ ],
401
+ });
402
+
403
+ const view = buildSignalBaselineView(baseline, current);
404
+ expect(view.regressedSignals.map((item) => item.stableKey)).toEqual(['contract:a']);
405
+ expect(view.newSignals.map((item) => item.stableKey)).toEqual(['contract:new']);
406
+ expect(view.resolved.map((item) => item.stableKey)).toEqual(['contract:resolved']);
407
+ });
408
+ });
409
+
410
+ function scanFixture(overrides: Partial<ScanResult> = {}): ScanResult {
411
+ return {
412
+ project: { id: 'p', name: 'project', rootPath: '/repo', detectedFramework: 'generic' },
413
+ modules: [
414
+ {
415
+ id: 'src/pages/login/Page.ts',
416
+ absPath: '/repo/src/pages/login/Page.ts',
417
+ kind: 'route',
418
+ language: 'ts',
419
+ loc: 20,
420
+ exports: [],
421
+ isInfra: false,
422
+ },
423
+ {
424
+ id: 'src/features/auth/model/session.ts',
425
+ absPath: '/repo/src/features/auth/model/session.ts',
426
+ kind: 'store',
427
+ language: 'ts',
428
+ loc: 50,
429
+ exports: [],
430
+ isInfra: false,
431
+ },
432
+ ],
433
+ edges: [
434
+ {
435
+ from: 'src/pages/login/Page.ts',
436
+ to: 'src/features/auth/model/session.ts',
437
+ kind: 'static',
438
+ specifier: '@/features/auth/model/session',
439
+ resolved: true,
440
+ },
441
+ {
442
+ from: 'src/features/auth/model/session.ts',
443
+ to: 'src/pages/login/Page.ts',
444
+ kind: 'static',
445
+ specifier: '@/pages/login/Page',
446
+ resolved: true,
447
+ },
448
+ ],
449
+ cycles: [
450
+ {
451
+ id: 'cycle:auth',
452
+ modules: ['src/pages/login/Page.ts', 'src/features/auth/model/session.ts'],
453
+ length: 2,
454
+ severity: 'direct',
455
+ },
456
+ ],
457
+ metrics: {
458
+ 'src/pages/login/Page.ts': {
459
+ fanIn: 1,
460
+ fanOut: 1,
461
+ instability: 0.5,
462
+ depth: 0,
463
+ inCycle: true,
464
+ couplingScore: 1,
465
+ hotnessScore: 1,
466
+ },
467
+ 'src/features/auth/model/session.ts': {
468
+ fanIn: 1,
469
+ fanOut: 1,
470
+ instability: 0.5,
471
+ depth: 1,
472
+ inCycle: true,
473
+ couplingScore: 1,
474
+ hotnessScore: 1,
475
+ },
476
+ },
477
+ hotZones: [],
478
+ layerViolations: [
479
+ {
480
+ edgeId: 'src/features/auth/model/session.ts\u0001src/pages/login/Page.ts',
481
+ from: 'src/features/auth/model/session.ts',
482
+ to: 'src/pages/login/Page.ts',
483
+ fromLayer: 'features',
484
+ toLayer: 'pages',
485
+ severity: 'warning',
486
+ },
487
+ ],
488
+ archDebt: {
489
+ score: 30,
490
+ grade: 'B',
491
+ breakdown: { cycles: 1, layerViolations: 1, hotZones: 0, coupling: 1 },
492
+ },
493
+ recommendations: [],
494
+ contractViolations: [],
495
+ scannedAt: '2026-05-19T00:00:00.000Z',
496
+ durationMs: 1,
497
+ warnings: [],
498
+ ...overrides,
499
+ };
500
+ }
501
+
502
+ function moduleNode(
503
+ id: string,
504
+ kind: ScanResult['modules'][number]['kind'],
505
+ exports: string[] = [],
506
+ ) {
507
+ return {
508
+ id,
509
+ absPath: `/repo/${id}`,
510
+ kind,
511
+ language: 'ts',
512
+ loc: 10,
513
+ exports,
514
+ isInfra: false,
515
+ } satisfies ScanResult['modules'][number];
516
+ }
517
+
518
+ function metric(overrides: Partial<ScanResult['metrics'][string]> = {}) {
519
+ return {
520
+ fanIn: 0,
521
+ fanOut: 0,
522
+ instability: 0,
523
+ depth: 0,
524
+ inCycle: false,
525
+ couplingScore: 0,
526
+ hotnessScore: 0,
527
+ ...overrides,
528
+ } satisfies ScanResult['metrics'][string];
529
+ }
530
+
531
+ function edge(from: string, to: string) {
532
+ return {
533
+ from,
534
+ to,
535
+ kind: 'static',
536
+ specifier: to,
537
+ resolved: true,
538
+ } satisfies ScanResult['edges'][number];
539
+ }
540
+
541
+ function signal(overrides: Partial<ArchitectureSignal>): ArchitectureSignal {
542
+ return {
543
+ id: `signal:${overrides.stableKey ?? 'default'}`,
544
+ stableKey: 'contract:default',
545
+ kind: 'contract-violation',
546
+ title: 'contract',
547
+ severity: 'high',
548
+ confidence: 'high',
549
+ actionability: 'manual',
550
+ status: 'new',
551
+ maturity: 'stable',
552
+ modules: ['src/features/auth/model/session.ts'],
553
+ evidence: [{ kind: 'contract', message: 'contract issue', confidence: 'high' }],
554
+ limitations: [],
555
+ ranking: { score: 10, reasons: [], noisePenalty: 0, noveltyBoost: 0 },
556
+ ...overrides,
557
+ };
558
+ }