@harness-engineering/cli 1.13.1 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +39 -0
  2. package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +44 -0
  3. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +44 -0
  4. package/dist/agents/skills/claude-code/harness-planning/SKILL.md +39 -0
  5. package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +3 -3
  6. package/dist/agents/skills/claude-code/harness-verification/SKILL.md +35 -0
  7. package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +11 -3
  8. package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +39 -0
  9. package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +44 -0
  10. package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +44 -0
  11. package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +39 -0
  12. package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +3 -3
  13. package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +35 -0
  14. package/dist/agents/skills/gemini-cli/initialize-harness-project/SKILL.md +11 -3
  15. package/dist/agents-md-YTYQDA3P.js +8 -0
  16. package/dist/{architecture-2R5Z4ZAF.js → architecture-JQZYM4US.js} +4 -4
  17. package/dist/bin/harness-mcp.js +14 -14
  18. package/dist/bin/harness.js +24 -24
  19. package/dist/{check-phase-gate-2OFZ7OWW.js → check-phase-gate-L3RADYWO.js} +4 -4
  20. package/dist/{chunk-QY4T6YAZ.js → chunk-3C2MLBPJ.js} +4 -4
  21. package/dist/{chunk-UAX4I5ZE.js → chunk-6KTUUFRN.js} +2 -2
  22. package/dist/{chunk-ND6PNADU.js → chunk-7IP4JIFL.js} +9 -9
  23. package/dist/{chunk-C2ERUR3L.js → chunk-7MJAPE3Z.js} +165 -49
  24. package/dist/{chunk-PQ5YK4AY.js → chunk-ABQHQ6I5.js} +1583 -1169
  25. package/dist/{chunk-QPEH2QPG.js → chunk-DBSOCI3G.js} +53 -54
  26. package/dist/{chunk-MHBMTPW7.js → chunk-ERS5EVUZ.js} +9 -0
  27. package/dist/{chunk-JSTQ3AWB.js → chunk-FIAPHX37.js} +1 -1
  28. package/dist/{chunk-IMFVFNJE.js → chunk-FTMXDOR6.js} +1 -1
  29. package/dist/{chunk-72GHBOL2.js → chunk-GZKSBLQL.js} +1 -1
  30. package/dist/{chunk-K6XAPGML.js → chunk-H7Y5CKTM.js} +1 -1
  31. package/dist/{chunk-4ZMOCPYO.js → chunk-NLVUVUGD.js} +1 -1
  32. package/dist/{chunk-Z77YQRQT.js → chunk-O5OJVPL6.js} +16 -5
  33. package/dist/{chunk-NKDM3FMH.js → chunk-OD3S2NHN.js} +1 -1
  34. package/dist/{chunk-65FRIL4D.js → chunk-OSXBPAMK.js} +1 -1
  35. package/dist/{chunk-DZS7CJKL.js → chunk-OXLLOSSR.js} +45 -47
  36. package/dist/{chunk-TS3XWPW5.js → chunk-RCWZBSK5.js} +1 -1
  37. package/dist/{chunk-NOPU4RZ4.js → chunk-S2FXOWOR.js} +3 -3
  38. package/dist/{chunk-VUCPTQ6G.js → chunk-SD3SQOZ2.js} +1 -1
  39. package/dist/{chunk-IM32EEDM.js → chunk-TPOTOBR7.js} +9 -9
  40. package/dist/{chunk-SSKDAOX5.js → chunk-XKECDXJS.js} +436 -340
  41. package/dist/{chunk-TKJZKICB.js → chunk-YPYGXRDR.js} +7 -7
  42. package/dist/{chunk-Q6AB7W5Z.js → chunk-YQ6KC6TE.js} +1 -1
  43. package/dist/{chunk-NERR4TAO.js → chunk-YZD2MRNQ.js} +972 -747
  44. package/dist/ci-workflow-EQZFVX3P.js +8 -0
  45. package/dist/{dist-HXHWB7SV.js → dist-B26DFXMP.js} +571 -478
  46. package/dist/{dist-L7LAAQAS.js → dist-DZ63LLUD.js} +1 -1
  47. package/dist/{dist-2B363XUH.js → dist-HWXF2C3R.js} +18 -2
  48. package/dist/{dist-D4RYGUZE.js → dist-USY2C5JL.js} +3 -1
  49. package/dist/{docs-FZOPM4GK.js → docs-7ECGYMAV.js} +4 -4
  50. package/dist/engine-EG4EH4IX.js +8 -0
  51. package/dist/{entropy-LVHJMFGH.js → entropy-5USWKLVS.js} +3 -3
  52. package/dist/{feedback-IHLVLMRD.js → feedback-UTBXZZHF.js} +1 -1
  53. package/dist/{generate-agent-definitions-64S3CLEZ.js → generate-agent-definitions-3PM5EU7V.js} +4 -4
  54. package/dist/{graph-loader-GJZ4FN4Y.js → graph-loader-2M2HXDQI.js} +1 -1
  55. package/dist/index.d.ts +148 -9
  56. package/dist/index.js +24 -24
  57. package/dist/loader-ZPALXIVR.js +10 -0
  58. package/dist/{mcp-JQUI7BVZ.js → mcp-362EZHF4.js} +14 -14
  59. package/dist/{performance-ZTVSUANN.js → performance-OQAFMJUD.js} +3 -3
  60. package/dist/{review-pipeline-76JHKGSV.js → review-pipeline-C4GCFVGP.js} +1 -1
  61. package/dist/runtime-7YLVK453.js +9 -0
  62. package/dist/{security-FWQZF2IZ.js → security-PZOX7AQS.js} +1 -1
  63. package/dist/templates/axum/Cargo.toml.hbs +8 -0
  64. package/dist/templates/axum/src/main.rs +12 -0
  65. package/dist/templates/axum/template.json +16 -0
  66. package/dist/templates/django/manage.py.hbs +19 -0
  67. package/dist/templates/django/requirements.txt.hbs +1 -0
  68. package/dist/templates/django/src/settings.py.hbs +44 -0
  69. package/dist/templates/django/src/urls.py +6 -0
  70. package/dist/templates/django/src/wsgi.py.hbs +9 -0
  71. package/dist/templates/django/template.json +21 -0
  72. package/dist/templates/express/package.json.hbs +15 -0
  73. package/dist/templates/express/src/app.ts +12 -0
  74. package/dist/templates/express/src/lib/.gitkeep +0 -0
  75. package/dist/templates/express/template.json +16 -0
  76. package/dist/templates/fastapi/requirements.txt.hbs +2 -0
  77. package/dist/templates/fastapi/src/main.py +8 -0
  78. package/dist/templates/fastapi/template.json +20 -0
  79. package/dist/templates/gin/go.mod.hbs +5 -0
  80. package/dist/templates/gin/main.go +15 -0
  81. package/dist/templates/gin/template.json +19 -0
  82. package/dist/templates/go-base/.golangci.yml +16 -0
  83. package/dist/templates/go-base/AGENTS.md.hbs +35 -0
  84. package/dist/templates/go-base/go.mod.hbs +3 -0
  85. package/dist/templates/go-base/harness.config.json.hbs +17 -0
  86. package/dist/templates/go-base/main.go +7 -0
  87. package/dist/templates/go-base/template.json +14 -0
  88. package/dist/templates/java-base/AGENTS.md.hbs +35 -0
  89. package/dist/templates/java-base/checkstyle.xml +20 -0
  90. package/dist/templates/java-base/harness.config.json.hbs +16 -0
  91. package/dist/templates/java-base/pom.xml.hbs +39 -0
  92. package/dist/templates/java-base/src/main/java/App.java.hbs +5 -0
  93. package/dist/templates/java-base/template.json +13 -0
  94. package/dist/templates/nestjs/nest-cli.json +5 -0
  95. package/dist/templates/nestjs/package.json.hbs +18 -0
  96. package/dist/templates/nestjs/src/app.module.ts +8 -0
  97. package/dist/templates/nestjs/src/lib/.gitkeep +0 -0
  98. package/dist/templates/nestjs/src/main.ts +11 -0
  99. package/dist/templates/nestjs/template.json +16 -0
  100. package/dist/templates/nextjs/template.json +15 -1
  101. package/dist/templates/python-base/.python-version +1 -0
  102. package/dist/templates/python-base/AGENTS.md.hbs +32 -0
  103. package/dist/templates/python-base/harness.config.json.hbs +16 -0
  104. package/dist/templates/python-base/pyproject.toml.hbs +18 -0
  105. package/dist/templates/python-base/ruff.toml +5 -0
  106. package/dist/templates/python-base/src/__init__.py +0 -0
  107. package/dist/templates/python-base/template.json +13 -0
  108. package/dist/templates/react-vite/index.html +12 -0
  109. package/dist/templates/react-vite/package.json.hbs +18 -0
  110. package/dist/templates/react-vite/src/App.tsx +7 -0
  111. package/dist/templates/react-vite/src/lib/.gitkeep +0 -0
  112. package/dist/templates/react-vite/src/main.tsx +9 -0
  113. package/dist/templates/react-vite/template.json +19 -0
  114. package/dist/templates/react-vite/vite.config.ts +6 -0
  115. package/dist/templates/rust-base/AGENTS.md.hbs +35 -0
  116. package/dist/templates/rust-base/Cargo.toml.hbs +6 -0
  117. package/dist/templates/rust-base/clippy.toml +2 -0
  118. package/dist/templates/rust-base/harness.config.json.hbs +17 -0
  119. package/dist/templates/rust-base/src/main.rs +3 -0
  120. package/dist/templates/rust-base/template.json +14 -0
  121. package/dist/templates/spring-boot/pom.xml.hbs +50 -0
  122. package/dist/templates/spring-boot/src/main/java/Application.java.hbs +19 -0
  123. package/dist/templates/spring-boot/template.json +15 -0
  124. package/dist/templates/vue/index.html +12 -0
  125. package/dist/templates/vue/package.json.hbs +16 -0
  126. package/dist/templates/vue/src/App.vue +7 -0
  127. package/dist/templates/vue/src/lib/.gitkeep +0 -0
  128. package/dist/templates/vue/src/main.ts +4 -0
  129. package/dist/templates/vue/template.json +19 -0
  130. package/dist/templates/vue/vite.config.ts +6 -0
  131. package/dist/{validate-GCHZJIL7.js → validate-FD3Z6VJD.js} +4 -4
  132. package/dist/validate-cross-check-WNJM6H2D.js +8 -0
  133. package/package.json +5 -5
  134. package/dist/agents-md-XU3BHE22.js +0 -8
  135. package/dist/ci-workflow-EHV65NQB.js +0 -8
  136. package/dist/engine-OL4T6NZS.js +0 -8
  137. package/dist/loader-DPYFB6R6.js +0 -10
  138. package/dist/runtime-X7U6SC7K.js +0 -9
  139. package/dist/validate-cross-check-STFHYMAZ.js +0 -8
@@ -1,9 +1,10 @@
1
1
  import {
2
2
  Err,
3
- Ok
4
- } from "./chunk-MHBMTPW7.js";
3
+ Ok,
4
+ SESSION_SECTION_NAMES
5
+ } from "./chunk-ERS5EVUZ.js";
5
6
 
6
- // ../core/dist/chunk-D6VFA6AS.mjs
7
+ // ../core/dist/chunk-BQUWXBGR.mjs
7
8
  import { z } from "zod";
8
9
  import { createHash } from "crypto";
9
10
  import { minimatch } from "minimatch";
@@ -134,17 +135,17 @@ function resolveFileToLayer(file, layers) {
134
135
  }
135
136
  var accessAsync = promisify(access);
136
137
  var readFileAsync = promisify(readFile);
