speclock 4.5.1 → 4.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -457,4 +457,4 @@ Built by **[Sandeep Roy](https://github.com/sgroy10)**
457
457
 
458
458
  ---
459
459
 
460
- <p align="center"><i>v4.4.260 tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
460
+ <p align="center"><i>v4.5.3600+ tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "4.5.1",
5
+ "version": "4.5.3",
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.5.1 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
120
+ SpecLock v4.5.3 — 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]
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "4.5.1";
12
+ const VERSION = "4.5.3";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -84,32 +84,28 @@ export function addLock(root, text, tags, source) {
84
84
  const brain = ensureInit(root);
85
85
  const lockId = newId("lock");
86
86
 
87
- // Smart Lock Authoring auto-normalize to prevent verb contamination
88
- const normResult = normalizeLock(text);
89
-
87
+ // Store the user's exact words no rewriting.
88
+ // The semantic engine handles verb contamination via subject extraction
89
+ // and scope matching, so rewriting is no longer needed.
90
90
  brain.specLock.items.unshift({
91
91
  id: lockId,
92
- text: normResult.normalized,
93
- originalText: normResult.wasRewritten ? normResult.original : undefined,
92
+ text: text,
94
93
  createdAt: nowIso(),
95
94
  source: source || "user",
96
95
  tags: tags || [],
97
96
  active: true,
98
97
  });
99
98
  const eventId = newId("evt");
100
- const rewriteNote = normResult.wasRewritten
101
- ? ` (auto-rewritten from: "${normResult.original.substring(0, 60)}")`
102
- : "";
103
99
  const event = {
104
100
  eventId,
105
101
  type: "lock_added",
106
102
  at: nowIso(),
107
103
  files: [],
108
- summary: `Lock added: ${normResult.normalized.substring(0, 80)}${rewriteNote}`,
104
+ summary: `Lock added: ${text.substring(0, 80)}`,
109
105
  patchPath: "",
110
106
  };
111
107
  recordEvent(root, brain, event);
112
- return { brain, lockId, rewritten: normResult.wasRewritten, rewriteReason: normResult.reason };
108
+ return { brain, lockId, rewritten: false, rewriteReason: null };
113
109
  }
114
110
 
