@ipation/specbridge 2.4.6 → 2.4.8
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 -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 +5 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.8] - 2026-02-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Monthly maintenance issue automation workflow: `.github/workflows/health-snapshot.yml`.
|
|
15
|
+
- ESLint v10 readiness probe script and npm commands:
|
|
16
|
+
- `npm run eslint10:readiness`
|
|
17
|
+
- `npm run eslint10:readiness:strict`
|
|
18
|
+
- CI `eslint10-readiness` job with staged strict gate toggle on `main` via `ESLINT10_STRICT_GATE`.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- CI health summary now records collision-safe integration metric keys and flakiness KPI fields.
|
|
23
|
+
- Health checklist and monthly snapshot template now capture ESLint v10 gate status and flaky retry metrics.
|
|
24
|
+
- Added architecture boundary warning alias command: `npm run architecture:check-boundaries:warning`.
|
|
25
|
+
|
|
26
|
+
## [2.4.7] - 2026-02-08
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- Shared CLI command context helper for consistent initialization/config loading across commands.
|
|
31
|
+
- Normalized verification request/result contracts under `src/core/types/verification-contracts.ts`.
|
|
32
|
+
- Architecture boundary check script: `npm run architecture:check-boundaries`.
|
|
33
|
+
- CI warning-mode `module-boundaries` job and health-summary integration for boundary status.
|
|
34
|
+
- Debt baseline snapshot document: `docs/maintenance/debt-baseline-2026Q1.md`.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
|
|
38
|
+
- Refactored `verify`, `report`, and `infer` commands to use shared command context and module entrypoint imports.
|
|
39
|
+
- Updated architecture and maintenance docs with dependency direction guardrails and boundary-check baseline command.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Removed deprecated Husky bootstrap lines from `.husky/pre-commit` to avoid v10 incompatibility warning.
|
|
44
|
+
|
|
10
45
|
## [2.4.6] - 2026-02-08
|
|
11
46
|
|
|
12
47
|
### Changed
|
|
@@ -1093,7 +1128,9 @@ This release adopts a **pragmatic testing approach**:
|
|
|
1093
1128
|
- Vitest for testing
|
|
1094
1129
|
- tsup for building
|
|
1095
1130
|
|
|
1096
|
-
[Unreleased]: https://github.com/nouatzi/specbridge/compare/v2.4.
|
|
1131
|
+
[Unreleased]: https://github.com/nouatzi/specbridge/compare/v2.4.8...HEAD
|
|
1132
|
+
[2.4.8]: https://github.com/nouatzi/specbridge/compare/v2.4.7...v2.4.8
|
|
1133
|
+
[2.4.7]: https://github.com/nouatzi/specbridge/compare/v2.4.6...v2.4.7
|
|
1097
1134
|
[2.4.6]: https://github.com/nouatzi/specbridge/compare/v2.4.5...v2.4.6
|
|
1098
1135
|
[2.4.5]: https://github.com/nouatzi/specbridge/compare/v2.4.4...v2.4.5
|
|
1099
1136
|
[2.4.4]: https://github.com/nouatzi/specbridge/compare/v2.4.3...v2.4.4
|
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(
|