@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.d.mts +500 -387
- package/dist/index.d.ts +500 -387
- package/dist/index.js +634 -147
- package/dist/index.mjs +633 -147
- package/package.json +2 -2
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(
|
|
130
|
+
async function fileExists(path3) {
|
|
130
131
|
try {
|
|
131
|
-
await accessAsync(
|
|
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(
|
|
138
|
+
async function readFileContent(path3) {
|
|
138
139
|
try {
|
|
139
|
-
const content = await readFileAsync(
|
|
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
|
|
188
|
-
const pathDisplay =
|
|
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 "${
|
|
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(
|
|
409
|
-
return
|
|
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(
|
|
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
|
+
);
|
|
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:
|
|
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)(
|
|
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(
|
|
557
|
-
const targetName = (0, import_path3.basename)(
|
|
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 "${
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
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
|
-
|
|
686
|
-
const
|
|
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
|
-
|
|
691
|
-
|
|
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
|
-
|
|
694
|
-
lines.push(
|
|
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
|
-
|
|
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(
|
|
847
|
+
const nonOverridden = Object.keys(ratios).filter(
|
|
749
848
|
(k) => !overrideKeys.includes(k)
|
|
750
849
|
);
|
|
751
|
-
const originalSum = nonOverridden.reduce((sum, k) => sum +
|
|
850
|
+
const originalSum = nonOverridden.reduce((sum, k) => sum + ratios[k], 0);
|
|
752
851
|
for (const k of nonOverridden) {
|
|
753
|
-
ratios[k] = remaining * (
|
|
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
|
|
1061
|
-
return
|
|
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(
|
|
1131
|
-
const contentResult = await readFileContent(
|
|
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: ${
|
|
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:
|
|
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 ${
|
|
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(
|
|
1440
|
-
const contentResult = await readFileContent(
|
|
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: ${
|
|
1446
|
-
{ file:
|
|
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 =
|
|
1569
|
+
const type = path3.endsWith(".md") ? "markdown" : "text";
|
|
1453
1570
|
return (0, import_types.Ok)({
|
|
1454
|
-
path:
|
|
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
|
|
2331
|
-
if (
|
|
2332
|
-
|
|
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
|
-
|
|
2981
|
-
(
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
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
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
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
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
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
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
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
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
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,
|