@ipation/specbridge 2.2.0 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/cli.js +75 -42
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +139 -542
- package/dist/index.js +83 -44
- package/dist/index.js.map +1 -1
- package/package.json +12 -12
package/dist/index.js
CHANGED
|
@@ -28,7 +28,7 @@ var ConstraintExceptionSchema = z.object({
|
|
|
28
28
|
});
|
|
29
29
|
var ConstraintCheckSchema = z.object({
|
|
30
30
|
verifier: z.string().min(1),
|
|
31
|
-
params: z.record(z.unknown()).optional()
|
|
31
|
+
params: z.record(z.string(), z.unknown()).optional()
|
|
32
32
|
});
|
|
33
33
|
var ConstraintSchema = z.object({
|
|
34
34
|
id: z.string().min(1).regex(/^[a-z0-9-]+$/, "Constraint ID must be lowercase alphanumeric with hyphens"),
|
|
@@ -70,7 +70,7 @@ function validateDecision(data) {
|
|
|
70
70
|
return { success: false, errors: result.error };
|
|
71
71
|
}
|
|
72
72
|
function formatValidationErrors(errors) {
|
|
73
|
-
return errors.
|
|
73
|
+
return errors.issues.map((err) => {
|
|
74
74
|
const path4 = err.path.join(".");
|
|
75
75
|
return `${path4}: ${err.message}`;
|
|
76
76
|
});
|
|
@@ -367,7 +367,7 @@ async function loadConfig(basePath = process.cwd()) {
|
|
|
367
367
|
const parsed = parseYaml(content);
|
|
368
368
|
const result = validateConfig(parsed);
|
|
369
369
|
if (!result.success) {
|
|
370
|
-
const errors = result.errors.
|
|
370
|
+
const errors = result.errors.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
|
|
371
371
|
throw new ConfigError(`Invalid configuration in ${configPath}`, { errors });
|
|
372
372
|
}
|
|
373
373
|
return result.data;
|
|
@@ -2055,7 +2055,10 @@ function buildDependencyGraph(project) {
|
|
|
2055
2055
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
2056
2056
|
const resolved = resolveToSourceFilePath(project, from, moduleSpec);
|
|
2057
2057
|
if (resolved) {
|
|
2058
|
-
graph.get(from)
|
|
2058
|
+
const dependencies = graph.get(from);
|
|
2059
|
+
if (dependencies) {
|
|
2060
|
+
dependencies.add(normalizeFsPath(resolved));
|
|
2061
|
+
}
|
|
2059
2062
|
}
|
|
2060
2063
|
}
|
|
2061
2064
|
}
|
|
@@ -2079,9 +2082,17 @@ function tarjanScc(graph) {
|
|
|
2079
2082
|
for (const w of edges) {
|
|
2080
2083
|
if (!indices.has(w)) {
|
|
2081
2084
|
strongConnect(w);
|
|
2082
|
-
|
|
2085
|
+
const currentLowlink = lowlink.get(v);
|
|
2086
|
+
const childLowlink = lowlink.get(w);
|
|
2087
|
+
if (currentLowlink !== void 0 && childLowlink !== void 0) {
|
|
2088
|
+
lowlink.set(v, Math.min(currentLowlink, childLowlink));
|
|
2089
|
+
}
|
|
2083
2090
|
} else if (onStack.has(w)) {
|
|
2084
|
-
|
|
2091
|
+
const currentLowlink = lowlink.get(v);
|
|
2092
|
+
const childIndex = indices.get(w);
|
|
2093
|
+
if (currentLowlink !== void 0 && childIndex !== void 0) {
|
|
2094
|
+
lowlink.set(v, Math.min(currentLowlink, childIndex));
|
|
2095
|
+
}
|
|
2085
2096
|
}
|
|
2086
2097
|
}
|
|
2087
2098
|
if (lowlink.get(v) === indices.get(v)) {
|
|
@@ -2103,18 +2114,21 @@ function tarjanScc(graph) {
|
|
|
2103
2114
|
}
|
|
2104
2115
|
function parseMaxImportDepth(rule) {
|
|
2105
2116
|
const m = rule.match(/maximum\s{1,5}import\s{1,5}depth\s{0,5}[:=]?\s{0,5}(\d+)/i);
|
|
2106
|
-
|
|
2117
|
+
const depthText = m?.[1];
|
|
2118
|
+
return depthText ? Number.parseInt(depthText, 10) : null;
|
|
2107
2119
|
}
|
|
2108
2120
|
function parseBannedDependency(rule) {
|
|
2109
2121
|
const m = rule.match(/no\s{1,5}dependencies?\s{1,5}on\s{1,5}(?:package\s{1,5})?(.+?)(?:\.|$)/i);
|
|
2110
|
-
|
|
2111
|
-
|
|
2122
|
+
const value = m?.[1]?.trim();
|
|
2123
|
+
if (!value) return null;
|
|
2112
2124
|
return value.length > 0 ? value : null;
|
|
2113
2125
|
}
|
|
2114
2126
|
function parseLayerRule(rule) {
|
|
2115
2127
|
const m = rule.match(/(\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);
|
|
2116
|
-
|
|
2117
|
-
|
|
2128
|
+
const fromLayer = m?.[1]?.toLowerCase();
|
|
2129
|
+
const toLayer = m?.[2]?.toLowerCase();
|
|
2130
|
+
if (!fromLayer || !toLayer) return null;
|
|
2131
|
+
return { fromLayer, toLayer };
|
|
2118
2132
|
}
|
|
2119
2133
|
function fileInLayer(filePath, layer) {
|
|
2120
2134
|
const fp = normalizeFsPath(filePath).toLowerCase();
|
|
@@ -2218,10 +2232,12 @@ var DependencyVerifier = class {
|
|
|
2218
2232
|
};
|
|
2219
2233
|
|
|
2220
2234
|
// src/verification/verifiers/complexity.ts
|
|
2235
|
+
import { Node as Node4 } from "ts-morph";
|
|
2221
2236
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2222
2237
|
function parseLimit(rule, pattern) {
|
|
2223
2238
|
const m = rule.match(pattern);
|
|
2224
|
-
|
|
2239
|
+
const value = m?.[1];
|
|
2240
|
+
return value ? Number.parseInt(value, 10) : null;
|
|
2225
2241
|
}
|
|
2226
2242
|
function getFileLineCount(text) {
|
|
2227
2243
|
if (text.length === 0) return 0;
|
|
@@ -2257,14 +2273,15 @@ function calculateCyclomaticComplexity(fn) {
|
|
|
2257
2273
|
return 1 + getDecisionPoints(fn);
|
|
2258
2274
|
}
|
|
2259
2275
|
function getFunctionDisplayName(fn) {
|
|
2260
|
-
if (
|
|
2276
|
+
if (Node4.isFunctionDeclaration(fn) || Node4.isMethodDeclaration(fn) || Node4.isFunctionExpression(fn)) {
|
|
2261
2277
|
const name = fn.getName();
|
|
2262
|
-
if (typeof name === "string" && name.length > 0)
|
|
2278
|
+
if (typeof name === "string" && name.length > 0) {
|
|
2279
|
+
return name;
|
|
2280
|
+
}
|
|
2263
2281
|
}
|
|
2264
2282
|
const parent = fn.getParent();
|
|
2265
|
-
if (parent
|
|
2266
|
-
|
|
2267
|
-
if (typeof vd.getName === "function") return vd.getName();
|
|
2283
|
+
if (parent && Node4.isVariableDeclaration(parent)) {
|
|
2284
|
+
return parent.getName();
|
|
2268
2285
|
}
|
|
2269
2286
|
return "<anonymous>";
|
|
2270
2287
|
}
|
|
@@ -2334,9 +2351,8 @@ var ComplexityVerifier = class {
|
|
|
2334
2351
|
}));
|
|
2335
2352
|
}
|
|
2336
2353
|
}
|
|
2337
|
-
if (maxParams !== null
|
|
2338
|
-
const
|
|
2339
|
-
const paramCount = Array.isArray(params) ? params.length : 0;
|
|
2354
|
+
if (maxParams !== null) {
|
|
2355
|
+
const paramCount = fn.getParameters().length;
|
|
2340
2356
|
if (paramCount > maxParams) {
|
|
2341
2357
|
violations.push(createViolation({
|
|
2342
2358
|
decisionId,
|
|
@@ -2410,8 +2426,7 @@ var SecurityVerifier = class {
|
|
|
2410
2426
|
}));
|
|
2411
2427
|
}
|
|
2412
2428
|
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2413
|
-
const
|
|
2414
|
-
const propName = nameNode?.getText?.() ?? "";
|
|
2429
|
+
const propName = pa.getNameNode().getText();
|
|
2415
2430
|
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2416
2431
|
const init = pa.getInitializer();
|
|
2417
2432
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
@@ -2447,9 +2462,9 @@ var SecurityVerifier = class {
|
|
|
2447
2462
|
if (checkXss) {
|
|
2448
2463
|
for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2449
2464
|
const left = bin.getLeft();
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
if (
|
|
2465
|
+
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2466
|
+
if (!propertyAccess) continue;
|
|
2467
|
+
if (propertyAccess.getName() === "innerHTML") {
|
|
2453
2468
|
violations.push(createViolation({
|
|
2454
2469
|
decisionId,
|
|
2455
2470
|
constraintId: constraint.id,
|
|
@@ -2478,8 +2493,9 @@ var SecurityVerifier = class {
|
|
|
2478
2493
|
if (checkSql) {
|
|
2479
2494
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2480
2495
|
const expr = call.getExpression();
|
|
2481
|
-
|
|
2482
|
-
|
|
2496
|
+
const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2497
|
+
if (!propertyAccess) continue;
|
|
2498
|
+
const name = propertyAccess.getName();
|
|
2483
2499
|
if (name !== "query" && name !== "execute") continue;
|
|
2484
2500
|
const arg = call.getArguments()[0];
|
|
2485
2501
|
if (!arg) continue;
|
|
@@ -2544,12 +2560,14 @@ var ApiVerifier = class {
|
|
|
2544
2560
|
if (!enforceKebab) return violations;
|
|
2545
2561
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
2546
2562
|
const expr = call.getExpression();
|
|
2547
|
-
|
|
2548
|
-
|
|
2563
|
+
const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
|
|
2564
|
+
if (!propertyAccess) continue;
|
|
2565
|
+
const method = propertyAccess.getName();
|
|
2549
2566
|
if (!method || !HTTP_METHODS.has(String(method))) continue;
|
|
2550
2567
|
const firstArg = call.getArguments()[0];
|
|
2551
|
-
|
|
2552
|
-
|
|
2568
|
+
const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
|
|
2569
|
+
if (!stringLiteral) continue;
|
|
2570
|
+
const pathValue = stringLiteral.getLiteralValue();
|
|
2553
2571
|
if (typeof pathValue !== "string") continue;
|
|
2554
2572
|
if (!isKebabPath(pathValue)) {
|
|
2555
2573
|
violations.push(createViolation({
|
|
@@ -3446,7 +3464,8 @@ var AutofixEngine = class {
|
|
|
3446
3464
|
};
|
|
3447
3465
|
|
|
3448
3466
|
// src/propagation/graph.ts
|
|
3449
|
-
async function buildDependencyGraph2(decisions, files) {
|
|
3467
|
+
async function buildDependencyGraph2(decisions, files, options = {}) {
|
|
3468
|
+
const { cwd } = options;
|
|
3450
3469
|
const nodes = /* @__PURE__ */ new Map();
|
|
3451
3470
|
const decisionToFiles = /* @__PURE__ */ new Map();
|
|
3452
3471
|
const fileToDecisions = /* @__PURE__ */ new Map();
|
|
@@ -3461,7 +3480,7 @@ async function buildDependencyGraph2(decisions, files) {
|
|
|
3461
3480
|
const constraintId = `constraint:${decision.metadata.id}/${constraint.id}`;
|
|
3462
3481
|
const matchingFiles = [];
|
|
3463
3482
|
for (const file of files) {
|
|
3464
|
-
if (matchesPattern(file, constraint.scope)) {
|
|
3483
|
+
if (matchesPattern(file, constraint.scope, { cwd })) {
|
|
3465
3484
|
matchingFiles.push(`file:${file}`);
|
|
3466
3485
|
const fileDecisions = fileToDecisions.get(file) || /* @__PURE__ */ new Set();
|
|
3467
3486
|
fileDecisions.add(decision.metadata.id);
|
|
@@ -3537,7 +3556,7 @@ var PropagationEngine = class {
|
|
|
3537
3556
|
absolute: true
|
|
3538
3557
|
});
|
|
3539
3558
|
const decisions = this.registry.getActive();
|
|
3540
|
-
this.graph = await buildDependencyGraph2(decisions, files);
|
|
3559
|
+
this.graph = await buildDependencyGraph2(decisions, files, { cwd });
|
|
3541
3560
|
}
|
|
3542
3561
|
/**
|
|
3543
3562
|
* Analyze impact of changing a decision
|
|
@@ -3855,7 +3874,10 @@ var Reporter = class {
|
|
|
3855
3874
|
result.violations.forEach((v) => {
|
|
3856
3875
|
const key = v.severity;
|
|
3857
3876
|
if (!grouped.has(key)) grouped.set(key, []);
|
|
3858
|
-
grouped.get(key)
|
|
3877
|
+
const bucket = grouped.get(key);
|
|
3878
|
+
if (bucket) {
|
|
3879
|
+
bucket.push(v);
|
|
3880
|
+
}
|
|
3859
3881
|
});
|
|
3860
3882
|
for (const [severity, violations] of grouped.entries()) {
|
|
3861
3883
|
lines.push(`Severity: ${severity}`);
|
|
@@ -3870,7 +3892,10 @@ var Reporter = class {
|
|
|
3870
3892
|
result.violations.forEach((v) => {
|
|
3871
3893
|
const key = v.location?.file || v.file || "unknown";
|
|
3872
3894
|
if (!grouped.has(key)) grouped.set(key, []);
|
|
3873
|
-
grouped.get(key)
|
|
3895
|
+
const bucket = grouped.get(key);
|
|
3896
|
+
if (bucket) {
|
|
3897
|
+
bucket.push(v);
|
|
3898
|
+
}
|
|
3874
3899
|
});
|
|
3875
3900
|
for (const [file, violations] of grouped.entries()) {
|
|
3876
3901
|
lines.push(`File: ${file}`);
|
|
@@ -4326,7 +4351,10 @@ async function analyzeTrend(reports) {
|
|
|
4326
4351
|
if (!decisionMap.has(decision.decisionId)) {
|
|
4327
4352
|
decisionMap.set(decision.decisionId, []);
|
|
4328
4353
|
}
|
|
4329
|
-
decisionMap.get(decision.decisionId)
|
|
4354
|
+
const decisionHistory = decisionMap.get(decision.decisionId);
|
|
4355
|
+
if (decisionHistory) {
|
|
4356
|
+
decisionHistory.push(decision);
|
|
4357
|
+
}
|
|
4330
4358
|
}
|
|
4331
4359
|
}
|
|
4332
4360
|
const decisions = Array.from(decisionMap.entries()).map(([decisionId, data]) => {
|
|
@@ -5156,7 +5184,8 @@ var DashboardServer = class {
|
|
|
5156
5184
|
});
|
|
5157
5185
|
this.app.get("/api/report/:date", async (req, res) => {
|
|
5158
5186
|
try {
|
|
5159
|
-
const
|
|
5187
|
+
const dateParam = req.params.date;
|
|
5188
|
+
const date = Array.isArray(dateParam) ? dateParam[0] : dateParam;
|
|
5160
5189
|
if (!date) {
|
|
5161
5190
|
res.status(400).json({ error: "Date parameter required" });
|
|
5162
5191
|
return;
|
|
@@ -5192,7 +5221,8 @@ var DashboardServer = class {
|
|
|
5192
5221
|
});
|
|
5193
5222
|
this.app.get("/api/decisions/:id", async (req, res) => {
|
|
5194
5223
|
try {
|
|
5195
|
-
const
|
|
5224
|
+
const idParam = req.params.id;
|
|
5225
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
5196
5226
|
if (!id) {
|
|
5197
5227
|
res.status(400).json({ error: "Decision ID required" });
|
|
5198
5228
|
return;
|
|
@@ -5204,6 +5234,10 @@ var DashboardServer = class {
|
|
|
5204
5234
|
}
|
|
5205
5235
|
res.json(decision);
|
|
5206
5236
|
} catch (error) {
|
|
5237
|
+
if (error instanceof DecisionNotFoundError) {
|
|
5238
|
+
res.status(404).json({ error: "Decision not found" });
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5207
5241
|
res.status(500).json({
|
|
5208
5242
|
error: "Failed to load decision",
|
|
5209
5243
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
@@ -5235,7 +5269,8 @@ var DashboardServer = class {
|
|
|
5235
5269
|
});
|
|
5236
5270
|
this.app.get("/api/analytics/decision/:id", async (req, res) => {
|
|
5237
5271
|
try {
|
|
5238
|
-
const
|
|
5272
|
+
const idParam = req.params.id;
|
|
5273
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
5239
5274
|
if (!id) {
|
|
5240
5275
|
res.status(400).json({ error: "Decision ID required" });
|
|
5241
5276
|
return;
|
|
@@ -5324,7 +5359,7 @@ var DashboardServer = class {
|
|
|
5324
5359
|
// Cache static assets
|
|
5325
5360
|
etag: true
|
|
5326
5361
|
}));
|
|
5327
|
-
this.app.get("*", (_req, res) => {
|
|
5362
|
+
this.app.get("/{*path}", (_req, res) => {
|
|
5328
5363
|
res.sendFile(join5(publicDir, "index.html"));
|
|
5329
5364
|
});
|
|
5330
5365
|
}
|
|
@@ -5421,7 +5456,11 @@ var SpecBridgeLspServer = class {
|
|
|
5421
5456
|
const doc = this.documents.get(params.textDocument.uri);
|
|
5422
5457
|
if (!doc) return [];
|
|
5423
5458
|
return violations.filter((v) => v.autofix && v.autofix.edits.length > 0).map((v) => {
|
|
5424
|
-
const
|
|
5459
|
+
const autofix = v.autofix;
|
|
5460
|
+
if (!autofix) {
|
|
5461
|
+
return null;
|
|
5462
|
+
}
|
|
5463
|
+
const edits = autofix.edits.map((edit) => ({
|
|
5425
5464
|
range: {
|
|
5426
5465
|
start: doc.positionAt(edit.start),
|
|
5427
5466
|
end: doc.positionAt(edit.end)
|
|
@@ -5429,7 +5468,7 @@ var SpecBridgeLspServer = class {
|
|
|
5429
5468
|
newText: edit.text
|
|
5430
5469
|
}));
|
|
5431
5470
|
return {
|
|
5432
|
-
title:
|
|
5471
|
+
title: autofix.description,
|
|
5433
5472
|
kind: CodeActionKind.QuickFix,
|
|
5434
5473
|
edit: {
|
|
5435
5474
|
changes: {
|
|
@@ -5437,7 +5476,7 @@ var SpecBridgeLspServer = class {
|
|
|
5437
5476
|
}
|
|
5438
5477
|
}
|
|
5439
5478
|
};
|
|
5440
|
-
});
|
|
5479
|
+
}).filter((action) => action !== null);
|
|
5441
5480
|
});
|
|
5442
5481
|
this.documents.listen(this.connection);
|
|
5443
5482
|
this.connection.listen();
|