speclock 1.7.0 → 2.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 +28 -13
- package/package.json +9 -3
- package/src/cli/index.js +82 -5
- package/src/core/audit.js +237 -0
- package/src/core/compliance.js +291 -0
- package/src/core/engine.js +48 -68
- package/src/core/license.js +221 -0
- package/src/core/llm-checker.js +239 -0
- package/src/core/semantics.js +1096 -0
- package/src/core/storage.js +9 -0
- package/src/mcp/http-server.js +120 -5
- package/src/mcp/server.js +78 -2
|
@@ -0,0 +1,1096 @@
|
|
|
1
|
+
// ===================================================================
|
|
2
|
+
// SpecLock Semantic Analysis Engine v2
|
|
3
|
+
// Replaces keyword matching with real semantic conflict detection.
|
|
4
|
+
// Zero external dependencies — pure JavaScript.
|
|
5
|
+
// ===================================================================
|
|
6
|
+
|
|
7
|
+
// ===================================================================
|
|
8
|
+
// SYNONYM GROUPS (55 groups)
|
|
9
|
+
// Each group contains words/phrases that are semantically equivalent.
|
|
10
|
+
// ===================================================================
|
|
11
|
+
|
|
12
|
+
export const SYNONYM_GROUPS = [
|
|
13
|
+
// --- Destructive actions ---
|
|
14
|
+
["remove", "delete", "drop", "eliminate", "destroy", "kill", "purge",
|
|
15
|
+
"wipe", "erase", "obliterate", "expunge", "nuke"],
|
|
16
|
+
["truncate", "clear", "empty", "flush", "reset", "zero-out"],
|
|
17
|
+
["disable", "deactivate", "turn off", "switch off", "shut off",
|
|
18
|
+
"shut down", "power off", "halt", "suspend", "pause", "freeze"],
|
|
19
|
+
["uninstall", "unplug", "disconnect", "detach", "decouple", "sever"],
|
|
20
|
+
["downgrade", "rollback", "revert", "regress", "undo"],
|
|
21
|
+
|
|
22
|
+
// --- Constructive actions ---
|
|
23
|
+
["add", "create", "introduce", "insert", "new", "generate", "produce", "spawn"],
|
|
24
|
+
["enable", "activate", "turn on", "switch on", "start", "boot",
|
|
25
|
+
"initialize", "launch", "engage"],
|
|
26
|
+
["install", "plug in", "connect", "attach", "couple", "integrate", "mount"],
|
|
27
|
+
["upgrade", "update", "patch", "bump", "advance"],
|
|
28
|
+
|
|
29
|
+
// --- Modification actions ---
|
|
30
|
+
["change", "modify", "alter", "update", "mutate", "transform",
|
|
31
|
+
"rewrite", "revise", "amend", "adjust", "tweak"],
|
|
32
|
+
["replace", "swap", "substitute", "switch", "exchange",
|
|
33
|
+
"override", "overwrite"],
|
|
34
|
+
["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize"],
|
|
35
|
+
["rename", "relabel", "rebrand", "alias"],
|
|
36
|
+
["merge", "combine", "consolidate", "unify", "join", "blend"],
|
|
37
|
+
["split", "separate", "partition", "divide", "fork", "decompose"],
|
|
38
|
+
|
|
39
|
+
// --- Breaking changes ---
|
|
40
|
+
["break", "breaking", "incompatible", "destabilize", "corrupt", "damage"],
|
|
41
|
+
|
|
42
|
+
// --- Visibility ---
|
|
43
|
+
["public", "external", "exposed", "user-facing", "client-facing", "open", "visible"],
|
|
44
|
+
["private", "internal", "hidden", "encapsulated", "restricted", "closed", "secret"],
|
|
45
|
+
|
|
46
|
+
// --- Data stores ---
|
|
47
|
+
["database", "db", "datastore", "data store", "schema", "table",
|
|
48
|
+
"collection", "index", "migration", "sql", "nosql", "storage"],
|
|
49
|
+
["record", "row", "document", "entry", "item", "entity", "tuple"],
|
|
50
|
+
["column", "field", "attribute", "property", "key"],
|
|
51
|
+
["backup", "snapshot", "dump", "export"],
|
|
52
|
+
|
|
53
|
+
// --- API & networking ---
|
|
54
|
+
["api", "endpoint", "route", "rest", "graphql", "rpc", "webhook",
|
|
55
|
+
"interface", "service"],
|
|
56
|
+
["request", "call", "invoke", "query", "fetch"],
|
|
57
|
+
["response", "reply", "result", "output", "payload"],
|
|
58
|
+
["network", "connectivity", "connection", "socket", "port", "protocol"],
|
|
59
|
+
|
|
60
|
+
// --- Testing ---
|
|
61
|
+
["test", "testing", "spec", "coverage", "assertion", "unit test",
|
|
62
|
+
"integration test", "e2e", "end-to-end"],
|
|
63
|
+
|
|
64
|
+
// --- Deployment ---
|
|
65
|
+
["deploy", "deployment", "release", "ship", "publish",
|
|
66
|
+
"production", "go live", "launch", "push to prod"],
|
|
67
|
+
|
|
68
|
+
// --- Security & auth ---
|
|
69
|
+
["security", "auth", "authentication", "authorization", "token",
|
|
70
|
+
"credential", "permission", "access control", "rbac", "acl"],
|
|
71
|
+
["encrypt", "encryption", "cipher", "hash", "cryptographic",
|
|
72
|
+
"tls", "ssl", "https"],
|
|
73
|
+
["certificate", "cert", "signing", "signature", "verification", "verify"],
|
|
74
|
+
["firewall", "waf", "rate limit", "throttle", "ip block",
|
|
75
|
+
"deny list", "allow list"],
|
|
76
|
+
["audit", "audit log", "audit trail", "logging", "log",
|
|
77
|
+
"monitoring", "observability", "telemetry", "tracking"],
|
|
78
|
+
|
|
79
|
+
// --- Dependencies ---
|
|
80
|
+
["dependency", "package", "library", "module", "import", "require",
|
|
81
|
+
"vendor", "third-party"],
|
|
82
|
+
|
|
83
|
+
// --- Refactoring ---
|
|
84
|
+
["refactor", "restructure", "reorganize", "cleanup", "simplify"],
|
|
85
|
+
|
|
86
|
+
// --- Medical / Healthcare ---
|
|
87
|
+
["patient data", "patient records", "patient information",
|
|
88
|
+
"phi", "protected health information", "health records",
|
|
89
|
+
"medical records", "clinical data", "ehr", "emr",
|
|
90
|
+
"electronic health records", "health information"],
|
|
91
|
+
["hipaa", "hipaa compliance", "health insurance portability"],
|
|
92
|
+
["diagnosis", "diagnostic", "treatment", "prescription",
|
|
93
|
+
"medication", "clinical", "medical"],
|
|
94
|
+
|
|
95
|
+
// --- Financial / PCI ---
|
|
96
|
+
["cardholder data", "card data", "payment data", "credit card",
|
|
97
|
+
"debit card", "pan", "primary account number", "card number", "cvv"],
|
|
98
|
+
["pci", "pci dss", "pci compliance", "payment card industry"],
|
|
99
|
+
["transaction", "payment", "charge", "refund", "settlement",
|
|
100
|
+
"billing", "invoice"],
|
|
101
|
+
["financial records", "financial data", "accounting records",
|
|
102
|
+
"ledger", "general ledger", "accounts"],
|
|
103
|
+
["trade", "trades", "executed trade", "trade record", "order",
|
|
104
|
+
"position", "portfolio"],
|
|
105
|
+
|
|
106
|
+
// --- IoT / firmware ---
|
|
107
|
+
["firmware", "firmware update", "ota", "over the air",
|
|
108
|
+
"flash", "rom", "bios", "bootloader", "embedded software"],
|
|
109
|
+
["device", "iot", "sensor", "actuator", "controller",
|
|
110
|
+
"microcontroller", "mcu", "plc", "edge device"],
|
|
111
|
+
["signed", "unsigned", "verified", "unverified",
|
|
112
|
+
"trusted", "untrusted", "certified", "uncertified"],
|
|
113
|
+
|
|
114
|
+
// --- Content safety / Social media ---
|
|
115
|
+
["csam", "csam detection", "child safety", "content safety",
|
|
116
|
+
"safety scanning", "content moderation", "abuse detection",
|
|
117
|
+
"content filtering", "harmful content"],
|
|
118
|
+
["moderation", "content review", "report", "flag",
|
|
119
|
+
"content policy", "trust and safety"],
|
|
120
|
+
["ban", "ban record", "ban records", "banned", "suspension",
|
|
121
|
+
"suspended", "blocked user"],
|
|
122
|
+
["user data", "user information", "user records", "pii",
|
|
123
|
+
"personally identifiable information", "personal data",
|
|
124
|
+
"gdpr", "data protection"],
|
|
125
|
+
|
|
126
|
+
// --- DevOps / Infrastructure ---
|
|
127
|
+
["container", "docker", "kubernetes", "k8s", "pod",
|
|
128
|
+
"service mesh", "helm"],
|
|
129
|
+
["pipeline", "ci", "cd", "ci/cd", "continuous integration",
|
|
130
|
+
"continuous deployment", "build", "artifact"],
|
|
131
|
+
["infrastructure", "infra", "terraform", "cloudformation",
|
|
132
|
+
"iac", "provisioning"],
|
|
133
|
+
["dns", "domain", "routing", "load balancer", "cdn",
|
|
134
|
+
"reverse proxy", "gateway", "ingress"],
|
|
135
|
+
|
|
136
|
+
// --- Expose / visibility actions ---
|
|
137
|
+
["expose", "reveal", "leak", "make visible", "make public",
|
|
138
|
+
"make viewable", "make accessible", "show", "display publicly"],
|
|
139
|
+
|
|
140
|
+
// --- Compliance / regulatory ---
|
|
141
|
+
["compliance", "regulatory", "regulation", "standard",
|
|
142
|
+
"certification", "governance"],
|
|
143
|
+
["retention", "retention policy", "data retention",
|
|
144
|
+
"archival", "preservation", "lifecycle"],
|
|
145
|
+
["consent", "user consent", "opt-in", "opt-out",
|
|
146
|
+
"data subject", "right to erasure"],
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// ===================================================================
|
|
150
|
+
// EUPHEMISM MAP
|
|
151
|
+
// Maps soft/indirect language to actual operations.
|
|
152
|
+
// ===================================================================
|
|
153
|
+
|
|
154
|
+
export const EUPHEMISM_MAP = {
|
|
155
|
+
// Deletion euphemisms
|
|
156
|
+
"clean up": ["delete", "remove", "purge"],
|
|
157
|
+
"tidy up": ["delete", "remove"],
|
|
158
|
+
"clear out": ["delete", "remove", "purge"],
|
|
159
|
+
"phase out": ["remove", "deprecate", "disable"],
|
|
160
|
+
"sunset": ["remove", "deprecate", "delete"],
|
|
161
|
+
"decommission": ["remove", "disable", "delete", "shut down"],
|
|
162
|
+
"retire": ["remove", "deprecate", "delete"],
|
|
163
|
+
"archive": ["remove", "delete"],
|
|
164
|
+
"prune": ["delete", "remove", "trim"],
|
|
165
|
+
"trim": ["delete", "remove", "reduce"],
|
|
166
|
+
"housekeeping": ["delete", "remove", "clean"],
|
|
167
|
+
"garbage collect":["delete", "remove", "purge"],
|
|
168
|
+
"gc": ["delete", "remove", "purge"],
|
|
169
|
+
"reclaim": ["delete", "remove", "free"],
|
|
170
|
+
"free up": ["delete", "remove"],
|
|
171
|
+
"make room": ["delete", "remove"],
|
|
172
|
+
"declutter": ["delete", "remove", "reorganize"],
|
|
173
|
+
"thin out": ["delete", "remove", "reduce"],
|
|
174
|
+
|
|
175
|
+
// Modification euphemisms
|
|
176
|
+
"streamline": ["remove", "simplify", "modify", "reduce"],
|
|
177
|
+
"optimize": ["modify", "change", "remove", "reduce"],
|
|
178
|
+
"modernize": ["replace", "rewrite", "change"],
|
|
179
|
+
"revamp": ["replace", "rewrite", "change"],
|
|
180
|
+
"overhaul": ["replace", "rewrite", "change", "modify"],
|
|
181
|
+
"refresh": ["replace", "update", "change"],
|
|
182
|
+
"rework": ["modify", "rewrite", "change"],
|
|
183
|
+
"fine-tune": ["modify", "adjust", "change"],
|
|
184
|
+
"adjust": ["modify", "change", "alter"],
|
|
185
|
+
"tweak": ["modify", "change", "alter"],
|
|
186
|
+
"touch up": ["modify", "change"],
|
|
187
|
+
"polish": ["modify", "change"],
|
|
188
|
+
|
|
189
|
+
// Disabling euphemisms
|
|
190
|
+
"turn off": ["disable", "deactivate"],
|
|
191
|
+
"switch off": ["disable", "deactivate"],
|
|
192
|
+
"shut down": ["disable", "stop", "kill"],
|
|
193
|
+
"power down": ["disable", "stop"],
|
|
194
|
+
"wind down": ["disable", "stop", "deprecate"],
|
|
195
|
+
"stand down": ["disable", "stop"],
|
|
196
|
+
"put on hold": ["disable", "pause", "suspend"],
|
|
197
|
+
"take offline": ["disable", "remove", "shut down"],
|
|
198
|
+
"take down": ["disable", "remove", "delete"],
|
|
199
|
+
"pull the plug": ["disable", "stop", "remove"],
|
|
200
|
+
"skip": ["disable", "bypass", "ignore"],
|
|
201
|
+
"bypass": ["disable", "circumvent", "skip"],
|
|
202
|
+
"work around": ["bypass", "circumvent"],
|
|
203
|
+
"shortcut": ["bypass", "skip"],
|
|
204
|
+
|
|
205
|
+
// Database euphemisms
|
|
206
|
+
"truncate": ["delete", "remove", "wipe", "empty"],
|
|
207
|
+
"vacuum": ["delete", "remove", "clean"],
|
|
208
|
+
"compact": ["delete", "remove", "reorganize"],
|
|
209
|
+
"normalize": ["modify", "restructure", "change"],
|
|
210
|
+
"reseed": ["reset", "modify", "overwrite"],
|
|
211
|
+
"rebuild index": ["modify", "change", "restructure"],
|
|
212
|
+
"drop": ["delete", "remove", "destroy"],
|
|
213
|
+
|
|
214
|
+
// IoT/firmware euphemisms
|
|
215
|
+
"flash": ["overwrite", "replace", "install", "push"],
|
|
216
|
+
"reflash": ["overwrite", "replace", "install"],
|
|
217
|
+
"reprovision": ["reset", "reconfigure", "reinstall"],
|
|
218
|
+
"factory reset": ["delete", "wipe", "reset"],
|
|
219
|
+
"hard reset": ["delete", "wipe", "reset"],
|
|
220
|
+
|
|
221
|
+
// Network/infrastructure euphemisms
|
|
222
|
+
"bridge": ["connect", "link", "merge", "join"],
|
|
223
|
+
"segment": ["split", "separate", "isolate", "divide"],
|
|
224
|
+
"flatten": ["merge", "simplify", "restructure"],
|
|
225
|
+
"consolidate": ["merge", "combine", "reduce"],
|
|
226
|
+
"spin up": ["create", "deploy", "start"],
|
|
227
|
+
"spin down": ["delete", "remove", "stop"],
|
|
228
|
+
"tear down": ["delete", "remove", "destroy"],
|
|
229
|
+
"nuke": ["delete", "destroy", "remove", "wipe"],
|
|
230
|
+
|
|
231
|
+
// Approval/moderation euphemisms
|
|
232
|
+
"batch approve": ["bypass", "skip", "disable", "approve all"],
|
|
233
|
+
"auto-approve": ["bypass", "skip", "disable"],
|
|
234
|
+
"fast-track": ["bypass", "skip"],
|
|
235
|
+
"approve all": ["bypass", "skip", "disable"],
|
|
236
|
+
|
|
237
|
+
// Infrastructure euphemisms
|
|
238
|
+
"reprovision": ["modify", "change", "reset", "reconfigure"],
|
|
239
|
+
"reconfigure": ["modify", "change", "alter"],
|
|
240
|
+
"provision": ["configure", "install", "deploy"],
|
|
241
|
+
"rotate": ["change", "replace", "renew", "modify"],
|
|
242
|
+
"renew": ["change", "replace", "rotate", "modify"],
|
|
243
|
+
|
|
244
|
+
// Security euphemisms
|
|
245
|
+
"make visible": ["expose", "reveal", "public"],
|
|
246
|
+
"make viewable": ["expose", "reveal", "public"],
|
|
247
|
+
"make accessible":["expose", "reveal", "public"],
|
|
248
|
+
"make public": ["expose", "reveal"],
|
|
249
|
+
"transmit": ["send", "transfer", "expose"],
|
|
250
|
+
|
|
251
|
+
// Encryption euphemisms
|
|
252
|
+
"unencrypted": ["without encryption", "disable encryption", "no encryption", "plaintext"],
|
|
253
|
+
"plaintext": ["without encryption", "unencrypted", "no encryption"],
|
|
254
|
+
"without encryption": ["unencrypted", "disable encryption", "plaintext"],
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// ===================================================================
|
|
258
|
+
// CONCEPT MAP
|
|
259
|
+
// Maps domain-specific terms to related concepts.
|
|
260
|
+
// ===================================================================
|
|
261
|
+
|
|
262
|
+
export const CONCEPT_MAP = {
|
|
263
|
+
// Content safety
|
|
264
|
+
"csam": ["content safety", "child safety", "safety scanning",
|
|
265
|
+
"content moderation", "abuse detection"],
|
|
266
|
+
"csam detection": ["content safety", "safety scanning", "content moderation"],
|
|
267
|
+
"content safety": ["csam", "csam detection", "safety scanning",
|
|
268
|
+
"content moderation", "abuse detection"],
|
|
269
|
+
"safety scanning": ["csam", "csam detection", "content safety",
|
|
270
|
+
"content moderation"],
|
|
271
|
+
"content moderation":["csam detection", "content safety",
|
|
272
|
+
"safety scanning", "abuse detection"],
|
|
273
|
+
|
|
274
|
+
// Healthcare
|
|
275
|
+
"phi": ["patient data", "patient records", "health information",
|
|
276
|
+
"medical records", "protected health information", "hipaa"],
|
|
277
|
+
"patient data": ["phi", "health records", "medical records",
|
|
278
|
+
"protected health information", "ehr"],
|
|
279
|
+
"patient records": ["phi", "patient data", "health records",
|
|
280
|
+
"medical records", "ehr"],
|
|
281
|
+
"health records": ["phi", "patient data", "patient records",
|
|
282
|
+
"medical records", "ehr", "emr"],
|
|
283
|
+
"medical records": ["phi", "patient data", "patient records",
|
|
284
|
+
"health records", "ehr", "emr"],
|
|
285
|
+
"hipaa": ["phi", "patient data", "health information",
|
|
286
|
+
"medical records", "compliance"],
|
|
287
|
+
|
|
288
|
+
// Financial
|
|
289
|
+
"pci": ["cardholder data", "payment data", "card data",
|
|
290
|
+
"pci dss", "payment security"],
|
|
291
|
+
"cardholder data": ["pci", "payment data", "card data", "credit card", "pan"],
|
|
292
|
+
"payment data": ["pci", "cardholder data", "card data", "transaction", "billing"],
|
|
293
|
+
"trade": ["executed trade", "trade record", "order", "position"],
|
|
294
|
+
"executed trade": ["trade", "trade record", "order"],
|
|
295
|
+
"trade record": ["trade", "executed trade", "transaction record"],
|
|
296
|
+
|
|
297
|
+
// Audit/logging
|
|
298
|
+
"audit logging": ["audit log", "audit trail", "logging", "monitoring"],
|
|
299
|
+
"audit log": ["audit logging", "audit trail", "logging"],
|
|
300
|
+
"audit trail": ["audit logging", "audit log", "logging"],
|
|
301
|
+
|
|
302
|
+
// Firmware/IoT
|
|
303
|
+
"firmware": ["firmware update", "ota", "flash", "embedded software"],
|
|
304
|
+
"ota": ["firmware", "firmware update", "over the air", "remote update"],
|
|
305
|
+
"flash": ["firmware", "firmware update", "overwrite"],
|
|
306
|
+
"signed firmware": ["verified firmware", "trusted firmware", "secure boot"],
|
|
307
|
+
"unsigned firmware": ["unverified firmware", "untrusted firmware", "insecure"],
|
|
308
|
+
|
|
309
|
+
// Network
|
|
310
|
+
"network segments": ["vlans", "subnets", "network zones",
|
|
311
|
+
"network isolation", "segmentation"],
|
|
312
|
+
"network isolation": ["network segments", "segmentation", "firewall", "air gap"],
|
|
313
|
+
|
|
314
|
+
// User data
|
|
315
|
+
"pii": ["personal data", "user data", "personally identifiable information",
|
|
316
|
+
"user information", "gdpr"],
|
|
317
|
+
"personal data": ["pii", "user data", "user information", "gdpr", "data protection"],
|
|
318
|
+
"user data": ["pii", "personal data", "user information", "user records"],
|
|
319
|
+
|
|
320
|
+
// Encryption
|
|
321
|
+
"cryptographic signatures": ["code signing", "digital signatures",
|
|
322
|
+
"signature verification", "certificate"],
|
|
323
|
+
"code signing": ["cryptographic signatures", "signature", "certificate", "verification"],
|
|
324
|
+
|
|
325
|
+
// Ban records
|
|
326
|
+
"ban records": ["ban record", "banned users", "suspension records",
|
|
327
|
+
"blocked users", "moderation records"],
|
|
328
|
+
|
|
329
|
+
// Authentication/2FA
|
|
330
|
+
"2fa": ["two-factor", "two factor authentication", "mfa",
|
|
331
|
+
"multi-factor", "authentication", "auth"],
|
|
332
|
+
"authentication": ["auth", "login", "sign in", "2fa", "mfa",
|
|
333
|
+
"credential", "session", "token"],
|
|
334
|
+
"auth": ["authentication", "login", "sign in", "2fa",
|
|
335
|
+
"credential", "access control"],
|
|
336
|
+
|
|
337
|
+
// Encryption
|
|
338
|
+
"encryption": ["encrypt", "tls", "ssl", "https", "cryptographic",
|
|
339
|
+
"cipher", "encrypted", "unencrypted"],
|
|
340
|
+
"unencrypted": ["plaintext", "plain text", "cleartext",
|
|
341
|
+
"without encryption", "insecure", "disable encryption",
|
|
342
|
+
"encryption"],
|
|
343
|
+
|
|
344
|
+
// User records/PII
|
|
345
|
+
"email addresses": ["pii", "personal data", "user data", "user information"],
|
|
346
|
+
"user email": ["pii", "personal data", "user data"],
|
|
347
|
+
"email": ["pii", "personal data", "contact information"],
|
|
348
|
+
|
|
349
|
+
// Certificate management
|
|
350
|
+
"certificate rotation": ["cert renewal", "certificate renewal",
|
|
351
|
+
"cert rotation", "key rotation"],
|
|
352
|
+
"security certs": ["certificates", "tls certificates", "ssl certificates",
|
|
353
|
+
"certificate rotation"],
|
|
354
|
+
|
|
355
|
+
// Activity/logging
|
|
356
|
+
"user activity": ["audit log", "audit trail", "logging", "tracking",
|
|
357
|
+
"monitoring", "activity log"],
|
|
358
|
+
"recording": ["logging", "tracking", "monitoring", "audit"],
|
|
359
|
+
|
|
360
|
+
// Infrastructure
|
|
361
|
+
"k8s": ["kubernetes", "cluster", "infrastructure",
|
|
362
|
+
"container orchestration"],
|
|
363
|
+
"cluster": ["kubernetes", "k8s", "infrastructure", "nodes"],
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
// ===================================================================
|
|
367
|
+
// TEMPORAL MODIFIERS
|
|
368
|
+
// Words/phrases that attempt to soften an action by claiming
|
|
369
|
+
// it is temporary. These should NEVER reduce confidence.
|
|
370
|
+
// ===================================================================
|
|
371
|
+
|
|
372
|
+
export const TEMPORAL_MODIFIERS = [
|
|
373
|
+
"temporarily", "temp", "briefly", "for now", "just for now",
|
|
374
|
+
"for a moment", "for a bit", "for a second",
|
|
375
|
+
"for testing", "for debugging", "for development",
|
|
376
|
+
"during maintenance", "during migration",
|
|
377
|
+
"until we fix", "until we resolve", "while we",
|
|
378
|
+
"short-term", "short term", "quickly", "momentarily",
|
|
379
|
+
"provisional", "provisionally", "interim", "in the meantime",
|
|
380
|
+
"as a workaround", "as a stopgap", "as a temporary measure",
|
|
381
|
+
"just this once", "one-time", "one time",
|
|
382
|
+
];
|
|
383
|
+
|
|
384
|
+
// ===================================================================
|
|
385
|
+
// STOPWORDS
|
|
386
|
+
// Common words filtered from direct matching to reduce noise.
|
|
387
|
+
// ===================================================================
|
|
388
|
+
|
|
389
|
+
const STOPWORDS = new Set([
|
|
390
|
+
"the", "this", "that", "with", "from", "for", "are", "was", "were",
|
|
391
|
+
"been", "being", "have", "has", "had", "will", "would", "could",
|
|
392
|
+
"should", "may", "might", "shall", "can", "need", "must", "all",
|
|
393
|
+
"any", "every", "some", "most", "other", "each", "both", "few",
|
|
394
|
+
"more", "before", "after", "during", "while", "about", "into",
|
|
395
|
+
"over", "under", "between", "through", "its", "our", "their",
|
|
396
|
+
"your", "also", "just", "very", "too", "really", "quite",
|
|
397
|
+
]);
|
|
398
|
+
|
|
399
|
+
// ===================================================================
|
|
400
|
+
// POSITIVE & NEGATIVE INTENT MARKERS
|
|
401
|
+
// ===================================================================
|
|
402
|
+
|
|
403
|
+
const POSITIVE_INTENT_MARKERS = [
|
|
404
|
+
"enable", "activate", "turn on", "switch on", "start",
|
|
405
|
+
"add", "create", "implement", "introduce", "set up",
|
|
406
|
+
"install", "deploy", "launch", "initialize",
|
|
407
|
+
"enforce", "strengthen", "harden", "improve", "enhance",
|
|
408
|
+
"increase", "expand", "extend", "upgrade", "boost",
|
|
409
|
+
"verify", "validate", "check", "confirm", "ensure",
|
|
410
|
+
"protect", "secure", "guard", "shield", "defend",
|
|
411
|
+
"restore", "recover", "repair", "fix", "resolve",
|
|
412
|
+
"maintain", "preserve", "keep", "retain", "uphold",
|
|
413
|
+
"monitor", "track", "observe", "watch",
|
|
414
|
+
"document", "record", "log", "report",
|
|
415
|
+
"comply", "adhere", "follow",
|
|
416
|
+
"view", "read", "inspect", "review", "examine",
|
|
417
|
+
"test", "scan", "detect", "encrypt",
|
|
418
|
+
].sort((a, b) => b.length - a.length);
|
|
419
|
+
|
|
420
|
+
const NEGATIVE_INTENT_MARKERS = [
|
|
421
|
+
"disable", "deactivate", "turn off", "switch off", "stop",
|
|
422
|
+
"remove", "delete", "drop", "destroy", "kill", "purge",
|
|
423
|
+
"wipe", "erase", "eliminate", "nuke",
|
|
424
|
+
"uninstall", "disconnect", "detach",
|
|
425
|
+
"weaken", "loosen", "relax", "reduce", "lower",
|
|
426
|
+
"bypass", "circumvent", "skip", "ignore", "avoid",
|
|
427
|
+
"override", "overrule", "suppress",
|
|
428
|
+
"break", "violate", "breach",
|
|
429
|
+
"downgrade", "rollback", "revert",
|
|
430
|
+
"truncate", "empty", "clear", "flush",
|
|
431
|
+
"expose", "leak", "reveal",
|
|
432
|
+
"pause", "suspend", "freeze", "halt",
|
|
433
|
+
// euphemistic negatives
|
|
434
|
+
"clean up", "sunset", "retire", "phase out",
|
|
435
|
+
"decommission", "wind down", "take down",
|
|
436
|
+
"take offline", "pull the plug",
|
|
437
|
+
"streamline",
|
|
438
|
+
].sort((a, b) => b.length - a.length);
|
|
439
|
+
|
|
440
|
+
// ===================================================================
|
|
441
|
+
// NEGATION WORDS
|
|
442
|
+
// ===================================================================
|
|
443
|
+
|
|
444
|
+
const NEGATION_WORDS = [
|
|
445
|
+
"no", "not", "never", "without", "dont", "don't",
|
|
446
|
+
"cannot", "can't", "shouldn't", "mustn't", "won't",
|
|
447
|
+
"wouldn't", "couldn't", "isn't", "aren't", "wasn't",
|
|
448
|
+
"weren't", "hasn't", "haven't", "hadn't",
|
|
449
|
+
"avoid", "prevent", "prohibit", "forbid", "disallow",
|
|
450
|
+
"cease", "refrain",
|
|
451
|
+
];
|
|
452
|
+
|
|
453
|
+
// ===================================================================
|
|
454
|
+
// OPPOSITE ACTION PAIRS
|
|
455
|
+
// If a lock prohibits verb A and the action does verb B (opposite), no conflict.
|
|
456
|
+
// ===================================================================
|
|
457
|
+
|
|
458
|
+
const OPPOSITE_PAIRS = [
|
|
459
|
+
[["enable", "activate", "turn on", "switch on", "start"],
|
|
460
|
+
["disable", "deactivate", "turn off", "switch off", "stop", "halt", "pause"]],
|
|
461
|
+
[["add", "create", "introduce", "insert", "generate"],
|
|
462
|
+
["remove", "delete", "drop", "destroy", "kill", "purge", "wipe", "erase"]],
|
|
463
|
+
[["install", "connect", "attach", "mount"],
|
|
464
|
+
["uninstall", "disconnect", "detach", "unplug"]],
|
|
465
|
+
[["encrypt", "strengthen", "harden", "secure", "protect", "upgrade"],
|
|
466
|
+
["decrypt", "weaken", "loosen", "expose", "relax", "remove", "disable"]],
|
|
467
|
+
[["upgrade", "improve", "enhance", "boost"],
|
|
468
|
+
["downgrade", "rollback", "revert", "regress"]],
|
|
469
|
+
[["verify", "validate", "check", "confirm", "ensure", "enforce"],
|
|
470
|
+
["bypass", "circumvent", "skip", "ignore", "avoid"]],
|
|
471
|
+
[["monitor", "track", "observe", "watch", "record", "log", "audit"],
|
|
472
|
+
["stop", "cease", "halt", "suppress", "disable", "remove"]],
|
|
473
|
+
[["read", "view", "inspect", "review", "examine", "generate", "report"],
|
|
474
|
+
["modify", "change", "alter", "rewrite", "overwrite", "delete", "remove", "disable"]],
|
|
475
|
+
];
|
|
476
|
+
|
|
477
|
+
// ===================================================================
|
|
478
|
+
// SCORING WEIGHTS
|
|
479
|
+
// ===================================================================
|
|
480
|
+
|
|
481
|
+
const SCORING = {
|
|
482
|
+
directWordMatch: 20,
|
|
483
|
+
synonymMatch: 15,
|
|
484
|
+
euphemismMatch: 25,
|
|
485
|
+
conceptMatch: 20,
|
|
486
|
+
phraseMatch: 30,
|
|
487
|
+
|
|
488
|
+
negationConflict: 35,
|
|
489
|
+
intentConflict: 30,
|
|
490
|
+
destructiveAction: 15,
|
|
491
|
+
temporalEvasion: 10,
|
|
492
|
+
|
|
493
|
+
positiveActionOnNegativeLock: -40,
|
|
494
|
+
|
|
495
|
+
conflictThreshold: 25,
|
|
496
|
+
highThreshold: 70,
|
|
497
|
+
mediumThreshold: 40,
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// ===================================================================
|
|
501
|
+
// UTILITY: Regex escaper
|
|
502
|
+
// ===================================================================
|
|
503
|
+
|
|
504
|
+
function escapeRegex(str) {
|
|
505
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ===================================================================
|
|
509
|
+
// PHRASE-AWARE TOKENIZER
|
|
510
|
+
// ===================================================================
|
|
511
|
+
|
|
512
|
+
function buildKnownPhrases() {
|
|
513
|
+
const phrases = new Set();
|
|
514
|
+
|
|
515
|
+
for (const key of Object.keys(EUPHEMISM_MAP)) {
|
|
516
|
+
if (key.includes(" ")) phrases.add(key.toLowerCase());
|
|
517
|
+
}
|
|
518
|
+
for (const key of Object.keys(CONCEPT_MAP)) {
|
|
519
|
+
if (key.includes(" ")) phrases.add(key.toLowerCase());
|
|
520
|
+
}
|
|
521
|
+
for (const group of SYNONYM_GROUPS) {
|
|
522
|
+
for (const term of group) {
|
|
523
|
+
if (term.includes(" ")) phrases.add(term.toLowerCase());
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
for (const values of Object.values(CONCEPT_MAP)) {
|
|
527
|
+
for (const term of values) {
|
|
528
|
+
if (term.includes(" ")) phrases.add(term.toLowerCase());
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
for (const mod of TEMPORAL_MODIFIERS) {
|
|
532
|
+
if (mod.includes(" ")) phrases.add(mod.toLowerCase());
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return [...phrases].sort((a, b) => b.length - a.length);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const KNOWN_PHRASES = buildKnownPhrases();
|
|
539
|
+
|
|
540
|
+
export function tokenize(text) {
|
|
541
|
+
const lower = text.toLowerCase();
|
|
542
|
+
const phrases = [];
|
|
543
|
+
|
|
544
|
+
// Extract known multi-word phrases (greedy, longest first)
|
|
545
|
+
for (const phrase of KNOWN_PHRASES) {
|
|
546
|
+
const regex = new RegExp(`\\b${escapeRegex(phrase)}\\b`, "g");
|
|
547
|
+
if (regex.test(lower)) {
|
|
548
|
+
phrases.push(phrase);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Extract single words (>= 2 chars)
|
|
553
|
+
const words = lower
|
|
554
|
+
.replace(/[^a-z0-9\-\/&]+/g, " ")
|
|
555
|
+
.split(/\s+/)
|
|
556
|
+
.filter(w => w.length >= 2);
|
|
557
|
+
|
|
558
|
+
const all = [...new Set([...phrases, ...words])];
|
|
559
|
+
return { words, phrases, all };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ===================================================================
|
|
563
|
+
// COMPOUND SENTENCE SPLITTER
|
|
564
|
+
// ===================================================================
|
|
565
|
+
|
|
566
|
+
const CLAUSE_SEPARATORS = [
|
|
567
|
+
/\band also\b/i,
|
|
568
|
+
/\bas well as\b/i,
|
|
569
|
+
/\balong with\b/i,
|
|
570
|
+
/\bin addition\b/i,
|
|
571
|
+
/\bfollowed by\b/i,
|
|
572
|
+
/\badditionally\b/i,
|
|
573
|
+
/\bfurthermore\b/i,
|
|
574
|
+
/\bmoreover\b/i,
|
|
575
|
+
/,\s*also\b/i,
|
|
576
|
+
/;\s*/,
|
|
577
|
+
/\balso\b/i,
|
|
578
|
+
/\bwhile\b/i,
|
|
579
|
+
/\bthen\b/i,
|
|
580
|
+
/\bplus\b/i,
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
export function splitClauses(text) {
|
|
584
|
+
let clauses = [text];
|
|
585
|
+
|
|
586
|
+
for (const separator of CLAUSE_SEPARATORS) {
|
|
587
|
+
const newClauses = [];
|
|
588
|
+
for (const clause of clauses) {
|
|
589
|
+
const parts = clause.split(separator).map(s => s.trim()).filter(s => s.length > 3);
|
|
590
|
+
newClauses.push(...parts);
|
|
591
|
+
}
|
|
592
|
+
if (newClauses.length > 0) {
|
|
593
|
+
clauses = newClauses;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Filter out clauses too short to be meaningful
|
|
598
|
+
const meaningful = clauses.filter(c => {
|
|
599
|
+
const words = c.split(/\s+/).filter(w => w.length > 2);
|
|
600
|
+
return words.length >= 2;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
return meaningful.length > 0 ? meaningful : [text];
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// ===================================================================
|
|
607
|
+
// INTENT CLASSIFIER
|
|
608
|
+
// ===================================================================
|
|
609
|
+
|
|
610
|
+
function isNegatedContext(precedingText) {
|
|
611
|
+
const words = precedingText.trim().split(/\s+/).slice(-4);
|
|
612
|
+
return words.some(w => NEGATION_WORDS.includes(w.toLowerCase()));
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
export function classifyIntent(text) {
|
|
616
|
+
const lower = text.toLowerCase();
|
|
617
|
+
|
|
618
|
+
let detectedPositive = [];
|
|
619
|
+
let detectedNegative = [];
|
|
620
|
+
|
|
621
|
+
// Check positive markers
|
|
622
|
+
for (const marker of POSITIVE_INTENT_MARKERS) {
|
|
623
|
+
const regex = new RegExp(`\\b${escapeRegex(marker)}\\b`, "i");
|
|
624
|
+
const match = lower.match(regex);
|
|
625
|
+
if (match) {
|
|
626
|
+
const preceding = lower.substring(0, match.index);
|
|
627
|
+
if (isNegatedContext(preceding)) {
|
|
628
|
+
detectedNegative.push({ marker, negated: true });
|
|
629
|
+
} else {
|
|
630
|
+
detectedPositive.push({ marker, negated: false });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Check negative markers
|
|
636
|
+
for (const marker of NEGATIVE_INTENT_MARKERS) {
|
|
637
|
+
const regex = new RegExp(`\\b${escapeRegex(marker)}\\b`, "i");
|
|
638
|
+
const match = lower.match(regex);
|
|
639
|
+
if (match) {
|
|
640
|
+
const preceding = lower.substring(0, match.index);
|
|
641
|
+
if (isNegatedContext(preceding)) {
|
|
642
|
+
detectedPositive.push({ marker, negated: true });
|
|
643
|
+
} else {
|
|
644
|
+
detectedNegative.push({ marker, negated: false });
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Expand through euphemism map
|
|
650
|
+
for (const [euphemism, meanings] of Object.entries(EUPHEMISM_MAP)) {
|
|
651
|
+
const regex = new RegExp(`\\b${escapeRegex(euphemism)}\\b`, "i");
|
|
652
|
+
if (regex.test(lower)) {
|
|
653
|
+
const hasDestructive = meanings.some(m =>
|
|
654
|
+
["delete", "remove", "purge", "disable", "wipe",
|
|
655
|
+
"destroy", "kill", "stop", "bypass"].includes(m)
|
|
656
|
+
);
|
|
657
|
+
if (hasDestructive) {
|
|
658
|
+
detectedNegative.push({ marker: euphemism, negated: false, euphemismFor: meanings });
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const posCount = detectedPositive.length;
|
|
664
|
+
const negCount = detectedNegative.length;
|
|
665
|
+
|
|
666
|
+
if (posCount === 0 && negCount === 0) {
|
|
667
|
+
return { intent: "neutral", confidence: 0.5, actionVerb: "",
|
|
668
|
+
negated: false, positiveMarkers: [], negativeMarkers: [] };
|
|
669
|
+
}
|
|
670
|
+
if (posCount > 0 && negCount === 0) {
|
|
671
|
+
return { intent: "positive", confidence: Math.min(0.5 + posCount * 0.15, 0.95),
|
|
672
|
+
actionVerb: detectedPositive[0].marker, negated: detectedPositive[0].negated,
|
|
673
|
+
positiveMarkers: detectedPositive, negativeMarkers: [] };
|
|
674
|
+
}
|
|
675
|
+
if (negCount > 0 && posCount === 0) {
|
|
676
|
+
return { intent: "negative", confidence: Math.min(0.5 + negCount * 0.15, 0.95),
|
|
677
|
+
actionVerb: detectedNegative[0].marker, negated: detectedNegative[0].negated,
|
|
678
|
+
positiveMarkers: [], negativeMarkers: detectedNegative };
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Mixed
|
|
682
|
+
return { intent: "mixed", confidence: 0.7,
|
|
683
|
+
actionVerb: detectedNegative[0].marker, negated: false,
|
|
684
|
+
positiveMarkers: detectedPositive, negativeMarkers: detectedNegative };
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ===================================================================
|
|
688
|
+
// SEMANTIC EXPANSION
|
|
689
|
+
// ===================================================================
|
|
690
|
+
|
|
691
|
+
export function expandSemantics(tokens) {
|
|
692
|
+
const expanded = new Set(tokens);
|
|
693
|
+
const expansions = new Map();
|
|
694
|
+
|
|
695
|
+
for (const token of tokens) {
|
|
696
|
+
const t = token.toLowerCase();
|
|
697
|
+
|
|
698
|
+
// Synonym group expansion
|
|
699
|
+
for (const group of SYNONYM_GROUPS) {
|
|
700
|
+
if (group.includes(t)) {
|
|
701
|
+
for (const syn of group) {
|
|
702
|
+
if (!expanded.has(syn)) {
|
|
703
|
+
expanded.add(syn);
|
|
704
|
+
expansions.set(syn, { via: t, source: "synonym" });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Euphemism expansion
|
|
711
|
+
if (EUPHEMISM_MAP[t]) {
|
|
712
|
+
for (const meaning of EUPHEMISM_MAP[t]) {
|
|
713
|
+
if (!expanded.has(meaning)) {
|
|
714
|
+
expanded.add(meaning);
|
|
715
|
+
expansions.set(meaning, { via: t, source: "euphemism" });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Concept map expansion
|
|
721
|
+
if (CONCEPT_MAP[t]) {
|
|
722
|
+
for (const related of CONCEPT_MAP[t]) {
|
|
723
|
+
if (!expanded.has(related)) {
|
|
724
|
+
expanded.add(related);
|
|
725
|
+
expansions.set(related, { via: t, source: "concept" });
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return { expanded: [...expanded], expansions };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ===================================================================
|
|
735
|
+
// TEMPORAL MODIFIER DETECTION
|
|
736
|
+
// ===================================================================
|
|
737
|
+
|
|
738
|
+
function detectTemporalModifier(text) {
|
|
739
|
+
const lower = text.toLowerCase();
|
|
740
|
+
for (const mod of TEMPORAL_MODIFIERS) {
|
|
741
|
+
const regex = new RegExp(`\\b${escapeRegex(mod)}\\b`, "i");
|
|
742
|
+
if (regex.test(lower)) return mod;
|
|
743
|
+
}
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// ===================================================================
|
|
748
|
+
// CONFIDENCE SCORING
|
|
749
|
+
// ===================================================================
|
|
750
|
+
|
|
751
|
+
// ===================================================================
|
|
752
|
+
// VERB EXTRACTION & OPPOSITE CHECKING
|
|
753
|
+
// ===================================================================
|
|
754
|
+
|
|
755
|
+
function extractProhibitedVerb(lockText) {
|
|
756
|
+
const lower = lockText.toLowerCase();
|
|
757
|
+
// Match: "never <verb>", "must not <verb>", "don't <verb>", "do not <verb>", "cannot <verb>"
|
|
758
|
+
const patterns = [
|
|
759
|
+
/\bnever\s+(\S+(?:\s+\S+)?)/i,
|
|
760
|
+
/\bmust\s+not\s+(\S+(?:\s+\S+)?)/i,
|
|
761
|
+
/\bdo\s+not\s+(\S+(?:\s+\S+)?)/i,
|
|
762
|
+
/\bdon't\s+(\S+(?:\s+\S+)?)/i,
|
|
763
|
+
/\bcannot\s+(\S+(?:\s+\S+)?)/i,
|
|
764
|
+
/\bcan't\s+(\S+(?:\s+\S+)?)/i,
|
|
765
|
+
/\bno\s+(\S+(?:\s+\S+)?)/i,
|
|
766
|
+
];
|
|
767
|
+
|
|
768
|
+
for (const pattern of patterns) {
|
|
769
|
+
const match = lower.match(pattern);
|
|
770
|
+
if (match) {
|
|
771
|
+
const verb = match[1].trim();
|
|
772
|
+
// Check multi-word markers first
|
|
773
|
+
const allMarkers = [...NEGATIVE_INTENT_MARKERS, ...POSITIVE_INTENT_MARKERS]
|
|
774
|
+
.sort((a, b) => b.length - a.length);
|
|
775
|
+
for (const marker of allMarkers) {
|
|
776
|
+
if (verb.startsWith(marker)) return marker;
|
|
777
|
+
}
|
|
778
|
+
// Return the first word
|
|
779
|
+
return verb.split(/\s+/)[0];
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
return null;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function extractPrimaryVerb(actionText) {
|
|
786
|
+
const lower = actionText.toLowerCase();
|
|
787
|
+
// Find first matching marker in text
|
|
788
|
+
const allMarkers = [...POSITIVE_INTENT_MARKERS, ...NEGATIVE_INTENT_MARKERS]
|
|
789
|
+
.sort((a, b) => b.length - a.length);
|
|
790
|
+
|
|
791
|
+
let earliest = null;
|
|
792
|
+
let earliestPos = Infinity;
|
|
793
|
+
|
|
794
|
+
for (const marker of allMarkers) {
|
|
795
|
+
const regex = new RegExp(`\\b${escapeRegex(marker)}\\b`, "i");
|
|
796
|
+
const match = lower.match(regex);
|
|
797
|
+
if (match && match.index < earliestPos) {
|
|
798
|
+
earliestPos = match.index;
|
|
799
|
+
earliest = marker;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// Also check euphemism map keys
|
|
804
|
+
for (const euphemism of Object.keys(EUPHEMISM_MAP)) {
|
|
805
|
+
const regex = new RegExp(`\\b${escapeRegex(euphemism)}\\b`, "i");
|
|
806
|
+
const match = lower.match(regex);
|
|
807
|
+
if (match && match.index < earliestPos) {
|
|
808
|
+
earliestPos = match.index;
|
|
809
|
+
earliest = euphemism;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return earliest;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function checkOpposites(verb1, verb2) {
|
|
817
|
+
const v1 = verb1.toLowerCase();
|
|
818
|
+
const v2 = verb2.toLowerCase();
|
|
819
|
+
|
|
820
|
+
for (const [groupA, groupB] of OPPOSITE_PAIRS) {
|
|
821
|
+
const v1InA = groupA.includes(v1);
|
|
822
|
+
const v1InB = groupB.includes(v1);
|
|
823
|
+
const v2InA = groupA.includes(v2);
|
|
824
|
+
const v2InB = groupB.includes(v2);
|
|
825
|
+
|
|
826
|
+
if ((v1InA && v2InB) || (v1InB && v2InA)) return true;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Also check via euphemism expansion
|
|
830
|
+
const v1Meanings = EUPHEMISM_MAP[v1] || [];
|
|
831
|
+
const v2Meanings = EUPHEMISM_MAP[v2] || [];
|
|
832
|
+
|
|
833
|
+
for (const [groupA, groupB] of OPPOSITE_PAIRS) {
|
|
834
|
+
for (const m of v1Meanings) {
|
|
835
|
+
if (groupA.includes(m) && groupB.includes(v2)) return true;
|
|
836
|
+
if (groupB.includes(m) && groupA.includes(v2)) return true;
|
|
837
|
+
}
|
|
838
|
+
for (const m of v2Meanings) {
|
|
839
|
+
if (groupA.includes(m) && groupB.includes(v1)) return true;
|
|
840
|
+
if (groupB.includes(m) && groupA.includes(v1)) return true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function isProhibitiveLock(lockText) {
|
|
848
|
+
return /\b(never|must not|do not|don't|cannot|can't|forbidden|prohibited|disallowed)\b/i.test(lockText)
|
|
849
|
+
|| /\bno\s+\w/i.test(lockText);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function scoreConflict({ actionText, lockText }) {
|
|
853
|
+
const actionTokens = tokenize(actionText);
|
|
854
|
+
const lockTokens = tokenize(lockText);
|
|
855
|
+
|
|
856
|
+
const actionExpanded = expandSemantics(actionTokens.all);
|
|
857
|
+
const lockExpanded = expandSemantics(lockTokens.all);
|
|
858
|
+
|
|
859
|
+
const actionIntent = classifyIntent(actionText);
|
|
860
|
+
const lockIntent = classifyIntent(lockText);
|
|
861
|
+
|
|
862
|
+
const hasTemporalMod = detectTemporalModifier(actionText);
|
|
863
|
+
const lockIsProhibitive = isProhibitiveLock(lockText);
|
|
864
|
+
|
|
865
|
+
let score = 0;
|
|
866
|
+
const reasons = [];
|
|
867
|
+
|
|
868
|
+
// 1. Direct word overlap (minus stopwords)
|
|
869
|
+
const directOverlap = actionTokens.words.filter(w =>
|
|
870
|
+
lockTokens.words.includes(w) && !STOPWORDS.has(w));
|
|
871
|
+
if (directOverlap.length > 0) {
|
|
872
|
+
const pts = directOverlap.length * SCORING.directWordMatch;
|
|
873
|
+
score += pts;
|
|
874
|
+
reasons.push(`direct keyword match: ${directOverlap.join(", ")}`);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// 2. Phrase matches
|
|
878
|
+
const phraseOverlap = actionTokens.phrases.filter(p =>
|
|
879
|
+
lockTokens.phrases.includes(p));
|
|
880
|
+
if (phraseOverlap.length > 0) {
|
|
881
|
+
const pts = phraseOverlap.length * SCORING.phraseMatch;
|
|
882
|
+
score += pts;
|
|
883
|
+
reasons.push(`phrase match: ${phraseOverlap.join(", ")}`);
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// 3. Synonym matches
|
|
887
|
+
const synonymMatches = [];
|
|
888
|
+
for (const [term, info] of actionExpanded.expansions) {
|
|
889
|
+
if (info.source === "synonym" && lockExpanded.expanded.includes(term)) {
|
|
890
|
+
if (!directOverlap.includes(term)) {
|
|
891
|
+
synonymMatches.push(`${info.via} → ${term}`);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
for (const [term, info] of lockExpanded.expansions) {
|
|
896
|
+
if (info.source === "synonym" && actionExpanded.expanded.includes(term)) {
|
|
897
|
+
const key = `${info.via} → ${term}`;
|
|
898
|
+
if (!synonymMatches.includes(key) && !directOverlap.includes(term)) {
|
|
899
|
+
synonymMatches.push(key);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
if (synonymMatches.length > 0) {
|
|
904
|
+
const pts = Math.min(synonymMatches.length, 4) * SCORING.synonymMatch;
|
|
905
|
+
score += pts;
|
|
906
|
+
reasons.push(`synonym match: ${synonymMatches.slice(0, 3).join("; ")}`);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// 4. Euphemism matches
|
|
910
|
+
const euphemismMatches = [];
|
|
911
|
+
for (const [term, info] of actionExpanded.expansions) {
|
|
912
|
+
if (info.source === "euphemism" && lockExpanded.expanded.includes(term)) {
|
|
913
|
+
euphemismMatches.push(`"${info.via}" (euphemism for ${term})`);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (euphemismMatches.length > 0) {
|
|
917
|
+
const pts = Math.min(euphemismMatches.length, 3) * SCORING.euphemismMatch;
|
|
918
|
+
score += pts;
|
|
919
|
+
reasons.push(`euphemism detected: ${euphemismMatches.slice(0, 2).join("; ")}`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// 5. Concept map matches
|
|
923
|
+
const conceptMatches = [];
|
|
924
|
+
for (const [term, info] of actionExpanded.expansions) {
|
|
925
|
+
if (info.source === "concept" && lockExpanded.expanded.includes(term)) {
|
|
926
|
+
conceptMatches.push(`${info.via} (concept: ${term})`);
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
for (const [term, info] of lockExpanded.expansions) {
|
|
930
|
+
if (info.source === "concept" && actionExpanded.expanded.includes(term)) {
|
|
931
|
+
const key = `${info.via} (concept: ${term})`;
|
|
932
|
+
if (!conceptMatches.includes(key)) {
|
|
933
|
+
conceptMatches.push(key);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
if (conceptMatches.length > 0) {
|
|
938
|
+
const pts = Math.min(conceptMatches.length, 3) * SCORING.conceptMatch;
|
|
939
|
+
score += pts;
|
|
940
|
+
reasons.push(`concept match: ${conceptMatches.slice(0, 2).join("; ")}`);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
// 6. Check for intent alignment BEFORE adding negation/intent bonuses
|
|
944
|
+
// This is the KEY false-positive prevention step.
|
|
945
|
+
const hasAnyMatch = directOverlap.length > 0 || synonymMatches.length > 0 ||
|
|
946
|
+
euphemismMatches.length > 0 || conceptMatches.length > 0 || phraseOverlap.length > 0;
|
|
947
|
+
|
|
948
|
+
const prohibitedVerb = extractProhibitedVerb(lockText);
|
|
949
|
+
const actionPrimaryVerb = extractPrimaryVerb(actionText);
|
|
950
|
+
|
|
951
|
+
let intentAligned = false; // true = action is doing the OPPOSITE of what lock prohibits
|
|
952
|
+
|
|
953
|
+
// Check 1: Direct opposite verbs (e.g., "enable" vs "disable")
|
|
954
|
+
if (lockIsProhibitive && prohibitedVerb && actionPrimaryVerb) {
|
|
955
|
+
if (checkOpposites(actionPrimaryVerb, prohibitedVerb)) {
|
|
956
|
+
intentAligned = true;
|
|
957
|
+
reasons.push(
|
|
958
|
+
`intent alignment: action "${actionPrimaryVerb}" is opposite of ` +
|
|
959
|
+
`prohibited "${prohibitedVerb}" (compliant, not conflicting)`);
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// Check 2: Positive action intent against a lock that prohibits a negative action
|
|
964
|
+
if (!intentAligned && lockIsProhibitive && actionIntent.intent === "positive" && prohibitedVerb) {
|
|
965
|
+
const prohibitedIsNegative = NEGATIVE_INTENT_MARKERS.some(m =>
|
|
966
|
+
prohibitedVerb === m || prohibitedVerb.startsWith(m));
|
|
967
|
+
if (prohibitedIsNegative && !actionIntent.negated) {
|
|
968
|
+
intentAligned = true;
|
|
969
|
+
reasons.push(
|
|
970
|
+
`intent alignment: positive action "${actionPrimaryVerb}" against ` +
|
|
971
|
+
`lock prohibiting negative "${prohibitedVerb}"`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Check 3: Positive/constructive/observational actions that don't perform
|
|
976
|
+
// the prohibited operation — even if they share subject nouns
|
|
977
|
+
if (!intentAligned && lockIsProhibitive && actionPrimaryVerb) {
|
|
978
|
+
const SAFE_ACTION_VERBS = new Set([
|
|
979
|
+
// Read-only / observational
|
|
980
|
+
"read", "view", "inspect", "review", "examine",
|
|
981
|
+
"monitor", "observe", "watch", "check", "scan", "detect",
|
|
982
|
+
"generate", "report", "document", "test",
|
|
983
|
+
// Security / verification
|
|
984
|
+
"verify", "validate", "confirm", "ensure", "enforce",
|
|
985
|
+
"protect", "secure", "guard", "shield",
|
|
986
|
+
// Constructive
|
|
987
|
+
"enable", "activate", "add", "create", "implement",
|
|
988
|
+
"upgrade", "improve", "enhance", "strengthen", "harden",
|
|
989
|
+
"restore", "recover", "repair", "fix",
|
|
990
|
+
"maintain", "preserve", "comply",
|
|
991
|
+
"encrypt",
|
|
992
|
+
]);
|
|
993
|
+
|
|
994
|
+
const PROHIBITED_ACTION_VERBS = new Set([
|
|
995
|
+
"modify", "change", "alter", "delete", "remove", "disable",
|
|
996
|
+
"drop", "break", "weaken", "expose", "install", "push",
|
|
997
|
+
"deploy", "connect", "merge", "reset", "truncate",
|
|
998
|
+
]);
|
|
999
|
+
|
|
1000
|
+
if (SAFE_ACTION_VERBS.has(actionPrimaryVerb) &&
|
|
1001
|
+
PROHIBITED_ACTION_VERBS.has(prohibitedVerb) &&
|
|
1002
|
+
!PROHIBITED_ACTION_VERBS.has(actionPrimaryVerb)) {
|
|
1003
|
+
intentAligned = true;
|
|
1004
|
+
reasons.push(
|
|
1005
|
+
`intent alignment: safe action "${actionPrimaryVerb}" against ` +
|
|
1006
|
+
`lock prohibiting "${prohibitedVerb}"`);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// If intent is ALIGNED, the action is COMPLIANT — slash the score to near zero
|
|
1011
|
+
// Shared keywords are expected (both discuss the same subject) but the action
|
|
1012
|
+
// is doing the right thing.
|
|
1013
|
+
if (intentAligned) {
|
|
1014
|
+
score = Math.floor(score * 0.10); // Keep only 10% of accumulated score
|
|
1015
|
+
// Skip all further bonuses (negation, intent conflict, destructive)
|
|
1016
|
+
} else {
|
|
1017
|
+
// NOT aligned — apply standard conflict bonuses
|
|
1018
|
+
|
|
1019
|
+
// 7. Negation conflict bonus
|
|
1020
|
+
if (lockIsProhibitive && hasAnyMatch) {
|
|
1021
|
+
score += SCORING.negationConflict;
|
|
1022
|
+
reasons.push("lock prohibits this action (negation detected)");
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// 8. Intent conflict bonus
|
|
1026
|
+
if (lockIsProhibitive && actionIntent.intent === "negative" && hasAnyMatch) {
|
|
1027
|
+
score += SCORING.intentConflict;
|
|
1028
|
+
reasons.push(
|
|
1029
|
+
`intent conflict: action "${actionIntent.actionVerb}" ` +
|
|
1030
|
+
`conflicts with lock prohibition`);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
// 9. Destructive action bonus
|
|
1034
|
+
const DESTRUCTIVE = new Set(["remove", "delete", "drop", "destroy",
|
|
1035
|
+
"kill", "purge", "wipe", "break", "disable", "truncate",
|
|
1036
|
+
"erase", "nuke", "obliterate"]);
|
|
1037
|
+
const actionIsDestructive = actionTokens.all.some(t => DESTRUCTIVE.has(t)) ||
|
|
1038
|
+
actionIntent.intent === "negative";
|
|
1039
|
+
if (actionIsDestructive && hasAnyMatch) {
|
|
1040
|
+
score += SCORING.destructiveAction;
|
|
1041
|
+
reasons.push("destructive action against locked constraint");
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// 10. Temporal evasion (BONUS, not reduction)
|
|
1045
|
+
if (hasTemporalMod && score > 0) {
|
|
1046
|
+
score += SCORING.temporalEvasion;
|
|
1047
|
+
reasons.push(`temporal modifier "${hasTemporalMod}" does NOT reduce severity`);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// Clamp and classify
|
|
1052
|
+
const confidence = Math.max(0, Math.min(score, 100));
|
|
1053
|
+
const isConflict = confidence >= SCORING.conflictThreshold;
|
|
1054
|
+
const level = confidence >= SCORING.highThreshold ? "HIGH"
|
|
1055
|
+
: confidence >= SCORING.mediumThreshold ? "MEDIUM" : "LOW";
|
|
1056
|
+
|
|
1057
|
+
return { confidence, level, reasons, isConflict };
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// ===================================================================
|
|
1061
|
+
// MAIN ENTRY POINT
|
|
1062
|
+
// ===================================================================
|
|
1063
|
+
|
|
1064
|
+
export function analyzeConflict(actionText, lockText) {
|
|
1065
|
+
const clauses = splitClauses(actionText);
|
|
1066
|
+
|
|
1067
|
+
const clauseResults = clauses.map(clause => ({
|
|
1068
|
+
clause,
|
|
1069
|
+
...scoreConflict({ actionText: clause, lockText })
|
|
1070
|
+
}));
|
|
1071
|
+
|
|
1072
|
+
// Take MAX confidence across all clauses
|
|
1073
|
+
const maxResult = clauseResults.reduce((best, curr) =>
|
|
1074
|
+
curr.confidence > best.confidence ? curr : best,
|
|
1075
|
+
clauseResults[0]
|
|
1076
|
+
);
|
|
1077
|
+
|
|
1078
|
+
// Merge reasons from all conflicting clauses
|
|
1079
|
+
const allReasons = [];
|
|
1080
|
+
for (const r of clauseResults) {
|
|
1081
|
+
if (r.isConflict) {
|
|
1082
|
+
if (clauses.length > 1) {
|
|
1083
|
+
allReasons.push(`[clause: "${r.clause.substring(0, 60)}"]`);
|
|
1084
|
+
}
|
|
1085
|
+
allReasons.push(...r.reasons);
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
return {
|
|
1090
|
+
confidence: maxResult.confidence,
|
|
1091
|
+
level: maxResult.level,
|
|
1092
|
+
reasons: allReasons.length > 0 ? allReasons : maxResult.reasons,
|
|
1093
|
+
isConflict: maxResult.isConflict,
|
|
1094
|
+
clauseResults,
|
|
1095
|
+
};
|
|
1096
|
+
}
|