@a13xu/lucid 1.0.0 → 1.4.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,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,10 @@ 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";
14
+ import { handleGrepCode, GrepCodeSchema } from "./tools/grep.js";
15
+ import { handleInitProject, InitProjectSchema } from "./tools/init.js";
16
+ import { handleSyncFile, SyncFileSchema, handleSyncProject, SyncProjectSchema, } from "./tools/sync.js";
13
17
  // ---------------------------------------------------------------------------
14
18
  // Init DB
15
19
  // ---------------------------------------------------------------------------
@@ -18,12 +22,13 @@ const stmts = prepareStatements(db);
18
22
  // ---------------------------------------------------------------------------
19
23
  // MCP Server
20
24
  // ---------------------------------------------------------------------------
21
- const server = new Server({ name: "lucid", version: "1.0.0" }, { capabilities: { tools: {} } });
25
+ const server = new Server({ name: "lucid", version: "1.1.0" }, { capabilities: { tools: {} } });
22
26
  // ---------------------------------------------------------------------------
23
27
  // Tool definitions
24
28
  // ---------------------------------------------------------------------------
25
29
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
26
30
  tools: [
31
+ // ── Memory ──────────────────────────────────────────────────────────────
27
32
  {
28
33
  name: "remember",
29
34
  description: "Store a fact, decision, or observation about an entity in the knowledge graph.",
@@ -88,6 +93,104 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
88
93
  description: "Get memory usage statistics.",
89
94
  inputSchema: { type: "object", properties: {} },
90
95
  },
96
+ // ── Init / Indexing ──────────────────────────────────────────────────────
97
+ {
98
+ name: "init_project",
99
+ description: "Scan and index a project directory into the knowledge graph. " +
100
+ "Reads CLAUDE.md (directives, conventions), package.json / pyproject.toml (dependencies, scripts), " +
101
+ "README.md (description), .mcp.json (MCP servers), logic-guardian.yaml (drift patterns), " +
102
+ "and source files (exported functions/classes). " +
103
+ "Call this once when starting work on a project to bootstrap memory with project context.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: {
107
+ directory: {
108
+ type: "string",
109
+ description: "Absolute path to the project root. Defaults to current working directory.",
110
+ },
111
+ },
112
+ },
113
+ },
114
+ {
115
+ name: "sync_file",
116
+ description: "Index or re-index a single source file after it was written or modified. " +
117
+ "Extracts exports, description, and open TODOs, then updates the knowledge graph. " +
118
+ "IMPORTANT: call this automatically after every Write or Edit tool call.",
119
+ inputSchema: {
120
+ type: "object",
121
+ properties: {
122
+ path: { type: "string", description: "Absolute or relative path to the modified file." },
123
+ },
124
+ required: ["path"],
125
+ },
126
+ },
127
+ {
128
+ name: "sync_project",
129
+ description: "Re-index the entire project directory incrementally. " +
130
+ "Use this when multiple files have changed (e.g. after a refactor or git pull).",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ directory: {
135
+ type: "string",
136
+ description: "Project root directory. Defaults to current working directory.",
137
+ },
138
+ },
139
+ },
140
+ },
141
+ {
142
+ name: "grep_code",
143
+ description: "Search indexed source files using a regex pattern. " +
144
+ "Decompresses stored binary content and returns only matching lines with context. " +
145
+ "Token-efficient: returns ~20-50 tokens instead of full file contents. " +
146
+ "Useful for finding function calls, variable usages, import patterns.",
147
+ inputSchema: {
148
+ type: "object",
149
+ properties: {
150
+ pattern: { type: "string", description: "Regex pattern to search for." },
151
+ language: { type: "string", enum: ["python", "javascript", "typescript", "generic"], description: "Filter by language." },
152
+ context: { type: "number", description: "Lines of context before/after each match (0-10, default 2)." },
153
+ },
154
+ required: ["pattern"],
155
+ },
156
+ },
157
+ // ── Logic Guardian ───────────────────────────────────────────────────────
158
+ {
159
+ name: "validate_file",
160
+ description: "Run Logic Guardian validation on a source file. Detects LLM drift patterns: " +
161
+ "logic inversions, null propagation, type confusion, copy-paste drift, silent exceptions, and more. " +
162
+ "Supports Python, JavaScript, TypeScript. Use after writing or modifying any code.",
163
+ inputSchema: {
164
+ type: "object",
165
+ properties: {
166
+ path: { type: "string", description: "Absolute or relative path to the file to validate." },
167
+ },
168
+ required: ["path"],
169
+ },
170
+ },
171
+ {
172
+ name: "check_drift",
173
+ description: "Analyze a code snippet for LLM drift patterns without saving to disk. " +
174
+ "Use this to validate code before writing it to a file.",
175
+ inputSchema: {
176
+ type: "object",
177
+ properties: {
178
+ code: { type: "string", description: "The code snippet to analyze." },
179
+ language: {
180
+ type: "string",
181
+ enum: ["python", "javascript", "typescript", "generic"],
182
+ description: "Programming language. Defaults to 'generic'.",
183
+ },
184
+ },
185
+ required: ["code"],
186
+ },
187
+ },
188
+ {
189
+ name: "get_checklist",
190
+ description: "Get the full Logic Guardian validation checklist (5 passes). " +
191
+ "Call this before marking any implementation task as done.",
192
+ inputSchema: { type: "object", properties: {} },
193
+ },
91
194
  ],
