@a13xu/lucid 1.0.0 → 1.1.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.
@@ -0,0 +1 @@
1
+ export declare const CHECKLIST = "# Logic Guardian \u2014 Validation Checklist (5 passes)\n\n## Pass 1: Logic Trace\nTrace through the code with CONCRETE values:\n- Happy path \u2192 real values, write each variable state\n- Empty/zero \u2192 null, 0, \"\", []\n- Boundary \u2192 first element, last element, max int, single char\n- Error case \u2192 network down, file missing, permission denied\n\nSTOP if any trace produces unexpected output. Fix before continuing.\n\n## Pass 2: Contract Verification\n- [ ] Preconditions: what must be true BEFORE this runs? Is it checked?\n- [ ] Postconditions: what must be true AFTER? Can you prove it?\n- [ ] Invariants: what must ALWAYS be true? Does the code maintain it?\n- [ ] Return type: does EVERY code path return the expected type?\n- [ ] Side effects: are all side effects intentional?\n\n## Pass 3: Stupid Mistakes Checklist\n\n### Off-by-one\n- [ ] < vs <= \u2014 verify with boundary values\n- [ ] Array indices \u2014 last is length - 1\n- [ ] Loop iterations \u2014 exactly N times?\n\n### Null/Undefined Propagation\n- [ ] Every .property access \u2014 can the object be null?\n- [ ] Every array index \u2014 can the array be empty?\n- [ ] Every map lookup \u2014 can the key be missing?\n\n### Type Confusion\n- [ ] String vs Number comparisons\n- [ ] Integer vs Float division\n- [ ] Boolean coercion edge cases\n\n### Logic Inversions (THE #1 LLM drift pattern)\n- [ ] if/else \u2014 is the condition testing what you THINK?\n- [ ] Early returns \u2014 does the guard return the RIGHT value?\n- [ ] filter/find/some \u2014 keeping the RIGHT elements?\n- [ ] Error handling \u2014 catching and re-throwing correctly?\n\n### State & Mutation\n- [ ] Mutating shared object when you should copy?\n- [ ] Async state read after it might have changed?\n\n### Copy-Paste Drift\n- [ ] ALL variable names updated in copied blocks?\n- [ ] Conditions changed, not just variable names?\n\n## Pass 4: Integration Sanity\n- [ ] Breaks existing callers?\n- [ ] Imports/exports correct?\n- [ ] If async, all callers awaiting it?\n- [ ] If type changed, all usages updated?\n\n## Pass 5: Explain It Test\nIn ONE sentence: what does this code do?\nIf you can't explain it, or the sentence doesn't match the code \u2192 something is wrong.\n\n## Anti-Drift Triggers\nSTOP if you find yourself thinking:\n- \"This is similar to...\" \u2192 You're pattern-matching. TRACE THE LOGIC.\n- \"This should work because the other one does\" \u2192 VERIFY INDEPENDENTLY.\n- \"I'll just copy and change the names\" \u2192 CHECK EVERY DIFFERENCE.\n- \"The error handling is probably fine\" \u2192 TRACE THE ERROR PATH.\n- \"This is standard boilerplate\" \u2192 Verify it fits this context.\n";
@@ -0,0 +1,67 @@
1
+ export const CHECKLIST = `# Logic Guardian — Validation Checklist (5 passes)
2
+
3
+ ## Pass 1: Logic Trace
4
+ Trace through the code with CONCRETE values:
5
+ - Happy path → real values, write each variable state
6
+ - Empty/zero → null, 0, "", []
7
+ - Boundary → first element, last element, max int, single char
8
+ - Error case → network down, file missing, permission denied
9
+
10
+ STOP if any trace produces unexpected output. Fix before continuing.
11
+
12
+ ## Pass 2: Contract Verification
13
+ - [ ] Preconditions: what must be true BEFORE this runs? Is it checked?
14
+ - [ ] Postconditions: what must be true AFTER? Can you prove it?
15
+ - [ ] Invariants: what must ALWAYS be true? Does the code maintain it?
16
+ - [ ] Return type: does EVERY code path return the expected type?
17
+ - [ ] Side effects: are all side effects intentional?
18
+
19
+ ## Pass 3: Stupid Mistakes Checklist
20
+
21
+ ### Off-by-one
22
+ - [ ] < vs <= — verify with boundary values
23
+ - [ ] Array indices — last is length - 1
24
+ - [ ] Loop iterations — exactly N times?
25
+
26
+ ### Null/Undefined Propagation
27
+ - [ ] Every .property access — can the object be null?
28
+ - [ ] Every array index — can the array be empty?
29
+ - [ ] Every map lookup — can the key be missing?
30
+
31
+ ### Type Confusion
32
+ - [ ] String vs Number comparisons
33
+ - [ ] Integer vs Float division
34
+ - [ ] Boolean coercion edge cases
35
+
36
+ ### Logic Inversions (THE #1 LLM drift pattern)
37
+ - [ ] if/else — is the condition testing what you THINK?
38
+ - [ ] Early returns — does the guard return the RIGHT value?
39
+ - [ ] filter/find/some — keeping the RIGHT elements?
40
+ - [ ] Error handling — catching and re-throwing correctly?
41
+
42
+ ### State & Mutation
43
+ - [ ] Mutating shared object when you should copy?
44
+ - [ ] Async state read after it might have changed?
45
+
46
+ ### Copy-Paste Drift
47
+ - [ ] ALL variable names updated in copied blocks?
48
+ - [ ] Conditions changed, not just variable names?
49
+
50
+ ## Pass 4: Integration Sanity
51
+ - [ ] Breaks existing callers?
52
+ - [ ] Imports/exports correct?
53
+ - [ ] If async, all callers awaiting it?
54
+ - [ ] If type changed, all usages updated?
55
+
56
+ ## Pass 5: Explain It Test
57
+ In ONE sentence: what does this code do?
58
+ If you can't explain it, or the sentence doesn't match the code → something is wrong.
59
+
60
+ ## Anti-Drift Triggers
61
+ STOP if you find yourself thinking:
62
+ - "This is similar to..." → You're pattern-matching. TRACE THE LOGIC.
63
+ - "This should work because the other one does" → VERIFY INDEPENDENTLY.
64
+ - "I'll just copy and change the names" → CHECK EVERY DIFFERENCE.
65
+ - "The error handling is probably fine" → TRACE THE ERROR PATH.
66
+ - "This is standard boilerplate" → Verify it fits this context.
67
+ `;
@@ -0,0 +1,21 @@
1
+ export type Severity = "critical" | "high" | "medium" | "low" | "info";
2
+ export interface Issue {
3
+ file: string;
4
+ line: number;
5
+ severity: Severity;
6
+ driftId: string;
7
+ message: string;
8
+ suggestion?: string;
9
+ snippet?: string;
10
+ }
11
+ export declare function formatIssue(issue: Issue): string;
12
+ export declare function detectLanguage(filepath: string): string;
13
+ export interface ValidationResult {
14
+ issues: Issue[];
15
+ filesChecked: number;
16
+ linesChecked: number;
17
+ passed: boolean;
18
+ }
19
+ export declare function validateSource(filepath: string, source: string, lang?: string): Issue[];
20
+ export declare function validateFile(filepath: string): Issue[];
21
+ export declare function formatReport(filepath: string, issues: Issue[]): string;
@@ -0,0 +1,332 @@
1
+ import { readFileSync } from "fs";
2
+ import { extname } from "path";
3
+ const SEVERITY_ORDER = {
4
+ critical: 0, high: 1, medium: 2, low: 3, info: 4,
5
+ };
6
+ const SEVERITY_ICON = {
7
+ critical: "🔴", high: "🟠", medium: "🟡", low: "🔵", info: "ℹ️",
8
+ };
9
+ export function formatIssue(issue) {
10
+ const icon = SEVERITY_ICON[issue.severity];
11
+ let s = `${icon} [${issue.driftId}] ${issue.file}:${issue.line} — ${issue.message}`;
12
+ if (issue.suggestion)
13
+ s += `\n 💡 ${issue.suggestion}`;
14
+ if (issue.snippet)
15
+ s += `\n 📄 ${issue.snippet.trim().slice(0, 80)}`;
16
+ return s;
17
+ }
18
+ // ---------------------------------------------------------------------------
19
+ // Language detection
20
+ // ---------------------------------------------------------------------------
21
+ const LANG_MAP = {
22
+ ".py": "python",
23
+ ".js": "javascript",
24
+ ".jsx": "javascript",
25
+ ".ts": "typescript",
26
+ ".tsx": "typescript",
27
+ ".rs": "rust",
28
+ ".go": "go",
29
+ };
30
+ export function detectLanguage(filepath) {
31
+ return LANG_MAP[extname(filepath).toLowerCase()] ?? "generic";
32
+ }
33
+ // ---------------------------------------------------------------------------
34
+ // Python analyzer (regex-based port of AST checks)
35
+ // ---------------------------------------------------------------------------
36
+ function analyzePython(filepath, lines) {
37
+ const issues = [];
38
+ for (let i = 0; i < lines.length; i++) {
39
+ const line = lines[i];
40
+ const num = i + 1;
41
+ const trimmed = line.trim();
42
+ // Mutable default argument: def f(x=[], def f(x={})
43
+ if (/def\s+\w+\s*\(/.test(trimmed) && /=\s*[\[\{]/.test(trimmed)) {
44
+ issues.push({
45
+ file: filepath, line: num, severity: "high",
46
+ driftId: "PY-MUT-DEFAULT",
47
+ message: "Mutable default argument in function definition",
48
+ suggestion: "Use None as default and create inside function body.",
49
+ snippet: trimmed,
50
+ });
51
+ }
52
+ // Bare except:
53
+ if (/^\s*except\s*:/.test(line)) {
54
+ issues.push({
55
+ file: filepath, line: num, severity: "high",
56
+ driftId: "PY-BARE-EXCEPT",
57
+ message: "Bare `except:` catches everything including KeyboardInterrupt",
58
+ suggestion: "Use `except Exception:` or catch specific exceptions.",
59
+ });
60
+ }
61
+ // Silent except (except followed by pass on next line)
62
+ if (/^\s*except[\s:]/.test(line) && i + 1 < lines.length) {
63
+ const next = lines[i + 1].trim();
64
+ if (next === "pass") {
65
+ issues.push({
66
+ file: filepath, line: num, severity: "critical",
67
+ driftId: "DRIFT-002",
68
+ message: "Exception silently swallowed with `pass`",
69
+ suggestion: "Log the error, re-raise, or handle explicitly.",
70
+ });
71
+ }
72
+ }
73
+ // == None instead of is None
74
+ if (/==\s*None/.test(trimmed) && !trimmed.startsWith("#")) {
75
+ issues.push({
76
+ file: filepath, line: num, severity: "medium",
77
+ driftId: "PY-IS-NONE",
78
+ message: "Using `== None` instead of `is None`",
79
+ suggestion: "Use `is None` for None checks (PEP 8).",
80
+ snippet: trimmed,
81
+ });
82
+ }
83
+ // != None instead of is not None
84
+ if (/!=\s*None/.test(trimmed) && !trimmed.startsWith("#")) {
85
+ issues.push({
86
+ file: filepath, line: num, severity: "medium",
87
+ driftId: "PY-IS-NONE",
88
+ message: "Using `!= None` instead of `is not None`",
89
+ suggestion: "Use `is not None` for None checks (PEP 8).",
90
+ snippet: trimmed,
91
+ });
92
+ }
93
+ // f-string without interpolation
94
+ if (/f['"][^{'"]*['"]/.test(trimmed) && !trimmed.includes("{")) {
95
+ issues.push({
96
+ file: filepath, line: num, severity: "low",
97
+ driftId: "PY-FSTRING-EMPTY",
98
+ message: "f-string without any interpolation",
99
+ suggestion: "Remove the `f` prefix or add variables.",
100
+ snippet: trimmed,
101
+ });
102
+ }
103
+ // async def without await in body (heuristic: next non-empty line doesn't await)
104
+ if (/^\s*async\s+def\s+/.test(line)) {
105
+ // Collect the function body (next ~20 lines) and check for await
106
+ const body = lines.slice(i + 1, i + 20).join("\n");
107
+ if (!body.includes("await ") && !body.includes("async for") && !body.includes("async with")) {
108
+ issues.push({
109
+ file: filepath, line: num, severity: "medium",
110
+ driftId: "DRIFT-003",
111
+ message: "async function may not use await — could be incorrectly async",
112
+ suggestion: "Verify this function actually needs to be async.",
113
+ snippet: trimmed,
114
+ });
115
+ }
116
+ }
117
+ }
118
+ return issues;
119
+ }
120
+ // ---------------------------------------------------------------------------
121
+ // JavaScript / TypeScript analyzer
122
+ // ---------------------------------------------------------------------------
123
+ function analyzeJavaScript(filepath, lines) {
124
+ const issues = [];
125
+ for (let i = 0; i < lines.length; i++) {
126
+ const line = lines[i];
127
+ const num = i + 1;
128
+ const trimmed = line.trim();
129
+ // Skip comments
130
+ if (trimmed.startsWith("//") || trimmed.startsWith("*"))
131
+ continue;
132
+ // == instead of === (but not !== or ===)
133
+ if (/[^!=><]={2}[^=]/.test(trimmed) && !/={3}/.test(trimmed)) {
134
+ issues.push({
135
+ file: filepath, line: num, severity: "medium",
136
+ driftId: "JS-STRICT-EQ",
137
+ message: "Using `==` instead of `===`",
138
+ suggestion: "Use strict equality `===` unless coercion is intentional.",
139
+ snippet: trimmed,
140
+ });
141
+ }
142
+ // console.log left in
143
+ if (/console\.log\s*\(/.test(trimmed)) {
144
+ issues.push({
145
+ file: filepath, line: num, severity: "low",
146
+ driftId: "JS-CONSOLE",
147
+ message: "console.log() left in code",
148
+ suggestion: "Remove or replace with proper logging.",
149
+ snippet: trimmed,
150
+ });
151
+ }
152
+ // .then() without .catch() on same line
153
+ if (/\.then\s*\(/.test(trimmed) && !/\.catch\s*\(/.test(trimmed)) {
154
+ issues.push({
155
+ file: filepath, line: num, severity: "medium",
156
+ driftId: "JS-UNCAUGHT-PROMISE",
157
+ message: "`.then()` without `.catch()` — unhandled promise rejection",
158
+ suggestion: "Add `.catch()` or use try/catch with async/await.",
159
+ snippet: trimmed,
160
+ });
161
+ }
162
+ // .sort() without comparator
163
+ if (/\.sort\s*\(\s*\)/.test(trimmed)) {
164
+ issues.push({
165
+ file: filepath, line: num, severity: "high",
166
+ driftId: "JS-SORT-DEFAULT",
167
+ message: "`.sort()` without comparator sorts as strings",
168
+ suggestion: "Use `.sort((a, b) => a - b)` for numeric sort.",
169
+ snippet: trimmed,
170
+ });
171
+ }
172
+ // any type in TypeScript
173
+ if (/:\s*any\b/.test(trimmed) || /as\s+any\b/.test(trimmed)) {
174
+ issues.push({
175
+ file: filepath, line: num, severity: "low",
176
+ driftId: "TS-ANY",
177
+ message: "`any` type leaking through — disables type safety",
178
+ suggestion: "Use a specific type or `unknown` with type narrowing.",
179
+ snippet: trimmed,
180
+ });
181
+ }
182
+ // non-null assertion masking bugs
183
+ if (/\w!\.\w/.test(trimmed) || /\w!\[/.test(trimmed)) {
184
+ issues.push({
185
+ file: filepath, line: num, severity: "medium",
186
+ driftId: "TS-NON-NULL",
187
+ message: "Non-null assertion `!` — could crash if value is actually null",
188
+ suggestion: "Add explicit null check instead.",
189
+ snippet: trimmed,
190
+ });
191
+ }
192
+ // Early return with potential wrong value (heuristic)
193
+ if (/return\s+true\b/.test(trimmed) || /return\s+false\b/.test(trimmed)) {
194
+ issues.push({
195
+ file: filepath, line: num, severity: "info",
196
+ driftId: "DRIFT-005",
197
+ message: "Boolean return — verify this is the correct value for this branch",
198
+ suggestion: "Logic inversions are the #1 LLM drift pattern. Double-check.",
199
+ snippet: trimmed,
200
+ });
201
+ }
202
+ }
203
+ return issues;
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Generic analyzer (language-agnostic)
207
+ // ---------------------------------------------------------------------------
208
+ function analyzeGeneric(filepath, lines) {
209
+ const issues = [];
210
+ // TODO / FIXME / HACK markers
211
+ for (let i = 0; i < lines.length; i++) {
212
+ const line = lines[i];
213
+ const num = i + 1;
214
+ for (const marker of ["TODO", "FIXME", "HACK", "XXX", "BUG"]) {
215
+ if (line.toUpperCase().includes(marker) && /[/#*\-]/.test(line)) {
216
+ issues.push({
217
+ file: filepath, line: num, severity: "info",
218
+ driftId: "MARKER",
219
+ message: `${marker} marker found`,
220
+ snippet: line.trim(),
221
+ });
222
+ }
223
+ }
224
+ }
225
+ // Near-duplicate blocks (copy-paste drift detection)
226
+ const BLOCK_SIZE = 5;
227
+ const seen = new Map();
228
+ for (let i = 0; i <= lines.length - BLOCK_SIZE; i++) {
229
+ const block = lines.slice(i, i + BLOCK_SIZE).map((l) => l.trim()).filter(Boolean);
230
+ if (block.length < 3)
231
+ continue;
232
+ // Normalize: replace identifiers with placeholder
233
+ const sig = block.map((l) => l.replace(/\b[a-z_]\w*\b/g, "_")).join("|");
234
+ const prev = seen.get(sig);
235
+ if (prev !== undefined && i - prev > BLOCK_SIZE) {
236
+ issues.push({
237
+ file: filepath, line: i + 1, severity: "medium",
238
+ driftId: "DRIFT-007",
239
+ message: `Near-duplicate block (similar to line ${prev + 1})`,
240
+ suggestion: "Verify all variable names were updated correctly in the copy.",
241
+ });
242
+ }
243
+ else {
244
+ seen.set(sig, i);
245
+ }
246
+ }
247
+ // Magic numbers (2+ digit numbers not in common whitelist)
248
+ const MAGIC_WHITELIST = new Set([10, 16, 32, 64, 100, 128, 256, 512, 1000, 1024, 2048, 4096, 8080, 3000, 8000]);
249
+ for (let i = 0; i < lines.length; i++) {
250
+ const line = lines[i];
251
+ const trimmed = line.trim();
252
+ if (/^[/#*\-]/.test(trimmed))
253
+ continue;
254
+ for (const match of trimmed.matchAll(/(?<![.\w])(\d{2,})(?![.\w])/g)) {
255
+ const num = parseInt(match[1], 10);
256
+ if (!MAGIC_WHITELIST.has(num) && !/port|size|limit|max|min/i.test(trimmed)) {
257
+ issues.push({
258
+ file: filepath, line: i + 1, severity: "low",
259
+ driftId: "MAGIC-NUM",
260
+ message: `Magic number \`${num}\` — consider a named constant`,
261
+ snippet: trimmed,
262
+ });
263
+ break; // one per line is enough
264
+ }
265
+ }
266
+ }
267
+ return issues;
268
+ }
269
+ export function validateSource(filepath, source, lang) {
270
+ const language = lang ?? detectLanguage(filepath);
271
+ const lines = source.split("\n");
272
+ const issues = [];
273
+ if (language === "python") {
274
+ issues.push(...analyzePython(filepath, lines));
275
+ }
276
+ else if (language === "javascript" || language === "typescript") {
277
+ issues.push(...analyzeJavaScript(filepath, lines));
278
+ }
279
+ issues.push(...analyzeGeneric(filepath, lines));
280
+ issues.sort((a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]);
281
+ return issues;
282
+ }
283
+ export function validateFile(filepath) {
284
+ let source;
285
+ try {
286
+ source = readFileSync(filepath, { encoding: "utf-8" });
287
+ }
288
+ catch (err) {
289
+ return [{
290
+ file: filepath, line: 0, severity: "critical",
291
+ driftId: "IO-ERROR",
292
+ message: `Cannot read file: ${err instanceof Error ? err.message : String(err)}`,
293
+ }];
294
+ }
295
+ return validateSource(filepath, source);
296
+ }
297
+ export function formatReport(filepath, issues) {
298
+ const lines = [
299
+ "=".repeat(60),
300
+ "🛡️ LOGIC GUARDIAN — Validation Report",
301
+ "=".repeat(60),
302
+ `File: ${filepath}`,
303
+ `Issues: ${issues.length}`,
304
+ "",
305
+ ];
306
+ if (issues.length === 0) {
307
+ lines.push("✅ No issues detected. Proceed with confidence.");
308
+ lines.push("");
309
+ lines.push("⚠️ Automated checks catch ~40% of drift patterns.");
310
+ lines.push(" The manual checklist (Passes 1-5) catches the rest.");
311
+ }
312
+ else {
313
+ const bySeverity = (s) => issues.filter((i) => i.severity === s);
314
+ for (const sev of ["critical", "high", "medium", "low", "info"]) {
315
+ const group = bySeverity(sev);
316
+ if (group.length > 0) {
317
+ lines.push(`--- ${sev.toUpperCase()} (${group.length}) ---`);
318
+ for (const issue of group)
319
+ lines.push(formatIssue(issue));
320
+ lines.push("");
321
+ }
322
+ }
323
+ const criticalCount = bySeverity("critical").length;
324
+ const highCount = bySeverity("high").length;
325
+ const passed = criticalCount === 0 && highCount === 0;
326
+ lines.push(passed
327
+ ? "✅ PASS — No critical or high severity issues."
328
+ : "❌ FAIL — Fix critical/high issues before proceeding.");
329
+ }
330
+ lines.push("=".repeat(60));
331
+ return lines.join("\n");
332
+ }
package/build/index.js CHANGED
@@ -10,6 +10,7 @@ import { recall, RecallSchema } from "./tools/recall.js";
10
10
  import { recallAll } from "./tools/recall-all.js";
11
11
  import { forget, ForgetSchema } from "./tools/forget.js";
12
12
  import { memoryStats } from "./tools/stats.js";
13
+ import { handleValidateFile, ValidateFileSchema, handleCheckDrift, CheckDriftSchema, handleGetChecklist, } from "./tools/guardian.js";
13
14
  // ---------------------------------------------------------------------------
14
15
  // Init DB
15
16
  // ---------------------------------------------------------------------------
@@ -18,12 +19,13 @@ const stmts = prepareStatements(db);
18
19
  // ---------------------------------------------------------------------------
19
20
  // MCP Server
20
21
  // ---------------------------------------------------------------------------
21
- const server = new Server({ name: "lucid", version: "1.0.0" }, { capabilities: { tools: {} } });
22
+ const server = new Server({ name: "lucid", version: "1.1.0" }, { capabilities: { tools: {} } });
22
23
  // ---------------------------------------------------------------------------
23
24
  // Tool definitions
24
25
  // ---------------------------------------------------------------------------
25
26
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
26
27
  tools: [
28
+ // ── Memory ──────────────────────────────────────────────────────────────
27
29
  {
28
30
  name: "remember",
29
31
  description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
@@ -88,6 +90,43 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
88
90
  description: "Get memory usage statistics.",
89
91
  inputSchema: { type: "object", properties: {} },
90
92
  },
93
+ // ── Logic Guardian ───────────────────────────────────────────────────────
94
+ {
95
+ name: "validate_file",
96
+ description: "Run Logic Guardian validation on a source file. Detects LLM drift patterns: " +
97
+ "logic inversions, null propagation, type confusion, copy-paste drift, silent exceptions, and more. " +
98
+ "Supports Python, JavaScript, TypeScript. Use after writing or modifying any code.",
99
+ inputSchema: {
100
+ type: "object",
101
+ properties: {
102
+ path: { type: "string", description: "Absolute or relative path to the file to validate." },
103
+ },
104
+ required: ["path"],
105
+ },
106
+ },
107
+ {
108
+ name: "check_drift",
109
+ description: "Analyze a code snippet for LLM drift patterns without saving to disk. " +
110
+ "Use this to validate code before writing it to a file.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ code: { type: "string", description: "The code snippet to analyze." },
115
+ language: {
116
+ type: "string",
117
+ enum: ["python", "javascript", "typescript", "generic"],
118
+ description: "Programming language. Defaults to 'generic'.",
119
+ },
120
+ },
121
+ required: ["code"],
122
+ },
123
+ },
124
+ {
125
+ name: "get_checklist",
126
+ description: "Get the full Logic Guardian validation checklist (5 passes). " +
127
+ "Call this before marking any implementation task as done.",
128
+ inputSchema: { type: "object", properties: {} },
129
+ },
91
130
  ],
92
131
  }));
93
132
  // ---------------------------------------------------------------------------
@@ -98,52 +137,45 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
137
  try {
99
138
  let text;
100
139
  switch (name) {
101
- case "remember": {
102
- const input = RememberSchema.parse(args);
103
- text = remember(stmts, input);
140
+ // Memory
141
+ case "remember":
142
+ text = remember(stmts, RememberSchema.parse(args));
104
143
  break;
105
- }
106
- case "relate": {
107
- const input = RelateSchema.parse(args);
108
- text = relate(stmts, input);
144
+ case "relate":
145
+ text = relate(stmts, RelateSchema.parse(args));
109
146
  break;
110
- }
111
- case "recall": {
112
- const input = RecallSchema.parse(args);
113
- text = recall(stmts, input);
147
+ case "recall":
148
+ text = recall(stmts, RecallSchema.parse(args));
114
149
  break;
115
- }
116
- case "recall_all": {
150
+ case "recall_all":
117
151
  text = recallAll(db, stmts);
118
152
  break;
119
- }
120
- case "forget": {
121
- const input = ForgetSchema.parse(args);
122
- text = forget(stmts, input);
153
+ case "forget":
154
+ text = forget(stmts, ForgetSchema.parse(args));
123
155
  break;
124
- }
125
- case "memory_stats": {
156
+ case "memory_stats":
126
157
  text = memoryStats(db, stmts);
127
158
  break;
128
- }
159
+ // Logic Guardian
160
+ case "validate_file":
161
+ text = handleValidateFile(ValidateFileSchema.parse(args));
162
+ break;
163
+ case "check_drift":
164
+ text = handleCheckDrift(CheckDriftSchema.parse(args));
165
+ break;
166
+ case "get_checklist":
167
+ text = handleGetChecklist();
168
+ break;
129
169
  default:
130
- return {
131
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
132
- isError: true,
133
- };
170
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
134
171
  }
135
172
  return { content: [{ type: "text", text }] };
136
173
  }
137
174
  catch (err) {
138
175
  const message = err instanceof z.ZodError
139
176
  ? `Validation error: ${err.errors.map((e) => e.message).join(", ")}`
140
- : err instanceof Error
141
- ? err.message
142
- : String(err);
143
- return {
144
- content: [{ type: "text", text: `Error: ${message}` }],
145
- isError: true,
146
- };
177
+ : err instanceof Error ? err.message : String(err);
178
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
147
179
  }
148
180
  });
149
181
  // ---------------------------------------------------------------------------
@@ -0,0 +1,21 @@
1
+ import { z } from "zod";
2
+ export declare const ValidateFileSchema: z.ZodObject<{
3
+ path: z.ZodString;
4
+ }, "strip", z.ZodTypeAny, {
5
+ path: string;
6
+ }, {
7
+ path: string;
8
+ }>;
9
+ export declare const CheckDriftSchema: z.ZodObject<{
10
+ code: z.ZodString;
11
+ language: z.ZodOptional<z.ZodEnum<["python", "javascript", "typescript", "generic"]>>;
12
+ }, "strip", z.ZodTypeAny, {
13
+ code: string;
14
+ language?: "python" | "javascript" | "typescript" | "generic" | undefined;
15
+ }, {
16
+ code: string;
17
+ language?: "python" | "javascript" | "typescript" | "generic" | undefined;
18
+ }>;
19
+ export declare function handleValidateFile(args: z.infer<typeof ValidateFileSchema>): string;
20
+ export declare function handleCheckDrift(args: z.infer<typeof CheckDriftSchema>): string;
21
+ export declare function handleGetChecklist(): string;
@@ -0,0 +1,58 @@
1
+ import { z } from "zod";
2
+ import { writeFileSync, unlinkSync } from "fs";
3
+ import { tmpdir } from "os";
4
+ import { join } from "path";
5
+ import { validateFile, validateSource, formatReport, } from "../guardian/validator.js";
6
+ import { CHECKLIST } from "../guardian/checklist.js";
7
+ // ---------------------------------------------------------------------------
8
+ // Schemas
9
+ // ---------------------------------------------------------------------------
10
+ export const ValidateFileSchema = z.object({
11
+ path: z.string().min(1),
12
+ });
13
+ export const CheckDriftSchema = z.object({
14
+ code: z.string().min(1),
15
+ language: z.enum(["python", "javascript", "typescript", "generic"]).optional(),
16
+ });
17
+ // ---------------------------------------------------------------------------
18
+ // Handlers
19
+ // ---------------------------------------------------------------------------
20
+ export function handleValidateFile(args) {
21
+ const issues = validateFile(args.path);
22
+ return formatReport(args.path, issues);
23
+ }
24
+ export function handleCheckDrift(args) {
25
+ const lang = args.language ?? "generic";
26
+ const extMap = {
27
+ python: ".py",
28
+ javascript: ".js",
29
+ typescript: ".ts",
30
+ generic: ".txt",
31
+ };
32
+ const ext = extMap[lang] ?? ".txt";
33
+ const tmpPath = join(tmpdir(), `lucid-drift-${Date.now()}${ext}`);
34
+ try {
35
+ writeFileSync(tmpPath, args.code, "utf-8");
36
+ const issues = validateSource(tmpPath, args.code, lang === "generic" ? undefined : lang);
37
+ if (issues.length === 0) {
38
+ return "✅ No drift patterns detected in this code snippet.";
39
+ }
40
+ const lines = [`Found ${issues.length} potential issue(s):\n`];
41
+ for (const issue of issues) {
42
+ const icon = { critical: "🔴", high: "🟠", medium: "🟡", low: "🔵", info: "ℹ️" }[issue.severity];
43
+ lines.push(`${icon} [${issue.driftId}] line ${issue.line} — ${issue.message}`);
44
+ if (issue.suggestion)
45
+ lines.push(` 💡 ${issue.suggestion}`);
46
+ }
47
+ return lines.join("\n");
48
+ }
49
+ finally {
50
+ try {
51
+ unlinkSync(tmpPath);
52
+ }
53
+ catch { /* ignore */ }
54
+ }
55
+ }
56
+ export function handleGetChecklist() {
57
+ return CHECKLIST;
58
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@a13xu/lucid",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Persistent memory for Claude Code agents — SQLite + FTS5 knowledge graph via MCP",
5
5
  "type": "module",
6
6
  "bin": {