speclock 2.1.1 → 2.5.0

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/README.md CHANGED
@@ -58,6 +58,7 @@ No other tool does this. Not Claude's native memory. Not Mem0. Not CLAUDE.md fil
58
58
  | Git-aware (checkpoints, rollback) | No | No | No | **Yes** |
59
59
  | Drift detection | No | No | No | **Yes — scans changes against locks** |
60
60
  | CI/CD integration | No | No | No | **Yes — GitHub Actions** |
61
+ | **Hard enforcement (block violations)** | No | No | No | **Yes — hard mode blocks above threshold** |
61
62
  | Multi-agent timeline | No | No | No | **Yes** |
62
63
  | Cross-platform | Claude only | MCP only | Tool-specific | **Universal (MCP + npm)** |
63
64
 
@@ -192,10 +193,10 @@ Result: NO CONFLICT (confidence: 7%)
192
193
  | Mode | Platforms | How It Works |
193
194
  |------|-----------|--------------|
194
195
  | **MCP Remote** | Lovable, bolt.diy, Base44 | Connect via URL — no install needed |
195
- | **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` — 24 tools via MCP |
196
+ | **MCP Local** | Claude Code, Cursor, Windsurf, Cline | `npx speclock serve` — 28 tools via MCP |
196
197
  | **npm File-Based** | Bolt.new, Aider, Rocket.new | `npx speclock setup` — AI reads SPECLOCK.md + uses CLI |
197
198
 
198
- ## 24 MCP Tools
199
+ ## 28 MCP Tools
199
200
 
200
201
  ### Memory Management
201
202
  | Tool | Purpose |
@@ -249,6 +250,14 @@ Result: NO CONFLICT (confidence: 7%)
249
250
  | `speclock_verify_audit` | Verify HMAC audit chain integrity — tamper detection |
250
251
  | `speclock_export_compliance` | Generate SOC 2 / HIPAA / CSV compliance reports |
251
252
 
253
+ ### Hard Enforcement (v2.5)
254
+ | Tool | Purpose |
255
+ |------|---------|
256
+ | `speclock_set_enforcement` | Set enforcement mode: advisory (warn) or hard (block) |
257
+ | `speclock_override_lock` | Override a lock with justification — logged to audit trail |
258
+ | `speclock_semantic_audit` | Semantic pre-commit: analyze code changes vs locks |
259
+ | `speclock_override_history` | View lock override history for audit review |
260
+
252
261
  ## Auto-Guard: Locks That Actually Work
253
262
 
254
263
  When you add a lock, SpecLock **automatically finds and guards related files**:
@@ -317,6 +326,12 @@ speclock audit-verify # Verify HMAC audit chain integrity
317
326
  speclock export --format <soc2|hipaa|csv> # Compliance export
318
327
  speclock license # Show license tier and usage
319
328
 
329
+ # Hard Enforcement (v2.5)
330
+ speclock enforce <advisory|hard> # Set enforcement mode
331
+ speclock override <lockId> <reason> # Override a lock with justification
332
+ speclock overrides [--lock <id>] # Show override history
333
+ speclock audit-semantic # Semantic pre-commit audit
334
+
320
335
  # Other
321
336
  speclock status # Show brain summary
322
337
  speclock serve [--project <path>] # Start MCP server
@@ -372,6 +387,19 @@ SOC 2 reports include: constraint change history, access logs, decision audit tr
372
387
  ```
373
388
  Audits changed files against locks, posts PR comments, fails workflow on violations.
374
389
 
390
+ ## Hard Enforcement (v2.5)
391
+
392
+ ### Advisory vs Hard Mode
393
+ ```
394
+ Advisory mode (default): AI gets a warning, decides what to do
395
+ Hard mode: AI is BLOCKED — cannot proceed (MCP returns isError: true)
396
+ ```
397
+
398
+ - **Block threshold**: Configurable (default 70%). Only HIGH confidence conflicts block.
399
+ - **Override mechanism**: `speclock_override_lock` with a reason (logged to audit trail)
400
+ - **Escalation**: Lock overridden 3+ times → auto-creates a review note
401
+ - **Semantic pre-commit**: Parses actual git diff content, runs semantic analysis against locks
402
+
375
403
  ---
