@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/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.errors.map((err) => {
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.errors.map((e) => `${e.path.join(".")}: ${e.message}`);
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).add(normalizeFsPath(resolved));
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
- lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
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
- lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
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
- return m ? Number.parseInt(m[1], 10) : null;
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
- if (!m) return null;
2111
- const value = m[1].trim();
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
- if (!m) return null;
2117
- return { fromLayer: m[1].toLowerCase(), toLayer: m[2].toLowerCase() };
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
- return m ? Number.parseInt(m[1], 10) : null;
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 ("getName" in fn && typeof fn.getName === "function") {
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) return name;
2278
+ if (typeof name === "string" && name.length > 0) {
2279
+ return name;
2280
+ }
2263
2281
  }
2264
2282
  const parent = fn.getParent();
2265
- if (parent?.getKind() === SyntaxKind2.VariableDeclaration) {
2266
- const vd = parent;
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 && "getParameters" in fn) {
2338
- const params = fn.getParameters();
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 nameNode = pa.getNameNode?.();
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
- if (left.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2451
- const pa = left;
2452
- if (pa.getName?.() === "innerHTML") {
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
- if (expr.getKind() !== SyntaxKind3.PropertyAccessExpression) continue;
2482
- const name = expr.getName?.();
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
- if (expr.getKind() !== SyntaxKind4.PropertyAccessExpression) continue;
2548
- const method = expr.getName?.();
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
- if (!firstArg || firstArg.getKind() !== SyntaxKind4.StringLiteral) continue;
2552
- const pathValue = firstArg.getLiteralValue?.() ?? firstArg.getText().slice(1, -1);
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).push(v);
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).push(v);
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).push(decision);
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 date = req.params.date;
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 id = req.params.id;
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 id = req.params.id;
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 edits = v.autofix.edits.map((edit) => ({
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: v.autofix.description,
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();