@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 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/phase4-analytics-demo.md` - Interactive demo guide (900+ lines)
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.5...HEAD
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-v2.0.md) | [🔧 Migration Guide](./docs/MIGRATION-V2.md)
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/commands/infer.ts
1324
- 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) => {
1325
- const cwd = process.cwd();
1326
- if (!await pathExists(getSpecBridgeDir(cwd))) {
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 loadConfig(cwd);
1332
- const minConfidence = parseInt(options.minConfidence || "50", 10);
1333
- const analyzerList = options.analyzers ? options.analyzers.split(",").map((a) => a.trim()) : config.inference?.analyzers || getAnalyzerIds();
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 loadConfig(cwd);
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?.split(",").map((f) => f.trim());
3799
- const decisions = options.decisions?.split(",").map((d) => d.trim());
3800
- const severity = options.severity?.split(",").map((s) => s.trim());
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 config = await loadConfig(cwd);
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(