speclock 5.1.0 → 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.
@@ -54,6 +54,10 @@ import {
54
54
  getCriticalPaths,
55
55
  reviewPatch,
56
56
  reviewPatchAsync,
57
+ reviewPatchDiff,
58
+ reviewPatchDiffAsync,
59
+ reviewPatchUnified,
60
+ parseUnifiedDiff,
57
61
  } from "../core/engine.js";
58
62
  import { generateContext, generateContextPack } from "../core/context.js";
59
63
  import {
@@ -109,7 +113,7 @@ import { fileURLToPath } from "url";
109
113
  import _path from "path";
110
114
 
111
115
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
112
- const VERSION = "5.1.0";
116
+ const VERSION = "5.2.0";
113
117
  const AUTHOR = "Sandeep Roy";
114
118
  const START_TIME = Date.now();
115
119
 
@@ -883,7 +887,7 @@ app.get("/health", (req, res) => {
883
887
  status: "healthy",
884
888
  version: VERSION,
885
889
  uptime: Math.floor((Date.now() - START_TIME) / 1000),
886
- tools: 40,
890
+ tools: 42,
887
891
  auditChain: auditStatus,
888
892
  authEnabled: isAuthEnabled(PROJECT_ROOT),
889
893
  rateLimit: { limit: RATE_LIMIT, windowMs: RATE_WINDOW_MS },
@@ -897,8 +901,8 @@ app.get("/", (req, res) => {
897
901
  name: "speclock",
898
902
  version: VERSION,
899
903
  author: AUTHOR,
900
- 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. 40 MCP tools. 940 tests, 99.4% accuracy.",
901
- tools: 40,
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,
902
906
  mcp_endpoint: "/mcp",
903
907
  health_endpoint: "/health",
904
908
  npm: "https://www.npmjs.com/package/speclock",
@@ -912,7 +916,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
912
916
  res.json({
913
917
  name: "SpecLock",
914
918
  version: VERSION,
915
- 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. 40 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.",
916
920
  author: {
917
921
  name: "Sandeep Roy",
918
922
  url: "https://github.com/sgroy10",
@@ -921,7 +925,7 @@ app.get("/.well-known/mcp/server-card.json", (req, res) => {
921
925
  homepage: "https://sgroy10.github.io/speclock/",
922
926
  license: "MIT",
923
927
  capabilities: {
924
- tools: 40,
928
+ tools: 42,
925
929
  categories: [
926
930
  "Memory Management",
927
931
  "Change Tracking",
@@ -1541,6 +1545,51 @@ app.post("/api/v2/gateway/review", async (req, res) => {
1541
1545
  }
1542
1546
  });
1543
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
+
1544
1593
  // ========================================
1545
1594
  // SSO ENDPOINTS (v3.5)
1546
1595
  // ========================================
package/src/mcp/server.js CHANGED
@@ -59,6 +59,10 @@ import {
59
59
  getCriticalPaths,
60
60
  reviewPatch,
61
61
  reviewPatchAsync,
62
+ reviewPatchDiff,
63
+ reviewPatchDiffAsync,
64
+ reviewPatchUnified,
65
+ parseUnifiedDiff,
62
66
  } from "../core/engine.js";
63
67
  import { generateContext, generateContextPack } from "../core/context.js";
64
68
  import {
@@ -116,7 +120,7 @@ const PROJECT_ROOT =
116
120
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
117
121
 
118
122
  // --- MCP Server ---
119
- const VERSION = "5.1.0";
123
+ const VERSION = "5.2.0";
120
124
  const AUTHOR = "Sandeep Roy";
121
125
 
122
126
  const server = new McpServer(
@@ -1782,6 +1786,115 @@ server.tool(
1782
1786
  }
1783
1787
  );
1784
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
+
1785
1898
  // --- Smithery sandbox export ---
1786
1899
  export default function createSandboxServer() {
1787
1900
  return server;