@harness-engineering/core 0.6.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
@@ -91,6 +91,7 @@ __export(index_exports, {
91
91
  requestPeerReview: () => requestPeerReview,
92
92
  resetFeedbackConfig: () => resetFeedbackConfig,
93
93
  resolveFileToLayer: () => resolveFileToLayer,
94
+ runCIChecks: () => runCIChecks,
94
95
  runMechanicalGate: () => runMechanicalGate,
95
96
  runMultiTurnPipeline: () => runMultiTurnPipeline,
96
97
  runPipeline: () => runPipeline,
@@ -126,17 +127,17 @@ var import_util = require("util");
126
127
  var import_glob = require("glob");
127
128
  var accessAsync = (0, import_util.promisify)(import_fs.access);
128
129
  var readFileAsync = (0, import_util.promisify)(import_fs.readFile);
129
- async function fileExists(path2) {
130
+ async function fileExists(path3) {
130
131
  try {
131
- await accessAsync(path2, import_fs.constants.F_OK);
132
+ await accessAsync(path3, import_fs.constants.F_OK);
132
133
  return true;
133
134
  } catch {
134
135
  return false;
135
136
  }
136
137
  }
137
- async function readFileContent(path2) {
138
+ async function readFileContent(path3) {
138
139
  try {
139
- const content = await readFileAsync(path2, "utf-8");
140
+ const content = await readFileAsync(path3, "utf-8");
140
141
  return (0, import_types.Ok)(content);
141
142
  } catch (error) {
142
143
  return (0, import_types.Err)(error);
@@ -184,15 +185,15 @@ function validateConfig(data, schema) {
184
185
  let message = "Configuration validation failed";
185
186
  const suggestions = [];
186
187
  if (firstError) {
187
- const path2 = firstError.path.join(".");
188
- const pathDisplay = path2 ? ` at "${path2}"` : "";
188
+ const path3 = firstError.path.join(".");
189
+ const pathDisplay = path3 ? ` at "${path3}"` : "";
189
190
  if (firstError.code === "invalid_type") {
190
191
  const received = firstError.received;
191
192
  const expected = firstError.expected;
192
193
  if (received === "undefined") {
193
194
  code = "MISSING_FIELD";
194
195
  message = `Missing required field${pathDisplay}: ${firstError.message}`;
195
- suggestions.push(`Field "${path2}" is required and must be of type "${expected}"`);
196
+ suggestions.push(`Field "${path3}" is required and must be of type "${expected}"`);
196
197
  } else {
197
198
  code = "INVALID_TYPE";
198
199
  message = `Invalid type${pathDisplay}: ${firstError.message}`;
@@ -405,27 +406,30 @@ function extractSections(content) {
405
406
  return result;
406
407
  });
407
408
  }
408
- function isExternalLink(path2) {
409
- return path2.startsWith("http://") || path2.startsWith("https://") || path2.startsWith("#") || path2.startsWith("mailto:");
409
+ function isExternalLink(path3) {
410
+ return path3.startsWith("http://") || path3.startsWith("https://") || path3.startsWith("#") || path3.startsWith("mailto:");
410
411
  }
411
412
  function resolveLinkPath(linkPath, baseDir) {
412
413
  return linkPath.startsWith(".") ? (0, import_path.join)(baseDir, linkPath) : linkPath;
413
414
  }
414
- async function validateAgentsMap(path2 = "./AGENTS.md") {
415
- const contentResult = await readFileContent(path2);
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
+ );
419
+ const contentResult = await readFileContent(path3);
416
420
  if (!contentResult.ok) {
417
421
  return (0, import_types.Err)(
418
422
  createError(
419
423
  "PARSE_ERROR",
420
424
  `Failed to read AGENTS.md: ${contentResult.error.message}`,
421
- { path: path2 },
425
+ { path: path3 },
422
426
  ["Ensure the file exists", "Check file permissions"]
423
427
  )
424
428
  );
425
429
  }
426
430
  const content = contentResult.value;
427
431
  const sections = extractSections(content);
428
- const baseDir = (0, import_path.dirname)(path2);
432
+ const baseDir = (0, import_path.dirname)(path3);
429
433
  const sectionTitles = sections.map((s) => s.title);
430
434
  const missingSections = REQUIRED_SECTIONS.filter(
431
435
  (required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
@@ -487,7 +491,21 @@ function suggestSection(filePath, domain) {
487
491
  return `${domain} Reference`;
488
492
  }
489
493
  async function checkDocCoverage(domain, options = {}) {
490
- 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
+ }
491
509
  try {
492
510
  const sourceFiles = await findFiles("**/*.{ts,js,tsx,jsx}", sourceDir);
493
511
  const filteredSourceFiles = sourceFiles.filter((file) => {
@@ -553,8 +571,8 @@ async function checkDocCoverage(domain, options = {}) {
553
571
 
554
572
  // src/context/knowledge-map.ts
555
573
  var import_path3 = require("path");
556
- function suggestFix(path2, existingFiles) {
557
- const targetName = (0, import_path3.basename)(path2).toLowerCase();
574
+ function suggestFix(path3, existingFiles) {
575
+ const targetName = (0, import_path3.basename)(path3).toLowerCase();
558
576
  const similar = existingFiles.find((file) => {
559
577
  const fileName = (0, import_path3.basename)(file).toLowerCase();
560
578
  return fileName.includes(targetName) || targetName.includes(fileName);
@@ -562,9 +580,12 @@ function suggestFix(path2, existingFiles) {
562
580
  if (similar) {
563
581
  return `Did you mean "${similar}"?`;
564
582
  }
565
- return `Create the file "${path2}" or remove the link`;
583
+ return `Create the file "${path3}" or remove the link`;
566
584
  }
567
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
+ );
568
589
  const agentsPath = (0, import_path3.join)(rootDir, "AGENTS.md");
569
590
  const agentsResult = await validateAgentsMap(agentsPath);
570
591
  if (!agentsResult.ok) {
@@ -638,18 +659,9 @@ function matchesExcludePattern(relativePath, excludePatterns) {
638
659
  return regex.test(relativePath);
639
660
  });
640
661
  }
641
- async function generateAgentsMap(config) {
662
+ async function generateAgentsMap(config, graphSections) {
642
663
  const { rootDir, includePaths, excludePaths, sections = DEFAULT_SECTIONS } = config;
643
664
  try {
644
- const allFiles = [];
645
- for (const pattern of includePaths) {
646
- const files = await findFiles(pattern, rootDir);
647
- allFiles.push(...files);
648
- }
649
- const filteredFiles = allFiles.filter((file) => {
650
- const relativePath = (0, import_path4.relative)(rootDir, file);
651
- return !matchesExcludePattern(relativePath, excludePaths);
652
- });
653
665
  const lines = [];
654
666
  lines.push("# AI Agent Knowledge Map");
655
667
  lines.push("");
@@ -659,41 +671,68 @@ async function generateAgentsMap(config) {
659
671
  lines.push("");
660
672
  lines.push("> Add a brief description of this project, its purpose, and key technologies.");
661
673
  lines.push("");
662
- lines.push("## Repository Structure");
663
- lines.push("");
664
- const grouped = groupByDirectory(filteredFiles, rootDir);
665
- for (const [dir, files] of grouped) {
666
- if (dir !== ".") {
667
- lines.push(`### ${dir}/`);
674
+ if (graphSections) {
675
+ for (const section of graphSections) {
676
+ lines.push(`## ${section.name}`);
668
677
  lines.push("");
669
- }
670
- for (const file of files.slice(0, 10)) {
671
- lines.push(formatFileLink(file));
672
- }
673
- if (files.length > 10) {
674
- lines.push(`- _... and ${files.length - 10} more files_`);
675
- }
676
- lines.push("");
677
- }
678
- for (const section of sections) {
679
- lines.push(`## ${section.name}`);
680
- lines.push("");
681
- if (section.description) {
682
- 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
+ }
683
688
  lines.push("");
684
689
  }
685
- const sectionFiles = await findFiles(section.pattern, rootDir);
686
- 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) => {
687
697
  const relativePath = (0, import_path4.relative)(rootDir, file);
688
698
  return !matchesExcludePattern(relativePath, excludePaths);
689
699
  });
690
- for (const file of filteredSectionFiles.slice(0, 20)) {
691
- 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("");
692
715
  }
693
- if (filteredSectionFiles.length > 20) {
694
- 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("");
695
735
  }
696
- lines.push("");
697
736
  }
698
737
  lines.push("## Development Workflow");
699
738
  lines.push("");
@@ -723,7 +762,21 @@ var DEFAULT_RATIOS = {
723
762
  interfaces: 0.1,
724
763
  reserve: 0.1
725
764
  };
726
- 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) {
727
780
  const ratios = {
728
781
  systemPrompt: DEFAULT_RATIOS.systemPrompt,
729
782
  projectManifest: DEFAULT_RATIOS.projectManifest,
@@ -732,6 +785,52 @@ function contextBudget(totalTokens, overrides) {
732
785
  interfaces: DEFAULT_RATIOS.interfaces,
733
786
  reserve: DEFAULT_RATIOS.reserve
734
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
+ }
735
834
  if (overrides) {
736
835
  let overrideSum = 0;
737
836
  const overrideKeys = [];
@@ -745,12 +844,12 @@ function contextBudget(totalTokens, overrides) {
745
844
  }
746
845
  if (overrideKeys.length > 0 && overrideKeys.length < 6) {
747
846
  const remaining = 1 - overrideSum;
748
- const nonOverridden = Object.keys(DEFAULT_RATIOS).filter(
847
+ const nonOverridden = Object.keys(ratios).filter(
749
848
  (k) => !overrideKeys.includes(k)
750
849
  );
751
- const originalSum = nonOverridden.reduce((sum, k) => sum + DEFAULT_RATIOS[k], 0);
850
+ const originalSum = nonOverridden.reduce((sum, k) => sum + ratios[k], 0);
752
851
  for (const k of nonOverridden) {
753
- ratios[k] = remaining * (DEFAULT_RATIOS[k] / originalSum);
852
+ ratios[k] = remaining * (ratios[k] / originalSum);
754
853
  }
755
854
  }
756
855
  }
@@ -801,7 +900,7 @@ var PHASE_PRIORITIES = {
801
900
  { category: "config", patterns: ["harness.config.json", "package.json"], priority: 5 }
802
901
  ]
803
902
  };
804
- function contextFilter(phase, maxCategories) {
903
+ function contextFilter(phase, maxCategories, graphFilePaths) {
805
904
  const categories = PHASE_PRIORITIES[phase];
806
905
  const limit = maxCategories ?? categories.length;
807
906
  const included = categories.slice(0, limit);
@@ -810,7 +909,7 @@ function contextFilter(phase, maxCategories) {
810
909
  phase,
811
910
  includedCategories: included.map((c) => c.category),
812
911
  excludedCategories: excluded.map((c) => c.category),
813
- filePatterns: included.flatMap((c) => c.patterns)
912
+ filePatterns: graphFilePaths ?? included.flatMap((c) => c.patterns)
814
913
  };
815
914
  }
816
915
  function getPhaseCategories(phase) {
@@ -854,7 +953,13 @@ function getImportType(imp) {
854
953
  if (imp.kind === "type") return "type-only";
855
954
  return "static";
856
955
  }
857
- 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
+ }
858
963
  const nodes = [...files];
859
964
  const edges = [];
860
965
  for (const file of files) {
@@ -904,7 +1009,19 @@ function checkLayerViolations(graph, layers, rootDir) {
904
1009
  return violations;
905
1010
  }
906
1011
  async function validateDependencies(config) {
907
- 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
+ }
908
1025
  const healthResult = await parser.health();
909
1026
  if (!healthResult.ok || !healthResult.value.available) {
910
1027
  if (fallbackBehavior === "skip") {
@@ -1038,8 +1155,8 @@ function detectCircularDeps(graph) {
1038
1155
  largestCycle
1039
1156
  });
1040
1157
  }
1041
- async function detectCircularDepsInFiles(files, parser) {
1042
- const graphResult = await buildDependencyGraph(files, parser);
1158
+ async function detectCircularDepsInFiles(files, parser, graphDependencyData) {
1159
+ const graphResult = await buildDependencyGraph(files, parser, graphDependencyData);
1043
1160
  if (!graphResult.ok) {
1044
1161
  return graphResult;
1045
1162
  }
@@ -1057,8 +1174,8 @@ function createBoundaryValidator(schema, name) {
1057
1174
  return (0, import_types.Ok)(result.data);
1058
1175
  }
1059
1176
  const suggestions = result.error.issues.map((issue) => {
1060
- const path2 = issue.path.join(".");
1061
- return path2 ? `${path2}: ${issue.message}` : issue.message;
1177
+ const path3 = issue.path.join(".");
1178
+ return path3 ? `${path3}: ${issue.message}` : issue.message;
1062
1179
  });
1063
1180
  return (0, import_types.Err)(
1064
1181
  createError(
@@ -1127,11 +1244,11 @@ function walk(node, visitor) {
1127
1244
  var TypeScriptParser = class {
1128
1245
  name = "typescript";
1129
1246
  extensions = [".ts", ".tsx", ".mts", ".cts"];
1130
- async parseFile(path2) {
1131
- const contentResult = await readFileContent(path2);
1247
+ async parseFile(path3) {
1248
+ const contentResult = await readFileContent(path3);
1132
1249
  if (!contentResult.ok) {
1133
1250
  return (0, import_types.Err)(
1134
- createParseError("NOT_FOUND", `File not found: ${path2}`, { path: path2 }, [
1251
+ createParseError("NOT_FOUND", `File not found: ${path3}`, { path: path3 }, [
1135
1252
  "Check that the file exists",
1136
1253
  "Verify the path is correct"
1137
1254
  ])
@@ -1141,7 +1258,7 @@ var TypeScriptParser = class {
1141
1258
  const ast = (0, import_typescript_estree.parse)(contentResult.value, {
1142
1259
  loc: true,
1143
1260
  range: true,
1144
- jsx: path2.endsWith(".tsx"),
1261
+ jsx: path3.endsWith(".tsx"),
1145
1262
  errorOnUnknownASTType: false
1146
1263
  });
1147
1264
  return (0, import_types.Ok)({
@@ -1152,7 +1269,7 @@ var TypeScriptParser = class {
1152
1269
  } catch (e) {
1153
1270
  const error = e;
1154
1271
  return (0, import_types.Err)(
1155
- createParseError("SYNTAX_ERROR", `Failed to parse ${path2}: ${error.message}`, { path: path2 }, [
1272
+ createParseError("SYNTAX_ERROR", `Failed to parse ${path3}: ${error.message}`, { path: path3 }, [
1156
1273
  "Check for syntax errors in the file",
1157
1274
  "Ensure valid TypeScript syntax"
1158
1275
  ])
@@ -1436,22 +1553,22 @@ function extractInlineRefs(content) {
1436
1553
  }
1437
1554
  return refs;
1438
1555
  }
1439
- async function parseDocumentationFile(path2) {
1440
- const contentResult = await readFileContent(path2);
1556
+ async function parseDocumentationFile(path3) {
1557
+ const contentResult = await readFileContent(path3);
1441
1558
  if (!contentResult.ok) {
1442
1559
  return (0, import_types.Err)(
1443
1560
  createEntropyError(
1444
1561
  "PARSE_ERROR",
1445
- `Failed to read documentation file: ${path2}`,
1446
- { file: path2 },
1562
+ `Failed to read documentation file: ${path3}`,
1563
+ { file: path3 },
1447
1564
  ["Check that the file exists"]
1448
1565
  )
1449
1566
  );
1450
1567
  }
1451
1568
  const content = contentResult.value;
1452
- const type = path2.endsWith(".md") ? "markdown" : "text";
1569
+ const type = path3.endsWith(".md") ? "markdown" : "text";
1453
1570
  return (0, import_types.Ok)({
1454
- path: path2,
1571
+ path: path3,
1455
1572
  type,
1456
1573
  content,
1457
1574
  codeBlocks: extractCodeBlocks(content),
@@ -1759,7 +1876,45 @@ async function checkStructureDrift(snapshot, _config) {
1759
1876
  }
1760
1877
  return drifts;
1761
1878
  }
1762
- 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
+ }
1763
1918
  const fullConfig = { ...DEFAULT_DRIFT_CONFIG, ...config };
1764
1919
  const drifts = [];
1765
1920
  if (fullConfig.checkApiSignatures) {
@@ -1997,7 +2152,54 @@ function findDeadInternals(snapshot, _reachability) {
1997
2152
  }
1998
2153
  return deadInternals;
1999
2154
  }
2000
- 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
+ }
2001
2203
  const reachability = buildReachabilityMap(snapshot);
2002
2204
  const usageMap = buildExportUsageMap(snapshot);
2003
2205
  const deadExports = findDeadExports(snapshot, usageMap, reachability);
@@ -2323,22 +2525,39 @@ var EntropyAnalyzer = class {
2323
2525
  };
2324
2526
  }
2325
2527
  /**
2326
- * 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.
2327
2531
  */
2328
- async analyze() {
2532
+ async analyze(graphOptions) {
2329
2533
  const startTime = Date.now();
2330
- const snapshotResult = await buildSnapshot(this.config);
2331
- if (!snapshotResult.ok) {
2332
- 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
+ };
2333
2553
  }
2334
- this.snapshot = snapshotResult.value;
2335
2554
  let driftReport;
2336
2555
  let deadCodeReport;
2337
2556
  let patternReport;
2338
2557
  const analysisErrors = [];
2339
2558
  if (this.config.analyze.drift) {
2340
2559
  const driftConfig = typeof this.config.analyze.drift === "object" ? this.config.analyze.drift : {};
2341
- const result = await detectDocDrift(this.snapshot, driftConfig);
2560
+ const result = await detectDocDrift(this.snapshot, driftConfig, graphOptions?.graphDriftData);
2342
2561
  if (result.ok) {
2343
2562
  driftReport = result.value;
2344
2563
  } else {
@@ -2346,7 +2565,7 @@ var EntropyAnalyzer = class {
2346
2565
  }
2347
2566
  }
2348
2567
  if (this.config.analyze.deadCode) {
2349
- const result = await detectDeadCode(this.snapshot);
2568
+ const result = await detectDeadCode(this.snapshot, graphOptions?.graphDeadCodeData);
2350
2569
  if (result.ok) {
2351
2570
  deadCodeReport = result.value;
2352
2571
  } else {
@@ -2443,22 +2662,22 @@ var EntropyAnalyzer = class {
2443
2662
  /**
2444
2663
  * Run drift detection only (snapshot must be built first)
2445
2664
  */
2446
- async detectDrift(config) {
2665
+ async detectDrift(config, graphDriftData) {
2447
2666
  const snapshotResult = await this.ensureSnapshot();
2448
2667
  if (!snapshotResult.ok) {
2449
2668
  return (0, import_types.Err)(snapshotResult.error);
2450
2669
  }
2451
- return detectDocDrift(snapshotResult.value, config || {});
2670
+ return detectDocDrift(snapshotResult.value, config || {}, graphDriftData);
2452
2671
  }
2453
2672
  /**
2454
2673
  * Run dead code detection only (snapshot must be built first)
2455
2674
  */
2456
- async detectDeadCode() {
2675
+ async detectDeadCode(graphDeadCodeData) {
2457
2676
  const snapshotResult = await this.ensureSnapshot();
2458
2677
  if (!snapshotResult.ok) {
2459
2678
  return (0, import_types.Err)(snapshotResult.error);
2460
2679
  }
2461
- return detectDeadCode(snapshotResult.value);
2680
+ return detectDeadCode(snapshotResult.value, graphDeadCodeData);
2462
2681
  }
2463
2682
  /**
2464
2683
  * Run pattern detection only (snapshot must be built first)
@@ -2926,7 +3145,7 @@ function parseDiff(diff) {
2926
3145
  });
2927
3146
  }
2928
3147
  }
2929
- async function analyzeDiff(changes, options) {
3148
+ async function analyzeDiff(changes, options, graphImpactData) {
2930
3149
  if (!options?.enabled) {
2931
3150
  return (0, import_types.Ok)([]);
2932
3151
  }
@@ -2977,26 +3196,75 @@ async function analyzeDiff(changes, options) {
2977
3196
  }
2978
3197
  }
2979
3198
  if (options.checkTestCoverage) {
2980
- const addedSourceFiles = changes.files.filter(
2981
- (f) => f.status === "added" && f.path.endsWith(".ts") && !f.path.includes(".test.")
2982
- );
2983
- const testFiles = changes.files.filter((f) => f.path.includes(".test."));
2984
- for (const sourceFile of addedSourceFiles) {
2985
- const expectedTestPath = sourceFile.path.replace(".ts", ".test.ts");
2986
- const hasTest = testFiles.some(
2987
- (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.")
2988
3221
  );
2989
- if (!hasTest) {
2990
- items.push({
2991
- id: `diff-${++itemId}`,
2992
- category: "diff",
2993
- check: `Test coverage: ${sourceFile.path}`,
2994
- passed: false,
2995
- severity: "warning",
2996
- details: "New source file added without corresponding test file",
2997
- file: sourceFile.path,
2998
- suggestion: `Add tests in ${expectedTestPath}`
2999
- });
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
+ }
3000
3268
  }
3001
3269
  }
3002
3270
  }
@@ -3007,13 +3275,16 @@ async function analyzeDiff(changes, options) {
3007
3275
  var ChecklistBuilder = class {
3008
3276
  rootDir;
3009
3277
  harnessOptions;
3278
+ graphHarnessData;
3010
3279
  customRules = [];
3011
3280
  diffOptions;
3281
+ graphImpactData;
3012
3282
  constructor(rootDir) {
3013
3283
  this.rootDir = rootDir;
3014
3284
  }
3015
- withHarnessChecks(options) {
3285
+ withHarnessChecks(options, graphData) {
3016
3286
  this.harnessOptions = options ?? { context: true, constraints: true, entropy: true };
3287
+ this.graphHarnessData = graphData;
3017
3288
  return this;
3018
3289
  }
3019
3290
  addRule(rule) {
@@ -3024,46 +3295,79 @@ var ChecklistBuilder = class {
3024
3295
  this.customRules.push(...rules);
3025
3296
  return this;
3026
3297
  }
3027
- withDiffAnalysis(options) {
3298
+ withDiffAnalysis(options, graphImpactData) {
3028
3299
  this.diffOptions = options;
3300
+ this.graphImpactData = graphImpactData;
3029
3301
  return this;
3030
3302
  }
3031
3303
  async run(changes) {
3032
3304
  const startTime = Date.now();
3033
3305
  const items = [];
3034
3306
  if (this.harnessOptions) {
3035
- if (this.harnessOptions.context) {
3036
- items.push({
3037
- id: "harness-context",
3038
- category: "harness",
3039
- check: "Context Engineering (AGENTS.md, doc coverage)",
3040
- passed: true,
3041
- severity: "info",
3042
- details: "Harness context validation not yet integrated. See Module 2 (context/).",
3043
- suggestion: "Integrate with validateAgentsMap(), checkDocCoverage() from context module"
3044
- });
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
+ }
3045
3327
  }
3046
- if (this.harnessOptions.constraints) {
3047
- items.push({
3048
- id: "harness-constraints",
3049
- category: "harness",
3050
- check: "Architectural Constraints (dependencies, boundaries)",
3051
- passed: true,
3052
- severity: "info",
3053
- details: "Harness constraints validation not yet integrated. See Module 3 (constraints/).",
3054
- suggestion: "Integrate with validateDependencies(), detectCircularDeps() from constraints module"
3055
- });
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
+ }
3056
3349
  }
3057
- if (this.harnessOptions.entropy) {
3058
- items.push({
3059
- id: "harness-entropy",
3060
- category: "harness",
3061
- check: "Entropy Management (drift, dead code)",
3062
- passed: true,
3063
- severity: "info",
3064
- details: "Harness entropy validation not yet integrated. See Module 4 (entropy/).",
3065
- suggestion: "Integrate with EntropyAnalyzer from entropy module"
3066
- });
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
+ }
3067
3371
  }
3068
3372
  }
3069
3373
  for (const rule of this.customRules) {
@@ -3099,7 +3403,7 @@ var ChecklistBuilder = class {
3099
3403
  }
3100
3404
  }
3101
3405
  if (this.diffOptions) {
3102
- const diffResult = await analyzeDiff(changes, this.diffOptions);
3406
+ const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
3103
3407
  if (diffResult.ok) {
3104
3408
  items.push(...diffResult.value);
3105
3409
  }
@@ -3126,16 +3430,16 @@ var ChecklistBuilder = class {
3126
3430
  };
3127
3431
 
3128
3432
  // src/feedback/review/self-review.ts
3129
- async function createSelfReview(changes, config) {
3433
+ async function createSelfReview(changes, config, graphData) {
3130
3434
  const builder = new ChecklistBuilder(config.rootDir);
3131
3435
  if (config.harness) {
3132
- builder.withHarnessChecks(config.harness);
3436
+ builder.withHarnessChecks(config.harness, graphData?.harness);
3133
3437
  }
3134
3438
  if (config.customRules) {
3135
3439
  builder.addRules(config.customRules);
3136
3440
  }
3137
3441
  if (config.diffAnalysis) {
3138
- builder.withDiffAnalysis(config.diffAnalysis);
3442
+ builder.withDiffAnalysis(config.diffAnalysis, graphData?.impact);
3139
3443
  }
3140
3444
  return builder.run(changes);
3141
3445
  }
@@ -3962,6 +4266,188 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
3962
4266
  };
3963
4267
  }
3964
4268
 
4269
+ // src/ci/check-orchestrator.ts
4270
+ var path2 = __toESM(require("path"));
4271
+ var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
4272
+ async function runSingleCheck(name, projectRoot, config) {
4273
+ const start = Date.now();
4274
+ const issues = [];
4275
+ try {
4276
+ switch (name) {
4277
+ case "validate": {
4278
+ const agentsPath = path2.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
4279
+ const result = await validateAgentsMap(agentsPath);
4280
+ if (!result.ok) {
4281
+ issues.push({ severity: "error", message: result.error.message });
4282
+ } else if (!result.value.valid) {
4283
+ if (result.value.errors) {
4284
+ for (const err of result.value.errors) {
4285
+ issues.push({ severity: "error", message: err.message });
4286
+ }
4287
+ }
4288
+ for (const section of result.value.missingSections) {
4289
+ issues.push({ severity: "warning", message: `Missing section: ${section}` });
4290
+ }
4291
+ for (const link of result.value.brokenLinks) {
4292
+ issues.push({
4293
+ severity: "warning",
4294
+ message: `Broken link: ${link.text} \u2192 ${link.path}`,
4295
+ file: link.path
4296
+ });
4297
+ }
4298
+ }
4299
+ break;
4300
+ }
4301
+ case "deps": {
4302
+ const rawLayers = config.layers;
4303
+ if (rawLayers && rawLayers.length > 0) {
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
+ );
4312
+ const result = await validateDependencies({
4313
+ layers,
4314
+ rootDir: projectRoot,
4315
+ parser
4316
+ });
4317
+ if (!result.ok) {
4318
+ issues.push({ severity: "error", message: result.error.message });
4319
+ } else if (result.value.violations.length > 0) {
4320
+ for (const v of result.value.violations) {
4321
+ issues.push({
4322
+ severity: "error",
4323
+ message: `${v.reason}: ${v.file} imports ${v.imports} (${v.fromLayer} \u2192 ${v.toLayer})`,
4324
+ file: v.file,
4325
+ line: v.line
4326
+ });
4327
+ }
4328
+ }
4329
+ }
4330
+ break;
4331
+ }
4332
+ case "docs": {
4333
+ const docsDir = path2.join(projectRoot, config.docsDir ?? "docs");
4334
+ const result = await checkDocCoverage("project", { docsDir });
4335
+ if (!result.ok) {
4336
+ issues.push({ severity: "warning", message: result.error.message });
4337
+ } else if (result.value.gaps.length > 0) {
4338
+ for (const gap of result.value.gaps) {
4339
+ issues.push({
4340
+ severity: "warning",
4341
+ message: `Undocumented: ${gap.file} (suggested: ${gap.suggestedSection})`,
4342
+ file: gap.file
4343
+ });
4344
+ }
4345
+ }
4346
+ break;
4347
+ }
4348
+ case "entropy": {
4349
+ const analyzer = new EntropyAnalyzer({
4350
+ rootDir: projectRoot,
4351
+ analyze: { drift: true, deadCode: true, patterns: false }
4352
+ });
4353
+ const result = await analyzer.analyze();
4354
+ if (!result.ok) {
4355
+ issues.push({ severity: "warning", message: result.error.message });
4356
+ } else {
4357
+ const report = result.value;
4358
+ if (report.drift) {
4359
+ for (const drift of report.drift.drifts) {
4360
+ issues.push({
4361
+ severity: "warning",
4362
+ message: `Doc drift (${drift.type}): ${drift.details}`,
4363
+ file: drift.docFile,
4364
+ line: drift.line
4365
+ });
4366
+ }
4367
+ }
4368
+ if (report.deadCode) {
4369
+ for (const dead of report.deadCode.deadExports) {
4370
+ issues.push({
4371
+ severity: "warning",
4372
+ message: `Dead export: ${dead.name}`,
4373
+ file: dead.file,
4374
+ line: dead.line
4375
+ });
4376
+ }
4377
+ }
4378
+ }
4379
+ break;
4380
+ }
4381
+ case "phase-gate": {
4382
+ const phaseGates = config.phaseGates;
4383
+ if (!phaseGates?.enabled) {
4384
+ break;
4385
+ }
4386
+ issues.push({
4387
+ severity: "warning",
4388
+ message: "Phase gate is enabled but requires CLI context. Run `harness check-phase-gate` separately for full validation."
4389
+ });
4390
+ break;
4391
+ }
4392
+ }
4393
+ } catch (error) {
4394
+ issues.push({
4395
+ severity: "error",
4396
+ message: `Check '${name}' threw: ${error instanceof Error ? error.message : String(error)}`
4397
+ });
4398
+ }
4399
+ const hasErrors = issues.some((i) => i.severity === "error");
4400
+ const hasWarnings = issues.some((i) => i.severity === "warning");
4401
+ const status = hasErrors ? "fail" : hasWarnings ? "warn" : "pass";
4402
+ return {
4403
+ name,
4404
+ status,
4405
+ issues,
4406
+ durationMs: Date.now() - start
4407
+ };
4408
+ }
4409
+ function buildSummary(checks) {
4410
+ return {
4411
+ total: checks.length,
4412
+ passed: checks.filter((c) => c.status === "pass").length,
4413
+ failed: checks.filter((c) => c.status === "fail").length,
4414
+ warnings: checks.filter((c) => c.status === "warn").length,
4415
+ skipped: checks.filter((c) => c.status === "skip").length
4416
+ };
4417
+ }
4418
+ function determineExitCode(summary, failOn = "error") {
4419
+ if (summary.failed > 0) return 1;
4420
+ if (failOn === "warning" && summary.warnings > 0) return 1;
4421
+ return 0;
4422
+ }
4423
+ async function runCIChecks(input) {
4424
+ const { projectRoot, config, skip = [], failOn = "error" } = input;
4425
+ try {
4426
+ const checks = [];
4427
+ for (const name of ALL_CHECKS) {
4428
+ if (skip.includes(name)) {
4429
+ checks.push({ name, status: "skip", issues: [], durationMs: 0 });
4430
+ } else {
4431
+ const result = await runSingleCheck(name, projectRoot, config);
4432
+ checks.push(result);
4433
+ }
4434
+ }
4435
+ const summary = buildSummary(checks);
4436
+ const exitCode = determineExitCode(summary, failOn);
4437
+ const report = {
4438
+ version: 1,
4439
+ project: config.name ?? "unknown",
4440
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4441
+ checks,
4442
+ summary,
4443
+ exitCode
4444
+ };
4445
+ return (0, import_types.Ok)(report);
4446
+ } catch (error) {
4447
+ return (0, import_types.Err)(error instanceof Error ? error : new Error(String(error)));
4448
+ }
4449
+ }
4450
+
3965
4451
  // src/index.ts
3966
4452
  var VERSION = "0.6.0";
3967
4453
  // Annotate the CommonJS export names for ESM import in node:
@@ -4026,6 +4512,7 @@ var VERSION = "0.6.0";
4026
4512
  requestPeerReview,
4027
4513
  resetFeedbackConfig,
4028
4514
  resolveFileToLayer,
4515
+ runCIChecks,
4029
4516
  runMechanicalGate,
4030
4517
  runMultiTurnPipeline,
4031
4518
  runPipeline,