@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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,38 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.0] - 2026-02-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Integration test suites for:
|
|
15
|
+
- MCP server (`tests/integration/mcp-server.test.ts`)
|
|
16
|
+
- LSP server (`tests/integration/lsp-server.test.ts`)
|
|
17
|
+
- Dashboard server (`tests/integration/dashboard-server.test.ts`)
|
|
18
|
+
- Propagation engine (`tests/integration/propagation.test.ts`)
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Upgraded major runtime dependencies:
|
|
23
|
+
- `zod` 3.x → 4.x
|
|
24
|
+
- `commander` 12.x → 14.x
|
|
25
|
+
- `ts-morph` 24.x → 27.x
|
|
26
|
+
- `chokidar` 3.x → 5.x
|
|
27
|
+
- `express` 4.x → 5.x (with `@types/express` 5.x)
|
|
28
|
+
- Updated CLI unit tests for Commander v14 parse semantics.
|
|
29
|
+
- Raised coverage thresholds to:
|
|
30
|
+
- lines: `70`
|
|
31
|
+
- statements: `69`
|
|
32
|
+
- functions: `73`
|
|
33
|
+
- branches: `60`
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Express 5 dashboard fallback routing compatibility (`*` → `/{*path}`).
|
|
38
|
+
- Dashboard API now returns `404` for missing decisions instead of `500`.
|
|
39
|
+
- Propagation graph matching now respects the analysis `cwd` for absolute file paths.
|
|
40
|
+
- Removed all remaining production non-null assertions in `src/**/*.ts`.
|
|
41
|
+
|
|
10
42
|
## [2.2.0] - 2026-02-06
|
|
11
43
|
|
|
12
44
|
### Infrastructure Modernization & Security
|
package/dist/cli.js
CHANGED
|
@@ -1261,7 +1261,7 @@ async function loadConfig(basePath = process.cwd()) {
|
|
|
1261
1261
|
const parsed = parseYaml(content);
|
|
1262
1262
|
const result = validateConfig(parsed);
|
|
1263
1263
|
if (!result.success) {
|
|
1264
|
-
const errors = result.errors.
|
|
1264
|
+
const errors = result.errors.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
|
|
1265
1265
|
throw new ConfigError(`Invalid configuration in ${configPath}`, { errors });
|
|
1266
1266
|
}
|
|
1267
1267
|
return result.data;
|
|
@@ -1385,7 +1385,7 @@ var ConstraintExceptionSchema = z2.object({
|
|
|
1385
1385
|
});
|
|
1386
1386
|
var ConstraintCheckSchema = z2.object({
|
|
1387
1387
|
verifier: z2.string().min(1),
|
|
1388
|
-
params: z2.record(z2.unknown()).optional()
|
|
1388
|
+
params: z2.record(z2.string(), z2.unknown()).optional()
|
|
1389
1389
|
});
|
|
1390
1390
|
var ConstraintSchema = z2.object({
|
|
1391
1391
|
id: z2.string().min(1).regex(/^[a-z0-9-]+$/, "Constraint ID must be lowercase alphanumeric with hyphens"),
|
|
@@ -1427,7 +1427,7 @@ function validateDecision(data) {
|
|
|
1427
1427
|
return { success: false, errors: result.error };
|
|
1428
1428
|
}
|
|
1429
1429
|
function formatValidationErrors(errors) {
|
|
1430
|
-
return errors.
|
|
1430
|
+
return errors.issues.map((err) => {
|
|
1431
1431
|
const path5 = err.path.join(".");
|
|
1432
1432
|
return `${path5}: ${err.message}`;
|
|
1433
1433
|
});
|
|
@@ -2149,7 +2149,10 @@ function buildDependencyGraph(project) {
|
|
|
2149
2149
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
2150
2150
|
const resolved = resolveToSourceFilePath(project, from, moduleSpec);
|
|
2151
2151
|
if (resolved) {
|
|
2152
|
-
graph.get(from)
|
|
2152
|
+
const dependencies = graph.get(from);
|
|
2153
|
+
if (dependencies) {
|
|
2154
|
+
dependencies.add(normalizeFsPath(resolved));
|
|
2155
|
+
}
|
|
2153
2156
|
}
|
|
2154
2157
|
}
|
|
2155
2158
|
}
|
|
@@ -2173,9 +2176,17 @@ function tarjanScc(graph) {
|
|
|
2173
2176
|
for (const w of edges) {
|
|
2174
2177
|
if (!indices.has(w)) {
|
|
2175
2178
|
strongConnect(w);
|
|
2176
|
-
|
|
2179
|
+
const currentLowlink = lowlink.get(v);
|
|
2180
|
+
const childLowlink = lowlink.get(w);
|
|
2181
|
+
if (currentLowlink !== void 0 && childLowlink !== void 0) {
|
|
2182
|
+
lowlink.set(v, Math.min(currentLowlink, childLowlink));
|
|
2183
|
+
}
|
|
2177
2184
|
} else if (onStack.has(w)) {
|
|
2178
|
-
|
|
2185
|
+
const currentLowlink = lowlink.get(v);
|
|
2186
|
+
const childIndex = indices.get(w);
|
|
2187
|
+
if (currentLowlink !== void 0 && childIndex !== void 0) {
|
|
2188
|
+
lowlink.set(v, Math.min(currentLowlink, childIndex));
|
|
2189
|
+
}
|
|
2179
2190
|
}
|
|
2180
2191
|
}
|
|
2181
2192
|
if (lowlink.get(v) === indices.get(v)) {
|
|
@@ -2197,18 +2208,21 @@ function tarjanScc(graph) {
|
|
|
2197
2208
|
}
|
|
2198
2209
|
function parseMaxImportDepth(rule) {
|
|
2199
2210
|
const m = rule.match(/maximum\s{1,5}import\s{1,5}depth\s{0,5}[:=]?\s{0,5}(\d+)/i);
|
|
2200
|
-
|
|
2211
|
+
const depthText = m?.[1];
|
|
2212
|
+
return depthText ? Number.parseInt(depthText, 10) : null;
|
|
2201
2213
|
}
|
|
2202
2214
|
function parseBannedDependency(rule) {
|
|
2203
2215
|
const m = rule.match(/no\s{1,5}dependencies?\s{1,5}on\s{1,5}(?:package\s{1,5})?(.+?)(?:\.|$)/i);
|
|
2204
|
-
|
|
2205
|
-
|
|
2216
|
+
const value = m?.[1]?.trim();
|
|
2217
|
+
if (!value) return null;
|
|
2206
2218
|
return value.length > 0 ? value : null;
|
|
2207
2219
|
}
|
|
2208
2220
|
function parseLayerRule(rule) {
|
|
2209
2221
|
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);
|
|
2210
|
-
|
|
2211
|
-
|
|
2222
|
+
const fromLayer = m?.[1]?.toLowerCase();
|
|
2223
|
+
const toLayer = m?.[2]?.toLowerCase();
|
|
2224
|
+
if (!fromLayer || !toLayer) return null;
|
|
2225
|
+
return { fromLayer, toLayer };
|
|
2212
2226
|
}
|
|
2213
2227
|
function fileInLayer(filePath, layer) {
|
|
2214
2228
|
const fp = normalizeFsPath(filePath).toLowerCase();
|
|
@@ -2312,10 +2326,12 @@ var DependencyVerifier = class {
|
|
|
2312
2326
|
};
|
|
2313
2327
|
|
|
2314
2328
|
// src/verification/verifiers/complexity.ts
|
|
2329
|
+
import { Node as Node4 } from "ts-morph";
|
|
2315
2330
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2316
2331
|
function parseLimit(rule, pattern) {
|
|
2317
2332
|
const m = rule.match(pattern);
|
|
2318
|
-
|
|
2333
|
+
const value = m?.[1];
|
|
2334
|
+
return value ? Number.parseInt(value, 10) : null;
|
|
2319
2335
|
}
|
|
2320
2336
|
function getFileLineCount(text) {
|
|
2321
2337
|
if (text.length === 0) return 0;
|
|
@@ -2351,14 +2367,15 @@ function calculateCyclomaticComplexity(fn) {
|
|
|
2351
2367
|
return 1 + getDecisionPoints(fn);
|
|
2352
2368
|
}
|
|
2353
2369
|
function getFunctionDisplayName(fn) {
|
|
2354
|
-
if (
|
|
2370
|
+
if (Node4.isFunctionDeclaration(fn) || Node4.isMethodDeclaration(fn) || Node4.isFunctionExpression(fn)) {
|
|
2355
2371
|
const name = fn.getName();
|
|
2356
|
-
if (typeof name === "string" && name.length > 0)
|
|
2372
|
+
if (typeof name === "string" && name.length > 0) {
|
|
2373
|
+
return name;
|
|
2374
|
+
}
|
|
2357
2375
|
}
|
|
2358
2376
|
const parent = fn.getParent();
|
|
2359
|
-
if (parent
|
|
2360
|
-
|
|
2361
|
-
if (typeof vd.getName === "function") return vd.getName();
|
|
2377
|
+
if (parent && Node4.isVariableDeclaration(parent)) {
|
|
2378
|
+
return parent.getName();
|
|
2362
2379
|
}
|
|
2363
2380
|
return "<anonymous>";
|
|
2364
2381
|
}
|
|
@@ -2428,9 +2445,8 @@ var ComplexityVerifier = class {
|
|
|
2428
2445
|
}));
|
|
2429
2446
|
}
|
|
2430
2447
|
}
|
|
2431
|
-
if (maxParams !== null
|
|
2432
|
-
const
|
|
2433
|
-
const paramCount = Array.isArray(params) ? params.length : 0;
|
|
2448
|
+
if (maxParams !== null) {
|
|
2449
|
+
const paramCount = fn.getParameters().length;
|
|
2434
2450
|
if (paramCount > maxParams) {
|
|
2435
2451
|
violations.push(createViolation({
|
|
2436
2452
|
decisionId,
|
|
@@ -2504,8 +2520,7 @@ var SecurityVerifier = class {
|
|
|
2504
2520
|
}));
|
|
2505
2521
|
}
|
|
2506
2522
|
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2507
|
-
const
|
|
2508
|
-
const propName = nameNode?.getText?.() ?? "";
|
|
2523
|
+
const propName = pa.getNameNode().getText();
|
|
2509
2524
|
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2510
2525
|
const init = pa.getInitializer();
|
|
2511
2526
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
@@ -2541,9 +2556,9 @@ var SecurityVerifier = class {
|
|
|
2541
2556
|
if (checkXss) {
|
|
2542
2557
|
for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2543
2558
|
const left = bin.getLeft();
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (
|
|
2559
|
+
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2560
|
+
if (!propertyAccess) continue;
|
|
2561
|
+
if (propertyAccess.getName() === "innerHTML") {
|
|
2547
2562
|
violations.push(createViolation({
|
|
2548
2563
|
decisionId,
|
|
2549
2564
|
constraintId: constraint.id,
|
|
@@ -2572,8 +2587,9 @@ var SecurityVerifier = class {
|
|
|
2572
2587
|
if (checkSql) {
|
|
2573
2588
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2574
2589
|
const expr = call.getExpression();
|
|
2575
|
-
|
|
2576
|
-
|
|
2590
|
+
const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2591
|
+
if (!propertyAccess) continue;
|
|
2592
|
+
const name = propertyAccess.getName();
|
|
2577
2593
|
if (name !== "query" && name !== "execute") continue;
|
|
2578
2594
|
const arg = call.getArguments()[0];
|
|
2579
2595
|
if (!arg) continue;
|
|
@@ -2638,12 +2654,14 @@ var ApiVerifier = class {
|
|
|
2638
2654
|
if (!enforceKebab) return violations;
|
|
2639
2655
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
2640
2656
|
const expr = call.getExpression();
|
|
2641
|
-
|
|
2642
|
-
|
|
2657
|
+
const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
|
|
2658
|
+
if (!propertyAccess) continue;
|
|
2659
|
+
const method = propertyAccess.getName();
|
|
2643
2660
|
if (!method || !HTTP_METHODS.has(String(method))) continue;
|
|
2644
2661
|
const firstArg = call.getArguments()[0];
|
|
2645
|
-
|
|
2646
|
-
|
|
2662
|
+
const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
|
|
2663
|
+
if (!stringLiteral) continue;
|
|
2664
|
+
const pathValue = stringLiteral.getLiteralValue();
|
|
2647
2665
|
if (typeof pathValue !== "string") continue;
|
|
2648
2666
|
if (!isKebabPath(pathValue)) {
|
|
2649
2667
|
violations.push(createViolation({
|
|
@@ -4805,7 +4823,10 @@ async function analyzeTrend(reports) {
|
|
|
4805
4823
|
if (!decisionMap.has(decision.decisionId)) {
|
|
4806
4824
|
decisionMap.set(decision.decisionId, []);
|
|
4807
4825
|
}
|
|
4808
|
-
decisionMap.get(decision.decisionId)
|
|
4826
|
+
const decisionHistory = decisionMap.get(decision.decisionId);
|
|
4827
|
+
if (decisionHistory) {
|
|
4828
|
+
decisionHistory.push(decision);
|
|
4829
|
+
}
|
|
4809
4830
|
}
|
|
4810
4831
|
}
|
|
4811
4832
|
const decisions = Array.from(decisionMap.entries()).map(([decisionId, data]) => {
|
|
@@ -5247,7 +5268,11 @@ var SpecBridgeLspServer = class {
|
|
|
5247
5268
|
const doc = this.documents.get(params.textDocument.uri);
|
|
5248
5269
|
if (!doc) return [];
|
|
5249
5270
|
return violations.filter((v) => v.autofix && v.autofix.edits.length > 0).map((v) => {
|
|
5250
|
-
const
|
|
5271
|
+
const autofix = v.autofix;
|
|
5272
|
+
if (!autofix) {
|
|
5273
|
+
return null;
|
|
5274
|
+
}
|
|
5275
|
+
const edits = autofix.edits.map((edit) => ({
|
|
5251
5276
|
range: {
|
|
5252
5277
|
start: doc.positionAt(edit.start),
|
|
5253
5278
|
end: doc.positionAt(edit.end)
|
|
@@ -5255,7 +5280,7 @@ var SpecBridgeLspServer = class {
|
|
|
5255
5280
|
newText: edit.text
|
|
5256
5281
|
}));
|
|
5257
5282
|
return {
|
|
5258
|
-
title:
|
|
5283
|
+
title: autofix.description,
|
|
5259
5284
|
kind: CodeActionKind.QuickFix,
|
|
5260
5285
|
edit: {
|
|
5261
5286
|
changes: {
|
|
@@ -5263,7 +5288,7 @@ var SpecBridgeLspServer = class {
|
|
|
5263
5288
|
}
|
|
5264
5289
|
}
|
|
5265
5290
|
};
|
|
5266
|
-
});
|
|
5291
|
+
}).filter((action) => action !== null);
|
|
5267
5292
|
});
|
|
5268
5293
|
this.documents.listen(this.connection);
|
|
5269
5294
|
this.connection.listen();
|
|
@@ -6178,7 +6203,8 @@ var DashboardServer = class {
|
|
|
6178
6203
|
});
|
|
6179
6204
|
this.app.get("/api/report/:date", async (req, res) => {
|
|
6180
6205
|
try {
|
|
6181
|
-
const
|
|
6206
|
+
const dateParam = req.params.date;
|
|
6207
|
+
const date = Array.isArray(dateParam) ? dateParam[0] : dateParam;
|
|
6182
6208
|
if (!date) {
|
|
6183
6209
|
res.status(400).json({ error: "Date parameter required" });
|
|
6184
6210
|
return;
|
|
@@ -6214,7 +6240,8 @@ var DashboardServer = class {
|
|
|
6214
6240
|
});
|
|
6215
6241
|
this.app.get("/api/decisions/:id", async (req, res) => {
|
|
6216
6242
|
try {
|
|
6217
|
-
const
|
|
6243
|
+
const idParam = req.params.id;
|
|
6244
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
6218
6245
|
if (!id) {
|
|
6219
6246
|
res.status(400).json({ error: "Decision ID required" });
|
|
6220
6247
|
return;
|
|
@@ -6226,6 +6253,10 @@ var DashboardServer = class {
|
|
|
6226
6253
|
}
|
|
6227
6254
|
res.json(decision);
|
|
6228
6255
|
} catch (error) {
|
|
6256
|
+
if (error instanceof DecisionNotFoundError) {
|
|
6257
|
+
res.status(404).json({ error: "Decision not found" });
|
|
6258
|
+
return;
|
|
6259
|
+
}
|
|
6229
6260
|
res.status(500).json({
|
|
6230
6261
|
error: "Failed to load decision",
|
|
6231
6262
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
@@ -6257,7 +6288,8 @@ var DashboardServer = class {
|
|
|
6257
6288
|
});
|
|
6258
6289
|
this.app.get("/api/analytics/decision/:id", async (req, res) => {
|
|
6259
6290
|
try {
|
|
6260
|
-
const
|
|
6291
|
+
const idParam = req.params.id;
|
|
6292
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
6261
6293
|
if (!id) {
|
|
6262
6294
|
res.status(400).json({ error: "Decision ID required" });
|
|
6263
6295
|
return;
|
|
@@ -6346,7 +6378,7 @@ var DashboardServer = class {
|
|
|
6346
6378
|
// Cache static assets
|
|
6347
6379
|
etag: true
|
|
6348
6380
|
}));
|
|
6349
|
-
this.app.get("*", (_req, res) => {
|
|
6381
|
+
this.app.get("/{*path}", (_req, res) => {
|
|
6350
6382
|
res.sendFile(join12(publicDir, "index.html"));
|
|
6351
6383
|
});
|
|
6352
6384
|
}
|
|
@@ -6398,7 +6430,8 @@ import chalk18 from "chalk";
|
|
|
6398
6430
|
import ora8 from "ora";
|
|
6399
6431
|
|
|
6400
6432
|
// src/propagation/graph.ts
|
|
6401
|
-
async function buildDependencyGraph2(decisions, files) {
|
|
6433
|
+
async function buildDependencyGraph2(decisions, files, options = {}) {
|
|
6434
|
+
const { cwd } = options;
|
|
6402
6435
|
const nodes = /* @__PURE__ */ new Map();
|
|
6403
6436
|
const decisionToFiles = /* @__PURE__ */ new Map();
|
|
6404
6437
|
const fileToDecisions = /* @__PURE__ */ new Map();
|
|
@@ -6413,7 +6446,7 @@ async function buildDependencyGraph2(decisions, files) {
|
|
|
6413
6446
|
const constraintId = `constraint:${decision.metadata.id}/${constraint.id}`;
|
|
6414
6447
|
const matchingFiles = [];
|
|
6415
6448
|
for (const file of files) {
|
|
6416
|
-
if (matchesPattern(file, constraint.scope)) {
|
|
6449
|
+
if (matchesPattern(file, constraint.scope, { cwd })) {
|
|
6417
6450
|
matchingFiles.push(`file:${file}`);
|
|
6418
6451
|
const fileDecisions = fileToDecisions.get(file) || /* @__PURE__ */ new Set();
|
|
6419
6452
|
fileDecisions.add(decision.metadata.id);
|
|
@@ -6470,7 +6503,7 @@ var PropagationEngine = class {
|
|
|
6470
6503
|
absolute: true
|
|
6471
6504
|
});
|
|
6472
6505
|
const decisions = this.registry.getActive();
|
|
6473
|
-
this.graph = await buildDependencyGraph2(decisions, files);
|
|
6506
|
+
this.graph = await buildDependencyGraph2(decisions, files, { cwd });
|
|
6474
6507
|
}
|
|
6475
6508
|
/**
|
|
6476
6509
|
* Analyze impact of changing a decision
|