speclock 5.5.5 → 5.5.7
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 +354 -24
- package/src/core/compliance.js +1 -1
- package/src/core/guardian.js +466 -457
- package/src/core/hooks.js +109 -91
- package/src/core/pre-commit-semantic.js +102 -2
- package/src/core/semantics.js +3019 -2717
- package/src/core/telemetry.js +940 -852
- 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,
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
ensureTelemetryDecision,
|
|
69
69
|
recordCommand,
|
|
70
70
|
TELEMETRY_DEFAULT_ENDPOINT,
|
|
71
|
+
buildUsageStats,
|
|
71
72
|
} from "../core/telemetry.js";
|
|
72
73
|
import {
|
|
73
74
|
isSSOEnabled,
|
|
@@ -286,7 +287,7 @@ export function initFromRulePack(root, framework) {
|
|
|
286
287
|
|
|
287
288
|
function printHelp() {
|
|
288
289
|
console.log(`
|
|
289
|
-
SpecLock v5.5.
|
|
290
|
+
SpecLock v5.5.7 — Your AI has rules. SpecLock makes them unbreakable.
|
|
290
291
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
291
292
|
|
|
292
293
|
Usage: speclock <command> [options]
|
|
@@ -340,6 +341,7 @@ Commands:
|
|
|
340
341
|
watch Start file watcher (live dashboard)
|
|
341
342
|
serve [--project <path>] Start MCP stdio server
|
|
342
343
|
status Show project brain summary
|
|
344
|
+
stats Show YOUR usage dashboard from local telemetry log
|
|
343
345
|
doctor Diagnostic health check (install, git, rules, MCP)
|
|
344
346
|
|
|
345
347
|
Options:
|
|
@@ -442,6 +444,152 @@ function showStatus(root) {
|
|
|
442
444
|
console.log("");
|
|
443
445
|
}
|
|
444
446
|
|
|
447
|
+
// --- Stats dashboard (speclock stats) ---
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Build a self-contained view-model for the `speclock stats` dashboard.
|
|
451
|
+
* Pure data — no console I/O — so tests can assert against it.
|
|
452
|
+
*
|
|
453
|
+
* Combines three sources:
|
|
454
|
+
* 1. Local telemetry log (~/.speclock/telemetry.jsonl) via buildUsageStats
|
|
455
|
+
* 2. Current project brain.json (enforcement mode, lock count)
|
|
456
|
+
* 3. Rule file discovery + MCP client detection (from a sample telemetry event)
|
|
457
|
+
*
|
|
458
|
+
* Falls back gracefully when telemetry is disabled or the log is missing —
|
|
459
|
+
* the "Current State" section is still populated from brain.json.
|
|
460
|
+
*
|
|
461
|
+
* @param {string} root - project root
|
|
462
|
+
* @param {object} [opts] - passed through to buildUsageStats (e.g. { events, now })
|
|
463
|
+
*/
|
|
464
|
+
export function buildStatsView(root, opts = {}) {
|
|
465
|
+
const usage = buildUsageStats(opts);
|
|
466
|
+
|
|
467
|
+
let lockCount = 0;
|
|
468
|
+
let enforcementMode = "unknown";
|
|
469
|
+
let brainExists = false;
|
|
470
|
+
try {
|
|
471
|
+
const brain = readBrain(root);
|
|
472
|
+
if (brain) {
|
|
473
|
+
brainExists = true;
|
|
474
|
+
const items = brain.specLock && Array.isArray(brain.specLock.items)
|
|
475
|
+
? brain.specLock.items
|
|
476
|
+
: [];
|
|
477
|
+
lockCount = items.filter((l) => l && l.active !== false).length;
|
|
478
|
+
const cfg = getEnforcementConfig(brain);
|
|
479
|
+
// Map internal ("advisory" | "hard") to user-facing ("warn" | "hard").
|
|
480
|
+
enforcementMode = cfg.mode === "hard" ? "hard" : "warn";
|
|
481
|
+
}
|
|
482
|
+
} catch (_) { /* swallow */ }
|
|
483
|
+
|
|
484
|
+
// Rule files (from the same list guardian.js uses).
|
|
485
|
+
let ruleFiles = [];
|
|
486
|
+
try {
|
|
487
|
+
const discovered = discoverRuleFiles(root);
|
|
488
|
+
ruleFiles = discovered.map((f) => f.file);
|
|
489
|
+
} catch (_) { /* swallow */ }
|
|
490
|
+
|
|
491
|
+
// MCP clients — reuse the detector embedded in the sample telemetry event.
|
|
492
|
+
let mcpClients = [];
|
|
493
|
+
try {
|
|
494
|
+
const sample = getOptInTelemetryStatus({ eventLimit: 0 }).sampleEvent;
|
|
495
|
+
if (sample && Array.isArray(sample.mcpClientsConfigured)) {
|
|
496
|
+
mcpClients = sample.mcpClientsConfigured;
|
|
497
|
+
}
|
|
498
|
+
} catch (_) { /* swallow */ }
|
|
499
|
+
|
|
500
|
+
return {
|
|
501
|
+
...usage,
|
|
502
|
+
brainExists,
|
|
503
|
+
enforcementMode,
|
|
504
|
+
lockCount,
|
|
505
|
+
ruleFiles,
|
|
506
|
+
mcpClients,
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Render the stats view-model as a human-readable dashboard string.
|
|
512
|
+
* Kept separate from console output so tests can assert on the rendered
|
|
513
|
+
* text without capturing stdout.
|
|
514
|
+
*/
|
|
515
|
+
export function formatStatsDashboard(view) {
|
|
516
|
+
const lines = [];
|
|
517
|
+
const firstInstallDate = view.firstInstallIso
|
|
518
|
+
? view.firstInstallIso.slice(0, 10)
|
|
519
|
+
: "(unknown)";
|
|
520
|
+
const installIdShort = view.installId && view.installId !== "unknown"
|
|
521
|
+
? view.installId.slice(0, 8) + "..."
|
|
522
|
+
: "(none)";
|
|
523
|
+
|
|
524
|
+
lines.push("");
|
|
525
|
+
lines.push("SpecLock Stats — Your Usage");
|
|
526
|
+
lines.push("=".repeat(32));
|
|
527
|
+
lines.push("");
|
|
528
|
+
lines.push("Installation");
|
|
529
|
+
lines.push(` First install: ${firstInstallDate}`);
|
|
530
|
+
lines.push(` Days active: ${view.daysActive}`);
|
|
531
|
+
lines.push(` Total events: ${view.totalEvents}`);
|
|
532
|
+
lines.push(` Install ID: ${installIdShort}`);
|
|
533
|
+
lines.push("");
|
|
534
|
+
|
|
535
|
+
lines.push("Commands Used");
|
|
536
|
+
const entries = Object.entries(view.commandsByType).sort(
|
|
537
|
+
([, a], [, b]) => b - a
|
|
538
|
+
);
|
|
539
|
+
if (entries.length === 0) {
|
|
540
|
+
if (view.telemetryEnabled) {
|
|
541
|
+
lines.push(" (no events recorded yet — run some commands!)");
|
|
542
|
+
} else {
|
|
543
|
+
lines.push(" (telemetry disabled — enable with 'speclock telemetry on' to track usage)");
|
|
544
|
+
}
|
|
545
|
+
} else {
|
|
546
|
+
const maxName = Math.max(...entries.map(([n]) => n.length));
|
|
547
|
+
for (const [name, count] of entries) {
|
|
548
|
+
lines.push(` ${(name + ":").padEnd(maxName + 2)}${count}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
lines.push("");
|
|
552
|
+
|
|
553
|
+
lines.push("Current State");
|
|
554
|
+
lines.push(` Enforcement: ${view.enforcementMode}`);
|
|
555
|
+
lines.push(` Locks: ${view.lockCount}`);
|
|
556
|
+
if (view.ruleFiles.length > 0) {
|
|
557
|
+
lines.push(` Rule files: ${view.ruleFiles.length} (${view.ruleFiles.join(", ")})`);
|
|
558
|
+
} else {
|
|
559
|
+
lines.push(" Rule files: 0");
|
|
560
|
+
}
|
|
561
|
+
if (view.mcpClients.length > 0) {
|
|
562
|
+
lines.push(` MCP clients: ${view.mcpClients.join(", ")}`);
|
|
563
|
+
} else {
|
|
564
|
+
lines.push(" MCP clients: (none detected)");
|
|
565
|
+
}
|
|
566
|
+
lines.push("");
|
|
567
|
+
|
|
568
|
+
lines.push(`Recent Activity (last ${view.recentEvents.length})`);
|
|
569
|
+
if (view.recentEvents.length === 0) {
|
|
570
|
+
lines.push(" (no activity recorded)");
|
|
571
|
+
} else {
|
|
572
|
+
// Most recent first in the dashboard.
|
|
573
|
+
const sorted = view.recentEvents.slice().reverse();
|
|
574
|
+
for (const e of sorted) {
|
|
575
|
+
const ts = (e.timestamp || "").replace("T", " ").slice(0, 16);
|
|
576
|
+
const cmd = (e.command || "unknown").padEnd(10);
|
|
577
|
+
const exit = typeof e.exitCode === "number" ? e.exitCode : "?";
|
|
578
|
+
lines.push(` ${ts} ${cmd} exit ${exit}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
lines.push("");
|
|
582
|
+
|
|
583
|
+
if (!view.telemetryEnabled) {
|
|
584
|
+
lines.push("Note: telemetry is DISABLED — stats above reflect any pre-existing log");
|
|
585
|
+
lines.push(" plus the current project state from .speclock/brain.json.");
|
|
586
|
+
}
|
|
587
|
+
lines.push("Tip: Run 'speclock telemetry status' to see telemetry settings");
|
|
588
|
+
lines.push("");
|
|
589
|
+
|
|
590
|
+
return lines.join("\n");
|
|
591
|
+
}
|
|
592
|
+
|
|
445
593
|
// --- Main ---
|
|
446
594
|
|
|
447
595
|
async function main() {
|
|
@@ -491,9 +639,16 @@ async function main() {
|
|
|
491
639
|
console.log(`Created SPECLOCK.md (AI instructions file).`);
|
|
492
640
|
|
|
493
641
|
// 4. Inject marker into package.json (so AI tools auto-discover SpecLock)
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
642
|
+
// — only when package.json actually exists. We do NOT create one.
|
|
643
|
+
const pkgJsonPath = path.join(root, "package.json");
|
|
644
|
+
const pkgJsonExistedBefore = fs.existsSync(pkgJsonPath);
|
|
645
|
+
let pkgJsonUpdated = false;
|
|
646
|
+
if (pkgJsonExistedBefore) {
|
|
647
|
+
const pkgResult = injectPackageJsonMarker(root);
|
|
648
|
+
if (pkgResult.success) {
|
|
649
|
+
pkgJsonUpdated = true;
|
|
650
|
+
console.log("Injected SpecLock marker into package.json.");
|
|
651
|
+
}
|
|
497
652
|
}
|
|
498
653
|
|
|
499
654
|
// 5. Apply template if specified
|
|
@@ -511,6 +666,9 @@ async function main() {
|
|
|
511
666
|
console.log("Generated .speclock/context/latest.md");
|
|
512
667
|
|
|
513
668
|
// 7. Print summary
|
|
669
|
+
const pkgJsonLine = pkgJsonUpdated
|
|
670
|
+
? " package.json — Active locks embedded (AI auto-discovery)"
|
|
671
|
+
: " package.json (skipped — not found)";
|
|
514
672
|
console.log(`
|
|
515
673
|
SpecLock is ready!
|
|
516
674
|
|
|
@@ -518,7 +676,7 @@ Files created/updated:
|
|
|
518
676
|
.speclock/brain.json — Project memory
|
|
519
677
|
.speclock/context/latest.md — Context for AI (read this)
|
|
520
678
|
SPECLOCK.md — AI rules (read this)
|
|
521
|
-
|
|
679
|
+
${pkgJsonLine}
|
|
522
680
|
|
|
523
681
|
Next steps:
|
|
524
682
|
To add constraints: npx speclock lock "Never touch auth files"
|
|
@@ -1243,6 +1401,10 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1243
1401
|
// --- AUDIT-SEMANTIC (v2.5) ---
|
|
1244
1402
|
if (cmd === "audit-semantic") {
|
|
1245
1403
|
const flags = parseFlags(args);
|
|
1404
|
+
const verbose =
|
|
1405
|
+
flags.verbose === true ||
|
|
1406
|
+
process.env.SPECLOCK_VERBOSE === "1" ||
|
|
1407
|
+
process.env.SPECLOCK_VERBOSE === "true";
|
|
1246
1408
|
const result = semanticAudit(root);
|
|
1247
1409
|
|
|
1248
1410
|
// --- Commit-message + diff-content semantic check ---
|
|
@@ -1269,9 +1431,15 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1269
1431
|
} catch { /* ignore */ }
|
|
1270
1432
|
}
|
|
1271
1433
|
|
|
1272
|
-
// 2. Collect added lines from the staged diff
|
|
1434
|
+
// 2. Collect added lines from the staged diff, skipping SpecLock-internal
|
|
1435
|
+
// files (see shouldSkipForSemanticAudit). Those files' contents literally
|
|
1436
|
+
// restate the locks, so scanning their added lines produces 100%
|
|
1437
|
+
// false-positive matches for every lock in the brain.
|
|
1273
1438
|
const diffText = getStagedDiff(root);
|
|
1274
|
-
const
|
|
1439
|
+
const allParsedChanges = diffText ? parseDiff(diffText) : [];
|
|
1440
|
+
const fileChanges = allParsedChanges.filter(
|
|
1441
|
+
(fc) => !shouldSkipForSemanticAudit(fc.file, root)
|
|
1442
|
+
);
|
|
1275
1443
|
const addedSnippets = [];
|
|
1276
1444
|
for (const fc of fileChanges) {
|
|
1277
1445
|
for (const line of fc.addedLines) {
|
|
@@ -1345,24 +1513,85 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1345
1513
|
: `WARNING: ${result.violations.length} violation(s) detected. Review before proceeding.`;
|
|
1346
1514
|
}
|
|
1347
1515
|
|
|
1348
|
-
// Warn mode default: only exit 1 if --strict, SPECLOCK_STRICT=1, or brain is in "hard" mode
|
|
1349
|
-
//
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1516
|
+
// Warn mode default: only exit 1 if --strict, SPECLOCK_STRICT=1, or brain is in "hard" mode.
|
|
1517
|
+
//
|
|
1518
|
+
// Enforcement mode precedence (first match wins):
|
|
1519
|
+
// 1. --strict / --block CLI flag
|
|
1520
|
+
// 2. SPECLOCK_STRICT=1 env var
|
|
1521
|
+
// 3. brain.enforcement.mode === "hard" from .speclock/brain.json
|
|
1522
|
+
// 4. Default: warn mode (exit 0)
|
|
1523
|
+
//
|
|
1524
|
+
// We re-read brain.json here as a belt-and-braces fallback because
|
|
1525
|
+
// semanticAudit() may early-return (no staged diff, no locks, brain
|
|
1526
|
+
// missing) WITHOUT setting result.mode/result.blocked, and git hooks
|
|
1527
|
+
// can run in sanitized environments where SPECLOCK_STRICT=1 on the
|
|
1528
|
+
// `git commit` command line gets stripped by some shells. The
|
|
1529
|
+
// persistent brain mode (set by `speclock enforce hard`) is the only
|
|
1530
|
+
// reliable way to enforce hard blocking across all git/shell combos.
|
|
1531
|
+
const cliStrict = flags.strict === true || flags.block === true;
|
|
1532
|
+
const envStrict =
|
|
1353
1533
|
process.env.SPECLOCK_STRICT === "1" ||
|
|
1354
|
-
process.env.SPECLOCK_STRICT === "true"
|
|
1355
|
-
|
|
1534
|
+
process.env.SPECLOCK_STRICT === "true";
|
|
1535
|
+
|
|
1536
|
+
let brainHardMode = false;
|
|
1537
|
+
try {
|
|
1538
|
+
const brainForMode = readBrain(root);
|
|
1539
|
+
if (brainForMode) {
|
|
1540
|
+
const cfg = getEnforcementConfig(brainForMode);
|
|
1541
|
+
brainHardMode = cfg.mode === "hard";
|
|
1542
|
+
}
|
|
1543
|
+
} catch { /* brain unreadable — treat as advisory */ }
|
|
1544
|
+
|
|
1545
|
+
// If brain is in hard mode but semanticAudit() returned without
|
|
1546
|
+
// reflecting that (early-return path), retro-fit the result so the
|
|
1547
|
+
// downstream printing + blocking decision stays consistent.
|
|
1548
|
+
if (brainHardMode && result.mode !== "hard") {
|
|
1549
|
+
result.mode = "hard";
|
|
1550
|
+
if (result.threshold === undefined) result.threshold = 70;
|
|
1551
|
+
}
|
|
1552
|
+
if (brainHardMode && !result.blocked) {
|
|
1553
|
+
const thresh = result.threshold || 70;
|
|
1554
|
+
result.blocked = (result.violations || []).some(
|
|
1555
|
+
(v) => (v.confidence || 0) >= thresh
|
|
1556
|
+
);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const strict = cliStrict || envStrict || brainHardMode || result.blocked;
|
|
1560
|
+
|
|
1561
|
+
// --- Three-tier output filter (v5.5.7) ---
|
|
1562
|
+
// Investor audit: walls of LOW-confidence matches are user-hostile.
|
|
1563
|
+
// Only HIGH and MEDIUM print by default. LOW rolls up into a one-liner.
|
|
1564
|
+
// --verbose / SPECLOCK_VERBOSE=1 shows everything.
|
|
1565
|
+
const OUTPUT_MIN_CONFIDENCE = 40; // below this = "LOW", hidden by default
|
|
1566
|
+
const MAX_VISIBLE_VIOLATIONS = 10; // hard cap on printed items
|
|
1567
|
+
|
|
1568
|
+
const allViolations = result.violations || [];
|
|
1569
|
+
const highViolations = allViolations.filter((v) => (v.confidence || 0) >= 70);
|
|
1570
|
+
const mediumViolations = allViolations.filter(
|
|
1571
|
+
(v) => (v.confidence || 0) >= OUTPUT_MIN_CONFIDENCE && (v.confidence || 0) < 70
|
|
1572
|
+
);
|
|
1573
|
+
const lowViolations = allViolations.filter(
|
|
1574
|
+
(v) => (v.confidence || 0) < OUTPUT_MIN_CONFIDENCE
|
|
1575
|
+
);
|
|
1576
|
+
|
|
1577
|
+
// What actually gets printed
|
|
1578
|
+
const visibleViolations = verbose
|
|
1579
|
+
? allViolations
|
|
1580
|
+
: [...highViolations, ...mediumViolations];
|
|
1356
1581
|
|
|
1357
1582
|
console.log(`\nSemantic Pre-Commit Audit`);
|
|
1358
1583
|
console.log("=".repeat(50));
|
|
1359
1584
|
console.log(`Mode: ${result.mode} | Threshold: ${result.threshold}%`);
|
|
1360
|
-
|
|
1585
|
+
const filesLine = result.filesSkipped
|
|
1586
|
+
? `Files analyzed: ${result.filesChecked} (${result.filesSkipped} skipped)`
|
|
1587
|
+
: `Files analyzed: ${result.filesChecked}`;
|
|
1588
|
+
console.log(filesLine);
|
|
1361
1589
|
console.log(`Active locks: ${result.activeLocks}`);
|
|
1362
|
-
|
|
1363
|
-
if (
|
|
1590
|
+
|
|
1591
|
+
if (visibleViolations.length > 0) {
|
|
1364
1592
|
console.log("");
|
|
1365
|
-
|
|
1593
|
+
const toPrint = visibleViolations.slice(0, MAX_VISIBLE_VIOLATIONS);
|
|
1594
|
+
for (const v of toPrint) {
|
|
1366
1595
|
console.log(` [${v.level}] ${v.file} (confidence: ${v.confidence}%)`);
|
|
1367
1596
|
console.log(` Lock: "${v.lockText}"`);
|
|
1368
1597
|
console.log(` Reason: ${v.reason}`);
|
|
@@ -1370,17 +1599,48 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1370
1599
|
console.log(` Changes: +${v.addedLines} / -${v.removedLines} lines`);
|
|
1371
1600
|
}
|
|
1372
1601
|
}
|
|
1602
|
+
const hiddenByCap = visibleViolations.length - toPrint.length;
|
|
1603
|
+
if (hiddenByCap > 0) {
|
|
1604
|
+
console.log(` ... + ${hiddenByCap} more (output capped at ${MAX_VISIBLE_VIOLATIONS})`);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
// LOW-confidence rollup
|
|
1609
|
+
if (!verbose && lowViolations.length > 0) {
|
|
1610
|
+
console.log(
|
|
1611
|
+
` + ${lowViolations.length} low-confidence match(es) hidden (use --verbose or SPECLOCK_VERBOSE=1 to see)`
|
|
1612
|
+
);
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
// --- New summary line ---
|
|
1616
|
+
console.log("");
|
|
1617
|
+
let summaryLine;
|
|
1618
|
+
if (highViolations.length === 0 && mediumViolations.length === 0) {
|
|
1619
|
+
summaryLine = `[OK] ${result.filesChecked} file(s) checked, no concerns.`;
|
|
1620
|
+
} else if (highViolations.length > 0) {
|
|
1621
|
+
summaryLine = `[!] ${highViolations.length} HIGH-confidence concern(s) — review before merging.`;
|
|
1622
|
+
} else {
|
|
1623
|
+
summaryLine = `[i] ${mediumViolations.length} medium-confidence note(s) (informational).`;
|
|
1624
|
+
}
|
|
1625
|
+
console.log(summaryLine);
|
|
1626
|
+
|
|
1627
|
+
// Preserve the machine-readable status message for any callers that grep for it
|
|
1628
|
+
if (result.blocked) {
|
|
1629
|
+
console.log(
|
|
1630
|
+
`BLOCKED: ${highViolations.length} high-confidence violation(s) — hard enforcement active.`
|
|
1631
|
+
);
|
|
1373
1632
|
}
|
|
1374
|
-
console.log(`\n${result.message}`);
|
|
1375
1633
|
|
|
1376
|
-
if (
|
|
1634
|
+
if (highViolations.length > 0 && !strict) {
|
|
1377
1635
|
console.log("\nWarning mode active — commit allowed. To enforce hard blocks, run:");
|
|
1378
1636
|
console.log(" speclock audit-semantic --strict");
|
|
1379
1637
|
console.log(" SPECLOCK_STRICT=1 git commit ...");
|
|
1380
1638
|
console.log(" speclock enforce hard (persistent, project-wide)");
|
|
1381
1639
|
}
|
|
1382
1640
|
|
|
1383
|
-
|
|
1641
|
+
// Blocking decision is still driven by the engine's 70% threshold, so
|
|
1642
|
+
// only HIGH-confidence matches can ever cause a non-zero exit.
|
|
1643
|
+
process.exit(strict && highViolations.length > 0 ? 1 : 0);
|
|
1384
1644
|
}
|
|
1385
1645
|
|
|
1386
1646
|
// --- AUTH (v3.0) ---
|
|
@@ -1563,6 +1823,18 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1563
1823
|
process.exit(1);
|
|
1564
1824
|
}
|
|
1565
1825
|
|
|
1826
|
+
// --- STATS (user-facing usage dashboard) ---
|
|
1827
|
+
if (cmd === "stats") {
|
|
1828
|
+
try {
|
|
1829
|
+
const view = buildStatsView(root);
|
|
1830
|
+
console.log(formatStatsDashboard(view));
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
console.error(`Failed to build stats: ${err.message}`);
|
|
1833
|
+
process.exit(1);
|
|
1834
|
+
}
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1566
1838
|
// --- TELEMETRY (opt-in, v5.5) ---
|
|
1567
1839
|
if (cmd === "telemetry") {
|
|
1568
1840
|
const sub = args[0];
|
|
@@ -1987,21 +2259,79 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1987
2259
|
lines.push("");
|
|
1988
2260
|
|
|
1989
2261
|
// --- 3. Rule Files ---
|
|
2262
|
+
// Doctor checks both:
|
|
2263
|
+
// (a) ORIGINAL rule files (.cursorrules, CLAUDE.md, etc.) that users author
|
|
2264
|
+
// (b) SYNCED files written by `speclock protect` / `speclock sync`
|
|
2265
|
+
// (.cursor/rules/speclock.mdc, .windsurf/rules/speclock.md, AGENTS.md
|
|
2266
|
+
// with SpecLock marker, GEMINI.md, .github/copilot-instructions.md,
|
|
2267
|
+
// .aider.conf.yml)
|
|
2268
|
+
// A file is considered "synced" if it has a SpecLock auto-gen marker in its header.
|
|
1990
2269
|
lines.push("Rule Files");
|
|
2270
|
+
|
|
2271
|
+
// All the files `speclock sync` can produce (mirrors FORMATS in rules-sync.js)
|
|
2272
|
+
const SYNCED_OUTPUT_FILES = [
|
|
2273
|
+
".cursor/rules/speclock.mdc",
|
|
2274
|
+
".windsurf/rules/speclock.md",
|
|
2275
|
+
".github/copilot-instructions.md",
|
|
2276
|
+
"GEMINI.md",
|
|
2277
|
+
".aider.conf.yml",
|
|
2278
|
+
"AGENTS.md",
|
|
2279
|
+
];
|
|
2280
|
+
|
|
2281
|
+
// Markers that indicate a file was written by SpecLock's sync pipeline.
|
|
2282
|
+
// Must match the markers used by isSpeclockGenerated() in guardian.js.
|
|
2283
|
+
const SPECLOCK_DOCTOR_SYNC_MARKERS = [
|
|
2284
|
+
"Auto-synced from SpecLock",
|
|
2285
|
+
"Auto-synced by SpecLock",
|
|
2286
|
+
"Auto-synced.",
|
|
2287
|
+
"(SpecLock)",
|
|
2288
|
+
"# SpecLock Constraints",
|
|
2289
|
+
"Do not edit manually — run `speclock sync`",
|
|
2290
|
+
"speclock sync --format",
|
|
2291
|
+
"speclock_session_briefing",
|
|
2292
|
+
];
|
|
2293
|
+
|
|
2294
|
+
function isSyncedFile(absPath) {
|
|
2295
|
+
try {
|
|
2296
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
2297
|
+
const header = content.split("\n").slice(0, 10).join("\n");
|
|
2298
|
+
return SPECLOCK_DOCTOR_SYNC_MARKERS.some((m) => header.includes(m));
|
|
2299
|
+
} catch (_) {
|
|
2300
|
+
return false;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
|
|
1991
2304
|
const discovered = discoverRuleFiles(root);
|
|
1992
2305
|
const discoveredMap = new Map(discovered.map((f) => [f.file, f]));
|
|
2306
|
+
const shownFiles = new Set();
|
|
1993
2307
|
let totalRuleFilesFound = 0;
|
|
2308
|
+
|
|
2309
|
+
// (a) Original/authored rule files
|
|
1994
2310
|
for (const entry of RULE_FILES) {
|
|
1995
2311
|
const found = discoveredMap.get(entry.file);
|
|
1996
2312
|
if (found) {
|
|
1997
2313
|
const extracted = extractConstraints(found.content, found.file);
|
|
1998
2314
|
lines.push(` ✓ ${entry.file} (${extracted.locks.length} locks extracted)`);
|
|
2315
|
+
shownFiles.add(entry.file);
|
|
2316
|
+
totalRuleFilesFound++;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// (b) Synced files written by `speclock protect` / `speclock sync`
|
|
2321
|
+
for (const relPath of SYNCED_OUTPUT_FILES) {
|
|
2322
|
+
if (shownFiles.has(relPath)) continue; // already shown as an authored file
|
|
2323
|
+
const abs = path.join(root, relPath);
|
|
2324
|
+
if (!fs.existsSync(abs)) continue;
|
|
2325
|
+
if (isSyncedFile(abs)) {
|
|
2326
|
+
lines.push(` ✓ ${relPath} (synced)`);
|
|
2327
|
+
shownFiles.add(relPath);
|
|
1999
2328
|
totalRuleFilesFound++;
|
|
2000
|
-
} else {
|
|
2001
|
-
lines.push(` ✗ ${entry.file} (not found)`);
|
|
2002
2329
|
}
|
|
2003
2330
|
}
|
|
2331
|
+
|
|
2332
|
+
// If nothing at all was found, show a clear diagnostic.
|
|
2004
2333
|
if (totalRuleFilesFound === 0) {
|
|
2334
|
+
lines.push(` ✗ No rule files found`);
|
|
2005
2335
|
fixes.push("Run: speclock protect (auto-creates a starter CLAUDE.md)");
|
|
2006
2336
|
issueCount++;
|
|
2007
2337
|
}
|
package/src/core/compliance.js
CHANGED