@ipation/specbridge 2.4.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/README.md +2 -0
- package/dist/cli.js +714 -513
- package/dist/cli.js.map +1 -1
- package/dist/index.js +610 -465
- package/dist/index.js.map +1 -1
- package/package.json +8 -4
package/dist/cli.js
CHANGED
|
@@ -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:"));
|
|
@@ -1607,17 +1664,13 @@ var Registry = class {
|
|
|
1607
1664
|
* Get decisions by tag
|
|
1608
1665
|
*/
|
|
1609
1666
|
getByTag(tag) {
|
|
1610
|
-
return this.getAll().filter(
|
|
1611
|
-
(d) => d.metadata.tags?.includes(tag)
|
|
1612
|
-
);
|
|
1667
|
+
return this.getAll().filter((d) => d.metadata.tags?.includes(tag));
|
|
1613
1668
|
}
|
|
1614
1669
|
/**
|
|
1615
1670
|
* Get decisions by owner
|
|
1616
1671
|
*/
|
|
1617
1672
|
getByOwner(owner) {
|
|
1618
|
-
return this.getAll().filter(
|
|
1619
|
-
(d) => d.metadata.owners.includes(owner)
|
|
1620
|
-
);
|
|
1673
|
+
return this.getAll().filter((d) => d.metadata.owners.includes(owner));
|
|
1621
1674
|
}
|
|
1622
1675
|
/**
|
|
1623
1676
|
* Apply filter to decisions
|
|
@@ -1628,21 +1681,15 @@ var Registry = class {
|
|
|
1628
1681
|
return false;
|
|
1629
1682
|
}
|
|
1630
1683
|
if (filter.tags) {
|
|
1631
|
-
const hasTags = filter.tags.some(
|
|
1632
|
-
(tag) => decision.metadata.tags?.includes(tag)
|
|
1633
|
-
);
|
|
1684
|
+
const hasTags = filter.tags.some((tag) => decision.metadata.tags?.includes(tag));
|
|
1634
1685
|
if (!hasTags) return false;
|
|
1635
1686
|
}
|
|
1636
1687
|
if (filter.constraintType) {
|
|
1637
|
-
const hasType = decision.constraints.some(
|
|
1638
|
-
(c) => filter.constraintType?.includes(c.type)
|
|
1639
|
-
);
|
|
1688
|
+
const hasType = decision.constraints.some((c) => filter.constraintType?.includes(c.type));
|
|
1640
1689
|
if (!hasType) return false;
|
|
1641
1690
|
}
|
|
1642
1691
|
if (filter.severity) {
|
|
1643
|
-
const hasSeverity = decision.constraints.some(
|
|
1644
|
-
(c) => filter.severity?.includes(c.severity)
|
|
1645
|
-
);
|
|
1692
|
+
const hasSeverity = decision.constraints.some((c) => filter.severity?.includes(c.severity));
|
|
1646
1693
|
if (!hasSeverity) return false;
|
|
1647
1694
|
}
|
|
1648
1695
|
return true;
|
|
@@ -1737,17 +1784,19 @@ var NamingVerifier = class {
|
|
|
1737
1784
|
for (const classDecl of sourceFile.getClasses()) {
|
|
1738
1785
|
const name = classDecl.getName();
|
|
1739
1786
|
if (name && !pattern.regex.test(name)) {
|
|
1740
|
-
violations.push(
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
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
|
+
);
|
|
1751
1800
|
}
|
|
1752
1801
|
}
|
|
1753
1802
|
}
|
|
@@ -1755,16 +1804,18 @@ var NamingVerifier = class {
|
|
|
1755
1804
|
for (const funcDecl of sourceFile.getFunctions()) {
|
|
1756
1805
|
const name = funcDecl.getName();
|
|
1757
1806
|
if (name && !pattern.regex.test(name)) {
|
|
1758
|
-
violations.push(
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
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
|
+
);
|
|
1768
1819
|
}
|
|
1769
1820
|
}
|
|
1770
1821
|
}
|
|
@@ -1772,16 +1823,18 @@ var NamingVerifier = class {
|
|
|
1772
1823
|
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
1773
1824
|
const name = interfaceDecl.getName();
|
|
1774
1825
|
if (!pattern.regex.test(name)) {
|
|
1775
|
-
violations.push(
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
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
|
+
);
|
|
1785
1838
|
}
|
|
1786
1839
|
}
|
|
1787
1840
|
}
|
|
@@ -1789,16 +1842,18 @@ var NamingVerifier = class {
|
|
|
1789
1842
|
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
1790
1843
|
const name = typeAlias.getName();
|
|
1791
1844
|
if (!pattern.regex.test(name)) {
|
|
1792
|
-
violations.push(
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
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
|
+
);
|
|
1802
1857
|
}
|
|
1803
1858
|
}
|
|
1804
1859
|
}
|
|
@@ -1833,20 +1888,22 @@ var ImportsVerifier = class {
|
|
|
1833
1888
|
const ms = importDecl.getModuleSpecifier();
|
|
1834
1889
|
const start = ms.getStart() + 1;
|
|
1835
1890
|
const end = ms.getEnd() - 1;
|
|
1836
|
-
violations.push(
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
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
|
+
);
|
|
1850
1907
|
}
|
|
1851
1908
|
}
|
|
1852
1909
|
if (rule.includes("barrel") || rule.includes("index")) {
|
|
@@ -1855,16 +1912,18 @@ var ImportsVerifier = class {
|
|
|
1855
1912
|
if (!moduleSpec.startsWith(".")) continue;
|
|
1856
1913
|
if (moduleSpec.match(/\.(ts|js|tsx|jsx)$/) || moduleSpec.match(/\/[^/]+$/)) {
|
|
1857
1914
|
if (!moduleSpec.endsWith("/index") && !moduleSpec.endsWith("index")) {
|
|
1858
|
-
violations.push(
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
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
|
+
);
|
|
1868
1927
|
}
|
|
1869
1928
|
}
|
|
1870
1929
|
}
|
|
@@ -1873,16 +1932,18 @@ var ImportsVerifier = class {
|
|
|
1873
1932
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1874
1933
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1875
1934
|
if (moduleSpec.match(/^\.\.\/\.\.\/\.\.\//)) {
|
|
1876
|
-
violations.push(
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
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
|
+
);
|
|
1886
1947
|
}
|
|
1887
1948
|
}
|
|
1888
1949
|
}
|
|
@@ -1891,16 +1952,18 @@ var ImportsVerifier = class {
|
|
|
1891
1952
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1892
1953
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1893
1954
|
if (moduleSpec.includes(currentFilename.split("/").pop() || "")) {
|
|
1894
|
-
violations.push(
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
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
|
+
);
|
|
1904
1967
|
}
|
|
1905
1968
|
}
|
|
1906
1969
|
}
|
|
@@ -1908,16 +1971,18 @@ var ImportsVerifier = class {
|
|
|
1908
1971
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1909
1972
|
const namespaceImport = importDecl.getNamespaceImport();
|
|
1910
1973
|
if (namespaceImport) {
|
|
1911
|
-
violations.push(
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
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
|
+
);
|
|
1921
1986
|
}
|
|
1922
1987
|
}
|
|
1923
1988
|
}
|
|
@@ -1943,16 +2008,18 @@ var ErrorsVerifier = class {
|
|
|
1943
2008
|
if (!className?.endsWith("Error") && !className?.endsWith("Exception")) continue;
|
|
1944
2009
|
const extendsClause = classDecl.getExtends();
|
|
1945
2010
|
if (!extendsClause) {
|
|
1946
|
-
violations.push(
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
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
|
+
);
|
|
1956
2023
|
} else if (requiredBase) {
|
|
1957
2024
|
const baseName = extendsClause.getText();
|
|
1958
2025
|
if (baseName !== requiredBase && baseName !== "Error") {
|
|
@@ -1967,16 +2034,18 @@ var ErrorsVerifier = class {
|
|
|
1967
2034
|
if (expression) {
|
|
1968
2035
|
const text = expression.getText();
|
|
1969
2036
|
if (text.startsWith("new Error(")) {
|
|
1970
|
-
violations.push(
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
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
|
+
);
|
|
1980
2049
|
}
|
|
1981
2050
|
}
|
|
1982
2051
|
}
|
|
@@ -1990,16 +2059,18 @@ var ErrorsVerifier = class {
|
|
|
1990
2059
|
const block = catchClause.getBlock();
|
|
1991
2060
|
const statements = block.getStatements();
|
|
1992
2061
|
if (statements.length === 0) {
|
|
1993
|
-
violations.push(
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
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
|
+
);
|
|
2003
2074
|
}
|
|
2004
2075
|
}
|
|
2005
2076
|
}
|
|
@@ -2011,16 +2082,18 @@ var ErrorsVerifier = class {
|
|
|
2011
2082
|
const expression = node.getExpression();
|
|
2012
2083
|
const text = expression.getText();
|
|
2013
2084
|
if (text === "console.error" || text === "console.log") {
|
|
2014
|
-
violations.push(
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
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
|
+
);
|
|
2024
2097
|
}
|
|
2025
2098
|
}
|
|
2026
2099
|
});
|
|
@@ -2051,16 +2124,18 @@ var RegexVerifier = class {
|
|
|
2051
2124
|
while ((match = regex.exec(fileText)) !== null) {
|
|
2052
2125
|
const beforeMatch = fileText.substring(0, match.index);
|
|
2053
2126
|
const lineNumber = beforeMatch.split("\n").length;
|
|
2054
|
-
violations.push(
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
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
|
+
);
|
|
2064
2139
|
}
|
|
2065
2140
|
} catch {
|
|
2066
2141
|
}
|
|
@@ -2070,15 +2145,17 @@ var RegexVerifier = class {
|
|
|
2070
2145
|
try {
|
|
2071
2146
|
const regex = new RegExp(patternToRequire);
|
|
2072
2147
|
if (!regex.test(fileText)) {
|
|
2073
|
-
violations.push(
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
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
|
+
);
|
|
2082
2159
|
}
|
|
2083
2160
|
} catch {
|
|
2084
2161
|
}
|
|
@@ -2217,7 +2294,9 @@ function parseBannedDependency(rule) {
|
|
|
2217
2294
|
return value.length > 0 ? value : null;
|
|
2218
2295
|
}
|
|
2219
2296
|
function parseLayerRule(rule) {
|
|
2220
|
-
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
|
+
);
|
|
2221
2300
|
const fromLayer = m?.[1]?.toLowerCase();
|
|
2222
2301
|
const toLayer = m?.[2]?.toLowerCase();
|
|
2223
2302
|
if (!fromLayer || !toLayer) return null;
|
|
@@ -2250,16 +2329,18 @@ var DependencyVerifier = class {
|
|
|
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;
|
|
@@ -2854,7 +2967,10 @@ var PluginLoader = class {
|
|
|
2854
2967
|
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
2855
2968
|
}
|
|
2856
2969
|
if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
|
|
2857
|
-
return {
|
|
2970
|
+
return {
|
|
2971
|
+
success: false,
|
|
2972
|
+
error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
|
|
2973
|
+
};
|
|
2858
2974
|
}
|
|
2859
2975
|
const schema = plugin.paramsSchema;
|
|
2860
2976
|
if (schema.safeParse) {
|
|
@@ -3299,12 +3415,15 @@ var VerificationEngine = class {
|
|
|
3299
3415
|
);
|
|
3300
3416
|
if (!verifier) {
|
|
3301
3417
|
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3302
|
-
this.logger.warn(
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
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"
|
|
3426
|
+
);
|
|
3308
3427
|
warnings.push({
|
|
3309
3428
|
type: "missing_verifier",
|
|
3310
3429
|
message: `No verifier found for constraint (requested: ${requestedVerifier})`,
|
|
@@ -3350,7 +3469,10 @@ var VerificationEngine = class {
|
|
|
3350
3469
|
}
|
|
3351
3470
|
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3352
3471
|
const pluginLoader2 = getPluginLoader();
|
|
3353
|
-
const validationResult = pluginLoader2.validateParams(
|
|
3472
|
+
const validationResult = pluginLoader2.validateParams(
|
|
3473
|
+
constraint.check.verifier,
|
|
3474
|
+
constraint.check.params
|
|
3475
|
+
);
|
|
3354
3476
|
if (!validationResult.success) {
|
|
3355
3477
|
warnings.push({
|
|
3356
3478
|
type: "invalid_params",
|
|
@@ -3410,14 +3532,17 @@ var VerificationEngine = class {
|
|
|
3410
3532
|
} catch (error) {
|
|
3411
3533
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3412
3534
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3413
|
-
this.logger.error(
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
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"
|
|
3545
|
+
);
|
|
3421
3546
|
errors.push({
|
|
3422
3547
|
type: "verifier_exception",
|
|
3423
3548
|
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
@@ -3538,7 +3663,9 @@ var AutofixEngine = class {
|
|
|
3538
3663
|
continue;
|
|
3539
3664
|
}
|
|
3540
3665
|
if (options.interactive) {
|
|
3541
|
-
const ok = await confirmFix(
|
|
3666
|
+
const ok = await confirmFix(
|
|
3667
|
+
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3668
|
+
);
|
|
3542
3669
|
if (!ok) {
|
|
3543
3670
|
skippedViolations++;
|
|
3544
3671
|
continue;
|
|
@@ -3569,7 +3696,11 @@ import { resolve } from "path";
|
|
|
3569
3696
|
var execFileAsync = promisify(execFile);
|
|
3570
3697
|
async function getChangedFiles(cwd) {
|
|
3571
3698
|
try {
|
|
3572
|
-
const { stdout: stdout2 } = await execFileAsync(
|
|
3699
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
3700
|
+
"git",
|
|
3701
|
+
["diff", "--name-only", "--diff-filter=AM", "HEAD"],
|
|
3702
|
+
{ cwd }
|
|
3703
|
+
);
|
|
3573
3704
|
const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3574
3705
|
const abs = [];
|
|
3575
3706
|
for (const file of rel) {
|
|
@@ -3652,7 +3783,10 @@ var ExplainReporter = class {
|
|
|
3652
3783
|
};
|
|
3653
3784
|
|
|
3654
3785
|
// src/cli/commands/verify.ts
|
|
3655
|
-
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) => {
|
|
3656
3790
|
const cwd = process.cwd();
|
|
3657
3791
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
3658
3792
|
throw new NotInitializedError();
|
|
@@ -3772,9 +3906,7 @@ function printResult(result, level) {
|
|
|
3772
3906
|
const typeIcon = getTypeIcon(v.type);
|
|
3773
3907
|
const severityColor = getSeverityColor(v.severity);
|
|
3774
3908
|
const location = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
3775
|
-
console.log(
|
|
3776
|
-
` ${typeIcon} ${severityColor(`[${v.severity}]`)} ${v.message}`
|
|
3777
|
-
);
|
|
3909
|
+
console.log(` ${typeIcon} ${severityColor(`[${v.severity}]`)} ${v.message}`);
|
|
3778
3910
|
console.log(chalk4.dim(` ${v.decisionId}/${v.constraintId}${location}`));
|
|
3779
3911
|
if (v.suggestion) {
|
|
3780
3912
|
console.log(chalk4.cyan(` Suggestion: ${v.suggestion}`));
|
|
@@ -3787,7 +3919,9 @@ function printResult(result, level) {
|
|
|
3787
3919
|
const mediumCount = result.violations.filter((v) => v.severity === "medium").length;
|
|
3788
3920
|
const lowCount = result.violations.filter((v) => v.severity === "low").length;
|
|
3789
3921
|
console.log(chalk4.bold("Summary:"));
|
|
3790
|
-
console.log(
|
|
3922
|
+
console.log(
|
|
3923
|
+
` Files: ${result.checked} checked, ${result.passed} passed, ${result.failed} failed`
|
|
3924
|
+
);
|
|
3791
3925
|
const violationParts = [];
|
|
3792
3926
|
if (criticalCount > 0) violationParts.push(chalk4.red(`${criticalCount} critical`));
|
|
3793
3927
|
if (highCount > 0) violationParts.push(chalk4.yellow(`${highCount} high`));
|
|
@@ -3881,26 +4015,28 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3881
4015
|
(decision.metadata.tags || []).join(", ") || "-"
|
|
3882
4016
|
]);
|
|
3883
4017
|
}
|
|
3884
|
-
console.log(
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
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
|
+
);
|
|
3904
4040
|
console.log(chalk5.dim(`Total: ${decisions.length} decision(s)`));
|
|
3905
4041
|
});
|
|
3906
4042
|
function getStatusColor(status) {
|
|
@@ -4131,13 +4267,23 @@ var validateDecisions = new Command6("validate").description("Validate decision
|
|
|
4131
4267
|
import { Command as Command7 } from "commander";
|
|
4132
4268
|
import chalk8 from "chalk";
|
|
4133
4269
|
import { join as join7 } from "path";
|
|
4134
|
-
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) => {
|
|
4135
4279
|
const cwd = process.cwd();
|
|
4136
4280
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
4137
4281
|
throw new NotInitializedError();
|
|
4138
4282
|
}
|
|
4139
4283
|
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
4140
|
-
console.error(
|
|
4284
|
+
console.error(
|
|
4285
|
+
chalk8.red("Error: Decision ID must be lowercase alphanumeric with hyphens only.")
|
|
4286
|
+
);
|
|
4141
4287
|
process.exit(1);
|
|
4142
4288
|
}
|
|
4143
4289
|
const decisionsDir = getDecisionsDir(cwd);
|
|
@@ -4185,7 +4331,9 @@ var createDecision = new Command7("create").description("Create a new decision f
|
|
|
4185
4331
|
console.log(` 1. Edit the file to add rationale, context, and consequences`);
|
|
4186
4332
|
console.log(` 2. Define constraints with appropriate scopes`);
|
|
4187
4333
|
console.log(` 3. Run ${chalk8.bold("specbridge decision validate")} to check syntax`);
|
|
4188
|
-
console.log(
|
|
4334
|
+
console.log(
|
|
4335
|
+
` 4. Change status from ${chalk8.yellow("draft")} to ${chalk8.green("active")} when ready`
|
|
4336
|
+
);
|
|
4189
4337
|
});
|
|
4190
4338
|
|
|
4191
4339
|
// src/cli/commands/decision/index.ts
|
|
@@ -4227,12 +4375,14 @@ function createHookCommand() {
|
|
|
4227
4375
|
console.log("");
|
|
4228
4376
|
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4229
4377
|
console.log("");
|
|
4230
|
-
console.log(
|
|
4378
|
+
console.log(
|
|
4379
|
+
chalk9.dim(`pre-commit:
|
|
4231
4380
|
commands:
|
|
4232
4381
|
specbridge:
|
|
4233
4382
|
glob: "*.{ts,tsx}"
|
|
4234
4383
|
run: npx specbridge hook run --level commit --files {staged_files}
|
|
4235
|
-
`)
|
|
4384
|
+
`)
|
|
4385
|
+
);
|
|
4236
4386
|
return;
|
|
4237
4387
|
} else {
|
|
4238
4388
|
if (await pathExists(join8(cwd, ".husky"))) {
|
|
@@ -4244,12 +4394,14 @@ function createHookCommand() {
|
|
|
4244
4394
|
console.log("");
|
|
4245
4395
|
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4246
4396
|
console.log("");
|
|
4247
|
-
console.log(
|
|
4397
|
+
console.log(
|
|
4398
|
+
chalk9.dim(`pre-commit:
|
|
4248
4399
|
commands:
|
|
4249
4400
|
specbridge:
|
|
4250
4401
|
glob: "*.{ts,tsx}"
|
|
4251
4402
|
run: npx specbridge hook run --level commit --files {staged_files}
|
|
4252
|
-
`)
|
|
4403
|
+
`)
|
|
4404
|
+
);
|
|
4253
4405
|
return;
|
|
4254
4406
|
} else {
|
|
4255
4407
|
hookPath = join8(cwd, ".git", "hooks", "pre-commit");
|
|
@@ -4291,7 +4443,11 @@ function createHookCommand() {
|
|
|
4291
4443
|
const { promisify: promisify2 } = await import("util");
|
|
4292
4444
|
const execFileAsync2 = promisify2(execFile2);
|
|
4293
4445
|
try {
|
|
4294
|
-
const { stdout: stdout2 } = await execFileAsync2(
|
|
4446
|
+
const { stdout: stdout2 } = await execFileAsync2(
|
|
4447
|
+
"git",
|
|
4448
|
+
["diff", "--cached", "--name-only", "--diff-filter=AM"],
|
|
4449
|
+
{ cwd }
|
|
4450
|
+
);
|
|
4295
4451
|
files = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean).filter((f) => /\.(ts|tsx|js|jsx)$/.test(f));
|
|
4296
4452
|
} catch {
|
|
4297
4453
|
files = [];
|
|
@@ -4393,10 +4549,7 @@ async function generateReport(config, options = {}) {
|
|
|
4393
4549
|
medium: decisionViolations.filter((v) => v.severity === "medium").length,
|
|
4394
4550
|
low: decisionViolations.filter((v) => v.severity === "low").length
|
|
4395
4551
|
};
|
|
4396
|
-
weightedScore = decisionViolations.reduce(
|
|
4397
|
-
(score, v) => score + weights[v.severity],
|
|
4398
|
-
0
|
|
4399
|
-
);
|
|
4552
|
+
weightedScore = decisionViolations.reduce((score, v) => score + weights[v.severity], 0);
|
|
4400
4553
|
compliance = Math.max(0, 100 - weightedScore);
|
|
4401
4554
|
if (decisionViolations.length > 0 && constraintCount > 0) {
|
|
4402
4555
|
const violationRate = decisionViolations.length / constraintCount;
|
|
@@ -4455,10 +4608,14 @@ function formatConsoleReport(report) {
|
|
|
4455
4608
|
lines.push("");
|
|
4456
4609
|
const complianceColor = getComplianceColor(report.summary.compliance);
|
|
4457
4610
|
lines.push(chalk10.bold("Overall Compliance"));
|
|
4458
|
-
lines.push(
|
|
4611
|
+
lines.push(
|
|
4612
|
+
` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`
|
|
4613
|
+
);
|
|
4459
4614
|
lines.push("");
|
|
4460
4615
|
lines.push(chalk10.bold("Summary"));
|
|
4461
|
-
lines.push(
|
|
4616
|
+
lines.push(
|
|
4617
|
+
` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`
|
|
4618
|
+
);
|
|
4462
4619
|
lines.push(` Constraints: ${report.summary.totalConstraints}`);
|
|
4463
4620
|
lines.push("");
|
|
4464
4621
|
lines.push(chalk10.bold("Violations"));
|
|
@@ -4575,7 +4732,9 @@ function formatMarkdownReport(report) {
|
|
|
4575
4732
|
lines.push("");
|
|
4576
4733
|
lines.push("## Summary");
|
|
4577
4734
|
lines.push("");
|
|
4578
|
-
lines.push(
|
|
4735
|
+
lines.push(
|
|
4736
|
+
`- **Active Decisions:** ${report.summary.activeDecisions} / ${report.summary.totalDecisions}`
|
|
4737
|
+
);
|
|
4579
4738
|
lines.push(`- **Total Constraints:** ${report.summary.totalConstraints}`);
|
|
4580
4739
|
lines.push("");
|
|
4581
4740
|
lines.push("### Violations");
|
|
@@ -4739,9 +4898,7 @@ var ReportStorage = class {
|
|
|
4739
4898
|
async function detectDrift(current, previous) {
|
|
4740
4899
|
const byDecision = [];
|
|
4741
4900
|
for (const currDecision of current.byDecision) {
|
|
4742
|
-
const prevDecision = previous.byDecision.find(
|
|
4743
|
-
(d) => d.decisionId === currDecision.decisionId
|
|
4744
|
-
);
|
|
4901
|
+
const prevDecision = previous.byDecision.find((d) => d.decisionId === currDecision.decisionId);
|
|
4745
4902
|
if (!prevDecision) {
|
|
4746
4903
|
byDecision.push({
|
|
4747
4904
|
decisionId: currDecision.decisionId,
|
|
@@ -4934,12 +5091,22 @@ var reportCommand = new Command10("report").description("Generate compliance rep
|
|
|
4934
5091
|
const days = parseInt(options.days || "30", 10);
|
|
4935
5092
|
const history = await storage.loadHistory(days);
|
|
4936
5093
|
if (history.length < 2) {
|
|
4937
|
-
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
|
+
);
|
|
4938
5099
|
} else {
|
|
4939
5100
|
const trend = await analyzeTrend(history);
|
|
4940
|
-
console.log(
|
|
4941
|
-
|
|
4942
|
-
|
|
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
|
+
);
|
|
4943
5110
|
const trendEmoji = trend.overall.trend === "improving" ? "\u{1F4C8}" : trend.overall.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4944
5111
|
const trendColor = trend.overall.trend === "improving" ? chalk11.green : trend.overall.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4945
5112
|
console.log(trendColor(`${trendEmoji} Trend: ${trend.overall.trend.toUpperCase()}`));
|
|
@@ -4947,14 +5114,18 @@ Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.end
|
|
|
4947
5114
|
if (degrading.length > 0) {
|
|
4948
5115
|
console.log(chalk11.red("\n\u26A0\uFE0F Most Degraded Decisions:"));
|
|
4949
5116
|
degrading.forEach((d) => {
|
|
4950
|
-
console.log(
|
|
5117
|
+
console.log(
|
|
5118
|
+
` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (${d.change.toFixed(1)}%)`
|
|
5119
|
+
);
|
|
4951
5120
|
});
|
|
4952
5121
|
}
|
|
4953
5122
|
const improving = trend.decisions.filter((d) => d.trend === "improving").slice(0, 3);
|
|
4954
5123
|
if (improving.length > 0) {
|
|
4955
5124
|
console.log(chalk11.green("\n\u2705 Most Improved Decisions:"));
|
|
4956
5125
|
improving.forEach((d) => {
|
|
4957
|
-
console.log(
|
|
5126
|
+
console.log(
|
|
5127
|
+
` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (+${d.change.toFixed(1)}%)`
|
|
5128
|
+
);
|
|
4958
5129
|
});
|
|
4959
5130
|
}
|
|
4960
5131
|
}
|
|
@@ -4973,9 +5144,13 @@ Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.end
|
|
|
4973
5144
|
return;
|
|
4974
5145
|
}
|
|
4975
5146
|
const drift = await detectDrift(currentEntry.report, previousEntry.report);
|
|
4976
|
-
console.log(
|
|
4977
|
-
|
|
4978
|
-
|
|
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
|
+
);
|
|
4979
5154
|
const driftEmoji = drift.trend === "improving" ? "\u{1F4C8}" : drift.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4980
5155
|
const driftColor = drift.trend === "improving" ? chalk11.green : drift.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4981
5156
|
console.log(driftColor(`${driftEmoji} Overall Trend: ${drift.trend.toUpperCase()}`));
|
|
@@ -4996,8 +5171,10 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4996
5171
|
}
|
|
4997
5172
|
}
|
|
4998
5173
|
if (drift.summary.fixedViolations.total > 0) {
|
|
4999
|
-
console.log(
|
|
5000
|
-
|
|
5174
|
+
console.log(
|
|
5175
|
+
chalk11.green(`
|
|
5176
|
+
\u2705 Fixed Violations: ${drift.summary.fixedViolations.total}`)
|
|
5177
|
+
);
|
|
5001
5178
|
if (drift.summary.fixedViolations.critical > 0) {
|
|
5002
5179
|
console.log(` \u2022 Critical: ${drift.summary.fixedViolations.critical}`);
|
|
5003
5180
|
}
|
|
@@ -5014,7 +5191,9 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
5014
5191
|
if (drift.mostDegraded.length > 0) {
|
|
5015
5192
|
console.log(chalk11.red("\n\u{1F4C9} Most Degraded:"));
|
|
5016
5193
|
drift.mostDegraded.forEach((d) => {
|
|
5017
|
-
console.log(
|
|
5194
|
+
console.log(
|
|
5195
|
+
` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (${d.complianceChange.toFixed(1)}%)`
|
|
5196
|
+
);
|
|
5018
5197
|
if (d.newViolations > 0) {
|
|
5019
5198
|
console.log(` +${d.newViolations} new violation(s)`);
|
|
5020
5199
|
}
|
|
@@ -5023,7 +5202,9 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
5023
5202
|
if (drift.mostImproved.length > 0) {
|
|
5024
5203
|
console.log(chalk11.green("\n\u{1F4C8} Most Improved:"));
|
|
5025
5204
|
drift.mostImproved.forEach((d) => {
|
|
5026
|
-
console.log(
|
|
5205
|
+
console.log(
|
|
5206
|
+
` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (+${d.complianceChange.toFixed(1)}%)`
|
|
5207
|
+
);
|
|
5027
5208
|
if (d.fixedViolations > 0) {
|
|
5028
5209
|
console.log(` -${d.fixedViolations} fixed violation(s)`);
|
|
5029
5210
|
}
|
|
@@ -5056,10 +5237,7 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
5056
5237
|
}
|
|
5057
5238
|
}
|
|
5058
5239
|
if (options.output || options.save) {
|
|
5059
|
-
const outputPath = options.output || join10(
|
|
5060
|
-
getReportsDir(cwd),
|
|
5061
|
-
`health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`
|
|
5062
|
-
);
|
|
5240
|
+
const outputPath = options.output || join10(getReportsDir(cwd), `health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`);
|
|
5063
5241
|
await writeTextFile(outputPath, output);
|
|
5064
5242
|
console.log(chalk11.green(`
|
|
5065
5243
|
Report saved to: ${outputPath}`));
|
|
@@ -5210,7 +5388,14 @@ var contextCommand = new Command11("context").description("Generate architectura
|
|
|
5210
5388
|
import { Command as Command12 } from "commander";
|
|
5211
5389
|
|
|
5212
5390
|
// src/lsp/server.ts
|
|
5213
|
-
import {
|
|
5391
|
+
import {
|
|
5392
|
+
createConnection,
|
|
5393
|
+
ProposedFeatures,
|
|
5394
|
+
TextDocuments,
|
|
5395
|
+
TextDocumentSyncKind,
|
|
5396
|
+
DiagnosticSeverity,
|
|
5397
|
+
CodeActionKind
|
|
5398
|
+
} from "vscode-languageserver/node.js";
|
|
5214
5399
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5215
5400
|
import { fileURLToPath } from "url";
|
|
5216
5401
|
import path3 from "path";
|
|
@@ -5335,7 +5520,8 @@ var SpecBridgeLspServer = class {
|
|
|
5335
5520
|
await getPluginLoader().loadPlugins(this.cwd);
|
|
5336
5521
|
} catch (error) {
|
|
5337
5522
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5338
|
-
if (this.options.verbose)
|
|
5523
|
+
if (this.options.verbose)
|
|
5524
|
+
this.connection.console.error(chalk13.red(`Plugin load failed: ${msg}`));
|
|
5339
5525
|
}
|
|
5340
5526
|
this.registry = createRegistry({ basePath: this.cwd });
|
|
5341
5527
|
await this.registry.load();
|
|
@@ -5348,7 +5534,9 @@ var SpecBridgeLspServer = class {
|
|
|
5348
5534
|
}
|
|
5349
5535
|
}
|
|
5350
5536
|
if (this.options.verbose) {
|
|
5351
|
-
this.connection.console.log(
|
|
5537
|
+
this.connection.console.log(
|
|
5538
|
+
chalk13.dim(`Loaded ${this.decisions.length} active decision(s)`)
|
|
5539
|
+
);
|
|
5352
5540
|
}
|
|
5353
5541
|
} catch (error) {
|
|
5354
5542
|
this.initError = error instanceof Error ? error.message : String(error);
|
|
@@ -5366,7 +5554,11 @@ var SpecBridgeLspServer = class {
|
|
|
5366
5554
|
for (const decision of this.decisions) {
|
|
5367
5555
|
for (const constraint of decision.constraints) {
|
|
5368
5556
|
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd: this.cwd })) continue;
|
|
5369
|
-
const verifier = selectVerifierForConstraint(
|
|
5557
|
+
const verifier = selectVerifierForConstraint(
|
|
5558
|
+
constraint.rule,
|
|
5559
|
+
constraint.verifier,
|
|
5560
|
+
constraint.check
|
|
5561
|
+
);
|
|
5370
5562
|
if (!verifier) continue;
|
|
5371
5563
|
const ctx = {
|
|
5372
5564
|
filePath,
|
|
@@ -5944,9 +6136,7 @@ var AnalyticsEngine = class {
|
|
|
5944
6136
|
overallTrend = "down";
|
|
5945
6137
|
}
|
|
5946
6138
|
}
|
|
5947
|
-
const sortedByCompliance = [...latest.byDecision].sort(
|
|
5948
|
-
(a, b) => b.compliance - a.compliance
|
|
5949
|
-
);
|
|
6139
|
+
const sortedByCompliance = [...latest.byDecision].sort((a, b) => b.compliance - a.compliance);
|
|
5950
6140
|
const topDecisions = sortedByCompliance.slice(0, 5).map((d) => ({
|
|
5951
6141
|
decisionId: d.decisionId,
|
|
5952
6142
|
title: d.title,
|
|
@@ -6008,7 +6198,9 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
6008
6198
|
console.log(` Average Compliance: ${metrics.averageComplianceScore.toFixed(1)}%`);
|
|
6009
6199
|
const trendEmoji = metrics.trendDirection === "up" ? "\u{1F4C8}" : metrics.trendDirection === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
6010
6200
|
const trendColor = metrics.trendDirection === "up" ? chalk15.green : metrics.trendDirection === "down" ? chalk15.red : chalk15.yellow;
|
|
6011
|
-
console.log(
|
|
6201
|
+
console.log(
|
|
6202
|
+
` ${trendColor(`${trendEmoji} Trend: ${metrics.trendDirection.toUpperCase()}`)}`
|
|
6203
|
+
);
|
|
6012
6204
|
if (metrics.history.length > 0) {
|
|
6013
6205
|
console.log(chalk15.bold("\nCompliance History:"));
|
|
6014
6206
|
const recentHistory = metrics.history.slice(-10);
|
|
@@ -6026,7 +6218,9 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
6026
6218
|
console.log(` Critical Issues: ${summary.criticalIssues}`);
|
|
6027
6219
|
const trendEmoji = summary.overallTrend === "up" ? "\u{1F4C8}" : summary.overallTrend === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
6028
6220
|
const trendColor = summary.overallTrend === "up" ? chalk15.green : summary.overallTrend === "down" ? chalk15.red : chalk15.yellow;
|
|
6029
|
-
console.log(
|
|
6221
|
+
console.log(
|
|
6222
|
+
` ${trendColor(`${trendEmoji} Overall Trend: ${summary.overallTrend.toUpperCase()}`)}`
|
|
6223
|
+
);
|
|
6030
6224
|
if (summary.topDecisions.length > 0) {
|
|
6031
6225
|
console.log(chalk15.green("\n\u2705 Top Performing Decisions:"));
|
|
6032
6226
|
summary.topDecisions.forEach((d, i) => {
|
|
@@ -6080,8 +6274,10 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
6080
6274
|
const latestEntry = history[history.length - 1];
|
|
6081
6275
|
const oldestEntry = history[0];
|
|
6082
6276
|
if (latestEntry && oldestEntry) {
|
|
6083
|
-
console.log(
|
|
6084
|
-
|
|
6277
|
+
console.log(
|
|
6278
|
+
chalk15.gray(`
|
|
6279
|
+
Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`)
|
|
6280
|
+
);
|
|
6085
6281
|
}
|
|
6086
6282
|
console.log(chalk15.gray(`Analyzing ${history.length} report(s) over ${days} days
|
|
6087
6283
|
`));
|
|
@@ -6128,14 +6324,11 @@ var DashboardServer = class {
|
|
|
6128
6324
|
async start() {
|
|
6129
6325
|
await this.registry.load();
|
|
6130
6326
|
await this.refreshCache();
|
|
6131
|
-
this.refreshInterval = setInterval(
|
|
6132
|
-
() => {
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
},
|
|
6137
|
-
this.CACHE_TTL
|
|
6138
|
-
);
|
|
6327
|
+
this.refreshInterval = setInterval(() => {
|
|
6328
|
+
void this.refreshCache().catch((error) => {
|
|
6329
|
+
this.logger.error({ error }, "Background cache refresh failed");
|
|
6330
|
+
});
|
|
6331
|
+
}, this.CACHE_TTL);
|
|
6139
6332
|
}
|
|
6140
6333
|
/**
|
|
6141
6334
|
* Stop the server and clear intervals
|
|
@@ -6407,11 +6600,13 @@ var DashboardServer = class {
|
|
|
6407
6600
|
*/
|
|
6408
6601
|
setupStaticFiles() {
|
|
6409
6602
|
const publicDir = join12(__dirname, "public");
|
|
6410
|
-
this.app.use(
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6603
|
+
this.app.use(
|
|
6604
|
+
express.static(publicDir, {
|
|
6605
|
+
maxAge: "1h",
|
|
6606
|
+
// Cache static assets
|
|
6607
|
+
etag: true
|
|
6608
|
+
})
|
|
6609
|
+
);
|
|
6415
6610
|
this.app.get("/{*path}", (_req, res) => {
|
|
6416
6611
|
res.sendFile(join12(publicDir, "index.html"));
|
|
6417
6612
|
});
|
|
@@ -6440,7 +6635,9 @@ var dashboardCommand = new Command17("dashboard").description("Start compliance
|
|
|
6440
6635
|
console.log(chalk16.gray(" Press Ctrl+C to stop\n"));
|
|
6441
6636
|
console.log(chalk16.bold("API Endpoints:"));
|
|
6442
6637
|
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/health`)} - Health check`);
|
|
6443
|
-
console.log(
|
|
6638
|
+
console.log(
|
|
6639
|
+
` ${chalk16.cyan(`http://${host}:${port}/api/report/latest`)} - Latest report (cached)`
|
|
6640
|
+
);
|
|
6444
6641
|
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/decisions`)} - All decisions`);
|
|
6445
6642
|
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/analytics/summary`)} - Analytics`);
|
|
6446
6643
|
console.log("");
|
|
@@ -6597,10 +6794,7 @@ var PropagationEngine = class {
|
|
|
6597
6794
|
} else {
|
|
6598
6795
|
estimatedEffort = "high";
|
|
6599
6796
|
}
|
|
6600
|
-
const migrationSteps = this.generateMigrationSteps(
|
|
6601
|
-
affectedFiles,
|
|
6602
|
-
totalAutoFixable > 0
|
|
6603
|
-
);
|
|
6797
|
+
const migrationSteps = this.generateMigrationSteps(affectedFiles, totalAutoFixable > 0);
|
|
6604
6798
|
return {
|
|
6605
6799
|
decision: decisionId,
|
|
6606
6800
|
change,
|
|
@@ -6623,9 +6817,7 @@ var PropagationEngine = class {
|
|
|
6623
6817
|
automated: true
|
|
6624
6818
|
});
|
|
6625
6819
|
}
|
|
6626
|
-
const filesWithManualFixes = affectedFiles.filter(
|
|
6627
|
-
(f) => f.violations > f.autoFixable
|
|
6628
|
-
);
|
|
6820
|
+
const filesWithManualFixes = affectedFiles.filter((f) => f.violations > f.autoFixable);
|
|
6629
6821
|
if (filesWithManualFixes.length > 0) {
|
|
6630
6822
|
const highPriority = filesWithManualFixes.filter((f) => f.violations > 5);
|
|
6631
6823
|
const mediumPriority = filesWithManualFixes.filter(
|
|
@@ -6756,7 +6948,9 @@ function printImpactAnalysis(analysis, showSteps) {
|
|
|
6756
6948
|
console.log(chalk17.bold("Summary:"));
|
|
6757
6949
|
console.log(` Total Violations: ${totalViolations}`);
|
|
6758
6950
|
console.log(` Auto-fixable: ${chalk17.green(totalAutoFixable)}`);
|
|
6759
|
-
console.log(
|
|
6951
|
+
console.log(
|
|
6952
|
+
` Manual Fixes Required: ${manualFixes > 0 ? chalk17.yellow(manualFixes) : chalk17.green(0)}`
|
|
6953
|
+
);
|
|
6760
6954
|
}
|
|
6761
6955
|
|
|
6762
6956
|
// src/cli/commands/migrate.ts
|
|
@@ -6851,7 +7045,11 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6851
7045
|
console.log(` Difference: ${diffColor(`${diff > 0 ? "+" : ""}${diff.toFixed(1)}%`)}`);
|
|
6852
7046
|
console.log("");
|
|
6853
7047
|
if (Math.abs(diff) > 10) {
|
|
6854
|
-
console.log(
|
|
7048
|
+
console.log(
|
|
7049
|
+
chalk18.yellow(
|
|
7050
|
+
"\u26A0\uFE0F Note: Compliance score changed significantly due to severity weighting."
|
|
7051
|
+
)
|
|
7052
|
+
);
|
|
6855
7053
|
console.log(chalk18.gray(" Consider adjusting CI thresholds if needed.\n"));
|
|
6856
7054
|
}
|
|
6857
7055
|
}
|
|
@@ -6860,9 +7058,13 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6860
7058
|
console.log(chalk18.gray("Run without --dry-run to apply changes.\n"));
|
|
6861
7059
|
} else {
|
|
6862
7060
|
console.log(chalk18.green("\u2713 Migration successful!"));
|
|
6863
|
-
console.log(
|
|
7061
|
+
console.log(
|
|
7062
|
+
chalk18.gray(
|
|
7063
|
+
`
|
|
6864
7064
|
Rollback: Copy files from ${report.backupPath} back to .specbridge/decisions/
|
|
6865
|
-
`
|
|
7065
|
+
`
|
|
7066
|
+
)
|
|
7067
|
+
);
|
|
6866
7068
|
}
|
|
6867
7069
|
} catch (error) {
|
|
6868
7070
|
spinner.fail("Migration failed");
|
|
@@ -6883,10 +7085,7 @@ async function createBackup(cwd, dryRun) {
|
|
|
6883
7085
|
const files = await readdir2(decisionsDir);
|
|
6884
7086
|
for (const file of files) {
|
|
6885
7087
|
if (file.endsWith(".decision.yaml") || file.endsWith(".decision.yml")) {
|
|
6886
|
-
await copyFile(
|
|
6887
|
-
join13(decisionsDir, file),
|
|
6888
|
-
join13(backupDir, file)
|
|
6889
|
-
);
|
|
7088
|
+
await copyFile(join13(decisionsDir, file), join13(backupDir, file));
|
|
6890
7089
|
}
|
|
6891
7090
|
}
|
|
6892
7091
|
}
|
|
@@ -6929,7 +7128,9 @@ var __dirname2 = dirname5(fileURLToPath4(import.meta.url));
|
|
|
6929
7128
|
var packageJsonPath = join14(__dirname2, "../package.json");
|
|
6930
7129
|
var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
|
|
6931
7130
|
var program = new Command20();
|
|
6932
|
-
program.name("specbridge").description(
|
|
7131
|
+
program.name("specbridge").description(
|
|
7132
|
+
"Architecture Decision Runtime - Transform architectural decisions into executable, verifiable constraints"
|
|
7133
|
+
).version(packageJson.version);
|
|
6933
7134
|
program.addCommand(initCommand);
|
|
6934
7135
|
program.addCommand(inferCommand);
|
|
6935
7136
|
program.addCommand(verifyCommand);
|