speclock 4.0.0 → 4.1.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "speclock",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
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",
@@ -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
 
@@ -118,51 +123,62 @@ export function checkConflict(root, proposedAction) {
118
123
  }
119
124
 
120
125
  /**
121
- * Async conflict check with LLM fallback for ambiguous cases.
122
- * Strategy: Run heuristic first (fast, free). If any match falls in the
123
- * "medium confidence" zone (30–70%), optionally verify with LLM.
124
- * HIGH confidence (>70%) and NO conflict (<30%) are trusted as-is.
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.
125
133
  */
126
134
  export async function checkConflictAsync(root, proposedAction) {
127
135
  // 1. Always run the fast heuristic first
128
136
  const heuristicResult = checkConflict(root, proposedAction);
129
137
 
130
- // 2. If no conflict at all, trust the heuristic
131
- if (!heuristicResult.hasConflict) return heuristicResult;
132
-
133
- // 3. Check if any conflicts are in the ambiguous zone (30–70%)
134
- const ambiguous = heuristicResult.conflictingLocks.filter(
135
- (c) => c.confidence >= 30 && c.confidence <= 70
136
- );
137
-
138
- // If all conflicts are HIGH confidence (>70%), trust the heuristic
139
- if (ambiguous.length === 0) return heuristicResult;
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: all conflicts are HIGH (>70%) → heuristic is certain, skip LLM
146
+ if (
147
+ heuristicResult.hasConflict &&
148
+ heuristicResult.conflictingLocks.every((c) => c.confidence > 70)
149
+ ) {
150
+ return heuristicResult;
151
+ }
140
152
 
141
- // 4. Try LLM verification for the ambiguous cases
153
+ // 4. Call LLM for everything else — including score 0.
154
+ // Score 0 means "heuristic vocabulary doesn't cover this domain",
155
+ // which is EXACTLY when an LLM (which knows every domain) adds value.
142
156
  try {
143
157
  const { llmCheckConflict } = await import("./llm-checker.js");
144
158
  const llmResult = await llmCheckConflict(root, proposedAction);
145
159
  if (llmResult) {
146
- // Merge: keep HIGH heuristic results, replace ambiguous with LLM
160
+ // Keep HIGH heuristic conflicts (>70%) they're already certain
147
161
  const highConfidence = heuristicResult.conflictingLocks.filter(
148
162
  (c) => c.confidence > 70
149
163
  );
150
164
  const llmConflicts = llmResult.conflictingLocks || [];
151
165
  const merged = [...highConfidence, ...llmConflicts];
152
166
 
153
- // Deduplicate by lock text
154
- const seen = new Set();
155
- const unique = merged.filter((c) => {
156
- if (seen.has(c.text)) return false;
157
- seen.add(c.text);
158
- return true;
159
- });
167
+ // Deduplicate by lock text, keeping the higher-confidence entry
168
+ const byText = new Map();
169
+ for (const c of merged) {
170
+ const existing = byText.get(c.text);
171
+ if (!existing || c.confidence > existing.confidence) {
172
+ byText.set(c.text, c);
173
+ }
174
+ }
175
+ const unique = [...byText.values()];
160
176
 
161
177
  if (unique.length === 0) {
162
178
  return {
163
179
  hasConflict: false,
164
180
  conflictingLocks: [],
165
- analysis: `Heuristic flagged ${ambiguous.length} ambiguous case(s), LLM verified as safe. No conflicts.`,
181
+ analysis: `Heuristic had partial signal, LLM verified as safe. No conflicts.`,
166
182
  };
167
183
  }
168
184
 
@@ -1,8 +1,10 @@
1
1
  // ===================================================================
2
2
  // SpecLock LLM-Powered Conflict Checker (Optional)
3
- // Uses OpenAI or Anthropic APIs for enterprise-grade detection.
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: env var > brain.json config
42
- const apiKey = process.env.SPECLOCK_LLM_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
43
- const provider = process.env.SPECLOCK_LLM_PROVIDER || "openai"; // "openai" or "anthropic"
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 || "openai",
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 === "anthropic") {
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);
package/src/mcp/server.js CHANGED
@@ -470,10 +470,10 @@ server.tool(
470
470
  // CONTINUITY PROTECTION TOOLS
471
471
  // ========================================
472
472
 
473
- // Tool 12: speclock_check_conflict (v2.5: uses enforcer hard mode returns isError)
473
+ // Tool 12: speclock_check_conflict (v4.1: hybrid heuristic + Gemini LLM)
474
474
  server.tool(
475
475
  "speclock_check_conflict",
476
- "Check if a proposed action conflicts with any active SpecLock. Use before making significant changes. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
476
+ "Check if a proposed action conflicts with any active SpecLock. Uses fast heuristic + Gemini LLM for universal domain coverage. In hard enforcement mode, conflicts above the threshold will BLOCK the action (isError: true).",
477
477
  {
478
478
  proposedAction: z
479
479
  .string()
@@ -481,18 +481,18 @@ server.tool(
481
481
  .describe("Description of the action you plan to take"),
482
482
  },
483
483
  async ({ proposedAction }) => {
484
- // Try LLM-enhanced check first, fall back to heuristic enforcer
485
- let result;
486
- try {
487
- const { llmCheckConflict } = await import("../core/llm-checker.js");
488
- const llmResult = await llmCheckConflict(PROJECT_ROOT, proposedAction);
489
- if (llmResult) {
490
- result = llmResult;
484
+ // Hybrid check: heuristic first, LLM for grey-zone (1-70%)
485
+ let result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
486
+
487
+ // If async hybrid returned no conflict, also check enforcer for hard mode
488
+ if (!result.hasConflict) {
489
+ const enforced = enforceConflictCheck(PROJECT_ROOT, proposedAction);
490
+ if (enforced.blocked) {
491
+ return {
492
+ content: [{ type: "text", text: enforced.analysis }],
493
+ isError: true,
494
+ };
491
495
  }
492
- } catch (_) {}
493
-
494
- if (!result) {
495
- result = enforceConflictCheck(PROJECT_ROOT, proposedAction);
496
496
  }
497
497
 
498
498
  // In hard mode with blocking conflict, return isError: true