92
195
  }));
93
196
  // ---------------------------------------------------------------------------
@@ -98,52 +201,59 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
98
201
  try {
99
202
  let text;
100
203
  switch (name) {
101
- case "remember": {
102
- const input = RememberSchema.parse(args);
103
- text = remember(stmts, input);
204
+ // Memory
205
+ case "remember":
206
+ text = remember(stmts, RememberSchema.parse(args));
104
207
  break;
105
- }
106
- case "relate": {
107
- const input = RelateSchema.parse(args);
108
- text = relate(stmts, input);
208
+ case "relate":
209
+ text = relate(stmts, RelateSchema.parse(args));
109
210
  break;
110
- }
111
- case "recall": {
112
- const input = RecallSchema.parse(args);
113
- text = recall(stmts, input);
211
+ case "recall":
212
+ text = recall(stmts, RecallSchema.parse(args));
114
213
  break;
115
- }
116
- case "recall_all": {
214
+ case "recall_all":
117
215
  text = recallAll(db, stmts);
118
216
  break;
119
- }
120
- case "forget": {
121
- const input = ForgetSchema.parse(args);
122
- text = forget(stmts, input);
217
+ case "forget":
218
+ text = forget(stmts, ForgetSchema.parse(args));
123
219
  break;
124
- }
125
- case "memory_stats": {
220
+ case "memory_stats":
126
221
  text = memoryStats(db, stmts);
127
222
  break;
128
- }
223
+ // Init & Sync
224
+ case "init_project":
225
+ text = handleInitProject(stmts, InitProjectSchema.parse(args));
226
+ break;
227
+ case "sync_file":
228
+ text = handleSyncFile(stmts, SyncFileSchema.parse(args));
229
+ break;
230
+ case "sync_project":
231
+ text = handleSyncProject(stmts, SyncProjectSchema.parse(args));
232
+ break;
233
+ // Grep
234
+ case "grep_code":
235
+ text = handleGrepCode(stmts, GrepCodeSchema.parse(args));
236
+ break;
237
+ // Logic Guardian
238
+ case "validate_file":
239
+ text = handleValidateFile(ValidateFileSchema.parse(args));
240
+ break;
241
+ case "check_drift":
242
+ text = handleCheckDrift(CheckDriftSchema.parse(args));
243
+ break;
244
+ case "get_checklist":
245
+ text = handleGetChecklist();
246
+ break;
129
247
  default:
130
- return {
131
- content: [{ type: "text", text: `Unknown tool: ${name}` }],
132
- isError: true,
133
- };
248
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
134
249
  }
135
250
  return { content: [{ type: "text", text }] };
136
251
  }
