speclock 4.5.7 → 5.0.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.
@@ -36,6 +36,22 @@ import {
36
36
  overrideLock,
37
37
  getOverrideHistory,
38
38
  semanticAudit,
39
+ addTypedLock,
40
+ updateTypedLockThreshold,
41
+ checkAllTypedConstraints,
42
+ getEnforcementConfig,
43
+ CONSTRAINT_TYPES,
44
+ OPERATORS,
45
+ checkTypedConstraint,
46
+ formatTypedLockText,
47
+ compileSpec,
48
+ compileAndApply,
49
+ buildGraph,
50
+ getOrBuildGraph,
51
+ getBlastRadius,
52
+ mapLocksToFiles,
53
+ getModules,
54
+ getCriticalPaths,
39
55
  } from "../core/engine.js";
40
56
  import { generateContext, generateContextPack } from "../core/context.js";
41
57
  import {
@@ -91,7 +107,7 @@ import { fileURLToPath } from "url";
91
107
  import _path from "path";
92
108
 
93
109
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
94
- const VERSION = "4.5.7";
110
+ const VERSION = "5.0.0";
95
111
  const AUTHOR = "Sandeep Roy";
96
112
  const START_TIME = Date.now();
97
113
 
@@ -512,6 +528,101 @@ function createSpecLockServer() {
512
528
  return { content: [{ type: "text", text: `Overrides (${result.total}):\n${lines}` }] };
513
529
  });
514
530
 
531
+ // ========================================
532
+ // TYPED CONSTRAINTS — Autonomous Systems Governance (v5.0)
533
+ // ========================================
534
+
535
+ // Tool 29: speclock_add_typed_lock
536
+ server.tool("speclock_add_typed_lock", "Add a typed constraint for autonomous systems governance.", {
537
+ constraintType: z.enum(["numerical", "range", "state", "temporal"]),
538
+ metric: z.string().optional(),
539
+ operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional(),
540
+ value: z.number().optional(),
541
+ min: z.number().optional(),
542
+ max: z.number().optional(),
543
+ unit: z.string().optional(),
544
+ entity: z.string().optional(),
545
+ forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
546
+ requireApproval: z.boolean().optional(),
547
+ description: z.string().optional(),
548
+ tags: z.array(z.string()).default([]),
549
+ source: z.enum(["user", "agent"]).default("user"),
550
+ }, async (params) => {
551
+ const constraint = {
552
+ constraintType: params.constraintType,
553
+ ...(params.metric && { metric: params.metric }),
554
+ ...(params.operator && { operator: params.operator }),
555
+ ...(params.value !== undefined && { value: params.value }),
556
+ ...(params.min !== undefined && { min: params.min }),
557
+ ...(params.max !== undefined && { max: params.max }),
558
+ ...(params.unit && { unit: params.unit }),
559
+ ...(params.entity && { entity: params.entity }),
560
+ ...(params.forbidden && { forbidden: params.forbidden }),
561
+ ...(params.requireApproval !== undefined && { requireApproval: params.requireApproval }),
562
+ };
563
+ const result = addTypedLock(PROJECT_ROOT, constraint, params.tags, params.source, params.description);
564
+ if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
565
+ return { content: [{ type: "text", text: `Typed lock added (${params.constraintType}): ${result.lockId}` }] };
566
+ });
567
+
568
+ // Tool 30: speclock_check_typed
569
+ server.tool("speclock_check_typed", "Check a proposed value or state transition against typed constraints.", {
570
+ metric: z.string().optional(),
571
+ entity: z.string().optional(),
572
+ value: z.number().optional(),
573
+ from: z.string().optional(),
574
+ to: z.string().optional(),
575
+ }, async (params) => {
576
+ const brain = ensureInit(PROJECT_ROOT);
577
+ const proposed = {
578
+ ...(params.metric && { metric: params.metric }),
579
+ ...(params.entity && { entity: params.entity }),
580
+ ...(params.value !== undefined && { value: params.value }),
581
+ ...(params.from && { from: params.from }),
582
+ ...(params.to && { to: params.to }),
583
+ };
584
+ const result = checkAllTypedConstraints(brain.specLock?.items || [], proposed);
585
+ if (result.hasConflict) {
586
+ const enforcement = getEnforcementConfig(PROJECT_ROOT);
587
+ const isHard = enforcement.mode === "hard";
588
+ const topConf = result.conflictingLocks[0]?.confidence || 0;
589
+ return {
590
+ content: [{ type: "text", text: `VIOLATION: ${result.analysis}` }],
591
+ isError: isHard && topConf >= enforcement.blockThreshold,
592
+ };
593
+ }
594
+ return { content: [{ type: "text", text: result.analysis }] };
595
+ });
596
+
597
+ // Tool 31: speclock_list_typed_locks
598
+ server.tool("speclock_list_typed_locks", "List all typed constraints.", {}, async () => {
599
+ const brain = ensureInit(PROJECT_ROOT);
600
+ const typed = (brain.specLock?.items || []).filter(l => l.active !== false && l.constraintType);
601
+ if (typed.length === 0) return { content: [{ type: "text", text: "No typed constraints. Use speclock_add_typed_lock to add." }] };
602
+ const lines = typed.map(l => `[${l.constraintType}] ${l.id}: ${l.text}`).join("\n");
603
+ return { content: [{ type: "text", text: `Typed Constraints (${typed.length}):\n${lines}` }] };
604
+ });
605
+
606
+ // Tool 32: speclock_update_threshold
607
+ server.tool("speclock_update_threshold", "Update a typed lock threshold.", {
608
+ lockId: z.string().min(1),
609
+ value: z.number().optional(),
610
+ operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional(),
611
+ min: z.number().optional(),
612
+ max: z.number().optional(),
613
+ forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional(),
614
+ }, async (params) => {
615
+ const updates = {};
616
+ if (params.value !== undefined) updates.value = params.value;
617
+ if (params.operator) updates.operator = params.operator;
618
+ if (params.min !== undefined) updates.min = params.min;
619
+ if (params.max !== undefined) updates.max = params.max;
620
+ if (params.forbidden) updates.forbidden = params.forbidden;
621
+ const result = updateTypedLockThreshold(PROJECT_ROOT, params.lockId, updates);
622
+ if (result.error) return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
623
+ return { content: [{ type: "text", text: `Updated ${params.lockId}: ${JSON.stringify(result.newValues)}` }] };
624
+ });
625
+
515
626
  return server;
