@ipation/specbridge 2.3.0 → 2.4.2
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/CHANGELOG.md +28 -0
- package/README.md +4 -0
- package/dist/cli.js +1012 -760
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +707 -506
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command as Command20 } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk19 from "chalk";
|
|
6
6
|
import { readFileSync as readFileSync2 } from "fs";
|
|
7
7
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8
8
|
import { dirname as dirname5, join as join14 } from "path";
|
|
@@ -343,12 +343,7 @@ import fg from "fast-glob";
|
|
|
343
343
|
import { minimatch } from "minimatch";
|
|
344
344
|
import { relative, isAbsolute } from "path";
|
|
345
345
|
async function glob(patterns, options = {}) {
|
|
346
|
-
const {
|
|
347
|
-
cwd = process.cwd(),
|
|
348
|
-
ignore = [],
|
|
349
|
-
absolute = false,
|
|
350
|
-
onlyFiles = true
|
|
351
|
-
} = options;
|
|
346
|
+
const { cwd = process.cwd(), ignore = [], absolute = false, onlyFiles = true } = options;
|
|
352
347
|
return fg(patterns, {
|
|
353
348
|
cwd,
|
|
354
349
|
ignore,
|
|
@@ -582,12 +577,24 @@ var FUNCTION_PATTERNS = [
|
|
|
582
577
|
{ convention: "snake_case", regex: /^[a-z][a-z0-9_]*$/, description: "Functions use snake_case" }
|
|
583
578
|
];
|
|
584
579
|
var INTERFACE_PATTERNS = [
|
|
585
|
-
{
|
|
586
|
-
|
|
580
|
+
{
|
|
581
|
+
convention: "PascalCase",
|
|
582
|
+
regex: /^[A-Z][a-zA-Z0-9]*$/,
|
|
583
|
+
description: "Interfaces use PascalCase"
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
convention: "IPrefixed",
|
|
587
|
+
regex: /^I[A-Z][a-zA-Z0-9]*$/,
|
|
588
|
+
description: "Interfaces are prefixed with I"
|
|
589
|
+
}
|
|
587
590
|
];
|
|
588
591
|
var TYPE_PATTERNS = [
|
|
589
592
|
{ convention: "PascalCase", regex: /^[A-Z][a-zA-Z0-9]*$/, description: "Types use PascalCase" },
|
|
590
|
-
{
|
|
593
|
+
{
|
|
594
|
+
convention: "TSuffixed",
|
|
595
|
+
regex: /^[A-Z][a-zA-Z0-9]*Type$/,
|
|
596
|
+
description: "Types are suffixed with Type"
|
|
597
|
+
}
|
|
591
598
|
];
|
|
592
599
|
var NamingAnalyzer = class {
|
|
593
600
|
id = "naming";
|
|
@@ -608,7 +615,10 @@ var NamingAnalyzer = class {
|
|
|
608
615
|
analyzeClassNaming(scanner) {
|
|
609
616
|
const classes = scanner.findClasses();
|
|
610
617
|
if (classes.length < 3) return null;
|
|
611
|
-
const matches = this.findBestMatch(
|
|
618
|
+
const matches = this.findBestMatch(
|
|
619
|
+
classes.map((c) => c.name),
|
|
620
|
+
CLASS_PATTERNS
|
|
621
|
+
);
|
|
612
622
|
if (!matches) return null;
|
|
613
623
|
return createPattern(this.id, {
|
|
614
624
|
id: "naming-classes",
|
|
@@ -632,7 +642,10 @@ var NamingAnalyzer = class {
|
|
|
632
642
|
analyzeFunctionNaming(scanner) {
|
|
633
643
|
const functions = scanner.findFunctions();
|
|
634
644
|
if (functions.length < 3) return null;
|
|
635
|
-
const matches = this.findBestMatch(
|
|
645
|
+
const matches = this.findBestMatch(
|
|
646
|
+
functions.map((f) => f.name),
|
|
647
|
+
FUNCTION_PATTERNS
|
|
648
|
+
);
|
|
636
649
|
if (!matches) return null;
|
|
637
650
|
return createPattern(this.id, {
|
|
638
651
|
id: "naming-functions",
|
|
@@ -656,7 +669,10 @@ var NamingAnalyzer = class {
|
|
|
656
669
|
analyzeInterfaceNaming(scanner) {
|
|
657
670
|
const interfaces = scanner.findInterfaces();
|
|
658
671
|
if (interfaces.length < 3) return null;
|
|
659
|
-
const matches = this.findBestMatch(
|
|
672
|
+
const matches = this.findBestMatch(
|
|
673
|
+
interfaces.map((i) => i.name),
|
|
674
|
+
INTERFACE_PATTERNS
|
|
675
|
+
);
|
|
660
676
|
if (!matches) return null;
|
|
661
677
|
return createPattern(this.id, {
|
|
662
678
|
id: "naming-interfaces",
|
|
@@ -680,7 +696,10 @@ var NamingAnalyzer = class {
|
|
|
680
696
|
analyzeTypeNaming(scanner) {
|
|
681
697
|
const types = scanner.findTypeAliases();
|
|
682
698
|
if (types.length < 3) return null;
|
|
683
|
-
const matches = this.findBestMatch(
|
|
699
|
+
const matches = this.findBestMatch(
|
|
700
|
+
types.map((t) => t.name),
|
|
701
|
+
TYPE_PATTERNS
|
|
702
|
+
);
|
|
684
703
|
if (!matches) return null;
|
|
685
704
|
return createPattern(this.id, {
|
|
686
705
|
id: "naming-types",
|
|
@@ -769,8 +788,12 @@ var ImportsAnalyzer = class {
|
|
|
769
788
|
analyzeRelativeImports(scanner) {
|
|
770
789
|
const imports = scanner.findImports();
|
|
771
790
|
const relativeImports = imports.filter((i) => i.module.startsWith("."));
|
|
772
|
-
const absoluteImports = imports.filter(
|
|
773
|
-
|
|
791
|
+
const absoluteImports = imports.filter(
|
|
792
|
+
(i) => !i.module.startsWith(".") && !i.module.startsWith("@")
|
|
793
|
+
);
|
|
794
|
+
const aliasImports = imports.filter(
|
|
795
|
+
(i) => i.module.startsWith("@/") || i.module.startsWith("~")
|
|
796
|
+
);
|
|
774
797
|
const total = relativeImports.length + absoluteImports.length + aliasImports.length;
|
|
775
798
|
if (total < 10) return null;
|
|
776
799
|
if (aliasImports.length > relativeImports.length && aliasImports.length >= 5) {
|
|
@@ -837,18 +860,20 @@ var ImportsAnalyzer = class {
|
|
|
837
860
|
for (const [packageName, data] of moduleCounts) {
|
|
838
861
|
if (data.count >= 5) {
|
|
839
862
|
const confidence = Math.min(100, 50 + data.count * 2);
|
|
840
|
-
patterns.push(
|
|
841
|
-
id
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
863
|
+
patterns.push(
|
|
864
|
+
createPattern(this.id, {
|
|
865
|
+
id: `imports-module-${packageName.replace(/[/@]/g, "-")}`,
|
|
866
|
+
name: `${packageName} Usage`,
|
|
867
|
+
description: `${packageName} is used across ${data.count} files`,
|
|
868
|
+
confidence,
|
|
869
|
+
occurrences: data.count,
|
|
870
|
+
examples: data.examples.slice(0, 3).map((i) => ({
|
|
871
|
+
file: i.file,
|
|
872
|
+
line: i.line,
|
|
873
|
+
snippet: `import { ${i.named.slice(0, 2).join(", ") || "..."} } from '${i.module}'`
|
|
874
|
+
}))
|
|
875
|
+
})
|
|
876
|
+
);
|
|
852
877
|
}
|
|
853
878
|
}
|
|
854
879
|
return patterns;
|
|
@@ -893,24 +918,26 @@ var StructureAnalyzer = class {
|
|
|
893
918
|
const count = dirCounts.get(name);
|
|
894
919
|
if (count && count >= 3) {
|
|
895
920
|
const exampleFiles = files.filter((f) => basename(dirname2(f.path)) === name).slice(0, 3);
|
|
896
|
-
patterns.push(
|
|
897
|
-
id
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
921
|
+
patterns.push(
|
|
922
|
+
createPattern(this.id, {
|
|
923
|
+
id: `structure-dir-${name}`,
|
|
924
|
+
name: `${name}/ Directory Convention`,
|
|
925
|
+
description,
|
|
926
|
+
confidence: Math.min(100, 60 + count * 5),
|
|
927
|
+
occurrences: count,
|
|
928
|
+
examples: exampleFiles.map((f) => ({
|
|
929
|
+
file: f.path,
|
|
930
|
+
line: 1,
|
|
931
|
+
snippet: basename(f.path)
|
|
932
|
+
})),
|
|
933
|
+
suggestedConstraint: {
|
|
934
|
+
type: "convention",
|
|
935
|
+
rule: `${name.charAt(0).toUpperCase() + name.slice(1)} should be placed in the ${name}/ directory`,
|
|
936
|
+
severity: "low",
|
|
937
|
+
scope: `src/**/${name}/**/*.ts`
|
|
938
|
+
}
|
|
939
|
+
})
|
|
940
|
+
);
|
|
914
941
|
}
|
|
915
942
|
}
|
|
916
943
|
return patterns;
|
|
@@ -920,29 +947,55 @@ var StructureAnalyzer = class {
|
|
|
920
947
|
const suffixPatterns = [
|
|
921
948
|
{ suffix: ".test.ts", pattern: /\.test\.ts$/, description: "Test files use .test.ts suffix" },
|
|
922
949
|
{ suffix: ".spec.ts", pattern: /\.spec\.ts$/, description: "Test files use .spec.ts suffix" },
|
|
923
|
-
{
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
{
|
|
950
|
+
{
|
|
951
|
+
suffix: ".types.ts",
|
|
952
|
+
pattern: /\.types\.ts$/,
|
|
953
|
+
description: "Type definition files use .types.ts suffix"
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
suffix: ".utils.ts",
|
|
957
|
+
pattern: /\.utils\.ts$/,
|
|
958
|
+
description: "Utility files use .utils.ts suffix"
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
suffix: ".service.ts",
|
|
962
|
+
pattern: /\.service\.ts$/,
|
|
963
|
+
description: "Service files use .service.ts suffix"
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
suffix: ".controller.ts",
|
|
967
|
+
pattern: /\.controller\.ts$/,
|
|
968
|
+
description: "Controller files use .controller.ts suffix"
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
suffix: ".model.ts",
|
|
972
|
+
pattern: /\.model\.ts$/,
|
|
973
|
+
description: "Model files use .model.ts suffix"
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
suffix: ".schema.ts",
|
|
977
|
+
pattern: /\.schema\.ts$/,
|
|
978
|
+
description: "Schema files use .schema.ts suffix"
|
|
979
|
+
}
|
|
929
980
|
];
|
|
930
981
|
for (const { suffix, pattern, description } of suffixPatterns) {
|
|
931
982
|
const matchingFiles = files.filter((f) => pattern.test(f.path));
|
|
932
983
|
if (matchingFiles.length >= 3) {
|
|
933
984
|
const confidence = Math.min(100, 60 + matchingFiles.length * 3);
|
|
934
|
-
patterns.push(
|
|
935
|
-
id
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
985
|
+
patterns.push(
|
|
986
|
+
createPattern(this.id, {
|
|
987
|
+
id: `structure-suffix-${suffix.replace(/\./g, "-")}`,
|
|
988
|
+
name: `${suffix} File Naming`,
|
|
989
|
+
description,
|
|
990
|
+
confidence,
|
|
991
|
+
occurrences: matchingFiles.length,
|
|
992
|
+
examples: matchingFiles.slice(0, 3).map((f) => ({
|
|
993
|
+
file: f.path,
|
|
994
|
+
line: 1,
|
|
995
|
+
snippet: basename(f.path)
|
|
996
|
+
}))
|
|
997
|
+
})
|
|
998
|
+
);
|
|
946
999
|
}
|
|
947
1000
|
}
|
|
948
1001
|
return patterns;
|
|
@@ -1308,7 +1361,9 @@ Results saved to: ${outputPath}`));
|
|
|
1308
1361
|
console.log("");
|
|
1309
1362
|
console.log(chalk2.cyan("Next steps:"));
|
|
1310
1363
|
console.log(" Review detected patterns and create decisions for important ones.");
|
|
1311
|
-
console.log(
|
|
1364
|
+
console.log(
|
|
1365
|
+
` Use ${chalk2.bold("specbridge decision create <id>")} to create a new decision.`
|
|
1366
|
+
);
|
|
1312
1367
|
}
|
|
1313
1368
|
} catch (error) {
|
|
1314
1369
|
spinner.fail("Inference failed");
|
|
@@ -1324,7 +1379,9 @@ Detected ${patterns.length} pattern(s):
|
|
|
1324
1379
|
console.log(chalk2.bold(`${pattern.name}`));
|
|
1325
1380
|
console.log(chalk2.dim(` ID: ${pattern.id}`));
|
|
1326
1381
|
console.log(` ${pattern.description}`);
|
|
1327
|
-
console.log(
|
|
1382
|
+
console.log(
|
|
1383
|
+
` Confidence: ${confidenceColor(`${pattern.confidence}%`)} (${pattern.occurrences} occurrences)`
|
|
1384
|
+
);
|
|
1328
1385
|
console.log(chalk2.dim(` Analyzer: ${pattern.analyzer}`));
|
|
1329
1386
|
if (pattern.examples.length > 0) {
|
|
1330
1387
|
console.log(chalk2.dim(" Examples:"));
|
|
@@ -1345,12 +1402,11 @@ Detected ${patterns.length} pattern(s):
|
|
|
1345
1402
|
|
|
1346
1403
|
// src/cli/commands/verify.ts
|
|
1347
1404
|
import { Command as Command3 } from "commander";
|
|
1348
|
-
import
|
|
1405
|
+
import chalk4 from "chalk";
|
|
1349
1406
|
import ora3 from "ora";
|
|
1350
1407
|
|
|
1351
1408
|
// src/verification/engine.ts
|
|
1352
1409
|
import { Project as Project2 } from "ts-morph";
|
|
1353
|
-
import chalk3 from "chalk";
|
|
1354
1410
|
|
|
1355
1411
|
// src/registry/loader.ts
|
|
1356
1412
|
import { join as join4 } from "path";
|
|
@@ -1608,17 +1664,13 @@ var Registry = class {
|
|
|
1608
1664
|
* Get decisions by tag
|
|
1609
1665
|
*/
|
|
1610
1666
|
getByTag(tag) {
|
|
1611
|
-
return this.getAll().filter(
|
|
1612
|
-
(d) => d.metadata.tags?.includes(tag)
|
|
1613
|
-
);
|
|
1667
|
+
return this.getAll().filter((d) => d.metadata.tags?.includes(tag));
|
|
1614
1668
|
}
|
|
1615
1669
|
/**
|
|
1616
1670
|
* Get decisions by owner
|
|
1617
1671
|
*/
|
|
1618
1672
|
getByOwner(owner) {
|
|
1619
|
-
return this.getAll().filter(
|
|
1620
|
-
(d) => d.metadata.owners.includes(owner)
|
|
1621
|
-
);
|
|
1673
|
+
return this.getAll().filter((d) => d.metadata.owners.includes(owner));
|
|
1622
1674
|
}
|
|
1623
1675
|
/**
|
|
1624
1676
|
* Apply filter to decisions
|
|
@@ -1629,21 +1681,15 @@ var Registry = class {
|
|
|
1629
1681
|
return false;
|
|
1630
1682
|
}
|
|
1631
1683
|
if (filter.tags) {
|
|
1632
|
-
const hasTags = filter.tags.some(
|
|
1633
|
-
(tag) => decision.metadata.tags?.includes(tag)
|
|
1634
|
-
);
|
|
1684
|
+
const hasTags = filter.tags.some((tag) => decision.metadata.tags?.includes(tag));
|
|
1635
1685
|
if (!hasTags) return false;
|
|
1636
1686
|
}
|
|
1637
1687
|
if (filter.constraintType) {
|
|
1638
|
-
const hasType = decision.constraints.some(
|
|
1639
|
-
(c) => filter.constraintType?.includes(c.type)
|
|
1640
|
-
);
|
|
1688
|
+
const hasType = decision.constraints.some((c) => filter.constraintType?.includes(c.type));
|
|
1641
1689
|
if (!hasType) return false;
|
|
1642
1690
|
}
|
|
1643
1691
|
if (filter.severity) {
|
|
1644
|
-
const hasSeverity = decision.constraints.some(
|
|
1645
|
-
(c) => filter.severity?.includes(c.severity)
|
|
1646
|
-
);
|
|
1692
|
+
const hasSeverity = decision.constraints.some((c) => filter.severity?.includes(c.severity));
|
|
1647
1693
|
if (!hasSeverity) return false;
|
|
1648
1694
|
}
|
|
1649
1695
|
return true;
|
|
@@ -1738,17 +1784,19 @@ var NamingVerifier = class {
|
|
|
1738
1784
|
for (const classDecl of sourceFile.getClasses()) {
|
|
1739
1785
|
const name = classDecl.getName();
|
|
1740
1786
|
if (name && !pattern.regex.test(name)) {
|
|
1741
|
-
violations.push(
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1787
|
+
violations.push(
|
|
1788
|
+
createViolation({
|
|
1789
|
+
decisionId,
|
|
1790
|
+
constraintId: constraint.id,
|
|
1791
|
+
type: constraint.type,
|
|
1792
|
+
severity: constraint.severity,
|
|
1793
|
+
message: `Class "${name}" does not follow ${pattern.description} naming convention`,
|
|
1794
|
+
file: filePath,
|
|
1795
|
+
line: classDecl.getStartLineNumber(),
|
|
1796
|
+
column: classDecl.getStart() - classDecl.getStartLinePos(),
|
|
1797
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1798
|
+
})
|
|
1799
|
+
);
|
|
1752
1800
|
}
|
|
1753
1801
|
}
|
|
1754
1802
|
}
|
|
@@ -1756,16 +1804,18 @@ var NamingVerifier = class {
|
|
|
1756
1804
|
for (const funcDecl of sourceFile.getFunctions()) {
|
|
1757
1805
|
const name = funcDecl.getName();
|
|
1758
1806
|
if (name && !pattern.regex.test(name)) {
|
|
1759
|
-
violations.push(
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1807
|
+
violations.push(
|
|
1808
|
+
createViolation({
|
|
1809
|
+
decisionId,
|
|
1810
|
+
constraintId: constraint.id,
|
|
1811
|
+
type: constraint.type,
|
|
1812
|
+
severity: constraint.severity,
|
|
1813
|
+
message: `Function "${name}" does not follow ${pattern.description} naming convention`,
|
|
1814
|
+
file: filePath,
|
|
1815
|
+
line: funcDecl.getStartLineNumber(),
|
|
1816
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1817
|
+
})
|
|
1818
|
+
);
|
|
1769
1819
|
}
|
|
1770
1820
|
}
|
|
1771
1821
|
}
|
|
@@ -1773,16 +1823,18 @@ var NamingVerifier = class {
|
|
|
1773
1823
|
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
1774
1824
|
const name = interfaceDecl.getName();
|
|
1775
1825
|
if (!pattern.regex.test(name)) {
|
|
1776
|
-
violations.push(
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1826
|
+
violations.push(
|
|
1827
|
+
createViolation({
|
|
1828
|
+
decisionId,
|
|
1829
|
+
constraintId: constraint.id,
|
|
1830
|
+
type: constraint.type,
|
|
1831
|
+
severity: constraint.severity,
|
|
1832
|
+
message: `Interface "${name}" does not follow ${pattern.description} naming convention`,
|
|
1833
|
+
file: filePath,
|
|
1834
|
+
line: interfaceDecl.getStartLineNumber(),
|
|
1835
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1836
|
+
})
|
|
1837
|
+
);
|
|
1786
1838
|
}
|
|
1787
1839
|
}
|
|
1788
1840
|
}
|
|
@@ -1790,16 +1842,18 @@ var NamingVerifier = class {
|
|
|
1790
1842
|
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
1791
1843
|
const name = typeAlias.getName();
|
|
1792
1844
|
if (!pattern.regex.test(name)) {
|
|
1793
|
-
violations.push(
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1845
|
+
violations.push(
|
|
1846
|
+
createViolation({
|
|
1847
|
+
decisionId,
|
|
1848
|
+
constraintId: constraint.id,
|
|
1849
|
+
type: constraint.type,
|
|
1850
|
+
severity: constraint.severity,
|
|
1851
|
+
message: `Type "${name}" does not follow ${pattern.description} naming convention`,
|
|
1852
|
+
file: filePath,
|
|
1853
|
+
line: typeAlias.getStartLineNumber(),
|
|
1854
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1855
|
+
})
|
|
1856
|
+
);
|
|
1803
1857
|
}
|
|
1804
1858
|
}
|
|
1805
1859
|
}
|
|
@@ -1834,20 +1888,22 @@ var ImportsVerifier = class {
|
|
|
1834
1888
|
const ms = importDecl.getModuleSpecifier();
|
|
1835
1889
|
const start = ms.getStart() + 1;
|
|
1836
1890
|
const end = ms.getEnd() - 1;
|
|
1837
|
-
violations.push(
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1891
|
+
violations.push(
|
|
1892
|
+
createViolation({
|
|
1893
|
+
decisionId,
|
|
1894
|
+
constraintId: constraint.id,
|
|
1895
|
+
type: constraint.type,
|
|
1896
|
+
severity: constraint.severity,
|
|
1897
|
+
message: `Relative import "${moduleSpec}" should include a .js extension`,
|
|
1898
|
+
file: filePath,
|
|
1899
|
+
line: importDecl.getStartLineNumber(),
|
|
1900
|
+
suggestion: `Update to "${suggested}"`,
|
|
1901
|
+
autofix: {
|
|
1902
|
+
description: "Add/normalize .js extension in import specifier",
|
|
1903
|
+
edits: [{ start, end, text: suggested }]
|
|
1904
|
+
}
|
|
1905
|
+
})
|
|
1906
|
+
);
|
|
1851
1907
|
}
|
|
1852
1908
|
}
|
|
1853
1909
|
if (rule.includes("barrel") || rule.includes("index")) {
|
|
@@ -1856,16 +1912,18 @@ var ImportsVerifier = class {
|
|
|
1856
1912
|
if (!moduleSpec.startsWith(".")) continue;
|
|
1857
1913
|
if (moduleSpec.match(/\.(ts|js|tsx|jsx)$/) || moduleSpec.match(/\/[^/]+$/)) {
|
|
1858
1914
|
if (!moduleSpec.endsWith("/index") && !moduleSpec.endsWith("index")) {
|
|
1859
|
-
violations.push(
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1915
|
+
violations.push(
|
|
1916
|
+
createViolation({
|
|
1917
|
+
decisionId,
|
|
1918
|
+
constraintId: constraint.id,
|
|
1919
|
+
type: constraint.type,
|
|
1920
|
+
severity: constraint.severity,
|
|
1921
|
+
message: `Import from "${moduleSpec}" should use barrel (index) import`,
|
|
1922
|
+
file: filePath,
|
|
1923
|
+
line: importDecl.getStartLineNumber(),
|
|
1924
|
+
suggestion: "Import from the parent directory index file instead"
|
|
1925
|
+
})
|
|
1926
|
+
);
|
|
1869
1927
|
}
|
|
1870
1928
|
}
|
|
1871
1929
|
}
|
|
@@ -1874,16 +1932,18 @@ var ImportsVerifier = class {
|
|
|
1874
1932
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1875
1933
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1876
1934
|
if (moduleSpec.match(/^\.\.\/\.\.\/\.\.\//)) {
|
|
1877
|
-
violations.push(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1935
|
+
violations.push(
|
|
1936
|
+
createViolation({
|
|
1937
|
+
decisionId,
|
|
1938
|
+
constraintId: constraint.id,
|
|
1939
|
+
type: constraint.type,
|
|
1940
|
+
severity: constraint.severity,
|
|
1941
|
+
message: `Deep relative import "${moduleSpec}" should use path alias`,
|
|
1942
|
+
file: filePath,
|
|
1943
|
+
line: importDecl.getStartLineNumber(),
|
|
1944
|
+
suggestion: "Use path alias (e.g., @/module) for deep imports"
|
|
1945
|
+
})
|
|
1946
|
+
);
|
|
1887
1947
|
}
|
|
1888
1948
|
}
|
|
1889
1949
|
}
|
|
@@ -1892,16 +1952,18 @@ var ImportsVerifier = class {
|
|
|
1892
1952
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1893
1953
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1894
1954
|
if (moduleSpec.includes(currentFilename.split("/").pop() || "")) {
|
|
1895
|
-
violations.push(
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1955
|
+
violations.push(
|
|
1956
|
+
createViolation({
|
|
1957
|
+
decisionId,
|
|
1958
|
+
constraintId: constraint.id,
|
|
1959
|
+
type: constraint.type,
|
|
1960
|
+
severity: constraint.severity,
|
|
1961
|
+
message: `Possible circular import detected: "${moduleSpec}"`,
|
|
1962
|
+
file: filePath,
|
|
1963
|
+
line: importDecl.getStartLineNumber(),
|
|
1964
|
+
suggestion: "Review import structure for circular dependencies"
|
|
1965
|
+
})
|
|
1966
|
+
);
|
|
1905
1967
|
}
|
|
1906
1968
|
}
|
|
1907
1969
|
}
|
|
@@ -1909,16 +1971,18 @@ var ImportsVerifier = class {
|
|
|
1909
1971
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1910
1972
|
const namespaceImport = importDecl.getNamespaceImport();
|
|
1911
1973
|
if (namespaceImport) {
|
|
1912
|
-
violations.push(
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1974
|
+
violations.push(
|
|
1975
|
+
createViolation({
|
|
1976
|
+
decisionId,
|
|
1977
|
+
constraintId: constraint.id,
|
|
1978
|
+
type: constraint.type,
|
|
1979
|
+
severity: constraint.severity,
|
|
1980
|
+
message: `Namespace import "* as ${namespaceImport.getText()}" should use named imports`,
|
|
1981
|
+
file: filePath,
|
|
1982
|
+
line: importDecl.getStartLineNumber(),
|
|
1983
|
+
suggestion: "Use specific named imports instead of namespace import"
|
|
1984
|
+
})
|
|
1985
|
+
);
|
|
1922
1986
|
}
|
|
1923
1987
|
}
|
|
1924
1988
|
}
|
|
@@ -1944,16 +2008,18 @@ var ErrorsVerifier = class {
|
|
|
1944
2008
|
if (!className?.endsWith("Error") && !className?.endsWith("Exception")) continue;
|
|
1945
2009
|
const extendsClause = classDecl.getExtends();
|
|
1946
2010
|
if (!extendsClause) {
|
|
1947
|
-
violations.push(
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
2011
|
+
violations.push(
|
|
2012
|
+
createViolation({
|
|
2013
|
+
decisionId,
|
|
2014
|
+
constraintId: constraint.id,
|
|
2015
|
+
type: constraint.type,
|
|
2016
|
+
severity: constraint.severity,
|
|
2017
|
+
message: `Error class "${className}" does not extend any base class`,
|
|
2018
|
+
file: filePath,
|
|
2019
|
+
line: classDecl.getStartLineNumber(),
|
|
2020
|
+
suggestion: requiredBase ? `Extend ${requiredBase}` : "Extend a base error class for consistent error handling"
|
|
2021
|
+
})
|
|
2022
|
+
);
|
|
1957
2023
|
} else if (requiredBase) {
|
|
1958
2024
|
const baseName = extendsClause.getText();
|
|
1959
2025
|
if (baseName !== requiredBase && baseName !== "Error") {
|
|
@@ -1968,16 +2034,18 @@ var ErrorsVerifier = class {
|
|
|
1968
2034
|
if (expression) {
|
|
1969
2035
|
const text = expression.getText();
|
|
1970
2036
|
if (text.startsWith("new Error(")) {
|
|
1971
|
-
violations.push(
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
2037
|
+
violations.push(
|
|
2038
|
+
createViolation({
|
|
2039
|
+
decisionId,
|
|
2040
|
+
constraintId: constraint.id,
|
|
2041
|
+
type: constraint.type,
|
|
2042
|
+
severity: constraint.severity,
|
|
2043
|
+
message: "Throwing generic Error instead of custom error class",
|
|
2044
|
+
file: filePath,
|
|
2045
|
+
line: node.getStartLineNumber(),
|
|
2046
|
+
suggestion: "Use a custom error class for better error handling"
|
|
2047
|
+
})
|
|
2048
|
+
);
|
|
1981
2049
|
}
|
|
1982
2050
|
}
|
|
1983
2051
|
}
|
|
@@ -1991,16 +2059,18 @@ var ErrorsVerifier = class {
|
|
|
1991
2059
|
const block = catchClause.getBlock();
|
|
1992
2060
|
const statements = block.getStatements();
|
|
1993
2061
|
if (statements.length === 0) {
|
|
1994
|
-
violations.push(
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2062
|
+
violations.push(
|
|
2063
|
+
createViolation({
|
|
2064
|
+
decisionId,
|
|
2065
|
+
constraintId: constraint.id,
|
|
2066
|
+
type: constraint.type,
|
|
2067
|
+
severity: constraint.severity,
|
|
2068
|
+
message: "Empty catch block swallows error without handling",
|
|
2069
|
+
file: filePath,
|
|
2070
|
+
line: catchClause.getStartLineNumber(),
|
|
2071
|
+
suggestion: "Add error handling, logging, or rethrow the error"
|
|
2072
|
+
})
|
|
2073
|
+
);
|
|
2004
2074
|
}
|
|
2005
2075
|
}
|
|
2006
2076
|
}
|
|
@@ -2012,16 +2082,18 @@ var ErrorsVerifier = class {
|
|
|
2012
2082
|
const expression = node.getExpression();
|
|
2013
2083
|
const text = expression.getText();
|
|
2014
2084
|
if (text === "console.error" || text === "console.log") {
|
|
2015
|
-
violations.push(
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2085
|
+
violations.push(
|
|
2086
|
+
createViolation({
|
|
2087
|
+
decisionId,
|
|
2088
|
+
constraintId: constraint.id,
|
|
2089
|
+
type: constraint.type,
|
|
2090
|
+
severity: constraint.severity,
|
|
2091
|
+
message: `Using ${text} instead of proper logging`,
|
|
2092
|
+
file: filePath,
|
|
2093
|
+
line: node.getStartLineNumber(),
|
|
2094
|
+
suggestion: "Use a proper logging library"
|
|
2095
|
+
})
|
|
2096
|
+
);
|
|
2025
2097
|
}
|
|
2026
2098
|
}
|
|
2027
2099
|
});
|
|
@@ -2052,16 +2124,18 @@ var RegexVerifier = class {
|
|
|
2052
2124
|
while ((match = regex.exec(fileText)) !== null) {
|
|
2053
2125
|
const beforeMatch = fileText.substring(0, match.index);
|
|
2054
2126
|
const lineNumber = beforeMatch.split("\n").length;
|
|
2055
|
-
violations.push(
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2127
|
+
violations.push(
|
|
2128
|
+
createViolation({
|
|
2129
|
+
decisionId,
|
|
2130
|
+
constraintId: constraint.id,
|
|
2131
|
+
type: constraint.type,
|
|
2132
|
+
severity: constraint.severity,
|
|
2133
|
+
message: `Found forbidden pattern: "${match[0]}"`,
|
|
2134
|
+
file: filePath,
|
|
2135
|
+
line: lineNumber,
|
|
2136
|
+
suggestion: `Remove or replace the pattern matching /${patternToForbid}/`
|
|
2137
|
+
})
|
|
2138
|
+
);
|
|
2065
2139
|
}
|
|
2066
2140
|
} catch {
|
|
2067
2141
|
}
|
|
@@ -2071,15 +2145,17 @@ var RegexVerifier = class {
|
|
|
2071
2145
|
try {
|
|
2072
2146
|
const regex = new RegExp(patternToRequire);
|
|
2073
2147
|
if (!regex.test(fileText)) {
|
|
2074
|
-
violations.push(
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2148
|
+
violations.push(
|
|
2149
|
+
createViolation({
|
|
2150
|
+
decisionId,
|
|
2151
|
+
constraintId: constraint.id,
|
|
2152
|
+
type: constraint.type,
|
|
2153
|
+
severity: constraint.severity,
|
|
2154
|
+
message: `File does not contain required pattern: /${patternToRequire}/`,
|
|
2155
|
+
file: filePath,
|
|
2156
|
+
suggestion: `Add code matching /${patternToRequire}/`
|
|
2157
|
+
})
|
|
2158
|
+
);
|
|
2083
2159
|
}
|
|
2084
2160
|
} catch {
|
|
2085
2161
|
}
|
|
@@ -2218,7 +2294,9 @@ function parseBannedDependency(rule) {
|
|
|
2218
2294
|
return value.length > 0 ? value : null;
|
|
2219
2295
|
}
|
|
2220
2296
|
function parseLayerRule(rule) {
|
|
2221
|
-
const m = rule.match(
|
|
2297
|
+
const m = rule.match(
|
|
2298
|
+
/(\w+)\s{1,5}layer\s{1,5}cannot\s{1,5}depend\s{1,5}on\s{1,5}(\w+)\s{1,5}layer/i
|
|
2299
|
+
);
|
|
2222
2300
|
const fromLayer = m?.[1]?.toLowerCase();
|
|
2223
2301
|
const toLayer = m?.[2]?.toLowerCase();
|
|
2224
2302
|
if (!fromLayer || !toLayer) return null;
|
|
@@ -2244,22 +2322,25 @@ var DependencyVerifier = class {
|
|
|
2244
2322
|
const sccs = tarjanScc(graph);
|
|
2245
2323
|
const current = projectFilePath;
|
|
2246
2324
|
for (const scc of sccs) {
|
|
2247
|
-
const
|
|
2325
|
+
const first = scc[0];
|
|
2326
|
+
const hasSelfLoop = first !== void 0 && scc.length === 1 && (graph.get(first)?.has(first) ?? false);
|
|
2248
2327
|
const isCycle = scc.length > 1 || hasSelfLoop;
|
|
2249
2328
|
if (!isCycle) continue;
|
|
2250
2329
|
if (!scc.includes(current)) continue;
|
|
2251
2330
|
const sorted = [...scc].sort();
|
|
2252
2331
|
if (sorted[0] !== current) continue;
|
|
2253
|
-
violations.push(
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2332
|
+
violations.push(
|
|
2333
|
+
createViolation({
|
|
2334
|
+
decisionId,
|
|
2335
|
+
constraintId: constraint.id,
|
|
2336
|
+
type: constraint.type,
|
|
2337
|
+
severity: constraint.severity,
|
|
2338
|
+
message: `Circular dependency detected across: ${sorted.join(" -> ")}`,
|
|
2339
|
+
file: filePath,
|
|
2340
|
+
line: 1,
|
|
2341
|
+
suggestion: "Break the cycle by extracting shared abstractions or reversing the dependency"
|
|
2342
|
+
})
|
|
2343
|
+
);
|
|
2263
2344
|
}
|
|
2264
2345
|
}
|
|
2265
2346
|
const layerRule = parseLayerRule(rule);
|
|
@@ -2269,16 +2350,18 @@ var DependencyVerifier = class {
|
|
|
2269
2350
|
const resolved = resolveToSourceFilePath(project, projectFilePath, moduleSpec);
|
|
2270
2351
|
if (!resolved) continue;
|
|
2271
2352
|
if (fileInLayer(resolved, layerRule.toLayer)) {
|
|
2272
|
-
violations.push(
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2353
|
+
violations.push(
|
|
2354
|
+
createViolation({
|
|
2355
|
+
decisionId,
|
|
2356
|
+
constraintId: constraint.id,
|
|
2357
|
+
type: constraint.type,
|
|
2358
|
+
severity: constraint.severity,
|
|
2359
|
+
message: `Layer violation: ${layerRule.fromLayer} depends on ${layerRule.toLayer} via import "${moduleSpec}"`,
|
|
2360
|
+
file: filePath,
|
|
2361
|
+
line: importDecl.getStartLineNumber(),
|
|
2362
|
+
suggestion: `Refactor to remove dependency from ${layerRule.fromLayer} to ${layerRule.toLayer}`
|
|
2363
|
+
})
|
|
2364
|
+
);
|
|
2282
2365
|
}
|
|
2283
2366
|
}
|
|
2284
2367
|
}
|
|
@@ -2288,16 +2371,18 @@ var DependencyVerifier = class {
|
|
|
2288
2371
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2289
2372
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
2290
2373
|
if (moduleSpec.toLowerCase().includes(bannedLower)) {
|
|
2291
|
-
violations.push(
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2374
|
+
violations.push(
|
|
2375
|
+
createViolation({
|
|
2376
|
+
decisionId,
|
|
2377
|
+
constraintId: constraint.id,
|
|
2378
|
+
type: constraint.type,
|
|
2379
|
+
severity: constraint.severity,
|
|
2380
|
+
message: `Banned dependency import detected: "${moduleSpec}"`,
|
|
2381
|
+
file: filePath,
|
|
2382
|
+
line: importDecl.getStartLineNumber(),
|
|
2383
|
+
suggestion: `Remove or replace dependency "${banned}"`
|
|
2384
|
+
})
|
|
2385
|
+
);
|
|
2301
2386
|
}
|
|
2302
2387
|
}
|
|
2303
2388
|
}
|
|
@@ -2308,16 +2393,18 @@ var DependencyVerifier = class {
|
|
|
2308
2393
|
if (!moduleSpec.startsWith(".")) continue;
|
|
2309
2394
|
const depth = (moduleSpec.match(/\.\.\//g) || []).length;
|
|
2310
2395
|
if (depth > maxDepth) {
|
|
2311
|
-
violations.push(
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2396
|
+
violations.push(
|
|
2397
|
+
createViolation({
|
|
2398
|
+
decisionId,
|
|
2399
|
+
constraintId: constraint.id,
|
|
2400
|
+
type: constraint.type,
|
|
2401
|
+
severity: constraint.severity,
|
|
2402
|
+
message: `Import depth ${depth} exceeds maximum ${maxDepth}: "${moduleSpec}"`,
|
|
2403
|
+
file: filePath,
|
|
2404
|
+
line: importDecl.getStartLineNumber(),
|
|
2405
|
+
suggestion: "Use a shallower module boundary (or introduce a public entrypoint for this dependency)"
|
|
2406
|
+
})
|
|
2407
|
+
);
|
|
2321
2408
|
}
|
|
2322
2409
|
}
|
|
2323
2410
|
}
|
|
@@ -2326,7 +2413,9 @@ var DependencyVerifier = class {
|
|
|
2326
2413
|
};
|
|
2327
2414
|
|
|
2328
2415
|
// src/verification/verifiers/complexity.ts
|
|
2329
|
-
import {
|
|
2416
|
+
import {
|
|
2417
|
+
Node as Node4
|
|
2418
|
+
} from "ts-morph";
|
|
2330
2419
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2331
2420
|
function parseLimit(rule, pattern) {
|
|
2332
2421
|
const m = rule.match(pattern);
|
|
@@ -2410,16 +2499,18 @@ var ComplexityVerifier = class {
|
|
|
2410
2499
|
if (maxLines !== null) {
|
|
2411
2500
|
const lineCount = getFileLineCount(sourceFile.getFullText());
|
|
2412
2501
|
if (lineCount > maxLines) {
|
|
2413
|
-
violations.push(
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2502
|
+
violations.push(
|
|
2503
|
+
createViolation({
|
|
2504
|
+
decisionId,
|
|
2505
|
+
constraintId: constraint.id,
|
|
2506
|
+
type: constraint.type,
|
|
2507
|
+
severity: constraint.severity,
|
|
2508
|
+
message: `File has ${lineCount} lines which exceeds maximum ${maxLines}`,
|
|
2509
|
+
file: filePath,
|
|
2510
|
+
line: 1,
|
|
2511
|
+
suggestion: "Split the file into smaller modules"
|
|
2512
|
+
})
|
|
2513
|
+
);
|
|
2423
2514
|
}
|
|
2424
2515
|
}
|
|
2425
2516
|
const functionLikes = [
|
|
@@ -2433,46 +2524,52 @@ var ComplexityVerifier = class {
|
|
|
2433
2524
|
if (maxComplexity !== null) {
|
|
2434
2525
|
const complexity = calculateCyclomaticComplexity(fn);
|
|
2435
2526
|
if (complexity > maxComplexity) {
|
|
2436
|
-
violations.push(
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2527
|
+
violations.push(
|
|
2528
|
+
createViolation({
|
|
2529
|
+
decisionId,
|
|
2530
|
+
constraintId: constraint.id,
|
|
2531
|
+
type: constraint.type,
|
|
2532
|
+
severity: constraint.severity,
|
|
2533
|
+
message: `Function ${fnName} has cyclomatic complexity ${complexity} which exceeds maximum ${maxComplexity}`,
|
|
2534
|
+
file: filePath,
|
|
2535
|
+
line: fn.getStartLineNumber(),
|
|
2536
|
+
suggestion: "Refactor to reduce branching or extract smaller functions"
|
|
2537
|
+
})
|
|
2538
|
+
);
|
|
2446
2539
|
}
|
|
2447
2540
|
}
|
|
2448
2541
|
if (maxParams !== null) {
|
|
2449
2542
|
const paramCount = fn.getParameters().length;
|
|
2450
2543
|
if (paramCount > maxParams) {
|
|
2451
|
-
violations.push(
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2544
|
+
violations.push(
|
|
2545
|
+
createViolation({
|
|
2546
|
+
decisionId,
|
|
2547
|
+
constraintId: constraint.id,
|
|
2548
|
+
type: constraint.type,
|
|
2549
|
+
severity: constraint.severity,
|
|
2550
|
+
message: `Function ${fnName} has ${paramCount} parameters which exceeds maximum ${maxParams}`,
|
|
2551
|
+
file: filePath,
|
|
2552
|
+
line: fn.getStartLineNumber(),
|
|
2553
|
+
suggestion: "Consider grouping parameters into an options object"
|
|
2554
|
+
})
|
|
2555
|
+
);
|
|
2461
2556
|
}
|
|
2462
2557
|
}
|
|
2463
2558
|
if (maxNesting !== null) {
|
|
2464
2559
|
const depth = maxNestingDepth(fn);
|
|
2465
2560
|
if (depth > maxNesting) {
|
|
2466
|
-
violations.push(
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2561
|
+
violations.push(
|
|
2562
|
+
createViolation({
|
|
2563
|
+
decisionId,
|
|
2564
|
+
constraintId: constraint.id,
|
|
2565
|
+
type: constraint.type,
|
|
2566
|
+
severity: constraint.severity,
|
|
2567
|
+
message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
|
|
2568
|
+
file: filePath,
|
|
2569
|
+
line: fn.getStartLineNumber(),
|
|
2570
|
+
suggestion: "Reduce nesting by using early returns or extracting functions"
|
|
2571
|
+
})
|
|
2572
|
+
);
|
|
2476
2573
|
}
|
|
2477
2574
|
}
|
|
2478
2575
|
}
|
|
@@ -2508,48 +2605,54 @@ var SecurityVerifier = class {
|
|
|
2508
2605
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
2509
2606
|
const value = init.getText().slice(1, -1);
|
|
2510
2607
|
if (value.length === 0) continue;
|
|
2511
|
-
violations.push(
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2608
|
+
violations.push(
|
|
2609
|
+
createViolation({
|
|
2610
|
+
decisionId,
|
|
2611
|
+
constraintId: constraint.id,
|
|
2612
|
+
type: constraint.type,
|
|
2613
|
+
severity: constraint.severity,
|
|
2614
|
+
message: `Possible hardcoded secret in variable "${name}"`,
|
|
2615
|
+
file: filePath,
|
|
2616
|
+
line: vd.getStartLineNumber(),
|
|
2617
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2618
|
+
})
|
|
2619
|
+
);
|
|
2521
2620
|
}
|
|
2522
2621
|
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2523
2622
|
const propName = pa.getNameNode().getText();
|
|
2524
2623
|
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2525
2624
|
const init = pa.getInitializer();
|
|
2526
2625
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
2527
|
-
violations.push(
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2626
|
+
violations.push(
|
|
2627
|
+
createViolation({
|
|
2628
|
+
decisionId,
|
|
2629
|
+
constraintId: constraint.id,
|
|
2630
|
+
type: constraint.type,
|
|
2631
|
+
severity: constraint.severity,
|
|
2632
|
+
message: `Possible hardcoded secret in object property ${propName}`,
|
|
2633
|
+
file: filePath,
|
|
2634
|
+
line: pa.getStartLineNumber(),
|
|
2635
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2636
|
+
})
|
|
2637
|
+
);
|
|
2537
2638
|
}
|
|
2538
2639
|
}
|
|
2539
2640
|
if (checkEval) {
|
|
2540
2641
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2541
2642
|
const exprText = call.getExpression().getText();
|
|
2542
2643
|
if (exprText === "eval" || exprText === "Function") {
|
|
2543
|
-
violations.push(
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2644
|
+
violations.push(
|
|
2645
|
+
createViolation({
|
|
2646
|
+
decisionId,
|
|
2647
|
+
constraintId: constraint.id,
|
|
2648
|
+
type: constraint.type,
|
|
2649
|
+
severity: constraint.severity,
|
|
2650
|
+
message: `Unsafe dynamic code execution via ${exprText}()`,
|
|
2651
|
+
file: filePath,
|
|
2652
|
+
line: call.getStartLineNumber(),
|
|
2653
|
+
suggestion: "Avoid eval/Function; use safer alternatives"
|
|
2654
|
+
})
|
|
2655
|
+
);
|
|
2553
2656
|
}
|
|
2554
2657
|
}
|
|
2555
2658
|
}
|
|
@@ -2559,29 +2662,33 @@ var SecurityVerifier = class {
|
|
|
2559
2662
|
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2560
2663
|
if (!propertyAccess) continue;
|
|
2561
2664
|
if (propertyAccess.getName() === "innerHTML") {
|
|
2562
|
-
violations.push(
|
|
2665
|
+
violations.push(
|
|
2666
|
+
createViolation({
|
|
2667
|
+
decisionId,
|
|
2668
|
+
constraintId: constraint.id,
|
|
2669
|
+
type: constraint.type,
|
|
2670
|
+
severity: constraint.severity,
|
|
2671
|
+
message: "Potential XSS: assignment to innerHTML",
|
|
2672
|
+
file: filePath,
|
|
2673
|
+
line: bin.getStartLineNumber(),
|
|
2674
|
+
suggestion: "Prefer textContent or a safe templating/escaping strategy"
|
|
2675
|
+
})
|
|
2676
|
+
);
|
|
2677
|
+
}
|
|
2678
|
+
}
|
|
2679
|
+
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2680
|
+
violations.push(
|
|
2681
|
+
createViolation({
|
|
2563
2682
|
decisionId,
|
|
2564
2683
|
constraintId: constraint.id,
|
|
2565
2684
|
type: constraint.type,
|
|
2566
2685
|
severity: constraint.severity,
|
|
2567
|
-
message: "Potential XSS:
|
|
2686
|
+
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2568
2687
|
file: filePath,
|
|
2569
|
-
line:
|
|
2570
|
-
suggestion: "
|
|
2571
|
-
})
|
|
2572
|
-
|
|
2573
|
-
}
|
|
2574
|
-
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2575
|
-
violations.push(createViolation({
|
|
2576
|
-
decisionId,
|
|
2577
|
-
constraintId: constraint.id,
|
|
2578
|
-
type: constraint.type,
|
|
2579
|
-
severity: constraint.severity,
|
|
2580
|
-
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2581
|
-
file: filePath,
|
|
2582
|
-
line: 1,
|
|
2583
|
-
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2584
|
-
}));
|
|
2688
|
+
line: 1,
|
|
2689
|
+
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2690
|
+
})
|
|
2691
|
+
);
|
|
2585
2692
|
}
|
|
2586
2693
|
}
|
|
2587
2694
|
if (checkSql) {
|
|
@@ -2600,31 +2707,35 @@ var SecurityVerifier = class {
|
|
|
2600
2707
|
if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
|
|
2601
2708
|
continue;
|
|
2602
2709
|
}
|
|
2603
|
-
violations.push(
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2710
|
+
violations.push(
|
|
2711
|
+
createViolation({
|
|
2712
|
+
decisionId,
|
|
2713
|
+
constraintId: constraint.id,
|
|
2714
|
+
type: constraint.type,
|
|
2715
|
+
severity: constraint.severity,
|
|
2716
|
+
message: "Potential SQL injection: dynamically constructed SQL query",
|
|
2717
|
+
file: filePath,
|
|
2718
|
+
line: call.getStartLineNumber(),
|
|
2719
|
+
suggestion: "Use parameterized queries / prepared statements"
|
|
2720
|
+
})
|
|
2721
|
+
);
|
|
2613
2722
|
}
|
|
2614
2723
|
}
|
|
2615
2724
|
if (checkProto) {
|
|
2616
2725
|
const text = sourceFile.getFullText();
|
|
2617
2726
|
if (text.includes("__proto__") || text.includes("constructor.prototype")) {
|
|
2618
|
-
violations.push(
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2727
|
+
violations.push(
|
|
2728
|
+
createViolation({
|
|
2729
|
+
decisionId,
|
|
2730
|
+
constraintId: constraint.id,
|
|
2731
|
+
type: constraint.type,
|
|
2732
|
+
severity: constraint.severity,
|
|
2733
|
+
message: "Potential prototype pollution pattern detected",
|
|
2734
|
+
file: filePath,
|
|
2735
|
+
line: 1,
|
|
2736
|
+
suggestion: "Avoid writing to __proto__/prototype; validate object keys"
|
|
2737
|
+
})
|
|
2738
|
+
);
|
|
2628
2739
|
}
|
|
2629
2740
|
}
|
|
2630
2741
|
return violations;
|
|
@@ -2664,16 +2775,18 @@ var ApiVerifier = class {
|
|
|
2664
2775
|
const pathValue = stringLiteral.getLiteralValue();
|
|
2665
2776
|
if (typeof pathValue !== "string") continue;
|
|
2666
2777
|
if (!isKebabPath(pathValue)) {
|
|
2667
|
-
violations.push(
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2778
|
+
violations.push(
|
|
2779
|
+
createViolation({
|
|
2780
|
+
decisionId,
|
|
2781
|
+
constraintId: constraint.id,
|
|
2782
|
+
type: constraint.type,
|
|
2783
|
+
severity: constraint.severity,
|
|
2784
|
+
message: `Endpoint path "${pathValue}" is not kebab-case`,
|
|
2785
|
+
file: filePath,
|
|
2786
|
+
line: call.getStartLineNumber(),
|
|
2787
|
+
suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
|
|
2788
|
+
})
|
|
2789
|
+
);
|
|
2677
2790
|
}
|
|
2678
2791
|
}
|
|
2679
2792
|
return violations;
|
|
@@ -2685,10 +2798,36 @@ import { existsSync } from "fs";
|
|
|
2685
2798
|
import { join as join5 } from "path";
|
|
2686
2799
|
import { pathToFileURL } from "url";
|
|
2687
2800
|
import fg2 from "fast-glob";
|
|
2801
|
+
|
|
2802
|
+
// src/utils/logger.ts
|
|
2803
|
+
import pino from "pino";
|
|
2804
|
+
var defaultOptions = {
|
|
2805
|
+
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
2806
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2807
|
+
base: {
|
|
2808
|
+
service: "specbridge"
|
|
2809
|
+
}
|
|
2810
|
+
};
|
|
2811
|
+
var destination = pino.destination({
|
|
2812
|
+
fd: 2,
|
|
2813
|
+
// stderr
|
|
2814
|
+
sync: false
|
|
2815
|
+
});
|
|
2816
|
+
var rootLogger = pino(defaultOptions, destination);
|
|
2817
|
+
function getLogger(bindings) {
|
|
2818
|
+
if (!bindings) {
|
|
2819
|
+
return rootLogger;
|
|
2820
|
+
}
|
|
2821
|
+
return rootLogger.child(bindings);
|
|
2822
|
+
}
|
|
2823
|
+
var logger = getLogger();
|
|
2824
|
+
|
|
2825
|
+
// src/verification/plugins/loader.ts
|
|
2688
2826
|
var PluginLoader = class {
|
|
2689
2827
|
plugins = /* @__PURE__ */ new Map();
|
|
2690
2828
|
loaded = false;
|
|
2691
2829
|
loadErrors = [];
|
|
2830
|
+
logger = getLogger({ module: "verification.plugins.loader" });
|
|
2692
2831
|
/**
|
|
2693
2832
|
* Load all plugins from the specified base path
|
|
2694
2833
|
*
|
|
@@ -2711,15 +2850,15 @@ var PluginLoader = class {
|
|
|
2711
2850
|
} catch (error) {
|
|
2712
2851
|
const message = error instanceof Error ? error.message : String(error);
|
|
2713
2852
|
this.loadErrors.push({ file, error: message });
|
|
2714
|
-
|
|
2853
|
+
this.logger.warn({ file, error: message }, "Failed to load plugin");
|
|
2715
2854
|
}
|
|
2716
2855
|
}
|
|
2717
2856
|
this.loaded = true;
|
|
2718
2857
|
if (this.plugins.size > 0) {
|
|
2719
|
-
|
|
2858
|
+
this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
|
|
2720
2859
|
}
|
|
2721
2860
|
if (this.loadErrors.length > 0) {
|
|
2722
|
-
|
|
2861
|
+
this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
|
|
2723
2862
|
}
|
|
2724
2863
|
}
|
|
2725
2864
|
/**
|
|
@@ -2828,7 +2967,10 @@ var PluginLoader = class {
|
|
|
2828
2967
|
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
2829
2968
|
}
|
|
2830
2969
|
if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
|
|
2831
|
-
return {
|
|
2970
|
+
return {
|
|
2971
|
+
success: false,
|
|
2972
|
+
error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
|
|
2973
|
+
};
|
|
2832
2974
|
}
|
|
2833
2975
|
const schema = plugin.paramsSchema;
|
|
2834
2976
|
if (schema.safeParse) {
|
|
@@ -2895,8 +3037,9 @@ var builtinVerifiers = {
|
|
|
2895
3037
|
};
|
|
2896
3038
|
var verifierInstances = /* @__PURE__ */ new Map();
|
|
2897
3039
|
function getVerifier(id) {
|
|
2898
|
-
|
|
2899
|
-
|
|
3040
|
+
const pooled = verifierInstances.get(id);
|
|
3041
|
+
if (pooled) {
|
|
3042
|
+
return pooled;
|
|
2900
3043
|
}
|
|
2901
3044
|
const pluginLoader2 = getPluginLoader();
|
|
2902
3045
|
const customVerifier = pluginLoader2.getVerifier(id);
|
|
@@ -3098,6 +3241,7 @@ var VerificationEngine = class {
|
|
|
3098
3241
|
astCache;
|
|
3099
3242
|
resultsCache;
|
|
3100
3243
|
pluginsLoaded = false;
|
|
3244
|
+
logger = getLogger({ module: "verification.engine" });
|
|
3101
3245
|
constructor(registry) {
|
|
3102
3246
|
this.registry = registry || createRegistry();
|
|
3103
3247
|
this.project = new Project2({
|
|
@@ -3271,12 +3415,14 @@ var VerificationEngine = class {
|
|
|
3271
3415
|
);
|
|
3272
3416
|
if (!verifier) {
|
|
3273
3417
|
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3418
|
+
this.logger.warn(
|
|
3419
|
+
{
|
|
3420
|
+
decisionId: decision.metadata.id,
|
|
3421
|
+
constraintId: constraint.id,
|
|
3422
|
+
requestedVerifier,
|
|
3423
|
+
availableVerifiers: getVerifierIds()
|
|
3424
|
+
},
|
|
3425
|
+
"No verifier found for constraint"
|
|
3280
3426
|
);
|
|
3281
3427
|
warnings.push({
|
|
3282
3428
|
type: "missing_verifier",
|
|
@@ -3323,7 +3469,10 @@ var VerificationEngine = class {
|
|
|
3323
3469
|
}
|
|
3324
3470
|
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3325
3471
|
const pluginLoader2 = getPluginLoader();
|
|
3326
|
-
const validationResult = pluginLoader2.validateParams(
|
|
3472
|
+
const validationResult = pluginLoader2.validateParams(
|
|
3473
|
+
constraint.check.verifier,
|
|
3474
|
+
constraint.check.params
|
|
3475
|
+
);
|
|
3327
3476
|
if (!validationResult.success) {
|
|
3328
3477
|
warnings.push({
|
|
3329
3478
|
type: "invalid_params",
|
|
@@ -3383,17 +3532,17 @@ var VerificationEngine = class {
|
|
|
3383
3532
|
} catch (error) {
|
|
3384
3533
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3385
3534
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3535
|
+
this.logger.error(
|
|
3536
|
+
{
|
|
3537
|
+
verifierId: verifier.id,
|
|
3538
|
+
filePath,
|
|
3539
|
+
decisionId: decision.metadata.id,
|
|
3540
|
+
constraintId: constraint.id,
|
|
3541
|
+
error: errorMessage,
|
|
3542
|
+
stack: errorStack
|
|
3543
|
+
},
|
|
3544
|
+
"Verifier execution failed"
|
|
3393
3545
|
);
|
|
3394
|
-
if (errorStack) {
|
|
3395
|
-
console.error(chalk3.dim(errorStack));
|
|
3396
|
-
}
|
|
3397
3546
|
errors.push({
|
|
3398
3547
|
type: "verifier_exception",
|
|
3399
3548
|
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
@@ -3509,8 +3658,14 @@ var AutofixEngine = class {
|
|
|
3509
3658
|
const edits = [];
|
|
3510
3659
|
for (const violation of fileViolations) {
|
|
3511
3660
|
const fix = violation.autofix;
|
|
3661
|
+
if (!fix) {
|
|
3662
|
+
skippedViolations++;
|
|
3663
|
+
continue;
|
|
3664
|
+
}
|
|
3512
3665
|
if (options.interactive) {
|
|
3513
|
-
const ok = await confirmFix(
|
|
3666
|
+
const ok = await confirmFix(
|
|
3667
|
+
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3668
|
+
);
|
|
3514
3669
|
if (!ok) {
|
|
3515
3670
|
skippedViolations++;
|
|
3516
3671
|
continue;
|
|
@@ -3541,7 +3696,11 @@ import { resolve } from "path";
|
|
|
3541
3696
|
var execFileAsync = promisify(execFile);
|
|
3542
3697
|
async function getChangedFiles(cwd) {
|
|
3543
3698
|
try {
|
|
3544
|
-
const { stdout: stdout2 } = await execFileAsync(
|
|
3699
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
3700
|
+
"git",
|
|
3701
|
+
["diff", "--name-only", "--diff-filter=AM", "HEAD"],
|
|
3702
|
+
{ cwd }
|
|
3703
|
+
);
|
|
3545
3704
|
const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3546
3705
|
const abs = [];
|
|
3547
3706
|
for (const file of rel) {
|
|
@@ -3555,7 +3714,7 @@ async function getChangedFiles(cwd) {
|
|
|
3555
3714
|
}
|
|
3556
3715
|
|
|
3557
3716
|
// src/verification/explain.ts
|
|
3558
|
-
import
|
|
3717
|
+
import chalk3 from "chalk";
|
|
3559
3718
|
var ExplainReporter = class {
|
|
3560
3719
|
entries = [];
|
|
3561
3720
|
/**
|
|
@@ -3569,10 +3728,10 @@ var ExplainReporter = class {
|
|
|
3569
3728
|
*/
|
|
3570
3729
|
print() {
|
|
3571
3730
|
if (this.entries.length === 0) {
|
|
3572
|
-
console.log(
|
|
3731
|
+
console.log(chalk3.dim("No constraints were evaluated."));
|
|
3573
3732
|
return;
|
|
3574
3733
|
}
|
|
3575
|
-
console.log(
|
|
3734
|
+
console.log(chalk3.bold("\n=== Verification Explanation ===\n"));
|
|
3576
3735
|
const byFile = /* @__PURE__ */ new Map();
|
|
3577
3736
|
for (const entry of this.entries) {
|
|
3578
3737
|
const existing = byFile.get(entry.file) || [];
|
|
@@ -3580,22 +3739,22 @@ var ExplainReporter = class {
|
|
|
3580
3739
|
byFile.set(entry.file, existing);
|
|
3581
3740
|
}
|
|
3582
3741
|
for (const [file, entries] of byFile) {
|
|
3583
|
-
console.log(
|
|
3742
|
+
console.log(chalk3.underline(file));
|
|
3584
3743
|
for (const entry of entries) {
|
|
3585
|
-
const icon = entry.applied ?
|
|
3744
|
+
const icon = entry.applied ? chalk3.green("\u2713") : chalk3.dim("\u2298");
|
|
3586
3745
|
const constraintId = `${entry.decision.metadata.id}/${entry.constraint.id}`;
|
|
3587
3746
|
console.log(` ${icon} ${constraintId}`);
|
|
3588
|
-
console.log(
|
|
3747
|
+
console.log(chalk3.dim(` ${entry.reason}`));
|
|
3589
3748
|
if (entry.applied && entry.selectedVerifier) {
|
|
3590
|
-
console.log(
|
|
3749
|
+
console.log(chalk3.dim(` Verifier: ${entry.selectedVerifier}`));
|
|
3591
3750
|
if (entry.verifierOutput) {
|
|
3592
3751
|
if (entry.verifierOutput.error) {
|
|
3593
|
-
console.log(
|
|
3752
|
+
console.log(chalk3.red(` Error: ${entry.verifierOutput.error}`));
|
|
3594
3753
|
} else {
|
|
3595
3754
|
const violationText = entry.verifierOutput.violations === 1 ? "1 violation" : `${entry.verifierOutput.violations} violations`;
|
|
3596
|
-
const resultColor = entry.verifierOutput.violations > 0 ?
|
|
3755
|
+
const resultColor = entry.verifierOutput.violations > 0 ? chalk3.red : chalk3.green;
|
|
3597
3756
|
console.log(
|
|
3598
|
-
|
|
3757
|
+
chalk3.dim(` Result: `) + resultColor(violationText) + chalk3.dim(` in ${entry.verifierOutput.duration}ms`)
|
|
3599
3758
|
);
|
|
3600
3759
|
}
|
|
3601
3760
|
}
|
|
@@ -3605,9 +3764,9 @@ var ExplainReporter = class {
|
|
|
3605
3764
|
}
|
|
3606
3765
|
const applied = this.entries.filter((e) => e.applied).length;
|
|
3607
3766
|
const skipped = this.entries.filter((e) => !e.applied).length;
|
|
3608
|
-
console.log(
|
|
3609
|
-
console.log(` Constraints Applied: ${
|
|
3610
|
-
console.log(` Constraints Skipped: ${
|
|
3767
|
+
console.log(chalk3.bold("Summary:"));
|
|
3768
|
+
console.log(` Constraints Applied: ${chalk3.green(applied)}`);
|
|
3769
|
+
console.log(` Constraints Skipped: ${chalk3.dim(skipped)}`);
|
|
3611
3770
|
}
|
|
3612
3771
|
/**
|
|
3613
3772
|
* Get all entries
|
|
@@ -3624,7 +3783,10 @@ var ExplainReporter = class {
|
|
|
3624
3783
|
};
|
|
3625
3784
|
|
|
3626
3785
|
// src/cli/commands/verify.ts
|
|
3627
|
-
var verifyCommand = new Command3("verify").description("Verify code compliance against decisions").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("-f, --files <patterns>", "Comma-separated file patterns to check").option("-d, --decisions <ids>", "Comma-separated decision IDs to check").option(
|
|
3786
|
+
var verifyCommand = new Command3("verify").description("Verify code compliance against decisions").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("-f, --files <patterns>", "Comma-separated file patterns to check").option("-d, --decisions <ids>", "Comma-separated decision IDs to check").option(
|
|
3787
|
+
"-s, --severity <levels>",
|
|
3788
|
+
"Comma-separated severity levels (critical, high, medium, low)"
|
|
3789
|
+
).option("--json", "Output as JSON").option("--incremental", "Only verify changed files (git diff --name-only --diff-filter=AM HEAD)").option("--explain", "Show detailed explanation of verification process").option("--fix", "Apply auto-fixes for supported violations").option("--dry-run", "Show what would be fixed without applying (requires --fix)").option("--interactive", "Confirm each fix interactively (requires --fix)").action(async (options) => {
|
|
3628
3790
|
const cwd = process.cwd();
|
|
3629
3791
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
3630
3792
|
throw new NotInitializedError();
|
|
@@ -3657,7 +3819,7 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3657
3819
|
if (fixableCount === 0) {
|
|
3658
3820
|
spinner.stop();
|
|
3659
3821
|
if (!options.json) {
|
|
3660
|
-
console.log(
|
|
3822
|
+
console.log(chalk4.yellow("No auto-fixable violations found"));
|
|
3661
3823
|
}
|
|
3662
3824
|
} else {
|
|
3663
3825
|
spinner.text = `Applying ${fixableCount} auto-fix(es)...`;
|
|
@@ -3682,34 +3844,34 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3682
3844
|
console.log(JSON.stringify({ ...result, autofix: fixResult }, null, 2));
|
|
3683
3845
|
} else {
|
|
3684
3846
|
if (result.warnings && result.warnings.length > 0) {
|
|
3685
|
-
console.log(
|
|
3847
|
+
console.log(chalk4.yellow.bold("\nWarnings:"));
|
|
3686
3848
|
for (const warning of result.warnings) {
|
|
3687
|
-
console.log(
|
|
3688
|
-
console.log(
|
|
3849
|
+
console.log(chalk4.yellow(` \u26A0 ${warning.message}`));
|
|
3850
|
+
console.log(chalk4.dim(` ${warning.decisionId}/${warning.constraintId}`));
|
|
3689
3851
|
if (warning.file) {
|
|
3690
|
-
console.log(
|
|
3852
|
+
console.log(chalk4.dim(` File: ${warning.file}`));
|
|
3691
3853
|
}
|
|
3692
3854
|
}
|
|
3693
3855
|
console.log("");
|
|
3694
3856
|
}
|
|
3695
3857
|
if (result.errors && result.errors.length > 0) {
|
|
3696
|
-
console.log(
|
|
3858
|
+
console.log(chalk4.red.bold("\nErrors:"));
|
|
3697
3859
|
for (const error of result.errors) {
|
|
3698
|
-
console.log(
|
|
3860
|
+
console.log(chalk4.red(` \u2717 ${error.message}`));
|
|
3699
3861
|
if (error.decisionId && error.constraintId) {
|
|
3700
|
-
console.log(
|
|
3862
|
+
console.log(chalk4.dim(` ${error.decisionId}/${error.constraintId}`));
|
|
3701
3863
|
}
|
|
3702
3864
|
if (error.file) {
|
|
3703
|
-
console.log(
|
|
3865
|
+
console.log(chalk4.dim(` File: ${error.file}`));
|
|
3704
3866
|
}
|
|
3705
3867
|
}
|
|
3706
3868
|
console.log("");
|
|
3707
3869
|
}
|
|
3708
3870
|
printResult(result, level);
|
|
3709
3871
|
if (options.fix && fixResult) {
|
|
3710
|
-
console.log(
|
|
3872
|
+
console.log(chalk4.green(`\u2713 Applied ${fixResult.applied.length} fix(es)`));
|
|
3711
3873
|
if (fixResult.skipped > 0) {
|
|
3712
|
-
console.log(
|
|
3874
|
+
console.log(chalk4.yellow(`\u2298 Skipped ${fixResult.skipped} fix(es)`));
|
|
3713
3875
|
}
|
|
3714
3876
|
console.log("");
|
|
3715
3877
|
}
|
|
@@ -3728,8 +3890,8 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3728
3890
|
function printResult(result, level) {
|
|
3729
3891
|
console.log("");
|
|
3730
3892
|
if (result.violations.length === 0) {
|
|
3731
|
-
console.log(
|
|
3732
|
-
console.log(
|
|
3893
|
+
console.log(chalk4.green("\u2713 All checks passed!"));
|
|
3894
|
+
console.log(chalk4.dim(` ${result.checked} files checked in ${result.duration}ms`));
|
|
3733
3895
|
return;
|
|
3734
3896
|
}
|
|
3735
3897
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3739,17 +3901,15 @@ function printResult(result, level) {
|
|
|
3739
3901
|
byFile.set(violation.file, existing);
|
|
3740
3902
|
}
|
|
3741
3903
|
for (const [file, violations] of byFile) {
|
|
3742
|
-
console.log(
|
|
3904
|
+
console.log(chalk4.underline(file));
|
|
3743
3905
|
for (const v of violations) {
|
|
3744
3906
|
const typeIcon = getTypeIcon(v.type);
|
|
3745
3907
|
const severityColor = getSeverityColor(v.severity);
|
|
3746
3908
|
const location = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
3747
|
-
console.log(
|
|
3748
|
-
|
|
3749
|
-
);
|
|
3750
|
-
console.log(chalk5.dim(` ${v.decisionId}/${v.constraintId}${location}`));
|
|
3909
|
+
console.log(` ${typeIcon} ${severityColor(`[${v.severity}]`)} ${v.message}`);
|
|
3910
|
+
console.log(chalk4.dim(` ${v.decisionId}/${v.constraintId}${location}`));
|
|
3751
3911
|
if (v.suggestion) {
|
|
3752
|
-
console.log(
|
|
3912
|
+
console.log(chalk4.cyan(` Suggestion: ${v.suggestion}`));
|
|
3753
3913
|
}
|
|
3754
3914
|
}
|
|
3755
3915
|
console.log("");
|
|
@@ -3758,29 +3918,31 @@ function printResult(result, level) {
|
|
|
3758
3918
|
const highCount = result.violations.filter((v) => v.severity === "high").length;
|
|
3759
3919
|
const mediumCount = result.violations.filter((v) => v.severity === "medium").length;
|
|
3760
3920
|
const lowCount = result.violations.filter((v) => v.severity === "low").length;
|
|
3761
|
-
console.log(
|
|
3762
|
-
console.log(
|
|
3921
|
+
console.log(chalk4.bold("Summary:"));
|
|
3922
|
+
console.log(
|
|
3923
|
+
` Files: ${result.checked} checked, ${result.passed} passed, ${result.failed} failed`
|
|
3924
|
+
);
|
|
3763
3925
|
const violationParts = [];
|
|
3764
|
-
if (criticalCount > 0) violationParts.push(
|
|
3765
|
-
if (highCount > 0) violationParts.push(
|
|
3766
|
-
if (mediumCount > 0) violationParts.push(
|
|
3767
|
-
if (lowCount > 0) violationParts.push(
|
|
3926
|
+
if (criticalCount > 0) violationParts.push(chalk4.red(`${criticalCount} critical`));
|
|
3927
|
+
if (highCount > 0) violationParts.push(chalk4.yellow(`${highCount} high`));
|
|
3928
|
+
if (mediumCount > 0) violationParts.push(chalk4.cyan(`${mediumCount} medium`));
|
|
3929
|
+
if (lowCount > 0) violationParts.push(chalk4.dim(`${lowCount} low`));
|
|
3768
3930
|
console.log(` Violations: ${violationParts.join(", ")}`);
|
|
3769
3931
|
console.log(` Duration: ${result.duration}ms`);
|
|
3770
3932
|
if (!result.success) {
|
|
3771
3933
|
console.log("");
|
|
3772
3934
|
const blockingTypes = level === "commit" ? "invariant or critical" : level === "pr" ? "invariant, critical, or high" : "invariant";
|
|
3773
|
-
console.log(
|
|
3935
|
+
console.log(chalk4.red(`\u2717 Verification failed. ${blockingTypes} violations must be resolved.`));
|
|
3774
3936
|
}
|
|
3775
3937
|
}
|
|
3776
3938
|
function getTypeIcon(type) {
|
|
3777
3939
|
switch (type) {
|
|
3778
3940
|
case "invariant":
|
|
3779
|
-
return
|
|
3941
|
+
return chalk4.red("\u25CF");
|
|
3780
3942
|
case "convention":
|
|
3781
|
-
return
|
|
3943
|
+
return chalk4.yellow("\u25CF");
|
|
3782
3944
|
case "guideline":
|
|
3783
|
-
return
|
|
3945
|
+
return chalk4.green("\u25CF");
|
|
3784
3946
|
default:
|
|
3785
3947
|
return "\u25CB";
|
|
3786
3948
|
}
|
|
@@ -3788,15 +3950,15 @@ function getTypeIcon(type) {
|
|
|
3788
3950
|
function getSeverityColor(severity) {
|
|
3789
3951
|
switch (severity) {
|
|
3790
3952
|
case "critical":
|
|
3791
|
-
return
|
|
3953
|
+
return chalk4.red;
|
|
3792
3954
|
case "high":
|
|
3793
|
-
return
|
|
3955
|
+
return chalk4.yellow;
|
|
3794
3956
|
case "medium":
|
|
3795
|
-
return
|
|
3957
|
+
return chalk4.cyan;
|
|
3796
3958
|
case "low":
|
|
3797
|
-
return
|
|
3959
|
+
return chalk4.dim;
|
|
3798
3960
|
default:
|
|
3799
|
-
return
|
|
3961
|
+
return chalk4.white;
|
|
3800
3962
|
}
|
|
3801
3963
|
}
|
|
3802
3964
|
|
|
@@ -3805,15 +3967,15 @@ import { Command as Command8 } from "commander";
|
|
|
3805
3967
|
|
|
3806
3968
|
// src/cli/commands/decision/list.ts
|
|
3807
3969
|
import { Command as Command4 } from "commander";
|
|
3808
|
-
import
|
|
3970
|
+
import chalk5 from "chalk";
|
|
3809
3971
|
import { table } from "table";
|
|
3810
3972
|
var listDecisions = new Command4("list").description("List all architectural decisions").option("-s, --status <status>", "Filter by status (draft, active, deprecated, superseded)").option("-t, --tag <tag>", "Filter by tag").option("--json", "Output as JSON").action(async (options) => {
|
|
3811
3973
|
const registry = createRegistry();
|
|
3812
3974
|
const result = await registry.load();
|
|
3813
3975
|
if (result.errors.length > 0) {
|
|
3814
|
-
console.warn(
|
|
3976
|
+
console.warn(chalk5.yellow("\nWarnings:"));
|
|
3815
3977
|
for (const err of result.errors) {
|
|
3816
|
-
console.warn(
|
|
3978
|
+
console.warn(chalk5.yellow(` - ${err.filePath}: ${err.error}`));
|
|
3817
3979
|
}
|
|
3818
3980
|
console.log("");
|
|
3819
3981
|
}
|
|
@@ -3826,7 +3988,7 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3826
3988
|
}
|
|
3827
3989
|
const decisions = registry.getAll(filter);
|
|
3828
3990
|
if (decisions.length === 0) {
|
|
3829
|
-
console.log(
|
|
3991
|
+
console.log(chalk5.yellow("No decisions found."));
|
|
3830
3992
|
return;
|
|
3831
3993
|
}
|
|
3832
3994
|
if (options.json) {
|
|
@@ -3835,11 +3997,11 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3835
3997
|
}
|
|
3836
3998
|
const data = [
|
|
3837
3999
|
[
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
4000
|
+
chalk5.bold("ID"),
|
|
4001
|
+
chalk5.bold("Title"),
|
|
4002
|
+
chalk5.bold("Status"),
|
|
4003
|
+
chalk5.bold("Constraints"),
|
|
4004
|
+
chalk5.bold("Tags")
|
|
3843
4005
|
]
|
|
3844
4006
|
];
|
|
3845
4007
|
for (const decision of decisions) {
|
|
@@ -3853,40 +4015,42 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3853
4015
|
(decision.metadata.tags || []).join(", ") || "-"
|
|
3854
4016
|
]);
|
|
3855
4017
|
}
|
|
3856
|
-
console.log(
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4018
|
+
console.log(
|
|
4019
|
+
table(data, {
|
|
4020
|
+
border: {
|
|
4021
|
+
topBody: "",
|
|
4022
|
+
topJoin: "",
|
|
4023
|
+
topLeft: "",
|
|
4024
|
+
topRight: "",
|
|
4025
|
+
bottomBody: "",
|
|
4026
|
+
bottomJoin: "",
|
|
4027
|
+
bottomLeft: "",
|
|
4028
|
+
bottomRight: "",
|
|
4029
|
+
bodyLeft: "",
|
|
4030
|
+
bodyRight: "",
|
|
4031
|
+
bodyJoin: " ",
|
|
4032
|
+
joinBody: "",
|
|
4033
|
+
joinLeft: "",
|
|
4034
|
+
joinRight: "",
|
|
4035
|
+
joinJoin: ""
|
|
4036
|
+
},
|
|
4037
|
+
drawHorizontalLine: (index) => index === 1
|
|
4038
|
+
})
|
|
4039
|
+
);
|
|
4040
|
+
console.log(chalk5.dim(`Total: ${decisions.length} decision(s)`));
|
|
3877
4041
|
});
|
|
3878
4042
|
function getStatusColor(status) {
|
|
3879
4043
|
switch (status) {
|
|
3880
4044
|
case "active":
|
|
3881
|
-
return
|
|
4045
|
+
return chalk5.green;
|
|
3882
4046
|
case "draft":
|
|
3883
|
-
return
|
|
4047
|
+
return chalk5.yellow;
|
|
3884
4048
|
case "deprecated":
|
|
3885
|
-
return
|
|
4049
|
+
return chalk5.gray;
|
|
3886
4050
|
case "superseded":
|
|
3887
|
-
return
|
|
4051
|
+
return chalk5.blue;
|
|
3888
4052
|
default:
|
|
3889
|
-
return
|
|
4053
|
+
return chalk5.white;
|
|
3890
4054
|
}
|
|
3891
4055
|
}
|
|
3892
4056
|
function getConstraintTypeSummary(types) {
|
|
@@ -3899,9 +4063,9 @@ function getConstraintTypeSummary(types) {
|
|
|
3899
4063
|
counts[type]++;
|
|
3900
4064
|
}
|
|
3901
4065
|
const parts = [];
|
|
3902
|
-
if (counts.invariant > 0) parts.push(
|
|
3903
|
-
if (counts.convention > 0) parts.push(
|
|
3904
|
-
if (counts.guideline > 0) parts.push(
|
|
4066
|
+
if (counts.invariant > 0) parts.push(chalk5.red(`${counts.invariant}I`));
|
|
4067
|
+
if (counts.convention > 0) parts.push(chalk5.yellow(`${counts.convention}C`));
|
|
4068
|
+
if (counts.guideline > 0) parts.push(chalk5.green(`${counts.guideline}G`));
|
|
3905
4069
|
return parts.join(" ") || "-";
|
|
3906
4070
|
}
|
|
3907
4071
|
function truncate(str, length) {
|
|
@@ -3911,7 +4075,7 @@ function truncate(str, length) {
|
|
|
3911
4075
|
|
|
3912
4076
|
// src/cli/commands/decision/show.ts
|
|
3913
4077
|
import { Command as Command5 } from "commander";
|
|
3914
|
-
import
|
|
4078
|
+
import chalk6 from "chalk";
|
|
3915
4079
|
var showDecision = new Command5("show").description("Show details of a specific decision").argument("<id>", "Decision ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
3916
4080
|
const registry = createRegistry();
|
|
3917
4081
|
await registry.load();
|
|
@@ -3924,74 +4088,74 @@ var showDecision = new Command5("show").description("Show details of a specific
|
|
|
3924
4088
|
});
|
|
3925
4089
|
function printDecision(decision) {
|
|
3926
4090
|
const { metadata, decision: content, constraints } = decision;
|
|
3927
|
-
console.log(
|
|
4091
|
+
console.log(chalk6.bold.blue(`
|
|
3928
4092
|
${metadata.title}`));
|
|
3929
|
-
console.log(
|
|
4093
|
+
console.log(chalk6.dim(`ID: ${metadata.id}`));
|
|
3930
4094
|
console.log("");
|
|
3931
|
-
console.log(
|
|
3932
|
-
console.log(
|
|
4095
|
+
console.log(chalk6.bold("Status:"), getStatusBadge(metadata.status));
|
|
4096
|
+
console.log(chalk6.bold("Owners:"), metadata.owners.join(", "));
|
|
3933
4097
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3934
|
-
console.log(
|
|
4098
|
+
console.log(chalk6.bold("Tags:"), metadata.tags.map((t) => chalk6.cyan(t)).join(", "));
|
|
3935
4099
|
}
|
|
3936
4100
|
if (metadata.createdAt) {
|
|
3937
|
-
console.log(
|
|
4101
|
+
console.log(chalk6.bold("Created:"), metadata.createdAt);
|
|
3938
4102
|
}
|
|
3939
4103
|
if (metadata.supersededBy) {
|
|
3940
|
-
console.log(
|
|
4104
|
+
console.log(chalk6.bold("Superseded by:"), chalk6.yellow(metadata.supersededBy));
|
|
3941
4105
|
}
|
|
3942
4106
|
console.log("");
|
|
3943
|
-
console.log(
|
|
4107
|
+
console.log(chalk6.bold.underline("Summary"));
|
|
3944
4108
|
console.log(content.summary);
|
|
3945
4109
|
console.log("");
|
|
3946
|
-
console.log(
|
|
4110
|
+
console.log(chalk6.bold.underline("Rationale"));
|
|
3947
4111
|
console.log(content.rationale);
|
|
3948
4112
|
console.log("");
|
|
3949
4113
|
if (content.context) {
|
|
3950
|
-
console.log(
|
|
4114
|
+
console.log(chalk6.bold.underline("Context"));
|
|
3951
4115
|
console.log(content.context);
|
|
3952
4116
|
console.log("");
|
|
3953
4117
|
}
|
|
3954
4118
|
if (content.consequences && content.consequences.length > 0) {
|
|
3955
|
-
console.log(
|
|
4119
|
+
console.log(chalk6.bold.underline("Consequences"));
|
|
3956
4120
|
for (const consequence of content.consequences) {
|
|
3957
4121
|
console.log(` \u2022 ${consequence}`);
|
|
3958
4122
|
}
|
|
3959
4123
|
console.log("");
|
|
3960
4124
|
}
|
|
3961
|
-
console.log(
|
|
4125
|
+
console.log(chalk6.bold.underline(`Constraints (${constraints.length})`));
|
|
3962
4126
|
for (const constraint of constraints) {
|
|
3963
4127
|
const typeIcon = getTypeIcon2(constraint.type);
|
|
3964
4128
|
const severityBadge = getSeverityBadge(constraint.severity);
|
|
3965
4129
|
console.log(`
|
|
3966
|
-
${typeIcon} ${
|
|
4130
|
+
${typeIcon} ${chalk6.bold(constraint.id)} ${severityBadge}`);
|
|
3967
4131
|
console.log(` ${constraint.rule}`);
|
|
3968
|
-
console.log(
|
|
4132
|
+
console.log(chalk6.dim(` Scope: ${constraint.scope}`));
|
|
3969
4133
|
if (constraint.verifier) {
|
|
3970
|
-
console.log(
|
|
4134
|
+
console.log(chalk6.dim(` Verifier: ${constraint.verifier}`));
|
|
3971
4135
|
}
|
|
3972
4136
|
if (constraint.exceptions && constraint.exceptions.length > 0) {
|
|
3973
|
-
console.log(
|
|
4137
|
+
console.log(chalk6.dim(` Exceptions: ${constraint.exceptions.length}`));
|
|
3974
4138
|
}
|
|
3975
4139
|
}
|
|
3976
4140
|
console.log("");
|
|
3977
4141
|
if (decision.verification?.automated && decision.verification.automated.length > 0) {
|
|
3978
|
-
console.log(
|
|
4142
|
+
console.log(chalk6.bold.underline("Automated Verification"));
|
|
3979
4143
|
for (const check of decision.verification.automated) {
|
|
3980
4144
|
console.log(` \u2022 ${check.check} (${check.frequency})`);
|
|
3981
|
-
console.log(
|
|
4145
|
+
console.log(chalk6.dim(` Target: ${check.target}`));
|
|
3982
4146
|
}
|
|
3983
4147
|
console.log("");
|
|
3984
4148
|
}
|
|
3985
4149
|
if (decision.links) {
|
|
3986
4150
|
const { related, supersedes, references } = decision.links;
|
|
3987
4151
|
if (related && related.length > 0) {
|
|
3988
|
-
console.log(
|
|
4152
|
+
console.log(chalk6.bold("Related:"), related.join(", "));
|
|
3989
4153
|
}
|
|
3990
4154
|
if (supersedes && supersedes.length > 0) {
|
|
3991
|
-
console.log(
|
|
4155
|
+
console.log(chalk6.bold("Supersedes:"), supersedes.join(", "));
|
|
3992
4156
|
}
|
|
3993
4157
|
if (references && references.length > 0) {
|
|
3994
|
-
console.log(
|
|
4158
|
+
console.log(chalk6.bold("References:"));
|
|
3995
4159
|
for (const ref of references) {
|
|
3996
4160
|
console.log(` \u2022 ${ref}`);
|
|
3997
4161
|
}
|
|
@@ -4001,13 +4165,13 @@ ${metadata.title}`));
|
|
|
4001
4165
|
function getStatusBadge(status) {
|
|
4002
4166
|
switch (status) {
|
|
4003
4167
|
case "active":
|
|
4004
|
-
return
|
|
4168
|
+
return chalk6.bgGreen.black(" ACTIVE ");
|
|
4005
4169
|
case "draft":
|
|
4006
|
-
return
|
|
4170
|
+
return chalk6.bgYellow.black(" DRAFT ");
|
|
4007
4171
|
case "deprecated":
|
|
4008
|
-
return
|
|
4172
|
+
return chalk6.bgGray.white(" DEPRECATED ");
|
|
4009
4173
|
case "superseded":
|
|
4010
|
-
return
|
|
4174
|
+
return chalk6.bgBlue.white(" SUPERSEDED ");
|
|
4011
4175
|
default:
|
|
4012
4176
|
return status;
|
|
4013
4177
|
}
|
|
@@ -4015,11 +4179,11 @@ function getStatusBadge(status) {
|
|
|
4015
4179
|
function getTypeIcon2(type) {
|
|
4016
4180
|
switch (type) {
|
|
4017
4181
|
case "invariant":
|
|
4018
|
-
return
|
|
4182
|
+
return chalk6.red("\u25CF");
|
|
4019
4183
|
case "convention":
|
|
4020
|
-
return
|
|
4184
|
+
return chalk6.yellow("\u25CF");
|
|
4021
4185
|
case "guideline":
|
|
4022
|
-
return
|
|
4186
|
+
return chalk6.green("\u25CF");
|
|
4023
4187
|
default:
|
|
4024
4188
|
return "\u25CB";
|
|
4025
4189
|
}
|
|
@@ -4027,13 +4191,13 @@ function getTypeIcon2(type) {
|
|
|
4027
4191
|
function getSeverityBadge(severity) {
|
|
4028
4192
|
switch (severity) {
|
|
4029
4193
|
case "critical":
|
|
4030
|
-
return
|
|
4194
|
+
return chalk6.bgRed.white(" CRITICAL ");
|
|
4031
4195
|
case "high":
|
|
4032
|
-
return
|
|
4196
|
+
return chalk6.bgYellow.black(" HIGH ");
|
|
4033
4197
|
case "medium":
|
|
4034
|
-
return
|
|
4198
|
+
return chalk6.bgCyan.black(" MEDIUM ");
|
|
4035
4199
|
case "low":
|
|
4036
|
-
return
|
|
4200
|
+
return chalk6.bgGray.white(" LOW ");
|
|
4037
4201
|
default:
|
|
4038
4202
|
return severity;
|
|
4039
4203
|
}
|
|
@@ -4041,7 +4205,7 @@ function getSeverityBadge(severity) {
|
|
|
4041
4205
|
|
|
4042
4206
|
// src/cli/commands/decision/validate.ts
|
|
4043
4207
|
import { Command as Command6 } from "commander";
|
|
4044
|
-
import
|
|
4208
|
+
import chalk7 from "chalk";
|
|
4045
4209
|
import ora4 from "ora";
|
|
4046
4210
|
import { join as join6 } from "path";
|
|
4047
4211
|
var validateDecisions = new Command6("validate").description("Validate decision files").option("-f, --file <path>", "Validate a specific file").action(async (options) => {
|
|
@@ -4080,14 +4244,14 @@ var validateDecisions = new Command6("validate").description("Validate decision
|
|
|
4080
4244
|
}
|
|
4081
4245
|
spinner.stop();
|
|
4082
4246
|
if (invalid === 0) {
|
|
4083
|
-
console.log(
|
|
4247
|
+
console.log(chalk7.green(`\u2713 All ${valid} decision file(s) are valid.`));
|
|
4084
4248
|
} else {
|
|
4085
|
-
console.log(
|
|
4249
|
+
console.log(chalk7.red(`\u2717 ${invalid} of ${files.length} decision file(s) have errors.
|
|
4086
4250
|
`));
|
|
4087
4251
|
for (const { file, errors: fileErrors } of errors) {
|
|
4088
|
-
console.log(
|
|
4252
|
+
console.log(chalk7.red(`File: ${file}`));
|
|
4089
4253
|
for (const err of fileErrors) {
|
|
4090
|
-
console.log(
|
|
4254
|
+
console.log(chalk7.dim(` - ${err}`));
|
|
4091
4255
|
}
|
|
4092
4256
|
console.log("");
|
|
4093
4257
|
}
|
|
@@ -4101,21 +4265,31 @@ var validateDecisions = new Command6("validate").description("Validate decision
|
|
|
4101
4265
|
|
|
4102
4266
|
// src/cli/commands/decision/create.ts
|
|
4103
4267
|
import { Command as Command7 } from "commander";
|
|
4104
|
-
import
|
|
4268
|
+
import chalk8 from "chalk";
|
|
4105
4269
|
import { join as join7 } from "path";
|
|
4106
|
-
var createDecision = new Command7("create").description("Create a new decision file").argument("<id>", "Decision ID (e.g., auth-001)").requiredOption("-t, --title <title>", "Decision title").requiredOption("-s, --summary <summary>", "One-sentence summary").option(
|
|
4270
|
+
var createDecision = new Command7("create").description("Create a new decision file").argument("<id>", "Decision ID (e.g., auth-001)").requiredOption("-t, --title <title>", "Decision title").requiredOption("-s, --summary <summary>", "One-sentence summary").option(
|
|
4271
|
+
"--type <type>",
|
|
4272
|
+
"Default constraint type (invariant, convention, guideline)",
|
|
4273
|
+
"convention"
|
|
4274
|
+
).option(
|
|
4275
|
+
"--severity <severity>",
|
|
4276
|
+
"Default constraint severity (critical, high, medium, low)",
|
|
4277
|
+
"medium"
|
|
4278
|
+
).option("--scope <scope>", "Default constraint scope (glob pattern)", "src/**/*.ts").option("-o, --owner <owner>", "Owner name", "team").action(async (id, options) => {
|
|
4107
4279
|
const cwd = process.cwd();
|
|
4108
4280
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
4109
4281
|
throw new NotInitializedError();
|
|
4110
4282
|
}
|
|
4111
4283
|
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
4112
|
-
console.error(
|
|
4284
|
+
console.error(
|
|
4285
|
+
chalk8.red("Error: Decision ID must be lowercase alphanumeric with hyphens only.")
|
|
4286
|
+
);
|
|
4113
4287
|
process.exit(1);
|
|
4114
4288
|
}
|
|
4115
4289
|
const decisionsDir = getDecisionsDir(cwd);
|
|
4116
4290
|
const filePath = join7(decisionsDir, `${id}.decision.yaml`);
|
|
4117
4291
|
if (await pathExists(filePath)) {
|
|
4118
|
-
console.error(
|
|
4292
|
+
console.error(chalk8.red(`Error: Decision file already exists: ${filePath}`));
|
|
4119
4293
|
process.exit(1);
|
|
4120
4294
|
}
|
|
4121
4295
|
const decision = {
|
|
@@ -4151,13 +4325,15 @@ var createDecision = new Command7("create").description("Create a new decision f
|
|
|
4151
4325
|
}
|
|
4152
4326
|
};
|
|
4153
4327
|
await writeTextFile(filePath, stringifyYaml(decision));
|
|
4154
|
-
console.log(
|
|
4328
|
+
console.log(chalk8.green(`\u2713 Created decision: ${filePath}`));
|
|
4155
4329
|
console.log("");
|
|
4156
|
-
console.log(
|
|
4330
|
+
console.log(chalk8.cyan("Next steps:"));
|
|
4157
4331
|
console.log(` 1. Edit the file to add rationale, context, and consequences`);
|
|
4158
4332
|
console.log(` 2. Define constraints with appropriate scopes`);
|
|
4159
|
-
console.log(` 3. Run ${
|
|
4160
|
-
console.log(
|
|
4333
|
+
console.log(` 3. Run ${chalk8.bold("specbridge decision validate")} to check syntax`);
|
|
4334
|
+
console.log(
|
|
4335
|
+
` 4. Change status from ${chalk8.yellow("draft")} to ${chalk8.green("active")} when ready`
|
|
4336
|
+
);
|
|
4161
4337
|
});
|
|
4162
4338
|
|
|
4163
4339
|
// src/cli/commands/decision/index.ts
|
|
@@ -4165,7 +4341,7 @@ var decisionCommand = new Command8("decision").description("Manage architectural
|
|
|
4165
4341
|
|
|
4166
4342
|
// src/cli/commands/hook.ts
|
|
4167
4343
|
import { Command as Command9 } from "commander";
|
|
4168
|
-
import
|
|
4344
|
+
import chalk9 from "chalk";
|
|
4169
4345
|
import ora5 from "ora";
|
|
4170
4346
|
import { join as join8 } from "path";
|
|
4171
4347
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -4197,14 +4373,16 @@ function createHookCommand() {
|
|
|
4197
4373
|
} else if (options.lefthook) {
|
|
4198
4374
|
spinner.succeed("Lefthook detected");
|
|
4199
4375
|
console.log("");
|
|
4200
|
-
console.log(
|
|
4376
|
+
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4201
4377
|
console.log("");
|
|
4202
|
-
console.log(
|
|
4378
|
+
console.log(
|
|
4379
|
+
chalk9.dim(`pre-commit:
|
|
4203
4380
|
commands:
|
|
4204
4381
|
specbridge:
|
|
4205
4382
|
glob: "*.{ts,tsx}"
|
|
4206
4383
|
run: npx specbridge hook run --level commit --files {staged_files}
|
|
4207
|
-
`)
|
|
4384
|
+
`)
|
|
4385
|
+
);
|
|
4208
4386
|
return;
|
|
4209
4387
|
} else {
|
|
4210
4388
|
if (await pathExists(join8(cwd, ".husky"))) {
|
|
@@ -4214,14 +4392,16 @@ function createHookCommand() {
|
|
|
4214
4392
|
} else if (await pathExists(join8(cwd, "lefthook.yml"))) {
|
|
4215
4393
|
spinner.succeed("Lefthook detected");
|
|
4216
4394
|
console.log("");
|
|
4217
|
-
console.log(
|
|
4395
|
+
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4218
4396
|
console.log("");
|
|
4219
|
-
console.log(
|
|
4397
|
+
console.log(
|
|
4398
|
+
chalk9.dim(`pre-commit:
|
|
4220
4399
|
commands:
|
|
4221
4400
|
specbridge:
|
|
4222
4401
|
glob: "*.{ts,tsx}"
|
|
4223
4402
|
run: npx specbridge hook run --level commit --files {staged_files}
|
|
4224
|
-
`)
|
|
4403
|
+
`)
|
|
4404
|
+
);
|
|
4225
4405
|
return;
|
|
4226
4406
|
} else {
|
|
4227
4407
|
hookPath = join8(cwd, ".git", "hooks", "pre-commit");
|
|
@@ -4231,7 +4411,7 @@ function createHookCommand() {
|
|
|
4231
4411
|
}
|
|
4232
4412
|
if (await pathExists(hookPath) && !options.force) {
|
|
4233
4413
|
spinner.fail("Hook already exists");
|
|
4234
|
-
console.log(
|
|
4414
|
+
console.log(chalk9.yellow(`Use --force to overwrite: ${hookPath}`));
|
|
4235
4415
|
return;
|
|
4236
4416
|
}
|
|
4237
4417
|
await writeTextFile(hookPath, hookContent);
|
|
@@ -4241,9 +4421,9 @@ function createHookCommand() {
|
|
|
4241
4421
|
} catch {
|
|
4242
4422
|
}
|
|
4243
4423
|
spinner.succeed("Pre-commit hook installed");
|
|
4244
|
-
console.log(
|
|
4424
|
+
console.log(chalk9.dim(` Path: ${hookPath}`));
|
|
4245
4425
|
console.log("");
|
|
4246
|
-
console.log(
|
|
4426
|
+
console.log(chalk9.cyan("The hook will run on each commit and verify staged files."));
|
|
4247
4427
|
} catch (error) {
|
|
4248
4428
|
spinner.fail("Failed to install hook");
|
|
4249
4429
|
throw error;
|
|
@@ -4263,7 +4443,11 @@ function createHookCommand() {
|
|
|
4263
4443
|
const { promisify: promisify2 } = await import("util");
|
|
4264
4444
|
const execFileAsync2 = promisify2(execFile2);
|
|
4265
4445
|
try {
|
|
4266
|
-
const { stdout: stdout2 } = await execFileAsync2(
|
|
4446
|
+
const { stdout: stdout2 } = await execFileAsync2(
|
|
4447
|
+
"git",
|
|
4448
|
+
["diff", "--cached", "--name-only", "--diff-filter=AM"],
|
|
4449
|
+
{ cwd }
|
|
4450
|
+
);
|
|
4267
4451
|
files = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean).filter((f) => /\.(ts|tsx|js|jsx)$/.test(f));
|
|
4268
4452
|
} catch {
|
|
4269
4453
|
files = [];
|
|
@@ -4279,21 +4463,21 @@ function createHookCommand() {
|
|
|
4279
4463
|
cwd
|
|
4280
4464
|
});
|
|
4281
4465
|
if (result.violations.length === 0) {
|
|
4282
|
-
console.log(
|
|
4466
|
+
console.log(chalk9.green("\u2713 SpecBridge: All checks passed"));
|
|
4283
4467
|
process.exit(0);
|
|
4284
4468
|
}
|
|
4285
|
-
console.log(
|
|
4469
|
+
console.log(chalk9.red(`\u2717 SpecBridge: ${result.violations.length} violation(s) found`));
|
|
4286
4470
|
console.log("");
|
|
4287
4471
|
for (const v of result.violations) {
|
|
4288
4472
|
const location = v.line ? `:${v.line}` : "";
|
|
4289
4473
|
console.log(` ${v.file}${location}: ${v.message}`);
|
|
4290
|
-
console.log(
|
|
4474
|
+
console.log(chalk9.dim(` [${v.severity}] ${v.decisionId}/${v.constraintId}`));
|
|
4291
4475
|
}
|
|
4292
4476
|
console.log("");
|
|
4293
|
-
console.log(
|
|
4477
|
+
console.log(chalk9.yellow("Run `specbridge verify` for full details."));
|
|
4294
4478
|
process.exit(result.success ? 0 : 1);
|
|
4295
4479
|
} catch (error) {
|
|
4296
|
-
console.error(
|
|
4480
|
+
console.error(chalk9.red("SpecBridge verification failed"));
|
|
4297
4481
|
console.error(error instanceof Error ? error.message : String(error));
|
|
4298
4482
|
process.exit(1);
|
|
4299
4483
|
}
|
|
@@ -4332,7 +4516,7 @@ var hookCommand = createHookCommand();
|
|
|
4332
4516
|
|
|
4333
4517
|
// src/cli/commands/report.ts
|
|
4334
4518
|
import { Command as Command10 } from "commander";
|
|
4335
|
-
import
|
|
4519
|
+
import chalk11 from "chalk";
|
|
4336
4520
|
import ora6 from "ora";
|
|
4337
4521
|
import { join as join10 } from "path";
|
|
4338
4522
|
|
|
@@ -4365,10 +4549,7 @@ async function generateReport(config, options = {}) {
|
|
|
4365
4549
|
medium: decisionViolations.filter((v) => v.severity === "medium").length,
|
|
4366
4550
|
low: decisionViolations.filter((v) => v.severity === "low").length
|
|
4367
4551
|
};
|
|
4368
|
-
weightedScore = decisionViolations.reduce(
|
|
4369
|
-
(score, v) => score + weights[v.severity],
|
|
4370
|
-
0
|
|
4371
|
-
);
|
|
4552
|
+
weightedScore = decisionViolations.reduce((score, v) => score + weights[v.severity], 0);
|
|
4372
4553
|
compliance = Math.max(0, 100 - weightedScore);
|
|
4373
4554
|
if (decisionViolations.length > 0 && constraintCount > 0) {
|
|
4374
4555
|
const violationRate = decisionViolations.length / constraintCount;
|
|
@@ -4416,54 +4597,58 @@ async function generateReport(config, options = {}) {
|
|
|
4416
4597
|
}
|
|
4417
4598
|
|
|
4418
4599
|
// src/reporting/formats/console.ts
|
|
4419
|
-
import
|
|
4600
|
+
import chalk10 from "chalk";
|
|
4420
4601
|
import { table as table2 } from "table";
|
|
4421
4602
|
function formatConsoleReport(report) {
|
|
4422
4603
|
const lines = [];
|
|
4423
4604
|
lines.push("");
|
|
4424
|
-
lines.push(
|
|
4425
|
-
lines.push(
|
|
4426
|
-
lines.push(
|
|
4605
|
+
lines.push(chalk10.bold.blue("SpecBridge Compliance Report"));
|
|
4606
|
+
lines.push(chalk10.dim(`Generated: ${new Date(report.timestamp).toLocaleString()}`));
|
|
4607
|
+
lines.push(chalk10.dim(`Project: ${report.project}`));
|
|
4427
4608
|
lines.push("");
|
|
4428
4609
|
const complianceColor = getComplianceColor(report.summary.compliance);
|
|
4429
|
-
lines.push(
|
|
4430
|
-
lines.push(
|
|
4610
|
+
lines.push(chalk10.bold("Overall Compliance"));
|
|
4611
|
+
lines.push(
|
|
4612
|
+
` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`
|
|
4613
|
+
);
|
|
4431
4614
|
lines.push("");
|
|
4432
|
-
lines.push(
|
|
4433
|
-
lines.push(
|
|
4615
|
+
lines.push(chalk10.bold("Summary"));
|
|
4616
|
+
lines.push(
|
|
4617
|
+
` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`
|
|
4618
|
+
);
|
|
4434
4619
|
lines.push(` Constraints: ${report.summary.totalConstraints}`);
|
|
4435
4620
|
lines.push("");
|
|
4436
|
-
lines.push(
|
|
4621
|
+
lines.push(chalk10.bold("Violations"));
|
|
4437
4622
|
const { violations } = report.summary;
|
|
4438
4623
|
const violationParts = [];
|
|
4439
4624
|
if (violations.critical > 0) {
|
|
4440
|
-
violationParts.push(
|
|
4625
|
+
violationParts.push(chalk10.red(`${violations.critical} critical`));
|
|
4441
4626
|
}
|
|
4442
4627
|
if (violations.high > 0) {
|
|
4443
|
-
violationParts.push(
|
|
4628
|
+
violationParts.push(chalk10.yellow(`${violations.high} high`));
|
|
4444
4629
|
}
|
|
4445
4630
|
if (violations.medium > 0) {
|
|
4446
|
-
violationParts.push(
|
|
4631
|
+
violationParts.push(chalk10.cyan(`${violations.medium} medium`));
|
|
4447
4632
|
}
|
|
4448
4633
|
if (violations.low > 0) {
|
|
4449
|
-
violationParts.push(
|
|
4634
|
+
violationParts.push(chalk10.dim(`${violations.low} low`));
|
|
4450
4635
|
}
|
|
4451
4636
|
if (violationParts.length > 0) {
|
|
4452
4637
|
lines.push(` ${violationParts.join(" | ")}`);
|
|
4453
4638
|
} else {
|
|
4454
|
-
lines.push(
|
|
4639
|
+
lines.push(chalk10.green(" No violations"));
|
|
4455
4640
|
}
|
|
4456
4641
|
lines.push("");
|
|
4457
4642
|
if (report.byDecision.length > 0) {
|
|
4458
|
-
lines.push(
|
|
4643
|
+
lines.push(chalk10.bold("By Decision"));
|
|
4459
4644
|
lines.push("");
|
|
4460
4645
|
const tableData = [
|
|
4461
4646
|
[
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4647
|
+
chalk10.bold("Decision"),
|
|
4648
|
+
chalk10.bold("Status"),
|
|
4649
|
+
chalk10.bold("Constraints"),
|
|
4650
|
+
chalk10.bold("Violations"),
|
|
4651
|
+
chalk10.bold("Compliance")
|
|
4467
4652
|
]
|
|
4468
4653
|
];
|
|
4469
4654
|
for (const dec of report.byDecision) {
|
|
@@ -4473,7 +4658,7 @@ function formatConsoleReport(report) {
|
|
|
4473
4658
|
truncate2(dec.title, 40),
|
|
4474
4659
|
statusColor(dec.status),
|
|
4475
4660
|
String(dec.constraints),
|
|
4476
|
-
dec.violations > 0 ?
|
|
4661
|
+
dec.violations > 0 ? chalk10.red(String(dec.violations)) : chalk10.green("0"),
|
|
4477
4662
|
compColor(`${dec.compliance}%`)
|
|
4478
4663
|
]);
|
|
4479
4664
|
}
|
|
@@ -4507,23 +4692,23 @@ function formatComplianceBar(compliance) {
|
|
|
4507
4692
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
4508
4693
|
}
|
|
4509
4694
|
function getComplianceColor(compliance) {
|
|
4510
|
-
if (compliance >= 90) return
|
|
4511
|
-
if (compliance >= 70) return
|
|
4512
|
-
if (compliance >= 50) return
|
|
4513
|
-
return
|
|
4695
|
+
if (compliance >= 90) return chalk10.green;
|
|
4696
|
+
if (compliance >= 70) return chalk10.yellow;
|
|
4697
|
+
if (compliance >= 50) return chalk10.hex("#FFA500");
|
|
4698
|
+
return chalk10.red;
|
|
4514
4699
|
}
|
|
4515
4700
|
function getStatusColor2(status) {
|
|
4516
4701
|
switch (status) {
|
|
4517
4702
|
case "active":
|
|
4518
|
-
return
|
|
4703
|
+
return chalk10.green;
|
|
4519
4704
|
case "draft":
|
|
4520
|
-
return
|
|
4705
|
+
return chalk10.yellow;
|
|
4521
4706
|
case "deprecated":
|
|
4522
|
-
return
|
|
4707
|
+
return chalk10.gray;
|
|
4523
4708
|
case "superseded":
|
|
4524
|
-
return
|
|
4709
|
+
return chalk10.blue;
|
|
4525
4710
|
default:
|
|
4526
|
-
return
|
|
4711
|
+
return chalk10.white;
|
|
4527
4712
|
}
|
|
4528
4713
|
}
|
|
4529
4714
|
function truncate2(str, length) {
|
|
@@ -4547,7 +4732,9 @@ function formatMarkdownReport(report) {
|
|
|
4547
4732
|
lines.push("");
|
|
4548
4733
|
lines.push("## Summary");
|
|
4549
4734
|
lines.push("");
|
|
4550
|
-
lines.push(
|
|
4735
|
+
lines.push(
|
|
4736
|
+
`- **Active Decisions:** ${report.summary.activeDecisions} / ${report.summary.totalDecisions}`
|
|
4737
|
+
);
|
|
4551
4738
|
lines.push(`- **Total Constraints:** ${report.summary.totalConstraints}`);
|
|
4552
4739
|
lines.push("");
|
|
4553
4740
|
lines.push("### Violations");
|
|
@@ -4597,6 +4784,7 @@ function formatProgressBar(percentage) {
|
|
|
4597
4784
|
import { join as join9 } from "path";
|
|
4598
4785
|
var ReportStorage = class {
|
|
4599
4786
|
storageDir;
|
|
4787
|
+
logger = getLogger({ module: "reporting.storage" });
|
|
4600
4788
|
constructor(basePath) {
|
|
4601
4789
|
this.storageDir = join9(getSpecBridgeDir(basePath), "reports", "history");
|
|
4602
4790
|
}
|
|
@@ -4655,7 +4843,7 @@ var ReportStorage = class {
|
|
|
4655
4843
|
const timestamp = file.replace("report-", "").replace(".json", "");
|
|
4656
4844
|
return { timestamp, report };
|
|
4657
4845
|
} catch (error) {
|
|
4658
|
-
|
|
4846
|
+
this.logger.warn({ file, error }, "Failed to load report file");
|
|
4659
4847
|
return null;
|
|
4660
4848
|
}
|
|
4661
4849
|
});
|
|
@@ -4699,7 +4887,7 @@ var ReportStorage = class {
|
|
|
4699
4887
|
const fs = await import("fs/promises");
|
|
4700
4888
|
await fs.unlink(filepath);
|
|
4701
4889
|
} catch (error) {
|
|
4702
|
-
|
|
4890
|
+
this.logger.warn({ file, error }, "Failed to delete old report file");
|
|
4703
4891
|
}
|
|
4704
4892
|
}
|
|
4705
4893
|
return filesToDelete.length;
|
|
@@ -4710,9 +4898,7 @@ var ReportStorage = class {
|
|
|
4710
4898
|
async function detectDrift(current, previous) {
|
|
4711
4899
|
const byDecision = [];
|
|
4712
4900
|
for (const currDecision of current.byDecision) {
|
|
4713
|
-
const prevDecision = previous.byDecision.find(
|
|
4714
|
-
(d) => d.decisionId === currDecision.decisionId
|
|
4715
|
-
);
|
|
4901
|
+
const prevDecision = previous.byDecision.find((d) => d.decisionId === currDecision.decisionId);
|
|
4716
4902
|
if (!prevDecision) {
|
|
4717
4903
|
byDecision.push({
|
|
4718
4904
|
decisionId: currDecision.decisionId,
|
|
@@ -4901,57 +5087,75 @@ var reportCommand = new Command10("report").description("Generate compliance rep
|
|
|
4901
5087
|
const storage = new ReportStorage(cwd);
|
|
4902
5088
|
await storage.save(report);
|
|
4903
5089
|
if (options.trend) {
|
|
4904
|
-
console.log("\n" +
|
|
5090
|
+
console.log("\n" + chalk11.blue.bold("=== Compliance Trend Analysis ===\n"));
|
|
4905
5091
|
const days = parseInt(options.days || "30", 10);
|
|
4906
5092
|
const history = await storage.loadHistory(days);
|
|
4907
5093
|
if (history.length < 2) {
|
|
4908
|
-
console.log(
|
|
5094
|
+
console.log(
|
|
5095
|
+
chalk11.yellow(
|
|
5096
|
+
`Not enough data for trend analysis. Found ${history.length} report(s), need at least 2.`
|
|
5097
|
+
)
|
|
5098
|
+
);
|
|
4909
5099
|
} else {
|
|
4910
5100
|
const trend = await analyzeTrend(history);
|
|
4911
|
-
console.log(
|
|
4912
|
-
|
|
4913
|
-
|
|
5101
|
+
console.log(
|
|
5102
|
+
chalk11.bold(
|
|
5103
|
+
`Period: ${trend.period.start} to ${trend.period.end} (${trend.period.days} days)`
|
|
5104
|
+
)
|
|
5105
|
+
);
|
|
5106
|
+
console.log(
|
|
5107
|
+
`
|
|
5108
|
+
Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.endCompliance}% (${trend.overall.change > 0 ? "+" : ""}${trend.overall.change.toFixed(1)}%)`
|
|
5109
|
+
);
|
|
4914
5110
|
const trendEmoji = trend.overall.trend === "improving" ? "\u{1F4C8}" : trend.overall.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4915
|
-
const trendColor = trend.overall.trend === "improving" ?
|
|
5111
|
+
const trendColor = trend.overall.trend === "improving" ? chalk11.green : trend.overall.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4916
5112
|
console.log(trendColor(`${trendEmoji} Trend: ${trend.overall.trend.toUpperCase()}`));
|
|
4917
5113
|
const degrading = trend.decisions.filter((d) => d.trend === "degrading").slice(0, 3);
|
|
4918
5114
|
if (degrading.length > 0) {
|
|
4919
|
-
console.log(
|
|
5115
|
+
console.log(chalk11.red("\n\u26A0\uFE0F Most Degraded Decisions:"));
|
|
4920
5116
|
degrading.forEach((d) => {
|
|
4921
|
-
console.log(
|
|
5117
|
+
console.log(
|
|
5118
|
+
` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (${d.change.toFixed(1)}%)`
|
|
5119
|
+
);
|
|
4922
5120
|
});
|
|
4923
5121
|
}
|
|
4924
5122
|
const improving = trend.decisions.filter((d) => d.trend === "improving").slice(0, 3);
|
|
4925
5123
|
if (improving.length > 0) {
|
|
4926
|
-
console.log(
|
|
5124
|
+
console.log(chalk11.green("\n\u2705 Most Improved Decisions:"));
|
|
4927
5125
|
improving.forEach((d) => {
|
|
4928
|
-
console.log(
|
|
5126
|
+
console.log(
|
|
5127
|
+
` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (+${d.change.toFixed(1)}%)`
|
|
5128
|
+
);
|
|
4929
5129
|
});
|
|
4930
5130
|
}
|
|
4931
5131
|
}
|
|
4932
5132
|
console.log("");
|
|
4933
5133
|
}
|
|
4934
5134
|
if (options.drift) {
|
|
4935
|
-
console.log("\n" +
|
|
5135
|
+
console.log("\n" + chalk11.blue.bold("=== Drift Analysis ===\n"));
|
|
4936
5136
|
const history = await storage.loadHistory(2);
|
|
4937
5137
|
if (history.length < 2) {
|
|
4938
|
-
console.log(
|
|
5138
|
+
console.log(chalk11.yellow("Not enough data for drift analysis. Need at least 2 reports."));
|
|
4939
5139
|
} else {
|
|
4940
5140
|
const currentEntry = history[0];
|
|
4941
5141
|
const previousEntry = history[1];
|
|
4942
5142
|
if (!currentEntry || !previousEntry) {
|
|
4943
|
-
console.log(
|
|
5143
|
+
console.log(chalk11.yellow("Invalid history data."));
|
|
4944
5144
|
return;
|
|
4945
5145
|
}
|
|
4946
5146
|
const drift = await detectDrift(currentEntry.report, previousEntry.report);
|
|
4947
|
-
console.log(
|
|
4948
|
-
|
|
4949
|
-
|
|
5147
|
+
console.log(
|
|
5148
|
+
chalk11.bold(`Comparing: ${previousEntry.timestamp} vs ${currentEntry.timestamp}`)
|
|
5149
|
+
);
|
|
5150
|
+
console.log(
|
|
5151
|
+
`
|
|
5152
|
+
Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceChange.toFixed(1)}%`
|
|
5153
|
+
);
|
|
4950
5154
|
const driftEmoji = drift.trend === "improving" ? "\u{1F4C8}" : drift.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4951
|
-
const driftColor = drift.trend === "improving" ?
|
|
5155
|
+
const driftColor = drift.trend === "improving" ? chalk11.green : drift.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4952
5156
|
console.log(driftColor(`${driftEmoji} Overall Trend: ${drift.trend.toUpperCase()}`));
|
|
4953
5157
|
if (drift.summary.newViolations.total > 0) {
|
|
4954
|
-
console.log(
|
|
5158
|
+
console.log(chalk11.red(`
|
|
4955
5159
|
\u26A0\uFE0F New Violations: ${drift.summary.newViolations.total}`));
|
|
4956
5160
|
if (drift.summary.newViolations.critical > 0) {
|
|
4957
5161
|
console.log(` \u2022 Critical: ${drift.summary.newViolations.critical}`);
|
|
@@ -4967,8 +5171,10 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4967
5171
|
}
|
|
4968
5172
|
}
|
|
4969
5173
|
if (drift.summary.fixedViolations.total > 0) {
|
|
4970
|
-
console.log(
|
|
4971
|
-
|
|
5174
|
+
console.log(
|
|
5175
|
+
chalk11.green(`
|
|
5176
|
+
\u2705 Fixed Violations: ${drift.summary.fixedViolations.total}`)
|
|
5177
|
+
);
|
|
4972
5178
|
if (drift.summary.fixedViolations.critical > 0) {
|
|
4973
5179
|
console.log(` \u2022 Critical: ${drift.summary.fixedViolations.critical}`);
|
|
4974
5180
|
}
|
|
@@ -4983,18 +5189,22 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4983
5189
|
}
|
|
4984
5190
|
}
|
|
4985
5191
|
if (drift.mostDegraded.length > 0) {
|
|
4986
|
-
console.log(
|
|
5192
|
+
console.log(chalk11.red("\n\u{1F4C9} Most Degraded:"));
|
|
4987
5193
|
drift.mostDegraded.forEach((d) => {
|
|
4988
|
-
console.log(
|
|
5194
|
+
console.log(
|
|
5195
|
+
` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (${d.complianceChange.toFixed(1)}%)`
|
|
5196
|
+
);
|
|
4989
5197
|
if (d.newViolations > 0) {
|
|
4990
5198
|
console.log(` +${d.newViolations} new violation(s)`);
|
|
4991
5199
|
}
|
|
4992
5200
|
});
|
|
4993
5201
|
}
|
|
4994
5202
|
if (drift.mostImproved.length > 0) {
|
|
4995
|
-
console.log(
|
|
5203
|
+
console.log(chalk11.green("\n\u{1F4C8} Most Improved:"));
|
|
4996
5204
|
drift.mostImproved.forEach((d) => {
|
|
4997
|
-
console.log(
|
|
5205
|
+
console.log(
|
|
5206
|
+
` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (+${d.complianceChange.toFixed(1)}%)`
|
|
5207
|
+
);
|
|
4998
5208
|
if (d.fixedViolations > 0) {
|
|
4999
5209
|
console.log(` -${d.fixedViolations} fixed violation(s)`);
|
|
5000
5210
|
}
|
|
@@ -5027,12 +5237,9 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
5027
5237
|
}
|
|
5028
5238
|
}
|
|
5029
5239
|
if (options.output || options.save) {
|
|
5030
|
-
const outputPath = options.output || join10(
|
|
5031
|
-
getReportsDir(cwd),
|
|
5032
|
-
`health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`
|
|
5033
|
-
);
|
|
5240
|
+
const outputPath = options.output || join10(getReportsDir(cwd), `health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`);
|
|
5034
5241
|
await writeTextFile(outputPath, output);
|
|
5035
|
-
console.log(
|
|
5242
|
+
console.log(chalk11.green(`
|
|
5036
5243
|
Report saved to: ${outputPath}`));
|
|
5037
5244
|
if (options.save && !options.output) {
|
|
5038
5245
|
const latestPath = join10(getReportsDir(cwd), `health-latest.${extension}`);
|
|
@@ -5047,7 +5254,7 @@ Report saved to: ${outputPath}`));
|
|
|
5047
5254
|
|
|
5048
5255
|
// src/cli/commands/context.ts
|
|
5049
5256
|
import { Command as Command11 } from "commander";
|
|
5050
|
-
import
|
|
5257
|
+
import chalk12 from "chalk";
|
|
5051
5258
|
|
|
5052
5259
|
// src/agent/context.generator.ts
|
|
5053
5260
|
async function generateContext(filePath, config, options = {}) {
|
|
@@ -5167,12 +5374,12 @@ var contextCommand = new Command11("context").description("Generate architectura
|
|
|
5167
5374
|
});
|
|
5168
5375
|
if (options.output) {
|
|
5169
5376
|
await writeTextFile(options.output, output);
|
|
5170
|
-
console.log(
|
|
5377
|
+
console.log(chalk12.green(`Context saved to: ${options.output}`));
|
|
5171
5378
|
} else {
|
|
5172
5379
|
console.log(output);
|
|
5173
5380
|
}
|
|
5174
5381
|
} catch (error) {
|
|
5175
|
-
console.error(
|
|
5382
|
+
console.error(chalk12.red("Failed to generate context"));
|
|
5176
5383
|
throw error;
|
|
5177
5384
|
}
|
|
5178
5385
|
});
|
|
@@ -5181,12 +5388,19 @@ var contextCommand = new Command11("context").description("Generate architectura
|
|
|
5181
5388
|
import { Command as Command12 } from "commander";
|
|
5182
5389
|
|
|
5183
5390
|
// src/lsp/server.ts
|
|
5184
|
-
import {
|
|
5391
|
+
import {
|
|
5392
|
+
createConnection,
|
|
5393
|
+
ProposedFeatures,
|
|
5394
|
+
TextDocuments,
|
|
5395
|
+
TextDocumentSyncKind,
|
|
5396
|
+
DiagnosticSeverity,
|
|
5397
|
+
CodeActionKind
|
|
5398
|
+
} from "vscode-languageserver/node.js";
|
|
5185
5399
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5186
5400
|
import { fileURLToPath } from "url";
|
|
5187
5401
|
import path3 from "path";
|
|
5188
5402
|
import { Project as Project3 } from "ts-morph";
|
|
5189
|
-
import
|
|
5403
|
+
import chalk13 from "chalk";
|
|
5190
5404
|
function severityToDiagnostic(severity) {
|
|
5191
5405
|
switch (severity) {
|
|
5192
5406
|
case "critical":
|
|
@@ -5297,7 +5511,7 @@ var SpecBridgeLspServer = class {
|
|
|
5297
5511
|
if (!await pathExists(getSpecBridgeDir(this.cwd))) {
|
|
5298
5512
|
const err = new NotInitializedError();
|
|
5299
5513
|
this.initError = err.message;
|
|
5300
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5514
|
+
if (this.options.verbose) this.connection.console.error(chalk13.red(this.initError));
|
|
5301
5515
|
return;
|
|
5302
5516
|
}
|
|
5303
5517
|
try {
|
|
@@ -5306,7 +5520,8 @@ var SpecBridgeLspServer = class {
|
|
|
5306
5520
|
await getPluginLoader().loadPlugins(this.cwd);
|
|
5307
5521
|
} catch (error) {
|
|
5308
5522
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5309
|
-
if (this.options.verbose)
|
|
5523
|
+
if (this.options.verbose)
|
|
5524
|
+
this.connection.console.error(chalk13.red(`Plugin load failed: ${msg}`));
|
|
5310
5525
|
}
|
|
5311
5526
|
this.registry = createRegistry({ basePath: this.cwd });
|
|
5312
5527
|
await this.registry.load();
|
|
@@ -5319,11 +5534,13 @@ var SpecBridgeLspServer = class {
|
|
|
5319
5534
|
}
|
|
5320
5535
|
}
|
|
5321
5536
|
if (this.options.verbose) {
|
|
5322
|
-
this.connection.console.log(
|
|
5537
|
+
this.connection.console.log(
|
|
5538
|
+
chalk13.dim(`Loaded ${this.decisions.length} active decision(s)`)
|
|
5539
|
+
);
|
|
5323
5540
|
}
|
|
5324
5541
|
} catch (error) {
|
|
5325
5542
|
this.initError = error instanceof Error ? error.message : String(error);
|
|
5326
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5543
|
+
if (this.options.verbose) this.connection.console.error(chalk13.red(this.initError));
|
|
5327
5544
|
}
|
|
5328
5545
|
}
|
|
5329
5546
|
async verifyTextDocument(doc) {
|
|
@@ -5337,7 +5554,11 @@ var SpecBridgeLspServer = class {
|
|
|
5337
5554
|
for (const decision of this.decisions) {
|
|
5338
5555
|
for (const constraint of decision.constraints) {
|
|
5339
5556
|
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd: this.cwd })) continue;
|
|
5340
|
-
const verifier = selectVerifierForConstraint(
|
|
5557
|
+
const verifier = selectVerifierForConstraint(
|
|
5558
|
+
constraint.rule,
|
|
5559
|
+
constraint.verifier,
|
|
5560
|
+
constraint.check
|
|
5561
|
+
);
|
|
5341
5562
|
if (!verifier) continue;
|
|
5342
5563
|
const ctx = {
|
|
5343
5564
|
filePath,
|
|
@@ -5395,7 +5616,7 @@ var lspCommand = new Command12("lsp").description("Start SpecBridge language ser
|
|
|
5395
5616
|
|
|
5396
5617
|
// src/cli/commands/watch.ts
|
|
5397
5618
|
import { Command as Command13 } from "commander";
|
|
5398
|
-
import
|
|
5619
|
+
import chalk14 from "chalk";
|
|
5399
5620
|
import chokidar from "chokidar";
|
|
5400
5621
|
import path4 from "path";
|
|
5401
5622
|
var watchCommand = new Command13("watch").description("Watch for changes and verify files continuously").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("--debounce <ms>", "Debounce verify on rapid changes", "150").action(async (options) => {
|
|
@@ -5416,15 +5637,15 @@ var watchCommand = new Command13("watch").description("Watch for changes and ver
|
|
|
5416
5637
|
files: [absolutePath],
|
|
5417
5638
|
cwd
|
|
5418
5639
|
});
|
|
5419
|
-
const prefix = result.success ?
|
|
5640
|
+
const prefix = result.success ? chalk14.green("\u2713") : chalk14.red("\u2717");
|
|
5420
5641
|
const summary = `${prefix} ${path4.relative(cwd, absolutePath)}: ${result.violations.length} violation(s)`;
|
|
5421
5642
|
console.log(summary);
|
|
5422
5643
|
for (const v of result.violations.slice(0, 20)) {
|
|
5423
5644
|
const loc = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
5424
|
-
console.log(
|
|
5645
|
+
console.log(chalk14.dim(` - ${v.file}${loc}: ${v.message} [${v.severity}]`));
|
|
5425
5646
|
}
|
|
5426
5647
|
if (result.violations.length > 20) {
|
|
5427
|
-
console.log(
|
|
5648
|
+
console.log(chalk14.dim(` \u2026 ${result.violations.length - 20} more`));
|
|
5428
5649
|
}
|
|
5429
5650
|
};
|
|
5430
5651
|
const watcher = chokidar.watch(config.project.sourceRoots, {
|
|
@@ -5433,7 +5654,7 @@ var watchCommand = new Command13("watch").description("Watch for changes and ver
|
|
|
5433
5654
|
ignoreInitial: true,
|
|
5434
5655
|
persistent: true
|
|
5435
5656
|
});
|
|
5436
|
-
console.log(
|
|
5657
|
+
console.log(chalk14.blue("Watching for changes..."));
|
|
5437
5658
|
watcher.on("change", (changedPath) => {
|
|
5438
5659
|
pendingPath = changedPath;
|
|
5439
5660
|
if (timer) clearTimeout(timer);
|
|
@@ -5725,7 +5946,7 @@ var promptCommand = new Command15("prompt").description("Generate AI agent promp
|
|
|
5725
5946
|
|
|
5726
5947
|
// src/cli/commands/analytics.ts
|
|
5727
5948
|
import { Command as Command16 } from "commander";
|
|
5728
|
-
import
|
|
5949
|
+
import chalk15 from "chalk";
|
|
5729
5950
|
import ora7 from "ora";
|
|
5730
5951
|
|
|
5731
5952
|
// src/analytics/engine.ts
|
|
@@ -5915,9 +6136,7 @@ var AnalyticsEngine = class {
|
|
|
5915
6136
|
overallTrend = "down";
|
|
5916
6137
|
}
|
|
5917
6138
|
}
|
|
5918
|
-
const sortedByCompliance = [...latest.byDecision].sort(
|
|
5919
|
-
(a, b) => b.compliance - a.compliance
|
|
5920
|
-
);
|
|
6139
|
+
const sortedByCompliance = [...latest.byDecision].sort((a, b) => b.compliance - a.compliance);
|
|
5921
6140
|
const topDecisions = sortedByCompliance.slice(0, 5).map((d) => ({
|
|
5922
6141
|
decisionId: d.decisionId,
|
|
5923
6142
|
title: d.title,
|
|
@@ -5954,7 +6173,7 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5954
6173
|
const history = await storage.loadHistory(days);
|
|
5955
6174
|
if (history.length === 0) {
|
|
5956
6175
|
spinner.fail("No historical reports found");
|
|
5957
|
-
console.log(
|
|
6176
|
+
console.log(chalk15.yellow("\nGenerate reports with: specbridge report"));
|
|
5958
6177
|
return;
|
|
5959
6178
|
}
|
|
5960
6179
|
spinner.succeed(`Loaded ${history.length} historical report(s)`);
|
|
@@ -5971,17 +6190,19 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5971
6190
|
}
|
|
5972
6191
|
if (decisionId) {
|
|
5973
6192
|
const metrics = await engine.analyzeDecision(decisionId, history);
|
|
5974
|
-
console.log("\n" +
|
|
6193
|
+
console.log("\n" + chalk15.blue.bold(`=== Decision Analytics: ${metrics.title} ===
|
|
5975
6194
|
`));
|
|
5976
|
-
console.log(
|
|
6195
|
+
console.log(chalk15.bold("Overview:"));
|
|
5977
6196
|
console.log(` ID: ${metrics.decisionId}`);
|
|
5978
6197
|
console.log(` Current Violations: ${metrics.totalViolations}`);
|
|
5979
6198
|
console.log(` Average Compliance: ${metrics.averageComplianceScore.toFixed(1)}%`);
|
|
5980
6199
|
const trendEmoji = metrics.trendDirection === "up" ? "\u{1F4C8}" : metrics.trendDirection === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
5981
|
-
const trendColor = metrics.trendDirection === "up" ?
|
|
5982
|
-
console.log(
|
|
6200
|
+
const trendColor = metrics.trendDirection === "up" ? chalk15.green : metrics.trendDirection === "down" ? chalk15.red : chalk15.yellow;
|
|
6201
|
+
console.log(
|
|
6202
|
+
` ${trendColor(`${trendEmoji} Trend: ${metrics.trendDirection.toUpperCase()}`)}`
|
|
6203
|
+
);
|
|
5983
6204
|
if (metrics.history.length > 0) {
|
|
5984
|
-
console.log(
|
|
6205
|
+
console.log(chalk15.bold("\nCompliance History:"));
|
|
5985
6206
|
const recentHistory = metrics.history.slice(-10);
|
|
5986
6207
|
recentHistory.forEach((h) => {
|
|
5987
6208
|
const icon = h.violations === 0 ? "\u2705" : "\u26A0\uFE0F";
|
|
@@ -5990,58 +6211,60 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5990
6211
|
}
|
|
5991
6212
|
} else {
|
|
5992
6213
|
const summary = await engine.generateSummary(history);
|
|
5993
|
-
console.log("\n" +
|
|
5994
|
-
console.log(
|
|
6214
|
+
console.log("\n" + chalk15.blue.bold("=== Overall Analytics ===\n"));
|
|
6215
|
+
console.log(chalk15.bold("Summary:"));
|
|
5995
6216
|
console.log(` Total Decisions: ${summary.totalDecisions}`);
|
|
5996
6217
|
console.log(` Average Compliance: ${summary.averageCompliance}%`);
|
|
5997
6218
|
console.log(` Critical Issues: ${summary.criticalIssues}`);
|
|
5998
6219
|
const trendEmoji = summary.overallTrend === "up" ? "\u{1F4C8}" : summary.overallTrend === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
5999
|
-
const trendColor = summary.overallTrend === "up" ?
|
|
6000
|
-
console.log(
|
|
6220
|
+
const trendColor = summary.overallTrend === "up" ? chalk15.green : summary.overallTrend === "down" ? chalk15.red : chalk15.yellow;
|
|
6221
|
+
console.log(
|
|
6222
|
+
` ${trendColor(`${trendEmoji} Overall Trend: ${summary.overallTrend.toUpperCase()}`)}`
|
|
6223
|
+
);
|
|
6001
6224
|
if (summary.topDecisions.length > 0) {
|
|
6002
|
-
console.log(
|
|
6225
|
+
console.log(chalk15.green("\n\u2705 Top Performing Decisions:"));
|
|
6003
6226
|
summary.topDecisions.forEach((d, i) => {
|
|
6004
6227
|
console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
|
|
6005
6228
|
});
|
|
6006
6229
|
}
|
|
6007
6230
|
if (summary.bottomDecisions.length > 0) {
|
|
6008
|
-
console.log(
|
|
6231
|
+
console.log(chalk15.red("\n\u26A0\uFE0F Decisions Needing Attention:"));
|
|
6009
6232
|
summary.bottomDecisions.forEach((d, i) => {
|
|
6010
6233
|
console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
|
|
6011
6234
|
});
|
|
6012
6235
|
}
|
|
6013
6236
|
if (options.insights || summary.criticalIssues > 0) {
|
|
6014
|
-
console.log(
|
|
6237
|
+
console.log(chalk15.blue.bold("\n=== Insights ===\n"));
|
|
6015
6238
|
const insights = summary.insights;
|
|
6016
6239
|
const warnings = insights.filter((i) => i.type === "warning");
|
|
6017
6240
|
const successes = insights.filter((i) => i.type === "success");
|
|
6018
6241
|
const infos = insights.filter((i) => i.type === "info");
|
|
6019
6242
|
if (warnings.length > 0) {
|
|
6020
|
-
console.log(
|
|
6243
|
+
console.log(chalk15.red("\u26A0\uFE0F Warnings:"));
|
|
6021
6244
|
warnings.forEach((i) => {
|
|
6022
6245
|
console.log(` \u2022 ${i.message}`);
|
|
6023
6246
|
if (i.details) {
|
|
6024
|
-
console.log(
|
|
6247
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6025
6248
|
}
|
|
6026
6249
|
});
|
|
6027
6250
|
console.log("");
|
|
6028
6251
|
}
|
|
6029
6252
|
if (successes.length > 0) {
|
|
6030
|
-
console.log(
|
|
6253
|
+
console.log(chalk15.green("\u2705 Positive Trends:"));
|
|
6031
6254
|
successes.forEach((i) => {
|
|
6032
6255
|
console.log(` \u2022 ${i.message}`);
|
|
6033
6256
|
if (i.details) {
|
|
6034
|
-
console.log(
|
|
6257
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6035
6258
|
}
|
|
6036
6259
|
});
|
|
6037
6260
|
console.log("");
|
|
6038
6261
|
}
|
|
6039
6262
|
if (infos.length > 0) {
|
|
6040
|
-
console.log(
|
|
6263
|
+
console.log(chalk15.blue("\u{1F4A1} Suggestions:"));
|
|
6041
6264
|
infos.forEach((i) => {
|
|
6042
6265
|
console.log(` \u2022 ${i.message}`);
|
|
6043
6266
|
if (i.details) {
|
|
6044
|
-
console.log(
|
|
6267
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6045
6268
|
}
|
|
6046
6269
|
});
|
|
6047
6270
|
console.log("");
|
|
@@ -6051,10 +6274,12 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
6051
6274
|
const latestEntry = history[history.length - 1];
|
|
6052
6275
|
const oldestEntry = history[0];
|
|
6053
6276
|
if (latestEntry && oldestEntry) {
|
|
6054
|
-
console.log(
|
|
6055
|
-
|
|
6277
|
+
console.log(
|
|
6278
|
+
chalk15.gray(`
|
|
6279
|
+
Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`)
|
|
6280
|
+
);
|
|
6056
6281
|
}
|
|
6057
|
-
console.log(
|
|
6282
|
+
console.log(chalk15.gray(`Analyzing ${history.length} report(s) over ${days} days
|
|
6058
6283
|
`));
|
|
6059
6284
|
} catch (error) {
|
|
6060
6285
|
spinner.fail("Analytics failed");
|
|
@@ -6064,7 +6289,7 @@ Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`));
|
|
|
6064
6289
|
|
|
6065
6290
|
// src/cli/commands/dashboard.ts
|
|
6066
6291
|
import { Command as Command17 } from "commander";
|
|
6067
|
-
import
|
|
6292
|
+
import chalk16 from "chalk";
|
|
6068
6293
|
|
|
6069
6294
|
// src/dashboard/server.ts
|
|
6070
6295
|
import express from "express";
|
|
@@ -6083,6 +6308,7 @@ var DashboardServer = class {
|
|
|
6083
6308
|
CACHE_TTL = 6e4;
|
|
6084
6309
|
// 1 minute
|
|
6085
6310
|
refreshInterval = null;
|
|
6311
|
+
logger = getLogger({ module: "dashboard.server" });
|
|
6086
6312
|
constructor(options) {
|
|
6087
6313
|
this.cwd = options.cwd;
|
|
6088
6314
|
this.config = options.config;
|
|
@@ -6098,10 +6324,11 @@ var DashboardServer = class {
|
|
|
6098
6324
|
async start() {
|
|
6099
6325
|
await this.registry.load();
|
|
6100
6326
|
await this.refreshCache();
|
|
6101
|
-
this.refreshInterval = setInterval(
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
|
|
6327
|
+
this.refreshInterval = setInterval(() => {
|
|
6328
|
+
void this.refreshCache().catch((error) => {
|
|
6329
|
+
this.logger.error({ error }, "Background cache refresh failed");
|
|
6330
|
+
});
|
|
6331
|
+
}, this.CACHE_TTL);
|
|
6105
6332
|
}
|
|
6106
6333
|
/**
|
|
6107
6334
|
* Stop the server and clear intervals
|
|
@@ -6122,7 +6349,7 @@ var DashboardServer = class {
|
|
|
6122
6349
|
this.cacheTimestamp = Date.now();
|
|
6123
6350
|
await this.reportStorage.save(report);
|
|
6124
6351
|
} catch (error) {
|
|
6125
|
-
|
|
6352
|
+
this.logger.error({ error }, "Cache refresh failed");
|
|
6126
6353
|
if (!this.cachedReport) {
|
|
6127
6354
|
try {
|
|
6128
6355
|
const stored = await this.reportStorage.loadLatest();
|
|
@@ -6130,7 +6357,7 @@ var DashboardServer = class {
|
|
|
6130
6357
|
this.cachedReport = stored.report;
|
|
6131
6358
|
}
|
|
6132
6359
|
} catch (fallbackError) {
|
|
6133
|
-
|
|
6360
|
+
this.logger.error({ error: fallbackError }, "Failed to load fallback report");
|
|
6134
6361
|
}
|
|
6135
6362
|
}
|
|
6136
6363
|
}
|
|
@@ -6373,11 +6600,13 @@ var DashboardServer = class {
|
|
|
6373
6600
|
*/
|
|
6374
6601
|
setupStaticFiles() {
|
|
6375
6602
|
const publicDir = join12(__dirname, "public");
|
|
6376
|
-
this.app.use(
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6603
|
+
this.app.use(
|
|
6604
|
+
express.static(publicDir, {
|
|
6605
|
+
maxAge: "1h",
|
|
6606
|
+
// Cache static assets
|
|
6607
|
+
etag: true
|
|
6608
|
+
})
|
|
6609
|
+
);
|
|
6381
6610
|
this.app.get("/{*path}", (_req, res) => {
|
|
6382
6611
|
res.sendFile(join12(publicDir, "index.html"));
|
|
6383
6612
|
});
|
|
@@ -6393,7 +6622,7 @@ var dashboardCommand = new Command17("dashboard").description("Start compliance
|
|
|
6393
6622
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
6394
6623
|
throw new NotInitializedError();
|
|
6395
6624
|
}
|
|
6396
|
-
console.log(
|
|
6625
|
+
console.log(chalk16.blue("Starting SpecBridge dashboard..."));
|
|
6397
6626
|
try {
|
|
6398
6627
|
const config = await loadConfig(cwd);
|
|
6399
6628
|
const server = createDashboardServer({ cwd, config });
|
|
@@ -6401,32 +6630,34 @@ var dashboardCommand = new Command17("dashboard").description("Start compliance
|
|
|
6401
6630
|
const port = parseInt(options.port || "3000", 10);
|
|
6402
6631
|
const host = options.host || "localhost";
|
|
6403
6632
|
server.getApp().listen(port, host, () => {
|
|
6404
|
-
console.log(
|
|
6633
|
+
console.log(chalk16.green(`
|
|
6405
6634
|
\u2713 Dashboard running at http://${host}:${port}`));
|
|
6406
|
-
console.log(
|
|
6407
|
-
console.log(
|
|
6408
|
-
console.log(` ${
|
|
6409
|
-
console.log(
|
|
6410
|
-
|
|
6411
|
-
|
|
6635
|
+
console.log(chalk16.gray(" Press Ctrl+C to stop\n"));
|
|
6636
|
+
console.log(chalk16.bold("API Endpoints:"));
|
|
6637
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/health`)} - Health check`);
|
|
6638
|
+
console.log(
|
|
6639
|
+
` ${chalk16.cyan(`http://${host}:${port}/api/report/latest`)} - Latest report (cached)`
|
|
6640
|
+
);
|
|
6641
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/decisions`)} - All decisions`);
|
|
6642
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/analytics/summary`)} - Analytics`);
|
|
6412
6643
|
console.log("");
|
|
6413
6644
|
});
|
|
6414
6645
|
const shutdown = () => {
|
|
6415
|
-
console.log(
|
|
6646
|
+
console.log(chalk16.yellow("\n\nShutting down dashboard..."));
|
|
6416
6647
|
server.stop();
|
|
6417
6648
|
process.exit(0);
|
|
6418
6649
|
};
|
|
6419
6650
|
process.on("SIGINT", shutdown);
|
|
6420
6651
|
process.on("SIGTERM", shutdown);
|
|
6421
6652
|
} catch (error) {
|
|
6422
|
-
console.error(
|
|
6653
|
+
console.error(chalk16.red("Failed to start dashboard:"), error);
|
|
6423
6654
|
throw error;
|
|
6424
6655
|
}
|
|
6425
6656
|
});
|
|
6426
6657
|
|
|
6427
6658
|
// src/cli/commands/impact.ts
|
|
6428
6659
|
import { Command as Command18 } from "commander";
|
|
6429
|
-
import
|
|
6660
|
+
import chalk17 from "chalk";
|
|
6430
6661
|
import ora8 from "ora";
|
|
6431
6662
|
|
|
6432
6663
|
// src/propagation/graph.ts
|
|
@@ -6513,7 +6744,24 @@ var PropagationEngine = class {
|
|
|
6513
6744
|
if (!this.graph) {
|
|
6514
6745
|
await this.initialize(config, options);
|
|
6515
6746
|
}
|
|
6516
|
-
const
|
|
6747
|
+
const graph = this.graph;
|
|
6748
|
+
if (!graph) {
|
|
6749
|
+
return {
|
|
6750
|
+
decision: decisionId,
|
|
6751
|
+
change,
|
|
6752
|
+
affectedFiles: [],
|
|
6753
|
+
estimatedEffort: "low",
|
|
6754
|
+
migrationSteps: [
|
|
6755
|
+
{
|
|
6756
|
+
order: 1,
|
|
6757
|
+
description: "Run verification to confirm all violations resolved",
|
|
6758
|
+
files: [],
|
|
6759
|
+
automated: true
|
|
6760
|
+
}
|
|
6761
|
+
]
|
|
6762
|
+
};
|
|
6763
|
+
}
|
|
6764
|
+
const affectedFilePaths = getAffectedFiles(graph, decisionId);
|
|
6517
6765
|
const verificationEngine = createVerificationEngine(this.registry);
|
|
6518
6766
|
const result = await verificationEngine.verify(config, {
|
|
6519
6767
|
files: affectedFilePaths,
|
|
@@ -6546,10 +6794,7 @@ var PropagationEngine = class {
|
|
|
6546
6794
|
} else {
|
|
6547
6795
|
estimatedEffort = "high";
|
|
6548
6796
|
}
|
|
6549
|
-
const migrationSteps = this.generateMigrationSteps(
|
|
6550
|
-
affectedFiles,
|
|
6551
|
-
totalAutoFixable > 0
|
|
6552
|
-
);
|
|
6797
|
+
const migrationSteps = this.generateMigrationSteps(affectedFiles, totalAutoFixable > 0);
|
|
6553
6798
|
return {
|
|
6554
6799
|
decision: decisionId,
|
|
6555
6800
|
change,
|
|
@@ -6572,9 +6817,7 @@ var PropagationEngine = class {
|
|
|
6572
6817
|
automated: true
|
|
6573
6818
|
});
|
|
6574
6819
|
}
|
|
6575
|
-
const filesWithManualFixes = affectedFiles.filter(
|
|
6576
|
-
(f) => f.violations > f.autoFixable
|
|
6577
|
-
);
|
|
6820
|
+
const filesWithManualFixes = affectedFiles.filter((f) => f.violations > f.autoFixable);
|
|
6578
6821
|
if (filesWithManualFixes.length > 0) {
|
|
6579
6822
|
const highPriority = filesWithManualFixes.filter((f) => f.violations > 5);
|
|
6580
6823
|
const mediumPriority = filesWithManualFixes.filter(
|
|
@@ -6637,8 +6880,8 @@ var impactCommand = new Command18("impact").description("Analyze impact of decis
|
|
|
6637
6880
|
const changeType = options.change || "modified";
|
|
6638
6881
|
if (!["created", "modified", "deprecated"].includes(changeType)) {
|
|
6639
6882
|
spinner.fail();
|
|
6640
|
-
console.error(
|
|
6641
|
-
console.error(
|
|
6883
|
+
console.error(chalk17.red(`Invalid change type: ${changeType}`));
|
|
6884
|
+
console.error(chalk17.dim("Valid types: created, modified, deprecated"));
|
|
6642
6885
|
process.exit(1);
|
|
6643
6886
|
}
|
|
6644
6887
|
spinner.text = `Analyzing impact of ${changeType} decision ${decisionId}...`;
|
|
@@ -6656,44 +6899,44 @@ var impactCommand = new Command18("impact").description("Analyze impact of decis
|
|
|
6656
6899
|
}
|
|
6657
6900
|
});
|
|
6658
6901
|
function printImpactAnalysis(analysis, showSteps) {
|
|
6659
|
-
console.log(
|
|
6902
|
+
console.log(chalk17.bold(`
|
|
6660
6903
|
=== Impact Analysis: ${analysis.decision} ===
|
|
6661
6904
|
`));
|
|
6662
|
-
const changeLabel =
|
|
6905
|
+
const changeLabel = chalk17.cyan(analysis.change);
|
|
6663
6906
|
console.log(`Change Type: ${changeLabel}`);
|
|
6664
|
-
const effortColor = analysis.estimatedEffort === "high" ?
|
|
6907
|
+
const effortColor = analysis.estimatedEffort === "high" ? chalk17.red : analysis.estimatedEffort === "medium" ? chalk17.yellow : chalk17.green;
|
|
6665
6908
|
console.log(`Estimated Effort: ${effortColor(analysis.estimatedEffort.toUpperCase())}
|
|
6666
6909
|
`);
|
|
6667
|
-
console.log(
|
|
6910
|
+
console.log(chalk17.bold(`Affected Files: ${analysis.affectedFiles.length}`));
|
|
6668
6911
|
if (analysis.affectedFiles.length > 0) {
|
|
6669
6912
|
const displayCount = Math.min(analysis.affectedFiles.length, 10);
|
|
6670
6913
|
for (let i = 0; i < displayCount; i++) {
|
|
6671
6914
|
const file = analysis.affectedFiles[i];
|
|
6672
6915
|
if (!file) continue;
|
|
6673
6916
|
const violationText = file.violations === 1 ? "1 violation" : `${file.violations} violations`;
|
|
6674
|
-
const autoFixText = file.autoFixable > 0 ?
|
|
6675
|
-
console.log(` ${
|
|
6917
|
+
const autoFixText = file.autoFixable > 0 ? chalk17.green(` (${file.autoFixable} auto-fixable)`) : "";
|
|
6918
|
+
console.log(` ${chalk17.red("\u25CF")} ${file.path} - ${violationText}${autoFixText}`);
|
|
6676
6919
|
}
|
|
6677
6920
|
if (analysis.affectedFiles.length > displayCount) {
|
|
6678
6921
|
const remaining = analysis.affectedFiles.length - displayCount;
|
|
6679
|
-
console.log(
|
|
6922
|
+
console.log(chalk17.dim(` ... and ${remaining} more file(s)`));
|
|
6680
6923
|
}
|
|
6681
6924
|
} else {
|
|
6682
|
-
console.log(
|
|
6925
|
+
console.log(chalk17.green(" No violations found"));
|
|
6683
6926
|
}
|
|
6684
6927
|
if (showSteps && analysis.migrationSteps && analysis.migrationSteps.length > 0) {
|
|
6685
|
-
console.log(
|
|
6928
|
+
console.log(chalk17.bold("\nMigration Plan:"));
|
|
6686
6929
|
for (const step of analysis.migrationSteps) {
|
|
6687
6930
|
const icon = step.automated ? "\u{1F916}" : "\u{1F464}";
|
|
6688
|
-
const typeLabel = step.automated ?
|
|
6931
|
+
const typeLabel = step.automated ? chalk17.green("[Automated]") : chalk17.yellow("[Manual]");
|
|
6689
6932
|
console.log(` ${icon} Step ${step.order}: ${step.description} ${typeLabel}`);
|
|
6690
6933
|
if (step.files.length > 0) {
|
|
6691
6934
|
const displayFiles = Math.min(step.files.length, 3);
|
|
6692
6935
|
for (let i = 0; i < displayFiles; i++) {
|
|
6693
|
-
console.log(
|
|
6936
|
+
console.log(chalk17.dim(` - ${step.files[i]}`));
|
|
6694
6937
|
}
|
|
6695
6938
|
if (step.files.length > displayFiles) {
|
|
6696
|
-
console.log(
|
|
6939
|
+
console.log(chalk17.dim(` ... and ${step.files.length - displayFiles} more file(s)`));
|
|
6697
6940
|
}
|
|
6698
6941
|
}
|
|
6699
6942
|
console.log("");
|
|
@@ -6702,15 +6945,17 @@ function printImpactAnalysis(analysis, showSteps) {
|
|
|
6702
6945
|
const totalViolations = analysis.affectedFiles.reduce((sum, f) => sum + f.violations, 0);
|
|
6703
6946
|
const totalAutoFixable = analysis.affectedFiles.reduce((sum, f) => sum + f.autoFixable, 0);
|
|
6704
6947
|
const manualFixes = totalViolations - totalAutoFixable;
|
|
6705
|
-
console.log(
|
|
6948
|
+
console.log(chalk17.bold("Summary:"));
|
|
6706
6949
|
console.log(` Total Violations: ${totalViolations}`);
|
|
6707
|
-
console.log(` Auto-fixable: ${
|
|
6708
|
-
console.log(
|
|
6950
|
+
console.log(` Auto-fixable: ${chalk17.green(totalAutoFixable)}`);
|
|
6951
|
+
console.log(
|
|
6952
|
+
` Manual Fixes Required: ${manualFixes > 0 ? chalk17.yellow(manualFixes) : chalk17.green(0)}`
|
|
6953
|
+
);
|
|
6709
6954
|
}
|
|
6710
6955
|
|
|
6711
6956
|
// src/cli/commands/migrate.ts
|
|
6712
6957
|
import { Command as Command19 } from "commander";
|
|
6713
|
-
import
|
|
6958
|
+
import chalk18 from "chalk";
|
|
6714
6959
|
import ora9 from "ora";
|
|
6715
6960
|
import { join as join13 } from "path";
|
|
6716
6961
|
import { readdir as readdir2, copyFile, mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -6721,17 +6966,17 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6721
6966
|
}
|
|
6722
6967
|
const from = options.from || "v1";
|
|
6723
6968
|
const to = options.to || "v2";
|
|
6724
|
-
console.log(
|
|
6969
|
+
console.log(chalk18.blue.bold(`
|
|
6725
6970
|
=== SpecBridge Migration: ${from} \u2192 ${to} ===
|
|
6726
6971
|
`));
|
|
6727
6972
|
if (from !== "v1" && from !== "v1.3") {
|
|
6728
|
-
console.error(
|
|
6729
|
-
console.log(
|
|
6973
|
+
console.error(chalk18.red(`Unsupported source version: ${from}`));
|
|
6974
|
+
console.log(chalk18.gray("Supported: v1, v1.3"));
|
|
6730
6975
|
process.exit(1);
|
|
6731
6976
|
}
|
|
6732
6977
|
if (to !== "v2" && to !== "v2.0") {
|
|
6733
|
-
console.error(
|
|
6734
|
-
console.log(
|
|
6978
|
+
console.error(chalk18.red(`Unsupported target version: ${to}`));
|
|
6979
|
+
console.log(chalk18.gray("Supported: v2, v2.0"));
|
|
6735
6980
|
process.exit(1);
|
|
6736
6981
|
}
|
|
6737
6982
|
const spinner = ora9("Analyzing current configuration...").start();
|
|
@@ -6751,7 +6996,7 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6751
6996
|
const config = await loadConfig(cwd);
|
|
6752
6997
|
const v1Report = await generateReport(config, { cwd, legacyCompliance: true });
|
|
6753
6998
|
v1Compliance = v1Report.summary.compliance;
|
|
6754
|
-
} catch
|
|
6999
|
+
} catch {
|
|
6755
7000
|
spinner.warn("Could not generate v1 baseline report");
|
|
6756
7001
|
}
|
|
6757
7002
|
spinner.text = "Migrating decision files...";
|
|
@@ -6773,7 +7018,7 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6773
7018
|
v2: v2Compliance,
|
|
6774
7019
|
difference: v2Compliance - v1Compliance
|
|
6775
7020
|
};
|
|
6776
|
-
} catch
|
|
7021
|
+
} catch {
|
|
6777
7022
|
spinner.warn("Could not generate v2 comparison report");
|
|
6778
7023
|
}
|
|
6779
7024
|
}
|
|
@@ -6782,41 +7027,49 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6782
7027
|
report.changes.push("All decisions validated successfully");
|
|
6783
7028
|
}
|
|
6784
7029
|
spinner.succeed(options.dryRun ? "Migration preview complete" : "Migration complete");
|
|
6785
|
-
console.log(
|
|
6786
|
-
console.log(
|
|
7030
|
+
console.log(chalk18.green.bold("\n\u2713 Migration Summary:\n"));
|
|
7031
|
+
console.log(chalk18.bold("Backup:"));
|
|
6787
7032
|
console.log(` ${report.backupPath}`);
|
|
6788
7033
|
console.log("");
|
|
6789
|
-
console.log(
|
|
7034
|
+
console.log(chalk18.bold("Changes:"));
|
|
6790
7035
|
for (const change of report.changes) {
|
|
6791
7036
|
console.log(` \u2022 ${change}`);
|
|
6792
7037
|
}
|
|
6793
7038
|
console.log("");
|
|
6794
7039
|
if (report.complianceComparison) {
|
|
6795
|
-
console.log(
|
|
7040
|
+
console.log(chalk18.bold("Compliance Comparison:"));
|
|
6796
7041
|
console.log(` v1.3 formula: ${report.complianceComparison.v1}%`);
|
|
6797
7042
|
console.log(` v2.0 formula: ${report.complianceComparison.v2}%`);
|
|
6798
7043
|
const diff = report.complianceComparison.difference;
|
|
6799
|
-
const diffColor = diff > 0 ?
|
|
7044
|
+
const diffColor = diff > 0 ? chalk18.green : diff < 0 ? chalk18.red : chalk18.yellow;
|
|
6800
7045
|
console.log(` Difference: ${diffColor(`${diff > 0 ? "+" : ""}${diff.toFixed(1)}%`)}`);
|
|
6801
7046
|
console.log("");
|
|
6802
7047
|
if (Math.abs(diff) > 10) {
|
|
6803
|
-
console.log(
|
|
6804
|
-
|
|
7048
|
+
console.log(
|
|
7049
|
+
chalk18.yellow(
|
|
7050
|
+
"\u26A0\uFE0F Note: Compliance score changed significantly due to severity weighting."
|
|
7051
|
+
)
|
|
7052
|
+
);
|
|
7053
|
+
console.log(chalk18.gray(" Consider adjusting CI thresholds if needed.\n"));
|
|
6805
7054
|
}
|
|
6806
7055
|
}
|
|
6807
7056
|
if (options.dryRun) {
|
|
6808
|
-
console.log(
|
|
6809
|
-
console.log(
|
|
7057
|
+
console.log(chalk18.yellow("This was a dry run. No changes were applied."));
|
|
7058
|
+
console.log(chalk18.gray("Run without --dry-run to apply changes.\n"));
|
|
6810
7059
|
} else {
|
|
6811
|
-
console.log(
|
|
6812
|
-
console.log(
|
|
7060
|
+
console.log(chalk18.green("\u2713 Migration successful!"));
|
|
7061
|
+
console.log(
|
|
7062
|
+
chalk18.gray(
|
|
7063
|
+
`
|
|
6813
7064
|
Rollback: Copy files from ${report.backupPath} back to .specbridge/decisions/
|
|
6814
|
-
`
|
|
7065
|
+
`
|
|
7066
|
+
)
|
|
7067
|
+
);
|
|
6815
7068
|
}
|
|
6816
7069
|
} catch (error) {
|
|
6817
7070
|
spinner.fail("Migration failed");
|
|
6818
|
-
console.error(
|
|
6819
|
-
console.log(
|
|
7071
|
+
console.error(chalk18.red("\nError:"), error instanceof Error ? error.message : error);
|
|
7072
|
+
console.log(chalk18.gray("\nNo changes were applied."));
|
|
6820
7073
|
throw error;
|
|
6821
7074
|
}
|
|
6822
7075
|
});
|
|
@@ -6832,10 +7085,7 @@ async function createBackup(cwd, dryRun) {
|
|
|
6832
7085
|
const files = await readdir2(decisionsDir);
|
|
6833
7086
|
for (const file of files) {
|
|
6834
7087
|
if (file.endsWith(".decision.yaml") || file.endsWith(".decision.yml")) {
|
|
6835
|
-
await copyFile(
|
|
6836
|
-
join13(decisionsDir, file),
|
|
6837
|
-
join13(backupDir, file)
|
|
6838
|
-
);
|
|
7088
|
+
await copyFile(join13(decisionsDir, file), join13(backupDir, file));
|
|
6839
7089
|
}
|
|
6840
7090
|
}
|
|
6841
7091
|
}
|
|
@@ -6863,7 +7113,7 @@ async function migrateDecisions(cwd, dryRun) {
|
|
|
6863
7113
|
"$1check:\n$1 verifier: $2"
|
|
6864
7114
|
);
|
|
6865
7115
|
if (dryRun) {
|
|
6866
|
-
console.log(
|
|
7116
|
+
console.log(chalk18.gray(` Would migrate: ${file}`));
|
|
6867
7117
|
updatedCount++;
|
|
6868
7118
|
} else {
|
|
6869
7119
|
await writeFile3(filePath, migratedContent, "utf-8");
|
|
@@ -6878,7 +7128,9 @@ var __dirname2 = dirname5(fileURLToPath4(import.meta.url));
|
|
|
6878
7128
|
var packageJsonPath = join14(__dirname2, "../package.json");
|
|
6879
7129
|
var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
6880
7130
|
var program = new Command20();
|
|
6881
|
-
program.name("specbridge").description(
|
|
7131
|
+
program.name("specbridge").description(
|
|
7132
|
+
"Architecture Decision Runtime - Transform architectural decisions into executable, verifiable constraints"
|
|
7133
|
+
).version(packageJson.version);
|
|
6882
7134
|
program.addCommand(initCommand);
|
|
6883
7135
|
program.addCommand(inferCommand);
|
|
6884
7136
|
program.addCommand(verifyCommand);
|
|
@@ -6901,11 +7153,11 @@ program.exitOverride((err) => {
|
|
|
6901
7153
|
if (err.code === "commander.version") {
|
|
6902
7154
|
process.exit(0);
|
|
6903
7155
|
}
|
|
6904
|
-
console.error(
|
|
7156
|
+
console.error(chalk19.red(formatError(err)));
|
|
6905
7157
|
process.exit(1);
|
|
6906
7158
|
});
|
|
6907
7159
|
program.parseAsync(process.argv).catch((error) => {
|
|
6908
|
-
console.error(
|
|
7160
|
+
console.error(chalk19.red(formatError(error)));
|
|
6909
7161
|
process.exit(1);
|
|
6910
7162
|
});
|
|
6911
7163
|
//# sourceMappingURL=cli.js.map
|