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