speclock 5.5.5 → 5.5.6

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/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "5.5.5",
5
+ "version": "5.5.6",
6
6
 
7
7
  "mcpName": "io.github.sgroy10/speclock",
8
8
 
package/src/cli/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { fileURLToPath } from "url";
4
- import { getStagedDiff, parseDiff } from "../core/pre-commit-semantic.js";
4
+ import { getStagedDiff, parseDiff, shouldSkipForSemanticAudit } from "../core/pre-commit-semantic.js";
5
5
  import {
6
6
  ensureInit,
7
7
  setGoal,
@@ -286,7 +286,7 @@ export function initFromRulePack(root, framework) {
286
286
 
287
287
  function printHelp() {
288
288
  console.log(`
289
- SpecLock v5.5.5 — Your AI has rules. SpecLock makes them unbreakable.
289
+ SpecLock v5.5.6 — Your AI has rules. SpecLock makes them unbreakable.
290
290
  Developed by Sandeep Roy (github.com/sgroy10)
291
291
 
292
292
  Usage: speclock <command> [options]
@@ -1243,6 +1243,10 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1243
1243
  // --- AUDIT-SEMANTIC (v2.5) ---
1244
1244
  if (cmd === "audit-semantic") {
1245
1245
  const flags = parseFlags(args);
1246
+ const verbose =
1247
+ flags.verbose === true ||
1248
+ process.env.SPECLOCK_VERBOSE === "1" ||
1249
+ process.env.SPECLOCK_VERBOSE === "true";
1246
1250
  const result = semanticAudit(root);
1247
1251
 
1248
1252
  // --- Commit-message + diff-content semantic check ---
@@ -1269,9 +1273,15 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1269
1273
  } catch { /* ignore */ }
1270
1274
  }
1271
1275
 
1272
- // 2. Collect added lines from the staged diff.
1276
+ // 2. Collect added lines from the staged diff, skipping SpecLock-internal
1277
+ // files (see shouldSkipForSemanticAudit). Those files' contents literally
1278
+ // restate the locks, so scanning their added lines produces 100%
1279
+ // false-positive matches for every lock in the brain.
1273
1280
  const diffText = getStagedDiff(root);
1274
- const fileChanges = diffText ? parseDiff(diffText) : [];
1281
+ const allParsedChanges = diffText ? parseDiff(diffText) : [];
1282
+ const fileChanges = allParsedChanges.filter(
1283
+ (fc) => !shouldSkipForSemanticAudit(fc.file, root)
1284
+ );
1275
1285
  const addedSnippets = [];
1276
1286
  for (const fc of fileChanges) {
1277
1287
  for (const line of fc.addedLines) {
@@ -1354,15 +1364,40 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1354
1364
  process.env.SPECLOCK_STRICT === "true" ||
1355
1365
  result.blocked;
1356
1366
 
1367
+ // --- Three-tier output filter (v5.5.6) ---
1368
+ // Investor audit: walls of LOW-confidence matches are user-hostile.
1369
+ // Only HIGH and MEDIUM print by default. LOW rolls up into a one-liner.
1370
+ // --verbose / SPECLOCK_VERBOSE=1 shows everything.
1371
+ const OUTPUT_MIN_CONFIDENCE = 40; // below this = "LOW", hidden by default
1372
+ const MAX_VISIBLE_VIOLATIONS = 10; // hard cap on printed items
1373
+
1374
+ const allViolations = result.violations || [];
1375
+ const highViolations = allViolations.filter((v) => (v.confidence || 0) >= 70);
1376
+ const mediumViolations = allViolations.filter(
1377
+ (v) => (v.confidence || 0) >= OUTPUT_MIN_CONFIDENCE && (v.confidence || 0) < 70
1378
+ );
1379
+ const lowViolations = allViolations.filter(
1380
+ (v) => (v.confidence || 0) < OUTPUT_MIN_CONFIDENCE
1381
+ );
1382
+
1383
+ // What actually gets printed
1384
+ const visibleViolations = verbose
1385
+ ? allViolations
1386
+ : [...highViolations, ...mediumViolations];
1387
+
1357
1388
  console.log(`\nSemantic Pre-Commit Audit`);
1358
1389
  console.log("=".repeat(50));
1359
1390
  console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
1360
- console.log(`Files analyzed: ${result.filesChecked}`);
1391
+ const filesLine = result.filesSkipped
1392
+ ? `Files analyzed: ${result.filesChecked} (${result.filesSkipped} skipped)`
1393
+ : `Files analyzed: ${result.filesChecked}`;
1394
+ console.log(filesLine);
1361
1395
  console.log(`Active locks: ${result.activeLocks}`);
1362
- console.log(`Violations: ${result.violations.length}`);
1363
- if (result.violations.length > 0) {
1396
+
1397
+ if (visibleViolations.length > 0) {
1364
1398
  console.log("");
1365
- for (const v of result.violations) {
1399
+ const toPrint = visibleViolations.slice(0, MAX_VISIBLE_VIOLATIONS);
1400
+ for (const v of toPrint) {
1366
1401
  console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
1367
1402
  console.log(` Lock: "${v.lockText}"`);
1368
1403
  console.log(` Reason: ${v.reason}`);
@@ -1370,17 +1405,48 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
1370
1405
  console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
1371
1406
  }
1372
1407
  }
1408
+ const hiddenByCap = visibleViolations.length - toPrint.length;
1409
+ if (hiddenByCap > 0) {
1410
+ console.log(` ... + ${hiddenByCap} more (output capped at ${MAX_VISIBLE_VIOLATIONS})`);
1411
+ }
1412
+ }
1413
+
1414
+ // LOW-confidence rollup
1415
+ if (!verbose && lowViolations.length > 0) {
1416
+ console.log(
1417
+ ` + ${lowViolations.length} low-confidence match(es) hidden (use --verbose or SPECLOCK_VERBOSE=1 to see)`
1418
+ );
1419
+ }
1420
+
1421
+ // --- New summary line ---
1422
+ console.log("");
1423
+ let summaryLine;
1424
+ if (highViolations.length === 0 && mediumViolations.length === 0) {
1425
+ summaryLine = `[OK] ${result.filesChecked} file(s) checked, no concerns.`;
1426
+ } else if (highViolations.length > 0) {
1427
+ summaryLine = `[!] ${highViolations.length} HIGH-confidence concern(s) — review before merging.`;
1428
+ } else {
1429
+ summaryLine = `[i] ${mediumViolations.length} medium-confidence note(s) (informational).`;
1430
+ }
1431
+ console.log(summaryLine);
1432
+
1433
+ // Preserve the machine-readable status message for any callers that grep for it
1434
+ if (result.blocked) {
1435
+ console.log(
1436
+ `BLOCKED: ${highViolations.length} high-confidence violation(s) — hard enforcement active.`
1437
+ );
1373
1438
  }
1374
- console.log(`\n${result.message}`);
1375
1439
 
1376
- if (result.violations.length > 0 && !strict) {
1440
+ if (highViolations.length > 0 && !strict) {
1377
1441
  console.log("\nWarning mode active — commit allowed. To enforce hard blocks, run:");
1378
1442
  console.log(" speclock audit-semantic --strict");
1379
1443
  console.log(" SPECLOCK_STRICT=1 git commit ...");
1380
1444
  console.log(" speclock enforce hard (persistent, project-wide)");
1381
1445
  }
1382
1446
 
1383
- process.exit(strict && result.violations.length > 0 ? 1 : 0);
1447
+ // Blocking decision is still driven by the engine's 70% threshold, so
1448
+ // only HIGH-confidence matches can ever cause a non-zero exit.
1449
+ process.exit(strict && highViolations.length > 0 ? 1 : 0);
1384
1450
  }
1385
1451
 
1386
1452
  // --- AUTH (v3.0) ---
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "5.5.5";
12
+ const VERSION = "5.5.6";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -16,6 +16,7 @@ import { analyzeConflict } from "./semantics.js";
16
16
  import { getEnforcementConfig } from "./enforcer.js";
17
17
 
18
18
  const GUARD_TAG = "SPECLOCK-GUARD";
19
+ const SPECLOCK_AUTOGEN_MARKER = "SpecLock";
19
20
  const MAX_LINES_PER_FILE = 500;
20
21
  const BINARY_EXTENSIONS = new Set([
21
22
  "png", "jpg", "jpeg", "gif", "bmp", "ico", "svg", "webp",
@@ -27,6 +28,98 @@ const BINARY_EXTENSIONS = new Set([
27
28
  "lock", "map",
28
29
  ]);
29
30
 
31
+ // Files / dirs that SpecLock itself auto-creates during `protect` or that
32
+ // are just noise for semantic analysis. These are ALWAYS skipped from the
33
+ // diff-level semantic audit because matching against them produces nothing
34
+ // but false positives (e.g. rules files describe the same concepts the
35
+ // locks describe, so they always "conflict" with themselves).
36
+ const ALWAYS_SKIP_EXACT = new Set([
37
+ ".cursor/rules/speclock.mdc",
38
+ ".windsurf/rules/speclock.md",
39
+ ".aider.conf.yml",
40
+ ".mcp.json",
41
+ "package-lock.json",
42
+ "yarn.lock",
43
+ "pnpm-lock.yaml",
44
+ "Cargo.lock",
45
+ "poetry.lock",
46
+ "Gemfile.lock",
47
+ "composer.lock",
48
+ ]);
49
+
50
+ // Directory prefixes that are always skipped.
51
+ const ALWAYS_SKIP_DIR_PREFIXES = [
52
+ ".speclock/",
53
+ "node_modules/",
54
+ "dist/",
55
+ "build/",
56
+ ".next/",
57
+ ".nuxt/",
58
+ "__pycache__/",
59
+ ".venv/",
60
+ "venv/",
61
+ ".cache/",
62
+ "coverage/",
63
+ ".turbo/",
64
+ ];
65
+
66
+ // Files that are skipped ONLY if their content carries the SpecLock
67
+ // auto-generated marker (so hand-written AGENTS.md etc. still get audited).
68
+ // CLAUDE.md is included because `speclock protect` seeds it with the active
69
+ // locks — on the initial commit after protect, the file literally IS the
70
+ // locks, so every semantic check would produce a false positive.
71
+ const CONDITIONAL_SKIP_IF_AUTOGEN = new Set([
72
+ "AGENTS.md",
73
+ "GEMINI.md",
74
+ "CLAUDE.md",
75
+ ".github/copilot-instructions.md",
76
+ ]);
77
+
78
+ /**
79
+ * Normalize a path to forward slashes for comparison.
80
+ */
81
+ function normalizePath(p) {
82
+ return (p || "").replace(/\\/g, "/");
83
+ }
84
+
85
+ /**
86
+ * Decide whether a file should be skipped by the semantic pre-commit audit.
87
+ * This is the single source of truth for "is this a SpecLock internal file
88
+ * or generated noise we should not audit".
89
+ *
90
+ * @param {string} file - repo-relative path
91
+ * @param {string} root - repo root (to check content of conditional files)
92
+ * @returns {boolean} true if the file should be skipped
93
+ */
94
+ export function shouldSkipForSemanticAudit(file, root) {
95
+ const norm = normalizePath(file);
96
+
97
+ // 1. Binary extensions
98
+ const ext = path.extname(norm).slice(1).toLowerCase();
99
+ if (BINARY_EXTENSIONS.has(ext)) return true;
100
+
101
+ // 2. Exact path matches (lockfiles, auto-generated rules files, .mcp.json)
102
+ if (ALWAYS_SKIP_EXACT.has(norm)) return true;
103
+
104
+ // 3. Directory prefix matches
105
+ for (const prefix of ALWAYS_SKIP_DIR_PREFIXES) {
106
+ if (norm === prefix.slice(0, -1) || norm.startsWith(prefix)) return true;
107
+ }
108
+
109
+ // 4. Conditionally-skipped files (only if they carry the auto-gen marker)
110
+ if (CONDITIONAL_SKIP_IF_AUTOGEN.has(norm)) {
111
+ try {
112
+ const fullPath = path.join(root, file);
113
+ if (fs.existsSync(fullPath)) {
114
+ const content = fs.readFileSync(fullPath, "utf-8");
115
+ if (content.includes(SPECLOCK_AUTOGEN_MARKER)) return true;
116
+ }
117
+ } catch { /* ignore read errors */ }
118
+ }
119
+
120
+ return false;
121
+ }
122
+
30
123
  /**
31
124
  * Parse a unified diff into per-file change blocks.
32
125
  * Returns array of { file, addedLines, removedLines, hunks }.
@@ -191,8 +284,14 @@ export function semanticAudit(root) {
191
284
  };
192
285
  }
193
286
 
194
- // Parse diff into per-file changes
195
- const fileChanges = parseDiff(diff);
287
+ // Parse diff into per-file changes, then drop SpecLock-internal / generated
288
+ // files so we don't flood the user with false positives from files that
289
+ // SpecLock itself creates or manages.
290
+ const allFileChanges = parseDiff(diff);
291
+ const fileChanges = allFileChanges.filter(
292
+ (fc) => !shouldSkipForSemanticAudit(fc.file, root)
293
+ );
294
+ const skippedCount = allFileChanges.length - fileChanges.length;
196
295
  const violations = [];
197
296
 
198
297
  for (const fc of fileChanges) {
@@ -276,6 +375,7 @@ export function semanticAudit(root) {
276
375
  blocked,
277
376
  violations: uniqueViolations,
278
377
  filesChecked: fileChanges.length,
378
+ filesSkipped: skippedCount,
279
379
  activeLocks: activeLocks.length,
280
380
  mode: config.mode,
281
381
  threshold: config.blockThreshold,
@@ -89,7 +89,7 @@
89
89
  <div class="header">
90
90
  <div>
91
91
  <h1><span>SpecLock</span> Dashboard</h1>
92
- <div class="meta">v5.5.5 &mdash; Your AI has rules. SpecLock makes them unbreakable.</div>
92
+ <div class="meta">v5.5.6 &mdash; Your AI has rules. SpecLock makes them unbreakable.</div>
93
93
  </div>
94
94
  <div style="display:flex;align-items:center;gap:12px;">
95
95
  <span id="health-badge" class="status-badge healthy">Loading...</span>
@@ -182,7 +182,7 @@
182
182
  </div>
183
183
 
184
184
  <div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
185
- SpecLock v5.5.5 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v5.5.6 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
186
186
  </div>
187
187
 
188
188
  <script>
@@ -113,7 +113,7 @@ import { fileURLToPath } from "url";
113
113
  import _path from "path";
114
114
 
115
115
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
116
- const VERSION = "5.5.5";
116
+ const VERSION = "5.5.6";
117
117
  const AUTHOR = "Sandeep Roy";
118
118
  const START_TIME = Date.now();
119
119
 
package/src/mcp/server.js CHANGED
@@ -126,7 +126,7 @@ const PROJECT_ROOT =
126
126
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
127
127
 
128
128
  // --- MCP Server ---
129
- const VERSION = "5.5.5";
129
+ const VERSION = "5.5.6";
130
130
  const AUTHOR = "Sandeep Roy";
131
131
 
132
132
  const server = new McpServer(