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