speclock 5.3.1 → 5.4.0
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 +82 -6
- package/package.json +2 -2
- package/src/cli/index.js +39 -1
- package/src/core/compliance.js +1 -1
- package/src/core/coverage.js +274 -0
- package/src/core/drift-score.js +201 -0
- package/src/core/strengthen.js +199 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +3 -3
- package/src/mcp/server.js +43 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<a href="https://www.npmjs.com/package/speclock"><img src="https://img.shields.io/npm/v/speclock.svg?style=flat-square&color=4F46E5" alt="npm version" /></a>
|
|
9
9
|
<a href="https://www.npmjs.com/package/speclock"><img src="https://img.shields.io/npm/dm/speclock.svg?style=flat-square&color=22C55E" alt="npm downloads" /></a>
|
|
10
10
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="MIT License" /></a>
|
|
11
|
-
<a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-42_tools-green.svg?style=flat-square" alt="MCP
|
|
11
|
+
<a href="https://modelcontextprotocol.io"><img src="https://img.shields.io/badge/MCP-42_tools-green.svg?style=flat-square" alt="MCP 49 tools" /></a>
|
|
12
12
|
</p>
|
|
13
13
|
|
|
14
14
|
<p align="center">
|
|
@@ -205,6 +205,73 @@ One command. Instant protection. `npx speclock setup --template safe-defaults`
|
|
|
205
205
|
|
|
206
206
|
---
|
|
207
207
|
|
|
208
|
+
## Drift Score (v5.4)
|
|
209
|
+
|
|
210
|
+
How much has your AI-built project drifted from your original intent? Only SpecLock can answer this — because only SpecLock knows what was *intended* vs what was *done*.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
$ speclock drift
|
|
214
|
+
|
|
215
|
+
Drift Score: 23/100 (B) — minor drift
|
|
216
|
+
Trend: improving | Period: 30 days | Active locks: 8
|
|
217
|
+
|
|
218
|
+
Signal Breakdown:
|
|
219
|
+
Violations: 6/30 (4 violations in 12 checks)
|
|
220
|
+
Overrides: 5/20 (1 override)
|
|
221
|
+
Reverts: 3/15 (1 revert detected)
|
|
222
|
+
Lock churn: 0/15 (0 removed, 3 added)
|
|
223
|
+
Goal stability: 0/10 (1 goal change)
|
|
224
|
+
Session gaps: 9/10 (3/5 unsummarized)
|
|
225
|
+
|
|
226
|
+
README badge: 
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Put the badge in your README. Show the world your AI respects your architecture.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Lock Coverage Audit (v5.4)
|
|
234
|
+
|
|
235
|
+
SpecLock scans your codebase and tells you what's **unprotected**:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
$ speclock coverage
|
|
239
|
+
|
|
240
|
+
Lock Coverage: 60% (B) — partially protected
|
|
241
|
+
|
|
242
|
+
[COVERED] CRITICAL authentication 2 file(s)
|
|
243
|
+
[EXPOSED] CRITICAL payments 1 file(s)
|
|
244
|
+
[COVERED] CRITICAL secrets 0 file(s)
|
|
245
|
+
[COVERED] HIGH api-routes 2 file(s)
|
|
246
|
+
|
|
247
|
+
Suggested Locks (ready to apply):
|
|
248
|
+
1. [CRITICAL] payments (1 file at risk)
|
|
249
|
+
speclock lock "Never modify payment processing or billing without permission"
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Like a security scanner, but for AI constraint gaps. Solo founders building fast don't know what they haven't protected — SpecLock tells them.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Lock Strengthener (v5.4)
|
|
257
|
+
|
|
258
|
+
Your locks might be too vague. SpecLock grades each one and suggests improvements:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
$ speclock strengthen
|
|
262
|
+
|
|
263
|
+
Lock Strength: 72/100 (B) — 3 strong, 1 weak
|
|
264
|
+
|
|
265
|
+
[WEAK ] 45/100 (D) "don't touch auth"
|
|
266
|
+
Issue: Too vague — short locks miss edge cases
|
|
267
|
+
Issue: No specific scope
|
|
268
|
+
Suggested: "Never modify, refactor, or delete auth..."
|
|
269
|
+
|
|
270
|
+
[STRONG] 90/100 (A) "Never expose API keys in client-side code, logs, or error messages"
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
208
275
|
## Semantic Engine
|
|
209
276
|
|
|
210
277
|
Not keyword matching — **real semantic analysis** with Gemini Flash hybrid for universal domain coverage. Scored **100/100** on Claude's independent adversarial test battery (7 suites, including false positives, question framing, patch gateway, and diff analysis).
|
|
@@ -514,7 +581,7 @@ POST /api/v2/graph/build
|
|
|
514
581
|
|
|
515
582
|
---
|
|
516
583
|
|
|
517
|
-
##
|
|
584
|
+
## 49 MCP Tools
|
|
518
585
|
|
|
519
586
|
<details>
|
|
520
587
|
<summary><b>Memory</b> — goal, locks, decisions, notes, deploy facts</summary>
|
|
@@ -631,6 +698,9 @@ POST /api/v2/graph/build
|
|
|
631
698
|
| `speclock_list_sync_formats` | List all available sync formats |
|
|
632
699
|
| `speclock_replay` | Replay a session's activity — what AI tried and what was caught |
|
|
633
700
|
| `speclock_list_sessions` | List available sessions for replay |
|
|
701
|
+
| `speclock_drift_score` | 0-100 project integrity metric — how much AI deviated from intent |
|
|
702
|
+
| `speclock_coverage` | Lock Coverage Audit — find unprotected code areas |
|
|
703
|
+
| `speclock_strengthen` | Grade locks and suggest stronger versions |
|
|
634
704
|
|
|
635
705
|
</details>
|
|
636
706
|
|
|
@@ -679,6 +749,12 @@ speclock replay # Replay last session
|
|
|
679
749
|
speclock replay --list # List sessions
|
|
680
750
|
speclock replay --session <id> # Replay specific session
|
|
681
751
|
|
|
752
|
+
# Project Health
|
|
753
|
+
speclock drift # Drift Score (0-100)
|
|
754
|
+
speclock drift --days 7 # Last 7 days only
|
|
755
|
+
speclock coverage # Lock Coverage Audit
|
|
756
|
+
speclock strengthen # Grade and improve locks
|
|
757
|
+
|
|
682
758
|
# Auth
|
|
683
759
|
speclock auth create-key --role developer
|
|
684
760
|
speclock auth rotate-key <keyId>
|
|
@@ -721,7 +797,7 @@ The AI opens the file and sees:
|
|
|
721
797
|
│ AI Tool (Claude Code, Cursor, Bolt.new...) │
|
|
722
798
|
└────────────┬──────────────────┬──────────────────┘
|
|
723
799
|
│ │
|
|
724
|
-
MCP Protocol (
|
|
800
|
+
MCP Protocol (49 tools) npm File-Based
|
|
725
801
|
│ (SPECLOCK.md + CLI)
|
|
726
802
|
│ │
|
|
727
803
|
┌────────────▼──────────────────▼──────────────────┐
|
|
@@ -791,7 +867,7 @@ The AI opens the file and sees:
|
|
|
791
867
|
| PII/Export Detection | 8 | 100% | SSN, email export, data access violations |
|
|
792
868
|
| **Total** | **929** | **100%** | **18 suites, 15+ domains** |
|
|
793
869
|
|
|
794
|
-
**External validation:** Claude's independent 7-suite adversarial test battery — **100/100 (100%)** on v5.
|
|
870
|
+
**External validation:** Claude's independent 7-suite adversarial test battery — **100/100 (100%)** on v5.4.0. Zero false positives. Zero missed violations. 15.7ms per check.
|
|
795
871
|
|
|
796
872
|
Tested across: fintech, e-commerce, IoT, healthcare, SaaS, gaming, biotech, aerospace, payments, payroll, robotics, autonomous systems, telecom, insurance, government. All 11 Indian payment gateways detected. Zero false positives on UI/cosmetic actions.
|
|
797
873
|
|
|
@@ -829,11 +905,11 @@ Issues and PRs welcome on [GitHub](https://github.com/sgroy10/speclock).
|
|
|
829
905
|
|
|
830
906
|
**SpecLock** is created and maintained by **[Sandeep Roy](https://github.com/sgroy10)**.
|
|
831
907
|
|
|
832
|
-
Sandeep Roy is the sole developer of SpecLock — the AI Constraint Engine that enforces project rules across AI coding sessions. All
|
|
908
|
+
Sandeep Roy is the sole developer of SpecLock — the AI Constraint Engine that enforces project rules across AI coding sessions. All 49 MCP tools, the semantic conflict detection engine, enterprise security features (SOC 2, HIPAA, RBAC, encryption), and the pre-publish test gate were designed and built by Sandeep Roy.
|
|
833
909
|
|
|
834
910
|
- GitHub: [@sgroy10](https://github.com/sgroy10)
|
|
835
911
|
- npm: [speclock](https://www.npmjs.com/package/speclock)
|
|
836
912
|
|
|
837
913
|
---
|
|
838
914
|
|
|
839
|
-
<p align="center"><i>SpecLock v5.
|
|
915
|
+
<p align="center"><i>SpecLock v5.4.0 — Developed by Sandeep Roy — 929 tests, 100% pass rate, 49 MCP tools, Universal Rules Sync, Incident Replay, AI Patch Firewall, Spec Compiler, Code Graph, Typed Constraints, Python SDK, ROS2, REST API v2. Because remembering isn't enough.</i></p>
|
package/package.json
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
"name": "speclock",
|
|
4
4
|
|
|
5
|
-
"version": "5.
|
|
5
|
+
"version": "5.4.0",
|
|
6
6
|
|
|
7
|
-
"description": "AI Constraint Engine by Sandeep Roy — Universal Rules Sync (one command syncs constraints to Cursor, Claude Code, Copilot, Windsurf, Gemini, Aider, AGENTS.md). AI Patch Firewall, diff-native review, Patch Gateway (ALLOW/WARN/BLOCK), Spec Compiler (NL→constraints), Code Graph (blast radius), Typed constraints, REST API v2, Python SDK, ROS2 integration.
|
|
7
|
+
"description": "AI Constraint Engine by Sandeep Roy — Universal Rules Sync (one command syncs constraints to Cursor, Claude Code, Copilot, Windsurf, Gemini, Aider, AGENTS.md). AI Patch Firewall, diff-native review, Patch Gateway (ALLOW/WARN/BLOCK), Spec Compiler (NL→constraints), Code Graph (blast radius), Typed constraints, REST API v2, Python SDK, ROS2 integration. 49 MCP tools, Gemini LLM hybrid, HMAC audit chain, RBAC, encryption, SOC 2/HIPAA compliance. Developed by Sandeep Roy.",
|
|
8
8
|
|
|
9
9
|
"type": "module",
|
|
10
10
|
|
package/src/cli/index.js
CHANGED
|
@@ -64,6 +64,9 @@ import {
|
|
|
64
64
|
} from "../core/sso.js";
|
|
65
65
|
import { syncRules, getSyncFormats } from "../core/rules-sync.js";
|
|
66
66
|
import { getReplay, listSessions, formatReplay } from "../core/replay.js";
|
|
67
|
+
import { computeDriftScore, formatDriftScore } from "../core/drift-score.js";
|
|
68
|
+
import { computeCoverage, formatCoverage } from "../core/coverage.js";
|
|
69
|
+
import { analyzeLockStrength, formatStrength } from "../core/strengthen.js";
|
|
67
70
|
|
|
68
71
|
// --- Argument parsing ---
|
|
69
72
|
|
|
@@ -119,7 +122,7 @@ function refreshContext(root) {
|
|
|
119
122
|
|
|
120
123
|
function printHelp() {
|
|
121
124
|
console.log(`
|
|
122
|
-
SpecLock v5.
|
|
125
|
+
SpecLock v5.4.0 — AI Constraint Engine (Universal Rules Sync + Spec Compiler + Code Graph + Typed Constraints + Python SDK + ROS2 + REST API v2 + Gemini LLM + Policy-as-Code + Auth + RBAC + Encryption)
|
|
123
126
|
Developed by Sandeep Roy (github.com/sgroy10)
|
|
124
127
|
|
|
125
128
|
Usage: speclock <command> [options]
|
|
@@ -157,6 +160,9 @@ Commands:
|
|
|
157
160
|
sync --preview <format> Preview without writing files
|
|
158
161
|
replay [--session <id>] Replay session activity — what AI tried & what was caught
|
|
159
162
|
replay --list List available sessions for replay
|
|
163
|
+
drift [--days 30] Drift Score — how much has AI deviated from intent (0-100)
|
|
164
|
+
coverage Lock Coverage Audit — find unprotected code areas
|
|
165
|
+
strengthen Lock Strengthener — grade locks and suggest improvements
|
|
160
166
|
watch Start file watcher (live dashboard)
|
|
161
167
|
serve [--project <path>] Start MCP stdio server
|
|
162
168
|
status Show project brain summary
|
|
@@ -1113,6 +1119,38 @@ Tip: Run "speclock sync --all" to push constraints to Cursor, Claude, Copilot, W
|
|
|
1113
1119
|
process.exit(1);
|
|
1114
1120
|
}
|
|
1115
1121
|
|
|
1122
|
+
// --- DRIFT (new: drift score) ---
|
|
1123
|
+
if (cmd === "drift") {
|
|
1124
|
+
const flags = parseFlags(args);
|
|
1125
|
+
const days = flags.days ? parseInt(flags.days, 10) : 30;
|
|
1126
|
+
const result = computeDriftScore(root, { days });
|
|
1127
|
+
|
|
1128
|
+
console.log(`\nSpecLock Drift Score`);
|
|
1129
|
+
console.log("=".repeat(60));
|
|
1130
|
+
console.log(formatDriftScore(result));
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// --- COVERAGE (new: lock coverage audit) ---
|
|
1135
|
+
if (cmd === "coverage") {
|
|
1136
|
+
const result = computeCoverage(root);
|
|
1137
|
+
|
|
1138
|
+
console.log(`\nSpecLock Lock Coverage Audit`);
|
|
1139
|
+
console.log("=".repeat(60));
|
|
1140
|
+
console.log(formatCoverage(result));
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// --- STRENGTHEN (new: lock strengthener) ---
|
|
1145
|
+
if (cmd === "strengthen") {
|
|
1146
|
+
const result = analyzeLockStrength(root);
|
|
1147
|
+
|
|
1148
|
+
console.log(`\nSpecLock Lock Strengthener`);
|
|
1149
|
+
console.log("=".repeat(60));
|
|
1150
|
+
console.log(formatStrength(result));
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1116
1154
|
// --- REPLAY (new: incident replay) ---
|
|
1117
1155
|
if (cmd === "replay") {
|
|
1118
1156
|
const flags = parseFlags(args);
|
package/src/core/compliance.js
CHANGED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Lock Coverage Audit — Find Unprotected Code
|
|
3
|
+
* Scans the codebase for high-risk patterns and identifies files/areas
|
|
4
|
+
* that have no lock covering them. Auto-suggests missing locks.
|
|
5
|
+
*
|
|
6
|
+
* Like a security scanner, but for AI constraint gaps.
|
|
7
|
+
*
|
|
8
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { readBrain } from "./storage.js";
|
|
14
|
+
import { ensureInit } from "./memory.js";
|
|
15
|
+
|
|
16
|
+
// --- High-risk patterns to detect ---
|
|
17
|
+
|
|
18
|
+
const RISK_PATTERNS = [
|
|
19
|
+
{
|
|
20
|
+
category: "authentication",
|
|
21
|
+
keywords: ["auth", "login", "signup", "session", "jwt", "token", "oauth", "passport", "credential"],
|
|
22
|
+
filePatterns: ["auth", "login", "signup", "session", "passport", "middleware/auth"],
|
|
23
|
+
severity: "critical",
|
|
24
|
+
suggestedLock: "Never modify authentication or authorization without explicit permission",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
category: "payments",
|
|
28
|
+
keywords: ["payment", "stripe", "billing", "checkout", "subscription", "invoice", "price", "razorpay", "paypal"],
|
|
29
|
+
filePatterns: ["payment", "billing", "checkout", "stripe", "subscription", "invoice"],
|
|
30
|
+
severity: "critical",
|
|
31
|
+
suggestedLock: "Never modify payment processing, billing, or subscription logic without explicit permission",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
category: "database",
|
|
35
|
+
keywords: ["migration", "schema", "model", "prisma", "knex", "sequelize", "typeorm", "drizzle"],
|
|
36
|
+
filePatterns: ["migration", "schema", "model", "prisma", "db", "database", "seed"],
|
|
37
|
+
severity: "high",
|
|
38
|
+
suggestedLock: "Database schema changes must not drop tables or columns — migrations must be additive only",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
category: "secrets",
|
|
42
|
+
keywords: ["env", "secret", "key", "credential", "password", "config"],
|
|
43
|
+
filePatterns: [".env", "config/secret", "credentials"],
|
|
44
|
+
severity: "critical",
|
|
45
|
+
suggestedLock: "Never expose API keys, secrets, or credentials in client-side code, logs, or error messages",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
category: "api-routes",
|
|
49
|
+
keywords: ["route", "endpoint", "api", "controller", "handler"],
|
|
50
|
+
filePatterns: ["routes/", "api/", "controllers/", "handlers/", "app/api/"],
|
|
51
|
+
severity: "high",
|
|
52
|
+
suggestedLock: "Never remove or change existing API endpoints without explicit permission — clients depend on stability",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
category: "security",
|
|
56
|
+
keywords: ["cors", "helmet", "csp", "csrf", "xss", "sanitize", "rate-limit", "rateLimit"],
|
|
57
|
+
filePatterns: ["security", "cors", "helmet", "middleware/rate"],
|
|
58
|
+
severity: "critical",
|
|
59
|
+
suggestedLock: "Security middleware (CORS, CSP, rate limiting) must not be weakened or removed",
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
category: "error-handling",
|
|
63
|
+
keywords: ["error", "catch", "exception", "fallback", "boundary"],
|
|
64
|
+
filePatterns: ["error", "boundary", "fallback", "exception"],
|
|
65
|
+
severity: "medium",
|
|
66
|
+
suggestedLock: "Never remove error handling, error boundaries, or fallback logic",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
category: "logging",
|
|
70
|
+
keywords: ["logger", "logging", "log", "monitor", "telemetry", "sentry", "datadog"],
|
|
71
|
+
filePatterns: ["logger", "logging", "monitor", "telemetry"],
|
|
72
|
+
severity: "medium",
|
|
73
|
+
suggestedLock: "Never disable logging, monitoring, or observability — these keep production alive",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
category: "testing",
|
|
77
|
+
keywords: ["test", "spec", "jest", "mocha", "vitest", "cypress", "playwright"],
|
|
78
|
+
filePatterns: ["test", "spec", "__tests__", "e2e", "cypress"],
|
|
79
|
+
severity: "low",
|
|
80
|
+
suggestedLock: "Never delete or skip existing tests — test coverage must not decrease",
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// Files/dirs to ignore
|
|
85
|
+
const IGNORE = [
|
|
86
|
+
"node_modules", ".git", ".speclock", "dist", "build", ".next",
|
|
87
|
+
".cache", "coverage", ".turbo", ".vercel", ".netlify",
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Scan project and compute lock coverage.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} root - Project root
|
|
94
|
+
* @param {Object} [options]
|
|
95
|
+
* @param {number} [options.maxFiles] - Max files to scan (default: 500)
|
|
96
|
+
* @returns {Object} Coverage analysis
|
|
97
|
+
*/
|
|
98
|
+
export function computeCoverage(root, options = {}) {
|
|
99
|
+
const brain = ensureInit(root);
|
|
100
|
+
const maxFiles = options.maxFiles || 500;
|
|
101
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
102
|
+
const lockTexts = activeLocks.map((l) => (l.text || "").toLowerCase());
|
|
103
|
+
|
|
104
|
+
// Scan for source files
|
|
105
|
+
const files = scanFiles(root, maxFiles);
|
|
106
|
+
|
|
107
|
+
// Analyze each risk category
|
|
108
|
+
const categories = RISK_PATTERNS.map((pattern) => {
|
|
109
|
+
// Find files matching this category
|
|
110
|
+
const matchingFiles = files.filter((f) => {
|
|
111
|
+
const fileLower = f.toLowerCase();
|
|
112
|
+
return pattern.filePatterns.some((fp) => fileLower.includes(fp));
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Check if any lock covers this category
|
|
116
|
+
const coveredBy = activeLocks.filter((lock) => {
|
|
117
|
+
const lockLower = (lock.text || "").toLowerCase();
|
|
118
|
+
return pattern.keywords.some((kw) => lockLower.includes(kw));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const isCovered = coveredBy.length > 0;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
category: pattern.category,
|
|
125
|
+
severity: pattern.severity,
|
|
126
|
+
filesFound: matchingFiles.length,
|
|
127
|
+
files: matchingFiles.slice(0, 5), // show up to 5
|
|
128
|
+
covered: isCovered,
|
|
129
|
+
coveredBy: coveredBy.map((l) => l.text.substring(0, 80)),
|
|
130
|
+
suggestedLock: !isCovered ? pattern.suggestedLock : null,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Only include categories that have files OR are critical
|
|
135
|
+
const relevant = categories.filter(
|
|
136
|
+
(c) => c.filesFound > 0 || c.severity === "critical"
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const totalCategories = relevant.length;
|
|
140
|
+
const coveredCategories = relevant.filter((c) => c.covered).length;
|
|
141
|
+
const coveragePercent = totalCategories > 0
|
|
142
|
+
? Math.round((coveredCategories / totalCategories) * 100)
|
|
143
|
+
: 0;
|
|
144
|
+
|
|
145
|
+
// Unprotected = has files but no lock
|
|
146
|
+
const unprotected = relevant.filter((c) => !c.covered && c.filesFound > 0);
|
|
147
|
+
const suggestions = unprotected
|
|
148
|
+
.sort((a, b) => severityRank(a.severity) - severityRank(b.severity))
|
|
149
|
+
.map((c) => ({
|
|
150
|
+
category: c.category,
|
|
151
|
+
severity: c.severity,
|
|
152
|
+
lock: c.suggestedLock,
|
|
153
|
+
filesAtRisk: c.filesFound,
|
|
154
|
+
}));
|
|
155
|
+
|
|
156
|
+
// Grade
|
|
157
|
+
let grade, status;
|
|
158
|
+
if (coveragePercent >= 90) { grade = "A+"; status = "excellent"; }
|
|
159
|
+
else if (coveragePercent >= 75) { grade = "A"; status = "well protected"; }
|
|
160
|
+
else if (coveragePercent >= 60) { grade = "B"; status = "partially protected"; }
|
|
161
|
+
else if (coveragePercent >= 40) { grade = "C"; status = "gaps found"; }
|
|
162
|
+
else if (coveragePercent >= 20) { grade = "D"; status = "mostly unprotected"; }
|
|
163
|
+
else { grade = "F"; status = "no protection"; }
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
coveragePercent,
|
|
167
|
+
grade,
|
|
168
|
+
status,
|
|
169
|
+
totalFiles: files.length,
|
|
170
|
+
totalCategories,
|
|
171
|
+
coveredCategories,
|
|
172
|
+
categories: relevant,
|
|
173
|
+
unprotected,
|
|
174
|
+
suggestions,
|
|
175
|
+
activeLocks: activeLocks.length,
|
|
176
|
+
badge: ``,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function severityRank(s) {
|
|
181
|
+
if (s === "critical") return 0;
|
|
182
|
+
if (s === "high") return 1;
|
|
183
|
+
if (s === "medium") return 2;
|
|
184
|
+
return 3;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Scan for source files in the project.
|
|
189
|
+
*/
|
|
190
|
+
function scanFiles(root, maxFiles) {
|
|
191
|
+
const files = [];
|
|
192
|
+
const extensions = new Set([
|
|
193
|
+
".js", ".ts", ".jsx", ".tsx", ".py", ".rb", ".go",
|
|
194
|
+
".java", ".rs", ".php", ".vue", ".svelte", ".astro",
|
|
195
|
+
".mjs", ".cjs", ".mts", ".cts",
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
function walk(dir, depth) {
|
|
199
|
+
if (depth > 6 || files.length >= maxFiles) return;
|
|
200
|
+
|
|
201
|
+
let entries;
|
|
202
|
+
try {
|
|
203
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
204
|
+
} catch {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (files.length >= maxFiles) break;
|
|
210
|
+
const name = entry.name;
|
|
211
|
+
|
|
212
|
+
if (IGNORE.some((ig) => name === ig || name.startsWith("."))) continue;
|
|
213
|
+
|
|
214
|
+
const fullPath = path.join(dir, name);
|
|
215
|
+
const relPath = path.relative(root, fullPath).replace(/\\/g, "/");
|
|
216
|
+
|
|
217
|
+
if (entry.isDirectory()) {
|
|
218
|
+
walk(fullPath, depth + 1);
|
|
219
|
+
} else if (entry.isFile()) {
|
|
220
|
+
const ext = path.extname(name).toLowerCase();
|
|
221
|
+
if (extensions.has(ext)) {
|
|
222
|
+
files.push(relPath);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
walk(root, 0);
|
|
229
|
+
return files;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Format coverage report for CLI output.
|
|
234
|
+
*/
|
|
235
|
+
export function formatCoverage(result) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
|
|
238
|
+
lines.push(`Lock Coverage: ${result.coveragePercent}% (${result.grade}) — ${result.status}`);
|
|
239
|
+
lines.push(`Files scanned: ${result.totalFiles} | Categories: ${result.coveredCategories}/${result.totalCategories} covered | Active locks: ${result.activeLocks}`);
|
|
240
|
+
lines.push("");
|
|
241
|
+
|
|
242
|
+
// Category breakdown
|
|
243
|
+
lines.push("Category Breakdown:");
|
|
244
|
+
lines.push(" " + "-".repeat(55));
|
|
245
|
+
|
|
246
|
+
for (const c of result.categories) {
|
|
247
|
+
const icon = c.covered ? "COVERED" : (c.filesFound > 0 ? "EXPOSED" : "N/A");
|
|
248
|
+
const sev = c.severity.toUpperCase().padEnd(8);
|
|
249
|
+
lines.push(` [${icon.padEnd(7)}] ${sev} ${c.category.padEnd(16)} ${c.filesFound} file(s)`);
|
|
250
|
+
if (c.covered && c.coveredBy.length > 0) {
|
|
251
|
+
lines.push(` Lock: "${c.coveredBy[0]}"`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Suggestions
|
|
256
|
+
if (result.suggestions.length > 0) {
|
|
257
|
+
lines.push("");
|
|
258
|
+
lines.push("Suggested Locks (ready to apply):");
|
|
259
|
+
lines.push(" " + "-".repeat(55));
|
|
260
|
+
for (let i = 0; i < result.suggestions.length; i++) {
|
|
261
|
+
const s = result.suggestions[i];
|
|
262
|
+
lines.push(` ${i + 1}. [${s.severity.toUpperCase()}] ${s.category} (${s.filesAtRisk} file(s) at risk)`);
|
|
263
|
+
lines.push(` speclock lock "${s.lock}"`);
|
|
264
|
+
lines.push("");
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
lines.push("");
|
|
268
|
+
lines.push("All detected categories are covered by locks.");
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
lines.push(`README badge: ${result.badge}`);
|
|
272
|
+
|
|
273
|
+
return lines.join("\n");
|
|
274
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Drift Score — Project Integrity Metric
|
|
3
|
+
* Measures how much a project has drifted from the founder's original intent.
|
|
4
|
+
* 0 = perfect alignment, 100 = complete drift.
|
|
5
|
+
*
|
|
6
|
+
* Signals:
|
|
7
|
+
* - Lock violations (blocked + warned)
|
|
8
|
+
* - Override frequency
|
|
9
|
+
* - Revert detections
|
|
10
|
+
* - Lock churn (locks removed)
|
|
11
|
+
* - Session continuity gaps
|
|
12
|
+
* - Decision stability
|
|
13
|
+
*
|
|
14
|
+
* Only SpecLock can compute this because only SpecLock knows
|
|
15
|
+
* what was INTENDED vs what was DONE.
|
|
16
|
+
*
|
|
17
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readBrain, readEvents } from "./storage.js";
|
|
21
|
+
import { ensureInit } from "./memory.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Compute drift score for the project.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} root - Project root
|
|
27
|
+
* @param {Object} [options]
|
|
28
|
+
* @param {number} [options.days] - Look back N days (default: 30)
|
|
29
|
+
* @returns {Object} Drift analysis
|
|
30
|
+
*/
|
|
31
|
+
export function computeDriftScore(root, options = {}) {
|
|
32
|
+
const brain = ensureInit(root);
|
|
33
|
+
const days = options.days || 30;
|
|
34
|
+
const cutoff = new Date(Date.now() - days * 86400000).toISOString();
|
|
35
|
+
|
|
36
|
+
const allEvents = readEvents(root, {});
|
|
37
|
+
const recentEvents = allEvents.filter((e) => e.at >= cutoff);
|
|
38
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
39
|
+
|
|
40
|
+
// --- Signal 1: Violation Rate (0-30 points) ---
|
|
41
|
+
// How often did the AI hit constraints?
|
|
42
|
+
const violations = recentEvents.filter(
|
|
43
|
+
(e) => e.type === "conflict_blocked" || e.type === "conflict_warned" ||
|
|
44
|
+
(e.summary && (e.summary.includes("CONFLICT") || e.summary.includes("BLOCK")))
|
|
45
|
+
);
|
|
46
|
+
const checks = recentEvents.filter(
|
|
47
|
+
(e) => e.type === "conflict_checked" || e.type === "conflict_blocked" ||
|
|
48
|
+
e.type === "conflict_warned"
|
|
49
|
+
);
|
|
50
|
+
const violationRate = checks.length > 0
|
|
51
|
+
? (violations.length / checks.length) * 100
|
|
52
|
+
: 0;
|
|
53
|
+
// 0% violations = 0 drift, 50%+ = 30 drift
|
|
54
|
+
const violationScore = Math.min(30, Math.round(violationRate * 0.6));
|
|
55
|
+
|
|
56
|
+
// --- Signal 2: Override Frequency (0-20 points) ---
|
|
57
|
+
// Overrides mean someone bypassed a constraint — that's drift
|
|
58
|
+
const overrides = recentEvents.filter((e) => e.type === "override_applied");
|
|
59
|
+
const overrideScore = Math.min(20, overrides.length * 5);
|
|
60
|
+
|
|
61
|
+
// --- Signal 3: Revert Detections (0-15 points) ---
|
|
62
|
+
// Reverts indicate instability — code going back and forth
|
|
63
|
+
const reverts = recentEvents.filter((e) => e.type === "revert_detected");
|
|
64
|
+
const revertScore = Math.min(15, reverts.length * 3);
|
|
65
|
+
|
|
66
|
+
// --- Signal 4: Lock Churn (0-15 points) ---
|
|
67
|
+
// Locks being removed means constraints are weakening
|
|
68
|
+
const locksRemoved = recentEvents.filter((e) => e.type === "lock_removed");
|
|
69
|
+
const locksAdded = recentEvents.filter((e) => e.type === "lock_added");
|
|
70
|
+
const churnRatio = locksAdded.length > 0
|
|
71
|
+
? locksRemoved.length / locksAdded.length
|
|
72
|
+
: locksRemoved.length > 0 ? 1 : 0;
|
|
73
|
+
const churnScore = Math.min(15, Math.round(churnRatio * 15));
|
|
74
|
+
|
|
75
|
+
// --- Signal 5: Goal Stability (0-10 points) ---
|
|
76
|
+
// Frequent goal changes indicate lack of direction
|
|
77
|
+
const goalChanges = recentEvents.filter((e) => e.type === "goal_updated");
|
|
78
|
+
const goalScore = Math.min(10, goalChanges.length > 1 ? (goalChanges.length - 1) * 5 : 0);
|
|
79
|
+
|
|
80
|
+
// --- Signal 6: Session Gaps (0-10 points) ---
|
|
81
|
+
// Many short sessions without summaries indicate fragmented work
|
|
82
|
+
const sessions = brain.sessions.history.filter((s) => s.startedAt >= cutoff);
|
|
83
|
+
const unsummarized = sessions.filter(
|
|
84
|
+
(s) => !s.summary || s.summary === "Session auto-closed (new session started)"
|
|
85
|
+
);
|
|
86
|
+
const gapRatio = sessions.length > 0 ? unsummarized.length / sessions.length : 0;
|
|
87
|
+
const gapScore = Math.min(10, Math.round(gapRatio * 10));
|
|
88
|
+
|
|
89
|
+
// --- Total ---
|
|
90
|
+
const totalScore = violationScore + overrideScore + revertScore +
|
|
91
|
+
churnScore + goalScore + gapScore;
|
|
92
|
+
const driftScore = Math.min(100, totalScore);
|
|
93
|
+
|
|
94
|
+
// --- Grade ---
|
|
95
|
+
let grade, status;
|
|
96
|
+
if (driftScore <= 10) { grade = "A+"; status = "excellent"; }
|
|
97
|
+
else if (driftScore <= 20) { grade = "A"; status = "healthy"; }
|
|
98
|
+
else if (driftScore <= 35) { grade = "B"; status = "minor drift"; }
|
|
99
|
+
else if (driftScore <= 50) { grade = "C"; status = "moderate drift"; }
|
|
100
|
+
else if (driftScore <= 70) { grade = "D"; status = "significant drift"; }
|
|
101
|
+
else { grade = "F"; status = "severe drift"; }
|
|
102
|
+
|
|
103
|
+
// --- Per-session drift breakdown ---
|
|
104
|
+
const sessionDrift = sessions.map((s) => {
|
|
105
|
+
const sessionEvents = recentEvents.filter(
|
|
106
|
+
(e) => e.at >= s.startedAt && (s.endedAt ? e.at <= s.endedAt : true)
|
|
107
|
+
);
|
|
108
|
+
const sessionViolations = sessionEvents.filter(
|
|
109
|
+
(e) => e.type === "conflict_blocked" || e.type === "conflict_warned" ||
|
|
110
|
+
(e.summary && e.summary.includes("CONFLICT"))
|
|
111
|
+
);
|
|
112
|
+
const sessionOverrides = sessionEvents.filter(
|
|
113
|
+
(e) => e.type === "override_applied"
|
|
114
|
+
);
|
|
115
|
+
return {
|
|
116
|
+
id: s.id,
|
|
117
|
+
tool: s.toolUsed,
|
|
118
|
+
date: s.startedAt.substring(0, 10),
|
|
119
|
+
events: sessionEvents.length,
|
|
120
|
+
violations: sessionViolations.length,
|
|
121
|
+
overrides: sessionOverrides.length,
|
|
122
|
+
impact: sessionViolations.length * 3 + sessionOverrides.length * 5,
|
|
123
|
+
};
|
|
124
|
+
}).sort((a, b) => b.impact - a.impact);
|
|
125
|
+
|
|
126
|
+
// --- Trend ---
|
|
127
|
+
// Compare first half vs second half of the period
|
|
128
|
+
const midpoint = new Date(Date.now() - (days / 2) * 86400000).toISOString();
|
|
129
|
+
const firstHalf = recentEvents.filter((e) => e.at < midpoint);
|
|
130
|
+
const secondHalf = recentEvents.filter((e) => e.at >= midpoint);
|
|
131
|
+
const firstViolations = firstHalf.filter(
|
|
132
|
+
(e) => e.type === "conflict_blocked" || e.type === "conflict_warned"
|
|
133
|
+
).length;
|
|
134
|
+
const secondViolations = secondHalf.filter(
|
|
135
|
+
(e) => e.type === "conflict_blocked" || e.type === "conflict_warned"
|
|
136
|
+
).length;
|
|
137
|
+
|
|
138
|
+
let trend;
|
|
139
|
+
if (firstViolations === 0 && secondViolations === 0) trend = "stable";
|
|
140
|
+
else if (secondViolations > firstViolations) trend = "worsening";
|
|
141
|
+
else if (secondViolations < firstViolations) trend = "improving";
|
|
142
|
+
else trend = "stable";
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
score: driftScore,
|
|
146
|
+
grade,
|
|
147
|
+
status,
|
|
148
|
+
trend,
|
|
149
|
+
period: `${days} days`,
|
|
150
|
+
signals: {
|
|
151
|
+
violations: { score: violationScore, max: 30, count: violations.length, total: checks.length },
|
|
152
|
+
overrides: { score: overrideScore, max: 20, count: overrides.length },
|
|
153
|
+
reverts: { score: revertScore, max: 15, count: reverts.length },
|
|
154
|
+
lockChurn: { score: churnScore, max: 15, removed: locksRemoved.length, added: locksAdded.length },
|
|
155
|
+
goalStability: { score: goalScore, max: 10, changes: goalChanges.length },
|
|
156
|
+
sessionGaps: { score: gapScore, max: 10, total: sessions.length, unsummarized: unsummarized.length },
|
|
157
|
+
},
|
|
158
|
+
activeLocks: activeLocks.length,
|
|
159
|
+
topDriftSessions: sessionDrift.slice(0, 5),
|
|
160
|
+
badge: ``,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Format drift score for CLI output.
|
|
166
|
+
*/
|
|
167
|
+
export function formatDriftScore(result) {
|
|
168
|
+
const lines = [];
|
|
169
|
+
|
|
170
|
+
lines.push(`Drift Score: ${result.score}/100 (${result.grade}) — ${result.status}`);
|
|
171
|
+
lines.push(`Trend: ${result.trend} | Period: ${result.period} | Active locks: ${result.activeLocks}`);
|
|
172
|
+
lines.push("");
|
|
173
|
+
lines.push("Signal Breakdown:");
|
|
174
|
+
lines.push(" " + "-".repeat(50));
|
|
175
|
+
|
|
176
|
+
const s = result.signals;
|
|
177
|
+
lines.push(` Violations: ${pad(s.violations.score)}/${s.violations.max} (${s.violations.count} violations in ${s.violations.total} checks)`);
|
|
178
|
+
lines.push(` Overrides: ${pad(s.overrides.score)}/${s.overrides.max} (${s.overrides.count} overrides)`);
|
|
179
|
+
lines.push(` Reverts: ${pad(s.reverts.score)}/${s.reverts.max} (${s.reverts.count} reverts detected)`);
|
|
180
|
+
lines.push(` Lock churn: ${pad(s.lockChurn.score)}/${s.lockChurn.max} (${s.lockChurn.removed} removed, ${s.lockChurn.added} added)`);
|
|
181
|
+
lines.push(` Goal stability: ${pad(s.goalStability.score)}/${s.goalStability.max} (${s.goalStability.changes} goal change(s))`);
|
|
182
|
+
lines.push(` Session gaps: ${pad(s.sessionGaps.score)}/${s.sessionGaps.max} (${s.sessionGaps.unsummarized}/${s.sessionGaps.total} unsummarized)`);
|
|
183
|
+
|
|
184
|
+
if (result.topDriftSessions.length > 0) {
|
|
185
|
+
lines.push("");
|
|
186
|
+
lines.push("Top Drift Sessions:");
|
|
187
|
+
for (const ds of result.topDriftSessions) {
|
|
188
|
+
if (ds.impact === 0) continue;
|
|
189
|
+
lines.push(` ${ds.date} ${ds.tool.padEnd(12)} ${ds.violations} violation(s), ${ds.overrides} override(s) [impact: ${ds.impact}]`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push(`README badge: ${result.badge}`);
|
|
195
|
+
|
|
196
|
+
return lines.join("\n");
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function pad(n) {
|
|
200
|
+
return String(n).padStart(2, " ");
|
|
201
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Lock Strengthener — Grade and Improve Weak Locks
|
|
3
|
+
* Analyzes each lock's specificity, scope, and detection power.
|
|
4
|
+
* Suggests stronger versions that catch more violations.
|
|
5
|
+
*
|
|
6
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readBrain } from "./storage.js";
|
|
10
|
+
import { ensureInit } from "./memory.js";
|
|
11
|
+
|
|
12
|
+
// --- Weakness patterns ---
|
|
13
|
+
|
|
14
|
+
const WEAKNESS_RULES = [
|
|
15
|
+
{
|
|
16
|
+
id: "too_short",
|
|
17
|
+
test: (text) => text.split(/\s+/).length < 4,
|
|
18
|
+
issue: "Too vague — short locks miss edge cases",
|
|
19
|
+
fix: (text) => `${text} — no modifications, refactoring, or rewriting allowed without explicit permission`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "no_action_verb",
|
|
23
|
+
test: (text) => {
|
|
24
|
+
const lower = text.toLowerCase();
|
|
25
|
+
return !["never", "don't", "do not", "must not", "cannot", "must", "always", "no "].some(
|
|
26
|
+
(v) => lower.includes(v)
|
|
27
|
+
);
|
|
28
|
+
},
|
|
29
|
+
issue: "No enforcement verb — AI may interpret as suggestion rather than rule",
|
|
30
|
+
fix: (text) => `Never ${text.charAt(0).toLowerCase() + text.slice(1)}`,
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "no_scope",
|
|
34
|
+
test: (text) => {
|
|
35
|
+
const lower = text.toLowerCase();
|
|
36
|
+
const hasScope = ["file", "module", "function", "endpoint", "route", "table",
|
|
37
|
+
"column", "field", "component", "page", "api", "database", "schema",
|
|
38
|
+
"auth", "payment", "config", "secret", "key", "middleware"].some(
|
|
39
|
+
(s) => lower.includes(s)
|
|
40
|
+
);
|
|
41
|
+
return !hasScope;
|
|
42
|
+
},
|
|
43
|
+
issue: "No specific scope — doesn't target files, modules, or components",
|
|
44
|
+
fix: (text) => text, // Can't auto-fix without context
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "no_consequence",
|
|
48
|
+
test: (text) => {
|
|
49
|
+
const lower = text.toLowerCase();
|
|
50
|
+
return !["because", "reason", "will break", "will fail", "causes", "leads to",
|
|
51
|
+
"depends on", "clients depend", "users expect", "production", "critical"].some(
|
|
52
|
+
(c) => lower.includes(c)
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
issue: "No consequence explained — AI may override without understanding impact",
|
|
56
|
+
fix: null, // Needs context
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "ambiguous_touch",
|
|
60
|
+
test: (text) => {
|
|
61
|
+
const lower = text.toLowerCase();
|
|
62
|
+
return lower.includes("touch") && !lower.includes("modify") && !lower.includes("change");
|
|
63
|
+
},
|
|
64
|
+
issue: '"Touch" is ambiguous — does it mean read, write, or delete?',
|
|
65
|
+
fix: (text) => text.replace(/touch/gi, "modify, refactor, or delete"),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "missing_euphemism_guard",
|
|
69
|
+
test: (text) => {
|
|
70
|
+
const lower = text.toLowerCase();
|
|
71
|
+
const hasDelete = lower.includes("delete") || lower.includes("remove");
|
|
72
|
+
const hasCleanup = lower.includes("clean") || lower.includes("simplif") || lower.includes("reorganiz");
|
|
73
|
+
return hasDelete && !hasCleanup;
|
|
74
|
+
},
|
|
75
|
+
issue: 'Doesn\'t guard against euphemisms like "clean up" or "simplify" which often mean deletion',
|
|
76
|
+
fix: (text) => `${text}. This includes euphemisms like "clean up", "simplify", "modernize", or "reorganize" — these often mask destructive changes`,
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Analyze all locks and suggest improvements.
|
|
82
|
+
*
|
|
83
|
+
* @param {string} root - Project root
|
|
84
|
+
* @returns {Object} Strength analysis
|
|
85
|
+
*/
|
|
86
|
+
export function analyzeLockStrength(root) {
|
|
87
|
+
const brain = ensureInit(root);
|
|
88
|
+
const activeLocks = (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
89
|
+
|
|
90
|
+
if (activeLocks.length === 0) {
|
|
91
|
+
return {
|
|
92
|
+
totalLocks: 0,
|
|
93
|
+
avgStrength: 0,
|
|
94
|
+
locks: [],
|
|
95
|
+
summary: "No active locks to analyze. Add constraints first.",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const analyzed = activeLocks.map((lock) => {
|
|
100
|
+
const text = lock.text || "";
|
|
101
|
+
const weaknesses = [];
|
|
102
|
+
const suggestions = [];
|
|
103
|
+
|
|
104
|
+
for (const rule of WEAKNESS_RULES) {
|
|
105
|
+
if (rule.test(text)) {
|
|
106
|
+
weaknesses.push({ id: rule.id, issue: rule.issue });
|
|
107
|
+
if (rule.fix) {
|
|
108
|
+
const fixed = rule.fix(text);
|
|
109
|
+
if (fixed !== text) {
|
|
110
|
+
suggestions.push({ id: rule.id, improved: fixed });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Score: start at 100, deduct per weakness
|
|
117
|
+
const deductions = {
|
|
118
|
+
too_short: 25,
|
|
119
|
+
no_action_verb: 20,
|
|
120
|
+
no_scope: 15,
|
|
121
|
+
no_consequence: 10,
|
|
122
|
+
ambiguous_touch: 15,
|
|
123
|
+
missing_euphemism_guard: 10,
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let strength = 100;
|
|
127
|
+
for (const w of weaknesses) {
|
|
128
|
+
strength -= deductions[w.id] || 10;
|
|
129
|
+
}
|
|
130
|
+
strength = Math.max(0, strength);
|
|
131
|
+
|
|
132
|
+
let grade;
|
|
133
|
+
if (strength >= 90) grade = "A";
|
|
134
|
+
else if (strength >= 75) grade = "B";
|
|
135
|
+
else if (strength >= 60) grade = "C";
|
|
136
|
+
else if (strength >= 40) grade = "D";
|
|
137
|
+
else grade = "F";
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
id: lock.id,
|
|
141
|
+
text: text.substring(0, 100),
|
|
142
|
+
fullText: text,
|
|
143
|
+
strength,
|
|
144
|
+
grade,
|
|
145
|
+
weaknesses,
|
|
146
|
+
suggestions: suggestions.slice(0, 2), // top 2 suggestions
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const avgStrength = Math.round(
|
|
151
|
+
analyzed.reduce((sum, l) => sum + l.strength, 0) / analyzed.length
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const weak = analyzed.filter((l) => l.strength < 60);
|
|
155
|
+
const strong = analyzed.filter((l) => l.strength >= 80);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
totalLocks: analyzed.length,
|
|
159
|
+
avgStrength,
|
|
160
|
+
avgGrade: avgStrength >= 90 ? "A" : avgStrength >= 75 ? "B" : avgStrength >= 60 ? "C" : avgStrength >= 40 ? "D" : "F",
|
|
161
|
+
strongCount: strong.length,
|
|
162
|
+
weakCount: weak.length,
|
|
163
|
+
locks: analyzed,
|
|
164
|
+
summary: `${analyzed.length} locks analyzed. Average strength: ${avgStrength}/100 (${weak.length} weak, ${strong.length} strong).`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format strength analysis for CLI output.
|
|
170
|
+
*/
|
|
171
|
+
export function formatStrength(result) {
|
|
172
|
+
if (result.totalLocks === 0) return result.summary;
|
|
173
|
+
|
|
174
|
+
const lines = [];
|
|
175
|
+
|
|
176
|
+
lines.push(`Lock Strength: ${result.avgStrength}/100 (${result.avgGrade}) — ${result.strongCount} strong, ${result.weakCount} weak`);
|
|
177
|
+
lines.push("");
|
|
178
|
+
|
|
179
|
+
// Sort weakest first
|
|
180
|
+
const sorted = [...result.locks].sort((a, b) => a.strength - b.strength);
|
|
181
|
+
|
|
182
|
+
for (const lock of sorted) {
|
|
183
|
+
const icon = lock.strength >= 80 ? "STRONG" : lock.strength >= 60 ? "OK" : "WEAK";
|
|
184
|
+
lines.push(`[${icon.padEnd(6)}] ${lock.strength}/100 (${lock.grade}) "${lock.text}"`);
|
|
185
|
+
|
|
186
|
+
if (lock.weaknesses.length > 0) {
|
|
187
|
+
for (const w of lock.weaknesses) {
|
|
188
|
+
lines.push(` Issue: ${w.issue}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (lock.suggestions.length > 0) {
|
|
192
|
+
lines.push(` Suggested: "${lock.suggestions[0].improved.substring(0, 100)}"`);
|
|
193
|
+
}
|
|
194
|
+
lines.push("");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
lines.push(result.summary);
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
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.
|
|
92
|
+
<div class="meta">v5.4.0 — 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 v5.
|
|
185
|
+
SpecLock v5.4.0 — 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.
|
|
116
|
+
const VERSION = "5.4.0";
|
|
117
117
|
const AUTHOR = "Sandeep Roy";
|
|
118
118
|
const START_TIME = Date.now();
|
|
119
119
|
|
|
@@ -901,8 +901,8 @@ app.get("/", (req, res) => {
|
|
|
901
901
|
name: "speclock",
|
|
902
902
|
version: VERSION,
|
|
903
903
|
author: AUTHOR,
|
|
904
|
-
description: "AI Constraint Engine — Universal Rules Sync + AI Patch Firewall. Syncs constraints to Cursor, Claude Code, Copilot, Windsurf, Gemini, Aider, AGENTS.md. Patch Gateway (ALLOW/WARN/BLOCK verdicts), diff-native review (interface breaks, protected symbols, dependency drift, schema changes, API impact). Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints, REST API v2, Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, HMAC audit chain, SOC 2/HIPAA compliance.
|
|
905
|
-
tools:
|
|
904
|
+
description: "AI Constraint Engine — Universal Rules Sync + AI Patch Firewall. Syncs constraints to Cursor, Claude Code, Copilot, Windsurf, Gemini, Aider, AGENTS.md. Patch Gateway (ALLOW/WARN/BLOCK verdicts), diff-native review (interface breaks, protected symbols, dependency drift, schema changes, API impact). Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints, REST API v2, Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, HMAC audit chain, SOC 2/HIPAA compliance. 49 MCP tools. 929 tests, 100% accuracy.",
|
|
905
|
+
tools: 49,
|
|
906
906
|
mcp_endpoint: "/mcp",
|
|
907
907
|
health_endpoint: "/health",
|
|
908
908
|
npm: "https://www.npmjs.com/package/speclock",
|
package/src/mcp/server.js
CHANGED
|
@@ -67,6 +67,9 @@ import {
|
|
|
67
67
|
import { generateContext, generateContextPack } from "../core/context.js";
|
|
68
68
|
import { syncRules, getSyncFormats } from "../core/rules-sync.js";
|
|
69
69
|
import { getReplay, listSessions, formatReplay } from "../core/replay.js";
|
|
70
|
+
import { computeDriftScore, formatDriftScore } from "../core/drift-score.js";
|
|
71
|
+
import { computeCoverage, formatCoverage } from "../core/coverage.js";
|
|
72
|
+
import { analyzeLockStrength, formatStrength } from "../core/strengthen.js";
|
|
70
73
|
import {
|
|
71
74
|
readBrain,
|
|
72
75
|
readEvents,
|
|
@@ -122,7 +125,7 @@ const PROJECT_ROOT =
|
|
|
122
125
|
args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
|
|
123
126
|
|
|
124
127
|
// --- MCP Server ---
|
|
125
|
-
const VERSION = "5.
|
|
128
|
+
const VERSION = "5.4.0";
|
|
126
129
|
const AUTHOR = "Sandeep Roy";
|
|
127
130
|
|
|
128
131
|
const server = new McpServer(
|
|
@@ -1999,7 +2002,45 @@ server.tool(
|
|
|
1999
2002
|
}
|
|
2000
2003
|
);
|
|
2001
2004
|
|
|
2002
|
-
// Tool 39:
|
|
2005
|
+
// Tool 39: speclock_drift_score
|
|
2006
|
+
server.tool(
|
|
2007
|
+
"speclock_drift_score",
|
|
2008
|
+
"Compute a 0-100 Drift Score measuring how much the project has drifted from the founder's original intent. Analyzes violations, overrides, reverts, lock churn, goal stability, and session gaps. Returns score, grade, trend, signal breakdown, and per-session impact. Only SpecLock can compute this because only SpecLock knows what was INTENDED vs what was DONE.",
|
|
2009
|
+
{
|
|
2010
|
+
days: z.number().optional().default(30).describe("Look back N days (default: 30)"),
|
|
2011
|
+
},
|
|
2012
|
+
async ({ days }) => {
|
|
2013
|
+
const result = computeDriftScore(PROJECT_ROOT, { days });
|
|
2014
|
+
const formatted = formatDriftScore(result);
|
|
2015
|
+
return { content: [{ type: "text", text: `## Drift Score\n\n\`\`\`\n${formatted}\n\`\`\`` }] };
|
|
2016
|
+
}
|
|
2017
|
+
);
|
|
2018
|
+
|
|
2019
|
+
// Tool 40: speclock_coverage
|
|
2020
|
+
server.tool(
|
|
2021
|
+
"speclock_coverage",
|
|
2022
|
+
"Lock Coverage Audit — scans the codebase for high-risk patterns (auth, payments, database, secrets, API routes, security, error handling, logging) and identifies areas with no lock protecting them. Auto-suggests the missing locks you need. Like a security scanner but for AI constraint gaps.",
|
|
2023
|
+
{},
|
|
2024
|
+
async () => {
|
|
2025
|
+
const result = computeCoverage(PROJECT_ROOT);
|
|
2026
|
+
const formatted = formatCoverage(result);
|
|
2027
|
+
return { content: [{ type: "text", text: `## Lock Coverage Audit\n\n\`\`\`\n${formatted}\n\`\`\`` }] };
|
|
2028
|
+
}
|
|
2029
|
+
);
|
|
2030
|
+
|
|
2031
|
+
// Tool 41: speclock_strengthen
|
|
2032
|
+
server.tool(
|
|
2033
|
+
"speclock_strengthen",
|
|
2034
|
+
"Lock Strengthener — grades each lock's specificity, scope, and detection power (0-100). Identifies weak locks and suggests stronger versions that catch more violations. Checks for: vagueness, missing enforcement verbs, no scope, no consequence, ambiguous language, missing euphemism guards.",
|
|
2035
|
+
{},
|
|
2036
|
+
async () => {
|
|
2037
|
+
const result = analyzeLockStrength(PROJECT_ROOT);
|
|
2038
|
+
const formatted = formatStrength(result);
|
|
2039
|
+
return { content: [{ type: "text", text: `## Lock Strength Analysis\n\n\`\`\`\n${formatted}\n\`\`\`` }] };
|
|
2040
|
+
}
|
|
2041
|
+
);
|
|
2042
|
+
|
|
2043
|
+
// Tool 42: speclock_list_sync_formats
|
|
2003
2044
|
server.tool(
|
|
2004
2045
|
"speclock_list_sync_formats",
|
|
2005
2046
|
"List all available AI tool formats that SpecLock can sync constraints to. Shows format key, tool name, output file path, and description.",
|