@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.mjs CHANGED
@@ -304,6 +304,9 @@ function resolveLinkPath(linkPath, baseDir) {
304
304
  return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
305
305
  }
306
306
  async function validateAgentsMap(path3 = "./AGENTS.md") {
307
+ console.warn(
308
+ "[harness] validateAgentsMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
309
+ );
307
310
  const contentResult = await readFileContent(path3);
308
311
  if (!contentResult.ok) {
309
312
  return Err(
@@ -379,7 +382,21 @@ function suggestSection(filePath, domain) {
379
382
  return `${domain} Reference`;
380
383
  }
381
384
  async function checkDocCoverage(domain, options = {}) {
382
- const { docsDir = "./docs", sourceDir = "./src", excludePatterns = [] } = options;
385
+ const { docsDir = "./docs", sourceDir = "./src", excludePatterns = [], graphCoverage } = options;
386
+ if (graphCoverage) {
387
+ const gaps = graphCoverage.undocumented.map((file) => ({
388
+ file,
389
+ suggestedSection: suggestSection(file, domain),
390
+ importance: determineImportance(file)
391
+ }));
392
+ return Ok({
393
+ domain,
394
+ documented: graphCoverage.documented,
395
+ undocumented: graphCoverage.undocumented,
396
+ coveragePercentage: graphCoverage.coveragePercentage,
397
+ gaps
398
+ });
399
+ }
383
400
  try {
384
401
  const sourceFiles = await findFiles("**/*.{ts,js,tsx,jsx}", sourceDir);
385
402
  const filteredSourceFiles = sourceFiles.filter((file) => {
@@ -457,6 +474,9 @@ function suggestFix(path3, existingFiles) {
457
474
  return `Create the file "${path3}" or remove the link`;
458
475
  }
459
476
  async function validateKnowledgeMap(rootDir = process.cwd()) {
477
+ console.warn(
478
+ "[harness] validateKnowledgeMap() is deprecated. Use graph-based validation via Assembler.checkCoverage() from @harness-engineering/graph"
479
+ );
460
480
  const agentsPath = join2(rootDir, "AGENTS.md");
461
481
  const agentsResult = await validateAgentsMap(agentsPath);
462
482
  if (!agentsResult.ok) {
@@ -530,18 +550,9 @@ function matchesExcludePattern(relativePath, excludePatterns) {
530
550
  return regex.test(relativePath);
531
551
  });
532
552
  }
533
- async function generateAgentsMap(config) {
553
+ async function generateAgentsMap(config, graphSections) {
534
554
  const { rootDir, includePaths, excludePaths, sections = DEFAULT_SECTIONS } = config;
535
555
  try {
536
- const allFiles = [];
537
- for (const pattern of includePaths) {
538
- const files = await findFiles(pattern, rootDir);
539
- allFiles.push(...files);
540
- }
541
- const filteredFiles = allFiles.filter((file) => {
542
- const relativePath = relative3(rootDir, file);
543
- return !matchesExcludePattern(relativePath, excludePaths);
544
- });
545
556
  const lines = [];
546
557
  lines.push("# AI Agent Knowledge Map");
547
558
  lines.push("");
@@ -551,41 +562,68 @@ async function generateAgentsMap(config) {
551
562
  lines.push("");
552
563
  lines.push("> Add a brief description of this project, its purpose, and key technologies.");
553
564
  lines.push("");
554
- lines.push("## Repository Structure");
555
- lines.push("");
556
- const grouped = groupByDirectory(filteredFiles, rootDir);
557
- for (const [dir, files] of grouped) {
558
- if (dir !== ".") {
559
- lines.push(`### ${dir}/`);
565
+ if (graphSections) {
566
+ for (const section of graphSections) {
567
+ lines.push(`## ${section.name}`);
560
568
  lines.push("");
561
- }
562
- for (const file of files.slice(0, 10)) {
563
- lines.push(formatFileLink(file));
564
- }
565
- if (files.length > 10) {
566
- lines.push(`- _... and ${files.length - 10} more files_`);
567
- }
568
- lines.push("");
569
- }
570
- for (const section of sections) {
571
- lines.push(`## ${section.name}`);
572
- lines.push("");
573
- if (section.description) {
574
- lines.push(section.description);
569
+ if (section.description) {
570
+ lines.push(section.description);
571
+ lines.push("");
572
+ }
573
+ for (const file of section.files.slice(0, 20)) {
574
+ lines.push(formatFileLink(file));
575
+ }
576
+ if (section.files.length > 20) {
577
+ lines.push(`- _... and ${section.files.length - 20} more files_`);
578
+ }
575
579
  lines.push("");
576
580
  }
577
- const sectionFiles = await findFiles(section.pattern, rootDir);
578
- const filteredSectionFiles = sectionFiles.filter((file) => {
581
+ } else {
582
+ const allFiles = [];
583
+ for (const pattern of includePaths) {
584
+ const files = await findFiles(pattern, rootDir);
585
+ allFiles.push(...files);
586
+ }
587
+ const filteredFiles = allFiles.filter((file) => {
579
588
  const relativePath = relative3(rootDir, file);
580
589
  return !matchesExcludePattern(relativePath, excludePaths);
581
590
  });
582
- for (const file of filteredSectionFiles.slice(0, 20)) {
583
- lines.push(formatFileLink(relative3(rootDir, file)));
591
+ lines.push("## Repository Structure");
592
+ lines.push("");
593
+ const grouped = groupByDirectory(filteredFiles, rootDir);
594
+ for (const [dir, files] of grouped) {
595
+ if (dir !== ".") {
596
+ lines.push(`### ${dir}/`);
597
+ lines.push("");
598
+ }
599
+ for (const file of files.slice(0, 10)) {
600
+ lines.push(formatFileLink(file));
601
+ }
602
+ if (files.length > 10) {
603
+ lines.push(`- _... and ${files.length - 10} more files_`);
604
+ }
605
+ lines.push("");
584
606
  }
585
- if (filteredSectionFiles.length > 20) {
586
- lines.push(`- _... and ${filteredSectionFiles.length - 20} more files_`);
607
+ for (const section of sections) {
608
+ lines.push(`## ${section.name}`);
609
+ lines.push("");
610
+ if (section.description) {
611
+ lines.push(section.description);
612
+ lines.push("");
613
+ }
614
+ const sectionFiles = await findFiles(section.pattern, rootDir);
615
+ const filteredSectionFiles = sectionFiles.filter((file) => {
616
+ const relativePath = relative3(rootDir, file);
617
+ return !matchesExcludePattern(relativePath, excludePaths);
618
+ });
619
+ for (const file of filteredSectionFiles.slice(0, 20)) {
620
+ lines.push(formatFileLink(relative3(rootDir, file)));
621
+ }
622
+ if (filteredSectionFiles.length > 20) {
623
+ lines.push(`- _... and ${filteredSectionFiles.length - 20} more files_`);
624
+ }
625
+ lines.push("");
587
626
  }
588
- lines.push("");
589
627
  }
590
628
  lines.push("## Development Workflow");
591
629
  lines.push("");
@@ -615,7 +653,21 @@ var DEFAULT_RATIOS = {
615
653
  interfaces: 0.1,
616
654
  reserve: 0.1
617
655
  };
618
- function contextBudget(totalTokens, overrides) {
656
+ var NODE_TYPE_TO_CATEGORY = {
657
+ file: "activeCode",
658
+ function: "activeCode",
659
+ class: "activeCode",
660
+ method: "activeCode",
661
+ interface: "interfaces",
662
+ variable: "interfaces",
663
+ adr: "projectManifest",
664
+ document: "projectManifest",
665
+ spec: "taskSpec",
666
+ task: "taskSpec",
667
+ prompt: "systemPrompt",
668
+ system: "systemPrompt"
669
+ };
670
+ function contextBudget(totalTokens, overrides, graphDensity) {
619
671
  const ratios = {
620
672
  systemPrompt: DEFAULT_RATIOS.systemPrompt,
621
673
  projectManifest: DEFAULT_RATIOS.projectManifest,
@@ -624,6 +676,52 @@ function contextBudget(totalTokens, overrides) {
624
676
  interfaces: DEFAULT_RATIOS.interfaces,
625
677
  reserve: DEFAULT_RATIOS.reserve
626
678
  };
679
+ if (graphDensity) {
680
+ const categoryWeights = {
681
+ systemPrompt: 0,
682
+ projectManifest: 0,
683
+ taskSpec: 0,
684
+ activeCode: 0,
685
+ interfaces: 0,
686
+ reserve: 0
687
+ };
688
+ for (const [nodeType, count] of Object.entries(graphDensity)) {
689
+ const category = NODE_TYPE_TO_CATEGORY[nodeType];
690
+ if (category) {
691
+ categoryWeights[category] += count;
692
+ }
693
+ }
694
+ const totalWeight = Object.values(categoryWeights).reduce((sum, w) => sum + w, 0);
695
+ if (totalWeight > 0) {
696
+ const MIN_ALLOCATION = 0.01;
697
+ for (const key of Object.keys(ratios)) {
698
+ if (categoryWeights[key] > 0) {
699
+ ratios[key] = categoryWeights[key] / totalWeight;
700
+ } else {
701
+ ratios[key] = MIN_ALLOCATION;
702
+ }
703
+ }
704
+ if (ratios.reserve < DEFAULT_RATIOS.reserve) {
705
+ ratios.reserve = DEFAULT_RATIOS.reserve;
706
+ }
707
+ if (ratios.systemPrompt < DEFAULT_RATIOS.systemPrompt) {
708
+ ratios.systemPrompt = DEFAULT_RATIOS.systemPrompt;
709
+ }
710
+ const ratioSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
711
+ for (const key of Object.keys(ratios)) {
712
+ ratios[key] = ratios[key] / ratioSum;
713
+ }
714
+ for (const key of Object.keys(ratios)) {
715
+ if (ratios[key] < MIN_ALLOCATION) {
716
+ ratios[key] = MIN_ALLOCATION;
717
+ }
718
+ }
719
+ const finalSum = Object.values(ratios).reduce((sum, r) => sum + r, 0);
720
+ for (const key of Object.keys(ratios)) {
721
+ ratios[key] = ratios[key] / finalSum;
722
+ }
723
+ }
724
+ }
627
725
  if (overrides) {
628
726
  let overrideSum = 0;
629
727
  const overrideKeys = [];
@@ -637,12 +735,12 @@ function contextBudget(totalTokens, overrides) {
637
735
  }
638
736
  if (overrideKeys.length > 0 && overrideKeys.length < 6) {
639
737
  const remaining = 1 - overrideSum;
640
- const nonOverridden = Object.keys(DEFAULT_RATIOS).filter(
738
+ const nonOverridden = Object.keys(ratios).filter(
641
739
  (k) => !overrideKeys.includes(k)
642
740
  );
643
- const originalSum = nonOverridden.reduce((sum, k) => sum + DEFAULT_RATIOS[k], 0);
741
+ const originalSum = nonOverridden.reduce((sum, k) => sum + ratios[k], 0);
644
742
  for (const k of nonOverridden) {
645
- ratios[k] = remaining * (DEFAULT_RATIOS[k] / originalSum);
743
+ ratios[k] = remaining * (ratios[k] / originalSum);
646
744
  }
647
745
  }
648
746
  }
@@ -693,7 +791,7 @@ var PHASE_PRIORITIES = {
693
791
  { category: "config", patterns: ["harness.config.json", "package.json"], priority: 5 }
694
792
  ]
695
793
  };
696
- function contextFilter(phase, maxCategories) {
794
+ function contextFilter(phase, maxCategories, graphFilePaths) {
697
795
  const categories = PHASE_PRIORITIES[phase];
698
796
  const limit = maxCategories ?? categories.length;
699
797
  const included = categories.slice(0, limit);
@@ -702,7 +800,7 @@ function contextFilter(phase, maxCategories) {
702
800
  phase,
703
801
  includedCategories: included.map((c) => c.category),
704
802
  excludedCategories: excluded.map((c) => c.category),
705
- filePatterns: included.flatMap((c) => c.patterns)
803
+ filePatterns: graphFilePaths ?? included.flatMap((c) => c.patterns)
706
804
  };
707
805
  }
708
806
  function getPhaseCategories(phase) {
@@ -746,7 +844,13 @@ function getImportType(imp) {
746
844
  if (imp.kind === "type") return "type-only";
747
845
  return "static";
748
846
  }
749
- async function buildDependencyGraph(files, parser) {
847
+ async function buildDependencyGraph(files, parser, graphDependencyData) {
848
+ if (graphDependencyData) {
849
+ return Ok({
850
+ nodes: graphDependencyData.nodes,
851
+ edges: graphDependencyData.edges
852
+ });
853
+ }
750
854
  const nodes = [...files];
751
855
  const edges = [];
752
856
  for (const file of files) {
@@ -796,7 +900,19 @@ function checkLayerViolations(graph, layers, rootDir) {
796
900
  return violations;
797
901
  }
798
902
  async function validateDependencies(config) {
799
- const { layers, rootDir, parser, fallbackBehavior = "error" } = config;
903
+ const { layers, rootDir, parser, fallbackBehavior = "error", graphDependencyData } = config;
904
+ if (graphDependencyData) {
905
+ const graphResult2 = await buildDependencyGraph([], parser, graphDependencyData);
906
+ if (!graphResult2.ok) {
907
+ return Err(graphResult2.error);
908
+ }
909
+ const violations2 = checkLayerViolations(graphResult2.value, layers, rootDir);
910
+ return Ok({
911
+ valid: violations2.length === 0,
912
+ violations: violations2,
913
+ graph: graphResult2.value
914
+ });
915
+ }
800
916
  const healthResult = await parser.health();
801
917
  if (!healthResult.ok || !healthResult.value.available) {
802
918
  if (fallbackBehavior === "skip") {
@@ -930,8 +1046,8 @@ function detectCircularDeps(graph) {
930
1046
  largestCycle
931
1047
  });
932
1048
  }
933
- async function detectCircularDepsInFiles(files, parser) {
934
- const graphResult = await buildDependencyGraph(files, parser);
1049
+ async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
1050
+ const graphResult = await buildDependencyGraph(files, parser, graphDependencyData);
935
1051
  if (!graphResult.ok) {
936
1052
  return graphResult;
937
1053
  }
@@ -1651,7 +1767,45 @@ async function checkStructureDrift(snapshot, _config) {
1651
1767
  }
1652
1768
  return drifts;
1653
1769
  }
1654
- async function detectDocDrift(snapshot, config) {
1770
+ async function detectDocDrift(snapshot, config, graphDriftData) {
1771
+ if (graphDriftData) {
1772
+ const drifts2 = [];
1773
+ for (const target of graphDriftData.missingTargets) {
1774
+ drifts2.push({
1775
+ type: "api-signature",
1776
+ docFile: target,
1777
+ line: 0,
1778
+ reference: target,
1779
+ context: "graph-missing-target",
1780
+ issue: "NOT_FOUND",
1781
+ details: `Graph node "${target}" has no matching code target`,
1782
+ confidence: "high"
1783
+ });
1784
+ }
1785
+ for (const edge of graphDriftData.staleEdges) {
1786
+ drifts2.push({
1787
+ type: "api-signature",
1788
+ docFile: edge.docNodeId,
1789
+ line: 0,
1790
+ reference: edge.codeNodeId,
1791
+ context: `graph-stale-edge:${edge.edgeType}`,
1792
+ issue: "NOT_FOUND",
1793
+ details: `Stale edge from doc "${edge.docNodeId}" to code "${edge.codeNodeId}" (${edge.edgeType})`,
1794
+ confidence: "medium"
1795
+ });
1796
+ }
1797
+ const severity2 = drifts2.length === 0 ? "none" : drifts2.length <= 3 ? "low" : drifts2.length <= 10 ? "medium" : "high";
1798
+ return Ok({
1799
+ drifts: drifts2,
1800
+ stats: {
1801
+ docsScanned: graphDriftData.staleEdges.length,
1802
+ referencesChecked: graphDriftData.staleEdges.length + graphDriftData.missingTargets.length,
1803
+ driftsFound: drifts2.length,
1804
+ byType: { api: drifts2.length, example: 0, structure: 0 }
1805
+ },
1806
+ severity: severity2
1807
+ });
1808
+ }
1655
1809
  const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
1656
1810
  const drifts = [];
1657
1811
  if (fullConfig.checkApiSignatures) {
@@ -1889,7 +2043,54 @@ function findDeadInternals(snapshot, _reachability) {
1889
2043
  }
1890
2044
  return deadInternals;
1891
2045
  }
1892
- async function detectDeadCode(snapshot) {
2046
+ async function detectDeadCode(snapshot, graphDeadCodeData) {
2047
+ if (graphDeadCodeData) {
2048
+ const deadFiles2 = [];
2049
+ const deadExports2 = [];
2050
+ const fileTypes = /* @__PURE__ */ new Set(["file", "module"]);
2051
+ const exportTypes = /* @__PURE__ */ new Set(["function", "class", "method", "interface", "variable"]);
2052
+ for (const node of graphDeadCodeData.unreachableNodes) {
2053
+ if (fileTypes.has(node.type)) {
2054
+ deadFiles2.push({
2055
+ path: node.path || node.id,
2056
+ reason: "NO_IMPORTERS",
2057
+ exportCount: 0,
2058
+ lineCount: 0
2059
+ });
2060
+ } else if (exportTypes.has(node.type)) {
2061
+ const exportType = node.type === "method" ? "function" : node.type;
2062
+ deadExports2.push({
2063
+ file: node.path || node.id,
2064
+ name: node.name,
2065
+ line: 0,
2066
+ type: exportType,
2067
+ isDefault: false,
2068
+ reason: "NO_IMPORTERS"
2069
+ });
2070
+ }
2071
+ }
2072
+ const reachableCount = graphDeadCodeData.reachableNodeIds instanceof Set ? graphDeadCodeData.reachableNodeIds.size : graphDeadCodeData.reachableNodeIds.length;
2073
+ const fileNodes = graphDeadCodeData.unreachableNodes.filter((n) => fileTypes.has(n.type));
2074
+ const exportNodes = graphDeadCodeData.unreachableNodes.filter((n) => exportTypes.has(n.type));
2075
+ const totalFiles = reachableCount + fileNodes.length;
2076
+ const totalExports2 = exportNodes.length + (reachableCount > 0 ? reachableCount : 0);
2077
+ const report2 = {
2078
+ deadExports: deadExports2,
2079
+ deadFiles: deadFiles2,
2080
+ deadInternals: [],
2081
+ unusedImports: [],
2082
+ stats: {
2083
+ filesAnalyzed: totalFiles,
2084
+ entryPointsUsed: [],
2085
+ totalExports: totalExports2,
2086
+ deadExportCount: deadExports2.length,
2087
+ totalFiles,
2088
+ deadFileCount: deadFiles2.length,
2089
+ estimatedDeadLines: 0
2090
+ }
2091
+ };
2092
+ return Ok(report2);
2093
+ }
1893
2094
  const reachability = buildReachabilityMap(snapshot);
1894
2095
  const usageMap = buildExportUsageMap(snapshot);
1895
2096
  const deadExports = findDeadExports(snapshot, usageMap, reachability);
@@ -2215,22 +2416,39 @@ var EntropyAnalyzer = class {
2215
2416
  };
2216
2417
  }
2217
2418
  /**
2218
- * Run full entropy analysis
2419
+ * Run full entropy analysis.
2420
+ * When graphOptions is provided, passes graph data to drift and dead code detectors
2421
+ * for graph-enhanced analysis instead of snapshot-based analysis.
2219
2422
  */
2220
- async analyze() {
2423
+ async analyze(graphOptions) {
2221
2424
  const startTime = Date.now();
2222
- const snapshotResult = await buildSnapshot(this.config);
2223
- if (!snapshotResult.ok) {
2224
- return Err(snapshotResult.error);
2425
+ const needsSnapshot = !graphOptions || !graphOptions.graphDriftData || !graphOptions.graphDeadCodeData;
2426
+ if (needsSnapshot) {
2427
+ const snapshotResult = await buildSnapshot(this.config);
2428
+ if (!snapshotResult.ok) {
2429
+ return Err(snapshotResult.error);
2430
+ }
2431
+ this.snapshot = snapshotResult.value;
2432
+ } else {
2433
+ this.snapshot = {
2434
+ files: [],
2435
+ dependencyGraph: { nodes: [], edges: [] },
2436
+ exportMap: { byFile: /* @__PURE__ */ new Map(), byName: /* @__PURE__ */ new Map() },
2437
+ docs: [],
2438
+ codeReferences: [],
2439
+ entryPoints: [],
2440
+ rootDir: this.config.rootDir,
2441
+ config: this.config,
2442
+ buildTime: 0
2443
+ };
2225
2444
  }
2226
- this.snapshot = snapshotResult.value;
2227
2445
  let driftReport;
2228
2446
  let deadCodeReport;
2229
2447
  let patternReport;
2230
2448
  const analysisErrors = [];
2231
2449
  if (this.config.analyze.drift) {
2232
2450
  const driftConfig = typeof this.config.analyze.drift === "object" ? this.config.analyze.drift : {};
2233
- const result = await detectDocDrift(this.snapshot, driftConfig);
2451
+ const result = await detectDocDrift(this.snapshot, driftConfig, graphOptions?.graphDriftData);
2234
2452
  if (result.ok) {
2235
2453
  driftReport = result.value;
2236
2454
  } else {
@@ -2238,7 +2456,7 @@ var EntropyAnalyzer = class {
2238
2456
  }
2239
2457
  }
2240
2458
  if (this.config.analyze.deadCode) {
2241
- const result = await detectDeadCode(this.snapshot);
2459
+ const result = await detectDeadCode(this.snapshot, graphOptions?.graphDeadCodeData);
2242
2460
  if (result.ok) {
2243
2461
  deadCodeReport = result.value;
2244
2462
  } else {
@@ -2335,22 +2553,22 @@ var EntropyAnalyzer = class {
2335
2553
  /**
2336
2554
  * Run drift detection only (snapshot must be built first)
2337
2555
  */
2338
- async detectDrift(config) {
2556
+ async detectDrift(config, graphDriftData) {
2339
2557
  const snapshotResult = await this.ensureSnapshot();
2340
2558
  if (!snapshotResult.ok) {
2341
2559
  return Err(snapshotResult.error);
2342
2560
  }
2343
- return detectDocDrift(snapshotResult.value, config || {});
2561
+ return detectDocDrift(snapshotResult.value, config || {}, graphDriftData);
2344
2562
  }
2345
2563
  /**
2346
2564
  * Run dead code detection only (snapshot must be built first)
2347
2565
  */
2348
- async detectDeadCode() {
2566
+ async detectDeadCode(graphDeadCodeData) {
2349
2567
  const snapshotResult = await this.ensureSnapshot();
2350
2568
  if (!snapshotResult.ok) {
2351
2569
  return Err(snapshotResult.error);
2352
2570
  }
2353
- return detectDeadCode(snapshotResult.value);
2571
+ return detectDeadCode(snapshotResult.value, graphDeadCodeData);
2354
2572
  }
2355
2573
  /**
2356
2574
  * Run pattern detection only (snapshot must be built first)
@@ -2818,7 +3036,7 @@ function parseDiff(diff) {
2818
3036
  });
2819
3037
  }
2820
3038
  }
2821
- async function analyzeDiff(changes, options) {
3039
+ async function analyzeDiff(changes, options, graphImpactData) {
2822
3040
  if (!options?.enabled) {
2823
3041
  return Ok([]);
2824
3042
  }
@@ -2869,26 +3087,75 @@ async function analyzeDiff(changes, options) {
2869
3087
  }
2870
3088
  }
2871
3089
  if (options.checkTestCoverage) {
2872
- const addedSourceFiles = changes.files.filter(
2873
- (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
2874
- );
2875
- const testFiles = changes.files.filter((f) => f.path.includes(".test."));
2876
- for (const sourceFile of addedSourceFiles) {
2877
- const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
2878
- const hasTest = testFiles.some(
2879
- (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
3090
+ if (graphImpactData) {
3091
+ for (const file of changes.files) {
3092
+ if (file.status === "added" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
3093
+ const hasGraphTest = graphImpactData.affectedTests.some(
3094
+ (t) => t.coversFile === file.path
3095
+ );
3096
+ if (!hasGraphTest) {
3097
+ items.push({
3098
+ id: `test-coverage-${file.path}`,
3099
+ category: "diff",
3100
+ check: "Test coverage (graph)",
3101
+ passed: false,
3102
+ severity: "warning",
3103
+ details: `New file ${file.path} has no test file linked in the graph`,
3104
+ file: file.path
3105
+ });
3106
+ }
3107
+ }
3108
+ }
3109
+ } else {
3110
+ const addedSourceFiles = changes.files.filter(
3111
+ (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
2880
3112
  );
2881
- if (!hasTest) {
2882
- items.push({
2883
- id: `diff-${++itemId}`,
2884
- category: "diff",
2885
- check: `Test coverage: ${sourceFile.path}`,
2886
- passed: false,
2887
- severity: "warning",
2888
- details: "New source file added without corresponding test file",
2889
- file: sourceFile.path,
2890
- suggestion: `Add tests in ${expectedTestPath}`
2891
- });
3113
+ const testFiles = changes.files.filter((f) => f.path.includes(".test."));
3114
+ for (const sourceFile of addedSourceFiles) {
3115
+ const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
3116
+ const hasTest = testFiles.some(
3117
+ (t) => t.path.includes(expectedTestPath) || t.path.includes(sourceFile.path.replace(".ts", ""))
3118
+ );
3119
+ if (!hasTest) {
3120
+ items.push({
3121
+ id: `diff-${++itemId}`,
3122
+ category: "diff",
3123
+ check: `Test coverage: ${sourceFile.path}`,
3124
+ passed: false,
3125
+ severity: "warning",
3126
+ details: "New source file added without corresponding test file",
3127
+ file: sourceFile.path,
3128
+ suggestion: `Add tests in ${expectedTestPath}`
3129
+ });
3130
+ }
3131
+ }
3132
+ }
3133
+ }
3134
+ if (graphImpactData && graphImpactData.impactScope > 20) {
3135
+ items.push({
3136
+ id: "impact-scope",
3137
+ category: "diff",
3138
+ check: "Impact scope",
3139
+ passed: false,
3140
+ severity: "warning",
3141
+ details: `Changes affect ${graphImpactData.impactScope} downstream dependents \u2014 consider a thorough review`
3142
+ });
3143
+ }
3144
+ if (graphImpactData) {
3145
+ for (const file of changes.files) {
3146
+ if (file.status === "modified" && file.path.endsWith(".ts") && !file.path.includes(".test.")) {
3147
+ const hasDoc = graphImpactData.affectedDocs.some((d) => d.documentsFile === file.path);
3148
+ if (!hasDoc) {
3149
+ items.push({
3150
+ id: `doc-coverage-${file.path}`,
3151
+ category: "diff",
3152
+ check: "Documentation coverage (graph)",
3153
+ passed: true,
3154
+ severity: "info",
3155
+ details: `Modified file ${file.path} has no documentation linked in the graph`,
3156
+ file: file.path
3157
+ });
3158
+ }
2892
3159
  }
2893
3160
  }
2894
3161
  }
@@ -2899,13 +3166,16 @@ async function analyzeDiff(changes, options) {
2899
3166
  var ChecklistBuilder = class {
2900
3167
  rootDir;
2901
3168
  harnessOptions;
3169
+ graphHarnessData;
2902
3170
  customRules = [];
2903
3171
  diffOptions;
3172
+ graphImpactData;
2904
3173
  constructor(rootDir) {
2905
3174
  this.rootDir = rootDir;
2906
3175
  }
2907
- withHarnessChecks(options) {
3176
+ withHarnessChecks(options, graphData) {
2908
3177
  this.harnessOptions = options ?? { context: true, constraints: true, entropy: true };
3178
+ this.graphHarnessData = graphData;
2909
3179
  return this;
2910
3180
  }
2911
3181
  addRule(rule) {
@@ -2916,46 +3186,79 @@ var ChecklistBuilder = class {
2916
3186
  this.customRules.push(...rules);
2917
3187
  return this;
2918
3188
  }
2919
- withDiffAnalysis(options) {
3189
+ withDiffAnalysis(options, graphImpactData) {
2920
3190
  this.diffOptions = options;
3191
+ this.graphImpactData = graphImpactData;
2921
3192
  return this;
2922
3193
  }
2923
3194
  async run(changes) {
2924
3195
  const startTime = Date.now();
2925
3196
  const items = [];
2926
3197
  if (this.harnessOptions) {
2927
- if (this.harnessOptions.context) {
2928
- items.push({
2929
- id: "harness-context",
2930
- category: "harness",
2931
- check: "Context Engineering (AGENTS.md, doc coverage)",
2932
- passed: true,
2933
- severity: "info",
2934
- details: "Harness context validation not yet integrated. See Module 2 (context/).",
2935
- suggestion: "Integrate with validateAgentsMap(), checkDocCoverage() from context module"
2936
- });
3198
+ if (this.harnessOptions.context !== false) {
3199
+ if (this.graphHarnessData) {
3200
+ items.push({
3201
+ id: "harness-context",
3202
+ category: "harness",
3203
+ check: "Context validation",
3204
+ passed: this.graphHarnessData.graphExists && this.graphHarnessData.nodeCount > 0,
3205
+ severity: "info",
3206
+ 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"
3207
+ });
3208
+ } else {
3209
+ items.push({
3210
+ id: "harness-context",
3211
+ category: "harness",
3212
+ check: "Context validation",
3213
+ passed: true,
3214
+ severity: "info",
3215
+ details: "Harness context validation not yet integrated (run with graph for real checks)"
3216
+ });
3217
+ }
2937
3218
  }
2938
- if (this.harnessOptions.constraints) {
2939
- items.push({
2940
- id: "harness-constraints",
2941
- category: "harness",
2942
- check: "Architectural Constraints (dependencies, boundaries)",
2943
- passed: true,
2944
- severity: "info",
2945
- details: "Harness constraints validation not yet integrated. See Module 3 (constraints/).",
2946
- suggestion: "Integrate with validateDependencies(), detectCircularDeps() from constraints module"
2947
- });
3219
+ if (this.harnessOptions.constraints !== false) {
3220
+ if (this.graphHarnessData) {
3221
+ const violations = this.graphHarnessData.constraintViolations;
3222
+ items.push({
3223
+ id: "harness-constraints",
3224
+ category: "harness",
3225
+ check: "Constraint validation",
3226
+ passed: violations === 0,
3227
+ severity: violations > 0 ? "error" : "info",
3228
+ details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
3229
+ });
3230
+ } else {
3231
+ items.push({
3232
+ id: "harness-constraints",
3233
+ category: "harness",
3234
+ check: "Constraint validation",
3235
+ passed: true,
3236
+ severity: "info",
3237
+ details: "Harness constraint validation not yet integrated (run with graph for real checks)"
3238
+ });
3239
+ }
2948
3240
  }
2949
- if (this.harnessOptions.entropy) {
2950
- items.push({
2951
- id: "harness-entropy",
2952
- category: "harness",
2953
- check: "Entropy Management (drift, dead code)",
2954
- passed: true,
2955
- severity: "info",
2956
- details: "Harness entropy validation not yet integrated. See Module 4 (entropy/).",
2957
- suggestion: "Integrate with EntropyAnalyzer from entropy module"
2958
- });
3241
+ if (this.harnessOptions.entropy !== false) {
3242
+ if (this.graphHarnessData) {
3243
+ const issues = this.graphHarnessData.unreachableNodes + this.graphHarnessData.undocumentedFiles;
3244
+ items.push({
3245
+ id: "harness-entropy",
3246
+ category: "harness",
3247
+ check: "Entropy detection",
3248
+ passed: issues === 0,
3249
+ severity: issues > 0 ? "warning" : "info",
3250
+ details: issues === 0 ? "No entropy issues detected" : `${this.graphHarnessData.unreachableNodes} unreachable node(s), ${this.graphHarnessData.undocumentedFiles} undocumented file(s)`
3251
+ });
3252
+ } else {
3253
+ items.push({
3254
+ id: "harness-entropy",
3255
+ category: "harness",
3256
+ check: "Entropy detection",
3257
+ passed: true,
3258
+ severity: "info",
3259
+ details: "Harness entropy detection not yet integrated (run with graph for real checks)"
3260
+ });
3261
+ }
2959
3262
  }
2960
3263
  }
2961
3264
  for (const rule of this.customRules) {
@@ -2991,7 +3294,7 @@ var ChecklistBuilder = class {
2991
3294
  }
2992
3295
  }
2993
3296
  if (this.diffOptions) {
2994
- const diffResult = await analyzeDiff(changes, this.diffOptions);
3297
+ const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
2995
3298
  if (diffResult.ok) {
2996
3299
  items.push(...diffResult.value);
2997
3300
  }
@@ -3018,16 +3321,16 @@ var ChecklistBuilder = class {
3018
3321
  };
3019
3322
 
3020
3323
  // src/feedback/review/self-review.ts
3021
- async function createSelfReview(changes, config) {
3324
+ async function createSelfReview(changes, config, graphData) {
3022
3325
  const builder = new ChecklistBuilder(config.rootDir);
3023
3326
  if (config.harness) {
3024
- builder.withHarnessChecks(config.harness);
3327
+ builder.withHarnessChecks(config.harness, graphData?.harness);
3025
3328
  }
3026
3329
  if (config.customRules) {
3027
3330
  builder.addRules(config.customRules);
3028
3331
  }
3029
3332
  if (config.diffAnalysis) {
3030
- builder.withDiffAnalysis(config.diffAnalysis);
3333
+ builder.withDiffAnalysis(config.diffAnalysis, graphData?.impact);
3031
3334
  }
3032
3335
  return builder.run(changes);
3033
3336
  }
@@ -3887,9 +4190,16 @@ async function runSingleCheck(name, projectRoot, config) {
3887
4190
  break;
3888
4191
  }
3889
4192
  case "deps": {
3890
- const layers = config.layers;
3891
- if (layers && layers.length > 0) {
4193
+ const rawLayers = config.layers;
4194
+ if (rawLayers && rawLayers.length > 0) {
3892
4195
  const parser = new TypeScriptParser();
4196
+ const layers = rawLayers.map(
4197
+ (l) => defineLayer(
4198
+ l.name,
4199
+ Array.isArray(l.patterns) ? l.patterns : [l.pattern],
4200
+ l.allowedDependencies
4201
+ )
4202
+ );
3893
4203
  const result = await validateDependencies({
3894
4204
  layers,
3895
4205
  rootDir: projectRoot,