speclock 5.0.2 → 5.1.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
@@ -8,7 +8,7 @@
8
8
  <a href="https://www.npmjs.com/package/speclock"><img src="https://img.shields.io/npm/v/speclock.svg?style=flat-square&color=4F46E5" alt="npm version" /></a>
9
9
  <a href="https://www.npmjs.com/package/speclock"><img src="https://img.shields.io/npm/dm/speclock.svg?style=flat-square&color=22C55E" alt="npm downloads" /></a>
10
10
  <a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="MIT License" /></a>
11
- <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-39_tools-green.svg?style=flat-square" alt="MCP 39 tools" /></a>
11
+ <a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-40_tools-green.svg?style=flat-square" alt="MCP 40 tools" /></a>
12
12
  </p>
13
13
 
14
14
  <p align="center">
@@ -32,7 +32,7 @@ AI: ⚠️ BLOCKED — violates lock "Never touch the auth system"
32
32
  Should I find another approach?
33
33
  ```
34
34
 
35
- **940 tests. 99.4% pass rate. 0 false positives across 13 suites. Gemini Flash hybrid, Spec Compiler, Code Graph, Typed Constraints, Python SDK, ROS2 integration.**
35
+ **997 tests. 99.4% pass rate. 0 false positives across 14 suites. Gemini Flash hybrid, Spec Compiler, Code Graph, Typed Constraints, Python SDK, ROS2 integration.**
36
36
 
37
37
  ---
38
38
 
@@ -339,7 +339,7 @@ GET /api/v2/graph/lock-map
339
339
 
340
340
  ---
341
341
 
342
- ## 39 MCP Tools
342
+ ## 40 MCP Tools
343
343
 
344
344
  <details>
345
345
  <summary><b>Memory</b> — goal, locks, decisions, notes, deploy facts</summary>
@@ -508,7 +508,7 @@ The AI opens the file and sees:
508
508
  │ AI Tool (Claude Code, Cursor, Bolt.new...) │
509
509
  └────────────┬──────────────────┬──────────────────┘
510
510
  │ │
511
- MCP Protocol (39 tools) npm File-Based
511
+ MCP Protocol (40 tools) npm File-Based
512
512
  │ (SPECLOCK.md + CLI)
513
513
  │ │
514
514
  ┌────────────▼──────────────────▼──────────────────┐
@@ -571,7 +571,7 @@ The AI opens the file and sees:
571
571
  | Python SDK | 62 | 100% | pip install, constraint checking |
572
572
  | ROS2 Guardian | 26 | 100% | Robot safety constraint enforcement |
573
573
  | Real-World Testers | 105 | 95% | 5 developers, 30+ locks, diverse domains |
574
- | **Total** | **940** | **99.4%** | **13 suites, 15 domains** |
574
+ | **Total** | **940** | **99.4%** | **14 suites, 15 domains** |
575
575
 
576
576
  Tested across: fintech, e-commerce, IoT, healthcare, SaaS, gaming, biotech, aerospace, payments, payroll, robotics, autonomous systems. All 11 Indian payment gateways detected. Zero false positives on UI/cosmetic actions.
577
577
 
@@ -611,4 +611,4 @@ Built by **[Sandeep Roy](https://github.com/sgroy10)**
611
611
 
612
612
  ---
613
613
 
614
- <p align="center"><i>v5.0.0 — 940 tests, 99.4% pass rate, 39 MCP tools, Spec Compiler, Code Graph, Typed Constraints, Python SDK, ROS2, REST API v2. Because remembering isn't enough.</i></p>
614
+ <p align="center"><i>v5.0.0 — 997 tests, 99.4% pass rate, 40 MCP tools, Spec Compiler, Code Graph, Typed Constraints, Python SDK, ROS2, REST API v2. Because remembering isn't enough.</i></p>
package/package.json CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "5.0.2",
5
+ "version": "5.1.0",
6
6
 
7
- "description": "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK, ROS2 integration. 39 MCP tools, Gemini LLM hybrid, HMAC audit chain, RBAC, encryption, SOC 2/HIPAA compliance.",
7
+ "description": "AI Constraint Engine for autonomous systems governance. Patch Gateway (ALLOW/WARN/BLOCK change verdicts), Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK, ROS2 integration. 40 MCP tools, Gemini LLM hybrid, HMAC audit chain, RBAC, encryption, SOC 2/HIPAA compliance.",
8
8
 
9
9
  "type": "module",
10
10
 
package/src/cli/index.js CHANGED
@@ -117,7 +117,7 @@ function refreshContext(root) {
117
117
 
118
118
  function printHelp() {
119
119
  console.log(`
