@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 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.errors.map((e) => `${e.path.join(".")}: ${e.message}`);
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.errors.map((err) => {
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).add(normalizeFsPath(resolved));
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
- lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
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
- lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
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
- return m ? Number.parseInt(m[1], 10) : null;
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
- if (!m) return null;
2205
- const value = m[1].trim();
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
- if (!m) return null;
2211
- return { fromLayer: m[1].toLowerCase(), toLayer: m[2].toLowerCase() };
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
- return m ? Number.parseInt(m[1], 10) : null;
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 ("getName" in fn && typeof fn.getName === "function") {
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) return name;
2372
+ if (typeof name === "string" && name.length > 0) {
2373
+ return name;
2374
+ }
2357
2375
  }
2358
2376
  const parent = fn.getParent();
2359
- if (parent?.getKind() === SyntaxKind2.VariableDeclaration) {
2360
- const vd = parent;
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 && "getParameters" in fn) {
2432
- const params = fn.getParameters();
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 nameNode = pa.getNameNode?.();
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
- if (left.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2545
- const pa = left;
2546
- if (pa.getName?.() === "innerHTML") {
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
- if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2576
- const name = expr.getName?.();
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
- if (expr.getKind() !== SyntaxKind4.PropertyAccessExpression) continue;
2642
- const method = expr.getName?.();
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
- if (!firstArg || firstArg.getKind() !== SyntaxKind4.StringLiteral) continue;
2646
- const pathValue = firstArg.getLiteralValue?.() ?? firstArg.getText().slice(1, -1);
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).push(decision);
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 edits = v.autofix.edits.map((edit) => ({
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: v.autofix.description,
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 date = req.params.date;
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 id = req.params.id;
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 id = req.params.id;
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