@ipation/specbridge 2.3.0 → 2.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +4 -0
- package/dist/cli.js +1012 -760
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +707 -506
- package/dist/index.js.map +1 -1
- package/package.json +10 -5
package/dist/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;
|
|
@@ -1582,7 +1625,6 @@ async function runInference(config, options) {
|
|
|
1582
1625
|
|
|
1583
1626
|
// src/verification/engine.ts
|
|
1584
1627
|
import { Project as Project2 } from "ts-morph";
|
|
1585
|
-
import chalk from "chalk";
|
|
1586
1628
|
|
|
1587
1629
|
// src/verification/verifiers/base.ts
|
|
1588
1630
|
function defineVerifierPlugin(plugin) {
|
|
@@ -1644,17 +1686,19 @@ var NamingVerifier = class {
|
|
|
1644
1686
|
for (const classDecl of sourceFile.getClasses()) {
|
|
1645
1687
|
const name = classDecl.getName();
|
|
1646
1688
|
if (name && !pattern.regex.test(name)) {
|
|
1647
|
-
violations.push(
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
+
);
|
|
1658
1702
|
}
|
|
1659
1703
|
}
|
|
1660
1704
|
}
|
|
@@ -1662,16 +1706,18 @@ var NamingVerifier = class {
|
|
|
1662
1706
|
for (const funcDecl of sourceFile.getFunctions()) {
|
|
1663
1707
|
const name = funcDecl.getName();
|
|
1664
1708
|
if (name && !pattern.regex.test(name)) {
|
|
1665
|
-
violations.push(
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
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
|
+
);
|
|
1675
1721
|
}
|
|
1676
1722
|
}
|
|
1677
1723
|
}
|
|
@@ -1679,16 +1725,18 @@ var NamingVerifier = class {
|
|
|
1679
1725
|
for (const interfaceDecl of sourceFile.getInterfaces()) {
|
|
1680
1726
|
const name = interfaceDecl.getName();
|
|
1681
1727
|
if (!pattern.regex.test(name)) {
|
|
1682
|
-
violations.push(
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
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
|
+
);
|
|
1692
1740
|
}
|
|
1693
1741
|
}
|
|
1694
1742
|
}
|
|
@@ -1696,16 +1744,18 @@ var NamingVerifier = class {
|
|
|
1696
1744
|
for (const typeAlias of sourceFile.getTypeAliases()) {
|
|
1697
1745
|
const name = typeAlias.getName();
|
|
1698
1746
|
if (!pattern.regex.test(name)) {
|
|
1699
|
-
violations.push(
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
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
|
+
);
|
|
1709
1759
|
}
|
|
1710
1760
|
}
|
|
1711
1761
|
}
|
|
@@ -1740,20 +1790,22 @@ var ImportsVerifier = class {
|
|
|
1740
1790
|
const ms = importDecl.getModuleSpecifier();
|
|
1741
1791
|
const start = ms.getStart() + 1;
|
|
1742
1792
|
const end = ms.getEnd() - 1;
|
|
1743
|
-
violations.push(
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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
|
+
);
|
|
1757
1809
|
}
|
|
1758
1810
|
}
|
|
1759
1811
|
if (rule.includes("barrel") || rule.includes("index")) {
|
|
@@ -1762,16 +1814,18 @@ var ImportsVerifier = class {
|
|
|
1762
1814
|
if (!moduleSpec.startsWith(".")) continue;
|
|
1763
1815
|
if (moduleSpec.match(/\.(ts|js|tsx|jsx)$/) || moduleSpec.match(/\/[^/]+$/)) {
|
|
1764
1816
|
if (!moduleSpec.endsWith("/index") && !moduleSpec.endsWith("index")) {
|
|
1765
|
-
violations.push(
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
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
|
+
);
|
|
1775
1829
|
}
|
|
1776
1830
|
}
|
|
1777
1831
|
}
|
|
@@ -1780,16 +1834,18 @@ var ImportsVerifier = class {
|
|
|
1780
1834
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1781
1835
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1782
1836
|
if (moduleSpec.match(/^\.\.\/\.\.\/\.\.\//)) {
|
|
1783
|
-
violations.push(
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
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
|
+
);
|
|
1793
1849
|
}
|
|
1794
1850
|
}
|
|
1795
1851
|
}
|
|
@@ -1798,16 +1854,18 @@ var ImportsVerifier = class {
|
|
|
1798
1854
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1799
1855
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
1800
1856
|
if (moduleSpec.includes(currentFilename.split("/").pop() || "")) {
|
|
1801
|
-
violations.push(
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
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
|
+
);
|
|
1811
1869
|
}
|
|
1812
1870
|
}
|
|
1813
1871
|
}
|
|
@@ -1815,16 +1873,18 @@ var ImportsVerifier = class {
|
|
|
1815
1873
|
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
1816
1874
|
const namespaceImport = importDecl.getNamespaceImport();
|
|
1817
1875
|
if (namespaceImport) {
|
|
1818
|
-
violations.push(
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
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
|
+
);
|
|
1828
1888
|
}
|
|
1829
1889
|
}
|
|
1830
1890
|
}
|
|
@@ -1850,16 +1910,18 @@ var ErrorsVerifier = class {
|
|
|
1850
1910
|
if (!className?.endsWith("Error") && !className?.endsWith("Exception")) continue;
|
|
1851
1911
|
const extendsClause = classDecl.getExtends();
|
|
1852
1912
|
if (!extendsClause) {
|
|
1853
|
-
violations.push(
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
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
|
+
);
|
|
1863
1925
|
} else if (requiredBase) {
|
|
1864
1926
|
const baseName = extendsClause.getText();
|
|
1865
1927
|
if (baseName !== requiredBase && baseName !== "Error") {
|
|
@@ -1874,16 +1936,18 @@ var ErrorsVerifier = class {
|
|
|
1874
1936
|
if (expression) {
|
|
1875
1937
|
const text = expression.getText();
|
|
1876
1938
|
if (text.startsWith("new Error(")) {
|
|
1877
|
-
violations.push(
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
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
|
+
);
|
|
1887
1951
|
}
|
|
1888
1952
|
}
|
|
1889
1953
|
}
|
|
@@ -1897,16 +1961,18 @@ var ErrorsVerifier = class {
|
|
|
1897
1961
|
const block = catchClause.getBlock();
|
|
1898
1962
|
const statements = block.getStatements();
|
|
1899
1963
|
if (statements.length === 0) {
|
|
1900
|
-
violations.push(
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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
|
+
);
|
|
1910
1976
|
}
|
|
1911
1977
|
}
|
|
1912
1978
|
}
|
|
@@ -1918,16 +1984,18 @@ var ErrorsVerifier = class {
|
|
|
1918
1984
|
const expression = node.getExpression();
|
|
1919
1985
|
const text = expression.getText();
|
|
1920
1986
|
if (text === "console.error" || text === "console.log") {
|
|
1921
|
-
violations.push(
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
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
|
+
);
|
|
1931
1999
|
}
|
|
1932
2000
|
}
|
|
1933
2001
|
});
|
|
@@ -1958,16 +2026,18 @@ var RegexVerifier = class {
|
|
|
1958
2026
|
while ((match = regex.exec(fileText)) !== null) {
|
|
1959
2027
|
const beforeMatch = fileText.substring(0, match.index);
|
|
1960
2028
|
const lineNumber = beforeMatch.split("\n").length;
|
|
1961
|
-
violations.push(
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
+
);
|
|
1971
2041
|
}
|
|
1972
2042
|
} catch {
|
|
1973
2043
|
}
|
|
@@ -1977,15 +2047,17 @@ var RegexVerifier = class {
|
|
|
1977
2047
|
try {
|
|
1978
2048
|
const regex = new RegExp(patternToRequire);
|
|
1979
2049
|
if (!regex.test(fileText)) {
|
|
1980
|
-
violations.push(
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
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
|
+
);
|
|
1989
2061
|
}
|
|
1990
2062
|
} catch {
|
|
1991
2063
|
}
|
|
@@ -2124,7 +2196,9 @@ function parseBannedDependency(rule) {
|
|
|
2124
2196
|
return value.length > 0 ? value : null;
|
|
2125
2197
|
}
|
|
2126
2198
|
function parseLayerRule(rule) {
|
|
2127
|
-
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
|
+
);
|
|
2128
2202
|
const fromLayer = m?.[1]?.toLowerCase();
|
|
2129
2203
|
const toLayer = m?.[2]?.toLowerCase();
|
|
2130
2204
|
if (!fromLayer || !toLayer) return null;
|
|
@@ -2150,22 +2224,25 @@ var DependencyVerifier = class {
|
|
|
2150
2224
|
const sccs = tarjanScc(graph);
|
|
2151
2225
|
const current = projectFilePath;
|
|
2152
2226
|
for (const scc of sccs) {
|
|
2153
|
-
const
|
|
2227
|
+
const first = scc[0];
|
|
2228
|
+
const hasSelfLoop = first !== void 0 && scc.length === 1 && (graph.get(first)?.has(first) ?? false);
|
|
2154
2229
|
const isCycle = scc.length > 1 || hasSelfLoop;
|
|
2155
2230
|
if (!isCycle) continue;
|
|
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;
|
|
@@ -2591,10 +2700,36 @@ import { existsSync } from "fs";
|
|
|
2591
2700
|
import { join as join3 } from "path";
|
|
2592
2701
|
import { pathToFileURL } from "url";
|
|
2593
2702
|
import fg2 from "fast-glob";
|
|
2703
|
+
|
|
2704
|
+
// src/utils/logger.ts
|
|
2705
|
+
import pino from "pino";
|
|
2706
|
+
var defaultOptions = {
|
|
2707
|
+
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
2708
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2709
|
+
base: {
|
|
2710
|
+
service: "specbridge"
|
|
2711
|
+
}
|
|
2712
|
+
};
|
|
2713
|
+
var destination = pino.destination({
|
|
2714
|
+
fd: 2,
|
|
2715
|
+
// stderr
|
|
2716
|
+
sync: false
|
|
2717
|
+
});
|
|
2718
|
+
var rootLogger = pino(defaultOptions, destination);
|
|
2719
|
+
function getLogger(bindings) {
|
|
2720
|
+
if (!bindings) {
|
|
2721
|
+
return rootLogger;
|
|
2722
|
+
}
|
|
2723
|
+
return rootLogger.child(bindings);
|
|
2724
|
+
}
|
|
2725
|
+
var logger = getLogger();
|
|
2726
|
+
|
|
2727
|
+
// src/verification/plugins/loader.ts
|
|
2594
2728
|
var PluginLoader = class {
|
|
2595
2729
|
plugins = /* @__PURE__ */ new Map();
|
|
2596
2730
|
loaded = false;
|
|
2597
2731
|
loadErrors = [];
|
|
2732
|
+
logger = getLogger({ module: "verification.plugins.loader" });
|
|
2598
2733
|
/**
|
|
2599
2734
|
* Load all plugins from the specified base path
|
|
2600
2735
|
*
|
|
@@ -2617,15 +2752,15 @@ var PluginLoader = class {
|
|
|
2617
2752
|
} catch (error) {
|
|
2618
2753
|
const message = error instanceof Error ? error.message : String(error);
|
|
2619
2754
|
this.loadErrors.push({ file, error: message });
|
|
2620
|
-
|
|
2755
|
+
this.logger.warn({ file, error: message }, "Failed to load plugin");
|
|
2621
2756
|
}
|
|
2622
2757
|
}
|
|
2623
2758
|
this.loaded = true;
|
|
2624
2759
|
if (this.plugins.size > 0) {
|
|
2625
|
-
|
|
2760
|
+
this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
|
|
2626
2761
|
}
|
|
2627
2762
|
if (this.loadErrors.length > 0) {
|
|
2628
|
-
|
|
2763
|
+
this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
|
|
2629
2764
|
}
|
|
2630
2765
|
}
|
|
2631
2766
|
/**
|
|
@@ -2734,7 +2869,10 @@ var PluginLoader = class {
|
|
|
2734
2869
|
return { success: false, error: `Plugin ${id} requires params but none were provided` };
|
|
2735
2870
|
}
|
|
2736
2871
|
if (typeof plugin.paramsSchema !== "object" || !plugin.paramsSchema || !("parse" in plugin.paramsSchema)) {
|
|
2737
|
-
return {
|
|
2872
|
+
return {
|
|
2873
|
+
success: false,
|
|
2874
|
+
error: `Plugin ${id} has invalid paramsSchema (must be a Zod schema)`
|
|
2875
|
+
};
|
|
2738
2876
|
}
|
|
2739
2877
|
const schema = plugin.paramsSchema;
|
|
2740
2878
|
if (schema.safeParse) {
|
|
@@ -2801,8 +2939,9 @@ var builtinVerifiers = {
|
|
|
2801
2939
|
};
|
|
2802
2940
|
var verifierInstances = /* @__PURE__ */ new Map();
|
|
2803
2941
|
function getVerifier(id) {
|
|
2804
|
-
|
|
2805
|
-
|
|
2942
|
+
const pooled = verifierInstances.get(id);
|
|
2943
|
+
if (pooled) {
|
|
2944
|
+
return pooled;
|
|
2806
2945
|
}
|
|
2807
2946
|
const pluginLoader2 = getPluginLoader();
|
|
2808
2947
|
const customVerifier = pluginLoader2.getVerifier(id);
|
|
@@ -3007,6 +3146,7 @@ var VerificationEngine = class {
|
|
|
3007
3146
|
astCache;
|
|
3008
3147
|
resultsCache;
|
|
3009
3148
|
pluginsLoaded = false;
|
|
3149
|
+
logger = getLogger({ module: "verification.engine" });
|
|
3010
3150
|
constructor(registry) {
|
|
3011
3151
|
this.registry = registry || createRegistry();
|
|
3012
3152
|
this.project = new Project2({
|
|
@@ -3180,12 +3320,14 @@ var VerificationEngine = class {
|
|
|
3180
3320
|
);
|
|
3181
3321
|
if (!verifier) {
|
|
3182
3322
|
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
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"
|
|
3189
3331
|
);
|
|
3190
3332
|
warnings.push({
|
|
3191
3333
|
type: "missing_verifier",
|
|
@@ -3232,7 +3374,10 @@ var VerificationEngine = class {
|
|
|
3232
3374
|
}
|
|
3233
3375
|
if (constraint.check?.verifier && constraint.check?.params) {
|
|
3234
3376
|
const pluginLoader2 = getPluginLoader();
|
|
3235
|
-
const validationResult = pluginLoader2.validateParams(
|
|
3377
|
+
const validationResult = pluginLoader2.validateParams(
|
|
3378
|
+
constraint.check.verifier,
|
|
3379
|
+
constraint.check.params
|
|
3380
|
+
);
|
|
3236
3381
|
if (!validationResult.success) {
|
|
3237
3382
|
warnings.push({
|
|
3238
3383
|
type: "invalid_params",
|
|
@@ -3292,17 +3437,17 @@ var VerificationEngine = class {
|
|
|
3292
3437
|
} catch (error) {
|
|
3293
3438
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3294
3439
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
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"
|
|
3302
3450
|
);
|
|
3303
|
-
if (errorStack) {
|
|
3304
|
-
console.error(chalk.dim(errorStack));
|
|
3305
|
-
}
|
|
3306
3451
|
errors.push({
|
|
3307
3452
|
type: "verifier_exception",
|
|
3308
3453
|
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
@@ -3367,7 +3512,11 @@ import { resolve } from "path";
|
|
|
3367
3512
|
var execFileAsync = promisify(execFile);
|
|
3368
3513
|
async function getChangedFiles(cwd) {
|
|
3369
3514
|
try {
|
|
3370
|
-
const { stdout: stdout2 } = await execFileAsync(
|
|
3515
|
+
const { stdout: stdout2 } = await execFileAsync(
|
|
3516
|
+
"git",
|
|
3517
|
+
["diff", "--name-only", "--diff-filter=AM", "HEAD"],
|
|
3518
|
+
{ cwd }
|
|
3519
|
+
);
|
|
3371
3520
|
const rel = stdout2.trim().split("\n").map((s) => s.trim()).filter(Boolean);
|
|
3372
3521
|
const abs = [];
|
|
3373
3522
|
for (const file of rel) {
|
|
@@ -3438,8 +3587,14 @@ var AutofixEngine = class {
|
|
|
3438
3587
|
const edits = [];
|
|
3439
3588
|
for (const violation of fileViolations) {
|
|
3440
3589
|
const fix = violation.autofix;
|
|
3590
|
+
if (!fix) {
|
|
3591
|
+
skippedViolations++;
|
|
3592
|
+
continue;
|
|
3593
|
+
}
|
|
3441
3594
|
if (options.interactive) {
|
|
3442
|
-
const ok = await confirmFix(
|
|
3595
|
+
const ok = await confirmFix(
|
|
3596
|
+
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3597
|
+
);
|
|
3443
3598
|
if (!ok) {
|
|
3444
3599
|
skippedViolations++;
|
|
3445
3600
|
continue;
|
|
@@ -3566,7 +3721,24 @@ var PropagationEngine = class {
|
|
|
3566
3721
|
if (!this.graph) {
|
|
3567
3722
|
await this.initialize(config, options);
|
|
3568
3723
|
}
|
|
3569
|
-
const
|
|
3724
|
+
const graph = this.graph;
|
|
3725
|
+
if (!graph) {
|
|
3726
|
+
return {
|
|
3727
|
+
decision: decisionId,
|
|
3728
|
+
change,
|
|
3729
|
+
affectedFiles: [],
|
|
3730
|
+
estimatedEffort: "low",
|
|
3731
|
+
migrationSteps: [
|
|
3732
|
+
{
|
|
3733
|
+
order: 1,
|
|
3734
|
+
description: "Run verification to confirm all violations resolved",
|
|
3735
|
+
files: [],
|
|
3736
|
+
automated: true
|
|
3737
|
+
}
|
|
3738
|
+
]
|
|
3739
|
+
};
|
|
3740
|
+
}
|
|
3741
|
+
const affectedFilePaths = getAffectedFiles(graph, decisionId);
|
|
3570
3742
|
const verificationEngine = createVerificationEngine(this.registry);
|
|
3571
3743
|
const result = await verificationEngine.verify(config, {
|
|
3572
3744
|
files: affectedFilePaths,
|
|
@@ -3599,10 +3771,7 @@ var PropagationEngine = class {
|
|
|
3599
3771
|
} else {
|
|
3600
3772
|
estimatedEffort = "high";
|
|
3601
3773
|
}
|
|
3602
|
-
const migrationSteps = this.generateMigrationSteps(
|
|
3603
|
-
affectedFiles,
|
|
3604
|
-
totalAutoFixable > 0
|
|
3605
|
-
);
|
|
3774
|
+
const migrationSteps = this.generateMigrationSteps(affectedFiles, totalAutoFixable > 0);
|
|
3606
3775
|
return {
|
|
3607
3776
|
decision: decisionId,
|
|
3608
3777
|
change,
|
|
@@ -3625,9 +3794,7 @@ var PropagationEngine = class {
|
|
|
3625
3794
|
automated: true
|
|
3626
3795
|
});
|
|
3627
3796
|
}
|
|
3628
|
-
const filesWithManualFixes = affectedFiles.filter(
|
|
3629
|
-
(f) => f.violations > f.autoFixable
|
|
3630
|
-
);
|
|
3797
|
+
const filesWithManualFixes = affectedFiles.filter((f) => f.violations > f.autoFixable);
|
|
3631
3798
|
if (filesWithManualFixes.length > 0) {
|
|
3632
3799
|
const highPriority = filesWithManualFixes.filter((f) => f.violations > 5);
|
|
3633
3800
|
const mediumPriority = filesWithManualFixes.filter(
|
|
@@ -3707,10 +3874,7 @@ async function generateReport(config, options = {}) {
|
|
|
3707
3874
|
medium: decisionViolations.filter((v) => v.severity === "medium").length,
|
|
3708
3875
|
low: decisionViolations.filter((v) => v.severity === "low").length
|
|
3709
3876
|
};
|
|
3710
|
-
weightedScore = decisionViolations.reduce(
|
|
3711
|
-
(score, v) => score + weights[v.severity],
|
|
3712
|
-
0
|
|
3713
|
-
);
|
|
3877
|
+
weightedScore = decisionViolations.reduce((score, v) => score + weights[v.severity], 0);
|
|
3714
3878
|
compliance = Math.max(0, 100 - weightedScore);
|
|
3715
3879
|
if (decisionViolations.length > 0 && constraintCount > 0) {
|
|
3716
3880
|
const violationRate = decisionViolations.length / constraintCount;
|
|
@@ -3830,7 +3994,9 @@ var Reporter = class {
|
|
|
3830
3994
|
lines.push("Summary:");
|
|
3831
3995
|
lines.push(` Decisions Checked: ${result.summary.decisionsChecked || 0}`);
|
|
3832
3996
|
lines.push(` Files Checked: ${result.summary.filesChecked || 0}`);
|
|
3833
|
-
lines.push(
|
|
3997
|
+
lines.push(
|
|
3998
|
+
` Total Violations: ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
3999
|
+
);
|
|
3834
4000
|
lines.push(` Critical: ${result.summary.critical || 0}`);
|
|
3835
4001
|
lines.push(` High: ${result.summary.high || 0}`);
|
|
3836
4002
|
lines.push(` Medium: ${result.summary.medium || 0}`);
|
|
@@ -3844,7 +4010,9 @@ var Reporter = class {
|
|
|
3844
4010
|
lines.push("-".repeat(50));
|
|
3845
4011
|
result.violations.forEach((v) => {
|
|
3846
4012
|
const severity = v.severity.toLowerCase();
|
|
3847
|
-
lines.push(
|
|
4013
|
+
lines.push(
|
|
4014
|
+
` [${v.severity.toUpperCase()}] ${v.decisionId} - ${v.constraintId} (${severity})`
|
|
4015
|
+
);
|
|
3848
4016
|
lines.push(` ${v.message}`);
|
|
3849
4017
|
const file = v.location?.file || v.file;
|
|
3850
4018
|
const line = v.location?.line || v.line || 0;
|
|
@@ -3865,7 +4033,9 @@ var Reporter = class {
|
|
|
3865
4033
|
lines.push("");
|
|
3866
4034
|
if (result.summary) {
|
|
3867
4035
|
lines.push("Summary:");
|
|
3868
|
-
lines.push(
|
|
4036
|
+
lines.push(
|
|
4037
|
+
` Total Violations: ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
4038
|
+
);
|
|
3869
4039
|
lines.push("");
|
|
3870
4040
|
}
|
|
3871
4041
|
if (result.violations && result.violations.length > 0) {
|
|
@@ -3919,7 +4089,9 @@ var Reporter = class {
|
|
|
3919
4089
|
lines.push("### Summary\n");
|
|
3920
4090
|
lines.push(`- **Decisions Checked:** ${result.summary.decisionsChecked || 0}`);
|
|
3921
4091
|
lines.push(`- **Files Checked:** ${result.summary.filesChecked || 0}`);
|
|
3922
|
-
lines.push(
|
|
4092
|
+
lines.push(
|
|
4093
|
+
`- **Total Violations:** ${result.summary.totalViolations || result.violations?.length || 0}`
|
|
4094
|
+
);
|
|
3923
4095
|
lines.push(`- **Critical:** ${result.summary.critical || 0}`);
|
|
3924
4096
|
lines.push(`- **High:** ${result.summary.high || 0}`);
|
|
3925
4097
|
lines.push(`- **Medium:** ${result.summary.medium || 0}`);
|
|
@@ -3944,54 +4116,58 @@ var Reporter = class {
|
|
|
3944
4116
|
};
|
|
3945
4117
|
|
|
3946
4118
|
// src/reporting/formats/console.ts
|
|
3947
|
-
import
|
|
4119
|
+
import chalk from "chalk";
|
|
3948
4120
|
import { table } from "table";
|
|
3949
4121
|
function formatConsoleReport(report) {
|
|
3950
4122
|
const lines = [];
|
|
3951
4123
|
lines.push("");
|
|
3952
|
-
lines.push(
|
|
3953
|
-
lines.push(
|
|
3954
|
-
lines.push(
|
|
4124
|
+
lines.push(chalk.bold.blue("SpecBridge Compliance Report"));
|
|
4125
|
+
lines.push(chalk.dim(`Generated: ${new Date(report.timestamp).toLocaleString()}`));
|
|
4126
|
+
lines.push(chalk.dim(`Project: ${report.project}`));
|
|
3955
4127
|
lines.push("");
|
|
3956
4128
|
const complianceColor = getComplianceColor(report.summary.compliance);
|
|
3957
|
-
lines.push(
|
|
3958
|
-
lines.push(
|
|
4129
|
+
lines.push(chalk.bold("Overall Compliance"));
|
|
4130
|
+
lines.push(
|
|
4131
|
+
` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`
|
|
4132
|
+
);
|
|
3959
4133
|
lines.push("");
|
|
3960
|
-
lines.push(
|
|
3961
|
-
lines.push(
|
|
4134
|
+
lines.push(chalk.bold("Summary"));
|
|
4135
|
+
lines.push(
|
|
4136
|
+
` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`
|
|
4137
|
+
);
|
|
3962
4138
|
lines.push(` Constraints: ${report.summary.totalConstraints}`);
|
|
3963
4139
|
lines.push("");
|
|
3964
|
-
lines.push(
|
|
4140
|
+
lines.push(chalk.bold("Violations"));
|
|
3965
4141
|
const { violations } = report.summary;
|
|
3966
4142
|
const violationParts = [];
|
|
3967
4143
|
if (violations.critical > 0) {
|
|
3968
|
-
violationParts.push(
|
|
4144
|
+
violationParts.push(chalk.red(`${violations.critical} critical`));
|
|
3969
4145
|
}
|
|
3970
4146
|
if (violations.high > 0) {
|
|
3971
|
-
violationParts.push(
|
|
4147
|
+
violationParts.push(chalk.yellow(`${violations.high} high`));
|
|
3972
4148
|
}
|
|
3973
4149
|
if (violations.medium > 0) {
|
|
3974
|
-
violationParts.push(
|
|
4150
|
+
violationParts.push(chalk.cyan(`${violations.medium} medium`));
|
|
3975
4151
|
}
|
|
3976
4152
|
if (violations.low > 0) {
|
|
3977
|
-
violationParts.push(
|
|
4153
|
+
violationParts.push(chalk.dim(`${violations.low} low`));
|
|
3978
4154
|
}
|
|
3979
4155
|
if (violationParts.length > 0) {
|
|
3980
4156
|
lines.push(` ${violationParts.join(" | ")}`);
|
|
3981
4157
|
} else {
|
|
3982
|
-
lines.push(
|
|
4158
|
+
lines.push(chalk.green(" No violations"));
|
|
3983
4159
|
}
|
|
3984
4160
|
lines.push("");
|
|
3985
4161
|
if (report.byDecision.length > 0) {
|
|
3986
|
-
lines.push(
|
|
4162
|
+
lines.push(chalk.bold("By Decision"));
|
|
3987
4163
|
lines.push("");
|
|
3988
4164
|
const tableData = [
|
|
3989
4165
|
[
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
4166
|
+
chalk.bold("Decision"),
|
|
4167
|
+
chalk.bold("Status"),
|
|
4168
|
+
chalk.bold("Constraints"),
|
|
4169
|
+
chalk.bold("Violations"),
|
|
4170
|
+
chalk.bold("Compliance")
|
|
3995
4171
|
]
|
|
3996
4172
|
];
|
|
3997
4173
|
for (const dec of report.byDecision) {
|
|
@@ -4001,7 +4177,7 @@ function formatConsoleReport(report) {
|
|
|
4001
4177
|
truncate(dec.title, 40),
|
|
4002
4178
|
statusColor(dec.status),
|
|
4003
4179
|
String(dec.constraints),
|
|
4004
|
-
dec.violations > 0 ?
|
|
4180
|
+
dec.violations > 0 ? chalk.red(String(dec.violations)) : chalk.green("0"),
|
|
4005
4181
|
compColor(`${dec.compliance}%`)
|
|
4006
4182
|
]);
|
|
4007
4183
|
}
|
|
@@ -4035,23 +4211,23 @@ function formatComplianceBar(compliance) {
|
|
|
4035
4211
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
4036
4212
|
}
|
|
4037
4213
|
function getComplianceColor(compliance) {
|
|
4038
|
-
if (compliance >= 90) return
|
|
4039
|
-
if (compliance >= 70) return
|
|
4040
|
-
if (compliance >= 50) return
|
|
4041
|
-
return
|
|
4214
|
+
if (compliance >= 90) return chalk.green;
|
|
4215
|
+
if (compliance >= 70) return chalk.yellow;
|
|
4216
|
+
if (compliance >= 50) return chalk.hex("#FFA500");
|
|
4217
|
+
return chalk.red;
|
|
4042
4218
|
}
|
|
4043
4219
|
function getStatusColor(status) {
|
|
4044
4220
|
switch (status) {
|
|
4045
4221
|
case "active":
|
|
4046
|
-
return
|
|
4222
|
+
return chalk.green;
|
|
4047
4223
|
case "draft":
|
|
4048
|
-
return
|
|
4224
|
+
return chalk.yellow;
|
|
4049
4225
|
case "deprecated":
|
|
4050
|
-
return
|
|
4226
|
+
return chalk.gray;
|
|
4051
4227
|
case "superseded":
|
|
4052
|
-
return
|
|
4228
|
+
return chalk.blue;
|
|
4053
4229
|
default:
|
|
4054
|
-
return
|
|
4230
|
+
return chalk.white;
|
|
4055
4231
|
}
|
|
4056
4232
|
}
|
|
4057
4233
|
function truncate(str, length) {
|
|
@@ -4075,7 +4251,9 @@ function formatMarkdownReport(report) {
|
|
|
4075
4251
|
lines.push("");
|
|
4076
4252
|
lines.push("## Summary");
|
|
4077
4253
|
lines.push("");
|
|
4078
|
-
lines.push(
|
|
4254
|
+
lines.push(
|
|
4255
|
+
`- **Active Decisions:** ${report.summary.activeDecisions} / ${report.summary.totalDecisions}`
|
|
4256
|
+
);
|
|
4079
4257
|
lines.push(`- **Total Constraints:** ${report.summary.totalConstraints}`);
|
|
4080
4258
|
lines.push("");
|
|
4081
4259
|
lines.push("### Violations");
|
|
@@ -4125,6 +4303,7 @@ function formatProgressBar(percentage) {
|
|
|
4125
4303
|
import { join as join4 } from "path";
|
|
4126
4304
|
var ReportStorage = class {
|
|
4127
4305
|
storageDir;
|
|
4306
|
+
logger = getLogger({ module: "reporting.storage" });
|
|
4128
4307
|
constructor(basePath) {
|
|
4129
4308
|
this.storageDir = join4(getSpecBridgeDir(basePath), "reports", "history");
|
|
4130
4309
|
}
|
|
@@ -4183,7 +4362,7 @@ var ReportStorage = class {
|
|
|
4183
4362
|
const timestamp = file.replace("report-", "").replace(".json", "");
|
|
4184
4363
|
return { timestamp, report };
|
|
4185
4364
|
} catch (error) {
|
|
4186
|
-
|
|
4365
|
+
this.logger.warn({ file, error }, "Failed to load report file");
|
|
4187
4366
|
return null;
|
|
4188
4367
|
}
|
|
4189
4368
|
});
|
|
@@ -4227,7 +4406,7 @@ var ReportStorage = class {
|
|
|
4227
4406
|
const fs = await import("fs/promises");
|
|
4228
4407
|
await fs.unlink(filepath);
|
|
4229
4408
|
} catch (error) {
|
|
4230
|
-
|
|
4409
|
+
this.logger.warn({ file, error }, "Failed to delete old report file");
|
|
4231
4410
|
}
|
|
4232
4411
|
}
|
|
4233
4412
|
return filesToDelete.length;
|
|
@@ -4238,9 +4417,7 @@ var ReportStorage = class {
|
|
|
4238
4417
|
async function detectDrift(current, previous) {
|
|
4239
4418
|
const byDecision = [];
|
|
4240
4419
|
for (const currDecision of current.byDecision) {
|
|
4241
|
-
const prevDecision = previous.byDecision.find(
|
|
4242
|
-
(d) => d.decisionId === currDecision.decisionId
|
|
4243
|
-
);
|
|
4420
|
+
const prevDecision = previous.byDecision.find((d) => d.decisionId === currDecision.decisionId);
|
|
4244
4421
|
if (!prevDecision) {
|
|
4245
4422
|
byDecision.push({
|
|
4246
4423
|
decisionId: currDecision.decisionId,
|
|
@@ -5021,9 +5198,7 @@ var AnalyticsEngine = class {
|
|
|
5021
5198
|
overallTrend = "down";
|
|
5022
5199
|
}
|
|
5023
5200
|
}
|
|
5024
|
-
const sortedByCompliance = [...latest.byDecision].sort(
|
|
5025
|
-
(a, b) => b.compliance - a.compliance
|
|
5026
|
-
);
|
|
5201
|
+
const sortedByCompliance = [...latest.byDecision].sort((a, b) => b.compliance - a.compliance);
|
|
5027
5202
|
const topDecisions = sortedByCompliance.slice(0, 5).map((d) => ({
|
|
5028
5203
|
decisionId: d.decisionId,
|
|
5029
5204
|
title: d.title,
|
|
@@ -5064,6 +5239,7 @@ var DashboardServer = class {
|
|
|
5064
5239
|
CACHE_TTL = 6e4;
|
|
5065
5240
|
// 1 minute
|
|
5066
5241
|
refreshInterval = null;
|
|
5242
|
+
logger = getLogger({ module: "dashboard.server" });
|
|
5067
5243
|
constructor(options) {
|
|
5068
5244
|
this.cwd = options.cwd;
|
|
5069
5245
|
this.config = options.config;
|
|
@@ -5079,10 +5255,11 @@ var DashboardServer = class {
|
|
|
5079
5255
|
async start() {
|
|
5080
5256
|
await this.registry.load();
|
|
5081
5257
|
await this.refreshCache();
|
|
5082
|
-
this.refreshInterval = setInterval(
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5258
|
+
this.refreshInterval = setInterval(() => {
|
|
5259
|
+
void this.refreshCache().catch((error) => {
|
|
5260
|
+
this.logger.error({ error }, "Background cache refresh failed");
|
|
5261
|
+
});
|
|
5262
|
+
}, this.CACHE_TTL);
|
|
5086
5263
|
}
|
|
5087
5264
|
/**
|
|
5088
5265
|
* Stop the server and clear intervals
|
|
@@ -5103,7 +5280,7 @@ var DashboardServer = class {
|
|
|
5103
5280
|
this.cacheTimestamp = Date.now();
|
|
5104
5281
|
await this.reportStorage.save(report);
|
|
5105
5282
|
} catch (error) {
|
|
5106
|
-
|
|
5283
|
+
this.logger.error({ error }, "Cache refresh failed");
|
|
5107
5284
|
if (!this.cachedReport) {
|
|
5108
5285
|
try {
|
|
5109
5286
|
const stored = await this.reportStorage.loadLatest();
|
|
@@ -5111,7 +5288,7 @@ var DashboardServer = class {
|
|
|
5111
5288
|
this.cachedReport = stored.report;
|
|
5112
5289
|
}
|
|
5113
5290
|
} catch (fallbackError) {
|
|
5114
|
-
|
|
5291
|
+
this.logger.error({ error: fallbackError }, "Failed to load fallback report");
|
|
5115
5292
|
}
|
|
5116
5293
|
}
|
|
5117
5294
|
}
|
|
@@ -5354,11 +5531,13 @@ var DashboardServer = class {
|
|
|
5354
5531
|
*/
|
|
5355
5532
|
setupStaticFiles() {
|
|
5356
5533
|
const publicDir = join5(__dirname, "public");
|
|
5357
|
-
this.app.use(
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5534
|
+
this.app.use(
|
|
5535
|
+
express.static(publicDir, {
|
|
5536
|
+
maxAge: "1h",
|
|
5537
|
+
// Cache static assets
|
|
5538
|
+
etag: true
|
|
5539
|
+
})
|
|
5540
|
+
);
|
|
5362
5541
|
this.app.get("/{*path}", (_req, res) => {
|
|
5363
5542
|
res.sendFile(join5(publicDir, "index.html"));
|
|
5364
5543
|
});
|
|
@@ -5369,12 +5548,19 @@ function createDashboardServer(options) {
|
|
|
5369
5548
|
}
|
|
5370
5549
|
|
|
5371
5550
|
// src/lsp/server.ts
|
|
5372
|
-
import {
|
|
5551
|
+
import {
|
|
5552
|
+
createConnection,
|
|
5553
|
+
ProposedFeatures,
|
|
5554
|
+
TextDocuments,
|
|
5555
|
+
TextDocumentSyncKind,
|
|
5556
|
+
DiagnosticSeverity,
|
|
5557
|
+
CodeActionKind
|
|
5558
|
+
} from "vscode-languageserver/node.js";
|
|
5373
5559
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
5374
5560
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
5375
5561
|
import path3 from "path";
|
|
5376
5562
|
import { Project as Project3 } from "ts-morph";
|
|
5377
|
-
import
|
|
5563
|
+
import chalk2 from "chalk";
|
|
5378
5564
|
function severityToDiagnostic(severity) {
|
|
5379
5565
|
switch (severity) {
|
|
5380
5566
|
case "critical":
|
|
@@ -5485,7 +5671,7 @@ var SpecBridgeLspServer = class {
|
|
|
5485
5671
|
if (!await pathExists(getSpecBridgeDir(this.cwd))) {
|
|
5486
5672
|
const err = new NotInitializedError();
|
|
5487
5673
|
this.initError = err.message;
|
|
5488
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5674
|
+
if (this.options.verbose) this.connection.console.error(chalk2.red(this.initError));
|
|
5489
5675
|
return;
|
|
5490
5676
|
}
|
|
5491
5677
|
try {
|
|
@@ -5494,7 +5680,8 @@ var SpecBridgeLspServer = class {
|
|
|
5494
5680
|
await getPluginLoader().loadPlugins(this.cwd);
|
|
5495
5681
|
} catch (error) {
|
|
5496
5682
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5497
|
-
if (this.options.verbose)
|
|
5683
|
+
if (this.options.verbose)
|
|
5684
|
+
this.connection.console.error(chalk2.red(`Plugin load failed: ${msg}`));
|
|
5498
5685
|
}
|
|
5499
5686
|
this.registry = createRegistry({ basePath: this.cwd });
|
|
5500
5687
|
await this.registry.load();
|
|
@@ -5507,11 +5694,13 @@ var SpecBridgeLspServer = class {
|
|
|
5507
5694
|
}
|
|
5508
5695
|
}
|
|
5509
5696
|
if (this.options.verbose) {
|
|
5510
|
-
this.connection.console.log(
|
|
5697
|
+
this.connection.console.log(
|
|
5698
|
+
chalk2.dim(`Loaded ${this.decisions.length} active decision(s)`)
|
|
5699
|
+
);
|
|
5511
5700
|
}
|
|
5512
5701
|
} catch (error) {
|
|
5513
5702
|
this.initError = error instanceof Error ? error.message : String(error);
|
|
5514
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5703
|
+
if (this.options.verbose) this.connection.console.error(chalk2.red(this.initError));
|
|
5515
5704
|
}
|
|
5516
5705
|
}
|
|
5517
5706
|
async verifyTextDocument(doc) {
|
|
@@ -5525,7 +5714,11 @@ var SpecBridgeLspServer = class {
|
|
|
5525
5714
|
for (const decision of this.decisions) {
|
|
5526
5715
|
for (const constraint of decision.constraints) {
|
|
5527
5716
|
if (!shouldApplyConstraintToFile({ filePath, constraint, cwd: this.cwd })) continue;
|
|
5528
|
-
const verifier = selectVerifierForConstraint(
|
|
5717
|
+
const verifier = selectVerifierForConstraint(
|
|
5718
|
+
constraint.rule,
|
|
5719
|
+
constraint.verifier,
|
|
5720
|
+
constraint.check
|
|
5721
|
+
);
|
|
5529
5722
|
if (!verifier) continue;
|
|
5530
5723
|
const ctx = {
|
|
5531
5724
|
filePath,
|
|
@@ -5579,6 +5772,9 @@ async function startLspServer(options) {
|
|
|
5579
5772
|
// src/integrations/github.ts
|
|
5580
5773
|
function toMdTable(rows) {
|
|
5581
5774
|
const header = rows[0];
|
|
5775
|
+
if (!header) {
|
|
5776
|
+
return "";
|
|
5777
|
+
}
|
|
5582
5778
|
const body = rows.slice(1);
|
|
5583
5779
|
const sep = header.map(() => "---");
|
|
5584
5780
|
const lines = [
|
|
@@ -5592,9 +5788,7 @@ function formatViolationsForGitHub(violations, limit = 50) {
|
|
|
5592
5788
|
if (violations.length === 0) {
|
|
5593
5789
|
return "## SpecBridge\n\n\u2705 No violations found.";
|
|
5594
5790
|
}
|
|
5595
|
-
const rows = [
|
|
5596
|
-
["Severity", "Type", "File", "Decision/Constraint", "Message"]
|
|
5597
|
-
];
|
|
5791
|
+
const rows = [["Severity", "Type", "File", "Decision/Constraint", "Message"]];
|
|
5598
5792
|
for (const v of violations.slice(0, limit)) {
|
|
5599
5793
|
const loc = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
5600
5794
|
rows.push([
|
|
@@ -5616,19 +5810,24 @@ ${toMdTable(rows)}${extra}`;
|
|
|
5616
5810
|
}
|
|
5617
5811
|
async function postPrComment(violations, options) {
|
|
5618
5812
|
const body = formatViolationsForGitHub(violations);
|
|
5619
|
-
const res = await fetch(
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
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
|
+
);
|
|
5629
5826
|
if (!res.ok) {
|
|
5630
5827
|
const text = await res.text().catch(() => "");
|
|
5631
|
-
throw new Error(
|
|
5828
|
+
throw new Error(
|
|
5829
|
+
`GitHub comment failed: ${res.status} ${res.statusText}${text ? ` - ${text}` : ""}`
|
|
5830
|
+
);
|
|
5632
5831
|
}
|
|
5633
5832
|
}
|
|
5634
5833
|
export {
|
|
@@ -5722,6 +5921,7 @@ export {
|
|
|
5722
5921
|
getConfigPath,
|
|
5723
5922
|
getDecisionsDir,
|
|
5724
5923
|
getInferredDir,
|
|
5924
|
+
getLogger,
|
|
5725
5925
|
getReportsDir,
|
|
5726
5926
|
getSpecBridgeDir,
|
|
5727
5927
|
getTransitiveDependencies,
|
|
@@ -5734,6 +5934,7 @@ export {
|
|
|
5734
5934
|
loadConfig,
|
|
5735
5935
|
loadDecisionFile,
|
|
5736
5936
|
loadDecisionsFromDir,
|
|
5937
|
+
logger,
|
|
5737
5938
|
matchesAnyPattern,
|
|
5738
5939
|
matchesPattern,
|
|
5739
5940
|
mergeWithDefaults,
|