speclock 1.6.0 → 2.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.
@@ -73,6 +73,7 @@ export function makeBrain(root, hasGitRepo, defaultBranch) {
73
73
  },
74
74
  recentChanges: [],
75
75
  reverts: [],
76
+ violations: [],
76
77
  },
77
78
  events: { lastEventId: "", count: 0 },
78
79
  };
@@ -104,6 +105,11 @@ export function migrateBrainV1toV2(brain) {
104
105
  brain.facts.deploy.url = "";
105
106
  }
106
107
 
108
+ // Add violations array if missing
109
+ if (brain.state && !brain.state.violations) {
110
+ brain.state.violations = [];
111
+ }
112
+
107
113
  // Remove old importance field
108
114
  delete brain.importance;
109
115
 
@@ -120,6 +126,10 @@ export function readBrain(root) {
120
126
  brain = migrateBrainV1toV2(brain);
121
127
  writeBrain(root, brain);
122
128
  }
129
+ // Ensure violations array exists (added in v1.7.0)
130
+ if (brain.state && !brain.state.violations) {
131
+ brain.state.violations = [];
132
+ }
123
133
  return brain;
124
134
  }
125
135
 
@@ -184,3 +194,11 @@ export function addRecentChange(brain, item) {
184
194
  export function addRevert(brain, item) {
185
195
  brain.state.reverts.unshift(item);
186
196
  }
197
+
198
+ export function addViolation(brain, item) {
199
+ if (!brain.state.violations) brain.state.violations = [];
200
+ brain.state.violations.unshift(item);
201
+ if (brain.state.violations.length > 100) {
202
+ brain.state.violations = brain.state.violations.slice(0, 100);
203
+ }
204
+ }
@@ -0,0 +1,114 @@
1
+ // SpecLock Constraint Templates — Pre-built lock packs for common frameworks
2
+
3
+ export const TEMPLATES = {
4
+ nextjs: {
5
+ name: "nextjs",
6
+ displayName: "Next.js",
7
+ description: "Constraints for Next.js applications — protects routing, API routes, and middleware",
8
+ locks: [
9
+ "Never modify the authentication system without explicit permission",
10
+ "Never change the Next.js routing structure (app/ or pages/ directory layout)",
11
+ "API routes must not expose internal server logic to the client",
12
+ "Middleware must not be modified without review",
13
+ "Environment variables must not be hardcoded in source files",
14
+ ],
15
+ decisions: [
16
+ "Framework: Next.js (App Router or Pages Router as configured)",
17
+ "Server components are the default; client components require 'use client' directive",
18
+ ],
19
+ },
20
+
21
+ react: {
22
+ name: "react",
23
+ displayName: "React",
24
+ description: "Constraints for React applications — protects state management, component architecture",
25
+ locks: [
26
+ "Never modify the authentication system without explicit permission",
27
+ "Global state management pattern must not change without review",
28
+ "Component prop interfaces must maintain backward compatibility",
29
+ "Shared utility functions must not have breaking changes",
30
+ "Environment variables must not be hardcoded in source files",
31
+ ],
32
+ decisions: [
33
+ "Framework: React with functional components and hooks",
34
+ "Styling approach must remain consistent across the project",
35
+ ],
36
+ },
37
+
38
+ express: {
39
+ name: "express",
40
+ displayName: "Express.js API",
41
+ description: "Constraints for Express.js backends — protects middleware, routes, and database layer",
42
+ locks: [
43
+ "Never modify authentication or authorization middleware without explicit permission",
44
+ "Database connection configuration must not change without review",
45
+ "No breaking changes to public API endpoints",
46
+ "Rate limiting and security middleware must not be disabled",
47
+ "Environment variables and secrets must not be hardcoded",
48
+ ],
49
+ decisions: [
50
+ "Backend: Express.js with REST API pattern",
51
+ "Error handling follows centralized middleware pattern",
52
+ ],
53
+ },
54
+
55
+ supabase: {
56
+ name: "supabase",
57
+ displayName: "Supabase",
58
+ description: "Constraints for Supabase projects — protects auth, RLS policies, and database schema",
59
+ locks: [
60
+ "Database must always be Supabase — never switch to another provider",
61
+ "Row Level Security (RLS) policies must not be disabled or weakened",
62
+ "Supabase auth configuration must not change without explicit permission",
63
+ "Database schema migrations must not drop tables or columns without review",
64
+ "Supabase client initialization must not be modified",
65
+ ],
66
+ decisions: [
67
+ "Database and auth provider: Supabase",
68
+ "All database access must go through Supabase client (no direct SQL in application code)",
69
+ ],
70
+ },
71
+
72
+ stripe: {
73
+ name: "stripe",
74
+ displayName: "Stripe Payments",
75
+ description: "Constraints for Stripe integration — protects payment logic, webhooks, and pricing",
76
+ locks: [
77
+ "Payment processing logic must not be modified without explicit permission",
78
+ "Stripe webhook handlers must not change without review",
79
+ "Pricing and subscription tier definitions must not change without permission",
80
+ "Stripe API keys must never be hardcoded or exposed to the client",
81
+ "Payment error handling must not be weakened or removed",
82
+ ],
83
+ decisions: [
84
+ "Payment provider: Stripe",
85
+ "All payment operations must be server-side only",
86
+ ],
87
+ },
88
+
89
+ "security-hardened": {
90
+ name: "security-hardened",
91
+ displayName: "Security Hardened",
92
+ description: "Strict security constraints — protects auth, secrets, CORS, input validation",
93
+ locks: [
94
+ "Never modify authentication or authorization without explicit permission",
95
+ "No secrets, API keys, or credentials in source code",
96
+ "CORS configuration must not be loosened without review",
97
+ "Input validation must not be weakened or bypassed",
98
+ "Security headers and CSP must not be removed or weakened",
99
+ "Dependencies must not be downgraded without security review",
100
+ ],
101
+ decisions: [
102
+ "Security-first development: all inputs validated, all outputs encoded",
103
+ "Authentication changes require explicit user approval",
104
+ ],
105
+ },
106
+ };
107
+
108
+ export function getTemplateNames() {
109
+ return Object.keys(TEMPLATES);
110
+ }
111
+
112
+ export function getTemplate(name) {
113
+ return TEMPLATES[name] || null;
114
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * SpecLock MCP HTTP Server — for Railway / remote deployment
3
- * Wraps the same 19 tools as the stdio server using Streamable HTTP transport.
3
+ * Wraps the same 22 tools as the stdio server using Streamable HTTP transport.
4
4
  * Developed by Sandeep Roy (https://github.com/sgroy10)
5
5
  */
6
6
 
@@ -19,10 +19,15 @@ import {
19
19
  updateDeployFacts,
20
20
  logChange,
21
21
  checkConflict,
22
+ checkConflictAsync,
22
23
  getSessionBriefing,
23
24
  endSession,
24
25
  suggestLocks,
25
26
  detectDrift,
27
+ listTemplates,
28
+ applyTemplate,
29
+ generateReport,
30
+ auditStagedFiles,
26
31
  } from "../core/engine.js";
27
32
  import { generateContext, generateContextPack } from "../core/context.js";
28
33
  import {
@@ -41,7 +46,7 @@ import {
41
46
  } from "../core/git.js";
42
47
 
43
48
  const PROJECT_ROOT = process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
44
- const VERSION = "1.2.0";
49
+ const VERSION = "2.0.0";
45
50
  const AUTHOR = "Sandeep Roy";
46
51
 
47
52
  function createSpecLockServer() {
@@ -172,7 +177,7 @@ function createSpecLockServer() {
172
177
  // Tool 12: speclock_check_conflict
173
178
  server.tool("speclock_check_conflict", "Check if a proposed action conflicts with any active SpecLock.", { proposedAction: z.string().min(1).describe("Description of the action") }, async ({ proposedAction }) => {
174
179
  ensureInit(PROJECT_ROOT);
175
- const result = checkConflict(PROJECT_ROOT, proposedAction);
180
+ const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
176
181
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
177
182
  });
178
183
 
@@ -253,6 +258,41 @@ function createSpecLockServer() {
253
258
  return { content: [{ type: "text", text: `## SpecLock Health Check\n\nScore: **${score}/100** (Grade: ${grade})\nEvents: ${brain.events.count} | Reverts: ${brain.state.reverts.length}\n\n### Checks\n${checks.join("\n")}${agentTimeline}\n\n---\n*SpecLock v${VERSION} — Developed by ${AUTHOR}*` }] };
254
259
  });
255
260
 
261
+ // Tool 20: speclock_apply_template
262
+ server.tool("speclock_apply_template", "Apply a pre-built constraint template (nextjs, react, express, supabase, stripe, security-hardened).", { name: z.string().optional().describe("Template name. Omit to list.") }, async ({ name }) => {
263
+ ensureInit(PROJECT_ROOT);
264
+ if (!name) {
265
+ const templates = listTemplates();
266
+ const text = templates.map(t => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`).join("\n");
267
+ return { content: [{ type: "text", text: `## Available Templates\n\n${text}\n\nCall again with a name to apply.` }] };
268
+ }
269
+ const result = applyTemplate(PROJECT_ROOT, name);
270
+ if (!result.applied) return { content: [{ type: "text", text: result.error }], isError: true };
271
+ return { content: [{ type: "text", text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s).` }] };
272
+ });
273
+
274
+ // Tool 21: speclock_report
275
+ server.tool("speclock_report", "Violation report — how many times SpecLock blocked changes.", {}, async () => {
276
+ ensureInit(PROJECT_ROOT);
277
+ const report = generateReport(PROJECT_ROOT);
278
+ const parts = [`## Violation Report`, `Total blocked: **${report.totalViolations}**`];
279
+ if (report.mostTestedLocks.length > 0) {
280
+ parts.push("", "### Most Tested Locks");
281
+ for (const l of report.mostTestedLocks) parts.push(`- ${l.count}x — "${l.text}"`);
282
+ }
283
+ parts.push("", report.summary);
284
+ return { content: [{ type: "text", text: parts.join("\n") }] };
285
+ });
286
+
287
+ // Tool 22: speclock_audit
288
+ server.tool("speclock_audit", "Audit staged files against active locks.", {}, async () => {
289
+ ensureInit(PROJECT_ROOT);
290
+ const result = auditStagedFiles(PROJECT_ROOT);
291
+ if (result.passed) return { content: [{ type: "text", text: result.message }] };
292
+ const text = result.violations.map(v => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`).join("\n");
293
+ return { content: [{ type: "text", text: `## Audit Failed\n\n${text}\n\n${result.message}` }] };
294
+ });
295
+
256
296
  return server;
257
297
  }
258
298
 
@@ -292,7 +332,7 @@ app.get("/", (req, res) => {
292
332
  version: VERSION,
293
333
  author: AUTHOR,
294
334
  description: "AI Continuity Engine — Kill AI amnesia",
295
- tools: 19,
335
+ tools: 22,
296
336
  mcp_endpoint: "/mcp",
297
337
  npm: "https://www.npmjs.com/package/speclock",
298
338
  github: "https://github.com/sgroy10/speclock",
package/src/mcp/server.js CHANGED
@@ -12,12 +12,17 @@ import {
12
12
  updateDeployFacts,
13
13
  logChange,
14
14
  checkConflict,
15
+ checkConflictAsync,
15
16
  getSessionBriefing,
16
17
  endSession,
17
18
  suggestLocks,
18
19
  detectDrift,
19
20
  syncLocksToPackageJson,
20
21
  autoGuardRelatedFiles,
22
+ listTemplates,
23
+ applyTemplate,
24
+ generateReport,
25
+ auditStagedFiles,
21
26
  } from "../core/engine.js";
22
27
  import { generateContext, generateContextPack } from "../core/context.js";
23
28
  import {
@@ -52,7 +57,7 @@ const PROJECT_ROOT =
52
57
  args.project || process.env.SPECLOCK_PROJECT_ROOT || process.cwd();
53
58
 
54
59
  // --- MCP Server ---
55
- const VERSION = "1.2.0";
60
+ const VERSION = "2.0.0";
56
61
  const AUTHOR = "Sandeep Roy";
57
62
 
58
63
  const server = new McpServer(
@@ -423,7 +428,7 @@ server.tool(
423
428
  .describe("Description of the action you plan to take"),
424
429
  },
425
430
  async ({ proposedAction }) => {
426
- const result = checkConflict(PROJECT_ROOT, proposedAction);
431
+ const result = await checkConflictAsync(PROJECT_ROOT, proposedAction);
427
432
  return {
428
433
  content: [{ type: "text", text: result.analysis }],
429
434
  };
@@ -766,6 +771,118 @@ server.tool(
766
771
  }
767
772
  );
768
773
 
774
+ // ========================================
775
+ // TEMPLATE, REPORT & AUDIT TOOLS (v1.7.0)
776
+ // ========================================
777
+
778
+ // Tool 20: speclock_apply_template
779
+ server.tool(
780
+ "speclock_apply_template",
781
+ "Apply a pre-built constraint template (e.g., nextjs, react, express, supabase, stripe, security-hardened). Templates add recommended locks and decisions for common frameworks.",
782
+ {
783
+ name: z
784
+ .string()
785
+ .optional()
786
+ .describe("Template name to apply. Omit to list available templates."),
787
+ },
788
+ async ({ name }) => {
789
+ if (!name) {
790
+ const templates = listTemplates();
791
+ const formatted = templates
792
+ .map((t) => `- **${t.name}** (${t.displayName}): ${t.description} — ${t.lockCount} locks, ${t.decisionCount} decisions`)
793
+ .join("\n");
794
+ return {
795
+ content: [
796
+ {
797
+ type: "text",
798
+ text: `## Available Templates\n\n${formatted}\n\nCall again with a name to apply.`,
799
+ },
800
+ ],
801
+ };
802
+ }
803
+ const result = applyTemplate(PROJECT_ROOT, name);
804
+ if (!result.applied) {
805
+ return {
806
+ content: [{ type: "text", text: result.error }],
807
+ isError: true,
808
+ };
809
+ }
810
+ return {
811
+ content: [
812
+ {
813
+ type: "text",
814
+ text: `Template "${result.displayName}" applied: ${result.locksAdded} lock(s) + ${result.decisionsAdded} decision(s) added.`,
815
+ },
816
+ ],
817
+ };
818
+ }
819
+ );
820
+
821
+ // Tool 21: speclock_report
822
+ server.tool(
823
+ "speclock_report",
824
+ "Get a violation report showing how many times SpecLock blocked constraint violations, which locks were tested most, and recent violations.",
825
+ {},
826
+ async () => {
827
+ const report = generateReport(PROJECT_ROOT);
828
+
829
+ const parts = [`## SpecLock Violation Report`, ``, `Total violations blocked: **${report.totalViolations}**`];
830
+
831
+ if (report.timeRange) {
832
+ parts.push(`Period: ${report.timeRange.from.substring(0, 10)} to ${report.timeRange.to.substring(0, 10)}`);
833
+ }
834
+
835
+ if (report.mostTestedLocks.length > 0) {
836
+ parts.push("", "### Most Tested Locks");
837
+ for (const lock of report.mostTestedLocks) {
838
+ parts.push(`- ${lock.count}x — "${lock.text}"`);
839
+ }
840
+ }
841
+
842
+ if (report.recentViolations.length > 0) {
843
+ parts.push("", "### Recent Violations");
844
+ for (const v of report.recentViolations) {
845
+ parts.push(`- [${v.at.substring(0, 19)}] ${v.topLevel} (${v.topConfidence}%) — "${v.action}"`);
846
+ }
847
+ }
848
+
849
+ parts.push("", `---`, report.summary);
850
+
851
+ return {
852
+ content: [{ type: "text", text: parts.join("\n") }],
853
+ };
854
+ }
855
+ );
856
+
857
+ // Tool 22: speclock_audit
858
+ server.tool(
859
+ "speclock_audit",
860
+ "Audit git staged files against active SpecLock constraints. Returns pass/fail with details on which files violate locks. Used by the pre-commit hook.",
861
+ {},
862
+ async () => {
863
+ const result = auditStagedFiles(PROJECT_ROOT);
864
+
865
+ if (result.passed) {
866
+ return {
867
+ content: [{ type: "text", text: result.message }],
868
+ };
869
+ }
870
+
871
+ const formatted = result.violations
872
+ .map((v) => `- [${v.severity}] **${v.file}** — ${v.reason}\n Lock: "${v.lockText}"`)
873
+ .join("\n");
874
+
875
+ return {
876
+ content: [
877
+ {
878
+ type: "text",
879
+ text: `## Audit Failed\n\n${formatted}\n\n${result.message}`,
880
+ },
881
+ ],
882
+ };
883
+ }
884
+ );
885
+
769
886
  // --- Smithery sandbox export ---
770
887
  export default function createSandboxServer() {
771
888
  return server;