@ipation/specbridge 2.4.0 → 2.4.3
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 +12 -4
package/dist/index.js
CHANGED
|
@@ -464,12 +464,7 @@ import fg from "fast-glob";
|
|
|
464
464
|
import { minimatch } from "minimatch";
|
|
465
465
|
import { relative, isAbsolute } from "path";
|
|
466
466
|
async function glob(patterns, options = {}) {
|
|
467
|
-
const {
|
|
468
|
-
cwd = process.cwd(),
|
|
469
|
-
ignore = [],
|
|
470
|
-
absolute = false,
|
|
471
|
-
onlyFiles = true
|
|
472
|
-
} = options;
|
|
467
|
+
const { cwd = process.cwd(), ignore = [], absolute = false, onlyFiles = true } = options;
|
|
473
468
|
return fg(patterns, {
|
|
474
469
|
cwd,
|
|
475
470
|
ignore,
|
|
@@ -612,17 +607,13 @@ var Registry = class {
|
|
|
612
607
|
* Get decisions by tag
|
|
613
608
|
*/
|
|
614
609
|
getByTag(tag) {
|
|
615
|
-
return this.getAll().filter(
|
|
616
|
-
(d) => d.metadata.tags?.includes(tag)
|
|
617
|
-
);
|
|
610
|
+
return this.getAll().filter((d) => d.metadata.tags?.includes(tag));
|
|
618
611
|
}
|
|
619
612
|
/**
|
|
620
613
|
* Get decisions by owner
|
|
621
614
|
*/
|
|
622
615
|
getByOwner(owner) {
|
|
623
|
-
return this.getAll().filter(
|
|
624
|
-
(d) => d.metadata.owners.includes(owner)
|
|
625
|
-
);
|
|
616
|
+
return this.getAll().filter((d) => d.metadata.owners.includes(owner));
|
|
626
617
|
}
|
|
627
618
|
/**
|
|
628
619
|
* Apply filter to decisions
|
|
@@ -633,21 +624,15 @@ var Registry = class {
|
|
|
633
624
|
return false;
|
|
634
625
|
}
|
|
635
626
|
if (filter.tags) {
|
|
636
|
-
const hasTags = filter.tags.some(
|
|
637
|
-
(tag) => decision.metadata.tags?.includes(tag)
|
|
638
|
-
);
|
|
627
|
+
const hasTags = filter.tags.some((tag) => decision.metadata.tags?.includes(tag));
|
|
639
628
|
if (!hasTags) return false;
|
|
640
629
|
}
|
|
641
630
|
if (filter.constraintType) {
|
|
642
|
-
const hasType = decision.constraints.some(
|
|
643
|
-
(c) => filter.constraintType?.includes(c.type)
|
|
644
|
-
);
|
|
631
|
+
const hasType = decision.constraints.some((c) => filter.constraintType?.includes(c.type));
|
|
645
632
|
if (!hasType) return false;
|
|
646
633
|
}
|
|
647
634
|
if (filter.severity) {
|
|
648
|
-
const hasSeverity = decision.constraints.some(
|
|
649
|
-
(c) => filter.severity?.includes(c.severity)
|
|
650
|
-
);
|
|
635
|
+
const hasSeverity = decision.constraints.some((c) => filter.severity?.includes(c.severity));
|
|
651
636
|
if (!hasSeverity) return false;
|
|
652
637
|
}
|
|
653
638
|
return true;
|
|
@@ -905,12 +890,24 @@ var FUNCTION_PATTERNS = [
|
|
|
905
890
|
{ convention: "snake_case", regex: /^[a-z][a-z0-9_]*$/, description: "Functions use snake_case" }
|
|
906
891
|
];
|
|
907
892
|
var INTERFACE_PATTERNS = [
|
|
908
|
-
{
|
|
909
|
-
|
|
893
|
+
{
|
|
894
|
+
convention: "PascalCase",
|
|
895
|
+
regex: /^[A-Z][a-zA-Z0-9]*$/,
|
|
896
|
+
description: "Interfaces use PascalCase"
|
|
897
|
+
},
|
|
898
|
+
{
|
|
899
|
+
convention: "IPrefixed",
|
|
900
|
+
regex: /^I[A-Z][a-zA-Z0-9]*$/,
|
|
901
|
+
description: "Interfaces are prefixed with I"
|
|
902
|
+
}
|
|
910
903
|
];
|
|
911
904
|
var TYPE_PATTERNS = [
|
|
912
905
|
{ convention: "PascalCase", regex: /^[A-Z][a-zA-Z0-9]*$/, description: "Types use PascalCase" },
|
|
913
|
-
{
|
|
906
|
+
{
|
|
907
|
+
convention: "TSuffixed",
|
|
908
|
+
regex: /^[A-Z][a-zA-Z0-9]*Type$/,
|
|
909
|
+
description: "Types are suffixed with Type"
|
|
910
|
+
}
|
|
914
911
|
];
|
|
915
912
|
var NamingAnalyzer = class {
|
|
916
913
|
id = "naming";
|
|
@@ -931,7 +928,10 @@ var NamingAnalyzer = class {
|
|
|
931
928
|
analyzeClassNaming(scanner) {
|
|
932
929
|
const classes = scanner.findClasses();
|
|
933
930
|
if (classes.length < 3) return null;
|
|
934
|
-
const matches = this.findBestMatch(
|
|
931
|
+
const matches = this.findBestMatch(
|
|
932
|
+
classes.map((c) => c.name),
|
|
933
|
+
CLASS_PATTERNS
|
|
934
|
+
);
|
|
935
935
|
if (!matches) return null;
|
|
936
936
|
return createPattern(this.id, {
|
|
937
937
|
id: "naming-classes",
|
|
@@ -955,7 +955,10 @@ var NamingAnalyzer = class {
|
|
|
955
955
|
analyzeFunctionNaming(scanner) {
|
|
956
956
|
const functions = scanner.findFunctions();
|
|
957
957
|
if (functions.length < 3) return null;
|
|
958
|
-
const matches = this.findBestMatch(
|
|
958
|
+
const matches = this.findBestMatch(
|
|
959
|
+
functions.map((f) => f.name),
|
|
960
|
+
FUNCTION_PATTERNS
|
|
961
|
+
);
|
|
959
962
|
if (!matches) return null;
|
|
960
963
|
return createPattern(this.id, {
|
|
961
964
|
id: "naming-functions",
|
|
@@ -979,7 +982,10 @@ var NamingAnalyzer = class {
|
|
|
979
982
|
analyzeInterfaceNaming(scanner) {
|
|
980
983
|
const interfaces = scanner.findInterfaces();
|
|
981
984
|
if (interfaces.length < 3) return null;
|
|
982
|
-
const matches = this.findBestMatch(
|
|
985
|
+
const matches = this.findBestMatch(
|
|
986
|
+
interfaces.map((i) => i.name),
|
|
987
|
+
INTERFACE_PATTERNS
|
|
988
|
+
);
|
|
983
989
|
if (!matches) return null;
|
|
984
990
|
return createPattern(this.id, {
|
|
985
991
|
id: "naming-interfaces",
|
|
@@ -1003,7 +1009,10 @@ var NamingAnalyzer = class {
|
|
|
1003
1009
|
analyzeTypeNaming(scanner) {
|
|
1004
1010
|
const types = scanner.findTypeAliases();
|
|
1005
1011
|
if (types.length < 3) return null;
|
|
1006
|
-
const matches = this.findBestMatch(
|
|
1012
|
+
const matches = this.findBestMatch(
|
|
1013
|
+
types.map((t) => t.name),
|
|
1014
|
+
TYPE_PATTERNS
|
|
1015
|
+
);
|
|
1007
1016
|
if (!matches) return null;
|
|
1008
1017
|
return createPattern(this.id, {
|
|
1009
1018
|
id: "naming-types",
|
|
@@ -1092,8 +1101,12 @@ var ImportsAnalyzer = class {
|
|
|
1092
1101
|
analyzeRelativeImports(scanner) {
|
|
1093
1102
|
const imports = scanner.findImports();
|
|
1094
1103
|
const relativeImports = imports.filter((i) => i.module.startsWith("."));
|
|
1095
|
-
const absoluteImports = imports.filter(
|
|
1096
|
-
|
|
1104
|
+
const absoluteImports = imports.filter(
|
|
1105
|
+
(i) => !i.module.startsWith(".") && !i.module.startsWith("@")
|
|
1106
|
+
);
|
|
1107
|
+
const aliasImports = imports.filter(
|
|
1108
|
+
(i) => i.module.startsWith("@/") || i.module.startsWith("~")
|
|
1109
|
+
);
|
|
1097
1110
|
const total = relativeImports.length + absoluteImports.length + aliasImports.length;
|
|
1098
1111
|
if (total < 10) return null;
|
|
1099
1112
|
if (aliasImports.length > relativeImports.length && aliasImports.length >= 5) {
|
|
@@ -1160,18 +1173,20 @@ var ImportsAnalyzer = class {
|
|
|
1160
1173
|
for (const [packageName, data] of moduleCounts) {
|
|
1161
1174
|
if (data.count >= 5) {
|
|
1162
1175
|
const confidence = Math.min(100, 50 + data.count * 2);
|
|
1163
|
-
patterns.push(
|
|
1164
|
-
id
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1176
|
+
patterns.push(
|
|
1177
|
+
createPattern(this.id, {
|
|
1178
|
+
id: `imports-module-${packageName.replace(/[/@]/g, "-")}`,
|
|
1179
|
+
name: `${packageName} Usage`,
|
|
1180
|
+
description: `${packageName} is used across ${data.count} files`,
|
|
1181
|
+
confidence,
|
|
1182
|
+
occurrences: data.count,
|
|
1183
|
+
examples: data.examples.slice(0, 3).map((i) => ({
|
|
1184
|
+
file: i.file,
|
|
1185
|
+
line: i.line,
|
|
1186
|
+
snippet: `import { ${i.named.slice(0, 2).join(", ") || "..."} } from '${i.module}'`
|
|
1187
|
+
}))
|
|
1188
|
+
})
|
|
1189
|
+
);
|
|
1175
1190
|
}
|
|
1176
1191
|
}
|
|
1177
1192
|
return patterns;
|
|
@@ -1216,24 +1231,26 @@ var StructureAnalyzer = class {
|
|
|
1216
1231
|
const count = dirCounts.get(name);
|
|
1217
1232
|
if (count && count >= 3) {
|
|
1218
1233
|
const exampleFiles = files.filter((f) => basename(dirname2(f.path)) === name).slice(0, 3);
|
|
1219
|
-
patterns.push(
|
|
1220
|
-
id
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1234
|
+
patterns.push(
|
|
1235
|
+
createPattern(this.id, {
|
|
1236
|
+
id: `structure-dir-${name}`,
|
|
1237
|
+
name: `${name}/ Directory Convention`,
|
|
1238
|
+
description,
|
|
1239
|
+
confidence: Math.min(100, 60 + count * 5),
|
|
1240
|
+
occurrences: count,
|
|
1241
|
+
examples: exampleFiles.map((f) => ({
|
|
1242
|
+
file: f.path,
|
|
1243
|
+
line: 1,
|
|
1244
|
+
snippet: basename(f.path)
|
|
1245
|
+
})),
|
|
1246
|
+
suggestedConstraint: {
|
|
1247
|
+
type: "convention",
|
|
1248
|
+
rule: `${name.charAt(0).toUpperCase() + name.slice(1)} should be placed in the ${name}/ directory`,
|
|
1249
|
+
severity: "low",
|
|
1250
|
+
scope: `src/**/${name}/**/*.ts`
|
|
1251
|
+
}
|
|
1252
|
+
})
|
|
1253
|
+
);
|
|
1237
1254
|
}
|
|
1238
1255
|
}
|
|
1239
1256
|
return patterns;
|
|
@@ -1243,29 +1260,55 @@ var StructureAnalyzer = class {
|
|
|
1243
1260
|
const suffixPatterns = [
|
|
1244
1261
|
{ suffix: ".test.ts", pattern: /\.test\.ts$/, description: "Test files use .test.ts suffix" },
|
|
1245
1262
|
{ suffix: ".spec.ts", pattern: /\.spec\.ts$/, description: "Test files use .spec.ts suffix" },
|
|
1246
|
-
{
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
{
|
|
1263
|
+
{
|
|
1264
|
+
suffix: ".types.ts",
|
|
1265
|
+
pattern: /\.types\.ts$/,
|
|
1266
|
+
description: "Type definition files use .types.ts suffix"
|
|
1267
|
+
},
|
|
1268
|
+
{
|
|
1269
|
+
suffix: ".utils.ts",
|
|
1270
|
+
pattern: /\.utils\.ts$/,
|
|
1271
|
+
description: "Utility files use .utils.ts suffix"
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
suffix: ".service.ts",
|
|
1275
|
+
pattern: /\.service\.ts$/,
|
|
1276
|
+
description: "Service files use .service.ts suffix"
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
suffix: ".controller.ts",
|
|
1280
|
+
pattern: /\.controller\.ts$/,
|
|
1281
|
+
description: "Controller files use .controller.ts suffix"
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
suffix: ".model.ts",
|
|
1285
|
+
pattern: /\.model\.ts$/,
|
|
1286
|
+
description: "Model files use .model.ts suffix"
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
suffix: ".schema.ts",
|
|
1290
|
+
pattern: /\.schema\.ts$/,
|
|
1291
|
+
description: "Schema files use .schema.ts suffix"
|
|
1292
|
+
}
|
|
1252
1293
|
];
|
|
1253
1294
|
for (const { suffix, pattern, description } of suffixPatterns) {
|
|
1254
1295
|
const matchingFiles = files.filter((f) => pattern.test(f.path));
|
|
1255
1296
|
if (matchingFiles.length >= 3) {
|
|
1256
1297
|
const confidence = Math.min(100, 60 + matchingFiles.length * 3);
|
|
1257
|
-
patterns.push(
|
|
1258
|
-
id
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1298
|
+
patterns.push(
|
|
1299
|
+
createPattern(this.id, {
|
|
1300
|
+
id: `structure-suffix-${suffix.replace(/\./g, "-")}`,
|
|
1301
|
+
name: `${suffix} File Naming`,
|
|
1302
|
+
description,
|
|
1303
|
+
confidence,
|
|
1304
|
+
occurrences: matchingFiles.length,
|
|
1305
|
+
examples: matchingFiles.slice(0, 3).map((f) => ({
|
|
1306
|
+
file: f.path,
|
|
1307
|
+
line: 1,
|
|
1308
|
+
snippet: basename(f.path)
|
|
1309
|
+
}))
|
|
1310
|
+
})
|
|
1311
|
+
);
|
|
1269
1312
|
}
|
|
1270
1313
|
}
|
|
1271
1314
|
return patterns;
|
|
@@ -1643,17 +1686,19 @@ var NamingVerifier = class {
|
|
|
1643
1686
|
for (const classDecl of sourceFile.getClasses()) {
|
|
1644
1687
|
const name = classDecl.getName();
|
|
1645
1688
|
if (name && !pattern.regex.test(name)) {
|
|
1646
|
-
violations.push(
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1689
|
+
violations.push(
|
|
1690
|
+
createViolation({
|
|
1691
|
+
decisionId,
|
|
1692
|
+
constraintId: constraint.id,
|
|
1693
|
+
type: constraint.type,
|
|
1694
|
+
severity: constraint.severity,
|
|
1695
|
+
message: `Class "${name}" does not follow ${pattern.description} naming convention`,
|
|
1696
|
+
file: filePath,
|
|
1697
|
+
line: classDecl.getStartLineNumber(),
|
|
1698
|
+
column: classDecl.getStart() - classDecl.getStartLinePos(),
|
|
1699
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1657
1702
|
}
|
|
1658
1703
|
}
|
|
1659
1704
|
}
|
|
@@ -1661,16 +1706,18 @@ var NamingVerifier = class {
|
|
|
1661
1706
|
for (const funcDecl of sourceFile.getFunctions()) {
|
|
1662
1707
|
const name = funcDecl.getName();
|
|
1663
1708
|
if (name && !pattern.regex.test(name)) {
|
|
1664
|
-
violations.push(
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1709
|
+
violations.push(
|
|
1710
|
+
createViolation({
|
|
1711
|
+
decisionId,
|
|
1712
|
+
constraintId: constraint.id,
|
|
1713
|
+
type: constraint.type,
|
|
1714
|
+
severity: constraint.severity,
|
|
1715
|
+
message: `Function "${name}" does not follow ${pattern.description} naming convention`,
|
|
1716
|
+
file: filePath,
|
|
1717
|
+
line: funcDecl.getStartLineNumber(),
|
|
1718
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1719
|
+
})
|
|
1720
|
+
);
|
|
1674
1721
|
}
|
|
1675
1722
|
}
|
|
1676
1723
|
}
|
|
@@ -1678,16 +1725,18 @@ var NamingVerifier = class {
|
|
|
1678
1725
|
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
1679
1726
|
const name = interfaceDecl.getName();
|
|
1680
1727
|
if (!pattern.regex.test(name)) {
|
|
1681
|
-
violations.push(
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1728
|
+
violations.push(
|
|
1729
|
+
createViolation({
|
|
1730
|
+
decisionId,
|
|
1731
|
+
constraintId: constraint.id,
|
|
1732
|
+
type: constraint.type,
|
|
1733
|
+
severity: constraint.severity,
|
|
1734
|
+
message: `Interface "${name}" does not follow ${pattern.description} naming convention`,
|
|
1735
|
+
file: filePath,
|
|
1736
|
+
line: interfaceDecl.getStartLineNumber(),
|
|
1737
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1738
|
+
})
|
|
1739
|
+
);
|
|
1691
1740
|
}
|
|
1692
1741
|
}
|
|
1693
1742
|
}
|
|
@@ -1695,16 +1744,18 @@ var NamingVerifier = class {
|
|
|
1695
1744
|
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
1696
1745
|
const name = typeAlias.getName();
|
|
1697
1746
|
if (!pattern.regex.test(name)) {
|
|
1698
|
-
violations.push(
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1747
|
+
violations.push(
|
|
1748
|
+
createViolation({
|
|
1749
|
+
decisionId,
|
|
1750
|
+
constraintId: constraint.id,
|
|
1751
|
+
type: constraint.type,
|
|
1752
|
+
severity: constraint.severity,
|
|
1753
|
+
message: `Type "${name}" does not follow ${pattern.description} naming convention`,
|
|
1754
|
+
file: filePath,
|
|
1755
|
+
line: typeAlias.getStartLineNumber(),
|
|
1756
|
+
suggestion: `Rename to follow ${pattern.description}`
|
|
1757
|
+
})
|
|
1758
|
+
);
|
|
1708
1759
|
}
|
|
1709
1760
|
}
|
|
1710
1761
|
}
|
|
@@ -1739,20 +1790,22 @@ var ImportsVerifier = class {
|
|
|
1739
1790
|
const ms = importDecl.getModuleSpecifier();
|
|
1740
1791
|
const start = ms.getStart() + 1;
|
|
1741
1792
|
const end = ms.getEnd() - 1;
|
|
1742
|
-
violations.push(
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1793
|
+
violations.push(
|
|
1794
|
+
createViolation({
|
|
1795
|
+
decisionId,
|
|
1796
|
+
constraintId: constraint.id,
|
|
1797
|
+
type: constraint.type,
|
|
1798
|
+
severity: constraint.severity,
|
|
1799
|
+
message: `Relative import "${moduleSpec}" should include a .js extension`,
|
|
1800
|
+
file: filePath,
|
|
1801
|
+
line: importDecl.getStartLineNumber(),
|
|
1802
|
+
suggestion: `Update to "${suggested}"`,
|
|
1803
|
+
autofix: {
|
|
1804
|
+
description: "Add/normalize .js extension in import specifier",
|
|
1805
|
+
edits: [{ start, end, text: suggested }]
|
|
1806
|
+
}
|
|
1807
|
+
})
|
|
1808
|
+
);
|
|
1756
1809
|
}
|
|
1757
1810
|
}
|
|
1758
1811
|
if (rule.includes("barrel") || rule.includes("index")) {
|
|
@@ -1761,16 +1814,18 @@ var ImportsVerifier = class {
|
|
|
1761
1814
|
if (!moduleSpec.startsWith(".")) continue;
|
|
1762
1815
|
if (moduleSpec.match(/\.(ts|js|tsx|jsx)$/) || moduleSpec.match(/\/[^/]+$/)) {
|
|
1763
1816
|
if (!moduleSpec.endsWith("/index") && !moduleSpec.endsWith("index")) {
|
|
1764
|
-
violations.push(
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1817
|
+
violations.push(
|
|
1818
|
+
createViolation({
|
|
1819
|
+
decisionId,
|
|
1820
|
+
constraintId: constraint.id,
|
|
1821
|
+
type: constraint.type,
|
|
1822
|
+
severity: constraint.severity,
|
|
1823
|
+
message: `Import from "${moduleSpec}" should use barrel (index) import`,
|
|
1824
|
+
file: filePath,
|
|
1825
|
+
line: importDecl.getStartLineNumber(),
|
|
1826
|
+
suggestion: "Import from the parent directory index file instead"
|
|
1827
|
+
})
|
|
1828
|
+
);
|
|
1774
1829
|
}
|
|
1775
1830
|
}
|
|
1776
1831
|
}
|
|
@@ -1779,16 +1834,18 @@ var ImportsVerifier = class {
|
|
|
1779
1834
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1780
1835
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1781
1836
|
if (moduleSpec.match(/^\.\.\/\.\.\/\.\.\//)) {
|
|
1782
|
-
violations.push(
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1837
|
+
violations.push(
|
|
1838
|
+
createViolation({
|
|
1839
|
+
decisionId,
|
|
1840
|
+
constraintId: constraint.id,
|
|
1841
|
+
type: constraint.type,
|
|
1842
|
+
severity: constraint.severity,
|
|
1843
|
+
message: `Deep relative import "${moduleSpec}" should use path alias`,
|
|
1844
|
+
file: filePath,
|
|
1845
|
+
line: importDecl.getStartLineNumber(),
|
|
1846
|
+
suggestion: "Use path alias (e.g., @/module) for deep imports"
|
|
1847
|
+
})
|
|
1848
|
+
);
|
|
1792
1849
|
}
|
|
1793
1850
|
}
|
|
1794
1851
|
}
|
|
@@ -1797,16 +1854,18 @@ var ImportsVerifier = class {
|
|
|
1797
1854
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1798
1855
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1799
1856
|
if (moduleSpec.includes(currentFilename.split("/").pop() || "")) {
|
|
1800
|
-
violations.push(
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1857
|
+
violations.push(
|
|
1858
|
+
createViolation({
|
|
1859
|
+
decisionId,
|
|
1860
|
+
constraintId: constraint.id,
|
|
1861
|
+
type: constraint.type,
|
|
1862
|
+
severity: constraint.severity,
|
|
1863
|
+
message: `Possible circular import detected: "${moduleSpec}"`,
|
|
1864
|
+
file: filePath,
|
|
1865
|
+
line: importDecl.getStartLineNumber(),
|
|
1866
|
+
suggestion: "Review import structure for circular dependencies"
|
|
1867
|
+
})
|
|
1868
|
+
);
|
|
1810
1869
|
}
|
|
1811
1870
|
}
|
|
1812
1871
|
}
|
|
@@ -1814,16 +1873,18 @@ var ImportsVerifier = class {
|
|
|
1814
1873
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1815
1874
|
const namespaceImport = importDecl.getNamespaceImport();
|
|
1816
1875
|
if (namespaceImport) {
|
|
1817
|
-
violations.push(
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1876
|
+
violations.push(
|
|
1877
|
+
createViolation({
|
|
1878
|
+
decisionId,
|
|
1879
|
+
constraintId: constraint.id,
|
|
1880
|
+
type: constraint.type,
|
|
1881
|
+
severity: constraint.severity,
|
|
1882
|
+
message: `Namespace import "* as ${namespaceImport.getText()}" should use named imports`,
|
|
1883
|
+
file: filePath,
|
|
1884
|
+
line: importDecl.getStartLineNumber(),
|
|
1885
|
+
suggestion: "Use specific named imports instead of namespace import"
|
|
1886
|
+
})
|
|
1887
|
+
);
|
|
1827
1888
|
}
|
|
1828
1889
|
}
|
|
1829
1890
|
}
|
|
@@ -1849,16 +1910,18 @@ var ErrorsVerifier = class {
|
|
|
1849
1910
|
if (!className?.endsWith("Error") && !className?.endsWith("Exception")) continue;
|
|
1850
1911
|
const extendsClause = classDecl.getExtends();
|
|
1851
1912
|
if (!extendsClause) {
|
|
1852
|
-
violations.push(
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1913
|
+
violations.push(
|
|
1914
|
+
createViolation({
|
|
1915
|
+
decisionId,
|
|
1916
|
+
constraintId: constraint.id,
|
|
1917
|
+
type: constraint.type,
|
|
1918
|
+
severity: constraint.severity,
|
|
1919
|
+
message: `Error class "${className}" does not extend any base class`,
|
|
1920
|
+
file: filePath,
|
|
1921
|
+
line: classDecl.getStartLineNumber(),
|
|
1922
|
+
suggestion: requiredBase ? `Extend ${requiredBase}` : "Extend a base error class for consistent error handling"
|
|
1923
|
+
})
|
|
1924
|
+
);
|
|
1862
1925
|
} else if (requiredBase) {
|
|
1863
1926
|
const baseName = extendsClause.getText();
|
|
1864
1927
|
if (baseName !== requiredBase && baseName !== "Error") {
|
|
@@ -1873,16 +1936,18 @@ var ErrorsVerifier = class {
|
|
|
1873
1936
|
if (expression) {
|
|
1874
1937
|
const text = expression.getText();
|
|
1875
1938
|
if (text.startsWith("new Error(")) {
|
|
1876
|
-
violations.push(
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1939
|
+
violations.push(
|
|
1940
|
+
createViolation({
|
|
1941
|
+
decisionId,
|
|
1942
|
+
constraintId: constraint.id,
|
|
1943
|
+
type: constraint.type,
|
|
1944
|
+
severity: constraint.severity,
|
|
1945
|
+
message: "Throwing generic Error instead of custom error class",
|
|
1946
|
+
file: filePath,
|
|
1947
|
+
line: node.getStartLineNumber(),
|
|
1948
|
+
suggestion: "Use a custom error class for better error handling"
|
|
1949
|
+
})
|
|
1950
|
+
);
|
|
1886
1951
|
}
|
|
1887
1952
|
}
|
|
1888
1953
|
}
|
|
@@ -1896,16 +1961,18 @@ var ErrorsVerifier = class {
|
|
|
1896
1961
|
const block = catchClause.getBlock();
|
|
1897
1962
|
const statements = block.getStatements();
|
|
1898
1963
|
if (statements.length === 0) {
|
|
1899
|
-
violations.push(
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1964
|
+
violations.push(
|
|
1965
|
+
createViolation({
|
|
1966
|
+
decisionId,
|
|
1967
|
+
constraintId: constraint.id,
|
|
1968
|
+
type: constraint.type,
|
|
1969
|
+
severity: constraint.severity,
|
|
1970
|
+
message: "Empty catch block swallows error without handling",
|
|
1971
|
+
file: filePath,
|
|
1972
|
+
line: catchClause.getStartLineNumber(),
|
|
1973
|
+
suggestion: "Add error handling, logging, or rethrow the error"
|
|
1974
|
+
})
|
|
1975
|
+
);
|
|
1909
1976
|
}
|
|
1910
1977
|
}
|
|
1911
1978
|
}
|
|
@@ -1917,16 +1984,18 @@ var ErrorsVerifier = class {
|
|
|
1917
1984
|
const expression = node.getExpression();
|
|
1918
1985
|
const text = expression.getText();
|
|
1919
1986
|
if (text === "console.error" || text === "console.log") {
|
|
1920
|
-
violations.push(
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1987
|
+
violations.push(
|
|
1988
|
+
createViolation({
|
|
1989
|
+
decisionId,
|
|
1990
|
+
constraintId: constraint.id,
|
|
1991
|
+
type: constraint.type,
|
|
1992
|
+
severity: constraint.severity,
|
|
1993
|
+
message: `Using ${text} instead of proper logging`,
|
|
1994
|
+
file: filePath,
|
|
1995
|
+
line: node.getStartLineNumber(),
|
|
1996
|
+
suggestion: "Use a proper logging library"
|
|
1997
|
+
})
|
|
1998
|
+
);
|
|
1930
1999
|
}
|
|
1931
2000
|
}
|
|
1932
2001
|
});
|
|
@@ -1957,16 +2026,18 @@ var RegexVerifier = class {
|
|
|
1957
2026
|
while ((match = regex.exec(fileText)) !== null) {
|
|
1958
2027
|
const beforeMatch = fileText.substring(0, match.index);
|
|
1959
2028
|
const lineNumber = beforeMatch.split("\n").length;
|
|
1960
|
-
violations.push(
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
2029
|
+
violations.push(
|
|
2030
|
+
createViolation({
|
|
2031
|
+
decisionId,
|
|
2032
|
+
constraintId: constraint.id,
|
|
2033
|
+
type: constraint.type,
|
|
2034
|
+
severity: constraint.severity,
|
|
2035
|
+
message: `Found forbidden pattern: "${match[0]}"`,
|
|
2036
|
+
file: filePath,
|
|
2037
|
+
line: lineNumber,
|
|
2038
|
+
suggestion: `Remove or replace the pattern matching /${patternToForbid}/`
|
|
2039
|
+
})
|
|
2040
|
+
);
|
|
1970
2041
|
}
|
|
1971
2042
|
} catch {
|
|
1972
2043
|
}
|
|
@@ -1976,15 +2047,17 @@ var RegexVerifier = class {
|
|
|
1976
2047
|
try {
|
|
1977
2048
|
const regex = new RegExp(patternToRequire);
|
|
1978
2049
|
if (!regex.test(fileText)) {
|
|
1979
|
-
violations.push(
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
2050
|
+
violations.push(
|
|
2051
|
+
createViolation({
|
|
2052
|
+
decisionId,
|
|
2053
|
+
constraintId: constraint.id,
|
|
2054
|
+
type: constraint.type,
|
|
2055
|
+
severity: constraint.severity,
|
|
2056
|
+
message: `File does not contain required pattern: /${patternToRequire}/`,
|
|
2057
|
+
file: filePath,
|
|
2058
|
+
suggestion: `Add code matching /${patternToRequire}/`
|
|
2059
|
+
})
|
|
2060
|
+
);
|
|
1988
2061
|
}
|
|
1989
2062
|
} catch {
|
|
1990
2063
|
}
|
|
@@ -2123,7 +2196,9 @@ function parseBannedDependency(rule) {
|
|
|
2123
2196
|
return value.length > 0 ? value : null;
|
|
2124
2197
|
}
|
|
2125
2198
|
function parseLayerRule(rule) {
|
|
2126
|
-
const m = rule.match(
|
|
2199
|
+
const m = rule.match(
|
|
2200
|
+
/(\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
|
|
2201
|
+
);
|
|
2127
2202
|
const fromLayer = m?.[1]?.toLowerCase();
|
|
2128
2203
|
const toLayer = m?.[2]?.toLowerCase();
|
|
2129
2204
|
if (!fromLayer || !toLayer) return null;
|
|
@@ -2156,16 +2231,18 @@ var DependencyVerifier = class {
|
|
|
2156
2231
|
if (!scc.includes(current)) continue;
|
|
2157
2232
|
const sorted = [...scc].sort();
|
|
2158
2233
|
if (sorted[0] !== current) continue;
|
|
2159
|
-
violations.push(
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2234
|
+
violations.push(
|
|
2235
|
+
createViolation({
|
|
2236
|
+
decisionId,
|
|
2237
|
+
constraintId: constraint.id,
|
|
2238
|
+
type: constraint.type,
|
|
2239
|
+
severity: constraint.severity,
|
|
2240
|
+
message: `Circular dependency detected across: ${sorted.join(" -> ")}`,
|
|
2241
|
+
file: filePath,
|
|
2242
|
+
line: 1,
|
|
2243
|
+
suggestion: "Break the cycle by extracting shared abstractions or reversing the dependency"
|
|
2244
|
+
})
|
|
2245
|
+
);
|
|
2169
2246
|
}
|
|
2170
2247
|
}
|
|
2171
2248
|
const layerRule = parseLayerRule(rule);
|
|
@@ -2175,16 +2252,18 @@ var DependencyVerifier = class {
|
|
|
2175
2252
|
const resolved = resolveToSourceFilePath(project, projectFilePath, moduleSpec);
|
|
2176
2253
|
if (!resolved) continue;
|
|
2177
2254
|
if (fileInLayer(resolved, layerRule.toLayer)) {
|
|
2178
|
-
violations.push(
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2255
|
+
violations.push(
|
|
2256
|
+
createViolation({
|
|
2257
|
+
decisionId,
|
|
2258
|
+
constraintId: constraint.id,
|
|
2259
|
+
type: constraint.type,
|
|
2260
|
+
severity: constraint.severity,
|
|
2261
|
+
message: `Layer violation: ${layerRule.fromLayer} depends on ${layerRule.toLayer} via import "${moduleSpec}"`,
|
|
2262
|
+
file: filePath,
|
|
2263
|
+
line: importDecl.getStartLineNumber(),
|
|
2264
|
+
suggestion: `Refactor to remove dependency from ${layerRule.fromLayer} to ${layerRule.toLayer}`
|
|
2265
|
+
})
|
|
2266
|
+
);
|
|
2188
2267
|
}
|
|
2189
2268
|
}
|
|
2190
2269
|
}
|
|
@@ -2194,16 +2273,18 @@ var DependencyVerifier = class {
|
|
|
2194
2273
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
2195
2274
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
2196
2275
|
if (moduleSpec.toLowerCase().includes(bannedLower)) {
|
|
2197
|
-
violations.push(
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2276
|
+
violations.push(
|
|
2277
|
+
createViolation({
|
|
2278
|
+
decisionId,
|
|
2279
|
+
constraintId: constraint.id,
|
|
2280
|
+
type: constraint.type,
|
|
2281
|
+
severity: constraint.severity,
|
|
2282
|
+
message: `Banned dependency import detected: "${moduleSpec}"`,
|
|
2283
|
+
file: filePath,
|
|
2284
|
+
line: importDecl.getStartLineNumber(),
|
|
2285
|
+
suggestion: `Remove or replace dependency "${banned}"`
|
|
2286
|
+
})
|
|
2287
|
+
);
|
|
2207
2288
|
}
|
|
2208
2289
|
}
|
|
2209
2290
|
}
|
|
@@ -2214,16 +2295,18 @@ var DependencyVerifier = class {
|
|
|
2214
2295
|
if (!moduleSpec.startsWith(".")) continue;
|
|
2215
2296
|
const depth = (moduleSpec.match(/\.\.\//g) || []).length;
|
|
2216
2297
|
if (depth > maxDepth) {
|
|
2217
|
-
violations.push(
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2298
|
+
violations.push(
|
|
2299
|
+
createViolation({
|
|
2300
|
+
decisionId,
|
|
2301
|
+
constraintId: constraint.id,
|
|
2302
|
+
type: constraint.type,
|
|
2303
|
+
severity: constraint.severity,
|
|
2304
|
+
message: `Import depth ${depth} exceeds maximum ${maxDepth}: "${moduleSpec}"`,
|
|
2305
|
+
file: filePath,
|
|
2306
|
+
line: importDecl.getStartLineNumber(),
|
|
2307
|
+
suggestion: "Use a shallower module boundary (or introduce a public entrypoint for this dependency)"
|
|
2308
|
+
})
|
|
2309
|
+
);
|
|
2227
2310
|
}
|
|
2228
2311
|
}
|
|
2229
2312
|
}
|
|
@@ -2232,7 +2315,9 @@ var DependencyVerifier = class {
|
|
|
2232
2315
|
};
|
|
2233
2316
|
|
|
2234
2317
|
// src/verification/verifiers/complexity.ts
|
|
2235
|
-
import {
|
|
2318
|
+
import {
|
|
2319
|
+
Node as Node4
|
|
2320
|
+
} from "ts-morph";
|
|
2236
2321
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2237
2322
|
function parseLimit(rule, pattern) {
|
|
2238
2323
|
const m = rule.match(pattern);
|
|
@@ -2316,16 +2401,18 @@ var ComplexityVerifier = class {
|
|
|
2316
2401
|
if (maxLines !== null) {
|
|
2317
2402
|
const lineCount = getFileLineCount(sourceFile.getFullText());
|
|
2318
2403
|
if (lineCount > maxLines) {
|
|
2319
|
-
violations.push(
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2404
|
+
violations.push(
|
|
2405
|
+
createViolation({
|
|
2406
|
+
decisionId,
|
|
2407
|
+
constraintId: constraint.id,
|
|
2408
|
+
type: constraint.type,
|
|
2409
|
+
severity: constraint.severity,
|
|
2410
|
+
message: `File has ${lineCount} lines which exceeds maximum ${maxLines}`,
|
|
2411
|
+
file: filePath,
|
|
2412
|
+
line: 1,
|
|
2413
|
+
suggestion: "Split the file into smaller modules"
|
|
2414
|
+
})
|
|
2415
|
+
);
|
|
2329
2416
|
}
|
|
2330
2417
|
}
|
|
2331
2418
|
const functionLikes = [
|
|
@@ -2339,46 +2426,52 @@ var ComplexityVerifier = class {
|
|
|
2339
2426
|
if (maxComplexity !== null) {
|
|
2340
2427
|
const complexity = calculateCyclomaticComplexity(fn);
|
|
2341
2428
|
if (complexity > maxComplexity) {
|
|
2342
|
-
violations.push(
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2429
|
+
violations.push(
|
|
2430
|
+
createViolation({
|
|
2431
|
+
decisionId,
|
|
2432
|
+
constraintId: constraint.id,
|
|
2433
|
+
type: constraint.type,
|
|
2434
|
+
severity: constraint.severity,
|
|
2435
|
+
message: `Function ${fnName} has cyclomatic complexity ${complexity} which exceeds maximum ${maxComplexity}`,
|
|
2436
|
+
file: filePath,
|
|
2437
|
+
line: fn.getStartLineNumber(),
|
|
2438
|
+
suggestion: "Refactor to reduce branching or extract smaller functions"
|
|
2439
|
+
})
|
|
2440
|
+
);
|
|
2352
2441
|
}
|
|
2353
2442
|
}
|
|
2354
2443
|
if (maxParams !== null) {
|
|
2355
2444
|
const paramCount = fn.getParameters().length;
|
|
2356
2445
|
if (paramCount > maxParams) {
|
|
2357
|
-
violations.push(
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2446
|
+
violations.push(
|
|
2447
|
+
createViolation({
|
|
2448
|
+
decisionId,
|
|
2449
|
+
constraintId: constraint.id,
|
|
2450
|
+
type: constraint.type,
|
|
2451
|
+
severity: constraint.severity,
|
|
2452
|
+
message: `Function ${fnName} has ${paramCount} parameters which exceeds maximum ${maxParams}`,
|
|
2453
|
+
file: filePath,
|
|
2454
|
+
line: fn.getStartLineNumber(),
|
|
2455
|
+
suggestion: "Consider grouping parameters into an options object"
|
|
2456
|
+
})
|
|
2457
|
+
);
|
|
2367
2458
|
}
|
|
2368
2459
|
}
|
|
2369
2460
|
if (maxNesting !== null) {
|
|
2370
2461
|
const depth = maxNestingDepth(fn);
|
|
2371
2462
|
if (depth > maxNesting) {
|
|
2372
|
-
violations.push(
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2463
|
+
violations.push(
|
|
2464
|
+
createViolation({
|
|
2465
|
+
decisionId,
|
|
2466
|
+
constraintId: constraint.id,
|
|
2467
|
+
type: constraint.type,
|
|
2468
|
+
severity: constraint.severity,
|
|
2469
|
+
message: `Function ${fnName} has nesting depth ${depth} which exceeds maximum ${maxNesting}`,
|
|
2470
|
+
file: filePath,
|
|
2471
|
+
line: fn.getStartLineNumber(),
|
|
2472
|
+
suggestion: "Reduce nesting by using early returns or extracting functions"
|
|
2473
|
+
})
|
|
2474
|
+
);
|
|
2382
2475
|
}
|
|
2383
2476
|
}
|
|
2384
2477
|
}
|
|
@@ -2414,48 +2507,54 @@ var SecurityVerifier = class {
|
|
|
2414
2507
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
2415
2508
|
const value = init.getText().slice(1, -1);
|
|
2416
2509
|
if (value.length === 0) continue;
|
|
2417
|
-
violations.push(
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2510
|
+
violations.push(
|
|
2511
|
+
createViolation({
|
|
2512
|
+
decisionId,
|
|
2513
|
+
constraintId: constraint.id,
|
|
2514
|
+
type: constraint.type,
|
|
2515
|
+
severity: constraint.severity,
|
|
2516
|
+
message: `Possible hardcoded secret in variable "${name}"`,
|
|
2517
|
+
file: filePath,
|
|
2518
|
+
line: vd.getStartLineNumber(),
|
|
2519
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2520
|
+
})
|
|
2521
|
+
);
|
|
2427
2522
|
}
|
|
2428
2523
|
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2429
2524
|
const propName = pa.getNameNode().getText();
|
|
2430
2525
|
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2431
2526
|
const init = pa.getInitializer();
|
|
2432
2527
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
2433
|
-
violations.push(
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2528
|
+
violations.push(
|
|
2529
|
+
createViolation({
|
|
2530
|
+
decisionId,
|
|
2531
|
+
constraintId: constraint.id,
|
|
2532
|
+
type: constraint.type,
|
|
2533
|
+
severity: constraint.severity,
|
|
2534
|
+
message: `Possible hardcoded secret in object property ${propName}`,
|
|
2535
|
+
file: filePath,
|
|
2536
|
+
line: pa.getStartLineNumber(),
|
|
2537
|
+
suggestion: "Move secrets to environment variables or a secret manager"
|
|
2538
|
+
})
|
|
2539
|
+
);
|
|
2443
2540
|
}
|
|
2444
2541
|
}
|
|
2445
2542
|
if (checkEval) {
|
|
2446
2543
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2447
2544
|
const exprText = call.getExpression().getText();
|
|
2448
2545
|
if (exprText === "eval" || exprText === "Function") {
|
|
2449
|
-
violations.push(
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2546
|
+
violations.push(
|
|
2547
|
+
createViolation({
|
|
2548
|
+
decisionId,
|
|
2549
|
+
constraintId: constraint.id,
|
|
2550
|
+
type: constraint.type,
|
|
2551
|
+
severity: constraint.severity,
|
|
2552
|
+
message: `Unsafe dynamic code execution via ${exprText}()`,
|
|
2553
|
+
file: filePath,
|
|
2554
|
+
line: call.getStartLineNumber(),
|
|
2555
|
+
suggestion: "Avoid eval/Function; use safer alternatives"
|
|
2556
|
+
})
|
|
2557
|
+
);
|
|
2459
2558
|
}
|
|
2460
2559
|
}
|
|
2461
2560
|
}
|
|
@@ -2465,29 +2564,33 @@ var SecurityVerifier = class {
|
|
|
2465
2564
|
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2466
2565
|
if (!propertyAccess) continue;
|
|
2467
2566
|
if (propertyAccess.getName() === "innerHTML") {
|
|
2468
|
-
violations.push(
|
|
2567
|
+
violations.push(
|
|
2568
|
+
createViolation({
|
|
2569
|
+
decisionId,
|
|
2570
|
+
constraintId: constraint.id,
|
|
2571
|
+
type: constraint.type,
|
|
2572
|
+
severity: constraint.severity,
|
|
2573
|
+
message: "Potential XSS: assignment to innerHTML",
|
|
2574
|
+
file: filePath,
|
|
2575
|
+
line: bin.getStartLineNumber(),
|
|
2576
|
+
suggestion: "Prefer textContent or a safe templating/escaping strategy"
|
|
2577
|
+
})
|
|
2578
|
+
);
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2582
|
+
violations.push(
|
|
2583
|
+
createViolation({
|
|
2469
2584
|
decisionId,
|
|
2470
2585
|
constraintId: constraint.id,
|
|
2471
2586
|
type: constraint.type,
|
|
2472
2587
|
severity: constraint.severity,
|
|
2473
|
-
message: "Potential XSS:
|
|
2588
|
+
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2474
2589
|
file: filePath,
|
|
2475
|
-
line:
|
|
2476
|
-
suggestion: "
|
|
2477
|
-
})
|
|
2478
|
-
|
|
2479
|
-
}
|
|
2480
|
-
if (sourceFile.getFullText().includes("dangerouslySetInnerHTML")) {
|
|
2481
|
-
violations.push(createViolation({
|
|
2482
|
-
decisionId,
|
|
2483
|
-
constraintId: constraint.id,
|
|
2484
|
-
type: constraint.type,
|
|
2485
|
-
severity: constraint.severity,
|
|
2486
|
-
message: "Potential XSS: usage of dangerouslySetInnerHTML",
|
|
2487
|
-
file: filePath,
|
|
2488
|
-
line: 1,
|
|
2489
|
-
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2490
|
-
}));
|
|
2590
|
+
line: 1,
|
|
2591
|
+
suggestion: "Avoid dangerouslySetInnerHTML or ensure content is sanitized"
|
|
2592
|
+
})
|
|
2593
|
+
);
|
|
2491
2594
|
}
|
|
2492
2595
|
}
|
|
2493
2596
|
if (checkSql) {
|
|
@@ -2506,31 +2609,35 @@ var SecurityVerifier = class {
|
|
|
2506
2609
|
if (!text.includes("select") && !text.includes("insert") && !text.includes("update") && !text.includes("delete")) {
|
|
2507
2610
|
continue;
|
|
2508
2611
|
}
|
|
2509
|
-
violations.push(
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2612
|
+
violations.push(
|
|
2613
|
+
createViolation({
|
|
2614
|
+
decisionId,
|
|
2615
|
+
constraintId: constraint.id,
|
|
2616
|
+
type: constraint.type,
|
|
2617
|
+
severity: constraint.severity,
|
|
2618
|
+
message: "Potential SQL injection: dynamically constructed SQL query",
|
|
2619
|
+
file: filePath,
|
|
2620
|
+
line: call.getStartLineNumber(),
|
|
2621
|
+
suggestion: "Use parameterized queries / prepared statements"
|
|
2622
|
+
})
|
|
2623
|
+
);
|
|
2519
2624
|
}
|
|
2520
2625
|
}
|
|
2521
2626
|
if (checkProto) {
|
|
2522
2627
|
const text = sourceFile.getFullText();
|
|
2523
2628
|
if (text.includes("__proto__") || text.includes("constructor.prototype")) {
|
|
2524
|
-
violations.push(
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2629
|
+
violations.push(
|
|
2630
|
+
createViolation({
|
|
2631
|
+
decisionId,
|
|
2632
|
+
constraintId: constraint.id,
|
|
2633
|
+
type: constraint.type,
|
|
2634
|
+
severity: constraint.severity,
|
|
2635
|
+
message: "Potential prototype pollution pattern detected",
|
|
2636
|
+
file: filePath,
|
|
2637
|
+
line: 1,
|
|
2638
|
+
suggestion: "Avoid writing to __proto__/prototype; validate object keys"
|
|
2639
|
+
})
|
|
2640
|
+
);
|
|
2534
2641
|
}
|
|
2535
2642
|
}
|
|
2536
2643
|
return violations;
|
|
@@ -2570,16 +2677,18 @@ var ApiVerifier = class {
|
|
|
2570
2677
|
const pathValue = stringLiteral.getLiteralValue();
|
|
2571
2678
|
if (typeof pathValue !== "string") continue;
|
|
2572
2679
|
if (!isKebabPath(pathValue)) {
|
|
2573
|
-
violations.push(
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2680
|
+
violations.push(
|
|
2681
|
+
createViolation({
|
|
2682
|
+
decisionId,
|
|
2683
|
+
constraintId: constraint.id,
|
|
2684
|
+
type: constraint.type,
|
|
2685
|
+
severity: constraint.severity,
|
|
2686
|
+
message: `Endpoint path "${pathValue}" is not kebab-case`,
|
|
2687
|
+
file: filePath,
|
|
2688
|
+
line: call.getStartLineNumber(),
|
|
2689
|
+
suggestion: "Use lowercase and hyphens in static path segments (e.g., /user-settings)"
|
|
2690
|
+
})
|
|
2691
|
+
);
|
|
2583
2692
|
}
|
|
2584
2693
|
}
|
|
2585
2694
|
return violations;
|
|
@@ -2760,7 +2869,10 @@ var PluginLoader = class {
|
|
|
2760
2869
|
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
2761
2870
|
}
|
|
2762
2871
|
if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
|
|
2763
|
-
return {
|
|
2872
|
+
return {
|
|
2873
|
+
success: false,
|
|
2874
|
+
error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
|
|
2875
|
+
};
|
|
2764
2876
|
}
|
|
2765
2877
|
const schema = plugin.paramsSchema;
|
|
2766
2878
|
if (schema.safeParse) {
|
|
@@ -3208,12 +3320,15 @@ var VerificationEngine = class {
|
|
|
3208
3320
|
);
|
|
3209
3321
|
if (!verifier) {
|
|
3210
3322
|
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3211
|
-
this.logger.warn(
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3323
|
+
this.logger.warn(
|
|
3324
|
+
{
|
|
3325
|
+
decisionId: decision.metadata.id,
|
|
3326
|
+
constraintId: constraint.id,
|
|
3327
|
+
requestedVerifier,
|
|
3328
|
+
availableVerifiers: getVerifierIds()
|
|
3329
|
+
},
|
|
3330
|
+
"No verifier found for constraint"
|
|
3331
|
+
);
|
|
3217
3332
|
warnings.push({
|
|
3218
3333
|
type: "missing_verifier",
|
|
3219
3334
|
message: `No verifier found for constraint (requested: ${requestedVerifier})`,
|
|
@@ -3259,7 +3374,10 @@ var VerificationEngine = class {
|
|
|
3259
3374
|
}
|
|
3260
3375
|
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3261
3376
|
const pluginLoader2 = getPluginLoader();
|
|
3262
|
-
const validationResult = pluginLoader2.validateParams(
|
|
3377
|
+
const validationResult = pluginLoader2.validateParams(
|
|
3378
|
+
constraint.check.verifier,
|
|
3379
|
+
constraint.check.params
|
|
3380
|
+
);
|
|
3263
3381
|
if (!validationResult.success) {
|
|
3264
3382
|
warnings.push({
|
|
3265
3383
|
type: "invalid_params",
|
|
@@ -3319,14 +3437,17 @@ var VerificationEngine = class {
|
|
|
3319
3437
|
} catch (error) {
|
|
3320
3438
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3321
3439
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3322
|
-
this.logger.error(
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3440
|
+
this.logger.error(
|
|
3441
|
+
{
|
|
3442
|
+
verifierId: verifier.id,
|
|
3443
|
+
filePath,
|
|
3444
|
+
decisionId: decision.metadata.id,
|
|
3445
|
+
constraintId: constraint.id,
|
|
3446
|
+
error: errorMessage,
|
|
3447
|
+
stack: errorStack
|
|
3448
|
+
},
|
|
3449
|
+
"Verifier execution failed"
|
|
3450
|
+
);
|
|
3330
3451
|
errors.push({
|
|
3331
3452
|
type: "verifier_exception",
|
|
3332
3453
|
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
@@ -3391,7 +3512,11 @@ import { resolve } from "path";
|
|
|
3391
3512
|
var execFileAsync = promisify(execFile);
|
|
3392
3513
|
async function getChangedFiles(cwd) {
|
|
3393
3514
|
try {
|
|
3394
|
-
const { stdout: stdout2 } = await execFileAsync(
|
|
3515
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
3516
|
+
"git",
|
|
3517
|
+
["diff", "--name-only", "--diff-filter=AM", "HEAD"],
|
|
3518
|
+
{ cwd }
|
|
3519
|
+
);
|
|
3395
3520
|
const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3396
3521
|
const abs = [];
|
|
3397
3522
|
for (const file of rel) {
|
|
@@ -3467,7 +3592,9 @@ var AutofixEngine = class {
|
|
|
3467
3592
|
continue;
|
|
3468
3593
|
}
|
|
3469
3594
|
if (options.interactive) {
|
|
3470
|
-
const ok = await confirmFix(
|
|
3595
|
+
const ok = await confirmFix(
|
|
3596
|
+
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3597
|
+
);
|
|
3471
3598
|
if (!ok) {
|
|
3472
3599
|
skippedViolations++;
|
|
3473
3600
|
continue;
|
|
@@ -3644,10 +3771,7 @@ var PropagationEngine = class {
|
|
|
3644
3771
|
} else {
|
|
3645
3772
|
estimatedEffort = "high";
|
|
3646
3773
|
}
|
|
3647
|
-
const migrationSteps = this.generateMigrationSteps(
|
|
3648
|
-
affectedFiles,
|
|
3649
|
-
totalAutoFixable > 0
|
|
3650
|
-
);
|
|
3774
|
+
const migrationSteps = this.generateMigrationSteps(affectedFiles, totalAutoFixable > 0);
|
|
3651
3775
|
return {
|
|
3652
3776
|
decision: decisionId,
|
|
3653
3777
|
change,
|
|
@@ -3670,9 +3794,7 @@ var PropagationEngine = class {
|
|
|
3670
3794
|
automated: true
|
|
3671
3795
|
});
|
|
3672
3796
|
}
|
|
3673
|
-
const filesWithManualFixes = affectedFiles.filter(
|
|
3674
|
-
(f) => f.violations > f.autoFixable
|
|
3675
|
-
);
|
|
3797
|
+
const filesWithManualFixes = affectedFiles.filter((f) => f.violations > f.autoFixable);
|
|
3676
3798
|
if (filesWithManualFixes.length > 0) {
|
|
3677
3799
|
const highPriority = filesWithManualFixes.filter((f) => f.violations > 5);
|
|
3678
3800
|
const mediumPriority = filesWithManualFixes.filter(
|
|
@@ -3752,10 +3874,7 @@ async function generateReport(config, options = {}) {
|
|
|
3752
3874
|
medium: decisionViolations.filter((v) => v.severity === "medium").length,
|
|
3753
3875
|
low: decisionViolations.filter((v) => v.severity === "low").length
|
|
3754
3876
|
};
|
|
3755
|
-
weightedScore = decisionViolations.reduce(
|
|
3756
|
-
(score, v) => score + weights[v.severity],
|
|
3757
|
-
0
|
|
3758
|
-
);
|
|
3877
|
+
weightedScore = decisionViolations.reduce((score, v) => score + weights[v.severity], 0);
|
|
3759
3878
|
compliance = Math.max(0, 100 - weightedScore);
|
|
3760
3879
|
if (decisionViolations.length > 0 && constraintCount > 0) {
|
|
3761
3880
|
const violationRate = decisionViolations.length / constraintCount;
|
|
@@ -3875,7 +3994,9 @@ var Reporter = class {
|
|
|
3875
3994
|
lines.push("Summary:");
|
|
3876
3995
|
lines.push(` Decisions Checked: ${result.summary.decisionsChecked || 0}`);
|
|
3877
3996
|
lines.push(` Files Checked: ${result.summary.filesChecked || 0}`);
|
|
3878
|
-
lines.push(
|
|
3997
|
+
lines.push(
|
|
3998
|
+
` Total Violations: ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
3999
|
+
);
|
|
3879
4000
|
lines.push(` Critical: ${result.summary.critical || 0}`);
|
|
3880
4001
|
lines.push(` High: ${result.summary.high || 0}`);
|
|
3881
4002
|
lines.push(` Medium: ${result.summary.medium || 0}`);
|
|
@@ -3889,7 +4010,9 @@ var Reporter = class {
|
|
|
3889
4010
|
lines.push("-".repeat(50));
|
|
3890
4011
|
result.violations.forEach((v) => {
|
|
3891
4012
|
const severity = v.severity.toLowerCase();
|
|
3892
|
-
lines.push(
|
|
4013
|
+
lines.push(
|
|
4014
|
+
` [${v.severity.toUpperCase()}] ${v.decisionId} - ${v.constraintId} (${severity})`
|
|
4015
|
+
);
|
|
3893
4016
|
lines.push(` ${v.message}`);
|
|
3894
4017
|
const file = v.location?.file || v.file;
|
|
3895
4018
|
const line = v.location?.line || v.line || 0;
|
|
@@ -3910,7 +4033,9 @@ var Reporter = class {
|
|
|
3910
4033
|
lines.push("");
|
|
3911
4034
|
if (result.summary) {
|
|
3912
4035
|
lines.push("Summary:");
|
|
3913
|
-
lines.push(
|
|
4036
|
+
lines.push(
|
|
4037
|
+
` Total Violations: ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
4038
|
+
);
|
|
3914
4039
|
lines.push("");
|
|
3915
4040
|
}
|
|
3916
4041
|
if (result.violations && result.violations.length > 0) {
|
|
@@ -3964,7 +4089,9 @@ var Reporter = class {
|
|
|
3964
4089
|
lines.push("### Summary\n");
|
|
3965
4090
|
lines.push(`- **Decisions Checked:** ${result.summary.decisionsChecked || 0}`);
|
|
3966
4091
|
lines.push(`- **Files Checked:** ${result.summary.filesChecked || 0}`);
|
|
3967
|
-
lines.push(
|
|
4092
|
+
lines.push(
|
|
4093
|
+
`- **Total Violations:** ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
4094
|
+
);
|
|
3968
4095
|
lines.push(`- **Critical:** ${result.summary.critical || 0}`);
|
|
3969
4096
|
lines.push(`- **High:** ${result.summary.high || 0}`);
|
|
3970
4097
|
lines.push(`- **Medium:** ${result.summary.medium || 0}`);
|
|
@@ -4000,10 +4127,14 @@ function formatConsoleReport(report) {
|
|
|
4000
4127
|
lines.push("");
|
|
4001
4128
|
const complianceColor = getComplianceColor(report.summary.compliance);
|
|
4002
4129
|
lines.push(chalk.bold("Overall Compliance"));
|
|
4003
|
-
lines.push(
|
|
4130
|
+
lines.push(
|
|
4131
|
+
` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`
|
|
4132
|
+
);
|
|
4004
4133
|
lines.push("");
|
|
4005
4134
|
lines.push(chalk.bold("Summary"));
|
|
4006
|
-
lines.push(
|
|
4135
|
+
lines.push(
|
|
4136
|
+
` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`
|
|
4137
|
+
);
|
|
4007
4138
|
lines.push(` Constraints: ${report.summary.totalConstraints}`);
|
|
4008
4139
|
lines.push("");
|
|
4009
4140
|
lines.push(chalk.bold("Violations"));
|
|
@@ -4120,7 +4251,9 @@ function formatMarkdownReport(report) {
|
|
|
4120
4251
|
lines.push("");
|
|
4121
4252
|
lines.push("## Summary");
|
|
4122
4253
|
lines.push("");
|
|
4123
|
-
lines.push(
|
|
4254
|
+
lines.push(
|
|
4255
|
+
`- **Active Decisions:** ${report.summary.activeDecisions} / ${report.summary.totalDecisions}`
|
|
4256
|
+
);
|
|
4124
4257
|
lines.push(`- **Total Constraints:** ${report.summary.totalConstraints}`);
|
|
4125
4258
|
lines.push("");
|
|
4126
4259
|
lines.push("### Violations");
|
|
@@ -4284,9 +4417,7 @@ var ReportStorage = class {
|
|
|
4284
4417
|
async function detectDrift(current, previous) {
|
|
4285
4418
|
const byDecision = [];
|
|
4286
4419
|
for (const currDecision of current.byDecision) {
|
|
4287
|
-
const prevDecision = previous.byDecision.find(
|
|
4288
|
-
(d) => d.decisionId === currDecision.decisionId
|
|
4289
|
-
);
|
|
4420
|
+
const prevDecision = previous.byDecision.find((d) => d.decisionId === currDecision.decisionId);
|
|
4290
4421
|
if (!prevDecision) {
|
|
4291
4422
|
byDecision.push({
|
|
4292
4423
|
decisionId: currDecision.decisionId,
|
|
@@ -5067,9 +5198,7 @@ var AnalyticsEngine = class {
|
|
|
5067
5198
|
overallTrend = "down";
|
|
5068
5199
|
}
|
|
5069
5200
|
}
|
|
5070
|
-
const sortedByCompliance = [...latest.byDecision].sort(
|
|
5071
|
-
(a, b) => b.compliance - a.compliance
|
|
5072
|
-
);
|
|
5201
|
+
const sortedByCompliance = [...latest.byDecision].sort((a, b) => b.compliance - a.compliance);
|
|
5073
5202
|
const topDecisions = sortedByCompliance.slice(0, 5).map((d) => ({
|
|
5074
5203
|
decisionId: d.decisionId,
|
|
5075
5204
|
title: d.title,
|
|
@@ -5126,14 +5255,11 @@ var DashboardServer = class {
|
|
|
5126
5255
|
async start() {
|
|
5127
5256
|
await this.registry.load();
|
|
5128
5257
|
await this.refreshCache();
|
|
5129
|
-
this.refreshInterval = setInterval(
|
|
5130
|
-
() => {
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
},
|
|
5135
|
-
this.CACHE_TTL
|
|
5136
|
-
);
|
|
5258
|
+
this.refreshInterval = setInterval(() => {
|
|
5259
|
+
void this.refreshCache().catch((error) => {
|
|
5260
|
+
this.logger.error({ error }, "Background cache refresh failed");
|
|
5261
|
+
});
|
|
5262
|
+
}, this.CACHE_TTL);
|
|
5137
5263
|
}
|
|
5138
5264
|
/**
|
|
5139
5265
|
* Stop the server and clear intervals
|
|
@@ -5405,11 +5531,13 @@ var DashboardServer = class {
|
|
|
5405
5531
|
*/
|
|
5406
5532
|
setupStaticFiles() {
|
|
5407
5533
|
const publicDir = join5(__dirname, "public");
|
|
5408
|
-
this.app.use(
|
|
5409
|
-
|
|
5410
|
-
|
|
5411
|
-
|
|
5412
|
-
|
|
5534
|
+
this.app.use(
|
|
5535
|
+
express.static(publicDir, {
|
|
5536
|
+
maxAge: "1h",
|
|
5537
|
+
// Cache static assets
|
|
5538
|
+
etag: true
|
|
5539
|
+
})
|
|
5540
|
+
);
|
|
5413
5541
|
this.app.get("/{*path}", (_req, res) => {
|
|
5414
5542
|
res.sendFile(join5(publicDir, "index.html"));
|
|
5415
5543
|
});
|
|
@@ -5420,7 +5548,14 @@ function createDashboardServer(options) {
|
|
|
5420
5548
|
}
|
|
5421
5549
|
|
|
5422
5550
|
// src/lsp/server.ts
|
|
5423
|
-
import {
|
|
5551
|
+
import {
|
|
5552
|
+
createConnection,
|
|
5553
|
+
ProposedFeatures,
|
|
5554
|
+
TextDocuments,
|
|
5555
|
+
TextDocumentSyncKind,
|
|
5556
|
+
DiagnosticSeverity,
|
|
5557
|
+
CodeActionKind
|
|
5558
|
+
} from "vscode-languageserver/node.js";
|
|
5424
5559
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5425
5560
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5426
5561
|
import path3 from "path";
|
|
@@ -5545,7 +5680,8 @@ var SpecBridgeLspServer = class {
|
|
|
5545
5680
|
await getPluginLoader().loadPlugins(this.cwd);
|
|
5546
5681
|
} catch (error) {
|
|
5547
5682
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5548
|
-
if (this.options.verbose)
|
|
5683
|
+
if (this.options.verbose)
|
|
5684
|
+
this.connection.console.error(chalk2.red(`Plugin load failed: ${msg}`));
|
|
5549
5685
|
}
|
|
5550
5686
|
this.registry = createRegistry({ basePath: this.cwd });
|
|
5551
5687
|
await this.registry.load();
|
|
@@ -5558,7 +5694,9 @@ var SpecBridgeLspServer = class {
|
|
|
5558
5694
|
}
|
|
5559
5695
|
}
|
|
5560
5696
|
if (this.options.verbose) {
|
|
5561
|
-
this.connection.console.log(
|
|
5697
|
+
this.connection.console.log(
|
|
5698
|
+
chalk2.dim(`Loaded ${this.decisions.length} active decision(s)`)
|
|
5699
|
+
);
|
|
5562
5700
|
}
|
|
5563
5701
|
} catch (error) {
|
|
5564
5702
|
this.initError = error instanceof Error ? error.message : String(error);
|
|
@@ -5576,7 +5714,11 @@ var SpecBridgeLspServer = class {
|
|
|
5576
5714
|
for (const decision of this.decisions) {
|
|
5577
5715
|
for (const constraint of decision.constraints) {
|
|
5578
5716
|
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd: this.cwd })) continue;
|
|
5579
|
-
const verifier = selectVerifierForConstraint(
|
|
5717
|
+
const verifier = selectVerifierForConstraint(
|
|
5718
|
+
constraint.rule,
|
|
5719
|
+
constraint.verifier,
|
|
5720
|
+
constraint.check
|
|
5721
|
+
);
|
|
5580
5722
|
if (!verifier) continue;
|
|
5581
5723
|
const ctx = {
|
|
5582
5724
|
filePath,
|
|
@@ -5646,9 +5788,7 @@ function formatViolationsForGitHub(violations, limit = 50) {
|
|
|
5646
5788
|
if (violations.length === 0) {
|
|
5647
5789
|
return "## SpecBridge\n\n\u2705 No violations found.";
|
|
5648
5790
|
}
|
|
5649
|
-
const rows = [
|
|
5650
|
-
["Severity", "Type", "File", "Decision/Constraint", "Message"]
|
|
5651
|
-
];
|
|
5791
|
+
const rows = [["Severity", "Type", "File", "Decision/Constraint", "Message"]];
|
|
5652
5792
|
for (const v of violations.slice(0, limit)) {
|
|
5653
5793
|
const loc = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
5654
5794
|
rows.push([
|
|
@@ -5670,19 +5810,24 @@ ${toMdTable(rows)}${extra}`;
|
|
|
5670
5810
|
}
|
|
5671
5811
|
async function postPrComment(violations, options) {
|
|
5672
5812
|
const body = formatViolationsForGitHub(violations);
|
|
5673
|
-
const res = await fetch(
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
5813
|
+
const res = await fetch(
|
|
5814
|
+
`https://api.github.com/repos/${options.repo}/issues/${options.pr}/comments`,
|
|
5815
|
+
{
|
|
5816
|
+
method: "POST",
|
|
5817
|
+
headers: {
|
|
5818
|
+
Authorization: `Bearer ${options.token}`,
|
|
5819
|
+
Accept: "application/vnd.github+json",
|
|
5820
|
+
"Content-Type": "application/json",
|
|
5821
|
+
"User-Agent": "specbridge"
|
|
5822
|
+
},
|
|
5823
|
+
body: JSON.stringify({ body })
|
|
5824
|
+
}
|
|
5825
|
+
);
|
|
5683
5826
|
if (!res.ok) {
|
|
5684
5827
|
const text = await res.text().catch(() => "");
|
|
5685
|
-
throw new Error(
|
|
5828
|
+
throw new Error(
|
|
5829
|
+
`GitHub comment failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ""}`
|
|
5830
|
+
);
|
|
5686
5831
|
}
|
|
5687
5832
|
}
|
|
5688
5833
|
export {
|