speclock 5.1.0 → 5.2.1

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.
@@ -272,6 +272,10 @@ export const EUPHEMISM_MAP = {
272
272
  "reconcile": ["modify", "adjust", "change", "alter"],
273
273
  "reverse": ["undo", "revert", "modify", "change"],
274
274
  "recalculate": ["modify", "change", "update", "alter"],
275
+ "recompute": ["modify", "change", "recalculate", "alter"],
276
+ "reprocess": ["override", "modify", "recalculate", "delete", "redo"],
277
+ "round up": ["modify", "tamper", "falsify", "alter", "inflate"],
278
+ "round down": ["modify", "tamper", "falsify", "alter", "deflate"],
275
279
  "backdate": ["modify", "tamper", "falsify", "change"],
276
280
  "rebalance": ["modify", "adjust", "change", "redistribute"],
277
281
  "reclassify": ["modify", "change", "recategorize"],
@@ -319,7 +323,7 @@ export const EUPHEMISM_MAP = {
319
323
  "bridge": ["connect", "link", "merge", "join"],
320
324
  "segment": ["split", "separate", "isolate", "divide"],
321
325
  "flatten": ["merge", "simplify", "restructure"],
322
- "consolidate": ["merge", "combine", "reduce"],
326
+ "consolidate": ["merge", "combine", "reduce", "delete", "expose"],
323
327
  "spin up": ["create", "deploy", "start"],
324
328
  "spin down": ["delete", "remove", "stop"],
325
329
  "tear down": ["delete", "remove", "destroy"],
@@ -349,6 +353,23 @@ export const EUPHEMISM_MAP = {
349
353
  "scrape": ["extract", "collect", "harvest"],
350
354
  "harvest": ["collect", "extract", "scrape"],
351
355
 
356
+ // Verification/compliance bypass euphemisms
357
+ "skip check": ["bypass", "disable", "remove"],
358
+ "skip verification": ["bypass", "disable", "remove"],
359
+ "streamline verification": ["bypass", "weaken", "remove", "skip"],
360
+ "streamline application": ["bypass", "skip", "remove"],
361
+ "streamline detection": ["weaken", "disable", "bypass"],
362
+ "for research purposes": ["excuse", "bypass", "unauthorized"],
363
+ "in dev": ["excuse", "bypass"],
364
+ "mock change": ["bypass", "modify", "test excuse"],
365
+
366
+ // Record manipulation/destruction euphemisms
367
+ "clean up records": ["delete", "remove", "destroy"],
368
+ "clean up old": ["delete", "remove", "purge"],
369
+ "archive and delete": ["delete", "remove", "destroy"],
370
+ "refresh timestamps": ["falsify", "tamper", "modify"],
371
+ "refresh inspection": ["falsify", "tamper", "modify"],
372
+
352
373
  // Encryption euphemisms
353
374
  "unencrypted": ["without encryption", "disable encryption", "no encryption", "plaintext"],
354
375
  "plaintext": ["without encryption", "unencrypted", "no encryption"],
@@ -689,6 +710,45 @@ export const CONCEPT_MAP = {
689
710
  "k8s": ["kubernetes", "cluster", "infrastructure",
690
711
  "container orchestration"],
691
712
  "cluster": ["kubernetes", "k8s", "infrastructure", "nodes"],
713
+
714
+ // Education / student records
715
+ "gpa": ["grades", "grade point", "academic record", "transcript"],
716
+ "grades": ["gpa", "academic record", "transcript", "marks", "scores"],
717
+ "transcript": ["grades", "gpa", "academic record", "student record"],
718
+ "financial aid": ["student loans", "scholarships", "grants", "student data", "student records"],
719
+ "student records": ["grades", "transcript", "enrollment", "academic data"],
720
+ "weighted averages": ["grades", "gpa", "academic calculation"],
721
+
722
+ // Government / benefits
723
+ "voter rolls": ["voter registration", "election records", "voter data"],
724
+ "citizen database": ["pii", "personal data", "government records", "citizen data"],
725
+ "benefit applications": ["claims", "welfare", "government benefits"],
726
+ "denied applications": ["rejected claims", "denied benefits", "denied requests"],
727
+
728
+ // Insurance / claims
729
+ "claims": ["insurance claims", "benefit claims", "applications"],
730
+ "denied claims": ["rejected claims", "denied applications"],
731
+ "cancelled applications": ["denied claims", "rejected applications", "voided applications"],
732
+
733
+ // Aerospace / aviation safety
734
+ "inspection records": ["safety records", "maintenance records", "compliance records"],
735
+ "discrepancy reports": ["safety reports", "incident reports", "audit findings"],
736
+ "black box": ["flight recorder", "flight data", "telemetry data", "safety data"],
737
+ "inspection timestamps": ["safety records", "maintenance dates", "compliance dates"],
738
+
739
+ // Gaming / virtual economy
740
+ "virtual currency": ["in-game currency", "game tokens", "game economy"],
741
+ "player data": ["user data", "gamer data", "player records", "pii"],
742
+ "player ips": ["ip addresses", "pii", "player data", "network data"],
743
+ "cheat detection": ["anti-cheat", "cheat prevention", "security", "game integrity"],
744
+
745
+ // Real estate / tenant screening
746
+ "background check": ["tenant screening", "verification", "due diligence"],
747
+ "tenant screening": ["background check", "credit check", "verification"],
748
+
749
+ // Telecom / billing
750
+ "call records": ["cdr", "call data", "telecom records", "billing records"],
751
+ "subscriber data": ["customer data", "user data", "telecom records"],
692
752
  };
693
753
 
694
754
  // ===================================================================
@@ -780,7 +840,9 @@ const NEGATIVE_INTENT_MARKERS = [
780
840
  "clean up", "sunset", "retire", "phase out",
781
841
  "decommission", "wind down", "take down",
782
842
  "take offline", "pull the plug",
783
- "streamline",
843
+ "streamline", "consolidate",
844
+ "round up", "round down", "reprocess", "recompute",
845
+ "falsify", "tamper",
784
846
  ].sort((a, b) => b.length - a.length);
785
847
 
786
848
  // ===================================================================
@@ -1892,15 +1954,50 @@ export function scoreConflict({ actionText, lockText }) {
1892
1954
  rawWordOverlap &&
1893
1955
  euphemismMatches.some(m => m.includes(`euphemism for ${_prohibVerb}`));
1894
1956
 
1895
- if (!euphemismMatchesProhibitedVerb) {
1957
+ // NEW: Check if euphemism maps to lock's prohibited verb even without
1958
+ // word overlap. Cross-domain attacks like "truncate audit_log" vs
1959
+ // "Never delete student records" have no shared nouns but the euphemism
1960
+ // still proves destructive intent matching the lock's prohibition.
1961
+ const _DESTRUCTIVE_VERBS = new Set([
1962
+ "delete", "remove", "destroy", "wipe", "purge", "erase",
1963
+ "disable", "bypass", "expose", "tamper", "falsify",
1964
+ "override", "leak", "steal", "skip", "weaken",
1965
+ ]);
1966
+ const euphemismMatchesDestructiveProhibition = !euphemismMatchesProhibitedVerb &&
1967
+ _prohibVerb && _DESTRUCTIVE_VERBS.has(_prohibVerb) &&
1968
+ euphemismMatches.some(m => m.includes(`euphemism for ${_prohibVerb}`));
1969
+
1970
+ if (euphemismMatchesProhibitedVerb) {
1971
+ // Full bypass — euphemism matches prohibited verb AND has content overlap
1972
+ // No reduction applied (existing behavior)
1973
+ } else if (euphemismMatchesDestructiveProhibition) {
1974
+ // Euphemism maps to destructive prohibited verb but no subject overlap
1975
+ // Apply moderate reduction (not 0.15) — still suspicious enough to flag
1976
+ score = Math.floor(score * 0.50);
1977
+ reasons.push("scope gate softened: euphemism matches destructive prohibition without subject overlap");
1978
+ } else {
1896
1979
  // NO subject match at all — verb-only match → heavy reduction
1897
1980
  score = Math.floor(score * 0.15);
1898
1981
  reasons.push("subject gate: no subject overlap — verb-only match, likely false positive");
1899
1982
  }
1900
1983
  } else if (hasVocabSubjectMatch && !hasScopeMatch && subjectComparison.lockSubjects.length > 0 && subjectComparison.actionSubjects.length > 0) {
1901
1984
  // Vocabulary overlap exists but subjects point to DIFFERENT scopes
1902
- score = Math.floor(score * 0.35);
1903
- reasons.push(`scope gate: shared vocabulary but different scope — lock targets "${subjectComparison.lockSubjects[0]}", action targets "${subjectComparison.actionSubjects[0]}"`);
1985
+ // If euphemism maps to a destructive verb, soften the gate
1986
+ const _prohibVerb2 = extractProhibitedVerb(lockText);
1987
+ const _DESTRUCTIVE_VERBS2 = new Set([
1988
+ "delete", "remove", "destroy", "wipe", "purge", "erase",
1989
+ "disable", "bypass", "expose", "tamper", "falsify",
1990
+ "override", "leak", "steal", "skip", "weaken",
1991
+ ]);
1992
+ const hasDestructiveEuphemism = _prohibVerb2 && _DESTRUCTIVE_VERBS2.has(_prohibVerb2) &&
1993
+ euphemismMatches.some(m => m.includes(`euphemism for ${_prohibVerb2}`));
1994
+ if (hasDestructiveEuphemism) {
1995
+ score = Math.floor(score * 0.55);
1996
+ reasons.push(`scope gate softened: destructive euphemism with different scope — lock targets "${subjectComparison.lockSubjects[0]}", action targets "${subjectComparison.actionSubjects[0]}"`);
1997
+ } else {
1998
+ score = Math.floor(score * 0.35);
1999
+ reasons.push(`scope gate: shared vocabulary but different scope — lock targets "${subjectComparison.lockSubjects[0]}", action targets "${subjectComparison.actionSubjects[0]}"`);
2000
+ }
1904
2001
  }
1905
2002
 
1906
2003
  const prohibitedVerb = extractProhibitedVerb(lockText);
@@ -89,7 +89,7 @@
89
89
  <div class="header">
90
90
  <div>
91
91
  <h1><span>SpecLock</span> Dashboard</h1>
92
- <div class="meta">v5.1.0 &mdash; AI Constraint Engine</div>
92
+ <div class="meta">v5.2.1 &mdash; AI Constraint Engine</div>
93
93
  </div>
94
94
  <div style="display:flex;align-items:center;gap:12px;">
95
95
  <span id="health-badge" class="status-badge healthy">Loading...</span>
@@ -182,7 +182,7 @@
182
182
  </div>
183
183
 
184
184
  <div style="text-align:center;padding:24px;color:var(--muted);font-size:12px;">
185
- SpecLock v5.1.0 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
185
+ SpecLock v5.2.1 &mdash; Developed by Sandeep Roy &mdash; <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
186
186
  </div>
187
187
 
188
188
  <script>
@@ -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.1";
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 AI Patch Firewall. Patch Gateway (ALLOW/WARN/BLOCK verdicts), diff-native review (interface breaks, protected symbols, dependency drift, schema changes, API impact). Spec Compiler (NL→constraints), Code Graph (blast radius, lock-to-file mapping), Typed constraints, REST API v2, Python SDK + ROS2 integration. Policy-as-Code, RBAC, AES-256-GCM encryption, HMAC audit chain, SOC 2/HIPAA compliance. 42 MCP tools. 1073 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",
@@ -909,45 +913,60 @@ app.get("/", (req, res) => {
909
913
  // Smithery server card for listing metadata
910
914
  app.get("/.well-known/mcp/server-card.json", (req, res) => {
911
915
  setCorsHeaders(res);
916
+ // Smithery-compatible server card format (SEP-1649)
912
917
  res.json({
913
- name: "SpecLock",
914
- 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.",
916
- author: {
917
- name: "Sandeep Roy",
918
- url: "https://github.com/sgroy10",
919
- },
920
- repository: "https://github.com/sgroy10/speclock",
921
- homepage: "https://sgroy10.github.io/speclock/",
922
- license: "MIT",
923
- capabilities: {
924
- tools: 40,
925
- categories: [
926
- "Memory Management",
927
- "Change Tracking",
928
- "Constraint Enforcement",
929
- "Git Integration",
930
- "AI Intelligence",
931
- "Templates & Reports",
932
- "Compliance & Audit",
933
- "Hard Enforcement",
934
- "Policy-as-Code",
935
- "Telemetry",
936
- "Typed Constraints",
937
- "REST API v2",
938
- "Real-Time Streaming",
939
- "Robotics / ROS2",
940
- "Python SDK",
941
- ],
918
+ serverInfo: {
919
+ name: "speclock",
920
+ version: VERSION,
942
921
  },
943
- keywords: [
944
- "ai-memory", "constraint-enforcement", "mcp", "policy-as-code",
945
- "sso", "oauth", "rbac", "encryption", "audit", "compliance",
946
- "soc2", "hipaa", "dashboard", "telemetry", "claude-code",
947
- "cursor", "bolt-new", "lovable", "enterprise",
948
- "robotics", "ros2", "autonomous-systems", "iot", "typed-constraints",
949
- "real-time", "sse", "batch-api", "python-sdk", "safety",
922
+ tools: [
923
+ { name: "speclock_init", description: "Initialize SpecLock in the current project directory.", inputSchema: { type: "object", properties: {} } },
924
+ { name: "speclock_get_context", description: "THE KEY TOOL. Returns the full structured context pack.", inputSchema: { type: "object", properties: { format: { enum: ["markdown","json"], type: "string", default: "markdown" } } } },
925
+ { name: "speclock_set_goal", description: "Set or update the project goal.", inputSchema: { type: "object", properties: { text: { type: "string", minLength: 1 } } } },
926
+ { name: "speclock_add_lock", description: "Add a non-negotiable constraint (SpecLock).", inputSchema: { type: "object", properties: { text: { type: "string", minLength: 1 }, tags: { type: "array", items: { type: "string" }, default: [] }, source: { enum: ["user","agent"], type: "string", default: "agent" } } } },
927
+ { name: "speclock_remove_lock", description: "Remove (deactivate) a SpecLock by its ID.", inputSchema: { type: "object", properties: { lockId: { type: "string", minLength: 1 } } } },
928
+ { name: "speclock_add_decision", description: "Record an architectural or design decision.", inputSchema: { type: "object", properties: { text: { type: "string", minLength: 1 }, tags: { type: "array", items: { type: "string" }, default: [] }, source: { enum: ["user","agent"], type: "string", default: "agent" } } } },
929
+ { name: "speclock_add_note", description: "Add a pinned note for reference.", inputSchema: { type: "object", properties: { text: { type: "string", minLength: 1 }, pinned: { type: "boolean", default: true } } } },
930
+ { name: "speclock_set_deploy_facts", description: "Record deployment configuration facts.", inputSchema: { type: "object", properties: { provider: { type: "string" }, branch: { type: "string" }, url: { type: "string" }, autoDeploy: { type: "boolean" }, notes: { type: "string" } } } },
931
+ { name: "speclock_log_change", description: "Manually log a significant change.", inputSchema: { type: "object", properties: { summary: { type: "string", minLength: 1 }, files: { type: "array", items: { type: "string" }, default: [] } } } },
932
+ { name: "speclock_get_changes", description: "Get recent file changes tracked by SpecLock.", inputSchema: { type: "object", properties: { limit: { type: "integer", default: 20, minimum: 1, maximum: 100 } } } },
933
+ { name: "speclock_get_events", description: "Get the event log, optionally filtered by type.", inputSchema: { type: "object", properties: { type: { type: "string" }, limit: { type: "integer", default: 50, minimum: 1, maximum: 200 }, since: { type: "string" } } } },
934
+ { name: "speclock_check_conflict", description: "Check if a proposed action conflicts with any active SpecLock. In hard mode, blocks above threshold.", inputSchema: { type: "object", properties: { proposedAction: { type: "string", minLength: 1 } } } },
935
+ { name: "speclock_session_briefing", description: "Start a new session and get a full briefing.", inputSchema: { type: "object", properties: { toolName: { enum: ["claude-code","cursor","codex","windsurf","cline","unknown"], type: "string", default: "unknown" } } } },
936
+ { name: "speclock_session_summary", description: "End the current session and record what was accomplished.", inputSchema: { type: "object", properties: { summary: { type: "string", minLength: 1 } } } },
937
+ { name: "speclock_checkpoint", description: "Create a named git tag checkpoint for easy rollback.", inputSchema: { type: "object", properties: { name: { type: "string", minLength: 1 } } } },
938
+ { name: "speclock_repo_status", description: "Get current git repository status.", inputSchema: { type: "object", properties: {} } },
939
+ { name: "speclock_suggest_locks", description: "AI-powered lock suggestions based on project patterns.", inputSchema: { type: "object", properties: {} } },
940
+ { name: "speclock_detect_drift", description: "Scan recent changes for constraint violations.", inputSchema: { type: "object", properties: {} } },
941
+ { name: "speclock_health", description: "Health check with completeness score and multi-agent timeline.", inputSchema: { type: "object", properties: {} } },
942
+ { name: "speclock_apply_template", description: "Apply a pre-built constraint template (nextjs, react, express, supabase, stripe, security-hardened).", inputSchema: { type: "object", properties: { name: { type: "string" } } } },
943
+ { name: "speclock_report", description: "Violation report — how many times SpecLock blocked changes.", inputSchema: { type: "object", properties: {} } },
944
+ { name: "speclock_audit", description: "Audit staged files against active locks.", inputSchema: { type: "object", properties: {} } },
945
+ { name: "speclock_verify_audit", description: "Verify the integrity of the HMAC audit chain.", inputSchema: { type: "object", properties: {} } },
946
+ { name: "speclock_export_compliance", description: "Generate compliance reports (SOC 2, HIPAA, CSV).", inputSchema: { type: "object", properties: { format: { enum: ["soc2","hipaa","csv"], type: "string" } } } },
947
+ { name: "speclock_set_enforcement", description: "Set enforcement mode: advisory (warn) or hard (block).", inputSchema: { type: "object", properties: { mode: { enum: ["advisory","hard"], type: "string" }, blockThreshold: { type: "integer", default: 70, minimum: 0, maximum: 100 } } } },
948
+ { name: "speclock_override_lock", description: "Override a lock with justification. Logged to audit trail.", inputSchema: { type: "object", properties: { lockId: { type: "string", minLength: 1 }, action: { type: "string", minLength: 1 }, reason: { type: "string", minLength: 1 } } } },
949
+ { name: "speclock_semantic_audit", description: "Semantic pre-commit: analyzes code changes vs locks.", inputSchema: { type: "object", properties: {} } },
950
+ { name: "speclock_override_history", description: "Show lock override history.", inputSchema: { type: "object", properties: { lockId: { type: "string" } } } },
951
+ { name: "speclock_policy_evaluate", description: "Evaluate policy-as-code rules against proposed actions.", inputSchema: { type: "object", properties: { files: { type: "array", items: { type: "string" } }, actionType: { type: "string" } } } },
952
+ { name: "speclock_policy_manage", description: "Policy CRUD: list, add, remove policy rules.", inputSchema: { type: "object", properties: { action: { enum: ["list","add","remove"], type: "string" } } } },
953
+ { name: "speclock_telemetry", description: "Opt-in usage analytics summary.", inputSchema: { type: "object", properties: { action: { enum: ["status","enable","disable","report"], type: "string" } } } },
954
+ { name: "speclock_guard_file", description: "Add SPECLOCK-GUARD header to lock specific files.", inputSchema: { type: "object", properties: { file: { type: "string" }, lockId: { type: "string" } } } },
955
+ { name: "speclock_auto_guard", description: "Auto-guard files related to lock keywords.", inputSchema: { type: "object", properties: {} } },
956
+ { name: "speclock_add_typed_lock", description: "Add typed constraint (numerical/range/state/temporal).", inputSchema: { type: "object", properties: { constraintType: { enum: ["numerical","range","state","temporal"], type: "string" }, metric: { type: "string" }, operator: { type: "string" }, value: {}, unit: { type: "string" }, description: { type: "string" } } } },
957
+ { name: "speclock_check_typed", description: "Check proposed values against typed constraints.", inputSchema: { type: "object", properties: { metric: { type: "string" }, value: {} } } },
958
+ { name: "speclock_list_typed_locks", description: "List all typed constraints with current thresholds.", inputSchema: { type: "object", properties: {} } },
959
+ { name: "speclock_update_threshold", description: "Update typed lock thresholds dynamically.", inputSchema: { type: "object", properties: { lockId: { type: "string" }, value: {}, operator: { type: "string" } } } },
960
+ { name: "speclock_compile_spec", description: "Compile natural language (PRDs, READMEs) into structured constraints via Gemini Flash.", inputSchema: { type: "object", properties: { text: { type: "string" }, autoApply: { type: "boolean", default: false } } } },
961
+ { name: "speclock_build_graph", description: "Build/refresh code dependency graph from imports (JS/TS/Python).", inputSchema: { type: "object", properties: {} } },
962
+ { name: "speclock_blast_radius", description: "Calculate blast radius — transitive dependents, impact %, depth.", inputSchema: { type: "object", properties: { file: { type: "string" } } } },
963
+ { name: "speclock_map_locks", description: "Map active locks to actual code files via the dependency graph.", inputSchema: { type: "object", properties: {} } },
964
+ { name: "speclock_review_patch", description: "ALLOW/WARN/BLOCK verdict — combines semantic conflict + lock-file mapping + blast radius.", inputSchema: { type: "object", properties: { description: { type: "string" }, files: { type: "array", items: { type: "string" } } } } },
965
+ { name: "speclock_review_patch_diff", description: "Diff-native review — parses actual diffs for interface breaks, protected symbols, dependency drift, schema changes.", inputSchema: { type: "object", properties: { description: { type: "string" }, diff: { type: "string" } } } },
966
+ { name: "speclock_parse_diff", description: "Parse unified diff into structured changes — imports, exports, symbols, routes, schema detection.", inputSchema: { type: "object", properties: { diff: { type: "string" } } } },
950
967
  ],
968
+ resources: [],
969
+ prompts: [],
951
970
  });
952
971
  });
953
972
 
@@ -1541,6 +1560,51 @@ app.post("/api/v2/gateway/review", async (req, res) => {
1541
1560
  }
1542
1561
  });
1543
1562
 
1563
+ app.post("/api/v2/gateway/review-diff", async (req, res) => {
1564
+ setCorsHeaders(res);
1565
+
1566
+ const clientIp = req.headers["x-forwarded-for"]?.split(",")[0]?.trim() || req.socket?.remoteAddress || "unknown";
1567
+ if (!checkRateLimit(clientIp)) {
1568
+ return res.status(429).json({ error: "Rate limit exceeded", api_version: "v2" });
1569
+ }
1570
+
1571
+ const { description, files, diff, useLLM, options } = req.body || {};
1572
+ if (!description || typeof description !== "string") {
1573
+ return res.status(400).json({ error: "Missing 'description' field", api_version: "v2" });
1574
+ }
1575
+ if (!diff || typeof diff !== "string") {
1576
+ return res.status(400).json({ error: "Missing 'diff' field (provide git diff output)", api_version: "v2" });
1577
+ }
1578
+
1579
+ try {
1580
+ ensureInit(PROJECT_ROOT);
1581
+ const fileList = Array.isArray(files) ? files : [];
1582
+ const result = useLLM
1583
+ ? await reviewPatchDiffAsync(PROJECT_ROOT, { description, files: fileList, diff, options })
1584
+ : reviewPatchUnified(PROJECT_ROOT, { description, files: fileList, diff, options });
1585
+
1586
+ return res.json({ ...result, api_version: "v2" });
1587
+ } catch (err) {
1588
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1589
+ }
1590
+ });
1591
+
1592
+ app.post("/api/v2/gateway/parse-diff", (req, res) => {
1593
+ setCorsHeaders(res);
1594
+
1595
+ const { diff } = req.body || {};
1596
+ if (!diff || typeof diff !== "string") {
1597
+ return res.status(400).json({ error: "Missing 'diff' field", api_version: "v2" });
1598
+ }
1599
+
1600
+ try {
1601
+ const parsed = parseUnifiedDiff(diff);
1602
+ return res.json({ ...parsed, api_version: "v2" });
1603
+ } catch (err) {
1604
+ return res.status(500).json({ error: err.message, api_version: "v2" });
1605
+ }
1606
+ });
1607
+
1544
1608
  // ========================================
1545
1609
  // SSO ENDPOINTS (v3.5)
1546
1610
  // ========================================
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.1";
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;