137
252
  catch (err) {
138
253
  const message = err instanceof z.ZodError
139
254
  ? `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
- };
255
+ : err instanceof Error ? err.message : String(err);
256
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
147
257
  }
148
258
  });
149
259
  // ---------------------------------------------------------------------------
@@ -0,0 +1,15 @@
1
+ import type { Statements } from "../database.js";
2
+ export interface FileIndex {
3
+ module: string;
4
+ exports: string[];
5
+ description: string;
6
+ todos: string[];
7
+ language: string;
8
+ }
9
+ export declare function indexFile(filepath: string): FileIndex | null;
10
+ export interface UpsertResult {
11
+ observations: string[];
12
+ stored: boolean;
13
+ savedBytes: number;
14
+ }
15
+ export declare function upsertFileIndex(index: FileIndex, source: string, stmts: Statements): UpsertResult;
@@ -0,0 +1,100 @@
1
+ import { readFileSync } from "fs";
2
+ import { extname } from "path";
3
+ import { compress, sha256 } from "../store/content.js";
4
+ function extractTS(source) {
5
+ const exports = [];
6
+ const todos = [];
7
+ // Exported symbols
8
+ for (const m of source.matchAll(/export\s+(?:async\s+)?(?:function|class|const|type|interface|enum)\s+(\w+)/g)) {
9
+ exports.push(m[1]);
10
+ }
11
+ // First JSDoc / block comment as description
12
+ const docMatch = source.match(/^\/\*\*([\s\S]*?)\*\//m) ?? source.match(/^\/\/(.*)/m);
13
+ const description = docMatch
14
+ ? docMatch[1].replace(/\s*\*\s*/g, " ").trim().slice(0, 200)
15
+ : "";
16
+ // TODOs
17
+ for (const m of source.matchAll(/\/\/\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
18
+ todos.push(`${m[1]}: ${m[2].trim()}`);
19
+ }
20
+ return { exports, description, todos };
21
+ }
22
+ function extractPython(source) {
23
+ const exports = [];
24
+ const todos = [];
25
+ // Public functions and classes
26
+ for (const m of source.matchAll(/^(?:def|class|async def)\s+(\w+)/gm)) {
27
+ if (!m[1].startsWith("_"))
28
+ exports.push(m[1]);
29
+ }
30
+ // Module docstring
31
+ const docMatch = source.match(/^["']{3}([\s\S]*?)["']{3}/m);
32
+ const description = docMatch ? docMatch[1].trim().slice(0, 200) : "";
33
+ // TODOs
34
+ for (const m of source.matchAll(/#\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
35
+ todos.push(`${m[1]}: ${m[2].trim()}`);
36
+ }
37
+ return { exports, description, todos };
38
+ }
39
+ function extractGeneric(source) {
40
+ const todos = [];
41
+ for (const m of source.matchAll(/(?:\/\/|#)\s*(TODO|FIXME|HACK)[:\s]+(.+)/gi)) {
42
+ todos.push(`${m[1]}: ${m[2].trim()}`);
43
+ }
44
+ return { exports: [], description: "", todos };
45
+ }
46
+ export function indexFile(filepath) {
47
+ let source;
48
+ try {
49
+ source = readFileSync(filepath, { encoding: "utf-8" });
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ const ext = extname(filepath).toLowerCase();
55
+ const module = filepath.replace(/\\/g, "/");
56
+ let extracted;
57
+ let language;
58
+ if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
59
+ extracted = extractTS(source);
60
+ language = ext.includes("ts") ? "typescript" : "javascript";
61
+ }
62
+ else if (ext === ".py") {
63
+ extracted = extractPython(source);
64
+ language = "python";
65
+ }
66
+ else {
67
+ extracted = extractGeneric(source);
68
+ language = "generic";
69
+ }
70
+ return { module, language, ...extracted };
71
+ }
72
+ export function upsertFileIndex(index, source, stmts) {
73
+ const fileHash = sha256(source);
74
+ // Change detection — skip everything se hash-ul e identic
75
+ const existing = stmts.getFileByPath.get(index.module);
76
+ if (existing?.content_hash === fileHash) {
77
+ return { observations: [], stored: false, savedBytes: 0 };
78
+ }
79
+ // Comprimă și stochează conținutul binar
80
+ const blob = compress(source);
81
+ stmts.upsertFile.run(index.module, blob, fileHash, Buffer.byteLength(source, "utf-8"), blob.byteLength, index.language);
82
+ // Index structural în entities (compact, pentru recall)
83
+ const observations = [];
84
+ if (index.description)
85
+ observations.push(`description: ${index.description}`);
86
+ if (index.exports.length > 0)
87
+ observations.push(`exports: ${index.exports.join(", ")}`);
88
+ if (index.todos.length > 0)
89
+ observations.push(`TODOs: ${index.todos.join(" | ")}`);
90
+ observations.push(`language: ${index.language}`);
91
+ const entityRow = stmts.getEntityByName.get(index.module);
92
+ if (entityRow) {
93
+ stmts.updateEntity.run(JSON.stringify(observations), entityRow.id);
94
+ }
95
+ else {
96
+ stmts.insertEntity.run(index.module, "pattern", JSON.stringify(observations));
97
+ }
98
+ const savedBytes = Buffer.byteLength(source, "utf-8") - blob.byteLength;
99
+ return { observations, stored: true, savedBytes };
100
+ }
@@ -0,0 +1,8 @@
1
+ import type { Statements } from "../database.js";
2
+ export interface IndexResult {
3
+ entity: string;
4
+ type: string;
5
+ observations: number;
6
+ source: string;
7
+ }
8
+ export declare function indexProject(directory: string, stmts: Statements): IndexResult[];