@ipation/specbridge 1.0.3 → 1.0.5

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,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.4] - 2026-01-30
11
+
12
+ ### Fixed
13
+
14
+ #### Critical Fixes
15
+ - **Verification Timeout Bug** (src/verification/engine.ts)
16
+ - Fixed event loop hanging issue where verification would wait for full 60-second timeout even after completing
17
+ - Added proper timeout handle cleanup with try-finally block
18
+ - Used `unref()` to prevent blocking process exit
19
+ - **Impact**: Integration tests now complete in ~97 seconds instead of hanging for 10+ minutes
20
+ - CLI verify command exits immediately (~600ms) instead of waiting 60 seconds
21
+
22
+ #### Inference Issues
23
+ - **Non-Existent Verifier References** (src/inference/analyzers/errors.ts)
24
+ - Fixed `error-hierarchy` verifier reference → `errors` (line 90)
25
+ - Fixed `custom-errors-only` verifier reference → `errors` (line 205)
26
+ - Updated corresponding unit tests to match
27
+ - **Impact**: Inferred constraints now reference valid verifiers that exist in the registry
28
+
29
+ #### CLI Command Issues
30
+ - **`infer --output` Bug** (src/cli/commands/infer.ts)
31
+ - Fixed early return preventing file save when no patterns detected
32
+ - Moved file saving logic before pattern check
33
+ - **Impact**: Output file is always created when `--output` flag is used, even with empty results
34
+
35
+ #### Visual Formatting
36
+ - **Empty Placeholder Characters** (src/reporting/formats/markdown.ts, src/agent/context.generator.ts)
37
+ - Added compliance emojis: ✅ (≥90%), ⚠️ (70-89%), ❌ (<70%)
38
+ - Added progress bar characters: █ (filled), ░ (empty)
39
+ - Added constraint type emojis: 🔒 (invariant), 📋 (convention), 💡 (guideline)
40
+ - **Impact**: Reports and agent context now display with proper visual indicators
41
+
42
+ ### Changed
43
+
44
+ #### Documentation
45
+ - **Version Consistency**
46
+ - Archived outdated `IMPLEMENTATION_STATUS.md` → `IMPLEMENTATION_STATUS_v0.2.2_ARCHIVED.md`
47
+ - Added archive notice indicating current version is 1.0.4
48
+ - **Impact**: No misleading version information
49
+
50
+ - **GitHub URL Standardization**
51
+ - Standardized all repository references to `nouatzi/specbridge`
52
+ - Fixed docs/README.md, docs/troubleshooting.md, src/reporting/formats/markdown.ts
53
+ - Corrected wrong `anthropics/claude-code` reference in archived file
54
+ - **Impact**: Consistent repository URLs throughout documentation and code
55
+
56
+ ### Improved
57
+
58
+ #### Test Suite
59
+ - **Integration Tests** (tests/integration/cli.test.ts)
60
+ - Fixed `decision create` test syntax (positional argument instead of `--id` flag)
61
+ - Added valid constraints to manually created decision files (schema compliance)
62
+ - Fixed 3 previously failing CLI integration tests
63
+ - **Result**: All 30 integration tests now pass (100% success rate)
64
+ - **Duration**: Tests complete in ~97 seconds (down from 10+ minutes)
65
+
66
+ ### Test Results Summary
67
+ - ✅ Type Checking: Passed
68
+ - ✅ Unit Tests: 762/762 passed (24 test files)
69
+ - ✅ Integration Tests: 30/30 passed (2 test files)
70
+ - ✅ Total Duration: ~97 seconds
71
+
72
+ ### Files Modified
73
+ - 9 source/test files updated
74
+ - 2 documentation files updated
75
+ - 1 file archived
76
+ - All changes backward compatible (no breaking changes)
77
+
10
78
  ## [1.0.3] - 2026-01-30
11
79
 
12
80
  ### Improved
