@harness-engineering/core 0.7.0 → 0.8.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.
package/dist/index.js CHANGED
@@ -413,6 +413,9 @@ function resolveLinkPath(linkPath, baseDir) {
413
413
  return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
414
414
  }
415
415
  async function validateAgentsMap(path3 = "./AGENTS.md") {
416
+ console.warn(
417
+ "[harness] validateAgentsMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
418
+ );
416
419
  const contentResult = await readFileContent(path3);
417
420
  if (!contentResult.ok) {
418
421
  return (0, import_types.Err)(
@@ -488,7 +491,21 @@ function suggestSection(filePath, domain) {
488
491
  return `${domain} Reference`;
489
492
  }
490
493
  async function checkDocCoverage(domain, options = {}) {
491
- const { docsDir = "./docs", sourceDir = "./src", excludePatterns = [] } = options;
494
+ const { docsDir = "./docs", sourceDir = "./src", excludePatterns = [], graphCoverage } = options;
495
+ if (graphCoverage) {
496
+ const gaps = graphCoverage.undocumented.map((file) => ({
497
+ file,
498
+ suggestedSection: suggestSection(file, domain),
499
+ importance: determineImportance(file)
500
+ }));
501
+ return (0, import_types.Ok)({
502
+ domain,
503
+ documented: graphCoverage.documented,
504
+ undocumented: graphCoverage.undocumented,
505
+ coveragePercentage: graphCoverage.coveragePercentage,
506
+ gaps
507
+ });
508
+ }
492
509
  try {
493
510
  const sourceFiles = await findFiles("**/*.{ts,js,tsx,jsx}", sourceDir);
494
511
  const filteredSourceFiles = sourceFiles.filter((file) => {
@@ -566,6 +583,9 @@ function suggestFix(path3, existingFiles) {
566
583
  return `Create the file "${path3}" or remove the link`;
567
584
  }
568
585
  async function validateKnowledgeMap(rootDir = process.cwd()) {
586
+ console.warn(
587
+ "[harness] validateKnowledgeMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
588
+ );
569
589
  const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
570
590
  const agentsResult = await validateAgentsMap(agentsPath);
571
591
  if (!agentsResult.ok) {
@@ -639,18 +659,9 @@ function matchesExcludePattern(relativePath, excludePatterns) {
639
659
  return regex.test(relativePath);
640
660
  });
641
661
  }
642
- async function generateAgentsMap(config) {
662
+ async function generateAgentsMap(config, graphSections) {
643
663
  const { rootDir, includePaths, excludePaths, sections = DEFAULT_SECTIONS } = config;
644
664
  try {
645
- const allFiles = [];
646
- for (const pattern of includePaths) {
647
- const files = await findFiles(pattern, rootDir);
648
- allFiles.push(...files);
649
- }
650
- const filteredFiles = allFiles.filter((file) => {
651
- const relativePath = (0, import_path4.relative)(rootDir, file);
652
- return !matchesExcludePattern(relativePath, excludePaths);
653
- });
654
665
  const lines = [];
655
666
  lines.push("# AI Agent Knowledge Map");
656
667
  lines.push("");
@@ -660,41 +671,68 @@ async function generateAgentsMap(config) {
660
671
  lines.push("");
661
672
  lines.push("> Add a brief description of this project, its purpose, and key technologies.");
662
673
  lines.push("");
663
- lines.push("## Repository Structure");
664
- lines.push("");
665
- const grouped = groupByDirectory(filteredFiles, rootDir);
666
- for (const [dir, files] of grouped) {
667
- if (dir !== ".") {
668
- lines.push(`### ${dir}/`);
674
+ if (graphSections) {
675
+ for (const section of graphSections) {
676
+ lines.push(`## ${section.name}`);
669
677
  lines.push("");
670
- }
671
- for (const file of files.slice(0, 10)) {
672
- lines.push(formatFileLink(file));
673
- }
674
- if (files.length > 10) {
675
- lines.push(`- _... and ${files.length - 10} more files_`);
676
- }
677
- lines.push("");
678
- }
679
- for (const section of sections) {
680
- lines.push(`## ${section.name}`);
681
- lines.push("");
682
- if (section.description) {
683
- lines.push(section.description);
678
+ if (section.description) {
679
+ lines.push(section.description);
680
+ lines.push("");
681
+ }
682
+ for (const file of section.files.slice(0, 20)) {
683
+ lines.push(formatFileLink(file));
684
+ }
685
+ if (section.files.length > 20) {
686
+ lines.push(`- _... and ${section.files.length - 20} more files_`);
687
+ }
684
688
  lines.push("");
685
689
  }
686
- const sectionFiles = await findFiles(section.pattern, rootDir);
687
- const filteredSectionFiles = sectionFiles.filter((file) => {
690
+ } else {
691
+ const allFiles = [];
692
+ for (const pattern of includePaths) {
693
+ const files = await findFiles(pattern, rootDir);
694
+ allFiles.push(...files);
695
+ }
696
+ const filteredFiles = allFiles.filter((file) => {
688
697
  const relativePath = (0, import_path4.relative)(rootDir, file);
689
698
  return !matchesExcludePattern(relativePath, excludePaths);
690
699
  });
691
- for (const file of filteredSectionFiles.slice(0, 20)) {
692
- lines.push(formatFileLink((0, import_path4.relative)(rootDir, file)));
700
+ lines.push("## Repository Structure");
701
+ lines.push("");
702
+ const grouped = groupByDirectory(filteredFiles, rootDir);
703
+ for (const [dir, files] of grouped) {
704
+ if (dir !== ".") {
705
+ lines.push(`### ${dir}/`);
706
+ lines.push("");
707
+ }
708
+ for (const file of files.slice(0, 10)) {
709
+ lines.push(formatFileLink(file));
710
+ }
711
+ if (files.length > 10) {
712
+ lines.push(`- _... and ${files.length - 10} more files_`);
713
+ }
714
+ lines.push("");
693
715
  }
694
- if (filteredSectionFiles.length > 20) {
695
- lines.push(`- _... and ${filteredSectionFiles.length - 20} more files_`);
716
+ for (const section of sections) {
717
+ lines.push(`## ${section.name}`);
718
+ lines.push("");
719
+ if (section.description) {
720
+ lines.push(section.description);
721
+ lines.push("");
722
+ }
723
+ const sectionFiles = await findFiles(section.pattern, rootDir);
724
+ const filteredSectionFiles = sectionFiles.filter((file) => {
725
+ const relativePath = (0, import_path4.relative)(rootDir, file);
726
+ return !matchesExcludePattern(relativePath, excludePaths);
727
+ });
728
+ for (const file of filteredSectionFiles.slice(0, 20)) {
729
+ lines.push(formatFileLink((0, import_path4.relative)(rootDir, file)));
730
+ }
731
+ if (filteredSectionFiles.length > 20) {
732
+ lines.push(`- _... and ${filteredSectionFiles.length - 20} more files_`);
733
+ }
734
+ lines.push("");
696
735
  }
697
- lines.push("");
698
736
  }
699
737
  lines.push("## Development Workflow");
700
738
  lines.push("");
@@ -724,7 +762,21 @@ var DEFAULT_RATIOS = {
724
762
  interfaces: 0.1,
725
763
  reserve: 0.1
726
764
  };
727
- function contextBudget(totalTokens, overrides) {
765
+ var NODE_TYPE_TO_CATEGORY = {
766
+ file: "activeCode",
767
+ function: "activeCode",
768
+ class: "activeCode",
769
+ method: "activeCode",
770
+ interface: "interfaces",
771
+ variable: "interfaces",
772
+ adr: "projectManifest",
773
+ document: "projectManifest",
774
+ spec: "taskSpec",
775
+ task: "taskSpec",
776
+ prompt: "systemPrompt",
777
+ system: "systemPrompt"
778
+ };
779
+ function contextBudget(totalTokens, overrides, graphDensity) {
728
780
  const ratios = {
729
781
  systemPrompt: DEFAULT_RATIOS.systemPrompt,
730
782
  projectManifest: DEFAULT_RATIOS.projectManifest,
@@ -733,6 +785,52 @@ function contextBudget(totalTokens, overrides) {
733
785
  interfaces: DEFAULT_RATIOS.interfaces,
734
786
  reserve: DEFAULT_RATIOS.reserve
735
787
  };
788
+ if (graphDensity) {
789
+ const categoryWeights = {
790
+ systemPrompt: 0,
791
+ projectManifest: 0,
792
+ taskSpec: 0,
793
+ activeCode: 0,
794
+ interfaces: 0,
795
+ reserve: 0
796
+ };
797
+ for (const [nodeType, count] of Object.entries(graphDensity)) {
798
+ const category = NODE_TYPE_TO_CATEGORY[nodeType];
799
+ if (category) {
800
+ categoryWeights[category] += count;
801
+ }
802
+ }
803
+ const totalWeight = Object.values(categoryWeights).reduce((sum, w) => sum + w, 0);
804
+ if (totalWeight > 0) {
805
+ const MIN_ALLOCATION = 0.01;
806
+ for (const key of Object.keys(ratios)) {
807
+ if (categoryWeights[key] > 0) {
808
+ ratios[key] = categoryWeights[key] / totalWeight;
809
+ } else {
810
+ ratios[key] = MIN_ALLOCATION;
811
+ }
812
+ }
813
+ if (ratios.reserve < DEFAULT_RATIOS.reserve) {
814
+ ratios.reserve = DEFAULT_RATIOS.reserve;
815
+ }
816
+ if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt) {
817
+ ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
818
+ }
819
+ const ratioSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
820
+ for (const key of Object.keys(ratios)) {
821
+ ratios[key] = ratios[key] / ratioSum;
822
+ }
823
+ for (const key of Object.keys(ratios)) {
824
+ if (ratios[key] < MIN_ALLOCATION) {
825
+ ratios[key] = MIN_ALLOCATION;
826
+ }
827
+ }
828
+ const finalSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
829
+ for (const key of Object.keys(ratios)) {
830
+ ratios[key] = ratios[key] / finalSum;
831
+ }
832
+ }
833
+ }
736
834
  if (overrides) {
737
835
  let overrideSum = 0;
738
836
  const overrideKeys = [];
@@ -746,12 +844,12 @@ function contextBudget(totalTokens, overrides) {
746
844
  }
747
845
  if (overrideKeys.length > 0 && overrideKeys.length < 6) {
748
846
  const remaining = 1 - overrideSum;
749
- const nonOverridden = Object.keys(DEFAULT_RATIOS).filter(
847
+ const nonOverridden = Object.keys(ratios).filter(
750
848
  (k) => !overrideKeys.includes(k)
751
849
  );
752
- const originalSum = nonOverridden.reduce((sum, k) => sum + DEFAULT_RATIOS[k], 0);
850
+ const originalSum = nonOverridden.reduce((sum, k) => sum + ratios[k], 0);
753
851
  for (const k of nonOverridden) {
754
- ratios[k] = remaining * (DEFAULT_RATIOS[k] / originalSum);
852
+ ratios[k] = remaining * (ratios[k] / originalSum);
755
853
  }
756
854
  }
757
855
  }
@@ -802,7 +900,7 @@ var PHASE_PRIORITIES = {
802
900
  { category: "config", patterns: ["harness.config.json", "package.json"], priority: 5 }
803
901
  ]
804
902
  };
805
- function contextFilter(phase, maxCategories) {
903
+ function contextFilter(phase, maxCategories, graphFilePaths) {
806
904
  const categories = PHASE_PRIORITIES[phase];
807
905
  const limit = maxCategories ?? categories.length;
808
906
  const included = categories.slice(0, limit);
@@ -811,7 +909,7 @@ function contextFilter(phase, maxCategories) {
811
909
  phase,
812
910
  includedCategories: included.map((c) => c.category),
813
911
  excludedCategories: excluded.map((c) => c.category),
814
- filePatterns: included.flatMap((c) => c.patterns)
912
+ filePatterns: graphFilePaths ?? included.flatMap((c) => c.patterns)
815
913
  };
816
914
  }
817
915
  function getPhaseCategories(phase) {
@@ -855,7 +953,13 @@ function getImportType(imp) {
855
953
  if (imp.kind === "type") return "type-only";
856
954
  return "static";
857
955
  }
858
- async function buildDependencyGraph(files, parser) {
956
+ async function buildDependencyGraph(files, parser, graphDependencyData) {
957
+ if (graphDependencyData) {
958
+ return (0, import_types.Ok)({
959
+ nodes: graphDependencyData.nodes,
960
+ edges: graphDependencyData.edges
961
+ });
962
+ }
859
963
  const nodes = [...files];
860
964
  const edges = [];
861
965
  for (const file of files) {
@@ -905,7 +1009,19 @@ function checkLayerViolations(graph, layers, rootDir) {
905
1009
  return violations;
906
1010
  }
907
1011
  async function validateDependencies(config) {
908
- const { layers, rootDir, parser, fallbackBehavior = "error" } = config;
1012
+ const { layers, rootDir, parser, fallbackBehavior = "error", graphDependencyData } = config;
1013
+ if (graphDependencyData) {
1014
+ const graphResult2 = await buildDependencyGraph([], parser, graphDependencyData);
1015
+ if (!graphResult2.ok) {
1016
+ return (0, import_types.Err)(graphResult2.error);
1017
+ }
1018
+ const violations2 = checkLayerViolations(graphResult2.value, layers, rootDir);
1019
+ return (0, import_types.Ok)({
1020
+ valid: violations2.length === 0,
1021
+ violations: violations2,
1022
+ graph: graphResult2.value
1023
+ });
1024
+ }
909
1025
  const healthResult = await parser.health();
910
1026
  if (!healthResult.ok || !healthResult.value.available) {
911
1027
  if (fallbackBehavior === "skip") {
@@ -1039,8 +1155,8 @@ function detectCircularDeps(graph) {
1039
1155
  largestCycle
1040
1156
  });
1041
1157
  }
1042
- async function detectCircularDepsInFiles(files, parser) {
1043
- const graphResult = await buildDependencyGraph(files, parser);
1158
+ async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
1159
+ const graphResult = await buildDependencyGraph(files, parser, graphDependencyData);
1044
1160
  if (!graphResult.ok) {
1045
1161
  return graphResult;
1046
1162
  }
@@ -1760,7 +1876,45 @@ async function checkStructureDrift(snapshot, _config) {
1760
1876
  }
1761
1877
  return drifts;
1762
1878
  }
1763
- async function detectDocDrift(snapshot, config) {
1879
+ async function detectDocDrift(snapshot, config, graphDriftData) {
1880
+ if (graphDriftData) {
1881
+ const drifts2 = [];
1882
+ for (const target of graphDriftData.missingTargets) {
1883
+ drifts2.push({
1884
+ type: "api-signature",
1885
+ docFile: target,
1886
+ line: 0,
1887
+ reference: target,
1888
+ context: "graph-missing-target",
1889
+ issue: "NOT_FOUND",
1890
+ details: `Graph node "${target}" has no matching code target`,
1891
+ confidence: "high"
1892
+ });
1893
+ }
1894
+ for (const edge of graphDriftData.staleEdges) {
1895
+ drifts2.push({
1896
+ type: "api-signature",
1897
+ docFile: edge.docNodeId,
1898
+ line: 0,
1899
+ reference: edge.codeNodeId,
1900
+ context: `graph-stale-edge:${edge.edgeType}`,
1901
+ issue: "NOT_FOUND",
1902
+ details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
1903
+ confidence: "medium"
1904
+ });
1905
+ }
1906
+ const severity2 = drifts2.length === 0 ? "none" : drifts2.length <= 3 ? "low" : drifts2.length <= 10 ? "medium" : "high";
1907
+ return (0, import_types.Ok)({
1908
+ drifts: drifts2,
1909
+ stats: {
1910
+ docsScanned: graphDriftData.staleEdges.length,
1911
+ referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
1912
+ driftsFound: drifts2.length,
1913
+ byType: { api: drifts2.length, example: 0, structure: 0 }
1914
+ },
1915
+ severity: severity2
1916
+ });
1917
+ }
1764
1918
  const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
1765
1919
  const drifts = [];
1766
1920
  if (fullConfig.checkApiSignatures) {
@@ -1998,7 +2152,54 @@ function findDeadInternals(snapshot, _reachability) {
1998
2152
  }
1999
2153
  return deadInternals;
2000
2154
  }
2001
- async function detectDeadCode(snapshot) {
2155
+ async function detectDeadCode(snapshot, graphDeadCodeData) {
2156
+ if (graphDeadCodeData) {
2157
+ const deadFiles2 = [];
2158
+ const deadExports2 = [];
2159
+ const fileTypes = /* @__PURE__ */ new Set(["file", "module"]);
2160
+ const exportTypes = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
2161
+ for (const node of graphDeadCodeData.unreachableNodes) {
2162
+ if (fileTypes.has(node.type)) {
2163
+ deadFiles2.push({
2164
+ path: node.path || node.id,
2165
+ reason: "NO_IMPORTERS",
2166
+ exportCount: 0,
2167
+ lineCount: 0
2168
+ });
2169
+ } else if (exportTypes.has(node.type)) {
2170
+ const exportType = node.type === "method" ? "function" : node.type;
2171
+ deadExports2.push({
2172
+ file: node.path || node.id,
2173
+ name: node.name,
2174
+ line: 0,
2175
+ type: exportType,
2176
+ isDefault: false,
2177
+ reason: "NO_IMPORTERS"
2178
+ });
2179
+ }
2180
+ }
2181
+ const reachableCount = graphDeadCodeData.reachableNodeIds instanceof Set ? graphDeadCodeData.reachableNodeIds.size : graphDeadCodeData.reachableNodeIds.length;
2182
+ const fileNodes = graphDeadCodeData.unreachableNodes.filter((n) => fileTypes.has(n.type));
2183
+ const exportNodes = graphDeadCodeData.unreachableNodes.filter((n) => exportTypes.has(n.type));
2184
+ const totalFiles = reachableCount + fileNodes.length;
2185
+ const totalExports2 = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
2186
+ const report2 = {
2187
+ deadExports: deadExports2,
2188
+ deadFiles: deadFiles2,
2189
+ deadInternals: [],
2190
+ unusedImports: [],
2191
+ stats: {
2192
+ filesAnalyzed: totalFiles,
2193
+ entryPointsUsed: [],
2194
+ totalExports: totalExports2,
2195
+ deadExportCount: deadExports2.length,
2196
+ totalFiles,
2197
+ deadFileCount: deadFiles2.length,
2198
+ estimatedDeadLines: 0
2199
+ }
2200
+ };
2201
+ return (0, import_types.Ok)(report2);
2202
+ }
2002
2203
  const reachability = buildReachabilityMap(snapshot);
2003
2204
  const usageMap = buildExportUsageMap(snapshot);
2004
2205
  const deadExports = findDeadExports(snapshot, usageMap, reachability);
@@ -2324,22 +2525,39 @@ var EntropyAnalyzer = class {
2324
2525
  };
2325
2526
  }
2326
2527
  /**
2327
- * Run full entropy analysis
2528
+ * Run full entropy analysis.
2529
+ * When graphOptions is provided, passes graph data to drift and dead code detectors
2530
+ * for graph-enhanced analysis instead of snapshot-based analysis.
2328
2531
  */
2329
- async analyze() {
2532
+ async analyze(graphOptions) {
2330
2533
  const startTime = Date.now();
2331
- const snapshotResult = await buildSnapshot(this.config);
2332
- if (!snapshotResult.ok) {
2333
- return (0, import_types.Err)(snapshotResult.error);
2534
+ const needsSnapshot = !graphOptions || !graphOptions.graphDriftData || !graphOptions.graphDeadCodeData;
2535
+ if (needsSnapshot) {
2536
+ const snapshotResult = await buildSnapshot(this.config);
2537
+ if (!snapshotResult.ok) {
2538
+ return (0, import_types.Err)(snapshotResult.error);
2539
+ }
2540
+ this.snapshot = snapshotResult.value;
2541
+ } else {
2542
+ this.snapshot = {
2543
+ files: [],
2544
+ dependencyGraph: { nodes: [], edges: [] },
2545
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
2546
+ docs: [],
2547
+ codeReferences: [],
2548
+ entryPoints: [],
2549
+ rootDir: this.config.rootDir,
2550
+ config: this.config,
2551
+ buildTime: 0
2552
+ };
2334
2553
  }
2335
- this.snapshot = snapshotResult.value;
2336
2554
  let driftReport;
2337
2555
  let deadCodeReport;
2338
2556
  let patternReport;
2339
2557
  const analysisErrors = [];
2340
2558
  if (this.config.analyze.drift) {
2341
2559
  const driftConfig = typeof this.config.analyze.drift === "object" ? this.config.analyze.drift : {};
2342
- const result = await detectDocDrift(this.snapshot, driftConfig);
2560
+ const result = await detectDocDrift(this.snapshot, driftConfig, graphOptions?.graphDriftData);
2343
2561
  if (result.ok) {
2344
2562
  driftReport = result.value;
2345
2563
  } else {
@@ -2347,7 +2565,7 @@ var EntropyAnalyzer = class {
2347
2565
  }
2348
2566
  }
2349
2567
  if (this.config.analyze.deadCode) {
2350
- const result = await detectDeadCode(this.snapshot);
2568
+ const result = await detectDeadCode(this.snapshot, graphOptions?.graphDeadCodeData);
2351
2569
  if (result.ok) {
2352
2570
  deadCodeReport = result.value;
2353
2571
  } else {
@@ -2444,22 +2662,22 @@ var EntropyAnalyzer = class {
2444
2662
  /**
2445
2663
  * Run drift detection only (snapshot must be built first)
2446
2664
  */
2447
- async detectDrift(config) {
2665
+ async detectDrift(config, graphDriftData) {
2448
2666
  const snapshotResult = await this.ensureSnapshot();
2449
2667
  if (!snapshotResult.ok) {
2450
2668
  return (0, import_types.Err)(snapshotResult.error);
2451
2669
  }
2452
- return detectDocDrift(snapshotResult.value, config || {});
2670
+ return detectDocDrift(snapshotResult.value, config || {}, graphDriftData);
2453
2671
  }
2454
2672
  /**
2455
2673
  * Run dead code detection only (snapshot must be built first)
2456
2674
  */
2457
- async detectDeadCode() {
2675
+ async detectDeadCode(graphDeadCodeData) {
2458
2676
  const snapshotResult = await this.ensureSnapshot();
2459
2677
  if (!snapshotResult.ok) {
2460
2678
  return (0, import_types.Err)(snapshotResult.error);
2461
2679
  }
2462
- return detectDeadCode(snapshotResult.value);
2680
+ return detectDeadCode(snapshotResult.value, graphDeadCodeData);
2463
2681
  }
2464
2682
  /**
2465
2683
  * Run pattern detection only (snapshot must be built first)
@@ -2927,7 +3145,7 @@ function parseDiff(diff) {
2927
3145
  });
2928
3146
  }
2929
3147
  }
2930
- async function analyzeDiff(changes, options) {
3148
+ async function analyzeDiff(changes, options, graphImpactData) {
2931
3149
  if (!options?.enabled) {
2932
3150
  return (0, import_types.Ok)([]);
2933
3151
  }
@@ -2978,26 +3196,75 @@ async function analyzeDiff(changes, options) {
2978
3196
  }
2979
3197
  }
2980
3198
  if (options.checkTestCoverage) {
2981
- const addedSourceFiles = changes.files.filter(
2982
- (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
2983
- );
2984
- const testFiles = changes.files.filter((f) => f.path.includes(".test."));
2985
- for (const sourceFile of addedSourceFiles) {
2986
- const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
2987
- const hasTest = testFiles.some(
2988
- (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
3199
+ if (graphImpactData) {
3200
+ for (const file of changes.files) {
3201
+ if (file.status === "added" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
3202
+ const hasGraphTest = graphImpactData.affectedTests.some(
3203
+ (t) => t.coversFile === file.path
3204
+ );
3205
+ if (!hasGraphTest) {
3206
+ items.push({
3207
+ id: `test-coverage-${file.path}`,
3208
+ category: "diff",
3209
+ check: "Test coverage (graph)",
3210
+ passed: false,
3211
+ severity: "warning",
3212
+ details: `New file ${file.path} has no test file linked in the graph`,
3213
+ file: file.path
3214
+ });
3215
+ }
3216
+ }
3217
+ }
3218
+ } else {
3219
+ const addedSourceFiles = changes.files.filter(
3220
+ (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
2989
3221
  );
2990
- if (!hasTest) {
2991
- items.push({
2992
- id: `diff-${++itemId}`,
2993
- category: "diff",
2994
- check: `Test coverage: ${sourceFile.path}`,
2995
- passed: false,
2996
- severity: "warning",
2997
- details: "New source file added without corresponding test file",
2998
- file: sourceFile.path,
2999
- suggestion: `Add tests in ${expectedTestPath}`
3000
- });
3222
+ const testFiles = changes.files.filter((f) => f.path.includes(".test."));
3223
+ for (const sourceFile of addedSourceFiles) {
3224
+ const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
3225
+ const hasTest = testFiles.some(
3226
+ (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
3227
+ );
3228
+ if (!hasTest) {
3229
+ items.push({
3230
+ id: `diff-${++itemId}`,
3231
+ category: "diff",
3232
+ check: `Test coverage: ${sourceFile.path}`,
3233
+ passed: false,
3234
+ severity: "warning",
3235
+ details: "New source file added without corresponding test file",
3236
+ file: sourceFile.path,
3237
+ suggestion: `Add tests in ${expectedTestPath}`
3238
+ });
3239
+ }
3240
+ }
3241
+ }
3242
+ }
3243
+ if (graphImpactData && graphImpactData.impactScope > 20) {
3244
+ items.push({
3245
+ id: "impact-scope",
3246
+ category: "diff",
3247
+ check: "Impact scope",
3248
+ passed: false,
3249
+ severity: "warning",
3250
+ details: `Changes affect ${graphImpactData.impactScope} downstream dependents \u2014 consider a thorough review`
3251
+ });
3252
+ }
3253
+ if (graphImpactData) {
3254
+ for (const file of changes.files) {
3255
+ if (file.status === "modified" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
3256
+ const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
3257
+ if (!hasDoc) {
3258
+ items.push({
3259
+ id: `doc-coverage-${file.path}`,
3260
+ category: "diff",
3261
+ check: "Documentation coverage (graph)",
3262
+ passed: true,
3263
+ severity: "info",
3264
+ details: `Modified file ${file.path} has no documentation linked in the graph`,
3265
+ file: file.path
3266
+ });
3267
+ }
3001
3268
  }
3002
3269
  }
3003
3270
  }
@@ -3008,13 +3275,16 @@ async function analyzeDiff(changes, options) {
3008
3275
  var ChecklistBuilder = class {
3009
3276
  rootDir;
3010
3277
  harnessOptions;
3278
+ graphHarnessData;
3011
3279
  customRules = [];
3012
3280
  diffOptions;
3281
+ graphImpactData;
3013
3282
  constructor(rootDir) {
3014
3283
  this.rootDir = rootDir;
3015
3284
  }
3016
- withHarnessChecks(options) {
3285
+ withHarnessChecks(options, graphData) {
3017
3286
  this.harnessOptions = options ?? { context: true, constraints: true, entropy: true };
3287
+ this.graphHarnessData = graphData;
3018
3288
  return this;
3019
3289
  }
3020
3290
  addRule(rule) {
@@ -3025,46 +3295,79 @@ var ChecklistBuilder = class {
3025
3295
  this.customRules.push(...rules);
3026
3296
  return this;
3027
3297
  }
3028
- withDiffAnalysis(options) {
3298
+ withDiffAnalysis(options, graphImpactData) {
3029
3299
  this.diffOptions = options;
3300
+ this.graphImpactData = graphImpactData;
3030
3301
  return this;
3031
3302
  }
3032
3303
  async run(changes) {
3033
3304
  const startTime = Date.now();
3034
3305
  const items = [];
3035
3306
  if (this.harnessOptions) {
3036
- if (this.harnessOptions.context) {
3037
- items.push({
3038
- id: "harness-context",
3039
- category: "harness",
3040
- check: "Context Engineering (AGENTS.md, doc coverage)",
3041
- passed: true,
3042
- severity: "info",
3043
- details: "Harness context validation not yet integrated. See Module 2 (context/).",
3044
- suggestion: "Integrate with validateAgentsMap(), checkDocCoverage() from context module"
3045
- });
3307
+ if (this.harnessOptions.context !== false) {
3308
+ if (this.graphHarnessData) {
3309
+ items.push({
3310
+ id: "harness-context",
3311
+ category: "harness",
3312
+ check: "Context validation",
3313
+ passed: this.graphHarnessData.graphExists && this.graphHarnessData.nodeCount > 0,
3314
+ severity: "info",
3315
+ 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"
3316
+ });
3317
+ } else {
3318
+ items.push({
3319
+ id: "harness-context",
3320
+ category: "harness",
3321
+ check: "Context validation",
3322
+ passed: true,
3323
+ severity: "info",
3324
+ details: "Harness context validation not yet integrated (run with graph for real checks)"
3325
+ });
3326
+ }
3046
3327
  }
3047
- if (this.harnessOptions.constraints) {
3048
- items.push({
3049
- id: "harness-constraints",
3050
- category: "harness",
3051
- check: "Architectural Constraints (dependencies, boundaries)",
3052
- passed: true,
3053
- severity: "info",
3054
- details: "Harness constraints validation not yet integrated. See Module 3 (constraints/).",
3055
- suggestion: "Integrate with validateDependencies(), detectCircularDeps() from constraints module"
3056
- });
3328
+ if (this.harnessOptions.constraints !== false) {
3329
+ if (this.graphHarnessData) {
3330
+ const violations = this.graphHarnessData.constraintViolations;
3331
+ items.push({
3332
+ id: "harness-constraints",
3333
+ category: "harness",
3334
+ check: "Constraint validation",
3335
+ passed: violations === 0,
3336
+ severity: violations > 0 ? "error" : "info",
3337
+ details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
3338
+ });
3339
+ } else {
3340
+ items.push({
3341
+ id: "harness-constraints",
3342
+ category: "harness",
3343
+ check: "Constraint validation",
3344
+ passed: true,
3345
+ severity: "info",
3346
+ details: "Harness constraint validation not yet integrated (run with graph for real checks)"
3347
+ });
3348
+ }
3057
3349
  }
3058
- if (this.harnessOptions.entropy) {
3059
- items.push({
3060
- id: "harness-entropy",
3061
- category: "harness",
3062
- check: "Entropy Management (drift, dead code)",
3063
- passed: true,
3064
- severity: "info",
3065
- details: "Harness entropy validation not yet integrated. See Module 4 (entropy/).",
3066
- suggestion: "Integrate with EntropyAnalyzer from entropy module"
3067
- });
3350
+ if (this.harnessOptions.entropy !== false) {
3351
+ if (this.graphHarnessData) {
3352
+ const issues = this.graphHarnessData.unreachableNodes + this.graphHarnessData.undocumentedFiles;
3353
+ items.push({
3354
+ id: "harness-entropy",
3355
+ category: "harness",
3356
+ check: "Entropy detection",
3357
+ passed: issues === 0,
3358
+ severity: issues > 0 ? "warning" : "info",
3359
+ details: issues === 0 ? "No entropy issues detected" : `${this.graphHarnessData.unreachableNodes} unreachable node(s), ${this.graphHarnessData.undocumentedFiles} undocumented file(s)`
3360
+ });
3361
+ } else {
3362
+ items.push({
3363
+ id: "harness-entropy",
3364
+ category: "harness",
3365
+ check: "Entropy detection",
3366
+ passed: true,
3367
+ severity: "info",
3368
+ details: "Harness entropy detection not yet integrated (run with graph for real checks)"
3369
+ });
3370
+ }
3068
3371
  }
3069
3372
  }
3070
3373
  for (const rule of this.customRules) {
@@ -3100,7 +3403,7 @@ var ChecklistBuilder = class {
3100
3403
  }
3101
3404
  }
3102
3405
  if (this.diffOptions) {
3103
- const diffResult = await analyzeDiff(changes, this.diffOptions);
3406
+ const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
3104
3407
  if (diffResult.ok) {
3105
3408
  items.push(...diffResult.value);
3106
3409
  }
@@ -3127,16 +3430,16 @@ var ChecklistBuilder = class {
3127
3430
  };
3128
3431
 
3129
3432
  // src/feedback/review/self-review.ts
3130
- async function createSelfReview(changes, config) {
3433
+ async function createSelfReview(changes, config, graphData) {
3131
3434
  const builder = new ChecklistBuilder(config.rootDir);
3132
3435
  if (config.harness) {
3133
- builder.withHarnessChecks(config.harness);
3436
+ builder.withHarnessChecks(config.harness, graphData?.harness);
3134
3437
  }
3135
3438
  if (config.customRules) {
3136
3439
  builder.addRules(config.customRules);
3137
3440
  }
3138
3441
  if (config.diffAnalysis) {
3139
- builder.withDiffAnalysis(config.diffAnalysis);
3442
+ builder.withDiffAnalysis(config.diffAnalysis, graphData?.impact);
3140
3443
  }
3141
3444
  return builder.run(changes);
3142
3445
  }
@@ -3996,9 +4299,16 @@ async function runSingleCheck(name, projectRoot, config) {
3996
4299
  break;
3997
4300
  }
3998
4301
  case "deps": {
3999
- const layers = config.layers;
4000
- if (layers && layers.length > 0) {
4302
+ const rawLayers = config.layers;
4303
+ if (rawLayers && rawLayers.length > 0) {
4001
4304
  const parser = new TypeScriptParser();
4305
+ const layers = rawLayers.map(
4306
+ (l) => defineLayer(
4307
+ l.name,
4308
+ Array.isArray(l.patterns) ? l.patterns : [l.pattern],
4309
+ l.allowedDependencies
4310
+ )
4311
+ );
4002
4312
  const result = await validateDependencies({
4003
4313
  layers,
4004
4314
  rootDir: projectRoot,