speclock 5.0.2 → 5.2.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.
@@ -52,6 +52,12 @@ import {
52
52
  mapLocksToFiles,
53
53
  getModules,
54
54
  getCriticalPaths,
55
+ reviewPatch,
56
+ reviewPatchAsync,
57
+ reviewPatchDiff,
58
+ reviewPatchDiffAsync,
59
+ reviewPatchUnified,
60
+ parseUnifiedDiff,
55
61
  } from "../core/engine.js";
56
62
  import { generateContext, generateContextPack } from "../core/context.js";
57
63
  import {
@@ -107,7 +113,7 @@ import { fileURLToPath } from "url";
107
113
  import _path from "path";
108
114
 
109
115
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
110
- const VERSION = "5.0.0";
116
+ const VERSION = "5.2.0";
111
117
  const AUTHOR = "Sandeep Roy";
112
118
  const START_TIME = Date.now();
113
119
 
@@ -881,7 +887,7 @@ app.get("/health", (req, res) => {
881
887
  status: "healthy",
882
888
  version: VERSION,
883
889
  uptime: Math.floor((Date.now() - START_TIME) / 1000),
884
- tools: 39,
890
+ tools: 42,
885
891
  auditChain: auditStatus,
886
892
  authEnabled: isAuthEnabled(PROJECT_ROOT),
887
893
  rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
@@ -895,8 +901,8 @@ app.get("/", (req, res) => {
895
901
  name: "speclock",
896
902
  version: VERSION,
897
903
  author: AUTHOR,
898
- description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 39 MCP tools. 940 tests, 99.4% accuracy.",
899
- tools: 39,
904
+ description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2 with batch checking & SSE streaming. Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 42 MCP tools. 940 tests, 99.4% accuracy.",
905
+ tools: 42,
900
906
  mcp_endpoint: "/mcp",
901
907
  health_endpoint: "/health",
902
908
  npm: "https://www.npmjs.com/package/speclock",
@@ -910,7 +916,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
910
916
  res.json({
911
917
  name: "SpecLock",
912
918
  version: VERSION,
913
- description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints via Gemini Flash), Code Graph (dependency parsing, blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 39 MCP tools. 940 tests, 99.4% accuracy. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
919
+ description: "AI Constraint Engine for autonomous systems governance. Spec Compiler (NL→constraints via Gemini Flash), Code Graph (dependency parsing, blast radius, lock-to-file mapping), Typed constraints (numerical, range, state, temporal), REST API v2, Python SDK + ROS2 Guardian Node. Hybrid heuristic + Gemini LLM. Policy-as-Code, RBAC, AES-256-GCM encryption, hard enforcement, HMAC audit chain, SOC 2/HIPAA compliance. 42 MCP tools. 940 tests, 99.4% accuracy. Works with Claude Code, Cursor, Windsurf, Cline, Bolt.new, Lovable.",
914
920
  author: {
915
921
  name: "Sandeep Roy",
916
922
  url: "https://github.com/sgroy10",
@@ -919,7 +925,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
919
925
  homepage: "https://sgroy10.github.io/speclock/",
920
926
  license: "MIT",
921
927
  capabilities: {
922
- tools: 39,
928
+ tools: 42,
923
929
  categories: [
924
930
  "Memory Management",
925
931
  "Change Tracking",
@@ -1509,6 +1515,81 @@ app.get("/api/v2/graph/lock-map", (req, res) => {
1509
1515
  }
1510
1516
  });
1511
1517
 
1518
+ // ========================================
1519
+ // PATCH GATEWAY (v5.1)
1520
+ // ========================================
1521
+
1522
+ app.post("/api/v2/gateway/review", async (req, res) => {
1523
+ setCorsHeaders(res);
1524
+
1525
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1526
+ if (!checkRateLimit(clientIp)) {
1527
+ return res.status(429).json({ error: "Rate limit exceeded", api_version: "v2" });
1528
+ }
1529
+
1530
+ const { description, files, useLLM } = req.body || {};
1531
+ if (!description || typeof description !== "string") {
1532
+ return res.status(400).json({ error: "Missing 'description' field (describe what the change does)", api_version: "v2" });
1533
+ }
1534
+
1535
+ try {
1536
+ ensureInit(PROJECT_ROOT);
1537
+ const fileList = Array.isArray(files) ? files : [];
1538
+ const result = useLLM
1539
+ ? await reviewPatchAsync(PROJECT_ROOT, { description, files: fileList })
1540
+ : reviewPatch(PROJECT_ROOT, { description, files: fileList });
1541
+
1542
+ return res.json({ ...result, api_version: "v2" });
1543
+ } catch (err) {
1544
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1545
+ }
1546
+ });
1547
+
1548
+ app.post("/api/v2/gateway/review-diff", async (req, res) => {
1549
+ setCorsHeaders(res);
1550
+
1551
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1552
+ if (!checkRateLimit(clientIp)) {
1553
+ return res.status(429).json({ error: "Rate limit exceeded", api_version: "v2" });
1554
+ }
1555
+
1556
+ const { description, files, diff, useLLM, options } = req.body || {};
1557
+ if (!description || typeof description !== "string") {
1558
+ return res.status(400).json({ error: "Missing 'description' field", api_version: "v2" });
1559
+ }
1560
+ if (!diff || typeof diff !== "string") {
1561
+ return res.status(400).json({ error: "Missing 'diff' field (provide git diff output)", api_version: "v2" });
1562
+ }
1563
+
1564
+ try {
1565
+ ensureInit(PROJECT_ROOT);
1566
+ const fileList = Array.isArray(files) ? files : [];
1567
+ const result = useLLM
1568
+ ? await reviewPatchDiffAsync(PROJECT_ROOT, { description, files: fileList, diff, options })
1569
+ : reviewPatchUnified(PROJECT_ROOT, { description, files: fileList, diff, options });
1570
+
1571
+ return res.json({ ...result, api_version: "v2" });
1572
+ } catch (err) {
1573
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1574
+ }
1575
+ });
1576
+
1577
+ app.post("/api/v2/gateway/parse-diff", (req, res) => {
1578
+ setCorsHeaders(res);
1579
+
1580
+ const { diff } = req.body || {};
1581
+ if (!diff || typeof diff !== "string") {
1582
+ return res.status(400).json({ error: "Missing 'diff' field", api_version: "v2" });
1583
+ }
1584
+
1585
+ try {
1586
+ const parsed = parseUnifiedDiff(diff);
1587
+ return res.json({ ...parsed, api_version: "v2" });
1588
+ } catch (err) {
1589
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1590
+ }
1591
+ });
1592
+
1512
1593
  // ========================================
1513
1594
  // SSO ENDPOINTS (v3.5)
1514
1595
  // ========================================
package/src/mcp/server.js CHANGED
@@ -57,6 +57,12 @@ import {
57
57
  mapLocksToFiles,
58
58
  getModules,
59
59
  getCriticalPaths,
60
+ reviewPatch,
61
+ reviewPatchAsync,
62
+ reviewPatchDiff,
63
+ reviewPatchDiffAsync,
64
+ reviewPatchUnified,
65
+ parseUnifiedDiff,
60
66
  } from "../core/engine.js";
61
67
  import { generateContext, generateContextPack } from "../core/context.js";
62
68
  import {
@@ -114,7 +120,7 @@ const PROJECT_ROOT =
114
120
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
115
121
 
116
122
  // --- MCP Server ---
117
- const VERSION = "5.0.0";
123
+ const VERSION = "5.2.0";
118
124
  const AUTHOR = "Sandeep Roy";
119
125
 
120
126
  const server = new McpServer(
@@ -1716,6 +1722,179 @@ server.tool(
1716
1722
  }
1717
1723
  );
1718
1724
 
1725
+ // --- Patch Gateway (v5.1) ---
1726
+
1727
+ server.tool(
1728
+ "speclock_review_patch",
1729
+ "Review a proposed code change and get an ALLOW/WARN/BLOCK verdict. Combines semantic conflict detection, lock-to-file mapping, blast radius analysis, and typed constraint awareness into a single risk assessment. The patch-time decision engine that gates every change.",
1730
+ {
1731
+ description: z.string().describe("What the change does (e.g. 'Add social login to auth page')"),
1732
+ files: z.array(z.string()).optional().default([]).describe("Files being changed (project-relative paths, e.g. ['src/auth/login.js'])"),
1733
+ useLLM: z.boolean().optional().default(false).describe("If true, uses LLM for enhanced conflict detection on ambiguous cases"),
1734
+ },
1735
+ async ({ description, files, useLLM }) => {
1736
+ const perm = requirePermission("speclock_review_patch");
1737
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1738
+
1739
+ const result = useLLM
1740
+ ? await reviewPatchAsync(PROJECT_ROOT, { description, files })
1741
+ : reviewPatch(PROJECT_ROOT, { description, files });
1742
+
1743
+ if (result.verdict === "ERROR") {
1744
+ return { content: [{ type: "text", text: result.error }], isError: true };
1745
+ }
1746
+
1747
+ const lines = [
1748
+ `Patch Gateway Verdict: ${result.verdict}`,
1749
+ `Risk Score: ${result.riskScore}/100`,
1750
+ `Source: ${result.source || "heuristic"}`,
1751
+ `Active Locks: ${result.lockCount}`,
1752
+ `Files Reviewed: ${result.fileCount}`,
1753
+ ``,
1754
+ result.summary,
1755
+ ];
1756
+
1757
+ if (result.reasons.length > 0) {
1758
+ lines.push(``, `Reasons:`);
1759
+ for (const r of result.reasons) {
1760
+ const icon = r.severity === "block" ? "BLOCK" : r.severity === "warn" ? "WARN" : "INFO";
1761
+ lines.push(` [${icon}] ${r.type}: "${r.lockText || r.details?.[0] || ""}" (confidence: ${r.confidence}%)`);
1762
+ if (r.details && r.details.length > 0) {
1763
+ r.details.forEach(d => lines.push(` - ${d}`));
1764
+ }
1765
+ }
1766
+ }
1767
+
1768
+ if (result.blastRadius) {
1769
+ lines.push(``, `Blast Radius:`);
1770
+ for (const b of result.blastRadius.files) {
1771
+ lines.push(` ${b.file}: ${b.transitiveDependents} dependents (${b.impactPercent.toFixed(1)}% impact)`);
1772
+ }
1773
+ }
1774
+
1775
+ if (result.lockFileOverlaps) {
1776
+ lines.push(``, `Lock-File Overlaps:`);
1777
+ for (const o of result.lockFileOverlaps) {
1778
+ lines.push(` Lock: "${o.lockText}" → Files: ${o.overlappingFiles.join(", ")}`);
1779
+ }
1780
+ }
1781
+
1782
+ return {
1783
+ content: [{ type: "text", text: lines.join("\n") }],
1784
+ isError: result.verdict === "BLOCK",
1785
+ };
1786
+ }
1787
+ );
1788
+
1789
+ // --- Diff-Native Patch Review (v5.2) ---
1790
+
1791
+ server.tool(
1792
+ "speclock_review_patch_diff",
1793
+ "Review a code change using actual diff content (git diff output). Analyzes interface breaks, protected symbol edits, dependency drift, schema changes, and public API impact. Returns ALLOW/WARN/BLOCK with per-signal scoring. When diff is provided, runs unified review (intent + diff merged, 35/65 weight).",
1794
+ {
1795
+ description: z.string().describe("What the change does"),
1796
+ files: z.array(z.string()).optional().default([]).describe("Files being changed"),
1797
+ diff: z.string().describe("Raw unified diff (git diff output)"),
1798
+ useLLM: z.boolean().optional().default(false).describe("Use LLM for enhanced detection"),
1799
+ options: z.object({
1800
+ includeSymbolAnalysis: z.boolean().optional().default(true),
1801
+ includeDependencyAnalysis: z.boolean().optional().default(true),
1802
+ includeSchemaAnalysis: z.boolean().optional().default(true),
1803
+ includeApiAnalysis: z.boolean().optional().default(true),
1804
+ }).optional().default({}),
1805
+ },
1806
+ async ({ description, files, diff, useLLM, options }) => {
1807
+ const perm = requirePermission("speclock_review_patch_diff");
1808
+ if (!perm.allowed) return { content: [{ type: "text", text: perm.error }], isError: true };
1809
+
1810
+ const result = useLLM
1811
+ ? await reviewPatchDiffAsync(PROJECT_ROOT, { description, files, diff, options })
1812
+ : reviewPatchUnified(PROJECT_ROOT, { description, files, diff, options });
1813
+
1814
+ if (result.verdict === "ERROR") {
1815
+ return { content: [{ type: "text", text: result.error }], isError: true };
1816
+ }
1817
+
1818
+ const lines = [
1819
+ `Patch Review Verdict: ${result.verdict}`,
1820
+ `Risk Score: ${result.riskScore}/100`,
1821
+ `Review Mode: ${result.reviewMode}`,
1822
+ `Source: ${result.source || "diff-native"}`,
1823
+ ``,
1824
+ result.summary,
1825
+ ];
1826
+
1827
+ if (result.intentVerdict) {
1828
+ lines.push(``, `Layer Breakdown:`, ` Intent: ${result.intentVerdict} (${result.intentRisk}/100)`, ` Diff: ${result.diffVerdict} (${result.diffRisk}/100)`);
1829
+ }
1830
+
1831
+ if (result.parsedDiff) {
1832
+ lines.push(``, `Diff Stats:`, ` Files: ${result.parsedDiff.filesChanged}`, ` Additions: +${result.parsedDiff.additions}`, ` Deletions: -${result.parsedDiff.deletions}`, ` Hunks: ${result.parsedDiff.hunks}`);
1833
+ }
1834
+
1835
+ if (result.signals) {
1836
+ const activeSignals = Object.entries(result.signals).filter(([_, s]) => s.score > 0);
1837
+ if (activeSignals.length > 0) {
1838
+ lines.push(``, `Active Signals:`);
1839
+ for (const [name, sig] of activeSignals) {
1840
+ lines.push(` ${name}: ${sig.score} pts`);
1841
+ }
1842
+ }
1843
+ }
1844
+
1845
+ if (result.reasons && result.reasons.length > 0) {
1846
+ lines.push(``, `Reasons:`);
1847
+ for (const r of result.reasons) {
1848
+ const icon = r.severity === "critical" ? "CRITICAL" : r.severity === "high" ? "HIGH" : r.severity === "medium" ? "MEDIUM" : "LOW";
1849
+ lines.push(` [${icon}] ${r.type}: ${r.message} (conf: ${typeof r.confidence === "number" ? (r.confidence > 1 ? r.confidence + "%" : Math.round(r.confidence * 100) + "%") : "N/A"})`);
1850
+ }
1851
+ }
1852
+
1853
+ if (result.recommendation) {
1854
+ lines.push(``, `Recommendation: ${result.recommendation.action}`, ` ${result.recommendation.why}`);
1855
+ }
1856
+
1857
+ return {
1858
+ content: [{ type: "text", text: lines.join("\n") }],
1859
+ isError: result.verdict === "BLOCK",
1860
+ };
1861
+ }
1862
+ );
1863
+
1864
+ server.tool(
1865
+ "speclock_parse_diff",
1866
+ "Parse a raw unified diff into structured changes. Shows imports added/removed, exports changed, symbols touched, route changes, and schema file detection. Useful for debugging, observability, and inspecting what SpecLock thinks changed.",
1867
+ {
1868
+ diff: z.string().describe("Raw unified diff (git diff output)"),
1869
+ },
1870
+ async ({ diff }) => {
1871
+ const parsed = parseUnifiedDiff(diff);
1872
+
1873
+ const lines = [
1874
+ `Parsed Diff:`,
1875
+ ` Files Changed: ${parsed.stats.filesChanged}`,
1876
+ ` Additions: +${parsed.stats.additions}`,
1877
+ ` Deletions: -${parsed.stats.deletions}`,
1878
+ ` Hunks: ${parsed.stats.hunks}`,
1879
+ ];
1880
+
1881
+ for (const file of parsed.files) {
1882
+ lines.push(``, `File: ${file.path} (${file.language})`);
1883
+ lines.push(` +${file.additions} / -${file.deletions}`);
1884
+ if (file.importsAdded.length > 0) lines.push(` Imports Added: ${file.importsAdded.join(", ")}`);
1885
+ if (file.importsRemoved.length > 0) lines.push(` Imports Removed: ${file.importsRemoved.join(", ")}`);
1886
+ if (file.exportsAdded.length > 0) lines.push(` Exports Added: ${file.exportsAdded.map(e => e.symbol).join(", ")}`);
1887
+ if (file.exportsRemoved.length > 0) lines.push(` Exports Removed: ${file.exportsRemoved.map(e => e.symbol).join(", ")}`);
1888
+ if (file.exportsModified.length > 0) lines.push(` Exports Modified: ${file.exportsModified.map(e => e.symbol).join(", ")}`);
1889
+ if (file.symbolsTouched.length > 0) lines.push(` Symbols Touched: ${file.symbolsTouched.map(s => `${s.symbol} (${s.changeType})`).join(", ")}`);
1890
+ if (file.routeChanges.length > 0) lines.push(` Route Changes: ${file.routeChanges.map(r => `${r.method} ${r.path} [${r.changeType}]`).join(", ")}`);
1891
+ if (file.isSchemaFile) lines.push(` ** Schema/Migration File **`);
1892
+ }
1893
+
1894
+ return { content: [{ type: "text", text: lines.join("\n") }] };
1895
+ }
1896
+ );
1897
+
1719
1898
  // --- Smithery sandbox export ---
1720
1899
  export default function createSandboxServer() {
1721
1900
  return server;