@harness-engineering/core 0.26.4 → 0.27.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.
@@ -1,131 +1,3 @@
1
- var __defProp = Object.defineProperty;
2
- var __export = (target, all) => {
3
- for (var name in all)
4
- __defProp(target, name, { get: all[name], enumerable: true });
5
- };
6
-
7
- // src/architecture/types.ts
8
- import { z } from "zod";
9
- var ArchMetricCategorySchema = z.enum([
10
- "circular-deps",
11
- "layer-violations",
12
- "complexity",
13
- "coupling",
14
- "forbidden-imports",
15
- "module-size",
16
- "dependency-depth"
17
- ]);
18
- var ViolationSchema = z.object({
19
- id: z.string(),
20
- // stable hash: sha256(relativePath + ':' + category + ':' + normalizedDetail)
21
- file: z.string(),
22
- // relative to project root
23
- category: ArchMetricCategorySchema.optional(),
24
- // context for baseline reporting
25
- detail: z.string(),
26
- // human-readable description
27
- severity: z.enum(["error", "warning"])
28
- });
29
- var MetricResultSchema = z.object({
30
- category: ArchMetricCategorySchema,
31
- scope: z.string(),
32
- // e.g., 'project', 'src/services', 'src/api/routes.ts'
33
- value: z.number(),
34
- // numeric metric (violation count, complexity score, etc.)
35
- violations: z.array(ViolationSchema),
36
- metadata: z.record(z.unknown()).optional()
37
- });
38
- var CategoryBaselineSchema = z.object({
39
- value: z.number(),
40
- // aggregate metric value at baseline time
41
- violationIds: z.array(z.string())
42
- // stable IDs of known violations (the allowlist)
43
- });
44
- var ArchBaselineSchema = z.object({
45
- version: z.literal(1),
46
- updatedAt: z.string().datetime(),
47
- // ISO 8601
48
- updatedFrom: z.string(),
49
- // commit hash
50
- metrics: z.record(ArchMetricCategorySchema, CategoryBaselineSchema)
51
- });
52
- var CategoryRegressionSchema = z.object({
53
- category: ArchMetricCategorySchema,
54
- baselineValue: z.number(),
55
- currentValue: z.number(),
56
- delta: z.number()
57
- });
58
- var ArchDiffResultSchema = z.object({
59
- passed: z.boolean(),
60
- newViolations: z.array(ViolationSchema),
61
- // in current but not in baseline -> FAIL
62
- resolvedViolations: z.array(z.string()),
63
- // in baseline but not in current -> celebrate
64
- preExisting: z.array(z.string()),
65
- // in both -> allowed, tracked
66
- regressions: z.array(CategoryRegressionSchema)
67
- // aggregate value exceeded baseline
68
- });
69
- var ThresholdConfigSchema = z.record(
70
- ArchMetricCategorySchema,
71
- z.union([z.number(), z.record(z.string(), z.number())])
72
- );
73
- var ArchConfigSchema = z.object({
74
- enabled: z.boolean().default(true),
75
- baselinePath: z.string().default(".harness/arch/baselines.json"),
76
- thresholds: ThresholdConfigSchema.default({}),
77
- modules: z.record(z.string(), ThresholdConfigSchema).default({})
78
- });
79
- var ConstraintRuleSchema = z.object({
80
- id: z.string(),
81
- // stable hash: sha256(category + ':' + scope + ':' + description)
82
- category: ArchMetricCategorySchema,
83
- description: z.string(),
84
- // e.g., "Layer 'services' must not import from 'ui'"
85
- scope: z.string(),
86
- // e.g., 'src/services/', 'project'
87
- targets: z.array(z.string()).optional()
88
- // forward-compat for governs edges
89
- });
90
- var ViolationSnapshotSchema = z.object({
91
- timestamp: z.string().datetime(),
92
- violations: z.array(ViolationSchema)
93
- });
94
- var ViolationHistorySchema = z.object({
95
- version: z.literal(1),
96
- snapshots: z.array(ViolationSnapshotSchema)
97
- });
98
- var EmergenceConfidenceSchema = z.enum(["low", "medium", "high"]);
99
- var EmergentConstraintSuggestionSchema = z.object({
100
- suggestedRule: ConstraintRuleSchema,
101
- confidence: EmergenceConfidenceSchema,
102
- occurrences: z.number(),
103
- uniqueFiles: z.number(),
104
- pattern: z.string(),
105
- sampleViolations: z.array(ViolationSchema),
106
- rationale: z.string()
107
- });
108
- var EmergenceResultSchema = z.object({
109
- suggestions: z.array(EmergentConstraintSuggestionSchema),
110
- totalViolationsAnalyzed: z.number(),
111
- windowWeeks: z.number(),
112
- minOccurrences: z.number()
113
- });
114
-
115
- // src/architecture/collectors/hash.ts
116
- import { createHash } from "crypto";
117
- function violationId(relativePath, category, normalizedDetail) {
118
- const input = `${relativePath}:${category}:${normalizedDetail}`;
119
- return createHash("sha256").update(input).digest("hex");
120
- }
121
- function constraintRuleId(category, scope, description) {
122
- const input = `${category}:${scope}:${description}`;
123
- return createHash("sha256").update(input).digest("hex");
124
- }
125
-
126
- // src/shared/result.ts
127
- import { Ok, Err, isOk, isErr } from "@harness-engineering/types";
128
-
129
1
  // src/shared/errors.ts
