speclock 4.5.3 → 4.5.5

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.
@@ -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.3";
12
+ const VERSION = "4.5.5";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -342,16 +342,15 @@ export async function checkConflictAsync(rootOrAction, proposedActionOrLock) {
342
342
 
343
343
  /**
344
344
  * Merge heuristic result with LLM/proxy result.
345
- * Keeps HIGH heuristic conflicts + all LLM conflicts, deduplicates, takes MAX.
345
+ * Takes MAX(heuristic, LLM) confidence per lock neither engine can override the other.
346
+ * The heuristic catches domain-specific patterns; the LLM catches cross-domain vocabulary.
346
347
  */
347
348
  function mergeLLMResult(heuristicResult, llmResult) {
348
- const highConfidence = heuristicResult.conflictingLocks.filter(
349
- (c) => c.confidence > 70
350
- );
349
+ const allHeuristic = heuristicResult.conflictingLocks || [];
351
350
  const llmConflicts = llmResult.conflictingLocks || [];
352
- const merged = [...highConfidence, ...llmConflicts];
351
+ const merged = [...allHeuristic, ...llmConflicts];
353
352
 
354
- // Deduplicate by lock text, keeping the higher-confidence entry
353
+ // Deduplicate by lock text, keeping the higher-confidence entry (MAX per lock)
355
354
  const byText = new Map();
356
355
  for (const c of merged) {
357
356
  const existing = byText.get(c.text);
@@ -365,15 +364,17 @@ function mergeLLMResult(heuristicResult, llmResult) {
365
364
  return {
366
365
  hasConflict: false,
367
366
  conflictingLocks: [],
368
- analysis: `Heuristic had partial signal, LLM verified as safe. No conflicts.`,
367
+ analysis: `Both heuristic and LLM agree: no conflicts detected.`,
369
368
  };
370
369
  }
371
370
 
372
371
  unique.sort((a, b) => b.confidence - a.confidence);
372
+ const heuristicCount = allHeuristic.filter(c => !llmConflicts.some(l => l.text === c.text)).length;
373
+ const llmOnlyCount = llmConflicts.filter(l => !allHeuristic.some(h => h.text === l.text)).length;
373
374
  return {
374
375
  hasConflict: true,
375
376
  conflictingLocks: unique,
376
- analysis: `${unique.length} conflict(s) confirmed (${highConfidence.length} heuristic + ${llmConflicts.length} LLM-verified).`,
377
+ analysis: `${unique.length} conflict(s) detected (${heuristicCount} heuristic-only, ${llmOnlyCount} LLM-only, ${unique.length - heuristicCount - llmOnlyCount} both).`,
377
378
  };
378
379
  }
379
380
 
@@ -1650,6 +1650,7 @@ export function scoreConflict({ actionText, lockText }) {
1650
1650
  let score = 0;
1651
1651
  const reasons = [];
1652
1652
  let hasSecurityViolationPattern = false; // Set when credential-exposure detected
1653
+ let actionPerformsProhibitedOp = false; // Set when action verb is synonym of lock's prohibited verb
1653
1654
 
1654
1655
  // 1. Direct word overlap (minus stopwords)
1655
1656
  const directOverlap = actionTokens.words.filter(w =>
@@ -1904,58 +1905,33 @@ export function scoreConflict({ actionText, lockText }) {
1904
1905
 
1905
1906
  let intentAligned = false; // true = action is doing the OPPOSITE of what lock prohibits
1906
1907
 
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;
1908
+ // Pre-compute: does the action verb match the lock's prohibited verb (or its synonyms)?
1909
+ // This flag is used by multiple checks below to prevent false negatives.
1910
+ if (lockIsProhibitive && prohibitedVerb) {
1911
+ const actionWordsLower = actionText.toLowerCase().split(/\s+/)
1912
+ .map(w => w.replace(/[^a-z]/g, ""));
1913
+ for (const w of actionWordsLower) {
1914
+ if (!w) continue;
1915
+ // Direct match (including conjugations: show/shows/showing/showed)
1916
+ if (w === prohibitedVerb || w.startsWith(prohibitedVerb)) {
1917
+ actionPerformsProhibitedOp = true;
1918
+ break;
1919
+ }
1920
+ // Synonym match: check if word is in the same synonym group as prohibited verb
1921
+ for (const group of SYNONYM_GROUPS) {
1922
+ if (group.includes(prohibitedVerb)) {
1923
+ if (group.some(syn => w === syn || w.startsWith(syn) && w.length <= syn.length + 3)) {
1924
+ actionPerformsProhibitedOp = true;
1948
1925
  }
1926
+ break;
1949
1927
  }
1950
- if (actionPerformsProhibitedOp) break;
1951
1928
  }
1929
+ if (actionPerformsProhibitedOp) break;
1952
1930
  }
1953
1931
 
1954
1932
  // Special case: "add/put/store/embed key/secret/credential in/to frontend/component/state"
1955
1933
  // is SEMANTICALLY EQUIVALENT to "expose key in frontend" — NOT an opposite action.
1956
- // Placing credentials in client-side code IS exposing them.
1957
1934
  if (!actionPerformsProhibitedOp && (prohibitedVerb === "expose" || prohibitedVerb === "leak" || prohibitedVerb === "reveal")) {
1958
- // Pre-process env vars: STRIPE_SECRET_KEY → stripe secret key
1959
1935
  const actionNorm = actionText
1960
1936
  .replace(/\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, m => m.replace(/_/g, " "))
1961
1937
  .toLowerCase();
@@ -1968,6 +1944,25 @@ export function scoreConflict({ actionText, lockText }) {
1968
1944
  reasons.push("security: placing credentials in client-side code is equivalent to exposing them");
1969
1945
  }
1970
1946
  }
1947
+ }
1948
+
1949
+ // Check 1: Direct opposite verbs (e.g., "enable" vs "disable")
1950
+ if (lockIsProhibitive && prohibitedVerb && actionPrimaryVerb) {
1951
+ if (checkOpposites(actionPrimaryVerb, prohibitedVerb)) {
1952
+ intentAligned = true;
1953
+ reasons.push(
1954
+ `intent alignment: action "${actionPrimaryVerb}" is opposite of ` +
1955
+ `prohibited "${prohibitedVerb}" (compliant, not conflicting)`);
1956
+ }
1957
+ }
1958
+
1959
+ // Check 2: Positive action intent against a lock that prohibits a negative action
1960
+ // ONLY applies when there are no euphemism/synonym matches suggesting the
1961
+ // action is actually destructive despite sounding positive (e.g., "reseed" → "reset")
1962
+ if (!intentAligned && lockIsProhibitive && actionIntent.intent === "positive" && prohibitedVerb) {
1963
+ const prohibitedIsNegative = NEGATIVE_INTENT_MARKERS.some(m =>
1964
+ prohibitedVerb === m || prohibitedVerb.startsWith(m));
1965
+ const hasDestructiveLanguageMatch = euphemismMatches.length > 0;
1971
1966
 
1972
1967
  if (prohibitedIsNegative && !actionIntent.negated &&
1973
1968
  !hasDestructiveLanguageMatch && !actionPerformsProhibitedOp) {
@@ -2067,11 +2062,12 @@ export function scoreConflict({ actionText, lockText }) {
2067
2062
 
2068
2063
  // Check 3c: Working WITH locked technology (not replacing it)
2069
2064
  // "Update the Stripe UI components" vs "must always use Stripe" → working WITH Stripe → safe
2065
+ // "Update the Stripe payment UI" vs "Stripe API keys must never be exposed" → different subject → safe
2070
2066
  // "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
2067
  // But: "Update payment to use Razorpay" vs "Stripe lock" → introducing competitor → NOT safe
2068
+ // But: "Add Stripe key to frontend" → "add" not in WORKING_WITH_VERBS → NOT safe
2073
2069
  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);
2070
+ 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
2071
  if (lockIsPreservationOrFreeze) {
2076
2072
  // Extract specific brand/tech names from the lock text
2077
2073
  const lockWords = lockText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
@@ -2111,7 +2107,10 @@ export function scoreConflict({ actionText, lockText }) {
2111
2107
  "disable", "deactivate", "replace", "switch", "migrate", "move",
2112
2108
  ]);
2113
2109
  if (WORKING_WITH_VERBS.has(actionPrimaryVerb) && !hasCompetitorInAction &&
2114
- !DESTRUCTIVE_VERBS.has(actionPrimaryVerb)) {
2110
+ !DESTRUCTIVE_VERBS.has(actionPrimaryVerb) &&
2111
+ !actionPerformsProhibitedOp) {
2112
+ // Guard: if the action verb is a synonym of the lock's prohibited verb
2113
+ // (e.g., "update" ≈ "modify"), that's a real conflict, not working-with.
2115
2114
  intentAligned = true;
2116
2115
  reasons.push(
2117
2116
  `intent alignment: "${actionPrimaryVerb}" works WITH locked tech ` +
@@ -2231,9 +2230,9 @@ export function scoreConflict({ actionText, lockText }) {
2231
2230
 
2232
2231
  // If intent is ALIGNED, the action is COMPLIANT — slash the score to near zero
2233
2232
  // Shared keywords are expected (both discuss the same subject) but the action
2234
- // is doing the right thing.
2233
+ // is doing the right thing. Cap at threshold-1 so aligned actions never trigger.
2235
2234
  if (intentAligned) {
2236
- score = Math.floor(score * 0.10); // Keep only 10% of accumulated score
2235
+ score = Math.min(Math.floor(score * 0.10), SCORING.conflictThreshold - 1);
2237
2236
  // Skip all further bonuses (negation, intent conflict, destructive)
2238
2237
  } else {
2239
2238
  // 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.3",
260
+ version: "4.5.5",
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.3 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v4.5.5 &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.3 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v4.5.5 &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.3";
94
+ const VERSION = "4.5.5";
95
95
  const AUTHOR = "Sandeep Roy";
96
96
  const START_TIME = Date.now();
97
97