376
404
 
377
405
  ## Architecture
@@ -382,7 +410,7 @@ Audits changed files against locks, posts PR comments, fails workflow on violati
382
410
  └──────────────┬──────────────────┬────────────────────┘
383
411
  │ │
384
412
  MCP Protocol File-Based (npm)
385
- (24 tool calls) (reads SPECLOCK.md +
413
+ (28 tool calls) (reads SPECLOCK.md +
386
414
  .speclock/context/latest.md,
387
415
  runs CLI commands)
388
416
  │ │
@@ -419,4 +447,4 @@ MIT License - see [LICENSE](LICENSE) file.
419
447
 
420
448
  ---
421
449
 
422
- *SpecLock v2.1.0 — Semantic conflict detection + enterprise audit & compliance. 100% detection, 0% false positives. HMAC audit chain, SOC 2/HIPAA exports. Because remembering isn't enough — AI needs to respect boundaries.*
450
+ *SpecLock v2.5.0 — Semantic conflict detection + enterprise audit & compliance. 100% detection, 0% false positives. HMAC audit chain, SOC 2/HIPAA exports. Hard enforcement mode. Because remembering isn't enough — AI needs to respect boundaries.*
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "2.1.1",
4
- "description": "AI constraint engine with semantic conflict detection, HMAC audit chain, SOC 2/HIPAA compliance exports. 100% detection, 0% false positives. 24 MCP tools + CLI. Enterprise-ready memory + enforcement.",
3
+ "version": "2.5.0",
4
+ "description": "AI constraint engine with hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 100% detection, 0% false positives. 28 MCP tools + CLI. Enterprise-ready.",
5
5
  "type": "module",
6
6
  "main": "src/mcp/server.js",
7
7
  "bin": {
@@ -64,6 +64,7 @@
64
64
  "LICENSE"
65
65
  ],
66
66
  "devDependencies": {
67
- "esbuild": "^0.27.3"
67
+ "esbuild": "^0.27.3",
68
+ "jest": "^30.2.0"
68
69
  }
69
70
  }
package/src/cli/index.js CHANGED
@@ -23,6 +23,12 @@ import {
23
23
  verifyAuditChain,
24
24
  exportCompliance,
25
25
  getLicenseInfo,
26
+ enforceConflictCheck,
27
+ setEnforcementMode,
28
+ overrideLock,
29
+ getOverrideHistory,
30
+ getEnforcementConfig,
31
+ semanticAudit,
26
32
  } from "../core/engine.js";
27
33
  import { generateContext } from "../core/context.js";
28
34
  import { readBrain } from "../core/storage.js";
