speclock 3.5.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +55 -3
- package/src/core/engine.js +10 -0
- package/src/core/lock-author.js +478 -0
- package/src/core/memory.js +12 -3
- package/src/core/semantics.js +777 -39
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +14 -4
package/src/core/semantics.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// ===================================================================
|
|
2
|
-
// SpecLock Semantic Analysis Engine
|
|
3
|
-
//
|
|
2
|
+
// SpecLock Semantic Analysis Engine v3
|
|
3
|
+
// Subject-aware conflict detection with scope matching.
|
|
4
4
|
// Zero external dependencies — pure JavaScript.
|
|
5
5
|
// ===================================================================
|
|
6
6
|
|
|
7
7
|
// ===================================================================
|
|
8
|
-
// SYNONYM GROUPS (
|
|
8
|
+
// SYNONYM GROUPS (75+ groups)
|
|
9
9
|
// Each group contains words/phrases that are semantically equivalent.
|
|
10
10
|
// ===================================================================
|
|
11
11
|
|
|
@@ -55,7 +55,7 @@ export const SYNONYM_GROUPS = [
|
|
|
55
55
|
"interface", "service"],
|
|
56
56
|
["request", "call", "invoke", "query", "fetch"],
|
|
57
57
|
["response", "reply", "result", "output", "payload"],
|
|
58
|
-
["network", "connectivity", "connection", "socket", "port", "protocol"],
|
|
58
|
+
["network", "connectivity", "connection", "socket", "websocket", "port", "protocol"],
|
|
59
59
|
|
|
60
60
|
// --- Testing ---
|
|
61
61
|
["test", "testing", "spec", "coverage", "assertion", "unit test",
|
|
@@ -66,13 +66,15 @@ export const SYNONYM_GROUPS = [
|
|
|
66
66
|
"production", "go live", "launch", "push to prod"],
|
|
67
67
|
|
|
68
68
|
// --- Security & auth ---
|
|
69
|
-
["security", "auth", "authentication", "authorization", "
|
|
70
|
-
"credential", "permission", "access control", "rbac", "acl"],
|
|
69
|
+
["security", "auth", "authentication", "authorization", "login",
|
|
70
|
+
"token", "credential", "permission", "access control", "rbac", "acl"],
|
|
71
71
|
["encrypt", "encryption", "cipher", "hash", "cryptographic",
|
|
72
72
|
"tls", "ssl", "https"],
|
|
73
73
|
["certificate", "cert", "signing", "signature", "verification", "verify"],
|
|
74
74
|
["firewall", "waf", "rate limit", "throttle", "ip block",
|
|
75
75
|
"deny list", "allow list"],
|
|
76
|
+
["mfa", "multi-factor authentication", "multi-factor", "2fa",
|
|
77
|
+
"two-factor authentication", "two-factor"],
|
|
76
78
|
["audit", "audit log", "audit trail", "logging", "log",
|
|
77
79
|
"monitoring", "observability", "telemetry", "tracking"],
|
|
78
80
|
|
|
@@ -144,6 +146,40 @@ export const SYNONYM_GROUPS = [
|
|
|
144
146
|
"archival", "preservation", "lifecycle"],
|
|
145
147
|
["consent", "user consent", "opt-in", "opt-out",
|
|
146
148
|
"data subject", "right to erasure"],
|
|
149
|
+
|
|
150
|
+
// --- Logistics / Supply Chain ---
|
|
151
|
+
["shipment", "shipping", "consignment", "delivery", "dispatch",
|
|
152
|
+
"freight", "cargo", "package", "parcel"],
|
|
153
|
+
["manifest", "bill of lading", "shipping document", "waybill",
|
|
154
|
+
"consignment note", "packing list"],
|
|
155
|
+
["warehouse", "fulfillment center", "distribution center",
|
|
156
|
+
"storage facility", "depot"],
|
|
157
|
+
["carrier", "shipping provider", "logistics provider",
|
|
158
|
+
"transport company", "freight forwarder"],
|
|
159
|
+
["eta", "estimated arrival", "delivery time", "arrival time",
|
|
160
|
+
"expected delivery"],
|
|
161
|
+
["customs", "customs clearance", "import", "export",
|
|
162
|
+
"tariff", "duty", "declaration"],
|
|
163
|
+
["tracking number", "tracking id", "shipment tracking",
|
|
164
|
+
"delivery tracking", "consignment tracking"],
|
|
165
|
+
|
|
166
|
+
// --- Travel / Booking ---
|
|
167
|
+
["reservation", "booking", "appointment", "ticket",
|
|
168
|
+
"confirmation", "itinerary"],
|
|
169
|
+
["passenger", "traveler", "guest", "visitor", "tourist"],
|
|
170
|
+
["passport", "passport data", "travel document",
|
|
171
|
+
"identity document", "visa"],
|
|
172
|
+
["flight", "airline", "aviation", "air travel"],
|
|
173
|
+
["hotel", "accommodation", "lodging", "stay", "room"],
|
|
174
|
+
|
|
175
|
+
// --- E-commerce ---
|
|
176
|
+
["checkout", "cart", "shopping cart", "basket",
|
|
177
|
+
"purchase flow", "buy flow"],
|
|
178
|
+
["order", "purchase", "buy", "acquisition"],
|
|
179
|
+
["product", "item", "sku", "merchandise", "product listing", "catalog"],
|
|
180
|
+
["price", "pricing", "cost", "rate", "amount", "charge"],
|
|
181
|
+
["coupon", "discount", "promo", "promotion", "voucher", "deal"],
|
|
182
|
+
["inventory", "stock", "supply", "availability"],
|
|
147
183
|
];
|
|
148
184
|
|
|
149
185
|
// ===================================================================
|
|
@@ -202,6 +238,37 @@ export const EUPHEMISM_MAP = {
|
|
|
202
238
|
"work around": ["bypass", "circumvent"],
|
|
203
239
|
"shortcut": ["bypass", "skip"],
|
|
204
240
|
|
|
241
|
+
// Financial / accounting euphemisms
|
|
242
|
+
"reconcile": ["modify", "adjust", "change", "alter"],
|
|
243
|
+
"reverse": ["undo", "revert", "modify", "change"],
|
|
244
|
+
"recalculate": ["modify", "change", "update", "alter"],
|
|
245
|
+
"backdate": ["modify", "tamper", "falsify", "change"],
|
|
246
|
+
"rebalance": ["modify", "adjust", "change", "redistribute"],
|
|
247
|
+
"reclassify": ["modify", "change", "recategorize"],
|
|
248
|
+
"redistribute": ["modify", "change", "move", "reallocate"],
|
|
249
|
+
"reallocate": ["modify", "change", "move"],
|
|
250
|
+
"write off": ["delete", "remove", "eliminate"],
|
|
251
|
+
"write down": ["modify", "reduce", "change"],
|
|
252
|
+
"void": ["delete", "cancel", "remove", "nullify"],
|
|
253
|
+
"post": ["modify", "change", "write", "record"],
|
|
254
|
+
"unpost": ["revert", "undo", "modify", "delete"],
|
|
255
|
+
"journal": ["modify", "record", "change"],
|
|
256
|
+
"accrue": ["modify", "add", "change"],
|
|
257
|
+
"amortize": ["modify", "reduce", "change"],
|
|
258
|
+
"depreciate": ["modify", "reduce", "change"],
|
|
259
|
+
|
|
260
|
+
// Logistics euphemisms
|
|
261
|
+
"reroute": ["modify", "change", "redirect"],
|
|
262
|
+
"divert": ["modify", "change", "redirect", "reroute"],
|
|
263
|
+
"reassign": ["modify", "change", "move", "transfer"],
|
|
264
|
+
"remanifest": ["modify", "change", "update"],
|
|
265
|
+
"deconsolidate": ["split", "separate", "modify"],
|
|
266
|
+
|
|
267
|
+
// Travel / booking euphemisms
|
|
268
|
+
"rebook": ["modify", "change", "replace", "cancel"],
|
|
269
|
+
"no-show": ["cancel", "void", "remove"],
|
|
270
|
+
"waitlist": ["modify", "change", "queue"],
|
|
271
|
+
|
|
205
272
|
// Database euphemisms
|
|
206
273
|
"truncate": ["delete", "remove", "wipe", "empty"],
|
|
207
274
|
"vacuum": ["delete", "remove", "clean"],
|
|
@@ -285,7 +352,7 @@ export const CONCEPT_MAP = {
|
|
|
285
352
|
"hipaa": ["phi", "patient data", "health information",
|
|
286
353
|
"medical records", "compliance"],
|
|
287
354
|
|
|
288
|
-
// Financial
|
|
355
|
+
// Financial / Fintech
|
|
289
356
|
"pci": ["cardholder data", "payment data", "card data",
|
|
290
357
|
"pci dss", "payment security"],
|
|
291
358
|
"cardholder data": ["pci", "payment data", "card data", "credit card", "pan"],
|
|
@@ -293,6 +360,77 @@ export const CONCEPT_MAP = {
|
|
|
293
360
|
"trade": ["executed trade", "trade record", "order", "position"],
|
|
294
361
|
"executed trade": ["trade", "trade record", "order"],
|
|
295
362
|
"trade record": ["trade", "executed trade", "transaction record"],
|
|
363
|
+
"transaction": ["payment", "transfer", "ledger entry", "posting",
|
|
364
|
+
"settlement", "billing", "charge", "balance"],
|
|
365
|
+
"ledger": ["transaction", "financial records", "accounting",
|
|
366
|
+
"general ledger", "journal", "balance sheet", "posting"],
|
|
367
|
+
"balance": ["account balance", "ledger", "transaction", "funds",
|
|
368
|
+
"financial records", "settlement"],
|
|
369
|
+
"account": ["balance", "ledger", "financial records", "customer account",
|
|
370
|
+
"account balance"],
|
|
371
|
+
"credit card": ["payment data", "cardholder data", "card data", "pci",
|
|
372
|
+
"card number", "pan"],
|
|
373
|
+
"card number": ["credit card", "cardholder data", "payment data", "pan"],
|
|
374
|
+
"settlement": ["transaction", "payment", "clearing", "reconciliation",
|
|
375
|
+
"transfer"],
|
|
376
|
+
"fraud detection": ["fraud", "fraud prevention", "anti-fraud", "fraud monitoring",
|
|
377
|
+
"suspicious activity", "transaction monitoring"],
|
|
378
|
+
"fraud": ["fraud detection", "fraud prevention", "suspicious activity",
|
|
379
|
+
"anti-fraud"],
|
|
380
|
+
"posting": ["transaction", "ledger entry", "journal entry", "record"],
|
|
381
|
+
"reconciliation": ["balance", "ledger", "account", "transaction", "audit"],
|
|
382
|
+
"checkout": ["payment", "cart", "purchase", "transaction", "billing",
|
|
383
|
+
"payment processing", "order"],
|
|
384
|
+
"revenue": ["payment", "billing", "income", "sales", "earnings",
|
|
385
|
+
"transaction"],
|
|
386
|
+
"invoice": ["billing", "payment", "charge", "transaction", "accounts receivable"],
|
|
387
|
+
|
|
388
|
+
// Logistics / Supply Chain
|
|
389
|
+
"shipment": ["cargo", "freight", "consignment", "delivery", "package",
|
|
390
|
+
"manifest", "tracking", "shipping"],
|
|
391
|
+
"manifest": ["shipment", "cargo", "freight", "bill of lading",
|
|
392
|
+
"shipping document", "consignment"],
|
|
393
|
+
"cargo": ["shipment", "freight", "manifest", "consignment", "goods"],
|
|
394
|
+
"freight": ["shipment", "cargo", "manifest", "logistics"],
|
|
395
|
+
"delivery": ["shipment", "shipping", "tracking", "eta", "transit"],
|
|
396
|
+
"eta": ["delivery time", "estimated arrival", "timestamp",
|
|
397
|
+
"delivery", "tracking", "schedule"],
|
|
398
|
+
"warehouse": ["inventory", "stock", "storage", "fulfillment"],
|
|
399
|
+
"inventory": ["warehouse", "stock", "supply", "goods"],
|
|
400
|
+
"customs": ["import", "export", "tariff", "duty", "clearance",
|
|
401
|
+
"border", "declaration"],
|
|
402
|
+
"carrier": ["shipping provider", "logistics provider", "trucker",
|
|
403
|
+
"transport", "shipping company"],
|
|
404
|
+
"tracking": ["shipment tracking", "delivery tracking", "status",
|
|
405
|
+
"location", "transit"],
|
|
406
|
+
"bill of lading": ["manifest", "shipping document", "consignment note"],
|
|
407
|
+
|
|
408
|
+
// Travel / Booking
|
|
409
|
+
"reservation": ["booking", "appointment", "ticket", "confirmation",
|
|
410
|
+
"itinerary", "payment record"],
|
|
411
|
+
"booking": ["reservation", "appointment", "ticket", "itinerary",
|
|
412
|
+
"confirmation"],
|
|
413
|
+
"passenger": ["traveler", "guest", "customer", "pii", "personal data",
|
|
414
|
+
"user data", "passenger data"],
|
|
415
|
+
"itinerary": ["booking", "reservation", "travel plan", "route",
|
|
416
|
+
"schedule", "flight"],
|
|
417
|
+
"passport": ["pii", "personal data", "identity document", "travel document",
|
|
418
|
+
"passenger data"],
|
|
419
|
+
"passport data": ["pii", "personal data", "identity", "passenger",
|
|
420
|
+
"travel document"],
|
|
421
|
+
"flight": ["booking", "reservation", "itinerary", "travel"],
|
|
422
|
+
"hotel": ["booking", "reservation", "accommodation", "lodging"],
|
|
423
|
+
"rate limiting": ["throttle", "request limit", "api limit", "rate limit",
|
|
424
|
+
"quota", "access control"],
|
|
425
|
+
|
|
426
|
+
// E-commerce
|
|
427
|
+
"cart": ["checkout", "purchase", "shopping cart"],
|
|
428
|
+
"payment processing":["payment", "checkout", "billing", "transaction",
|
|
429
|
+
"stripe", "payment gateway"],
|
|
430
|
+
"payment gateway": ["payment processing", "stripe", "paypal", "checkout",
|
|
431
|
+
"billing", "transaction"],
|
|
432
|
+
"product": ["item", "sku", "catalog", "merchandise", "product listing"],
|
|
433
|
+
"price": ["pricing", "cost", "amount", "rate", "charge"],
|
|
296
434
|
|
|
297
435
|
// Audit/logging
|
|
298
436
|
"audit logging": ["audit log", "audit trail", "logging", "monitoring"],
|
|
@@ -315,6 +453,12 @@ export const CONCEPT_MAP = {
|
|
|
315
453
|
"pii": ["personal data", "user data", "personally identifiable information",
|
|
316
454
|
"user information", "gdpr"],
|
|
317
455
|
"personal data": ["pii", "user data", "user information", "gdpr", "data protection"],
|
|
456
|
+
"gdpr": ["data protection", "consent", "privacy", "personal data", "pii",
|
|
457
|
+
"data subject", "right to erasure", "user data"],
|
|
458
|
+
"data protection": ["gdpr", "privacy", "consent", "personal data", "pii",
|
|
459
|
+
"data subject", "compliance"],
|
|
460
|
+
"consent": ["gdpr", "data protection", "opt-in", "opt-out", "user consent",
|
|
461
|
+
"privacy", "data subject"],
|
|
318
462
|
"user data": ["pii", "personal data", "user information", "user records"],
|
|
319
463
|
|
|
320
464
|
// Encryption
|
|
@@ -326,6 +470,13 @@ export const CONCEPT_MAP = {
|
|
|
326
470
|
"ban records": ["ban record", "banned users", "suspension records",
|
|
327
471
|
"blocked users", "moderation records"],
|
|
328
472
|
|
|
473
|
+
// Approval / moderation concepts
|
|
474
|
+
"approve": ["moderation", "content review", "content moderation",
|
|
475
|
+
"review queue"],
|
|
476
|
+
"batch approve": ["bypass moderation", "skip review", "auto-approve",
|
|
477
|
+
"content moderation", "moderation"],
|
|
478
|
+
"approval queue": ["moderation", "review queue", "content review"],
|
|
479
|
+
|
|
329
480
|
// Authentication/2FA
|
|
330
481
|
"2fa": ["two-factor", "two factor authentication", "mfa",
|
|
331
482
|
"multi-factor", "authentication", "auth"],
|
|
@@ -333,6 +484,20 @@ export const CONCEPT_MAP = {
|
|
|
333
484
|
"credential", "session", "token"],
|
|
334
485
|
"auth": ["authentication", "login", "sign in", "2fa",
|
|
335
486
|
"credential", "access control"],
|
|
487
|
+
"mfa": ["multi-factor authentication", "multi-factor", "2fa",
|
|
488
|
+
"two-factor", "authentication"],
|
|
489
|
+
"multi-factor": ["mfa", "2fa", "two-factor", "authentication",
|
|
490
|
+
"multi-factor authentication"],
|
|
491
|
+
"sso": ["saml", "oidc", "single sign-on", "oauth",
|
|
492
|
+
"authentication", "identity provider"],
|
|
493
|
+
"saml": ["sso", "oidc", "single sign-on", "authentication",
|
|
494
|
+
"identity provider"],
|
|
495
|
+
|
|
496
|
+
// Networking
|
|
497
|
+
"websocket": ["socket", "real-time connection", "ws", "wss",
|
|
498
|
+
"socket connection"],
|
|
499
|
+
"socket": ["websocket", "connection", "real-time connection",
|
|
500
|
+
"socket connection"],
|
|
336
501
|
|
|
337
502
|
// Encryption
|
|
338
503
|
"encryption": ["encrypt", "tls", "ssl", "https", "cryptographic",
|
|
@@ -420,7 +585,7 @@ const STOPWORDS = new Set([
|
|
|
420
585
|
|
|
421
586
|
const POSITIVE_INTENT_MARKERS = [
|
|
422
587
|
"enable", "activate", "turn on", "switch on", "start",
|
|
423
|
-
"add", "create", "implement", "introduce", "set up",
|
|
588
|
+
"add", "create", "implement", "introduce", "set up", "build",
|
|
424
589
|
"install", "deploy", "launch", "initialize",
|
|
425
590
|
"enforce", "strengthen", "harden", "improve", "enhance",
|
|
426
591
|
"increase", "expand", "extend", "upgrade", "boost",
|
|
@@ -568,13 +733,39 @@ export function tokenize(text) {
|
|
|
568
733
|
}
|
|
569
734
|
|
|
570
735
|
// Extract single words (>= 2 chars)
|
|
571
|
-
const
|
|
736
|
+
const rawWords = lower
|
|
572
737
|
.replace(/[^a-z0-9\-\/&]+/g, " ")
|
|
573
738
|
.split(/\s+/)
|
|
574
739
|
.filter(w => w.length >= 2);
|
|
575
740
|
|
|
576
|
-
|
|
577
|
-
|
|
741
|
+
// Split slash-separated tokens into parts — "sso/saml" also adds "sso", "saml"
|
|
742
|
+
for (const w of [...rawWords]) {
|
|
743
|
+
if (w.includes('/')) {
|
|
744
|
+
for (const part of w.split('/')) {
|
|
745
|
+
if (part.length >= 2 && !rawWords.includes(part)) rawWords.push(part);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Basic plural normalization — add both singular and plural forms
|
|
751
|
+
// so "databases" matches "database" and vice versa
|
|
752
|
+
const words = [...rawWords];
|
|
753
|
+
for (const w of rawWords) {
|
|
754
|
+
if (w.endsWith("ses") && w.length > 4) {
|
|
755
|
+
// "databases" → "database"
|
|
756
|
+
words.push(w.slice(0, -1));
|
|
757
|
+
} else if (w.endsWith("ies") && w.length > 4) {
|
|
758
|
+
// "entries" → "entry"
|
|
759
|
+
words.push(w.slice(0, -3) + "y");
|
|
760
|
+
} else if (w.endsWith("s") && !w.endsWith("ss") && !w.endsWith("us") && w.length > 3) {
|
|
761
|
+
// "records" → "record", "logs" → "log"
|
|
762
|
+
words.push(w.slice(0, -1));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const uniqueWords = [...new Set(words)];
|
|
766
|
+
|
|
767
|
+
const all = [...new Set([...phrases, ...uniqueWords])];
|
|
768
|
+
return { words: uniqueWords, phrases, all };
|
|
578
769
|
}
|
|
579
770
|
|
|
580
771
|
// ===================================================================
|
|
@@ -803,10 +994,20 @@ function extractProhibitedVerb(lockText) {
|
|
|
803
994
|
return null;
|
|
804
995
|
}
|
|
805
996
|
|
|
997
|
+
// Neutral modification/replacement verbs — not inherently positive or negative,
|
|
998
|
+
// but important for extractPrimaryVerb to detect as action verbs.
|
|
999
|
+
const NEUTRAL_ACTION_VERBS = [
|
|
1000
|
+
"modify", "change", "alter", "reconfigure", "rework",
|
|
1001
|
+
"overhaul", "restructure", "refactor", "redesign",
|
|
1002
|
+
"replace", "swap", "switch", "migrate", "substitute",
|
|
1003
|
+
"touch", "mess", "configure", "optimize", "tweak",
|
|
1004
|
+
"extend", "shorten", "adjust", "customize", "personalize",
|
|
1005
|
+
];
|
|
1006
|
+
|
|
806
1007
|
function extractPrimaryVerb(actionText) {
|
|
807
1008
|
const lower = actionText.toLowerCase();
|
|
808
|
-
// Find first matching marker in text
|
|
809
|
-
const allMarkers = [...POSITIVE_INTENT_MARKERS, ...NEGATIVE_INTENT_MARKERS]
|
|
1009
|
+
// Find first matching marker in text (includes neutral action verbs)
|
|
1010
|
+
const allMarkers = [...POSITIVE_INTENT_MARKERS, ...NEGATIVE_INTENT_MARKERS, ...NEUTRAL_ACTION_VERBS]
|
|
810
1011
|
.sort((a, b) => b.length - a.length);
|
|
811
1012
|
|
|
812
1013
|
let earliest = null;
|
|
@@ -867,7 +1068,322 @@ function checkOpposites(verb1, verb2) {
|
|
|
867
1068
|
|
|
868
1069
|
function isProhibitiveLock(lockText) {
|
|
869
1070
|
return /\b(never|must not|do not|don't|cannot|can't|forbidden|prohibited|disallowed)\b/i.test(lockText)
|
|
870
|
-
|| /\bno\s+\w/i.test(lockText)
|
|
1071
|
+
|| /\bno\s+\w/i.test(lockText)
|
|
1072
|
+
// Normalized lock patterns from lock-author.js rewriting
|
|
1073
|
+
|| /\bis\s+frozen\b/i.test(lockText)
|
|
1074
|
+
|| /\bmust\s+(remain|be\s+preserved|stay|always)\b/i.test(lockText);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// ===================================================================
|
|
1078
|
+
// INLINE SUBJECT EXTRACTION (avoids circular dependency with lock-author.js)
|
|
1079
|
+
// Extracts noun-phrase subjects from action/lock text for scope matching.
|
|
1080
|
+
// ===================================================================
|
|
1081
|
+
|
|
1082
|
+
const _CONTAMINATING_VERBS = new Set([
|
|
1083
|
+
"add", "create", "introduce", "insert", "implement", "build", "make",
|
|
1084
|
+
"include", "put", "set", "use", "install", "deploy", "attach", "connect",
|
|
1085
|
+
"remove", "delete", "drop", "destroy", "kill", "purge", "wipe", "erase",
|
|
1086
|
+
"eliminate", "clear", "empty", "nuke",
|
|
1087
|
+
"change", "modify", "alter", "update", "mutate", "transform", "rewrite",
|
|
1088
|
+
"edit", "adjust", "tweak", "revise", "amend", "touch", "rework",
|
|
1089
|
+
"move", "migrate", "transfer", "shift", "relocate", "switch", "swap",
|
|
1090
|
+
"replace", "substitute", "exchange",
|
|
1091
|
+
"enable", "disable", "activate", "deactivate", "start", "stop",
|
|
1092
|
+
"turn", "pause", "suspend", "halt",
|
|
1093
|
+
"push", "pull", "send", "expose", "leak", "reveal", "show",
|
|
1094
|
+
"allow", "permit", "let", "give", "grant", "open",
|
|
1095
|
+
"bypass", "skip", "ignore", "circumvent",
|
|
1096
|
+
"refactor", "restructure", "simplify", "consolidate",
|
|
1097
|
+
"mess",
|
|
1098
|
+
"fix", "repair", "restore", "recover", "break", "revert", "rollback",
|
|
1099
|
+
"reconcile", "reverse", "recalculate", "backdate", "rebalance",
|
|
1100
|
+
"reroute", "divert", "reassign", "rebook", "cancel",
|
|
1101
|
+
"upgrade", "downgrade", "patch", "bump",
|
|
1102
|
+
]);
|
|
1103
|
+
|
|
1104
|
+
const _FILLER_WORDS = new Set([
|
|
1105
|
+
// Articles & determiners
|
|
1106
|
+
"the", "a", "an", "any", "another", "other", "new", "additional",
|
|
1107
|
+
"more", "extra", "further", "existing", "current", "old", "all",
|
|
1108
|
+
"our", "their", "user", "users",
|
|
1109
|
+
// Prepositions
|
|
1110
|
+
"to", "of", "in", "on", "for", "from", "with", "by", "at", "up",
|
|
1111
|
+
"as", "into", "through", "between", "about", "before", "after",
|
|
1112
|
+
"without", "within", "during", "under", "over", "above", "below",
|
|
1113
|
+
// Conjunctions — CRITICAL: "and"/"or" must not create false bigram overlaps
|
|
1114
|
+
"and", "or", "nor", "but", "yet", "so",
|
|
1115
|
+
// Auxiliary/modal verbs — these are grammar, not subjects
|
|
1116
|
+
"is", "be", "are", "was", "were", "been", "being",
|
|
1117
|
+
"must", "never", "should", "shall", "can", "could", "would",
|
|
1118
|
+
"will", "may", "might", "do", "does", "did", "has", "have", "had",
|
|
1119
|
+
// Negation
|
|
1120
|
+
"not", "no", "none",
|
|
1121
|
+
// Common adverbs
|
|
1122
|
+
"just", "only", "also", "always", "every",
|
|
1123
|
+
]);
|
|
1124
|
+
|
|
1125
|
+
const _PROHIBITION_PATTERNS = [
|
|
1126
|
+
/^never\s+/i, /^must\s+not\s+/i, /^do\s+not\s+/i, /^don'?t\s+/i,
|
|
1127
|
+
/^cannot\s+/i, /^can'?t\s+/i, /^should\s+not\s+/i, /^shouldn'?t\s+/i,
|
|
1128
|
+
/^no\s+(?:one\s+(?:should|may|can)\s+)?/i,
|
|
1129
|
+
/^(?:it\s+is\s+)?(?:forbidden|prohibited|not\s+allowed)\s+to\s+/i,
|
|
1130
|
+
/^avoid\s+/i, /^prevent\s+/i, /^refrain\s+from\s+/i, /^stop\s+/i,
|
|
1131
|
+
];
|
|
1132
|
+
|
|
1133
|
+
// Generic words that are too vague to establish subject overlap on their own
|
|
1134
|
+
const _GENERIC_SUBJECT_WORDS = new Set([
|
|
1135
|
+
"system", "service", "module", "component", "feature", "function",
|
|
1136
|
+
"method", "class", "model", "handler", "controller", "manager",
|
|
1137
|
+
"process", "workflow", "flow", "logic", "config", "configuration",
|
|
1138
|
+
"settings", "options", "parameters", "data", "information", "record",
|
|
1139
|
+
"records", "file", "files", "page", "section", "area", "zone",
|
|
1140
|
+
"layer", "level", "tier", "part", "piece", "item", "items",
|
|
1141
|
+
"thing", "stuff", "code", "app", "application", "project",
|
|
1142
|
+
// Rewritten lock qualifiers (from lock-author.js output)
|
|
1143
|
+
"frozen", "prohibited", "preserved", "active", "enabled",
|
|
1144
|
+
"disabled", "allowed", "introduced", "added", "modifications",
|
|
1145
|
+
"deletion", "removal", "migration", "replacement",
|
|
1146
|
+
]);
|
|
1147
|
+
|
|
1148
|
+
// Check if a word is a verb form (past participle / gerund) of a contaminating verb.
|
|
1149
|
+
// e.g., "exposed" → "expose", "logged" → "log", "modified" → "modif" → "modify"
|
|
1150
|
+
function _isVerbForm(word) {
|
|
1151
|
+
// -ed suffix: "exposed" → "expos" → check "expose"; "logged" → "logg" → check "log"
|
|
1152
|
+
if (word.endsWith("ed")) {
|
|
1153
|
+
const stem1 = word.slice(0, -1); // "exposed" → "expose"
|
|
1154
|
+
if (_CONTAMINATING_VERBS.has(stem1)) return true;
|
|
1155
|
+
const stem2 = word.slice(0, -2); // "logged" → "logg" — nope
|
|
1156
|
+
if (_CONTAMINATING_VERBS.has(stem2)) return true;
|
|
1157
|
+
const stem3 = word.slice(0, -3); // "logged" → "log" (double consonant)
|
|
1158
|
+
if (_CONTAMINATING_VERBS.has(stem3)) return true;
|
|
1159
|
+
// "modified" → "modifi" → "modify" (ied → y)
|
|
1160
|
+
if (word.endsWith("ied")) {
|
|
1161
|
+
const stem4 = word.slice(0, -3) + "y"; // "modified" → "modify"
|
|
1162
|
+
if (_CONTAMINATING_VERBS.has(stem4)) return true;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// -ing suffix: "exposing" → "expos" → check "expose"; "logging" → "logg" → "log"
|
|
1166
|
+
if (word.endsWith("ing")) {
|
|
1167
|
+
const stem1 = word.slice(0, -3); // "building" → "build"
|
|
1168
|
+
if (_CONTAMINATING_VERBS.has(stem1)) return true;
|
|
1169
|
+
const stem2 = word.slice(0, -3) + "e"; // "exposing" → "expose"
|
|
1170
|
+
if (_CONTAMINATING_VERBS.has(stem2)) return true;
|
|
1171
|
+
const stem3 = word.slice(0, -4); // "logging" → "log" (double consonant)
|
|
1172
|
+
if (_CONTAMINATING_VERBS.has(stem3)) return true;
|
|
1173
|
+
}
|
|
1174
|
+
// -s suffix: "exposes" → "expose"
|
|
1175
|
+
if (word.endsWith("s") && !word.endsWith("ss")) {
|
|
1176
|
+
const stem1 = word.slice(0, -1);
|
|
1177
|
+
if (_CONTAMINATING_VERBS.has(stem1)) return true;
|
|
1178
|
+
// "pushes" → "push"
|
|
1179
|
+
if (word.endsWith("es")) {
|
|
1180
|
+
const stem2 = word.slice(0, -2);
|
|
1181
|
+
if (_CONTAMINATING_VERBS.has(stem2)) return true;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return false;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function _extractSubjectsInline(text) {
|
|
1188
|
+
const lower = text.toLowerCase().trim();
|
|
1189
|
+
const subjects = [];
|
|
1190
|
+
|
|
1191
|
+
// Strip prohibition prefix
|
|
1192
|
+
let content = lower;
|
|
1193
|
+
for (const pattern of _PROHIBITION_PATTERNS) {
|
|
1194
|
+
const match = content.match(pattern);
|
|
1195
|
+
if (match) {
|
|
1196
|
+
content = content.slice(match[0].length).trim();
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Handle rewritten lock format — truncate at qualifier phrases
|
|
1202
|
+
// "Authentication system is frozen — no modifications allowed."
|
|
1203
|
+
// "Patient records must be preserved — deletion and removal are prohibited."
|
|
1204
|
+
// "Audit logging must remain active and enabled at all times."
|
|
1205
|
+
content = content.replace(/\s+is\s+frozen\b.*$/i, "").trim();
|
|
1206
|
+
content = content.replace(/\s+must\s+(?:be\s+)?(?:preserved|remain)\b.*$/i, "").trim();
|
|
1207
|
+
content = content.replace(/\s*[—–]\s+(?:prohibited|no\s+|must\s+not|deletion|do\s+not|migration)\b.*$/i, "").trim();
|
|
1208
|
+
|
|
1209
|
+
// Strip leading verb
|
|
1210
|
+
const words = content.split(/\s+/);
|
|
1211
|
+
let startIdx = 0;
|
|
1212
|
+
for (let i = 0; i < Math.min(2, words.length); i++) {
|
|
1213
|
+
const w = words[i].replace(/[^a-z]/g, "");
|
|
1214
|
+
if (_CONTAMINATING_VERBS.has(w)) {
|
|
1215
|
+
startIdx = i + 1;
|
|
1216
|
+
break;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
// Skip fillers
|
|
1221
|
+
while (startIdx < words.length - 1) {
|
|
1222
|
+
const w = words[startIdx].replace(/[^a-z]/g, "");
|
|
1223
|
+
if (_FILLER_WORDS.has(w)) {
|
|
1224
|
+
startIdx++;
|
|
1225
|
+
} else {
|
|
1226
|
+
break;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
const subjectWords = words.slice(startIdx);
|
|
1231
|
+
if (subjectWords.length === 0) return subjects;
|
|
1232
|
+
|
|
1233
|
+
// Full noun phrase
|
|
1234
|
+
const fullPhrase = subjectWords.join(" ").replace(/[^a-z0-9\s\-\/]/g, "").trim();
|
|
1235
|
+
if (fullPhrase.length > 1) subjects.push(fullPhrase);
|
|
1236
|
+
|
|
1237
|
+
// Split on conjunctions
|
|
1238
|
+
const conjSplit = fullPhrase.split(/\s+(?:and|or|,)\s+/).map(s => s.trim()).filter(s => s.length > 1);
|
|
1239
|
+
if (conjSplit.length > 1) {
|
|
1240
|
+
for (const s of conjSplit) subjects.push(s);
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Individual significant words (excluding generic words)
|
|
1244
|
+
const significantWords = subjectWords
|
|
1245
|
+
.map(w => w.replace(/[^a-z0-9\-\/]/g, ""))
|
|
1246
|
+
.filter(w => w.length > 2 && !_FILLER_WORDS.has(w));
|
|
1247
|
+
|
|
1248
|
+
for (const w of significantWords) {
|
|
1249
|
+
if (_GENERIC_SUBJECT_WORDS.has(w) || w.length <= 3) continue;
|
|
1250
|
+
if (_CONTAMINATING_VERBS.has(w)) continue;
|
|
1251
|
+
// Also skip past participles/gerunds of contaminating verbs
|
|
1252
|
+
// "exposed" → "expose", "logged" → "log", "modified" → "modify"
|
|
1253
|
+
if (_isVerbForm(w)) continue;
|
|
1254
|
+
subjects.push(w);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
// Adjacent bigrams from significant words
|
|
1258
|
+
for (let i = 0; i < significantWords.length - 1; i++) {
|
|
1259
|
+
const bigram = `${significantWords[i]} ${significantWords[i + 1]}`;
|
|
1260
|
+
if (!subjects.includes(bigram)) {
|
|
1261
|
+
subjects.push(bigram);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return [...new Set(subjects)];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
function _compareSubjectsInline(actionText, lockText) {
|
|
1269
|
+
const lockSubjects = _extractSubjectsInline(lockText);
|
|
1270
|
+
const actionSubjects = _extractSubjectsInline(actionText);
|
|
1271
|
+
|
|
1272
|
+
if (lockSubjects.length === 0 || actionSubjects.length === 0) {
|
|
1273
|
+
return { overlaps: false, overlapScore: 0, matchedSubjects: [], lockSubjects, actionSubjects };
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
const matched = [];
|
|
1277
|
+
|
|
1278
|
+
// Expand subjects through concept map for cross-domain matching.
|
|
1279
|
+
// IMPORTANT: Do NOT seed with originals — only concept-derived terms go here.
|
|
1280
|
+
// Including originals causes self-matches: if "device" is in both sides,
|
|
1281
|
+
// actionExpanded.has("device") fires for EVERY (ls, as) pair, creating
|
|
1282
|
+
// spurious "concept: dashboard ~ device" matches that inflate strongMatchCount.
|
|
1283
|
+
const lockExpanded = new Set();
|
|
1284
|
+
const actionExpanded = new Set();
|
|
1285
|
+
|
|
1286
|
+
for (const ls of lockSubjects) {
|
|
1287
|
+
// Check concept map — if a lock subject maps to a concept that
|
|
1288
|
+
// contains an action subject (or vice versa), it's a scope match
|
|
1289
|
+
if (CONCEPT_MAP[ls]) {
|
|
1290
|
+
for (const related of CONCEPT_MAP[ls]) {
|
|
1291
|
+
lockExpanded.add(related);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
// Also check individual words within the subject
|
|
1295
|
+
for (const word of ls.split(/\s+/)) {
|
|
1296
|
+
if (CONCEPT_MAP[word]) {
|
|
1297
|
+
for (const related of CONCEPT_MAP[word]) {
|
|
1298
|
+
lockExpanded.add(related);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
for (const as of actionSubjects) {
|
|
1305
|
+
if (CONCEPT_MAP[as]) {
|
|
1306
|
+
for (const related of CONCEPT_MAP[as]) {
|
|
1307
|
+
actionExpanded.add(related);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
for (const word of as.split(/\s+/)) {
|
|
1311
|
+
if (CONCEPT_MAP[word]) {
|
|
1312
|
+
for (const related of CONCEPT_MAP[word]) {
|
|
1313
|
+
actionExpanded.add(related);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
let strongMatchCount = 0; // multi-word phrase or containment matches
|
|
1320
|
+
|
|
1321
|
+
for (const ls of lockSubjects) {
|
|
1322
|
+
for (const as of actionSubjects) {
|
|
1323
|
+
// Exact match
|
|
1324
|
+
if (ls === as) {
|
|
1325
|
+
matched.push(ls);
|
|
1326
|
+
// Multi-word exact = STRONG. Single word exact = WEAK (one shared entity word).
|
|
1327
|
+
if (ls.includes(" ")) strongMatchCount++;
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
// Word-level containment — "patient records" is inside "old patient records archive"
|
|
1331
|
+
// Multi-word containment is always a STRONG match.
|
|
1332
|
+
const asRegex = new RegExp(`\\b${as.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
1333
|
+
const lsRegex = new RegExp(`\\b${ls.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
1334
|
+
if (asRegex.test(ls) || lsRegex.test(as)) {
|
|
1335
|
+
matched.push(`${as} ⊂ ${ls}`);
|
|
1336
|
+
// STRONG only if the contained phrase is multi-word (a compound concept inside a bigger one)
|
|
1337
|
+
// "patient records" ⊂ "old patient records" = STRONG (multi-word phrase match)
|
|
1338
|
+
// "device" ⊂ "device api" = WEAK (single word found in a bigram — just one shared word)
|
|
1339
|
+
if (as.includes(" ") && ls.includes(" ")) strongMatchCount++;
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
// Concept-expanded match — only STRONG if BOTH sides are multi-word phrases
|
|
1343
|
+
// Single-word concept matches (account~ledger, device~iot) are too ambiguous
|
|
1344
|
+
// to be considered strong scope overlap.
|
|
1345
|
+
if (actionExpanded.has(ls) || lockExpanded.has(as)) {
|
|
1346
|
+
matched.push(`concept: ${as} ~ ${ls}`);
|
|
1347
|
+
if (as.includes(" ") && ls.includes(" ") && !_GENERIC_SUBJECT_WORDS.has(as) && !_GENERIC_SUBJECT_WORDS.has(ls)) {
|
|
1348
|
+
strongMatchCount++;
|
|
1349
|
+
}
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
// Word-level overlap for multi-word subjects — skip generic words
|
|
1353
|
+
if (ls.includes(" ") && as.includes(" ")) {
|
|
1354
|
+
const lsWords = new Set(ls.split(/\s+/));
|
|
1355
|
+
const asWords = new Set(as.split(/\s+/));
|
|
1356
|
+
const intersection = [...lsWords].filter(w =>
|
|
1357
|
+
asWords.has(w) && w.length > 2 &&
|
|
1358
|
+
!_FILLER_WORDS.has(w) && !_CONTAMINATING_VERBS.has(w) &&
|
|
1359
|
+
!_GENERIC_SUBJECT_WORDS.has(w));
|
|
1360
|
+
if (intersection.length >= 2 && intersection.length >= Math.min(lsWords.size, asWords.size) * 0.4) {
|
|
1361
|
+
// 2+ shared words in a multi-word subject = STRONG
|
|
1362
|
+
matched.push(`word overlap: ${intersection.join(", ")}`);
|
|
1363
|
+
strongMatchCount++;
|
|
1364
|
+
} else if (intersection.length >= 1 && intersection.length >= Math.min(lsWords.size, asWords.size) * 0.4) {
|
|
1365
|
+
// 1 shared word in multi-word subjects = WEAK (different components of same entity)
|
|
1366
|
+
matched.push(`weak overlap: ${intersection.join(", ")}`);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Count unique single-word exact matches — 2+ different single words = STRONG
|
|
1373
|
+
const singleWordMatches = new Set(
|
|
1374
|
+
matched.filter(m => !m.includes(" ") && !m.includes("⊂") && !m.includes("~") && !m.includes(":"))
|
|
1375
|
+
);
|
|
1376
|
+
if (singleWordMatches.size >= 2) strongMatchCount++;
|
|
1377
|
+
|
|
1378
|
+
const uniqueMatched = [...new Set(matched)];
|
|
1379
|
+
return {
|
|
1380
|
+
overlaps: uniqueMatched.length > 0,
|
|
1381
|
+
strongOverlap: strongMatchCount > 0,
|
|
1382
|
+
overlapScore: uniqueMatched.length > 0 ? Math.min(uniqueMatched.length / Math.max(lockSubjects.length, 1), 1.0) : 0,
|
|
1383
|
+
matchedSubjects: uniqueMatched,
|
|
1384
|
+
lockSubjects,
|
|
1385
|
+
actionSubjects,
|
|
1386
|
+
};
|
|
871
1387
|
}
|
|
872
1388
|
|
|
873
1389
|
export function scoreConflict({ actionText, lockText }) {
|
|
@@ -934,6 +1450,31 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
934
1450
|
euphemismMatches.push(`"${info.via}" (euphemism for ${term})`);
|
|
935
1451
|
}
|
|
936
1452
|
}
|
|
1453
|
+
|
|
1454
|
+
// 4b. Destructive method verbs — "by replacing", "through overwriting", "via deleting"
|
|
1455
|
+
// When an action uses a positive primary verb but employs a destructive method,
|
|
1456
|
+
// the method verb is the real operation. "Optimize X by replacing Y" = replacement.
|
|
1457
|
+
const DESTRUCTIVE_METHODS = new Set([
|
|
1458
|
+
"replace", "remove", "delete", "destroy", "rewrite", "overwrite",
|
|
1459
|
+
"restructure", "reconfigure", "migrate", "swap", "switch",
|
|
1460
|
+
"override", "bypass", "eliminate", "strip", "gut", "scrap",
|
|
1461
|
+
"discard", "drop", "disable", "break", "wipe", "erase",
|
|
1462
|
+
]);
|
|
1463
|
+
const methodVerbMatch = actionText.toLowerCase().match(
|
|
1464
|
+
/\b(?:by|through|via)\s+(\w+ing)\b/i);
|
|
1465
|
+
if (methodVerbMatch) {
|
|
1466
|
+
const gerund = methodVerbMatch[1];
|
|
1467
|
+
const stem1 = gerund.slice(0, -3); // "switching" → "switch"
|
|
1468
|
+
const stem2 = gerund.slice(0, -3) + "e"; // "replacing" → "replace"
|
|
1469
|
+
const stem3 = gerund.slice(0, -4); // "dropping" → "drop"
|
|
1470
|
+
for (const verb of DESTRUCTIVE_METHODS) {
|
|
1471
|
+
if (verb === stem1 || verb === stem2 || verb === stem3) {
|
|
1472
|
+
euphemismMatches.push(`"${gerund}" (destructive method: ${verb})`);
|
|
1473
|
+
break;
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
937
1478
|
if (euphemismMatches.length > 0) {
|
|
938
1479
|
const pts = Math.min(euphemismMatches.length, 3) * SCORING.euphemismMatch;
|
|
939
1480
|
score += pts;
|
|
@@ -961,40 +1502,102 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
961
1502
|
reasons.push(`concept match: ${conceptMatches.slice(0, 2).join("; ")}`);
|
|
962
1503
|
}
|
|
963
1504
|
|
|
964
|
-
// 6.
|
|
965
|
-
//
|
|
966
|
-
//
|
|
967
|
-
//
|
|
1505
|
+
// 6. SUBJECT RELEVANCE GATE (v3 — scope-aware)
|
|
1506
|
+
// The core innovation: extract noun-phrase subjects from BOTH the lock
|
|
1507
|
+
// and the action, then check if they target the same component.
|
|
1508
|
+
//
|
|
1509
|
+
// "Update WhatsApp message formatting" vs "Never modify WhatsApp session handler"
|
|
1510
|
+
// → Lock subject: "whatsapp session handler"
|
|
1511
|
+
// → Action subject: "whatsapp message formatting"
|
|
1512
|
+
// → Subjects DON'T match (different components) → reduce score
|
|
968
1513
|
//
|
|
969
|
-
//
|
|
970
|
-
//
|
|
1514
|
+
// "Delete patient records" vs "Never delete patient records"
|
|
1515
|
+
// → Lock subject: "patient records"
|
|
1516
|
+
// → Action subject: "patient records"
|
|
1517
|
+
// → Subjects MATCH → keep score
|
|
1518
|
+
//
|
|
1519
|
+
// This replaces the old verb-only check with proper scope awareness.
|
|
1520
|
+
|
|
971
1521
|
const ACTION_VERBS_SET = new Set([
|
|
972
|
-
|
|
973
|
-
"
|
|
974
|
-
"
|
|
975
|
-
|
|
976
|
-
"
|
|
977
|
-
"
|
|
978
|
-
"
|
|
1522
|
+
// Modification verbs
|
|
1523
|
+
"modify", "change", "alter", "update", "mutate", "transform", "rewrite",
|
|
1524
|
+
"revise", "amend", "adjust", "tweak", "tune", "rework", "overhaul",
|
|
1525
|
+
// Destructive verbs
|
|
1526
|
+
"delete", "remove", "drop", "kill", "destroy", "purge", "wipe", "erase",
|
|
1527
|
+
"eliminate", "obliterate", "expunge", "nuke", "truncate", "clear", "empty",
|
|
1528
|
+
"flush", "reset", "void",
|
|
1529
|
+
// Creation verbs
|
|
1530
|
+
"add", "create", "introduce", "insert", "generate", "produce", "spawn",
|
|
1531
|
+
// Toggle verbs
|
|
1532
|
+
"disable", "enable", "activate", "deactivate", "start", "stop", "halt",
|
|
1533
|
+
"pause", "suspend", "freeze",
|
|
1534
|
+
// Replacement verbs
|
|
1535
|
+
"replace", "swap", "substitute", "switch", "exchange", "override", "overwrite",
|
|
1536
|
+
// Movement verbs
|
|
1537
|
+
"move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize",
|
|
1538
|
+
"merge", "split", "separate", "partition", "divide", "fork",
|
|
1539
|
+
// Installation verbs
|
|
1540
|
+
"install", "uninstall", "deploy", "connect", "disconnect", "detach",
|
|
1541
|
+
// Structural verbs
|
|
1542
|
+
"refactor", "restructure", "simplify", "reduce", "consolidate",
|
|
1543
|
+
"clean", "normalize", "flatten",
|
|
1544
|
+
// Recovery verbs
|
|
1545
|
+
"fix", "repair", "restore", "recover", "break", "revert", "rollback",
|
|
1546
|
+
// Visibility verbs
|
|
1547
|
+
"expose", "hide", "reveal", "leak",
|
|
1548
|
+
// Bypass verbs
|
|
1549
|
+
"bypass", "skip", "ignore", "circumvent",
|
|
1550
|
+
// Financial verbs
|
|
1551
|
+
"reconcile", "reverse", "recalculate", "backdate", "rebalance",
|
|
1552
|
+
"post", "unpost", "accrue", "amortize", "depreciate", "journal",
|
|
1553
|
+
// Logistics verbs
|
|
1554
|
+
"reroute", "divert", "reassign", "deconsolidate",
|
|
1555
|
+
// Booking verbs
|
|
1556
|
+
"rebook", "cancel",
|
|
1557
|
+
// Upgrade/downgrade
|
|
1558
|
+
"upgrade", "downgrade", "patch", "bump", "advance",
|
|
979
1559
|
]);
|
|
980
1560
|
|
|
981
1561
|
// Check if any synonym/concept match involves a non-verb term (= subject match)
|
|
982
1562
|
const hasSynonymSubjectMatch = synonymMatches.some(m => {
|
|
983
|
-
// Format: "term → expansion" — check if expansion is not a common verb
|
|
984
1563
|
const parts = m.split(" → ");
|
|
985
1564
|
const expansion = (parts[1] || "").trim();
|
|
986
1565
|
return !ACTION_VERBS_SET.has(expansion);
|
|
987
1566
|
});
|
|
988
1567
|
|
|
989
|
-
|
|
1568
|
+
// OLD check (vocabulary overlap) — kept as one input
|
|
1569
|
+
const hasVocabSubjectMatch = directOverlap.length > 0 || phraseOverlap.length > 0 ||
|
|
990
1570
|
conceptMatches.length > 0 || hasSynonymSubjectMatch;
|
|
1571
|
+
|
|
1572
|
+
// NEW check (scope-aware subject extraction)
|
|
1573
|
+
// Extract actual noun-phrase subjects and compare scopes
|
|
1574
|
+
const subjectComparison = _compareSubjectsInline(actionText, lockText);
|
|
1575
|
+
const hasScopeMatch = subjectComparison.overlaps;
|
|
1576
|
+
// strongOverlap = multi-word phrase match, containment, or 2+ individual word matches
|
|
1577
|
+
// Single-word overlaps like "product" alone are WEAK — different components of same entity
|
|
1578
|
+
const hasStrongScopeMatch = subjectComparison.strongOverlap;
|
|
1579
|
+
|
|
1580
|
+
// Combined subject match: EITHER vocabulary overlap on non-verbs
|
|
1581
|
+
// OR proper subject/scope overlap
|
|
1582
|
+
const hasSubjectMatch = hasVocabSubjectMatch || hasScopeMatch;
|
|
991
1583
|
const hasAnyMatch = hasSubjectMatch || synonymMatches.length > 0 ||
|
|
992
1584
|
euphemismMatches.length > 0;
|
|
993
1585
|
|
|
994
|
-
//
|
|
995
|
-
//
|
|
1586
|
+
// A "strong" vocabulary match means 2+ direct keyword overlap or a multi-word
|
|
1587
|
+
// phrase match. A single shared word + synonym/concept expansion = WEAK.
|
|
1588
|
+
// Synonym expansion inflates score ("device" → "iot, sensor, actuator") but
|
|
1589
|
+
// doesn't prove scope overlap — the action could be about a different "device".
|
|
1590
|
+
const hasStrongVocabMatch = directOverlap.length >= 2 || phraseOverlap.length > 0;
|
|
1591
|
+
|
|
1592
|
+
// Apply the subject relevance gate based on match quality
|
|
996
1593
|
if (!hasSubjectMatch && (synonymMatches.length > 0 || euphemismMatches.length > 0)) {
|
|
1594
|
+
// NO subject match at all — verb-only match → heavy reduction
|
|
997
1595
|
score = Math.floor(score * 0.15);
|
|
1596
|
+
reasons.push("subject gate: no subject overlap — verb-only match, likely false positive");
|
|
1597
|
+
} else if (hasVocabSubjectMatch && !hasScopeMatch && subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
|
|
1598
|
+
// Vocabulary overlap exists but subjects point to DIFFERENT scopes
|
|
1599
|
+
score = Math.floor(score * 0.35);
|
|
1600
|
+
reasons.push(`scope gate: shared vocabulary but different scope — lock targets "${subjectComparison.lockSubjects[0]}", action targets "${subjectComparison.actionSubjects[0]}"`);
|
|
998
1601
|
}
|
|
999
1602
|
|
|
1000
1603
|
const prohibitedVerb = extractProhibitedVerb(lockText);
|
|
@@ -1018,8 +1621,39 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1018
1621
|
if (!intentAligned && lockIsProhibitive && actionIntent.intent === "positive" && prohibitedVerb) {
|
|
1019
1622
|
const prohibitedIsNegative = NEGATIVE_INTENT_MARKERS.some(m =>
|
|
1020
1623
|
prohibitedVerb === m || prohibitedVerb.startsWith(m));
|
|
1021
|
-
|
|
1022
|
-
|
|
1624
|
+
// Only block alignment if the action actually performs the prohibited operation.
|
|
1625
|
+
// Noun synonyms ("price → pricing") are incidental and should NOT block.
|
|
1626
|
+
// But if the action contains the prohibited verb or its synonym ("shows" ≈ "expose"),
|
|
1627
|
+
// that's a real violation signal.
|
|
1628
|
+
const hasDestructiveLanguageMatch = euphemismMatches.length > 0;
|
|
1629
|
+
|
|
1630
|
+
// Check if action text contains the prohibited verb or its synonyms
|
|
1631
|
+
let actionPerformsProhibitedOp = false;
|
|
1632
|
+
if (prohibitedVerb) {
|
|
1633
|
+
const actionWordsLower = actionText.toLowerCase().split(/\s+/)
|
|
1634
|
+
.map(w => w.replace(/[^a-z]/g, ""));
|
|
1635
|
+
for (const w of actionWordsLower) {
|
|
1636
|
+
if (!w) continue;
|
|
1637
|
+
// Direct match (including conjugations: show/shows/showing/showed)
|
|
1638
|
+
if (w === prohibitedVerb || w.startsWith(prohibitedVerb)) {
|
|
1639
|
+
actionPerformsProhibitedOp = true;
|
|
1640
|
+
break;
|
|
1641
|
+
}
|
|
1642
|
+
// Synonym match: check if word is in the same synonym group as prohibited verb
|
|
1643
|
+
for (const group of SYNONYM_GROUPS) {
|
|
1644
|
+
if (group.includes(prohibitedVerb)) {
|
|
1645
|
+
if (group.some(syn => w === syn || w.startsWith(syn) && w.length <= syn.length + 3)) {
|
|
1646
|
+
actionPerformsProhibitedOp = true;
|
|
1647
|
+
}
|
|
1648
|
+
break;
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
if (actionPerformsProhibitedOp) break;
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
if (prohibitedIsNegative && !actionIntent.negated &&
|
|
1656
|
+
!hasDestructiveLanguageMatch && !actionPerformsProhibitedOp) {
|
|
1023
1657
|
intentAligned = true;
|
|
1024
1658
|
reasons.push(
|
|
1025
1659
|
`intent alignment: positive action "${actionPrimaryVerb}" against ` +
|
|
@@ -1047,6 +1681,26 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1047
1681
|
// IS a modification. Only truly read-only and activation verbs are safe.
|
|
1048
1682
|
]);
|
|
1049
1683
|
|
|
1684
|
+
// OBSERVABILITY ACTIONS: "add logging", "add monitoring", "add tracking"
|
|
1685
|
+
// are constructive observability actions, NOT modifications to the locked system.
|
|
1686
|
+
// If the action verb is "add/create/implement" AND the object is an
|
|
1687
|
+
// observability concept, treat it as safe.
|
|
1688
|
+
const OBSERVABILITY_KEYWORDS = new Set([
|
|
1689
|
+
"logging", "log", "logs", "monitoring", "monitor", "tracking",
|
|
1690
|
+
"tracing", "trace", "metrics", "alerting", "alerts", "alert",
|
|
1691
|
+
"observability", "telemetry", "analytics", "reporting", "auditing",
|
|
1692
|
+
"profiling", "instrumentation", "dashboard",
|
|
1693
|
+
]);
|
|
1694
|
+
const actionLower = actionText.toLowerCase();
|
|
1695
|
+
const actionWords = actionLower.split(/\s+/);
|
|
1696
|
+
const hasObservabilityObject = actionWords.some(w => OBSERVABILITY_KEYWORDS.has(w));
|
|
1697
|
+
const CONSTRUCTIVE_VERBS = new Set(["add", "create", "implement", "introduce", "set up", "enable"]);
|
|
1698
|
+
if (CONSTRUCTIVE_VERBS.has(actionPrimaryVerb) && hasObservabilityObject) {
|
|
1699
|
+
intentAligned = true;
|
|
1700
|
+
reasons.push(
|
|
1701
|
+
`intent alignment: observability action "${actionPrimaryVerb} ... ${actionWords.find(w => OBSERVABILITY_KEYWORDS.has(w))}" is non-destructive`);
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1050
1704
|
const PROHIBITED_ACTION_VERBS = new Set([
|
|
1051
1705
|
"modify", "change", "alter", "delete", "remove", "disable",
|
|
1052
1706
|
"drop", "break", "weaken", "expose", "install", "push",
|
|
@@ -1063,6 +1717,80 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1063
1717
|
}
|
|
1064
1718
|
}
|
|
1065
1719
|
|
|
1720
|
+
// Check 4: Enhancement/constructive actions against preservation/maintenance locks
|
|
1721
|
+
// "Increase the rate limit" is COMPLIANT with "rate limiting must remain active"
|
|
1722
|
+
// "Add better rate limit error messages" is COMPLIANT (doesn't disable rate limiting)
|
|
1723
|
+
// But "Add a way to bypass rate limiting" is NOT safe (contains negative op "bypass")
|
|
1724
|
+
if (!intentAligned && actionPrimaryVerb) {
|
|
1725
|
+
const ENHANCEMENT_VERBS = new Set([
|
|
1726
|
+
"increase", "improve", "enhance", "boost", "strengthen",
|
|
1727
|
+
"upgrade", "raise", "expand", "extend", "grow",
|
|
1728
|
+
]);
|
|
1729
|
+
const CONSTRUCTIVE_FOR_PRESERVATION = new Set([
|
|
1730
|
+
"build", "add", "create", "implement", "make", "design",
|
|
1731
|
+
"develop", "introduce",
|
|
1732
|
+
]);
|
|
1733
|
+
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/i.test(lockText);
|
|
1734
|
+
if (lockIsPreservation) {
|
|
1735
|
+
if (ENHANCEMENT_VERBS.has(actionPrimaryVerb)) {
|
|
1736
|
+
// Enhancement verbs always align with preservation locks
|
|
1737
|
+
intentAligned = true;
|
|
1738
|
+
reasons.push(
|
|
1739
|
+
`intent alignment: enhancement action "${actionPrimaryVerb}" is ` +
|
|
1740
|
+
`compliant with preservation lock`);
|
|
1741
|
+
} else if (CONSTRUCTIVE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
|
|
1742
|
+
// Constructive verbs align ONLY if the action doesn't contain negative ops
|
|
1743
|
+
const actionLower = actionText.toLowerCase();
|
|
1744
|
+
const hasNegativeOp = NEGATIVE_INTENT_MARKERS.some(m =>
|
|
1745
|
+
new RegExp(`\\b${escapeRegex(m)}\\b`, "i").test(actionLower));
|
|
1746
|
+
if (!hasNegativeOp) {
|
|
1747
|
+
intentAligned = true;
|
|
1748
|
+
reasons.push(
|
|
1749
|
+
`intent alignment: constructive "${actionPrimaryVerb}" is ` +
|
|
1750
|
+
`compliant with preservation lock (no negative operations)`);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Check 5: Constructive actions with weak/no scope overlap
|
|
1757
|
+
// "Build a device dashboard" shares "device" with "Device API keys" lock,
|
|
1758
|
+
// but building a dashboard doesn't modify API key handling.
|
|
1759
|
+
{
|
|
1760
|
+
const FEATURE_BUILDING_VERBS = new Set([
|
|
1761
|
+
"build", "add", "create", "implement", "make", "design",
|
|
1762
|
+
"develop", "introduce", "setup", "establish", "launch",
|
|
1763
|
+
"integrate", "include", "support",
|
|
1764
|
+
]);
|
|
1765
|
+
// 5a: Weak scope overlap (single shared word, no strong vocab match)
|
|
1766
|
+
if (!intentAligned && hasScopeMatch && !hasStrongScopeMatch && !hasStrongVocabMatch) {
|
|
1767
|
+
if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
|
|
1768
|
+
// Guard: if the shared word is a long, specific entity name (10+ chars),
|
|
1769
|
+
// it strongly identifies the target system — not an incidental overlap.
|
|
1770
|
+
// "authentication" (14 chars) clearly points to the auth system.
|
|
1771
|
+
// "product" (7 chars) or "device" (6 chars) are generic modifiers.
|
|
1772
|
+
const hasSpecificOverlap = directOverlap.some(w => w.length >= 10);
|
|
1773
|
+
if (!hasSpecificOverlap) {
|
|
1774
|
+
intentAligned = true;
|
|
1775
|
+
reasons.push(
|
|
1776
|
+
`intent alignment: constructive "${actionPrimaryVerb}" with ` +
|
|
1777
|
+
`weak scope overlap — new feature, not modification of locked component`);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
// 5b: Vocab overlap but NO scope overlap (subjects point to different things)
|
|
1782
|
+
// Even weaker signal — shared vocabulary but different actual components.
|
|
1783
|
+
if (!intentAligned && hasVocabSubjectMatch && !hasScopeMatch &&
|
|
1784
|
+
subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
|
|
1785
|
+
if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
|
|
1786
|
+
intentAligned = true;
|
|
1787
|
+
reasons.push(
|
|
1788
|
+
`intent alignment: constructive "${actionPrimaryVerb}" with ` +
|
|
1789
|
+
`no scope overlap — different components despite shared vocabulary`);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1066
1794
|
// If intent is ALIGNED, the action is COMPLIANT — slash the score to near zero
|
|
1067
1795
|
// Shared keywords are expected (both discuss the same subject) but the action
|
|
1068
1796
|
// is doing the right thing.
|
|
@@ -1072,27 +1800,37 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1072
1800
|
} else {
|
|
1073
1801
|
// NOT aligned — apply standard conflict bonuses
|
|
1074
1802
|
|
|
1075
|
-
// 7. Negation conflict bonus — requires subject match
|
|
1076
|
-
|
|
1803
|
+
// 7. Negation conflict bonus — requires STRONG subject match
|
|
1804
|
+
// Either: scope overlap (subject extraction confirms same target)
|
|
1805
|
+
// Or: 2+ direct word overlaps (not just a single shared word)
|
|
1806
|
+
// Or: phrase overlap (multi-word match is strong signal)
|
|
1807
|
+
// Or: concept match (domain-level relevance)
|
|
1808
|
+
// Concept matches contribute to base score but should NOT gate the negation
|
|
1809
|
+
// bonus — they're too indirect ("account" → "ledger" via concept map shouldn't
|
|
1810
|
+
// trigger the +35 negation bonus). Only direct evidence counts.
|
|
1811
|
+
const hasStrongSubjectMatch = hasStrongScopeMatch ||
|
|
1812
|
+
directOverlap.length >= 2 ||
|
|
1813
|
+
phraseOverlap.length > 0;
|
|
1814
|
+
if (lockIsProhibitive && hasStrongSubjectMatch) {
|
|
1077
1815
|
score += SCORING.negationConflict;
|
|
1078
1816
|
reasons.push("lock prohibits this action (negation detected)");
|
|
1079
1817
|
}
|
|
1080
1818
|
|
|
1081
|
-
// 8. Intent conflict bonus — requires subject match
|
|
1082
|
-
if (lockIsProhibitive && actionIntent.intent === "negative" &&
|
|
1819
|
+
// 8. Intent conflict bonus — requires strong subject match
|
|
1820
|
+
if (lockIsProhibitive && actionIntent.intent === "negative" && hasStrongSubjectMatch) {
|
|
1083
1821
|
score += SCORING.intentConflict;
|
|
1084
1822
|
reasons.push(
|
|
1085
1823
|
`intent conflict: action "${actionIntent.actionVerb}" ` +
|
|
1086
1824
|
`conflicts with lock prohibition`);
|
|
1087
1825
|
}
|
|
1088
1826
|
|
|
1089
|
-
// 9. Destructive action bonus — requires subject match
|
|
1827
|
+
// 9. Destructive action bonus — requires strong subject match
|
|
1090
1828
|
const DESTRUCTIVE = new Set(["remove", "delete", "drop", "destroy",
|
|
1091
1829
|
"kill", "purge", "wipe", "break", "disable", "truncate",
|
|
1092
1830
|
"erase", "nuke", "obliterate"]);
|
|
1093
1831
|
const actionIsDestructive = actionTokens.all.some(t => DESTRUCTIVE.has(t)) ||
|
|
1094
1832
|
actionIntent.intent === "negative";
|
|
1095
|
-
if (actionIsDestructive &&
|
|
1833
|
+
if (actionIsDestructive && hasStrongSubjectMatch) {
|
|
1096
1834
|
score += SCORING.destructiveAction;
|
|
1097
1835
|
reasons.push("destructive action against locked constraint");
|
|
1098
1836
|
}
|