516
627
  }
517
628
 
@@ -770,7 +881,7 @@ app.get("/health", (req, res) => {
770
881
  status: "healthy",
771
882
  version: VERSION,
772
883
  uptime: Math.floor((Date.now() - START_TIME) / 1000),
773
- tools: 28,
884
+ tools: 35,
774
885
  auditChain: auditStatus,
775
886
  authEnabled: isAuthEnabled(PROJECT_ROOT),
776
887
  rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
@@ -784,8 +895,8 @@ app.get("/", (req, res) => {
784
895
  name: "speclock",
785
896
  version: VERSION,
786
897
  author: AUTHOR,
787
- description: "AI Constraint Engine with Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 31 MCP tools. Enterprise platform.",
788
- tools: 31,
898
+ description: "AI Constraint Engine for autonomous systems governance. Typed constraints (numerical, range, state, temporal) + REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, OAuth/OIDC SSO, admin dashboard, telemetry, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 35 MCP tools.",
899
+ tools: 35,
789
900
  mcp_endpoint: "/mcp",
790
901
  health_endpoint: "/health",
791
902
  npm: "https://www.npmjs.com/package/speclock",
@@ -799,7 +910,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
799
910
  res.json({
800
911
  name: "SpecLock",
801
912
  version: VERSION,
802
- description: "AI Constraint Engine memory + enforcement for AI coding tools. Hybrid heuristic + Gemini LLM for universal domain coverage. Policy-as-Code DSL, OAuth/OIDC SSO, admin dashboard, telemetry, API key auth, RBAC, AES-256-GCM encryption, hard enforcement, semantic pre-commit, HMAC audit chain, SOC 2/HIPAA compliance. 31 MCP tools + CLI. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
913
+ description: "AI Constraint Engine for autonomous systems governance. Typed constraints + REST API v2 with batch checking & SSE streaming. Python SDK (pip install speclock) + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, OAuth/OIDC SSO, admin dashboard, telemetry, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 35 MCP tools + CLI. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
803
914
  author: {
804
915
  name: "Sandeep Roy",
805
916
  url: "https://github.com/sgroy10",
@@ -808,7 +919,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
808
919
  homepage: "https://sgroy10.github.io/speclock/",
809
920
  license: "MIT",
810
921
  capabilities: {
811
- tools: 31,
922
+ tools: 35,
812
923
  categories: [
813
924
  "Memory Management",
814
925
  "Change Tracking",
@@ -820,6 +931,11 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
820
931
  "Hard Enforcement",
821
932
  "Policy-as-Code",
822
933
  "Telemetry",
934
+ "Typed Constraints",
935
+ "REST API v2",
936
+ "Real-Time Streaming",
937
+ "Robotics / ROS2",
938
+ "Python SDK",
823
939
  ],
824
940
  },
825
941
  keywords: [
@@ -827,6 +943,8 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
827
943
  "sso", "oauth", "rbac", "encryption", "audit", "compliance",
828
944
  "soc2", "hipaa", "dashboard", "telemetry", "claude-code",
829
945
  "cursor", "bolt-new", "lovable", "enterprise",
946
+ "robotics", "ros2", "autonomous-systems", "iot", "typed-constraints",
947
+ "real-time", "sse", "batch-api", "python-sdk", "safety",
830
948
  ],
831
949
  });
832
950
  });
@@ -922,6 +1040,477 @@ app.post("/policy", async (req, res) => {
922
1040
  }
923
1041
  });
924
1042
 
1043
+ // ========================================
1044
+ // REST API v2 — Real-Time Constraint Checking (v5.0)
1045
+ // For robotics, IoT, autonomous systems, trading platforms.
1046
+ // Sub-millisecond local checks, batch operations, SSE streaming.
1047
+ // Developed by Sandeep Roy (https://github.com/sgroy10)
1048
+ // ========================================
1049
+
1050
+ // --- v2: Check a single typed constraint ---
1051
+ app.post("/api/v2/check-typed", (req, res) => {
1052
+ setCorsHeaders(res);
1053
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1054
+ if (!checkRateLimit(clientIp)) {
1055
+ return res.status(429).json({ error: "Rate limit exceeded." });
1056
+ }
1057
+
1058
+ const start = performance.now();
1059
+ const { metric, entity, value, from_state, to_state } = req.body || {};
1060
+
1061
+ if (!metric && !entity) {
1062
+ return res.status(400).json({ error: "Required: metric (string) or entity (string)" });
1063
+ }
1064
+
1065
+ try {
1066
+ ensureInit(PROJECT_ROOT);
1067
+ const brain = readBrain(PROJECT_ROOT);
1068
+ const locks = brain?.specLock?.items || [];
1069
+
1070
+ const proposed = {};
1071
+ if (metric) proposed.metric = metric;
1072
+ if (entity) proposed.entity = entity;
1073
+ if (value !== undefined) proposed.value = value;
1074
+ if (from_state) proposed.from = from_state;
1075
+ if (to_state) proposed.to = to_state;
1076
+
1077
+ const result = checkAllTypedConstraints(locks, proposed);
1078
+ const elapsed = performance.now() - start;
1079
+
1080
+ return res.json({
1081
+ ...result,
1082
+ response_time_ms: Number(elapsed.toFixed(3)),
1083
+ api_version: "v2",
1084
+ });
1085
+ } catch (err) {
1086
+ return res.status(500).json({ error: err.message });
1087
+ }
1088
+ });
1089
+
1090
+ // --- v2: Batch check multiple values at once ---
1091
+ // Single HTTP call for all sensor readings in a tick.
1092
+ // Body: { checks: [{ metric, value }, { entity, from_state, to_state }, ...] }
1093
+ app.post("/api/v2/check-batch", (req, res) => {
1094
+ setCorsHeaders(res);
1095
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1096
+ if (!checkRateLimit(clientIp)) {
1097
+ return res.status(429).json({ error: "Rate limit exceeded." });
1098
+ }
1099
+
1100
+ const start = performance.now();
1101
+ const { checks } = req.body || {};
1102
+
1103
+ if (!Array.isArray(checks) || checks.length === 0) {
1104
+ return res.status(400).json({ error: "Required: checks (non-empty array)" });
1105
+ }
1106
+ if (checks.length > 100) {
1107
+ return res.status(400).json({ error: "Too many checks (max 100 per batch)" });
1108
+ }
1109
+
1110
+ try {
1111
+ ensureInit(PROJECT_ROOT);
1112
+ const brain = readBrain(PROJECT_ROOT);
1113
+ const locks = brain?.specLock?.items || [];
1114
+
1115
+ const results = [];
1116
+ let totalViolations = 0;
1117
+ let criticalCount = 0;
1118
+
1119
+ for (const check of checks) {
1120
+ const proposed = {};
1121
+ if (check.metric) proposed.metric = check.metric;
1122
+ if (check.entity) proposed.entity = check.entity;
1123
+ if (check.value !== undefined) proposed.value = check.value;
1124
+ if (check.from_state) proposed.from = check.from_state;
1125
+ if (check.to_state) proposed.to = check.to_state;
1126
+
1127
+ const result = checkAllTypedConstraints(locks, proposed);
1128
+ if (result.hasConflict) {
1129
+ totalViolations++;
1130
+ const topConfidence = result.conflictingLocks?.[0]?.confidence || 0;
1131
+ if (topConfidence >= 90) criticalCount++;
1132
+ }
1133
+
1134
+ results.push({
1135
+ input: check,
1136
+ ...result,
1137
+ });
1138
+ }
1139
+
1140
+ const elapsed = performance.now() - start;
1141
+
1142
+ return res.json({
1143
+ batch_size: checks.length,
1144
+ total_violations: totalViolations,
1145
+ critical_violations: criticalCount,
1146
+ emergency_stop: criticalCount > 0,
1147
+ results,
1148
+ response_time_ms: Number(elapsed.toFixed(3)),
1149
+ avg_check_ms: Number((elapsed / checks.length).toFixed(3)),
1150
+ api_version: "v2",
1151
+ });
1152
+ } catch (err) {
1153
+ return res.status(500).json({ error: err.message });
1154
+ }
1155
+ });
1156
+
1157
+ // --- v2: Add typed constraint via REST ---
1158
+ app.post("/api/v2/constraints", (req, res) => {
1159
+ setCorsHeaders(res);
1160
+ const auth = authenticateRequest(req);
1161
+ if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
1162
+ return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
1163
+ }
1164
+
1165
+ const { constraint_type, description, tags, ...kwargs } = req.body || {};
1166
+
1167
+ if (!CONSTRAINT_TYPES.includes(constraint_type)) {
1168
+ return res.status(400).json({
1169
+ error: `Invalid constraint_type. Must be one of: ${CONSTRAINT_TYPES.join(", ")}`,
1170
+ });
1171
+ }
1172
+
1173
+ try {
1174
+ ensureInit(PROJECT_ROOT);
1175
+ const lockId = addTypedLock(PROJECT_ROOT, { constraintType: constraint_type, ...kwargs }, tags || [], "user", description);
1176
+ return res.json({ success: true, lock_id: lockId, constraint_type });
1177
+ } catch (err) {
1178
+ return res.status(400).json({ error: err.message });
1179
+ }
1180
+ });
1181
+
1182
+ // --- v2: List typed constraints ---
1183
+ app.get("/api/v2/constraints", (req, res) => {
1184
+ setCorsHeaders(res);
1185
+ try {
1186
+ ensureInit(PROJECT_ROOT);
1187
+ const brain = readBrain(PROJECT_ROOT);
1188
+ const locks = (brain?.specLock?.items || []).filter(
1189
+ (l) => l.active !== false && CONSTRAINT_TYPES.includes(l.constraintType)
1190
+ );
1191
+
1192
+ const byType = {};
1193
+ for (const ct of CONSTRAINT_TYPES) byType[ct] = 0;
1194
+ for (const l of locks) byType[l.constraintType]++;
1195
+
1196
+ return res.json({
1197
+ total: locks.length,
1198
+ by_type: byType,
1199
+ constraints: locks.map((l) => ({
1200
+ id: l.id,
1201
+ type: l.constraintType,
1202
+ metric: l.metric,
1203
+ entity: l.entity,
1204
+ text: l.text || formatTypedLockText(l),
1205
+ tags: l.tags || [],
1206
+ created: l.createdAt,
1207
+ })),
1208
+ api_version: "v2",
1209
+ });
1210
+ } catch (err) {
1211
+ return res.status(500).json({ error: err.message });
1212
+ }
1213
+ });
1214
+
1215
+ // --- v2: Update constraint threshold ---
1216
+ app.put("/api/v2/constraints/:lockId", (req, res) => {
1217
+ setCorsHeaders(res);
1218
+ const auth = authenticateRequest(req);
1219
+ if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
1220
+ return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
1221
+ }
1222
+
1223
+ const { lockId } = req.params;
1224
+ const updates = req.body || {};
1225
+
1226
+ try {
1227
+ ensureInit(PROJECT_ROOT);
1228
+ const result = updateTypedLockThreshold(PROJECT_ROOT, lockId, updates);
1229
+ return res.json({ success: true, ...result });
1230
+ } catch (err) {
1231
+ return res.status(400).json({ error: err.message });
1232
+ }
1233
+ });
1234
+
1235
+ // --- v2: Delete constraint ---
1236
+ app.delete("/api/v2/constraints/:lockId", (req, res) => {
1237
+ setCorsHeaders(res);
1238
+ const auth = authenticateRequest(req);
1239
+ if (auth.authEnabled && (!auth.valid || !checkPermission(auth.role, "speclock_add_lock"))) {
1240
+ return res.status(auth.valid ? 403 : 401).json({ error: "Write permission required." });
1241
+ }
1242
+
1243
+ const { lockId } = req.params;
1244
+ try {
1245
+ ensureInit(PROJECT_ROOT);
1246
+ removeLock(PROJECT_ROOT, lockId);
1247
+ return res.json({ success: true, removed: lockId });
1248
+ } catch (err) {
1249
+ return res.status(400).json({ error: err.message });
1250
+ }
1251
+ });
1252
+
1253
+ // --- v2: SSE Stream for real-time constraint status ---
1254
+ // Clients connect once and receive constraint violation events in real-time.
1255
+ // Usage: const evtSource = new EventSource("/api/v2/stream");
1256
+ const sseClients = new Set();
1257
+
1258
+ app.get("/api/v2/stream", (req, res) => {
1259
+ setCorsHeaders(res);
1260
+ res.setHeader("Content-Type", "text/event-stream");
1261
+ res.setHeader("Cache-Control", "no-cache");
1262
+ res.setHeader("Connection", "keep-alive");
1263
+ res.flushHeaders();
1264
+
1265
+ // Send initial status
1266
+ try {
1267
+ ensureInit(PROJECT_ROOT);
1268
+ const brain = readBrain(PROJECT_ROOT);
1269
+ const locks = (brain?.specLock?.items || []).filter(
1270
+ (l) => l.active !== false && CONSTRAINT_TYPES.includes(l.constraintType)
1271
+ );
1272
+ const initData = {
1273
+ type: "connected",
1274
+ typed_constraints: locks.length,
1275
+ total_locks: (brain?.specLock?.items || []).filter((l) => l.active !== false).length,
1276
+ timestamp: new Date().toISOString(),
1277
+ };
1278
+ res.write(`event: status\ndata: ${JSON.stringify(initData)}\n\n`);
1279
+ } catch {
1280
+ res.write(`event: status\ndata: ${JSON.stringify({ type: "connected", typed_constraints: 0 })}\n\n`);
1281
+ }
1282
+
1283
+ // Register client
1284
+ const client = { res, id: Date.now() };
1285
+ sseClients.add(client);
1286
+
1287
+ // Keep alive every 15 seconds
1288
+ const keepAlive = setInterval(() => {
1289
+ res.write(`:keepalive ${new Date().toISOString()}\n\n`);
1290
+ }, 15_000);
1291
+
1292
+ req.on("close", () => {
1293
+ sseClients.delete(client);
1294
+ clearInterval(keepAlive);
1295
+ });
1296
+ });
1297
+
1298
+ // --- v2: Push a check and broadcast violations via SSE ---
1299
+ // POST /api/v2/stream/check — check constraints AND push violations to all SSE clients
1300
+ app.post("/api/v2/stream/check", (req, res) => {
1301
+ setCorsHeaders(res);
1302
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1303
+ if (!checkRateLimit(clientIp)) {
1304
+ return res.status(429).json({ error: "Rate limit exceeded." });
1305
+ }
1306
+
1307
+ const start = performance.now();
1308
+ const { checks } = req.body || {};
1309
+
1310
+ // Accept single check or batch
1311
+ const checkList = Array.isArray(checks) ? checks : [req.body];
1312
+
1313
+ try {
1314
+ ensureInit(PROJECT_ROOT);
1315
+ const brain = readBrain(PROJECT_ROOT);
1316
+ const locks = brain?.specLock?.items || [];
1317
+ const violations = [];
1318
+
1319
+ for (const check of checkList) {
1320
+ const proposed = {};
1321
+ if (check.metric) proposed.metric = check.metric;
1322
+ if (check.entity) proposed.entity = check.entity;
1323
+ if (check.value !== undefined) proposed.value = check.value;
1324
+ if (check.from_state) proposed.from = check.from_state;
1325
+ if (check.to_state) proposed.to = check.to_state;
1326
+
1327
+ const result = checkAllTypedConstraints(locks, proposed);
1328
+ if (result.hasConflict) {
1329
+ const violation = {
1330
+ type: "violation",
1331
+ input: check,
1332
+ conflicts: result.conflictingLocks,
1333
+ analysis: result.analysis,
1334
+ timestamp: new Date().toISOString(),
1335
+ };
1336
+ violations.push(violation);
1337
+
1338
+ // Broadcast to all SSE clients
1339
+ for (const client of sseClients) {
1340
+ try {
1341
+ client.res.write(`event: violation\ndata: ${JSON.stringify(violation)}\n\n`);
1342
+ } catch {
1343
+ sseClients.delete(client);
1344
+ }
1345
+ }
1346
+ }
1347
+ }
1348
+
1349
+ const elapsed = performance.now() - start;
1350
+ return res.json({
1351
+ checked: checkList.length,
1352
+ violations: violations.length,
1353
+ emergency_stop: violations.some(
1354
+ (v) => v.conflicts?.some((c) => c.confidence >= 90)
1355
+ ),
1356
+ details: violations,
1357
+ sse_clients: sseClients.size,
1358
+ response_time_ms: Number(elapsed.toFixed(3)),
1359
+ api_version: "v2",
1360
+ });
1361
+ } catch (err) {
1362
+ return res.status(500).json({ error: err.message });
1363
+ }
1364
+ });
1365
+
1366
+ // --- v2: Real-time system status ---
1367
+ app.get("/api/v2/status", (req, res) => {
1368
+ setCorsHeaders(res);
1369
+ try {
1370
+ ensureInit(PROJECT_ROOT);
1371
+ const brain = readBrain(PROJECT_ROOT);
1372
+ const allLocks = (brain?.specLock?.items || []).filter((l) => l.active !== false);
1373
+ const typedLocks = allLocks.filter((l) => CONSTRAINT_TYPES.includes(l.constraintType));
1374
+ const textLocks = allLocks.filter((l) => !l.constraintType);
1375
+
1376
+ const byType = {};
1377
+ for (const ct of CONSTRAINT_TYPES) byType[ct] = 0;
1378
+ for (const l of typedLocks) byType[l.constraintType]++;
1379
+
1380
+ // Unique metrics and entities being monitored
1381
+ const metrics = [...new Set(typedLocks.filter((l) => l.metric).map((l) => l.metric))];
1382
+ const entities = [...new Set(typedLocks.filter((l) => l.entity).map((l) => l.entity))];
1383
+
1384
+ return res.json({
1385
+ status: "active",
1386
+ version: VERSION,
1387
+ uptime: Math.floor((Date.now() - START_TIME) / 1000),
1388
+ constraints: {
1389
+ typed: typedLocks.length,
1390
+ text: textLocks.length,
1391
+ total: allLocks.length,
1392
+ by_type: byType,
1393
+ },
1394
+ monitoring: {
1395
+ metrics,
1396
+ entities,
1397
+ sse_clients: sseClients.size,
1398
+ },
1399
+ violations: (brain?.state?.violations || []).length,
1400
+ goal: brain?.goal?.text || "",
1401
+ api_version: "v2",
1402
+ });
1403
+ } catch (err) {
1404
+ return res.status(500).json({ error: err.message });
1405
+ }
1406
+ });
1407
+
1408
+ // ========================================
1409
+ // SPEC COMPILER ENDPOINTS (v5.0)
1410
+ // ========================================
1411
+
1412
+ app.post("/api/v2/compiler/compile", async (req, res) => {
1413
+ setCorsHeaders(res);
1414
+ if (!checkAuth(req, res)) return;
1415
+ if (!checkRateLimit(req, res)) return;
1416
+
1417
+ try {
1418
+ ensureInit(PROJECT_ROOT);
1419
+ const { text, autoApply } = req.body || {};
1420
+ if (!text || typeof text !== "string") {
1421
+ return res.status(400).json({ error: "Missing or invalid 'text' field", api_version: "v2" });
1422
+ }
1423
+
1424
+ const result = autoApply
1425
+ ? await compileAndApply(PROJECT_ROOT, text)
1426
+ : await compileSpec(PROJECT_ROOT, text);
1427
+
1428
+ if (!result.success) {
1429
+ return res.status(400).json({ error: result.error, api_version: "v2" });
1430
+ }
1431
+
1432
+ return res.json({
1433
+ success: true,
1434
+ locks: result.locks,
1435
+ typedLocks: result.typedLocks,
1436
+ decisions: result.decisions,
1437
+ notes: result.notes,
1438
+ summary: result.summary || "",
1439
+ applied: result.applied || null,
1440
+ totalApplied: result.totalApplied || 0,
1441
+ api_version: "v2",
1442
+ });
1443
+ } catch (err) {
1444
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1445
+ }
1446
+ });
1447
+
1448
+ // ========================================
1449
+ // CODE GRAPH ENDPOINTS (v5.0)
1450
+ // ========================================
1451
+
1452
+ app.get("/api/v2/graph", (req, res) => {
1453
+ setCorsHeaders(res);
1454
+ if (!checkAuth(req, res)) return;
1455
+
1456
+ try {
1457
+ ensureInit(PROJECT_ROOT);
1458
+ const graph = getOrBuildGraph(PROJECT_ROOT);
1459
+ return res.json({ ...graph, api_version: "v2" });
1460
+ } catch (err) {
1461
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1462
+ }
1463
+ });
1464
+
1465
+ app.post("/api/v2/graph/build", (req, res) => {
1466
+ setCorsHeaders(res);
1467
+ if (!checkAuth(req, res)) return;
1468
+
1469
+ try {
1470
+ ensureInit(PROJECT_ROOT);
1471
+ const graph = buildGraph(PROJECT_ROOT, { force: true });
1472
+ return res.json({
1473
+ success: true,
1474
+ stats: graph.stats,
1475
+ builtAt: graph.builtAt,
1476
+ api_version: "v2",
1477
+ });
1478
+ } catch (err) {
1479
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1480
+ }
1481
+ });
1482
+
1483
+ app.get("/api/v2/graph/blast-radius", (req, res) => {
1484
+ setCorsHeaders(res);
1485
+ if (!checkAuth(req, res)) return;
1486
+
1487
+ try {
1488
+ ensureInit(PROJECT_ROOT);
1489
+ const file = req.query?.file;
1490
+ if (!file) {
1491
+ return res.status(400).json({ error: "Missing 'file' query parameter", api_version: "v2" });
1492
+ }
1493
+
1494
+ const result = getBlastRadius(PROJECT_ROOT, file);
1495
+ return res.json({ ...result, api_version: "v2" });
1496
+ } catch (err) {
1497
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1498
+ }
1499
+ });
1500
+
1501
+ app.get("/api/v2/graph/lock-map", (req, res) => {
1502
+ setCorsHeaders(res);
1503
+ if (!checkAuth(req, res)) return;
1504
+
1505
+ try {
1506
+ ensureInit(PROJECT_ROOT);
1507
+ const mappings = mapLocksToFiles(PROJECT_ROOT);
1508
+ return res.json({ mappings, count: mappings.length, api_version: "v2" });
1509
+ } catch (err) {
1510
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1511
+ }
1512
+ });
1513
+
925
1514
  // ========================================
926
1515
  // SSO ENDPOINTS (v3.5)
927
1516
  // ========================================
@@ -962,5 +1551,7 @@ app.post("/auth/sso/logout", (req, res) => {
962
1551
  const PORT = parseInt(process.env.PORT || "3000", 10);
963
1552
  app.listen(PORT, "0.0.0.0", () => {
964
1553
  console.log(`SpecLock MCP HTTP Server v${VERSION} running on port ${PORT} — Developed by ${AUTHOR}`);
965
- console.log(` Dashboard: http://localhost:${PORT}/dashboard`);
1554
+ console.log(` Dashboard: http://localhost:${PORT}/dashboard`);
1555
+ console.log(` REST API v2: http://localhost:${PORT}/api/v2/status`);
1556
+ console.log(` SSE Stream: http://localhost:${PORT}/api/v2/stream`);
966
1557
  });