speclock 4.5.4 → 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.
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/index.js +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/semantics.js +48 -49
- 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/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.5.
|
|
460
|
+
<p align="center"><i>v4.5.5 — 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.
|
|
5
|
+
"version": "4.5.5",
|
|
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.
|
|
120
|
+
SpecLock v4.5.5 — 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]
|
package/src/core/compliance.js
CHANGED
package/src/core/semantics.js
CHANGED
|
@@ -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
|
-
//
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
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)
|
|
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
|
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.5.
|
|
260
|
+
version: "4.5.5",
|
|
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.5.
|
|
92
|
+
<div class="meta">v4.5.5 — 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.
|
|
185
|
+
SpecLock v4.5.5 — 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.5.
|
|
94
|
+
const VERSION = "4.5.5";
|
|
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.
|
|
103
|
+
const VERSION = "4.5.5";
|
|
104
104
|
const AUTHOR = "Sandeep Roy";
|
|
105
105
|
|
|
106
106
|
const server = new McpServer(
|