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.
package/src/mcp/server.js CHANGED
@@ -43,6 +43,20 @@ import {
43
43
  isTelemetryEnabled,
44
44
  getTelemetrySummary,
45
45
  trackToolUsage,
46
+ addTypedLock,
47
+ updateTypedLockThreshold,
48
+ checkAllTypedConstraints,
49
+ CONSTRAINT_TYPES,
50
+ OPERATORS,
51
+ formatTypedLockText,
52
+ compileSpec,
53
+ compileAndApply,
54
+ buildGraph,
55
+ getOrBuildGraph,
56
+ getBlastRadius,
57
+ mapLocksToFiles,
58
+ getModules,
59
+ getCriticalPaths,
46
60
  } from "../core/engine.js";
47
61
  import { generateContext, generateContextPack } from "../core/context.js";
48
62
  import {
@@ -100,7 +114,7 @@ const PROJECT_ROOT =
100
114
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
101
115
 
102
116
  // --- MCP Server ---
103
- const VERSION = "4.5.7";
117
+ const VERSION = "5.0.0";
104
118
  const AUTHOR = "Sandeep Roy";
105
119
 
106
120
  const server = new McpServer(
@@ -1334,6 +1348,374 @@ server.tool(
1334
1348
  }
1335
1349
  );
1336
1350
 
1351
+ // ========================================
1352
+ // TYPED CONSTRAINTS — Autonomous Systems Governance (v5.0)
1353
+ // ========================================
1354
+
1355
+ // Tool 32: speclock_add_typed_lock
1356
+ server.tool(
1357
+ "speclock_add_typed_lock",
1358
+ "Add a typed constraint lock for autonomous systems governance. Supports numerical (motor speed <= 3000 RPM), range (temperature between 20-80°C), state (forbidden transitions: EMERGENCY -> IDLE), and temporal (sensor interval <= 100ms) constraints. These are for real-time value/state checking in robotics, vehicles, trading, and medical systems.",
1359
+ {
1360
+ constraintType: z.enum(["numerical", "range", "state", "temporal"]).describe("Type of constraint"),
1361
+ metric: z.string().optional().describe("Metric name (for numerical/range/temporal). E.g., 'motor_speed', 'temperature', 'sensor_interval'"),
1362
+ operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional().describe("Comparison operator (for numerical/temporal)"),
1363
+ value: z.number().optional().describe("Threshold value (for numerical/temporal)"),
1364
+ min: z.number().optional().describe("Minimum value (for range)"),
1365
+ max: z.number().optional().describe("Maximum value (for range)"),
1366
+ unit: z.string().optional().describe("Unit of measurement. E.g., 'RPM', '°C', 'ms', 'km/h'"),
1367
+ entity: z.string().optional().describe("Entity name (for state). E.g., 'robot_arm', 'vehicle', 'trading_engine'"),
1368
+ forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional().describe("Forbidden state transitions (for state). E.g., [{ from: 'EMERGENCY', to: 'IDLE' }]"),
1369
+ requireApproval: z.boolean().optional().describe("Whether forbidden transitions require human approval (for state)"),
1370
+ description: z.string().optional().describe("Human-readable description (auto-generated if omitted)"),
1371
+ tags: z.array(z.string()).optional().describe("Category tags"),
1372
+ source: z.enum(["user", "agent"]).optional().describe("Who created this lock"),
1373
+ },
1374
+ async (params) => {
1375
+ const perm = requirePermission("speclock_add_typed_lock");
1376
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1377
+
1378
+ const constraint = {
1379
+ constraintType: params.constraintType,
1380
+ ...(params.metric && { metric: params.metric }),
1381
+ ...(params.operator && { operator: params.operator }),
1382
+ ...(params.value !== undefined && { value: params.value }),
1383
+ ...(params.min !== undefined && { min: params.min }),
1384
+ ...(params.max !== undefined && { max: params.max }),
1385
+ ...(params.unit && { unit: params.unit }),
1386
+ ...(params.entity && { entity: params.entity }),
1387
+ ...(params.forbidden && { forbidden: params.forbidden }),
1388
+ ...(params.requireApproval !== undefined && { requireApproval: params.requireApproval }),
1389
+ };
1390
+
1391
+ const result = addTypedLock(
1392
+ PROJECT_ROOT,
1393
+ constraint,
1394
+ params.tags || [],
1395
+ params.source || "user",
1396
+ params.description
1397
+ );
1398
+
1399
+ if (result.error) {
1400
+ return { content: [{ type: "text", text: `Validation error: ${result.error}` }], isError: true };
1401
+ }
1402
+
1403
+ return {
1404
+ content: [{
1405
+ type: "text",
1406
+ text: `Typed lock added (${params.constraintType}):\nID: ${result.lockId}\nDescription: ${result.brain.specLock.items[0].text}\n\nThis constraint will be checked against real-time values using speclock_check_typed.`,
1407
+ }],
1408
+ };
1409
+ }
1410
+ );
1411
+
1412
+ // Tool 33: speclock_check_typed
1413
+ server.tool(
1414
+ "speclock_check_typed",
1415
+ "Check a proposed value or state transition against typed constraints. For numerical/range/temporal: provide metric and value. For state: provide entity, from, and to. Returns CONFLICT if the proposed value violates any typed lock.",
1416
+ {
1417
+ metric: z.string().optional().describe("Metric to check (matches typed locks by metric name)"),
1418
+ entity: z.string().optional().describe("Entity to check (matches state locks by entity name)"),
1419
+ value: z.number().optional().describe("Proposed value (for numerical/range/temporal checks)"),
1420
+ from: z.string().optional().describe("Current state (for state transition checks)"),
1421
+ to: z.string().optional().describe("Target state (for state transition checks)"),
1422
+ },
1423
+ async (params) => {
1424
+ const perm = requirePermission("speclock_check_typed");
1425
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1426
+
1427
+ const brain = ensureInit(PROJECT_ROOT);
1428
+ const allLocks = brain.specLock?.items || [];
1429
+
1430
+ const proposed = {
1431
+ ...(params.metric && { metric: params.metric }),
1432
+ ...(params.entity && { entity: params.entity }),
1433
+ ...(params.value !== undefined && { value: params.value }),
1434
+ ...(params.from && { from: params.from }),
1435
+ ...(params.to && { to: params.to }),
1436
+ };
1437
+
1438
+ const result = checkAllTypedConstraints(allLocks, proposed);
1439
+
1440
+ if (result.hasConflict) {
1441
+ // Check enforcement mode
1442
+ const enforcement = getEnforcementConfig(PROJECT_ROOT);
1443
+ const isHardMode = enforcement.mode === "hard";
1444
+ const topConfidence = result.conflictingLocks[0]?.confidence || 0;
1445
+
1446
+ const text = [
1447
+ `CONSTRAINT VIOLATION DETECTED`,
1448
+ ``,
1449
+ result.analysis,
1450
+ ``,
1451
+ isHardMode && topConfidence >= enforcement.blockThreshold
1452
+ ? `BLOCKED — Hard enforcement mode active. This action cannot proceed.`
1453
+ : `Advisory — Review these violations before proceeding.`,
1454
+ ].join("\n");
1455
+
1456
+ return {
1457
+ content: [{ type: "text", text }],
1458
+ isError: isHardMode && topConfidence >= enforcement.blockThreshold,
1459
+ };
1460
+ }
1461
+
1462
+ return {
1463
+ content: [{ type: "text", text: result.analysis }],
1464
+ };
1465
+ }
1466
+ );
1467
+
1468
+ // Tool 34: speclock_list_typed_locks
1469
+ server.tool(
1470
+ "speclock_list_typed_locks",
1471
+ "List all typed constraints (numerical, range, state, temporal) with their current thresholds and parameters. Shows constraints used for autonomous systems governance.",
1472
+ {},
1473
+ async () => {
1474
+ const perm = requirePermission("speclock_list_typed_locks");
1475
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1476
+
1477
+ const brain = ensureInit(PROJECT_ROOT);
1478
+ const allLocks = brain.specLock?.items || [];
1479
+ const typedLocks = allLocks.filter(l => l.active !== false && l.constraintType);
1480
+
1481
+ if (typedLocks.length === 0) {
1482
+ return {
1483
+ content: [{
1484
+ type: "text",
1485
+ text: `No typed constraints found. Use speclock_add_typed_lock to add numerical, range, state, or temporal constraints.\n\nTotal text locks: ${allLocks.filter(l => l.active !== false && !l.constraintType).length}`,
1486
+ }],
1487
+ };
1488
+ }
1489
+
1490
+ const sections = {
1491
+ numerical: [],
1492
+ range: [],
1493
+ state: [],
1494
+ temporal: [],
1495
+ };
1496
+
1497
+ for (const lock of typedLocks) {
1498
+ sections[lock.constraintType]?.push(lock);
1499
+ }
1500
+
1501
+ const parts = [`## Typed Constraints (${typedLocks.length} total)`, ``];
1502
+
1503
+ for (const [type, locks] of Object.entries(sections)) {
1504
+ if (locks.length === 0) continue;
1505
+ parts.push(`### ${type.charAt(0).toUpperCase() + type.slice(1)} (${locks.length})`);
1506
+ for (const l of locks) {
1507
+ parts.push(`- **${l.id}**: ${l.text}`);
1508
+ if (l.metric) parts.push(` Metric: ${l.metric}`);
1509
+ if (l.entity) parts.push(` Entity: ${l.entity}`);
1510
+ if (l.unit) parts.push(` Unit: ${l.unit}`);
1511
+ if (l.tags?.length) parts.push(` Tags: ${l.tags.join(", ")}`);
1512
+ }
1513
+ parts.push(``);
1514
+ }
1515
+
1516
+ const textLockCount = allLocks.filter(l => l.active !== false && !l.constraintType).length;
1517
+ parts.push(`---`, `Text locks (semantic): ${textLockCount}`);
1518
+
1519
+ return { content: [{ type: "text", text: parts.join("\n") }] };
1520
+ }
1521
+ );
1522
+
1523
+ // Tool 35: speclock_update_threshold
1524
+ server.tool(
1525
+ "speclock_update_threshold",
1526
+ "Update a typed lock's threshold value. For numerical/temporal: update value and/or operator. For range: update min and/or max. Changes are recorded in the audit trail.",
1527
+ {
1528
+ lockId: z.string().describe("The lock ID to update"),
1529
+ value: z.number().optional().describe("New threshold value (for numerical/temporal)"),
1530
+ operator: z.enum(["<", "<=", "==", "!=", ">=", ">"]).optional().describe("New operator (for numerical/temporal)"),
1531
+ min: z.number().optional().describe("New minimum (for range)"),
1532
+ max: z.number().optional().describe("New maximum (for range)"),
1533
+ forbidden: z.array(z.object({ from: z.string(), to: z.string() })).optional().describe("New forbidden transitions (for state)"),
1534
+ },
1535
+ async (params) => {
1536
+ const perm = requirePermission("speclock_update_threshold");
1537
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1538
+
1539
+ const updates = {};
1540
+ if (params.value !== undefined) updates.value = params.value;
1541
+ if (params.operator) updates.operator = params.operator;
1542
+ if (params.min !== undefined) updates.min = params.min;
1543
+ if (params.max !== undefined) updates.max = params.max;
1544
+ if (params.forbidden) updates.forbidden = params.forbidden;
1545
+
1546
+ const result = updateTypedLockThreshold(PROJECT_ROOT, params.lockId, updates);
1547
+
1548
+ if (result.error) {
1549
+ return { content: [{ type: "text", text: `Error: ${result.error}` }], isError: true };
1550
+ }
1551
+
1552
+ return {
1553
+ content: [{
1554
+ type: "text",
1555
+ text: `Threshold updated for ${params.lockId}:\nOld: ${JSON.stringify(result.oldValues)}\nNew: ${JSON.stringify(result.newValues)}\nUpdated description: ${result.brain.specLock.items.find(l => l.id === params.lockId)?.text}`,
1556
+ }],
1557
+ };
1558
+ }
1559
+ );
1560
+
1561
+ // --- Spec Compiler (v5.0) ---
1562
+
1563
+ server.tool(
1564
+ "speclock_compile_spec",
1565
+ "Compile natural language text (PRDs, READMEs, architecture docs, chat logs) into structured SpecLock constraints. Extracts text locks, typed locks (numerical/range/state/temporal), decisions, and notes. Set autoApply=true to automatically add extracted items to brain.json.",
1566
+ {
1567
+ text: z.string().describe("Natural language text to compile into constraints"),
1568
+ autoApply: z.boolean().optional().default(false).describe("If true, automatically apply extracted constraints to brain.json"),
1569
+ },
1570
+ async ({ text, autoApply }) => {
1571
+ const perm = requirePermission("speclock_compile_spec");
1572
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1573
+
1574
+ const result = autoApply
1575
+ ? await compileAndApply(PROJECT_ROOT, text)
1576
+ : await compileSpec(PROJECT_ROOT, text);
1577
+
1578
+ if (!result.success) {
1579
+ return { content: [{ type: "text", text: `Compilation failed: ${result.error}` }], isError: true };
1580
+ }
1581
+
1582
+ const lines = [
1583
+ `Spec Compiler Results:`,
1584
+ ` Text Locks: ${result.locks.length}`,
1585
+ ` Typed Locks: ${result.typedLocks.length}`,
1586
+ ` Decisions: ${result.decisions.length}`,
1587
+ ` Notes: ${result.notes.length}`,
1588
+ ];
1589
+
1590
+ if (result.locks.length > 0) {
1591
+ lines.push(`\nExtracted Locks:`);
1592
+ result.locks.forEach((l, i) => lines.push(` ${i + 1}. ${l.text} [${(l.tags || []).join(", ")}]`));
1593
+ }
1594
+ if (result.typedLocks.length > 0) {
1595
+ lines.push(`\nExtracted Typed Locks:`);
1596
+ result.typedLocks.forEach((tl, i) => lines.push(` ${i + 1}. [${tl.constraintType}] ${tl.description || tl.metric}`));
1597
+ }
1598
+ if (result.decisions.length > 0) {
1599
+ lines.push(`\nExtracted Decisions:`);
1600
+ result.decisions.forEach((d, i) => lines.push(` ${i + 1}. ${d.text}`));
1601
+ }
1602
+ if (result.notes.length > 0) {
1603
+ lines.push(`\nExtracted Notes:`);
1604
+ result.notes.forEach((n, i) => lines.push(` ${i + 1}. ${n.text}`));
1605
+ }
1606
+
1607
+ if (autoApply && result.applied) {
1608
+ lines.push(`\nApplied to brain.json: ${result.totalApplied} items (${result.applied.locks} locks, ${result.applied.typedLocks} typed, ${result.applied.decisions} decisions, ${result.applied.notes} notes)`);
1609
+ }
1610
+
1611
+ if (result.summary) lines.push(`\nSummary: ${result.summary}`);
1612
+
1613
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1614
+ }
1615
+ );
1616
+
1617
+ // --- Code Graph (v5.0) ---
1618
+
1619
+ server.tool(
1620
+ "speclock_build_graph",
1621
+ "Build or refresh the code dependency graph. Scans JS/TS/Python source files, parses imports, and builds an adjacency graph. The graph is cached in .speclock/code-graph.json and auto-rebuilds when stale (>1 hour).",
1622
+ {},
1623
+ async () => {
1624
+ const perm = requirePermission("speclock_build_graph");
1625
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1626
+
1627
+ const graph = buildGraph(PROJECT_ROOT, { force: true });
1628
+
1629
+ const lines = [
1630
+ `Code Graph Built:`,
1631
+ ` Total Files: ${graph.stats.totalFiles}`,
1632
+ ` Total Edges: ${graph.stats.totalEdges}`,
1633
+ ` Entry Points: ${graph.stats.entryPoints.length}`,
1634
+ ` Languages: ${Object.entries(graph.stats.languages).map(([k, v]) => `${k}(${v})`).join(", ")}`,
1635
+ ];
1636
+
1637
+ if (graph.stats.entryPoints.length > 0) {
1638
+ lines.push(`\nEntry Points:`);
1639
+ graph.stats.entryPoints.slice(0, 10).forEach(ep => lines.push(` - ${ep}`));
1640
+ }
1641
+
1642
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1643
+ }
1644
+ );
1645
+
1646
+ server.tool(
1647
+ "speclock_blast_radius",
1648
+ "Calculate the blast radius of changing a specific file. Shows direct and transitive dependents, depth, and impact percentage. Useful for understanding the risk of modifying a file.",
1649
+ {
1650
+ file: z.string().describe("Relative file path to analyze (e.g., 'src/core/memory.js')"),
1651
+ },
1652
+ async ({ file }) => {
1653
+ const perm = requirePermission("speclock_blast_radius");
1654
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1655
+
1656
+ const result = getBlastRadius(PROJECT_ROOT, file);
1657
+
1658
+ if (!result.found) {
1659
+ return { content: [{ type: "text", text: `File not found in graph: ${file}\nTry running speclock_build_graph first.` }], isError: true };
1660
+ }
1661
+
1662
+ const lines = [
1663
+ `Blast Radius for ${file}:`,
1664
+ ` Direct Dependents: ${result.directDependents.length}`,
1665
+ ` Transitive Dependents: ${result.transitiveDependents.length}`,
1666
+ ` Max Depth: ${result.depth}`,
1667
+ ` Impact: ${result.impactPercent}% (${result.blastRadius}/${result.totalFiles} files)`,
1668
+ ];
1669
+
1670
+ if (result.directDependents.length > 0) {
1671
+ lines.push(`\nDirect Dependents:`);
1672
+ result.directDependents.forEach(f => lines.push(` - ${f}`));
1673
+ }
1674
+
1675
+ if (result.transitiveDependents.length > result.directDependents.length) {
1676
+ lines.push(`\nAll Affected Files:`);
1677
+ result.transitiveDependents.forEach(f => lines.push(` - ${f}`));
1678
+ }
1679
+
1680
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1681
+ }
1682
+ );
1683
+
1684
+ server.tool(
1685
+ "speclock_map_locks",
1686
+ "Map all active locks to actual code files using the dependency graph. Shows which files each lock protects and the combined blast radius. Helps understand the structural impact of constraints.",
1687
+ {},
1688
+ async () => {
1689
+ const perm = requirePermission("speclock_map_locks");
1690
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1691
+
1692
+ const mappings = mapLocksToFiles(PROJECT_ROOT);
1693
+
1694
+ if (mappings.length === 0) {
1695
+ return { content: [{ type: "text", text: "No active locks to map. Add locks first." }] };
1696
+ }
1697
+
1698
+ const lines = [`Lock-to-File Mappings (${mappings.length} locks):\n`];
1699
+
1700
+ for (const m of mappings) {
1701
+ lines.push(`Lock: "${m.lockText}"`);
1702
+ lines.push(` ID: ${m.lockId}`);
1703
+ lines.push(` Matched Files: ${m.matchedFiles.length}`);
1704
+ if (m.matchedFiles.length > 0) {
1705
+ m.matchedFiles.slice(0, 10).forEach(f => lines.push(` - ${f}`));
1706
+ if (m.matchedFiles.length > 10) lines.push(` ... and ${m.matchedFiles.length - 10} more`);
1707
+ }
1708
+ if (m.matchedModules.length > 0) {
1709
+ lines.push(` Modules: ${m.matchedModules.join(", ")}`);
1710
+ }
1711
+ lines.push(` Blast Radius: ${m.blastRadius} files`);
1712
+ lines.push("");
1713
+ }
1714
+
1715
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1716
+ }
1717
+ );
1718
+
1337
1719
  // --- Smithery sandbox export ---
1338
1720
  export default function createSandboxServer() {
1339
1721
  return server;