speclock 4.5.4 → 4.5.6

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
@@ -409,6 +409,21 @@ The AI opens the file and sees:
409
409
 
410
410
  ---
411
411
 
412
+ ## Configuration
413
+
414
+ | Variable | Default | Description |
415
+ |----------|---------|-------------|
416
+ | `SPECLOCK_API_KEY` | — | API key for authenticated access |
417
+ | `SPECLOCK_ENCRYPTION_KEY` | — | Enables AES-256-GCM encryption at rest |
418
+ | `SPECLOCK_NO_PROXY` | `false` | Set `true` for heuristic-only mode (~250ms). Skips the Gemini proxy (~2s) |
419
+ | `SPECLOCK_LLM_KEY` | — | Your own LLM API key (Gemini/OpenAI/Anthropic) |
420
+ | `GEMINI_API_KEY` | — | Google Gemini API key for hybrid conflict detection |
421
+ | `SPECLOCK_TELEMETRY` | `false` | Opt-in anonymous usage analytics |
422
+
423
+ > **Tip:** The heuristic engine alone scores 95%+ accuracy at ~250ms. The Gemini proxy adds cross-domain coverage but takes ~2s. For fastest response, set `SPECLOCK_NO_PROXY=true`.
424
+
425
+ ---
426
+
412
427
  ## Test Results
413
428
 
414
429
  | Suite | Tests | Pass Rate |
@@ -457,4 +472,4 @@ Built by **[Sandeep Roy](https://github.com/sgroy10)**
457
472
 
458
473
  ---
459
474
 
460
- <p align="center"><i>v4.5.4 — 600+ tests, 31 MCP tools, 0 false positives, Gemini hybrid. Because remembering isn't enough.</i></p>
475
+ <p align="center"><i>v4.5.6 — 600+ 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.4",
5
+ "version": "4.5.6",
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.4 — AI Constraint Engine (Gemini LLM + Policy-as-Code + SSO + Dashboard + Telemetry + Auth + RBAC + Encryption)
120
+ SpecLock v4.5.6 — 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.4";
12
+ const VERSION = "4.5.6";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -124,7 +124,7 @@ export const SYNONYM_GROUPS = [
124
124
  "payment service", "payment platform"],
125
125
  ["razorpay", "stripe", "paypal", "phonepe", "paytm", "ccavenue",
126
126
  "cashfree", "braintree", "adyen", "square", "google pay", "gpay",
127
- "juspay", "billdesk", "instamojo"],
127
+ "juspay", "billdesk", "instamojo", "payu"],
128
128
 
129
129
  // --- IoT / firmware ---
