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.
@@ -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
+ }