speclock 5.5.4 → 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.
@@ -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.4";
12
+ const VERSION = "5.5.6";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
package/src/core/hooks.js CHANGED
@@ -8,11 +8,14 @@ const HOOK_MARKER = "# SPECLOCK-HOOK";
8
8
 
9
9
  const HOOK_SCRIPT = `#!/bin/sh
10
10
  ${HOOK_MARKER} — Do not remove this line
11
- # SpecLock pre-commit hook: checks staged files against active locks
11
+ # SpecLock pre-commit hook: runs semantic audit of staged diff + commit message
12
+ # against active locks. Unlike the legacy 'audit' subcommand, this one feeds
13
+ # the actual diff content AND the commit message through the semantic conflict
14
+ # engine — the same one used by 'speclock check'.
12
15
  # Install: npx speclock hook install
13
16
  # Remove: npx speclock hook remove
14
17
 
15
- npx speclock audit
18
+ npx speclock audit-semantic --pre-commit
16
19
  exit $?
17
20
  `;
18
21
 
@@ -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,