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