@ipation/specbridge 2.4.5 → 2.4.7
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 +38 -2
- package/README.md +1 -1
- package/dist/cli.js +156 -135
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +42 -12
- package/dist/index.js +103 -33
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.7] - 2026-02-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Shared CLI command context helper for consistent initialization/config loading across commands.
|
|
15
|
+
- Normalized verification request/result contracts under `src/core/types/verification-contracts.ts`.
|
|
16
|
+
- Architecture boundary check script: `npm run architecture:check-boundaries`.
|
|
17
|
+
- CI warning-mode `module-boundaries` job and health-summary integration for boundary status.
|
|
18
|
+
- Debt baseline snapshot document: `docs/maintenance/debt-baseline-2026Q1.md`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- Refactored `verify`, `report`, and `infer` commands to use shared command context and module entrypoint imports.
|
|
23
|
+
- Updated architecture and maintenance docs with dependency direction guardrails and boundary-check baseline command.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Removed deprecated Husky bootstrap lines from `.husky/pre-commit` to avoid v10 incompatibility warning.
|
|
28
|
+
|
|
29
|
+
## [2.4.6] - 2026-02-08
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- Cleaned and consolidated project documentation:
|
|
34
|
+
- Archived historical root/docs markdown files into `docs/archive/`.
|
|
35
|
+
- Removed stale top-level `PROJECT_ASSESSMENT.md`.
|
|
36
|
+
- Renamed `docs/PHASE4_QUICK_REFERENCE.md` to `docs/analytics-quick-reference.md`.
|
|
37
|
+
- Renamed `docs/demos/phase4-analytics-demo.md` to `docs/demos/analytics-demo.md`.
|
|
38
|
+
- Updated docs links and metadata for consistency:
|
|
39
|
+
- `README.md` and `docs/MIGRATION-V2.md` now point to `CHANGELOG.md`.
|
|
40
|
+
- `CONTRIBUTING.md` Node baseline aligned to `20.19.0+`.
|
|
41
|
+
- `SECURITY.md` supported versions table updated for current major versions.
|
|
42
|
+
- Fixed stale/broken markdown links in active docs.
|
|
43
|
+
|
|
10
44
|
## [2.4.5] - 2026-02-08
|
|
11
45
|
|
|
12
46
|
### Added
|
|
@@ -373,7 +407,7 @@ This release focuses on critical infrastructure upgrades, security improvements,
|
|
|
373
407
|
- `src/dashboard/public/index.html` - React dashboard UI
|
|
374
408
|
- `src/cli/commands/analytics.ts` - Analytics CLI command
|
|
375
409
|
- `src/cli/commands/dashboard.ts` - Dashboard CLI command
|
|
376
|
-
- `docs/demos/
|
|
410
|
+
- `docs/demos/analytics-demo.md` - Interactive demo guide (900+ lines)
|
|
377
411
|
- `docs/demos/QUICKSTART.md` - Quick start guide
|
|
378
412
|
- `docs/demos/generate-sample-data.sh` - Sample data generator
|
|
379
413
|
- `docs/features/analytics-and-insights.md` - Feature documentation (1200+ lines)
|
|
@@ -1078,7 +1112,9 @@ This release adopts a **pragmatic testing approach**:
|
|
|
1078
1112
|
- Vitest for testing
|
|
1079
1113
|
- tsup for building
|
|
1080
1114
|
|
|
1081
|
-
[Unreleased]: https://github.com/nouatzi/specbridge/compare/v2.4.
|
|
1115
|
+
[Unreleased]: https://github.com/nouatzi/specbridge/compare/v2.4.7...HEAD
|
|
1116
|
+
[2.4.7]: https://github.com/nouatzi/specbridge/compare/v2.4.6...v2.4.7
|
|
1117
|
+
[2.4.6]: https://github.com/nouatzi/specbridge/compare/v2.4.5...v2.4.6
|
|
1082
1118
|
[2.4.5]: https://github.com/nouatzi/specbridge/compare/v2.4.4...v2.4.5
|
|
1083
1119
|
[2.4.4]: https://github.com/nouatzi/specbridge/compare/v2.4.3...v2.4.4
|
|
1084
1120
|
[2.4.3]: https://github.com/nouatzi/specbridge/compare/v2.4.2...v2.4.3
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ SpecBridge creates a living integration layer between design intent and implemen
|
|
|
28
28
|
- 📊 **Sub-1s Dashboard** - In-memory caching for instant report loading
|
|
29
29
|
- 🔄 **Migration Tool** - Automated v1 → v2 migration with comparison reports
|
|
30
30
|
|
|
31
|
-
[📖 See full changelog](./CHANGELOG
|
|
31
|
+
[📖 See full changelog](./CHANGELOG.md) | [🔧 Migration Guide](./docs/MIGRATION-V2.md)
|
|
32
32
|
|
|
33
33
|
Project vision: [French](./docs/VISION.md) | [English summary](./docs/VISION.en.md)
|
|
34
34
|
|
package/dist/cli.js
CHANGED
|
@@ -1300,6 +1300,29 @@ function createInferenceEngine() {
|
|
|
1300
1300
|
return new InferenceEngine();
|
|
1301
1301
|
}
|
|
1302
1302
|
|
|
1303
|
+
// src/utils/logger.ts
|
|
1304
|
+
import pino from "pino";
|
|
1305
|
+
var defaultOptions = {
|
|
1306
|
+
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
1307
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
1308
|
+
base: {
|
|
1309
|
+
service: "specbridge"
|
|
1310
|
+
}
|
|
1311
|
+
};
|
|
1312
|
+
var destination = pino.destination({
|
|
1313
|
+
fd: 2,
|
|
1314
|
+
// stderr
|
|
1315
|
+
sync: false
|
|
1316
|
+
});
|
|
1317
|
+
var rootLogger = pino(defaultOptions, destination);
|
|
1318
|
+
function getLogger(bindings) {
|
|
1319
|
+
if (!bindings) {
|
|
1320
|
+
return rootLogger;
|
|
1321
|
+
}
|
|
1322
|
+
return rootLogger.child(bindings);
|
|
1323
|
+
}
|
|
1324
|
+
var logger = getLogger();
|
|
1325
|
+
|
|
1303
1326
|
// src/config/loader.ts
|
|
1304
1327
|
async function loadConfig(basePath = process.cwd()) {
|
|
1305
1328
|
const specbridgeDir = getSpecBridgeDir(basePath);
|
|
@@ -1320,17 +1343,41 @@ async function loadConfig(basePath = process.cwd()) {
|
|
|
1320
1343
|
return result.data;
|
|
1321
1344
|
}
|
|
1322
1345
|
|
|
1323
|
-
// src/cli/
|
|
1324
|
-
|
|
1325
|
-
const cwd = process.cwd();
|
|
1326
|
-
|
|
1346
|
+
// src/cli/command-context.ts
|
|
1347
|
+
async function createConfiguredCommandContext(options = {}) {
|
|
1348
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1349
|
+
const outputFormat = options.outputFormat ?? "console";
|
|
1350
|
+
const requireInitialized = options.requireInitialized ?? true;
|
|
1351
|
+
if (requireInitialized && !await pathExists(getSpecBridgeDir(cwd))) {
|
|
1327
1352
|
throw new NotInitializedError();
|
|
1328
1353
|
}
|
|
1354
|
+
const config = await loadConfig(cwd);
|
|
1355
|
+
return {
|
|
1356
|
+
context: {
|
|
1357
|
+
cwd,
|
|
1358
|
+
outputFormat
|
|
1359
|
+
},
|
|
1360
|
+
config
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function parseCsvOption(value) {
|
|
1364
|
+
if (!value) {
|
|
1365
|
+
return void 0;
|
|
1366
|
+
}
|
|
1367
|
+
const parsed = value.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
1368
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// src/cli/commands/infer.ts
|
|
1372
|
+
var inferCommand = new Command2("infer").description("Analyze codebase and detect patterns").option("-o, --output <file>", "Output file path").option("-c, --min-confidence <number>", "Minimum confidence threshold (0-100)", "50").option("-a, --analyzers <list>", "Comma-separated list of analyzers to run").option("--json", "Output as JSON").option("--save", "Save results to .specbridge/inferred/").action(async (options) => {
|
|
1329
1373
|
const spinner = ora2("Loading configuration...").start();
|
|
1330
1374
|
try {
|
|
1331
|
-
const config = await
|
|
1332
|
-
|
|
1333
|
-
|
|
1375
|
+
const { context, config } = await createConfiguredCommandContext({
|
|
1376
|
+
outputFormat: options.json ? "json" : "console"
|
|
1377
|
+
});
|
|
1378
|
+
const { cwd } = context;
|
|
1379
|
+
const minConfidence = Number.parseInt(options.minConfidence || "50", 10);
|
|
1380
|
+
const analyzerList = parseCsvOption(options.analyzers) || config.inference?.analyzers || getAnalyzerIds();
|
|
1334
1381
|
spinner.text = `Scanning codebase (analyzers: ${analyzerList.join(", ")})...`;
|
|
1335
1382
|
const engine = createInferenceEngine();
|
|
1336
1383
|
const result = await engine.infer({
|
|
@@ -2798,31 +2845,6 @@ import { existsSync } from "fs";
|
|
|
2798
2845
|
import { join as join5 } from "path";
|
|
2799
2846
|
import { pathToFileURL } from "url";
|
|
2800
2847
|
import fg2 from "fast-glob";
|
|
2801
|
-
|
|
2802
|
-
// src/utils/logger.ts
|
|
2803
|
-
import pino from "pino";
|
|
2804
|
-
var defaultOptions = {
|
|
2805
|
-
level: process.env.SPECBRIDGE_LOG_LEVEL || "info",
|
|
2806
|
-
timestamp: pino.stdTimeFunctions.isoTime,
|
|
2807
|
-
base: {
|
|
2808
|
-
service: "specbridge"
|
|
2809
|
-
}
|
|
2810
|
-
};
|
|
2811
|
-
var destination = pino.destination({
|
|
2812
|
-
fd: 2,
|
|
2813
|
-
// stderr
|
|
2814
|
-
sync: false
|
|
2815
|
-
});
|
|
2816
|
-
var rootLogger = pino(defaultOptions, destination);
|
|
2817
|
-
function getLogger(bindings) {
|
|
2818
|
-
if (!bindings) {
|
|
2819
|
-
return rootLogger;
|
|
2820
|
-
}
|
|
2821
|
-
return rootLogger.child(bindings);
|
|
2822
|
-
}
|
|
2823
|
-
var logger = getLogger();
|
|
2824
|
-
|
|
2825
|
-
// src/verification/plugins/loader.ts
|
|
2826
2848
|
var PluginLoader = class {
|
|
2827
2849
|
plugins = /* @__PURE__ */ new Map();
|
|
2828
2850
|
loaded = false;
|
|
@@ -3600,95 +3622,6 @@ function createVerificationEngine(registry) {
|
|
|
3600
3622
|
return new VerificationEngine(registry);
|
|
3601
3623
|
}
|
|
3602
3624
|
|
|
3603
|
-
// src/verification/autofix/engine.ts
|
|
3604
|
-
import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
3605
|
-
import readline from "readline/promises";
|
|
3606
|
-
import { stdin, stdout } from "process";
|
|
3607
|
-
function applyEdits(content, edits) {
|
|
3608
|
-
const sorted = [...edits].sort((a, b) => b.start - a.start);
|
|
3609
|
-
let next = content;
|
|
3610
|
-
const patches = [];
|
|
3611
|
-
let skippedEdits = 0;
|
|
3612
|
-
let lastStart = Number.POSITIVE_INFINITY;
|
|
3613
|
-
for (const edit of sorted) {
|
|
3614
|
-
if (edit.start < 0 || edit.end < edit.start || edit.end > next.length) {
|
|
3615
|
-
skippedEdits++;
|
|
3616
|
-
continue;
|
|
3617
|
-
}
|
|
3618
|
-
if (edit.end > lastStart) {
|
|
3619
|
-
skippedEdits++;
|
|
3620
|
-
continue;
|
|
3621
|
-
}
|
|
3622
|
-
lastStart = edit.start;
|
|
3623
|
-
const originalText = next.slice(edit.start, edit.end);
|
|
3624
|
-
next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
|
|
3625
|
-
patches.push({
|
|
3626
|
-
filePath: "",
|
|
3627
|
-
description: edit.description,
|
|
3628
|
-
start: edit.start,
|
|
3629
|
-
end: edit.end,
|
|
3630
|
-
originalText,
|
|
3631
|
-
fixedText: edit.text
|
|
3632
|
-
});
|
|
3633
|
-
}
|
|
3634
|
-
return { next, patches, skippedEdits };
|
|
3635
|
-
}
|
|
3636
|
-
async function confirmFix(prompt) {
|
|
3637
|
-
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
3638
|
-
try {
|
|
3639
|
-
const answer = await rl.question(`${prompt} (y/N) `);
|
|
3640
|
-
return answer.trim().toLowerCase() === "y";
|
|
3641
|
-
} finally {
|
|
3642
|
-
rl.close();
|
|
3643
|
-
}
|
|
3644
|
-
}
|
|
3645
|
-
var AutofixEngine = class {
|
|
3646
|
-
async applyFixes(violations, options = {}) {
|
|
3647
|
-
const fixable = violations.filter((v) => v.autofix && v.autofix.edits.length > 0);
|
|
3648
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
3649
|
-
for (const v of fixable) {
|
|
3650
|
-
const list = byFile.get(v.file) ?? [];
|
|
3651
|
-
list.push(v);
|
|
3652
|
-
byFile.set(v.file, list);
|
|
3653
|
-
}
|
|
3654
|
-
const applied = [];
|
|
3655
|
-
let skippedViolations = 0;
|
|
3656
|
-
for (const [filePath, fileViolations] of byFile) {
|
|
3657
|
-
const original = await readFile4(filePath, "utf-8");
|
|
3658
|
-
const edits = [];
|
|
3659
|
-
for (const violation of fileViolations) {
|
|
3660
|
-
const fix = violation.autofix;
|
|
3661
|
-
if (!fix) {
|
|
3662
|
-
skippedViolations++;
|
|
3663
|
-
continue;
|
|
3664
|
-
}
|
|
3665
|
-
if (options.interactive) {
|
|
3666
|
-
const ok = await confirmFix(
|
|
3667
|
-
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3668
|
-
);
|
|
3669
|
-
if (!ok) {
|
|
3670
|
-
skippedViolations++;
|
|
3671
|
-
continue;
|
|
3672
|
-
}
|
|
3673
|
-
}
|
|
3674
|
-
for (const edit of fix.edits) {
|
|
3675
|
-
edits.push({ ...edit, description: fix.description });
|
|
3676
|
-
}
|
|
3677
|
-
}
|
|
3678
|
-
if (edits.length === 0) continue;
|
|
3679
|
-
const { next, patches, skippedEdits } = applyEdits(original, edits);
|
|
3680
|
-
skippedViolations += skippedEdits;
|
|
3681
|
-
if (!options.dryRun) {
|
|
3682
|
-
await writeFile2(filePath, next, "utf-8");
|
|
3683
|
-
}
|
|
3684
|
-
for (const patch of patches) {
|
|
3685
|
-
applied.push({ ...patch, filePath });
|
|
3686
|
-
}
|
|
3687
|
-
}
|
|
3688
|
-
return { applied, skipped: skippedViolations };
|
|
3689
|
-
}
|
|
3690
|
-
};
|
|
3691
|
-
|
|
3692
3625
|
// src/verification/incremental.ts
|
|
3693
3626
|
import { execFile } from "child_process";
|
|
3694
3627
|
import { promisify } from "util";
|
|
@@ -3782,22 +3715,110 @@ var ExplainReporter = class {
|
|
|
3782
3715
|
}
|
|
3783
3716
|
};
|
|
3784
3717
|
|
|
3718
|
+
// src/verification/autofix/engine.ts
|
|
3719
|
+
import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
3720
|
+
import readline from "readline/promises";
|
|
3721
|
+
import { stdin, stdout } from "process";
|
|
3722
|
+
function applyEdits(content, edits) {
|
|
3723
|
+
const sorted = [...edits].sort((a, b) => b.start - a.start);
|
|
3724
|
+
let next = content;
|
|
3725
|
+
const patches = [];
|
|
3726
|
+
let skippedEdits = 0;
|
|
3727
|
+
let lastStart = Number.POSITIVE_INFINITY;
|
|
3728
|
+
for (const edit of sorted) {
|
|
3729
|
+
if (edit.start < 0 || edit.end < edit.start || edit.end > next.length) {
|
|
3730
|
+
skippedEdits++;
|
|
3731
|
+
continue;
|
|
3732
|
+
}
|
|
3733
|
+
if (edit.end > lastStart) {
|
|
3734
|
+
skippedEdits++;
|
|
3735
|
+
continue;
|
|
3736
|
+
}
|
|
3737
|
+
lastStart = edit.start;
|
|
3738
|
+
const originalText = next.slice(edit.start, edit.end);
|
|
3739
|
+
next = next.slice(0, edit.start) + edit.text + next.slice(edit.end);
|
|
3740
|
+
patches.push({
|
|
3741
|
+
filePath: "",
|
|
3742
|
+
description: edit.description,
|
|
3743
|
+
start: edit.start,
|
|
3744
|
+
end: edit.end,
|
|
3745
|
+
originalText,
|
|
3746
|
+
fixedText: edit.text
|
|
3747
|
+
});
|
|
3748
|
+
}
|
|
3749
|
+
return { next, patches, skippedEdits };
|
|
3750
|
+
}
|
|
3751
|
+
async function confirmFix(prompt) {
|
|
3752
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
3753
|
+
try {
|
|
3754
|
+
const answer = await rl.question(`${prompt} (y/N) `);
|
|
3755
|
+
return answer.trim().toLowerCase() === "y";
|
|
3756
|
+
} finally {
|
|
3757
|
+
rl.close();
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
var AutofixEngine = class {
|
|
3761
|
+
async applyFixes(violations, options = {}) {
|
|
3762
|
+
const fixable = violations.filter((v) => v.autofix && v.autofix.edits.length > 0);
|
|
3763
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
3764
|
+
for (const v of fixable) {
|
|
3765
|
+
const list = byFile.get(v.file) ?? [];
|
|
3766
|
+
list.push(v);
|
|
3767
|
+
byFile.set(v.file, list);
|
|
3768
|
+
}
|
|
3769
|
+
const applied = [];
|
|
3770
|
+
let skippedViolations = 0;
|
|
3771
|
+
for (const [filePath, fileViolations] of byFile) {
|
|
3772
|
+
const original = await readFile4(filePath, "utf-8");
|
|
3773
|
+
const edits = [];
|
|
3774
|
+
for (const violation of fileViolations) {
|
|
3775
|
+
const fix = violation.autofix;
|
|
3776
|
+
if (!fix) {
|
|
3777
|
+
skippedViolations++;
|
|
3778
|
+
continue;
|
|
3779
|
+
}
|
|
3780
|
+
if (options.interactive) {
|
|
3781
|
+
const ok = await confirmFix(
|
|
3782
|
+
`Apply fix: ${fix.description} (${filePath}:${violation.line ?? 1})?`
|
|
3783
|
+
);
|
|
3784
|
+
if (!ok) {
|
|
3785
|
+
skippedViolations++;
|
|
3786
|
+
continue;
|
|
3787
|
+
}
|
|
3788
|
+
}
|
|
3789
|
+
for (const edit of fix.edits) {
|
|
3790
|
+
edits.push({ ...edit, description: fix.description });
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
if (edits.length === 0) continue;
|
|
3794
|
+
const { next, patches, skippedEdits } = applyEdits(original, edits);
|
|
3795
|
+
skippedViolations += skippedEdits;
|
|
3796
|
+
if (!options.dryRun) {
|
|
3797
|
+
await writeFile2(filePath, next, "utf-8");
|
|
3798
|
+
}
|
|
3799
|
+
for (const patch of patches) {
|
|
3800
|
+
applied.push({ ...patch, filePath });
|
|
3801
|
+
}
|
|
3802
|
+
}
|
|
3803
|
+
return { applied, skipped: skippedViolations };
|
|
3804
|
+
}
|
|
3805
|
+
};
|
|
3806
|
+
|
|
3785
3807
|
// src/cli/commands/verify.ts
|
|
3786
3808
|
var verifyCommand = new Command3("verify").description("Verify code compliance against decisions").option("-l, --level <level>", "Verification level (commit, pr, full)", "full").option("-f, --files <patterns>", "Comma-separated file patterns to check").option("-d, --decisions <ids>", "Comma-separated decision IDs to check").option(
|
|
3787
3809
|
"-s, --severity <levels>",
|
|
3788
3810
|
"Comma-separated severity levels (critical, high, medium, low)"
|
|
3789
3811
|
).option("--json", "Output as JSON").option("--incremental", "Only verify changed files (git diff --name-only --diff-filter=AM HEAD)").option("--explain", "Show detailed explanation of verification process").option("--fix", "Apply auto-fixes for supported violations").option("--dry-run", "Show what would be fixed without applying (requires --fix)").option("--interactive", "Confirm each fix interactively (requires --fix)").action(async (options) => {
|
|
3790
|
-
const cwd = process.cwd();
|
|
3791
|
-
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
3792
|
-
throw new NotInitializedError();
|
|
3793
|
-
}
|
|
3794
3812
|
const spinner = ora3("Loading configuration...").start();
|
|
3795
3813
|
try {
|
|
3796
|
-
const config = await
|
|
3814
|
+
const { context, config } = await createConfiguredCommandContext({
|
|
3815
|
+
outputFormat: options.json ? "json" : "console"
|
|
3816
|
+
});
|
|
3817
|
+
const { cwd } = context;
|
|
3797
3818
|
const level = options.level || "full";
|
|
3798
|
-
let files = options.files
|
|
3799
|
-
const decisions = options.decisions
|
|
3800
|
-
const severity = options.severity?.
|
|
3819
|
+
let files = parseCsvOption(options.files);
|
|
3820
|
+
const decisions = parseCsvOption(options.decisions);
|
|
3821
|
+
const severity = parseCsvOption(options.severity)?.map((value) => value);
|
|
3801
3822
|
if (options.incremental) {
|
|
3802
3823
|
const changed = await getChangedFiles(cwd);
|
|
3803
3824
|
files = changed.length > 0 ? changed : [];
|
|
@@ -5071,13 +5092,13 @@ async function analyzeTrend(reports) {
|
|
|
5071
5092
|
|
|
5072
5093
|
// src/cli/commands/report.ts
|
|
5073
5094
|
var reportCommand = new Command10("report").description("Generate compliance report").option("-f, --format <format>", "Output format (console, json, markdown)", "console").option("-o, --output <file>", "Output file path").option("--save", "Save to .specbridge/reports/").option("-a, --all", "Include all decisions (not just active)").option("--trend", "Show compliance trend over time").option("--drift", "Analyze drift since last report").option("--days <n>", "Number of days for trend analysis", "30").option("--legacy-compliance", "Use v1.3 compliance formula (for comparison)").action(async (options) => {
|
|
5074
|
-
const cwd = process.cwd();
|
|
5075
|
-
if (!await pathExists(getSpecBridgeDir(cwd))) {
|
|
5076
|
-
throw new NotInitializedError();
|
|
5077
|
-
}
|
|
5078
5095
|
const spinner = ora6("Generating compliance report...").start();
|
|
5079
5096
|
try {
|
|
5080
|
-
const
|
|
5097
|
+
const outputFormat = options.format === "json" ? "json" : options.format === "markdown" || options.format === "md" ? "markdown" : "console";
|
|
5098
|
+
const { context, config } = await createConfiguredCommandContext({
|
|
5099
|
+
outputFormat
|
|
5100
|
+
});
|
|
5101
|
+
const { cwd } = context;
|
|
5081
5102
|
const report = await generateReport(config, {
|
|
5082
5103
|
includeAll: options.all,
|
|
5083
5104
|
cwd,
|
|
@@ -5088,7 +5109,7 @@ var reportCommand = new Command10("report").description("Generate compliance rep
|
|
|
5088
5109
|
await storage.save(report);
|
|
5089
5110
|
if (options.trend) {
|
|
5090
5111
|
console.log("\n" + chalk11.blue.bold("=== Compliance Trend Analysis ===\n"));
|
|
5091
|
-
const days = parseInt(options.days || "30", 10);
|
|
5112
|
+
const days = Number.parseInt(options.days || "30", 10);
|
|
5092
5113
|
const history = await storage.loadHistory(days);
|
|
5093
5114
|
if (history.length < 2) {
|
|
5094
5115
|
console.log(
|