137
- async function fileExists(path20) {
138
+ async function fileExists(path22) {
138
139
  try {
139
- await accessAsync(path20, constants.F_OK);
140
+ await accessAsync(path22, constants.F_OK);
140
141
  return true;
141
142
  } catch {
142
143
  return false;
143
144
  }
144
145
  }
145
- async function readFileContent(path20) {
146
+ async function readFileContent(path22) {
146
147
  try {
147
- const content = await readFileAsync(path20, "utf-8");
148
+ const content = await readFileAsync(path22, "utf-8");
148
149
  return Ok(content);
149
150
  } catch (error) {
150
151
  return Err(error);
@@ -290,65 +291,71 @@ async function validateDependencies(config) {
290
291
  graph: graphResult.value
291
292
  });
292
293
  }
293
- function tarjanSCC(graph) {
294
- const nodeMap = /* @__PURE__ */ new Map();
295
- const stack = [];
296
- const sccs = [];
297
- let index = 0;
294
+ function buildAdjacencyList(graph) {
298
295
  const adjacency = /* @__PURE__ */ new Map();
296
+ const nodeSet = new Set(graph.nodes);
299
297
  for (const node of graph.nodes) {
300
298
  adjacency.set(node, []);
301
299
  }
302
300
  for (const edge of graph.edges) {
303
301
  const neighbors = adjacency.get(edge.from);
304
- if (neighbors && graph.nodes.includes(edge.to)) {
302
+ if (neighbors && nodeSet.has(edge.to)) {
305
303
  neighbors.push(edge.to);
306
304
  }
307
305
  }
308
- function strongConnect(node) {
309
- nodeMap.set(node, {
310
- index,
311
- lowlink: index,
312
- onStack: true
313
- });
314
- index++;
315
- stack.push(node);
316
- const neighbors = adjacency.get(node) ?? [];
317
- for (const neighbor of neighbors) {
318
- const neighborData = nodeMap.get(neighbor);
319
- if (!neighborData) {
320
- strongConnect(neighbor);
321
- const nodeData2 = nodeMap.get(node);
322
- const updatedNeighborData = nodeMap.get(neighbor);
323
- nodeData2.lowlink = Math.min(nodeData2.lowlink, updatedNeighborData.lowlink);
324
- } else if (neighborData.onStack) {
325
- const nodeData2 = nodeMap.get(node);
326
- nodeData2.lowlink = Math.min(nodeData2.lowlink, neighborData.index);
327
- }
328
- }
329
- const nodeData = nodeMap.get(node);
330
- if (nodeData.lowlink === nodeData.index) {
331
- const scc = [];
332
- let w;
333
- do {
334
- w = stack.pop();
335
- nodeMap.get(w).onStack = false;
336
- scc.push(w);
337
- } while (w !== node);
338
- if (scc.length > 1) {
339
- sccs.push(scc);
340
- } else if (scc.length === 1) {
341
- const selfNode = scc[0];
342
- const selfNeighbors = adjacency.get(selfNode) ?? [];
343
- if (selfNeighbors.includes(selfNode)) {
344
- sccs.push(scc);
345
- }
346
- }
306
+ return adjacency;
307
+ }
308
+ function isCyclicSCC(scc, adjacency) {
309
+ if (scc.length > 1) return true;
310
+ if (scc.length === 1) {
311
+ const selfNode = scc[0];
312
+ const selfNeighbors = adjacency.get(selfNode) ?? [];
313
+ return selfNeighbors.includes(selfNode);
314
+ }
315
+ return false;
316
+ }
317
+ function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
318
+ for (const neighbor of neighbors) {
319
+ const neighborData = nodeMap.get(neighbor);
320
+ if (!neighborData) {
321
+ strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
322
+ const nodeData = nodeMap.get(node);
323
+ const updatedNeighborData = nodeMap.get(neighbor);
324
+ nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
325
+ } else if (neighborData.onStack) {
326
+ const nodeData = nodeMap.get(node);
327
+ nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
328
+ }
329
+ }
330
+ }
331
+ function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
332
+ nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
333
+ indexRef.value++;
334
+ stack.push(node);
335
+ processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
336
+ const nodeData = nodeMap.get(node);
337
+ if (nodeData.lowlink === nodeData.index) {
338
+ const scc = [];
339
+ let w;
340
+ do {
341
+ w = stack.pop();
342
+ nodeMap.get(w).onStack = false;
343
+ scc.push(w);
344
+ } while (w !== node);
345
+ if (isCyclicSCC(scc, adjacency)) {
346
+ sccs.push(scc);
347
347
  }
348
348
  }
349
+ }
350
+ function tarjanSCC(graph) {
351
+ const nodeMap = /* @__PURE__ */ new Map();
352
+ const stack = [];
353
+ const sccs = [];
354
+ const indexRef = { value: 0 };
355
+ const adjacency = buildAdjacencyList(graph);
349
356
  for (const node of graph.nodes) {
350
357
  if (!nodeMap.has(node)) {
351
- strongConnect(node);
358
+ strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
352
359
  }
353
360
  }
354
361
  return sccs;
@@ -607,6 +614,31 @@ function aggregateByCategory(results) {
607
614
  }
608
615
  return map;
609
616
  }
617
+ function classifyViolations(violations, baselineViolationIds) {
618
+ const newViolations = [];
619
+ const preExisting = [];
620
+ for (const violation of violations) {
621
+ if (baselineViolationIds.has(violation.id)) {
622
+ preExisting.push(violation.id);
623
+ } else {
624
+ newViolations.push(violation);
625
+ }
626
+ }
627
+ return { newViolations, preExisting };
628
+ }
629
+ function findResolvedViolations(baselineCategory, currentViolationIds) {
630
+ if (!baselineCategory) return [];
631
+ return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
632
+ }
633
+ function collectOrphanedBaselineViolations(baseline, visitedCategories) {
634
+ const resolved = [];
635
+ for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
636
+ if (!visitedCategories.has(category) && baselineCategory) {
637
+ resolved.push(...baselineCategory.violationIds);
638
+ }
639
+ }
640
+ return resolved;
641
+ }
610
642
  function diff(current, baseline) {
611
643
  const aggregated = aggregateByCategory(current);
612
644
  const newViolations = [];
@@ -619,21 +651,11 @@ function diff(current, baseline) {
619
651
  const baselineCategory = baseline.metrics[category];
620
652
  const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
621
653
  const baselineValue = baselineCategory?.value ?? 0;
622
- for (const violation of agg.violations) {
623
- if (baselineViolationIds.has(violation.id)) {
624
- preExisting.push(violation.id);
625
- } else {
626
- newViolations.push(violation);
627
- }
628
- }
654
+ const classified = classifyViolations(agg.violations, baselineViolationIds);
655
+ newViolations.push(...classified.newViolations);
656
+ preExisting.push(...classified.preExisting);
629
657
  const currentViolationIds = new Set(agg.violations.map((v) => v.id));
630
- if (baselineCategory) {
631
- for (const id of baselineCategory.violationIds) {
632
- if (!currentViolationIds.has(id)) {
633
- resolvedViolations.push(id);
634
- }
635
- }
636
- }
658
+ resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
637
659
  if (baselineCategory && agg.value > baselineValue) {
638
660
  regressions.push({
639
661
  category,
@@ -643,16 +665,9 @@ function diff(current, baseline) {
643
665
  });
644
666
  }
645
667
  }
646
- for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
647
- if (!visitedCategories.has(category) && baselineCategory) {
648
- for (const id of baselineCategory.violationIds) {
649
- resolvedViolations.push(id);
650
- }
651
- }
652
- }
653
- const passed = newViolations.length === 0 && regressions.length === 0;
668
+ resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
654
669
  return {
655
- passed,
670
+ passed: newViolations.length === 0 && regressions.length === 0,
656
671
  newViolations,
657
672
  resolvedViolations,
658
673
  preExisting,
@@ -667,22 +682,22 @@ var DEFAULT_THRESHOLDS = {
667
682
  fileLength: { info: 300 },
668
683
  hotspotPercentile: { error: 95 }
669
684
  };
685
+ var FUNCTION_PATTERNS = [
686
+ // function declarations: function name(params) {
687
+ /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
688
+ // method declarations: name(params) {
689
+ /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
690
+ // arrow functions assigned to const/let/var: const name = (params) =>
691
+ /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
692
+ // arrow functions assigned to const/let/var with single param: const name = param =>
693
+ /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
694
+ ];
670
695
  function extractFunctions(content) {
671
696
  const functions = [];
672
697
  const lines = content.split("\n");
673
- const patterns = [
674
- // function declarations: function name(params) {
675
- /^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
676
- // method declarations: name(params) {
677
- /^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
678
- // arrow functions assigned to const/let/var: const name = (params) =>
679
- /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
680
- // arrow functions assigned to const/let/var with single param: const name = param =>
681
- /^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
682
- ];
683
698
  for (let i = 0; i < lines.length; i++) {
684
699
  const line = lines[i];
685
- for (const pattern of patterns) {
700
+ for (const pattern of FUNCTION_PATTERNS) {
686
701
  const match = line.match(pattern);
687
702
  if (match) {
688
703
  const name = match[1] ?? "anonymous";
@@ -771,26 +786,155 @@ function computeNestingDepth(body) {
771
786
  }
772
787
  return maxDepth;
773
788
  }
774
- async function detectComplexityViolations(snapshot, config, graphData) {
775
- const violations = [];
776
- const thresholds = {
789
+ function resolveThresholds(config) {
790
+ const userThresholds = config?.thresholds;
791
+ if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
792
+ return {
777
793
  cyclomaticComplexity: {
778
- error: config?.thresholds?.cyclomaticComplexity?.error ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.error,
779
- warn: config?.thresholds?.cyclomaticComplexity?.warn ?? DEFAULT_THRESHOLDS.cyclomaticComplexity.warn
794
+ ...DEFAULT_THRESHOLDS.cyclomaticComplexity,
795
+ ...stripUndefined(userThresholds.cyclomaticComplexity)
780
796
  },
781
797
  nestingDepth: {
782
- warn: config?.thresholds?.nestingDepth?.warn ?? DEFAULT_THRESHOLDS.nestingDepth.warn
798
+ ...DEFAULT_THRESHOLDS.nestingDepth,
799
+ ...stripUndefined(userThresholds.nestingDepth)
783
800
  },
784
801
  functionLength: {
785
- warn: config?.thresholds?.functionLength?.warn ?? DEFAULT_THRESHOLDS.functionLength.warn
802
+ ...DEFAULT_THRESHOLDS.functionLength,
803
+ ...stripUndefined(userThresholds.functionLength)
786
804
  },
787
805
  parameterCount: {
788
- warn: config?.thresholds?.parameterCount?.warn ?? DEFAULT_THRESHOLDS.parameterCount.warn
806
+ ...DEFAULT_THRESHOLDS.parameterCount,
807
+ ...stripUndefined(userThresholds.parameterCount)
789
808
  },
790
- fileLength: {
791
- info: config?.thresholds?.fileLength?.info ?? DEFAULT_THRESHOLDS.fileLength.info
792
- }
809
+ fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
810
+ };
811
+ }
812
+ function stripUndefined(obj) {
813
+ if (!obj) return {};
814
+ const result = {};
815
+ for (const [key, val] of Object.entries(obj)) {
816
+ if (val !== void 0) result[key] = val;
817
+ }
818
+ return result;
819
+ }
820
+ function checkFileLengthViolation(filePath, lineCount, threshold) {
821
+ if (lineCount <= threshold) return null;
822
+ return {
823
+ file: filePath,
824
+ function: "<file>",
825
+ line: 1,
826
+ metric: "fileLength",
827
+ value: lineCount,
828
+ threshold,
829
+ tier: 3,
830
+ severity: "info",
831
+ message: `File has ${lineCount} lines (threshold: ${threshold})`
832
+ };
833
+ }
834
+ function checkCyclomaticComplexity(filePath, fn, thresholds) {
835
+ const complexity = computeCyclomaticComplexity(fn.body);
836
+ if (complexity > thresholds.error) {
837
+ return {
838
+ file: filePath,
839
+ function: fn.name,
840
+ line: fn.line,
841
+ metric: "cyclomaticComplexity",
842
+ value: complexity,
843
+ threshold: thresholds.error,
844
+ tier: 1,
845
+ severity: "error",
846
+ message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.error})`
847
+ };
848
+ }
849
+ if (complexity > thresholds.warn) {
850
+ return {
851
+ file: filePath,
852
+ function: fn.name,
853
+ line: fn.line,
854
+ metric: "cyclomaticComplexity",
855
+ value: complexity,
856
+ threshold: thresholds.warn,
857
+ tier: 2,
858
+ severity: "warning",
859
+ message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.warn})`
860
+ };
861
+ }
862
+ return null;
863
+ }
864
+ function checkNestingDepth(filePath, fn, threshold) {
865
+ const depth = computeNestingDepth(fn.body);
866
+ if (depth <= threshold) return null;
867
+ return {
868
+ file: filePath,
869
+ function: fn.name,
870
+ line: fn.line,
871
+ metric: "nestingDepth",
872
+ value: depth,
873
+ threshold,
874
+ tier: 2,
875
+ severity: "warning",
876
+ message: `Function "${fn.name}" has nesting depth of ${depth} (threshold: ${threshold})`
877
+ };
878
+ }
879
+ function checkFunctionLength(filePath, fn, threshold) {
880
+ const fnLength = fn.endLine - fn.startLine + 1;
881
+ if (fnLength <= threshold) return null;
882
+ return {
883
+ file: filePath,
884
+ function: fn.name,
885
+ line: fn.line,
886
+ metric: "functionLength",
887
+ value: fnLength,
888
+ threshold,
889
+ tier: 2,
890
+ severity: "warning",
891
+ message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${threshold})`
892
+ };
893
+ }
894
+ function checkParameterCount(filePath, fn, threshold) {
895
+ if (fn.params <= threshold) return null;
896
+ return {
897
+ file: filePath,
898
+ function: fn.name,
899
+ line: fn.line,
900
+ metric: "parameterCount",
901
+ value: fn.params,
902
+ threshold,
903
+ tier: 2,
904
+ severity: "warning",
905
+ message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${threshold})`
906
+ };
907
+ }
908
+ function checkHotspot(filePath, fn, graphData) {
909
+ const hotspot = graphData.hotspots.find((h) => h.file === filePath && h.function === fn.name);
910
+ if (!hotspot || hotspot.hotspotScore <= graphData.percentile95Score) return null;
911
+ return {
912
+ file: filePath,
913
+ function: fn.name,
914
+ line: fn.line,
915
+ metric: "hotspotScore",
916
+ value: hotspot.hotspotScore,
917
+ threshold: graphData.percentile95Score,
918
+ tier: 1,
919
+ severity: "error",
920
+ message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
793
921
  };
922
+ }
923
+ function collectFunctionViolations(filePath, fn, thresholds, graphData) {
924
+ const checks = [
925
+ checkCyclomaticComplexity(filePath, fn, thresholds.cyclomaticComplexity),
926
+ checkNestingDepth(filePath, fn, thresholds.nestingDepth.warn),
927
+ checkFunctionLength(filePath, fn, thresholds.functionLength.warn),
928
+ checkParameterCount(filePath, fn, thresholds.parameterCount.warn)
929
+ ];
930
+ if (graphData) {
931
+ checks.push(checkHotspot(filePath, fn, graphData));
932
+ }
933
+ return checks.filter((v) => v !== null);
934
+ }
935
+ async function detectComplexityViolations(snapshot, config, graphData) {
936
+ const violations = [];
937
+ const thresholds = resolveThresholds(config);
794
938
  let totalFunctions = 0;
795
939
  for (const file of snapshot.files) {
796
940
  let content;
@@ -800,107 +944,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
800
944
  continue;
801
945
  }
802
946
  const lines = content.split("\n");
803
- if (lines.length > thresholds.fileLength.info) {
804
- violations.push({
805
- file: file.path,
806
- function: "<file>",
807
- line: 1,
808
- metric: "fileLength",
809
- value: lines.length,
810
- threshold: thresholds.fileLength.info,
811
- tier: 3,
812
- severity: "info",
813
- message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
814
- });
815
- }
947
+ const fileLenViolation = checkFileLengthViolation(
948
+ file.path,
949
+ lines.length,
950
+ thresholds.fileLength.info
951
+ );
952
+ if (fileLenViolation) violations.push(fileLenViolation);
816
953
  const functions = extractFunctions(content);
817
954
  totalFunctions += functions.length;
818
955
  for (const fn of functions) {
819
- const complexity = computeCyclomaticComplexity(fn.body);
820
- if (complexity > thresholds.cyclomaticComplexity.error) {
821
- violations.push({
822
- file: file.path,
823
- function: fn.name,
824
- line: fn.line,
825
- metric: "cyclomaticComplexity",
826
- value: complexity,
827
- threshold: thresholds.cyclomaticComplexity.error,
828
- tier: 1,
829
- severity: "error",
830
- message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
831
- });
832
- } else if (complexity > thresholds.cyclomaticComplexity.warn) {
833
- violations.push({
834
- file: file.path,
835
- function: fn.name,
836
- line: fn.line,
837
- metric: "cyclomaticComplexity",
838
- value: complexity,
839
- threshold: thresholds.cyclomaticComplexity.warn,
840
- tier: 2,
841
- severity: "warning",
842
- message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
843
- });
844
- }
845
- const nestingDepth = computeNestingDepth(fn.body);
846
- if (nestingDepth > thresholds.nestingDepth.warn) {
847
- violations.push({
848
- file: file.path,
849
- function: fn.name,
850
- line: fn.line,
851
- metric: "nestingDepth",
852
- value: nestingDepth,
853
- threshold: thresholds.nestingDepth.warn,
854
- tier: 2,
855
- severity: "warning",
856
- message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
857
- });
858
- }
859
- const fnLength = fn.endLine - fn.startLine + 1;
860
- if (fnLength > thresholds.functionLength.warn) {
861
- violations.push({
862
- file: file.path,
863
- function: fn.name,
864
- line: fn.line,
865
- metric: "functionLength",
866
- value: fnLength,
867
- threshold: thresholds.functionLength.warn,
868
- tier: 2,
869
- severity: "warning",
870
- message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
871
- });
872
- }
873
- if (fn.params > thresholds.parameterCount.warn) {
874
- violations.push({
875
- file: file.path,
876
- function: fn.name,
877
- line: fn.line,
878
- metric: "parameterCount",
879
- value: fn.params,
880
- threshold: thresholds.parameterCount.warn,
881
- tier: 2,
882
- severity: "warning",
883
- message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
884
- });
885
- }
886
- if (graphData) {
887
- const hotspot = graphData.hotspots.find(
888
- (h) => h.file === file.path && h.function === fn.name
889
- );
890
- if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
891
- violations.push({
892
- file: file.path,
893
- function: fn.name,
894
- line: fn.line,
895
- metric: "hotspotScore",
896
- value: hotspot.hotspotScore,
897
- threshold: graphData.percentile95Score,
898
- tier: 1,
899
- severity: "error",
900
- message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
901
- });
902
- }
903
- }
956
+ violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
904
957
  }
905
958
  }
906
959
  const errorCount = violations.filter((v) => v.severity === "error").length;
@@ -1788,24 +1841,28 @@ import * as path9 from "path";
1788
1841
  import { execSync as execSync2 } from "child_process";
1789
1842
  import * as fs13 from "fs";
1790
1843
  import * as path10 from "path";
1791
- import * as fs15 from "fs/promises";
1792
- import { z as z5 } from "zod";
1793
1844
  import * as fs14 from "fs";
1794
1845
  import * as path11 from "path";
1846
+ import * as fs15 from "fs";
1795
1847
  import * as path12 from "path";
1848
+ import * as fs17 from "fs/promises";
1849
+ import { z as z5 } from "zod";
1850
+ import * as fs16 from "fs";
1796
1851
  import * as path13 from "path";
1797
1852
  import * as path14 from "path";
1798
1853
  import * as path15 from "path";
1799
- import * as fs16 from "fs";
1800
1854
  import * as path16 from "path";
1801
- import { z as z6 } from "zod";
1802
- import * as fs17 from "fs/promises";
1803
1855
  import * as path17 from "path";
1804
- import * as fs18 from "fs/promises";
1856
+ import * as fs18 from "fs";
1805
1857
  import * as path18 from "path";
1806
- import * as ejs from "ejs";
1807
- import * as fs19 from "fs";
1858
+ import { z as z6 } from "zod";
1859
+ import * as fs19 from "fs/promises";
1808
1860
  import * as path19 from "path";
1861
+ import * as fs20 from "fs/promises";
1862
+ import * as path20 from "path";
1863
+ import * as ejs from "ejs";
1864
+ import * as fs21 from "fs";
1865
+ import * as path21 from "path";
1809
1866
  import * as os from "os";
1810
1867
  import { spawn } from "child_process";
1811
1868
  async function validateFileStructure(projectPath, conventions) {
@@ -1843,15 +1900,15 @@ function validateConfig(data, schema) {
1843
1900
  let message = "Configuration validation failed";
1844
1901
  const suggestions = [];
1845
1902
  if (firstError) {
1846
- const path20 = firstError.path.join(".");
1847
- const pathDisplay = path20 ? ` at "${path20}"` : "";
1903
+ const path22 = firstError.path.join(".");
1904
+ const pathDisplay = path22 ? ` at "${path22}"` : "";
1848
1905
  if (firstError.code === "invalid_type") {
1849
1906
  const received = firstError.received;
1850
1907
  const expected = firstError.expected;
1851
1908
  if (received === "undefined") {
1852
1909
  code = "MISSING_FIELD";
1853
1910
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
1854
- suggestions.push(`Field "${path20}" is required and must be of type "${expected}"`);
1911
+ suggestions.push(`Field "${path22}" is required and must be of type "${expected}"`);
1855
1912
  } else {
1856
1913
  code = "INVALID_TYPE";
1857
1914
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -1998,6 +2055,43 @@ function extractMarkdownLinks(content) {
1998
2055
  }
1999
2056
  return links;
2000
2057
  }
2058
+ function isDescriptionTerminator(trimmed) {
2059
+ return trimmed.startsWith("#") || trimmed.startsWith("-") || trimmed.startsWith("*") || trimmed.startsWith("```");
2060
+ }
2061
+ function extractDescription(sectionLines) {
2062
+ const descriptionLines = [];
2063
+ for (const line of sectionLines) {
2064
+ const trimmed = line.trim();
2065
+ if (trimmed === "") {
2066
+ if (descriptionLines.length > 0) break;
2067
+ continue;
2068
+ }
2069
+ if (isDescriptionTerminator(trimmed)) break;
2070
+ descriptionLines.push(trimmed);
2071
+ }
2072
+ return descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0;
2073
+ }
2074
+ function buildAgentMapSection(section, lines) {
2075
+ const endIndex = section.endIndex ?? lines.length;
2076
+ const sectionLines = lines.slice(section.startIndex + 1, endIndex);
2077
+ const sectionContent = sectionLines.join("\n");
2078
+ const links = extractMarkdownLinks(sectionContent).map((link) => ({
2079
+ ...link,
2080
+ line: link.line + section.startIndex + 1,
2081
+ exists: false
2082
+ }));
2083
+ const result = {
2084
+ title: section.title,
2085
+ level: section.level,
2086
+ line: section.line,
2087
+ links
2088
+ };
2089
+ const description = extractDescription(sectionLines);
2090
+ if (description) {
2091
+ result.description = description;
2092
+ }
2093
+ return result;
2094
+ }
2001
2095
  function extractSections(content) {
2002
2096
  const lines = content.split("\n");
2003
2097
  const sections = [];
@@ -2010,7 +2104,6 @@ function extractSections(content) {
2010
2104
  title: match[2].trim(),
2011
2105
  level: match[1].length,
2012
2106
  line: i + 1,
2013
- // 1-indexed
2014
2107
  startIndex: i
2015
2108
  });
2016
2109
  }
@@ -2022,62 +2115,29 @@ function extractSections(content) {
2022
2115
  currentSection.endIndex = nextSection ? nextSection.startIndex : lines.length;
2023
2116
  }
2024
2117
  }
2025
- return sections.map((section) => {
2026
- const endIndex = section.endIndex ?? lines.length;
2027
- const sectionLines = lines.slice(section.startIndex + 1, endIndex);
2028
- const sectionContent = sectionLines.join("\n");
2029
- const links = extractMarkdownLinks(sectionContent).map((link) => ({
2030
- ...link,
2031
- line: link.line + section.startIndex + 1,
2032
- // Adjust line number
2033
- exists: false
2034
- // Will be set later by validateAgentsMap
2035
- }));
2036
- const descriptionLines = [];
2037
- for (const line of sectionLines) {
2038
- const trimmed = line.trim();
2039
- if (trimmed === "") {
2040
- if (descriptionLines.length > 0) break;
2041
- continue;
2042
- }
2043
- if (trimmed.startsWith("#")) break;
2044
- if (trimmed.startsWith("-") || trimmed.startsWith("*")) break;
2045
- if (trimmed.startsWith("```")) break;
2046
- descriptionLines.push(trimmed);
2047
- }
2048
- const result = {
2049
- title: section.title,
2050
- level: section.level,
2051
- line: section.line,
2052
- links
2053
- };
2054
- if (descriptionLines.length > 0) {
2055
- result.description = descriptionLines.join(" ");
2056
- }
2057
- return result;
2058
- });
2118
+ return sections.map((section) => buildAgentMapSection(section, lines));
2059
2119
  }
2060
- function isExternalLink(path20) {
2061
- return path20.startsWith("http://") || path20.startsWith("https://") || path20.startsWith("#") || path20.startsWith("mailto:");
2120
+ function isExternalLink(path22) {
2121
+ return path22.startsWith("http://") || path22.startsWith("https://") || path22.startsWith("#") || path22.startsWith("mailto:");
2062
2122
  }
2063
2123
  function resolveLinkPath(linkPath, baseDir) {
2064
2124
  return linkPath.startsWith(".") ? join4(baseDir, linkPath) : linkPath;
2065
2125
  }
2066
- async function validateAgentsMap(path20 = "./AGENTS.md") {
2067
- const contentResult = await readFileContent(path20);
2126
+ async function validateAgentsMap(path22 = "./AGENTS.md") {
2127
+ const contentResult = await readFileContent(path22);
2068
2128
  if (!contentResult.ok) {
2069
2129
  return Err(
2070
2130
  createError(
2071
2131
  "PARSE_ERROR",
2072
2132
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
2073
- { path: path20 },
2133
+ { path: path22 },
2074
2134
  ["Ensure the file exists", "Check file permissions"]
2075
2135
  )
2076
2136
  );
2077
2137
  }
2078
2138
  const content = contentResult.value;
2079
2139
  const sections = extractSections(content);
2080
- const baseDir = dirname4(path20);
2140
+ const baseDir = dirname4(path22);
2081
2141
  const sectionTitles = sections.map((s) => s.title);
2082
2142
  const missingSections = REQUIRED_SECTIONS.filter(
2083
2143
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -2211,8 +2271,8 @@ async function checkDocCoverage(domain, options = {}) {
2211
2271
  );
2212
2272
  }
2213
2273
  }
2214
- function suggestFix(path20, existingFiles) {
2215
- const targetName = basename2(path20).toLowerCase();
2274
+ function suggestFix(path22, existingFiles) {
2275
+ const targetName = basename2(path22).toLowerCase();
2216
2276
  const similar = existingFiles.find((file) => {
2217
2277
  const fileName = basename2(file).toLowerCase();
2218
2278
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -2220,7 +2280,7 @@ function suggestFix(path20, existingFiles) {
2220
2280
  if (similar) {
2221
2281
  return `Did you mean "${similar}"?`;
2222
2282
  }
2223
- return `Create the file "${path20}" or remove the link`;
2283
+ return `Create the file "${path22}" or remove the link`;
2224
2284
  }
2225
2285
  async function validateKnowledgeMap(rootDir = process.cwd()) {
2226
2286
  const agentsPath = join22(rootDir, "AGENTS.md");
@@ -2563,8 +2623,8 @@ function createBoundaryValidator(schema, name) {
2563
2623
  return Ok(result.data);
2564
2624
  }
2565
2625
  const suggestions = result.error.issues.map((issue) => {
2566
- const path20 = issue.path.join(".");
2567
- return path20 ? `${path20}: ${issue.message}` : issue.message;
2626
+ const path22 = issue.path.join(".");
2627
+ return path22 ? `${path22}: ${issue.message}` : issue.message;
2568
2628
  });
2569
2629
  return Err(
2570
2630
  createError(
@@ -2774,175 +2834,183 @@ function stringArraysEqual(a, b) {
2774
2834
  const sortedB = [...b].sort();
2775
2835
  return sortedA.every((val, i) => val === sortedB[i]);
2776
2836
  }
2777
- function deepMergeConstraints(localConfig, bundleConstraints, _existingContributions) {
2778
- const config = { ...localConfig };
2779
- const contributions = {};
2780
- const conflicts = [];
2781
- if (bundleConstraints.layers && bundleConstraints.layers.length > 0) {
2782
- const localLayers = Array.isArray(localConfig.layers) ? localConfig.layers : [];
2783
- const mergedLayers = [...localLayers];
2784
- const contributedLayerNames = [];
2785
- for (const bundleLayer of bundleConstraints.layers) {
2786
- const existing = localLayers.find((l) => l.name === bundleLayer.name);
2787
- if (!existing) {
2788
- mergedLayers.push(bundleLayer);
2789
- contributedLayerNames.push(bundleLayer.name);
2790
- } else {
2791
- const same = existing.pattern === bundleLayer.pattern && stringArraysEqual(existing.allowedDependencies, bundleLayer.allowedDependencies);
2792
- if (!same) {
2793
- conflicts.push({
2794
- section: "layers",
2795
- key: bundleLayer.name,
2796
- localValue: existing,
2797
- packageValue: bundleLayer,
2798
- description: `Layer '${bundleLayer.name}' already exists locally with different configuration`
2799
- });
2800
- }
2837
+ function mergeLayers(localConfig, bundleLayers, config, contributions, conflicts) {
2838
+ const localLayers = Array.isArray(localConfig.layers) ? localConfig.layers : [];
2839
+ const mergedLayers = [...localLayers];
2840
+ const contributedLayerNames = [];
2841
+ for (const bundleLayer of bundleLayers) {
2842
+ const existing = localLayers.find((l) => l.name === bundleLayer.name);
2843
+ if (!existing) {
2844
+ mergedLayers.push(bundleLayer);
2845
+ contributedLayerNames.push(bundleLayer.name);
2846
+ } else {
2847
+ const same = existing.pattern === bundleLayer.pattern && stringArraysEqual(existing.allowedDependencies, bundleLayer.allowedDependencies);
2848
+ if (!same) {
2849
+ conflicts.push({
2850
+ section: "layers",
2851
+ key: bundleLayer.name,
2852
+ localValue: existing,
2853
+ packageValue: bundleLayer,
2854
+ description: `Layer '${bundleLayer.name}' already exists locally with different configuration`
2855
+ });
2856
+ }
2857
+ }
2858
+ }
2859
+ config.layers = mergedLayers;
2860
+ if (contributedLayerNames.length > 0) contributions.layers = contributedLayerNames;
2861
+ }
2862
+ function mergeForbiddenImports(localConfig, bundleRules, config, contributions, conflicts) {
2863
+ const localFI = Array.isArray(localConfig.forbiddenImports) ? localConfig.forbiddenImports : [];
2864
+ const mergedFI = [...localFI];
2865
+ const contributedFromKeys = [];
2866
+ for (const bundleRule of bundleRules) {
2867
+ const existing = localFI.find((r) => r.from === bundleRule.from);
2868
+ if (!existing) {
2869
+ const entry = { from: bundleRule.from, disallow: bundleRule.disallow };
2870
+ if (bundleRule.message !== void 0) entry.message = bundleRule.message;
2871
+ mergedFI.push(entry);
2872
+ contributedFromKeys.push(bundleRule.from);
2873
+ } else {
2874
+ if (!stringArraysEqual(existing.disallow, bundleRule.disallow)) {
2875
+ conflicts.push({
2876
+ section: "forbiddenImports",
2877
+ key: bundleRule.from,
2878
+ localValue: existing,
2879
+ packageValue: bundleRule,
2880
+ description: `Forbidden import rule for '${bundleRule.from}' already exists locally with different disallow list`
2881
+ });
2801
2882
  }
2802
2883
  }
2803
- config.layers = mergedLayers;
2804
- if (contributedLayerNames.length > 0) {
2805
- contributions.layers = contributedLayerNames;
2884
+ }
2885
+ config.forbiddenImports = mergedFI;
2886
+ if (contributedFromKeys.length > 0) contributions.forbiddenImports = contributedFromKeys;
2887
+ }
2888
+ function mergeBoundaries(localConfig, bundleBoundaries, config, contributions) {
2889
+ const localBoundaries = localConfig.boundaries ?? { requireSchema: [] };
2890
+ const localSchemas = new Set(localBoundaries.requireSchema ?? []);
2891
+ const newSchemas = [];
2892
+ for (const schema of bundleBoundaries.requireSchema ?? []) {
2893
+ if (!localSchemas.has(schema)) {
2894
+ newSchemas.push(schema);
2895
+ localSchemas.add(schema);
2896
+ }
2897
+ }
2898
+ config.boundaries = { requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas] };
2899
+ if (newSchemas.length > 0) contributions.boundaries = newSchemas;
2900
+ }
2901
+ function mergeArchitecture(localConfig, bundleArch, config, contributions, conflicts) {
2902
+ const localArch = localConfig.architecture ?? { thresholds: {}, modules: {} };
2903
+ const mergedThresholds = { ...localArch.thresholds };
2904
+ const contributedThresholdKeys = [];
2905
+ for (const [category, value] of Object.entries(bundleArch.thresholds ?? {})) {
2906
+ if (!(category in mergedThresholds)) {
2907
+ mergedThresholds[category] = value;
2908
+ contributedThresholdKeys.push(category);
2909
+ } else if (!deepEqual(mergedThresholds[category], value)) {
2910
+ conflicts.push({
2911
+ section: "architecture.thresholds",
2912
+ key: category,
2913
+ localValue: mergedThresholds[category],
2914
+ packageValue: value,
2915
+ description: `Architecture threshold '${category}' already exists locally with a different value`
2916
+ });
2806
2917
  }
2807
2918
  }
2808
- if (bundleConstraints.forbiddenImports && bundleConstraints.forbiddenImports.length > 0) {
2809
- const localFI = Array.isArray(localConfig.forbiddenImports) ? localConfig.forbiddenImports : [];
2810
- const mergedFI = [...localFI];
2811
- const contributedFromKeys = [];
2812
- for (const bundleRule of bundleConstraints.forbiddenImports) {
2813
- const existing = localFI.find((r) => r.from === bundleRule.from);
2814
- if (!existing) {
2815
- const entry = {
2816
- from: bundleRule.from,
2817
- disallow: bundleRule.disallow
2818
- };
2819
- if (bundleRule.message !== void 0) {
2820
- entry.message = bundleRule.message;
2821
- }
2822
- mergedFI.push(entry);
2823
- contributedFromKeys.push(bundleRule.from);
2824
- } else {
2825
- const same = stringArraysEqual(existing.disallow, bundleRule.disallow);
2826
- if (!same) {
2919
+ const mergedModules = { ...localArch.modules };
2920
+ const contributedModuleKeys = [];
2921
+ for (const [modulePath, bundleCategoryMap] of Object.entries(bundleArch.modules ?? {})) {
2922
+ if (!(modulePath in mergedModules)) {
2923
+ mergedModules[modulePath] = bundleCategoryMap;
2924
+ for (const cat of Object.keys(bundleCategoryMap))
2925
+ contributedModuleKeys.push(`${modulePath}:${cat}`);
2926
+ } else {
2927
+ const mergedCategoryMap = { ...mergedModules[modulePath] };
2928
+ for (const [category, value] of Object.entries(bundleCategoryMap)) {
2929
+ if (!(category in mergedCategoryMap)) {
2930
+ mergedCategoryMap[category] = value;
2931
+ contributedModuleKeys.push(`${modulePath}:${category}`);
2932
+ } else if (!deepEqual(mergedCategoryMap[category], value)) {
2827
2933
  conflicts.push({
2828
- section: "forbiddenImports",
2829
- key: bundleRule.from,
2830
- localValue: existing,
2831
- packageValue: bundleRule,
2832
- description: `Forbidden import rule for '${bundleRule.from}' already exists locally with different disallow list`
2934
+ section: "architecture.modules",
2935
+ key: `${modulePath}:${category}`,
2936
+ localValue: mergedCategoryMap[category],
2937
+ packageValue: value,
2938
+ description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
2833
2939
  });
2834
2940
  }
2835
2941
  }
2942
+ mergedModules[modulePath] = mergedCategoryMap;
2943
+ }
2944
+ }
2945
+ config.architecture = { ...localArch, thresholds: mergedThresholds, modules: mergedModules };
2946
+ if (contributedThresholdKeys.length > 0)
2947
+ contributions["architecture.thresholds"] = contributedThresholdKeys;
2948
+ if (contributedModuleKeys.length > 0)
2949
+ contributions["architecture.modules"] = contributedModuleKeys;
2950
+ }
2951
+ function mergeSecurityRules(localConfig, bundleRules, config, contributions, conflicts) {
2952
+ const localSecurity = localConfig.security ?? { rules: {} };
2953
+ const localRules = localSecurity.rules ?? {};
2954
+ const mergedRules = { ...localRules };
2955
+ const contributedRuleIds = [];
2956
+ for (const [ruleId, severity] of Object.entries(bundleRules)) {
2957
+ if (!(ruleId in mergedRules)) {
2958
+ mergedRules[ruleId] = severity;
2959
+ contributedRuleIds.push(ruleId);
2960
+ } else if (mergedRules[ruleId] !== severity) {
2961
+ conflicts.push({
2962
+ section: "security.rules",
2963
+ key: ruleId,
2964
+ localValue: mergedRules[ruleId],
2965
+ packageValue: severity,
2966
+ description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
2967
+ });
2836
2968
  }
2837
- config.forbiddenImports = mergedFI;
2838
- if (contributedFromKeys.length > 0) {
2839
- contributions.forbiddenImports = contributedFromKeys;
2840
- }
2969
+ }
2970
+ config.security = { ...localSecurity, rules: mergedRules };
2971
+ if (contributedRuleIds.length > 0) contributions["security.rules"] = contributedRuleIds;
2972
+ }
2973
+ function deepMergeConstraints(localConfig, bundleConstraints, _existingContributions) {
2974
+ const config = { ...localConfig };
2975
+ const contributions = {};
2976
+ const conflicts = [];
2977
+ if (bundleConstraints.layers && bundleConstraints.layers.length > 0) {
2978
+ mergeLayers(localConfig, bundleConstraints.layers, config, contributions, conflicts);
2979
+ }
2980
+ if (bundleConstraints.forbiddenImports && bundleConstraints.forbiddenImports.length > 0) {
2981
+ mergeForbiddenImports(
2982
+ localConfig,
2983
+ bundleConstraints.forbiddenImports,
2984
+ config,
2985
+ contributions,
2986
+ conflicts
2987
+ );
2841
2988
  }
2842
2989
  if (bundleConstraints.boundaries) {
2843
- const localBoundaries = localConfig.boundaries ?? { requireSchema: [] };
2844
- const localSchemas = new Set(localBoundaries.requireSchema ?? []);
2845
- const bundleSchemas = bundleConstraints.boundaries.requireSchema ?? [];
2846
- const newSchemas = [];
2847
- for (const schema of bundleSchemas) {
2848
- if (!localSchemas.has(schema)) {
2849
- newSchemas.push(schema);
2850
- localSchemas.add(schema);
2851
- }
2852
- }
2853
- config.boundaries = {
2854
- requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas]
2855
- };
2856
- if (newSchemas.length > 0) {
2857
- contributions.boundaries = newSchemas;
2858
- }
2990
+ mergeBoundaries(
2991
+ localConfig,
2992
+ bundleConstraints.boundaries,
2993
+ config,
2994
+ contributions
2995
+ );
2859
2996
  }
2860
2997
  if (bundleConstraints.architecture) {
2861
- const localArch = localConfig.architecture ?? {
2862
- thresholds: {},
2863
- modules: {}
2864
- };
2865
- const mergedThresholds = { ...localArch.thresholds };
2866
- const contributedThresholdKeys = [];
2867
- const bundleThresholds = bundleConstraints.architecture.thresholds ?? {};
2868
- for (const [category, value] of Object.entries(bundleThresholds)) {
2869
- if (!(category in mergedThresholds)) {
2870
- mergedThresholds[category] = value;
2871
- contributedThresholdKeys.push(category);
2872
- } else if (!deepEqual(mergedThresholds[category], value)) {
2873
- conflicts.push({
2874
- section: "architecture.thresholds",
2875
- key: category,
2876
- localValue: mergedThresholds[category],
2877
- packageValue: value,
2878
- description: `Architecture threshold '${category}' already exists locally with a different value`
2879
- });
2880
- }
2881
- }
2882
- const mergedModules = { ...localArch.modules };
2883
- const contributedModuleKeys = [];
2884
- const bundleModules = bundleConstraints.architecture.modules ?? {};
2885
- for (const [modulePath, bundleCategoryMap] of Object.entries(bundleModules)) {
2886
- if (!(modulePath in mergedModules)) {
2887
- mergedModules[modulePath] = bundleCategoryMap;
2888
- for (const cat of Object.keys(bundleCategoryMap)) {
2889
- contributedModuleKeys.push(`${modulePath}:${cat}`);
2890
- }
2891
- } else {
2892
- const localCategoryMap = mergedModules[modulePath];
2893
- const mergedCategoryMap = { ...localCategoryMap };
2894
- for (const [category, value] of Object.entries(bundleCategoryMap)) {
2895
- if (!(category in mergedCategoryMap)) {
2896
- mergedCategoryMap[category] = value;
2897
- contributedModuleKeys.push(`${modulePath}:${category}`);
2898
- } else if (!deepEqual(mergedCategoryMap[category], value)) {
2899
- conflicts.push({
2900
- section: "architecture.modules",
2901
- key: `${modulePath}:${category}`,
2902
- localValue: mergedCategoryMap[category],
2903
- packageValue: value,
2904
- description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
2905
- });
2906
- }
2907
- }
2908
- mergedModules[modulePath] = mergedCategoryMap;
2909
- }
2910
- }
2911
- config.architecture = {
2912
- ...localArch,
2913
- thresholds: mergedThresholds,
2914
- modules: mergedModules
2915
- };
2916
- if (contributedThresholdKeys.length > 0) {
2917
- contributions["architecture.thresholds"] = contributedThresholdKeys;
2918
- }
2919
- if (contributedModuleKeys.length > 0) {
2920
- contributions["architecture.modules"] = contributedModuleKeys;
2921
- }
2998
+ mergeArchitecture(
2999
+ localConfig,
3000
+ bundleConstraints.architecture,
3001
+ config,
3002
+ contributions,
3003
+ conflicts
3004
+ );
2922
3005
  }
2923
3006
  if (bundleConstraints.security?.rules) {
2924
- const localSecurity = localConfig.security ?? { rules: {} };
2925
- const localRules = localSecurity.rules ?? {};
2926
- const mergedRules = { ...localRules };
2927
- const contributedRuleIds = [];
2928
- for (const [ruleId, severity] of Object.entries(bundleConstraints.security.rules)) {
2929
- if (!(ruleId in mergedRules)) {
2930
- mergedRules[ruleId] = severity;
2931
- contributedRuleIds.push(ruleId);
2932
- } else if (mergedRules[ruleId] !== severity) {
2933
- conflicts.push({
2934
- section: "security.rules",
2935
- key: ruleId,
2936
- localValue: mergedRules[ruleId],
2937
- packageValue: severity,
2938
- description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
2939
- });
2940
- }
2941
- }
2942
- config.security = { ...localSecurity, rules: mergedRules };
2943
- if (contributedRuleIds.length > 0) {
2944
- contributions["security.rules"] = contributedRuleIds;
2945
- }
3007
+ mergeSecurityRules(
3008
+ localConfig,
3009
+ bundleConstraints.security.rules,
3010
+ config,
3011
+ contributions,
3012
+ conflicts
3013
+ );
2946
3014
  }
2947
3015
  return { config, contributions, conflicts };
2948
3016
  }
@@ -3091,14 +3159,84 @@ function walk(node, visitor) {
3091
3159
  }
3092
3160
  }
3093
3161
  }
3162
+ function makeLocation(node) {
3163
+ return {
3164
+ file: "",
3165
+ line: node.loc?.start.line ?? 0,
3166
+ column: node.loc?.start.column ?? 0
3167
+ };
3168
+ }
3169
+ function processImportSpecifiers(importDecl, imp) {
3170
+ for (const spec of importDecl.specifiers) {
3171
+ if (spec.type === "ImportDefaultSpecifier") {
3172
+ imp.default = spec.local.name;
3173
+ } else if (spec.type === "ImportNamespaceSpecifier") {
3174
+ imp.namespace = spec.local.name;
3175
+ } else if (spec.type === "ImportSpecifier") {
3176
+ imp.specifiers.push(spec.local.name);
3177
+ if (spec.importKind === "type") {
3178
+ imp.kind = "type";
3179
+ }
3180
+ }
3181
+ }
3182
+ }
3183
+ function getExportedName(exported) {
3184
+ return exported.type === "Identifier" ? exported.name : String(exported.value);
3185
+ }
3186
+ function processReExportSpecifiers(exportDecl, exports) {
3187
+ for (const spec of exportDecl.specifiers) {
3188
+ if (spec.type !== "ExportSpecifier") continue;
3189
+ exports.push({
3190
+ name: getExportedName(spec.exported),
3191
+ type: "named",
3192
+ location: makeLocation(exportDecl),
3193
+ isReExport: true,
3194
+ source: exportDecl.source.value
3195
+ });
3196
+ }
3197
+ }
3198
+ function processExportDeclaration(exportDecl, exports) {
3199
+ const decl = exportDecl.declaration;
3200
+ if (!decl) return;
3201
+ if (decl.type === "VariableDeclaration") {
3202
+ for (const declarator of decl.declarations) {
3203
+ if (declarator.id.type === "Identifier") {
3204
+ exports.push({
3205
+ name: declarator.id.name,
3206
+ type: "named",
3207
+ location: makeLocation(decl),
3208
+ isReExport: false
3209
+ });
3210
+ }
3211
+ }
3212
+ } else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
3213
+ exports.push({
3214
+ name: decl.id.name,
3215
+ type: "named",
3216
+ location: makeLocation(decl),
3217
+ isReExport: false
3218
+ });
3219
+ }
3220
+ }
3221
+ function processExportListSpecifiers(exportDecl, exports) {
3222
+ for (const spec of exportDecl.specifiers) {
3223
+ if (spec.type !== "ExportSpecifier") continue;
3224
+ exports.push({
3225
+ name: getExportedName(spec.exported),
3226
+ type: "named",
3227
+ location: makeLocation(exportDecl),
3228
+ isReExport: false
3229
+ });
3230
+ }
3231
+ }
3094
3232
  var TypeScriptParser = class {
3095
3233
  name = "typescript";
3096
3234
  extensions = [".ts", ".tsx", ".mts", ".cts"];
3097
- async parseFile(path20) {
3098
- const contentResult = await readFileContent(path20);
3235
+ async parseFile(path22) {
3236
+ const contentResult = await readFileContent(path22);
3099
3237
  if (!contentResult.ok) {
3100
3238
  return Err(
3101
- createParseError("NOT_FOUND", `File not found: ${path20}`, { path: path20 }, [
3239
+ createParseError("NOT_FOUND", `File not found: ${path22}`, { path: path22 }, [
3102
3240
  "Check that the file exists",
3103
3241
  "Verify the path is correct"
3104
3242
  ])
@@ -3108,7 +3246,7 @@ var TypeScriptParser = class {
3108
3246
  const ast = parse(contentResult.value, {
3109
3247
  loc: true,
3110
3248
  range: true,
3111
- jsx: path20.endsWith(".tsx"),
3249
+ jsx: path22.endsWith(".tsx"),
3112
3250
  errorOnUnknownASTType: false
3113
3251
  });
3114
3252
  return Ok({
@@ -3119,7 +3257,7 @@ var TypeScriptParser = class {
3119
3257
  } catch (e) {
3120
3258
  const error = e;
3121
3259
  return Err(
3122
- createParseError("SYNTAX_ERROR", `Failed to parse ${path20}: ${error.message}`, { path: path20 }, [
3260
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path22}: ${error.message}`, { path: path22 }, [
3123
3261
  "Check for syntax errors in the file",
3124
3262
  "Ensure valid TypeScript syntax"
3125
3263
  ])
@@ -3135,26 +3273,12 @@ var TypeScriptParser = class {
3135
3273
  const imp = {
3136
3274
  source: importDecl.source.value,
3137
3275
  specifiers: [],
3138
- location: {
3139
- file: "",
3140
- line: importDecl.loc?.start.line ?? 0,
3141
- column: importDecl.loc?.start.column ?? 0
3142
- },
3276
+ location: makeLocation(importDecl),
3143
3277
  kind: importDecl.importKind === "type" ? "type" : "value"
3144
3278
  };
3145
- for (const spec of importDecl.specifiers) {
3146
- if (spec.type === "ImportDefaultSpecifier") {
3147
- imp.default = spec.local.name;
3148
- } else if (spec.type === "ImportNamespaceSpecifier") {
3149
- imp.namespace = spec.local.name;
3150
- } else if (spec.type === "ImportSpecifier") {
3151
- imp.specifiers.push(spec.local.name);
3152
- if (spec.importKind === "type") {
3153
- imp.kind = "type";
3154
- }
3155
- }
3156
- }
3279
+ processImportSpecifiers(importDecl, imp);
3157
3280
  imports.push(imp);
3281
+ return;
3158
3282
  }
3159
3283
  if (node.type === "ImportExpression") {
3160
3284
  const importExpr = node;
@@ -3162,11 +3286,7 @@ var TypeScriptParser = class {
3162
3286
  imports.push({
3163
3287
  source: importExpr.source.value,
3164
3288
  specifiers: [],
3165
- location: {
3166
- file: "",
3167
- line: importExpr.loc?.start.line ?? 0,
3168
- column: importExpr.loc?.start.column ?? 0
3169
- },
3289
+ location: makeLocation(importExpr),
3170
3290
  kind: "value"
3171
3291
  });
3172
3292
  }
@@ -3181,97 +3301,29 @@ var TypeScriptParser = class {
3181
3301
  if (node.type === "ExportNamedDeclaration") {
3182
3302
  const exportDecl = node;
3183
3303
  if (exportDecl.source) {
3184
- for (const spec of exportDecl.specifiers) {
3185
- if (spec.type === "ExportSpecifier") {
3186
- const exported = spec.exported;
3187
- const name = exported.type === "Identifier" ? exported.name : String(exported.value);
3188
- exports.push({
3189
- name,
3190
- type: "named",
3191
- location: {
3192
- file: "",
3193
- line: exportDecl.loc?.start.line ?? 0,
3194
- column: exportDecl.loc?.start.column ?? 0
3195
- },
3196
- isReExport: true,
3197
- source: exportDecl.source.value
3198
- });
3199
- }
3200
- }
3304
+ processReExportSpecifiers(exportDecl, exports);
3201
3305
  return;
3202
3306
  }
3203
- if (exportDecl.declaration) {
3204
- const decl = exportDecl.declaration;
3205
- if (decl.type === "VariableDeclaration") {
3206
- for (const declarator of decl.declarations) {
3207
- if (declarator.id.type === "Identifier") {
3208
- exports.push({
3209
- name: declarator.id.name,
3210
- type: "named",
3211
- location: {
3212
- file: "",
3213
- line: decl.loc?.start.line ?? 0,
3214
- column: decl.loc?.start.column ?? 0
3215
- },
3216
- isReExport: false
3217
- });
3218
- }
3219
- }
3220
- } else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
3221
- if (decl.id) {
3222
- exports.push({
3223
- name: decl.id.name,
3224
- type: "named",
3225
- location: {
3226
- file: "",
3227
- line: decl.loc?.start.line ?? 0,
3228
- column: decl.loc?.start.column ?? 0
3229
- },
3230
- isReExport: false
3231
- });
3232
- }
3233
- }
3234
- }
3235
- for (const spec of exportDecl.specifiers) {
3236
- if (spec.type === "ExportSpecifier") {
3237
- const exported = spec.exported;
3238
- const name = exported.type === "Identifier" ? exported.name : String(exported.value);
3239
- exports.push({
3240
- name,
3241
- type: "named",
3242
- location: {
3243
- file: "",
3244
- line: exportDecl.loc?.start.line ?? 0,
3245
- column: exportDecl.loc?.start.column ?? 0
3246
- },
3247
- isReExport: false
3248
- });
3249
- }
3250
- }
3307
+ processExportDeclaration(exportDecl, exports);
3308
+ processExportListSpecifiers(exportDecl, exports);
3309
+ return;
3251
3310
  }
3252
3311
  if (node.type === "ExportDefaultDeclaration") {
3253
3312
  const exportDecl = node;
3254
3313
  exports.push({
3255
3314
  name: "default",
3256
3315
  type: "default",
3257
- location: {
3258
- file: "",
3259
- line: exportDecl.loc?.start.line ?? 0,
3260
- column: exportDecl.loc?.start.column ?? 0
3261
- },
3316
+ location: makeLocation(exportDecl),
3262
3317
  isReExport: false
3263
3318
  });
3319
+ return;
3264
3320
  }
3265
3321
  if (node.type === "ExportAllDeclaration") {
3266
3322
  const exportDecl = node;
3267
3323
  exports.push({
3268
3324
  name: exportDecl.exported?.name ?? "*",
3269
3325
  type: "namespace",
3270
- location: {
3271
- file: "",
3272
- line: exportDecl.loc?.start.line ?? 0,
3273
- column: exportDecl.loc?.start.column ?? 0
3274
- },
3326
+ location: makeLocation(exportDecl),
3275
3327
  isReExport: true,
3276
3328
  source: exportDecl.source.value
3277
3329
  });
@@ -3283,10 +3335,27 @@ var TypeScriptParser = class {
3283
3335
  return Ok({ available: true, version: "7.0.0" });
3284
3336
  }
3285
3337
  };
3338
+ function collectFieldEntries(rootDir, field) {
3339
+ if (typeof field === "string") return [resolve3(rootDir, field)];
3340
+ if (typeof field === "object" && field !== null) {
3341
+ return Object.values(field).filter((v) => typeof v === "string").map((v) => resolve3(rootDir, v));
3342
+ }
3343
+ return [];
3344
+ }
3345
+ function extractPackageEntries(rootDir, pkg) {
3346
+ const entries = [];
3347
+ entries.push(...collectFieldEntries(rootDir, pkg["exports"]));
3348
+ if (entries.length === 0 && typeof pkg["main"] === "string") {
3349
+ entries.push(resolve3(rootDir, pkg["main"]));
3350
+ }
3351
+ if (pkg["bin"]) {
3352
+ entries.push(...collectFieldEntries(rootDir, pkg["bin"]));
3353
+ }
3354
+ return entries;
3355
+ }
3286
3356
  async function resolveEntryPoints(rootDir, explicitEntries) {
3287
3357
  if (explicitEntries && explicitEntries.length > 0) {
3288
- const resolved = explicitEntries.map((e) => resolve3(rootDir, e));
3289
- return Ok(resolved);
3358
+ return Ok(explicitEntries.map((e) => resolve3(rootDir, e)));
3290
3359
  }
3291
3360
  const pkgPath = join32(rootDir, "package.json");
3292
3361
  if (await fileExists(pkgPath)) {
@@ -3294,38 +3363,8 @@ async function resolveEntryPoints(rootDir, explicitEntries) {
3294
3363
  if (pkgContent.ok) {
3295
3364
  try {
3296
3365
  const pkg = JSON.parse(pkgContent.value);
3297
- const entries = [];
3298
- if (pkg["exports"]) {
3299
- const exports = pkg["exports"];
3300
- if (typeof exports === "string") {
3301
- entries.push(resolve3(rootDir, exports));
3302
- } else if (typeof exports === "object" && exports !== null) {
3303
- for (const value of Object.values(exports)) {
3304
- if (typeof value === "string") {
3305
- entries.push(resolve3(rootDir, value));
3306
- }
3307
- }
3308
- }
3309
- }
3310
- const main = pkg["main"];
3311
- if (typeof main === "string" && entries.length === 0) {
3312
- entries.push(resolve3(rootDir, main));
3313
- }
3314
- const bin = pkg["bin"];
3315
- if (bin) {
3316
- if (typeof bin === "string") {
3317
- entries.push(resolve3(rootDir, bin));
3318
- } else if (typeof bin === "object") {
3319
- for (const value of Object.values(bin)) {
3320
- if (typeof value === "string") {
3321
- entries.push(resolve3(rootDir, value));
3322
- }
3323
- }
3324
- }
3325
- }
3326
- if (entries.length > 0) {
3327
- return Ok(entries);
3328
- }
3366
+ const entries = extractPackageEntries(rootDir, pkg);
3367
+ if (entries.length > 0) return Ok(entries);
3329
3368
  } catch {
3330
3369
  }
3331
3370
  }
@@ -3399,66 +3438,49 @@ function extractInlineRefs(content) {
3399
3438
  }
3400
3439
  return refs;
3401
3440
  }
3402
- async function parseDocumentationFile(path20) {
3403
- const contentResult = await readFileContent(path20);
3441
+ async function parseDocumentationFile(path22) {
3442
+ const contentResult = await readFileContent(path22);
3404
3443
  if (!contentResult.ok) {
3405
3444
  return Err(
3406
3445
  createEntropyError(
3407
3446
  "PARSE_ERROR",
3408
- `Failed to read documentation file: ${path20}`,
3409
- { file: path20 },
3447
+ `Failed to read documentation file: ${path22}`,
3448
+ { file: path22 },
3410
3449
  ["Check that the file exists"]
3411
3450
  )
3412
3451
  );
3413
3452
  }
3414
3453
  const content = contentResult.value;
3415
- const type = path20.endsWith(".md") ? "markdown" : "text";
3454
+ const type = path22.endsWith(".md") ? "markdown" : "text";
3416
3455
  return Ok({
3417
- path: path20,
3456
+ path: path22,
3418
3457
  type,
3419
3458
  content,
3420
3459
  codeBlocks: extractCodeBlocks(content),
3421
3460
  inlineRefs: extractInlineRefs(content)
3422
3461
  });
3423
3462
  }
3463
+ function makeInternalSymbol(name, type, line) {
3464
+ return { name, type, line, references: 0, calledBy: [] };
3465
+ }
3466
+ function extractSymbolsFromNode(node) {
3467
+ const line = node.loc?.start?.line || 0;
3468
+ if (node.type === "FunctionDeclaration" && node.id?.name) {
3469
+ return [makeInternalSymbol(node.id.name, "function", line)];
3470
+ }
3471
+ if (node.type === "VariableDeclaration") {
3472
+ return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
3473
+ }
3474
+ if (node.type === "ClassDeclaration" && node.id?.name) {
3475
+ return [makeInternalSymbol(node.id.name, "class", line)];
3476
+ }
3477
+ return [];
3478
+ }
3424
3479
  function extractInternalSymbols(ast) {
3425
- const symbols = [];
3426
3480
  const body = ast.body;
3427
- if (!body?.body) return symbols;
3428
- for (const node of body.body) {
3429
- if (node.type === "FunctionDeclaration" && node.id?.name) {
3430
- symbols.push({
3431
- name: node.id.name,
3432
- type: "function",
3433
- line: node.loc?.start?.line || 0,
3434
- references: 0,
3435
- calledBy: []
3436
- });
3437
- }
3438
- if (node.type === "VariableDeclaration") {
3439
- for (const decl of node.declarations || []) {
3440
- if (decl.id?.name) {
3441
- symbols.push({
3442
- name: decl.id.name,
3443
- type: "variable",
3444
- line: node.loc?.start?.line || 0,
3445
- references: 0,
3446
- calledBy: []
3447
- });
3448
- }
3449
- }
3450
- }
3451
- if (node.type === "ClassDeclaration" && node.id?.name) {
3452
- symbols.push({
3453
- name: node.id.name,
3454
- type: "class",
3455
- line: node.loc?.start?.line || 0,
3456
- references: 0,
3457
- calledBy: []
3458
- });
3459
- }
3460
- }
3461
- return symbols;
3481
+ if (!body?.body) return [];
3482
+ const nodes = body.body;
3483
+ return nodes.flatMap(extractSymbolsFromNode);
3462
3484
  }
3463
3485
  function extractJSDocComments(ast) {
3464
3486
  const comments = [];
@@ -3596,27 +3618,34 @@ async function buildSnapshot(config) {
3596
3618
  buildTime
3597
3619
  });
3598
3620
  }
3599
- function levenshteinDistance(a, b) {
3621
+ function initLevenshteinMatrix(aLen, bLen) {
3600
3622
  const matrix = [];
3601
- for (let i = 0; i <= b.length; i++) {
3623
+ for (let i = 0; i <= bLen; i++) {
3602
3624
  matrix[i] = [i];
3603
3625
  }
3604
- for (let j = 0; j <= a.length; j++) {
3605
- const row = matrix[0];
3606
- if (row) {
3607
- row[j] = j;
3626
+ const firstRow = matrix[0];
3627
+ if (firstRow) {
3628
+ for (let j = 0; j <= aLen; j++) {
3629
+ firstRow[j] = j;
3608
3630
  }
3609
3631
  }
3632
+ return matrix;
3633
+ }
3634
+ function computeLevenshteinCell(row, prevRow, j, charsMatch) {
3635
+ if (charsMatch) {
3636
+ row[j] = prevRow[j - 1] ?? 0;
3637
+ } else {
3638
+ row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
3639
+ }
3640
+ }
3641
+ function levenshteinDistance(a, b) {
3642
+ const matrix = initLevenshteinMatrix(a.length, b.length);
3610
3643
  for (let i = 1; i <= b.length; i++) {
3611
3644
  for (let j = 1; j <= a.length; j++) {
3612
3645
  const row = matrix[i];
3613
3646
  const prevRow = matrix[i - 1];
3614
3647
  if (!row || !prevRow) continue;
3615
- if (b.charAt(i - 1) === a.charAt(j - 1)) {
3616
- row[j] = prevRow[j - 1] ?? 0;
3617
- } else {
3618
- row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
3619
- }
3648
+ computeLevenshteinCell(row, prevRow, j, b.charAt(i - 1) === a.charAt(j - 1));
3620
3649
  }
3621
3650
  }
3622
3651
  const lastRow = matrix[b.length];
@@ -3899,32 +3928,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
3899
3928
  }
3900
3929
  return deadExports;
3901
3930
  }
3902
- function countLinesFromAST(ast) {
3903
- if (ast.body && Array.isArray(ast.body)) {
3904
- let maxLine = 0;
3905
- const traverse = (node) => {
3906
- if (node && typeof node === "object") {
3907
- const n = node;
3908
- if (n.loc?.end?.line && n.loc.end.line > maxLine) {
3909
- maxLine = n.loc.end.line;
3910
- }
3911
- for (const key of Object.keys(node)) {
3912
- const value = node[key];
3913
- if (Array.isArray(value)) {
3914
- for (const item of value) {
3915
- traverse(item);
3916
- }
3917
- } else if (value && typeof value === "object") {
3918
- traverse(value);
3919
- }
3920
- }
3931
+ function findMaxLineInNode(node) {
3932
+ if (!node || typeof node !== "object") return 0;
3933
+ const n = node;
3934
+ let maxLine = n.loc?.end?.line ?? 0;
3935
+ for (const key of Object.keys(node)) {
3936
+ const value = node[key];
3937
+ if (Array.isArray(value)) {
3938
+ for (const item of value) {
3939
+ maxLine = Math.max(maxLine, findMaxLineInNode(item));
3921
3940
  }
3922
- };
3923
- traverse(ast);
3924
- if (maxLine > 0) return maxLine;
3925
- return Math.max(ast.body.length * 3, 1);
3941
+ } else if (value && typeof value === "object") {
3942
+ maxLine = Math.max(maxLine, findMaxLineInNode(value));
3943
+ }
3926
3944
  }
3927
- return 1;
3945
+ return maxLine;
3946
+ }
3947
+ function countLinesFromAST(ast) {
3948
+ if (!ast.body || !Array.isArray(ast.body)) return 1;
3949
+ const maxLine = findMaxLineInNode(ast);
3950
+ if (maxLine > 0) return maxLine;
3951
+ return Math.max(ast.body.length * 3, 1);
3928
3952
  }
3929
3953
  function findDeadFiles(snapshot, reachability) {
3930
3954
  const deadFiles = [];
@@ -4072,130 +4096,146 @@ function fileMatchesPattern(filePath, pattern, rootDir) {
4072
4096
  const relativePath = relativePosix(rootDir, filePath);
4073
4097
  return minimatch3(relativePath, pattern);
4074
4098
  }
4075
- function checkConfigPattern(pattern, file, rootDir) {
4099
+ var CONVENTION_DESCRIPTIONS = {
4100
+ camelCase: "camelCase (e.g., myFunction)",
4101
+ PascalCase: "PascalCase (e.g., MyClass)",
4102
+ UPPER_SNAKE: "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)",
4103
+ "kebab-case": "kebab-case (e.g., my-component)"
4104
+ };
4105
+ function checkMustExport(rule, file, message) {
4106
+ if (rule.type !== "must-export") return [];
4076
4107
  const matches = [];
4077
- const fileMatches = pattern.files.some((glob2) => fileMatchesPattern(file.path, glob2, rootDir));
4078
- if (!fileMatches) {
4079
- return matches;
4080
- }
4081
- const rule = pattern.rule;
4082
- switch (rule.type) {
4083
- case "must-export": {
4084
- for (const name of rule.names) {
4085
- const hasExport = file.exports.some((e) => e.name === name);
4086
- if (!hasExport) {
4087
- matches.push({
4088
- line: 1,
4089
- message: pattern.message || `Missing required export: "${name}"`,
4090
- suggestion: `Add export for "${name}"`
4091
- });
4092
- }
4093
- }
4094
- break;
4095
- }
4096
- case "must-export-default": {
4097
- const hasDefault = file.exports.some((e) => e.type === "default");
4098
- if (!hasDefault) {
4099
- matches.push({
4100
- line: 1,
4101
- message: pattern.message || "File must have a default export",
4102
- suggestion: "Add a default export"
4103
- });
4104
- }
4105
- break;
4106
- }
4107
- case "no-export": {
4108
- for (const name of rule.names) {
4109
- const exp = file.exports.find((e) => e.name === name);
4110
- if (exp) {
4111
- matches.push({
4112
- line: exp.location.line,
4113
- message: pattern.message || `Forbidden export: "${name}"`,
4114
- suggestion: `Remove export "${name}"`
4115
- });
4116
- }
4117
- }
4118
- break;
4108
+ for (const name of rule.names) {
4109
+ if (!file.exports.some((e) => e.name === name)) {
4110
+ matches.push({
4111
+ line: 1,
4112
+ message: message || `Missing required export: "${name}"`,
4113
+ suggestion: `Add export for "${name}"`
4114
+ });
4119
4115
  }
4120
- case "must-import": {
4121
- const hasImport = file.imports.some(
4122
- (i) => i.source === rule.from || i.source.endsWith(rule.from)
4123
- );
4124
- if (!hasImport) {
4125
- matches.push({
4126
- line: 1,
4127
- message: pattern.message || `Missing required import from "${rule.from}"`,
4128
- suggestion: `Add import from "${rule.from}"`
4129
- });
4116
+ }
4117
+ return matches;
4118
+ }
4119
+ function checkMustExportDefault(_rule, file, message) {
4120
+ if (!file.exports.some((e) => e.type === "default")) {
4121
+ return [
4122
+ {
4123
+ line: 1,
4124
+ message: message || "File must have a default export",
4125
+ suggestion: "Add a default export"
4130
4126
  }
4131
- break;
4127
+ ];
4128
+ }
4129
+ return [];
4130
+ }
4131
+ function checkNoExport(rule, file, message) {
4132
+ if (rule.type !== "no-export") return [];
4133
+ const matches = [];
4134
+ for (const name of rule.names) {
4135
+ const exp = file.exports.find((e) => e.name === name);
4136
+ if (exp) {
4137
+ matches.push({
4138
+ line: exp.location.line,
4139
+ message: message || `Forbidden export: "${name}"`,
4140
+ suggestion: `Remove export "${name}"`
4141
+ });
4132
4142
  }
4133
- case "no-import": {
4134
- const forbiddenImport = file.imports.find(
4135
- (i) => i.source === rule.from || i.source.endsWith(rule.from)
4136
- );
4137
- if (forbiddenImport) {
4138
- matches.push({
4139
- line: forbiddenImport.location.line,
4140
- message: pattern.message || `Forbidden import from "${rule.from}"`,
4141
- suggestion: `Remove import from "${rule.from}"`
4142
- });
4143
+ }
4144
+ return matches;
4145
+ }
4146
+ function checkMustImport(rule, file, message) {
4147
+ if (rule.type !== "must-import") return [];
4148
+ const hasImport = file.imports.some(
4149
+ (i) => i.source === rule.from || i.source.endsWith(rule.from)
4150
+ );
4151
+ if (!hasImport) {
4152
+ return [
4153
+ {
4154
+ line: 1,
4155
+ message: message || `Missing required import from "${rule.from}"`,
4156
+ suggestion: `Add import from "${rule.from}"`
4143
4157
  }
4144
- break;
4145
- }
4146
- case "naming": {
4147
- const regex = new RegExp(rule.match);
4148
- for (const exp of file.exports) {
4149
- if (!regex.test(exp.name)) {
4150
- let expected = "";
4151
- switch (rule.convention) {
4152
- case "camelCase":
4153
- expected = "camelCase (e.g., myFunction)";
4154
- break;
4155
- case "PascalCase":
4156
- expected = "PascalCase (e.g., MyClass)";
4157
- break;
4158
- case "UPPER_SNAKE":
4159
- expected = "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)";
4160
- break;
4161
- case "kebab-case":
4162
- expected = "kebab-case (e.g., my-component)";
4163
- break;
4164
- }
4165
- matches.push({
4166
- line: exp.location.line,
4167
- message: pattern.message || `"${exp.name}" does not follow ${rule.convention} convention`,
4168
- suggestion: `Rename to follow ${expected}`
4169
- });
4170
- }
4158
+ ];
4159
+ }
4160
+ return [];
4161
+ }
4162
+ function checkNoImport(rule, file, message) {
4163
+ if (rule.type !== "no-import") return [];
4164
+ const forbiddenImport = file.imports.find(
4165
+ (i) => i.source === rule.from || i.source.endsWith(rule.from)
4166
+ );
4167
+ if (forbiddenImport) {
4168
+ return [
4169
+ {
4170
+ line: forbiddenImport.location.line,
4171
+ message: message || `Forbidden import from "${rule.from}"`,
4172
+ suggestion: `Remove import from "${rule.from}"`
4171
4173
  }
4172
- break;
4174
+ ];
4175
+ }
4176
+ return [];
4177
+ }
4178
+ function checkNaming(rule, file, message) {
4179
+ if (rule.type !== "naming") return [];
4180
+ const regex = new RegExp(rule.match);
4181
+ const matches = [];
4182
+ for (const exp of file.exports) {
4183
+ if (!regex.test(exp.name)) {
4184
+ const expected = CONVENTION_DESCRIPTIONS[rule.convention] ?? rule.convention;
4185
+ matches.push({
4186
+ line: exp.location.line,
4187
+ message: message || `"${exp.name}" does not follow ${rule.convention} convention`,
4188
+ suggestion: `Rename to follow ${expected}`
4189
+ });
4173
4190
  }
4174
- case "max-exports": {
4175
- if (file.exports.length > rule.count) {
4176
- matches.push({
4177
- line: 1,
4178
- message: pattern.message || `File has ${file.exports.length} exports, max is ${rule.count}`,
4179
- suggestion: `Split into multiple files or reduce exports to ${rule.count}`
4180
- });
4191
+ }
4192
+ return matches;
4193
+ }
4194
+ function checkMaxExports(rule, file, message) {
4195
+ if (rule.type !== "max-exports") return [];
4196
+ if (file.exports.length > rule.count) {
4197
+ return [
4198
+ {
4199
+ line: 1,
4200
+ message: message || `File has ${file.exports.length} exports, max is ${rule.count}`,
4201
+ suggestion: `Split into multiple files or reduce exports to ${rule.count}`
4181
4202
  }
4182
- break;
4183
- }
4184
- case "max-lines": {
4185
- break;
4186
- }
4187
- case "require-jsdoc": {
4188
- if (file.jsDocComments.length === 0 && file.exports.length > 0) {
4189
- matches.push({
4190
- line: 1,
4191
- message: pattern.message || "Exported symbols require JSDoc documentation",
4192
- suggestion: "Add JSDoc comments to exports"
4193
- });
4203
+ ];
4204
+ }
4205
+ return [];
4206
+ }
4207
+ function checkMaxLines(_rule, _file, _message) {
4208
+ return [];
4209
+ }
4210
+ function checkRequireJsdoc(_rule, file, message) {
4211
+ if (file.jsDocComments.length === 0 && file.exports.length > 0) {
4212
+ return [
4213
+ {
4214
+ line: 1,
4215
+ message: message || "Exported symbols require JSDoc documentation",
4216
+ suggestion: "Add JSDoc comments to exports"
4194
4217
  }
4195
- break;
4196
- }
4218
+ ];
4197
4219
  }
4198
- return matches;
4220
+ return [];
4221
+ }
4222
+ var RULE_CHECKERS = {
4223
+ "must-export": checkMustExport,
4224
+ "must-export-default": checkMustExportDefault,
4225
+ "no-export": checkNoExport,
4226
+ "must-import": checkMustImport,
4227
+ "no-import": checkNoImport,
4228
+ naming: checkNaming,
4229
+ "max-exports": checkMaxExports,
4230
+ "max-lines": checkMaxLines,
4231
+ "require-jsdoc": checkRequireJsdoc
4232
+ };
4233
+ function checkConfigPattern(pattern, file, rootDir) {
4234
+ const fileMatches = pattern.files.some((glob2) => fileMatchesPattern(file.path, glob2, rootDir));
4235
+ if (!fileMatches) return [];
4236
+ const checker = RULE_CHECKERS[pattern.rule.type];
4237
+ if (!checker) return [];
4238
+ return checker(pattern.rule, file, pattern.message);
4199
4239
  }
4200
4240
  async function detectPatternViolations(snapshot, config) {
4201
4241
  const violations = [];
@@ -4701,17 +4741,35 @@ function createUnusedImportFixes(deadCodeReport) {
4701
4741
  reversible: true
4702
4742
  }));
4703
4743
  }
4744
+ var EXPORT_TYPE_KEYWORD = {
4745
+ class: "class",
4746
+ function: "function",
4747
+ variable: "const",
4748
+ type: "type",
4749
+ interface: "interface",
4750
+ enum: "enum"
4751
+ };
4752
+ function getExportKeyword(exportType) {
4753
+ return EXPORT_TYPE_KEYWORD[exportType] ?? "enum";
4754
+ }
4755
+ function getDefaultExportKeyword(exportType) {
4756
+ if (exportType === "class" || exportType === "function") return exportType;
4757
+ return "";
4758
+ }
4704
4759
  function createDeadExportFixes(deadCodeReport) {
4705
- return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) => ({
4706
- type: "dead-exports",
4707
- file: exp.file,
4708
- description: `Remove export keyword from ${exp.name} (${exp.reason})`,
4709
- action: "replace",
4710
- oldContent: exp.isDefault ? `export default ${exp.type === "class" ? "class" : exp.type === "function" ? "function" : ""} ${exp.name}` : `export ${exp.type === "class" ? "class" : exp.type === "function" ? "function" : exp.type === "variable" ? "const" : exp.type === "type" ? "type" : exp.type === "interface" ? "interface" : "enum"} ${exp.name}`,
4711
- newContent: exp.isDefault ? `${exp.type === "class" ? "class" : exp.type === "function" ? "function" : ""} ${exp.name}` : `${exp.type === "class" ? "class" : exp.type === "function" ? "function" : exp.type === "variable" ? "const" : exp.type === "type" ? "type" : exp.type === "interface" ? "interface" : "enum"} ${exp.name}`,
4712
- safe: true,
4713
- reversible: true
4714
- }));
4760
+ return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) => {
4761
+ const keyword = exp.isDefault ? getDefaultExportKeyword(exp.type) : getExportKeyword(exp.type);
4762
+ return {
4763
+ type: "dead-exports",
4764
+ file: exp.file,
4765
+ description: `Remove export keyword from ${exp.name} (${exp.reason})`,
4766
+ action: "replace",
4767
+ oldContent: exp.isDefault ? `export default ${keyword} ${exp.name}` : `export ${keyword} ${exp.name}`,
4768
+ newContent: `${keyword} ${exp.name}`,
4769
+ safe: true,
4770
+ reversible: true
4771
+ };
4772
+ });
4715
4773
  }
4716
4774
  function createCommentedCodeFixes(blocks) {
4717
4775
  return blocks.map((block) => ({
@@ -4886,53 +4944,80 @@ var ALWAYS_UNSAFE_TYPES = /* @__PURE__ */ new Set([
4886
4944
  "dead-internal"
4887
4945
  ]);
4888
4946
  var idCounter = 0;
4947
+ var DEAD_CODE_FIX_ACTIONS = {
4948
+ "dead-export": "Remove export keyword",
4949
+ "dead-file": "Delete file",
4950
+ "commented-code": "Delete commented block",
4951
+ "unused-import": "Remove import"
4952
+ };
4953
+ function classifyDeadCode(input) {
4954
+ if (input.isPublicApi) {
4955
+ return {
4956
+ safety: "unsafe",
4957
+ safetyReason: "Public API export may have external consumers",
4958
+ suggestion: "Deprecate before removing"
4959
+ };
4960
+ }
4961
+ const fixAction = DEAD_CODE_FIX_ACTIONS[input.type];
4962
+ if (fixAction) {
4963
+ return {
4964
+ safety: "safe",
4965
+ safetyReason: "zero importers, non-public",
4966
+ fixAction,
4967
+ suggestion: fixAction
4968
+ };
4969
+ }
4970
+ if (input.type === "orphaned-dep") {
4971
+ return {
4972
+ safety: "probably-safe",
4973
+ safetyReason: "No imports found, but needs install+test verification",
4974
+ fixAction: "Remove from package.json",
4975
+ suggestion: "Remove from package.json"
4976
+ };
4977
+ }
4978
+ return {
4979
+ safety: "unsafe",
4980
+ safetyReason: "Unknown dead code type",
4981
+ suggestion: "Manual review required"
4982
+ };
4983
+ }
4984
+ function classifyArchitecture(input) {
4985
+ if (input.type === "import-ordering") {
4986
+ return {
4987
+ safety: "safe",
4988
+ safetyReason: "Mechanical reorder, no semantic change",
4989
+ fixAction: "Reorder imports",
4990
+ suggestion: "Reorder imports"
4991
+ };
4992
+ }
4993
+ if (input.type === "forbidden-import" && input.hasAlternative) {
4994
+ return {
4995
+ safety: "probably-safe",
4996
+ safetyReason: "Alternative configured, needs typecheck+test",
4997
+ fixAction: "Replace with configured alternative",
4998
+ suggestion: "Replace with configured alternative"
4999
+ };
5000
+ }
5001
+ return {
5002
+ safety: "unsafe",
5003
+ safetyReason: `${input.type} requires structural changes`,
5004
+ suggestion: "Restructure code to fix violation"
5005
+ };
5006
+ }
4889
5007
  function classifyFinding(input) {
4890
5008
  idCounter++;
4891
5009
  const id = `${input.concern === "dead-code" ? "dc" : "arch"}-${idCounter}`;
4892
- let safety;
4893
- let safetyReason;
4894
- let fixAction;
4895
- let suggestion;
5010
+ let classification;
4896
5011
  if (ALWAYS_UNSAFE_TYPES.has(input.type)) {
4897
- safety = "unsafe";
4898
- safetyReason = `${input.type} requires human judgment`;
4899
- suggestion = "Review and refactor manually";
5012
+ classification = {
5013
+ safety: "unsafe",
5014
+ safetyReason: `${input.type} requires human judgment`,
5015
+ suggestion: "Review and refactor manually"
5016
+ };
4900
5017
  } else if (input.concern === "dead-code") {
4901
- if (input.isPublicApi) {
4902
- safety = "unsafe";
4903
- safetyReason = "Public API export may have external consumers";
4904
- suggestion = "Deprecate before removing";
4905
- } else if (input.type === "dead-export" || input.type === "unused-import" || input.type === "commented-code" || input.type === "dead-file") {
4906
- safety = "safe";
4907
- safetyReason = "zero importers, non-public";
4908
- fixAction = input.type === "dead-export" ? "Remove export keyword" : input.type === "dead-file" ? "Delete file" : input.type === "commented-code" ? "Delete commented block" : "Remove import";
4909
- suggestion = fixAction;
4910
- } else if (input.type === "orphaned-dep") {
4911
- safety = "probably-safe";
4912
- safetyReason = "No imports found, but needs install+test verification";
4913
- fixAction = "Remove from package.json";
4914
- suggestion = fixAction;
4915
- } else {
4916
- safety = "unsafe";
4917
- safetyReason = "Unknown dead code type";
4918
- suggestion = "Manual review required";
4919
- }
5018
+ classification = classifyDeadCode(input);
4920
5019
  } else {
4921
- if (input.type === "import-ordering") {
4922
- safety = "safe";
4923
- safetyReason = "Mechanical reorder, no semantic change";
4924
- fixAction = "Reorder imports";
4925
- suggestion = fixAction;
4926
- } else if (input.type === "forbidden-import" && input.hasAlternative) {
4927
- safety = "probably-safe";
4928
- safetyReason = "Alternative configured, needs typecheck+test";
4929
- fixAction = "Replace with configured alternative";
4930
- suggestion = fixAction;
4931
- } else {
4932
- safety = "unsafe";
4933
- safetyReason = `${input.type} requires structural changes`;
4934
- suggestion = "Restructure code to fix violation";
4935
- }
5020
+ classification = classifyArchitecture(input);
4936
5021
  }
4937
5022
  return {
4938
5023
  id,
@@ -4941,11 +5026,11 @@ function classifyFinding(input) {
4941
5026
  ...input.line !== void 0 ? { line: input.line } : {},
4942
5027
  type: input.type,
4943
5028
  description: input.description,
4944
- safety,
4945
- safetyReason,
5029
+ safety: classification.safety,
5030
+ safetyReason: classification.safetyReason,
4946
5031
  hotspotDowngraded: false,
4947
- ...fixAction !== void 0 ? { fixAction } : {},
4948
- suggestion
5032
+ ...classification.fixAction !== void 0 ? { fixAction: classification.fixAction } : {},
5033
+ suggestion: classification.suggestion
4949
5034
  };
4950
5035
  }
4951
5036
  function applyHotspotDowngrade(finding, hotspot) {
@@ -5229,43 +5314,57 @@ var BenchmarkRunner = class {
5229
5314
  };
5230
5315
  }
5231
5316
  }
5317
+ /**
5318
+ * Extract a BenchmarkResult from a single assertion with benchmark data.
5319
+ */
5320
+ parseBenchAssertion(assertion, file) {
5321
+ if (!assertion.benchmark) return null;
5322
+ const bench = assertion.benchmark;
5323
+ return {
5324
+ name: assertion.fullName || assertion.title || "unknown",
5325
+ file: file.replace(process.cwd() + "/", ""),
5326
+ opsPerSec: Math.round(bench.hz || 0),
5327
+ meanMs: bench.mean ? bench.mean * 1e3 : 0,
5328
+ p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
5329
+ marginOfError: bench.rme ? bench.rme / 100 : 0.05
5330
+ };
5331
+ }
5332
+ /**
5333
+ * Extract JSON from output that may contain non-JSON preamble.
5334
+ */
5335
+ extractJson(output) {
5336
+ const jsonStart = output.indexOf("{");
5337
+ const jsonEnd = output.lastIndexOf("}");
5338
+ if (jsonStart === -1 || jsonEnd === -1) return null;
5339
+ return JSON.parse(output.slice(jsonStart, jsonEnd + 1));
5340
+ }
5232
5341
  /**
5233
5342
  * Parse vitest bench JSON reporter output into BenchmarkResult[].
5234
5343
  * Vitest bench JSON output contains testResults with benchmark data.
5235
5344
  */
5236
- parseVitestBenchOutput(output) {
5345
+ collectAssertionResults(testResults) {
5237
5346
  const results = [];
5238
- try {
5239
- const jsonStart = output.indexOf("{");
5240
- const jsonEnd = output.lastIndexOf("}");
5241
- if (jsonStart === -1 || jsonEnd === -1) return results;
5242
- const jsonStr = output.slice(jsonStart, jsonEnd + 1);
5243
- const parsed = JSON.parse(jsonStr);
5244
- if (parsed.testResults) {
5245
- for (const testResult of parsed.testResults) {
5246
- const file = testResult.name || testResult.filepath || "";
5247
- if (testResult.assertionResults) {
5248
- for (const assertion of testResult.assertionResults) {
5249
- if (assertion.benchmark) {
5250
- const bench = assertion.benchmark;
5251
- results.push({
5252
- name: assertion.fullName || assertion.title || "unknown",
5253
- file: file.replace(process.cwd() + "/", ""),
5254
- opsPerSec: Math.round(bench.hz || 0),
5255
- meanMs: bench.mean ? bench.mean * 1e3 : 0,
5256
- // p99: use actual p99 if available, otherwise estimate as 1.5× mean
5257
- p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
5258
- marginOfError: bench.rme ? bench.rme / 100 : 0.05
5259
- });
5260
- }
5261
- }
5262
- }
5263
- }
5347
+ for (const testResult of testResults) {
5348
+ const file = testResult.name || testResult.filepath || "";
5349
+ const assertions = testResult.assertionResults ?? [];
5350
+ for (const assertion of assertions) {
5351
+ const result = this.parseBenchAssertion(assertion, file);
5352
+ if (result) results.push(result);
5264
5353
  }
5265
- } catch {
5266
5354
  }
5267
5355
  return results;
5268
5356
  }
5357
+ parseVitestBenchOutput(output) {
5358
+ try {
5359
+ const parsed = this.extractJson(output);
5360
+ if (!parsed) return [];
5361
+ const testResults = parsed.testResults;
5362
+ if (!testResults) return [];
5363
+ return this.collectAssertionResults(testResults);
5364
+ } catch {
5365
+ return [];
5366
+ }
5367
+ }
5269
5368
  };
5270
5369
  var RegressionDetector = class {
5271
5370
  detect(results, baselines, criticalPaths) {
@@ -5557,39 +5656,31 @@ function getFeedbackConfig() {
5557
5656
  function resetFeedbackConfig() {
5558
5657
  feedbackConfig = null;
5559
5658
  }
5659
+ function detectFileStatus(part) {
5660
+ if (/new file mode/.test(part)) return "added";
5661
+ if (/deleted file mode/.test(part)) return "deleted";
5662
+ if (part.includes("rename from")) return "renamed";
5663
+ return "modified";
5664
+ }
5665
+ function parseDiffPart(part) {
5666
+ if (!part.trim()) return null;
5667
+ const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
5668
+ if (!headerMatch || !headerMatch[2]) return null;
5669
+ const additionRegex = /^\+(?!\+\+)/gm;
5670
+ const deletionRegex = /^-(?!--)/gm;
5671
+ return {
5672
+ path: headerMatch[2],
5673
+ status: detectFileStatus(part),
5674
+ additions: (part.match(additionRegex) || []).length,
5675
+ deletions: (part.match(deletionRegex) || []).length
5676
+ };
5677
+ }
5560
5678
  function parseDiff(diff2) {
5561
5679
  try {
5562
5680
  if (!diff2.trim()) {
5563
5681
  return Ok({ diff: diff2, files: [] });
5564
5682
  }
5565
- const files = [];
5566
- const newFileRegex = /new file mode/;
5567
- const deletedFileRegex = /deleted file mode/;
5568
- const additionRegex = /^\+(?!\+\+)/gm;
5569
- const deletionRegex = /^-(?!--)/gm;
5570
- const diffParts = diff2.split(/(?=diff --git)/);
5571
- for (const part of diffParts) {
5572
- if (!part.trim()) continue;
5573
- const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
5574
- if (!headerMatch || !headerMatch[2]) continue;
5575
- const filePath = headerMatch[2];
5576
- let status = "modified";
5577
- if (newFileRegex.test(part)) {
5578
- status = "added";
5579
- } else if (deletedFileRegex.test(part)) {
5580
- status = "deleted";
5581
- } else if (part.includes("rename from")) {
5582
- status = "renamed";
5583
- }
5584
- const additions = (part.match(additionRegex) || []).length;
5585
- const deletions = (part.match(deletionRegex) || []).length;
5586
- files.push({
5587
- path: filePath,
5588
- status,
5589
- additions,
5590
- deletions
5591
- });
5592
- }
5683
+ const files = diff2.split(/(?=diff --git)/).map(parseDiffPart).filter((f) => f !== null);
5593
5684
  return Ok({ diff: diff2, files });
5594
5685
  } catch (error) {
5595
5686
  return Err({
@@ -5753,107 +5844,123 @@ var ChecklistBuilder = class {
5753
5844
  this.graphImpactData = graphImpactData;
5754
5845
  return this;
5755
5846
  }
5756
- async run(changes) {
5757
- const startTime = Date.now();
5758
- const items = [];
5759
- if (this.harnessOptions) {
5760
- if (this.harnessOptions.context !== false) {
5761
- if (this.graphHarnessData) {
5762
- items.push({
5763
- id: "harness-context",
5764
- category: "harness",
5765
- check: "Context validation",
5766
- passed: this.graphHarnessData.graphExists && this.graphHarnessData.nodeCount > 0,
5767
- severity: "info",
5768
- details: this.graphHarnessData.graphExists ? `Graph loaded: ${this.graphHarnessData.nodeCount} nodes, ${this.graphHarnessData.edgeCount} edges` : "No graph available \u2014 run harness scan to build the knowledge graph"
5769
- });
5770
- } else {
5771
- items.push({
5772
- id: "harness-context",
5773
- category: "harness",
5774
- check: "Context validation",
5775
- passed: true,
5776
- severity: "info",
5777
- details: "Harness context validation not yet integrated (run with graph for real checks)"
5778
- });
5779
- }
5780
- }
5781
- if (this.harnessOptions.constraints !== false) {
5782
- if (this.graphHarnessData) {
5783
- const violations = this.graphHarnessData.constraintViolations;
5784
- items.push({
5785
- id: "harness-constraints",
5786
- category: "harness",
5787
- check: "Constraint validation",
5788
- passed: violations === 0,
5789
- severity: violations > 0 ? "error" : "info",
5790
- details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
5791
- });
5792
- } else {
5793
- items.push({
5794
- id: "harness-constraints",
5795
- category: "harness",
5796
- check: "Constraint validation",
5797
- passed: true,
5798
- severity: "info",
5799
- details: "Harness constraint validation not yet integrated (run with graph for real checks)"
5800
- });
5801
- }
5802
- }
5803
- if (this.harnessOptions.entropy !== false) {
5804
- if (this.graphHarnessData) {
5805
- const issues = this.graphHarnessData.unreachableNodes + this.graphHarnessData.undocumentedFiles;
5806
- items.push({
5807
- id: "harness-entropy",
5808
- category: "harness",
5809
- check: "Entropy detection",
5810
- passed: issues === 0,
5811
- severity: issues > 0 ? "warning" : "info",
5812
- details: issues === 0 ? "No entropy issues detected" : `${this.graphHarnessData.unreachableNodes} unreachable node(s), ${this.graphHarnessData.undocumentedFiles} undocumented file(s)`
5813
- });
5814
- } else {
5815
- items.push({
5816
- id: "harness-entropy",
5847
+ /**
5848
+ * Build a single harness check item with or without graph data.
5849
+ */
5850
+ buildHarnessCheckItem(id, check, fallbackDetails, graphItemBuilder) {
5851
+ if (this.graphHarnessData && graphItemBuilder) {
5852
+ return graphItemBuilder();
5853
+ }
5854
+ return {
5855
+ id,
5856
+ category: "harness",
5857
+ check,
5858
+ passed: true,
5859
+ severity: "info",
5860
+ details: fallbackDetails
5861
+ };
5862
+ }
5863
+ /**
5864
+ * Build all harness check items based on harnessOptions and graph data.
5865
+ */
5866
+ buildHarnessItems() {
5867
+ if (!this.harnessOptions) return [];
5868
+ const items = [];
5869
+ const graphData = this.graphHarnessData;
5870
+ if (this.harnessOptions.context !== false) {
5871
+ items.push(
5872
+ this.buildHarnessCheckItem(
5873
+ "harness-context",
5874
+ "Context validation",
5875
+ "Harness context validation not yet integrated (run with graph for real checks)",
5876
+ graphData ? () => ({
5877
+ id: "harness-context",
5817
5878
  category: "harness",
5818
- check: "Entropy detection",
5819
- passed: true,
5879
+ check: "Context validation",
5880
+ passed: graphData.graphExists && graphData.nodeCount > 0,
5820
5881
  severity: "info",
5821
- details: "Harness entropy detection not yet integrated (run with graph for real checks)"
5822
- });
5823
- }
5824
- }
5882
+ details: graphData.graphExists ? `Graph loaded: ${graphData.nodeCount} nodes, ${graphData.edgeCount} edges` : "No graph available \u2014 run harness scan to build the knowledge graph"
5883
+ }) : void 0
5884
+ )
5885
+ );
5886
+ }
5887
+ if (this.harnessOptions.constraints !== false) {
5888
+ items.push(
5889
+ this.buildHarnessCheckItem(
5890
+ "harness-constraints",
5891
+ "Constraint validation",
5892
+ "Harness constraint validation not yet integrated (run with graph for real checks)",
5893
+ graphData ? () => {
5894
+ const violations = graphData.constraintViolations;
5895
+ return {
5896
+ id: "harness-constraints",
5897
+ category: "harness",
5898
+ check: "Constraint validation",
5899
+ passed: violations === 0,
5900
+ severity: violations > 0 ? "error" : "info",
5901
+ details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
5902
+ };
5903
+ } : void 0
5904
+ )
5905
+ );
5906
+ }
5907
+ if (this.harnessOptions.entropy !== false) {
5908
+ items.push(
5909
+ this.buildHarnessCheckItem(
5910
+ "harness-entropy",
5911
+ "Entropy detection",
5912
+ "Harness entropy detection not yet integrated (run with graph for real checks)",
5913
+ graphData ? () => {
5914
+ const issues = graphData.unreachableNodes + graphData.undocumentedFiles;
5915
+ return {
5916
+ id: "harness-entropy",
5917
+ category: "harness",
5918
+ check: "Entropy detection",
5919
+ passed: issues === 0,
5920
+ severity: issues > 0 ? "warning" : "info",
5921
+ details: issues === 0 ? "No entropy issues detected" : `${graphData.unreachableNodes} unreachable node(s), ${graphData.undocumentedFiles} undocumented file(s)`
5922
+ };
5923
+ } : void 0
5924
+ )
5925
+ );
5926
+ }
5927
+ return items;
5928
+ }
5929
+ /**
5930
+ * Execute a single custom rule and return a ReviewItem.
5931
+ */
5932
+ async executeCustomRule(rule, changes) {
5933
+ try {
5934
+ const result = await rule.check(changes, this.rootDir);
5935
+ const item = {
5936
+ id: rule.id,
5937
+ category: "custom",
5938
+ check: rule.name,
5939
+ passed: result.passed,
5940
+ severity: rule.severity,
5941
+ details: result.details
5942
+ };
5943
+ if (result.suggestion !== void 0) item.suggestion = result.suggestion;
5944
+ if (result.file !== void 0) item.file = result.file;
5945
+ if (result.line !== void 0) item.line = result.line;
5946
+ return item;
5947
+ } catch (error) {
5948
+ return {
5949
+ id: rule.id,
5950
+ category: "custom",
5951
+ check: rule.name,
5952
+ passed: false,
5953
+ severity: "error",
5954
+ details: `Rule execution failed: ${String(error)}`
5955
+ };
5825
5956
  }
5957
+ }
5958
+ async run(changes) {
5959
+ const startTime = Date.now();
5960
+ const items = [];
5961
+ items.push(...this.buildHarnessItems());
5826
5962
  for (const rule of this.customRules) {
5827
- try {
5828
- const result = await rule.check(changes, this.rootDir);
5829
- const item = {
5830
- id: rule.id,
5831
- category: "custom",
5832
- check: rule.name,
5833
- passed: result.passed,
5834
- severity: rule.severity,
5835
- details: result.details
5836
- };
5837
- if (result.suggestion !== void 0) {
5838
- item.suggestion = result.suggestion;
5839
- }
5840
- if (result.file !== void 0) {
5841
- item.file = result.file;
5842
- }
5843
- if (result.line !== void 0) {
5844
- item.line = result.line;
5845
- }
5846
- items.push(item);
5847
- } catch (error) {
5848
- items.push({
5849
- id: rule.id,
5850
- category: "custom",
5851
- check: rule.name,
5852
- passed: false,
5853
- severity: "error",
5854
- details: `Rule execution failed: ${String(error)}`
5855
- });
5856
- }
5963
+ items.push(await this.executeCustomRule(rule, changes));
5857
5964
  }
5858
5965
  if (this.diffOptions) {
5859
5966
  const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
@@ -5868,7 +5975,6 @@ var ChecklistBuilder = class {
5868
5975
  const checklist = {
5869
5976
  items,
5870
5977
  passed: failed === 0,
5871
- // Pass if no failed items
5872
5978
  summary: {
5873
5979
  total: items.length,
5874
5980
  passed,
@@ -6252,7 +6358,7 @@ function detectStaleConstraints(store, windowDays = 30, category) {
6252
6358
  staleConstraints.sort((a, b) => b.daysSinceLastViolation - a.daysSinceLastViolation);
6253
6359
  return { staleConstraints, totalConstraints, windowDays };
6254
6360
  }
6255
- function resolveThresholds(scope, config) {
6361
+ function resolveThresholds2(scope, config) {
6256
6362
  const projectThresholds = {};
6257
6363
  for (const [key, val] of Object.entries(config.thresholds)) {
6258
6364
  projectThresholds[key] = typeof val === "object" && val !== null && !Array.isArray(val) ? { ...val } : val;
@@ -6382,6 +6488,8 @@ var INDEX_FILE = "index.json";
6382
6488
  var SESSIONS_DIR = "sessions";
6383
6489
  var SESSION_INDEX_FILE = "index.md";
6384
6490
  var SUMMARY_FILE = "summary.md";
6491
+ var SESSION_STATE_FILE = "session-state.json";
6492
+ var ARCHIVE_DIR = "archive";
6385
6493
  var STREAMS_DIR = "streams";
6386
6494
  var STREAM_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
6387
6495
  function streamsDir(projectPath) {
@@ -7258,6 +7366,134 @@ function listActiveSessions(projectPath) {
7258
7366
  );
7259
7367
  }
7260
7368
  }
7369
+ function emptySections() {
7370
+ const sections = {};
7371
+ for (const name of SESSION_SECTION_NAMES) {
7372
+ sections[name] = [];
7373
+ }
7374
+ return sections;
7375
+ }
7376
+ async function loadSessionState(projectPath, sessionSlug) {
7377
+ const dirResult = resolveSessionDir(projectPath, sessionSlug);
7378
+ if (!dirResult.ok) return dirResult;
7379
+ const sessionDir = dirResult.value;
7380
+ const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
7381
+ if (!fs14.existsSync(filePath)) {
7382
+ return Ok(emptySections());
7383
+ }
7384
+ try {
7385
+ const raw = fs14.readFileSync(filePath, "utf-8");
7386
+ const parsed = JSON.parse(raw);
7387
+ const sections = emptySections();
7388
+ for (const name of SESSION_SECTION_NAMES) {
7389
+ if (Array.isArray(parsed[name])) {
7390
+ sections[name] = parsed[name];
7391
+ }
7392
+ }
7393
+ return Ok(sections);
7394
+ } catch (error) {
7395
+ return Err(
7396
+ new Error(
7397
+ `Failed to load session state: ${error instanceof Error ? error.message : String(error)}`
7398
+ )
7399
+ );
7400
+ }
7401
+ }
7402
+ async function saveSessionState(projectPath, sessionSlug, sections) {
7403
+ const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
7404
+ if (!dirResult.ok) return dirResult;
7405
+ const sessionDir = dirResult.value;
7406
+ const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
7407
+ try {
7408
+ fs14.writeFileSync(filePath, JSON.stringify(sections, null, 2));
7409
+ return Ok(void 0);
7410
+ } catch (error) {
7411
+ return Err(
7412
+ new Error(
7413
+ `Failed to save session state: ${error instanceof Error ? error.message : String(error)}`
7414
+ )
7415
+ );
7416
+ }
7417
+ }
7418
+ async function readSessionSections(projectPath, sessionSlug) {
7419
+ return loadSessionState(projectPath, sessionSlug);
7420
+ }
7421
+ async function readSessionSection(projectPath, sessionSlug, section) {
7422
+ const result = await loadSessionState(projectPath, sessionSlug);
7423
+ if (!result.ok) return result;
7424
+ return Ok(result.value[section]);
7425
+ }
7426
+ async function appendSessionEntry(projectPath, sessionSlug, section, authorSkill, content) {
7427
+ const loadResult = await loadSessionState(projectPath, sessionSlug);
7428
+ if (!loadResult.ok) return loadResult;
7429
+ const sections = loadResult.value;
7430
+ const entry = {
7431
+ id: generateEntryId(),
7432
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7433
+ authorSkill,
7434
+ content,
7435
+ status: "active"
7436
+ };
7437
+ sections[section].push(entry);
7438
+ const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
7439
+ if (!saveResult.ok) return saveResult;
7440
+ return Ok(entry);
7441
+ }
7442
+ async function updateSessionEntryStatus(projectPath, sessionSlug, section, entryId, newStatus) {
7443
+ const loadResult = await loadSessionState(projectPath, sessionSlug);
7444
+ if (!loadResult.ok) return loadResult;
7445
+ const sections = loadResult.value;
7446
+ const entry = sections[section].find((e) => e.id === entryId);
7447
+ if (!entry) {
7448
+ return Err(new Error(`Entry '${entryId}' not found in section '${section}'`));
7449
+ }
7450
+ entry.status = newStatus;
7451
+ const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
7452
+ if (!saveResult.ok) return saveResult;
7453
+ return Ok(entry);
7454
+ }
7455
+ function generateEntryId() {
7456
+ const timestamp = Date.now().toString(36);
7457
+ const random = Math.random().toString(36).substring(2, 8);
7458
+ return `${timestamp}-${random}`;
7459
+ }
7460
+ async function archiveSession(projectPath, sessionSlug) {
7461
+ const dirResult = resolveSessionDir(projectPath, sessionSlug);
7462
+ if (!dirResult.ok) return dirResult;
7463
+ const sessionDir = dirResult.value;
7464
+ if (!fs15.existsSync(sessionDir)) {
7465
+ return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
7466
+ }
7467
+ const archiveBase = path12.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
7468
+ try {
7469
+ fs15.mkdirSync(archiveBase, { recursive: true });
7470
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
7471
+ let archiveName = `${sessionSlug}-${date}`;
7472
+ let counter = 1;
7473
+ while (fs15.existsSync(path12.join(archiveBase, archiveName))) {
7474
+ archiveName = `${sessionSlug}-${date}-${counter}`;
7475
+ counter++;
7476
+ }
7477
+ const dest = path12.join(archiveBase, archiveName);
7478
+ try {
7479
+ fs15.renameSync(sessionDir, dest);
7480
+ } catch (renameErr) {
7481
+ if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
7482
+ fs15.cpSync(sessionDir, dest, { recursive: true });
7483
+ fs15.rmSync(sessionDir, { recursive: true });
7484
+ } else {
7485
+ throw renameErr;
7486
+ }
7487
+ }
7488
+ return Ok(void 0);
7489
+ } catch (error) {
7490
+ return Err(
7491
+ new Error(
7492
+ `Failed to archive session: ${error instanceof Error ? error.message : String(error)}`
7493
+ )
7494
+ );
7495
+ }
7496
+ }
7261
7497
  async function executeWorkflow(workflow, executor) {
7262
7498
  const stepResults = [];
7263
7499
  const startTime = Date.now();
@@ -7479,11 +7715,11 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
7479
7715
  }
7480
7716
  function detectStack(projectRoot) {
7481
7717
  const stacks = [];
7482
- const pkgJsonPath = path11.join(projectRoot, "package.json");
7483
- if (fs14.existsSync(pkgJsonPath)) {
7718
+ const pkgJsonPath = path13.join(projectRoot, "package.json");
7719
+ if (fs16.existsSync(pkgJsonPath)) {
7484
7720
  stacks.push("node");
7485
7721
  try {
7486
- const pkgJson = JSON.parse(fs14.readFileSync(pkgJsonPath, "utf-8"));
7722
+ const pkgJson = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
7487
7723
  const allDeps = {
7488
7724
  ...pkgJson.dependencies,
7489
7725
  ...pkgJson.devDependencies
@@ -7498,13 +7734,13 @@ function detectStack(projectRoot) {
7498
7734
  } catch {
7499
7735
  }
7500
7736
  }
7501
- const goModPath = path11.join(projectRoot, "go.mod");
7502
- if (fs14.existsSync(goModPath)) {
7737
+ const goModPath = path13.join(projectRoot, "go.mod");
7738
+ if (fs16.existsSync(goModPath)) {
7503
7739
  stacks.push("go");
7504
7740
  }
7505
- const requirementsPath = path11.join(projectRoot, "requirements.txt");
7506
- const pyprojectPath = path11.join(projectRoot, "pyproject.toml");
7507
- if (fs14.existsSync(requirementsPath) || fs14.existsSync(pyprojectPath)) {
7741
+ const requirementsPath = path13.join(projectRoot, "requirements.txt");
7742
+ const pyprojectPath = path13.join(projectRoot, "pyproject.toml");
7743
+ if (fs16.existsSync(requirementsPath) || fs16.existsSync(pyprojectPath)) {
7508
7744
  stacks.push("python");
7509
7745
  }
7510
7746
  return stacks;
@@ -7907,7 +8143,7 @@ var SecurityScanner = class {
7907
8143
  }
7908
8144
  async scanFile(filePath) {
7909
8145
  if (!this.config.enabled) return [];
7910
- const content = await fs15.readFile(filePath, "utf-8");
8146
+ const content = await fs17.readFile(filePath, "utf-8");
7911
8147
  return this.scanContent(content, filePath, 1);
7912
8148
  }
7913
8149
  async scanFiles(filePaths) {
@@ -7942,7 +8178,7 @@ var ALL_CHECKS = [
7942
8178
  ];
7943
8179
  async function runValidateCheck(projectRoot, config) {
7944
8180
  const issues = [];
7945
- const agentsPath = path12.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8181
+ const agentsPath = path14.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
7946
8182
  const result = await validateAgentsMap(agentsPath);
7947
8183
  if (!result.ok) {
7948
8184
  issues.push({ severity: "error", message: result.error.message });
@@ -7999,7 +8235,7 @@ async function runDepsCheck(projectRoot, config) {
7999
8235
  }
8000
8236
  async function runDocsCheck(projectRoot, config) {
8001
8237
  const issues = [];
8002
- const docsDir = path12.join(projectRoot, config.docsDir ?? "docs");
8238
+ const docsDir = path14.join(projectRoot, config.docsDir ?? "docs");
8003
8239
  const entropyConfig = config.entropy || {};
8004
8240
  const result = await checkDocCoverage("project", {
8005
8241
  docsDir,
@@ -8024,10 +8260,14 @@ async function runDocsCheck(projectRoot, config) {
8024
8260
  }
8025
8261
  return issues;
8026
8262
  }
8027
- async function runEntropyCheck(projectRoot, _config) {
8263
+ async function runEntropyCheck(projectRoot, config) {
8028
8264
  const issues = [];
8265
+ const entropyConfig = config.entropy || {};
8266
+ const perfConfig = config.performance || {};
8267
+ const entryPoints = entropyConfig.entryPoints ?? perfConfig.entryPoints;
8029
8268
  const analyzer = new EntropyAnalyzer({
8030
8269
  rootDir: projectRoot,
8270
+ ...entryPoints ? { entryPoints } : {},
8031
8271
  analyze: { drift: true, deadCode: true, patterns: false }
8032
8272
  });
8033
8273
  const result = await analyzer.analyze();
@@ -8089,8 +8329,10 @@ async function runSecurityCheck(projectRoot, config) {
8089
8329
  async function runPerfCheck(projectRoot, config) {
8090
8330
  const issues = [];
8091
8331
  const perfConfig = config.performance || {};
8332
+ const entryPoints = perfConfig.entryPoints;
8092
8333
  const perfAnalyzer = new EntropyAnalyzer({
8093
8334
  rootDir: projectRoot,
8335
+ ...entryPoints ? { entryPoints } : {},
8094
8336
  analyze: {
8095
8337
  complexity: perfConfig.complexity || true,
8096
8338
  coupling: perfConfig.coupling || true,
@@ -8280,7 +8522,7 @@ async function runMechanicalChecks(options) {
8280
8522
  };
8281
8523
  if (!skip.includes("validate")) {
8282
8524
  try {
8283
- const agentsPath = path13.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8525
+ const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
8284
8526
  const result = await validateAgentsMap(agentsPath);
8285
8527
  if (!result.ok) {
8286
8528
  statuses.validate = "fail";
@@ -8317,7 +8559,7 @@ async function runMechanicalChecks(options) {
8317
8559
  statuses.validate = "fail";
8318
8560
  findings.push({
8319
8561
  tool: "validate",
8320
- file: path13.join(projectRoot, "AGENTS.md"),
8562
+ file: path15.join(projectRoot, "AGENTS.md"),
8321
8563
  message: err instanceof Error ? err.message : String(err),
8322
8564
  severity: "error"
8323
8565
  });
@@ -8381,7 +8623,7 @@ async function runMechanicalChecks(options) {
8381
8623
  (async () => {
8382
8624
  const localFindings = [];
8383
8625
  try {
8384
- const docsDir = path13.join(projectRoot, config.docsDir ?? "docs");
8626
+ const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
8385
8627
  const result = await checkDocCoverage("project", { docsDir });
8386
8628
  if (!result.ok) {
8387
8629
  statuses["check-docs"] = "warn";
@@ -8408,7 +8650,7 @@ async function runMechanicalChecks(options) {
8408
8650
  statuses["check-docs"] = "warn";
8409
8651
  localFindings.push({
8410
8652
  tool: "check-docs",
8411
- file: path13.join(projectRoot, "docs"),
8653
+ file: path15.join(projectRoot, "docs"),
8412
8654
  message: err instanceof Error ? err.message : String(err),
8413
8655
  severity: "warning"
8414
8656
  });
@@ -8557,18 +8799,18 @@ function computeContextBudget(diffLines) {
8557
8799
  return diffLines;
8558
8800
  }
8559
8801
  function isWithinProject(absPath, projectRoot) {
8560
- const resolvedRoot = path14.resolve(projectRoot) + path14.sep;
8561
- const resolvedPath = path14.resolve(absPath);
8562
- return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path14.resolve(projectRoot);
8802
+ const resolvedRoot = path16.resolve(projectRoot) + path16.sep;
8803
+ const resolvedPath = path16.resolve(absPath);
8804
+ return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path16.resolve(projectRoot);
8563
8805
  }
8564
8806
  async function readContextFile(projectRoot, filePath, reason) {
8565
- const absPath = path14.isAbsolute(filePath) ? filePath : path14.join(projectRoot, filePath);
8807
+ const absPath = path16.isAbsolute(filePath) ? filePath : path16.join(projectRoot, filePath);
8566
8808
  if (!isWithinProject(absPath, projectRoot)) return null;
8567
8809
  const result = await readFileContent(absPath);
8568
8810
  if (!result.ok) return null;
8569
8811
  const content = result.value;
8570
8812
  const lines = content.split("\n").length;
8571
- const relPath = path14.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
8813
+ const relPath = path16.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
8572
8814
  return { path: relPath, content, reason, lines };
8573
8815
  }
8574
8816
  function extractImportSources2(content) {
@@ -8583,18 +8825,18 @@ function extractImportSources2(content) {
8583
8825
  }
8584
8826
  async function resolveImportPath2(projectRoot, fromFile, importSource) {
8585
8827
  if (!importSource.startsWith(".")) return null;
8586
- const fromDir = path14.dirname(path14.join(projectRoot, fromFile));
8587
- const basePath = path14.resolve(fromDir, importSource);
8828
+ const fromDir = path16.dirname(path16.join(projectRoot, fromFile));
8829
+ const basePath = path16.resolve(fromDir, importSource);
8588
8830
  if (!isWithinProject(basePath, projectRoot)) return null;
8589
8831
  const relBase = relativePosix(projectRoot, basePath);
8590
8832
  const candidates = [
8591
8833
  relBase + ".ts",
8592
8834
  relBase + ".tsx",
8593
8835
  relBase + ".mts",
8594
- path14.join(relBase, "index.ts")
8836
+ path16.join(relBase, "index.ts")
8595
8837
  ];
8596
8838
  for (const candidate of candidates) {
8597
- const absCandidate = path14.join(projectRoot, candidate);
8839
+ const absCandidate = path16.join(projectRoot, candidate);
8598
8840
  if (await fileExists(absCandidate)) {
8599
8841
  return candidate;
8600
8842
  }
@@ -8602,7 +8844,7 @@ async function resolveImportPath2(projectRoot, fromFile, importSource) {
8602
8844
  return null;
8603
8845
  }
8604
8846
  async function findTestFiles(projectRoot, sourceFile) {
8605
- const baseName = path14.basename(sourceFile, path14.extname(sourceFile));
8847
+ const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
8606
8848
  const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
8607
8849
  const results = await findFiles(pattern, projectRoot);
8608
8850
  return results.map((f) => relativePosix(projectRoot, f));
@@ -8887,101 +9129,102 @@ function findMissingJsDoc(bundle) {
8887
9129
  }
8888
9130
  return missing;
8889
9131
  }
8890
- function runComplianceAgent(bundle) {
9132
+ function checkMissingJsDoc(bundle, rules) {
9133
+ const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
9134
+ if (!jsDocRule) return [];
9135
+ const missingDocs = findMissingJsDoc(bundle);
9136
+ return missingDocs.map((m) => ({
9137
+ id: makeFindingId("compliance", m.file, m.line, `Missing JSDoc ${m.exportName}`),
9138
+ file: m.file,
9139
+ lineRange: [m.line, m.line],
9140
+ domain: "compliance",
9141
+ severity: "important",
9142
+ title: `Missing JSDoc on exported \`${m.exportName}\``,
9143
+ rationale: `Convention requires all exports to have JSDoc comments (from ${jsDocRule.source}).`,
9144
+ suggestion: `Add a JSDoc comment above the export of \`${m.exportName}\`.`,
9145
+ evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${jsDocRule.text}"`],
9146
+ validatedBy: "heuristic"
9147
+ }));
9148
+ }
9149
+ function checkFeatureSpec(bundle) {
9150
+ const hasSpecContext = bundle.contextFiles.some(
9151
+ (f) => f.reason === "spec" || f.reason === "convention"
9152
+ );
9153
+ if (hasSpecContext || bundle.changedFiles.length === 0) return [];
9154
+ const firstFile = bundle.changedFiles[0];
9155
+ return [
9156
+ {
9157
+ id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
9158
+ file: firstFile.path,
9159
+ lineRange: [1, 1],
9160
+ domain: "compliance",
9161
+ severity: "suggestion",
9162
+ title: "No spec/design doc found for feature change",
9163
+ rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
9164
+ evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
9165
+ validatedBy: "heuristic"
9166
+ }
9167
+ ];
9168
+ }
9169
+ function checkBugfixHistory(bundle) {
9170
+ if (bundle.commitHistory.length > 0 || bundle.changedFiles.length === 0) return [];
9171
+ const firstFile = bundle.changedFiles[0];
9172
+ return [
9173
+ {
9174
+ id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
9175
+ file: firstFile.path,
9176
+ lineRange: [1, 1],
9177
+ domain: "compliance",
9178
+ severity: "suggestion",
9179
+ title: "Bugfix without commit history context",
9180
+ rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
9181
+ evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
9182
+ validatedBy: "heuristic"
9183
+ }
9184
+ ];
9185
+ }
9186
+ function checkChangeTypeSpecific(bundle) {
9187
+ switch (bundle.changeType) {
9188
+ case "feature":
9189
+ return checkFeatureSpec(bundle);
9190
+ case "bugfix":
9191
+ return checkBugfixHistory(bundle);
9192
+ default:
9193
+ return [];
9194
+ }
9195
+ }
9196
+ function checkResultTypeConvention(bundle, rules) {
9197
+ const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
9198
+ if (!resultTypeRule) return [];
8891
9199
  const findings = [];
8892
- const rules = extractConventionRules(bundle);
8893
- const jsDocRuleExists = rules.some((r) => r.text.toLowerCase().includes("jsdoc"));
8894
- if (jsDocRuleExists) {
8895
- const missingDocs = findMissingJsDoc(bundle);
8896
- for (const m of missingDocs) {
9200
+ for (const cf of bundle.changedFiles) {
9201
+ const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
9202
+ const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
9203
+ if (hasTryCatch && !usesResult) {
8897
9204
  findings.push({
8898
- id: makeFindingId("compliance", m.file, m.line, `Missing JSDoc ${m.exportName}`),
8899
- file: m.file,
8900
- lineRange: [m.line, m.line],
9205
+ id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
9206
+ file: cf.path,
9207
+ lineRange: [1, cf.lines],
8901
9208
  domain: "compliance",
8902
- severity: "important",
8903
- title: `Missing JSDoc on exported \`${m.exportName}\``,
8904
- rationale: `Convention requires all exports to have JSDoc comments (from ${rules.find((r) => r.text.toLowerCase().includes("jsdoc"))?.source ?? "conventions"}).`,
8905
- suggestion: `Add a JSDoc comment above the export of \`${m.exportName}\`.`,
8906
- evidence: [
8907
- `changeType: ${bundle.changeType}`,
8908
- `Convention rule: "${rules.find((r) => r.text.toLowerCase().includes("jsdoc"))?.text ?? ""}"`
8909
- ],
9209
+ severity: "suggestion",
9210
+ title: "Fallible operation uses try/catch instead of Result type",
9211
+ rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
9212
+ suggestion: "Refactor error handling to use the Result type pattern.",
9213
+ evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
8910
9214
  validatedBy: "heuristic"
8911
9215
  });
8912
9216
  }
8913
9217
  }
8914
- switch (bundle.changeType) {
8915
- case "feature": {
8916
- const hasSpecContext = bundle.contextFiles.some(
8917
- (f) => f.reason === "spec" || f.reason === "convention"
8918
- );
8919
- if (!hasSpecContext && bundle.changedFiles.length > 0) {
8920
- const firstFile = bundle.changedFiles[0];
8921
- findings.push({
8922
- id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
8923
- file: firstFile.path,
8924
- lineRange: [1, 1],
8925
- domain: "compliance",
8926
- severity: "suggestion",
8927
- title: "No spec/design doc found for feature change",
8928
- rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
8929
- evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
8930
- validatedBy: "heuristic"
8931
- });
8932
- }
8933
- break;
8934
- }
8935
- case "bugfix": {
8936
- if (bundle.commitHistory.length === 0 && bundle.changedFiles.length > 0) {
8937
- const firstFile = bundle.changedFiles[0];
8938
- findings.push({
8939
- id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
8940
- file: firstFile.path,
8941
- lineRange: [1, 1],
8942
- domain: "compliance",
8943
- severity: "suggestion",
8944
- title: "Bugfix without commit history context",
8945
- rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
8946
- evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
8947
- validatedBy: "heuristic"
8948
- });
8949
- }
8950
- break;
8951
- }
8952
- case "refactor": {
8953
- break;
8954
- }
8955
- case "docs": {
8956
- break;
8957
- }
8958
- }
8959
- const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
8960
- if (resultTypeRule) {
8961
- for (const cf of bundle.changedFiles) {
8962
- const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
8963
- const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
8964
- if (hasTryCatch && !usesResult) {
8965
- findings.push({
8966
- id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
8967
- file: cf.path,
8968
- lineRange: [1, cf.lines],
8969
- domain: "compliance",
8970
- severity: "suggestion",
8971
- title: "Fallible operation uses try/catch instead of Result type",
8972
- rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
8973
- suggestion: "Refactor error handling to use the Result type pattern.",
8974
- evidence: [
8975
- `changeType: ${bundle.changeType}`,
8976
- `Convention rule: "${resultTypeRule.text}"`
8977
- ],
8978
- validatedBy: "heuristic"
8979
- });
8980
- }
8981
- }
8982
- }
8983
9218
  return findings;
8984
9219
  }
9220
+ function runComplianceAgent(bundle) {
9221
+ const rules = extractConventionRules(bundle);
9222
+ return [
9223
+ ...checkMissingJsDoc(bundle, rules),
9224
+ ...checkChangeTypeSpecific(bundle),
9225
+ ...checkResultTypeConvention(bundle, rules)
9226
+ ];
9227
+ }
8985
9228
  var BUG_DETECTION_DESCRIPTOR = {
8986
9229
  domain: "bug",
8987
9230
  tier: "strong",
@@ -9252,31 +9495,32 @@ var ARCHITECTURE_DESCRIPTOR = {
9252
9495
  ]
9253
9496
  };
9254
9497
  var LARGE_FILE_THRESHOLD = 300;
9498
+ function isViolationLine(line) {
9499
+ const lower = line.toLowerCase();
9500
+ return lower.includes("violation") || lower.includes("layer");
9501
+ }
9502
+ function createLayerViolationFinding(line, fallbackPath) {
9503
+ const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
9504
+ const file = fileMatch?.[1] ?? fallbackPath;
9505
+ const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
9506
+ return {
9507
+ id: makeFindingId("arch", file, lineNum, "layer violation"),
9508
+ file,
9509
+ lineRange: [lineNum, lineNum],
9510
+ domain: "architecture",
9511
+ severity: "critical",
9512
+ title: "Layer boundary violation detected by check-deps",
9513
+ rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
9514
+ suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
9515
+ evidence: [line.trim()],
9516
+ validatedBy: "heuristic"
9517
+ };
9518
+ }
9255
9519
  function detectLayerViolations(bundle) {
9256
- const findings = [];
9257
9520
  const checkDepsFile = bundle.contextFiles.find((f) => f.path === "harness-check-deps-output");
9258
- if (!checkDepsFile) return findings;
9259
- const lines = checkDepsFile.content.split("\n");
9260
- for (const line of lines) {
9261
- if (line.toLowerCase().includes("violation") || line.toLowerCase().includes("layer")) {
9262
- const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
9263
- const file = fileMatch?.[1] ?? bundle.changedFiles[0]?.path ?? "unknown";
9264
- const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
9265
- findings.push({
9266
- id: makeFindingId("arch", file, lineNum, "layer violation"),
9267
- file,
9268
- lineRange: [lineNum, lineNum],
9269
- domain: "architecture",
9270
- severity: "critical",
9271
- title: "Layer boundary violation detected by check-deps",
9272
- rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
9273
- suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
9274
- evidence: [line.trim()],
9275
- validatedBy: "heuristic"
9276
- });
9277
- }
9278
- }
9279
- return findings;
9521
+ if (!checkDepsFile) return [];
9522
+ const fallbackPath = bundle.changedFiles[0]?.path ?? "unknown";
9523
+ return checkDepsFile.content.split("\n").filter(isViolationLine).map((line) => createLayerViolationFinding(line, fallbackPath));
9280
9524
  }
9281
9525
  function detectLargeFiles(bundle) {
9282
9526
  const findings = [];
@@ -9298,45 +9542,61 @@ function detectLargeFiles(bundle) {
9298
9542
  }
9299
9543
  return findings;
9300
9544
  }
9545
+ function extractRelativeImports(content) {
9546
+ const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
9547
+ let match;
9548
+ const imports = /* @__PURE__ */ new Set();
9549
+ while ((match = importRegex.exec(content)) !== null) {
9550
+ const source = match[1];
9551
+ if (source.startsWith(".")) {
9552
+ imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
9553
+ }
9554
+ }
9555
+ return imports;
9556
+ }
9557
+ function fileBaseName(filePath) {
9558
+ return filePath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
9559
+ }
9560
+ function findCircularImportInCtxFile(ctxFile, changedFilePath, changedPaths, fileImports) {
9561
+ const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
9562
+ let ctxMatch;
9563
+ while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
9564
+ const ctxSource = ctxMatch[1];
9565
+ if (!ctxSource.startsWith(".")) continue;
9566
+ for (const changedPath of changedPaths) {
9567
+ const baseName = fileBaseName(changedPath);
9568
+ const ctxBaseName = fileBaseName(ctxFile.path);
9569
+ if (ctxSource.includes(baseName) && fileImports.has(ctxBaseName)) {
9570
+ return {
9571
+ id: makeFindingId("arch", changedFilePath, 1, `circular ${ctxFile.path}`),
9572
+ file: changedFilePath,
9573
+ lineRange: [1, 1],
9574
+ domain: "architecture",
9575
+ severity: "important",
9576
+ title: `Potential circular import between ${changedFilePath} and ${ctxFile.path}`,
9577
+ rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
9578
+ suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
9579
+ evidence: [
9580
+ `${changedFilePath} imports from a module that also imports from ${changedFilePath}`
9581
+ ],
9582
+ validatedBy: "heuristic"
9583
+ };
9584
+ }
9585
+ }
9586
+ }
9587
+ return null;
9588
+ }
9301
9589
  function detectCircularImports(bundle) {
9302
9590
  const findings = [];
9303
9591
  const changedPaths = new Set(bundle.changedFiles.map((f) => f.path));
9592
+ const relevantCtxFiles = bundle.contextFiles.filter(
9593
+ (f) => f.reason === "import" || f.reason === "graph-dependency"
9594
+ );
9304
9595
  for (const cf of bundle.changedFiles) {
9305
- const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
9306
- let match;
9307
- const imports = /* @__PURE__ */ new Set();
9308
- while ((match = importRegex.exec(cf.content)) !== null) {
9309
- const source = match[1];
9310
- if (source.startsWith(".")) {
9311
- imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
9312
- }
9313
- }
9314
- for (const ctxFile of bundle.contextFiles) {
9315
- if (ctxFile.reason !== "import" && ctxFile.reason !== "graph-dependency") continue;
9316
- const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
9317
- let ctxMatch;
9318
- while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
9319
- const ctxSource = ctxMatch[1];
9320
- if (ctxSource.startsWith(".")) {
9321
- for (const changedPath of changedPaths) {
9322
- const baseName = changedPath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
9323
- if (ctxSource.includes(baseName) && imports.has(ctxFile.path.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, ""))) {
9324
- findings.push({
9325
- id: makeFindingId("arch", cf.path, 1, `circular ${ctxFile.path}`),
9326
- file: cf.path,
9327
- lineRange: [1, 1],
9328
- domain: "architecture",
9329
- severity: "important",
9330
- title: `Potential circular import between ${cf.path} and ${ctxFile.path}`,
9331
- rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
9332
- suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
9333
- evidence: [`${cf.path} imports from a module that also imports from ${cf.path}`],
9334
- validatedBy: "heuristic"
9335
- });
9336
- }
9337
- }
9338
- }
9339
- }
9596
+ const imports = extractRelativeImports(cf.content);
9597
+ for (const ctxFile of relevantCtxFiles) {
9598
+ const finding = findCircularImportInCtxFile(ctxFile, cf.path, changedPaths, imports);
9599
+ if (finding) findings.push(finding);
9340
9600
  }
9341
9601
  }
9342
9602
  return findings;
@@ -9397,7 +9657,7 @@ function normalizePath(filePath, projectRoot) {
9397
9657
  let normalized = filePath;
9398
9658
  normalized = normalized.replace(/\\/g, "/");
9399
9659
  const normalizedRoot = projectRoot.replace(/\\/g, "/");
9400
- if (path15.isAbsolute(normalized)) {
9660
+ if (path17.isAbsolute(normalized)) {
9401
9661
  const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
9402
9662
  if (normalized.startsWith(root)) {
9403
9663
  normalized = normalized.slice(root.length);
@@ -9422,12 +9682,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
9422
9682
  while ((match = importRegex.exec(content)) !== null) {
9423
9683
  const importPath = match[1];
9424
9684
  if (!importPath.startsWith(".")) continue;
9425
- const dir = path15.dirname(current.file);
9426
- let resolved = path15.join(dir, importPath).replace(/\\/g, "/");
9685
+ const dir = path17.dirname(current.file);
9686
+ let resolved = path17.join(dir, importPath).replace(/\\/g, "/");
9427
9687
  if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
9428
9688
  resolved += ".ts";
9429
9689
  }
9430
- resolved = path15.normalize(resolved).replace(/\\/g, "/");
9690
+ resolved = path17.normalize(resolved).replace(/\\/g, "/");
9431
9691
  if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
9432
9692
  queue.push({ file: resolved, depth: current.depth + 1 });
9433
9693
  }
@@ -9444,7 +9704,7 @@ async function validateFindings(options) {
9444
9704
  if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
9445
9705
  continue;
9446
9706
  }
9447
- const absoluteFile = path15.isAbsolute(finding.file) ? finding.file : path15.join(projectRoot, finding.file).replace(/\\/g, "/");
9707
+ const absoluteFile = path17.isAbsolute(finding.file) ? finding.file : path17.join(projectRoot, finding.file).replace(/\\/g, "/");
9448
9708
  if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
9449
9709
  continue;
9450
9710
  }
@@ -9499,6 +9759,28 @@ async function validateFindings(options) {
9499
9759
  function rangesOverlap(a, b, gap) {
9500
9760
  return a[0] <= b[1] + gap && b[0] <= a[1] + gap;
9501
9761
  }
9762
+ function pickLongest(a, b) {
9763
+ if (a && b) return a.length >= b.length ? a : b;
9764
+ return a ?? b;
9765
+ }
9766
+ function buildMergedTitle(a, b, domains) {
9767
+ const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
9768
+ const domainList = [...domains].sort().join(", ");
9769
+ const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
9770
+ return { title: `[${domainList}] ${cleanTitle}`, primaryFinding };
9771
+ }
9772
+ function mergeSecurityFields(merged, primary, a, b) {
9773
+ const cweId = primary.cweId ?? a.cweId ?? b.cweId;
9774
+ const owaspCategory = primary.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
9775
+ const confidence = primary.confidence ?? a.confidence ?? b.confidence;
9776
+ const remediation = pickLongest(a.remediation, b.remediation);
9777
+ const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
9778
+ if (cweId !== void 0) merged.cweId = cweId;
9779
+ if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
9780
+ if (confidence !== void 0) merged.confidence = confidence;
9781
+ if (remediation !== void 0) merged.remediation = remediation;
9782
+ if (mergedRefs.length > 0) merged.references = mergedRefs;
9783
+ }
9502
9784
  function mergeFindings(a, b) {
9503
9785
  const highestSeverity = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a.severity : b.severity;
9504
9786
  const highestValidatedBy = (VALIDATED_BY_RANK[a.validatedBy] ?? 0) >= (VALIDATED_BY_RANK[b.validatedBy] ?? 0) ? a.validatedBy : b.validatedBy;
@@ -9508,18 +9790,12 @@ function mergeFindings(a, b) {
9508
9790
  Math.min(a.lineRange[0], b.lineRange[0]),
9509
9791
  Math.max(a.lineRange[1], b.lineRange[1])
9510
9792
  ];
9511
- const domains = /* @__PURE__ */ new Set();
9512
- domains.add(a.domain);
9513
- domains.add(b.domain);
9514
- const suggestion = a.suggestion && b.suggestion ? a.suggestion.length >= b.suggestion.length ? a.suggestion : b.suggestion : a.suggestion ?? b.suggestion;
9515
- const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
9516
- const domainList = [...domains].sort().join(", ");
9517
- const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
9518
- const title = `[${domainList}] ${cleanTitle}`;
9793
+ const domains = /* @__PURE__ */ new Set([a.domain, b.domain]);
9794
+ const suggestion = pickLongest(a.suggestion, b.suggestion);
9795
+ const { title, primaryFinding } = buildMergedTitle(a, b, domains);
9519
9796
  const merged = {
9520
9797
  id: primaryFinding.id,
9521
9798
  file: a.file,
9522
- // same file for all merged findings
9523
9799
  lineRange,
9524
9800
  domain: primaryFinding.domain,
9525
9801
  severity: highestSeverity,
@@ -9531,16 +9807,7 @@ function mergeFindings(a, b) {
9531
9807
  if (suggestion !== void 0) {
9532
9808
  merged.suggestion = suggestion;
9533
9809
  }
9534
- const cweId = primaryFinding.cweId ?? a.cweId ?? b.cweId;
9535
- const owaspCategory = primaryFinding.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
9536
- const confidence = primaryFinding.confidence ?? a.confidence ?? b.confidence;
9537
- const remediation = a.remediation && b.remediation ? a.remediation.length >= b.remediation.length ? a.remediation : b.remediation : a.remediation ?? b.remediation;
9538
- const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
9539
- if (cweId !== void 0) merged.cweId = cweId;
9540
- if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
9541
- if (confidence !== void 0) merged.confidence = confidence;
9542
- if (remediation !== void 0) merged.remediation = remediation;
9543
- if (mergedRefs.length > 0) merged.references = mergedRefs;
9810
+ mergeSecurityFields(merged, primaryFinding, a, b);
9544
9811
  return merged;
9545
9812
  }
9546
9813
  function deduplicateFindings(options) {
@@ -9704,6 +9971,17 @@ function formatTerminalOutput(options) {
9704
9971
  if (suggestionCount > 0) parts.push(`${suggestionCount} suggestion(s)`);
9705
9972
  sections.push(` Found ${issueCount} issue(s): ${parts.join(", ")}.`);
9706
9973
  }
9974
+ if (options.evidenceCoverage) {
9975
+ const ec = options.evidenceCoverage;
9976
+ sections.push("");
9977
+ sections.push("## Evidence Coverage\n");
9978
+ sections.push(` Evidence entries: ${ec.totalEntries}`);
9979
+ sections.push(
9980
+ ` Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
9981
+ );
9982
+ sections.push(` Uncited findings: ${ec.uncitedCount} (flagged as [UNVERIFIED])`);
9983
+ sections.push(` Coverage: ${ec.coveragePercentage}%`);
9984
+ }
9707
9985
  return sections.join("\n");
9708
9986
  }
9709
9987
  var SMALL_SUGGESTION_LINE_LIMIT = 10;
@@ -9778,8 +10056,105 @@ function formatGitHubSummary(options) {
9778
10056
  const assessment = determineAssessment(findings);
9779
10057
  const assessmentLabel = assessment === "approve" ? "Approve" : assessment === "comment" ? "Comment" : "Request Changes";
9780
10058
  sections.push(`## Assessment: ${assessmentLabel}`);
10059
+ if (options.evidenceCoverage) {
10060
+ const ec = options.evidenceCoverage;
10061
+ sections.push("");
10062
+ sections.push("## Evidence Coverage\n");
10063
+ sections.push(`- Evidence entries: ${ec.totalEntries}`);
10064
+ sections.push(
10065
+ `- Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
10066
+ );
10067
+ sections.push(`- Uncited findings: ${ec.uncitedCount} (flagged as \\[UNVERIFIED\\])`);
10068
+ sections.push(`- Coverage: ${ec.coveragePercentage}%`);
10069
+ }
9781
10070
  return sections.join("\n");
9782
10071
  }
10072
+ var FILE_LINE_RANGE_PATTERN = /^([\w./@-]+\.\w+):(\d+)-(\d+)/;
10073
+ var FILE_LINE_PATTERN = /^([\w./@-]+\.\w+):(\d+)/;
10074
+ var FILE_ONLY_PATTERN = /^([\w./@-]+\.\w+)\s/;
10075
+ function parseEvidenceRef(content) {
10076
+ const trimmed = content.trim();
10077
+ const rangeMatch = trimmed.match(FILE_LINE_RANGE_PATTERN);
10078
+ if (rangeMatch) {
10079
+ return {
10080
+ file: rangeMatch[1],
10081
+ lineStart: parseInt(rangeMatch[2], 10),
10082
+ lineEnd: parseInt(rangeMatch[3], 10)
10083
+ };
10084
+ }
10085
+ const lineMatch = trimmed.match(FILE_LINE_PATTERN);
10086
+ if (lineMatch) {
10087
+ return {
10088
+ file: lineMatch[1],
10089
+ lineStart: parseInt(lineMatch[2], 10)
10090
+ };
10091
+ }
10092
+ const fileMatch = trimmed.match(FILE_ONLY_PATTERN);
10093
+ if (fileMatch) {
10094
+ return { file: fileMatch[1] };
10095
+ }
10096
+ return null;
10097
+ }
10098
+ function evidenceMatchesFinding(ref, finding) {
10099
+ if (ref.file !== finding.file) return false;
10100
+ if (ref.lineStart === void 0) return true;
10101
+ const [findStart, findEnd] = finding.lineRange;
10102
+ if (ref.lineEnd !== void 0) {
10103
+ return ref.lineStart <= findEnd && ref.lineEnd >= findStart;
10104
+ }
10105
+ return ref.lineStart >= findStart && ref.lineStart <= findEnd;
10106
+ }
10107
+ function checkEvidenceCoverage(findings, evidenceEntries) {
10108
+ if (findings.length === 0) {
10109
+ return {
10110
+ totalEntries: evidenceEntries.filter((e) => e.status === "active").length,
10111
+ findingsWithEvidence: 0,
10112
+ uncitedCount: 0,
10113
+ uncitedFindings: [],
10114
+ coveragePercentage: 100
10115
+ };
10116
+ }
10117
+ const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
10118
+ const evidenceRefs = [];
10119
+ for (const entry of activeEvidence) {
10120
+ const ref = parseEvidenceRef(entry.content);
10121
+ if (ref) evidenceRefs.push(ref);
10122
+ }
10123
+ let findingsWithEvidence = 0;
10124
+ const uncitedFindings = [];
10125
+ for (const finding of findings) {
10126
+ const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
10127
+ if (hasEvidence) {
10128
+ findingsWithEvidence++;
10129
+ } else {
10130
+ uncitedFindings.push(finding.title);
10131
+ }
10132
+ }
10133
+ const uncitedCount = findings.length - findingsWithEvidence;
10134
+ const coveragePercentage = Math.round(findingsWithEvidence / findings.length * 100);
10135
+ return {
10136
+ totalEntries: activeEvidence.length,
10137
+ findingsWithEvidence,
10138
+ uncitedCount,
10139
+ uncitedFindings,
10140
+ coveragePercentage
10141
+ };
10142
+ }
10143
+ function tagUncitedFindings(findings, evidenceEntries) {
10144
+ const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
10145
+ const evidenceRefs = [];
10146
+ for (const entry of activeEvidence) {
10147
+ const ref = parseEvidenceRef(entry.content);
10148
+ if (ref) evidenceRefs.push(ref);
10149
+ }
10150
+ for (const finding of findings) {
10151
+ const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
10152
+ if (!hasEvidence && !finding.title.startsWith("[UNVERIFIED]")) {
10153
+ finding.title = `[UNVERIFIED] ${finding.title}`;
10154
+ }
10155
+ }
10156
+ return findings;
10157
+ }
9783
10158
  async function runReviewPipeline(options) {
9784
10159
  const {
9785
10160
  projectRoot,
@@ -9791,7 +10166,8 @@ async function runReviewPipeline(options) {
9791
10166
  conventionFiles,
9792
10167
  checkDepsOutput,
9793
10168
  config = {},
9794
- commitHistory
10169
+ commitHistory,
10170
+ sessionSlug
9795
10171
  } = options;
9796
10172
  if (flags.ci && prMetadata) {
9797
10173
  const eligibility = checkEligibility(prMetadata, true);
@@ -9887,13 +10263,25 @@ async function runReviewPipeline(options) {
9887
10263
  projectRoot,
9888
10264
  fileContents
9889
10265
  });
10266
+ let evidenceCoverage;
10267
+ if (sessionSlug) {
10268
+ try {
10269
+ const evidenceResult = await readSessionSection(projectRoot, sessionSlug, "evidence");
10270
+ if (evidenceResult.ok) {
10271
+ evidenceCoverage = checkEvidenceCoverage(validatedFindings, evidenceResult.value);
10272
+ tagUncitedFindings(validatedFindings, evidenceResult.value);
10273
+ }
10274
+ } catch {
10275
+ }
10276
+ }
9890
10277
  const dedupedFindings = deduplicateFindings({ findings: validatedFindings });
9891
10278
  const strengths = [];
9892
10279
  const assessment = determineAssessment(dedupedFindings);
9893
10280
  const exitCode = getExitCode(assessment);
9894
10281
  const terminalOutput = formatTerminalOutput({
9895
10282
  findings: dedupedFindings,
9896
- strengths
10283
+ strengths,
10284
+ ...evidenceCoverage != null ? { evidenceCoverage } : {}
9897
10285
  });
9898
10286
  let githubComments = [];
9899
10287
  if (flags.comment) {
@@ -9908,7 +10296,8 @@ async function runReviewPipeline(options) {
9908
10296
  terminalOutput,
9909
10297
  githubComments,
9910
10298
  exitCode,
9911
- ...mechanicalResult !== void 0 ? { mechanicalResult } : {}
10299
+ ...mechanicalResult != null ? { mechanicalResult } : {},
10300
+ ...evidenceCoverage != null ? { evidenceCoverage } : {}
9912
10301
  };
9913
10302
  }
9914
10303
  var VALID_STATUSES = /* @__PURE__ */ new Set([
@@ -10008,13 +10397,29 @@ function parseFeatures(sectionBody) {
10008
10397
  }
10009
10398
  return Ok(features);
10010
10399
  }
10011
- function parseFeatureFields(name, body) {
10400
+ function extractFieldMap(body) {
10012
10401
  const fieldMap = /* @__PURE__ */ new Map();
10013
10402
  const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
10014
10403
  let match;
10015
10404
  while ((match = fieldPattern.exec(body)) !== null) {
10016
10405
  fieldMap.set(match[1], match[2]);
10017
10406
  }
10407
+ return fieldMap;
10408
+ }
10409
+ function parseListField(fieldMap, ...keys) {
10410
+ let raw = EM_DASH;
10411
+ for (const key of keys) {
10412
+ const val = fieldMap.get(key);
10413
+ if (val !== void 0) {
10414
+ raw = val;
10415
+ break;
10416
+ }
10417
+ }
10418
+ if (raw === EM_DASH || raw === "none") return [];
10419
+ return raw.split(",").map((s) => s.trim());
10420
+ }
10421
+ function parseFeatureFields(name, body) {
10422
+ const fieldMap = extractFieldMap(body);
10018
10423
  const statusRaw = fieldMap.get("Status");
10019
10424
  if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
10020
10425
  return Err(
@@ -10023,15 +10428,17 @@ function parseFeatureFields(name, body) {
10023
10428
  )
10024
10429
  );
10025
10430
  }
10026
- const status = statusRaw;
10027
10431
  const specRaw = fieldMap.get("Spec") ?? EM_DASH;
10028
- const spec = specRaw === EM_DASH ? null : specRaw;
10029
- const plansRaw = fieldMap.get("Plans") ?? fieldMap.get("Plan") ?? EM_DASH;
10030
- const plans = plansRaw === EM_DASH || plansRaw === "none" ? [] : plansRaw.split(",").map((p) => p.trim());
10031
- const blockedByRaw = fieldMap.get("Blocked by") ?? fieldMap.get("Blockers") ?? EM_DASH;
10032
- const blockedBy = blockedByRaw === EM_DASH || blockedByRaw === "none" ? [] : blockedByRaw.split(",").map((b) => b.trim());
10033
- const summary = fieldMap.get("Summary") ?? "";
10034
- return Ok({ name, status, spec, plans, blockedBy, summary });
10432
+ const plans = parseListField(fieldMap, "Plans", "Plan");
10433
+ const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
10434
+ return Ok({
10435
+ name,
10436
+ status: statusRaw,
10437
+ spec: specRaw === EM_DASH ? null : specRaw,
10438
+ plans,
10439
+ blockedBy,
10440
+ summary: fieldMap.get("Summary") ?? ""
10441
+ });
10035
10442
  }
10036
10443
  var EM_DASH2 = "\u2014";
10037
10444
  function serializeRoadmap(roadmap) {
@@ -10091,10 +10498,10 @@ function inferStatus(feature, projectPath, allFeatures) {
10091
10498
  const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
10092
10499
  const useRootState = featuresWithPlans.length <= 1;
10093
10500
  if (useRootState) {
10094
- const rootStatePath = path16.join(projectPath, ".harness", "state.json");
10095
- if (fs16.existsSync(rootStatePath)) {
10501
+ const rootStatePath = path18.join(projectPath, ".harness", "state.json");
10502
+ if (fs18.existsSync(rootStatePath)) {
10096
10503
  try {
10097
- const raw = fs16.readFileSync(rootStatePath, "utf-8");
10504
+ const raw = fs18.readFileSync(rootStatePath, "utf-8");
10098
10505
  const state = JSON.parse(raw);
10099
10506
  if (state.progress) {
10100
10507
  for (const status of Object.values(state.progress)) {
@@ -10105,16 +10512,16 @@ function inferStatus(feature, projectPath, allFeatures) {
10105
10512
  }
10106
10513
  }
10107
10514
  }
10108
- const sessionsDir = path16.join(projectPath, ".harness", "sessions");
10109
- if (fs16.existsSync(sessionsDir)) {
10515
+ const sessionsDir = path18.join(projectPath, ".harness", "sessions");
10516
+ if (fs18.existsSync(sessionsDir)) {
10110
10517
  try {
10111
- const sessionDirs = fs16.readdirSync(sessionsDir, { withFileTypes: true });
10518
+ const sessionDirs = fs18.readdirSync(sessionsDir, { withFileTypes: true });
10112
10519
  for (const entry of sessionDirs) {
10113
10520
  if (!entry.isDirectory()) continue;
10114
- const autopilotPath = path16.join(sessionsDir, entry.name, "autopilot-state.json");
10115
- if (!fs16.existsSync(autopilotPath)) continue;
10521
+ const autopilotPath = path18.join(sessionsDir, entry.name, "autopilot-state.json");
10522
+ if (!fs18.existsSync(autopilotPath)) continue;
10116
10523
  try {
10117
- const raw = fs16.readFileSync(autopilotPath, "utf-8");
10524
+ const raw = fs18.readFileSync(autopilotPath, "utf-8");
10118
10525
  const autopilot = JSON.parse(raw);
10119
10526
  if (!autopilot.phases) continue;
10120
10527
  const linkedPhases = autopilot.phases.filter(
@@ -10194,10 +10601,10 @@ var ProjectScanner = class {
10194
10601
  this.rootDir = rootDir;
10195
10602
  }
10196
10603
  async scan() {
10197
- let projectName = path17.basename(this.rootDir);
10604
+ let projectName = path19.basename(this.rootDir);
10198
10605
  try {
10199
- const pkgPath = path17.join(this.rootDir, "package.json");
10200
- const pkgRaw = await fs17.readFile(pkgPath, "utf-8");
10606
+ const pkgPath = path19.join(this.rootDir, "package.json");
10607
+ const pkgRaw = await fs19.readFile(pkgPath, "utf-8");
10201
10608
  const pkg = JSON.parse(pkgRaw);
10202
10609
  if (pkg.name) projectName = pkg.name;
10203
10610
  } catch {
@@ -10310,13 +10717,13 @@ var BlueprintGenerator = class {
10310
10717
  styles: STYLES,
10311
10718
  scripts: SCRIPTS
10312
10719
  });
10313
- await fs18.mkdir(options.outputDir, { recursive: true });
10314
- await fs18.writeFile(path18.join(options.outputDir, "index.html"), html);
10720
+ await fs20.mkdir(options.outputDir, { recursive: true });
10721
+ await fs20.writeFile(path20.join(options.outputDir, "index.html"), html);
10315
10722
  }
10316
10723
  };
10317
10724
  function getStatePath() {
10318
10725
  const home = process.env["HOME"] || os.homedir();
10319
- return path19.join(home, ".harness", "update-check.json");
10726
+ return path21.join(home, ".harness", "update-check.json");
10320
10727
  }
10321
10728
  function isUpdateCheckEnabled(configInterval) {
10322
10729
  if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
@@ -10329,7 +10736,7 @@ function shouldRunCheck(state, intervalMs) {
10329
10736
  }
10330
10737
  function readCheckState() {
10331
10738
  try {
10332
- const raw = fs19.readFileSync(getStatePath(), "utf-8");
10739
+ const raw = fs21.readFileSync(getStatePath(), "utf-8");
10333
10740
  const parsed = JSON.parse(raw);
10334
10741
  if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
10335
10742
  const state = parsed;
@@ -10346,7 +10753,7 @@ function readCheckState() {
10346
10753
  }
10347
10754
  function spawnBackgroundCheck(currentVersion) {
10348
10755
  const statePath = getStatePath();
10349
- const stateDir = path19.dirname(statePath);
10756
+ const stateDir = path21.dirname(statePath);
10350
10757
  const script = `
10351
10758
  const { execSync } = require('child_process');
10352
10759
  const fs = require('fs');
@@ -10398,7 +10805,7 @@ function getUpdateNotification(currentVersion) {
10398
10805
  return `Update available: v${currentVersion} -> v${state.latestVersion}
10399
10806
  Run "harness update" to upgrade.`;
10400
10807
  }
10401
- var VERSION = "0.13.0";
10808
+ var VERSION = "0.14.0";
10402
10809
 
10403
10810
  export {
10404
10811
  ArchMetricCategorySchema,
@@ -10516,7 +10923,7 @@ export {
10516
10923
  NoOpSink,
10517
10924
  syncConstraintNodes,
10518
10925
  detectStaleConstraints,
10519
- resolveThresholds,
10926
+ resolveThresholds2 as resolveThresholds,
10520
10927
  FailureEntrySchema,
10521
10928
  HandoffSchema,
10522
10929
  GateResultSchema,
@@ -10558,6 +10965,11 @@ export {
10558
10965
  writeSessionSummary,
10559
10966
  loadSessionSummary,
10560
10967
  listActiveSessions,
10968
+ readSessionSections,
10969
+ readSessionSection,
10970
+ appendSessionEntry,
10971
+ updateSessionEntryStatus,
10972
+ archiveSession,
10561
10973
  executeWorkflow,
10562
10974
  runPipeline,
10563
10975
  runMultiTurnPipeline,
@@ -10607,6 +11019,8 @@ export {
10607
11019
  isSmallSuggestion,
10608
11020
  formatGitHubComment,
10609
11021
  formatGitHubSummary,
11022
+ checkEvidenceCoverage,
11023
+ tagUncitedFindings,
10610
11024
  runReviewPipeline,
10611
11025
  parseRoadmap,
10612
11026
  serializeRoadmap,