130
2
  function createError(code, message, details = {}, suggestions = []) {
131
3
  return { code, message, details, suggestions };
@@ -134,26 +6,17 @@ function createEntropyError(code, message, details = {}, suggestions = []) {
134
6
  return { code, message, details, suggestions };
135
7
  }
136
8
 
137
- // src/constraints/layers.ts
138
- import { minimatch } from "minimatch";
139
- function defineLayer(name, patterns, allowedDependencies) {
140
- return {
141
- name,
142
- patterns,
143
- allowedDependencies
144
- };
145
- }
146
- function resolveFileToLayer(file, layers) {
147
- for (const layer of layers) {
148
- for (const pattern of layer.patterns) {
149
- if (minimatch(file, pattern)) {
150
- return layer;
151
- }
152
- }
153
- }
154
- return void 0;
9
+ // src/shared/result.ts
10
+ import { Ok, Err, isOk, isErr } from "@harness-engineering/types";
11
+
12
+ // src/shared/parsers/base.ts
13
+ function createParseError(code, message, details = {}, suggestions = []) {
14
+ return { code, message, details, suggestions };
155
15
  }
156
16
 
17
+ // src/shared/parsers/typescript.ts
18
+ import { parse } from "@typescript-eslint/typescript-estree";
19
+
157
20
  // src/shared/fs-utils.ts
158
21
  import { access, constants, readFile } from "fs";
159
22
  import { promisify } from "util";
@@ -194,485 +57,110 @@ function relativePosix(from, to) {
194
57
  return relative(from, to).replaceAll("\\", "/");
195
58
  }
196
59
 
197
- // src/constraints/dependencies.ts
198
- import { dirname, resolve, extname } from "path";
199
- var EXTENSION_BY_LANG = {
200
- typescript: [".ts", ".tsx"],
201
- javascript: [".js", ".jsx", ".mjs", ".cjs"],
202
- python: [".py"],
203
- go: [".go"],
204
- rust: [".rs"],
205
- java: [".java"]
206
- };
207
- var JS_EXT_FALLBACKS = {
208
- ".js": [".ts", ".tsx", ".jsx"],
209
- ".jsx": [".tsx"],
210
- ".mjs": [".mts"],
211
- ".cjs": [".cts"]
212
- };
213
- function detectLangFromExt(ext) {
214
- for (const [lang, exts] of Object.entries(EXTENSION_BY_LANG)) {
215
- if (exts.includes(ext)) return lang;
60
+ // src/shared/parsers/typescript.ts
61
+ function walk(node, visitor) {
62
+ if (!node || typeof node !== "object") return;
63
+ if ("type" in node) {
64
+ visitor(node);
216
65
  }
217
- return null;
218
- }
219
- function getExtensionsForLang(lang) {
220
- switch (lang) {
221
- case "typescript":
222
- return [".ts", ".tsx"];
223
- case "javascript":
224
- return [".js", ".jsx", ".mjs", ".cjs"];
225
- case "python":
226
- return [".py"];
227
- case "go":
228
- return [".go"];
229
- case "rust":
230
- return [".rs"];
231
- case "java":
232
- return [".java"];
233
- default:
234
- return [".ts", ".tsx", ".js", ".jsx"];
66
+ for (const value of Object.values(node)) {
67
+ if (Array.isArray(value)) {
68
+ value.forEach((v) => walk(v, visitor));
69
+ } else {
70
+ walk(value, visitor);
71
+ }
235
72
  }
236
73
  }
237
- async function resolveImportPath(importSource, fromFile, _rootDir) {
238
- if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
239
- return null;
240
- }
241
- const fromDir = dirname(fromFile);
242
- const resolved = resolve(fromDir, importSource);
243
- const sourceExt = extname(resolved);
244
- const fromLang = detectLangFromExt(extname(fromFile));
245
- const fallbacks = JS_EXT_FALLBACKS[sourceExt];
246
- if (fallbacks) {
247
- const base = resolved.slice(0, -sourceExt.length);
248
- for (const ext of fallbacks) {
249
- const candidate = base + ext;
250
- if (await fileExists(candidate)) return candidate.replace(/\\/g, "/");
251
- }
252
- for (const indexExt of [".ts", ".tsx", ".jsx"]) {
253
- const indexPath = resolve(base, "index" + indexExt);
254
- if (await fileExists(indexPath)) return indexPath.replace(/\\/g, "/");
74
+ function makeLocation(node) {
75
+ return {
76
+ file: "",
77
+ line: node.loc?.start.line ?? 0,
78
+ column: node.loc?.start.column ?? 0
79
+ };
80
+ }
81
+ function processImportSpecifiers(importDecl, imp) {
82
+ for (const spec of importDecl.specifiers) {
83
+ if (spec.type === "ImportDefaultSpecifier") {
84
+ imp.default = spec.local.name;
85
+ } else if (spec.type === "ImportNamespaceSpecifier") {
86
+ imp.namespace = spec.local.name;
87
+ } else if (spec.type === "ImportSpecifier") {
88
+ imp.specifiers.push(spec.local.name);
89
+ if (spec.importKind === "type") {
90
+ imp.kind = "type";
91
+ }
255
92
  }
256
93
  }
257
- const hasKnownExt = Object.values(EXTENSION_BY_LANG).flat().some((e) => resolved.endsWith(e));
258
- if (hasKnownExt) {
259
- return resolved.replace(/\\/g, "/");
260
- }
261
- const extensions = getExtensionsForLang(fromLang);
262
- return (resolved + extensions[0]).replace(/\\/g, "/");
263
94
  }
264
- function getImportType(imp) {
265
- if (imp.kind === "type") return "type-only";
266
- return "static";
95
+ function getExportedName(exported) {
96
+ return exported.type === "Identifier" ? exported.name : String(exported.value);
267
97
  }
268
- async function buildDependencyGraph(files, parser, graphDependencyData) {
269
- if (graphDependencyData) {
270
- return Ok({
271
- nodes: graphDependencyData.nodes,
272
- edges: graphDependencyData.edges
98
+ function processReExportSpecifiers(exportDecl, exports) {
99
+ for (const spec of exportDecl.specifiers) {
100
+ if (spec.type !== "ExportSpecifier") continue;
101
+ exports.push({
102
+ name: getExportedName(spec.exported),
103
+ type: "named",
104
+ location: makeLocation(exportDecl),
105
+ isReExport: true,
106
+ source: exportDecl.source.value
273
107
  });
274
108
  }
275
- const isLookup = "getForFile" in parser;
276
- const nodes = files.map((f) => f.replace(/\\/g, "/"));
277
- const edges = [];
278
- for (const file of files) {
279
- const normalizedFile = file.replace(/\\/g, "/");
280
- const fileParser = isLookup ? parser.getForFile(file) : parser;
281
- if (!fileParser) continue;
282
- const parseResult = await fileParser.parseFile(file);
283
- if (!parseResult.ok) {
284
- continue;
285
- }
286
- const importsResult = fileParser.extractImports(parseResult.value);
287
- if (!importsResult.ok) {
288
- continue;
289
- }
290
- for (const imp of importsResult.value) {
291
- const resolvedPath = await resolveImportPath(imp.source, file, "");
292
- if (resolvedPath) {
293
- edges.push({
294
- from: normalizedFile,
295
- to: resolvedPath,
296
- importType: getImportType(imp),
297
- line: imp.location.line
109
+ }
110
+ function processExportDeclaration(exportDecl, exports) {
111
+ const decl = exportDecl.declaration;
112
+ if (!decl) return;
113
+ if (decl.type === "VariableDeclaration") {
114
+ for (const declarator of decl.declarations) {
115
+ if (declarator.id.type === "Identifier") {
116
+ exports.push({
117
+ name: declarator.id.name,
118
+ type: "named",
119
+ location: makeLocation(decl),
120
+ isReExport: false
298
121
  });
299
122
  }
300
123
  }
124
+ } else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
125
+ exports.push({
126
+ name: decl.id.name,
127
+ type: "named",
128
+ location: makeLocation(decl),
129
+ isReExport: false
130
+ });
301
131
  }
302
- return Ok({ nodes, edges });
303
132
  }
304
- function checkLayerViolations(graph, layers, rootDir) {
305
- const violations = [];
306
- for (const edge of graph.edges) {
307
- const fromRelative = relativePosix(rootDir, edge.from);
308
- const toRelative = relativePosix(rootDir, edge.to);
309
- const fromLayer = resolveFileToLayer(fromRelative, layers);
310
- const toLayer = resolveFileToLayer(toRelative, layers);
311
- if (!fromLayer || !toLayer) continue;
312
- if (fromLayer.name === toLayer.name) continue;
313
- if (!fromLayer.allowedDependencies.includes(toLayer.name)) {
314
- violations.push({
315
- file: edge.from,
316
- imports: edge.to,
317
- fromLayer: fromLayer.name,
318
- toLayer: toLayer.name,
319
- reason: "WRONG_LAYER",
320
- line: edge.line,
321
- suggestion: `Move the dependency to an allowed layer (${fromLayer.allowedDependencies.join(", ") || "none"}) or update layer rules`
322
- });
323
- }
133
+ function processExportListSpecifiers(exportDecl, exports) {
134
+ for (const spec of exportDecl.specifiers) {
135
+ if (spec.type !== "ExportSpecifier") continue;
136
+ exports.push({
137
+ name: getExportedName(spec.exported),
138
+ type: "named",
139
+ location: makeLocation(exportDecl),
140
+ isReExport: false
141
+ });
324
142
  }
325
- return violations;
326
143
  }
327
- async function validateDependencies(config) {
328
- const { layers, rootDir, parser, fallbackBehavior = "error", graphDependencyData } = config;
329
- if (graphDependencyData) {
330
- const graphResult2 = await buildDependencyGraph([], parser, graphDependencyData);
331
- if (!graphResult2.ok) {
332
- return Err(graphResult2.error);
144
+ var TypeScriptParser = class {
145
+ name = "typescript";
146
+ extensions = [".ts", ".tsx", ".mts", ".cts"];
147
+ async parseFile(path) {
148
+ const contentResult = await readFileContent(path);
149
+ if (!contentResult.ok) {
150
+ return Err(
151
+ createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
152
+ "Check that the file exists",
153
+ "Verify the path is correct"
154
+ ])
155
+ );
333
156
  }
334
- const violations2 = checkLayerViolations(graphResult2.value, layers, rootDir);
335
- return Ok({
336
- valid: violations2.length === 0,
337
- violations: violations2,
338
- graph: graphResult2.value
339
- });
340
- }
341
- const healthResult = await parser.health();
342
- if (!healthResult.ok || !healthResult.value.available) {
343
- if (fallbackBehavior === "skip") {
344
- return Ok({
345
- valid: true,
346
- violations: [],
347
- graph: { nodes: [], edges: [] },
348
- skipped: true,
349
- reason: "Parser unavailable"
350
- });
351
- }
352
- if (fallbackBehavior === "warn") {
353
- console.warn(`Parser ${parser.name} unavailable, skipping validation`);
354
- return Ok({
355
- valid: true,
356
- violations: [],
357
- graph: { nodes: [], edges: [] },
358
- skipped: true,
359
- reason: "Parser unavailable"
360
- });
361
- }
362
- return Err(
363
- createError(
364
- "PARSER_UNAVAILABLE",
365
- `Parser ${parser.name} is not available`,
366
- { parser: parser.name },
367
- ["Install required runtime", "Use different parser", 'Set fallbackBehavior: "skip"']
368
- )
369
- );
370
- }
371
- const allFiles = [];
372
- for (const layer of layers) {
373
- for (const pattern of layer.patterns) {
374
- const files = await findFiles(pattern, rootDir);
375
- allFiles.push(...files);
376
- }
377
- }
378
- const uniqueFiles = [...new Set(allFiles)];
379
- const graphResult = await buildDependencyGraph(uniqueFiles, parser);
380
- if (!graphResult.ok) {
381
- return Err(graphResult.error);
382
- }
383
- const violations = checkLayerViolations(graphResult.value, layers, rootDir);
384
- return Ok({
385
- valid: violations.length === 0,
386
- violations,
387
- graph: graphResult.value
388
- });
389
- }
390
-
391
- // src/constraints/circular-deps.ts
392
- function buildAdjacencyList(graph) {
393
- const adjacency = /* @__PURE__ */ new Map();
394
- const nodeSet = new Set(graph.nodes);
395
- for (const node of graph.nodes) {
396
- adjacency.set(node, []);
397
- }
398
- for (const edge of graph.edges) {
399
- const neighbors = adjacency.get(edge.from);
400
- if (neighbors && nodeSet.has(edge.to)) {
401
- neighbors.push(edge.to);
402
- }
403
- }
404
- return adjacency;
405
- }
406
- function isCyclicSCC(scc, adjacency) {
407
- if (scc.length > 1) return true;
408
- if (scc.length === 1) {
409
- const selfNode = scc[0];
410
- const selfNeighbors = adjacency.get(selfNode) ?? [];
411
- return selfNeighbors.includes(selfNode);
412
- }
413
- return false;
414
- }
415
- function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
416
- for (const neighbor of neighbors) {
417
- const neighborData = nodeMap.get(neighbor);
418
- if (!neighborData) {
419
- strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
420
- const nodeData = nodeMap.get(node);
421
- const updatedNeighborData = nodeMap.get(neighbor);
422
- nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
423
- } else if (neighborData.onStack) {
424
- const nodeData = nodeMap.get(node);
425
- nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
426
- }
427
- }
428
- }
429
- function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
430
- nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
431
- indexRef.value++;
432
- stack.push(node);
433
- processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
434
- const nodeData = nodeMap.get(node);
435
- if (nodeData.lowlink === nodeData.index) {
436
- const scc = [];
437
- let w;
438
- do {
439
- w = stack.pop();
440
- nodeMap.get(w).onStack = false;
441
- scc.push(w);
442
- } while (w !== node);
443
- if (isCyclicSCC(scc, adjacency)) {
444
- sccs.push(scc);
445
- }
446
- }
447
- }
448
- function tarjanSCC(graph) {
449
- const nodeMap = /* @__PURE__ */ new Map();
450
- const stack = [];
451
- const sccs = [];
452
- const indexRef = { value: 0 };
453
- const adjacency = buildAdjacencyList(graph);
454
- for (const node of graph.nodes) {
455
- if (!nodeMap.has(node)) {
456
- strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
457
- }
458
- }
459
- return sccs;
460
- }
461
- function detectCircularDeps(graph) {
462
- const sccs = tarjanSCC(graph);
463
- const cycles = sccs.map((scc) => {
464
- const reversed = scc.reverse();
465
- const firstNode = reversed[reversed.length - 1];
466
- const cycle = [...reversed, firstNode];
467
- return {
468
- cycle,
469
- severity: "error",
470
- size: scc.length
471
- };
472
- });
473
- const largestCycle = cycles.reduce((max, c) => Math.max(max, c.size), 0);
474
- return Ok({
475
- hasCycles: cycles.length > 0,
476
- cycles,
477
- largestCycle
478
- });
479
- }
480
- async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
481
- const graphResult = await buildDependencyGraph(files, parser, graphDependencyData);
482
- if (!graphResult.ok) {
483
- return graphResult;
484
- }
485
- return detectCircularDeps(graphResult.value);
486
- }
487
-
488
- // src/architecture/collectors/circular-deps.ts
489
- function makeStubParser() {
490
- return {
491
- name: "typescript",
492
- extensions: [".ts", ".tsx"],
493
- parseFile: async () => ({ ok: false, error: { code: "PARSE_ERROR", message: "not needed" } }),
494
- extractImports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
495
- extractExports: () => ({ ok: false, error: { code: "EXTRACT_ERROR", message: "not needed" } }),
496
- health: async () => ({ ok: true, value: { available: true } })
497
- };
498
- }
499
- function mapCycleViolations(cycles, rootDir, category) {
500
- return cycles.map((cycle) => {
501
- const cyclePath = cycle.cycle.map((f) => relativePosix(rootDir, f)).join(" -> ");
502
- const firstFile = relativePosix(rootDir, cycle.cycle[0]);
503
- return {
504
- id: violationId(firstFile, category, cyclePath),
505
- file: firstFile,
506
- detail: `Circular dependency: ${cyclePath}`,
507
- severity: cycle.severity
508
- };
509
- });
510
- }
511
- var CircularDepsCollector = class {
512
- category = "circular-deps";
513
- getRules(_config, _rootDir) {
514
- const description = "No circular dependencies allowed";
515
- return [
516
- {
517
- id: constraintRuleId(this.category, "project", description),
518
- category: this.category,
519
- description,
520
- scope: "project"
521
- }
522
- ];
523
- }
524
- async collect(_config, rootDir) {
525
- const files = await findFiles("**/*.ts", rootDir);
526
- const graphResult = await buildDependencyGraph(files, makeStubParser());
527
- if (!graphResult.ok) {
528
- return [
529
- {
530
- category: this.category,
531
- scope: "project",
532
- value: 0,
533
- violations: [],
534
- metadata: { error: "Failed to build dependency graph" }
535
- }
536
- ];
537
- }
538
- const result = detectCircularDeps(graphResult.value);
539
- if (!result.ok) {
540
- return [
541
- {
542
- category: this.category,
543
- scope: "project",
544
- value: 0,
545
- violations: [],
546
- metadata: { error: "Failed to detect circular deps" }
547
- }
548
- ];
549
- }
550
- const { cycles, largestCycle } = result.value;
551
- const violations = mapCycleViolations(cycles, rootDir, this.category);
552
- return [
553
- {
554
- category: this.category,
555
- scope: "project",
556
- value: cycles.length,
557
- violations,
558
- metadata: { largestCycle, cycleCount: cycles.length }
559
- }
560
- ];
561
- }
562
- };
563
-
564
- // src/shared/parsers/typescript.ts
565
- import { parse } from "@typescript-eslint/typescript-estree";
566
-
567
- // src/shared/parsers/base.ts
568
- function createParseError(code, message, details = {}, suggestions = []) {
569
- return { code, message, details, suggestions };
570
- }
571
-
572
- // src/shared/parsers/typescript.ts
573
- function walk(node, visitor) {
574
- if (!node || typeof node !== "object") return;
575
- if ("type" in node) {
576
- visitor(node);
577
- }
578
- for (const value of Object.values(node)) {
579
- if (Array.isArray(value)) {
580
- value.forEach((v) => walk(v, visitor));
581
- } else {
582
- walk(value, visitor);
583
- }
584
- }
585
- }
586
- function makeLocation(node) {
587
- return {
588
- file: "",
589
- line: node.loc?.start.line ?? 0,
590
- column: node.loc?.start.column ?? 0
591
- };
592
- }
593
- function processImportSpecifiers(importDecl, imp) {
594
- for (const spec of importDecl.specifiers) {
595
- if (spec.type === "ImportDefaultSpecifier") {
596
- imp.default = spec.local.name;
597
- } else if (spec.type === "ImportNamespaceSpecifier") {
598
- imp.namespace = spec.local.name;
599
- } else if (spec.type === "ImportSpecifier") {
600
- imp.specifiers.push(spec.local.name);
601
- if (spec.importKind === "type") {
602
- imp.kind = "type";
603
- }
604
- }
605
- }
606
- }
607
- function getExportedName(exported) {
608
- return exported.type === "Identifier" ? exported.name : String(exported.value);
609
- }
610
- function processReExportSpecifiers(exportDecl, exports) {
611
- for (const spec of exportDecl.specifiers) {
612
- if (spec.type !== "ExportSpecifier") continue;
613
- exports.push({
614
- name: getExportedName(spec.exported),
615
- type: "named",
616
- location: makeLocation(exportDecl),
617
- isReExport: true,
618
- source: exportDecl.source.value
619
- });
620
- }
621
- }
622
- function processExportDeclaration(exportDecl, exports) {
623
- const decl = exportDecl.declaration;
624
- if (!decl) return;
625
- if (decl.type === "VariableDeclaration") {
626
- for (const declarator of decl.declarations) {
627
- if (declarator.id.type === "Identifier") {
628
- exports.push({
629
- name: declarator.id.name,
630
- type: "named",
631
- location: makeLocation(decl),
632
- isReExport: false
633
- });
634
- }
635
- }
636
- } else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
637
- exports.push({
638
- name: decl.id.name,
639
- type: "named",
640
- location: makeLocation(decl),
641
- isReExport: false
642
- });
643
- }
644
- }
645
- function processExportListSpecifiers(exportDecl, exports) {
646
- for (const spec of exportDecl.specifiers) {
647
- if (spec.type !== "ExportSpecifier") continue;
648
- exports.push({
649
- name: getExportedName(spec.exported),
650
- type: "named",
651
- location: makeLocation(exportDecl),
652
- isReExport: false
653
- });
654
- }
655
- }
656
- var TypeScriptParser = class {
657
- name = "typescript";
658
- extensions = [".ts", ".tsx", ".mts", ".cts"];
659
- async parseFile(path) {
660
- const contentResult = await readFileContent(path);
661
- if (!contentResult.ok) {
662
- return Err(
663
- createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
664
- "Check that the file exists",
665
- "Verify the path is correct"
666
- ])
667
- );
668
- }
669
- try {
670
- const ast = parse(contentResult.value, {
671
- loc: true,
672
- range: true,
673
- jsx: path.endsWith(".tsx"),
674
- errorOnUnknownASTType: false
675
- });
157
+ try {
158
+ const ast = parse(contentResult.value, {
159
+ loc: true,
160
+ range: true,
161
+ jsx: path.endsWith(".tsx"),
162
+ errorOnUnknownASTType: false
163
+ });
676
164
  return Ok({
677
165
  type: "Program",
678
166
  body: ast,
@@ -760,9 +248,6 @@ var TypeScriptParser = class {
760
248
  }
761
249
  };
762
250
 
763
- // src/code-nav/parser.ts
764
- import Parser from "web-tree-sitter";
765
-
766
251
  // src/code-nav/types.ts
767
252
  var EXTENSION_MAP = {
768
253
  ".ts": "typescript",
@@ -784,6 +269,7 @@ function detectLanguage(filePath) {
784
269
  }
785
270
 
786
271
  // src/code-nav/parser.ts
272
+ import Parser from "web-tree-sitter";
787
273
  var parserCache = /* @__PURE__ */ new Map();
788
274
  var initialized = false;
789
275
  var GRAMMAR_MAP = {
@@ -1083,604 +569,218 @@ function formatOutline(outline) {
1083
569
  return lines.join("\n");
1084
570
  }
1085
571
 
1086
- // src/shared/parsers/tree-sitter.ts
1087
- function findSymbolByName(symbols, name) {
1088
- for (const sym of symbols) {
1089
- if (sym.name === name) return { line: sym.line, endLine: sym.endLine };
1090
- if (sym.children) {
1091
- const found = findSymbolByName(sym.children, name);
1092
- if (found) return found;
1093
- }
1094
- }
1095
- return null;
1096
- }
1097
- function makeLocation2(node) {
572
+ // src/constraints/layers.ts
573
+ import { minimatch } from "minimatch";
574
+ function defineLayer(name, patterns, allowedDependencies) {
1098
575
  return {
1099
- file: "",
1100
- line: node.startPosition.row + 1,
1101
- column: node.startPosition.column
576
+ name,
577
+ patterns,
578
+ allowedDependencies
1102
579
  };
1103
580
  }
1104
- function makeNamedExport(node) {
1105
- const name = node.childForFieldName("name");
1106
- if (!name) return null;
1107
- return { name: name.text, type: "named", location: makeLocation2(node), isReExport: false };
1108
- }
1109
- function makeImport(node, source) {
1110
- return { source, specifiers: [], location: makeLocation2(node), kind: "value" };
1111
- }
1112
- function extractPythonImport(child) {
1113
- const name = child.childForFieldName("name");
1114
- if (!name) return null;
1115
- return makeImport(child, name.text);
1116
- }
1117
- function extractPythonFromImport(child) {
1118
- const moduleName = child.childForFieldName("module_name");
1119
- const specifiers = [];
1120
- for (const c of child.children) {
1121
- if (c.type === "dotted_name" && c !== moduleName) specifiers.push(c.text);
1122
- if (c.type === "aliased_import") {
1123
- const n = c.childForFieldName("name");
1124
- if (n) specifiers.push(n.text);
581
+ function resolveFileToLayer(file, layers) {
582
+ for (const layer of layers) {
583
+ for (const pattern of layer.patterns) {
584
+ if (minimatch(file, pattern)) {
585
+ return layer;
586
+ }
1125
587
  }
1126
588
  }
1127
- return {
1128
- source: moduleName?.text ?? "",
1129
- specifiers,
1130
- location: makeLocation2(child),
1131
- kind: "value"
1132
- };
589
+ return void 0;
1133
590
  }
1134
- function extractPythonExport(child) {
1135
- if (child.type === "function_definition" || child.type === "class_definition") {
1136
- return makeNamedExport(child);
1137
- }
1138
- if (child.type === "assignment") {
1139
- const left = child.childForFieldName("left") ?? child.children[0];
1140
- if (left && !left.text.startsWith("_")) {
1141
- return { name: left.text, type: "named", location: makeLocation2(child), isReExport: false };
1142
- }
1143
- }
1144
- return null;
1145
- }
1146
- var pythonStrategy = {
1147
- extractImports(root) {
1148
- const imports = [];
1149
- for (const child of root.children) {
1150
- if (child.type === "import_statement") {
1151
- const imp = extractPythonImport(child);
1152
- if (imp) imports.push(imp);
1153
- } else if (child.type === "import_from_statement") {
1154
- imports.push(extractPythonFromImport(child));
1155
- }
1156
- }
1157
- return imports;
1158
- },
1159
- extractExports(root) {
1160
- const exports = [];
1161
- for (const child of root.children) {
1162
- const exp = extractPythonExport(child);
1163
- if (exp) exports.push(exp);
1164
- }
1165
- return exports;
1166
- }
591
+
592
+ // src/constraints/dependencies.ts
593
+ import { dirname, resolve, extname } from "path";
594
+ var EXTENSION_BY_LANG = {
595
+ typescript: [".ts", ".tsx"],
596
+ javascript: [".js", ".jsx", ".mjs", ".cjs"],
597
+ python: [".py"],
598
+ go: [".go"],
599
+ rust: [".rs"],
600
+ java: [".java"]
1167
601
  };
1168
- function extractGoImportPath(spec) {
1169
- const pathNode = spec.childForFieldName("path") ?? spec.children.find((c) => c.type === "interpreted_string_literal");
1170
- return pathNode ? pathNode.text.replace(/"/g, "") : null;
1171
- }
1172
- function extractGoImportsFromDecl(child) {
1173
- const imports = [];
1174
- for (const spec of child.children.filter((c) => c.type === "import_spec")) {
1175
- const source = extractGoImportPath(spec);
1176
- if (source) imports.push(makeImport(child, source));
1177
- }
1178
- const specList = child.children.find((c) => c.type === "import_spec_list");
1179
- if (specList) {
1180
- for (const spec of specList.children.filter((c) => c.type === "import_spec")) {
1181
- const source = extractGoImportPath(spec);
1182
- if (source) imports.push(makeImport(spec, source));
1183
- }
1184
- }
1185
- return imports;
1186
- }
1187
- function isGoExported(name) {
1188
- return /^[A-Z]/.test(name);
1189
- }
1190
- function extractGoExport(child) {
1191
- if (child.type === "function_declaration" || child.type === "method_declaration") {
1192
- const name = child.childForFieldName("name");
1193
- if (name && isGoExported(name.text)) return makeNamedExport(child);
1194
- }
1195
- if (child.type === "type_declaration") {
1196
- const typeSpec = child.children.find((c) => c.type === "type_spec");
1197
- if (typeSpec) {
1198
- const name = typeSpec.childForFieldName("name");
1199
- if (name && isGoExported(name.text)) {
1200
- return { name: name.text, type: "named", location: makeLocation2(child), isReExport: false };
1201
- }
1202
- }
602
+ var JS_EXT_FALLBACKS = {
603
+ ".js": [".ts", ".tsx", ".jsx"],
604
+ ".jsx": [".tsx"],
605
+ ".mjs": [".mts"],
606
+ ".cjs": [".cts"]
607
+ };
608
+ function detectLangFromExt(ext) {
609
+ for (const [lang, exts] of Object.entries(EXTENSION_BY_LANG)) {
610
+ if (exts.includes(ext)) return lang;
1203
611
  }
1204
612
  return null;
1205
613
  }
1206
- var goStrategy = {
1207
- extractImports(root) {
1208
- const imports = [];
1209
- for (const child of root.children) {
1210
- if (child.type === "import_declaration") imports.push(...extractGoImportsFromDecl(child));
1211
- }
1212
- return imports;
1213
- },
1214
- extractExports(root) {
1215
- const exports = [];
1216
- for (const child of root.children) {
1217
- const exp = extractGoExport(child);
1218
- if (exp) exports.push(exp);
1219
- }
1220
- return exports;
1221
- }
1222
- };
1223
- var RUST_USE_ARG_TYPES = /* @__PURE__ */ new Set([
1224
- "scoped_identifier",
1225
- "use_wildcard",
1226
- "scoped_use_list",
1227
- "identifier"
1228
- ]);
1229
- var RUST_PUB_ITEM_TYPES = /* @__PURE__ */ new Set([
1230
- "function_item",
1231
- "struct_item",
1232
- "enum_item",
1233
- "trait_item",
1234
- "type_item",
1235
- "const_item",
1236
- "static_item"
1237
- ]);
1238
- var rustStrategy = {
1239
- extractImports(root) {
1240
- const imports = [];
1241
- for (const child of root.children) {
1242
- if (child.type !== "use_declaration") continue;
1243
- const arg = child.childForFieldName("argument") ?? child.children.find((c) => RUST_USE_ARG_TYPES.has(c.type));
1244
- if (arg) imports.push(makeImport(child, arg.text));
1245
- }
1246
- return imports;
1247
- },
1248
- extractExports(root, source) {
1249
- const exports = [];
1250
- const lines = source.split("\n");
1251
- for (const child of root.children) {
1252
- const line = lines[child.startPosition.row] ?? "";
1253
- if (!/^\s*pub\b/.test(line)) continue;
1254
- if (RUST_PUB_ITEM_TYPES.has(child.type)) {
1255
- const exp = makeNamedExport(child);
1256
- if (exp) exports.push(exp);
1257
- } else if (child.type === "mod_item") {
1258
- const name = child.childForFieldName("name");
1259
- if (name) {
1260
- exports.push({
1261
- name: name.text,
1262
- type: "namespace",
1263
- location: makeLocation2(child),
1264
- isReExport: false
1265
- });
1266
- }
1267
- }
1268
- }
1269
- return exports;
1270
- }
1271
- };
1272
- var JAVA_IMPORT_TYPES = /* @__PURE__ */ new Set(["scoped_identifier", "scoped_absolute_identifier"]);
1273
- var JAVA_EXPORT_TYPES = /* @__PURE__ */ new Set([
1274
- "class_declaration",
1275
- "interface_declaration",
1276
- "enum_declaration",
1277
- "record_declaration"
1278
- ]);
1279
- var javaStrategy = {
1280
- extractImports(root) {
1281
- const imports = [];
1282
- for (const child of root.children) {
1283
- if (child.type !== "import_declaration") continue;
1284
- const scoped = child.children.find((c) => JAVA_IMPORT_TYPES.has(c.type));
1285
- if (scoped) imports.push(makeImport(child, scoped.text));
1286
- }
1287
- return imports;
1288
- },
1289
- extractExports(root, source) {
1290
- const exports = [];
1291
- const lines = source.split("\n");
1292
- for (const child of root.children) {
1293
- if (!JAVA_EXPORT_TYPES.has(child.type)) continue;
1294
- const line = lines[child.startPosition.row] ?? "";
1295
- if (!/\bpublic\b/.test(line)) continue;
1296
- const exp = makeNamedExport(child);
1297
- if (exp) exports.push(exp);
1298
- }
1299
- return exports;
614
+ function getExtensionsForLang(lang) {
615
+ switch (lang) {
616
+ case "typescript":
617
+ return [".ts", ".tsx"];
618
+ case "javascript":
619
+ return [".js", ".jsx", ".mjs", ".cjs"];
620
+ case "python":
621
+ return [".py"];
622
+ case "go":
623
+ return [".go"];
624
+ case "rust":
625
+ return [".rs"];
626
+ case "java":
627
+ return [".java"];
628
+ default:
629
+ return [".ts", ".tsx", ".js", ".jsx"];
1300
630
  }
1301
- };
1302
- var STRATEGIES = {
1303
- python: pythonStrategy,
1304
- go: goStrategy,
1305
- rust: rustStrategy,
1306
- java: javaStrategy
1307
- };
1308
- var TreeSitterParser = class {
1309
- name;
1310
- extensions;
1311
- lang;
1312
- strategy;
1313
- constructor(lang, extensions, strategy) {
1314
- this.name = lang;
1315
- this.lang = lang;
1316
- this.extensions = extensions;
1317
- this.strategy = strategy;
631
+ }
632
+ async function resolveImportPath(importSource, fromFile, _rootDir) {
633
+ if (!importSource.startsWith(".") && !importSource.startsWith("/")) {
634
+ return null;
1318
635
  }
1319
- async parseFile(path) {
1320
- const contentResult = await readFileContent(path);
1321
- if (!contentResult.ok) {
1322
- return Err(
1323
- createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
1324
- "Check that the file exists"
1325
- ])
1326
- );
1327
- }
1328
- try {
1329
- const parser = await getParser(this.lang);
1330
- const tree = parser.parse(contentResult.value);
1331
- return Ok({
1332
- type: "Program",
1333
- body: { tree, source: contentResult.value },
1334
- language: this.lang
1335
- });
1336
- } catch (e) {
1337
- const error = e;
1338
- return Err(
1339
- createParseError("SYNTAX_ERROR", `Failed to parse ${path}: ${error.message}`, { path }, [
1340
- "Check for syntax errors in the file"
1341
- ])
1342
- );
636
+ const fromDir = dirname(fromFile);
637
+ const resolved = resolve(fromDir, importSource);
638
+ const sourceExt = extname(resolved);
639
+ const fromLang = detectLangFromExt(extname(fromFile));
640
+ const fallbacks = JS_EXT_FALLBACKS[sourceExt];
641
+ if (fallbacks) {
642
+ const base = resolved.slice(0, -sourceExt.length);
643
+ for (const ext of fallbacks) {
644
+ const candidate = base + ext;
645
+ if (await fileExists(candidate)) return candidate.replace(/\\/g, "/");
1343
646
  }
1344
- }
1345
- extractImports(ast) {
1346
- const { tree, source } = ast.body;
1347
- return Ok(this.strategy.extractImports(tree.rootNode, source));
1348
- }
1349
- extractExports(ast) {
1350
- const { tree, source } = ast.body;
1351
- return Ok(this.strategy.extractExports(tree.rootNode, source));
1352
- }
1353
- async health() {
1354
- try {
1355
- await getParser(this.lang);
1356
- return Ok({ available: true, message: `tree-sitter ${this.lang} grammar loaded` });
1357
- } catch {
1358
- return Ok({ available: false, message: `tree-sitter ${this.lang} grammar not available` });
647
+ for (const indexExt of [".ts", ".tsx", ".jsx"]) {
648
+ const indexPath = resolve(base, "index" + indexExt);
649
+ if (await fileExists(indexPath)) return indexPath.replace(/\\/g, "/");
1359
650
  }
1360
651
  }
1361
- outline(filePath, ast) {
1362
- const { tree, source } = ast.body;
1363
- return extractOutlineFromTree(tree.rootNode, this.lang, source, filePath);
1364
- }
1365
- unfold(filePath, ast, symbolName) {
1366
- const outlineResult = this.outline(filePath, ast);
1367
- if (outlineResult.error || !outlineResult.symbols.length) return null;
1368
- const { source } = ast.body;
1369
- const match = findSymbolByName(outlineResult.symbols, symbolName);
1370
- if (!match) return null;
1371
- const lines = source.split("\n");
1372
- const content = lines.slice(match.line - 1, match.endLine).join("\n");
1373
- return {
1374
- file: filePath,
1375
- symbolName,
1376
- startLine: match.line,
1377
- endLine: match.endLine,
1378
- content,
1379
- language: this.lang,
1380
- fallback: false
1381
- };
1382
- }
1383
- };
1384
- function createTreeSitterParser(lang) {
1385
- const strategy = STRATEGIES[lang];
1386
- if (!strategy) return null;
1387
- const extensionMap = {
1388
- python: [".py"],
1389
- go: [".go"],
1390
- rust: [".rs"],
1391
- java: [".java"]
1392
- };
1393
- const extensions = extensionMap[lang];
1394
- if (!extensions) return null;
1395
- return new TreeSitterParser(lang, extensions, strategy);
1396
- }
1397
-
1398
- // src/shared/parsers/registry.ts
1399
- var ParserRegistry = class {
1400
- parsers = /* @__PURE__ */ new Map();
1401
- register(parser) {
1402
- this.parsers.set(parser.name, parser);
1403
- }
1404
- getByLanguage(lang) {
1405
- return this.parsers.get(lang) ?? null;
1406
- }
1407
- getForFile(filePath) {
1408
- const lang = detectLanguage(filePath);
1409
- if (!lang) return null;
1410
- return this.getByLanguage(lang);
1411
- }
1412
- getSupportedExtensions() {
1413
- return Object.keys(EXTENSION_MAP);
1414
- }
1415
- getSupportedLanguages() {
1416
- return Array.from(this.parsers.keys());
1417
- }
1418
- isSupportedExtension(ext) {
1419
- return ext in EXTENSION_MAP;
1420
- }
1421
- };
1422
- var defaultRegistry = null;
1423
- function getDefaultRegistry() {
1424
- if (defaultRegistry) return defaultRegistry;
1425
- const registry = new ParserRegistry();
1426
- const tsParser = new TypeScriptParser();
1427
- registry.register(tsParser);
1428
- registry.register({
1429
- name: "javascript",
1430
- extensions: [".js", ".jsx", ".mjs", ".cjs"],
1431
- parseFile: tsParser.parseFile.bind(tsParser),
1432
- extractImports: tsParser.extractImports.bind(tsParser),
1433
- extractExports: tsParser.extractExports.bind(tsParser),
1434
- health: tsParser.health.bind(tsParser)
1435
- });
1436
- const treeSitterLanguages = ["python", "go", "rust", "java"];
1437
- for (const lang of treeSitterLanguages) {
1438
- const parser = createTreeSitterParser(lang);
1439
- if (parser) {
1440
- registry.register(parser);
1441
- }
652
+ const hasKnownExt = Object.values(EXTENSION_BY_LANG).flat().some((e) => resolved.endsWith(e));
653
+ if (hasKnownExt) {
654
+ return resolved.replace(/\\/g, "/");
1442
655
  }
1443
- defaultRegistry = registry;
1444
- return registry;
656
+ const extensions = getExtensionsForLang(fromLang);
657
+ return (resolved + extensions[0]).replace(/\\/g, "/");
1445
658
  }
1446
-
1447
- // src/architecture/collectors/layer-violations.ts
1448
- function mapLayerViolations(layerViolations, rootDir, category) {
1449
- return layerViolations.map((v) => {
1450
- const relFile = relativePosix(rootDir, v.file);
1451
- const relImport = relativePosix(rootDir, v.imports);
1452
- const detail = `${v.fromLayer} -> ${v.toLayer}: ${relFile} imports ${relImport}`;
1453
- return {
1454
- id: violationId(relFile, category ?? "", detail),
1455
- file: relFile,
1456
- category,
1457
- detail,
1458
- severity: "error"
1459
- };
1460
- });
659
+ function getImportType(imp) {
660
+ if (imp.kind === "type") return "type-only";
661
+ return "static";
1461
662
  }
1462
- var LayerViolationCollector = class {
1463
- category = "layer-violations";
1464
- getRules(_config, _rootDir) {
1465
- const description = "No layer boundary violations allowed";
1466
- return [
1467
- {
1468
- id: constraintRuleId(this.category, "project", description),
1469
- category: this.category,
1470
- description,
1471
- scope: "project"
1472
- }
1473
- ];
1474
- }
1475
- async collect(_config, rootDir) {
1476
- const registry = getDefaultRegistry();
1477
- const parser = registry.getByLanguage("typescript") ?? registry.getByLanguage("javascript");
1478
- const result = await validateDependencies({
1479
- layers: [],
1480
- rootDir,
1481
- parser,
1482
- fallbackBehavior: "skip"
663
+ async function buildDependencyGraph(files, parser, graphDependencyData) {
664
+ if (graphDependencyData) {
665
+ return Ok({
666
+ nodes: graphDependencyData.nodes,
667
+ edges: graphDependencyData.edges
1483
668
  });
1484
- if (!result.ok) {
1485
- return [
1486
- {
1487
- category: this.category,
1488
- scope: "project",
1489
- value: 0,
1490
- violations: [],
1491
- metadata: { error: "Failed to validate dependencies" }
1492
- }
1493
- ];
1494
- }
1495
- const violations = mapLayerViolations(
1496
- result.value.violations.filter((v) => v.reason === "WRONG_LAYER"),
1497
- rootDir,
1498
- this.category
1499
- );
1500
- return [{ category: this.category, scope: "project", value: violations.length, violations }];
1501
669
  }
1502
- };
1503
-
1504
- // src/architecture/baseline-manager.ts
1505
- import { readFileSync, writeFileSync, renameSync, mkdirSync, existsSync } from "fs";
1506
- import { randomBytes } from "crypto";
1507
- import { join, dirname as dirname2 } from "path";
1508
- var ArchBaselineManager = class {
1509
- baselinesPath;
1510
- constructor(projectRoot, baselinePath) {
1511
- this.baselinesPath = baselinePath ? join(projectRoot, baselinePath) : join(projectRoot, ".harness", "arch", "baselines.json");
1512
- }
1513
- /**
1514
- * Snapshot the current metric results into an ArchBaseline.
1515
- * Aggregates multiple MetricResults for the same category by summing values
1516
- * and concatenating violation IDs.
1517
- */
1518
- capture(results, commitHash) {
1519
- const metrics = {};
1520
- for (const result of results) {
1521
- const existing = metrics[result.category];
1522
- if (existing) {
1523
- existing.value += result.value;
1524
- existing.violationIds.push(...result.violations.map((v) => v.id));
1525
- } else {
1526
- metrics[result.category] = {
1527
- value: result.value,
1528
- violationIds: result.violations.map((v) => v.id)
1529
- };
1530
- }
1531
- }
1532
- for (const baseline of Object.values(metrics)) {
1533
- baseline.violationIds = [...new Set(baseline.violationIds)];
670
+ const isLookup = "getForFile" in parser;
671
+ const nodes = files.map((f) => f.replace(/\\/g, "/"));
672
+ const edges = [];
673
+ for (const file of files) {
674
+ const normalizedFile = file.replace(/\\/g, "/");
675
+ const fileParser = isLookup ? parser.getForFile(file) : parser;
676
+ if (!fileParser) continue;
677
+ const parseResult = await fileParser.parseFile(file);
678
+ if (!parseResult.ok) {
679
+ continue;
1534
680
  }
1535
- return {
1536
- version: 1,
1537
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1538
- updatedFrom: commitHash,
1539
- metrics
1540
- };
1541
- }
1542
- /**
1543
- * Load the baselines file from disk.
1544
- * Returns null if the file does not exist, contains invalid JSON,
1545
- * or fails ArchBaselineSchema validation.
1546
- */
1547
- load() {
1548
- if (!existsSync(this.baselinesPath)) {
1549
- console.error(`Baseline file not found at: ${this.baselinesPath}`);
1550
- return null;
681
+ const importsResult = fileParser.extractImports(parseResult.value);
682
+ if (!importsResult.ok) {
683
+ continue;
1551
684
  }
1552
- try {
1553
- const raw = readFileSync(this.baselinesPath, "utf-8");
1554
- const data = JSON.parse(raw);
1555
- const parsed = ArchBaselineSchema.safeParse(data);
1556
- if (!parsed.success) {
1557
- console.error(
1558
- `Baseline validation failed for ${this.baselinesPath}:`,
1559
- parsed.error.format()
1560
- );
1561
- return null;
685
+ for (const imp of importsResult.value) {
686
+ const resolvedPath = await resolveImportPath(imp.source, file, "");
687
+ if (resolvedPath) {
688
+ edges.push({
689
+ from: normalizedFile,
690
+ to: resolvedPath,
691
+ importType: getImportType(imp),
692
+ line: imp.location.line
693
+ });
1562
694
  }
1563
- return parsed.data;
1564
- } catch (error) {
1565
- console.error(`Error loading baseline from ${this.baselinesPath}:`, error);
1566
- return null;
1567
695
  }
1568
696
  }
1569
- /**
1570
- * Refresh the on-disk baseline with new results.
1571
- *
1572
- * Categories present in `results` overwrite their on-disk entry; categories
1573
- * absent from `results` are preserved as-is. This prevents silent data loss
1574
- * when a collector returns no results (e.g. transient failure or a filtered
1575
- * run) and the regenerated file is committed (issue #268).
1576
- *
1577
- * Use this from the `--update-baseline` flow instead of `capture()` + `save()`.
1578
- */
1579
- update(results, commitHash) {
1580
- const fresh = this.capture(results, commitHash);
1581
- const existing = this.load();
1582
- if (existing) {
1583
- fresh.metrics = { ...existing.metrics, ...fresh.metrics };
697
+ return Ok({ nodes, edges });
698
+ }
699
+ function checkLayerViolations(graph, layers, rootDir) {
700
+ const violations = [];
701
+ for (const edge of graph.edges) {
702
+ const fromRelative = relativePosix(rootDir, edge.from);
703
+ const toRelative = relativePosix(rootDir, edge.to);
704
+ const fromLayer = resolveFileToLayer(fromRelative, layers);
705
+ const toLayer = resolveFileToLayer(toRelative, layers);
706
+ if (!fromLayer || !toLayer) continue;
707
+ if (fromLayer.name === toLayer.name) continue;
708
+ if (!fromLayer.allowedDependencies.includes(toLayer.name)) {
709
+ violations.push({
710
+ file: edge.from,
711
+ imports: edge.to,
712
+ fromLayer: fromLayer.name,
713
+ toLayer: toLayer.name,
714
+ reason: "WRONG_LAYER",
715
+ line: edge.line,
716
+ suggestion: `Move the dependency to an allowed layer (${fromLayer.allowedDependencies.join(", ") || "none"}) or update layer rules`
717
+ });
1584
718
  }
1585
- this.save(fresh);
1586
- return fresh;
1587
- }
1588
- /**
1589
- * Save an ArchBaseline to disk.
1590
- * Creates parent directories if they do not exist.
1591
- * Uses atomic write (write to temp file, then rename) to prevent corruption.
1592
- */
1593
- save(baseline) {
1594
- const dir = dirname2(this.baselinesPath);
1595
- if (!existsSync(dir)) {
1596
- mkdirSync(dir, { recursive: true });
719
+ }
720
+ return violations;
721
+ }
722
+ async function validateDependencies(config) {
723
+ const { layers, rootDir, parser, fallbackBehavior = "error", graphDependencyData } = config;
724
+ if (graphDependencyData) {
725
+ const graphResult2 = await buildDependencyGraph([], parser, graphDependencyData);
726
+ if (!graphResult2.ok) {
727
+ return Err(graphResult2.error);
1597
728
  }
1598
- const tmp = this.baselinesPath + "." + randomBytes(4).toString("hex") + ".tmp";
1599
- writeFileSync(tmp, JSON.stringify(baseline, null, 2));
1600
- renameSync(tmp, this.baselinesPath);
729
+ const violations2 = checkLayerViolations(graphResult2.value, layers, rootDir);
730
+ return Ok({
731
+ valid: violations2.length === 0,
732
+ violations: violations2,
733
+ graph: graphResult2.value
734
+ });
1601
735
  }
1602
- };
1603
-
1604
- // src/architecture/diff.ts
1605
- function aggregateByCategory(results) {
1606
- const map = /* @__PURE__ */ new Map();
1607
- for (const result of results) {
1608
- const existing = map.get(result.category);
1609
- if (existing) {
1610
- existing.value += result.value;
1611
- existing.violations.push(...result.violations);
1612
- } else {
1613
- map.set(result.category, {
1614
- value: result.value,
1615
- violations: [...result.violations]
736
+ const healthResult = await parser.health();
737
+ if (!healthResult.ok || !healthResult.value.available) {
738
+ if (fallbackBehavior === "skip") {
739
+ return Ok({
740
+ valid: true,
741
+ violations: [],
742
+ graph: { nodes: [], edges: [] },
743
+ skipped: true,
744
+ reason: "Parser unavailable"
1616
745
  });
1617
746
  }
1618
- }
1619
- return map;
1620
- }
1621
- function classifyViolations(violations, baselineViolationIds) {
1622
- const newViolations = [];
1623
- const preExisting = [];
1624
- for (const violation of violations) {
1625
- if (baselineViolationIds.has(violation.id)) {
1626
- preExisting.push(violation.id);
1627
- } else {
1628
- newViolations.push(violation);
747
+ if (fallbackBehavior === "warn") {
748
+ console.warn(`Parser ${parser.name} unavailable, skipping validation`);
749
+ return Ok({
750
+ valid: true,
751
+ violations: [],
752
+ graph: { nodes: [], edges: [] },
753
+ skipped: true,
754
+ reason: "Parser unavailable"
755
+ });
1629
756
  }
757
+ return Err(
758
+ createError(
759
+ "PARSER_UNAVAILABLE",
760
+ `Parser ${parser.name} is not available`,
761
+ { parser: parser.name },
762
+ ["Install required runtime", "Use different parser", 'Set fallbackBehavior: "skip"']
763
+ )
764
+ );
1630
765
  }
1631
- return { newViolations, preExisting };
1632
- }
1633
- function findResolvedViolations(baselineCategory, currentViolationIds) {
1634
- if (!baselineCategory) return [];
1635
- return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
1636
- }
1637
- function collectOrphanedBaselineViolations(baseline, visitedCategories) {
1638
- const resolved = [];
1639
- for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
1640
- if (!visitedCategories.has(category) && baselineCategory) {
1641
- resolved.push(...baselineCategory.violationIds);
766
+ const allFiles = [];
767
+ for (const layer of layers) {
768
+ for (const pattern of layer.patterns) {
769
+ const files = await findFiles(pattern, rootDir);
770
+ allFiles.push(...files);
1642
771
  }
1643
772
  }
1644
- return resolved;
1645
- }
1646
- function diffCategory(category, agg, baselineCategory, acc) {
1647
- const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
1648
- const baselineValue = baselineCategory?.value ?? 0;
1649
- const classified = classifyViolations(agg.violations, baselineViolationIds);
1650
- acc.newViolations.push(...classified.newViolations);
1651
- acc.preExisting.push(...classified.preExisting);
1652
- const currentViolationIds = new Set(agg.violations.map((v) => v.id));
1653
- acc.resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
1654
- if (baselineCategory && agg.value > baselineValue) {
1655
- acc.regressions.push({
1656
- category,
1657
- baselineValue,
1658
- currentValue: agg.value,
1659
- delta: agg.value - baselineValue
1660
- });
1661
- }
1662
- }
1663
- function diff(current, baseline) {
1664
- const aggregated = aggregateByCategory(current);
1665
- const acc = {
1666
- newViolations: [],
1667
- resolvedViolations: [],
1668
- preExisting: [],
1669
- regressions: []
1670
- };
1671
- const visitedCategories = /* @__PURE__ */ new Set();
1672
- for (const [category, agg] of aggregated) {
1673
- visitedCategories.add(category);
1674
- diffCategory(category, agg, baseline.metrics[category], acc);
773
+ const uniqueFiles = [...new Set(allFiles)];
774
+ const graphResult = await buildDependencyGraph(uniqueFiles, parser);
775
+ if (!graphResult.ok) {
776
+ return Err(graphResult.error);
1675
777
  }
1676
- acc.resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
1677
- return {
1678
- passed: acc.newViolations.length === 0 && acc.regressions.length === 0,
1679
- newViolations: acc.newViolations,
1680
- resolvedViolations: acc.resolvedViolations,
1681
- preExisting: acc.preExisting,
1682
- regressions: acc.regressions
1683
- };
778
+ const violations = checkLayerViolations(graphResult.value, layers, rootDir);
779
+ return Ok({
780
+ valid: violations.length === 0,
781
+ violations,
782
+ graph: graphResult.value
783
+ });
1684
784
  }
1685
785
 
1686
786
  // src/entropy/detectors/complexity.ts
@@ -1985,94 +1085,6 @@ async function detectComplexityViolations(snapshot, config, graphData) {
1985
1085
  });
1986
1086
  }
1987
1087
 
1988
- // src/architecture/collectors/complexity.ts
1989
- function buildSnapshot(files, rootDir) {
1990
- return {
1991
- files: files.map((f) => ({
1992
- path: f,
1993
- ast: { type: "Program", body: null, language: "typescript" },
1994
- imports: [],
1995
- exports: [],
1996
- internalSymbols: [],
1997
- jsDocComments: []
1998
- })),
1999
- dependencyGraph: { nodes: [], edges: [] },
2000
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
2001
- docs: [],
2002
- codeReferences: [],
2003
- entryPoints: [],
2004
- rootDir,
2005
- config: { rootDir, analyze: {} },
2006
- buildTime: 0
2007
- };
2008
- }
2009
- function resolveMaxComplexity(config) {
2010
- const threshold = config.thresholds.complexity;
2011
- return typeof threshold === "number" ? threshold : threshold?.max ?? 15;
2012
- }
2013
- function mapComplexityViolations(complexityViolations, rootDir, category) {
2014
- return complexityViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
2015
- const relFile = relativePosix(rootDir, v.file);
2016
- return {
2017
- id: violationId(relFile, category ?? "", `${v.metric}:${v.function}`),
2018
- file: relFile,
2019
- category,
2020
- detail: `${v.metric}=${v.value} in ${v.function} (threshold: ${v.threshold})`,
2021
- severity: v.severity
2022
- };
2023
- });
2024
- }
2025
- var ComplexityCollector = class {
2026
- category = "complexity";
2027
- getRules(_config, _rootDir) {
2028
- const description = "Cyclomatic complexity must stay within thresholds";
2029
- return [
2030
- {
2031
- id: constraintRuleId(this.category, "project", description),
2032
- category: this.category,
2033
- description,
2034
- scope: "project"
2035
- }
2036
- ];
2037
- }
2038
- async collect(_config, rootDir) {
2039
- const files = await findFiles("**/*.ts", rootDir);
2040
- const snapshot = buildSnapshot(files, rootDir);
2041
- const maxComplexity = resolveMaxComplexity(_config);
2042
- const complexityConfig = {
2043
- thresholds: {
2044
- cyclomaticComplexity: { error: maxComplexity, warn: Math.floor(maxComplexity * 0.7) }
2045
- }
2046
- };
2047
- const result = await detectComplexityViolations(snapshot, complexityConfig);
2048
- if (!result.ok) {
2049
- return [
2050
- {
2051
- category: this.category,
2052
- scope: "project",
2053
- value: 0,
2054
- violations: [],
2055
- metadata: { error: "Failed to detect complexity violations" }
2056
- }
2057
- ];
2058
- }
2059
- const { violations: complexityViolations, stats } = result.value;
2060
- const violations = mapComplexityViolations(complexityViolations, rootDir, this.category);
2061
- return [
2062
- {
2063
- category: this.category,
2064
- scope: "project",
2065
- value: violations.length,
2066
- violations,
2067
- metadata: {
2068
- filesAnalyzed: stats.filesAnalyzed,
2069
- functionsAnalyzed: stats.functionsAnalyzed
2070
- }
2071
- }
2072
- ];
2073
- }
2074
- };
2075
-
2076
1088
  // src/entropy/detectors/coupling.ts
2077
1089
  var DEFAULT_THRESHOLDS2 = {
2078
1090
  fanOut: { warn: 15 },
@@ -2240,633 +1252,368 @@ async function detectCouplingViolations(snapshot, config, graphData) {
2240
1252
  });
2241
1253
  }
2242
1254
 
2243
- // src/architecture/collectors/coupling.ts
2244
- function buildCouplingSnapshot(files, rootDir) {
1255
+ // src/shared/parsers/tree-sitter.ts
1256
+ function findSymbolByName(symbols, name) {
1257
+ for (const sym of symbols) {
1258
+ if (sym.name === name) return { line: sym.line, endLine: sym.endLine };
1259
+ if (sym.children) {
1260
+ const found = findSymbolByName(sym.children, name);
1261
+ if (found) return found;
1262
+ }
1263
+ }
1264
+ return null;
1265
+ }
1266
+ function makeLocation2(node) {
2245
1267
  return {
2246
- files: files.map((f) => ({
2247
- path: f,
2248
- ast: { type: "Program", body: null, language: "typescript" },
2249
- imports: [],
2250
- exports: [],
2251
- internalSymbols: [],
2252
- jsDocComments: []
2253
- })),
2254
- dependencyGraph: { nodes: [], edges: [] },
2255
- exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
2256
- docs: [],
2257
- codeReferences: [],
2258
- entryPoints: [],
2259
- rootDir,
2260
- config: { rootDir, analyze: {} },
2261
- buildTime: 0
1268
+ file: "",
1269
+ line: node.startPosition.row + 1,
1270
+ column: node.startPosition.column
2262
1271
  };
2263
1272
  }
2264
- function mapCouplingViolations(couplingViolations, rootDir, category) {
2265
- return couplingViolations.filter((v) => v.severity === "error" || v.severity === "warning").map((v) => {
2266
- const relFile = relativePosix(rootDir, v.file);
2267
- return {
2268
- id: violationId(relFile, category ?? "", v.metric),
2269
- file: relFile,
2270
- category,
2271
- detail: `${v.metric}=${v.value} (threshold: ${v.threshold})`,
2272
- severity: v.severity
2273
- };
2274
- });
1273
+ function makeNamedExport(node) {
1274
+ const name = node.childForFieldName("name");
1275
+ if (!name) return null;
1276
+ return { name: name.text, type: "named", location: makeLocation2(node), isReExport: false };
2275
1277
  }
2276
- var CouplingCollector = class {
2277
- category = "coupling";
2278
- getRules(_config, _rootDir) {
2279
- const description = "Coupling metrics must stay within thresholds";
2280
- return [
2281
- {
2282
- id: constraintRuleId(this.category, "project", description),
2283
- category: this.category,
2284
- description,
2285
- scope: "project"
2286
- }
2287
- ];
2288
- }
2289
- async collect(_config, rootDir) {
2290
- const files = await findFiles("**/*.ts", rootDir);
2291
- const snapshot = buildCouplingSnapshot(files, rootDir);
2292
- const result = await detectCouplingViolations(snapshot);
2293
- if (!result.ok) {
2294
- return [
2295
- {
2296
- category: this.category,
2297
- scope: "project",
2298
- value: 0,
2299
- violations: [],
2300
- metadata: { error: "Failed to detect coupling violations" }
2301
- }
2302
- ];
1278
+ function makeImport(node, source) {
1279
+ return { source, specifiers: [], location: makeLocation2(node), kind: "value" };
1280
+ }
1281
+ function extractPythonImport(child) {
1282
+ const name = child.childForFieldName("name");
1283
+ if (!name) return null;
1284
+ return makeImport(child, name.text);
1285
+ }
1286
+ function extractPythonFromImport(child) {
1287
+ const moduleName = child.childForFieldName("module_name");
1288
+ const specifiers = [];
1289
+ for (const c of child.children) {
1290
+ if (c.type === "dotted_name" && c !== moduleName) specifiers.push(c.text);
1291
+ if (c.type === "aliased_import") {
1292
+ const n = c.childForFieldName("name");
1293
+ if (n) specifiers.push(n.text);
1294
+ }
1295
+ }
1296
+ return {
1297
+ source: moduleName?.text ?? "",
1298
+ specifiers,
1299
+ location: makeLocation2(child),
1300
+ kind: "value"
1301
+ };
1302
+ }
1303
+ function extractPythonExport(child) {
1304
+ if (child.type === "function_definition" || child.type === "class_definition") {
1305
+ return makeNamedExport(child);
1306
+ }
1307
+ if (child.type === "assignment") {
1308
+ const left = child.childForFieldName("left") ?? child.children[0];
1309
+ if (left && !left.text.startsWith("_")) {
1310
+ return { name: left.text, type: "named", location: makeLocation2(child), isReExport: false };
2303
1311
  }
2304
- const { violations: couplingViolations, stats } = result.value;
2305
- const violations = mapCouplingViolations(couplingViolations, rootDir, this.category);
2306
- return [
2307
- {
2308
- category: this.category,
2309
- scope: "project",
2310
- value: violations.length,
2311
- violations,
2312
- metadata: { filesAnalyzed: stats.filesAnalyzed }
1312
+ }
1313
+ return null;
1314
+ }
1315
+ var pythonStrategy = {
1316
+ extractImports(root) {
1317
+ const imports = [];
1318
+ for (const child of root.children) {
1319
+ if (child.type === "import_statement") {
1320
+ const imp = extractPythonImport(child);
1321
+ if (imp) imports.push(imp);
1322
+ } else if (child.type === "import_from_statement") {
1323
+ imports.push(extractPythonFromImport(child));
2313
1324
  }
2314
- ];
1325
+ }
1326
+ return imports;
1327
+ },
1328
+ extractExports(root) {
1329
+ const exports = [];
1330
+ for (const child of root.children) {
1331
+ const exp = extractPythonExport(child);
1332
+ if (exp) exports.push(exp);
1333
+ }
1334
+ return exports;
2315
1335
  }
2316
1336
  };
2317
-
2318
- // src/architecture/collectors/forbidden-imports.ts
2319
- function mapForbiddenImportViolations(forbidden, rootDir, category) {
2320
- return forbidden.map((v) => {
2321
- const relFile = relativePosix(rootDir, v.file);
2322
- const relImport = relativePosix(rootDir, v.imports);
2323
- const detail = `forbidden import: ${relFile} -> ${relImport}`;
2324
- return {
2325
- id: violationId(relFile, category ?? "", detail),
2326
- file: relFile,
2327
- category,
2328
- detail,
2329
- severity: "error"
2330
- };
2331
- });
1337
+ function extractGoImportPath(spec) {
1338
+ const pathNode = spec.childForFieldName("path") ?? spec.children.find((c) => c.type === "interpreted_string_literal");
1339
+ return pathNode ? pathNode.text.replace(/"/g, "") : null;
1340
+ }
1341
+ function extractGoImportsFromDecl(child) {
1342
+ const imports = [];
1343
+ for (const spec of child.children.filter((c) => c.type === "import_spec")) {
1344
+ const source = extractGoImportPath(spec);
1345
+ if (source) imports.push(makeImport(child, source));
1346
+ }
1347
+ const specList = child.children.find((c) => c.type === "import_spec_list");
1348
+ if (specList) {
1349
+ for (const spec of specList.children.filter((c) => c.type === "import_spec")) {
1350
+ const source = extractGoImportPath(spec);
1351
+ if (source) imports.push(makeImport(spec, source));
1352
+ }
1353
+ }
1354
+ return imports;
1355
+ }
1356
+ function isGoExported(name) {
1357
+ return /^[A-Z]/.test(name);
2332
1358
  }
2333
- var ForbiddenImportCollector = class {
2334
- category = "forbidden-imports";
2335
- getRules(_config, _rootDir) {
2336
- const description = "No forbidden imports allowed";
2337
- return [
2338
- {
2339
- id: constraintRuleId(this.category, "project", description),
2340
- category: this.category,
2341
- description,
2342
- scope: "project"
1359
+ function extractGoExport(child) {
1360
+ if (child.type === "function_declaration" || child.type === "method_declaration") {
1361
+ const name = child.childForFieldName("name");
1362
+ if (name && isGoExported(name.text)) return makeNamedExport(child);
1363
+ }
1364
+ if (child.type === "type_declaration") {
1365
+ const typeSpec = child.children.find((c) => c.type === "type_spec");
1366
+ if (typeSpec) {
1367
+ const name = typeSpec.childForFieldName("name");
1368
+ if (name && isGoExported(name.text)) {
1369
+ return { name: name.text, type: "named", location: makeLocation2(child), isReExport: false };
2343
1370
  }
2344
- ];
2345
- }
2346
- async collect(_config, rootDir) {
2347
- const registry = getDefaultRegistry();
2348
- const parser = registry.getByLanguage("typescript") ?? registry.getByLanguage("javascript");
2349
- const result = await validateDependencies({
2350
- layers: [],
2351
- rootDir,
2352
- parser,
2353
- fallbackBehavior: "skip"
2354
- });
2355
- if (!result.ok) {
2356
- return [
2357
- {
2358
- category: this.category,
2359
- scope: "project",
2360
- value: 0,
2361
- violations: [],
2362
- metadata: { error: "Failed to validate dependencies" }
1371
+ }
1372
+ }
1373
+ return null;
1374
+ }
1375
+ var goStrategy = {
1376
+ extractImports(root) {
1377
+ const imports = [];
1378
+ for (const child of root.children) {
1379
+ if (child.type === "import_declaration") imports.push(...extractGoImportsFromDecl(child));
1380
+ }
1381
+ return imports;
1382
+ },
1383
+ extractExports(root) {
1384
+ const exports = [];
1385
+ for (const child of root.children) {
1386
+ const exp = extractGoExport(child);
1387
+ if (exp) exports.push(exp);
1388
+ }
1389
+ return exports;
1390
+ }
1391
+ };
1392
+ var RUST_USE_ARG_TYPES = /* @__PURE__ */ new Set([
1393
+ "scoped_identifier",
1394
+ "use_wildcard",
1395
+ "scoped_use_list",
1396
+ "identifier"
1397
+ ]);
1398
+ var RUST_PUB_ITEM_TYPES = /* @__PURE__ */ new Set([
1399
+ "function_item",
1400
+ "struct_item",
1401
+ "enum_item",
1402
+ "trait_item",
1403
+ "type_item",
1404
+ "const_item",
1405
+ "static_item"
1406
+ ]);
1407
+ var rustStrategy = {
1408
+ extractImports(root) {
1409
+ const imports = [];
1410
+ for (const child of root.children) {
1411
+ if (child.type !== "use_declaration") continue;
1412
+ const arg = child.childForFieldName("argument") ?? child.children.find((c) => RUST_USE_ARG_TYPES.has(c.type));
1413
+ if (arg) imports.push(makeImport(child, arg.text));
1414
+ }
1415
+ return imports;
1416
+ },
1417
+ extractExports(root, source) {
1418
+ const exports = [];
1419
+ const lines = source.split("\n");
1420
+ for (const child of root.children) {
1421
+ const line = lines[child.startPosition.row] ?? "";
1422
+ if (!/^\s*pub\b/.test(line)) continue;
1423
+ if (RUST_PUB_ITEM_TYPES.has(child.type)) {
1424
+ const exp = makeNamedExport(child);
1425
+ if (exp) exports.push(exp);
1426
+ } else if (child.type === "mod_item") {
1427
+ const name = child.childForFieldName("name");
1428
+ if (name) {
1429
+ exports.push({
1430
+ name: name.text,
1431
+ type: "namespace",
1432
+ location: makeLocation2(child),
1433
+ isReExport: false
1434
+ });
2363
1435
  }
2364
- ];
1436
+ }
2365
1437
  }
2366
- const violations = mapForbiddenImportViolations(
2367
- result.value.violations.filter((v) => v.reason === "FORBIDDEN_IMPORT"),
2368
- rootDir,
2369
- this.category
2370
- );
2371
- return [{ category: this.category, scope: "project", value: violations.length, violations }];
1438
+ return exports;
2372
1439
  }
2373
1440
  };
2374
-
2375
- // src/architecture/collectors/module-size.ts
2376
- import { readFile as readFile3, readdir } from "fs/promises";
2377
- import { join as join2 } from "path";
2378
- import { DEFAULT_SKIP_DIRS } from "@harness-engineering/graph";
2379
- function isSkippedEntry(name) {
2380
- return name.startsWith(".") || DEFAULT_SKIP_DIRS.has(name);
2381
- }
2382
- function isTsSourceFile(name) {
2383
- if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
2384
- if (name.endsWith(".test.ts") || name.endsWith(".test.tsx") || name.endsWith(".spec.ts"))
2385
- return false;
2386
- return true;
2387
- }
2388
- async function countLoc(filePath) {
2389
- try {
2390
- const content = await readFile3(filePath, "utf-8");
2391
- return content.split("\n").filter((line) => line.trim().length > 0).length;
2392
- } catch {
2393
- return 0;
2394
- }
2395
- }
2396
- async function buildModuleStats(rootDir, dir, tsFiles) {
2397
- let totalLoc = 0;
2398
- for (const f of tsFiles) {
2399
- totalLoc += await countLoc(f);
2400
- }
2401
- return {
2402
- modulePath: relativePosix(rootDir, dir),
2403
- fileCount: tsFiles.length,
2404
- totalLoc,
2405
- files: tsFiles.map((f) => relativePosix(rootDir, f))
2406
- };
2407
- }
2408
- async function scanDir(rootDir, dir, modules) {
2409
- let entries;
2410
- try {
2411
- entries = await readdir(dir, { withFileTypes: true });
2412
- } catch {
2413
- return;
2414
- }
2415
- const tsFiles = [];
2416
- const subdirs = [];
2417
- for (const entry of entries) {
2418
- if (isSkippedEntry(entry.name)) continue;
2419
- const fullPath = join2(dir, entry.name);
2420
- if (entry.isDirectory()) {
2421
- subdirs.push(fullPath);
2422
- continue;
1441
+ var JAVA_IMPORT_TYPES = /* @__PURE__ */ new Set(["scoped_identifier", "scoped_absolute_identifier"]);
1442
+ var JAVA_EXPORT_TYPES = /* @__PURE__ */ new Set([
1443
+ "class_declaration",
1444
+ "interface_declaration",
1445
+ "enum_declaration",
1446
+ "record_declaration"
1447
+ ]);
1448
+ var javaStrategy = {
1449
+ extractImports(root) {
1450
+ const imports = [];
1451
+ for (const child of root.children) {
1452
+ if (child.type !== "import_declaration") continue;
1453
+ const scoped = child.children.find((c) => JAVA_IMPORT_TYPES.has(c.type));
1454
+ if (scoped) imports.push(makeImport(child, scoped.text));
2423
1455
  }
2424
- if (entry.isFile() && isTsSourceFile(entry.name)) {
2425
- tsFiles.push(fullPath);
1456
+ return imports;
1457
+ },
1458
+ extractExports(root, source) {
1459
+ const exports = [];
1460
+ const lines = source.split("\n");
1461
+ for (const child of root.children) {
1462
+ if (!JAVA_EXPORT_TYPES.has(child.type)) continue;
1463
+ const line = lines[child.startPosition.row] ?? "";
1464
+ if (!/\bpublic\b/.test(line)) continue;
1465
+ const exp = makeNamedExport(child);
1466
+ if (exp) exports.push(exp);
2426
1467
  }
1468
+ return exports;
2427
1469
  }
2428
- if (tsFiles.length > 0) {
2429
- modules.push(await buildModuleStats(rootDir, dir, tsFiles));
2430
- }
2431
- for (const sub of subdirs) {
2432
- await scanDir(rootDir, sub, modules);
2433
- }
2434
- }
2435
- async function discoverModules(rootDir) {
2436
- const modules = [];
2437
- await scanDir(rootDir, rootDir, modules);
2438
- return modules;
2439
- }
2440
- function extractThresholds(config) {
2441
- const thresholds = config.thresholds["module-size"];
2442
- let maxLoc = Infinity;
2443
- let maxFiles = Infinity;
2444
- if (typeof thresholds === "object" && thresholds !== null) {
2445
- const t = thresholds;
2446
- if (t.maxLoc !== void 0) maxLoc = t.maxLoc;
2447
- if (t.maxFiles !== void 0) maxFiles = t.maxFiles;
2448
- }
2449
- return { maxLoc, maxFiles };
2450
- }
2451
- var ModuleSizeCollector = class {
2452
- category = "module-size";
2453
- getRules(config, _rootDir) {
2454
- const { maxLoc, maxFiles } = extractThresholds(config);
2455
- const rules = [];
2456
- if (maxLoc < Infinity) {
2457
- const desc = `Module LOC must not exceed ${maxLoc}`;
2458
- rules.push({
2459
- id: constraintRuleId(this.category, "project", desc),
2460
- category: this.category,
2461
- description: desc,
2462
- scope: "project"
2463
- });
2464
- }
2465
- if (maxFiles < Infinity) {
2466
- const desc = `Module file count must not exceed ${maxFiles}`;
2467
- rules.push({
2468
- id: constraintRuleId(this.category, "project", desc),
2469
- category: this.category,
2470
- description: desc,
2471
- scope: "project"
2472
- });
1470
+ };
1471
+ var STRATEGIES = {
1472
+ python: pythonStrategy,
1473
+ go: goStrategy,
1474
+ rust: rustStrategy,
1475
+ java: javaStrategy
1476
+ };
1477
+ var TreeSitterParser = class {
1478
+ name;
1479
+ extensions;
1480
+ lang;
1481
+ strategy;
1482
+ constructor(lang, extensions, strategy) {
1483
+ this.name = lang;
1484
+ this.lang = lang;
1485
+ this.extensions = extensions;
1486
+ this.strategy = strategy;
1487
+ }
1488
+ async parseFile(path) {
1489
+ const contentResult = await readFileContent(path);
1490
+ if (!contentResult.ok) {
1491
+ return Err(
1492
+ createParseError("NOT_FOUND", `File not found: ${path}`, { path }, [
1493
+ "Check that the file exists"
1494
+ ])
1495
+ );
2473
1496
  }
2474
- if (rules.length === 0) {
2475
- const desc = "Module size must stay within thresholds";
2476
- rules.push({
2477
- id: constraintRuleId(this.category, "project", desc),
2478
- category: this.category,
2479
- description: desc,
2480
- scope: "project"
1497
+ try {
1498
+ const parser = await getParser(this.lang);
1499
+ const tree = parser.parse(contentResult.value);
1500
+ return Ok({
1501
+ type: "Program",
1502
+ body: { tree, source: contentResult.value },
1503
+ language: this.lang
2481
1504
  });
1505
+ } catch (e) {
1506
+ const error = e;
1507
+ return Err(
1508
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path}: ${error.message}`, { path }, [
1509
+ "Check for syntax errors in the file"
1510
+ ])
1511
+ );
2482
1512
  }
2483
- return rules;
2484
- }
2485
- async collect(config, rootDir) {
2486
- const modules = await discoverModules(rootDir);
2487
- const { maxLoc, maxFiles } = extractThresholds(config);
2488
- return modules.map((mod) => {
2489
- const violations = [];
2490
- if (mod.totalLoc > maxLoc) {
2491
- violations.push({
2492
- id: violationId(mod.modulePath, this.category, "totalLoc-exceeded"),
2493
- file: mod.modulePath,
2494
- detail: `Module has ${mod.totalLoc} lines of code (threshold: ${maxLoc})`,
2495
- severity: "warning"
2496
- });
2497
- }
2498
- if (mod.fileCount > maxFiles) {
2499
- violations.push({
2500
- id: violationId(mod.modulePath, this.category, "fileCount-exceeded"),
2501
- file: mod.modulePath,
2502
- detail: `Module has ${mod.fileCount} files (threshold: ${maxFiles})`,
2503
- severity: "warning"
2504
- });
2505
- }
2506
- return {
2507
- category: this.category,
2508
- scope: mod.modulePath,
2509
- value: mod.totalLoc,
2510
- violations,
2511
- metadata: { fileCount: mod.fileCount, totalLoc: mod.totalLoc }
2512
- };
2513
- });
2514
1513
  }
2515
- };
2516
-
2517
- // src/architecture/collectors/dep-depth.ts
2518
- import { readFile as readFile4, readdir as readdir2 } from "fs/promises";
2519
- import { join as join3, dirname as dirname3, resolve as resolve2 } from "path";
2520
- import { DEFAULT_SKIP_DIRS as DEFAULT_SKIP_DIRS2 } from "@harness-engineering/graph";
2521
- function extractImportSources(content, filePath) {
2522
- const importRegex = /(?:import|export)\s+.*?from\s+['"](\.[^'"]+)['"]/g;
2523
- const dynamicRegex = /import\s*\(\s*['"](\.[^'"]+)['"]\s*\)/g;
2524
- const sources = [];
2525
- const dir = dirname3(filePath);
2526
- for (const regex of [importRegex, dynamicRegex]) {
2527
- let match;
2528
- while ((match = regex.exec(content)) !== null) {
2529
- let resolved = resolve2(dir, match[1]);
2530
- if (!resolved.endsWith(".ts") && !resolved.endsWith(".tsx")) {
2531
- resolved += ".ts";
2532
- }
2533
- sources.push(resolved);
2534
- }
1514
+ extractImports(ast) {
1515
+ const { tree, source } = ast.body;
1516
+ return Ok(this.strategy.extractImports(tree.rootNode, source));
2535
1517
  }
2536
- return sources;
2537
- }
2538
- function isSkippedEntry2(name) {
2539
- return name.startsWith(".") || DEFAULT_SKIP_DIRS2.has(name);
2540
- }
2541
- function isTsSourceFile2(name) {
2542
- if (!name.endsWith(".ts") && !name.endsWith(".tsx")) return false;
2543
- return !name.endsWith(".test.ts") && !name.endsWith(".test.tsx") && !name.endsWith(".spec.ts");
2544
- }
2545
- async function scanDir2(d, results) {
2546
- let entries;
2547
- try {
2548
- entries = await readdir2(d, { withFileTypes: true });
2549
- } catch {
2550
- return;
2551
- }
2552
- for (const entry of entries) {
2553
- if (isSkippedEntry2(entry.name)) continue;
2554
- const fullPath = join3(d, entry.name);
2555
- if (entry.isDirectory()) {
2556
- await scanDir2(fullPath, results);
2557
- } else if (entry.isFile() && isTsSourceFile2(entry.name)) {
2558
- results.push(fullPath);
1518
+ extractExports(ast) {
1519
+ const { tree, source } = ast.body;
1520
+ return Ok(this.strategy.extractExports(tree.rootNode, source));
1521
+ }
1522
+ async health() {
1523
+ try {
1524
+ await getParser(this.lang);
1525
+ return Ok({ available: true, message: `tree-sitter ${this.lang} grammar loaded` });
1526
+ } catch {
1527
+ return Ok({ available: false, message: `tree-sitter ${this.lang} grammar not available` });
2559
1528
  }
2560
1529
  }
2561
- }
2562
- async function collectTsFiles(dir) {
2563
- const results = [];
2564
- await scanDir2(dir, results);
2565
- return results;
2566
- }
2567
- function computeLongestChain(file, graph, visited, memo) {
2568
- if (memo.has(file)) return memo.get(file);
2569
- if (visited.has(file)) return 0;
2570
- visited.add(file);
2571
- const deps = graph.get(file) || [];
2572
- let maxDepth = 0;
2573
- for (const dep of deps) {
2574
- const depth = 1 + computeLongestChain(dep, graph, visited, memo);
2575
- if (depth > maxDepth) maxDepth = depth;
1530
+ outline(filePath, ast) {
1531
+ const { tree, source } = ast.body;
1532
+ return extractOutlineFromTree(tree.rootNode, this.lang, source, filePath);
2576
1533
  }
2577
- visited.delete(file);
2578
- memo.set(file, maxDepth);
2579
- return maxDepth;
2580
- }
2581
- var DepDepthCollector = class {
2582
- category = "dependency-depth";
2583
- getRules(config, _rootDir) {
2584
- const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : null;
2585
- const desc = threshold !== null ? `Dependency chain depth must not exceed ${threshold}` : "Dependency chain depth must stay within thresholds";
2586
- return [
2587
- {
2588
- id: constraintRuleId(this.category, "project", desc),
2589
- category: this.category,
2590
- description: desc,
2591
- scope: "project"
2592
- }
2593
- ];
2594
- }
2595
- async buildImportGraph(allFiles) {
2596
- const graph = /* @__PURE__ */ new Map();
2597
- const fileSet = new Set(allFiles);
2598
- for (const file of allFiles) {
2599
- try {
2600
- const content = await readFile4(file, "utf-8");
2601
- graph.set(
2602
- file,
2603
- extractImportSources(content, file).filter((imp) => fileSet.has(imp))
2604
- );
2605
- } catch {
2606
- graph.set(file, []);
2607
- }
2608
- }
2609
- return graph;
2610
- }
2611
- buildModuleMap(allFiles, rootDir) {
2612
- const moduleMap = /* @__PURE__ */ new Map();
2613
- for (const file of allFiles) {
2614
- const relDir = relativePosix(rootDir, dirname3(file));
2615
- if (!moduleMap.has(relDir)) moduleMap.set(relDir, []);
2616
- moduleMap.get(relDir).push(file);
2617
- }
2618
- return moduleMap;
2619
- }
2620
- async collect(config, rootDir) {
2621
- const allFiles = await collectTsFiles(rootDir);
2622
- const graph = await this.buildImportGraph(allFiles);
2623
- const moduleMap = this.buildModuleMap(allFiles, rootDir);
2624
- const memo = /* @__PURE__ */ new Map();
2625
- const threshold = typeof config.thresholds["dependency-depth"] === "number" ? config.thresholds["dependency-depth"] : Infinity;
2626
- const results = [];
2627
- for (const [modulePath, files] of moduleMap) {
2628
- const longestChain = files.reduce((max, file) => {
2629
- return Math.max(max, computeLongestChain(file, graph, /* @__PURE__ */ new Set(), memo));
2630
- }, 0);
2631
- const violations = [];
2632
- if (longestChain > threshold) {
2633
- violations.push({
2634
- id: violationId(modulePath, this.category, "depth-exceeded"),
2635
- file: modulePath,
2636
- detail: `Import chain depth is ${longestChain} (threshold: ${threshold})`,
2637
- severity: "warning"
2638
- });
2639
- }
2640
- results.push({
2641
- category: this.category,
2642
- scope: modulePath,
2643
- value: longestChain,
2644
- violations,
2645
- metadata: { longestChain }
2646
- });
2647
- }
2648
- return results;
1534
+ unfold(filePath, ast, symbolName) {
1535
+ const outlineResult = this.outline(filePath, ast);
1536
+ if (outlineResult.error || !outlineResult.symbols.length) return null;
1537
+ const { source } = ast.body;
1538
+ const match = findSymbolByName(outlineResult.symbols, symbolName);
1539
+ if (!match) return null;
1540
+ const lines = source.split("\n");
1541
+ const content = lines.slice(match.line - 1, match.endLine).join("\n");
1542
+ return {
1543
+ file: filePath,
1544
+ symbolName,
1545
+ startLine: match.line,
1546
+ endLine: match.endLine,
1547
+ content,
1548
+ language: this.lang,
1549
+ fallback: false
1550
+ };
2649
1551
  }
2650
1552
  };
2651
-
2652
- // src/architecture/collectors/index.ts
2653
- var defaultCollectors = [
2654
- new CircularDepsCollector(),
2655
- new LayerViolationCollector(),
2656
- new ComplexityCollector(),
2657
- new CouplingCollector(),
2658
- new ForbiddenImportCollector(),
2659
- new ModuleSizeCollector(),
2660
- new DepDepthCollector()
2661
- ];
2662
- async function runAll(config, rootDir, collectors = defaultCollectors) {
2663
- const results = await Promise.allSettled(collectors.map((c) => c.collect(config, rootDir)));
2664
- const allResults = [];
2665
- for (let i = 0; i < results.length; i++) {
2666
- const result = results[i];
2667
- if (result.status === "fulfilled") {
2668
- allResults.push(...result.value);
2669
- } else {
2670
- allResults.push({
2671
- category: collectors[i].category,
2672
- scope: "project",
2673
- value: 0,
2674
- violations: [],
2675
- metadata: { error: String(result.reason) }
2676
- });
2677
- }
2678
- }
2679
- return allResults;
2680
- }
2681
-
2682
- // src/architecture/matchers.ts
2683
- function architecture(options) {
2684
- return {
2685
- kind: "arch-handle",
2686
- scope: "project",
2687
- rootDir: options?.rootDir ?? process.cwd(),
2688
- config: options?.config
2689
- };
2690
- }
2691
- function archModule(modulePath, options) {
2692
- return {
2693
- kind: "arch-handle",
2694
- scope: modulePath,
2695
- rootDir: options?.rootDir ?? process.cwd(),
2696
- config: options?.config
1553
+ function createTreeSitterParser(lang) {
1554
+ const strategy = STRATEGIES[lang];
1555
+ if (!strategy) return null;
1556
+ const extensionMap = {
1557
+ python: [".py"],
1558
+ go: [".go"],
1559
+ rust: [".rs"],
1560
+ java: [".java"]
2697
1561
  };
1562
+ const extensions = extensionMap[lang];
1563
+ if (!extensions) return null;
1564
+ return new TreeSitterParser(lang, extensions, strategy);
2698
1565
  }
2699
- function resolveConfig(handle) {
2700
- return ArchConfigSchema.parse(handle.config ?? {});
2701
- }
2702
- async function collectCategory(handle, collector) {
2703
- if ("_mockResults" in handle && handle._mockResults) {
2704
- return handle._mockResults;
1566
+
1567
+ // src/shared/parsers/registry.ts
1568
+ var ParserRegistry = class {
1569
+ parsers = /* @__PURE__ */ new Map();
1570
+ register(parser) {
1571
+ this.parsers.set(parser.name, parser);
2705
1572
  }
2706
- const config = resolveConfig(handle);
2707
- return collector.collect(config, handle.rootDir);
2708
- }
2709
- function formatViolationList(violations, limit = 10) {
2710
- const lines = violations.slice(0, limit).map((v) => ` - ${v.file}: ${v.detail}`);
2711
- if (violations.length > limit) {
2712
- lines.push(` ... and ${violations.length - limit} more`);
1573
+ getByLanguage(lang) {
1574
+ return this.parsers.get(lang) ?? null;
2713
1575
  }
2714
- return lines.join("\n");
2715
- }
2716
- async function toHaveNoCircularDeps(received) {
2717
- const results = await collectCategory(received, new CircularDepsCollector());
2718
- const violations = results.flatMap((r) => r.violations);
2719
- const pass = violations.length === 0;
2720
- return {
2721
- pass,
2722
- message: () => pass ? "Expected circular dependencies but found none" : `Found ${violations.length} circular dependenc${violations.length === 1 ? "y" : "ies"}:
2723
- ${formatViolationList(violations)}`
2724
- };
2725
- }
2726
- async function toHaveNoLayerViolations(received) {
2727
- const results = await collectCategory(received, new LayerViolationCollector());
2728
- const violations = results.flatMap((r) => r.violations);
2729
- const pass = violations.length === 0;
2730
- return {
2731
- pass,
2732
- message: () => pass ? "Expected layer violations but found none" : `Found ${violations.length} layer violation${violations.length === 1 ? "" : "s"}:
2733
- ${formatViolationList(violations)}`
2734
- };
2735
- }
2736
- async function toMatchBaseline(received, options) {
2737
- let diffResult;
2738
- if ("_mockDiff" in received && received._mockDiff) {
2739
- diffResult = received._mockDiff;
2740
- } else {
2741
- const config = resolveConfig(received);
2742
- const results = await runAll(config, received.rootDir);
2743
- const manager = new ArchBaselineManager(received.rootDir, config.baselinePath);
2744
- const baseline = manager.load();
2745
- if (!baseline) {
2746
- return {
2747
- pass: false,
2748
- message: () => "No baseline found. Run `harness check-arch --update-baseline` to create one."
2749
- };
2750
- }
2751
- diffResult = diff(results, baseline);
1576
+ getForFile(filePath) {
1577
+ const lang = detectLanguage(filePath);
1578
+ if (!lang) return null;
1579
+ return this.getByLanguage(lang);
2752
1580
  }
2753
- const tolerance = options?.tolerance ?? 0;
2754
- const effectiveNewCount = Math.max(0, diffResult.newViolations.length - tolerance);
2755
- const pass = effectiveNewCount === 0 && diffResult.regressions.length === 0;
2756
- return {
2757
- pass,
2758
- message: () => {
2759
- if (pass) {
2760
- return "Expected baseline regression but architecture matches baseline";
2761
- }
2762
- const parts = [];
2763
- if (diffResult.newViolations.length > 0) {
2764
- parts.push(
2765
- `${diffResult.newViolations.length} new violation${diffResult.newViolations.length === 1 ? "" : "s"}${tolerance > 0 ? ` (tolerance: ${tolerance})` : ""}:
2766
- ${formatViolationList(diffResult.newViolations)}`
2767
- );
2768
- }
2769
- if (diffResult.regressions.length > 0) {
2770
- const regLines = diffResult.regressions.map(
2771
- (r) => ` - ${r.category}: ${r.baselineValue} -> ${r.currentValue} (+${r.delta})`
2772
- );
2773
- parts.push(`Regressions:
2774
- ${regLines.join("\n")}`);
2775
- }
2776
- return `Baseline check failed:
2777
- ${parts.join("\n\n")}`;
1581
+ getSupportedExtensions() {
1582
+ return Object.keys(EXTENSION_MAP);
1583
+ }
1584
+ getSupportedLanguages() {
1585
+ return Array.from(this.parsers.keys());
1586
+ }
1587
+ isSupportedExtension(ext) {
1588
+ return ext in EXTENSION_MAP;
1589
+ }
1590
+ };
1591
+ var defaultRegistry = null;
1592
+ function getDefaultRegistry() {
1593
+ if (defaultRegistry) return defaultRegistry;
1594
+ const registry = new ParserRegistry();
1595
+ const tsParser = new TypeScriptParser();
1596
+ registry.register(tsParser);
1597
+ registry.register({
1598
+ name: "javascript",
1599
+ extensions: [".js", ".jsx", ".mjs", ".cjs"],
1600
+ parseFile: tsParser.parseFile.bind(tsParser),
1601
+ extractImports: tsParser.extractImports.bind(tsParser),
1602
+ extractExports: tsParser.extractExports.bind(tsParser),
1603
+ health: tsParser.health.bind(tsParser)
1604
+ });
1605
+ const treeSitterLanguages = ["python", "go", "rust", "java"];
1606
+ for (const lang of treeSitterLanguages) {
1607
+ const parser = createTreeSitterParser(lang);
1608
+ if (parser) {
1609
+ registry.register(parser);
2778
1610
  }
2779
- };
2780
- }
2781
- function filterByScope(results, scope) {
2782
- return results.filter(
2783
- (r) => r.scope === scope || r.scope.startsWith(scope + "/") || r.scope === "project"
2784
- );
2785
- }
2786
- async function toHaveMaxComplexity(received, maxComplexity) {
2787
- const results = await collectCategory(received, new ComplexityCollector());
2788
- const scoped = filterByScope(results, received.scope);
2789
- const violations = scoped.flatMap((r) => r.violations);
2790
- const totalValue = scoped.reduce((sum, r) => sum + r.value, 0);
2791
- const pass = totalValue <= maxComplexity && violations.length === 0;
2792
- return {
2793
- pass,
2794
- message: () => pass ? `Expected complexity to exceed ${maxComplexity} but it was within limits` : `Module '${received.scope}' has complexity violations (${violations.length} violation${violations.length === 1 ? "" : "s"}):
2795
- ${formatViolationList(violations)}`
2796
- };
2797
- }
2798
- async function toHaveMaxCoupling(received, limits) {
2799
- const config = resolveConfig(received);
2800
- if (limits.fanIn !== void 0 || limits.fanOut !== void 0) {
2801
- config.thresholds.coupling = {
2802
- ...typeof config.thresholds.coupling === "object" ? config.thresholds.coupling : {},
2803
- ...limits.fanIn !== void 0 ? { maxFanIn: limits.fanIn } : {},
2804
- ...limits.fanOut !== void 0 ? { maxFanOut: limits.fanOut } : {}
2805
- };
2806
1611
  }
2807
- const collector = new CouplingCollector();
2808
- const results = "_mockResults" in received && received._mockResults ? received._mockResults : await collector.collect(config, received.rootDir);
2809
- const scoped = filterByScope(results, received.scope);
2810
- const violations = scoped.flatMap((r) => r.violations);
2811
- const pass = violations.length === 0;
2812
- return {
2813
- pass,
2814
- message: () => pass ? `Expected coupling violations in '${received.scope}' but found none` : `Module '${received.scope}' has ${violations.length} coupling violation${violations.length === 1 ? "" : "s"} (fanIn limit: ${limits.fanIn ?? "none"}, fanOut limit: ${limits.fanOut ?? "none"}):
2815
- ${formatViolationList(violations)}`
2816
- };
2817
- }
2818
- async function toHaveMaxFileCount(received, maxFiles) {
2819
- const results = await collectCategory(received, new ModuleSizeCollector());
2820
- const scoped = filterByScope(results, received.scope);
2821
- const fileCount = scoped.reduce((max, r) => {
2822
- const meta = r.metadata;
2823
- const fc = typeof meta?.fileCount === "number" ? meta.fileCount : 0;
2824
- return fc > max ? fc : max;
2825
- }, 0);
2826
- const pass = fileCount <= maxFiles;
2827
- return {
2828
- pass,
2829
- message: () => pass ? `Expected file count in '${received.scope}' to exceed ${maxFiles} but it was ${fileCount}` : `Module '${received.scope}' has ${fileCount} files (limit: ${maxFiles})`
2830
- };
2831
- }
2832
- async function toNotDependOn(received, forbiddenModule) {
2833
- const results = await collectCategory(received, new ForbiddenImportCollector());
2834
- const allViolations = results.flatMap((r) => r.violations);
2835
- const scopePrefix = received.scope.replace(/\/+$/, "");
2836
- const forbiddenPrefix = forbiddenModule.replace(/\/+$/, "");
2837
- const relevantViolations = allViolations.filter(
2838
- (v) => (v.file === scopePrefix || v.file.startsWith(scopePrefix + "/")) && (v.detail.includes(forbiddenPrefix + "/") || v.detail.endsWith(forbiddenPrefix))
2839
- );
2840
- const pass = relevantViolations.length === 0;
2841
- return {
2842
- pass,
2843
- message: () => pass ? `Expected '${received.scope}' to depend on '${forbiddenModule}' but no such imports found` : `Module '${received.scope}' depends on '${forbiddenModule}' (${relevantViolations.length} import${relevantViolations.length === 1 ? "" : "s"}):
2844
- ${formatViolationList(relevantViolations)}`
2845
- };
2846
- }
2847
- async function toHaveMaxDepDepth(received, maxDepth) {
2848
- const results = await collectCategory(received, new DepDepthCollector());
2849
- const scoped = filterByScope(results, received.scope);
2850
- const maxActual = scoped.reduce((max, r) => r.value > max ? r.value : max, 0);
2851
- const pass = maxActual <= maxDepth;
2852
- return {
2853
- pass,
2854
- message: () => pass ? `Expected dependency depth in '${received.scope}' to exceed ${maxDepth} but it was ${maxActual}` : `Module '${received.scope}' has dependency depth ${maxActual} (limit: ${maxDepth})`
2855
- };
1612
+ defaultRegistry = registry;
1613
+ return registry;
2856
1614
  }
2857
- var archMatchers = {
2858
- toHaveNoCircularDeps,
2859
- toHaveNoLayerViolations,
2860
- toMatchBaseline,
2861
- toHaveMaxComplexity,
2862
- toHaveMaxCoupling,
2863
- toHaveMaxFileCount,
2864
- toNotDependOn,
2865
- toHaveMaxDepDepth
2866
- };
2867
1615
 
2868
1616
  export {
2869
- __export,
2870
1617
  createError,
2871
1618
  createEntropyError,
2872
1619
  Ok,
@@ -2889,39 +1636,6 @@ export {
2889
1636
  resolveFileToLayer,
2890
1637
  buildDependencyGraph,
2891
1638
  validateDependencies,
2892
- detectCircularDeps,
2893
- detectCircularDepsInFiles,
2894
1639
  detectComplexityViolations,
2895
- detectCouplingViolations,
2896
- ArchMetricCategorySchema,
2897
- ViolationSchema,
2898
- MetricResultSchema,
2899
- CategoryBaselineSchema,
2900
- ArchBaselineSchema,
2901
- CategoryRegressionSchema,
2902
- ArchDiffResultSchema,
2903
- ThresholdConfigSchema,
2904
- ArchConfigSchema,
2905
- ConstraintRuleSchema,
2906
- ViolationSnapshotSchema,
2907
- ViolationHistorySchema,
2908
- EmergenceConfidenceSchema,
2909
- EmergentConstraintSuggestionSchema,
2910
- EmergenceResultSchema,
2911
- violationId,
2912
- constraintRuleId,
2913
- CircularDepsCollector,
2914
- LayerViolationCollector,
2915
- ComplexityCollector,
2916
- CouplingCollector,
2917
- ForbiddenImportCollector,
2918
- ModuleSizeCollector,
2919
- DepDepthCollector,
2920
- defaultCollectors,
2921
- runAll,
2922
- ArchBaselineManager,
2923
- diff,
2924
- architecture,
2925
- archModule,
2926
- archMatchers
1640
+ detectCouplingViolations
2927
1641
  };