@ipation/specbridge 2.2.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +2 -0
- package/dist/cli.js +409 -325
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +147 -542
- package/dist/index.js +203 -108
- package/dist/index.js.map +1 -1
- package/package.json +14 -13
package/dist/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli/index.ts
|
|
4
4
|
import { Command as Command20 } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import chalk19 from "chalk";
|
|
6
6
|
import { readFileSync as readFileSync2 } from "fs";
|
|
7
7
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8
8
|
import { dirname as dirname5, join as join14 } from "path";
|
|
@@ -1261,7 +1261,7 @@ async function loadConfig(basePath = process.cwd()) {
|
|
|
1261
1261
|
const parsed = parseYaml(content);
|
|
1262
1262
|
const result = validateConfig(parsed);
|
|
1263
1263
|
if (!result.success) {
|
|
1264
|
-
const errors = result.errors.
|
|
1264
|
+
const errors = result.errors.issues.map((e) => `${e.path.join(".")}: ${e.message}`);
|
|
1265
1265
|
throw new ConfigError(`Invalid configuration in ${configPath}`, { errors });
|
|
1266
1266
|
}
|
|
1267
1267
|
return result.data;
|
|
@@ -1345,12 +1345,11 @@ Detected ${patterns.length} pattern(s):
|
|
|
1345
1345
|
|
|
1346
1346
|
// src/cli/commands/verify.ts
|
|
1347
1347
|
import { Command as Command3 } from "commander";
|
|
1348
|
-
import
|
|
1348
|
+
import chalk4 from "chalk";
|
|
1349
1349
|
import ora3 from "ora";
|
|
1350
1350
|
|
|
1351
1351
|
// src/verification/engine.ts
|
|
1352
1352
|
import { Project as Project2 } from "ts-morph";
|
|
1353
|
-
import chalk3 from "chalk";
|
|
1354
1353
|
|
|
1355
1354
|
// src/registry/loader.ts
|
|
1356
1355
|
import { join as join4 } from "path";
|
|
@@ -1385,7 +1384,7 @@ var ConstraintExceptionSchema = z2.object({
|
|
|
1385
1384
|
});
|
|
1386
1385
|
var ConstraintCheckSchema = z2.object({
|
|
1387
1386
|
verifier: z2.string().min(1),
|
|
1388
|
-
params: z2.record(z2.unknown()).optional()
|
|
1387
|
+
params: z2.record(z2.string(), z2.unknown()).optional()
|
|
1389
1388
|
});
|
|
1390
1389
|
var ConstraintSchema = z2.object({
|
|
1391
1390
|
id: z2.string().min(1).regex(/^[a-z0-9-]+$/, "Constraint ID must be lowercase alphanumeric with hyphens"),
|
|
@@ -1427,7 +1426,7 @@ function validateDecision(data) {
|
|
|
1427
1426
|
return { success: false, errors: result.error };
|
|
1428
1427
|
}
|
|
1429
1428
|
function formatValidationErrors(errors) {
|
|
1430
|
-
return errors.
|
|
1429
|
+
return errors.issues.map((err) => {
|
|
1431
1430
|
const path5 = err.path.join(".");
|
|
1432
1431
|
return `${path5}: ${err.message}`;
|
|
1433
1432
|
});
|
|
@@ -2149,7 +2148,10 @@ function buildDependencyGraph(project) {
|
|
|
2149
2148
|
const moduleSpec = importDecl.getModuleSpecifierValue();
|
|
2150
2149
|
const resolved = resolveToSourceFilePath(project, from, moduleSpec);
|
|
2151
2150
|
if (resolved) {
|
|
2152
|
-
graph.get(from)
|
|
2151
|
+
const dependencies = graph.get(from);
|
|
2152
|
+
if (dependencies) {
|
|
2153
|
+
dependencies.add(normalizeFsPath(resolved));
|
|
2154
|
+
}
|
|
2153
2155
|
}
|
|
2154
2156
|
}
|
|
2155
2157
|
}
|
|
@@ -2173,9 +2175,17 @@ function tarjanScc(graph) {
|
|
|
2173
2175
|
for (const w of edges) {
|
|
2174
2176
|
if (!indices.has(w)) {
|
|
2175
2177
|
strongConnect(w);
|
|
2176
|
-
|
|
2178
|
+
const currentLowlink = lowlink.get(v);
|
|
2179
|
+
const childLowlink = lowlink.get(w);
|
|
2180
|
+
if (currentLowlink !== void 0 && childLowlink !== void 0) {
|
|
2181
|
+
lowlink.set(v, Math.min(currentLowlink, childLowlink));
|
|
2182
|
+
}
|
|
2177
2183
|
} else if (onStack.has(w)) {
|
|
2178
|
-
|
|
2184
|
+
const currentLowlink = lowlink.get(v);
|
|
2185
|
+
const childIndex = indices.get(w);
|
|
2186
|
+
if (currentLowlink !== void 0 && childIndex !== void 0) {
|
|
2187
|
+
lowlink.set(v, Math.min(currentLowlink, childIndex));
|
|
2188
|
+
}
|
|
2179
2189
|
}
|
|
2180
2190
|
}
|
|
2181
2191
|
if (lowlink.get(v) === indices.get(v)) {
|
|
@@ -2197,18 +2207,21 @@ function tarjanScc(graph) {
|
|
|
2197
2207
|
}
|
|
2198
2208
|
function parseMaxImportDepth(rule) {
|
|
2199
2209
|
const m = rule.match(/maximum\s{1,5}import\s{1,5}depth\s{0,5}[:=]?\s{0,5}(\d+)/i);
|
|
2200
|
-
|
|
2210
|
+
const depthText = m?.[1];
|
|
2211
|
+
return depthText ? Number.parseInt(depthText, 10) : null;
|
|
2201
2212
|
}
|
|
2202
2213
|
function parseBannedDependency(rule) {
|
|
2203
2214
|
const m = rule.match(/no\s{1,5}dependencies?\s{1,5}on\s{1,5}(?:package\s{1,5})?(.+?)(?:\.|$)/i);
|
|
2204
|
-
|
|
2205
|
-
|
|
2215
|
+
const value = m?.[1]?.trim();
|
|
2216
|
+
if (!value) return null;
|
|
2206
2217
|
return value.length > 0 ? value : null;
|
|
2207
2218
|
}
|
|
2208
2219
|
function parseLayerRule(rule) {
|
|
2209
2220
|
const m = rule.match(/(\w+)\s{1,5}layer\s{1,5}cannot\s{1,5}depend\s{1,5}on\s{1,5}(\w+)\s{1,5}layer/i);
|
|
2210
|
-
|
|
2211
|
-
|
|
2221
|
+
const fromLayer = m?.[1]?.toLowerCase();
|
|
2222
|
+
const toLayer = m?.[2]?.toLowerCase();
|
|
2223
|
+
if (!fromLayer || !toLayer) return null;
|
|
2224
|
+
return { fromLayer, toLayer };
|
|
2212
2225
|
}
|
|
2213
2226
|
function fileInLayer(filePath, layer) {
|
|
2214
2227
|
const fp = normalizeFsPath(filePath).toLowerCase();
|
|
@@ -2230,7 +2243,8 @@ var DependencyVerifier = class {
|
|
|
2230
2243
|
const sccs = tarjanScc(graph);
|
|
2231
2244
|
const current = projectFilePath;
|
|
2232
2245
|
for (const scc of sccs) {
|
|
2233
|
-
const
|
|
2246
|
+
const first = scc[0];
|
|
2247
|
+
const hasSelfLoop = first !== void 0 && scc.length === 1 && (graph.get(first)?.has(first) ?? false);
|
|
2234
2248
|
const isCycle = scc.length > 1 || hasSelfLoop;
|
|
2235
2249
|
if (!isCycle) continue;
|
|
2236
2250
|
if (!scc.includes(current)) continue;
|
|
@@ -2312,10 +2326,12 @@ var DependencyVerifier = class {
|
|
|
2312
2326
|
};
|
|
2313
2327
|
|
|
2314
2328
|
// src/verification/verifiers/complexity.ts
|
|
2329
|
+
import { Node as Node4 } from "ts-morph";
|
|
2315
2330
|
import { SyntaxKind as SyntaxKind2 } from "ts-morph";
|
|
2316
2331
|
function parseLimit(rule, pattern) {
|
|
2317
2332
|
const m = rule.match(pattern);
|
|
2318
|
-
|
|
2333
|
+
const value = m?.[1];
|
|
2334
|
+
return value ? Number.parseInt(value, 10) : null;
|
|
2319
2335
|
}
|
|
2320
2336
|
function getFileLineCount(text) {
|
|
2321
2337
|
if (text.length === 0) return 0;
|
|
@@ -2351,14 +2367,15 @@ function calculateCyclomaticComplexity(fn) {
|
|
|
2351
2367
|
return 1 + getDecisionPoints(fn);
|
|
2352
2368
|
}
|
|
2353
2369
|
function getFunctionDisplayName(fn) {
|
|
2354
|
-
if (
|
|
2370
|
+
if (Node4.isFunctionDeclaration(fn) || Node4.isMethodDeclaration(fn) || Node4.isFunctionExpression(fn)) {
|
|
2355
2371
|
const name = fn.getName();
|
|
2356
|
-
if (typeof name === "string" && name.length > 0)
|
|
2372
|
+
if (typeof name === "string" && name.length > 0) {
|
|
2373
|
+
return name;
|
|
2374
|
+
}
|
|
2357
2375
|
}
|
|
2358
2376
|
const parent = fn.getParent();
|
|
2359
|
-
if (parent
|
|
2360
|
-
|
|
2361
|
-
if (typeof vd.getName === "function") return vd.getName();
|
|
2377
|
+
if (parent && Node4.isVariableDeclaration(parent)) {
|
|
2378
|
+
return parent.getName();
|
|
2362
2379
|
}
|
|
2363
2380
|
return "<anonymous>";
|
|
2364
2381
|
}
|
|
@@ -2428,9 +2445,8 @@ var ComplexityVerifier = class {
|
|
|
2428
2445
|
}));
|
|
2429
2446
|
}
|
|
2430
2447
|
}
|
|
2431
|
-
if (maxParams !== null
|
|
2432
|
-
const
|
|
2433
|
-
const paramCount = Array.isArray(params) ? params.length : 0;
|
|
2448
|
+
if (maxParams !== null) {
|
|
2449
|
+
const paramCount = fn.getParameters().length;
|
|
2434
2450
|
if (paramCount > maxParams) {
|
|
2435
2451
|
violations.push(createViolation({
|
|
2436
2452
|
decisionId,
|
|
@@ -2504,8 +2520,7 @@ var SecurityVerifier = class {
|
|
|
2504
2520
|
}));
|
|
2505
2521
|
}
|
|
2506
2522
|
for (const pa of sourceFile.getDescendantsOfKind(SyntaxKind3.PropertyAssignment)) {
|
|
2507
|
-
const
|
|
2508
|
-
const propName = nameNode?.getText?.() ?? "";
|
|
2523
|
+
const propName = pa.getNameNode().getText();
|
|
2509
2524
|
if (!SECRET_NAME_RE.test(propName)) continue;
|
|
2510
2525
|
const init = pa.getInitializer();
|
|
2511
2526
|
if (!init || !isStringLiteralLike(init)) continue;
|
|
@@ -2541,9 +2556,9 @@ var SecurityVerifier = class {
|
|
|
2541
2556
|
if (checkXss) {
|
|
2542
2557
|
for (const bin of sourceFile.getDescendantsOfKind(SyntaxKind3.BinaryExpression)) {
|
|
2543
2558
|
const left = bin.getLeft();
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
if (
|
|
2559
|
+
const propertyAccess = left.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2560
|
+
if (!propertyAccess) continue;
|
|
2561
|
+
if (propertyAccess.getName() === "innerHTML") {
|
|
2547
2562
|
violations.push(createViolation({
|
|
2548
2563
|
decisionId,
|
|
2549
2564
|
constraintId: constraint.id,
|
|
@@ -2572,8 +2587,9 @@ var SecurityVerifier = class {
|
|
|
2572
2587
|
if (checkSql) {
|
|
2573
2588
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind3.CallExpression)) {
|
|
2574
2589
|
const expr = call.getExpression();
|
|
2575
|
-
|
|
2576
|
-
|
|
2590
|
+
const propertyAccess = expr.asKind(SyntaxKind3.PropertyAccessExpression);
|
|
2591
|
+
if (!propertyAccess) continue;
|
|
2592
|
+
const name = propertyAccess.getName();
|
|
2577
2593
|
if (name !== "query" && name !== "execute") continue;
|
|
2578
2594
|
const arg = call.getArguments()[0];
|
|
2579
2595
|
if (!arg) continue;
|
|
@@ -2638,12 +2654,14 @@ var ApiVerifier = class {
|
|
|
2638
2654
|
if (!enforceKebab) return violations;
|
|
2639
2655
|
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind4.CallExpression)) {
|
|
2640
2656
|
const expr = call.getExpression();
|
|
2641
|
-
|
|
2642
|
-
|
|
2657
|
+
const propertyAccess = expr.asKind(SyntaxKind4.PropertyAccessExpression);
|
|
2658
|
+
if (!propertyAccess) continue;
|
|
2659
|
+
const method = propertyAccess.getName();
|
|
2643
2660
|
if (!method || !HTTP_METHODS.has(String(method))) continue;
|
|
2644
2661
|
const firstArg = call.getArguments()[0];
|
|
2645
|
-
|
|
2646
|
-
|
|
2662
|
+
const stringLiteral = firstArg?.asKind(SyntaxKind4.StringLiteral);
|
|
2663
|
+
if (!stringLiteral) continue;
|
|
2664
|
+
const pathValue = stringLiteral.getLiteralValue();
|
|
2647
2665
|
if (typeof pathValue !== "string") continue;
|
|
2648
2666
|
if (!isKebabPath(pathValue)) {
|
|
2649
2667
|
violations.push(createViolation({
|
|
@@ -2667,10 +2685,36 @@ import { existsSync } from "fs";
|
|
|
2667
2685
|
import { join as join5 } from "path";
|
|
2668
2686
|
import { pathToFileURL } from "url";
|
|
2669
2687
|
import fg2 from "fast-glob";
|
|
2688
|
+
|
|
2689
|
+
// src/utils/logger.ts
|
|
2690
|
+
import pino from "pino";
|
|
2691
|
+
var defaultOptions = {
|
|
2692
|
+
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
2693
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2694
|
+
base: {
|
|
2695
|
+
service: "specbridge"
|
|
2696
|
+
}
|
|
2697
|
+
};
|
|
2698
|
+
var destination = pino.destination({
|
|
2699
|
+
fd: 2,
|
|
2700
|
+
// stderr
|
|
2701
|
+
sync: false
|
|
2702
|
+
});
|
|
2703
|
+
var rootLogger = pino(defaultOptions, destination);
|
|
2704
|
+
function getLogger(bindings) {
|
|
2705
|
+
if (!bindings) {
|
|
2706
|
+
return rootLogger;
|
|
2707
|
+
}
|
|
2708
|
+
return rootLogger.child(bindings);
|
|
2709
|
+
}
|
|
2710
|
+
var logger = getLogger();
|
|
2711
|
+
|
|
2712
|
+
// src/verification/plugins/loader.ts
|
|
2670
2713
|
var PluginLoader = class {
|
|
2671
2714
|
plugins = /* @__PURE__ */ new Map();
|
|
2672
2715
|
loaded = false;
|
|
2673
2716
|
loadErrors = [];
|
|
2717
|
+
logger = getLogger({ module: "verification.plugins.loader" });
|
|
2674
2718
|
/**
|
|
2675
2719
|
* Load all plugins from the specified base path
|
|
2676
2720
|
*
|
|
@@ -2693,15 +2737,15 @@ var PluginLoader = class {
|
|
|
2693
2737
|
} catch (error) {
|
|
2694
2738
|
const message = error instanceof Error ? error.message : String(error);
|
|
2695
2739
|
this.loadErrors.push({ file, error: message });
|
|
2696
|
-
|
|
2740
|
+
this.logger.warn({ file, error: message }, "Failed to load plugin");
|
|
2697
2741
|
}
|
|
2698
2742
|
}
|
|
2699
2743
|
this.loaded = true;
|
|
2700
2744
|
if (this.plugins.size > 0) {
|
|
2701
|
-
|
|
2745
|
+
this.logger.info({ count: this.plugins.size }, "Loaded custom verifier plugins");
|
|
2702
2746
|
}
|
|
2703
2747
|
if (this.loadErrors.length > 0) {
|
|
2704
|
-
|
|
2748
|
+
this.logger.warn({ count: this.loadErrors.length }, "Plugin load failures");
|
|
2705
2749
|
}
|
|
2706
2750
|
}
|
|
2707
2751
|
/**
|
|
@@ -2877,8 +2921,9 @@ var builtinVerifiers = {
|
|
|
2877
2921
|
};
|
|
2878
2922
|
var verifierInstances = /* @__PURE__ */ new Map();
|
|
2879
2923
|
function getVerifier(id) {
|
|
2880
|
-
|
|
2881
|
-
|
|
2924
|
+
const pooled = verifierInstances.get(id);
|
|
2925
|
+
if (pooled) {
|
|
2926
|
+
return pooled;
|
|
2882
2927
|
}
|
|
2883
2928
|
const pluginLoader2 = getPluginLoader();
|
|
2884
2929
|
const customVerifier = pluginLoader2.getVerifier(id);
|
|
@@ -3080,6 +3125,7 @@ var VerificationEngine = class {
|
|
|
3080
3125
|
astCache;
|
|
3081
3126
|
resultsCache;
|
|
3082
3127
|
pluginsLoaded = false;
|
|
3128
|
+
logger = getLogger({ module: "verification.engine" });
|
|
3083
3129
|
constructor(registry) {
|
|
3084
3130
|
this.registry = registry || createRegistry();
|
|
3085
3131
|
this.project = new Project2({
|
|
@@ -3253,13 +3299,12 @@ var VerificationEngine = class {
|
|
|
3253
3299
|
);
|
|
3254
3300
|
if (!verifier) {
|
|
3255
3301
|
const requestedVerifier = constraint.check?.verifier || constraint.verifier || "auto-detected";
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
);
|
|
3302
|
+
this.logger.warn({
|
|
3303
|
+
decisionId: decision.metadata.id,
|
|
3304
|
+
constraintId: constraint.id,
|
|
3305
|
+
requestedVerifier,
|
|
3306
|
+
availableVerifiers: getVerifierIds()
|
|
3307
|
+
}, "No verifier found for constraint");
|
|
3263
3308
|
warnings.push({
|
|
3264
3309
|
type: "missing_verifier",
|
|
3265
3310
|
message: `No verifier found for constraint (requested: ${requestedVerifier})`,
|
|
@@ -3365,17 +3410,14 @@ var VerificationEngine = class {
|
|
|
3365
3410
|
} catch (error) {
|
|
3366
3411
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3367
3412
|
const errorStack = error instanceof Error ? error.stack : void 0;
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
);
|
|
3376
|
-
if (errorStack) {
|
|
3377
|
-
console.error(chalk3.dim(errorStack));
|
|
3378
|
-
}
|
|
3413
|
+
this.logger.error({
|
|
3414
|
+
verifierId: verifier.id,
|
|
3415
|
+
filePath,
|
|
3416
|
+
decisionId: decision.metadata.id,
|
|
3417
|
+
constraintId: constraint.id,
|
|
3418
|
+
error: errorMessage,
|
|
3419
|
+
stack: errorStack
|
|
3420
|
+
}, "Verifier execution failed");
|
|
3379
3421
|
errors.push({
|
|
3380
3422
|
type: "verifier_exception",
|
|
3381
3423
|
message: `Verifier '${verifier.id}' failed: ${errorMessage}`,
|
|
@@ -3491,6 +3533,10 @@ var AutofixEngine = class {
|
|
|
3491
3533
|
const edits = [];
|
|
3492
3534
|
for (const violation of fileViolations) {
|
|
3493
3535
|
const fix = violation.autofix;
|
|
3536
|
+
if (!fix) {
|
|
3537
|
+
skippedViolations++;
|
|
3538
|
+
continue;
|
|
3539
|
+
}
|
|
3494
3540
|
if (options.interactive) {
|
|
3495
3541
|
const ok = await confirmFix(`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`);
|
|
3496
3542
|
if (!ok) {
|
|
@@ -3537,7 +3583,7 @@ async function getChangedFiles(cwd) {
|
|
|
3537
3583
|
}
|
|
3538
3584
|
|
|
3539
3585
|
// src/verification/explain.ts
|
|
3540
|
-
import
|
|
3586
|
+
import chalk3 from "chalk";
|
|
3541
3587
|
var ExplainReporter = class {
|
|
3542
3588
|
entries = [];
|
|
3543
3589
|
/**
|
|
@@ -3551,10 +3597,10 @@ var ExplainReporter = class {
|
|
|
3551
3597
|
*/
|
|
3552
3598
|
print() {
|
|
3553
3599
|
if (this.entries.length === 0) {
|
|
3554
|
-
console.log(
|
|
3600
|
+
console.log(chalk3.dim("No constraints were evaluated."));
|
|
3555
3601
|
return;
|
|
3556
3602
|
}
|
|
3557
|
-
console.log(
|
|
3603
|
+
console.log(chalk3.bold("\n=== Verification Explanation ===\n"));
|
|
3558
3604
|
const byFile = /* @__PURE__ */ new Map();
|
|
3559
3605
|
for (const entry of this.entries) {
|
|
3560
3606
|
const existing = byFile.get(entry.file) || [];
|
|
@@ -3562,22 +3608,22 @@ var ExplainReporter = class {
|
|
|
3562
3608
|
byFile.set(entry.file, existing);
|
|
3563
3609
|
}
|
|
3564
3610
|
for (const [file, entries] of byFile) {
|
|
3565
|
-
console.log(
|
|
3611
|
+
console.log(chalk3.underline(file));
|
|
3566
3612
|
for (const entry of entries) {
|
|
3567
|
-
const icon = entry.applied ?
|
|
3613
|
+
const icon = entry.applied ? chalk3.green("\u2713") : chalk3.dim("\u2298");
|
|
3568
3614
|
const constraintId = `${entry.decision.metadata.id}/${entry.constraint.id}`;
|
|
3569
3615
|
console.log(` ${icon} ${constraintId}`);
|
|
3570
|
-
console.log(
|
|
3616
|
+
console.log(chalk3.dim(` ${entry.reason}`));
|
|
3571
3617
|
if (entry.applied && entry.selectedVerifier) {
|
|
3572
|
-
console.log(
|
|
3618
|
+
console.log(chalk3.dim(` Verifier: ${entry.selectedVerifier}`));
|
|
3573
3619
|
if (entry.verifierOutput) {
|
|
3574
3620
|
if (entry.verifierOutput.error) {
|
|
3575
|
-
console.log(
|
|
3621
|
+
console.log(chalk3.red(` Error: ${entry.verifierOutput.error}`));
|
|
3576
3622
|
} else {
|
|
3577
3623
|
const violationText = entry.verifierOutput.violations === 1 ? "1 violation" : `${entry.verifierOutput.violations} violations`;
|
|
3578
|
-
const resultColor = entry.verifierOutput.violations > 0 ?
|
|
3624
|
+
const resultColor = entry.verifierOutput.violations > 0 ? chalk3.red : chalk3.green;
|
|
3579
3625
|
console.log(
|
|
3580
|
-
|
|
3626
|
+
chalk3.dim(` Result: `) + resultColor(violationText) + chalk3.dim(` in ${entry.verifierOutput.duration}ms`)
|
|
3581
3627
|
);
|
|
3582
3628
|
}
|
|
3583
3629
|
}
|
|
@@ -3587,9 +3633,9 @@ var ExplainReporter = class {
|
|
|
3587
3633
|
}
|
|
3588
3634
|
const applied = this.entries.filter((e) => e.applied).length;
|
|
3589
3635
|
const skipped = this.entries.filter((e) => !e.applied).length;
|
|
3590
|
-
console.log(
|
|
3591
|
-
console.log(` Constraints Applied: ${
|
|
3592
|
-
console.log(` Constraints Skipped: ${
|
|
3636
|
+
console.log(chalk3.bold("Summary:"));
|
|
3637
|
+
console.log(` Constraints Applied: ${chalk3.green(applied)}`);
|
|
3638
|
+
console.log(` Constraints Skipped: ${chalk3.dim(skipped)}`);
|
|
3593
3639
|
}
|
|
3594
3640
|
/**
|
|
3595
3641
|
* Get all entries
|
|
@@ -3639,7 +3685,7 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3639
3685
|
if (fixableCount === 0) {
|
|
3640
3686
|
spinner.stop();
|
|
3641
3687
|
if (!options.json) {
|
|
3642
|
-
console.log(
|
|
3688
|
+
console.log(chalk4.yellow("No auto-fixable violations found"));
|
|
3643
3689
|
}
|
|
3644
3690
|
} else {
|
|
3645
3691
|
spinner.text = `Applying ${fixableCount} auto-fix(es)...`;
|
|
@@ -3664,34 +3710,34 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3664
3710
|
console.log(JSON.stringify({ ...result, autofix: fixResult }, null, 2));
|
|
3665
3711
|
} else {
|
|
3666
3712
|
if (result.warnings && result.warnings.length > 0) {
|
|
3667
|
-
console.log(
|
|
3713
|
+
console.log(chalk4.yellow.bold("\nWarnings:"));
|
|
3668
3714
|
for (const warning of result.warnings) {
|
|
3669
|
-
console.log(
|
|
3670
|
-
console.log(
|
|
3715
|
+
console.log(chalk4.yellow(` \u26A0 ${warning.message}`));
|
|
3716
|
+
console.log(chalk4.dim(` ${warning.decisionId}/${warning.constraintId}`));
|
|
3671
3717
|
if (warning.file) {
|
|
3672
|
-
console.log(
|
|
3718
|
+
console.log(chalk4.dim(` File: ${warning.file}`));
|
|
3673
3719
|
}
|
|
3674
3720
|
}
|
|
3675
3721
|
console.log("");
|
|
3676
3722
|
}
|
|
3677
3723
|
if (result.errors && result.errors.length > 0) {
|
|
3678
|
-
console.log(
|
|
3724
|
+
console.log(chalk4.red.bold("\nErrors:"));
|
|
3679
3725
|
for (const error of result.errors) {
|
|
3680
|
-
console.log(
|
|
3726
|
+
console.log(chalk4.red(` \u2717 ${error.message}`));
|
|
3681
3727
|
if (error.decisionId && error.constraintId) {
|
|
3682
|
-
console.log(
|
|
3728
|
+
console.log(chalk4.dim(` ${error.decisionId}/${error.constraintId}`));
|
|
3683
3729
|
}
|
|
3684
3730
|
if (error.file) {
|
|
3685
|
-
console.log(
|
|
3731
|
+
console.log(chalk4.dim(` File: ${error.file}`));
|
|
3686
3732
|
}
|
|
3687
3733
|
}
|
|
3688
3734
|
console.log("");
|
|
3689
3735
|
}
|
|
3690
3736
|
printResult(result, level);
|
|
3691
3737
|
if (options.fix && fixResult) {
|
|
3692
|
-
console.log(
|
|
3738
|
+
console.log(chalk4.green(`\u2713 Applied ${fixResult.applied.length} fix(es)`));
|
|
3693
3739
|
if (fixResult.skipped > 0) {
|
|
3694
|
-
console.log(
|
|
3740
|
+
console.log(chalk4.yellow(`\u2298 Skipped ${fixResult.skipped} fix(es)`));
|
|
3695
3741
|
}
|
|
3696
3742
|
console.log("");
|
|
3697
3743
|
}
|
|
@@ -3710,8 +3756,8 @@ var verifyCommand = new Command3("verify").description("Verify code compliance a
|
|
|
3710
3756
|
function printResult(result, level) {
|
|
3711
3757
|
console.log("");
|
|
3712
3758
|
if (result.violations.length === 0) {
|
|
3713
|
-
console.log(
|
|
3714
|
-
console.log(
|
|
3759
|
+
console.log(chalk4.green("\u2713 All checks passed!"));
|
|
3760
|
+
console.log(chalk4.dim(` ${result.checked} files checked in ${result.duration}ms`));
|
|
3715
3761
|
return;
|
|
3716
3762
|
}
|
|
3717
3763
|
const byFile = /* @__PURE__ */ new Map();
|
|
@@ -3721,7 +3767,7 @@ function printResult(result, level) {
|
|
|
3721
3767
|
byFile.set(violation.file, existing);
|
|
3722
3768
|
}
|
|
3723
3769
|
for (const [file, violations] of byFile) {
|
|
3724
|
-
console.log(
|
|
3770
|
+
console.log(chalk4.underline(file));
|
|
3725
3771
|
for (const v of violations) {
|
|
3726
3772
|
const typeIcon = getTypeIcon(v.type);
|
|
3727
3773
|
const severityColor = getSeverityColor(v.severity);
|
|
@@ -3729,9 +3775,9 @@ function printResult(result, level) {
|
|
|
3729
3775
|
console.log(
|
|
3730
3776
|
` ${typeIcon} ${severityColor(`[${v.severity}]`)} ${v.message}`
|
|
3731
3777
|
);
|
|
3732
|
-
console.log(
|
|
3778
|
+
console.log(chalk4.dim(` ${v.decisionId}/${v.constraintId}${location}`));
|
|
3733
3779
|
if (v.suggestion) {
|
|
3734
|
-
console.log(
|
|
3780
|
+
console.log(chalk4.cyan(` Suggestion: ${v.suggestion}`));
|
|
3735
3781
|
}
|
|
3736
3782
|
}
|
|
3737
3783
|
console.log("");
|
|
@@ -3740,29 +3786,29 @@ function printResult(result, level) {
|
|
|
3740
3786
|
const highCount = result.violations.filter((v) => v.severity === "high").length;
|
|
3741
3787
|
const mediumCount = result.violations.filter((v) => v.severity === "medium").length;
|
|
3742
3788
|
const lowCount = result.violations.filter((v) => v.severity === "low").length;
|
|
3743
|
-
console.log(
|
|
3789
|
+
console.log(chalk4.bold("Summary:"));
|
|
3744
3790
|
console.log(` Files: ${result.checked} checked, ${result.passed} passed, ${result.failed} failed`);
|
|
3745
3791
|
const violationParts = [];
|
|
3746
|
-
if (criticalCount > 0) violationParts.push(
|
|
3747
|
-
if (highCount > 0) violationParts.push(
|
|
3748
|
-
if (mediumCount > 0) violationParts.push(
|
|
3749
|
-
if (lowCount > 0) violationParts.push(
|
|
3792
|
+
if (criticalCount > 0) violationParts.push(chalk4.red(`${criticalCount} critical`));
|
|
3793
|
+
if (highCount > 0) violationParts.push(chalk4.yellow(`${highCount} high`));
|
|
3794
|
+
if (mediumCount > 0) violationParts.push(chalk4.cyan(`${mediumCount} medium`));
|
|
3795
|
+
if (lowCount > 0) violationParts.push(chalk4.dim(`${lowCount} low`));
|
|
3750
3796
|
console.log(` Violations: ${violationParts.join(", ")}`);
|
|
3751
3797
|
console.log(` Duration: ${result.duration}ms`);
|
|
3752
3798
|
if (!result.success) {
|
|
3753
3799
|
console.log("");
|
|
3754
3800
|
const blockingTypes = level === "commit" ? "invariant or critical" : level === "pr" ? "invariant, critical, or high" : "invariant";
|
|
3755
|
-
console.log(
|
|
3801
|
+
console.log(chalk4.red(`\u2717 Verification failed. ${blockingTypes} violations must be resolved.`));
|
|
3756
3802
|
}
|
|
3757
3803
|
}
|
|
3758
3804
|
function getTypeIcon(type) {
|
|
3759
3805
|
switch (type) {
|
|
3760
3806
|
case "invariant":
|
|
3761
|
-
return
|
|
3807
|
+
return chalk4.red("\u25CF");
|
|
3762
3808
|
case "convention":
|
|
3763
|
-
return
|
|
3809
|
+
return chalk4.yellow("\u25CF");
|
|
3764
3810
|
case "guideline":
|
|
3765
|
-
return
|
|
3811
|
+
return chalk4.green("\u25CF");
|
|
3766
3812
|
default:
|
|
3767
3813
|
return "\u25CB";
|
|
3768
3814
|
}
|
|
@@ -3770,15 +3816,15 @@ function getTypeIcon(type) {
|
|
|
3770
3816
|
function getSeverityColor(severity) {
|
|
3771
3817
|
switch (severity) {
|
|
3772
3818
|
case "critical":
|
|
3773
|
-
return
|
|
3819
|
+
return chalk4.red;
|
|
3774
3820
|
case "high":
|
|
3775
|
-
return
|
|
3821
|
+
return chalk4.yellow;
|
|
3776
3822
|
case "medium":
|
|
3777
|
-
return
|
|
3823
|
+
return chalk4.cyan;
|
|
3778
3824
|
case "low":
|
|
3779
|
-
return
|
|
3825
|
+
return chalk4.dim;
|
|
3780
3826
|
default:
|
|
3781
|
-
return
|
|
3827
|
+
return chalk4.white;
|
|
3782
3828
|
}
|
|
3783
3829
|
}
|
|
3784
3830
|
|
|
@@ -3787,15 +3833,15 @@ import { Command as Command8 } from "commander";
|
|
|
3787
3833
|
|
|
3788
3834
|
// src/cli/commands/decision/list.ts
|
|
3789
3835
|
import { Command as Command4 } from "commander";
|
|
3790
|
-
import
|
|
3836
|
+
import chalk5 from "chalk";
|
|
3791
3837
|
import { table } from "table";
|
|
3792
3838
|
var listDecisions = new Command4("list").description("List all architectural decisions").option("-s, --status <status>", "Filter by status (draft, active, deprecated, superseded)").option("-t, --tag <tag>", "Filter by tag").option("--json", "Output as JSON").action(async (options) => {
|
|
3793
3839
|
const registry = createRegistry();
|
|
3794
3840
|
const result = await registry.load();
|
|
3795
3841
|
if (result.errors.length > 0) {
|
|
3796
|
-
console.warn(
|
|
3842
|
+
console.warn(chalk5.yellow("\nWarnings:"));
|
|
3797
3843
|
for (const err of result.errors) {
|
|
3798
|
-
console.warn(
|
|
3844
|
+
console.warn(chalk5.yellow(` - ${err.filePath}: ${err.error}`));
|
|
3799
3845
|
}
|
|
3800
3846
|
console.log("");
|
|
3801
3847
|
}
|
|
@@ -3808,7 +3854,7 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3808
3854
|
}
|
|
3809
3855
|
const decisions = registry.getAll(filter);
|
|
3810
3856
|
if (decisions.length === 0) {
|
|
3811
|
-
console.log(
|
|
3857
|
+
console.log(chalk5.yellow("No decisions found."));
|
|
3812
3858
|
return;
|
|
3813
3859
|
}
|
|
3814
3860
|
if (options.json) {
|
|
@@ -3817,11 +3863,11 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3817
3863
|
}
|
|
3818
3864
|
const data = [
|
|
3819
3865
|
[
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3866
|
+
chalk5.bold("ID"),
|
|
3867
|
+
chalk5.bold("Title"),
|
|
3868
|
+
chalk5.bold("Status"),
|
|
3869
|
+
chalk5.bold("Constraints"),
|
|
3870
|
+
chalk5.bold("Tags")
|
|
3825
3871
|
]
|
|
3826
3872
|
];
|
|
3827
3873
|
for (const decision of decisions) {
|
|
@@ -3855,20 +3901,20 @@ var listDecisions = new Command4("list").description("List all architectural dec
|
|
|
3855
3901
|
},
|
|
3856
3902
|
drawHorizontalLine: (index) => index === 1
|
|
3857
3903
|
}));
|
|
3858
|
-
console.log(
|
|
3904
|
+
console.log(chalk5.dim(`Total: ${decisions.length} decision(s)`));
|
|
3859
3905
|
});
|
|
3860
3906
|
function getStatusColor(status) {
|
|
3861
3907
|
switch (status) {
|
|
3862
3908
|
case "active":
|
|
3863
|
-
return
|
|
3909
|
+
return chalk5.green;
|
|
3864
3910
|
case "draft":
|
|
3865
|
-
return
|
|
3911
|
+
return chalk5.yellow;
|
|
3866
3912
|
case "deprecated":
|
|
3867
|
-
return
|
|
3913
|
+
return chalk5.gray;
|
|
3868
3914
|
case "superseded":
|
|
3869
|
-
return
|
|
3915
|
+
return chalk5.blue;
|
|
3870
3916
|
default:
|
|
3871
|
-
return
|
|
3917
|
+
return chalk5.white;
|
|
3872
3918
|
}
|
|
3873
3919
|
}
|
|
3874
3920
|
function getConstraintTypeSummary(types) {
|
|
@@ -3881,9 +3927,9 @@ function getConstraintTypeSummary(types) {
|
|
|
3881
3927
|
counts[type]++;
|
|
3882
3928
|
}
|
|
3883
3929
|
const parts = [];
|
|
3884
|
-
if (counts.invariant > 0) parts.push(
|
|
3885
|
-
if (counts.convention > 0) parts.push(
|
|
3886
|
-
if (counts.guideline > 0) parts.push(
|
|
3930
|
+
if (counts.invariant > 0) parts.push(chalk5.red(`${counts.invariant}I`));
|
|
3931
|
+
if (counts.convention > 0) parts.push(chalk5.yellow(`${counts.convention}C`));
|
|
3932
|
+
if (counts.guideline > 0) parts.push(chalk5.green(`${counts.guideline}G`));
|
|
3887
3933
|
return parts.join(" ") || "-";
|
|
3888
3934
|
}
|
|
3889
3935
|
function truncate(str, length) {
|
|
@@ -3893,7 +3939,7 @@ function truncate(str, length) {
|
|
|
3893
3939
|
|
|
3894
3940
|
// src/cli/commands/decision/show.ts
|
|
3895
3941
|
import { Command as Command5 } from "commander";
|
|
3896
|
-
import
|
|
3942
|
+
import chalk6 from "chalk";
|
|
3897
3943
|
var showDecision = new Command5("show").description("Show details of a specific decision").argument("<id>", "Decision ID").option("--json", "Output as JSON").action(async (id, options) => {
|
|
3898
3944
|
const registry = createRegistry();
|
|
3899
3945
|
await registry.load();
|
|
@@ -3906,74 +3952,74 @@ var showDecision = new Command5("show").description("Show details of a specific
|
|
|
3906
3952
|
});
|
|
3907
3953
|
function printDecision(decision) {
|
|
3908
3954
|
const { metadata, decision: content, constraints } = decision;
|
|
3909
|
-
console.log(
|
|
3955
|
+
console.log(chalk6.bold.blue(`
|
|
3910
3956
|
${metadata.title}`));
|
|
3911
|
-
console.log(
|
|
3957
|
+
console.log(chalk6.dim(`ID: ${metadata.id}`));
|
|
3912
3958
|
console.log("");
|
|
3913
|
-
console.log(
|
|
3914
|
-
console.log(
|
|
3959
|
+
console.log(chalk6.bold("Status:"), getStatusBadge(metadata.status));
|
|
3960
|
+
console.log(chalk6.bold("Owners:"), metadata.owners.join(", "));
|
|
3915
3961
|
if (metadata.tags && metadata.tags.length > 0) {
|
|
3916
|
-
console.log(
|
|
3962
|
+
console.log(chalk6.bold("Tags:"), metadata.tags.map((t) => chalk6.cyan(t)).join(", "));
|
|
3917
3963
|
}
|
|
3918
3964
|
if (metadata.createdAt) {
|
|
3919
|
-
console.log(
|
|
3965
|
+
console.log(chalk6.bold("Created:"), metadata.createdAt);
|
|
3920
3966
|
}
|
|
3921
3967
|
if (metadata.supersededBy) {
|
|
3922
|
-
console.log(
|
|
3968
|
+
console.log(chalk6.bold("Superseded by:"), chalk6.yellow(metadata.supersededBy));
|
|
3923
3969
|
}
|
|
3924
3970
|
console.log("");
|
|
3925
|
-
console.log(
|
|
3971
|
+
console.log(chalk6.bold.underline("Summary"));
|
|
3926
3972
|
console.log(content.summary);
|
|
3927
3973
|
console.log("");
|
|
3928
|
-
console.log(
|
|
3974
|
+
console.log(chalk6.bold.underline("Rationale"));
|
|
3929
3975
|
console.log(content.rationale);
|
|
3930
3976
|
console.log("");
|
|
3931
3977
|
if (content.context) {
|
|
3932
|
-
console.log(
|
|
3978
|
+
console.log(chalk6.bold.underline("Context"));
|
|
3933
3979
|
console.log(content.context);
|
|
3934
3980
|
console.log("");
|
|
3935
3981
|
}
|
|
3936
3982
|
if (content.consequences && content.consequences.length > 0) {
|
|
3937
|
-
console.log(
|
|
3983
|
+
console.log(chalk6.bold.underline("Consequences"));
|
|
3938
3984
|
for (const consequence of content.consequences) {
|
|
3939
3985
|
console.log(` \u2022 ${consequence}`);
|
|
3940
3986
|
}
|
|
3941
3987
|
console.log("");
|
|
3942
3988
|
}
|
|
3943
|
-
console.log(
|
|
3989
|
+
console.log(chalk6.bold.underline(`Constraints (${constraints.length})`));
|
|
3944
3990
|
for (const constraint of constraints) {
|
|
3945
3991
|
const typeIcon = getTypeIcon2(constraint.type);
|
|
3946
3992
|
const severityBadge = getSeverityBadge(constraint.severity);
|
|
3947
3993
|
console.log(`
|
|
3948
|
-
${typeIcon} ${
|
|
3994
|
+
${typeIcon} ${chalk6.bold(constraint.id)} ${severityBadge}`);
|
|
3949
3995
|
console.log(` ${constraint.rule}`);
|
|
3950
|
-
console.log(
|
|
3996
|
+
console.log(chalk6.dim(` Scope: ${constraint.scope}`));
|
|
3951
3997
|
if (constraint.verifier) {
|
|
3952
|
-
console.log(
|
|
3998
|
+
console.log(chalk6.dim(` Verifier: ${constraint.verifier}`));
|
|
3953
3999
|
}
|
|
3954
4000
|
if (constraint.exceptions && constraint.exceptions.length > 0) {
|
|
3955
|
-
console.log(
|
|
4001
|
+
console.log(chalk6.dim(` Exceptions: ${constraint.exceptions.length}`));
|
|
3956
4002
|
}
|
|
3957
4003
|
}
|
|
3958
4004
|
console.log("");
|
|
3959
4005
|
if (decision.verification?.automated && decision.verification.automated.length > 0) {
|
|
3960
|
-
console.log(
|
|
4006
|
+
console.log(chalk6.bold.underline("Automated Verification"));
|
|
3961
4007
|
for (const check of decision.verification.automated) {
|
|
3962
4008
|
console.log(` \u2022 ${check.check} (${check.frequency})`);
|
|
3963
|
-
console.log(
|
|
4009
|
+
console.log(chalk6.dim(` Target: ${check.target}`));
|
|
3964
4010
|
}
|
|
3965
4011
|
console.log("");
|
|
3966
4012
|
}
|
|
3967
4013
|
if (decision.links) {
|
|
3968
4014
|
const { related, supersedes, references } = decision.links;
|
|
3969
4015
|
if (related && related.length > 0) {
|
|
3970
|
-
console.log(
|
|
4016
|
+
console.log(chalk6.bold("Related:"), related.join(", "));
|
|
3971
4017
|
}
|
|
3972
4018
|
if (supersedes && supersedes.length > 0) {
|
|
3973
|
-
console.log(
|
|
4019
|
+
console.log(chalk6.bold("Supersedes:"), supersedes.join(", "));
|
|
3974
4020
|
}
|
|
3975
4021
|
if (references && references.length > 0) {
|
|
3976
|
-
console.log(
|
|
4022
|
+
console.log(chalk6.bold("References:"));
|
|
3977
4023
|
for (const ref of references) {
|
|
3978
4024
|
console.log(` \u2022 ${ref}`);
|
|
3979
4025
|
}
|
|
@@ -3983,13 +4029,13 @@ ${metadata.title}`));
|
|
|
3983
4029
|
function getStatusBadge(status) {
|
|
3984
4030
|
switch (status) {
|
|
3985
4031
|
case "active":
|
|
3986
|
-
return
|
|
4032
|
+
return chalk6.bgGreen.black(" ACTIVE ");
|
|
3987
4033
|
case "draft":
|
|
3988
|
-
return
|
|
4034
|
+
return chalk6.bgYellow.black(" DRAFT ");
|
|
3989
4035
|
case "deprecated":
|
|
3990
|
-
return
|
|
4036
|
+
return chalk6.bgGray.white(" DEPRECATED ");
|
|
3991
4037
|
case "superseded":
|
|
3992
|
-
return
|
|
4038
|
+
return chalk6.bgBlue.white(" SUPERSEDED ");
|
|
3993
4039
|
default:
|
|
3994
4040
|
return status;
|
|
3995
4041
|
}
|
|
@@ -3997,11 +4043,11 @@ function getStatusBadge(status) {
|
|
|
3997
4043
|
function getTypeIcon2(type) {
|
|
3998
4044
|
switch (type) {
|
|
3999
4045
|
case "invariant":
|
|
4000
|
-
return
|
|
4046
|
+
return chalk6.red("\u25CF");
|
|
4001
4047
|
case "convention":
|
|
4002
|
-
return
|
|
4048
|
+
return chalk6.yellow("\u25CF");
|
|
4003
4049
|
case "guideline":
|
|
4004
|
-
return
|
|
4050
|
+
return chalk6.green("\u25CF");
|
|
4005
4051
|
default:
|
|
4006
4052
|
return "\u25CB";
|
|
4007
4053
|
}
|
|
@@ -4009,13 +4055,13 @@ function getTypeIcon2(type) {
|
|
|
4009
4055
|
function getSeverityBadge(severity) {
|
|
4010
4056
|
switch (severity) {
|
|
4011
4057
|
case "critical":
|
|
4012
|
-
return
|
|
4058
|
+
return chalk6.bgRed.white(" CRITICAL ");
|
|
4013
4059
|
case "high":
|
|
4014
|
-
return
|
|
4060
|
+
return chalk6.bgYellow.black(" HIGH ");
|
|
4015
4061
|
case "medium":
|
|
4016
|
-
return
|
|
4062
|
+
return chalk6.bgCyan.black(" MEDIUM ");
|
|
4017
4063
|
case "low":
|
|
4018
|
-
return
|
|
4064
|
+
return chalk6.bgGray.white(" LOW ");
|
|
4019
4065
|
default:
|
|
4020
4066
|
return severity;
|
|
4021
4067
|
}
|
|
@@ -4023,7 +4069,7 @@ function getSeverityBadge(severity) {
|
|
|
4023
4069
|
|
|
4024
4070
|
// src/cli/commands/decision/validate.ts
|
|
4025
4071
|
import { Command as Command6 } from "commander";
|
|
4026
|
-
import
|
|
4072
|
+
import chalk7 from "chalk";
|
|
4027
4073
|
import ora4 from "ora";
|
|
4028
4074
|
import { join as join6 } from "path";
|
|
4029
4075
|
var validateDecisions = new Command6("validate").description("Validate decision files").option("-f, --file <path>", "Validate a specific file").action(async (options) => {
|
|
@@ -4062,14 +4108,14 @@ var validateDecisions = new Command6("validate").description("Validate decision
|
|
|
4062
4108
|
}
|
|
4063
4109
|
spinner.stop();
|
|
4064
4110
|
if (invalid === 0) {
|
|
4065
|
-
console.log(
|
|
4111
|
+
console.log(chalk7.green(`\u2713 All ${valid} decision file(s) are valid.`));
|
|
4066
4112
|
} else {
|
|
4067
|
-
console.log(
|
|
4113
|
+
console.log(chalk7.red(`\u2717 ${invalid} of ${files.length} decision file(s) have errors.
|
|
4068
4114
|
`));
|
|
4069
4115
|
for (const { file, errors: fileErrors } of errors) {
|
|
4070
|
-
console.log(
|
|
4116
|
+
console.log(chalk7.red(`File: ${file}`));
|
|
4071
4117
|
for (const err of fileErrors) {
|
|
4072
|
-
console.log(
|
|
4118
|
+
console.log(chalk7.dim(` - ${err}`));
|
|
4073
4119
|
}
|
|
4074
4120
|
console.log("");
|
|
4075
4121
|
}
|
|
@@ -4083,7 +4129,7 @@ var validateDecisions = new Command6("validate").description("Validate decision
|
|
|
4083
4129
|
|
|
4084
4130
|
// src/cli/commands/decision/create.ts
|
|
4085
4131
|
import { Command as Command7 } from "commander";
|
|
4086
|
-
import
|
|
4132
|
+
import chalk8 from "chalk";
|
|
4087
4133
|
import { join as join7 } from "path";
|
|
4088
4134
|
var createDecision = new Command7("create").description("Create a new decision file").argument("<id>", "Decision ID (e.g., auth-001)").requiredOption("-t, --title <title>", "Decision title").requiredOption("-s, --summary <summary>", "One-sentence summary").option("--type <type>", "Default constraint type (invariant, convention, guideline)", "convention").option("--severity <severity>", "Default constraint severity (critical, high, medium, low)", "medium").option("--scope <scope>", "Default constraint scope (glob pattern)", "src/**/*.ts").option("-o, --owner <owner>", "Owner name", "team").action(async (id, options) => {
|
|
4089
4135
|
const cwd = process.cwd();
|
|
@@ -4091,13 +4137,13 @@ var createDecision = new Command7("create").description("Create a new decision f
|
|
|
4091
4137
|
throw new NotInitializedError();
|
|
4092
4138
|
}
|
|
4093
4139
|
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
4094
|
-
console.error(
|
|
4140
|
+
console.error(chalk8.red("Error: Decision ID must be lowercase alphanumeric with hyphens only."));
|
|
4095
4141
|
process.exit(1);
|
|
4096
4142
|
}
|
|
4097
4143
|
const decisionsDir = getDecisionsDir(cwd);
|
|
4098
4144
|
const filePath = join7(decisionsDir, `${id}.decision.yaml`);
|
|
4099
4145
|
if (await pathExists(filePath)) {
|
|
4100
|
-
console.error(
|
|
4146
|
+
console.error(chalk8.red(`Error: Decision file already exists: ${filePath}`));
|
|
4101
4147
|
process.exit(1);
|
|
4102
4148
|
}
|
|
4103
4149
|
const decision = {
|
|
@@ -4133,13 +4179,13 @@ var createDecision = new Command7("create").description("Create a new decision f
|
|
|
4133
4179
|
}
|
|
4134
4180
|
};
|
|
4135
4181
|
await writeTextFile(filePath, stringifyYaml(decision));
|
|
4136
|
-
console.log(
|
|
4182
|
+
console.log(chalk8.green(`\u2713 Created decision: ${filePath}`));
|
|
4137
4183
|
console.log("");
|
|
4138
|
-
console.log(
|
|
4184
|
+
console.log(chalk8.cyan("Next steps:"));
|
|
4139
4185
|
console.log(` 1. Edit the file to add rationale, context, and consequences`);
|
|
4140
4186
|
console.log(` 2. Define constraints with appropriate scopes`);
|
|
4141
|
-
console.log(` 3. Run ${
|
|
4142
|
-
console.log(` 4. Change status from ${
|
|
4187
|
+
console.log(` 3. Run ${chalk8.bold("specbridge decision validate")} to check syntax`);
|
|
4188
|
+
console.log(` 4. Change status from ${chalk8.yellow("draft")} to ${chalk8.green("active")} when ready`);
|
|
4143
4189
|
});
|
|
4144
4190
|
|
|
4145
4191
|
// src/cli/commands/decision/index.ts
|
|
@@ -4147,7 +4193,7 @@ var decisionCommand = new Command8("decision").description("Manage architectural
|
|
|
4147
4193
|
|
|
4148
4194
|
// src/cli/commands/hook.ts
|
|
4149
4195
|
import { Command as Command9 } from "commander";
|
|
4150
|
-
import
|
|
4196
|
+
import chalk9 from "chalk";
|
|
4151
4197
|
import ora5 from "ora";
|
|
4152
4198
|
import { join as join8 } from "path";
|
|
4153
4199
|
var HOOK_SCRIPT = `#!/bin/sh
|
|
@@ -4179,9 +4225,9 @@ function createHookCommand() {
|
|
|
4179
4225
|
} else if (options.lefthook) {
|
|
4180
4226
|
spinner.succeed("Lefthook detected");
|
|
4181
4227
|
console.log("");
|
|
4182
|
-
console.log(
|
|
4228
|
+
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4183
4229
|
console.log("");
|
|
4184
|
-
console.log(
|
|
4230
|
+
console.log(chalk9.dim(`pre-commit:
|
|
4185
4231
|
commands:
|
|
4186
4232
|
specbridge:
|
|
4187
4233
|
glob: "*.{ts,tsx}"
|
|
@@ -4196,9 +4242,9 @@ function createHookCommand() {
|
|
|
4196
4242
|
} else if (await pathExists(join8(cwd, "lefthook.yml"))) {
|
|
4197
4243
|
spinner.succeed("Lefthook detected");
|
|
4198
4244
|
console.log("");
|
|
4199
|
-
console.log(
|
|
4245
|
+
console.log(chalk9.cyan("Add this to your lefthook.yml:"));
|
|
4200
4246
|
console.log("");
|
|
4201
|
-
console.log(
|
|
4247
|
+
console.log(chalk9.dim(`pre-commit:
|
|
4202
4248
|
commands:
|
|
4203
4249
|
specbridge:
|
|
4204
4250
|
glob: "*.{ts,tsx}"
|
|
@@ -4213,7 +4259,7 @@ function createHookCommand() {
|
|
|
4213
4259
|
}
|
|
4214
4260
|
if (await pathExists(hookPath) && !options.force) {
|
|
4215
4261
|
spinner.fail("Hook already exists");
|
|
4216
|
-
console.log(
|
|
4262
|
+
console.log(chalk9.yellow(`Use --force to overwrite: ${hookPath}`));
|
|
4217
4263
|
return;
|
|
4218
4264
|
}
|
|
4219
4265
|
await writeTextFile(hookPath, hookContent);
|
|
@@ -4223,9 +4269,9 @@ function createHookCommand() {
|
|
|
4223
4269
|
} catch {
|
|
4224
4270
|
}
|
|
4225
4271
|
spinner.succeed("Pre-commit hook installed");
|
|
4226
|
-
console.log(
|
|
4272
|
+
console.log(chalk9.dim(` Path: ${hookPath}`));
|
|
4227
4273
|
console.log("");
|
|
4228
|
-
console.log(
|
|
4274
|
+
console.log(chalk9.cyan("The hook will run on each commit and verify staged files."));
|
|
4229
4275
|
} catch (error) {
|
|
4230
4276
|
spinner.fail("Failed to install hook");
|
|
4231
4277
|
throw error;
|
|
@@ -4261,21 +4307,21 @@ function createHookCommand() {
|
|
|
4261
4307
|
cwd
|
|
4262
4308
|
});
|
|
4263
4309
|
if (result.violations.length === 0) {
|
|
4264
|
-
console.log(
|
|
4310
|
+
console.log(chalk9.green("\u2713 SpecBridge: All checks passed"));
|
|
4265
4311
|
process.exit(0);
|
|
4266
4312
|
}
|
|
4267
|
-
console.log(
|
|
4313
|
+
console.log(chalk9.red(`\u2717 SpecBridge: ${result.violations.length} violation(s) found`));
|
|
4268
4314
|
console.log("");
|
|
4269
4315
|
for (const v of result.violations) {
|
|
4270
4316
|
const location = v.line ? `:${v.line}` : "";
|
|
4271
4317
|
console.log(` ${v.file}${location}: ${v.message}`);
|
|
4272
|
-
console.log(
|
|
4318
|
+
console.log(chalk9.dim(` [${v.severity}] ${v.decisionId}/${v.constraintId}`));
|
|
4273
4319
|
}
|
|
4274
4320
|
console.log("");
|
|
4275
|
-
console.log(
|
|
4321
|
+
console.log(chalk9.yellow("Run `specbridge verify` for full details."));
|
|
4276
4322
|
process.exit(result.success ? 0 : 1);
|
|
4277
4323
|
} catch (error) {
|
|
4278
|
-
console.error(
|
|
4324
|
+
console.error(chalk9.red("SpecBridge verification failed"));
|
|
4279
4325
|
console.error(error instanceof Error ? error.message : String(error));
|
|
4280
4326
|
process.exit(1);
|
|
4281
4327
|
}
|
|
@@ -4314,7 +4360,7 @@ var hookCommand = createHookCommand();
|
|
|
4314
4360
|
|
|
4315
4361
|
// src/cli/commands/report.ts
|
|
4316
4362
|
import { Command as Command10 } from "commander";
|
|
4317
|
-
import
|
|
4363
|
+
import chalk11 from "chalk";
|
|
4318
4364
|
import ora6 from "ora";
|
|
4319
4365
|
import { join as join10 } from "path";
|
|
4320
4366
|
|
|
@@ -4398,54 +4444,54 @@ async function generateReport(config, options = {}) {
|
|
|
4398
4444
|
}
|
|
4399
4445
|
|
|
4400
4446
|
// src/reporting/formats/console.ts
|
|
4401
|
-
import
|
|
4447
|
+
import chalk10 from "chalk";
|
|
4402
4448
|
import { table as table2 } from "table";
|
|
4403
4449
|
function formatConsoleReport(report) {
|
|
4404
4450
|
const lines = [];
|
|
4405
4451
|
lines.push("");
|
|
4406
|
-
lines.push(
|
|
4407
|
-
lines.push(
|
|
4408
|
-
lines.push(
|
|
4452
|
+
lines.push(chalk10.bold.blue("SpecBridge Compliance Report"));
|
|
4453
|
+
lines.push(chalk10.dim(`Generated: ${new Date(report.timestamp).toLocaleString()}`));
|
|
4454
|
+
lines.push(chalk10.dim(`Project: ${report.project}`));
|
|
4409
4455
|
lines.push("");
|
|
4410
4456
|
const complianceColor = getComplianceColor(report.summary.compliance);
|
|
4411
|
-
lines.push(
|
|
4457
|
+
lines.push(chalk10.bold("Overall Compliance"));
|
|
4412
4458
|
lines.push(` ${complianceColor(formatComplianceBar(report.summary.compliance))} ${complianceColor(`${report.summary.compliance}%`)}`);
|
|
4413
4459
|
lines.push("");
|
|
4414
|
-
lines.push(
|
|
4460
|
+
lines.push(chalk10.bold("Summary"));
|
|
4415
4461
|
lines.push(` Decisions: ${report.summary.activeDecisions} active / ${report.summary.totalDecisions} total`);
|
|
4416
4462
|
lines.push(` Constraints: ${report.summary.totalConstraints}`);
|
|
4417
4463
|
lines.push("");
|
|
4418
|
-
lines.push(
|
|
4464
|
+
lines.push(chalk10.bold("Violations"));
|
|
4419
4465
|
const { violations } = report.summary;
|
|
4420
4466
|
const violationParts = [];
|
|
4421
4467
|
if (violations.critical > 0) {
|
|
4422
|
-
violationParts.push(
|
|
4468
|
+
violationParts.push(chalk10.red(`${violations.critical} critical`));
|
|
4423
4469
|
}
|
|
4424
4470
|
if (violations.high > 0) {
|
|
4425
|
-
violationParts.push(
|
|
4471
|
+
violationParts.push(chalk10.yellow(`${violations.high} high`));
|
|
4426
4472
|
}
|
|
4427
4473
|
if (violations.medium > 0) {
|
|
4428
|
-
violationParts.push(
|
|
4474
|
+
violationParts.push(chalk10.cyan(`${violations.medium} medium`));
|
|
4429
4475
|
}
|
|
4430
4476
|
if (violations.low > 0) {
|
|
4431
|
-
violationParts.push(
|
|
4477
|
+
violationParts.push(chalk10.dim(`${violations.low} low`));
|
|
4432
4478
|
}
|
|
4433
4479
|
if (violationParts.length > 0) {
|
|
4434
4480
|
lines.push(` ${violationParts.join(" | ")}`);
|
|
4435
4481
|
} else {
|
|
4436
|
-
lines.push(
|
|
4482
|
+
lines.push(chalk10.green(" No violations"));
|
|
4437
4483
|
}
|
|
4438
4484
|
lines.push("");
|
|
4439
4485
|
if (report.byDecision.length > 0) {
|
|
4440
|
-
lines.push(
|
|
4486
|
+
lines.push(chalk10.bold("By Decision"));
|
|
4441
4487
|
lines.push("");
|
|
4442
4488
|
const tableData = [
|
|
4443
4489
|
[
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4490
|
+
chalk10.bold("Decision"),
|
|
4491
|
+
chalk10.bold("Status"),
|
|
4492
|
+
chalk10.bold("Constraints"),
|
|
4493
|
+
chalk10.bold("Violations"),
|
|
4494
|
+
chalk10.bold("Compliance")
|
|
4449
4495
|
]
|
|
4450
4496
|
];
|
|
4451
4497
|
for (const dec of report.byDecision) {
|
|
@@ -4455,7 +4501,7 @@ function formatConsoleReport(report) {
|
|
|
4455
4501
|
truncate2(dec.title, 40),
|
|
4456
4502
|
statusColor(dec.status),
|
|
4457
4503
|
String(dec.constraints),
|
|
4458
|
-
dec.violations > 0 ?
|
|
4504
|
+
dec.violations > 0 ? chalk10.red(String(dec.violations)) : chalk10.green("0"),
|
|
4459
4505
|
compColor(`${dec.compliance}%`)
|
|
4460
4506
|
]);
|
|
4461
4507
|
}
|
|
@@ -4489,23 +4535,23 @@ function formatComplianceBar(compliance) {
|
|
|
4489
4535
|
return "\u2588".repeat(filled) + "\u2591".repeat(empty);
|
|
4490
4536
|
}
|
|
4491
4537
|
function getComplianceColor(compliance) {
|
|
4492
|
-
if (compliance >= 90) return
|
|
4493
|
-
if (compliance >= 70) return
|
|
4494
|
-
if (compliance >= 50) return
|
|
4495
|
-
return
|
|
4538
|
+
if (compliance >= 90) return chalk10.green;
|
|
4539
|
+
if (compliance >= 70) return chalk10.yellow;
|
|
4540
|
+
if (compliance >= 50) return chalk10.hex("#FFA500");
|
|
4541
|
+
return chalk10.red;
|
|
4496
4542
|
}
|
|
4497
4543
|
function getStatusColor2(status) {
|
|
4498
4544
|
switch (status) {
|
|
4499
4545
|
case "active":
|
|
4500
|
-
return
|
|
4546
|
+
return chalk10.green;
|
|
4501
4547
|
case "draft":
|
|
4502
|
-
return
|
|
4548
|
+
return chalk10.yellow;
|
|
4503
4549
|
case "deprecated":
|
|
4504
|
-
return
|
|
4550
|
+
return chalk10.gray;
|
|
4505
4551
|
case "superseded":
|
|
4506
|
-
return
|
|
4552
|
+
return chalk10.blue;
|
|
4507
4553
|
default:
|
|
4508
|
-
return
|
|
4554
|
+
return chalk10.white;
|
|
4509
4555
|
}
|
|
4510
4556
|
}
|
|
4511
4557
|
function truncate2(str, length) {
|
|
@@ -4579,6 +4625,7 @@ function formatProgressBar(percentage) {
|
|
|
4579
4625
|
import { join as join9 } from "path";
|
|
4580
4626
|
var ReportStorage = class {
|
|
4581
4627
|
storageDir;
|
|
4628
|
+
logger = getLogger({ module: "reporting.storage" });
|
|
4582
4629
|
constructor(basePath) {
|
|
4583
4630
|
this.storageDir = join9(getSpecBridgeDir(basePath), "reports", "history");
|
|
4584
4631
|
}
|
|
@@ -4637,7 +4684,7 @@ var ReportStorage = class {
|
|
|
4637
4684
|
const timestamp = file.replace("report-", "").replace(".json", "");
|
|
4638
4685
|
return { timestamp, report };
|
|
4639
4686
|
} catch (error) {
|
|
4640
|
-
|
|
4687
|
+
this.logger.warn({ file, error }, "Failed to load report file");
|
|
4641
4688
|
return null;
|
|
4642
4689
|
}
|
|
4643
4690
|
});
|
|
@@ -4681,7 +4728,7 @@ var ReportStorage = class {
|
|
|
4681
4728
|
const fs = await import("fs/promises");
|
|
4682
4729
|
await fs.unlink(filepath);
|
|
4683
4730
|
} catch (error) {
|
|
4684
|
-
|
|
4731
|
+
this.logger.warn({ file, error }, "Failed to delete old report file");
|
|
4685
4732
|
}
|
|
4686
4733
|
}
|
|
4687
4734
|
return filesToDelete.length;
|
|
@@ -4805,7 +4852,10 @@ async function analyzeTrend(reports) {
|
|
|
4805
4852
|
if (!decisionMap.has(decision.decisionId)) {
|
|
4806
4853
|
decisionMap.set(decision.decisionId, []);
|
|
4807
4854
|
}
|
|
4808
|
-
decisionMap.get(decision.decisionId)
|
|
4855
|
+
const decisionHistory = decisionMap.get(decision.decisionId);
|
|
4856
|
+
if (decisionHistory) {
|
|
4857
|
+
decisionHistory.push(decision);
|
|
4858
|
+
}
|
|
4809
4859
|
}
|
|
4810
4860
|
}
|
|
4811
4861
|
const decisions = Array.from(decisionMap.entries()).map(([decisionId, data]) => {
|
|
@@ -4880,29 +4930,29 @@ var reportCommand = new Command10("report").description("Generate compliance rep
|
|
|
4880
4930
|
const storage = new ReportStorage(cwd);
|
|
4881
4931
|
await storage.save(report);
|
|
4882
4932
|
if (options.trend) {
|
|
4883
|
-
console.log("\n" +
|
|
4933
|
+
console.log("\n" + chalk11.blue.bold("=== Compliance Trend Analysis ===\n"));
|
|
4884
4934
|
const days = parseInt(options.days || "30", 10);
|
|
4885
4935
|
const history = await storage.loadHistory(days);
|
|
4886
4936
|
if (history.length < 2) {
|
|
4887
|
-
console.log(
|
|
4937
|
+
console.log(chalk11.yellow(`Not enough data for trend analysis. Found ${history.length} report(s), need at least 2.`));
|
|
4888
4938
|
} else {
|
|
4889
4939
|
const trend = await analyzeTrend(history);
|
|
4890
|
-
console.log(
|
|
4940
|
+
console.log(chalk11.bold(`Period: ${trend.period.start} to ${trend.period.end} (${trend.period.days} days)`));
|
|
4891
4941
|
console.log(`
|
|
4892
4942
|
Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.endCompliance}% (${trend.overall.change > 0 ? "+" : ""}${trend.overall.change.toFixed(1)}%)`);
|
|
4893
4943
|
const trendEmoji = trend.overall.trend === "improving" ? "\u{1F4C8}" : trend.overall.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4894
|
-
const trendColor = trend.overall.trend === "improving" ?
|
|
4944
|
+
const trendColor = trend.overall.trend === "improving" ? chalk11.green : trend.overall.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4895
4945
|
console.log(trendColor(`${trendEmoji} Trend: ${trend.overall.trend.toUpperCase()}`));
|
|
4896
4946
|
const degrading = trend.decisions.filter((d) => d.trend === "degrading").slice(0, 3);
|
|
4897
4947
|
if (degrading.length > 0) {
|
|
4898
|
-
console.log(
|
|
4948
|
+
console.log(chalk11.red("\n\u26A0\uFE0F Most Degraded Decisions:"));
|
|
4899
4949
|
degrading.forEach((d) => {
|
|
4900
4950
|
console.log(` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (${d.change.toFixed(1)}%)`);
|
|
4901
4951
|
});
|
|
4902
4952
|
}
|
|
4903
4953
|
const improving = trend.decisions.filter((d) => d.trend === "improving").slice(0, 3);
|
|
4904
4954
|
if (improving.length > 0) {
|
|
4905
|
-
console.log(
|
|
4955
|
+
console.log(chalk11.green("\n\u2705 Most Improved Decisions:"));
|
|
4906
4956
|
improving.forEach((d) => {
|
|
4907
4957
|
console.log(` \u2022 ${d.title}: ${d.startCompliance}% \u2192 ${d.endCompliance}% (+${d.change.toFixed(1)}%)`);
|
|
4908
4958
|
});
|
|
@@ -4911,26 +4961,26 @@ Overall Compliance: ${trend.overall.startCompliance}% \u2192 ${trend.overall.end
|
|
|
4911
4961
|
console.log("");
|
|
4912
4962
|
}
|
|
4913
4963
|
if (options.drift) {
|
|
4914
|
-
console.log("\n" +
|
|
4964
|
+
console.log("\n" + chalk11.blue.bold("=== Drift Analysis ===\n"));
|
|
4915
4965
|
const history = await storage.loadHistory(2);
|
|
4916
4966
|
if (history.length < 2) {
|
|
4917
|
-
console.log(
|
|
4967
|
+
console.log(chalk11.yellow("Not enough data for drift analysis. Need at least 2 reports."));
|
|
4918
4968
|
} else {
|
|
4919
4969
|
const currentEntry = history[0];
|
|
4920
4970
|
const previousEntry = history[1];
|
|
4921
4971
|
if (!currentEntry || !previousEntry) {
|
|
4922
|
-
console.log(
|
|
4972
|
+
console.log(chalk11.yellow("Invalid history data."));
|
|
4923
4973
|
return;
|
|
4924
4974
|
}
|
|
4925
4975
|
const drift = await detectDrift(currentEntry.report, previousEntry.report);
|
|
4926
|
-
console.log(
|
|
4976
|
+
console.log(chalk11.bold(`Comparing: ${previousEntry.timestamp} vs ${currentEntry.timestamp}`));
|
|
4927
4977
|
console.log(`
|
|
4928
4978
|
Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceChange.toFixed(1)}%`);
|
|
4929
4979
|
const driftEmoji = drift.trend === "improving" ? "\u{1F4C8}" : drift.trend === "degrading" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
4930
|
-
const driftColor = drift.trend === "improving" ?
|
|
4980
|
+
const driftColor = drift.trend === "improving" ? chalk11.green : drift.trend === "degrading" ? chalk11.red : chalk11.yellow;
|
|
4931
4981
|
console.log(driftColor(`${driftEmoji} Overall Trend: ${drift.trend.toUpperCase()}`));
|
|
4932
4982
|
if (drift.summary.newViolations.total > 0) {
|
|
4933
|
-
console.log(
|
|
4983
|
+
console.log(chalk11.red(`
|
|
4934
4984
|
\u26A0\uFE0F New Violations: ${drift.summary.newViolations.total}`));
|
|
4935
4985
|
if (drift.summary.newViolations.critical > 0) {
|
|
4936
4986
|
console.log(` \u2022 Critical: ${drift.summary.newViolations.critical}`);
|
|
@@ -4946,7 +4996,7 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4946
4996
|
}
|
|
4947
4997
|
}
|
|
4948
4998
|
if (drift.summary.fixedViolations.total > 0) {
|
|
4949
|
-
console.log(
|
|
4999
|
+
console.log(chalk11.green(`
|
|
4950
5000
|
\u2705 Fixed Violations: ${drift.summary.fixedViolations.total}`));
|
|
4951
5001
|
if (drift.summary.fixedViolations.critical > 0) {
|
|
4952
5002
|
console.log(` \u2022 Critical: ${drift.summary.fixedViolations.critical}`);
|
|
@@ -4962,7 +5012,7 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4962
5012
|
}
|
|
4963
5013
|
}
|
|
4964
5014
|
if (drift.mostDegraded.length > 0) {
|
|
4965
|
-
console.log(
|
|
5015
|
+
console.log(chalk11.red("\n\u{1F4C9} Most Degraded:"));
|
|
4966
5016
|
drift.mostDegraded.forEach((d) => {
|
|
4967
5017
|
console.log(` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (${d.complianceChange.toFixed(1)}%)`);
|
|
4968
5018
|
if (d.newViolations > 0) {
|
|
@@ -4971,7 +5021,7 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
4971
5021
|
});
|
|
4972
5022
|
}
|
|
4973
5023
|
if (drift.mostImproved.length > 0) {
|
|
4974
|
-
console.log(
|
|
5024
|
+
console.log(chalk11.green("\n\u{1F4C8} Most Improved:"));
|
|
4975
5025
|
drift.mostImproved.forEach((d) => {
|
|
4976
5026
|
console.log(` \u2022 ${d.title}: ${d.previousCompliance}% \u2192 ${d.currentCompliance}% (+${d.complianceChange.toFixed(1)}%)`);
|
|
4977
5027
|
if (d.fixedViolations > 0) {
|
|
@@ -5011,7 +5061,7 @@ Compliance Change: ${drift.complianceChange > 0 ? "+" : ""}${drift.complianceCha
|
|
|
5011
5061
|
`health-${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${extension}`
|
|
5012
5062
|
);
|
|
5013
5063
|
await writeTextFile(outputPath, output);
|
|
5014
|
-
console.log(
|
|
5064
|
+
console.log(chalk11.green(`
|
|
5015
5065
|
Report saved to: ${outputPath}`));
|
|
5016
5066
|
if (options.save && !options.output) {
|
|
5017
5067
|
const latestPath = join10(getReportsDir(cwd), `health-latest.${extension}`);
|
|
@@ -5026,7 +5076,7 @@ Report saved to: ${outputPath}`));
|
|
|
5026
5076
|
|
|
5027
5077
|
// src/cli/commands/context.ts
|
|
5028
5078
|
import { Command as Command11 } from "commander";
|
|
5029
|
-
import
|
|
5079
|
+
import chalk12 from "chalk";
|
|
5030
5080
|
|
|
5031
5081
|
// src/agent/context.generator.ts
|
|
5032
5082
|
async function generateContext(filePath, config, options = {}) {
|
|
@@ -5146,12 +5196,12 @@ var contextCommand = new Command11("context").description("Generate architectura
|
|
|
5146
5196
|
});
|
|
5147
5197
|
if (options.output) {
|
|
5148
5198
|
await writeTextFile(options.output, output);
|
|
5149
|
-
console.log(
|
|
5199
|
+
console.log(chalk12.green(`Context saved to: ${options.output}`));
|
|
5150
5200
|
} else {
|
|
5151
5201
|
console.log(output);
|
|
5152
5202
|
}
|
|
5153
5203
|
} catch (error) {
|
|
5154
|
-
console.error(
|
|
5204
|
+
console.error(chalk12.red("Failed to generate context"));
|
|
5155
5205
|
throw error;
|
|
5156
5206
|
}
|
|
5157
5207
|
});
|
|
@@ -5165,7 +5215,7 @@ import { TextDocument } from "vscode-languageserver-textdocument";
|
|
|
5165
5215
|
import { fileURLToPath } from "url";
|
|
5166
5216
|
import path3 from "path";
|
|
5167
5217
|
import { Project as Project3 } from "ts-morph";
|
|
5168
|
-
import
|
|
5218
|
+
import chalk13 from "chalk";
|
|
5169
5219
|
function severityToDiagnostic(severity) {
|
|
5170
5220
|
switch (severity) {
|
|
5171
5221
|
case "critical":
|
|
@@ -5247,7 +5297,11 @@ var SpecBridgeLspServer = class {
|
|
|
5247
5297
|
const doc = this.documents.get(params.textDocument.uri);
|
|
5248
5298
|
if (!doc) return [];
|
|
5249
5299
|
return violations.filter((v) => v.autofix && v.autofix.edits.length > 0).map((v) => {
|
|
5250
|
-
const
|
|
5300
|
+
const autofix = v.autofix;
|
|
5301
|
+
if (!autofix) {
|
|
5302
|
+
return null;
|
|
5303
|
+
}
|
|
5304
|
+
const edits = autofix.edits.map((edit) => ({
|
|
5251
5305
|
range: {
|
|
5252
5306
|
start: doc.positionAt(edit.start),
|
|
5253
5307
|
end: doc.positionAt(edit.end)
|
|
@@ -5255,7 +5309,7 @@ var SpecBridgeLspServer = class {
|
|
|
5255
5309
|
newText: edit.text
|
|
5256
5310
|
}));
|
|
5257
5311
|
return {
|
|
5258
|
-
title:
|
|
5312
|
+
title: autofix.description,
|
|
5259
5313
|
kind: CodeActionKind.QuickFix,
|
|
5260
5314
|
edit: {
|
|
5261
5315
|
changes: {
|
|
@@ -5263,7 +5317,7 @@ var SpecBridgeLspServer = class {
|
|
|
5263
5317
|
}
|
|
5264
5318
|
}
|
|
5265
5319
|
};
|
|
5266
|
-
});
|
|
5320
|
+
}).filter((action) => action !== null);
|
|
5267
5321
|
});
|
|
5268
5322
|
this.documents.listen(this.connection);
|
|
5269
5323
|
this.connection.listen();
|
|
@@ -5272,7 +5326,7 @@ var SpecBridgeLspServer = class {
|
|
|
5272
5326
|
if (!await pathExists(getSpecBridgeDir(this.cwd))) {
|
|
5273
5327
|
const err = new NotInitializedError();
|
|
5274
5328
|
this.initError = err.message;
|
|
5275
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5329
|
+
if (this.options.verbose) this.connection.console.error(chalk13.red(this.initError));
|
|
5276
5330
|
return;
|
|
5277
5331
|
}
|
|
5278
5332
|
try {
|
|
@@ -5281,7 +5335,7 @@ var SpecBridgeLspServer = class {
|
|
|
5281
5335
|
await getPluginLoader().loadPlugins(this.cwd);
|
|
5282
5336
|
} catch (error) {
|
|
5283
5337
|
const msg = error instanceof Error ? error.message : String(error);
|
|
5284
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5338
|
+
if (this.options.verbose) this.connection.console.error(chalk13.red(`Plugin load failed: ${msg}`));
|
|
5285
5339
|
}
|
|
5286
5340
|
this.registry = createRegistry({ basePath: this.cwd });
|
|
5287
5341
|
await this.registry.load();
|
|
@@ -5294,11 +5348,11 @@ var SpecBridgeLspServer = class {
|
|
|
5294
5348
|
}
|
|
5295
5349
|
}
|
|
5296
5350
|
if (this.options.verbose) {
|
|
5297
|
-
this.connection.console.log(
|
|
5351
|
+
this.connection.console.log(chalk13.dim(`Loaded ${this.decisions.length} active decision(s)`));
|
|
5298
5352
|
}
|
|
5299
5353
|
} catch (error) {
|
|
5300
5354
|
this.initError = error instanceof Error ? error.message : String(error);
|
|
5301
|
-
if (this.options.verbose) this.connection.console.error(
|
|
5355
|
+
if (this.options.verbose) this.connection.console.error(chalk13.red(this.initError));
|
|
5302
5356
|
}
|
|
5303
5357
|
}
|
|
5304
5358
|
async verifyTextDocument(doc) {
|
|
@@ -5370,7 +5424,7 @@ var lspCommand = new Command12("lsp").description("Start SpecBridge language ser
|
|
|
5370
5424
|
|
|
5371
5425
|
// src/cli/commands/watch.ts
|
|
5372
5426
|
import { Command as Command13 } from "commander";
|
|
5373
|
-
import
|
|
5427
|
+
import chalk14 from "chalk";
|
|
5374
5428
|
import chokidar from "chokidar";
|
|
5375
5429
|
import path4 from "path";
|
|
5376
5430
|
var watchCommand = new Command13("watch").description("Watch for changes and verify files continuously").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("--debounce <ms>", "Debounce verify on rapid changes", "150").action(async (options) => {
|
|
@@ -5391,15 +5445,15 @@ var watchCommand = new Command13("watch").description("Watch for changes and ver
|
|
|
5391
5445
|
files: [absolutePath],
|
|
5392
5446
|
cwd
|
|
5393
5447
|
});
|
|
5394
|
-
const prefix = result.success ?
|
|
5448
|
+
const prefix = result.success ? chalk14.green("\u2713") : chalk14.red("\u2717");
|
|
5395
5449
|
const summary = `${prefix} ${path4.relative(cwd, absolutePath)}: ${result.violations.length} violation(s)`;
|
|
5396
5450
|
console.log(summary);
|
|
5397
5451
|
for (const v of result.violations.slice(0, 20)) {
|
|
5398
5452
|
const loc = v.line ? `:${v.line}${v.column ? `:${v.column}` : ""}` : "";
|
|
5399
|
-
console.log(
|
|
5453
|
+
console.log(chalk14.dim(` - ${v.file}${loc}: ${v.message} [${v.severity}]`));
|
|
5400
5454
|
}
|
|
5401
5455
|
if (result.violations.length > 20) {
|
|
5402
|
-
console.log(
|
|
5456
|
+
console.log(chalk14.dim(` \u2026 ${result.violations.length - 20} more`));
|
|
5403
5457
|
}
|
|
5404
5458
|
};
|
|
5405
5459
|
const watcher = chokidar.watch(config.project.sourceRoots, {
|
|
@@ -5408,7 +5462,7 @@ var watchCommand = new Command13("watch").description("Watch for changes and ver
|
|
|
5408
5462
|
ignoreInitial: true,
|
|
5409
5463
|
persistent: true
|
|
5410
5464
|
});
|
|
5411
|
-
console.log(
|
|
5465
|
+
console.log(chalk14.blue("Watching for changes..."));
|
|
5412
5466
|
watcher.on("change", (changedPath) => {
|
|
5413
5467
|
pendingPath = changedPath;
|
|
5414
5468
|
if (timer) clearTimeout(timer);
|
|
@@ -5700,7 +5754,7 @@ var promptCommand = new Command15("prompt").description("Generate AI agent promp
|
|
|
5700
5754
|
|
|
5701
5755
|
// src/cli/commands/analytics.ts
|
|
5702
5756
|
import { Command as Command16 } from "commander";
|
|
5703
|
-
import
|
|
5757
|
+
import chalk15 from "chalk";
|
|
5704
5758
|
import ora7 from "ora";
|
|
5705
5759
|
|
|
5706
5760
|
// src/analytics/engine.ts
|
|
@@ -5929,7 +5983,7 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5929
5983
|
const history = await storage.loadHistory(days);
|
|
5930
5984
|
if (history.length === 0) {
|
|
5931
5985
|
spinner.fail("No historical reports found");
|
|
5932
|
-
console.log(
|
|
5986
|
+
console.log(chalk15.yellow("\nGenerate reports with: specbridge report"));
|
|
5933
5987
|
return;
|
|
5934
5988
|
}
|
|
5935
5989
|
spinner.succeed(`Loaded ${history.length} historical report(s)`);
|
|
@@ -5946,17 +6000,17 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5946
6000
|
}
|
|
5947
6001
|
if (decisionId) {
|
|
5948
6002
|
const metrics = await engine.analyzeDecision(decisionId, history);
|
|
5949
|
-
console.log("\n" +
|
|
6003
|
+
console.log("\n" + chalk15.blue.bold(`=== Decision Analytics: ${metrics.title} ===
|
|
5950
6004
|
`));
|
|
5951
|
-
console.log(
|
|
6005
|
+
console.log(chalk15.bold("Overview:"));
|
|
5952
6006
|
console.log(` ID: ${metrics.decisionId}`);
|
|
5953
6007
|
console.log(` Current Violations: ${metrics.totalViolations}`);
|
|
5954
6008
|
console.log(` Average Compliance: ${metrics.averageComplianceScore.toFixed(1)}%`);
|
|
5955
6009
|
const trendEmoji = metrics.trendDirection === "up" ? "\u{1F4C8}" : metrics.trendDirection === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
5956
|
-
const trendColor = metrics.trendDirection === "up" ?
|
|
6010
|
+
const trendColor = metrics.trendDirection === "up" ? chalk15.green : metrics.trendDirection === "down" ? chalk15.red : chalk15.yellow;
|
|
5957
6011
|
console.log(` ${trendColor(`${trendEmoji} Trend: ${metrics.trendDirection.toUpperCase()}`)}`);
|
|
5958
6012
|
if (metrics.history.length > 0) {
|
|
5959
|
-
console.log(
|
|
6013
|
+
console.log(chalk15.bold("\nCompliance History:"));
|
|
5960
6014
|
const recentHistory = metrics.history.slice(-10);
|
|
5961
6015
|
recentHistory.forEach((h) => {
|
|
5962
6016
|
const icon = h.violations === 0 ? "\u2705" : "\u26A0\uFE0F";
|
|
@@ -5965,58 +6019,58 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
5965
6019
|
}
|
|
5966
6020
|
} else {
|
|
5967
6021
|
const summary = await engine.generateSummary(history);
|
|
5968
|
-
console.log("\n" +
|
|
5969
|
-
console.log(
|
|
6022
|
+
console.log("\n" + chalk15.blue.bold("=== Overall Analytics ===\n"));
|
|
6023
|
+
console.log(chalk15.bold("Summary:"));
|
|
5970
6024
|
console.log(` Total Decisions: ${summary.totalDecisions}`);
|
|
5971
6025
|
console.log(` Average Compliance: ${summary.averageCompliance}%`);
|
|
5972
6026
|
console.log(` Critical Issues: ${summary.criticalIssues}`);
|
|
5973
6027
|
const trendEmoji = summary.overallTrend === "up" ? "\u{1F4C8}" : summary.overallTrend === "down" ? "\u{1F4C9}" : "\u27A1\uFE0F";
|
|
5974
|
-
const trendColor = summary.overallTrend === "up" ?
|
|
6028
|
+
const trendColor = summary.overallTrend === "up" ? chalk15.green : summary.overallTrend === "down" ? chalk15.red : chalk15.yellow;
|
|
5975
6029
|
console.log(` ${trendColor(`${trendEmoji} Overall Trend: ${summary.overallTrend.toUpperCase()}`)}`);
|
|
5976
6030
|
if (summary.topDecisions.length > 0) {
|
|
5977
|
-
console.log(
|
|
6031
|
+
console.log(chalk15.green("\n\u2705 Top Performing Decisions:"));
|
|
5978
6032
|
summary.topDecisions.forEach((d, i) => {
|
|
5979
6033
|
console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
|
|
5980
6034
|
});
|
|
5981
6035
|
}
|
|
5982
6036
|
if (summary.bottomDecisions.length > 0) {
|
|
5983
|
-
console.log(
|
|
6037
|
+
console.log(chalk15.red("\n\u26A0\uFE0F Decisions Needing Attention:"));
|
|
5984
6038
|
summary.bottomDecisions.forEach((d, i) => {
|
|
5985
6039
|
console.log(` ${i + 1}. ${d.title}: ${d.compliance}%`);
|
|
5986
6040
|
});
|
|
5987
6041
|
}
|
|
5988
6042
|
if (options.insights || summary.criticalIssues > 0) {
|
|
5989
|
-
console.log(
|
|
6043
|
+
console.log(chalk15.blue.bold("\n=== Insights ===\n"));
|
|
5990
6044
|
const insights = summary.insights;
|
|
5991
6045
|
const warnings = insights.filter((i) => i.type === "warning");
|
|
5992
6046
|
const successes = insights.filter((i) => i.type === "success");
|
|
5993
6047
|
const infos = insights.filter((i) => i.type === "info");
|
|
5994
6048
|
if (warnings.length > 0) {
|
|
5995
|
-
console.log(
|
|
6049
|
+
console.log(chalk15.red("\u26A0\uFE0F Warnings:"));
|
|
5996
6050
|
warnings.forEach((i) => {
|
|
5997
6051
|
console.log(` \u2022 ${i.message}`);
|
|
5998
6052
|
if (i.details) {
|
|
5999
|
-
console.log(
|
|
6053
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6000
6054
|
}
|
|
6001
6055
|
});
|
|
6002
6056
|
console.log("");
|
|
6003
6057
|
}
|
|
6004
6058
|
if (successes.length > 0) {
|
|
6005
|
-
console.log(
|
|
6059
|
+
console.log(chalk15.green("\u2705 Positive Trends:"));
|
|
6006
6060
|
successes.forEach((i) => {
|
|
6007
6061
|
console.log(` \u2022 ${i.message}`);
|
|
6008
6062
|
if (i.details) {
|
|
6009
|
-
console.log(
|
|
6063
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6010
6064
|
}
|
|
6011
6065
|
});
|
|
6012
6066
|
console.log("");
|
|
6013
6067
|
}
|
|
6014
6068
|
if (infos.length > 0) {
|
|
6015
|
-
console.log(
|
|
6069
|
+
console.log(chalk15.blue("\u{1F4A1} Suggestions:"));
|
|
6016
6070
|
infos.forEach((i) => {
|
|
6017
6071
|
console.log(` \u2022 ${i.message}`);
|
|
6018
6072
|
if (i.details) {
|
|
6019
|
-
console.log(
|
|
6073
|
+
console.log(chalk15.gray(` ${i.details}`));
|
|
6020
6074
|
}
|
|
6021
6075
|
});
|
|
6022
6076
|
console.log("");
|
|
@@ -6026,10 +6080,10 @@ var analyticsCommand = new Command16("analytics").description("Analyze complianc
|
|
|
6026
6080
|
const latestEntry = history[history.length - 1];
|
|
6027
6081
|
const oldestEntry = history[0];
|
|
6028
6082
|
if (latestEntry && oldestEntry) {
|
|
6029
|
-
console.log(
|
|
6083
|
+
console.log(chalk15.gray(`
|
|
6030
6084
|
Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`));
|
|
6031
6085
|
}
|
|
6032
|
-
console.log(
|
|
6086
|
+
console.log(chalk15.gray(`Analyzing ${history.length} report(s) over ${days} days
|
|
6033
6087
|
`));
|
|
6034
6088
|
} catch (error) {
|
|
6035
6089
|
spinner.fail("Analytics failed");
|
|
@@ -6039,7 +6093,7 @@ Data range: ${latestEntry.timestamp} to ${oldestEntry.timestamp}`));
|
|
|
6039
6093
|
|
|
6040
6094
|
// src/cli/commands/dashboard.ts
|
|
6041
6095
|
import { Command as Command17 } from "commander";
|
|
6042
|
-
import
|
|
6096
|
+
import chalk16 from "chalk";
|
|
6043
6097
|
|
|
6044
6098
|
// src/dashboard/server.ts
|
|
6045
6099
|
import express from "express";
|
|
@@ -6058,6 +6112,7 @@ var DashboardServer = class {
|
|
|
6058
6112
|
CACHE_TTL = 6e4;
|
|
6059
6113
|
// 1 minute
|
|
6060
6114
|
refreshInterval = null;
|
|
6115
|
+
logger = getLogger({ module: "dashboard.server" });
|
|
6061
6116
|
constructor(options) {
|
|
6062
6117
|
this.cwd = options.cwd;
|
|
6063
6118
|
this.config = options.config;
|
|
@@ -6074,7 +6129,11 @@ var DashboardServer = class {
|
|
|
6074
6129
|
await this.registry.load();
|
|
6075
6130
|
await this.refreshCache();
|
|
6076
6131
|
this.refreshInterval = setInterval(
|
|
6077
|
-
() =>
|
|
6132
|
+
() => {
|
|
6133
|
+
void this.refreshCache().catch((error) => {
|
|
6134
|
+
this.logger.error({ error }, "Background cache refresh failed");
|
|
6135
|
+
});
|
|
6136
|
+
},
|
|
6078
6137
|
this.CACHE_TTL
|
|
6079
6138
|
);
|
|
6080
6139
|
}
|
|
@@ -6097,7 +6156,7 @@ var DashboardServer = class {
|
|
|
6097
6156
|
this.cacheTimestamp = Date.now();
|
|
6098
6157
|
await this.reportStorage.save(report);
|
|
6099
6158
|
} catch (error) {
|
|
6100
|
-
|
|
6159
|
+
this.logger.error({ error }, "Cache refresh failed");
|
|
6101
6160
|
if (!this.cachedReport) {
|
|
6102
6161
|
try {
|
|
6103
6162
|
const stored = await this.reportStorage.loadLatest();
|
|
@@ -6105,7 +6164,7 @@ var DashboardServer = class {
|
|
|
6105
6164
|
this.cachedReport = stored.report;
|
|
6106
6165
|
}
|
|
6107
6166
|
} catch (fallbackError) {
|
|
6108
|
-
|
|
6167
|
+
this.logger.error({ error: fallbackError }, "Failed to load fallback report");
|
|
6109
6168
|
}
|
|
6110
6169
|
}
|
|
6111
6170
|
}
|
|
@@ -6178,7 +6237,8 @@ var DashboardServer = class {
|
|
|
6178
6237
|
});
|
|
6179
6238
|
this.app.get("/api/report/:date", async (req, res) => {
|
|
6180
6239
|
try {
|
|
6181
|
-
const
|
|
6240
|
+
const dateParam = req.params.date;
|
|
6241
|
+
const date = Array.isArray(dateParam) ? dateParam[0] : dateParam;
|
|
6182
6242
|
if (!date) {
|
|
6183
6243
|
res.status(400).json({ error: "Date parameter required" });
|
|
6184
6244
|
return;
|
|
@@ -6214,7 +6274,8 @@ var DashboardServer = class {
|
|
|
6214
6274
|
});
|
|
6215
6275
|
this.app.get("/api/decisions/:id", async (req, res) => {
|
|
6216
6276
|
try {
|
|
6217
|
-
const
|
|
6277
|
+
const idParam = req.params.id;
|
|
6278
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
6218
6279
|
if (!id) {
|
|
6219
6280
|
res.status(400).json({ error: "Decision ID required" });
|
|
6220
6281
|
return;
|
|
@@ -6226,6 +6287,10 @@ var DashboardServer = class {
|
|
|
6226
6287
|
}
|
|
6227
6288
|
res.json(decision);
|
|
6228
6289
|
} catch (error) {
|
|
6290
|
+
if (error instanceof DecisionNotFoundError) {
|
|
6291
|
+
res.status(404).json({ error: "Decision not found" });
|
|
6292
|
+
return;
|
|
6293
|
+
}
|
|
6229
6294
|
res.status(500).json({
|
|
6230
6295
|
error: "Failed to load decision",
|
|
6231
6296
|
message: error instanceof Error ? error.message : "Unknown error"
|
|
@@ -6257,7 +6322,8 @@ var DashboardServer = class {
|
|
|
6257
6322
|
});
|
|
6258
6323
|
this.app.get("/api/analytics/decision/:id", async (req, res) => {
|
|
6259
6324
|
try {
|
|
6260
|
-
const
|
|
6325
|
+
const idParam = req.params.id;
|
|
6326
|
+
const id = Array.isArray(idParam) ? idParam[0] : idParam;
|
|
6261
6327
|
if (!id) {
|
|
6262
6328
|
res.status(400).json({ error: "Decision ID required" });
|
|
6263
6329
|
return;
|
|
@@ -6346,7 +6412,7 @@ var DashboardServer = class {
|
|
|
6346
6412
|
// Cache static assets
|
|
6347
6413
|
etag: true
|
|
6348
6414
|
}));
|
|
6349
|
-
this.app.get("*", (_req, res) => {
|
|
6415
|
+
this.app.get("/{*path}", (_req, res) => {
|
|
6350
6416
|
res.sendFile(join12(publicDir, "index.html"));
|
|
6351
6417
|
});
|
|
6352
6418
|
}
|
|
@@ -6361,7 +6427,7 @@ var dashboardCommand = new Command17("dashboard").description("Start compliance
|
|
|
6361
6427
|
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
6362
6428
|
throw new NotInitializedError();
|
|
6363
6429
|
}
|
|
6364
|
-
console.log(
|
|
6430
|
+
console.log(chalk16.blue("Starting SpecBridge dashboard..."));
|
|
6365
6431
|
try {
|
|
6366
6432
|
const config = await loadConfig(cwd);
|
|
6367
6433
|
const server = createDashboardServer({ cwd, config });
|
|
@@ -6369,36 +6435,37 @@ var dashboardCommand = new Command17("dashboard").description("Start compliance
|
|
|
6369
6435
|
const port = parseInt(options.port || "3000", 10);
|
|
6370
6436
|
const host = options.host || "localhost";
|
|
6371
6437
|
server.getApp().listen(port, host, () => {
|
|
6372
|
-
console.log(
|
|
6438
|
+
console.log(chalk16.green(`
|
|
6373
6439
|
\u2713 Dashboard running at http://${host}:${port}`));
|
|
6374
|
-
console.log(
|
|
6375
|
-
console.log(
|
|
6376
|
-
console.log(` ${
|
|
6377
|
-
console.log(` ${
|
|
6378
|
-
console.log(` ${
|
|
6379
|
-
console.log(` ${
|
|
6440
|
+
console.log(chalk16.gray(" Press Ctrl+C to stop\n"));
|
|
6441
|
+
console.log(chalk16.bold("API Endpoints:"));
|
|
6442
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/health`)} - Health check`);
|
|
6443
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/report/latest`)} - Latest report (cached)`);
|
|
6444
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/decisions`)} - All decisions`);
|
|
6445
|
+
console.log(` ${chalk16.cyan(`http://${host}:${port}/api/analytics/summary`)} - Analytics`);
|
|
6380
6446
|
console.log("");
|
|
6381
6447
|
});
|
|
6382
6448
|
const shutdown = () => {
|
|
6383
|
-
console.log(
|
|
6449
|
+
console.log(chalk16.yellow("\n\nShutting down dashboard..."));
|
|
6384
6450
|
server.stop();
|
|
6385
6451
|
process.exit(0);
|
|
6386
6452
|
};
|
|
6387
6453
|
process.on("SIGINT", shutdown);
|
|
6388
6454
|
process.on("SIGTERM", shutdown);
|
|
6389
6455
|
} catch (error) {
|
|
6390
|
-
console.error(
|
|
6456
|
+
console.error(chalk16.red("Failed to start dashboard:"), error);
|
|
6391
6457
|
throw error;
|
|
6392
6458
|
}
|
|
6393
6459
|
});
|
|
6394
6460
|
|
|
6395
6461
|
// src/cli/commands/impact.ts
|
|
6396
6462
|
import { Command as Command18 } from "commander";
|
|
6397
|
-
import
|
|
6463
|
+
import chalk17 from "chalk";
|
|
6398
6464
|
import ora8 from "ora";
|
|
6399
6465
|
|
|
6400
6466
|
// src/propagation/graph.ts
|
|
6401
|
-
async function buildDependencyGraph2(decisions, files) {
|
|
6467
|
+
async function buildDependencyGraph2(decisions, files, options = {}) {
|
|
6468
|
+
const { cwd } = options;
|
|
6402
6469
|
const nodes = /* @__PURE__ */ new Map();
|
|
6403
6470
|
const decisionToFiles = /* @__PURE__ */ new Map();
|
|
6404
6471
|
const fileToDecisions = /* @__PURE__ */ new Map();
|
|
@@ -6413,7 +6480,7 @@ async function buildDependencyGraph2(decisions, files) {
|
|
|
6413
6480
|
const constraintId = `constraint:${decision.metadata.id}/${constraint.id}`;
|
|
6414
6481
|
const matchingFiles = [];
|
|
6415
6482
|
for (const file of files) {
|
|
6416
|
-
if (matchesPattern(file, constraint.scope)) {
|
|
6483
|
+
if (matchesPattern(file, constraint.scope, { cwd })) {
|
|
6417
6484
|
matchingFiles.push(`file:${file}`);
|
|
6418
6485
|
const fileDecisions = fileToDecisions.get(file) || /* @__PURE__ */ new Set();
|
|
6419
6486
|
fileDecisions.add(decision.metadata.id);
|
|
@@ -6470,7 +6537,7 @@ var PropagationEngine = class {
|
|
|
6470
6537
|
absolute: true
|
|
6471
6538
|
});
|
|
6472
6539
|
const decisions = this.registry.getActive();
|
|
6473
|
-
this.graph = await buildDependencyGraph2(decisions, files);
|
|
6540
|
+
this.graph = await buildDependencyGraph2(decisions, files, { cwd });
|
|
6474
6541
|
}
|
|
6475
6542
|
/**
|
|
6476
6543
|
* Analyze impact of changing a decision
|
|
@@ -6480,7 +6547,24 @@ var PropagationEngine = class {
|
|
|
6480
6547
|
if (!this.graph) {
|
|
6481
6548
|
await this.initialize(config, options);
|
|
6482
6549
|
}
|
|
6483
|
-
const
|
|
6550
|
+
const graph = this.graph;
|
|
6551
|
+
if (!graph) {
|
|
6552
|
+
return {
|
|
6553
|
+
decision: decisionId,
|
|
6554
|
+
change,
|
|
6555
|
+
affectedFiles: [],
|
|
6556
|
+
estimatedEffort: "low",
|
|
6557
|
+
migrationSteps: [
|
|
6558
|
+
{
|
|
6559
|
+
order: 1,
|
|
6560
|
+
description: "Run verification to confirm all violations resolved",
|
|
6561
|
+
files: [],
|
|
6562
|
+
automated: true
|
|
6563
|
+
}
|
|
6564
|
+
]
|
|
6565
|
+
};
|
|
6566
|
+
}
|
|
6567
|
+
const affectedFilePaths = getAffectedFiles(graph, decisionId);
|
|
6484
6568
|
const verificationEngine = createVerificationEngine(this.registry);
|
|
6485
6569
|
const result = await verificationEngine.verify(config, {
|
|
6486
6570
|
files: affectedFilePaths,
|
|
@@ -6604,8 +6688,8 @@ var impactCommand = new Command18("impact").description("Analyze impact of decis
|
|
|
6604
6688
|
const changeType = options.change || "modified";
|
|
6605
6689
|
if (!["created", "modified", "deprecated"].includes(changeType)) {
|
|
6606
6690
|
spinner.fail();
|
|
6607
|
-
console.error(
|
|
6608
|
-
console.error(
|
|
6691
|
+
console.error(chalk17.red(`Invalid change type: ${changeType}`));
|
|
6692
|
+
console.error(chalk17.dim("Valid types: created, modified, deprecated"));
|
|
6609
6693
|
process.exit(1);
|
|
6610
6694
|
}
|
|
6611
6695
|
spinner.text = `Analyzing impact of ${changeType} decision ${decisionId}...`;
|
|
@@ -6623,44 +6707,44 @@ var impactCommand = new Command18("impact").description("Analyze impact of decis
|
|
|
6623
6707
|
}
|
|
6624
6708
|
});
|
|
6625
6709
|
function printImpactAnalysis(analysis, showSteps) {
|
|
6626
|
-
console.log(
|
|
6710
|
+
console.log(chalk17.bold(`
|
|
6627
6711
|
=== Impact Analysis: ${analysis.decision} ===
|
|
6628
6712
|
`));
|
|
6629
|
-
const changeLabel =
|
|
6713
|
+
const changeLabel = chalk17.cyan(analysis.change);
|
|
6630
6714
|
console.log(`Change Type: ${changeLabel}`);
|
|
6631
|
-
const effortColor = analysis.estimatedEffort === "high" ?
|
|
6715
|
+
const effortColor = analysis.estimatedEffort === "high" ? chalk17.red : analysis.estimatedEffort === "medium" ? chalk17.yellow : chalk17.green;
|
|
6632
6716
|
console.log(`Estimated Effort: ${effortColor(analysis.estimatedEffort.toUpperCase())}
|
|
6633
6717
|
`);
|
|
6634
|
-
console.log(
|
|
6718
|
+
console.log(chalk17.bold(`Affected Files: ${analysis.affectedFiles.length}`));
|
|
6635
6719
|
if (analysis.affectedFiles.length > 0) {
|
|
6636
6720
|
const displayCount = Math.min(analysis.affectedFiles.length, 10);
|
|
6637
6721
|
for (let i = 0; i < displayCount; i++) {
|
|
6638
6722
|
const file = analysis.affectedFiles[i];
|
|
6639
6723
|
if (!file) continue;
|
|
6640
6724
|
const violationText = file.violations === 1 ? "1 violation" : `${file.violations} violations`;
|
|
6641
|
-
const autoFixText = file.autoFixable > 0 ?
|
|
6642
|
-
console.log(` ${
|
|
6725
|
+
const autoFixText = file.autoFixable > 0 ? chalk17.green(` (${file.autoFixable} auto-fixable)`) : "";
|
|
6726
|
+
console.log(` ${chalk17.red("\u25CF")} ${file.path} - ${violationText}${autoFixText}`);
|
|
6643
6727
|
}
|
|
6644
6728
|
if (analysis.affectedFiles.length > displayCount) {
|
|
6645
6729
|
const remaining = analysis.affectedFiles.length - displayCount;
|
|
6646
|
-
console.log(
|
|
6730
|
+
console.log(chalk17.dim(` ... and ${remaining} more file(s)`));
|
|
6647
6731
|
}
|
|
6648
6732
|
} else {
|
|
6649
|
-
console.log(
|
|
6733
|
+
console.log(chalk17.green(" No violations found"));
|
|
6650
6734
|
}
|
|
6651
6735
|
if (showSteps && analysis.migrationSteps && analysis.migrationSteps.length > 0) {
|
|
6652
|
-
console.log(
|
|
6736
|
+
console.log(chalk17.bold("\nMigration Plan:"));
|
|
6653
6737
|
for (const step of analysis.migrationSteps) {
|
|
6654
6738
|
const icon = step.automated ? "\u{1F916}" : "\u{1F464}";
|
|
6655
|
-
const typeLabel = step.automated ?
|
|
6739
|
+
const typeLabel = step.automated ? chalk17.green("[Automated]") : chalk17.yellow("[Manual]");
|
|
6656
6740
|
console.log(` ${icon} Step ${step.order}: ${step.description} ${typeLabel}`);
|
|
6657
6741
|
if (step.files.length > 0) {
|
|
6658
6742
|
const displayFiles = Math.min(step.files.length, 3);
|
|
6659
6743
|
for (let i = 0; i < displayFiles; i++) {
|
|
6660
|
-
console.log(
|
|
6744
|
+
console.log(chalk17.dim(` - ${step.files[i]}`));
|
|
6661
6745
|
}
|
|
6662
6746
|
if (step.files.length > displayFiles) {
|
|
6663
|
-
console.log(
|
|
6747
|
+
console.log(chalk17.dim(` ... and ${step.files.length - displayFiles} more file(s)`));
|
|
6664
6748
|
}
|
|
6665
6749
|
}
|
|
6666
6750
|
console.log("");
|
|
@@ -6669,15 +6753,15 @@ function printImpactAnalysis(analysis, showSteps) {
|
|
|
6669
6753
|
const totalViolations = analysis.affectedFiles.reduce((sum, f) => sum + f.violations, 0);
|
|
6670
6754
|
const totalAutoFixable = analysis.affectedFiles.reduce((sum, f) => sum + f.autoFixable, 0);
|
|
6671
6755
|
const manualFixes = totalViolations - totalAutoFixable;
|
|
6672
|
-
console.log(
|
|
6756
|
+
console.log(chalk17.bold("Summary:"));
|
|
6673
6757
|
console.log(` Total Violations: ${totalViolations}`);
|
|
6674
|
-
console.log(` Auto-fixable: ${
|
|
6675
|
-
console.log(` Manual Fixes Required: ${manualFixes > 0 ?
|
|
6758
|
+
console.log(` Auto-fixable: ${chalk17.green(totalAutoFixable)}`);
|
|
6759
|
+
console.log(` Manual Fixes Required: ${manualFixes > 0 ? chalk17.yellow(manualFixes) : chalk17.green(0)}`);
|
|
6676
6760
|
}
|
|
6677
6761
|
|
|
6678
6762
|
// src/cli/commands/migrate.ts
|
|
6679
6763
|
import { Command as Command19 } from "commander";
|
|
6680
|
-
import
|
|
6764
|
+
import chalk18 from "chalk";
|
|
6681
6765
|
import ora9 from "ora";
|
|
6682
6766
|
import { join as join13 } from "path";
|
|
6683
6767
|
import { readdir as readdir2, copyFile, mkdir as mkdir2, readFile as readFile5, writeFile as writeFile3 } from "fs/promises";
|
|
@@ -6688,17 +6772,17 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6688
6772
|
}
|
|
6689
6773
|
const from = options.from || "v1";
|
|
6690
6774
|
const to = options.to || "v2";
|
|
6691
|
-
console.log(
|
|
6775
|
+
console.log(chalk18.blue.bold(`
|
|
6692
6776
|
=== SpecBridge Migration: ${from} \u2192 ${to} ===
|
|
6693
6777
|
`));
|
|
6694
6778
|
if (from !== "v1" && from !== "v1.3") {
|
|
6695
|
-
console.error(
|
|
6696
|
-
console.log(
|
|
6779
|
+
console.error(chalk18.red(`Unsupported source version: ${from}`));
|
|
6780
|
+
console.log(chalk18.gray("Supported: v1, v1.3"));
|
|
6697
6781
|
process.exit(1);
|
|
6698
6782
|
}
|
|
6699
6783
|
if (to !== "v2" && to !== "v2.0") {
|
|
6700
|
-
console.error(
|
|
6701
|
-
console.log(
|
|
6784
|
+
console.error(chalk18.red(`Unsupported target version: ${to}`));
|
|
6785
|
+
console.log(chalk18.gray("Supported: v2, v2.0"));
|
|
6702
6786
|
process.exit(1);
|
|
6703
6787
|
}
|
|
6704
6788
|
const spinner = ora9("Analyzing current configuration...").start();
|
|
@@ -6718,7 +6802,7 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6718
6802
|
const config = await loadConfig(cwd);
|
|
6719
6803
|
const v1Report = await generateReport(config, { cwd, legacyCompliance: true });
|
|
6720
6804
|
v1Compliance = v1Report.summary.compliance;
|
|
6721
|
-
} catch
|
|
6805
|
+
} catch {
|
|
6722
6806
|
spinner.warn("Could not generate v1 baseline report");
|
|
6723
6807
|
}
|
|
6724
6808
|
spinner.text = "Migrating decision files...";
|
|
@@ -6740,7 +6824,7 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6740
6824
|
v2: v2Compliance,
|
|
6741
6825
|
difference: v2Compliance - v1Compliance
|
|
6742
6826
|
};
|
|
6743
|
-
} catch
|
|
6827
|
+
} catch {
|
|
6744
6828
|
spinner.warn("Could not generate v2 comparison report");
|
|
6745
6829
|
}
|
|
6746
6830
|
}
|
|
@@ -6749,41 +6833,41 @@ var migrateCommand = new Command19("migrate").description("Migrate SpecBridge co
|
|
|
6749
6833
|
report.changes.push("All decisions validated successfully");
|
|
6750
6834
|
}
|
|
6751
6835
|
spinner.succeed(options.dryRun ? "Migration preview complete" : "Migration complete");
|
|
6752
|
-
console.log(
|
|
6753
|
-
console.log(
|
|
6836
|
+
console.log(chalk18.green.bold("\n\u2713 Migration Summary:\n"));
|
|
6837
|
+
console.log(chalk18.bold("Backup:"));
|
|
6754
6838
|
console.log(` ${report.backupPath}`);
|
|
6755
6839
|
console.log("");
|
|
6756
|
-
console.log(
|
|
6840
|
+
console.log(chalk18.bold("Changes:"));
|
|
6757
6841
|
for (const change of report.changes) {
|
|
6758
6842
|
console.log(` \u2022 ${change}`);
|
|
6759
6843
|
}
|
|
6760
6844
|
console.log("");
|
|
6761
6845
|
if (report.complianceComparison) {
|
|
6762
|
-
console.log(
|
|
6846
|
+
console.log(chalk18.bold("Compliance Comparison:"));
|
|
6763
6847
|
console.log(` v1.3 formula: ${report.complianceComparison.v1}%`);
|
|
6764
6848
|
console.log(` v2.0 formula: ${report.complianceComparison.v2}%`);
|
|
6765
6849
|
const diff = report.complianceComparison.difference;
|
|
6766
|
-
const diffColor = diff > 0 ?
|
|
6850
|
+
const diffColor = diff > 0 ? chalk18.green : diff < 0 ? chalk18.red : chalk18.yellow;
|
|
6767
6851
|
console.log(` Difference: ${diffColor(`${diff > 0 ? "+" : ""}${diff.toFixed(1)}%`)}`);
|
|
6768
6852
|
console.log("");
|
|
6769
6853
|
if (Math.abs(diff) > 10) {
|
|
6770
|
-
console.log(
|
|
6771
|
-
console.log(
|
|
6854
|
+
console.log(chalk18.yellow("\u26A0\uFE0F Note: Compliance score changed significantly due to severity weighting."));
|
|
6855
|
+
console.log(chalk18.gray(" Consider adjusting CI thresholds if needed.\n"));
|
|
6772
6856
|
}
|
|
6773
6857
|
}
|
|
6774
6858
|
if (options.dryRun) {
|
|
6775
|
-
console.log(
|
|
6776
|
-
console.log(
|
|
6859
|
+
console.log(chalk18.yellow("This was a dry run. No changes were applied."));
|
|
6860
|
+
console.log(chalk18.gray("Run without --dry-run to apply changes.\n"));
|
|
6777
6861
|
} else {
|
|
6778
|
-
console.log(
|
|
6779
|
-
console.log(
|
|
6862
|
+
console.log(chalk18.green("\u2713 Migration successful!"));
|
|
6863
|
+
console.log(chalk18.gray(`
|
|
6780
6864
|
Rollback: Copy files from ${report.backupPath} back to .specbridge/decisions/
|
|
6781
6865
|
`));
|
|
6782
6866
|
}
|
|
6783
6867
|
} catch (error) {
|
|
6784
6868
|
spinner.fail("Migration failed");
|
|
6785
|
-
console.error(
|
|
6786
|
-
console.log(
|
|
6869
|
+
console.error(chalk18.red("\nError:"), error instanceof Error ? error.message : error);
|
|
6870
|
+
console.log(chalk18.gray("\nNo changes were applied."));
|
|
6787
6871
|
throw error;
|
|
6788
6872
|
}
|
|
6789
6873
|
});
|
|
@@ -6830,7 +6914,7 @@ async function migrateDecisions(cwd, dryRun) {
|
|
|
6830
6914
|
"$1check:\n$1 verifier: $2"
|
|
6831
6915
|
);
|
|
6832
6916
|
if (dryRun) {
|
|
6833
|
-
console.log(
|
|
6917
|
+
console.log(chalk18.gray(` Would migrate: ${file}`));
|
|
6834
6918
|
updatedCount++;
|
|
6835
6919
|
} else {
|
|
6836
6920
|
await writeFile3(filePath, migratedContent, "utf-8");
|
|
@@ -6868,11 +6952,11 @@ program.exitOverride((err) => {
|
|
|
6868
6952
|
if (err.code === "commander.version") {
|
|
6869
6953
|
process.exit(0);
|
|
6870
6954
|
}
|
|
6871
|
-
console.error(
|
|
6955
|
+
console.error(chalk19.red(formatError(err)));
|
|
6872
6956
|
process.exit(1);
|
|
6873
6957
|
});
|
|
6874
6958
|
program.parseAsync(process.argv).catch((error) => {
|
|
6875
|
-
console.error(
|
|
6959
|
+
console.error(chalk19.red(formatError(error)));
|
|
6876
6960
|
process.exit(1);
|
|
6877
6961
|
});
|
|
6878
6962
|
//# sourceMappingURL=cli.js.map
|