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.
- package/README.md +6 -6
- package/package.json +2 -2
- package/src/cli/index.js +1 -1
- package/src/core/compliance.js +1 -1
- package/src/core/diff-analyzer.js +547 -0
- package/src/core/diff-parser.js +349 -0
- package/src/core/engine.js +7 -1
- package/src/core/patch-gateway.js +219 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +55 -6
- package/src/mcp/server.js +114 -1
package/src/mcp/http-server.js
CHANGED
|
@@ -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.
|
|
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:
|
|
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.
|
|
901
|
-
tools:
|
|
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.
|
|
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:
|
|
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.
|
|
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;
|