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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  "name": "speclock",
4
4
 
5
- "version": "5.5.0",
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.0 — Your AI has rules. SpecLock makes them unbreakable.
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]
@@ -9,7 +9,7 @@
9
9
  import { readBrain, readEvents } from "./storage.js";
10
10
  import { verifyAuditChain } from "./audit.js";
11
11
 
12
- const VERSION = "5.5.0";
12
+ const VERSION = "5.5.2";
13
13
 
14
14
  // PHI-related keywords for HIPAA filtering
15
15
  const PHI_KEYWORDS = [
@@ -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 > 0) {
45
- found.push({
46
- file: entry.file,
47
- tool: entry.tool,
48
- path: fullPath,
49
- content,
50
- size: content.length,
51
- lines: content.split("\n").length,
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
@@ -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/i.test(lockText);
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/i.test(lockText);
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/i.test(lockText);
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
@@ -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.0",
260
+ version: "5.5.2",
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">v5.5.0 &mdash; Your AI has rules. SpecLock makes them unbreakable.</div>
92
+ <div class="meta">v5.5.2 &mdash; 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.0 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v5.5.2 &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>
@@ -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.0";
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.0";
129
+ const VERSION = "5.5.2";
130
130
  const AUTHOR = "Sandeep Roy";
131
131
 
132
132
  const server = new McpServer(