@@ -82,7 +88,7 @@ function refreshContext(root) {
82
88
 
83
89
  function printHelp() {
84
90
  console.log(`
85
- SpecLock v2.1.1 — AI Constraint Engine (Enterprise)
91
+ SpecLock v2.5.0 — AI Constraint Engine (Hard Enforcement + Semantic Pre-Commit)
86
92
  Developed by Sandeep Roy (github.com/sgroy10)
87
93
 
88
94
  Usage: speclock <command> [options]
@@ -105,7 +111,11 @@ Commands:
105
111
  hook install Install git pre-commit hook
106
112
  hook remove Remove git pre-commit hook
107
113
  audit Audit staged files against locks
114
+ audit-semantic Semantic audit: analyze code changes vs locks
108
115
  audit-verify Verify HMAC audit chain integrity
116
+ enforce <advisory|hard> Set enforcement mode (advisory=warn, hard=block)
117
+ override <lockId> <reason> Override a lock with justification
118
+ overrides [--lock <id>] Show override history
109
119
  export --format <soc2|hipaa|csv> Export compliance report
110
120
  license Show license tier and usage info
111
121
  context Generate and print context pack
@@ -385,10 +395,12 @@ Tip: When starting a new chat, tell the AI:
385
395
  console.error('Usage: speclock check "what you plan to do"');
386
396
  process.exit(1);
387
397
  }
388
- const result = checkConflict(root, text);
398
+ const result = enforceConflictCheck(root, text);
389
399
  if (result.hasConflict) {
390
- console.log(`\nCONFLICT DETECTED`);
400
+ console.log(`\n${result.blocked ? "BLOCKED" : "CONFLICT DETECTED"}`);
391
401
  console.log("=".repeat(50));
402
+ console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
403
+ console.log("");
392
404
  for (const lock of result.conflictingLocks) {
393
405
  console.log(` [${lock.level}] "${lock.text}"`);
394
406
  console.log(` Confidence: ${lock.confidence}%`);
@@ -400,6 +412,9 @@ Tip: When starting a new chat, tell the AI:
400
412
  console.log("");
401
413
  }
402
414
  console.log(result.analysis);
415
+ if (result.blocked) {
416
+ process.exit(1);
417
+ }
403
418
  } else {
404
419
  console.log(`No conflicts found. Safe to proceed with: "${text}"`);
405
420
  }
@@ -674,6 +689,95 @@ Tip: When starting a new chat, tell the AI:
674
689
  return;
675
690
  }
676
691
 
692
+ // --- ENFORCE (v2.5) ---
693
+ if (cmd === "enforce") {
694
+ const mode = args[0];
695
+ if (!mode || (mode !== "advisory" && mode !== "hard")) {
696
+ console.error("Usage: speclock enforce <advisory|hard> [--threshold 70]");
697
+ process.exit(1);
698
+ }
699
+ const flags = parseFlags(args.slice(1));
700
+ const options = {};
701
+ if (flags.threshold) options.blockThreshold = parseInt(flags.threshold, 10);
702
+ if (flags.override !== undefined) options.allowOverride = flags.override !== "false";
703
+ const result = setEnforcementMode(root, mode, options);
704
+ if (!result.success) {
705
+ console.error(result.error);
706
+ process.exit(1);
707
+ }
708
+ console.log(`\nEnforcement mode: ${result.mode.toUpperCase()}`);
709
+ console.log(`Block threshold: ${result.config.blockThreshold}%`);
710
+ console.log(`Overrides: ${result.config.allowOverride ? "allowed" : "disabled"}`);
711
+ if (result.mode === "hard") {
712
+ console.log(`\nHard mode active — conflicts above ${result.config.blockThreshold}% confidence will BLOCK actions.`);
713
+ }
714
+ return;
715
+ }
716
+
717
+ // --- OVERRIDE (v2.5) ---
718
+ if (cmd === "override") {
719
+ const lockId = args[0];
720
+ const reason = args.slice(1).join(" ");
721
+ if (!lockId || !reason) {
722
+ console.error("Usage: speclock override <lockId> <reason>");
723
+ process.exit(1);
724
+ }
725
+ const result = overrideLock(root, lockId, "(CLI override)", reason);
726
+ if (!result.success) {
727
+ console.error(result.error);
728
+ process.exit(1);
729
+ }
730
+ console.log(`Lock overridden: "${result.lockText}"`);
731
+ console.log(`Override count: ${result.overrideCount}`);
732
+ if (result.escalated) {
733
+ console.log(`\n${result.escalationMessage}`);
734
+ }
735
+ return;
736
+ }
737
+
738
+ // --- OVERRIDES (v2.5) ---
739
+ if (cmd === "overrides") {
740
+ const flags = parseFlags(args);
741
+ const result = getOverrideHistory(root, flags.lock || null);
742
+ if (result.total === 0) {
743
+ console.log("No overrides recorded.");
744
+ return;
745
+ }
746
+ console.log(`\nOverride History (${result.total})`);
747
+ console.log("=".repeat(50));
748
+ for (const o of result.overrides) {
749
+ console.log(`[${o.at.substring(0, 19)}] Lock: "${o.lockText}"`);
750
+ console.log(` Action: ${o.action}`);
751
+ console.log(` Reason: ${o.reason}`);
752
+ console.log("");
753
+ }
754
+ return;
755
+ }
756
+
757
+ // --- AUDIT-SEMANTIC (v2.5) ---
758
+ if (cmd === "audit-semantic") {
759
+ const result = semanticAudit(root);
760
+ console.log(`\nSemantic Pre-Commit Audit`);
761
+ console.log("=".repeat(50));
762
+ console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
763
+ console.log(`Files analyzed: ${result.filesChecked}`);
764
+ console.log(`Active locks: ${result.activeLocks}`);
765
+ console.log(`Violations: ${result.violations.length}`);
766
+ if (result.violations.length > 0) {
767
+ console.log("");
768
+ for (const v of result.violations) {
769
+ console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
770
+ console.log(` Lock: "${v.lockText}"`);
771
+ console.log(` Reason: ${v.reason}`);
772
+ if (v.addedLines !== undefined) {
773
+ console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
774
+ }
775
+ }
776
+ }
777
+ console.log(`\n${result.message}`);
778
+ process.exit(result.blocked ? 1 : 0);
779
+ }
780
+
677
781
  // --- STATUS ---
678
782
  if (cmd === "status") {
679
783
  showStatus(root);
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "2.1.1";
12
+ const VERSION = "2.5.0";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -0,0 +1,363 @@
1
+ /**
2
+ * SpecLock Conflict Detection Module
3
+ * Conflict checking, drift detection, lock suggestions, audit.
4
+ * Extracted from engine.js for modularity.
5
+ *
6
+ * Developed by Sandeep Roy (https://github.com/sgroy10)
7
+ */
8
+
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import {
12
+ nowIso,
13
+ readBrain,
14
+ writeBrain,
15
+ readEvents,
16
+ addViolation,
17
+ } from "./storage.js";
18
+ import { getStagedFiles } from "./git.js";
19
+ import { analyzeConflict } from "./semantics.js";
20
+ import { ensureInit } from "./memory.js";
21
+
22
+ // --- Legacy helpers (kept for pre-commit audit backward compat) ---
23
+
24
+ const NEGATION_WORDS = ["no", "not", "never", "without", "dont", "don't", "cannot", "can't", "shouldn't", "mustn't", "avoid", "prevent", "prohibit", "forbid", "disallow"];
25
+
26
+ function hasNegation(text) {
27
+ const lower = text.toLowerCase();
28
+ return NEGATION_WORDS.some((neg) => lower.includes(neg));
29
+ }
30
+
31
+ const FILE_KEYWORD_PATTERNS = [
32
+ { keywords: ["auth", "authentication", "login", "signup", "signin", "sign-in", "sign-up"], patterns: ["**/Auth*", "**/auth*", "**/Login*", "**/login*", "**/SignUp*", "**/signup*", "**/SignIn*", "**/signin*", "**/*Auth*", "**/*auth*"] },
33
+ { keywords: ["database", "db", "supabase", "firebase", "mongo", "postgres", "sql", "prisma"], patterns: ["**/supabase*", "**/firebase*", "**/database*", "**/db.*", "**/db/**", "**/prisma/**", "**/*Client*", "**/*client*"] },
34
+ { keywords: ["payment", "pay", "stripe", "billing", "checkout", "subscription"], patterns: ["**/payment*", "**/Payment*", "**/pay*", "**/Pay*", "**/stripe*", "**/Stripe*", "**/billing*", "**/Billing*", "**/checkout*", "**/Checkout*"] },
35
+ { keywords: ["api", "endpoint", "route", "routes"], patterns: ["**/api/**", "**/routes/**", "**/endpoints/**"] },
36
+ { keywords: ["config", "configuration", "settings", "env"], patterns: ["**/config*", "**/Config*", "**/settings*", "**/Settings*"] },
37
+ ];
38
+
39
+ function patternMatchesFile(pattern, filePath) {
40
+ const clean = pattern.replace(/\\/g, "/");
41
+ const fileLower = filePath.toLowerCase();
42
+ const patternLower = clean.toLowerCase();
43
+ const namePattern = patternLower.replace(/^\*\*\//, "");
44
+ if (!namePattern.includes("/")) {
45
+ const fileName = fileLower.split("/").pop();
46
+ const regex = new RegExp("^" + namePattern.replace(/\*/g, ".*") + "$");
47
+ if (regex.test(fileName)) return true;
48
+ const corePattern = namePattern.replace(/\*/g, "");
49
+ if (corePattern.length > 2 && fileName.includes(corePattern)) return true;
50
+ }
51
+ const regex = new RegExp("^" + patternLower.replace(/\*\*\//g, "(.*/)?").replace(/\*/g, "[^/]*") + "$");
52
+ return regex.test(fileLower);
53
+ }
54
+
55
+ const GUARD_TAG = "SPECLOCK-GUARD";
56
+
57
+ // --- Core functions ---
58
+
59
+ export function checkConflict(root, proposedAction) {
60
+ const brain = ensureInit(root);
61
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
62
+ if (activeLocks.length === 0) {
63
+ return {
64
+ hasConflict: false,
65
+ conflictingLocks: [],
66
+ analysis: "No active locks. No constraints to check against.",
67
+ };
68
+ }
69
+
70
+ const conflicting = [];
71
+ for (const lock of activeLocks) {
72
+ const result = analyzeConflict(proposedAction, lock.text);
73
+ if (result.isConflict) {
74
+ conflicting.push({
75
+ id: lock.id,
76
+ text: lock.text,
77
+ matchedKeywords: [],
78
+ confidence: result.confidence,
79
+ level: result.level,
80
+ reasons: result.reasons,
81
+ });
82
+ }
83
+ }
84
+
85
+ if (conflicting.length === 0) {
86
+ return {
87
+ hasConflict: false,
88
+ conflictingLocks: [],
89
+ analysis: `Checked against ${activeLocks.length} active lock(s). No conflicts detected (semantic analysis v2). Proceed with caution.`,
90
+ };
91
+ }
92
+
93
+ conflicting.sort((a, b) => b.confidence - a.confidence);
94
+
95
+ const details = conflicting
96
+ .map(
97
+ (c) =>
98
+ `- [${c.level}] "${c.text}" (confidence: ${c.confidence}%)\n Reasons: ${c.reasons.join("; ")}`
99
+ )
100
+ .join("\n");
101
+
102
+ const result = {
103
+ hasConflict: true,
104
+ conflictingLocks: conflicting,
105
+ analysis: `Potential conflict with ${conflicting.length} lock(s):\n${details}\nReview before proceeding.`,
106
+ };
107
+
108
+ addViolation(brain, {
109
+ at: nowIso(),
110
+ action: proposedAction,
111
+ locks: conflicting.map((c) => ({ id: c.id, text: c.text, confidence: c.confidence, level: c.level })),
112
+ topLevel: conflicting[0].level,
113
+ topConfidence: conflicting[0].confidence,
114
+ });
115
+ writeBrain(root, brain);
116
+
117
+ return result;
118
+ }
119
+
120
+ export async function checkConflictAsync(root, proposedAction) {
121
+ try {
122
+ const { llmCheckConflict } = await import("./llm-checker.js");
123
+ const llmResult = await llmCheckConflict(root, proposedAction);
124
+ if (llmResult) return llmResult;
125
+ } catch (_) {
126
+ // LLM checker not available — fall through
127
+ }
128
+ return checkConflict(root, proposedAction);
129
+ }
130
+
131
+ export function suggestLocks(root) {
132
+ const brain = ensureInit(root);
133
+ const suggestions = [];
134
+
135
+ for (const dec of brain.decisions) {
136
+ const lower = dec.text.toLowerCase();
137
+ if (/\b(always|must|only|exclusively|never|required)\b/.test(lower)) {
138
+ suggestions.push({
139
+ text: dec.text,
140
+ source: "decision",
141
+ sourceId: dec.id,
142
+ reason: `Decision contains strong commitment language — consider promoting to a lock`,
143
+ });
144
+ }
145
+ }
146
+
147
+ for (const note of brain.notes) {
148
+ const lower = note.text.toLowerCase();
149
+ if (/\b(never|must not|do not|don't|avoid|prohibit|forbidden)\b/.test(lower)) {
150
+ suggestions.push({
151
+ text: note.text,
152
+ source: "note",
153
+ sourceId: note.id,
154
+ reason: `Note contains prohibitive language — consider promoting to a lock`,
155
+ });
156
+ }
157
+ }
158
+
159
+ const existingLockTexts = brain.specLock.items
160
+ .filter((l) => l.active)
161
+ .map((l) => l.text.toLowerCase());
162
+
163
+ const commonPatterns = [
164
+ { keyword: "api", suggestion: "No breaking changes to public API" },
165
+ { keyword: "database", suggestion: "No destructive database migrations without backup" },
166
+ { keyword: "deploy", suggestion: "All deployments must pass CI checks" },
167
+ { keyword: "security", suggestion: "No secrets or credentials in source code" },
168
+ { keyword: "test", suggestion: "No merging without passing tests" },
169
+ ];
170
+
171
+ const allText = [
172
+ brain.goal.text,
173
+ ...brain.decisions.map((d) => d.text),
174
+ ...brain.notes.map((n) => n.text),
175
+ ].join(" ").toLowerCase();
176
+
177
+ for (const pattern of commonPatterns) {
178
+ if (allText.includes(pattern.keyword)) {
179
+ const alreadyLocked = existingLockTexts.some((t) =>
180
+ t.includes(pattern.keyword)
181
+ );
182
+ if (!alreadyLocked) {
183
+ suggestions.push({
184
+ text: pattern.suggestion,
185
+ source: "pattern",
186
+ sourceId: null,
187
+ reason: `Project mentions "${pattern.keyword}" but has no lock protecting it`,
188
+ });
189
+ }
190
+ }
191
+ }
192
+
193
+ return { suggestions, totalLocks: brain.specLock.items.filter((l) => l.active).length };
194
+ }
195
+
196
+ export function detectDrift(root) {
197
+ const brain = ensureInit(root);
198
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
199
+ if (activeLocks.length === 0) {
200
+ return { drifts: [], status: "no_locks", message: "No active locks to check against." };
201
+ }
202
+
203
+ const drifts = [];
204
+
205
+ for (const change of brain.state.recentChanges) {
206
+ for (const lock of activeLocks) {
207
+ const result = analyzeConflict(change.summary, lock.text);
208
+ if (result.isConflict) {
209
+ drifts.push({
210
+ lockId: lock.id,
211
+ lockText: lock.text,
212
+ changeEventId: change.eventId,
213
+ changeSummary: change.summary,
214
+ changeAt: change.at,
215
+ matchedTerms: result.reasons,
216
+ severity: result.level === "HIGH" ? "high" : "medium",
217
+ });
218
+ }
219
+ }
220
+ }
221
+
222
+ for (const revert of brain.state.reverts) {
223
+ drifts.push({
224
+ lockId: null,
225
+ lockText: "(git revert detected)",
226
+ changeEventId: revert.eventId,
227
+ changeSummary: `Git ${revert.kind} to ${revert.target.substring(0, 12)}`,
228
+ changeAt: revert.at,
229
+ matchedTerms: ["revert"],
230
+ severity: "high",
231
+ });
232
+ }
233
+
234
+ const status = drifts.length === 0 ? "clean" : "drift_detected";
235
+ const message = drifts.length === 0
236
+ ? `All clear. ${activeLocks.length} lock(s) checked against ${brain.state.recentChanges.length} recent change(s). No drift detected.`
237
+ : `WARNING: ${drifts.length} potential drift(s) detected. Review immediately.`;
238
+
239
+ return { drifts, status, message };
240
+ }
241
+
242
+ export function generateReport(root) {
243
+ const brain = ensureInit(root);
244
+ const violations = brain.state.violations || [];
245
+
246
+ if (violations.length === 0) {
247
+ return {
248
+ totalViolations: 0,
249
+ violationsByLock: {},
250
+ mostTestedLocks: [],
251
+ recentViolations: [],
252
+ summary: "No violations recorded yet. SpecLock is watching.",
253
+ };
254
+ }
255
+
256
+ const byLock = {};
257
+ for (const v of violations) {
258
+ for (const lock of v.locks) {
259
+ if (!byLock[lock.text]) {
260
+ byLock[lock.text] = { count: 0, lockId: lock.id, text: lock.text };
261
+ }
262
+ byLock[lock.text].count++;
263
+ }
264
+ }
265
+
266
+ const mostTested = Object.values(byLock).sort((a, b) => b.count - a.count);
267
+ const recent = violations.slice(0, 10).map((v) => ({
268
+ at: v.at,
269
+ action: v.action,
270
+ topLevel: v.topLevel,
271
+ topConfidence: v.topConfidence,
272
+ lockCount: v.locks.length,
273
+ }));
274
+
275
+ const oldest = violations[violations.length - 1];
276
+ const newest = violations[0];
277
+
278
+ return {
279
+ totalViolations: violations.length,
280
+ timeRange: { from: oldest.at, to: newest.at },
281
+ violationsByLock: byLock,
282
+ mostTestedLocks: mostTested.slice(0, 5),
283
+ recentViolations: recent,
284
+ summary: `SpecLock blocked ${violations.length} violation(s). Most tested lock: "${mostTested[0].text}" (${mostTested[0].count} blocks).`,
285
+ };
286
+ }
287
+
288
+ export function auditStagedFiles(root) {
289
+ const brain = ensureInit(root);
290
+ const activeLocks = brain.specLock.items.filter((l) => l.active !== false);
291
+
292
+ if (activeLocks.length === 0) {
293
+ return { passed: true, violations: [], checkedFiles: 0, activeLocks: 0, message: "No active locks. Audit passed." };
294
+ }
295
+
296
+ const stagedFiles = getStagedFiles(root);
297
+ if (stagedFiles.length === 0) {
298
+ return { passed: true, violations: [], checkedFiles: 0, activeLocks: activeLocks.length, message: "No staged files. Audit passed." };
299
+ }
300
+
301
+ const violations = [];
302
+
303
+ for (const file of stagedFiles) {
304
+ const fullPath = path.join(root, file);
305
+ if (fs.existsSync(fullPath)) {
306
+ try {
307
+ const content = fs.readFileSync(fullPath, "utf-8");
308
+ if (content.includes(GUARD_TAG)) {
309
+ violations.push({
310
+ file,
311
+ reason: "File has SPECLOCK-GUARD header — it is locked and must not be modified",
312
+ lockText: "(file-level guard)",
313
+ severity: "HIGH",
314
+ });
315
+ continue;
316
+ }
317
+ } catch (_) {}
318
+ }
319
+
320
+ const fileLower = file.toLowerCase();
321
+ for (const lock of activeLocks) {
322
+ const lockLower = lock.text.toLowerCase();
323
+ const lockHasNegation = hasNegation(lockLower);
324
+ if (!lockHasNegation) continue;
325
+
326
+ for (const group of FILE_KEYWORD_PATTERNS) {
327
+ const lockMatchesKeyword = group.keywords.some((kw) => lockLower.includes(kw));
328
+ if (!lockMatchesKeyword) continue;
329
+
330
+ const fileMatchesPattern = group.patterns.some((pattern) => patternMatchesFile(pattern, fileLower) || patternMatchesFile(pattern, fileLower.split("/").pop()));
331
+ if (fileMatchesPattern) {
332
+ violations.push({
333
+ file,
334
+ reason: `File matches lock keyword pattern`,
335
+ lockText: lock.text,
336
+ severity: "MEDIUM",
337
+ });
338
+ break;
339
+ }
340
+ }
341
+ }
342
+ }
343
+
344
+ const seen = new Set();
345
+ const unique = violations.filter((v) => {
346
+ if (seen.has(v.file)) return false;
347
+ seen.add(v.file);
348
+ return true;
349
+ });
350
+
351
+ const passed = unique.length === 0;
352
+ const message = passed
353
+ ? `Audit passed. ${stagedFiles.length} file(s) checked against ${activeLocks.length} lock(s).`
354
+ : `AUDIT FAILED: ${unique.length} violation(s) in ${stagedFiles.length} staged file(s).`;
355
+
356
+ return {
357
+ passed,
358
+ violations: unique,
359
+ checkedFiles: stagedFiles.length,
360
+ activeLocks: activeLocks.length,
361
+ message,
362
+ };
363
+ }