130
130
  ["firmware", "firmware update", "ota", "over the air",
@@ -467,6 +467,8 @@ export const CONCEPT_MAP = {
467
467
  "transaction", "billing", "razorpay", "ccavenue"],
468
468
  "instamojo": ["payment gateway", "payment processing", "payment",
469
469
  "transaction", "billing", "razorpay", "cashfree"],
470
+ "payu": ["payment gateway", "payment processing", "payment",
471
+ "transaction", "billing", "razorpay", "stripe", "cashfree"],
470
472
  "upi": ["payment gateway", "payment processing", "phonepe",
471
473
  "paytm", "google pay", "razorpay",
472
474
  "transaction", "payment"],
@@ -1650,6 +1652,7 @@ export function scoreConflict({ actionText, lockText }) {
1650
1652
  let score = 0;
1651
1653
  const reasons = [];
1652
1654
  let hasSecurityViolationPattern = false; // Set when credential-exposure detected
1655
+ let actionPerformsProhibitedOp = false; // Set when action verb is synonym of lock's prohibited verb
1653
1656
 
1654
1657
  // 1. Direct word overlap (minus stopwords)
1655
1658
  const directOverlap = actionTokens.words.filter(w =>
@@ -1904,58 +1907,33 @@ export function scoreConflict({ actionText, lockText }) {
1904
1907
 
1905
1908
  let intentAligned = false; // true = action is doing the OPPOSITE of what lock prohibits
1906
1909
 
1907
- // Check 1: Direct opposite verbs (e.g., "enable" vs "disable")
1908
- if (lockIsProhibitive && prohibitedVerb && actionPrimaryVerb) {
1909
- if (checkOpposites(actionPrimaryVerb, prohibitedVerb)) {
1910
- intentAligned = true;
1911
- reasons.push(
1912
- `intent alignment: action "${actionPrimaryVerb}" is opposite of ` +
1913
- `prohibited "${prohibitedVerb}" (compliant, not conflicting)`);
1914
- }
1915
- }
1916
-
1917
- // Check 2: Positive action intent against a lock that prohibits a negative action
1918
- // ONLY applies when there are no euphemism/synonym matches suggesting the
1919
- // action is actually destructive despite sounding positive (e.g., "reseed" → "reset")
1920
- if (!intentAligned && lockIsProhibitive && actionIntent.intent === "positive" && prohibitedVerb) {
1921
- const prohibitedIsNegative = NEGATIVE_INTENT_MARKERS.some(m =>
1922
- prohibitedVerb === m || prohibitedVerb.startsWith(m));
1923
- // Only block alignment if the action actually performs the prohibited operation.
1924
- // Noun synonyms ("price → pricing") are incidental and should NOT block.
1925
- // But if the action contains the prohibited verb or its synonym ("shows" ≈ "expose"),
1926
- // that's a real violation signal.
1927
- const hasDestructiveLanguageMatch = euphemismMatches.length > 0;
1928
-
1929
- // Check if action text contains the prohibited verb or its synonyms
1930
- let actionPerformsProhibitedOp = false;
1931
- if (prohibitedVerb) {
1932
- const actionWordsLower = actionText.toLowerCase().split(/\s+/)
1933
- .map(w => w.replace(/[^a-z]/g, ""));
1934
- for (const w of actionWordsLower) {
1935
- if (!w) continue;
1936
- // Direct match (including conjugations: show/shows/showing/showed)
1937
- if (w === prohibitedVerb || w.startsWith(prohibitedVerb)) {
1938
- actionPerformsProhibitedOp = true;
1939
- break;
1940
- }
1941
- // Synonym match: check if word is in the same synonym group as prohibited verb
1942
- for (const group of SYNONYM_GROUPS) {
1943
- if (group.includes(prohibitedVerb)) {
1944
- if (group.some(syn => w === syn || w.startsWith(syn) && w.length <= syn.length + 3)) {
1945
- actionPerformsProhibitedOp = true;
1946
- }
1947
- break;
1910
+ // Pre-compute: does the action verb match the lock's prohibited verb (or its synonyms)?
1911
+ // This flag is used by multiple checks below to prevent false negatives.
1912
+ if (lockIsProhibitive && prohibitedVerb) {
1913
+ const actionWordsLower = actionText.toLowerCase().split(/\s+/)
1914
+ .map(w => w.replace(/[^a-z]/g, ""));
1915
+ for (const w of actionWordsLower) {
1916
+ if (!w) continue;
1917
+ // Direct match (including conjugations: show/shows/showing/showed)
1918
+ if (w === prohibitedVerb || w.startsWith(prohibitedVerb)) {
1919
+ actionPerformsProhibitedOp = true;
1920
+ break;
1921
+ }
1922
+ // Synonym match: check if word is in the same synonym group as prohibited verb
1923
+ for (const group of SYNONYM_GROUPS) {
1924
+ if (group.includes(prohibitedVerb)) {
1925
+ if (group.some(syn => w === syn || w.startsWith(syn) && w.length <= syn.length + 3)) {
1926
+ actionPerformsProhibitedOp = true;
1948
1927
  }
1928
+ break;
1949
1929
  }
1950
- if (actionPerformsProhibitedOp) break;
1951
1930
  }
1931
+ if (actionPerformsProhibitedOp) break;
1952
1932
  }
1953
1933
 
1954
1934
  // Special case: "add/put/store/embed key/secret/credential in/to frontend/component/state"
1955
1935
  // is SEMANTICALLY EQUIVALENT to "expose key in frontend" — NOT an opposite action.
1956
- // Placing credentials in client-side code IS exposing them.
1957
1936
  if (!actionPerformsProhibitedOp && (prohibitedVerb === "expose" || prohibitedVerb === "leak" || prohibitedVerb === "reveal")) {
1958
- // Pre-process env vars: STRIPE_SECRET_KEY → stripe secret key
1959
1937
  const actionNorm = actionText
1960
1938
  .replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, m => m.replace(/_/g, " "))
1961
1939
  .toLowerCase();
@@ -1968,6 +1946,25 @@ export function scoreConflict({ actionText, lockText }) {
1968
1946
  reasons.push("security: placing credentials in client-side code is equivalent to exposing them");
1969
1947
  }
1970
1948
  }
1949
+ }
1950
+
1951
+ // Check 1: Direct opposite verbs (e.g., "enable" vs "disable")
1952
+ if (lockIsProhibitive && prohibitedVerb && actionPrimaryVerb) {
1953
+ if (checkOpposites(actionPrimaryVerb, prohibitedVerb)) {
1954
+ intentAligned = true;
1955
+ reasons.push(
1956
+ `intent alignment: action "${actionPrimaryVerb}" is opposite of ` +
1957
+ `prohibited "${prohibitedVerb}" (compliant, not conflicting)`);
1958
+ }
1959
+ }
1960
+
1961
+ // Check 2: Positive action intent against a lock that prohibits a negative action
1962
+ // ONLY applies when there are no euphemism/synonym matches suggesting the
1963
+ // action is actually destructive despite sounding positive (e.g., "reseed" → "reset")
1964
+ if (!intentAligned && lockIsProhibitive && actionIntent.intent === "positive" && prohibitedVerb) {
1965
+ const prohibitedIsNegative = NEGATIVE_INTENT_MARKERS.some(m =>
1966
+ prohibitedVerb === m || prohibitedVerb.startsWith(m));
1967
+ const hasDestructiveLanguageMatch = euphemismMatches.length > 0;
1971
1968
 
1972
1969
  if (prohibitedIsNegative && !actionIntent.negated &&
1973
1970
  !hasDestructiveLanguageMatch && !actionPerformsProhibitedOp) {
@@ -2067,11 +2064,12 @@ export function scoreConflict({ actionText, lockText }) {
2067
2064
 
2068
2065
  // Check 3c: Working WITH locked technology (not replacing it)
2069
2066
  // "Update the Stripe UI components" vs "must always use Stripe" → working WITH Stripe → safe
2067
+ // "Update the Stripe payment UI" vs "Stripe API keys must never be exposed" → different subject → safe
2070
2068
  // "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
2069
  // But: "Update payment to use Razorpay" vs "Stripe lock" → introducing competitor → NOT safe
2070
+ // But: "Add Stripe key to frontend" → "add" not in WORKING_WITH_VERBS → NOT safe
2073
2071
  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);
2072
+ const lockIsPreservationOrFreeze = /must remain|must be preserved|must always|at all times|must stay|must never|must not|should never|do not replace|do not remove|do not switch|don't replace|don't remove|don't switch|don't|do not|never|uses .+ library/i.test(lockText);
2075
2073
  if (lockIsPreservationOrFreeze) {
2076
2074
  // Extract specific brand/tech names from the lock text
2077
2075
  const lockWords = lockText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
@@ -2081,7 +2079,7 @@ export function scoreConflict({ actionText, lockText }) {
2081
2079
  // These are specific nouns (not verbs, not stopwords) that identify the technology
2082
2080
  const TECH_BRANDS = new Set([
2083
2081
  "stripe", "razorpay", "paypal", "phonepe", "paytm", "ccavenue", "cashfree",
2084
- "braintree", "adyen", "square", "billdesk", "instamojo", "juspay",
2082
+ "braintree", "adyen", "square", "billdesk", "instamojo", "juspay", "payu",
2085
2083
  "postgresql", "postgres", "mysql", "mongodb", "mongo", "firebase",
2086
2084
  "firestore", "supabase", "dynamodb", "redis", "sqlite", "mariadb",
2087
2085
  "cassandra", "couchdb", "neo4j",
@@ -2111,7 +2109,10 @@ export function scoreConflict({ actionText, lockText }) {
2111
2109
  "disable", "deactivate", "replace", "switch", "migrate", "move",
2112
2110
  ]);
2113
2111
  if (WORKING_WITH_VERBS.has(actionPrimaryVerb) && !hasCompetitorInAction &&
2114
- !DESTRUCTIVE_VERBS.has(actionPrimaryVerb)) {
2112
+ !DESTRUCTIVE_VERBS.has(actionPrimaryVerb) &&
2113
+ !actionPerformsProhibitedOp) {
2114
+ // Guard: if the action verb is a synonym of the lock's prohibited verb
2115
+ // (e.g., "update" ≈ "modify"), that's a real conflict, not working-with.
2115
2116
  intentAligned = true;
2116
2117
  reasons.push(
2117
2118
  `intent alignment: "${actionPrimaryVerb}" works WITH locked tech ` +
@@ -2231,9 +2232,9 @@ export function scoreConflict({ actionText, lockText }) {
2231
2232
 
2232
2233
  // If intent is ALIGNED, the action is COMPLIANT — slash the score to near zero
2233
2234
  // Shared keywords are expected (both discuss the same subject) but the action
2234
- // is doing the right thing.
2235
+ // is doing the right thing. Cap at threshold-1 so aligned actions never trigger.
2235
2236
  if (intentAligned) {
2236
- score = Math.floor(score * 0.10); // Keep only 10% of accumulated score
2237
+ score = Math.min(Math.floor(score * 0.10), SCORING.conflictThreshold - 1);
2237
2238
  // Skip all further bonuses (negation, intent conflict, destructive)
2238
2239
  } else {
2239
2240
  // NOT aligned — apply standard conflict bonuses
@@ -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.4",
260
+ version: "4.5.6",
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.4 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v4.5.6 &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.4 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v4.5.6 &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.4";
94
+ const VERSION = "4.5.6";
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.4";
103
+ const VERSION = "4.5.6";
104
104
  const AUTHOR = "Sandeep Roy";
105
105
 
106
106
  const server = new McpServer(