speclock 5.5.5 → 5.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/package.json +1 -1
- package/src/cli/index.js +77 -11
- package/src/core/compliance.js +1 -1
- package/src/core/pre-commit-semantic.js +102 -2
- 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
package/src/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { getStagedDiff, parseDiff } from "../core/pre-commit-semantic.js";
|
|
4
|
+
import { getStagedDiff, parseDiff, shouldSkipForSemanticAudit } from "../core/pre-commit-semantic.js";
|
|
5
5
|
import {
|
|
6
6
|
ensureInit,
|
|
7
7
|
setGoal,
|
|
@@ -286,7 +286,7 @@ export function initFromRulePack(root, framework) {
|
|
|
286
286
|
|
|
287
287
|
function printHelp() {
|
|
288
288
|
console.log(`
|
|
289
|
-
SpecLock v5.5.
|
|
289
|
+
SpecLock v5.5.6 — Your AI has rules. SpecLock makes them unbreakable.
|
|
290
290
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
291
291
|
|
|
292
292
|
Usage: speclock <command> [options]
|
|
@@ -1243,6 +1243,10 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1243
1243
|
// --- AUDIT-SEMANTIC (v2.5) ---
|
|
1244
1244
|
if (cmd === "audit-semantic") {
|
|
1245
1245
|
const flags = parseFlags(args);
|
|
1246
|
+
const verbose =
|
|
1247
|
+
flags.verbose === true ||
|
|
1248
|
+
process.env.SPECLOCK_VERBOSE === "1" ||
|
|
1249
|
+
process.env.SPECLOCK_VERBOSE === "true";
|
|
1246
1250
|
const result = semanticAudit(root);
|
|
1247
1251
|
|
|
1248
1252
|
// --- Commit-message + diff-content semantic check ---
|
|
@@ -1269,9 +1273,15 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1269
1273
|
} catch { /* ignore */ }
|
|
1270
1274
|
}
|
|
1271
1275
|
|
|
1272
|
-
// 2. Collect added lines from the staged diff
|
|
1276
|
+
// 2. Collect added lines from the staged diff, skipping SpecLock-internal
|
|
1277
|
+
// files (see shouldSkipForSemanticAudit). Those files' contents literally
|
|
1278
|
+
// restate the locks, so scanning their added lines produces 100%
|
|
1279
|
+
// false-positive matches for every lock in the brain.
|
|
1273
1280
|
const diffText = getStagedDiff(root);
|
|
1274
|
-
const
|
|
1281
|
+
const allParsedChanges = diffText ? parseDiff(diffText) : [];
|
|
1282
|
+
const fileChanges = allParsedChanges.filter(
|
|
1283
|
+
(fc) => !shouldSkipForSemanticAudit(fc.file, root)
|
|
1284
|
+
);
|
|
1275
1285
|
const addedSnippets = [];
|
|
1276
1286
|
for (const fc of fileChanges) {
|
|
1277
1287
|
for (const line of fc.addedLines) {
|
|
@@ -1354,15 +1364,40 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1354
1364
|
process.env.SPECLOCK_STRICT === "true" ||
|
|
1355
1365
|
result.blocked;
|
|
1356
1366
|
|
|
1367
|
+
// --- Three-tier output filter (v5.5.6) ---
|
|
1368
|
+
// Investor audit: walls of LOW-confidence matches are user-hostile.
|
|
1369
|
+
// Only HIGH and MEDIUM print by default. LOW rolls up into a one-liner.
|
|
1370
|
+
// --verbose / SPECLOCK_VERBOSE=1 shows everything.
|
|
1371
|
+
const OUTPUT_MIN_CONFIDENCE = 40; // below this = "LOW", hidden by default
|
|
1372
|
+
const MAX_VISIBLE_VIOLATIONS = 10; // hard cap on printed items
|
|
1373
|
+
|
|
1374
|
+
const allViolations = result.violations || [];
|
|
1375
|
+
const highViolations = allViolations.filter((v) => (v.confidence || 0) >= 70);
|
|
1376
|
+
const mediumViolations = allViolations.filter(
|
|
1377
|
+
(v) => (v.confidence || 0) >= OUTPUT_MIN_CONFIDENCE && (v.confidence || 0) < 70
|
|
1378
|
+
);
|
|
1379
|
+
const lowViolations = allViolations.filter(
|
|
1380
|
+
(v) => (v.confidence || 0) < OUTPUT_MIN_CONFIDENCE
|
|
1381
|
+
);
|
|
1382
|
+
|
|
1383
|
+
// What actually gets printed
|
|
1384
|
+
const visibleViolations = verbose
|
|
1385
|
+
? allViolations
|
|
1386
|
+
: [...highViolations, ...mediumViolations];
|
|
1387
|
+
|
|
1357
1388
|
console.log(`\nSemantic Pre-Commit Audit`);
|
|
1358
1389
|
console.log("=".repeat(50));
|
|
1359
1390
|
console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
|
|
1360
|
-
|
|
1391
|
+
const filesLine = result.filesSkipped
|
|
1392
|
+
? `Files analyzed: ${result.filesChecked} (${result.filesSkipped} skipped)`
|
|
1393
|
+
: `Files analyzed: ${result.filesChecked}`;
|
|
1394
|
+
console.log(filesLine);
|
|
1361
1395
|
console.log(`Active locks: ${result.activeLocks}`);
|
|
1362
|
-
|
|
1363
|
-
if (
|
|
1396
|
+
|
|
1397
|
+
if (visibleViolations.length > 0) {
|
|
1364
1398
|
console.log("");
|
|
1365
|
-
|
|
1399
|
+
const toPrint = visibleViolations.slice(0, MAX_VISIBLE_VIOLATIONS);
|
|
1400
|
+
for (const v of toPrint) {
|
|
1366
1401
|
console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
|
|
1367
1402
|
console.log(` Lock: "${v.lockText}"`);
|
|
1368
1403
|
console.log(` Reason: ${v.reason}`);
|
|
@@ -1370,17 +1405,48 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1370
1405
|
console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
|
|
1371
1406
|
}
|
|
1372
1407
|
}
|
|
1408
|
+
const hiddenByCap = visibleViolations.length - toPrint.length;
|
|
1409
|
+
if (hiddenByCap > 0) {
|
|
1410
|
+
console.log(` ... + ${hiddenByCap} more (output capped at ${MAX_VISIBLE_VIOLATIONS})`);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// LOW-confidence rollup
|
|
1415
|
+
if (!verbose && lowViolations.length > 0) {
|
|
1416
|
+
console.log(
|
|
1417
|
+
` + ${lowViolations.length} low-confidence match(es) hidden (use --verbose or SPECLOCK_VERBOSE=1 to see)`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// --- New summary line ---
|
|
1422
|
+
console.log("");
|
|
1423
|
+
let summaryLine;
|
|
1424
|
+
if (highViolations.length === 0 && mediumViolations.length === 0) {
|
|
1425
|
+
summaryLine = `[OK] ${result.filesChecked} file(s) checked, no concerns.`;
|
|
1426
|
+
} else if (highViolations.length > 0) {
|
|
1427
|
+
summaryLine = `[!] ${highViolations.length} HIGH-confidence concern(s) — review before merging.`;
|
|
1428
|
+
} else {
|
|
1429
|
+
summaryLine = `[i] ${mediumViolations.length} medium-confidence note(s) (informational).`;
|
|
1430
|
+
}
|
|
1431
|
+
console.log(summaryLine);
|
|
1432
|
+
|
|
1433
|
+
// Preserve the machine-readable status message for any callers that grep for it
|
|
1434
|
+
if (result.blocked) {
|
|
1435
|
+
console.log(
|
|
1436
|
+
`BLOCKED: ${highViolations.length} high-confidence violation(s) — hard enforcement active.`
|
|
1437
|
+
);
|
|
1373
1438
|
}
|
|
1374
|
-
console.log(`\n${result.message}`);
|
|
1375
1439
|
|
|
1376
|
-
if (
|
|
1440
|
+
if (highViolations.length > 0 && !strict) {
|
|
1377
1441
|
console.log("\nWarning mode active — commit allowed. To enforce hard blocks, run:");
|
|
1378
1442
|
console.log(" speclock audit-semantic --strict");
|
|
1379
1443
|
console.log(" SPECLOCK_STRICT=1 git commit ...");
|
|
1380
1444
|
console.log(" speclock enforce hard (persistent, project-wide)");
|
|
1381
1445
|
}
|
|
1382
1446
|
|
|
1383
|
-
|
|
1447
|
+
// Blocking decision is still driven by the engine's 70% threshold, so
|
|
1448
|
+
// only HIGH-confidence matches can ever cause a non-zero exit.
|
|
1449
|
+
process.exit(strict && highViolations.length > 0 ? 1 : 0);
|
|
1384
1450
|
}
|
|
1385
1451
|
|
|
1386
1452
|
// --- AUTH (v3.0) ---
|
package/src/core/compliance.js
CHANGED
|
@@ -16,6 +16,7 @@ import { analyzeConflict } from "./semantics.js";
|
|
|
16
16
|
import { getEnforcementConfig } from "./enforcer.js";
|
|
17
17
|
|
|
18
18
|
const GUARD_TAG = "SPECLOCK-GUARD";
|
|
19
|
+
const SPECLOCK_AUTOGEN_MARKER = "SpecLock";
|
|
19
20
|
const MAX_LINES_PER_FILE = 500;
|
|
20
21
|
const BINARY_EXTENSIONS = new Set([
|
|
21
22
|
"png", "jpg", "jpeg", "gif", "bmp", "ico", "svg", "webp",
|
|
@@ -27,6 +28,98 @@ const BINARY_EXTENSIONS = new Set([
|
|
|
27
28
|
"lock", "map",
|
|
28
29
|
]);
|
|
29
30
|
|
|
31
|
+
// Files / dirs that SpecLock itself auto-creates during `protect` or that
|
|
32
|
+
// are just noise for semantic analysis. These are ALWAYS skipped from the
|
|
33
|
+
// diff-level semantic audit because matching against them produces nothing
|
|
34
|
+
// but false positives (e.g. rules files describe the same concepts the
|
|
35
|
+
// locks describe, so they always "conflict" with themselves).
|
|
36
|
+
const ALWAYS_SKIP_EXACT = new Set([
|
|
37
|
+
".cursor/rules/speclock.mdc",
|
|
38
|
+
".windsurf/rules/speclock.md",
|
|
39
|
+
".aider.conf.yml",
|
|
40
|
+
".mcp.json",
|
|
41
|
+
"package-lock.json",
|
|
42
|
+
"yarn.lock",
|
|
43
|
+
"pnpm-lock.yaml",
|
|
44
|
+
"Cargo.lock",
|
|
45
|
+
"poetry.lock",
|
|
46
|
+
"Gemfile.lock",
|
|
47
|
+
"composer.lock",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
// Directory prefixes that are always skipped.
|
|
51
|
+
const ALWAYS_SKIP_DIR_PREFIXES = [
|
|
52
|
+
".speclock/",
|
|
53
|
+
"node_modules/",
|
|
54
|
+
"dist/",
|
|
55
|
+
"build/",
|
|
56
|
+
".next/",
|
|
57
|
+
".nuxt/",
|
|
58
|
+
"__pycache__/",
|
|
59
|
+
".venv/",
|
|
60
|
+
"venv/",
|
|
61
|
+
".cache/",
|
|
62
|
+
"coverage/",
|
|
63
|
+
".turbo/",
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
// Files that are skipped ONLY if their content carries the SpecLock
|
|
67
|
+
// auto-generated marker (so hand-written AGENTS.md etc. still get audited).
|
|
68
|
+
// CLAUDE.md is included because `speclock protect` seeds it with the active
|
|
69
|
+
// locks — on the initial commit after protect, the file literally IS the
|
|
70
|
+
// locks, so every semantic check would produce a false positive.
|
|
71
|
+
const CONDITIONAL_SKIP_IF_AUTOGEN = new Set([
|
|
72
|
+
"AGENTS.md",
|
|
73
|
+
"GEMINI.md",
|
|
74
|
+
"CLAUDE.md",
|
|
75
|
+
".github/copilot-instructions.md",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalize a path to forward slashes for comparison.
|
|
80
|
+
*/
|
|
81
|
+
function normalizePath(p) {
|
|
82
|
+
return (p || "").replace(/\\/g, "/");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Decide whether a file should be skipped by the semantic pre-commit audit.
|
|
87
|
+
* This is the single source of truth for "is this a SpecLock internal file
|
|
88
|
+
* or generated noise we should not audit".
|
|
89
|
+
*
|
|
90
|
+
* @param {string} file - repo-relative path
|
|
91
|
+
* @param {string} root - repo root (to check content of conditional files)
|
|
92
|
+
* @returns {boolean} true if the file should be skipped
|
|
93
|
+
*/
|
|
94
|
+
export function shouldSkipForSemanticAudit(file, root) {
|
|
95
|
+
const norm = normalizePath(file);
|
|
96
|
+
|
|
97
|
+
// 1. Binary extensions
|
|
98
|
+
const ext = path.extname(norm).slice(1).toLowerCase();
|
|
99
|
+
if (BINARY_EXTENSIONS.has(ext)) return true;
|
|
100
|
+
|
|
101
|
+
// 2. Exact path matches (lockfiles, auto-generated rules files, .mcp.json)
|
|
102
|
+
if (ALWAYS_SKIP_EXACT.has(norm)) return true;
|
|
103
|
+
|
|
104
|
+
// 3. Directory prefix matches
|
|
105
|
+
for (const prefix of ALWAYS_SKIP_DIR_PREFIXES) {
|
|
106
|
+
if (norm === prefix.slice(0, -1) || norm.startsWith(prefix)) return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Conditionally-skipped files (only if they carry the auto-gen marker)
|
|
110
|
+
if (CONDITIONAL_SKIP_IF_AUTOGEN.has(norm)) {
|
|
111
|
+
try {
|
|
112
|
+
const fullPath = path.join(root, file);
|
|
113
|
+
if (fs.existsSync(fullPath)) {
|
|
114
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
115
|
+
if (content.includes(SPECLOCK_AUTOGEN_MARKER)) return true;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore read errors */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
30
123
|
/**
|
|
31
124
|
* Parse a unified diff into per-file change blocks.
|
|
32
125
|
* Returns array of { file, addedLines, removedLines, hunks }.
|
|
@@ -191,8 +284,14 @@ export function semanticAudit(root) {
|
|
|
191
284
|
};
|
|
192
285
|
}
|
|
193
286
|
|
|
194
|
-
// Parse diff into per-file changes
|
|
195
|
-
|
|
287
|
+
// Parse diff into per-file changes, then drop SpecLock-internal / generated
|
|
288
|
+
// files so we don't flood the user with false positives from files that
|
|
289
|
+
// SpecLock itself creates or manages.
|
|
290
|
+
const allFileChanges = parseDiff(diff);
|
|
291
|
+
const fileChanges = allFileChanges.filter(
|
|
292
|
+
(fc) => !shouldSkipForSemanticAudit(fc.file, root)
|
|
293
|
+
);
|
|
294
|
+
const skippedCount = allFileChanges.length - fileChanges.length;
|
|
196
295
|
const violations = [];
|
|
197
296
|
|
|
198
297
|
for (const fc of fileChanges) {
|
|
@@ -276,6 +375,7 @@ export function semanticAudit(root) {
|
|
|
276
375
|
blocked,
|
|
277
376
|
violations: uniqueViolations,
|
|
278
377
|
filesChecked: fileChanges.length,
|
|
378
|
+
filesSkipped: skippedCount,
|
|
279
379
|
activeLocks: activeLocks.length,
|
|
280
380
|
mode: config.mode,
|
|
281
381
|
threshold: config.blockThreshold,
|
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.6 — 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.6 — 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.6";
|
|
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.6";
|
|
130
130
|
const AUTHOR = "Sandeep Roy";
|
|
131
131
|
|
|
132
132
|
const server = new McpServer(
|