speclock 5.0.1 → 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 +6 -6
- package/SPECLOCK-INSTRUCTIONS.md +2 -0
- package/bin/speclock.js +2 -0
- package/package.json +2 -2
- package/src/cli/index.js +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/engine.js +6 -0
- package/src/core/git.js +3 -0
- package/src/core/hooks.js +1 -0
- package/src/core/patch-gateway.js +346 -0
- package/src/core/semantics.js +1 -0
- package/src/core/storage.js +3 -0
- package/src/core/templates.js +1 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +38 -6
- package/src/mcp/server.js +67 -1
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-
|
|
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
|
-
**
|
|
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
|
-
##
|
|
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 (
|
|
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%** | **
|
|
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 —
|
|
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/SPECLOCK-INSTRUCTIONS.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# SpecLock Project Instructions — Copy-Paste Templates
|
|
2
2
|
|
|
3
|
+
> Developed by **Sandeep Roy** ([github.com/sgroy10](https://github.com/sgroy10))
|
|
4
|
+
|
|
3
5
|
These are **project-level instructions** that you paste into your AI coding platform's settings. They force the AI to use SpecLock on every action — turning it from a passive notepad into an active guardrail.
|
|
4
6
|
|
|
5
7
|
---
|
package/bin/speclock.js
CHANGED
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"name": "speclock",
|
|
4
4
|
|
|
5
|
-
"version": "5.0
|
|
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.
|
|
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.
|
|
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]
|
package/src/core/compliance.js
CHANGED
package/src/core/engine.js
CHANGED
package/src/core/git.js
CHANGED
package/src/core/hooks.js
CHANGED
|
@@ -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
|
+
}
|
package/src/core/semantics.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SpecLock Semantic Analysis Engine v3
|
|
3
3
|
// Subject-aware conflict detection with scope matching.
|
|
4
4
|
// Zero external dependencies — pure JavaScript.
|
|
5
|
+
// Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
5
6
|
// ===================================================================
|
|
6
7
|
|
|
7
8
|
// ===================================================================
|
package/src/core/storage.js
CHANGED
package/src/core/templates.js
CHANGED
package/src/dashboard/index.html
CHANGED
|
@@ -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.
|
|
92
|
+
<div class="meta">v5.1.0 — 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.
|
|
185
|
+
SpecLock v5.1.0 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
package/src/mcp/http-server.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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. Typed constraints (numerical, range, state, temporal)
|
|
899
|
-
tools:
|
|
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.
|
|
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:
|
|
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.
|
|
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;
|