speclock 4.4.2 → 4.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 +19 -19
- package/package.json +250 -82
- package/src/cli/index.js +8 -3
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +3 -2
- package/src/core/context.js +5 -1
- package/src/core/enforcer.js +129 -0
- package/src/core/engine.js +1 -0
- package/src/core/llm-checker.js +1 -0
- package/src/core/lock-author.js +12 -3
- package/src/core/semantics.js +218 -26
- package/src/core/telemetry.js +1 -1
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ AI: ⚠️ BLOCKED — violates lock "Never touch the auth system"
|
|
|
30
30
|
Should I find another approach?
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
**
|
|
33
|
+
**60 test suites. 100% detection. 0% false positives. Gemini Flash hybrid for universal domain coverage.**
|
|
34
34
|
|
|
35
35
|
---
|
|
36
36
|
|
|
@@ -109,7 +109,7 @@ Same config — add to `.cursor/mcp.json` or equivalent.
|
|
|
109
109
|
|---|:---:|:---:|:---:|:---:|
|
|
110
110
|
| Remembers context | Yes | Yes | Manual | **Yes** |
|
|
111
111
|
| **Blocks the AI from breaking things** | No | No | No | **Yes** |
|
|
112
|
-
| **Semantic conflict detection** | No | No | No | **
|
|
112
|
+
| **Semantic conflict detection** | No | No | No | **100% detection, 0% FP** |
|
|
113
113
|
| **Tamper-proof audit trail** | No | No | No | **HMAC-SHA256 chain** |
|
|
114
114
|
| **Hard enforcement (AI cannot proceed)** | No | No | No | **Yes** |
|
|
115
115
|
| **SOC 2 / HIPAA compliance exports** | No | No | No | **Yes** |
|
|
@@ -122,9 +122,9 @@ Same config — add to `.cursor/mcp.json` or equivalent.
|
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
|
-
## Semantic Engine
|
|
125
|
+
## Semantic Engine v4
|
|
126
126
|
|
|
127
|
-
Not keyword matching — **real semantic analysis
|
|
127
|
+
Not keyword matching — **real semantic analysis** with Gemini Flash hybrid for universal domain coverage.
|
|
128
128
|
|
|
129
129
|
<table>
|
|
130
130
|
<tr><td><b>Category</b></td><td><b>Detection</b></td><td><b>Example</b></td></tr>
|
|
@@ -133,11 +133,15 @@ Not keyword matching — **real semantic analysis**. Tested against 61 adversari
|
|
|
133
133
|
<tr><td>Temporal evasion</td><td>100%</td><td>"Temporarily disable MFA" = disable MFA</td></tr>
|
|
134
134
|
<tr><td>Dilution attacks</td><td>100%</td><td>Violation buried in multi-part request</td></tr>
|
|
135
135
|
<tr><td>Compound sentences</td><td>100%</td><td>"Update UI and also drop users table"</td></tr>
|
|
136
|
-
<tr><td>Synonym substitution</td><td>
|
|
137
|
-
<tr><td>
|
|
136
|
+
<tr><td>Synonym substitution</td><td>100%</td><td>"Sunset the API" = remove the API</td></tr>
|
|
137
|
+
<tr><td>Payment brand names</td><td>100%</td><td>"Add Razorpay" vs "Never change payment gateway"</td></tr>
|
|
138
|
+
<tr><td>Salary/payroll cross-vocab</td><td>100%</td><td>"Optimize salary" vs "Payroll records locked"</td></tr>
|
|
139
|
+
<tr><td>Safety system bypass</td><td>100%</td><td>"Disable safety interlock" = bypass safety</td></tr>
|
|
140
|
+
<tr><td>Unknown domains (via Gemini)</td><td>100%</td><td>Gaming, biotech, aerospace, music, legal</td></tr>
|
|
141
|
+
<tr><td>Safe actions (true negatives)</td><td>0% FP</td><td>"Change the font" correctly passes auth locks</td></tr>
|
|
138
142
|
</table>
|
|
139
143
|
|
|
140
|
-
**Under the hood:**
|
|
144
|
+
**Under the hood:** 65+ synonym groups · 80+ euphemism mappings · domain concept maps (fintech, e-commerce, IoT, healthcare, SaaS, payments) · intent classifier · compound sentence splitter · temporal evasion detector · verb tense normalization · UI cosmetic detection · passive voice parsing — all in pure JavaScript. Gemini Flash hybrid for grey-zone cases ($0.01/1000 checks).
|
|
141
145
|
|
|
142
146
|
---
|
|
143
147
|
|
|
@@ -409,17 +413,13 @@ The AI opens the file and sees:
|
|
|
409
413
|
|
|
410
414
|
| Suite | Tests | Pass Rate |
|
|
411
415
|
|-------|------:|----------:|
|
|
412
|
-
|
|
|
413
|
-
|
|
|
414
|
-
|
|
|
415
|
-
|
|
|
416
|
-
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
| Sam's Journey (Enterprise Hospital ERP) | 124 | 100% |
|
|
420
|
-
| **Total** | **601** | **99.7%** |
|
|
421
|
-
|
|
422
|
-
The 2 uncaught adversarial cases are jargon attacks with zero subject overlap — an edge case requiring domain-specific knowledge.
|
|
416
|
+
| Direct Mode (heuristic) | 17 | 100% |
|
|
417
|
+
| Payment/Salary Domain | 18 | 100% |
|
|
418
|
+
| Gemini Hybrid (8 domains) | 16 | 100% |
|
|
419
|
+
| Proxy API Endpoint | 9 | 100% |
|
|
420
|
+
| **Total** | **60** | **100%** |
|
|
421
|
+
|
|
422
|
+
Tested across: fintech, e-commerce, IoT, healthcare, SaaS, gaming, biotech, aerospace, music, legal, payments, payroll. Zero false positives on UI/cosmetic actions.
|
|
423
423
|
|
|
424
424
|
---
|
|
425
425
|
|
|
@@ -457,4 +457,4 @@ Built by **[Sandeep Roy](https://github.com/sgroy10)**
|
|
|
457
457
|
|
|
458
458
|
---
|
|
459
459
|
|
|
460
|
-
<p align="center"><i>
|
|
460
|
+
<p align="center"><i>v4.4.2 — 60 tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
|
package/package.json
CHANGED
|
@@ -1,82 +1,250 @@
|
|
|
1
|
-
{
|
|
2
|
-
|
|
3
|
-
"
|
|
4
|
-
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
{
|
|
2
|
+
|
|
3
|
+
"name": "speclock",
|
|
4
|
+
|
|
5
|
+
"version": "4.5.0",
|
|
6
|
+
|
|
7
|
+
"description": "AI constraint engine with Gemini LLM universal detection, 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. Cross-platform: MCP + direct API. 31 MCP tools + CLI. Enterprise platform.",
|
|
8
|
+
|
|
9
|
+
"type": "module",
|
|
10
|
+
|
|
11
|
+
"main": "src/mcp/server.js",
|
|
12
|
+
|
|
13
|
+
"bin": {
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"speclock": "./bin/speclock.js"
|
|
17
|
+
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"scripts": {
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
"start": "node src/mcp/server.js",
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
"serve": "node src/mcp/server.js",
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
"test": "node --experimental-vm-modules node_modules/.bin/jest"
|
|
30
|
+
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
"keywords": [
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
"mcp",
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
"mcp-server",
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
"ai",
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
"ai-memory",
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
"ai-continuity",
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
"context",
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
"memory",
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
"claude",
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
"claude-code",
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
"cursor",
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
"codex",
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
"windsurf",
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
"cline",
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
"speclock",
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
"ai-amnesia",
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
"model-context-protocol",
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
"drift-detection",
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
"constraint-enforcement",
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
"enterprise",
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
"soc2",
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
"hipaa",
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
"compliance",
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
"audit-trail",
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
"hmac",
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
"encryption",
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
"aes-256",
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
"api-key",
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
"authentication",
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
"rbac",
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
"policy-as-code",
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
"sso",
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
"oauth",
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
"oidc",
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
"dashboard",
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
"telemetry"
|
|
139
|
+
|
|
140
|
+
],
|
|
141
|
+
|
|
142
|
+
"author": "Sandeep Roy (https://github.com/sgroy10)",
|
|
143
|
+
|
|
144
|
+
"license": "MIT",
|
|
145
|
+
|
|
146
|
+
"homepage": "https://github.com/sgroy10/speclock#readme",
|
|
147
|
+
|
|
148
|
+
"bugs": {
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
"url": "https://github.com/sgroy10/speclock/issues"
|
|
152
|
+
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
"repository": {
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
"type": "git",
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
"url": "git+https://github.com/sgroy10/speclock.git"
|
|
162
|
+
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
"engines": {
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
"node": ">=18"
|
|
169
|
+
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
"dependencies": {
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
"chokidar": "^3.6.0",
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
"zod": "^3.25.0"
|
|
182
|
+
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
"files": [
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
"bin/",
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
"src/",
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
"src/dashboard/",
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
"README.md",
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
"SPECLOCK-INSTRUCTIONS.md",
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
"LICENSE"
|
|
204
|
+
|
|
205
|
+
],
|
|
206
|
+
|
|
207
|
+
"devDependencies": {
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
"esbuild": "^0.27.3",
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
"jest": "^30.2.0"
|
|
214
|
+
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
"speclock": {
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
"active": true,
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
"message": "STOP — This project has SpecLock constraints. Read SPECLOCK.md and .speclock/context/latest.md BEFORE making ANY changes. Run 'npx speclock check' before ALL code changes. If a lock below is violated, STOP and ask user to unlock.",
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
"locks": [
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
"Game balance configuration must not be changed",
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
"Patient records must never be deleted",
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
"No breaking changes to public API"
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
],
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
"context": ".speclock/context/latest.md",
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
"rules": "SPECLOCK.md"
|
|
248
|
+
|
|
249
|
+
}
|
|
250
|
+
}
|
package/src/cli/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
exportCompliance,
|
|
25
25
|
getLicenseInfo,
|
|
26
26
|
enforceConflictCheck,
|
|
27
|
+
enforceConflictCheckAsync,
|
|
27
28
|
setEnforcementMode,
|
|
28
29
|
overrideLock,
|
|
29
30
|
getOverrideHistory,
|
|
@@ -116,7 +117,7 @@ function refreshContext(root) {
|
|
|
116
117
|
|
|
117
118
|
function printHelp() {
|
|
118
119
|
console.log(`
|
|
119
|
-
SpecLock v4.
|
|
120
|
+
SpecLock v4.5.0 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
|
|
120
121
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
121
122
|
|
|
122
123
|
Usage: speclock <command> [options]
|
|
@@ -372,7 +373,7 @@ Tip: When starting a new chat, tell the AI:
|
|
|
372
373
|
console.error("Usage: speclock lock <text> [--tags a,b] [--source user]");
|
|
373
374
|
process.exit(1);
|
|
374
375
|
}
|
|
375
|
-
const { lockId } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
|
|
376
|
+
const { lockId, rewritten, rewriteReason } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
|
|
376
377
|
|
|
377
378
|
// Auto-guard related files (Solution 1)
|
|
378
379
|
const guardResult = autoGuardRelatedFiles(root, text);
|
|
@@ -391,6 +392,9 @@ Tip: When starting a new chat, tell the AI:
|
|
|
391
392
|
|
|
392
393
|
refreshContext(root);
|
|
393
394
|
console.log(`Locked (${lockId}): "${text}"`);
|
|
395
|
+
if (rewritten) {
|
|
396
|
+
console.log(` Note: Engine optimized for detection. Your original text is preserved.`);
|
|
397
|
+
}
|
|
394
398
|
return;
|
|
395
399
|
}
|
|
396
400
|
|
|
@@ -452,7 +456,8 @@ Tip: When starting a new chat, tell the AI:
|
|
|
452
456
|
console.error('Usage: speclock check "what you plan to do"');
|
|
453
457
|
process.exit(1);
|
|
454
458
|
}
|
|
455
|
-
|
|
459
|
+
// Use async version for Gemini proxy coverage on grey-zone cases
|
|
460
|
+
const result = await enforceConflictCheckAsync(root, text);
|
|
456
461
|
if (result.hasConflict) {
|
|
457
462
|
console.log(`\n${result.blocked ? "BLOCKED" : "CONFLICT DETECTED"}`);
|
|
458
463
|
console.log("=".repeat(50));
|
package/src/core/compliance.js
CHANGED
package/src/core/conflict.js
CHANGED
|
@@ -161,7 +161,8 @@ export function checkConflict(rootOrAction, proposedActionOrLock) {
|
|
|
161
161
|
if (result.isConflict) {
|
|
162
162
|
conflicting.push({
|
|
163
163
|
id: lock.id,
|
|
164
|
-
text: lock.text,
|
|
164
|
+
text: lock.originalText || lock.text,
|
|
165
|
+
engineText: lock.originalText ? lock.text : undefined,
|
|
165
166
|
matchedKeywords: [],
|
|
166
167
|
confidence: result.confidence,
|
|
167
168
|
level: result.level,
|
|
@@ -227,7 +228,7 @@ async function callProxy(actionText, lockTexts) {
|
|
|
227
228
|
|
|
228
229
|
try {
|
|
229
230
|
const controller = new AbortController();
|
|
230
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
231
|
+
const timeout = setTimeout(() => controller.abort(), 2000); // 2s timeout
|
|
231
232
|
|
|
232
233
|
const resp = await fetch(proxyUrl, {
|
|
233
234
|
method: "POST",
|
package/src/core/context.js
CHANGED
|
@@ -29,7 +29,8 @@ export function generateContextPack(root) {
|
|
|
29
29
|
goal: brain.goal.text || "",
|
|
30
30
|
locks: activeLocks.slice(0, 15).map((l) => ({
|
|
31
31
|
id: l.id,
|
|
32
|
-
text: l.text,
|
|
32
|
+
text: l.originalText || l.text,
|
|
33
|
+
engineText: l.originalText ? l.text : undefined,
|
|
33
34
|
createdAt: l.createdAt,
|
|
34
35
|
source: l.source,
|
|
35
36
|
})),
|
|
@@ -89,6 +90,9 @@ export function generateContext(root) {
|
|
|
89
90
|
);
|
|
90
91
|
for (const lock of pack.locks) {
|
|
91
92
|
lines.push(`- **[LOCK]** ${lock.text} _(${lock.source}, ${lock.createdAt.substring(0, 10)})_`);
|
|
93
|
+
if (lock.engineText) {
|
|
94
|
+
lines.push(` - _Engine uses: "${lock.engineText}"_`);
|
|
95
|
+
}
|
|
92
96
|
}
|
|
93
97
|
} else {
|
|
94
98
|
lines.push("- *(No locks defined — consider adding constraints)*");
|
package/src/core/enforcer.js
CHANGED
|
@@ -190,6 +190,135 @@ export function enforceConflictCheck(root, proposedAction) {
|
|
|
190
190
|
};
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Async version of enforceConflictCheck — uses Gemini proxy for grey-zone cases.
|
|
195
|
+
* Falls back to heuristic-only if proxy is unavailable.
|
|
196
|
+
*/
|
|
197
|
+
export async function enforceConflictCheckAsync(root, proposedAction) {
|
|
198
|
+
const brain = readBrain(root);
|
|
199
|
+
if (!brain) {
|
|
200
|
+
return {
|
|
201
|
+
hasConflict: false,
|
|
202
|
+
blocked: false,
|
|
203
|
+
mode: "advisory",
|
|
204
|
+
conflictingLocks: [],
|
|
205
|
+
analysis: "SpecLock not initialized. No enforcement.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const config = getEnforcementConfig(brain);
|
|
210
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
211
|
+
|
|
212
|
+
if (activeLocks.length === 0) {
|
|
213
|
+
return {
|
|
214
|
+
hasConflict: false,
|
|
215
|
+
blocked: false,
|
|
216
|
+
mode: config.mode,
|
|
217
|
+
conflictingLocks: [],
|
|
218
|
+
analysis: "No active locks. No constraints to check against.",
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Run heuristic against all active locks
|
|
223
|
+
const conflicting = [];
|
|
224
|
+
for (const lock of activeLocks) {
|
|
225
|
+
const result = analyzeConflict(proposedAction, lock.text);
|
|
226
|
+
if (result.isConflict) {
|
|
227
|
+
conflicting.push({
|
|
228
|
+
id: lock.id,
|
|
229
|
+
text: lock.text,
|
|
230
|
+
confidence: result.confidence,
|
|
231
|
+
level: result.level,
|
|
232
|
+
reasons: result.reasons,
|
|
233
|
+
source: "heuristic",
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// If all heuristic conflicts are HIGH, trust them — skip proxy
|
|
239
|
+
const allHigh = conflicting.length > 0 && conflicting.every((c) => c.confidence > 70);
|
|
240
|
+
|
|
241
|
+
// Grey zone: call proxy for Gemini coverage
|
|
242
|
+
if (!allHigh) {
|
|
243
|
+
try {
|
|
244
|
+
const { checkConflictAsync } = await import("./conflict.js");
|
|
245
|
+
const asyncResult = await checkConflictAsync(root, proposedAction);
|
|
246
|
+
|
|
247
|
+
if (asyncResult.hasConflict) {
|
|
248
|
+
// Merge: use async result's locks (which already merged heuristic + proxy)
|
|
249
|
+
const merged = new Map();
|
|
250
|
+
for (const c of conflicting) merged.set(c.text, c);
|
|
251
|
+
for (const c of asyncResult.conflictingLocks) {
|
|
252
|
+
const existing = merged.get(c.text);
|
|
253
|
+
if (!existing || c.confidence > existing.confidence) {
|
|
254
|
+
merged.set(c.text, {
|
|
255
|
+
id: c.id || c.lockId,
|
|
256
|
+
text: c.text,
|
|
257
|
+
confidence: c.confidence,
|
|
258
|
+
level: c.level,
|
|
259
|
+
reasons: c.reasons || [],
|
|
260
|
+
source: c.source || "proxy",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
conflicting.length = 0;
|
|
265
|
+
conflicting.push(...merged.values());
|
|
266
|
+
}
|
|
267
|
+
} catch (_) {
|
|
268
|
+
// Proxy unavailable — continue with heuristic results
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (conflicting.length === 0) {
|
|
273
|
+
return {
|
|
274
|
+
hasConflict: false,
|
|
275
|
+
blocked: false,
|
|
276
|
+
mode: config.mode,
|
|
277
|
+
conflictingLocks: [],
|
|
278
|
+
analysis: `Checked against ${activeLocks.length} active lock(s). No conflicts detected. Proceed with caution.`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Sort by confidence descending
|
|
283
|
+
conflicting.sort((a, b) => b.confidence - a.confidence);
|
|
284
|
+
|
|
285
|
+
const topConfidence = conflicting[0].confidence;
|
|
286
|
+
const meetsThreshold = topConfidence >= config.blockThreshold;
|
|
287
|
+
const blocked = config.mode === "hard" && meetsThreshold;
|
|
288
|
+
|
|
289
|
+
const details = conflicting
|
|
290
|
+
.map(
|
|
291
|
+
(c) =>
|
|
292
|
+
`- [${c.level}] "${c.text}" (confidence: ${c.confidence}%)\n Reasons: ${c.reasons.join("; ")}`
|
|
293
|
+
)
|
|
294
|
+
.join("\n");
|
|
295
|
+
|
|
296
|
+
addViolation(brain, {
|
|
297
|
+
at: nowIso(),
|
|
298
|
+
action: proposedAction,
|
|
299
|
+
locks: conflicting.map((c) => ({ id: c.id, text: c.text, confidence: c.confidence, level: c.level })),
|
|
300
|
+
topLevel: conflicting[0].level,
|
|
301
|
+
topConfidence,
|
|
302
|
+
enforced: blocked,
|
|
303
|
+
mode: config.mode,
|
|
304
|
+
});
|
|
305
|
+
writeBrain(root, brain);
|
|
306
|
+
|
|
307
|
+
const modeLabel = blocked
|
|
308
|
+
? "BLOCKED — Hard enforcement active. This action cannot proceed."
|
|
309
|
+
: "WARNING — Advisory mode. Review before proceeding.";
|
|
310
|
+
|
|
311
|
+
return {
|
|
312
|
+
hasConflict: true,
|
|
313
|
+
blocked,
|
|
314
|
+
mode: config.mode,
|
|
315
|
+
threshold: config.blockThreshold,
|
|
316
|
+
topConfidence,
|
|
317
|
+
conflictingLocks: conflicting,
|
|
318
|
+
analysis: `${modeLabel}\n\nConflict with ${conflicting.length} lock(s):\n${details}`,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
193
322
|
/**
|
|
194
323
|
* Override a lock for a specific action, with a reason.
|
|
195
324
|
* Logged to audit trail. Triggers escalation if overridden too many times.
|
package/src/core/engine.js
CHANGED
package/src/core/llm-checker.js
CHANGED
package/src/core/lock-author.js
CHANGED
|
@@ -251,17 +251,26 @@ export function rewriteLock(lockText, verb, subject) {
|
|
|
251
251
|
// "Never delete X" → "X must be preserved — delete and remove operations are prohibited"
|
|
252
252
|
// CRITICAL: include the original verb so euphemism matching can find it
|
|
253
253
|
// ("phase out" → "remove" needs "remove" in the lock text)
|
|
254
|
-
|
|
254
|
+
const destNote = verb === "remove"
|
|
255
|
+
? "remove and delete operations are prohibited"
|
|
256
|
+
: `${verb} and remove operations are prohibited`;
|
|
257
|
+
return `${cleanSubject} must be preserved — ${destNote}.`;
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
if (isModification) {
|
|
258
261
|
// "Never modify X" → "X is frozen — modify and change operations are prohibited"
|
|
259
|
-
|
|
262
|
+
const modNote = verb === "change"
|
|
263
|
+
? "change operations are prohibited"
|
|
264
|
+
: `${verb} and change operations are prohibited`;
|
|
265
|
+
return `${cleanSubject} is frozen — ${modNote}.`;
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
if (isMovement) {
|
|
263
269
|
// "Never migrate X" → "X must remain unchanged — migrate and replace operations are prohibited"
|
|
264
|
-
|
|
270
|
+
const moveNote = verb === "replace"
|
|
271
|
+
? "replace operations are prohibited"
|
|
272
|
+
: `${verb} and replace operations are prohibited`;
|
|
273
|
+
return `${cleanSubject} must remain unchanged — ${moveNote}.`;
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
if (isToggle) {
|
package/src/core/semantics.js
CHANGED
|
@@ -31,7 +31,8 @@ export const SYNONYM_GROUPS = [
|
|
|
31
31
|
"rewrite", "revise", "amend", "adjust", "tweak"],
|
|
32
32
|
["replace", "swap", "substitute", "switch", "exchange",
|
|
33
33
|
"override", "overwrite"],
|
|
34
|
-
["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize"
|
|
34
|
+
["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize",
|
|
35
|
+
"transition"],
|
|
35
36
|
["rename", "relabel", "rebrand", "alias"],
|
|
36
37
|
["merge", "combine", "consolidate", "unify", "join", "blend"],
|
|
37
38
|
["split", "separate", "partition", "divide", "fork", "decompose"],
|
|
@@ -46,6 +47,9 @@ export const SYNONYM_GROUPS = [
|
|
|
46
47
|
// --- Data stores ---
|
|
47
48
|
["database", "db", "datastore", "data store", "schema", "table",
|
|
48
49
|
"collection", "index", "migration", "sql", "nosql", "storage"],
|
|
50
|
+
["postgresql", "postgres", "mysql", "mongodb", "mongo", "firebase",
|
|
51
|
+
"firestore", "supabase", "dynamodb", "redis", "sqlite", "mariadb",
|
|
52
|
+
"cockroachdb", "cassandra", "couchdb", "neo4j"],
|
|
49
53
|
["record", "row", "document", "entry", "item", "entity", "tuple"],
|
|
50
54
|
["column", "field", "attribute", "property", "key"],
|
|
51
55
|
["backup", "snapshot", "dump", "export"],
|
|
@@ -108,6 +112,9 @@ export const SYNONYM_GROUPS = [
|
|
|
108
112
|
"remuneration", "stipend"],
|
|
109
113
|
["payment gateway", "payment provider", "payment processor",
|
|
110
114
|
"payment service", "payment platform"],
|
|
115
|
+
["razorpay", "stripe", "paypal", "phonepe", "paytm", "ccavenue",
|
|
116
|
+
"cashfree", "braintree", "adyen", "square", "google pay", "gpay",
|
|
117
|
+
"juspay", "billdesk", "instamojo"],
|
|
111
118
|
|
|
112
119
|
// --- IoT / firmware ---
|
|
113
120
|
["firmware", "firmware update", "ota", "over the air",
|
|
@@ -127,7 +134,10 @@ export const SYNONYM_GROUPS = [
|
|
|
127
134
|
"suspended", "blocked user"],
|
|
128
135
|
["user data", "user information", "user records", "pii",
|
|
129
136
|
"personally identifiable information", "personal data",
|
|
130
|
-
"gdpr", "data protection"
|
|
137
|
+
"gdpr", "data protection", "ssn", "social security number",
|
|
138
|
+
"social security", "email address", "email addresses",
|
|
139
|
+
"phone number", "phone numbers", "date of birth", "dob",
|
|
140
|
+
"passport number", "driver license", "national id"],
|
|
131
141
|
|
|
132
142
|
// --- DevOps / Infrastructure ---
|
|
133
143
|
["container", "docker", "kubernetes", "k8s", "pod",
|
|
@@ -243,6 +253,10 @@ export const EUPHEMISM_MAP = {
|
|
|
243
253
|
"work around": ["bypass", "circumvent"],
|
|
244
254
|
"shortcut": ["bypass", "skip"],
|
|
245
255
|
|
|
256
|
+
// Migration/transition euphemisms
|
|
257
|
+
"transition": ["migrate", "switch", "change", "move", "replace"],
|
|
258
|
+
"transition to": ["migrate to", "switch to", "change to", "move to"],
|
|
259
|
+
|
|
246
260
|
// Financial / accounting euphemisms
|
|
247
261
|
"reconcile": ["modify", "adjust", "change", "alter"],
|
|
248
262
|
"reverse": ["undo", "revert", "modify", "change"],
|
|
@@ -313,12 +327,16 @@ export const EUPHEMISM_MAP = {
|
|
|
313
327
|
"rotate": ["change", "replace", "renew", "modify"],
|
|
314
328
|
"renew": ["change", "replace", "rotate", "modify"],
|
|
315
329
|
|
|
316
|
-
// Security euphemisms
|
|
330
|
+
// Security / data exposure euphemisms
|
|
317
331
|
"make visible": ["expose", "reveal", "public"],
|
|
318
332
|
"make viewable": ["expose", "reveal", "public"],
|
|
319
333
|
"make accessible":["expose", "reveal", "public"],
|
|
320
334
|
"make public": ["expose", "reveal"],
|
|
321
335
|
"transmit": ["send", "transfer", "expose"],
|
|
336
|
+
"export": ["extract", "expose", "dump", "download"],
|
|
337
|
+
"exfiltrate": ["extract", "steal", "expose", "leak"],
|
|
338
|
+
"scrape": ["extract", "collect", "harvest"],
|
|
339
|
+
"harvest": ["collect", "extract", "scrape"],
|
|
322
340
|
|
|
323
341
|
// Encryption euphemisms
|
|
324
342
|
"unencrypted": ["without encryption", "disable encryption", "no encryption", "plaintext"],
|
|
@@ -397,27 +415,51 @@ export const CONCEPT_MAP = {
|
|
|
397
415
|
"wages": ["salary", "payroll", "compensation", "financial records"],
|
|
398
416
|
"compensation": ["salary", "payroll", "wages", "financial records"],
|
|
399
417
|
|
|
400
|
-
// Payment providers (brand names → payment gateway concept)
|
|
418
|
+
// Payment providers (brand names → payment gateway concept + cross-references)
|
|
401
419
|
"razorpay": ["payment gateway", "payment processing", "payment",
|
|
402
|
-
"transaction", "billing"
|
|
420
|
+
"transaction", "billing", "stripe", "paypal",
|
|
421
|
+
"phonepe", "paytm", "ccavenue", "cashfree"],
|
|
403
422
|
"phonepe": ["payment gateway", "payment processing", "payment",
|
|
404
|
-
"upi", "transaction"
|
|
423
|
+
"upi", "transaction", "razorpay", "paytm",
|
|
424
|
+
"stripe", "google pay"],
|
|
405
425
|
"ccavenue": ["payment gateway", "payment processing", "payment",
|
|
406
|
-
"transaction", "billing"
|
|
426
|
+
"transaction", "billing", "razorpay", "stripe",
|
|
427
|
+
"paypal", "cashfree"],
|
|
407
428
|
"paytm": ["payment gateway", "payment processing", "payment",
|
|
408
|
-
"upi", "transaction"
|
|
429
|
+
"upi", "transaction", "razorpay", "phonepe",
|
|
430
|
+
"stripe", "google pay"],
|
|
409
431
|
"paypal": ["payment gateway", "payment processing", "payment",
|
|
410
|
-
"transaction", "billing"
|
|
432
|
+
"transaction", "billing", "stripe", "razorpay",
|
|
433
|
+
"braintree", "adyen"],
|
|
411
434
|
"stripe": ["payment gateway", "payment processing", "payment",
|
|
412
|
-
"transaction", "billing"
|
|
435
|
+
"transaction", "billing", "razorpay", "paypal",
|
|
436
|
+
"braintree", "adyen", "square"],
|
|
413
437
|
"square": ["payment gateway", "payment processing", "payment",
|
|
414
|
-
"transaction", "billing"],
|
|
438
|
+
"transaction", "billing", "stripe", "paypal"],
|
|
415
439
|
"adyen": ["payment gateway", "payment processing", "payment",
|
|
416
|
-
"transaction", "billing"
|
|
440
|
+
"transaction", "billing", "stripe", "paypal",
|
|
441
|
+
"braintree"],
|
|
417
442
|
"braintree": ["payment gateway", "payment processing", "payment",
|
|
418
|
-
"transaction", "billing"
|
|
443
|
+
"transaction", "billing", "stripe", "paypal",
|
|
444
|
+
"adyen"],
|
|
445
|
+
"cashfree": ["payment gateway", "payment processing", "payment",
|
|
446
|
+
"transaction", "billing", "razorpay", "stripe",
|
|
447
|
+
"ccavenue", "paytm"],
|
|
448
|
+
"google pay": ["payment gateway", "payment processing", "payment",
|
|
449
|
+
"upi", "transaction", "phonepe", "paytm",
|
|
450
|
+
"razorpay", "gpay"],
|
|
451
|
+
"gpay": ["payment gateway", "payment processing", "payment",
|
|
452
|
+
"upi", "transaction", "google pay", "phonepe",
|
|
453
|
+
"paytm", "razorpay"],
|
|
454
|
+
"juspay": ["payment gateway", "payment processing", "payment",
|
|
455
|
+
"transaction", "razorpay", "stripe", "cashfree"],
|
|
456
|
+
"billdesk": ["payment gateway", "payment processing", "payment",
|
|
457
|
+
"transaction", "billing", "razorpay", "ccavenue"],
|
|
458
|
+
"instamojo": ["payment gateway", "payment processing", "payment",
|
|
459
|
+
"transaction", "billing", "razorpay", "cashfree"],
|
|
419
460
|
"upi": ["payment gateway", "payment processing", "phonepe",
|
|
420
|
-
"paytm", "
|
|
461
|
+
"paytm", "google pay", "razorpay",
|
|
462
|
+
"transaction", "payment"],
|
|
421
463
|
|
|
422
464
|
// Logistics / Supply Chain
|
|
423
465
|
"shipment": ["cargo", "freight", "consignment", "delivery", "package",
|
|
@@ -475,6 +517,30 @@ export const CONCEPT_MAP = {
|
|
|
475
517
|
"product": ["item", "sku", "catalog", "merchandise", "product listing"],
|
|
476
518
|
"price": ["pricing", "cost", "amount", "rate", "charge"],
|
|
477
519
|
|
|
520
|
+
// Database technologies (brand names → database concept)
|
|
521
|
+
"postgresql": ["database", "db", "sql", "postgres", "mysql",
|
|
522
|
+
"mongodb", "firebase", "supabase"],
|
|
523
|
+
"postgres": ["database", "db", "sql", "postgresql", "mysql",
|
|
524
|
+
"mongodb", "firebase", "supabase"],
|
|
525
|
+
"mysql": ["database", "db", "sql", "postgresql", "mongodb",
|
|
526
|
+
"firebase", "supabase", "mariadb"],
|
|
527
|
+
"mongodb": ["database", "db", "nosql", "mongo", "postgresql",
|
|
528
|
+
"firebase", "supabase", "dynamodb"],
|
|
529
|
+
"mongo": ["database", "db", "nosql", "mongodb", "postgresql",
|
|
530
|
+
"firebase", "supabase"],
|
|
531
|
+
"firebase": ["database", "db", "nosql", "firestore", "supabase",
|
|
532
|
+
"postgresql", "mongodb", "backend"],
|
|
533
|
+
"firestore": ["database", "db", "nosql", "firebase", "mongodb",
|
|
534
|
+
"supabase", "dynamodb"],
|
|
535
|
+
"supabase": ["database", "db", "postgresql", "firebase",
|
|
536
|
+
"mongodb", "backend", "auth"],
|
|
537
|
+
"dynamodb": ["database", "db", "nosql", "mongodb", "firebase",
|
|
538
|
+
"cassandra"],
|
|
539
|
+
"redis": ["database", "db", "cache", "nosql", "datastore"],
|
|
540
|
+
"sqlite": ["database", "db", "sql", "embedded database"],
|
|
541
|
+
"mariadb": ["database", "db", "sql", "mysql", "postgresql"],
|
|
542
|
+
"cassandra": ["database", "db", "nosql", "dynamodb", "mongodb"],
|
|
543
|
+
|
|
478
544
|
// Audit/logging
|
|
479
545
|
"audit logging": ["audit log", "audit trail", "logging", "monitoring"],
|
|
480
546
|
"audit log": ["audit logging", "audit trail", "logging"],
|
|
@@ -499,17 +565,27 @@ export const CONCEPT_MAP = {
|
|
|
499
565
|
"network isolation", "segmentation"],
|
|
500
566
|
"network isolation": ["network segments", "segmentation", "firewall", "air gap"],
|
|
501
567
|
|
|
502
|
-
// User data
|
|
568
|
+
// User data / PII
|
|
503
569
|
"pii": ["personal data", "user data", "personally identifiable information",
|
|
504
|
-
"user information", "gdpr"
|
|
505
|
-
|
|
570
|
+
"user information", "gdpr", "ssn", "social security",
|
|
571
|
+
"email address", "phone number"],
|
|
572
|
+
"personal data": ["pii", "user data", "user information", "gdpr", "data protection",
|
|
573
|
+
"ssn", "social security", "email address"],
|
|
506
574
|
"gdpr": ["data protection", "consent", "privacy", "personal data", "pii",
|
|
507
575
|
"data subject", "right to erasure", "user data"],
|
|
508
576
|
"data protection": ["gdpr", "privacy", "consent", "personal data", "pii",
|
|
509
577
|
"data subject", "compliance"],
|
|
510
578
|
"consent": ["gdpr", "data protection", "opt-in", "opt-out", "user consent",
|
|
511
579
|
"privacy", "data subject"],
|
|
512
|
-
"user data": ["pii", "personal data", "user information", "user records"
|
|
580
|
+
"user data": ["pii", "personal data", "user information", "user records",
|
|
581
|
+
"ssn", "email address"],
|
|
582
|
+
"ssn": ["social security number", "social security", "pii",
|
|
583
|
+
"personal data", "user data", "national id"],
|
|
584
|
+
"social security": ["ssn", "social security number", "pii", "personal data"],
|
|
585
|
+
"social security number": ["ssn", "social security", "pii", "personal data"],
|
|
586
|
+
"email address": ["pii", "user data", "personal data", "contact information"],
|
|
587
|
+
"email addresses": ["pii", "user data", "personal data", "email address"],
|
|
588
|
+
"phone number": ["pii", "user data", "personal data", "contact information"],
|
|
513
589
|
|
|
514
590
|
// Encryption
|
|
515
591
|
"cryptographic signatures": ["code signing", "digital signatures",
|
|
@@ -1088,7 +1164,7 @@ function extractProhibitedVerb(lockText) {
|
|
|
1088
1164
|
const NEUTRAL_ACTION_VERBS = [
|
|
1089
1165
|
"modify", "change", "alter", "reconfigure", "rework",
|
|
1090
1166
|
"overhaul", "restructure", "refactor", "redesign",
|
|
1091
|
-
"replace", "swap", "switch", "migrate", "substitute",
|
|
1167
|
+
"replace", "swap", "switch", "migrate", "transition", "substitute",
|
|
1092
1168
|
"touch", "mess", "configure", "optimize", "tweak",
|
|
1093
1169
|
"extend", "shorten", "adjust", "customize", "personalize",
|
|
1094
1170
|
];
|
|
@@ -1429,6 +1505,19 @@ function _compareSubjectsInline(actionText, lockText) {
|
|
|
1429
1505
|
if (as.includes(" ") && ls.includes(" ")) strongMatchCount++;
|
|
1430
1506
|
continue;
|
|
1431
1507
|
}
|
|
1508
|
+
// Synonym group match — same category items (e.g., Razorpay ↔ Stripe)
|
|
1509
|
+
// Always STRONG because being in the same synonym group means same domain scope.
|
|
1510
|
+
let isSynonym = false;
|
|
1511
|
+
for (const group of SYNONYM_GROUPS) {
|
|
1512
|
+
if (group.includes(as) && group.includes(ls)) {
|
|
1513
|
+
matched.push(`synonym: ${as} ↔ ${ls}`);
|
|
1514
|
+
strongMatchCount++;
|
|
1515
|
+
isSynonym = true;
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (isSynonym) continue;
|
|
1520
|
+
|
|
1432
1521
|
// Concept-expanded match — only STRONG if BOTH sides are multi-word phrases
|
|
1433
1522
|
// Single-word concept matches (account~ledger, device~iot) are too ambiguous
|
|
1434
1523
|
// to be considered strong scope overlap.
|
|
@@ -1559,7 +1648,21 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1559
1648
|
}
|
|
1560
1649
|
}
|
|
1561
1650
|
|
|
1562
|
-
// 4b.
|
|
1651
|
+
// 4b. Split-phrase euphemisms — "make X public", "make X visible", etc.
|
|
1652
|
+
// These have intervening words between the verb and the key modifier.
|
|
1653
|
+
const SPLIT_PHRASE_PATTERNS = [
|
|
1654
|
+
[/\bmake\s+\w+\s+public\b/i, "expose", "make ... public"],
|
|
1655
|
+
[/\bmake\s+\w+\s+visible\b/i, "expose", "make ... visible"],
|
|
1656
|
+
[/\bmake\s+\w+\s+accessible\b/i, "expose", "make ... accessible"],
|
|
1657
|
+
[/\bmake\s+\w+\s+(?:data\s+)?public\b/i, "expose", "make ... public"],
|
|
1658
|
+
];
|
|
1659
|
+
for (const [pattern, meaning, label] of SPLIT_PHRASE_PATTERNS) {
|
|
1660
|
+
if (pattern.test(actionText) && lockExpanded.expanded.includes(meaning)) {
|
|
1661
|
+
euphemismMatches.push(`"${label}" (euphemism for ${meaning})`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// 4c. Destructive method verbs — "by replacing", "through overwriting", "via deleting"
|
|
1563
1666
|
// When an action uses a positive primary verb but employs a destructive method,
|
|
1564
1667
|
// the method verb is the real operation. "Optimize X by replacing Y" = replacement.
|
|
1565
1668
|
const DESTRUCTIVE_METHODS = new Set([
|
|
@@ -1699,9 +1802,45 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1699
1802
|
|
|
1700
1803
|
// Apply the subject relevance gate based on match quality
|
|
1701
1804
|
if (!hasSubjectMatch && (synonymMatches.length > 0 || euphemismMatches.length > 0)) {
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1805
|
+
// Exception: if the action's euphemism DIRECTLY matches the lock's prohibited
|
|
1806
|
+
// verb AND there's at least some shared content word, skip the subject gate.
|
|
1807
|
+
// "Make the data public" euphemism = "expose", lock = "Never expose user data"
|
|
1808
|
+
// → euphemism proves the conflict + "data" provides content overlap.
|
|
1809
|
+
// But "Tax statement export" vs "Never expose portfolio positions" has no
|
|
1810
|
+
// content overlap — gate should still fire to prevent false positive.
|
|
1811
|
+
// Note: we check raw word overlap (ignoring stopwords filter) because common
|
|
1812
|
+
// words like "data" are stopwords but still provide content signal.
|
|
1813
|
+
const _prohibVerb = extractProhibitedVerb(lockText);
|
|
1814
|
+
const _GATE_SKIP_STOPWORDS = new Set([
|
|
1815
|
+
"a", "an", "the", "this", "that", "it", "its", "our", "their",
|
|
1816
|
+
"your", "my", "his", "her", "we", "they", "them", "i",
|
|
1817
|
+
"to", "of", "in", "on", "at", "by", "up", "as", "or", "and",
|
|
1818
|
+
"nor", "but", "so", "if", "no", "not", "is", "be", "do", "did",
|
|
1819
|
+
"with", "from", "for", "into", "over", "under", "between", "through",
|
|
1820
|
+
"about", "before", "after", "during", "while",
|
|
1821
|
+
"are", "was", "were", "been", "being", "have", "has", "had",
|
|
1822
|
+
"will", "would", "could", "should", "may", "might", "shall",
|
|
1823
|
+
"can", "need", "must", "does", "done",
|
|
1824
|
+
"all", "any", "every", "some", "most", "other", "each", "both",
|
|
1825
|
+
"few", "more", "less", "many", "much",
|
|
1826
|
+
"also", "just", "very", "too", "really", "quite", "only", "then",
|
|
1827
|
+
"now", "here", "there", "when", "where", "how", "what", "which",
|
|
1828
|
+
"who", "whom", "why",
|
|
1829
|
+
// Common verbs/adjectives (but NOT nouns like "data", "system")
|
|
1830
|
+
"way", "thing", "things", "part", "set", "use",
|
|
1831
|
+
"using", "used", "make", "made", "new", "get", "got",
|
|
1832
|
+
]);
|
|
1833
|
+
const rawWordOverlap = actionTokens.words.some(w =>
|
|
1834
|
+
lockTokens.words.includes(w) && !_GATE_SKIP_STOPWORDS.has(w));
|
|
1835
|
+
const euphemismMatchesProhibitedVerb = _prohibVerb &&
|
|
1836
|
+
rawWordOverlap &&
|
|
1837
|
+
euphemismMatches.some(m => m.includes(`euphemism for ${_prohibVerb}`));
|
|
1838
|
+
|
|
1839
|
+
if (!euphemismMatchesProhibitedVerb) {
|
|
1840
|
+
// NO subject match at all — verb-only match → heavy reduction
|
|
1841
|
+
score = Math.floor(score * 0.15);
|
|
1842
|
+
reasons.push("subject gate: no subject overlap — verb-only match, likely false positive");
|
|
1843
|
+
}
|
|
1705
1844
|
} else if (hasVocabSubjectMatch && !hasScopeMatch && subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
|
|
1706
1845
|
// Vocabulary overlap exists but subjects point to DIFFERENT scopes
|
|
1707
1846
|
score = Math.floor(score * 0.35);
|
|
@@ -1906,11 +2045,11 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1906
2045
|
"font", "fonts", "color", "colors", "colour", "theme", "themes",
|
|
1907
2046
|
"styling", "style", "styles", "css", "icon", "icons", "layout",
|
|
1908
2047
|
"margin", "padding", "border", "background", "typography", "spacing",
|
|
1909
|
-
"alignment", "animation", "
|
|
1910
|
-
"placeholder", "logo", "
|
|
2048
|
+
"alignment", "animation", "hover", "tooltip",
|
|
2049
|
+
"placeholder", "logo", "banner", "hero", "avatar",
|
|
1911
2050
|
"sidebar", "navigation", "menu", "breadcrumb", "footer",
|
|
1912
2051
|
]);
|
|
1913
|
-
if (!intentAligned && !
|
|
2052
|
+
if (!intentAligned && !hasStrongVocabMatch) {
|
|
1914
2053
|
const actionLower = actionText.toLowerCase();
|
|
1915
2054
|
const actionWords = actionLower.split(/\s+/).map(w => w.replace(/[^a-z]/g, ""));
|
|
1916
2055
|
const hasUISubject = actionWords.some(w => UI_COSMETIC_WORDS.has(w));
|
|
@@ -1983,7 +2122,60 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1983
2122
|
// MAIN ENTRY POINT
|
|
1984
2123
|
// ===================================================================
|
|
1985
2124
|
|
|
2125
|
+
// Question framing prefixes that should be stripped before analysis.
|
|
2126
|
+
// "Should we add Razorpay?" → "add Razorpay"
|
|
2127
|
+
// "What if we used Firebase?" → "used Firebase"
|
|
2128
|
+
const QUESTION_PREFIXES = [
|
|
2129
|
+
/^would\s+it\s+make\s+sense\s+to\s+/i,
|
|
2130
|
+
/^would\s+it\s+be\s+(?:better|good|wise|smart|possible)\s+(?:to|if)\s+(?:we\s+)?/i,
|
|
2131
|
+
/^what\s+if\s+we\s+(?:could\s+)?/i,
|
|
2132
|
+
/^what\s+about\s+/i,
|
|
2133
|
+
/^how\s+about\s+(?:we\s+)?/i,
|
|
2134
|
+
/^should\s+we\s+(?:consider\s+)?/i,
|
|
2135
|
+
/^could\s+we\s+(?:possibly\s+)?/i,
|
|
2136
|
+
/^can\s+we\s+/i,
|
|
2137
|
+
/^i\s+was\s+wondering\s+if\s+(?:we\s+)?(?:could\s+)?/i,
|
|
2138
|
+
/^maybe\s+we\s+(?:should\s+)?(?:consider\s+)?/i,
|
|
2139
|
+
/^perhaps\s+we\s+(?:should\s+)?(?:consider\s+)?/i,
|
|
2140
|
+
/^wouldn't\s+it\s+be\s+(?:better|good)\s+(?:to|if)\s+(?:we\s+)?/i,
|
|
2141
|
+
/^is\s+(?:it\s+)?(?:a\s+)?(?:good\s+idea\s+)?(?:to\s+)?/i,
|
|
2142
|
+
/^let\s+me\s+/i,
|
|
2143
|
+
/^we\s+should\s+(?:probably\s+)?(?:consider\s+)?(?:look\s+at\s+)?/i,
|
|
2144
|
+
/^explore\s+(?:using\s+)?/i,
|
|
2145
|
+
];
|
|
2146
|
+
|
|
2147
|
+
// Special transformations where simple prefix stripping loses the subject.
|
|
2148
|
+
// "Would Firebase be better for real-time sync?" → "switch to Firebase for real-time sync"
|
|
2149
|
+
const QUESTION_TRANSFORMS = [
|
|
2150
|
+
[/^would\s+(.+?)\s+be\s+(?:a\s+)?better\s+(?:option\s+)?(?:for|than)\s+(.+)/i, "switch to $1 for $2"],
|
|
2151
|
+
[/^is\s+(.+?)\s+(?:a\s+)?better\s+(?:option|choice|alternative)\s+(?:for|than)\s+(.+)/i, "switch to $1 for $2"],
|
|
2152
|
+
[/^wouldn't\s+(.+?)\s+be\s+(?:a\s+)?better\s+(?:option|choice)?\s*(?:for|than)?\s*(.+)?/i, "switch to $1 $2"],
|
|
2153
|
+
];
|
|
2154
|
+
|
|
2155
|
+
function stripQuestionFraming(text) {
|
|
2156
|
+
let stripped = text;
|
|
2157
|
+
|
|
2158
|
+
// Try special transformations first (they preserve the subject)
|
|
2159
|
+
for (const [pattern, replacement] of QUESTION_TRANSFORMS) {
|
|
2160
|
+
if (pattern.test(stripped)) {
|
|
2161
|
+
stripped = stripped.replace(pattern, replacement).trim();
|
|
2162
|
+
stripped = stripped.replace(/\?\s*$/, "").trim();
|
|
2163
|
+
return stripped || text;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
// Then try simple prefix stripping
|
|
2168
|
+
for (const pattern of QUESTION_PREFIXES) {
|
|
2169
|
+
stripped = stripped.replace(pattern, "");
|
|
2170
|
+
}
|
|
2171
|
+
// Also remove trailing question marks
|
|
2172
|
+
stripped = stripped.replace(/\?\s*$/, "").trim();
|
|
2173
|
+
return stripped || text; // fallback to original if everything was stripped
|
|
2174
|
+
}
|
|
2175
|
+
|
|
1986
2176
|
export function analyzeConflict(actionText, lockText) {
|
|
2177
|
+
// Strip question framing so "Should we add Razorpay?" → "add Razorpay"
|
|
2178
|
+
actionText = stripQuestionFraming(actionText);
|
|
1987
2179
|
const clauses = splitClauses(actionText);
|
|
1988
2180
|
|
|
1989
2181
|
const clauseResults = clauses.map(clause => ({
|
package/src/core/telemetry.js
CHANGED
|
@@ -257,7 +257,7 @@ export async function flushToRemote(root) {
|
|
|
257
257
|
// Build anonymized payload
|
|
258
258
|
const payload = {
|
|
259
259
|
instanceId: summary.instanceId,
|
|
260
|
-
version: "4.
|
|
260
|
+
version: "4.5.0",
|
|
261
261
|
totalCalls: summary.totalCalls,
|
|
262
262
|
avgResponseMs: summary.avgResponseMs,
|
|
263
263
|
conflicts: summary.conflicts,
|
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">v4.
|
|
92
|
+
<div class="meta">v4.5.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 v4.
|
|
185
|
+
SpecLock v4.5.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
|
@@ -91,7 +91,7 @@ import { fileURLToPath } from "url";
|
|
|
91
91
|
import _path from "path";
|
|
92
92
|
|
|
93
93
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
94
|
-
const VERSION = "4.
|
|
94
|
+
const VERSION = "4.5.0";
|
|
95
95
|
const AUTHOR = "Sandeep Roy";
|
|
96
96
|
const START_TIME = Date.now();
|
|
97
97
|
|
package/src/mcp/server.js
CHANGED
|
@@ -100,7 +100,7 @@ const PROJECT_ROOT =
|
|
|
100
100
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
101
101
|
|
|
102
102
|
// --- MCP Server ---
|
|
103
|
-
const VERSION = "4.
|
|
103
|
+
const VERSION = "4.5.0";
|
|
104
104
|
const AUTHOR = "Sandeep Roy";
|
|
105
105
|
|
|
106
106
|
const server = new McpServer(
|