@ipation/specbridge 1.1.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +125 -0
- package/dist/cli.js +999 -18
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +116 -1
- package/dist/index.js +288 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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
|
|
5
|
-
import
|
|
4
|
+
import { Command as Command18 } from "commander";
|
|
5
|
+
import chalk16 from "chalk";
|
|
6
6
|
import { readFileSync as readFileSync2 } from "fs";
|
|
7
|
-
import { fileURLToPath as
|
|
8
|
-
import { dirname as
|
|
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
|
|
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.
|
|
3977
|
-
|
|
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 ||
|
|
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 =
|
|
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
|
|
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
|
|
4576
|
-
const packageJsonPath2 =
|
|
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/
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
5675
|
+
console.error(chalk16.red(formatError(error)));
|
|
4695
5676
|
process.exit(1);
|
|
4696
5677
|
});
|
|
4697
5678
|
//# sourceMappingURL=cli.js.map
|