@ipation/specbridge 1.1.2 → 1.2.1

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/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import { Command as Command16 } from "commander";
5
- import chalk14 from "chalk";
4
+ import { Command as Command18 } from "commander";
5
+ import chalk16 from "chalk";
6
6
  import { readFileSync as readFileSync2 } from "fs";
7
- import { fileURLToPath as fileURLToPath3 } from "url";
8
- import { dirname as dirname4, join as join10 } from "path";
7
+ import { fileURLToPath as fileURLToPath4 } from "url";
8
+ import { dirname as dirname5, join as join12 } from "path";
9
9
 
10
10
  // src/core/errors/index.ts
11
11
  var SpecBridgeError = class extends Error {
@@ -3711,7 +3711,7 @@ var hookCommand = createHookCommand();
3711
3711
  import { Command as Command10 } from "commander";
3712
3712
  import chalk10 from "chalk";
3713
3713
  import ora6 from "ora";
3714
- import { join as join8 } from "path";
3714
+ import { join as join9 } from "path";
3715
3715
 
3716
3716
  // src/reporting/reporter.ts
3717
3717
  async function generateReport(config, options = {}) {
@@ -3941,8 +3941,293 @@ function formatProgressBar(percentage) {
3941
3941
  return `\`${filledChar.repeat(filled)}${emptyChar.repeat(empty)}\` ${percentage}%`;
3942
3942
  }
3943
3943
 
3944
+ // src/reporting/storage.ts
3945
+ import { join as join8 } from "path";
3946
+ var ReportStorage = class {
3947
+ storageDir;
3948
+ constructor(basePath) {
3949
+ this.storageDir = join8(getSpecBridgeDir(basePath), "reports", "history");
3950
+ }
3951
+ /**
3952
+ * Save a compliance report to storage
3953
+ */
3954
+ async save(report) {
3955
+ await ensureDir(this.storageDir);
3956
+ const date = new Date(report.timestamp).toISOString().split("T")[0];
3957
+ const filename = `report-${date}.json`;
3958
+ const filepath = join8(this.storageDir, filename);
3959
+ await writeTextFile(filepath, JSON.stringify(report, null, 2));
3960
+ return filepath;
3961
+ }
3962
+ /**
3963
+ * Load the most recent report
3964
+ */
3965
+ async loadLatest() {
3966
+ if (!await pathExists(this.storageDir)) {
3967
+ return null;
3968
+ }
3969
+ const files = await readFilesInDir(this.storageDir);
3970
+ if (files.length === 0) {
3971
+ return null;
3972
+ }
3973
+ const sortedFiles = files.filter((f) => f.startsWith("report-") && f.endsWith(".json")).sort().reverse();
3974
+ if (sortedFiles.length === 0) {
3975
+ return null;
3976
+ }
3977
+ const latestFile = sortedFiles[0];
3978
+ if (!latestFile) {
3979
+ return null;
3980
+ }
3981
+ const content = await readTextFile(join8(this.storageDir, latestFile));
3982
+ const report = JSON.parse(content);
3983
+ return {
3984
+ timestamp: latestFile.replace("report-", "").replace(".json", ""),
3985
+ report
3986
+ };
3987
+ }
3988
+ /**
3989
+ * Load historical reports for the specified number of days
3990
+ */
3991
+ async loadHistory(days = 30) {
3992
+ if (!await pathExists(this.storageDir)) {
3993
+ return [];
3994
+ }
3995
+ const files = await readFilesInDir(this.storageDir);
3996
+ const reportFiles = files.filter((f) => f.startsWith("report-") && f.endsWith(".json")).sort().reverse();
3997
+ const recentFiles = reportFiles.slice(0, days);
3998
+ const reports = [];
3999
+ for (const file of recentFiles) {
4000
+ try {
4001
+ const content = await readTextFile(join8(this.storageDir, file));
4002
+ const report = JSON.parse(content);
4003
+ const timestamp = file.replace("report-", "").replace(".json", "");
4004
+ reports.push({ timestamp, report });
4005
+ } catch (error) {
4006
+ console.warn(`Warning: Failed to load report ${file}:`, error);
4007
+ }
4008
+ }
4009
+ return reports;
4010
+ }
4011
+ /**
4012
+ * Load a specific report by date
4013
+ */
4014
+ async loadByDate(date) {
4015
+ const filepath = join8(this.storageDir, `report-${date}.json`);
4016
+ if (!await pathExists(filepath)) {
4017
+ return null;
4018
+ }
4019
+ const content = await readTextFile(filepath);
4020
+ return JSON.parse(content);
4021
+ }
4022
+ /**
4023
+ * Get all available report dates
4024
+ */
4025
+ async getAvailableDates() {
4026
+ if (!await pathExists(this.storageDir)) {
4027
+ return [];
4028
+ }
4029
+ const files = await readFilesInDir(this.storageDir);
4030
+ return files.filter((f) => f.startsWith("report-") && f.endsWith(".json")).map((f) => f.replace("report-", "").replace(".json", "")).sort().reverse();
4031
+ }
4032
+ /**
4033
+ * Clear old reports (keep only the most recent N days)
4034
+ */
4035
+ async cleanup(keepDays = 90) {
4036
+ if (!await pathExists(this.storageDir)) {
4037
+ return 0;
4038
+ }
4039
+ const files = await readFilesInDir(this.storageDir);
4040
+ const reportFiles = files.filter((f) => f.startsWith("report-") && f.endsWith(".json")).sort().reverse();
4041
+ const filesToDelete = reportFiles.slice(keepDays);
4042
+ for (const file of filesToDelete) {
4043
+ try {
4044
+ const filepath = join8(this.storageDir, file);
4045
+ const fs = await import("fs/promises");
4046
+ await fs.unlink(filepath);
4047
+ } catch (error) {
4048
+ console.warn(`Warning: Failed to delete old report ${file}:`, error);
4049
+ }
4050
+ }
4051
+ return filesToDelete.length;
4052
+ }
4053
+ };
4054
+
4055
+ // src/reporting/drift.ts
4056
+ async function detectDrift(current, previous) {
4057
+ const byDecision = [];
4058
+ for (const currDecision of current.byDecision) {
4059
+ const prevDecision = previous.byDecision.find(
4060
+ (d) => d.decisionId === currDecision.decisionId
4061
+ );
4062
+ if (!prevDecision) {
4063
+ byDecision.push({
4064
+ decisionId: currDecision.decisionId,
4065
+ title: currDecision.title,
4066
+ trend: "stable",
4067
+ complianceChange: 0,
4068
+ newViolations: currDecision.violations,
4069
+ fixedViolations: 0,
4070
+ currentCompliance: currDecision.compliance,
4071
+ previousCompliance: currDecision.compliance
4072
+ });
4073
+ continue;
4074
+ }
4075
+ const complianceChange = currDecision.compliance - prevDecision.compliance;
4076
+ const violationDiff = currDecision.violations - prevDecision.violations;
4077
+ let trend;
4078
+ if (complianceChange > 5) {
4079
+ trend = "improving";
4080
+ } else if (complianceChange < -5) {
4081
+ trend = "degrading";
4082
+ } else {
4083
+ trend = "stable";
4084
+ }
4085
+ byDecision.push({
4086
+ decisionId: currDecision.decisionId,
4087
+ title: currDecision.title,
4088
+ trend,
4089
+ complianceChange,
4090
+ newViolations: Math.max(0, violationDiff),
4091
+ fixedViolations: Math.max(0, -violationDiff),
4092
+ currentCompliance: currDecision.compliance,
4093
+ previousCompliance: prevDecision.compliance
4094
+ });
4095
+ }
4096
+ const overallComplianceChange = current.summary.compliance - previous.summary.compliance;
4097
+ let overallTrend;
4098
+ if (overallComplianceChange > 5) {
4099
+ overallTrend = "improving";
4100
+ } else if (overallComplianceChange < -5) {
4101
+ overallTrend = "degrading";
4102
+ } else {
4103
+ overallTrend = "stable";
4104
+ }
4105
+ const newViolations = {
4106
+ critical: Math.max(
4107
+ 0,
4108
+ current.summary.violations.critical - previous.summary.violations.critical
4109
+ ),
4110
+ high: Math.max(0, current.summary.violations.high - previous.summary.violations.high),
4111
+ medium: Math.max(0, current.summary.violations.medium - previous.summary.violations.medium),
4112
+ low: Math.max(0, current.summary.violations.low - previous.summary.violations.low),
4113
+ total: 0
4114
+ };
4115
+ newViolations.total = newViolations.critical + newViolations.high + newViolations.medium + newViolations.low;
4116
+ const fixedViolations = {
4117
+ critical: Math.max(
4118
+ 0,
4119
+ previous.summary.violations.critical - current.summary.violations.critical
4120
+ ),
4121
+ high: Math.max(0, previous.summary.violations.high - current.summary.violations.high),
4122
+ medium: Math.max(0, previous.summary.violations.medium - current.summary.violations.medium),
4123
+ low: Math.max(0, previous.summary.violations.low - current.summary.violations.low),
4124
+ total: 0
4125
+ };
4126
+ fixedViolations.total = fixedViolations.critical + fixedViolations.high + fixedViolations.medium + fixedViolations.low;
4127
+ const improving = byDecision.filter((d) => d.trend === "improving");
4128
+ const degrading = byDecision.filter((d) => d.trend === "degrading");
4129
+ const mostImproved = improving.sort((a, b) => b.complianceChange - a.complianceChange).slice(0, 5);
4130
+ const mostDegraded = degrading.sort((a, b) => a.complianceChange - b.complianceChange).slice(0, 5);
4131
+ return {
4132
+ trend: overallTrend,
4133
+ complianceChange: overallComplianceChange,
4134
+ summary: {
4135
+ newViolations,
4136
+ fixedViolations
4137
+ },
4138
+ byDecision,
4139
+ mostImproved,
4140
+ mostDegraded
4141
+ };
4142
+ }
4143
+ async function analyzeTrend(reports) {
4144
+ if (reports.length === 0) {
4145
+ throw new Error("No reports provided for trend analysis");
4146
+ }
4147
+ const sortedReports = reports.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
4148
+ const firstReport = sortedReports[0]?.report;
4149
+ const lastReport = sortedReports[sortedReports.length - 1]?.report;
4150
+ if (!firstReport || !lastReport) {
4151
+ throw new Error("Invalid reports data");
4152
+ }
4153
+ const overallChange = lastReport.summary.compliance - firstReport.summary.compliance;
4154
+ let overallTrend;
4155
+ if (overallChange > 5) {
4156
+ overallTrend = "improving";
4157
+ } else if (overallChange < -5) {
4158
+ overallTrend = "degrading";
4159
+ } else {
4160
+ overallTrend = "stable";
4161
+ }
4162
+ const overallDataPoints = sortedReports.map((r) => ({
4163
+ date: r.timestamp,
4164
+ compliance: r.report.summary.compliance
4165
+ }));
4166
+ const decisionMap = /* @__PURE__ */ new Map();
4167
+ for (const { report } of sortedReports) {
4168
+ for (const decision of report.byDecision) {
4169
+ if (!decisionMap.has(decision.decisionId)) {
4170
+ decisionMap.set(decision.decisionId, []);
4171
+ }
4172
+ decisionMap.get(decision.decisionId).push(decision);
4173
+ }
4174
+ }
4175
+ const decisions = Array.from(decisionMap.entries()).map(([decisionId, data]) => {
4176
+ const first = data[0];
4177
+ const last = data[data.length - 1];
4178
+ if (!first || !last) {
4179
+ throw new Error(`Invalid decision data for ${decisionId}`);
4180
+ }
4181
+ const change = last.compliance - first.compliance;
4182
+ let trend;
4183
+ if (change > 5) {
4184
+ trend = "improving";
4185
+ } else if (change < -5) {
4186
+ trend = "degrading";
4187
+ } else {
4188
+ trend = "stable";
4189
+ }
4190
+ const dataPoints = sortedReports.map((r) => {
4191
+ const decision = r.report.byDecision.find((d) => d.decisionId === decisionId);
4192
+ return {
4193
+ date: r.timestamp,
4194
+ compliance: decision?.compliance ?? 0
4195
+ };
4196
+ });
4197
+ return {
4198
+ decisionId,
4199
+ title: last.title,
4200
+ startCompliance: first.compliance,
4201
+ endCompliance: last.compliance,
4202
+ change,
4203
+ trend,
4204
+ dataPoints
4205
+ };
4206
+ });
4207
+ const firstTimestamp = sortedReports[0]?.timestamp;
4208
+ const lastTimestamp = sortedReports[sortedReports.length - 1]?.timestamp;
4209
+ if (!firstTimestamp || !lastTimestamp) {
4210
+ throw new Error("Invalid report timestamps");
4211
+ }
4212
+ return {
4213
+ period: {
4214
+ start: firstTimestamp,
4215
+ end: lastTimestamp,
4216
+ days: sortedReports.length
4217
+ },
4218
+ overall: {
4219
+ startCompliance: firstReport.summary.compliance,
4220
+ endCompliance: lastReport.summary.compliance,
4221
+ change: overallChange,
4222
+ trend: overallTrend,
4223
+ dataPoints: overallDataPoints
4224
+ },
4225
+ decisions
4226
+ };
4227
+ }
4228
+
3944
4229
  // src/cli/commands/report.ts
3945
- var reportCommand = new Command10("report").description("Generate compliance report").option("-f, --format <format>", "Output format (console, json, markdown)", "console").option("-o, --output <file>", "Output file path").option("--save", "Save to .specbridge/reports/").option("-a, --all", "Include all decisions (not just active)").action(async (options) => {
4230
+ var reportCommand = new Command10("report").description("Generate compliance report").option("-f, --format <format>", "Output format (console, json, markdown)", "console").option("-o, --output <file>", "Output file path").option("--save", "Save to .specbridge/reports/").option("-a, --all", "Include all decisions (not just active)").option("--trend", "Show compliance trend over time").option("--drift", "Analyze drift since last report").option("--days <n>", "Number of days for trend analysis", "30").action(async (options) => {
3946
4231
  const cwd = process.cwd();
3947
4232
  if (!await pathExists(getSpecBridgeDir(cwd))) {
3948
4233
  throw new NotInitializedError();
@@ -3955,6 +4240,111 @@ var reportCommand = new Command10("report").description("Generate compliance rep
3955
4240
  cwd
3956
4241
  });
3957
4242
  spinner.succeed("Report generated");
4243
+ const storage = new ReportStorage(cwd);
4244
+ await storage.save(report);
4245
+ if (options.trend) {
4246
+ console.log("\n" + chalk10.blue.bold("=== Compliance Trend Analysis ===\n"));
4247
+ const days = parseInt(options.days || "30", 10);
4248
+ const history = await storage.loadHistory(days);
4249
+ if (history.length < 2) {
4250
+ console.log(chalk10.yellow(`Not enough data for trend analysis. Found ${history.length} report(s), need at least 2.`));
4251
+ } else {
4252
+ const trend = await analyzeTrend(history);
4253
+ console.log(chalk10.bold(`Period: ${trend.period.start} to ${trend.period.end} (${trend.period.days} days)`));
4254
+ console.log(`
4255
+ Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.endCompliance}% (${trend.overall.change > 0 ? "+" : ""}${trend.overall.change.toFixed(1)}%)`);
4256
+ const trendEmoji = trend.overall.trend === "improving" ? "\u{1F4C8}" : trend.overall.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
4257
+ const trendColor = trend.overall.trend === "improving" ? chalk10.green : trend.overall.trend === "degrading" ? chalk10.red : chalk10.yellow;
4258
+ console.log(trendColor(`${trendEmoji} Trend: ${trend.overall.trend.toUpperCase()}`));
4259
+ const degrading = trend.decisions.filter((d) => d.trend === "degrading").slice(0, 3);
4260
+ if (degrading.length > 0) {
4261
+ console.log(chalk10.red("\n\u26A0\uFE0F Most Degraded Decisions:"));
4262
+ degrading.forEach((d) => {
4263
+ console.log(` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (${d.change.toFixed(1)}%)`);
4264
+ });
4265
+ }
4266
+ const improving = trend.decisions.filter((d) => d.trend === "improving").slice(0, 3);
4267
+ if (improving.length > 0) {
4268
+ console.log(chalk10.green("\n\u2705 Most Improved Decisions:"));
4269
+ improving.forEach((d) => {
4270
+ console.log(` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (+${d.change.toFixed(1)}%)`);
4271
+ });
4272
+ }
4273
+ }
4274
+ console.log("");
4275
+ }
4276
+ if (options.drift) {
4277
+ console.log("\n" + chalk10.blue.bold("=== Drift Analysis ===\n"));
4278
+ const history = await storage.loadHistory(2);
4279
+ if (history.length < 2) {
4280
+ console.log(chalk10.yellow("Not enough data for drift analysis. Need at least 2 reports."));
4281
+ } else {
4282
+ const currentEntry = history[0];
4283
+ const previousEntry = history[1];
4284
+ if (!currentEntry || !previousEntry) {
4285
+ console.log(chalk10.yellow("Invalid history data."));
4286
+ return;
4287
+ }
4288
+ const drift = await detectDrift(currentEntry.report, previousEntry.report);
4289
+ console.log(chalk10.bold(`Comparing: ${previousEntry.timestamp} vs ${currentEntry.timestamp}`));
4290
+ console.log(`
4291
+ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceChange.toFixed(1)}%`);
4292
+ const driftEmoji = drift.trend === "improving" ? "\u{1F4C8}" : drift.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
4293
+ const driftColor = drift.trend === "improving" ? chalk10.green : drift.trend === "degrading" ? chalk10.red : chalk10.yellow;
4294
+ console.log(driftColor(`${driftEmoji} Overall Trend: ${drift.trend.toUpperCase()}`));
4295
+ if (drift.summary.newViolations.total > 0) {
4296
+ console.log(chalk10.red(`
4297
+ \u26A0\uFE0F New Violations: ${drift.summary.newViolations.total}`));
4298
+ if (drift.summary.newViolations.critical > 0) {
4299
+ console.log(` \u2022 Critical: ${drift.summary.newViolations.critical}`);
4300
+ }
4301
+ if (drift.summary.newViolations.high > 0) {
4302
+ console.log(` \u2022 High: ${drift.summary.newViolations.high}`);
4303
+ }
4304
+ if (drift.summary.newViolations.medium > 0) {
4305
+ console.log(` \u2022 Medium: ${drift.summary.newViolations.medium}`);
4306
+ }
4307
+ if (drift.summary.newViolations.low > 0) {
4308
+ console.log(` \u2022 Low: ${drift.summary.newViolations.low}`);
4309
+ }
4310
+ }
4311
+ if (drift.summary.fixedViolations.total > 0) {
4312
+ console.log(chalk10.green(`
4313
+ \u2705 Fixed Violations: ${drift.summary.fixedViolations.total}`));
4314
+ if (drift.summary.fixedViolations.critical > 0) {
4315
+ console.log(` \u2022 Critical: ${drift.summary.fixedViolations.critical}`);
4316
+ }
4317
+ if (drift.summary.fixedViolations.high > 0) {
4318
+ console.log(` \u2022 High: ${drift.summary.fixedViolations.high}`);
4319
+ }
4320
+ if (drift.summary.fixedViolations.medium > 0) {
4321
+ console.log(` \u2022 Medium: ${drift.summary.fixedViolations.medium}`);
4322
+ }
4323
+ if (drift.summary.fixedViolations.low > 0) {
4324
+ console.log(` \u2022 Low: ${drift.summary.fixedViolations.low}`);
4325
+ }
4326
+ }
4327
+ if (drift.mostDegraded.length > 0) {
4328
+ console.log(chalk10.red("\n\u{1F4C9} Most Degraded:"));
4329
+ drift.mostDegraded.forEach((d) => {
4330
+ console.log(` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (${d.complianceChange.toFixed(1)}%)`);
4331
+ if (d.newViolations > 0) {
4332
+ console.log(` +${d.newViolations} new violation(s)`);
4333
+ }
4334
+ });
4335
+ }
4336
+ if (drift.mostImproved.length > 0) {
4337
+ console.log(chalk10.green("\n\u{1F4C8} Most Improved:"));
4338
+ drift.mostImproved.forEach((d) => {
4339
+ console.log(` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (+${d.complianceChange.toFixed(1)}%)`);
4340
+ if (d.fixedViolations > 0) {
4341
+ console.log(` -${d.fixedViolations} fixed violation(s)`);
4342
+ }
4343
+ });
4344
+ }
4345
+ }
4346
+ console.log("");
4347
+ }
3958
4348
  let output;
3959
4349
  let extension;
3960
4350
  switch (options.format) {
@@ -3973,11 +4363,13 @@ var reportCommand = new Command10("report").description("Generate compliance rep
3973
4363
  extension = "txt";
3974
4364
  break;
3975
4365
  }
3976
- if (options.format !== "json" || !options.output) {
3977
- console.log(output);
4366
+ if (!options.trend && !options.drift) {
4367
+ if (options.format !== "json" || !options.output) {
4368
+ console.log(output);
4369
+ }
3978
4370
  }
3979
4371
  if (options.output || options.save) {
3980
- const outputPath = options.output || join8(
4372
+ const outputPath = options.output || join9(
3981
4373
  getReportsDir(cwd),
3982
4374
  `health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`
3983
4375
  );
@@ -3985,7 +4377,7 @@ var reportCommand = new Command10("report").description("Generate compliance rep
3985
4377
  console.log(chalk10.green(`
3986
4378
  Report saved to: ${outputPath}`));
3987
4379
  if (options.save && !options.output) {
3988
- const latestPath = join8(getReportsDir(cwd), `health-latest.${extension}`);
4380
+ const latestPath = join9(getReportsDir(cwd), `health-latest.${extension}`);
3989
4381
  await writeTextFile(latestPath, output);
3990
4382
  }
3991
4383
  }
@@ -4389,7 +4781,7 @@ var watchCommand = new Command13("watch").description("Watch for changes and ver
4389
4781
  import { Command as Command14 } from "commander";
4390
4782
  import { readFileSync } from "fs";
4391
4783
  import { fileURLToPath as fileURLToPath2 } from "url";
4392
- import { dirname as dirname3, join as join9 } from "path";
4784
+ import { dirname as dirname3, join as join10 } from "path";
4393
4785
 
4394
4786
  // src/mcp/server.ts
4395
4787
  import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -4572,8 +4964,8 @@ var SpecBridgeMcpServer = class {
4572
4964
  // src/cli/commands/mcp-server.ts
4573
4965
  function getCliVersion() {
4574
4966
  try {
4575
- const __dirname2 = dirname3(fileURLToPath2(import.meta.url));
4576
- const packageJsonPath2 = join9(__dirname2, "../package.json");
4967
+ const __dirname3 = dirname3(fileURLToPath2(import.meta.url));
4968
+ const packageJsonPath2 = join10(__dirname3, "../package.json");
4577
4969
  const pkg = JSON.parse(readFileSync(packageJsonPath2, "utf-8"));
4578
4970
  return String(pkg.version || "0.0.0");
4579
4971
  } catch {
@@ -4663,11 +5055,598 @@ var promptCommand = new Command15("prompt").description("Generate AI agent promp
4663
5055
  console.log(prompt);
4664
5056
  });
4665
5057
 
4666
- // src/cli/index.ts
5058
+ // src/cli/commands/analytics.ts
5059
+ import { Command as Command16 } from "commander";
5060
+ import chalk14 from "chalk";
5061
+ import ora7 from "ora";
5062
+
5063
+ // src/analytics/engine.ts
5064
+ var AnalyticsEngine = class {
5065
+ /**
5066
+ * Analyze a specific decision across historical reports
5067
+ */
5068
+ async analyzeDecision(decisionId, history) {
5069
+ if (history.length === 0) {
5070
+ throw new Error("No historical reports provided");
5071
+ }
5072
+ const decisionHistory = [];
5073
+ for (const { timestamp, report } of history) {
5074
+ const decision = report.byDecision.find((d) => d.decisionId === decisionId);
5075
+ if (decision) {
5076
+ decisionHistory.push({ date: timestamp, data: decision });
5077
+ }
5078
+ }
5079
+ if (decisionHistory.length === 0) {
5080
+ throw new Error(`Decision ${decisionId} not found in any report`);
5081
+ }
5082
+ const latestEntry = decisionHistory[decisionHistory.length - 1];
5083
+ if (!latestEntry) {
5084
+ throw new Error(`No data found for decision ${decisionId}`);
5085
+ }
5086
+ const latest = latestEntry.data;
5087
+ const averageCompliance = decisionHistory.reduce((sum, h) => sum + h.data.compliance, 0) / decisionHistory.length;
5088
+ let trendDirection = "stable";
5089
+ if (decisionHistory.length >= 2) {
5090
+ const firstEntry = decisionHistory[0];
5091
+ if (!firstEntry) {
5092
+ throw new Error("Invalid decision history");
5093
+ }
5094
+ const first = firstEntry.data.compliance;
5095
+ const last = latest.compliance;
5096
+ const change = last - first;
5097
+ if (change > 5) {
5098
+ trendDirection = "up";
5099
+ } else if (change < -5) {
5100
+ trendDirection = "down";
5101
+ }
5102
+ }
5103
+ const historyData = decisionHistory.map((h) => ({
5104
+ date: h.date,
5105
+ compliance: h.data.compliance,
5106
+ violations: h.data.violations
5107
+ }));
5108
+ return {
5109
+ decisionId,
5110
+ title: latest.title,
5111
+ totalViolations: latest.violations,
5112
+ violationsByFile: /* @__PURE__ */ new Map(),
5113
+ // Would need violation details to populate
5114
+ violationsBySeverity: {
5115
+ critical: 0,
5116
+ high: 0,
5117
+ medium: 0,
5118
+ low: 0
5119
+ },
5120
+ // Would need violation details to populate
5121
+ mostViolatedConstraint: null,
5122
+ // Would need constraint details to populate
5123
+ averageComplianceScore: averageCompliance,
5124
+ trendDirection,
5125
+ history: historyData
5126
+ };
5127
+ }
5128
+ /**
5129
+ * Generate insights from historical data
5130
+ */
5131
+ async generateInsights(history) {
5132
+ if (history.length === 0) {
5133
+ return [];
5134
+ }
5135
+ const insights = [];
5136
+ const sorted = history.sort((a, b) => a.timestamp.localeCompare(b.timestamp));
5137
+ const latestEntry = sorted[sorted.length - 1];
5138
+ if (!latestEntry) {
5139
+ throw new Error("No reports in history");
5140
+ }
5141
+ const latest = latestEntry.report;
5142
+ if (sorted.length >= 2) {
5143
+ const firstEntry = sorted[0];
5144
+ if (!firstEntry) {
5145
+ return insights;
5146
+ }
5147
+ const first = firstEntry.report;
5148
+ const complianceChange = latest.summary.compliance - first.summary.compliance;
5149
+ if (complianceChange > 10) {
5150
+ insights.push({
5151
+ type: "success",
5152
+ category: "trend",
5153
+ message: `Compliance has improved by ${complianceChange.toFixed(1)}% over the past ${sorted.length} days`,
5154
+ details: `From ${first.summary.compliance}% to ${latest.summary.compliance}%`
5155
+ });
5156
+ } else if (complianceChange < -10) {
5157
+ insights.push({
5158
+ type: "warning",
5159
+ category: "trend",
5160
+ message: `Compliance has dropped by ${Math.abs(complianceChange).toFixed(1)}% over the past ${sorted.length} days`,
5161
+ details: `From ${first.summary.compliance}% to ${latest.summary.compliance}%`
5162
+ });
5163
+ }
5164
+ }
5165
+ if (latest.summary.violations.critical > 0) {
5166
+ insights.push({
5167
+ type: "warning",
5168
+ category: "compliance",
5169
+ message: `${latest.summary.violations.critical} critical violation(s) require immediate attention`,
5170
+ details: "Critical violations block deployments and should be resolved as soon as possible"
5171
+ });
5172
+ }
5173
+ const problematicDecisions = latest.byDecision.filter((d) => d.compliance < 50);
5174
+ if (problematicDecisions.length > 0) {
5175
+ insights.push({
5176
+ type: "warning",
5177
+ category: "hotspot",
5178
+ message: `${problematicDecisions.length} decision(s) have less than 50% compliance`,
5179
+ details: problematicDecisions.map((d) => `${d.title} (${d.compliance}%)`).join(", ")
5180
+ });
5181
+ }
5182
+ const excellentDecisions = latest.byDecision.filter((d) => d.compliance === 100);
5183
+ if (excellentDecisions.length > 0) {
5184
+ insights.push({
5185
+ type: "success",
5186
+ category: "compliance",
5187
+ message: `${excellentDecisions.length} decision(s) have 100% compliance`,
5188
+ details: excellentDecisions.map((d) => d.title).join(", ")
5189
+ });
5190
+ }
5191
+ const avgCompliance = latest.summary.compliance;
5192
+ const decisionsAboveAvg = latest.byDecision.filter((d) => d.compliance > avgCompliance);
5193
+ const decisionsBelowAvg = latest.byDecision.filter((d) => d.compliance < avgCompliance);
5194
+ if (decisionsBelowAvg.length > decisionsAboveAvg.length) {
5195
+ insights.push({
5196
+ type: "info",
5197
+ category: "suggestion",
5198
+ message: `${decisionsBelowAvg.length} decisions are below average compliance`,
5199
+ details: "Consider focusing improvement efforts on these lower-performing decisions"
5200
+ });
5201
+ }
5202
+ const totalViolations = latest.summary.violations.critical + latest.summary.violations.high + latest.summary.violations.medium + latest.summary.violations.low;
5203
+ if (totalViolations > 0) {
5204
+ const criticalPercent = latest.summary.violations.critical / totalViolations * 100;
5205
+ const highPercent = latest.summary.violations.high / totalViolations * 100;
5206
+ if (criticalPercent + highPercent > 60) {
5207
+ insights.push({
5208
+ type: "warning",
5209
+ category: "suggestion",
5210
+ message: "Most violations are high severity",
5211
+ details: `${criticalPercent.toFixed(0)}% critical, ${highPercent.toFixed(0)}% high. Prioritize these for the biggest impact.`
5212
+ });
5213
+ } else {
5214
+ insights.push({
5215
+ type: "info",
5216
+ category: "suggestion",
5217
+ message: "Most violations are lower severity",
5218
+ details: "Consider addressing high-severity issues first for maximum impact"
5219
+ });
5220
+ }
5221
+ }
5222
+ return insights;
5223
+ }
5224
+ /**
5225
+ * Generate analytics summary
5226
+ */
5227
+ async generateSummary(history) {
5228
+ if (history.length === 0) {
5229
+ throw new Error("No historical reports provided");
5230
+ }
5231
+ const latestEntry = history[history.length - 1];
5232
+ if (!latestEntry) {
5233
+ throw new Error("Invalid history data");
5234
+ }
5235
+ const latest = latestEntry.report;
5236
+ let overallTrend = "stable";
5237
+ if (history.length >= 2) {
5238
+ const firstEntry = history[0];
5239
+ if (!firstEntry) {
5240
+ throw new Error("Invalid history data");
5241
+ }
5242
+ const first = firstEntry.report;
5243
+ const change = latest.summary.compliance - first.summary.compliance;
5244
+ if (change > 5) {
5245
+ overallTrend = "up";
5246
+ } else if (change < -5) {
5247
+ overallTrend = "down";
5248
+ }
5249
+ }
5250
+ const sortedByCompliance = [...latest.byDecision].sort(
5251
+ (a, b) => b.compliance - a.compliance
5252
+ );
5253
+ const topDecisions = sortedByCompliance.slice(0, 5).map((d) => ({
5254
+ decisionId: d.decisionId,
5255
+ title: d.title,
5256
+ compliance: d.compliance
5257
+ }));
5258
+ const bottomDecisions = sortedByCompliance.slice(-5).reverse().map((d) => ({
5259
+ decisionId: d.decisionId,
5260
+ title: d.title,
5261
+ compliance: d.compliance
5262
+ }));
5263
+ const insights = await this.generateInsights(history);
5264
+ return {
5265
+ totalDecisions: latest.byDecision.length,
5266
+ averageCompliance: latest.summary.compliance,
5267
+ overallTrend,
5268
+ criticalIssues: latest.summary.violations.critical,
5269
+ topDecisions,
5270
+ bottomDecisions,
5271
+ insights
5272
+ };
5273
+ }
5274
+ };
5275
+
5276
+ // src/cli/commands/analytics.ts
5277
+ var analyticsCommand = new Command16("analytics").description("Analyze compliance trends and decision impact").argument("[decision-id]", "Specific decision to analyze").option("--insights", "Show AI-generated insights").option("--days <n>", "Number of days of history to analyze", "90").option("-f, --format <format>", "Output format (console, json)", "console").action(async (decisionId, options) => {
5278
+ const cwd = process.cwd();
5279
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
5280
+ throw new NotInitializedError();
5281
+ }
5282
+ const spinner = ora7("Analyzing compliance data...").start();
5283
+ try {
5284
+ const storage = new ReportStorage(cwd);
5285
+ const days = parseInt(options.days || "90", 10);
5286
+ const history = await storage.loadHistory(days);
5287
+ if (history.length === 0) {
5288
+ spinner.fail("No historical reports found");
5289
+ console.log(chalk14.yellow("\nGenerate reports with: specbridge report"));
5290
+ return;
5291
+ }
5292
+ spinner.succeed(`Loaded ${history.length} historical report(s)`);
5293
+ const engine = new AnalyticsEngine();
5294
+ if (options.format === "json") {
5295
+ if (decisionId) {
5296
+ const metrics = await engine.analyzeDecision(decisionId, history);
5297
+ console.log(JSON.stringify(metrics, null, 2));
5298
+ } else {
5299
+ const summary = await engine.generateSummary(history);
5300
+ console.log(JSON.stringify(summary, null, 2));
5301
+ }
5302
+ return;
5303
+ }
5304
+ if (decisionId) {
5305
+ const metrics = await engine.analyzeDecision(decisionId, history);
5306
+ console.log("\n" + chalk14.blue.bold(`=== Decision Analytics: ${metrics.title} ===
5307
+ `));
5308
+ console.log(chalk14.bold("Overview:"));
5309
+ console.log(` ID: ${metrics.decisionId}`);
5310
+ console.log(` Current Violations: ${metrics.totalViolations}`);
5311
+ console.log(` Average Compliance: ${metrics.averageComplianceScore.toFixed(1)}%`);
5312
+ const trendEmoji = metrics.trendDirection === "up" ? "\u{1F4C8}" : metrics.trendDirection === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
5313
+ const trendColor = metrics.trendDirection === "up" ? chalk14.green : metrics.trendDirection === "down" ? chalk14.red : chalk14.yellow;
5314
+ console.log(` ${trendColor(`${trendEmoji} Trend: ${metrics.trendDirection.toUpperCase()}`)}`);
5315
+ if (metrics.history.length > 0) {
5316
+ console.log(chalk14.bold("\nCompliance History:"));
5317
+ const recentHistory = metrics.history.slice(-10);
5318
+ recentHistory.forEach((h) => {
5319
+ const icon = h.violations === 0 ? "\u2705" : "\u26A0\uFE0F";
5320
+ console.log(` ${icon} ${h.date}: ${h.compliance}% (${h.violations} violations)`);
5321
+ });
5322
+ }
5323
+ } else {
5324
+ const summary = await engine.generateSummary(history);
5325
+ console.log("\n" + chalk14.blue.bold("=== Overall Analytics ===\n"));
5326
+ console.log(chalk14.bold("Summary:"));
5327
+ console.log(` Total Decisions: ${summary.totalDecisions}`);
5328
+ console.log(` Average Compliance: ${summary.averageCompliance}%`);
5329
+ console.log(` Critical Issues: ${summary.criticalIssues}`);
5330
+ const trendEmoji = summary.overallTrend === "up" ? "\u{1F4C8}" : summary.overallTrend === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
5331
+ const trendColor = summary.overallTrend === "up" ? chalk14.green : summary.overallTrend === "down" ? chalk14.red : chalk14.yellow;
5332
+ console.log(` ${trendColor(`${trendEmoji} Overall Trend: ${summary.overallTrend.toUpperCase()}`)}`);
5333
+ if (summary.topDecisions.length > 0) {
5334
+ console.log(chalk14.green("\n\u2705 Top Performing Decisions:"));
5335
+ summary.topDecisions.forEach((d, i) => {
5336
+ console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
5337
+ });
5338
+ }
5339
+ if (summary.bottomDecisions.length > 0) {
5340
+ console.log(chalk14.red("\n\u26A0\uFE0F Decisions Needing Attention:"));
5341
+ summary.bottomDecisions.forEach((d, i) => {
5342
+ console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
5343
+ });
5344
+ }
5345
+ if (options.insights || summary.criticalIssues > 0) {
5346
+ console.log(chalk14.blue.bold("\n=== Insights ===\n"));
5347
+ const insights = summary.insights;
5348
+ const warnings = insights.filter((i) => i.type === "warning");
5349
+ const successes = insights.filter((i) => i.type === "success");
5350
+ const infos = insights.filter((i) => i.type === "info");
5351
+ if (warnings.length > 0) {
5352
+ console.log(chalk14.red("\u26A0\uFE0F Warnings:"));
5353
+ warnings.forEach((i) => {
5354
+ console.log(` \u2022 ${i.message}`);
5355
+ if (i.details) {
5356
+ console.log(chalk14.gray(` ${i.details}`));
5357
+ }
5358
+ });
5359
+ console.log("");
5360
+ }
5361
+ if (successes.length > 0) {
5362
+ console.log(chalk14.green("\u2705 Positive Trends:"));
5363
+ successes.forEach((i) => {
5364
+ console.log(` \u2022 ${i.message}`);
5365
+ if (i.details) {
5366
+ console.log(chalk14.gray(` ${i.details}`));
5367
+ }
5368
+ });
5369
+ console.log("");
5370
+ }
5371
+ if (infos.length > 0) {
5372
+ console.log(chalk14.blue("\u{1F4A1} Suggestions:"));
5373
+ infos.forEach((i) => {
5374
+ console.log(` \u2022 ${i.message}`);
5375
+ if (i.details) {
5376
+ console.log(chalk14.gray(` ${i.details}`));
5377
+ }
5378
+ });
5379
+ console.log("");
5380
+ }
5381
+ }
5382
+ }
5383
+ const latestEntry = history[history.length - 1];
5384
+ const oldestEntry = history[0];
5385
+ if (latestEntry && oldestEntry) {
5386
+ console.log(chalk14.gray(`
5387
+ Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`));
5388
+ }
5389
+ console.log(chalk14.gray(`Analyzing ${history.length} report(s) over ${days} days
5390
+ `));
5391
+ } catch (error) {
5392
+ spinner.fail("Analytics failed");
5393
+ throw error;
5394
+ }
5395
+ });
5396
+
5397
+ // src/cli/commands/dashboard.ts
5398
+ import { Command as Command17 } from "commander";
5399
+ import chalk15 from "chalk";
5400
+
5401
+ // src/dashboard/server.ts
5402
+ import express from "express";
5403
+ import { join as join11, dirname as dirname4 } from "path";
5404
+ import { fileURLToPath as fileURLToPath3 } from "url";
4667
5405
  var __dirname = dirname4(fileURLToPath3(import.meta.url));
4668
- var packageJsonPath = join10(__dirname, "../package.json");
5406
+ function createDashboardServer(options) {
5407
+ const { cwd, config } = options;
5408
+ const app = express();
5409
+ app.use(express.json());
5410
+ app.use((_req, res, next) => {
5411
+ res.header("Access-Control-Allow-Origin", "*");
5412
+ res.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
5413
+ res.header("Access-Control-Allow-Headers", "Content-Type");
5414
+ next();
5415
+ });
5416
+ app.get("/api/report/latest", async (_req, res) => {
5417
+ try {
5418
+ const report = await generateReport(config, { cwd });
5419
+ res.json(report);
5420
+ } catch (error) {
5421
+ res.status(500).json({
5422
+ error: "Failed to generate report",
5423
+ message: error instanceof Error ? error.message : "Unknown error"
5424
+ });
5425
+ }
5426
+ });
5427
+ app.get("/api/report/history", async (req, res) => {
5428
+ try {
5429
+ const days = parseInt(req.query.days) || 30;
5430
+ const storage = new ReportStorage(cwd);
5431
+ const history = await storage.loadHistory(days);
5432
+ res.json(history);
5433
+ } catch (error) {
5434
+ res.status(500).json({
5435
+ error: "Failed to load history",
5436
+ message: error instanceof Error ? error.message : "Unknown error"
5437
+ });
5438
+ }
5439
+ });
5440
+ app.get("/api/report/dates", async (_req, res) => {
5441
+ try {
5442
+ const storage = new ReportStorage(cwd);
5443
+ const dates = await storage.getAvailableDates();
5444
+ res.json(dates);
5445
+ } catch (error) {
5446
+ res.status(500).json({
5447
+ error: "Failed to load dates",
5448
+ message: error instanceof Error ? error.message : "Unknown error"
5449
+ });
5450
+ }
5451
+ });
5452
+ app.get("/api/report/:date", async (req, res) => {
5453
+ try {
5454
+ const date = req.params.date;
5455
+ if (!date) {
5456
+ res.status(400).json({ error: "Date parameter required" });
5457
+ return;
5458
+ }
5459
+ const storage = new ReportStorage(cwd);
5460
+ const report = await storage.loadByDate(date);
5461
+ if (!report) {
5462
+ res.status(404).json({ error: "Report not found" });
5463
+ return;
5464
+ }
5465
+ res.json(report);
5466
+ } catch (error) {
5467
+ res.status(500).json({
5468
+ error: "Failed to load report",
5469
+ message: error instanceof Error ? error.message : "Unknown error"
5470
+ });
5471
+ }
5472
+ });
5473
+ app.get("/api/decisions", async (_req, res) => {
5474
+ try {
5475
+ const registry = createRegistry({ basePath: cwd });
5476
+ await registry.load();
5477
+ const decisions = registry.getAll();
5478
+ res.json(decisions);
5479
+ } catch (error) {
5480
+ res.status(500).json({
5481
+ error: "Failed to load decisions",
5482
+ message: error instanceof Error ? error.message : "Unknown error"
5483
+ });
5484
+ }
5485
+ });
5486
+ app.get("/api/decisions/:id", async (req, res) => {
5487
+ try {
5488
+ const id = req.params.id;
5489
+ if (!id) {
5490
+ res.status(400).json({ error: "Decision ID required" });
5491
+ return;
5492
+ }
5493
+ const registry = createRegistry({ basePath: cwd });
5494
+ await registry.load();
5495
+ const decision = registry.get(id);
5496
+ if (!decision) {
5497
+ res.status(404).json({ error: "Decision not found" });
5498
+ return;
5499
+ }
5500
+ res.json(decision);
5501
+ } catch (error) {
5502
+ res.status(500).json({
5503
+ error: "Failed to load decision",
5504
+ message: error instanceof Error ? error.message : "Unknown error"
5505
+ });
5506
+ }
5507
+ });
5508
+ app.get("/api/analytics/summary", async (req, res) => {
5509
+ try {
5510
+ const days = parseInt(req.query.days) || 90;
5511
+ const storage = new ReportStorage(cwd);
5512
+ const history = await storage.loadHistory(days);
5513
+ if (history.length === 0) {
5514
+ res.status(404).json({ error: "No historical data available" });
5515
+ return;
5516
+ }
5517
+ const engine = new AnalyticsEngine();
5518
+ const summary = await engine.generateSummary(history);
5519
+ res.json(summary);
5520
+ } catch (error) {
5521
+ res.status(500).json({
5522
+ error: "Failed to generate analytics",
5523
+ message: error instanceof Error ? error.message : "Unknown error"
5524
+ });
5525
+ }
5526
+ });
5527
+ app.get("/api/analytics/decision/:id", async (req, res) => {
5528
+ try {
5529
+ const id = req.params.id;
5530
+ if (!id) {
5531
+ res.status(400).json({ error: "Decision ID required" });
5532
+ return;
5533
+ }
5534
+ const days = parseInt(req.query.days) || 90;
5535
+ const storage = new ReportStorage(cwd);
5536
+ const history = await storage.loadHistory(days);
5537
+ if (history.length === 0) {
5538
+ res.status(404).json({ error: "No historical data available" });
5539
+ return;
5540
+ }
5541
+ const engine = new AnalyticsEngine();
5542
+ const metrics = await engine.analyzeDecision(id, history);
5543
+ res.json(metrics);
5544
+ } catch (error) {
5545
+ res.status(500).json({
5546
+ error: "Failed to analyze decision",
5547
+ message: error instanceof Error ? error.message : "Unknown error"
5548
+ });
5549
+ }
5550
+ });
5551
+ app.get("/api/drift", async (req, res) => {
5552
+ try {
5553
+ const days = parseInt(req.query.days) || 2;
5554
+ const storage = new ReportStorage(cwd);
5555
+ const history = await storage.loadHistory(days);
5556
+ if (history.length < 2) {
5557
+ res.status(404).json({ error: "Need at least 2 reports for drift analysis" });
5558
+ return;
5559
+ }
5560
+ const currentEntry = history[0];
5561
+ const previousEntry = history[1];
5562
+ if (!currentEntry || !previousEntry) {
5563
+ res.status(400).json({ error: "Invalid history data" });
5564
+ return;
5565
+ }
5566
+ const drift = await detectDrift(currentEntry.report, previousEntry.report);
5567
+ res.json(drift);
5568
+ } catch (error) {
5569
+ res.status(500).json({
5570
+ error: "Failed to analyze drift",
5571
+ message: error instanceof Error ? error.message : "Unknown error"
5572
+ });
5573
+ }
5574
+ });
5575
+ app.get("/api/trend", async (req, res) => {
5576
+ try {
5577
+ const days = parseInt(req.query.days) || 30;
5578
+ const storage = new ReportStorage(cwd);
5579
+ const history = await storage.loadHistory(days);
5580
+ if (history.length < 2) {
5581
+ res.status(404).json({ error: "Need at least 2 reports for trend analysis" });
5582
+ return;
5583
+ }
5584
+ const trend = await analyzeTrend(history);
5585
+ res.json(trend);
5586
+ } catch (error) {
5587
+ res.status(500).json({
5588
+ error: "Failed to analyze trend",
5589
+ message: error instanceof Error ? error.message : "Unknown error"
5590
+ });
5591
+ }
5592
+ });
5593
+ app.get("/api/health", (_req, res) => {
5594
+ res.json({
5595
+ status: "ok",
5596
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5597
+ project: config.project.name
5598
+ });
5599
+ });
5600
+ const publicDir = join11(__dirname, "public");
5601
+ app.use(express.static(publicDir));
5602
+ app.get("*", (_req, res) => {
5603
+ res.sendFile(join11(publicDir, "index.html"));
5604
+ });
5605
+ return app;
5606
+ }
5607
+
5608
+ // src/cli/commands/dashboard.ts
5609
+ var dashboardCommand = new Command17("dashboard").description("Start compliance dashboard web server").option("-p, --port <port>", "Port to listen on", "3000").option("-h, --host <host>", "Host to bind to", "localhost").action(async (options) => {
5610
+ const cwd = process.cwd();
5611
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
5612
+ throw new NotInitializedError();
5613
+ }
5614
+ console.log(chalk15.blue("Starting SpecBridge dashboard..."));
5615
+ try {
5616
+ const config = await loadConfig(cwd);
5617
+ const app = createDashboardServer({ cwd, config });
5618
+ const port = parseInt(options.port || "3000", 10);
5619
+ const host = options.host || "localhost";
5620
+ app.listen(port, host, () => {
5621
+ console.log(chalk15.green(`
5622
+ \u2713 Dashboard running at http://${host}:${port}`));
5623
+ console.log(chalk15.gray(" Press Ctrl+C to stop\n"));
5624
+ console.log(chalk15.bold("API Endpoints:"));
5625
+ console.log(` ${chalk15.cyan(`http://${host}:${port}/api/health`)} - Health check`);
5626
+ console.log(` ${chalk15.cyan(`http://${host}:${port}/api/report/latest`)} - Latest report`);
5627
+ console.log(` ${chalk15.cyan(`http://${host}:${port}/api/decisions`)} - All decisions`);
5628
+ console.log(` ${chalk15.cyan(`http://${host}:${port}/api/analytics/summary`)} - Analytics`);
5629
+ console.log("");
5630
+ });
5631
+ process.on("SIGINT", () => {
5632
+ console.log(chalk15.yellow("\n\nShutting down dashboard..."));
5633
+ process.exit(0);
5634
+ });
5635
+ process.on("SIGTERM", () => {
5636
+ console.log(chalk15.yellow("\n\nShutting down dashboard..."));
5637
+ process.exit(0);
5638
+ });
5639
+ } catch (error) {
5640
+ console.error(chalk15.red("Failed to start dashboard:"), error);
5641
+ throw error;
5642
+ }
5643
+ });
5644
+
5645
+ // src/cli/index.ts
5646
+ var __dirname2 = dirname5(fileURLToPath4(import.meta.url));
5647
+ var packageJsonPath = join12(__dirname2, "../package.json");
4669
5648
  var packageJson = JSON.parse(readFileSync2(packageJsonPath, "utf-8"));
4670
- var program = new Command16();
5649
+ var program = new Command18();
4671
5650
  program.name("specbridge").description("Architecture Decision Runtime - Transform architectural decisions into executable, verifiable constraints").version(packageJson.version);
4672
5651
  program.addCommand(initCommand);
4673
5652
  program.addCommand(inferCommand);
@@ -4680,6 +5659,8 @@ program.addCommand(lspCommand);
4680
5659
  program.addCommand(watchCommand);
4681
5660
  program.addCommand(mcpServerCommand);
4682
5661
  program.addCommand(promptCommand);
5662
+ program.addCommand(analyticsCommand);
5663
+ program.addCommand(dashboardCommand);
4683
5664
  program.exitOverride((err) => {
4684
5665
  if (err.code === "commander.help" || err.code === "commander.helpDisplayed") {
4685
5666
  process.exit(0);
@@ -4687,11 +5668,11 @@ program.exitOverride((err) => {
4687
5668
  if (err.code === "commander.version") {
4688
5669
  process.exit(0);
4689
5670
  }
4690
- console.error(chalk14.red(formatError(err)));
5671
+ console.error(chalk16.red(formatError(err)));
4691
5672
  process.exit(1);
4692
5673
  });
4693
5674
  program.parseAsync(process.argv).catch((error) => {
4694
- console.error(chalk14.red(formatError(error)));
5675
+ console.error(chalk16.red(formatError(error)));
4695
5676
  process.exit(1);
4696
5677
  });
4697
5678
  //# sourceMappingURL=cli.js.map