speclock 3.5.4 → 4.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/package.json +1 -1
- package/src/core/conflict.js +74 -3
- package/src/core/engine.js +10 -0
- package/src/core/llm-checker.js +60 -6
- package/src/core/lock-author.js +478 -0
- package/src/core/memory.js +12 -3
- package/src/core/semantics.js +562 -33
- package/src/mcp/server.js +26 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "speclock",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "AI constraint engine with Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 100% detection, 0% false positives. 31 MCP tools + CLI. Enterprise platform.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/mcp/server.js",
|
package/src/core/conflict.js
CHANGED
|
@@ -68,6 +68,7 @@ export function checkConflict(root, proposedAction) {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const conflicting = [];
|
|
71
|
+
let maxNonConflictScore = 0;
|
|
71
72
|
for (const lock of activeLocks) {
|
|
72
73
|
const result = analyzeConflict(proposedAction, lock.text);
|
|
73
74
|
if (result.isConflict) {
|
|
@@ -79,6 +80,8 @@ export function checkConflict(root, proposedAction) {
|
|
|
79
80
|
level: result.level,
|
|
80
81
|
reasons: result.reasons,
|
|
81
82
|
});
|
|
83
|
+
} else if (result.confidence > maxNonConflictScore) {
|
|
84
|
+
maxNonConflictScore = result.confidence;
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -86,6 +89,7 @@ export function checkConflict(root, proposedAction) {
|
|
|
86
89
|
return {
|
|
87
90
|
hasConflict: false,
|
|
88
91
|
conflictingLocks: [],
|
|
92
|
+
_maxNonConflictScore: maxNonConflictScore,
|
|
89
93
|
analysis: `Checked against ${activeLocks.length} active lock(s). No conflicts detected (semantic analysis v2). Proceed with caution.`,
|
|
90
94
|
};
|
|
91
95
|
}
|
|
@@ -102,6 +106,7 @@ export function checkConflict(root, proposedAction) {
|
|
|
102
106
|
const result = {
|
|
103
107
|
hasConflict: true,
|
|
104
108
|
conflictingLocks: conflicting,
|
|
109
|
+
_maxNonConflictScore: maxNonConflictScore,
|
|
105
110
|
analysis: `Potential conflict with ${conflicting.length} lock(s):\n${details}\nReview before proceeding.`,
|
|
106
111
|
};
|
|
107
112
|
|
|
@@ -117,15 +122,81 @@ export function checkConflict(root, proposedAction) {
|
|
|
117
122
|
return result;
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Async conflict check with LLM fallback for grey-zone cases.
|
|
127
|
+
* Strategy: Run heuristic first (fast, free, offline).
|
|
128
|
+
* - Score > 70% on ALL conflicts → trust heuristic (skip LLM)
|
|
129
|
+
* - Score == 0 everywhere (no signal at all) → trust heuristic (skip LLM)
|
|
130
|
+
* - Score 1–70% on ANY lock → GREY ZONE → call LLM for universal domain coverage
|
|
131
|
+
* This catches vocabulary gaps where the heuristic has partial/no signal
|
|
132
|
+
* but an LLM (which knows every domain) would detect the conflict.
|
|
133
|
+
*/
|
|
120
134
|
export async function checkConflictAsync(root, proposedAction) {
|
|
135
|
+
// 1. Always run the fast heuristic first
|
|
136
|
+
const heuristicResult = checkConflict(root, proposedAction);
|
|
137
|
+
|
|
138
|
+
// 2. Determine the max score across ALL locks (conflict + non-conflict)
|
|
139
|
+
const maxConflictScore = heuristicResult.conflictingLocks.length > 0
|
|
140
|
+
? Math.max(...heuristicResult.conflictingLocks.map((c) => c.confidence))
|
|
141
|
+
: 0;
|
|
142
|
+
const maxNonConflictScore = heuristicResult._maxNonConflictScore || 0;
|
|
143
|
+
const maxScore = Math.max(maxConflictScore, maxNonConflictScore);
|
|
144
|
+
|
|
145
|
+
// 3. Fast path: zero signal anywhere → truly unrelated, skip LLM
|
|
146
|
+
if (maxScore === 0 && !heuristicResult.hasConflict) {
|
|
147
|
+
return heuristicResult;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 4. Fast path: all conflicts are HIGH (>70%) → heuristic is certain, skip LLM
|
|
151
|
+
if (
|
|
152
|
+
heuristicResult.hasConflict &&
|
|
153
|
+
heuristicResult.conflictingLocks.every((c) => c.confidence > 70)
|
|
154
|
+
) {
|
|
155
|
+
return heuristicResult;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 5. GREY ZONE: some signal (1-70%) or low-confidence conflicts → call LLM
|
|
121
159
|
try {
|
|
122
160
|
const { llmCheckConflict } = await import("./llm-checker.js");
|
|
123
161
|
const llmResult = await llmCheckConflict(root, proposedAction);
|
|
124
|
-
if (llmResult)
|
|
162
|
+
if (llmResult) {
|
|
163
|
+
// Keep HIGH heuristic conflicts (>70%) — they're already certain
|
|
164
|
+
const highConfidence = heuristicResult.conflictingLocks.filter(
|
|
165
|
+
(c) => c.confidence > 70
|
|
166
|
+
);
|
|
167
|
+
const llmConflicts = llmResult.conflictingLocks || [];
|
|
168
|
+
const merged = [...highConfidence, ...llmConflicts];
|
|
169
|
+
|
|
170
|
+
// Deduplicate by lock text, keeping the higher-confidence entry
|
|
171
|
+
const byText = new Map();
|
|
172
|
+
for (const c of merged) {
|
|
173
|
+
const existing = byText.get(c.text);
|
|
174
|
+
if (!existing || c.confidence > existing.confidence) {
|
|
175
|
+
byText.set(c.text, c);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const unique = [...byText.values()];
|
|
179
|
+
|
|
180
|
+
if (unique.length === 0) {
|
|
181
|
+
return {
|
|
182
|
+
hasConflict: false,
|
|
183
|
+
conflictingLocks: [],
|
|
184
|
+
analysis: `Heuristic had partial signal, LLM verified as safe. No conflicts.`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
unique.sort((a, b) => b.confidence - a.confidence);
|
|
189
|
+
return {
|
|
190
|
+
hasConflict: true,
|
|
191
|
+
conflictingLocks: unique,
|
|
192
|
+
analysis: `${unique.length} conflict(s) confirmed (${highConfidence.length} heuristic + ${llmConflicts.length} LLM-verified).`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
125
195
|
} catch (_) {
|
|
126
|
-
// LLM
|
|
196
|
+
// LLM not available — return heuristic result as-is
|
|
127
197
|
}
|
|
128
|
-
|
|
198
|
+
|
|
199
|
+
return heuristicResult;
|
|
129
200
|
}
|
|
130
201
|
|
|
131
202
|
export function suggestLocks(root) {
|
package/src/core/engine.js
CHANGED
|
@@ -594,3 +594,13 @@ export {
|
|
|
594
594
|
revokeSession,
|
|
595
595
|
listSessions,
|
|
596
596
|
} from "./sso.js";
|
|
597
|
+
|
|
598
|
+
// --- Smart Lock Authoring (v4.0) ---
|
|
599
|
+
export {
|
|
600
|
+
normalizeLock,
|
|
601
|
+
detectVerbContamination,
|
|
602
|
+
extractSubjects,
|
|
603
|
+
compareSubjects,
|
|
604
|
+
extractLockSubject,
|
|
605
|
+
rewriteLock,
|
|
606
|
+
} from "./lock-author.js";
|
package/src/core/llm-checker.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// ===================================================================
|
|
2
2
|
// SpecLock LLM-Powered Conflict Checker (Optional)
|
|
3
|
-
// Uses OpenAI or Anthropic APIs for
|
|
3
|
+
// Uses Gemini, OpenAI, or Anthropic APIs for universal detection.
|
|
4
4
|
// Zero mandatory dependencies — uses built-in fetch().
|
|
5
5
|
// Falls back gracefully if no API key is configured.
|
|
6
|
+
//
|
|
7
|
+
// Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
6
8
|
// ===================================================================
|
|
7
9
|
|
|
8
10
|
import { readBrain } from "./storage.js";
|
|
@@ -38,9 +40,22 @@ function cacheSet(key, value) {
|
|
|
38
40
|
// --- Configuration ---
|
|
39
41
|
|
|
40
42
|
function getConfig(root) {
|
|
41
|
-
// Priority:
|
|
42
|
-
const apiKey =
|
|
43
|
-
|
|
43
|
+
// Priority: explicit SPECLOCK key > provider-specific keys > brain.json
|
|
44
|
+
const apiKey =
|
|
45
|
+
process.env.SPECLOCK_LLM_KEY ||
|
|
46
|
+
process.env.GEMINI_API_KEY ||
|
|
47
|
+
process.env.GOOGLE_API_KEY ||
|
|
48
|
+
process.env.OPENAI_API_KEY ||
|
|
49
|
+
process.env.ANTHROPIC_API_KEY;
|
|
50
|
+
|
|
51
|
+
// Auto-detect provider from which env var is set
|
|
52
|
+
const provider =
|
|
53
|
+
process.env.SPECLOCK_LLM_PROVIDER ||
|
|
54
|
+
(process.env.SPECLOCK_LLM_KEY ? "gemini" : null) ||
|
|
55
|
+
(process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY ? "gemini" : null) ||
|
|
56
|
+
(process.env.OPENAI_API_KEY ? "openai" : null) ||
|
|
57
|
+
(process.env.ANTHROPIC_API_KEY ? "anthropic" : null) ||
|
|
58
|
+
"gemini"; // default to gemini (cheapest, free tier)
|
|
44
59
|
|
|
45
60
|
if (apiKey) {
|
|
46
61
|
return { apiKey, provider };
|
|
@@ -52,7 +67,7 @@ function getConfig(root) {
|
|
|
52
67
|
if (brain?.facts?.llm) {
|
|
53
68
|
return {
|
|
54
69
|
apiKey: brain.facts.llm.apiKey,
|
|
55
|
-
provider: brain.facts.llm.provider || "
|
|
70
|
+
provider: brain.facts.llm.provider || "gemini",
|
|
56
71
|
};
|
|
57
72
|
}
|
|
58
73
|
} catch (_) {}
|
|
@@ -156,6 +171,43 @@ async function callAnthropic(apiKey, userPrompt) {
|
|
|
156
171
|
}
|
|
157
172
|
}
|
|
158
173
|
|
|
174
|
+
async function callGemini(apiKey, userPrompt) {
|
|
175
|
+
const resp = await fetch(
|
|
176
|
+
`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent?key=${apiKey}`,
|
|
177
|
+
{
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json" },
|
|
180
|
+
body: JSON.stringify({
|
|
181
|
+
contents: [
|
|
182
|
+
{
|
|
183
|
+
parts: [
|
|
184
|
+
{ text: SYSTEM_PROMPT + "\n\n" + userPrompt },
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
generationConfig: {
|
|
189
|
+
temperature: 0.1,
|
|
190
|
+
maxOutputTokens: 1000,
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (!resp.ok) return null;
|
|
197
|
+
const data = await resp.json();
|
|
198
|
+
const content = data.candidates?.[0]?.content?.parts?.[0]?.text;
|
|
199
|
+
if (!content) return null;
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(content);
|
|
203
|
+
} catch (_) {
|
|
204
|
+
// Try to extract JSON from markdown code block
|
|
205
|
+
const match = content.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
206
|
+
if (match) return JSON.parse(match[1]);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
159
211
|
// --- Main export ---
|
|
160
212
|
|
|
161
213
|
/**
|
|
@@ -199,7 +251,9 @@ export async function llmCheckConflict(root, proposedAction, activeLocks) {
|
|
|
199
251
|
// Call LLM
|
|
200
252
|
let llmResult = null;
|
|
201
253
|
try {
|
|
202
|
-
if (config.provider === "
|
|
254
|
+
if (config.provider === "gemini") {
|
|
255
|
+
llmResult = await callGemini(config.apiKey, userPrompt);
|
|
256
|
+
} else if (config.provider === "anthropic") {
|
|
203
257
|
llmResult = await callAnthropic(config.apiKey, userPrompt);
|
|
204
258
|
} else {
|
|
205
259
|
llmResult = await callOpenAI(config.apiKey, userPrompt);
|