package/dist/cli.js CHANGED
@@ -1045,7 +1045,7 @@ var ErrorsAnalyzer = class {
1045
1045
  rule: `Custom error classes should extend ${customBaseName}`,
1046
1046
  severity: "medium",
1047
1047
  scope: "src/**/*.ts",
1048
- verifier: "error-hierarchy"
1048
+ verifier: "errors"
1049
1049
  }
1050
1050
  });
1051
1051
  }
@@ -1142,7 +1142,7 @@ var ErrorsAnalyzer = class {
1142
1142
  rule: "Throw custom error classes instead of generic Error",
1143
1143
  severity: "medium",
1144
1144
  scope: "src/**/*.ts",
1145
- verifier: "custom-errors-only"
1145
+ verifier: "errors"
1146
1146
  }
1147
1147
  });
1148
1148
  }
@@ -1281,6 +1281,12 @@ var inferCommand = new Command2("infer").description("Analyze codebase and detec
1281
1281
  cwd
1282
1282
  });
1283
1283
  spinner.succeed(`Scanned ${result.filesScanned} files in ${result.duration}ms`);
1284
+ if (options.save || options.output) {
1285
+ const outputPath = options.output || join3(getInferredDir(cwd), "patterns.json");
1286
+ await writeTextFile(outputPath, JSON.stringify(result, null, 2));
1287
+ console.log(chalk2.green(`
1288
+ Results saved to: ${outputPath}`));
1289
+ }
1284
1290
  if (result.patterns.length === 0) {
1285
1291
  console.log(chalk2.yellow("\nNo patterns detected above confidence threshold."));
1286
1292
  console.log(chalk2.dim(`Try lowering --min-confidence (current: ${minConfidence})`));
@@ -1291,12 +1297,6 @@ var inferCommand = new Command2("infer").description("Analyze codebase and detec
1291
1297
  } else {
1292
1298
  printPatterns(result.patterns);
1293
1299
  }
1294
- if (options.save || options.output) {
1295
- const outputPath = options.output || join3(getInferredDir(cwd), "patterns.json");
1296
- await writeTextFile(outputPath, JSON.stringify(result, null, 2));
1297
- console.log(chalk2.green(`
1298
- Results saved to: ${outputPath}`));
1299
- }
1300
1300
  if (!options.json) {
1301
1301
  console.log("");
1302
1302
  console.log(chalk2.cyan("Next steps:"));
@@ -2128,9 +2128,11 @@ var VerificationEngine = class {
2128
2128
  let passed = 0;
2129
2129
  let failed = 0;
2130
2130
  const skipped = 0;
2131
- const timeoutPromise = new Promise(
2132
- (resolve) => setTimeout(() => resolve("timeout"), timeout)
2133
- );
2131
+ let timeoutHandle = null;
2132
+ const timeoutPromise = new Promise((resolve) => {
2133
+ timeoutHandle = setTimeout(() => resolve("timeout"), timeout);
2134
+ timeoutHandle.unref();
2135
+ });
2134
2136
  const verificationPromise = this.verifyFiles(
2135
2137
  filesToVerify,
2136
2138
  decisions,
@@ -2146,17 +2148,25 @@ var VerificationEngine = class {
2146
2148
  }
2147
2149
  }
2148
2150
  );
2149
- const result = await Promise.race([verificationPromise, timeoutPromise]);
2150
- if (result === "timeout") {
2151
- return {
2152
- success: false,
2153
- violations: allViolations,
2154
- checked,
2155
- passed,
2156
- failed,
2157
- skipped: filesToVerify.length - checked,
2158
- duration: timeout
2159
- };
2151
+ let result;
2152
+ try {
2153
+ result = await Promise.race([verificationPromise, timeoutPromise]);
2154
+ if (result === "timeout") {
2155
+ return {
2156
+ success: false,
2157
+ violations: allViolations,
2158
+ checked,
2159
+ passed,
2160
+ failed,
2161
+ skipped: filesToVerify.length - checked,
2162
+ duration: timeout
2163
+ };
2164
+ }
2165
+ } finally {
2166
+ if (timeoutHandle) {
2167
+ clearTimeout(timeoutHandle);
2168
+ timeoutHandle = null;
2169
+ }
2160
2170
  }
2161
2171
  const hasBlockingViolations = allViolations.some((v) => {
2162
2172
  if (level === "commit") {
@@ -2752,38 +2762,22 @@ npx specbridge hook run --level commit --files "$STAGED_FILES"
2752
2762
 
2753
2763
  exit $?
2754
2764
  `;
2755
- var hookCommand = new Command9("hook").description("Manage Git hooks for verification");
2756
- hookCommand.command("install").description("Install Git pre-commit hook").option("-f, --force", "Overwrite existing hook").option("--husky", "Install for husky").option("--lefthook", "Install for lefthook").action(async (options) => {
2757
- const cwd = process.cwd();
2758
- if (!await pathExists(getSpecBridgeDir(cwd))) {
2759
- throw new NotInitializedError();
2760
- }
2761
- const spinner = ora5("Detecting hook system...").start();
2762
- try {
2763
- let hookPath;
2764
- let hookContent;
2765
- if (options.husky) {
2766
- hookPath = join7(cwd, ".husky", "pre-commit");
2767
- hookContent = HOOK_SCRIPT;
2768
- spinner.text = "Installing husky pre-commit hook...";
2769
- } else if (options.lefthook) {
2770
- spinner.succeed("Lefthook detected");
2771
- console.log("");
2772
- console.log(chalk8.cyan("Add this to your lefthook.yml:"));
2773
- console.log("");
2774
- console.log(chalk8.dim(`pre-commit:
2775
- commands:
2776
- specbridge:
2777
- glob: "*.{ts,tsx}"
2778
- run: npx specbridge hook run --level commit --files {staged_files}
2779
- `));
2780
- return;
2781
- } else {
2782
- if (await pathExists(join7(cwd, ".husky"))) {
2765
+ function createHookCommand() {
2766
+ const hookCommand2 = new Command9("hook").description("Manage Git hooks for verification");
2767
+ hookCommand2.command("install").description("Install Git pre-commit hook").option("-f, --force", "Overwrite existing hook").option("--husky", "Install for husky").option("--lefthook", "Install for lefthook").action(async (options) => {
2768
+ const cwd = process.cwd();
2769
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
2770
+ throw new NotInitializedError();
2771
+ }
2772
+ const spinner = ora5("Detecting hook system...").start();
2773
+ try {
2774
+ let hookPath;
2775
+ let hookContent;
2776
+ if (options.husky) {
2783
2777
  hookPath = join7(cwd, ".husky", "pre-commit");
2784
2778
  hookContent = HOOK_SCRIPT;
2785
2779
  spinner.text = "Installing husky pre-commit hook...";
2786
- } else if (await pathExists(join7(cwd, "lefthook.yml"))) {
2780
+ } else if (options.lefthook) {
2787
2781
  spinner.succeed("Lefthook detected");
2788
2782
  console.log("");
2789
2783
  console.log(chalk8.cyan("Add this to your lefthook.yml:"));
@@ -2796,97 +2790,117 @@ hookCommand.command("install").description("Install Git pre-commit hook").option
2796
2790
  `));
2797
2791
  return;
2798
2792
  } else {
2799
- hookPath = join7(cwd, ".git", "hooks", "pre-commit");
2800
- hookContent = HOOK_SCRIPT;
2801
- spinner.text = "Installing Git pre-commit hook...";
2793
+ if (await pathExists(join7(cwd, ".husky"))) {
2794
+ hookPath = join7(cwd, ".husky", "pre-commit");
2795
+ hookContent = HOOK_SCRIPT;
2796
+ spinner.text = "Installing husky pre-commit hook...";
2797
+ } else if (await pathExists(join7(cwd, "lefthook.yml"))) {
2798
+ spinner.succeed("Lefthook detected");
2799
+ console.log("");
2800
+ console.log(chalk8.cyan("Add this to your lefthook.yml:"));
2801
+ console.log("");
2802
+ console.log(chalk8.dim(`pre-commit:
2803
+ commands:
2804
+ specbridge:
2805
+ glob: "*.{ts,tsx}"
2806
+ run: npx specbridge hook run --level commit --files {staged_files}
2807
+ `));
2808
+ return;
2809
+ } else {
2810
+ hookPath = join7(cwd, ".git", "hooks", "pre-commit");
2811
+ hookContent = HOOK_SCRIPT;
2812
+ spinner.text = "Installing Git pre-commit hook...";
2813
+ }
2802
2814
  }
2815
+ if (await pathExists(hookPath) && !options.force) {
2816
+ spinner.fail("Hook already exists");
2817
+ console.log(chalk8.yellow(`Use --force to overwrite: ${hookPath}`));
2818
+ return;
2819
+ }
2820
+ await writeTextFile(hookPath, hookContent);
2821
+ const { execSync } = await import("child_process");
2822
+ try {
2823
+ execSync(`chmod +x "${hookPath}"`, { stdio: "ignore" });
2824
+ } catch {
2825
+ }
2826
+ spinner.succeed("Pre-commit hook installed");
2827
+ console.log(chalk8.dim(` Path: ${hookPath}`));
2828
+ console.log("");
2829
+ console.log(chalk8.cyan("The hook will run on each commit and verify staged files."));
2830
+ } catch (error) {
2831
+ spinner.fail("Failed to install hook");
2832
+ throw error;
2803
2833
  }
2804
- if (await pathExists(hookPath) && !options.force) {
2805
- spinner.fail("Hook already exists");
2806
- console.log(chalk8.yellow(`Use --force to overwrite: ${hookPath}`));
2807
- return;
2834
+ });
2835
+ hookCommand2.command("run").description("Run verification (called by hook)").option("-l, --level <level>", "Verification level", "commit").option("-f, --files <files>", "Space or comma-separated file list").action(async (options) => {
2836
+ const cwd = process.cwd();
2837
+ if (!await pathExists(getSpecBridgeDir(cwd))) {
2838
+ throw new NotInitializedError();
2808
2839
  }
2809
- await writeTextFile(hookPath, hookContent);
2810
- const { execSync } = await import("child_process");
2811
2840
  try {
2812
- execSync(`chmod +x "${hookPath}"`, { stdio: "ignore" });
2813
- } catch {
2814
- }
2815
- spinner.succeed("Pre-commit hook installed");
2816
- console.log(chalk8.dim(` Path: ${hookPath}`));
2817
- console.log("");
2818
- console.log(chalk8.cyan("The hook will run on each commit and verify staged files."));
2819
- } catch (error) {
2820
- spinner.fail("Failed to install hook");
2821
- throw error;
2822
- }
2823
- });
2824
- hookCommand.command("run").description("Run verification (called by hook)").option("-l, --level <level>", "Verification level", "commit").option("-f, --files <files>", "Space or comma-separated file list").action(async (options) => {
2825
- const cwd = process.cwd();
2826
- if (!await pathExists(getSpecBridgeDir(cwd))) {
2827
- throw new NotInitializedError();
2828
- }
2829
- try {
2830
- const config = await loadConfig(cwd);
2831
- const level = options.level || "commit";
2832
- const files = options.files ? options.files.split(/[\s,]+/).filter((f) => f.length > 0) : void 0;
2833
- if (!files || files.length === 0) {
2834
- process.exit(0);
2835
- }
2836
- const engine = createVerificationEngine();
2837
- const result = await engine.verify(config, {
2838
- level,
2839
- files,
2840
- cwd
2841
- });
2842
- if (result.violations.length === 0) {
2843
- console.log(chalk8.green("\u2713 SpecBridge: All checks passed"));
2844
- process.exit(0);
2845
- }
2846
- console.log(chalk8.red(`\u2717 SpecBridge: ${result.violations.length} violation(s) found`));
2847
- console.log("");
2848
- for (const v of result.violations) {
2849
- const location = v.line ? `:${v.line}` : "";
2850
- console.log(` ${v.file}${location}: ${v.message}`);
2851
- console.log(chalk8.dim(` [${v.severity}] ${v.decisionId}/${v.constraintId}`));
2841
+ const config = await loadConfig(cwd);
2842
+ const level = options.level || "commit";
2843
+ const files = options.files ? options.files.split(/[\s,]+/).filter((f) => f.length > 0) : void 0;
2844
+ if (!files || files.length === 0) {
2845
+ process.exit(0);
2846
+ }
2847
+ const engine = createVerificationEngine();
2848
+ const result = await engine.verify(config, {
2849
+ level,
2850
+ files,
2851
+ cwd
2852
+ });
2853
+ if (result.violations.length === 0) {
2854
+ console.log(chalk8.green("\u2713 SpecBridge: All checks passed"));
2855
+ process.exit(0);
2856
+ }
2857
+ console.log(chalk8.red(`\u2717 SpecBridge: ${result.violations.length} violation(s) found`));
2858
+ console.log("");
2859
+ for (const v of result.violations) {
2860
+ const location = v.line ? `:${v.line}` : "";
2861
+ console.log(` ${v.file}${location}: ${v.message}`);
2862
+ console.log(chalk8.dim(` [${v.severity}] ${v.decisionId}/${v.constraintId}`));
2863
+ }
2864
+ console.log("");
2865
+ console.log(chalk8.yellow("Run `specbridge verify` for full details."));
2866
+ process.exit(result.success ? 0 : 1);
2867
+ } catch (error) {
2868
+ console.error(chalk8.red("SpecBridge verification failed"));
2869
+ console.error(error instanceof Error ? error.message : String(error));
2870
+ process.exit(1);
2852
2871
  }
2853
- console.log("");
2854
- console.log(chalk8.yellow("Run `specbridge verify` for full details."));
2855
- process.exit(result.success ? 0 : 1);
2856
- } catch (error) {
2857
- console.error(chalk8.red("SpecBridge verification failed"));
2858
- console.error(error instanceof Error ? error.message : String(error));
2859
- process.exit(1);
2860
- }
2861
- });
2862
- hookCommand.command("uninstall").description("Remove Git pre-commit hook").action(async () => {
2863
- const cwd = process.cwd();
2864
- const spinner = ora5("Removing hook...").start();
2865
- try {
2866
- const hookPaths = [
2867
- join7(cwd, ".husky", "pre-commit"),
2868
- join7(cwd, ".git", "hooks", "pre-commit")
2869
- ];
2870
- let removed = false;
2871
- for (const hookPath of hookPaths) {
2872
- if (await pathExists(hookPath)) {
2873
- const content = await readTextFile(hookPath);
2874
- if (content.includes("SpecBridge")) {
2875
- const { unlink } = await import("fs/promises");
2876
- await unlink(hookPath);
2877
- spinner.succeed(`Removed hook: ${hookPath}`);
2878
- removed = true;
2872
+ });
2873
+ hookCommand2.command("uninstall").description("Remove Git pre-commit hook").action(async () => {
2874
+ const cwd = process.cwd();
2875
+ const spinner = ora5("Removing hook...").start();
2876
+ try {
2877
+ const hookPaths = [
2878
+ join7(cwd, ".husky", "pre-commit"),
2879
+ join7(cwd, ".git", "hooks", "pre-commit")
2880
+ ];
2881
+ let removed = false;
2882
+ for (const hookPath of hookPaths) {
2883
+ if (await pathExists(hookPath)) {
2884
+ const content = await readTextFile(hookPath);
2885
+ if (content.includes("SpecBridge")) {
2886
+ const { unlink } = await import("fs/promises");
2887
+ await unlink(hookPath);
2888
+ spinner.succeed(`Removed hook: ${hookPath}`);
2889
+ removed = true;
2890
+ }
2879
2891
  }
2880
2892
  }
2893
+ if (!removed) {
2894
+ spinner.info("No SpecBridge hooks found");
2895
+ }
2896
+ } catch (error) {
2897
+ spinner.fail("Failed to remove hook");
2898
+ throw error;
2881
2899
  }
2882
- if (!removed) {
2883
- spinner.info("No SpecBridge hooks found");
2884
- }
2885
- } catch (error) {
2886
- spinner.fail("Failed to remove hook");
2887
- throw error;
2888
- }
2889
- });
2900
+ });
2901
+ return hookCommand2;
2902
+ }
2903
+ var hookCommand = createHookCommand();
2890
2904
 
