@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.mjs
CHANGED
|
@@ -18,17 +18,17 @@ import { promisify } from "util";
|
|
|
18
18
|
import { glob } from "glob";
|
|
19
19
|
var accessAsync = promisify(access);
|
|
20
20
|
var readFileAsync = promisify(readFile);
|
|
21
|
-
async function fileExists(
|
|
21
|
+
async function fileExists(path3) {
|
|
22
22
|
try {
|
|
23
|
-
await accessAsync(
|
|
23
|
+
await accessAsync(path3, constants.F_OK);
|
|
24
24
|
return true;
|
|
25
25
|
} catch {
|
|
26
26
|
return false;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
async function readFileContent(
|
|
29
|
+
async function readFileContent(path3) {
|
|
30
30
|
try {
|
|
31
|
-
const content = await readFileAsync(
|
|
31
|
+
const content = await readFileAsync(path3, "utf-8");
|
|
32
32
|
return Ok(content);
|
|
33
33
|
} catch (error) {
|
|
34
34
|
return Err(error);
|
|
@@ -76,15 +76,15 @@ function validateConfig(data, schema) {
|
|
|
76
76
|
let message = "Configuration validation failed";
|
|
77
77
|
const suggestions = [];
|
|
78
78
|
if (firstError) {
|
|
79
|
-
const
|
|
80
|
-
const pathDisplay =
|
|
79
|
+
const path3 = firstError.path.join(".");
|
|
80
|
+
const pathDisplay = path3 ? ` at "${path3}"` : "";
|
|
81
81
|
if (firstError.code === "invalid_type") {
|
|
82
82
|
const received = firstError.received;
|
|
83
83
|
const expected = firstError.expected;
|
|
84
84
|
if (received === "undefined") {
|
|
85
85
|
code = "MISSING_FIELD";
|
|
86
86
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
87
|
-
suggestions.push(`Field "${
|
|
87
|
+
suggestions.push(`Field "${path3}" is required and must be of type "${expected}"`);
|
|
88
88
|
} else {
|
|
89
89
|
code = "INVALID_TYPE";
|
|
90
90
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -297,27 +297,30 @@ function extractSections(content) {
|
|
|
297
297
|
return result;
|
|
298
298
|
});
|
|
299
299
|
}
|
|
300
|
-
function isExternalLink(
|
|
301
|
-
return
|
|
300
|
+
function isExternalLink(path3) {
|
|
301
|
+
return path3.startsWith("http://") || path3.startsWith("https://") || path3.startsWith("#") || path3.startsWith("mailto:");
|
|
302
302
|
}
|
|
303
303
|
function resolveLinkPath(linkPath, baseDir) {
|
|
304
304
|
return linkPath.startsWith(".") ? join(baseDir, linkPath) : linkPath;
|
|
305
305
|
}
|
|
306
|
-
async function validateAgentsMap(
|
|
307
|
-
|
|
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
|
+
);
|
|
310
|
+
const contentResult = await readFileContent(path3);
|
|
308
311
|
if (!contentResult.ok) {
|
|
309
312
|
return Err(
|
|
310
313
|
createError(
|
|
311
314
|
"PARSE_ERROR",
|
|
312
315
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
313
|
-
{ path:
|
|
316
|
+
{ path: path3 },
|
|
314
317
|
["Ensure the file exists", "Check file permissions"]
|
|
315
318
|
)
|
|
316
319
|
);
|
|
317
320
|
}
|
|
318
321
|
const content = contentResult.value;
|
|
319
322
|
const sections = extractSections(content);
|
|
320
|
-
const baseDir = dirname(
|
|
323
|
+
const baseDir = dirname(path3);
|
|
321
324
|
const sectionTitles = sections.map((s) => s.title);
|
|
322
325
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
323
326
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -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) => {
|
|
@@ -445,8 +462,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
445
462
|
|
|
446
463
|
// src/context/knowledge-map.ts
|
|
447
464
|
import { join as join2, basename as basename2, relative as relative2 } from "path";
|
|
448
|
-
function suggestFix(
|
|
449
|
-
const targetName = basename2(
|
|
465
|
+
function suggestFix(path3, existingFiles) {
|
|
466
|
+
const targetName = basename2(path3).toLowerCase();
|
|
450
467
|
const similar = existingFiles.find((file) => {
|
|
451
468
|
const fileName = basename2(file).toLowerCase();
|
|
452
469
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -454,9 +471,12 @@ function suggestFix(path2, existingFiles) {
|
|
|
454
471
|
if (similar) {
|
|
455
472
|
return `Did you mean "${similar}"?`;
|
|
456
473
|
}
|
|
457
|
-
return `Create the file "${
|
|
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
|
}
|
|
@@ -949,8 +1065,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
949
1065
|
return Ok(result.data);
|
|
950
1066
|
}
|
|
951
1067
|
const suggestions = result.error.issues.map((issue) => {
|
|
952
|
-
const
|
|
953
|
-
return
|
|
1068
|
+
const path3 = issue.path.join(".");
|
|
1069
|
+
return path3 ? `${path3}: ${issue.message}` : issue.message;
|
|
954
1070
|
});
|
|
955
1071
|
return Err(
|
|
956
1072
|
createError(
|
|
@@ -1019,11 +1135,11 @@ function walk(node, visitor) {
|
|
|
1019
1135
|
var TypeScriptParser = class {
|
|
1020
1136
|
name = "typescript";
|
|
1021
1137
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
1022
|
-
async parseFile(
|
|
1023
|
-
const contentResult = await readFileContent(
|
|
1138
|
+
async parseFile(path3) {
|
|
1139
|
+
const contentResult = await readFileContent(path3);
|
|
1024
1140
|
if (!contentResult.ok) {
|
|
1025
1141
|
return Err(
|
|
1026
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
1142
|
+
createParseError("NOT_FOUND", `File not found: ${path3}`, { path: path3 }, [
|
|
1027
1143
|
"Check that the file exists",
|
|
1028
1144
|
"Verify the path is correct"
|
|
1029
1145
|
])
|
|
@@ -1033,7 +1149,7 @@ var TypeScriptParser = class {
|
|
|
1033
1149
|
const ast = parse(contentResult.value, {
|
|
1034
1150
|
loc: true,
|
|
1035
1151
|
range: true,
|
|
1036
|
-
jsx:
|
|
1152
|
+
jsx: path3.endsWith(".tsx"),
|
|
1037
1153
|
errorOnUnknownASTType: false
|
|
1038
1154
|
});
|
|
1039
1155
|
return Ok({
|
|
@@ -1044,7 +1160,7 @@ var TypeScriptParser = class {
|
|
|
1044
1160
|
} catch (e) {
|
|
1045
1161
|
const error = e;
|
|
1046
1162
|
return Err(
|
|
1047
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
1163
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path3}: ${error.message}`, { path: path3 }, [
|
|
1048
1164
|
"Check for syntax errors in the file",
|
|
1049
1165
|
"Ensure valid TypeScript syntax"
|
|
1050
1166
|
])
|
|
@@ -1328,22 +1444,22 @@ function extractInlineRefs(content) {
|
|
|
1328
1444
|
}
|
|
1329
1445
|
return refs;
|
|
1330
1446
|
}
|
|
1331
|
-
async function parseDocumentationFile(
|
|
1332
|
-
const contentResult = await readFileContent(
|
|
1447
|
+
async function parseDocumentationFile(path3) {
|
|
1448
|
+
const contentResult = await readFileContent(path3);
|
|
1333
1449
|
if (!contentResult.ok) {
|
|
1334
1450
|
return Err(
|
|
1335
1451
|
createEntropyError(
|
|
1336
1452
|
"PARSE_ERROR",
|
|
1337
|
-
`Failed to read documentation file: ${
|
|
1338
|
-
{ file:
|
|
1453
|
+
`Failed to read documentation file: ${path3}`,
|
|
1454
|
+
{ file: path3 },
|
|
1339
1455
|
["Check that the file exists"]
|
|
1340
1456
|
)
|
|
1341
1457
|
);
|
|
1342
1458
|
}
|
|
1343
1459
|
const content = contentResult.value;
|
|
1344
|
-
const type =
|
|
1460
|
+
const type = path3.endsWith(".md") ? "markdown" : "text";
|
|
1345
1461
|
return Ok({
|
|
1346
|
-
path:
|
|
1462
|
+
path: path3,
|
|
1347
1463
|
type,
|
|
1348
1464
|
content,
|
|
1349
1465
|
codeBlocks: extractCodeBlocks(content),
|
|
@@ -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
|
}
|
|
@@ -3854,6 +4157,188 @@ async function runMultiTurnPipeline(initialContext, turnExecutor, options) {
|
|
|
3854
4157
|
};
|
|
3855
4158
|
}
|
|
3856
4159
|
|
|
4160
|
+
// src/ci/check-orchestrator.ts
|
|
4161
|
+
import * as path2 from "path";
|
|
4162
|
+
var ALL_CHECKS = ["validate", "deps", "docs", "entropy", "phase-gate"];
|
|
4163
|
+
async function runSingleCheck(name, projectRoot, config) {
|
|
4164
|
+
const start = Date.now();
|
|
4165
|
+
const issues = [];
|
|
4166
|
+
try {
|
|
4167
|
+
switch (name) {
|
|
4168
|
+
case "validate": {
|
|
4169
|
+
const agentsPath = path2.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
4170
|
+
const result = await validateAgentsMap(agentsPath);
|
|
4171
|
+
if (!result.ok) {
|
|
4172
|
+
issues.push({ severity: "error", message: result.error.message });
|
|
4173
|
+
} else if (!result.value.valid) {
|
|
4174
|
+
if (result.value.errors) {
|
|
4175
|
+
for (const err of result.value.errors) {
|
|
4176
|
+
issues.push({ severity: "error", message: err.message });
|
|
4177
|
+
}
|
|
4178
|
+
}
|
|
4179
|
+
for (const section of result.value.missingSections) {
|
|
4180
|
+
issues.push({ severity: "warning", message: `Missing section: ${section}` });
|
|
4181
|
+
}
|
|
4182
|
+
for (const link of result.value.brokenLinks) {
|
|
4183
|
+
issues.push({
|
|
4184
|
+
severity: "warning",
|
|
4185
|
+
message: `Broken link: ${link.text} \u2192 ${link.path}`,
|
|
4186
|
+
file: link.path
|
|
4187
|
+
});
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
break;
|
|
4191
|
+
}
|
|
4192
|
+
case "deps": {
|
|
4193
|
+
const rawLayers = config.layers;
|
|
4194
|
+
if (rawLayers && rawLayers.length > 0) {
|
|
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
|
+
);
|
|
4203
|
+
const result = await validateDependencies({
|
|
4204
|
+
layers,
|
|
4205
|
+
rootDir: projectRoot,
|
|
4206
|
+
parser
|
|
4207
|
+
});
|
|
4208
|
+
if (!result.ok) {
|
|
4209
|
+
issues.push({ severity: "error", message: result.error.message });
|
|
4210
|
+
} else if (result.value.violations.length > 0) {
|
|
4211
|
+
for (const v of result.value.violations) {
|
|
4212
|
+
issues.push({
|
|
4213
|
+
severity: "error",
|
|
4214
|
+
message: `${v.reason}: ${v.file} imports ${v.imports} (${v.fromLayer} \u2192 ${v.toLayer})`,
|
|
4215
|
+
file: v.file,
|
|
4216
|
+
line: v.line
|
|
4217
|
+
});
|
|
4218
|
+
}
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
break;
|
|
4222
|
+
}
|
|
4223
|
+
case "docs": {
|
|
4224
|
+
const docsDir = path2.join(projectRoot, config.docsDir ?? "docs");
|
|
4225
|
+
const result = await checkDocCoverage("project", { docsDir });
|
|
4226
|
+
if (!result.ok) {
|
|
4227
|
+
issues.push({ severity: "warning", message: result.error.message });
|
|
4228
|
+
} else if (result.value.gaps.length > 0) {
|
|
4229
|
+
for (const gap of result.value.gaps) {
|
|
4230
|
+
issues.push({
|
|
4231
|
+
severity: "warning",
|
|
4232
|
+
message: `Undocumented: ${gap.file} (suggested: ${gap.suggestedSection})`,
|
|
4233
|
+
file: gap.file
|
|
4234
|
+
});
|
|
4235
|
+
}
|
|
4236
|
+
}
|
|
4237
|
+
break;
|
|
4238
|
+
}
|
|
4239
|
+
case "entropy": {
|
|
4240
|
+
const analyzer = new EntropyAnalyzer({
|
|
4241
|
+
rootDir: projectRoot,
|
|
4242
|
+
analyze: { drift: true, deadCode: true, patterns: false }
|
|
4243
|
+
});
|
|
4244
|
+
const result = await analyzer.analyze();
|
|
4245
|
+
if (!result.ok) {
|
|
4246
|
+
issues.push({ severity: "warning", message: result.error.message });
|
|
4247
|
+
} else {
|
|
4248
|
+
const report = result.value;
|
|
4249
|
+
if (report.drift) {
|
|
4250
|
+
for (const drift of report.drift.drifts) {
|
|
4251
|
+
issues.push({
|
|
4252
|
+
severity: "warning",
|
|
4253
|
+
message: `Doc drift (${drift.type}): ${drift.details}`,
|
|
4254
|
+
file: drift.docFile,
|
|
4255
|
+
line: drift.line
|
|
4256
|
+
});
|
|
4257
|
+
}
|
|
4258
|
+
}
|
|
4259
|
+
if (report.deadCode) {
|
|
4260
|
+
for (const dead of report.deadCode.deadExports) {
|
|
4261
|
+
issues.push({
|
|
4262
|
+
severity: "warning",
|
|
4263
|
+
message: `Dead export: ${dead.name}`,
|
|
4264
|
+
file: dead.file,
|
|
4265
|
+
line: dead.line
|
|
4266
|
+
});
|
|
4267
|
+
}
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
break;
|
|
4271
|
+
}
|
|
4272
|
+
case "phase-gate": {
|
|
4273
|
+
const phaseGates = config.phaseGates;
|
|
4274
|
+
if (!phaseGates?.enabled) {
|
|
4275
|
+
break;
|
|
4276
|
+
}
|
|
4277
|
+
issues.push({
|
|
4278
|
+
severity: "warning",
|
|
4279
|
+
message: "Phase gate is enabled but requires CLI context. Run `harness check-phase-gate` separately for full validation."
|
|
4280
|
+
});
|
|
4281
|
+
break;
|
|
4282
|
+
}
|
|
4283
|
+
}
|
|
4284
|
+
} catch (error) {
|
|
4285
|
+
issues.push({
|
|
4286
|
+
severity: "error",
|
|
4287
|
+
message: `Check '${name}' threw: ${error instanceof Error ? error.message : String(error)}`
|
|
4288
|
+
});
|
|
4289
|
+
}
|
|
4290
|
+
const hasErrors = issues.some((i) => i.severity === "error");
|
|
4291
|
+
const hasWarnings = issues.some((i) => i.severity === "warning");
|
|
4292
|
+
const status = hasErrors ? "fail" : hasWarnings ? "warn" : "pass";
|
|
4293
|
+
return {
|
|
4294
|
+
name,
|
|
4295
|
+
status,
|
|
4296
|
+
issues,
|
|
4297
|
+
durationMs: Date.now() - start
|
|
4298
|
+
};
|
|
4299
|
+
}
|
|
4300
|
+
function buildSummary(checks) {
|
|
4301
|
+
return {
|
|
4302
|
+
total: checks.length,
|
|
4303
|
+
passed: checks.filter((c) => c.status === "pass").length,
|
|
4304
|
+
failed: checks.filter((c) => c.status === "fail").length,
|
|
4305
|
+
warnings: checks.filter((c) => c.status === "warn").length,
|
|
4306
|
+
skipped: checks.filter((c) => c.status === "skip").length
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
function determineExitCode(summary, failOn = "error") {
|
|
4310
|
+
if (summary.failed > 0) return 1;
|
|
4311
|
+
if (failOn === "warning" && summary.warnings > 0) return 1;
|
|
4312
|
+
return 0;
|
|
4313
|
+
}
|
|
4314
|
+
async function runCIChecks(input) {
|
|
4315
|
+
const { projectRoot, config, skip = [], failOn = "error" } = input;
|
|
4316
|
+
try {
|
|
4317
|
+
const checks = [];
|
|
4318
|
+
for (const name of ALL_CHECKS) {
|
|
4319
|
+
if (skip.includes(name)) {
|
|
4320
|
+
checks.push({ name, status: "skip", issues: [], durationMs: 0 });
|
|
4321
|
+
} else {
|
|
4322
|
+
const result = await runSingleCheck(name, projectRoot, config);
|
|
4323
|
+
checks.push(result);
|
|
4324
|
+
}
|
|
4325
|
+
}
|
|
4326
|
+
const summary = buildSummary(checks);
|
|
4327
|
+
const exitCode = determineExitCode(summary, failOn);
|
|
4328
|
+
const report = {
|
|
4329
|
+
version: 1,
|
|
4330
|
+
project: config.name ?? "unknown",
|
|
4331
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4332
|
+
checks,
|
|
4333
|
+
summary,
|
|
4334
|
+
exitCode
|
|
4335
|
+
};
|
|
4336
|
+
return Ok(report);
|
|
4337
|
+
} catch (error) {
|
|
4338
|
+
return Err(error instanceof Error ? error : new Error(String(error)));
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
|
|
3857
4342
|
// src/index.ts
|
|
3858
4343
|
var VERSION = "0.6.0";
|
|
3859
4344
|
export {
|
|
@@ -3917,6 +4402,7 @@ export {
|
|
|
3917
4402
|
requestPeerReview,
|
|
3918
4403
|
resetFeedbackConfig,
|
|
3919
4404
|
resolveFileToLayer,
|
|
4405
|
+
runCIChecks,
|
|
3920
4406
|
runMechanicalGate,
|
|
3921
4407
|
runMultiTurnPipeline,
|
|
3922
4408
|
runPipeline,
|