speclock 4.4.3 → 4.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli/index.js +5 -2
- package/src/core/compliance.js +1 -1
- package/src/core/conflict.js +3 -2
- package/src/core/context.js +5 -1
- package/src/core/llm-checker.js +1 -0
- package/src/core/lock-author.js +12 -3
- package/src/core/semantics.js +253 -29
- package/src/core/telemetry.js +1 -1
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +1 -1
- package/src/mcp/server.js +1 -1
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
"name": "speclock",
|
|
4
4
|
|
|
5
|
-
"version": "4.
|
|
5
|
+
"version": "4.5.1",
|
|
6
6
|
|
|
7
7
|
"description": "AI constraint engine with Gemini LLM universal detection, Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. Cross-platform: MCP + direct API. 31 MCP tools + CLI. Enterprise platform.",
|
|
8
8
|
|
package/src/cli/index.js
CHANGED
|
@@ -117,7 +117,7 @@ function refreshContext(root) {
|
|
|
117
117
|
|
|
118
118
|
function printHelp() {
|
|
119
119
|
console.log(`
|
|
120
|
-
SpecLock v4.
|
|
120
|
+
SpecLock v4.5.1 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
|
|
121
121
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
122
122
|
|
|
123
123
|
Usage: speclock <command> [options]
|
|
@@ -373,7 +373,7 @@ Tip: When starting a new chat, tell the AI:
|
|
|
373
373
|
console.error("Usage: speclock lock <text> [--tags a,b] [--source user]");
|
|
374
374
|
process.exit(1);
|
|
375
375
|
}
|
|
376
|
-
const { lockId } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
|
|
376
|
+
const { lockId, rewritten, rewriteReason } = addLock(root, text, parseTags(flags.tags), flags.source || "user");
|
|
377
377
|
|
|
378
378
|
// Auto-guard related files (Solution 1)
|
|
379
379
|
const guardResult = autoGuardRelatedFiles(root, text);
|
|
@@ -392,6 +392,9 @@ Tip: When starting a new chat, tell the AI:
|
|
|
392
392
|
|
|
393
393
|
refreshContext(root);
|
|
394
394
|
console.log(`Locked (${lockId}): "${text}"`);
|
|
395
|
+
if (rewritten) {
|
|
396
|
+
console.log(` Note: Engine optimized for detection. Your original text is preserved.`);
|
|
397
|
+
}
|
|
395
398
|
return;
|
|
396
399
|
}
|
|
397
400
|
|
package/src/core/compliance.js
CHANGED
package/src/core/conflict.js
CHANGED
|
@@ -161,7 +161,8 @@ export function checkConflict(rootOrAction, proposedActionOrLock) {
|
|
|
161
161
|
if (result.isConflict) {
|
|
162
162
|
conflicting.push({
|
|
163
163
|
id: lock.id,
|
|
164
|
-
text: lock.text,
|
|
164
|
+
text: lock.originalText || lock.text,
|
|
165
|
+
engineText: lock.originalText ? lock.text : undefined,
|
|
165
166
|
matchedKeywords: [],
|
|
166
167
|
confidence: result.confidence,
|
|
167
168
|
level: result.level,
|
|
@@ -227,7 +228,7 @@ async function callProxy(actionText, lockTexts) {
|
|
|
227
228
|
|
|
228
229
|
try {
|
|
229
230
|
const controller = new AbortController();
|
|
230
|
-
const timeout = setTimeout(() => controller.abort(),
|
|
231
|
+
const timeout = setTimeout(() => controller.abort(), 2000); // 2s timeout
|
|
231
232
|
|
|
232
233
|
const resp = await fetch(proxyUrl, {
|
|
233
234
|
method: "POST",
|
package/src/core/context.js
CHANGED
|
@@ -29,7 +29,8 @@ export function generateContextPack(root) {
|
|
|
29
29
|
goal: brain.goal.text || "",
|
|
30
30
|
locks: activeLocks.slice(0, 15).map((l) => ({
|
|
31
31
|
id: l.id,
|
|
32
|
-
text: l.text,
|
|
32
|
+
text: l.originalText || l.text,
|
|
33
|
+
engineText: l.originalText ? l.text : undefined,
|
|
33
34
|
createdAt: l.createdAt,
|
|
34
35
|
source: l.source,
|
|
35
36
|
})),
|
|
@@ -89,6 +90,9 @@ export function generateContext(root) {
|
|
|
89
90
|
);
|
|
90
91
|
for (const lock of pack.locks) {
|
|
91
92
|
lines.push(`- **[LOCK]** ${lock.text} _(${lock.source}, ${lock.createdAt.substring(0, 10)})_`);
|
|
93
|
+
if (lock.engineText) {
|
|
94
|
+
lines.push(` - _Engine uses: "${lock.engineText}"_`);
|
|
95
|
+
}
|
|
92
96
|
}
|
|
93
97
|
} else {
|
|
94
98
|
lines.push("- *(No locks defined — consider adding constraints)*");
|
package/src/core/llm-checker.js
CHANGED
package/src/core/lock-author.js
CHANGED
|
@@ -251,17 +251,26 @@ export function rewriteLock(lockText, verb, subject) {
|
|
|
251
251
|
// "Never delete X" → "X must be preserved — delete and remove operations are prohibited"
|
|
252
252
|
// CRITICAL: include the original verb so euphemism matching can find it
|
|
253
253
|
// ("phase out" → "remove" needs "remove" in the lock text)
|
|
254
|
-
|
|
254
|
+
const destNote = verb === "remove"
|
|
255
|
+
? "remove and delete operations are prohibited"
|
|
256
|
+
: `${verb} and remove operations are prohibited`;
|
|
257
|
+
return `${cleanSubject} must be preserved — ${destNote}.`;
|
|
255
258
|
}
|
|
256
259
|
|
|
257
260
|
if (isModification) {
|
|
258
261
|
// "Never modify X" → "X is frozen — modify and change operations are prohibited"
|
|
259
|
-
|
|
262
|
+
const modNote = verb === "change"
|
|
263
|
+
? "change operations are prohibited"
|
|
264
|
+
: `${verb} and change operations are prohibited`;
|
|
265
|
+
return `${cleanSubject} is frozen — ${modNote}.`;
|
|
260
266
|
}
|
|
261
267
|
|
|
262
268
|
if (isMovement) {
|
|
263
269
|
// "Never migrate X" → "X must remain unchanged — migrate and replace operations are prohibited"
|
|
264
|
-
|
|
270
|
+
const moveNote = verb === "replace"
|
|
271
|
+
? "replace operations are prohibited"
|
|
272
|
+
: `${verb} and replace operations are prohibited`;
|
|
273
|
+
return `${cleanSubject} must remain unchanged — ${moveNote}.`;
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
if (isToggle) {
|
package/src/core/semantics.js
CHANGED
|
@@ -31,7 +31,8 @@ export const SYNONYM_GROUPS = [
|
|
|
31
31
|
"rewrite", "revise", "amend", "adjust", "tweak"],
|
|
32
32
|
["replace", "swap", "substitute", "switch", "exchange",
|
|
33
33
|
"override", "overwrite"],
|
|
34
|
-
["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize"
|
|
34
|
+
["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize",
|
|
35
|
+
"transition"],
|
|
35
36
|
["rename", "relabel", "rebrand", "alias"],
|
|
36
37
|
["merge", "combine", "consolidate", "unify", "join", "blend"],
|
|
37
38
|
["split", "separate", "partition", "divide", "fork", "decompose"],
|
|
@@ -46,6 +47,9 @@ export const SYNONYM_GROUPS = [
|
|
|
46
47
|
// --- Data stores ---
|
|
47
48
|
["database", "db", "datastore", "data store", "schema", "table",
|
|
48
49
|
"collection", "index", "migration", "sql", "nosql", "storage"],
|
|
50
|
+
["postgresql", "postgres", "mysql", "mongodb", "mongo", "firebase",
|
|
51
|
+
"firestore", "supabase", "dynamodb", "redis", "sqlite", "mariadb",
|
|
52
|
+
"cockroachdb", "cassandra", "couchdb", "neo4j"],
|
|
49
53
|
["record", "row", "document", "entry", "item", "entity", "tuple"],
|
|
50
54
|
["column", "field", "attribute", "property", "key"],
|
|
51
55
|
["backup", "snapshot", "dump", "export"],
|
|
@@ -108,6 +112,9 @@ export const SYNONYM_GROUPS = [
|
|
|
108
112
|
"remuneration", "stipend"],
|
|
109
113
|
["payment gateway", "payment provider", "payment processor",
|
|
110
114
|
"payment service", "payment platform"],
|
|
115
|
+
["razorpay", "stripe", "paypal", "phonepe", "paytm", "ccavenue",
|
|
116
|
+
"cashfree", "braintree", "adyen", "square", "google pay", "gpay",
|
|
117
|
+
"juspay", "billdesk", "instamojo"],
|
|
111
118
|
|
|
112
119
|
// --- IoT / firmware ---
|
|
113
120
|
["firmware", "firmware update", "ota", "over the air",
|
|
@@ -127,7 +134,10 @@ export const SYNONYM_GROUPS = [
|
|
|
127
134
|
"suspended", "blocked user"],
|
|
128
135
|
["user data", "user information", "user records", "pii",
|
|
129
136
|
"personally identifiable information", "personal data",
|
|
130
|
-
"gdpr", "data protection"
|
|
137
|
+
"gdpr", "data protection", "ssn", "social security number",
|
|
138
|
+
"social security", "email address", "email addresses",
|
|
139
|
+
"phone number", "phone numbers", "date of birth", "dob",
|
|
140
|
+
"passport number", "driver license", "national id"],
|
|
131
141
|
|
|
132
142
|
// --- DevOps / Infrastructure ---
|
|
133
143
|
["container", "docker", "kubernetes", "k8s", "pod",
|
|
@@ -243,6 +253,10 @@ export const EUPHEMISM_MAP = {
|
|
|
243
253
|
"work around": ["bypass", "circumvent"],
|
|
244
254
|
"shortcut": ["bypass", "skip"],
|
|
245
255
|
|
|
256
|
+
// Migration/transition euphemisms
|
|
257
|
+
"transition": ["migrate", "switch", "change", "move", "replace"],
|
|
258
|
+
"transition to": ["migrate to", "switch to", "change to", "move to"],
|
|
259
|
+
|
|
246
260
|
// Financial / accounting euphemisms
|
|
247
261
|
"reconcile": ["modify", "adjust", "change", "alter"],
|
|
248
262
|
"reverse": ["undo", "revert", "modify", "change"],
|
|
@@ -313,12 +327,16 @@ export const EUPHEMISM_MAP = {
|
|
|
313
327
|
"rotate": ["change", "replace", "renew", "modify"],
|
|
314
328
|
"renew": ["change", "replace", "rotate", "modify"],
|
|
315
329
|
|
|
316
|
-
// Security euphemisms
|
|
330
|
+
// Security / data exposure euphemisms
|
|
317
331
|
"make visible": ["expose", "reveal", "public"],
|
|
318
332
|
"make viewable": ["expose", "reveal", "public"],
|
|
319
333
|
"make accessible":["expose", "reveal", "public"],
|
|
320
334
|
"make public": ["expose", "reveal"],
|
|
321
335
|
"transmit": ["send", "transfer", "expose"],
|
|
336
|
+
"export": ["extract", "expose", "dump", "download"],
|
|
337
|
+
"exfiltrate": ["extract", "steal", "expose", "leak"],
|
|
338
|
+
"scrape": ["extract", "collect", "harvest"],
|
|
339
|
+
"harvest": ["collect", "extract", "scrape"],
|
|
322
340
|
|
|
323
341
|
// Encryption euphemisms
|
|
324
342
|
"unencrypted": ["without encryption", "disable encryption", "no encryption", "plaintext"],
|
|
@@ -397,27 +415,51 @@ export const CONCEPT_MAP = {
|
|
|
397
415
|
"wages": ["salary", "payroll", "compensation", "financial records"],
|
|
398
416
|
"compensation": ["salary", "payroll", "wages", "financial records"],
|
|
399
417
|
|
|
400
|
-
// Payment providers (brand names → payment gateway concept)
|
|
418
|
+
// Payment providers (brand names → payment gateway concept + cross-references)
|
|
401
419
|
"razorpay": ["payment gateway", "payment processing", "payment",
|
|
402
|
-
"transaction", "billing"
|
|
420
|
+
"transaction", "billing", "stripe", "paypal",
|
|
421
|
+
"phonepe", "paytm", "ccavenue", "cashfree"],
|
|
403
422
|
"phonepe": ["payment gateway", "payment processing", "payment",
|
|
404
|
-
"upi", "transaction"
|
|
423
|
+
"upi", "transaction", "razorpay", "paytm",
|
|
424
|
+
"stripe", "google pay"],
|
|
405
425
|
"ccavenue": ["payment gateway", "payment processing", "payment",
|
|
406
|
-
"transaction", "billing"
|
|
426
|
+
"transaction", "billing", "razorpay", "stripe",
|
|
427
|
+
"paypal", "cashfree"],
|
|
407
428
|
"paytm": ["payment gateway", "payment processing", "payment",
|
|
408
|
-
"upi", "transaction"
|
|
429
|
+
"upi", "transaction", "razorpay", "phonepe",
|
|
430
|
+
"stripe", "google pay"],
|
|
409
431
|
"paypal": ["payment gateway", "payment processing", "payment",
|
|
410
|
-
"transaction", "billing"
|
|
432
|
+
"transaction", "billing", "stripe", "razorpay",
|
|
433
|
+
"braintree", "adyen"],
|
|
411
434
|
"stripe": ["payment gateway", "payment processing", "payment",
|
|
412
|
-
"transaction", "billing"
|
|
435
|
+
"transaction", "billing", "razorpay", "paypal",
|
|
436
|
+
"braintree", "adyen", "square"],
|
|
413
437
|
"square": ["payment gateway", "payment processing", "payment",
|
|
414
|
-
"transaction", "billing"],
|
|
438
|
+
"transaction", "billing", "stripe", "paypal"],
|
|
415
439
|
"adyen": ["payment gateway", "payment processing", "payment",
|
|
416
|
-
"transaction", "billing"
|
|
440
|
+
"transaction", "billing", "stripe", "paypal",
|
|
441
|
+
"braintree"],
|
|
417
442
|
"braintree": ["payment gateway", "payment processing", "payment",
|
|
418
|
-
"transaction", "billing"
|
|
443
|
+
"transaction", "billing", "stripe", "paypal",
|
|
444
|
+
"adyen"],
|
|
445
|
+
"cashfree": ["payment gateway", "payment processing", "payment",
|
|
446
|
+
"transaction", "billing", "razorpay", "stripe",
|
|
447
|
+
"ccavenue", "paytm"],
|
|
448
|
+
"google pay": ["payment gateway", "payment processing", "payment",
|
|
449
|
+
"upi", "transaction", "phonepe", "paytm",
|
|
450
|
+
"razorpay", "gpay"],
|
|
451
|
+
"gpay": ["payment gateway", "payment processing", "payment",
|
|
452
|
+
"upi", "transaction", "google pay", "phonepe",
|
|
453
|
+
"paytm", "razorpay"],
|
|
454
|
+
"juspay": ["payment gateway", "payment processing", "payment",
|
|
455
|
+
"transaction", "razorpay", "stripe", "cashfree"],
|
|
456
|
+
"billdesk": ["payment gateway", "payment processing", "payment",
|
|
457
|
+
"transaction", "billing", "razorpay", "ccavenue"],
|
|
458
|
+
"instamojo": ["payment gateway", "payment processing", "payment",
|
|
459
|
+
"transaction", "billing", "razorpay", "cashfree"],
|
|
419
460
|
"upi": ["payment gateway", "payment processing", "phonepe",
|
|
420
|
-
"paytm", "
|
|
461
|
+
"paytm", "google pay", "razorpay",
|
|
462
|
+
"transaction", "payment"],
|
|
421
463
|
|
|
422
464
|
// Logistics / Supply Chain
|
|
423
465
|
"shipment": ["cargo", "freight", "consignment", "delivery", "package",
|
|
@@ -475,6 +517,30 @@ export const CONCEPT_MAP = {
|
|
|
475
517
|
"product": ["item", "sku", "catalog", "merchandise", "product listing"],
|
|
476
518
|
"price": ["pricing", "cost", "amount", "rate", "charge"],
|
|
477
519
|
|
|
520
|
+
// Database technologies (brand names → database concept)
|
|
521
|
+
"postgresql": ["database", "db", "sql", "postgres", "mysql",
|
|
522
|
+
"mongodb", "firebase", "supabase"],
|
|
523
|
+
"postgres": ["database", "db", "sql", "postgresql", "mysql",
|
|
524
|
+
"mongodb", "firebase", "supabase"],
|
|
525
|
+
"mysql": ["database", "db", "sql", "postgresql", "mongodb",
|
|
526
|
+
"firebase", "supabase", "mariadb"],
|
|
527
|
+
"mongodb": ["database", "db", "nosql", "mongo", "postgresql",
|
|
528
|
+
"firebase", "supabase", "dynamodb"],
|
|
529
|
+
"mongo": ["database", "db", "nosql", "mongodb", "postgresql",
|
|
530
|
+
"firebase", "supabase"],
|
|
531
|
+
"firebase": ["database", "db", "nosql", "firestore", "supabase",
|
|
532
|
+
"postgresql", "mongodb", "backend"],
|
|
533
|
+
"firestore": ["database", "db", "nosql", "firebase", "mongodb",
|
|
534
|
+
"supabase", "dynamodb"],
|
|
535
|
+
"supabase": ["database", "db", "postgresql", "firebase",
|
|
536
|
+
"mongodb", "backend", "auth"],
|
|
537
|
+
"dynamodb": ["database", "db", "nosql", "mongodb", "firebase",
|
|
538
|
+
"cassandra"],
|
|
539
|
+
"redis": ["database", "db", "cache", "nosql", "datastore"],
|
|
540
|
+
"sqlite": ["database", "db", "sql", "embedded database"],
|
|
541
|
+
"mariadb": ["database", "db", "sql", "mysql", "postgresql"],
|
|
542
|
+
"cassandra": ["database", "db", "nosql", "dynamodb", "mongodb"],
|
|
543
|
+
|
|
478
544
|
// Audit/logging
|
|
479
545
|
"audit logging": ["audit log", "audit trail", "logging", "monitoring"],
|
|
480
546
|
"audit log": ["audit logging", "audit trail", "logging"],
|
|
@@ -499,17 +565,27 @@ export const CONCEPT_MAP = {
|
|
|
499
565
|
"network isolation", "segmentation"],
|
|
500
566
|
"network isolation": ["network segments", "segmentation", "firewall", "air gap"],
|
|
501
567
|
|
|
502
|
-
// User data
|
|
568
|
+
// User data / PII
|
|
503
569
|
"pii": ["personal data", "user data", "personally identifiable information",
|
|
504
|
-
"user information", "gdpr"
|
|
505
|
-
|
|
570
|
+
"user information", "gdpr", "ssn", "social security",
|
|
571
|
+
"email address", "phone number"],
|
|
572
|
+
"personal data": ["pii", "user data", "user information", "gdpr", "data protection",
|
|
573
|
+
"ssn", "social security", "email address"],
|
|
506
574
|
"gdpr": ["data protection", "consent", "privacy", "personal data", "pii",
|
|
507
575
|
"data subject", "right to erasure", "user data"],
|
|
508
576
|
"data protection": ["gdpr", "privacy", "consent", "personal data", "pii",
|
|
509
577
|
"data subject", "compliance"],
|
|
510
578
|
"consent": ["gdpr", "data protection", "opt-in", "opt-out", "user consent",
|
|
511
579
|
"privacy", "data subject"],
|
|
512
|
-
"user data": ["pii", "personal data", "user information", "user records"
|
|
580
|
+
"user data": ["pii", "personal data", "user information", "user records",
|
|
581
|
+
"ssn", "email address"],
|
|
582
|
+
"ssn": ["social security number", "social security", "pii",
|
|
583
|
+
"personal data", "user data", "national id"],
|
|
584
|
+
"social security": ["ssn", "social security number", "pii", "personal data"],
|
|
585
|
+
"social security number": ["ssn", "social security", "pii", "personal data"],
|
|
586
|
+
"email address": ["pii", "user data", "personal data", "contact information"],
|
|
587
|
+
"email addresses": ["pii", "user data", "personal data", "email address"],
|
|
588
|
+
"phone number": ["pii", "user data", "personal data", "contact information"],
|
|
513
589
|
|
|
514
590
|
// Encryption
|
|
515
591
|
"cryptographic signatures": ["code signing", "digital signatures",
|
|
@@ -1088,7 +1164,7 @@ function extractProhibitedVerb(lockText) {
|
|
|
1088
1164
|
const NEUTRAL_ACTION_VERBS = [
|
|
1089
1165
|
"modify", "change", "alter", "reconfigure", "rework",
|
|
1090
1166
|
"overhaul", "restructure", "refactor", "redesign",
|
|
1091
|
-
"replace", "swap", "switch", "migrate", "substitute",
|
|
1167
|
+
"replace", "swap", "switch", "migrate", "transition", "substitute",
|
|
1092
1168
|
"touch", "mess", "configure", "optimize", "tweak",
|
|
1093
1169
|
"extend", "shorten", "adjust", "customize", "personalize",
|
|
1094
1170
|
];
|
|
@@ -1429,6 +1505,19 @@ function _compareSubjectsInline(actionText, lockText) {
|
|
|
1429
1505
|
if (as.includes(" ") && ls.includes(" ")) strongMatchCount++;
|
|
1430
1506
|
continue;
|
|
1431
1507
|
}
|
|
1508
|
+
// Synonym group match — same category items (e.g., Razorpay ↔ Stripe)
|
|
1509
|
+
// Always STRONG because being in the same synonym group means same domain scope.
|
|
1510
|
+
let isSynonym = false;
|
|
1511
|
+
for (const group of SYNONYM_GROUPS) {
|
|
1512
|
+
if (group.includes(as) && group.includes(ls)) {
|
|
1513
|
+
matched.push(`synonym: ${as} ↔ ${ls}`);
|
|
1514
|
+
strongMatchCount++;
|
|
1515
|
+
isSynonym = true;
|
|
1516
|
+
break;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (isSynonym) continue;
|
|
1520
|
+
|
|
1432
1521
|
// Concept-expanded match — only STRONG if BOTH sides are multi-word phrases
|
|
1433
1522
|
// Single-word concept matches (account~ledger, device~iot) are too ambiguous
|
|
1434
1523
|
// to be considered strong scope overlap.
|
|
@@ -1559,7 +1648,21 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1559
1648
|
}
|
|
1560
1649
|
}
|
|
1561
1650
|
|
|
1562
|
-
// 4b.
|
|
1651
|
+
// 4b. Split-phrase euphemisms — "make X public", "make X visible", etc.
|
|
1652
|
+
// These have intervening words between the verb and the key modifier.
|
|
1653
|
+
const SPLIT_PHRASE_PATTERNS = [
|
|
1654
|
+
[/\bmake\s+\w+\s+public\b/i, "expose", "make ... public"],
|
|
1655
|
+
[/\bmake\s+\w+\s+visible\b/i, "expose", "make ... visible"],
|
|
1656
|
+
[/\bmake\s+\w+\s+accessible\b/i, "expose", "make ... accessible"],
|
|
1657
|
+
[/\bmake\s+\w+\s+(?:data\s+)?public\b/i, "expose", "make ... public"],
|
|
1658
|
+
];
|
|
1659
|
+
for (const [pattern, meaning, label] of SPLIT_PHRASE_PATTERNS) {
|
|
1660
|
+
if (pattern.test(actionText) && lockExpanded.expanded.includes(meaning)) {
|
|
1661
|
+
euphemismMatches.push(`"${label}" (euphemism for ${meaning})`);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// 4c. Destructive method verbs — "by replacing", "through overwriting", "via deleting"
|
|
1563
1666
|
// When an action uses a positive primary verb but employs a destructive method,
|
|
1564
1667
|
// the method verb is the real operation. "Optimize X by replacing Y" = replacement.
|
|
1565
1668
|
const DESTRUCTIVE_METHODS = new Set([
|
|
@@ -1699,9 +1802,45 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1699
1802
|
|
|
1700
1803
|
// Apply the subject relevance gate based on match quality
|
|
1701
1804
|
if (!hasSubjectMatch && (synonymMatches.length > 0 || euphemismMatches.length > 0)) {
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1805
|
+
// Exception: if the action's euphemism DIRECTLY matches the lock's prohibited
|
|
1806
|
+
// verb AND there's at least some shared content word, skip the subject gate.
|
|
1807
|
+
// "Make the data public" euphemism = "expose", lock = "Never expose user data"
|
|
1808
|
+
// → euphemism proves the conflict + "data" provides content overlap.
|
|
1809
|
+
// But "Tax statement export" vs "Never expose portfolio positions" has no
|
|
1810
|
+
// content overlap — gate should still fire to prevent false positive.
|
|
1811
|
+
// Note: we check raw word overlap (ignoring stopwords filter) because common
|
|
1812
|
+
// words like "data" are stopwords but still provide content signal.
|
|
1813
|
+
const _prohibVerb = extractProhibitedVerb(lockText);
|
|
1814
|
+
const _GATE_SKIP_STOPWORDS = new Set([
|
|
1815
|
+
"a", "an", "the", "this", "that", "it", "its", "our", "their",
|
|
1816
|
+
"your", "my", "his", "her", "we", "they", "them", "i",
|
|
1817
|
+
"to", "of", "in", "on", "at", "by", "up", "as", "or", "and",
|
|
1818
|
+
"nor", "but", "so", "if", "no", "not", "is", "be", "do", "did",
|
|
1819
|
+
"with", "from", "for", "into", "over", "under", "between", "through",
|
|
1820
|
+
"about", "before", "after", "during", "while",
|
|
1821
|
+
"are", "was", "were", "been", "being", "have", "has", "had",
|
|
1822
|
+
"will", "would", "could", "should", "may", "might", "shall",
|
|
1823
|
+
"can", "need", "must", "does", "done",
|
|
1824
|
+
"all", "any", "every", "some", "most", "other", "each", "both",
|
|
1825
|
+
"few", "more", "less", "many", "much",
|
|
1826
|
+
"also", "just", "very", "too", "really", "quite", "only", "then",
|
|
1827
|
+
"now", "here", "there", "when", "where", "how", "what", "which",
|
|
1828
|
+
"who", "whom", "why",
|
|
1829
|
+
// Common verbs/adjectives (but NOT nouns like "data", "system")
|
|
1830
|
+
"way", "thing", "things", "part", "set", "use",
|
|
1831
|
+
"using", "used", "make", "made", "new", "get", "got",
|
|
1832
|
+
]);
|
|
1833
|
+
const rawWordOverlap = actionTokens.words.some(w =>
|
|
1834
|
+
lockTokens.words.includes(w) && !_GATE_SKIP_STOPWORDS.has(w));
|
|
1835
|
+
const euphemismMatchesProhibitedVerb = _prohibVerb &&
|
|
1836
|
+
rawWordOverlap &&
|
|
1837
|
+
euphemismMatches.some(m => m.includes(`euphemism for ${_prohibVerb}`));
|
|
1838
|
+
|
|
1839
|
+
if (!euphemismMatchesProhibitedVerb) {
|
|
1840
|
+
// NO subject match at all — verb-only match → heavy reduction
|
|
1841
|
+
score = Math.floor(score * 0.15);
|
|
1842
|
+
reasons.push("subject gate: no subject overlap — verb-only match, likely false positive");
|
|
1843
|
+
}
|
|
1705
1844
|
} else if (hasVocabSubjectMatch && !hasScopeMatch && subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
|
|
1706
1845
|
// Vocabulary overlap exists but subjects point to DIFFERENT scopes
|
|
1707
1846
|
score = Math.floor(score * 0.35);
|
|
@@ -1825,10 +1964,34 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1825
1964
|
}
|
|
1826
1965
|
}
|
|
1827
1966
|
|
|
1967
|
+
// Check 3b: Safe/verification verbs against preservation/maintenance locks
|
|
1968
|
+
// "Test that Stripe is working" is COMPLIANT with "must always use Stripe"
|
|
1969
|
+
// "Debug the Stripe webhook" is COMPLIANT — it's verifying the preserved system
|
|
1970
|
+
if (!intentAligned && actionPrimaryVerb) {
|
|
1971
|
+
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/i.test(lockText);
|
|
1972
|
+
|
|
1973
|
+
if (lockIsPreservation) {
|
|
1974
|
+
const SAFE_FOR_PRESERVATION = new Set([
|
|
1975
|
+
"test", "verify", "check", "validate", "confirm", "ensure",
|
|
1976
|
+
"debug", "inspect", "review", "examine", "monitor", "observe",
|
|
1977
|
+
"watch", "scan", "detect", "audit", "report", "document",
|
|
1978
|
+
"read", "view", "generate", "fix", "repair", "patch",
|
|
1979
|
+
"protect", "secure", "guard", "maintain", "preserve",
|
|
1980
|
+
]);
|
|
1981
|
+
if (SAFE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
|
|
1982
|
+
intentAligned = true;
|
|
1983
|
+
reasons.push(
|
|
1984
|
+
`intent alignment: verification/maintenance "${actionPrimaryVerb}" is ` +
|
|
1985
|
+
`compliant with preservation lock`);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1828
1990
|
// Check 4: Enhancement/constructive actions against preservation/maintenance locks
|
|
1829
1991
|
// "Increase the rate limit" is COMPLIANT with "rate limiting must remain active"
|
|
1830
1992
|
// "Add better rate limit error messages" is COMPLIANT (doesn't disable rate limiting)
|
|
1831
1993
|
// But "Add a way to bypass rate limiting" is NOT safe (contains negative op "bypass")
|
|
1994
|
+
// And "Add Razorpay" vs "must always use Stripe" is NOT safe (competing alternative)
|
|
1832
1995
|
if (!intentAligned && actionPrimaryVerb) {
|
|
1833
1996
|
const ENHANCEMENT_VERBS = new Set([
|
|
1834
1997
|
"increase", "improve", "enhance", "boost", "strengthen",
|
|
@@ -1847,15 +2010,23 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1847
2010
|
`intent alignment: enhancement action "${actionPrimaryVerb}" is ` +
|
|
1848
2011
|
`compliant with preservation lock`);
|
|
1849
2012
|
} else if (CONSTRUCTIVE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
|
|
1850
|
-
// Constructive verbs align ONLY if
|
|
2013
|
+
// Constructive verbs align ONLY if:
|
|
2014
|
+
// 1. No negative operations in the action
|
|
2015
|
+
// 2. The action doesn't introduce a COMPETING alternative
|
|
2016
|
+
// "Add Razorpay" vs "must always use Stripe" → competitor (same synonym group)
|
|
2017
|
+
// "Add dark mode" vs "must always use Stripe" → unrelated (safe)
|
|
1851
2018
|
const actionLower = actionText.toLowerCase();
|
|
1852
2019
|
const hasNegativeOp = NEGATIVE_INTENT_MARKERS.some(m =>
|
|
1853
2020
|
new RegExp(`\\b${escapeRegex(m)}\\b`, "i").test(actionLower));
|
|
1854
|
-
if
|
|
2021
|
+
// Check if action introduces a competing product/brand from the same category
|
|
2022
|
+
const hasCompetitorMatch = subjectComparison.matchedSubjects.some(m =>
|
|
2023
|
+
typeof m === "string" && m.startsWith("synonym:")
|
|
2024
|
+
);
|
|
2025
|
+
if (!hasNegativeOp && !hasCompetitorMatch) {
|
|
1855
2026
|
intentAligned = true;
|
|
1856
2027
|
reasons.push(
|
|
1857
2028
|
`intent alignment: constructive "${actionPrimaryVerb}" is ` +
|
|
1858
|
-
`compliant with preservation lock (no negative operations)`);
|
|
2029
|
+
`compliant with preservation lock (no negative operations, no competitor)`);
|
|
1859
2030
|
}
|
|
1860
2031
|
}
|
|
1861
2032
|
}
|
|
@@ -1906,11 +2077,11 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1906
2077
|
"font", "fonts", "color", "colors", "colour", "theme", "themes",
|
|
1907
2078
|
"styling", "style", "styles", "css", "icon", "icons", "layout",
|
|
1908
2079
|
"margin", "padding", "border", "background", "typography", "spacing",
|
|
1909
|
-
"alignment", "animation", "
|
|
1910
|
-
"placeholder", "logo", "
|
|
2080
|
+
"alignment", "animation", "hover", "tooltip",
|
|
2081
|
+
"placeholder", "logo", "banner", "hero", "avatar",
|
|
1911
2082
|
"sidebar", "navigation", "menu", "breadcrumb", "footer",
|
|
1912
2083
|
]);
|
|
1913
|
-
if (!intentAligned && !
|
|
2084
|
+
if (!intentAligned && !hasStrongVocabMatch) {
|
|
1914
2085
|
const actionLower = actionText.toLowerCase();
|
|
1915
2086
|
const actionWords = actionLower.split(/\s+/).map(w => w.replace(/[^a-z]/g, ""));
|
|
1916
2087
|
const hasUISubject = actionWords.some(w => UI_COSMETIC_WORDS.has(w));
|
|
@@ -1983,7 +2154,60 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
1983
2154
|
// MAIN ENTRY POINT
|
|
1984
2155
|
// ===================================================================
|
|
1985
2156
|
|
|
2157
|
+
// Question framing prefixes that should be stripped before analysis.
|
|
2158
|
+
// "Should we add Razorpay?" → "add Razorpay"
|
|
2159
|
+
// "What if we used Firebase?" → "used Firebase"
|
|
2160
|
+
const QUESTION_PREFIXES = [
|
|
2161
|
+
/^would\s+it\s+make\s+sense\s+to\s+/i,
|
|
2162
|
+
/^would\s+it\s+be\s+(?:better|good|wise|smart|possible)\s+(?:to|if)\s+(?:we\s+)?/i,
|
|
2163
|
+
/^what\s+if\s+we\s+(?:could\s+)?/i,
|
|
2164
|
+
/^what\s+about\s+/i,
|
|
2165
|
+
/^how\s+about\s+(?:we\s+)?/i,
|
|
2166
|
+
/^should\s+we\s+(?:consider\s+)?/i,
|
|
2167
|
+
/^could\s+we\s+(?:possibly\s+)?/i,
|
|
2168
|
+
/^can\s+we\s+/i,
|
|
2169
|
+
/^i\s+was\s+wondering\s+if\s+(?:we\s+)?(?:could\s+)?/i,
|
|
2170
|
+
/^maybe\s+we\s+(?:should\s+)?(?:consider\s+)?/i,
|
|
2171
|
+
/^perhaps\s+we\s+(?:should\s+)?(?:consider\s+)?/i,
|
|
2172
|
+
/^wouldn't\s+it\s+be\s+(?:better|good)\s+(?:to|if)\s+(?:we\s+)?/i,
|
|
2173
|
+
/^is\s+(?:it\s+)?(?:a\s+)?(?:good\s+idea\s+)?(?:to\s+)?/i,
|
|
2174
|
+
/^let\s+me\s+/i,
|
|
2175
|
+
/^we\s+should\s+(?:probably\s+)?(?:consider\s+)?(?:look\s+at\s+)?/i,
|
|
2176
|
+
/^explore\s+(?:using\s+)?/i,
|
|
2177
|
+
];
|
|
2178
|
+
|
|
2179
|
+
// Special transformations where simple prefix stripping loses the subject.
|
|
2180
|
+
// "Would Firebase be better for real-time sync?" → "switch to Firebase for real-time sync"
|
|
2181
|
+
const QUESTION_TRANSFORMS = [
|
|
2182
|
+
[/^would\s+(.+?)\s+be\s+(?:a\s+)?better\s+(?:option\s+)?(?:for|than)\s+(.+)/i, "switch to $1 for $2"],
|
|
2183
|
+
[/^is\s+(.+?)\s+(?:a\s+)?better\s+(?:option|choice|alternative)\s+(?:for|than)\s+(.+)/i, "switch to $1 for $2"],
|
|
2184
|
+
[/^wouldn't\s+(.+?)\s+be\s+(?:a\s+)?better\s+(?:option|choice)?\s*(?:for|than)?\s*(.+)?/i, "switch to $1 $2"],
|
|
2185
|
+
];
|
|
2186
|
+
|
|
2187
|
+
function stripQuestionFraming(text) {
|
|
2188
|
+
let stripped = text;
|
|
2189
|
+
|
|
2190
|
+
// Try special transformations first (they preserve the subject)
|
|
2191
|
+
for (const [pattern, replacement] of QUESTION_TRANSFORMS) {
|
|
2192
|
+
if (pattern.test(stripped)) {
|
|
2193
|
+
stripped = stripped.replace(pattern, replacement).trim();
|
|
2194
|
+
stripped = stripped.replace(/\?\s*$/, "").trim();
|
|
2195
|
+
return stripped || text;
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2199
|
+
// Then try simple prefix stripping
|
|
2200
|
+
for (const pattern of QUESTION_PREFIXES) {
|
|
2201
|
+
stripped = stripped.replace(pattern, "");
|
|
2202
|
+
}
|
|
2203
|
+
// Also remove trailing question marks
|
|
2204
|
+
stripped = stripped.replace(/\?\s*$/, "").trim();
|
|
2205
|
+
return stripped || text; // fallback to original if everything was stripped
|
|
2206
|
+
}
|
|
2207
|
+
|
|
1986
2208
|
export function analyzeConflict(actionText, lockText) {
|
|
2209
|
+
// Strip question framing so "Should we add Razorpay?" → "add Razorpay"
|
|
2210
|
+
actionText = stripQuestionFraming(actionText);
|
|
1987
2211
|
const clauses = splitClauses(actionText);
|
|
1988
2212
|
|
|
1989
2213
|
const clauseResults = clauses.map(clause => ({
|
package/src/core/telemetry.js
CHANGED
|
@@ -257,7 +257,7 @@ export async function flushToRemote(root) {
|
|
|
257
257
|
// Build anonymized payload
|
|
258
258
|
const payload = {
|
|
259
259
|
instanceId: summary.instanceId,
|
|
260
|
-
version: "4.
|
|
260
|
+
version: "4.5.1",
|
|
261
261
|
totalCalls: summary.totalCalls,
|
|
262
262
|
avgResponseMs: summary.avgResponseMs,
|
|
263
263
|
conflicts: summary.conflicts,
|
package/src/dashboard/index.html
CHANGED
|
@@ -89,7 +89,7 @@
|
|
|
89
89
|
<div class="header">
|
|
90
90
|
<div>
|
|
91
91
|
<h1><span>SpecLock</span> Dashboard</h1>
|
|
92
|
-
<div class="meta">v4.
|
|
92
|
+
<div class="meta">v4.5.1 — AI Constraint Engine</div>
|
|
93
93
|
</div>
|
|
94
94
|
<div style="display:flex;align-items:center;gap:12px;">
|
|
95
95
|
<span id="health-badge" class="status-badge healthy">Loading...</span>
|
|
@@ -182,7 +182,7 @@
|
|
|
182
182
|
</div>
|
|
183
183
|
|
|
184
184
|
<div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
|
|
185
|
-
SpecLock v4.
|
|
185
|
+
SpecLock v4.5.1 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|
package/src/mcp/http-server.js
CHANGED
|
@@ -91,7 +91,7 @@ import { fileURLToPath } from "url";
|
|
|
91
91
|
import _path from "path";
|
|
92
92
|
|
|
93
93
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
94
|
-
const VERSION = "4.
|
|
94
|
+
const VERSION = "4.5.1";
|
|
95
95
|
const AUTHOR = "Sandeep Roy";
|
|
96
96
|
const START_TIME = Date.now();
|
|
97
97
|
|
package/src/mcp/server.js
CHANGED
|
@@ -100,7 +100,7 @@ const PROJECT_ROOT =
|
|
|
100
100
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
101
101
|
|
|
102
102
|
// --- MCP Server ---
|
|
103
|
-
const VERSION = "4.
|
|
103
|
+
const VERSION = "4.5.1";
|
|
104
104
|
const AUTHOR = "Sandeep Roy";
|
|
105
105
|
|
|
106
106
|
const server = new McpServer(
|