2891
2905
  // src/cli/commands/report.ts
2892
2906
  import { Command as Command10 } from "commander";
@@ -3101,7 +3115,7 @@ function formatMarkdownReport(report) {
3101
3115
  lines.push("| Decision | Status | Constraints | Violations | Compliance |");
3102
3116
  lines.push("|----------|--------|-------------|------------|------------|");
3103
3117
  for (const dec of report.byDecision) {
3104
- const complianceEmoji = dec.compliance >= 90 ? "" : dec.compliance >= 70 ? "" : "";
3118
+ const complianceEmoji = dec.compliance >= 90 ? "\u2705" : dec.compliance >= 70 ? "\u26A0\uFE0F" : "\u274C";
3105
3119
  lines.push(
3106
3120
  `| ${dec.title} | ${dec.status} | ${dec.constraints} | ${dec.violations} | ${complianceEmoji} ${dec.compliance}% |`
3107
3121
  );
@@ -3110,15 +3124,15 @@ function formatMarkdownReport(report) {
3110
3124
  }
3111
3125
  lines.push("---");
3112
3126
  lines.push("");
3113
- lines.push("*Generated by [SpecBridge](https://github.com/specbridge/specbridge)*");
3127
+ lines.push("*Generated by [SpecBridge](https://github.com/nouatzi/specbridge)*");
3114
3128
  return lines.join("\n");
3115
3129
  }
3116
3130
  function formatProgressBar(percentage) {
3117
3131
  const width = 20;
3118
3132
  const filled = Math.round(percentage / 100 * width);
3119
3133
  const empty = width - filled;
3120
- const filledChar = "";
3121
- const emptyChar = "";
3134
+ const filledChar = "\u2588";
3135
+ const emptyChar = "\u2591";
3122
3136
  return `\`${filledChar.repeat(filled)}${emptyChar.repeat(empty)}\` ${percentage}%`;
3123
3137
  }
3124
3138
 
@@ -3236,7 +3250,7 @@ function formatContextAsMarkdown(context) {
3236
3250
  lines.push("### Constraints");
3237
3251
  lines.push("");
3238
3252
  for (const constraint of decision.constraints) {
3239
- const typeEmoji = constraint.type === "invariant" ? "" : constraint.type === "convention" ? "" : "";
3253
+ const typeEmoji = constraint.type === "invariant" ? "\u{1F512}" : constraint.type === "convention" ? "\u{1F4CB}" : "\u{1F4A1}";
3240
3254
  const severityBadge = `[${constraint.severity.toUpperCase()}]`;
3241
3255
  lines.push(`- ${typeEmoji} **${severityBadge}** ${constraint.rule}`);
3242
3256
  }
@@ -3293,7 +3307,7 @@ var contextCommand = new Command11("context").description("Generate architectura
3293
3307
  const config = await loadConfig(cwd);
3294
3308
  const output = await generateFormattedContext(file, config, {
3295
3309
  format: options.format,
3296
- includeRationale: options.noRationale !== true,
3310
+ includeRationale: options.rationale !== false,
3297
3311
  cwd
3298
3312
  });
3299
3313
  if (options.output) {