120
- SpecLock v5.0.0 — AI Constraint Engine (Spec Compiler + Code Graph + Typed Constraints + Python SDK + ROS2 + REST API v2 + Gemini LLM + Policy-as-Code + Auth + RBAC + Encryption)
120
+ SpecLock v5.1.0 — AI Constraint Engine (Spec Compiler + Code Graph + Typed Constraints + Python SDK + ROS2 + REST API v2 + Gemini LLM + Policy-as-Code + Auth + RBAC + Encryption)
121
121
  Developed by Sandeep Roy (github.com/sgroy10)
122
122
 
123
123
  Usage: speclock <command> [options]
@@ -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.0.0";
12
+ const VERSION = "5.1.0";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -638,3 +638,9 @@ export {
638
638
  getModules,
639
639
  getCriticalPaths,
640
640
  } from "./code-graph.js";
641
+
642
+ // --- Patch Gateway (v5.1) ---
643
+ export {
644
+ reviewPatch,
645
+ reviewPatchAsync,
646
+ } from "./patch-gateway.js";
@@ -0,0 +1,346 @@
1
+ // ===================================================================
2
+ // SpecLock Patch Gateway — Change Decision Engine
3
+ // Combines semantic conflict detection, lock-to-file mapping, blast
4
+ // radius analysis, and typed constraints into a single ALLOW/WARN/BLOCK
5
+ // verdict for any proposed change.
6
+ //
7
+ // Developed by Sandeep Roy (https://github.com/sgroy10)
8
+ // ===================================================================
9
+
10
+ import { readBrain } from "./storage.js";
11
+ import { ensureInit } from "./memory.js";
12
+ import { analyzeConflict } from "./semantics.js";
13
+ import { checkAllTypedConstraints } from "./typed-constraints.js";
14
+ import { getOrBuildGraph, getBlastRadius, mapLocksToFiles } from "./code-graph.js";
15
+
16
+ // --- Thresholds ---
17
+
18
+ const BLOCK_CONFIDENCE = 70; // Semantic confidence >= 70 → BLOCK
19
+ const WARN_CONFIDENCE = 40; // Semantic confidence >= 40 → WARN
20
+ const HIGH_BLAST_RADIUS = 20; // > 20% impact → adds to risk
21
+ const MED_BLAST_RADIUS = 10; // > 10% impact → moderate risk
22
+
23
+ // --- Main Gateway ---
24
+
25
+ /**
26
+ * Review a proposed change against all active constraints.
27
+ *
28
+ * @param {string} root - Project root
29
+ * @param {object} opts
30
+ * @param {string} opts.description - What the change does (natural language)
31
+ * @param {string[]} [opts.files] - Files being changed (project-relative paths)
32
+ * @param {boolean} [opts.includeGraph=true] - Whether to run blast radius analysis
33
+ * @returns {object} Verdict with risk score, reasons, and summary
34
+ */
35
+ export function reviewPatch(root, { description, files = [], includeGraph = true }) {
36
+ if (!description || typeof description !== "string" || !description.trim()) {
37
+ return {
38
+ verdict: "ERROR",
39
+ riskScore: 0,
40
+ error: "description is required (describe what the change does)",
41
+ reasons: [],
42
+ summary: "No change description provided.",
43
+ };
44
+ }
45
+
46
+ const brain = ensureInit(root);
47
+ const activeLocks = (brain.specLock?.items || []).filter(l => l.active !== false);
48
+ const textLocks = activeLocks.filter(l => !l.constraintType);
49
+ const typedLocks = activeLocks.filter(l => l.constraintType);
50
+
51
+ const reasons = [];
52
+ let maxConfidence = 0;
53
+
54
+ // --- Step 1: Semantic conflict check against all text locks ---
55
+ for (const lock of textLocks) {
56
+ const result = analyzeConflict(description, lock.text);
57
+ if (result.isConflict) {
58
+ const confidence = result.confidence || 0;
59
+ if (confidence > maxConfidence) maxConfidence = confidence;
60
+ reasons.push({
61
+ type: "semantic_conflict",
62
+ severity: confidence >= BLOCK_CONFIDENCE ? "block" : "warn",
63
+ lockId: lock.id,
64
+ lockText: lock.text,
65
+ confidence,
66
+ level: result.level || "MEDIUM",
67
+ details: result.reasons || [],
68
+ });
69
+ }
70
+ }
71
+
72
+ // --- Step 2: Lock-to-file mapping (do changed files touch locked zones?) ---
73
+ let lockFileMatches = [];
74
+ if (files.length > 0) {
75
+ try {
76
+ const lockMap = mapLocksToFiles(root);
77
+ const normalizedFiles = files.map(f => f.replace(/\\/g, "/"));
78
+
79
+ for (const mapping of lockMap) {
80
+ const overlapping = mapping.matchedFiles.filter(mf =>
81
+ normalizedFiles.some(cf => {
82
+ const cfNorm = cf.toLowerCase();
83
+ const mfNorm = mf.toLowerCase();
84
+ return cfNorm === mfNorm || cfNorm.endsWith("/" + mfNorm) || mfNorm.endsWith("/" + cfNorm);
85
+ })
86
+ );
87
+ if (overlapping.length > 0) {
88
+ lockFileMatches.push({
89
+ lockId: mapping.lockId,
90
+ lockText: mapping.lockText,
91
+ overlappingFiles: overlapping,
92
+ });
93
+ // Find the lock's existing semantic confidence, or default to 60
94
+ const existingSemantic = reasons.find(r => r.lockId === mapping.lockId);
95
+ if (!existingSemantic) {
96
+ // This lock wasn't caught by semantic analysis but the files overlap
97
+ reasons.push({
98
+ type: "lock_file_overlap",
99
+ severity: "warn",
100
+ lockId: mapping.lockId,
101
+ lockText: mapping.lockText,
102
+ confidence: 60,
103
+ level: "MEDIUM",
104
+ details: [`Changed files overlap with locked zone: ${overlapping.join(", ")}`],
105
+ });
106
+ if (60 > maxConfidence) maxConfidence = 60;
107
+ } else {
108
+ // Boost existing semantic match — file evidence confirms it
109
+ existingSemantic.confidence = Math.min(100, existingSemantic.confidence + 15);
110
+ existingSemantic.details.push(`File-level confirmation: ${overlapping.join(", ")}`);
111
+ if (existingSemantic.confidence > maxConfidence) maxConfidence = existingSemantic.confidence;
112
+ if (existingSemantic.confidence >= BLOCK_CONFIDENCE) existingSemantic.severity = "block";
113
+ }
114
+ }
115
+ }
116
+ } catch (_) {
117
+ // Lock mapping failed (no graph), continue without it
118
+ }
119
+ }
120
+
121
+ // --- Step 3: Blast radius for each changed file ---
122
+ let blastDetails = [];
123
+ let maxImpactPercent = 0;
124
+ if (includeGraph && files.length > 0) {
125
+ try {
126
+ for (const file of files) {
127
+ const br = getBlastRadius(root, file);
128
+ if (br.found) {
129
+ blastDetails.push({
130
+ file: br.file,
131
+ directDependents: br.directDependents?.length || 0,
132
+ transitiveDependents: br.blastRadius || 0,
133
+ impactPercent: br.impactPercent || 0,
134
+ depth: br.depth || 0,
135
+ });
136
+ if (br.impactPercent > maxImpactPercent) maxImpactPercent = br.impactPercent;
137
+ }
138
+ }
139
+
140
+ if (maxImpactPercent > HIGH_BLAST_RADIUS) {
141
+ reasons.push({
142
+ type: "high_blast_radius",
143
+ severity: "warn",
144
+ confidence: Math.min(90, 50 + maxImpactPercent),
145
+ level: "HIGH",
146
+ details: [`Change affects ${maxImpactPercent.toFixed(1)}% of the codebase`],
147
+ });
148
+ } else if (maxImpactPercent > MED_BLAST_RADIUS) {
149
+ reasons.push({
150
+ type: "moderate_blast_radius",
151
+ severity: "info",
152
+ confidence: 30 + maxImpactPercent,
153
+ level: "MEDIUM",
154
+ details: [`Change affects ${maxImpactPercent.toFixed(1)}% of the codebase`],
155
+ });
156
+ }
157
+ } catch (_) {
158
+ // Graph not available, skip blast radius
159
+ }
160
+ }
161
+
162
+ // --- Step 4: Typed constraint awareness ---
163
+ let typedWarnings = [];
164
+ if (typedLocks.length > 0) {
165
+ // Check if the description mentions any typed constraint metrics
166
+ const descLower = description.toLowerCase();
167
+ for (const lock of typedLocks) {
168
+ const metric = (lock.metric || lock.description || lock.text || "").toLowerCase();
169
+ if (metric && descLower.includes(metric.split(" ")[0])) {
170
+ typedWarnings.push({
171
+ lockId: lock.id,
172
+ constraintType: lock.constraintType,
173
+ metric: lock.metric || lock.description,
174
+ text: lock.text,
175
+ });
176
+ reasons.push({
177
+ type: "typed_constraint_relevant",
178
+ severity: "info",
179
+ lockId: lock.id,
180
+ lockText: lock.text,
181
+ confidence: 30,
182
+ level: "LOW",
183
+ details: [`Typed constraint (${lock.constraintType}) may be relevant: ${lock.text}`],
184
+ });
185
+ }
186
+ }
187
+ }
188
+
189
+ // --- Step 5: Calculate risk score & verdict ---
190
+ let riskScore = 0;
191
+
192
+ // Base risk from semantic conflicts
193
+ if (maxConfidence > 0) {
194
+ riskScore = maxConfidence;
195
+ }
196
+
197
+ // Boost from lock-file overlaps
198
+ if (lockFileMatches.length > 0) {
199
+ riskScore = Math.max(riskScore, 55);
200
+ riskScore = Math.min(100, riskScore + lockFileMatches.length * 5);
201
+ }
202
+
203
+ // Boost from blast radius
204
+ if (maxImpactPercent > HIGH_BLAST_RADIUS) {
205
+ riskScore = Math.min(100, riskScore + 15);
206
+ } else if (maxImpactPercent > MED_BLAST_RADIUS) {
207
+ riskScore = Math.min(100, riskScore + 8);
208
+ }
209
+
210
+ // Determine verdict
211
+ let verdict;
212
+ const hasBlockSeverity = reasons.some(r => r.severity === "block");
213
+ if (hasBlockSeverity || riskScore >= BLOCK_CONFIDENCE) {
214
+ verdict = "BLOCK";
215
+ } else if (riskScore >= WARN_CONFIDENCE || reasons.some(r => r.severity === "warn")) {
216
+ verdict = "WARN";
217
+ } else {
218
+ verdict = "ALLOW";
219
+ }
220
+
221
+ // --- Step 6: Build human-readable summary ---
222
+ const summary = buildSummary(verdict, riskScore, reasons, files, blastDetails, lockFileMatches);
223
+
224
+ return {
225
+ verdict,
226
+ riskScore,
227
+ description,
228
+ fileCount: files.length,
229
+ lockCount: activeLocks.length,
230
+ reasons,
231
+ blastRadius: blastDetails.length > 0 ? {
232
+ files: blastDetails,
233
+ maxImpactPercent,
234
+ } : undefined,
235
+ lockFileOverlaps: lockFileMatches.length > 0 ? lockFileMatches : undefined,
236
+ typedConstraints: typedWarnings.length > 0 ? typedWarnings : undefined,
237
+ summary,
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Async version — adds LLM-powered conflict checking for grey-zone decisions.
243
+ */
244
+ export async function reviewPatchAsync(root, opts) {
245
+ // Start with heuristic review
246
+ const result = reviewPatch(root, opts);
247
+
248
+ // If verdict is already BLOCK or ALLOW with high confidence, return immediately
249
+ if (result.verdict === "BLOCK" || (result.verdict === "ALLOW" && result.riskScore < 20)) {
250
+ result.source = "heuristic";
251
+ return result;
252
+ }
253
+
254
+ // For WARN / uncertain cases, try LLM enhancement
255
+ try {
256
+ const { llmCheckConflict } = await import("./llm-checker.js");
257
+ const brain = readBrain(root);
258
+ const activeLocks = (brain?.specLock?.items || []).filter(l => l.active !== false && !l.constraintType);
259
+
260
+ if (activeLocks.length > 0) {
261
+ const llmResult = await llmCheckConflict(root, opts.description, activeLocks);
262
+ if (llmResult && llmResult.hasConflict) {
263
+ for (const lc of (llmResult.conflictingLocks || [])) {
264
+ const confidence = lc.confidence || 50;
265
+ const existing = result.reasons.find(r => r.lockId === lc.id);
266
+ if (existing) {
267
+ // LLM confirms heuristic — boost confidence
268
+ existing.confidence = Math.max(existing.confidence, confidence);
269
+ existing.details.push("LLM confirmed conflict");
270
+ if (existing.confidence >= BLOCK_CONFIDENCE) existing.severity = "block";
271
+ } else {
272
+ // LLM found new conflict heuristic missed
273
+ result.reasons.push({
274
+ type: "llm_conflict",
275
+ severity: confidence >= BLOCK_CONFIDENCE ? "block" : "warn",
276
+ lockId: lc.id,
277
+ lockText: lc.text,
278
+ confidence,
279
+ level: lc.level || "MEDIUM",
280
+ details: lc.reasons || ["LLM-detected conflict"],
281
+ });
282
+ }
283
+ if (confidence > result.riskScore) result.riskScore = confidence;
284
+ }
285
+
286
+ // Re-evaluate verdict with LLM data
287
+ const hasBlock = result.reasons.some(r => r.severity === "block");
288
+ if (hasBlock || result.riskScore >= BLOCK_CONFIDENCE) {
289
+ result.verdict = "BLOCK";
290
+ } else if (result.riskScore >= WARN_CONFIDENCE) {
291
+ result.verdict = "WARN";
292
+ }
293
+
294
+ result.summary = buildSummary(
295
+ result.verdict, result.riskScore, result.reasons,
296
+ opts.files || [], result.blastRadius?.files || [],
297
+ result.lockFileOverlaps || []
298
+ );
299
+ }
300
+ }
301
+ result.source = "hybrid";
302
+ } catch (_) {
303
+ result.source = "heuristic-only";
304
+ }
305
+
306
+ return result;
307
+ }
308
+
309
+ // --- Summary builder ---
310
+
311
+ function buildSummary(verdict, riskScore, reasons, files, blastDetails, lockFileMatches) {
312
+ const parts = [];
313
+
314
+ if (verdict === "BLOCK") {
315
+ parts.push(`BLOCKED (risk: ${riskScore}/100)`);
316
+ } else if (verdict === "WARN") {
317
+ parts.push(`WARNING (risk: ${riskScore}/100)`);
318
+ } else {
319
+ parts.push(`ALLOWED (risk: ${riskScore}/100)`);
320
+ }
321
+
322
+ const semanticConflicts = reasons.filter(r => r.type === "semantic_conflict" || r.type === "llm_conflict");
323
+ if (semanticConflicts.length > 0) {
324
+ parts.push(`${semanticConflicts.length} constraint conflict(s): ${semanticConflicts.map(r => `"${r.lockText}"`).join(", ")}`);
325
+ }
326
+
327
+ if (lockFileMatches.length > 0) {
328
+ const fileCount = lockFileMatches.reduce((acc, m) => acc + m.overlappingFiles.length, 0);
329
+ parts.push(`${fileCount} file(s) in locked zones`);
330
+ }
331
+
332
+ if (blastDetails.length > 0) {
333
+ const maxImpact = Math.max(...blastDetails.map(b => b.impactPercent));
334
+ const totalDeps = blastDetails.reduce((acc, b) => acc + b.transitiveDependents, 0);
335
+ if (maxImpact > 0) {
336
+ parts.push(`blast radius: ${totalDeps} dependent file(s), ${maxImpact.toFixed(1)}% impact`);
337
+ }
338
+ }
339
+
340
+ const typedReasons = reasons.filter(r => r.type === "typed_constraint_relevant");
341
+ if (typedReasons.length > 0) {
342
+ parts.push(`${typedReasons.length} typed constraint(s) may be affected`);
343
+ }
344
+
345
+ return parts.join(". ") + ".";
346
+ }
@@ -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.0.0 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v5.1.0 &mdash; AI Constraint Engine</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.0.0 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v5.1.0 &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>
@@ -52,6 +52,8 @@ import {
52
52
  mapLocksToFiles,
53
53
  getModules,
54
54
  getCriticalPaths,
55
+ reviewPatch,
56
+ reviewPatchAsync,
55
57
  } from "../core/engine.js";
56
58
  import { generateContext, generateContextPack } from "../core/context.js";
57
59
  import {
@@ -107,7 +109,7 @@ import { fileURLToPath } from "url";
107
109
  import _path from "path";
108
110
 
109
111
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
110
- const VERSION = "5.0.0";
112
+ const VERSION = "5.1.0";
111
113
  const AUTHOR = "Sandeep Roy";
112
114
  const START_TIME = Date.now();
113
115
 
@@ -881,7 +883,7 @@ app.get("/health", (req, res) => {
881
883
  status: "healthy",
882
884
  version: VERSION,
883
885
  uptime: Math.floor((Date.now() - START_TIME) / 1000),
884
- tools: 39,
886
+ tools: 40,
885
887
  auditChain: auditStatus,
886
888
  authEnabled: isAuthEnabled(PROJECT_ROOT),
887
889
  rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
@@ -895,8 +897,8 @@ app.get("/", (req, res) => {
895
897
  name: "speclock",
896
898
  version: VERSION,
897
899
  author: AUTHOR,
898
- description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 39 MCP tools. 940 tests, 99.4% accuracy.",
899
- tools: 39,
900
+ description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 40 MCP tools. 940 tests, 99.4% accuracy.",
901
+ tools: 40,
900
902
  mcp_endpoint: "/mcp",
901
903
  health_endpoint: "/health",
902
904
  npm: "https://www.npmjs.com/package/speclock",
@@ -910,7 +912,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
910
912
  res.json({
911
913
  name: "SpecLock",
912
914
  version: VERSION,
913
- description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints via Gemini Flash), Code Graph (dependency parsing, blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 39 MCP tools. 940 tests, 99.4% accuracy. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
915
+ description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints via Gemini Flash), Code Graph (dependency parsing, blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 40 MCP tools. 940 tests, 99.4% accuracy. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
914
916
  author: {
915
917
  name: "Sandeep Roy",
916
918
  url: "https://github.com/sgroy10",
@@ -919,7 +921,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
919
921
  homepage: "https://sgroy10.github.io/speclock/",
920
922
  license: "MIT",
921
923
  capabilities: {
922
- tools: 39,
924
+ tools: 40,
923
925
  categories: [
924
926
  "Memory Management",
925
927
  "Change Tracking",
@@ -1509,6 +1511,36 @@ app.get("/api/v2/graph/lock-map", (req, res) => {
1509
1511
  }
1510
1512
  });
1511
1513
 
1514
+ // ========================================
1515
+ // PATCH GATEWAY (v5.1)
1516
+ // ========================================
1517
+
1518
+ app.post("/api/v2/gateway/review", async (req, res) => {
1519
+ setCorsHeaders(res);
1520
+
1521
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1522
+ if (!checkRateLimit(clientIp)) {
1523
+ return res.status(429).json({ error: "Rate limit exceeded", api_version: "v2" });
1524
+ }
1525
+
1526
+ const { description, files, useLLM } = req.body || {};
1527
+ if (!description || typeof description !== "string") {
1528
+ return res.status(400).json({ error: "Missing 'description' field (describe what the change does)", api_version: "v2" });
1529
+ }
1530
+
1531
+ try {
1532
+ ensureInit(PROJECT_ROOT);
1533
+ const fileList = Array.isArray(files) ? files : [];
1534
+ const result = useLLM
1535
+ ? await reviewPatchAsync(PROJECT_ROOT, { description, files: fileList })
1536
+ : reviewPatch(PROJECT_ROOT, { description, files: fileList });
1537
+
1538
+ return res.json({ ...result, api_version: "v2" });
1539
+ } catch (err) {
1540
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1541
+ }
1542
+ });
1543
+
1512
1544
  // ========================================
1513
1545
  // SSO ENDPOINTS (v3.5)
1514
1546
  // ========================================
package/src/mcp/server.js CHANGED
@@ -57,6 +57,8 @@ import {
57
57
  mapLocksToFiles,
58
58
  getModules,
59
59
  getCriticalPaths,
60
+ reviewPatch,
61
+ reviewPatchAsync,
60
62
  } from "../core/engine.js";
61
63
  import { generateContext, generateContextPack } from "../core/context.js";
62
64
  import {
@@ -114,7 +116,7 @@ const PROJECT_ROOT =
114
116
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
115
117
 
116
118
  // --- MCP Server ---
117
- const VERSION = "5.0.0";
119
+ const VERSION = "5.1.0";
118
120
  const AUTHOR = "Sandeep Roy";
119
121
 
120
122
  const server = new McpServer(
@@ -1716,6 +1718,70 @@ server.tool(
1716
1718
  }
1717
1719
  );
1718
1720
 
1721
+ // --- Patch Gateway (v5.1) ---
1722
+
1723
+ server.tool(
1724
+ "speclock_review_patch",
1725
+ "Review a proposed code change and get an ALLOW/WARN/BLOCK verdict. Combines semantic conflict detection, lock-to-file mapping, blast radius analysis, and typed constraint awareness into a single risk assessment. The patch-time decision engine that gates every change.",
1726
+ {
1727
+ description: z.string().describe("What the change does (e.g. 'Add social login to auth page')"),
1728
+ files: z.array(z.string()).optional().default([]).describe("Files being changed (project-relative paths, e.g. ['src/auth/login.js'])"),
1729
+ useLLM: z.boolean().optional().default(false).describe("If true, uses LLM for enhanced conflict detection on ambiguous cases"),
1730
+ },
1731
+ async ({ description, files, useLLM }) => {
1732
+ const perm = requirePermission("speclock_review_patch");
1733
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1734
+
1735
+ const result = useLLM
1736
+ ? await reviewPatchAsync(PROJECT_ROOT, { description, files })
1737
+ : reviewPatch(PROJECT_ROOT, { description, files });
1738
+
1739
+ if (result.verdict === "ERROR") {
1740
+ return { content: [{ type: "text", text: result.error }], isError: true };
1741
+ }
1742
+
1743
+ const lines = [
1744
+ `Patch Gateway Verdict: ${result.verdict}`,
1745
+ `Risk Score: ${result.riskScore}/100`,
1746
+ `Source: ${result.source || "heuristic"}`,
1747
+ `Active Locks: ${result.lockCount}`,
1748
+ `Files Reviewed: ${result.fileCount}`,
1749
+ ``,
1750
+ result.summary,
1751
+ ];
1752
+
1753
+ if (result.reasons.length > 0) {
1754
+ lines.push(``, `Reasons:`);
1755
+ for (const r of result.reasons) {
1756
+ const icon = r.severity === "block" ? "BLOCK" : r.severity === "warn" ? "WARN" : "INFO";
1757
+ lines.push(` [${icon}] ${r.type}: "${r.lockText || r.details?.[0] || ""}" (confidence: ${r.confidence}%)`);
1758
+ if (r.details && r.details.length > 0) {
1759
+ r.details.forEach(d => lines.push(` - ${d}`));
1760
+ }
1761
+ }
1762
+ }
1763
+
1764
+ if (result.blastRadius) {
1765
+ lines.push(``, `Blast Radius:`);
1766
+ for (const b of result.blastRadius.files) {
1767
+ lines.push(` ${b.file}: ${b.transitiveDependents} dependents (${b.impactPercent.toFixed(1)}% impact)`);
1768
+ }
1769
+ }
1770
+
1771
+ if (result.lockFileOverlaps) {
1772
+ lines.push(``, `Lock-File Overlaps:`);
1773
+ for (const o of result.lockFileOverlaps) {
1774
+ lines.push(` Lock: "${o.lockText}" → Files: ${o.overlappingFiles.join(", ")}`);
1775
+ }
1776
+ }
1777
+
1778
+ return {
1779
+ content: [{ type: "text", text: lines.join("\n") }],
1780
+ isError: result.verdict === "BLOCK",
1781
+ };
1782
+ }
1783
+ );
1784
+
1719
1785
  // --- Smithery sandbox export ---
1720
1786
  export default function createSandboxServer() {
1721
1787
  return server;