115
111
  export function removeLock(root, lockId) {
@@ -28,7 +28,7 @@ export const SYNONYM_GROUPS = [
28
28
 
29
29
  // --- Modification actions ---
30
30
  ["change", "modify", "alter", "update", "mutate", "transform",
31
- "rewrite", "revise", "amend", "adjust", "tweak"],
31
+ "rewrite", "revise", "amend", "adjust", "tweak", "touch", "tamper"],
32
32
  ["replace", "swap", "substitute", "switch", "exchange",
33
33
  "override", "overwrite"],
34
34
  ["move", "relocate", "migrate", "transfer", "shift", "rearrange", "reorganize",
@@ -82,6 +82,16 @@ export const SYNONYM_GROUPS = [
82
82
  ["audit", "audit log", "audit trail", "logging", "log",
83
83
  "monitoring", "observability", "telemetry", "tracking"],
84
84
 
85
+ // --- Auth providers ---
86
+ ["auth0", "okta", "cognito", "keycloak", "supabase auth"],
87
+
88
+ // --- API keys & secrets ---
89
+ ["api key", "api keys", "secret key", "secret keys", "publishable key",
90
+ "private key", "access key", "api secret", "api token",
91
+ "credentials", "credential"],
92
+ ["frontend", "frontend code", "client-side", "client side",
93
+ "browser", "react state", "ui component"],
94
+
85
95
  // --- Dependencies ---
86
96
  ["dependency", "package", "library", "module", "import", "require",
87
97
  "vendor", "third-party"],
@@ -619,6 +629,30 @@ export const CONCEPT_MAP = {
619
629
  "saml": ["sso", "oidc", "single sign-on", "authentication",
620
630
  "identity provider"],
621
631
 
632
+ // API keys & secrets
633
+ "api key": ["api keys", "secret key", "api secret", "api token",
634
+ "credential", "publishable key", "access key",
635
+ "secret", "key", "frontend code", "expose"],
636
+ "api keys": ["api key", "secret key", "api secret", "credential",
637
+ "publishable key", "access key", "frontend code", "expose"],
638
+ "secret key": ["api key", "api secret", "credential", "secret",
639
+ "publishable key", "private key"],
640
+ "publishable key": ["api key", "public key", "stripe key", "client key",
641
+ "frontend", "credential"],
642
+ "secret": ["api key", "secret key", "credential", "api secret",
643
+ "private", "sensitive"],
644
+ "localstorage": ["client-side storage", "browser storage", "frontend",
645
+ "expose", "client-side"],
646
+ // Auth providers
647
+ "auth0": ["authentication", "auth", "identity provider", "sso",
648
+ "oauth", "supabase", "cognito", "okta", "keycloak"],
649
+ "cognito": ["authentication", "auth", "identity provider",
650
+ "auth0", "supabase", "okta"],
651
+ "okta": ["authentication", "auth", "identity provider", "sso",
652
+ "auth0", "supabase", "cognito"],
653
+ "keycloak": ["authentication", "auth", "identity provider", "sso",
654
+ "auth0", "supabase"],
655
+
622
656
  // Networking
623
657
  "websocket": ["socket", "real-time connection", "ws", "wss",
624
658
  "socket connection"],
@@ -847,7 +881,16 @@ function buildKnownPhrases() {
847
881
  const KNOWN_PHRASES = buildKnownPhrases();
848
882
 
849
883
  export function tokenize(text) {
850
- const lower = text.toLowerCase();
884
+ // Pre-process: split UPPER_CASE_ENV_VARS into component words
885
+ // "STRIPE_SECRET_KEY" → "STRIPE SECRET KEY"
886
+ // "process.env.STRIPE_KEY" → "process env STRIPE KEY"
887
+ let preprocessed = text.replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, match =>
888
+ match.replace(/_/g, " ")
889
+ );
890
+ // Also handle process.env.X patterns
891
+ preprocessed = preprocessed.replace(/process\.env\./gi, "env ");
892
+
893
+ const lower = preprocessed.toLowerCase();
851
894
  const phrases = [];
852
895
 
853
896
  // Extract known multi-word phrases (greedy, longest first)
@@ -1167,6 +1210,7 @@ const NEUTRAL_ACTION_VERBS = [
1167
1210
  "replace", "swap", "switch", "migrate", "transition", "substitute",
1168
1211
  "touch", "mess", "configure", "optimize", "tweak",
1169
1212
  "extend", "shorten", "adjust", "customize", "personalize",
1213
+ "update", "rewrite",
1170
1214
  ];
1171
1215
 
1172
1216
  function extractPrimaryVerb(actionText) {
@@ -1372,6 +1416,13 @@ function _extractSubjectsInline(text) {
1372
1416
  content = content.replace(/\s+must\s+(?:be\s+)?(?:preserved|remain)\b.*$/i, "").trim();
1373
1417
  content = content.replace(/\s*[—–]\s+(?:prohibited|no\s+|must\s+not|deletion|do\s+not|migration)\b.*$/i, "").trim();
1374
1418
 
1419
+ // Strip comma-separated explanatory clauses
1420
+ // "KYC verification flow, it's SEC-compliant" → "KYC verification flow"
1421
+ // "patient records, which are HIPAA-protected" → "patient records"
1422
+ // "the auth system, because it's production-critical" → "the auth system"
1423
+ content = content.replace(/,\s+(?:it|they|that|this|which|who)\s*(?:'s|'re|is|are|was|were|has|have|had)\b.*$/i, "").trim();
1424
+ content = content.replace(/,\s+(?:because|since|as|for|due\s+to|given\s+that)\b.*$/i, "").trim();
1425
+
1375
1426
  // Strip leading verb
1376
1427
  const words = content.split(/\s+/);
1377
1428
  let startIdx = 0;
@@ -1598,6 +1649,7 @@ export function scoreConflict({ actionText, lockText }) {
1598
1649
 
1599
1650
  let score = 0;
1600
1651
  const reasons = [];
1652
+ let hasSecurityViolationPattern = false; // Set when credential-exposure detected
1601
1653
 
1602
1654
  // 1. Direct word overlap (minus stopwords)
1603
1655
  const directOverlap = actionTokens.words.filter(w =>
@@ -1899,6 +1951,24 @@ export function scoreConflict({ actionText, lockText }) {
1899
1951
  }
1900
1952
  }
1901
1953
 
1954
+ // Special case: "add/put/store/embed key/secret/credential in/to frontend/component/state"
1955
+ // is SEMANTICALLY EQUIVALENT to "expose key in frontend" — NOT an opposite action.
1956
+ // Placing credentials in client-side code IS exposing them.
1957
+ if (!actionPerformsProhibitedOp && (prohibitedVerb === "expose" || prohibitedVerb === "leak" || prohibitedVerb === "reveal")) {
1958
+ // Pre-process env vars: STRIPE_SECRET_KEY → stripe secret key
1959
+ const actionNorm = actionText
1960
+ .replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, m => m.replace(/_/g, " "))
1961
+ .toLowerCase();
1962
+ const hasCredentialWord = /\b(key|keys|secret|secrets|credential|credentials|token|api.?key|password|cert)\b/i.test(actionNorm);
1963
+ const hasFrontendLocation = /\b(frontend|front.?end|client|component|state|localstorage|session.?storage|browser|ui|react|vue|angular|svelte|html|template)\b/i.test(actionNorm);
1964
+ const hasPlacementVerb = /\b(add|put|store|embed|include|place|insert|set|hardcode|inline)\b/i.test(actionNorm);
1965
+ if (hasCredentialWord && hasFrontendLocation && hasPlacementVerb) {
1966
+ actionPerformsProhibitedOp = true;
1967
+ hasSecurityViolationPattern = true;
1968
+ reasons.push("security: placing credentials in client-side code is equivalent to exposing them");
1969
+ }
1970
+ }
1971
+
1902
1972
  if (prohibitedIsNegative && !actionIntent.negated &&
1903
1973
  !hasDestructiveLanguageMatch && !actionPerformsProhibitedOp) {
1904
1974
  intentAligned = true;
@@ -1967,10 +2037,10 @@ export function scoreConflict({ actionText, lockText }) {
1967
2037
  // Check 3b: Safe/verification verbs against preservation/maintenance locks
1968
2038
  // "Test that Stripe is working" is COMPLIANT with "must always use Stripe"
1969
2039
  // "Debug the Stripe webhook" is COMPLIANT — it's verifying the preserved system
1970
- if (!intentAligned && actionPrimaryVerb) {
2040
+ {
1971
2041
  const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/i.test(lockText);
1972
2042
 
1973
- if (lockIsPreservation) {
2043
+ if (!intentAligned && lockIsPreservation) {
1974
2044
  const SAFE_FOR_PRESERVATION = new Set([
1975
2045
  "test", "verify", "check", "validate", "confirm", "ensure",
1976
2046
  "debug", "inspect", "review", "examine", "monitor", "observe",
@@ -1978,12 +2048,76 @@ export function scoreConflict({ actionText, lockText }) {
1978
2048
  "read", "view", "generate", "fix", "repair", "patch",
1979
2049
  "protect", "secure", "guard", "maintain", "preserve",
1980
2050
  ]);
1981
- if (SAFE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
2051
+ if (actionPrimaryVerb && SAFE_FOR_PRESERVATION.has(actionPrimaryVerb)) {
1982
2052
  intentAligned = true;
1983
2053
  reasons.push(
1984
2054
  `intent alignment: verification/maintenance "${actionPrimaryVerb}" is ` +
1985
2055
  `compliant with preservation lock`);
1986
2056
  }
2057
+ // "Write tests for X" — the verb is "write" but the intent is testing
2058
+ // Uses raw text match since "write" may not be in NEUTRAL_ACTION_VERBS
2059
+ if (!intentAligned && /\bwrite\s+tests?\b/i.test(actionText)) {
2060
+ intentAligned = true;
2061
+ reasons.push(
2062
+ `intent alignment: "write tests" is a testing/verification action — ` +
2063
+ `compliant with preservation lock`);
2064
+ }
2065
+ }
2066
+ }
2067
+
2068
+ // Check 3c: Working WITH locked technology (not replacing it)
2069
+ // "Update the Stripe UI components" vs "must always use Stripe" → working WITH Stripe → safe
2070
+ // "Optimize Supabase queries" vs "Supabase Auth lock" → improving existing Supabase → safe
2071
+ // "Write tests for Supabase queries" vs "Supabase lock" → testing existing tech → safe
2072
+ // But: "Update payment to use Razorpay" vs "Stripe lock" → introducing competitor → NOT safe
2073
+ if (!intentAligned && actionPrimaryVerb) {
2074
+ const lockIsPreservationOrFreeze = /must remain|must be preserved|must always|at all times|must stay|do not replace|do not remove|do not switch|don't replace|don't remove|don't switch|uses .+ library/i.test(lockText);
2075
+ if (lockIsPreservationOrFreeze) {
2076
+ // Extract specific brand/tech names from the lock text
2077
+ const lockWords = lockText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
2078
+ const actionWords = actionText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
2079
+
2080
+ // Find brand/technology names that appear in BOTH action and lock
2081
+ // These are specific nouns (not verbs, not stopwords) that identify the technology
2082
+ const TECH_BRANDS = new Set([
2083
+ "stripe", "razorpay", "paypal", "phonepe", "paytm", "ccavenue", "cashfree",
2084
+ "braintree", "adyen", "square", "billdesk", "instamojo", "juspay",
2085
+ "postgresql", "postgres", "mysql", "mongodb", "mongo", "firebase",
2086
+ "firestore", "supabase", "dynamodb", "redis", "sqlite", "mariadb",
2087
+ "cassandra", "couchdb", "neo4j",
2088
+ "baileys", "twilio", "whatsapp",
2089
+ "auth0", "okta", "cognito", "keycloak",
2090
+ "react", "vue", "angular", "svelte", "nextjs", "nuxt",
2091
+ "docker", "kubernetes", "terraform", "ansible",
2092
+ "aws", "gcp", "azure", "vercel", "netlify", "railway", "heroku",
2093
+ ]);
2094
+ const sharedBrands = lockWords.filter(w => TECH_BRANDS.has(w) && actionWords.includes(w));
2095
+
2096
+ if (sharedBrands.length > 0) {
2097
+ // Action references the SAME tech as the lock — check if it's working WITH it
2098
+ const hasCompetitorInAction = actionWords.some(w =>
2099
+ TECH_BRANDS.has(w) && !lockWords.includes(w)
2100
+ );
2101
+ const WORKING_WITH_VERBS = new Set([
2102
+ "update", "modify", "change", "refactor", "restructure",
2103
+ "optimize", "improve", "enhance", "write", "rewrite",
2104
+ "style", "format", "clean", "cleanup", "simplify",
2105
+ "test", "verify", "check", "validate", "debug", "fix",
2106
+ "repair", "patch", "maintain", "document", "monitor",
2107
+ "configure", "customize", "extend", "expand",
2108
+ ]);
2109
+ const DESTRUCTIVE_VERBS = new Set([
2110
+ "remove", "delete", "drop", "destroy", "kill", "purge", "wipe",
2111
+ "disable", "deactivate", "replace", "switch", "migrate", "move",
2112
+ ]);
2113
+ if (WORKING_WITH_VERBS.has(actionPrimaryVerb) && !hasCompetitorInAction &&
2114
+ !DESTRUCTIVE_VERBS.has(actionPrimaryVerb)) {
2115
+ intentAligned = true;
2116
+ reasons.push(
2117
+ `intent alignment: "${actionPrimaryVerb}" works WITH locked tech ` +
2118
+ `${sharedBrands.join(", ")} — not replacing it`);
2119
+ }
2120
+ }
1987
2121
  }
1988
2122
  }
1989
2123
 
@@ -1995,7 +2129,7 @@ export function scoreConflict({ actionText, lockText }) {
1995
2129
  if (!intentAligned && actionPrimaryVerb) {
1996
2130
  const ENHANCEMENT_VERBS = new Set([
1997
2131
  "increase", "improve", "enhance", "boost", "strengthen",
1998
- "upgrade", "raise", "expand", "extend", "grow",
2132
+ "upgrade", "raise", "expand", "extend", "grow", "optimize",
1999
2133
  ]);
2000
2134
  const CONSTRUCTIVE_FOR_PRESERVATION = new Set([
2001
2135
  "build", "add", "create", "implement", "make", "design",
@@ -2042,7 +2176,8 @@ export function scoreConflict({ actionText, lockText }) {
2042
2176
  "integrate", "include", "support",
2043
2177
  ]);
2044
2178
  // 5a: Weak scope overlap (single shared word, no strong vocab match)
2045
- if (!intentAligned && hasScopeMatch && !hasStrongScopeMatch && !hasStrongVocabMatch) {
2179
+ // Skip if a security violation pattern was detected (credential exposure)
2180
+ if (!intentAligned && !hasSecurityViolationPattern && hasScopeMatch && !hasStrongScopeMatch && !hasStrongVocabMatch) {
2046
2181
  if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
2047
2182
  // Guard: if the shared word is a long, specific entity name (10+ chars),
2048
2183
  // it strongly identifies the target system — not an incidental overlap.
@@ -2059,7 +2194,7 @@ export function scoreConflict({ actionText, lockText }) {
2059
2194
  }
2060
2195
  // 5b: Vocab overlap but NO scope overlap (subjects point to different things)
2061
2196
  // Even weaker signal — shared vocabulary but different actual components.
2062
- if (!intentAligned && hasVocabSubjectMatch && !hasScopeMatch &&
2197
+ if (!intentAligned && !hasSecurityViolationPattern && hasVocabSubjectMatch && !hasScopeMatch &&
2063
2198
  subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
2064
2199
  if (FEATURE_BUILDING_VERBS.has(actionPrimaryVerb)) {
2065
2200
  intentAligned = true;
@@ -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.5.1",
260
+ version: "4.5.3",
261
261
  totalCalls: summary.totalCalls,
262
262
  avgResponseMs: summary.avgResponseMs,
263
263
  conflicts: summary.conflicts,
@@ -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.5.1 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v4.5.3 &mdash; 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.5.1 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v4.5.3 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
186
186
  </div>
187
187
 
188
188
  <script>
@@ -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.5.1";
94
+ const VERSION = "4.5.3";
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.5.1";
103
+ const VERSION = "4.5.3";
104
104
  const AUTHOR = "Sandeep Roy";
105
105
 
106
106
  const server = new McpServer(