speclock 5.5.0 → 5.5.2
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 +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/guardian.js +46 -10
- package/src/core/semantics.js +64 -4
- 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": "5.5.
|
|
5
|
+
"version": "5.5.2",
|
|
6
6
|
|
|
7
7
|
"description": "Stop AI from breaking code you told it not to touch. Enforces .cursorrules, CLAUDE.md, and AGENTS.md — not just suggests. Zero-config: npx speclock protect reads your existing AI rule files, extracts constraints, installs pre-commit hooks, and makes your rules unbreakable. 51 MCP tools, Universal Rules Sync, AI Patch Firewall, Spec Compiler, Code Graph, Typed Constraints, Drift Score, HMAC audit chain, SOC 2/HIPAA compliance. Developed by Sandeep Roy.",
|
|
8
8
|
|
package/src/cli/index.js
CHANGED
|
@@ -123,7 +123,7 @@ function refreshContext(root) {
|
|
|
123
123
|
|
|
124
124
|
function printHelp() {
|
|
125
125
|
console.log(`
|
|
126
|
-
SpecLock v5.5.
|
|
126
|
+
SpecLock v5.5.2 — Your AI has rules. SpecLock makes them unbreakable.
|
|
127
127
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
128
128
|
|
|
129
129
|
Usage: speclock <command> [options]
|
package/src/core/compliance.js
CHANGED
package/src/core/guardian.js
CHANGED
|
@@ -32,30 +32,66 @@ const RULE_FILES = [
|
|
|
32
32
|
{ file: ".github/instructions.md", tool: "GitHub (alt)" },
|
|
33
33
|
];
|
|
34
34
|
|
|
35
|
+
// Files that SpecLock's sync creates — these are OUTPUT, not INPUT.
|
|
36
|
+
// Never read these back as source rule files.
|
|
37
|
+
const SPECLOCK_OUTPUT_FILES = new Set([
|
|
38
|
+
".cursor/rules/speclock.mdc",
|
|
39
|
+
".windsurf/rules/speclock.md",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// Header markers that indicate a file was auto-generated by SpecLock sync.
|
|
43
|
+
// If ANY of these appear in the first 8 lines, skip the file.
|
|
44
|
+
const SPECLOCK_SYNC_MARKERS = [
|
|
45
|
+
"Auto-synced from SpecLock",
|
|
46
|
+
"Auto-synced by SpecLock",
|
|
47
|
+
"Auto-synced.",
|
|
48
|
+
"(SpecLock)",
|
|
49
|
+
"# SpecLock Constraints",
|
|
50
|
+
"# Generated:",
|
|
51
|
+
"Do not edit manually — run `speclock sync`",
|
|
52
|
+
"speclock sync --format",
|
|
53
|
+
"speclock_session_briefing",
|
|
54
|
+
];
|
|
55
|
+
|
|
35
56
|
/**
|
|
36
57
|
* Discover all AI rule files in the project.
|
|
37
58
|
*/
|
|
38
59
|
export function discoverRuleFiles(root) {
|
|
39
60
|
const found = [];
|
|
40
61
|
for (const entry of RULE_FILES) {
|
|
62
|
+
// Skip known SpecLock output files
|
|
63
|
+
if (SPECLOCK_OUTPUT_FILES.has(entry.file)) continue;
|
|
64
|
+
|
|
41
65
|
const fullPath = path.join(root, entry.file);
|
|
42
66
|
if (fs.existsSync(fullPath)) {
|
|
43
67
|
const content = fs.readFileSync(fullPath, "utf-8").trim();
|
|
44
|
-
if (content.length
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
if (content.length === 0) continue;
|
|
69
|
+
|
|
70
|
+
// Skip files that were auto-generated by SpecLock sync
|
|
71
|
+
if (isSpeclockGenerated(content)) continue;
|
|
72
|
+
|
|
73
|
+
found.push({
|
|
74
|
+
file: entry.file,
|
|
75
|
+
tool: entry.tool,
|
|
76
|
+
path: fullPath,
|
|
77
|
+
content,
|
|
78
|
+
size: content.length,
|
|
79
|
+
lines: content.split("\n").length,
|
|
80
|
+
});
|
|
54
81
|
}
|
|
55
82
|
}
|
|
56
83
|
return found;
|
|
57
84
|
}
|
|
58
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Check if file content was auto-generated by SpecLock sync.
|
|
88
|
+
* Looks at the first 5 lines for sync markers.
|
|
89
|
+
*/
|
|
90
|
+
function isSpeclockGenerated(content) {
|
|
91
|
+
const header = content.split("\n").slice(0, 8).join("\n");
|
|
92
|
+
return SPECLOCK_SYNC_MARKERS.some((marker) => header.includes(marker));
|
|
93
|
+
}
|
|
94
|
+
|
|
59
95
|
// --- Heuristic constraint extraction (no API key needed) ---
|
|
60
96
|
|
|
61
97
|
// Patterns that signal a constraint/rule
|
package/src/core/semantics.js
CHANGED
|
@@ -787,6 +787,17 @@ export const CONCEPT_MAP = {
|
|
|
787
787
|
"location data": ["subscriber data", "tracking data", "geolocation",
|
|
788
788
|
"user location", "pii"],
|
|
789
789
|
|
|
790
|
+
// Programming languages (alternatives = language switch conflict)
|
|
791
|
+
"typescript": ["programming language", "typed language", "javascript",
|
|
792
|
+
"language", "ts"],
|
|
793
|
+
"javascript": ["programming language", "scripting language", "typescript",
|
|
794
|
+
"language", "js"],
|
|
795
|
+
"python": ["programming language", "scripting language", "language"],
|
|
796
|
+
"golang": ["programming language", "language", "go"],
|
|
797
|
+
"rust": ["programming language", "systems language", "language"],
|
|
798
|
+
"java": ["programming language", "language", "kotlin"],
|
|
799
|
+
"kotlin": ["programming language", "language", "java"],
|
|
800
|
+
|
|
790
801
|
// Frontend frameworks (alternatives = change framework conflict)
|
|
791
802
|
"react": ["frontend framework", "ui framework", "frontend", "ui",
|
|
792
803
|
"vue", "angular", "svelte", "sveltekit", "next.js", "nextjs"],
|
|
@@ -1433,7 +1444,9 @@ function isProhibitiveLock(lockText) {
|
|
|
1433
1444
|
|| /\bno\s+\w/i.test(lockText)
|
|
1434
1445
|
// Normalized lock patterns from lock-author.js rewriting
|
|
1435
1446
|
|| /\bis\s+frozen\b/i.test(lockText)
|
|
1436
|
-
|| /\bmust\s+(remain|be\s+preserved|stay|always)\b/i.test(lockText)
|
|
1447
|
+
|| /\bmust\s+(remain|be\s+preserved|stay|always)\b/i.test(lockText)
|
|
1448
|
+
// "ALWAYS use X" is a preservation mandate — removing X violates it
|
|
1449
|
+
|| /^\s*always\b/i.test(lockText);
|
|
1437
1450
|
}
|
|
1438
1451
|
|
|
1439
1452
|
// ===================================================================
|
|
@@ -2140,6 +2153,50 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2140
2153
|
}
|
|
2141
2154
|
}
|
|
2142
2155
|
|
|
2156
|
+
// Check 0b: MUST-mandate inversion
|
|
2157
|
+
// "MUST validate all user input" + "skip input validation" → conflict
|
|
2158
|
+
// When a lock says MUST/ALWAYS + <verb>, and the action uses a negation verb
|
|
2159
|
+
// (skip, remove, disable, bypass, omit, drop, eliminate, stop) targeting the same concept,
|
|
2160
|
+
// that's a violation of the mandate.
|
|
2161
|
+
{
|
|
2162
|
+
const mandateMatch = lockText.match(/^\s*(?:must|always)\s+(\w+(?:\s+\w+){0,3})/i);
|
|
2163
|
+
if (mandateMatch && !prohibitedVerb) {
|
|
2164
|
+
const mandatedPhrase = mandateMatch[1].toLowerCase();
|
|
2165
|
+
const mandatedWords = mandatedPhrase.split(/\s+/).filter(w => !STOPWORDS.has(w) && w.length > 2);
|
|
2166
|
+
const actionLower = actionText.toLowerCase();
|
|
2167
|
+
const NEGATION_VERBS = /\b(skip|skipping|remove|removing|disable|disabling|bypass|bypassing|omit|omitting|drop|dropping|eliminate|eliminating|stop|stopping|avoid|avoiding|delete|deleting|no|without)\b/;
|
|
2168
|
+
|
|
2169
|
+
if (NEGATION_VERBS.test(actionLower)) {
|
|
2170
|
+
// Check if the action targets the same concept as the mandated verb
|
|
2171
|
+
// Stem matching: "validate" ↔ "validation" share root "validat" (5+ chars)
|
|
2172
|
+
const actionWords = actionLower.split(/\s+/).map(w => w.replace(/[^a-z]/g, "")).filter(w => w.length > 2);
|
|
2173
|
+
let conceptOverlap = 0;
|
|
2174
|
+
for (const mw of mandatedWords) {
|
|
2175
|
+
for (const aw of actionWords) {
|
|
2176
|
+
if (aw === mw) { conceptOverlap++; break; }
|
|
2177
|
+
// Share a common root of 5+ chars: validate↔validation (validat)
|
|
2178
|
+
const minLen = Math.min(aw.length, mw.length);
|
|
2179
|
+
if (minLen >= 5) {
|
|
2180
|
+
const root = Math.min(minLen, Math.max(5, minLen - 2));
|
|
2181
|
+
if (aw.substring(0, root) === mw.substring(0, root)) {
|
|
2182
|
+
conceptOverlap++;
|
|
2183
|
+
break;
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
if (conceptOverlap >= 1) {
|
|
2189
|
+
const bonus = conceptOverlap >= 2 ? 45 : 25;
|
|
2190
|
+
score += bonus;
|
|
2191
|
+
actionPerformsProhibitedOp = true;
|
|
2192
|
+
reasons.push(
|
|
2193
|
+
`mandate violation: action negates MUST-requirement ` +
|
|
2194
|
+
`"${mandatedPhrase}" (${conceptOverlap} concept overlap)`);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2143
2200
|
// Check 1: Direct opposite verbs (e.g., "enable" vs "disable")
|
|
2144
2201
|
if (lockIsProhibitive && prohibitedVerb && actionPrimaryVerb) {
|
|
2145
2202
|
if (checkOpposites(actionPrimaryVerb, prohibitedVerb)) {
|
|
@@ -2227,7 +2284,7 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2227
2284
|
// "Test that Stripe is working" is COMPLIANT with "must always use Stripe"
|
|
2228
2285
|
// "Debug the Stripe webhook" is COMPLIANT — it's verifying the preserved system
|
|
2229
2286
|
{
|
|
2230
|
-
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/
|
|
2287
|
+
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay|^\s*always\b/im.test(lockText);
|
|
2231
2288
|
|
|
2232
2289
|
if (!intentAligned && lockIsPreservation) {
|
|
2233
2290
|
const SAFE_FOR_PRESERVATION = new Set([
|
|
@@ -2339,7 +2396,7 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2339
2396
|
// But: "Update payment to use Razorpay" vs "Stripe lock" → introducing competitor → NOT safe
|
|
2340
2397
|
// But: "Add Stripe key to frontend" → "add" not in WORKING_WITH_VERBS → NOT safe
|
|
2341
2398
|
if (!intentAligned && actionPrimaryVerb) {
|
|
2342
|
-
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/
|
|
2399
|
+
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|^\s*always\b/im.test(lockText);
|
|
2343
2400
|
if (lockIsPreservationOrFreeze) {
|
|
2344
2401
|
// Extract specific brand/tech names from the lock text
|
|
2345
2402
|
const lockWords = lockText.toLowerCase().split(/\s+/).map(w => w.replace(/[^a-z0-9]/g, "")).filter(w => w.length > 2);
|
|
@@ -2356,6 +2413,9 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2356
2413
|
"baileys", "twilio", "whatsapp",
|
|
2357
2414
|
"auth0", "okta", "cognito", "keycloak",
|
|
2358
2415
|
"react", "vue", "angular", "svelte", "nextjs", "nuxt",
|
|
2416
|
+
"typescript", "javascript", "python", "golang", "rust", "java", "kotlin",
|
|
2417
|
+
"tailwind", "bootstrap", "prisma", "drizzle", "sequelize",
|
|
2418
|
+
"express", "fastapi", "django", "flask", "rails",
|
|
2359
2419
|
"docker", "kubernetes", "terraform", "ansible",
|
|
2360
2420
|
"aws", "gcp", "azure", "vercel", "netlify", "railway", "heroku",
|
|
2361
2421
|
]);
|
|
@@ -2406,7 +2466,7 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2406
2466
|
"build", "add", "create", "implement", "make", "design",
|
|
2407
2467
|
"develop", "introduce",
|
|
2408
2468
|
]);
|
|
2409
|
-
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay/
|
|
2469
|
+
const lockIsPreservation = /must remain|must be preserved|must always|at all times|must stay|^\s*always\b/im.test(lockText);
|
|
2410
2470
|
if (lockIsPreservation) {
|
|
2411
2471
|
if (ENHANCEMENT_VERBS.has(actionPrimaryVerb)) {
|
|
2412
2472
|
// Enhancement verbs always align with preservation locks
|
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: "5.5.
|
|
260
|
+
version: "5.5.2",
|
|
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">v5.5.
|
|
92
|
+
<div class="meta">v5.5.2 — Your AI has rules. SpecLock makes them unbreakable.</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 v5.5.
|
|
185
|
+
SpecLock v5.5.2 — 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
|
@@ -113,7 +113,7 @@ import { fileURLToPath } from "url";
|
|
|
113
113
|
import _path from "path";
|
|
114
114
|
|
|
115
115
|
const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
116
|
-
const VERSION = "5.5.
|
|
116
|
+
const VERSION = "5.5.2";
|
|
117
117
|
const AUTHOR = "Sandeep Roy";
|
|
118
118
|
const START_TIME = Date.now();
|
|
119
119
|
|
package/src/mcp/server.js
CHANGED
|
@@ -126,7 +126,7 @@ const PROJECT_ROOT =
|
|
|
126
126
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
127
127
|
|
|
128
128
|
// --- MCP Server ---
|
|
129
|
-
const VERSION = "5.5.
|
|
129
|
+
const VERSION = "5.5.2";
|
|
130
130
|
const AUTHOR = "Sandeep Roy";
|
|
131
131
|
|
|
132
132
|
const server = new McpServer(
|