@ipation/specbridge 2.2.0 → 2.4.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;
@@ -1582,7 +1582,6 @@ async function runInference(config, options) {
1582
1582
 
1583
1583
  // src/verification/engine.ts
1584
1584
  import { Project as Project2 } from "ts-morph";
1585
- import chalk from "chalk";
1586
1585
 
1587
1586
  // src/verification/verifiers/base.ts
1588
1587
  function defineVerifierPlugin(plugin) {
@@ -2055,7 +2054,10 @@ function buildDependencyGraph(project) {
2055
2054
  const moduleSpec = importDecl.getModuleSpecifierValue();
2056
2055
  const resolved = resolveToSourceFilePath(project, from, moduleSpec);
2057
2056
  if (resolved) {
2058
- graph.get(from).add(normalizeFsPath(resolved));
2057
+ const dependencies = graph.get(from);
2058
+ if (dependencies) {
2059
+ dependencies.add(normalizeFsPath(resolved));
2060
+ }
2059
2061
  }
2060
2062
  }
2061
2063
  }
@@ -2079,9 +2081,17 @@ function tarjanScc(graph) {
2079
2081
  for (const w of edges) {
2080
2082
  if (!indices.has(w)) {
2081
2083
  strongConnect(w);
2082
- lowlink.set(v, Math.min(lowlink.get(v), lowlink.get(w)));
2084
+ const currentLowlink = lowlink.get(v);
2085
+ const childLowlink = lowlink.get(w);
2086
+ if (currentLowlink !== void 0 && childLowlink !== void 0) {
2087
+ lowlink.set(v, Math.min(currentLowlink, childLowlink));
2088
+ }
2083
2089
  } else if (onStack.has(w)) {
2084
- lowlink.set(v, Math.min(lowlink.get(v), indices.get(w)));
2090
+ const currentLowlink = lowlink.get(v);
2091
+ const childIndex = indices.get(w);
2092
+ if (currentLowlink !== void 0 && childIndex !== void 0) {
2093
+ lowlink.set(v, Math.min(currentLowlink, childIndex));
2094
+ }
2085
2095
  }
2086
2096
  }
2087
2097
  if (lowlink.get(v) === indices.get(v)) {
@@ -2103,18 +2113,21 @@ function tarjanScc(graph) {
2103
2113
  }
2104
2114
  function parseMaxImportDepth(rule) {
2105
2115
  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;
2116
+ const depthText = m?.[1];
2117
+ return depthText ? Number.parseInt(depthText, 10) : null;
2107
2118
  }
2108
2119
  function parseBannedDependency(rule) {
2109
2120
  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();
2121
+ const value = m?.[1]?.trim();
2122
+ if (!value) return null;
2112
2123
  return value.length > 0 ? value : null;
2113
2124
  }
2114
2125
  function parseLayerRule(rule) {
2115
2126
  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() };
2127
+ const fromLayer = m?.[1]?.toLowerCase();
2128
+ const toLayer = m?.[2]?.toLowerCase();
2129
+ if (!fromLayer || !toLayer) return null;
2130
+ return { fromLayer, toLayer };
2118
2131
  }
2119
2132
  function fileInLayer(filePath, layer) {
2120
2133
  const fp = normalizeFsPath(filePath).toLowerCase();
@@ -2136,7 +2149,8 @@ var DependencyVerifier = class {
2136
2149
  const sccs = tarjanScc(graph);
2137
2150
  const current = projectFilePath;
2138
2151
  for (const scc of sccs) {
2139
- const hasSelfLoop = scc.length === 1 && (graph.get(scc[0])?.has(scc[0]) ?? false);
2152
+ const first = scc[0];
2153
+ const hasSelfLoop = first !== void 0 && scc.length === 1 && (graph.get(first)?.has(first) ?? false);
2140
2154
  const isCycle = scc.length > 1 || hasSelfLoop;
2141
2155
  if (!isCycle) continue;
2142
2156
  if (!scc.includes(current)) continue;
@@ -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({
@@ -2573,10 +2591,36 @@ import { existsSync } from "fs";
2573
2591
  import { join as join3 } from "path";
2574
2592
  import { pathToFileURL } from "url";
2575
2593
  import fg2 from "fast-glob";
2594
+
2595
+ // src/utils/logger.ts
2596
+ import pino from "pino";
2597
+ var defaultOptions = {
2598
+ level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
2599
+ timestamp: pino.stdTimeFunctions.isoTime,
2600
+ base: {
2601
+ service: "specbridge"
2602
+ }
2603
+ };
2604
+ var destination = pino.destination({
2605
+ fd: 2,
2606
+ // stderr
2607
+ sync: false
2608
+ });
2609
+ var rootLogger = pino(defaultOptions, destination);
2610
+ function getLogger(bindings) {
2611
+ if (!bindings) {
2612
+ return rootLogger;
2613
+ }
2614
+ return rootLogger.child(bindings);
2615
+ }
2616
+ var logger = getLogger();
2617
+
2618
+ // src/verification/plugins/loader.ts
2576
2619
  var PluginLoader = class {
2577
2620
  plugins = /* @__PURE__ */ new Map();
2578
2621
  loaded = false;
2579
2622
  loadErrors = [];
2623
+ logger = getLogger({ module: "verification.plugins.loader" });
2580
2624
  /**
2581
2625
  * Load all plugins from the specified base path
2582
2626
  *
@@ -2599,15 +2643,15 @@ var PluginLoader = class {
2599
2643
  } catch (error) {
2600
2644
  const message = error instanceof Error ? error.message : String(error);
2601
2645
  this.loadErrors.push({ file, error: message });
2602
- console.warn(`Failed to load plugin from ${file}: ${message}`);
2646
+ this.logger.warn({ file, error: message }, "Failed to load plugin");
2603
2647
  }
2604
2648
  }
2605
2649
  this.loaded = true;
2606
2650
  if (this.plugins.size > 0) {
2607
- console.error(`Loaded ${this.plugins.size} custom verifier(s)`);
2651
+ this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
2608
2652
  }
2609
2653
  if (this.loadErrors.length > 0) {
2610
- console.warn(`Failed to load ${this.loadErrors.length} plugin(s)`);
2654
+ this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
2611
2655
  }
2612
2656
  }
2613
2657
  /**
@@ -2783,8 +2827,9 @@ var builtinVerifiers = {
2783
2827
  };
2784
2828
  var verifierInstances = /* @__PURE__ */ new Map();
2785
2829
  function getVerifier(id) {
2786
- if (verifierInstances.has(id)) {
2787
- return verifierInstances.get(id);
2830
+ const pooled = verifierInstances.get(id);
2831
+ if (pooled) {
2832
+ return pooled;
2788
2833
  }
2789
2834
  const pluginLoader2 = getPluginLoader();
2790
2835
  const customVerifier = pluginLoader2.getVerifier(id);
@@ -2989,6 +3034,7 @@ var VerificationEngine = class {
2989
3034
  astCache;
2990
3035
  resultsCache;
2991
3036
  pluginsLoaded = false;
3037
+ logger = getLogger({ module: "verification.engine" });
2992
3038
  constructor(registry) {
2993
3039
  this.registry = registry || createRegistry();
2994
3040
  this.project = new Project2({
@@ -3162,13 +3208,12 @@ var VerificationEngine = class {
3162
3208
  );
3163
3209
  if (!verifier) {
3164
3210
  const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
3165
- console.warn(
3166
- chalk.yellow(
3167
- `Warning: No verifier found for ${decision.metadata.id}/${constraint.id}
3168
- Requested: ${requestedVerifier}
3169
- Available: ${getVerifierIds().join(", ")}`
3170
- )
3171
- );
3211
+ this.logger.warn({
3212
+ decisionId: decision.metadata.id,
3213
+ constraintId: constraint.id,
3214
+ requestedVerifier,
3215
+ availableVerifiers: getVerifierIds()
3216
+ }, "No verifier found for constraint");
3172
3217
  warnings.push({
3173
3218
  type: "missing_verifier",
3174
3219
  message: `No verifier found for constraint (requested: ${requestedVerifier})`,
@@ -3274,17 +3319,14 @@ var VerificationEngine = class {
3274
3319
  } catch (error) {
3275
3320
  const errorMessage = error instanceof Error ? error.message : String(error);
3276
3321
  const errorStack = error instanceof Error ? error.stack : void 0;
3277
- console.error(
3278
- chalk.red(
3279
- `Error: Verifier '${verifier.id}' failed
3280
- File: ${filePath}
3281
- Decision: ${decision.metadata.id}/${constraint.id}
3282
- Error: ${errorMessage}`
3283
- )
3284
- );
3285
- if (errorStack) {
3286
- console.error(chalk.dim(errorStack));
3287
- }
3322
+ this.logger.error({
3323
+ verifierId: verifier.id,
3324
+ filePath,
3325
+ decisionId: decision.metadata.id,
3326
+ constraintId: constraint.id,
3327
+ error: errorMessage,
3328
+ stack: errorStack
3329
+ }, "Verifier execution failed");
3288
3330
  errors.push({
3289
3331
  type: "verifier_exception",
3290
3332
  message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
@@ -3420,6 +3462,10 @@ var AutofixEngine = class {
3420
3462
  const edits = [];
3421
3463
  for (const violation of fileViolations) {
3422
3464
  const fix = violation.autofix;
3465
+ if (!fix) {
3466
+ skippedViolations++;
3467
+ continue;
3468
+ }
3423
3469
  if (options.interactive) {
3424
3470
  const ok = await confirmFix(`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`);
3425
3471
  if (!ok) {
@@ -3446,7 +3492,8 @@ var AutofixEngine = class {
3446
3492
  };
3447
3493
 
3448
3494
  // src/propagation/graph.ts
3449
- async function buildDependencyGraph2(decisions, files) {
3495
+ async function buildDependencyGraph2(decisions, files, options = {}) {
3496
+ const { cwd } = options;
3450
3497
  const nodes = /* @__PURE__ */ new Map();
3451
3498
  const decisionToFiles = /* @__PURE__ */ new Map();
3452
3499
  const fileToDecisions = /* @__PURE__ */ new Map();
@@ -3461,7 +3508,7 @@ async function buildDependencyGraph2(decisions, files) {
3461
3508
  const constraintId = `constraint:${decision.metadata.id}/${constraint.id}`;
3462
3509
  const matchingFiles = [];
3463
3510
  for (const file of files) {
3464
- if (matchesPattern(file, constraint.scope)) {
3511
+ if (matchesPattern(file, constraint.scope, { cwd })) {
3465
3512
  matchingFiles.push(`file:${file}`);
3466
3513
  const fileDecisions = fileToDecisions.get(file) || /* @__PURE__ */ new Set();
3467
3514
  fileDecisions.add(decision.metadata.id);
@@ -3537,7 +3584,7 @@ var PropagationEngine = class {
3537
3584
  absolute: true
3538
3585
  });
3539
3586
  const decisions = this.registry.getActive();
3540
- this.graph = await buildDependencyGraph2(decisions, files);
3587
+ this.graph = await buildDependencyGraph2(decisions, files, { cwd });
3541
3588
  }
3542
3589
  /**
3543
3590
  * Analyze impact of changing a decision
@@ -3547,7 +3594,24 @@ var PropagationEngine = class {
3547
3594
  if (!this.graph) {
3548
3595
  await this.initialize(config, options);
3549
3596
  }
3550
- const affectedFilePaths = getAffectedFiles(this.graph, decisionId);
3597
+ const graph = this.graph;
3598
+ if (!graph) {
3599
+ return {
3600
+ decision: decisionId,
3601
+ change,
3602
+ affectedFiles: [],
3603
+ estimatedEffort: "low",
3604
+ migrationSteps: [
3605
+ {
3606
+ order: 1,
3607
+ description: "Run verification to confirm all violations resolved",
3608
+ files: [],
3609
+ automated: true
3610
+ }
3611
+ ]
3612
+ };
3613
+ }
3614
+ const affectedFilePaths = getAffectedFiles(graph, decisionId);
3551
3615
  const verificationEngine = createVerificationEngine(this.registry);
3552
3616
  const result = await verificationEngine.verify(config, {
3553
3617
  files: affectedFilePaths,
@@ -3855,7 +3919,10 @@ var Reporter = class {
3855
3919
  result.violations.forEach((v) => {
3856
3920
  const key = v.severity;
3857
3921
  if (!grouped.has(key)) grouped.set(key, []);
3858
- grouped.get(key).push(v);
3922
+ const bucket = grouped.get(key);
3923
+ if (bucket) {
3924
+ bucket.push(v);
3925
+ }
3859
3926
  });
3860
3927
  for (const [severity, violations] of grouped.entries()) {
3861
3928
  lines.push(`Severity: ${severity}`);
@@ -3870,7 +3937,10 @@ var Reporter = class {
3870
3937
  result.violations.forEach((v) => {
3871
3938
  const key = v.location?.file || v.file || "unknown";
3872
3939
  if (!grouped.has(key)) grouped.set(key, []);
3873
- grouped.get(key).push(v);
3940
+ const bucket = grouped.get(key);
3941
+ if (bucket) {
3942
+ bucket.push(v);
3943
+ }
3874
3944
  });
3875
3945
  for (const [file, violations] of grouped.entries()) {
3876
3946
  lines.push(`File: ${file}`);
@@ -3919,54 +3989,54 @@ var Reporter = class {
3919
3989
  };
3920
3990
 
3921
3991
  // src/reporting/formats/console.ts
3922
- import chalk2 from "chalk";
3992
+ import chalk from "chalk";
3923
3993
  import { table } from "table";
3924
3994
  function formatConsoleReport(report) {
3925
3995
  const lines = [];
3926
3996
  lines.push("");
3927
- lines.push(chalk2.bold.blue("SpecBridge Compliance Report"));
3928
- lines.push(chalk2.dim(`Generated: ${new Date(report.timestamp).toLocaleString()}`));
3929
- lines.push(chalk2.dim(`Project: ${report.project}`));
3997
+ lines.push(chalk.bold.blue("SpecBridge Compliance Report"));
3998
+ lines.push(chalk.dim(`Generated: ${new Date(report.timestamp).toLocaleString()}`));
3999
+ lines.push(chalk.dim(`Project: ${report.project}`));
3930
4000
  lines.push("");
3931
4001
  const complianceColor = getComplianceColor(report.summary.compliance);
3932
- lines.push(chalk2.bold("Overall Compliance"));
4002
+ lines.push(chalk.bold("Overall Compliance"));
3933
4003
  lines.push(` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`);
3934
4004
  lines.push("");
3935
- lines.push(chalk2.bold("Summary"));
4005
+ lines.push(chalk.bold("Summary"));
3936
4006
  lines.push(` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`);
3937
4007
  lines.push(` Constraints: ${report.summary.totalConstraints}`);
3938
4008
  lines.push("");
3939
- lines.push(chalk2.bold("Violations"));
4009
+ lines.push(chalk.bold("Violations"));
3940
4010
  const { violations } = report.summary;
3941
4011
  const violationParts = [];
3942
4012
  if (violations.critical > 0) {
3943
- violationParts.push(chalk2.red(`${violations.critical} critical`));
4013
+ violationParts.push(chalk.red(`${violations.critical} critical`));
3944
4014
  }
3945
4015
  if (violations.high > 0) {
3946
- violationParts.push(chalk2.yellow(`${violations.high} high`));
4016
+ violationParts.push(chalk.yellow(`${violations.high} high`));
3947
4017
  }
3948
4018
  if (violations.medium > 0) {
3949
- violationParts.push(chalk2.cyan(`${violations.medium} medium`));
4019
+ violationParts.push(chalk.cyan(`${violations.medium} medium`));
3950
4020
  }
3951
4021
  if (violations.low > 0) {
3952
- violationParts.push(chalk2.dim(`${violations.low} low`));
4022
+ violationParts.push(chalk.dim(`${violations.low} low`));
3953
4023
  }
3954
4024
  if (violationParts.length > 0) {
3955
4025
  lines.push(` ${violationParts.join(" | ")}`);
3956
4026
  } else {
3957
- lines.push(chalk2.green(" No violations"));
4027
+ lines.push(chalk.green(" No violations"));
3958
4028
  }
3959
4029
  lines.push("");
3960
4030
  if (report.byDecision.length > 0) {
3961
- lines.push(chalk2.bold("By Decision"));
4031
+ lines.push(chalk.bold("By Decision"));
3962
4032
  lines.push("");
3963
4033
  const tableData = [
3964
4034
  [
3965
- chalk2.bold("Decision"),
3966
- chalk2.bold("Status"),
3967
- chalk2.bold("Constraints"),
3968
- chalk2.bold("Violations"),
3969
- chalk2.bold("Compliance")
4035
+ chalk.bold("Decision"),
4036
+ chalk.bold("Status"),
4037
+ chalk.bold("Constraints"),
4038
+ chalk.bold("Violations"),
4039
+ chalk.bold("Compliance")
3970
4040
  ]
3971
4041
  ];
3972
4042
  for (const dec of report.byDecision) {
@@ -3976,7 +4046,7 @@ function formatConsoleReport(report) {
3976
4046
  truncate(dec.title, 40),
3977
4047
  statusColor(dec.status),
3978
4048
  String(dec.constraints),
3979
- dec.violations > 0 ? chalk2.red(String(dec.violations)) : chalk2.green("0"),
4049
+ dec.violations > 0 ? chalk.red(String(dec.violations)) : chalk.green("0"),
3980
4050
  compColor(`${dec.compliance}%`)
3981
4051
  ]);
3982
4052
  }
@@ -4010,23 +4080,23 @@ function formatComplianceBar(compliance) {
4010
4080
  return "\u2588".repeat(filled) + "\u2591".repeat(empty);
4011
4081
  }
4012
4082
  function getComplianceColor(compliance) {
4013
- if (compliance >= 90) return chalk2.green;
4014
- if (compliance >= 70) return chalk2.yellow;
4015
- if (compliance >= 50) return chalk2.hex("#FFA500");
4016
- return chalk2.red;
4083
+ if (compliance >= 90) return chalk.green;
4084
+ if (compliance >= 70) return chalk.yellow;
4085
+ if (compliance >= 50) return chalk.hex("#FFA500");
4086
+ return chalk.red;
4017
4087
  }
4018
4088
  function getStatusColor(status) {
4019
4089
  switch (status) {
4020
4090
  case "active":
4021
- return chalk2.green;
4091
+ return chalk.green;
4022
4092
  case "draft":
4023
- return chalk2.yellow;
4093
+ return chalk.yellow;
4024
4094
  case "deprecated":
4025
- return chalk2.gray;
4095
+ return chalk.gray;
4026
4096
  case "superseded":
4027
- return chalk2.blue;
4097
+ return chalk.blue;
4028
4098
  default:
4029
- return chalk2.white;
4099
+ return chalk.white;
4030
4100
  }
4031
4101
  }
4032
4102
  function truncate(str, length) {
@@ -4100,6 +4170,7 @@ function formatProgressBar(percentage) {
4100
4170
  import { join as join4 } from "path";
4101
4171
  var ReportStorage = class {
4102
4172
  storageDir;
4173
+ logger = getLogger({ module: "reporting.storage" });
4103
4174
  constructor(basePath) {
4104
4175
  this.storageDir = join4(getSpecBridgeDir(basePath), "reports", "history");
4105
4176
  }
@@ -4158,7 +4229,7 @@ var ReportStorage = class {
4158
4229
  const timestamp = file.replace("report-", "").replace(".json", "");
4159
4230
  return { timestamp, report };
4160
4231
  } catch (error) {
4161
- console.warn(`Warning: Failed to load report ${file}:`, error);
4232
+ this.logger.warn({ file, error }, "Failed to load report file");
4162
4233
  return null;
4163
4234
  }
4164
4235
  });
@@ -4202,7 +4273,7 @@ var ReportStorage = class {
4202
4273
  const fs = await import("fs/promises");
4203
4274
  await fs.unlink(filepath);
4204
4275
  } catch (error) {
4205
- console.warn(`Warning: Failed to delete old report ${file}:`, error);
4276
+ this.logger.warn({ file, error }, "Failed to delete old report file");
4206
4277
  }
4207
4278
  }
4208
4279
  return filesToDelete.length;
@@ -4326,7 +4397,10 @@ async function analyzeTrend(reports) {
4326
4397
  if (!decisionMap.has(decision.decisionId)) {
4327
4398
  decisionMap.set(decision.decisionId, []);
4328
4399
  }
4329
- decisionMap.get(decision.decisionId).push(decision);
4400
+ const decisionHistory = decisionMap.get(decision.decisionId);
4401
+ if (decisionHistory) {
4402
+ decisionHistory.push(decision);
4403
+ }
4330
4404
  }
4331
4405
  }
4332
4406
  const decisions = Array.from(decisionMap.entries()).map(([decisionId, data]) => {
@@ -5036,6 +5110,7 @@ var DashboardServer = class {
5036
5110
  CACHE_TTL = 6e4;
5037
5111
  // 1 minute
5038
5112
  refreshInterval = null;
5113
+ logger = getLogger({ module: "dashboard.server" });
5039
5114
  constructor(options) {
5040
5115
  this.cwd = options.cwd;
5041
5116
  this.config = options.config;
@@ -5052,7 +5127,11 @@ var DashboardServer = class {
5052
5127
  await this.registry.load();
5053
5128
  await this.refreshCache();
5054
5129
  this.refreshInterval = setInterval(
5055
- () => this.refreshCache().catch(console.error),
5130
+ () => {
5131
+ void this.refreshCache().catch((error) => {
5132
+ this.logger.error({ error }, "Background cache refresh failed");
5133
+ });
5134
+ },
5056
5135
  this.CACHE_TTL
5057
5136
  );
5058
5137
  }
@@ -5075,7 +5154,7 @@ var DashboardServer = class {
5075
5154
  this.cacheTimestamp = Date.now();
5076
5155
  await this.reportStorage.save(report);
5077
5156
  } catch (error) {
5078
- console.error("Cache refresh failed:", error);
5157
+ this.logger.error({ error }, "Cache refresh failed");
5079
5158
  if (!this.cachedReport) {
5080
5159
  try {
5081
5160
  const stored = await this.reportStorage.loadLatest();
@@ -5083,7 +5162,7 @@ var DashboardServer = class {
5083
5162
  this.cachedReport = stored.report;
5084
5163
  }
5085
5164
  } catch (fallbackError) {
5086
- console.error("Failed to load fallback report:", fallbackError);
5165
+ this.logger.error({ error: fallbackError }, "Failed to load fallback report");
5087
5166
  }
5088
5167
  }
5089
5168
  }
@@ -5156,7 +5235,8 @@ var DashboardServer = class {
5156
5235
  });
5157
5236
  this.app.get("/api/report/:date", async (req, res) => {
5158
5237
  try {
5159
- const date = req.params.date;
5238
+ const dateParam = req.params.date;
5239
+ const date = Array.isArray(dateParam) ? dateParam[0] : dateParam;
5160
5240
  if (!date) {
5161
5241
  res.status(400).json({ error: "Date parameter required" });
5162
5242
  return;
@@ -5192,7 +5272,8 @@ var DashboardServer = class {
5192
5272
  });
5193
5273
  this.app.get("/api/decisions/:id", async (req, res) => {
5194
5274
  try {
5195
- const id = req.params.id;
5275
+ const idParam = req.params.id;
5276
+ const id = Array.isArray(idParam) ? idParam[0] : idParam;
5196
5277
  if (!id) {
5197
5278
  res.status(400).json({ error: "Decision ID required" });
5198
5279
  return;
@@ -5204,6 +5285,10 @@ var DashboardServer = class {
5204
5285
  }
5205
5286
  res.json(decision);
5206
5287
  } catch (error) {
5288
+ if (error instanceof DecisionNotFoundError) {
5289
+ res.status(404).json({ error: "Decision not found" });
5290
+ return;
5291
+ }
5207
5292
  res.status(500).json({
5208
5293
  error: "Failed to load decision",
5209
5294
  message: error instanceof Error ? error.message : "Unknown error"
@@ -5235,7 +5320,8 @@ var DashboardServer = class {
5235
5320
  });
5236
5321
  this.app.get("/api/analytics/decision/:id", async (req, res) => {
5237
5322
  try {
5238
- const id = req.params.id;
5323
+ const idParam = req.params.id;
5324
+ const id = Array.isArray(idParam) ? idParam[0] : idParam;
5239
5325
  if (!id) {
5240
5326
  res.status(400).json({ error: "Decision ID required" });
5241
5327
  return;
@@ -5324,7 +5410,7 @@ var DashboardServer = class {
5324
5410
  // Cache static assets
5325
5411
  etag: true
5326
5412
  }));
5327
- this.app.get("*", (_req, res) => {
5413
+ this.app.get("/{*path}", (_req, res) => {
5328
5414
  res.sendFile(join5(publicDir, "index.html"));
5329
5415
  });
5330
5416
  }
@@ -5339,7 +5425,7 @@ import { TextDocument } from "vscode-languageserver-textdocument";
5339
5425
  import { fileURLToPath as fileURLToPath2 } from "url";
5340
5426
  import path3 from "path";
5341
5427
  import { Project as Project3 } from "ts-morph";
5342
- import chalk3 from "chalk";
5428
+ import chalk2 from "chalk";
5343
5429
  function severityToDiagnostic(severity) {
5344
5430
  switch (severity) {
5345
5431
  case "critical":
@@ -5421,7 +5507,11 @@ var SpecBridgeLspServer = class {
5421
5507
  const doc = this.documents.get(params.textDocument.uri);
5422
5508
  if (!doc) return [];
5423
5509
  return violations.filter((v) => v.autofix && v.autofix.edits.length > 0).map((v) => {
5424
- const edits = v.autofix.edits.map((edit) => ({
5510
+ const autofix = v.autofix;
5511
+ if (!autofix) {
5512
+ return null;
5513
+ }
5514
+ const edits = autofix.edits.map((edit) => ({
5425
5515
  range: {
5426
5516
  start: doc.positionAt(edit.start),
5427
5517
  end: doc.positionAt(edit.end)
@@ -5429,7 +5519,7 @@ var SpecBridgeLspServer = class {
5429
5519
  newText: edit.text
5430
5520
  }));
5431
5521
  return {
5432
- title: v.autofix.description,
5522
+ title: autofix.description,
5433
5523
  kind: CodeActionKind.QuickFix,
5434
5524
  edit: {
5435
5525
  changes: {
@@ -5437,7 +5527,7 @@ var SpecBridgeLspServer = class {
5437
5527
  }
5438
5528
  }
5439
5529
  };
5440
- });
5530
+ }).filter((action) => action !== null);
5441
5531
  });
5442
5532
  this.documents.listen(this.connection);
5443
5533
  this.connection.listen();
@@ -5446,7 +5536,7 @@ var SpecBridgeLspServer = class {
5446
5536
  if (!await pathExists(getSpecBridgeDir(this.cwd))) {
5447
5537
  const err = new NotInitializedError();
5448
5538
  this.initError = err.message;
5449
- if (this.options.verbose) this.connection.console.error(chalk3.red(this.initError));
5539
+ if (this.options.verbose) this.connection.console.error(chalk2.red(this.initError));
5450
5540
  return;
5451
5541
  }
5452
5542
  try {
@@ -5455,7 +5545,7 @@ var SpecBridgeLspServer = class {
5455
5545
  await getPluginLoader().loadPlugins(this.cwd);
5456
5546
  } catch (error) {
5457
5547
  const msg = error instanceof Error ? error.message : String(error);
5458
- if (this.options.verbose) this.connection.console.error(chalk3.red(`Plugin load failed: ${msg}`));
5548
+ if (this.options.verbose) this.connection.console.error(chalk2.red(`Plugin load failed: ${msg}`));
5459
5549
  }
5460
5550
  this.registry = createRegistry({ basePath: this.cwd });
5461
5551
  await this.registry.load();
@@ -5468,11 +5558,11 @@ var SpecBridgeLspServer = class {
5468
5558
  }
5469
5559
  }
5470
5560
  if (this.options.verbose) {
5471
- this.connection.console.log(chalk3.dim(`Loaded ${this.decisions.length} active decision(s)`));
5561
+ this.connection.console.log(chalk2.dim(`Loaded ${this.decisions.length} active decision(s)`));
5472
5562
  }
5473
5563
  } catch (error) {
5474
5564
  this.initError = error instanceof Error ? error.message : String(error);
5475
- if (this.options.verbose) this.connection.console.error(chalk3.red(this.initError));
5565
+ if (this.options.verbose) this.connection.console.error(chalk2.red(this.initError));
5476
5566
  }
5477
5567
  }
5478
5568
  async verifyTextDocument(doc) {
@@ -5540,6 +5630,9 @@ async function startLspServer(options) {
5540
5630
  // src/integrations/github.ts
5541
5631
  function toMdTable(rows) {
5542
5632
  const header = rows[0];
5633
+ if (!header) {
5634
+ return "";
5635
+ }
5543
5636
  const body = rows.slice(1);
5544
5637
  const sep = header.map(() => "---");
5545
5638
  const lines = [
@@ -5683,6 +5776,7 @@ export {
5683
5776
  getConfigPath,
5684
5777
  getDecisionsDir,
5685
5778
  getInferredDir,
5779
+ getLogger,
5686
5780
  getReportsDir,
5687
5781
  getSpecBridgeDir,
5688
5782
  getTransitiveDependencies,
@@ -5695,6 +5789,7 @@ export {
5695
5789
  loadConfig,
5696
5790
  loadDecisionFile,
5697
5791
  loadDecisionsFromDir,
5792
+ logger,
5698
5793
  matchesAnyPattern,
5699
5794
  matchesPattern,
5700
5795
  mergeWithDefaults,