speclock 5.2.5 → 5.3.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 +144 -24
- package/package.json +242 -67
- package/src/cli/index.js +137 -7
- package/src/core/auth.js +341 -341
- package/src/core/compliance.js +1 -1
- package/src/core/engine.js +63 -1
- package/src/core/lock-author.js +487 -487
- package/src/core/replay.js +236 -0
- package/src/core/rules-sync.js +548 -0
- package/src/core/semantics.js +33 -0
- package/src/core/templates.js +69 -0
- package/src/dashboard/index.html +2 -2
- package/src/mcp/http-server.js +3 -3
- package/src/mcp/server.js +130 -1
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SpecLock Rules Sync — Universal AI Rules File Generator
|
|
3
|
+
* Syncs SpecLock constraints to .cursorrules, CLAUDE.md, AGENTS.md,
|
|
4
|
+
* .windsurfrules, copilot-instructions.md, GEMINI.md, and more.
|
|
5
|
+
*
|
|
6
|
+
* One source of truth → every AI tool gets the same constraints.
|
|
7
|
+
*
|
|
8
|
+
* Developed by Sandeep Roy (https://github.com/sgroy10)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { readBrain } from "./storage.js";
|
|
14
|
+
import { ensureInit } from "./memory.js";
|
|
15
|
+
|
|
16
|
+
// --- Format definitions ---
|
|
17
|
+
|
|
18
|
+
const FORMATS = {
|
|
19
|
+
cursor: {
|
|
20
|
+
name: "Cursor",
|
|
21
|
+
file: ".cursor/rules/speclock.mdc",
|
|
22
|
+
description: "Cursor AI rules (MDC format)",
|
|
23
|
+
generate: generateCursorRules,
|
|
24
|
+
},
|
|
25
|
+
claude: {
|
|
26
|
+
name: "Claude Code",
|
|
27
|
+
file: "CLAUDE.md",
|
|
28
|
+
description: "Claude Code project instructions",
|
|
29
|
+
generate: generateClaudeMd,
|
|
30
|
+
},
|
|
31
|
+
agents: {
|
|
32
|
+
name: "AGENTS.md",
|
|
33
|
+
file: "AGENTS.md",
|
|
34
|
+
description: "Cross-tool agent instructions (Linux Foundation standard)",
|
|
35
|
+
generate: generateAgentsMd,
|
|
36
|
+
},
|
|
37
|
+
windsurf: {
|
|
38
|
+
name: "Windsurf",
|
|
39
|
+
file: ".windsurf/rules/speclock.md",
|
|
40
|
+
description: "Windsurf AI rules",
|
|
41
|
+
generate: generateWindsurfRules,
|
|
42
|
+
},
|
|
43
|
+
copilot: {
|
|
44
|
+
name: "GitHub Copilot",
|
|
45
|
+
file: ".github/copilot-instructions.md",
|
|
46
|
+
description: "GitHub Copilot custom instructions",
|
|
47
|
+
generate: generateCopilotInstructions,
|
|
48
|
+
},
|
|
49
|
+
gemini: {
|
|
50
|
+
name: "Gemini",
|
|
51
|
+
file: "GEMINI.md",
|
|
52
|
+
description: "Google Gemini CLI instructions",
|
|
53
|
+
generate: generateGeminiMd,
|
|
54
|
+
},
|
|
55
|
+
codex: {
|
|
56
|
+
name: "Codex",
|
|
57
|
+
file: "AGENTS.md",
|
|
58
|
+
description: "OpenAI Codex agent instructions (uses AGENTS.md)",
|
|
59
|
+
generate: generateAgentsMd,
|
|
60
|
+
},
|
|
61
|
+
aider: {
|
|
62
|
+
name: "Aider",
|
|
63
|
+
file: ".aider.conf.yml",
|
|
64
|
+
description: "Aider conventions file",
|
|
65
|
+
generate: generateAiderConf,
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// --- Helpers ---
|
|
70
|
+
|
|
71
|
+
function getActiveLocks(brain) {
|
|
72
|
+
return (brain.specLock?.items || []).filter((l) => l.active !== false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getActiveDecisions(brain) {
|
|
76
|
+
return brain.decisions || [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getGoal(brain) {
|
|
80
|
+
return brain.goal?.text || "";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function timestamp() {
|
|
84
|
+
return new Date().toISOString().substring(0, 19).replace("T", " ");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function header(format) {
|
|
88
|
+
return `# SpecLock Constraints — Auto-synced for ${format}\n# Generated: ${timestamp()}\n# Source: .speclock/brain.json\n# Do not edit manually — run \`speclock sync\` to regenerate\n# https://github.com/sgroy10/speclock\n`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// --- Format generators ---
|
|
92
|
+
|
|
93
|
+
function generateCursorRules(brain) {
|
|
94
|
+
const locks = getActiveLocks(brain);
|
|
95
|
+
const decisions = getActiveDecisions(brain);
|
|
96
|
+
const goal = getGoal(brain);
|
|
97
|
+
|
|
98
|
+
const lines = [];
|
|
99
|
+
|
|
100
|
+
// MDC frontmatter
|
|
101
|
+
lines.push("---");
|
|
102
|
+
lines.push("description: SpecLock project constraints — enforced automatically");
|
|
103
|
+
lines.push("globs: **/*");
|
|
104
|
+
lines.push("alwaysApply: true");
|
|
105
|
+
lines.push("---");
|
|
106
|
+
lines.push("");
|
|
107
|
+
lines.push("# SpecLock Constraints");
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(`> Auto-synced from SpecLock. Run \`speclock sync --format cursor\` to update.`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
|
|
112
|
+
if (goal) {
|
|
113
|
+
lines.push("## Project Goal");
|
|
114
|
+
lines.push(goal);
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (locks.length > 0) {
|
|
119
|
+
lines.push("## Non-Negotiable Constraints");
|
|
120
|
+
lines.push("");
|
|
121
|
+
lines.push("**These constraints MUST be followed. Violating any of them is an error.**");
|
|
122
|
+
lines.push("");
|
|
123
|
+
for (const lock of locks) {
|
|
124
|
+
lines.push(`- **[LOCKED]** ${lock.text}`);
|
|
125
|
+
}
|
|
126
|
+
lines.push("");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (decisions.length > 0) {
|
|
130
|
+
lines.push("## Architectural Decisions");
|
|
131
|
+
lines.push("");
|
|
132
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
133
|
+
lines.push(`- ${dec.text}`);
|
|
134
|
+
}
|
|
135
|
+
lines.push("");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
lines.push("---");
|
|
139
|
+
lines.push("*Powered by [SpecLock](https://github.com/sgroy10/speclock) — AI Constraint Engine by Sandeep Roy*");
|
|
140
|
+
lines.push("");
|
|
141
|
+
|
|
142
|
+
return lines.join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function generateClaudeMd(brain) {
|
|
146
|
+
const locks = getActiveLocks(brain);
|
|
147
|
+
const decisions = getActiveDecisions(brain);
|
|
148
|
+
const goal = getGoal(brain);
|
|
149
|
+
|
|
150
|
+
const lines = [];
|
|
151
|
+
lines.push(header("Claude Code"));
|
|
152
|
+
lines.push("");
|
|
153
|
+
lines.push("# Project Constraints (SpecLock)");
|
|
154
|
+
lines.push("");
|
|
155
|
+
|
|
156
|
+
if (goal) {
|
|
157
|
+
lines.push("## Goal");
|
|
158
|
+
lines.push(goal);
|
|
159
|
+
lines.push("");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (locks.length > 0) {
|
|
163
|
+
lines.push("## IMPORTANT: Non-Negotiable Rules");
|
|
164
|
+
lines.push("");
|
|
165
|
+
lines.push("The following constraints are enforced by SpecLock. Do NOT violate any of them.");
|
|
166
|
+
lines.push("If a task conflicts with these constraints, STOP and inform the user.");
|
|
167
|
+
lines.push("");
|
|
168
|
+
for (let i = 0; i < locks.length; i++) {
|
|
169
|
+
lines.push(`${i + 1}. **${lock_text(locks[i])}**`);
|
|
170
|
+
}
|
|
171
|
+
lines.push("");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (decisions.length > 0) {
|
|
175
|
+
lines.push("## Architectural Decisions");
|
|
176
|
+
lines.push("");
|
|
177
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
178
|
+
lines.push(`- ${dec.text}`);
|
|
179
|
+
}
|
|
180
|
+
lines.push("");
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
lines.push("---");
|
|
184
|
+
lines.push("*Auto-synced by [SpecLock](https://github.com/sgroy10/speclock). Run `speclock sync` to update.*");
|
|
185
|
+
lines.push("");
|
|
186
|
+
|
|
187
|
+
return lines.join("\n");
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function lock_text(lock) {
|
|
191
|
+
return lock.originalText || lock.text;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function generateAgentsMd(brain) {
|
|
195
|
+
const locks = getActiveLocks(brain);
|
|
196
|
+
const decisions = getActiveDecisions(brain);
|
|
197
|
+
const goal = getGoal(brain);
|
|
198
|
+
|
|
199
|
+
const lines = [];
|
|
200
|
+
lines.push("# AGENTS.md");
|
|
201
|
+
lines.push("");
|
|
202
|
+
lines.push(`> Auto-synced from SpecLock. Run \`speclock sync --format agents\` to update.`);
|
|
203
|
+
lines.push("");
|
|
204
|
+
|
|
205
|
+
if (goal) {
|
|
206
|
+
lines.push("## Project Goal");
|
|
207
|
+
lines.push("");
|
|
208
|
+
lines.push(goal);
|
|
209
|
+
lines.push("");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (locks.length > 0) {
|
|
213
|
+
lines.push("## Constraints");
|
|
214
|
+
lines.push("");
|
|
215
|
+
lines.push("The following rules are non-negotiable. Any AI agent working on this codebase MUST respect them:");
|
|
216
|
+
lines.push("");
|
|
217
|
+
for (const lock of locks) {
|
|
218
|
+
lines.push(`- **NEVER VIOLATE:** ${lock_text(lock)}`);
|
|
219
|
+
}
|
|
220
|
+
lines.push("");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (decisions.length > 0) {
|
|
224
|
+
lines.push("## Decisions");
|
|
225
|
+
lines.push("");
|
|
226
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
227
|
+
lines.push(`- ${dec.text}`);
|
|
228
|
+
}
|
|
229
|
+
lines.push("");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push("## Working with this project");
|
|
233
|
+
lines.push("");
|
|
234
|
+
lines.push("- Before making changes, check if they conflict with the constraints above");
|
|
235
|
+
lines.push("- If a requested change violates a constraint, inform the user and suggest alternatives");
|
|
236
|
+
lines.push("- Run `speclock check \"<your planned action>\"` to verify before proceeding");
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push("---");
|
|
239
|
+
lines.push("*Powered by [SpecLock](https://github.com/sgroy10/speclock) — AI Constraint Engine by Sandeep Roy*");
|
|
240
|
+
lines.push("");
|
|
241
|
+
|
|
242
|
+
return lines.join("\n");
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function generateWindsurfRules(brain) {
|
|
246
|
+
const locks = getActiveLocks(brain);
|
|
247
|
+
const decisions = getActiveDecisions(brain);
|
|
248
|
+
const goal = getGoal(brain);
|
|
249
|
+
|
|
250
|
+
const lines = [];
|
|
251
|
+
lines.push("# SpecLock Constraints for Windsurf");
|
|
252
|
+
lines.push("");
|
|
253
|
+
lines.push(`> Auto-synced. Run \`speclock sync --format windsurf\` to update.`);
|
|
254
|
+
lines.push("");
|
|
255
|
+
|
|
256
|
+
if (goal) {
|
|
257
|
+
lines.push(`## Project Goal: ${goal}`);
|
|
258
|
+
lines.push("");
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (locks.length > 0) {
|
|
262
|
+
lines.push("## Rules (Non-Negotiable)");
|
|
263
|
+
lines.push("");
|
|
264
|
+
for (const lock of locks) {
|
|
265
|
+
lines.push(`- MUST: ${lock_text(lock)}`);
|
|
266
|
+
}
|
|
267
|
+
lines.push("");
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (decisions.length > 0) {
|
|
271
|
+
lines.push("## Architectural Decisions");
|
|
272
|
+
lines.push("");
|
|
273
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
274
|
+
lines.push(`- ${dec.text}`);
|
|
275
|
+
}
|
|
276
|
+
lines.push("");
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
lines.push("---");
|
|
280
|
+
lines.push("*Powered by [SpecLock](https://github.com/sgroy10/speclock)*");
|
|
281
|
+
lines.push("");
|
|
282
|
+
|
|
283
|
+
return lines.join("\n");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function generateCopilotInstructions(brain) {
|
|
287
|
+
const locks = getActiveLocks(brain);
|
|
288
|
+
const decisions = getActiveDecisions(brain);
|
|
289
|
+
const goal = getGoal(brain);
|
|
290
|
+
|
|
291
|
+
const lines = [];
|
|
292
|
+
lines.push("# GitHub Copilot Instructions (SpecLock)");
|
|
293
|
+
lines.push("");
|
|
294
|
+
lines.push(`> Auto-synced. Run \`speclock sync --format copilot\` to update.`);
|
|
295
|
+
lines.push("");
|
|
296
|
+
|
|
297
|
+
if (goal) {
|
|
298
|
+
lines.push(`## Project: ${goal}`);
|
|
299
|
+
lines.push("");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (locks.length > 0) {
|
|
303
|
+
lines.push("## Constraints");
|
|
304
|
+
lines.push("");
|
|
305
|
+
lines.push("When generating or modifying code, respect these constraints:");
|
|
306
|
+
lines.push("");
|
|
307
|
+
for (const lock of locks) {
|
|
308
|
+
lines.push(`- **DO NOT** violate: ${lock_text(lock)}`);
|
|
309
|
+
}
|
|
310
|
+
lines.push("");
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (decisions.length > 0) {
|
|
314
|
+
lines.push("## Project Decisions");
|
|
315
|
+
lines.push("");
|
|
316
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
317
|
+
lines.push(`- ${dec.text}`);
|
|
318
|
+
}
|
|
319
|
+
lines.push("");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
lines.push("---");
|
|
323
|
+
lines.push("*Auto-synced by [SpecLock](https://github.com/sgroy10/speclock)*");
|
|
324
|
+
lines.push("");
|
|
325
|
+
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function generateGeminiMd(brain) {
|
|
330
|
+
const locks = getActiveLocks(brain);
|
|
331
|
+
const decisions = getActiveDecisions(brain);
|
|
332
|
+
const goal = getGoal(brain);
|
|
333
|
+
|
|
334
|
+
const lines = [];
|
|
335
|
+
lines.push("# GEMINI.md — Project Constraints (SpecLock)");
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(`> Auto-synced. Run \`speclock sync --format gemini\` to update.`);
|
|
338
|
+
lines.push("");
|
|
339
|
+
|
|
340
|
+
if (goal) {
|
|
341
|
+
lines.push("## Goal");
|
|
342
|
+
lines.push(goal);
|
|
343
|
+
lines.push("");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (locks.length > 0) {
|
|
347
|
+
lines.push("## Non-Negotiable Constraints");
|
|
348
|
+
lines.push("");
|
|
349
|
+
lines.push("These rules must NEVER be violated:");
|
|
350
|
+
lines.push("");
|
|
351
|
+
for (const lock of locks) {
|
|
352
|
+
lines.push(`- ${lock_text(lock)}`);
|
|
353
|
+
}
|
|
354
|
+
lines.push("");
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (decisions.length > 0) {
|
|
358
|
+
lines.push("## Decisions");
|
|
359
|
+
lines.push("");
|
|
360
|
+
for (const dec of decisions.slice(0, 10)) {
|
|
361
|
+
lines.push(`- ${dec.text}`);
|
|
362
|
+
}
|
|
363
|
+
lines.push("");
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
lines.push("---");
|
|
367
|
+
lines.push("*Powered by [SpecLock](https://github.com/sgroy10/speclock) — AI Constraint Engine by Sandeep Roy*");
|
|
368
|
+
lines.push("");
|
|
369
|
+
|
|
370
|
+
return lines.join("\n");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function generateAiderConf(brain) {
|
|
374
|
+
const locks = getActiveLocks(brain);
|
|
375
|
+
const goal = getGoal(brain);
|
|
376
|
+
|
|
377
|
+
const lines = [];
|
|
378
|
+
lines.push("# Aider configuration — SpecLock constraints");
|
|
379
|
+
lines.push(`# Auto-synced. Run \`speclock sync --format aider\` to update.`);
|
|
380
|
+
lines.push("");
|
|
381
|
+
|
|
382
|
+
const conventionLines = [];
|
|
383
|
+
if (goal) {
|
|
384
|
+
conventionLines.push(`Project goal: ${goal}`);
|
|
385
|
+
conventionLines.push("");
|
|
386
|
+
}
|
|
387
|
+
if (locks.length > 0) {
|
|
388
|
+
conventionLines.push("NON-NEGOTIABLE CONSTRAINTS:");
|
|
389
|
+
for (const lock of locks) {
|
|
390
|
+
conventionLines.push(`- ${lock_text(lock)}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (conventionLines.length > 0) {
|
|
395
|
+
lines.push("conventions: |");
|
|
396
|
+
for (const cl of conventionLines) {
|
|
397
|
+
lines.push(` ${cl}`);
|
|
398
|
+
}
|
|
399
|
+
lines.push("");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return lines.join("\n");
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// --- Main sync function ---
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Sync SpecLock constraints to one or all AI tool rule formats.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} root - Project root directory
|
|
411
|
+
* @param {Object} options
|
|
412
|
+
* @param {string} [options.format] - Specific format to sync (cursor, claude, agents, windsurf, copilot, gemini, aider). If omitted, syncs all.
|
|
413
|
+
* @param {boolean} [options.dryRun] - If true, returns content without writing files
|
|
414
|
+
* @param {boolean} [options.append] - If true, appends to existing file instead of overwriting (for claude format)
|
|
415
|
+
* @returns {{ synced: Array<{format: string, file: string, size: number}>, errors: string[] }}
|
|
416
|
+
*/
|
|
417
|
+
export function syncRules(root, options = {}) {
|
|
418
|
+
const brain = ensureInit(root);
|
|
419
|
+
const activeLocks = getActiveLocks(brain);
|
|
420
|
+
|
|
421
|
+
if (activeLocks.length === 0) {
|
|
422
|
+
return {
|
|
423
|
+
synced: [],
|
|
424
|
+
errors: ["No active locks found. Add constraints first: speclock lock \"Your constraint\""],
|
|
425
|
+
lockCount: 0,
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const formats = options.format
|
|
430
|
+
? { [options.format]: FORMATS[options.format] }
|
|
431
|
+
: { ...FORMATS };
|
|
432
|
+
|
|
433
|
+
// Remove codex duplicate (uses same file as agents)
|
|
434
|
+
if (!options.format) {
|
|
435
|
+
delete formats.codex;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (options.format && !FORMATS[options.format]) {
|
|
439
|
+
return {
|
|
440
|
+
synced: [],
|
|
441
|
+
errors: [`Unknown format: "${options.format}". Available: ${Object.keys(FORMATS).join(", ")}`],
|
|
442
|
+
lockCount: activeLocks.length,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const synced = [];
|
|
447
|
+
const errors = [];
|
|
448
|
+
|
|
449
|
+
for (const [key, fmt] of Object.entries(formats)) {
|
|
450
|
+
try {
|
|
451
|
+
const content = fmt.generate(brain);
|
|
452
|
+
const filePath = path.join(root, fmt.file);
|
|
453
|
+
|
|
454
|
+
if (options.dryRun) {
|
|
455
|
+
synced.push({
|
|
456
|
+
format: key,
|
|
457
|
+
name: fmt.name,
|
|
458
|
+
file: fmt.file,
|
|
459
|
+
size: content.length,
|
|
460
|
+
content,
|
|
461
|
+
});
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Handle append mode for CLAUDE.md
|
|
466
|
+
if (options.append && key === "claude" && fs.existsSync(filePath)) {
|
|
467
|
+
const existing = fs.readFileSync(filePath, "utf8");
|
|
468
|
+
// Remove old SpecLock section if present
|
|
469
|
+
const cleaned = removeSpecLockSection(existing);
|
|
470
|
+
const merged = cleaned.trimEnd() + "\n\n" + content;
|
|
471
|
+
fs.writeFileSync(filePath, merged);
|
|
472
|
+
} else {
|
|
473
|
+
// Ensure directory exists
|
|
474
|
+
const dir = path.dirname(filePath);
|
|
475
|
+
if (!fs.existsSync(dir)) {
|
|
476
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
477
|
+
}
|
|
478
|
+
fs.writeFileSync(filePath, content);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
synced.push({
|
|
482
|
+
format: key,
|
|
483
|
+
name: fmt.name,
|
|
484
|
+
file: fmt.file,
|
|
485
|
+
size: content.length,
|
|
486
|
+
});
|
|
487
|
+
} catch (err) {
|
|
488
|
+
errors.push(`${key}: ${err.message}`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
synced,
|
|
494
|
+
errors,
|
|
495
|
+
lockCount: activeLocks.length,
|
|
496
|
+
decisionCount: getActiveDecisions(brain).length,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Remove existing SpecLock section from a file's content.
|
|
502
|
+
* Looks for the auto-sync header comment and removes everything until the next --- or EOF.
|
|
503
|
+
*/
|
|
504
|
+
function removeSpecLockSection(content) {
|
|
505
|
+
// Remove from "# SpecLock Constraints" or "# Project Constraints (SpecLock)" to end of SpecLock block
|
|
506
|
+
const markers = [
|
|
507
|
+
/# SpecLock Constraints.*$/m,
|
|
508
|
+
/# Project Constraints \(SpecLock\).*$/m,
|
|
509
|
+
/^# SpecLock Constraints — Auto-synced.*$/m,
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
for (const marker of markers) {
|
|
513
|
+
const match = content.match(marker);
|
|
514
|
+
if (match) {
|
|
515
|
+
const startIdx = content.indexOf(match[0]);
|
|
516
|
+
// Find the end marker: "Auto-synced by [SpecLock]" line
|
|
517
|
+
const endMarker = "*Auto-synced by [SpecLock]";
|
|
518
|
+
const endIdx = content.indexOf(endMarker, startIdx);
|
|
519
|
+
if (endIdx !== -1) {
|
|
520
|
+
// Remove until end of that line + trailing newlines
|
|
521
|
+
const lineEnd = content.indexOf("\n", endIdx + endMarker.length);
|
|
522
|
+
const removeEnd = lineEnd !== -1 ? lineEnd + 1 : content.length;
|
|
523
|
+
content = content.substring(0, startIdx) + content.substring(removeEnd);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return content;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Get list of available sync formats.
|
|
533
|
+
*/
|
|
534
|
+
export function getSyncFormats() {
|
|
535
|
+
return Object.entries(FORMATS).map(([key, fmt]) => ({
|
|
536
|
+
key,
|
|
537
|
+
name: fmt.name,
|
|
538
|
+
file: fmt.file,
|
|
539
|
+
description: fmt.description,
|
|
540
|
+
}));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Preview what would be generated for a specific format without writing.
|
|
545
|
+
*/
|
|
546
|
+
export function previewSync(root, format) {
|
|
547
|
+
return syncRules(root, { format, dryRun: true });
|
|
548
|
+
}
|
package/src/core/semantics.js
CHANGED
|
@@ -2297,6 +2297,39 @@ export function scoreConflict({ actionText, lockText }) {
|
|
|
2297
2297
|
intentAligned = true;
|
|
2298
2298
|
reasons.push("intent alignment: adding a database index is a performance optimization — does not modify locked schema");
|
|
2299
2299
|
}
|
|
2300
|
+
|
|
2301
|
+
// Pattern 6: Technology maintenance/refactoring vs exposure/secrets locks
|
|
2302
|
+
// "Refactor React component file structure" vs "never expose API keys in frontend code" → safe
|
|
2303
|
+
// "Update React Router to v7" vs "never expose API keys in frontend code" → safe
|
|
2304
|
+
// But: "Expose React state to window" → action mentions "expos" → NOT safe
|
|
2305
|
+
// But: "Add API key to React config" → action mentions "api key" → NOT safe
|
|
2306
|
+
// But: "Update endpoint to include email" vs "never expose email" → direct subject overlap → NOT safe
|
|
2307
|
+
// Root cause: concept map links react→frontend, matching "frontend" in exposure lock.
|
|
2308
|
+
// Fix: constructive tech verbs against exposure locks are safe when action doesn't touch secrets
|
|
2309
|
+
// AND there's no direct subject overlap (overlap is only through concept map expansion).
|
|
2310
|
+
if (!intentAligned && !_compoundDestructive) {
|
|
2311
|
+
const _isMaintenanceAction = /\b(?:refactor|restructure|reorganize|update|upgrade|bump|install|configure|optimize|improve|enhance|test|debug|fix|review|clean|format|lint|style|document|migrate)\b/i.test(_actionLowerSafe);
|
|
2312
|
+
const _lockMentionsExposure = /\b(?:expos(?:e|ed|es|ing)?|leak(?:s|ed|ing)?|secrets?|credentials?|api.?keys?|passwords?|tokens?|sensitive)\b/i.test(lockText);
|
|
2313
|
+
const _actionMentionsExposure = /\b(?:expos(?:e|ed|es|ing)?|leak(?:s|ed|ing)?|secrets?|credentials?|api.?keys?|passwords?|tokens?|sensitive|plain.?text|unencrypt)\b/i.test(_actionLowerSafe);
|
|
2314
|
+
// Guard: check for direct subject overlap between action and lock.
|
|
2315
|
+
// If the action directly mentions the lock's protected subjects (not via concept map),
|
|
2316
|
+
// Pattern 6 should not apply — the action touches the lock's domain.
|
|
2317
|
+
const _p6Exclude = /^(?:expos(?:e[ds]?|ing)?|leak(?:s|ed|ing)?|secrets?|credentials?|passwords?|tokens?|sensitive|never|must|should|always|code|dont|does|through|from|with|into|that|this)$/;
|
|
2318
|
+
const _lockSubjects = lockText.toLowerCase()
|
|
2319
|
+
.split(/[\s,]+/)
|
|
2320
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
2321
|
+
.filter(w => w.length > 3 && !_p6Exclude.test(w));
|
|
2322
|
+
const _actionWords6 = new Set(
|
|
2323
|
+
_actionLowerSafe.split(/[\s,]+/)
|
|
2324
|
+
.map(w => w.replace(/[^a-z0-9]/g, ''))
|
|
2325
|
+
.filter(w => w.length > 3)
|
|
2326
|
+
);
|
|
2327
|
+
const _directSubjectOverlap = _lockSubjects.some(w => _actionWords6.has(w));
|
|
2328
|
+
if (_isMaintenanceAction && _lockMentionsExposure && !_actionMentionsExposure && !_directSubjectOverlap) {
|
|
2329
|
+
intentAligned = true;
|
|
2330
|
+
reasons.push("intent alignment: technology maintenance action does not involve secrets/exposure — safe against exposure lock");
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2300
2333
|
}
|
|
2301
2334
|
|
|
2302
2335
|
// Check 3c: Working WITH locked technology (not replacing it)
|
package/src/core/templates.js
CHANGED
|
@@ -104,6 +104,75 @@ export const TEMPLATES = {
|
|
|
104
104
|
"Authentication changes require explicit user approval",
|
|
105
105
|
],
|
|
106
106
|
},
|
|
107
|
+
|
|
108
|
+
"safe-defaults": {
|
|
109
|
+
name: "safe-defaults",
|
|
110
|
+
displayName: "Safe Defaults (Vibe Coding Seatbelt)",
|
|
111
|
+
description: "Prevents the 5 most common AI disasters — database deletion, auth removal, secret exposure, error handling removal, logging disablement",
|
|
112
|
+
locks: [
|
|
113
|
+
"Never delete database tables, columns, or user records — migrations must only add or modify, never drop",
|
|
114
|
+
"Never remove or bypass authentication or authorization — login, signup, session management, and access control are sacred",
|
|
115
|
+
"Never expose API keys, secrets, passwords, or credentials in client-side code, logs, or error messages",
|
|
116
|
+
"Never remove error handling, try-catch blocks, or validation logic — these exist for a reason",
|
|
117
|
+
"Never disable logging, monitoring, or audit trails — observability keeps production alive",
|
|
118
|
+
],
|
|
119
|
+
decisions: [
|
|
120
|
+
"Safety-first: AI must ask before destructive operations",
|
|
121
|
+
"All database changes must be additive, not destructive",
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
hipaa: {
|
|
126
|
+
name: "hipaa",
|
|
127
|
+
displayName: "HIPAA Healthcare",
|
|
128
|
+
description: "8 constraints for HIPAA-compliant healthcare applications — protects PHI, encryption, audit trails",
|
|
129
|
+
locks: [
|
|
130
|
+
"Protected Health Information (PHI) must never be logged, exposed in error messages, or sent to third-party services",
|
|
131
|
+
"All PHI must be encrypted at rest (AES-256) and in transit (TLS 1.2+) — never store PHI in plaintext",
|
|
132
|
+
"Authentication must use MFA — never disable or downgrade multi-factor authentication",
|
|
133
|
+
"Audit logging must capture all access to patient records — never disable audit trails",
|
|
134
|
+
"Patient data must never be deleted without explicit compliance review — soft-delete only",
|
|
135
|
+
"FHIR API endpoints must not have breaking changes — healthcare integrations depend on stability",
|
|
136
|
+
"Session timeout must not exceed 15 minutes of inactivity — never increase or disable session expiry",
|
|
137
|
+
"Role-based access control must be enforced on all patient data endpoints — never bypass RBAC",
|
|
138
|
+
],
|
|
139
|
+
decisions: [
|
|
140
|
+
"HIPAA compliance is mandatory — all features must pass compliance review",
|
|
141
|
+
"PHI storage uses encrypted-at-rest database with per-row access logging",
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
"api-stability": {
|
|
146
|
+
name: "api-stability",
|
|
147
|
+
displayName: "API Stability",
|
|
148
|
+
description: "6 constraints for public API projects — protects endpoints, response shapes, versioning",
|
|
149
|
+
locks: [
|
|
150
|
+
"Never remove or rename existing API endpoints — deprecated endpoints must continue to work",
|
|
151
|
+
"Never change the shape of API response objects — adding fields is OK, removing or renaming breaks clients",
|
|
152
|
+
"Never change HTTP status codes for existing endpoints — clients depend on specific codes",
|
|
153
|
+
"API versioning must be maintained — never merge v2 changes into v1 endpoints",
|
|
154
|
+
"Rate limiting and authentication on API endpoints must not be removed or weakened",
|
|
155
|
+
"Database schema changes must not break existing API contracts — migrations must be backward-compatible",
|
|
156
|
+
],
|
|
157
|
+
decisions: [
|
|
158
|
+
"API follows semantic versioning — breaking changes require a new API version",
|
|
159
|
+
"All API changes must be backward-compatible within the same version",
|
|
160
|
+
],
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
"solo-founder": {
|
|
164
|
+
name: "solo-founder",
|
|
165
|
+
displayName: "Solo Founder",
|
|
166
|
+
description: "3 essential constraints for solo builders — protects the things that cost you the most time to fix",
|
|
167
|
+
locks: [
|
|
168
|
+
"Never delete or drop database tables, user data, or production records — my users' data is sacred",
|
|
169
|
+
"Never modify the payment or billing system without explicit permission — revenue is life",
|
|
170
|
+
"Never remove authentication, session management, or access control — security is non-negotiable",
|
|
171
|
+
],
|
|
172
|
+
decisions: [
|
|
173
|
+
"Ship fast but never break auth, payments, or user data",
|
|
174
|
+
],
|
|
175
|
+
},
|
|
107
176
|
};
|
|
108
177
|
|
|
109
178
|
export function getTemplateNames() {
|
package/src/dashboard/index.html
CHANGED
|
@@ -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.
|
|
92
|
+
<div class="meta">v5.3.0 — 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.
|
|
185
|
+
SpecLock v5.3.0 — Developed by Sandeep Roy — <a href="https://github.com/sgroy10/speclock" style="color:var(--accent)">GitHub</a>
|
|
186
186
|
</div>
|
|
187
187
|
|
|
188
188
|
<script>
|