@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.
- package/build/guardian/checklist.d.ts +1 -0
- package/build/guardian/checklist.js +67 -0
- package/build/guardian/validator.d.ts +21 -0
- package/build/guardian/validator.js +332 -0
- package/build/index.js +64 -32
- package/build/tools/guardian.d.ts +21 -0
- package/build/tools/guardian.js +58 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
text = remember(stmts,
|
|
140
|
+
// Memory
|
|
141
|
+
case "remember":
|
|
142
|
+
text = remember(stmts, RememberSchema.parse(args));
|
|
104
143
|
break;
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|