@a13xu/lucid 1.4.0 → 1.9.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.
Files changed (46) hide show
  1. package/README.md +118 -14
  2. package/build/config.d.ts +37 -0
  3. package/build/config.js +45 -0
  4. package/build/database.d.ts +36 -1
  5. package/build/database.js +85 -1
  6. package/build/guardian/coding-analyzer.d.ts +11 -0
  7. package/build/guardian/coding-analyzer.js +393 -0
  8. package/build/guardian/coding-rules.d.ts +1 -0
  9. package/build/guardian/coding-rules.js +97 -0
  10. package/build/index.js +164 -3
  11. package/build/indexer/ast.d.ts +9 -0
  12. package/build/indexer/ast.js +158 -0
  13. package/build/indexer/project.js +21 -13
  14. package/build/memory/experience.d.ts +11 -0
  15. package/build/memory/experience.js +85 -0
  16. package/build/retrieval/context.d.ts +29 -0
  17. package/build/retrieval/context.js +219 -0
  18. package/build/retrieval/qdrant.d.ts +16 -0
  19. package/build/retrieval/qdrant.js +135 -0
  20. package/build/retrieval/tfidf.d.ts +14 -0
  21. package/build/retrieval/tfidf.js +64 -0
  22. package/build/security/alerts.d.ts +44 -0
  23. package/build/security/alerts.js +228 -0
  24. package/build/security/env.d.ts +24 -0
  25. package/build/security/env.js +85 -0
  26. package/build/security/guard.d.ts +35 -0
  27. package/build/security/guard.js +133 -0
  28. package/build/security/ratelimit.d.ts +34 -0
  29. package/build/security/ratelimit.js +105 -0
  30. package/build/security/smtp.d.ts +26 -0
  31. package/build/security/smtp.js +125 -0
  32. package/build/security/ssrf.d.ts +18 -0
  33. package/build/security/ssrf.js +109 -0
  34. package/build/security/waf.d.ts +33 -0
  35. package/build/security/waf.js +174 -0
  36. package/build/tools/coding-guard.d.ts +24 -0
  37. package/build/tools/coding-guard.js +82 -0
  38. package/build/tools/context.d.ts +39 -0
  39. package/build/tools/context.js +105 -0
  40. package/build/tools/init.d.ts +41 -1
  41. package/build/tools/init.js +124 -22
  42. package/build/tools/remember.d.ts +4 -4
  43. package/build/tools/reward.d.ts +29 -0
  44. package/build/tools/reward.js +154 -0
  45. package/build/tools/sync.js +15 -0
  46. package/package.json +9 -2
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @a13xu/lucid
2
2
 
3
- Memory, code indexing, and validation for Claude Code agents — backed by **SQLite + FTS5**.
3
+ Token-efficient memory, code indexing, and validation for Claude Code agents — backed by **SQLite + FTS5**.
4
4
 
5
- Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, and validates code for LLM drift patterns.
5
+ Stores a persistent knowledge graph (entities, relations, observations), indexes source files as compressed binary with change detection, retrieves minimal relevant context via TF-IDF or Qdrant, and validates code for LLM drift patterns.
6
6
 
7
7
  ## Install
8
8
 
@@ -35,13 +35,15 @@ Default DB path: `~/.claude/memory.db`
35
35
  ## Quick start
36
36
 
37
37
  ```
38
- 1. "Index this project" → init_project() → scans CLAUDE.md, package.json, src/
39
- 2. Write code → sync_file(path) → compressed + hashed in DB
40
- 3. "Where is X used?" → grep_code("X") → matching lines only, no full file
41
- 4. "What do we know?" recall("query") knowledge graph search
38
+ 1. "Index this project" → init_project() → scans CLAUDE.md, package.json, src/**
39
+ 2. Write code → sync_file(path) → compressed + hashed + diff stored
40
+ 3. "What's relevant?" → get_context("auth flow") → TF-IDF ranked skeletons, ~500 tokens
41
+ 4. "What changed?" get_recent(hours=2) line diffs of recent edits
42
+ 5. "Where is X used?" → grep_code("X") → matching lines only, ~30 tokens
43
+ 6. "What do we know?" → recall("query") → knowledge graph search
42
44
  ```
43
45
 
44
- ## Tools (13)
46
+ ## Tools (15)
45
47
 
46
48
  ### Memory
47
49
  | Tool | Description |
@@ -56,11 +58,17 @@ Default DB path: `~/.claude/memory.db`
56
58
  ### Code indexing
57
59
  | Tool | Description |
58
60
  |---|---|
59
- | `init_project` | Scan project directory and bootstrap knowledge graph. Reads `CLAUDE.md`, `package.json`/`pyproject.toml`, `README.md`, `.mcp.json`, `logic-guardian.yaml`, source files. Also installs a Claude Code hook for auto-sync. |
60
- | `sync_file` | Index or re-index a single file after writing/editing. Stores compressed binary (zlib-9), skips instantly if SHA-256 hash unchanged. |
61
+ | `init_project` | Scan project directory recursively and bootstrap knowledge graph. Reads `CLAUDE.md`, `package.json`/`pyproject.toml`, `README.md`, `.mcp.json`, `logic-guardian.yaml`, all source files. Installs a Claude Code hook for auto-sync. |
62
+ | `sync_file` | Index or re-index a single file after writing/editing. Stores compressed binary (zlib-9), skips instantly if SHA-256 hash unchanged. Stores line-level diff from previous version. |
61
63
  | `sync_project` | Re-index entire project incrementally. Reports compression ratio. |
62
64
  | `grep_code` | Regex search across all indexed files. Decompresses binary on-the-fly, returns only matching lines with context — ~20-50 tokens vs reading full files. |
63
65
 
66
+ ### Token optimization
67
+ | Tool | Description |
68
+ |---|---|
69
+ | `get_context` | **Smart context retrieval.** Ranks all indexed files by TF-IDF relevance (or Qdrant vector search if `QDRANT_URL` is set), applies recency boost, returns skeletons (imports + signatures only) for large files. Respects `maxContextTokens` budget. |
70
+ | `get_recent` | Return files modified in the last N hours with line-level diffs. |
71
+
64
72
  ### Logic Guardian
65
73
  | Tool | Description |
66
74
  |---|---|
@@ -68,18 +76,112 @@ Default DB path: `~/.claude/memory.db`
68
76
  | `check_drift` | Analyze a code snippet inline without saving to disk. |
69
77
  | `get_checklist` | Return the full 5-pass validation protocol (Logic Trace, Contract Verification, Stupid Mistakes, Integration Sanity, Explain It). |
70
78
 
71
- ## Why no vectors?
79
+ ## Token optimization in depth
80
+
81
+ ### How `get_context` works
82
+
83
+ ```
84
+ query: "auth middleware"
85
+
86
+ 1. TF-IDF score all indexed files against query
87
+ (or Qdrant top-k if QDRANT_URL is set)
88
+
89
+ 2. Boost recently-modified files (+0.3 score)
90
+
91
+ 3. Apply whitelist dirs filter (if configured)
92
+
93
+ 4. For each file within token budget:
94
+ file < maxTokensPerFile → return full source
95
+ file > maxTokensPerFile → return skeleton only
96
+ (imports + signatures + TODOs)
97
+ + relevant fragments around query terms
98
+
99
+ output: ~500–2000 tokens vs 5000–20000 for reading full files
100
+ ```
101
+
102
+ ### Skeleton pruning (AST-based)
103
+
104
+ Large files are replaced with their structural skeleton:
105
+
106
+ ```typescript
107
+ // src/middleware/auth.ts [skeleton]
108
+ // Validates JWT tokens and attaches user to request context
109
+
110
+ import { Request, Response, NextFunction } from "express"
111
+ import { verifyToken } from "../services/jwt.js"
112
+
113
+ // — exports —
114
+ export function authMiddleware(req: Request, res: Response, next: NextFunction): void { … }
115
+ export function requireRole(role: string): RequestHandler { … }
116
+ export type AuthenticatedRequest = Request & { user: User }
117
+ ```
118
+
119
+ vs reading the full 200-line file.
120
+
121
+ ### Qdrant vector search (optional)
122
+
123
+ Set env vars to enable semantic search instead of TF-IDF:
124
+
125
+ ```bash
126
+ QDRANT_URL=http://localhost:6333
127
+ QDRANT_API_KEY=your-key # optional
128
+ OPENAI_API_KEY=sk-... # for embeddings
129
+ EMBEDDING_MODEL=text-embedding-3-small # optional
130
+ ```
131
+
132
+ Or in `.mcp.json`:
133
+ ```json
134
+ {
135
+ "mcpServers": {
136
+ "lucid": {
137
+ "command": "npx", "args": ["-y", "@a13xu/lucid"],
138
+ "env": {
139
+ "QDRANT_URL": "http://localhost:6333",
140
+ "OPENAI_API_KEY": "sk-..."
141
+ }
142
+ }
143
+ }
144
+ }
145
+ ```
146
+
147
+ Falls back to TF-IDF automatically if Qdrant is unreachable.
148
+
149
+ ### Configuration (`lucid.config.json`)
150
+
151
+ Create in your project root to customize behavior:
152
+
153
+ ```json
154
+ {
155
+ "whitelistDirs": ["src", "backend", "api"],
156
+ "blacklistDirs": ["migrations", "fixtures"],
157
+ "maxTokensPerFile": 400,
158
+ "maxContextTokens": 6000,
159
+ "recentWindowHours": 12
160
+ }
161
+ ```
162
+
163
+ | Key | Default | Description |
164
+ |---|---|---|
165
+ | `whitelistDirs` | — | Only index/return files from these dirs |
166
+ | `blacklistDirs` | — | Extra dirs to skip (merged with built-in skips) |
167
+ | `maxTokensPerFile` | `400` | Files above this get skeleton treatment |
168
+ | `maxContextTokens` | `4000` | Total token budget for `get_context` |
169
+ | `recentWindowHours` | `24` | "Recently touched" threshold |
170
+
171
+ ## Why no vectors by default?
72
172
 
73
- Code has explicit structure — no NLP needed:
173
+ Code has explicit structure — no NLP needed for most queries:
74
174
 
75
175
  | Need | Approach | Tokens |
76
176
  |---|---|---|
77
177
  | "Where is X defined?" | `grep_code("export.*X")` | ~30 |
78
178
  | "What does auth.ts export?" | `recall("auth.ts")` | ~50 |
179
+ | "What changed recently?" | `get_recent(hours=2)` | ~200 |
180
+ | "Context for this task" | `get_context("auth flow")` | ~500 |
79
181
  | "Project conventions?" | `recall("CLAUDE.md conventions")` | ~80 |
80
- | Read full file | `Read tool` | ~500-2000 |
182
+ | Read full file | `Read tool` | ~5002000 |
81
183
 
82
- Source files are stored as **zlib-deflate level 9 BLOBs** (~70% smaller than plain text). Change detection via SHA-256 means `sync_file` is instant on unchanged files.
184
+ TF-IDF is fast, deterministic, and requires zero external services. Qdrant is available when you need semantic similarity across large codebases.
83
185
 
84
186
  ## Why SQLite + FTS5?
85
187
 
@@ -90,6 +192,7 @@ Source files are stored as **zlib-deflate level 9 BLOBs** (~70% smaller than pla
90
192
  | Concurrent reads | Lock entire file | WAL mode |
91
193
  | Code storage | Plain text | Compressed BLOB + hash |
92
194
  | Change detection | Manual diff | SHA-256 per file |
195
+ | Diff history | None | Line-level diffs per file |
93
196
 
94
197
  ## Entity types
95
198
  `person` · `project` · `decision` · `pattern` · `tool` · `config` · `bug` · `convention`
@@ -104,7 +207,7 @@ echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"capabilities":{},
104
207
  | npx @a13xu/lucid
105
208
  ```
106
209
 
107
- In Claude Code: run `/mcp` — you should see `lucid` with 13 tools.
210
+ In Claude Code: run `/mcp` — you should see `lucid` with 15 tools.
108
211
 
109
212
  ## Tech stack
110
213
 
@@ -113,5 +216,6 @@ In Claude Code: run `/mcp` — you should see `lucid` with 13 tools.
113
216
  - **Database:** `better-sqlite3` (synchronous, WAL mode)
114
217
  - **Compression:** Node.js built-in `zlib` (deflate level 9)
115
218
  - **Hashing:** SHA-256 via `crypto` (change detection)
219
+ - **Ranking:** TF-IDF (built-in) or Qdrant (optional, via REST)
116
220
  - **Validation:** `zod`
117
221
  - **Transport:** stdio
@@ -0,0 +1,37 @@
1
+ import type { SecurityConfig } from "./security/guard.js";
2
+ export interface LucidConfig {
3
+ /** Only index/return files from these directories (e.g. ["src", "lib", "backend"]) */
4
+ whitelistDirs?: string[];
5
+ /** Additional dirs to skip (merged with built-in SKIP_DIRS) */
6
+ blacklistDirs?: string[];
7
+ /** Max estimated tokens to return per file in get_context (default 400) */
8
+ maxTokensPerFile?: number;
9
+ /** Total token budget for get_context response (default 4000) */
10
+ maxContextTokens?: number;
11
+ /** "Recently touched" = modified within N hours (default 24) */
12
+ recentWindowHours?: number;
13
+ /** Security guard configuration */
14
+ security?: SecurityConfig;
15
+ /** Optional Qdrant vector search (falls back to TF-IDF if not configured) */
16
+ qdrant?: {
17
+ url: string;
18
+ apiKey?: string;
19
+ collection?: string;
20
+ /** Embedding endpoint — must be OpenAI-compatible */
21
+ embeddingUrl?: string;
22
+ embeddingApiKey?: string;
23
+ embeddingModel?: string;
24
+ vectorDim?: number;
25
+ };
26
+ }
27
+ export interface Defaults {
28
+ maxTokensPerFile: number;
29
+ maxContextTokens: number;
30
+ recentWindowHours: number;
31
+ }
32
+ export declare const DEFAULTS: Defaults;
33
+ export type ResolvedConfig = Defaults & LucidConfig;
34
+ export declare function loadConfig(projectDir?: string): ResolvedConfig;
35
+ /** Qdrant config from lucid.config.json or env vars */
36
+ export declare function getQdrantConfig(cfg: ResolvedConfig): ResolvedConfig["qdrant"] | null;
37
+ export declare function resetConfigCache(): void;
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync } from "fs";
2
+ import { join } from "path";
3
+ export const DEFAULTS = {
4
+ maxTokensPerFile: 400,
5
+ maxContextTokens: 4000,
6
+ recentWindowHours: 24,
7
+ };
8
+ let _cached = null;
9
+ export function loadConfig(projectDir) {
10
+ if (_cached)
11
+ return _cached;
12
+ const dirs = [projectDir, process.cwd()].filter(Boolean);
13
+ for (const dir of dirs) {
14
+ const cfgPath = join(dir, "lucid.config.json");
15
+ if (existsSync(cfgPath)) {
16
+ try {
17
+ const raw = JSON.parse(readFileSync(cfgPath, "utf-8"));
18
+ _cached = { ...DEFAULTS, ...raw };
19
+ return _cached;
20
+ }
21
+ catch { /* malformed — skip */ }
22
+ }
23
+ }
24
+ _cached = { ...DEFAULTS };
25
+ return _cached;
26
+ }
27
+ /** Qdrant config from lucid.config.json or env vars */
28
+ export function getQdrantConfig(cfg) {
29
+ // Env vars override config file
30
+ const url = process.env["QDRANT_URL"] ?? cfg.qdrant?.url;
31
+ if (!url)
32
+ return null;
33
+ return {
34
+ url,
35
+ apiKey: process.env["QDRANT_API_KEY"] ?? cfg.qdrant?.apiKey,
36
+ collection: process.env["QDRANT_COLLECTION"] ?? cfg.qdrant?.collection ?? "lucid",
37
+ embeddingUrl: process.env["EMBEDDING_URL"] ?? cfg.qdrant?.embeddingUrl ?? "https://api.openai.com/v1/embeddings",
38
+ embeddingApiKey: process.env["EMBEDDING_API_KEY"] ?? process.env["OPENAI_API_KEY"] ?? cfg.qdrant?.embeddingApiKey,
39
+ embeddingModel: process.env["EMBEDDING_MODEL"] ?? cfg.qdrant?.embeddingModel ?? "text-embedding-3-small",
40
+ vectorDim: cfg.qdrant?.vectorDim ?? 1536,
41
+ };
42
+ }
43
+ export function resetConfigCache() {
44
+ _cached = null;
45
+ }
@@ -13,16 +13,43 @@ export interface FileContentRow {
13
13
  language: string;
14
14
  indexed_at: number;
15
15
  }
16
+ export interface FileDiffRow {
17
+ filepath: string;
18
+ prev_hash: string;
19
+ diff_text: string;
20
+ changed_at: number;
21
+ }
22
+ export interface ExperienceRow {
23
+ id: number;
24
+ query: string;
25
+ query_terms: string;
26
+ context_fps: string;
27
+ strategy: string;
28
+ reward: number;
29
+ feedback: string | null;
30
+ created_at: number;
31
+ rewarded_at: number | null;
32
+ }
33
+ export interface FileRewardRow {
34
+ filepath: string;
35
+ total_reward: number;
36
+ use_count: number;
37
+ last_rewarded: number | null;
38
+ }
16
39
  export interface Statements {
17
40
  getFileByPath: Stmt<[string], FileContentRow>;
18
41
  upsertFile: WriteStmt<[string, Buffer, string, number, number, string]>;
19
- getAllFiles: Stmt<[], Pick<FileContentRow, "filepath" | "content" | "language" | "content_hash">>;
42
+ getAllFiles: Stmt<[], Pick<FileContentRow, "filepath" | "content" | "language" | "content_hash" | "indexed_at">>;
43
+ getRecentFiles: Stmt<[number], Pick<FileContentRow, "filepath" | "language" | "indexed_at">>;
20
44
  deleteFile: WriteStmt<[string]>;
21
45
  fileStorageStats: Stmt<[], {
22
46
  count: number;
23
47
  total_original: number;
24
48
  total_compressed: number;
25
49
  }>;
50
+ upsertDiff: WriteStmt<[string, string, string]>;
51
+ getDiff: Stmt<[string], FileDiffRow>;
52
+ getRecentDiffs: Stmt<[number], FileDiffRow>;
26
53
  getEntityByName: Stmt<[string], EntityRow>;
27
54
  insertEntity: WriteStmt<[string, string, string]>;
28
55
  updateEntity: WriteStmt<[string, number]>;
@@ -48,6 +75,14 @@ export interface Statements {
48
75
  from_name: string;
49
76
  to_name: string;
50
77
  }>;
78
+ insertExperience: WriteStmt<[string, string, string, string]>;
79
+ updateExperienceReward: WriteStmt<[number, string | null, number]>;
80
+ getExperienceById: Stmt<[number], ExperienceRow>;
81
+ getTopExperiences: Stmt<[number], ExperienceRow>;
82
+ searchExperiencesFTS: Stmt<[string, number], ExperienceRow>;
83
+ upsertFileReward: WriteStmt<[string, number]>;
84
+ getFileRewards: Stmt<[], FileRewardRow>;
85
+ getTopFileRewards: Stmt<[number], FileRewardRow>;
51
86
  }
52
87
  export declare function prepareStatements(db: Database.Database): Statements;
53
88
  export {};
package/build/database.js CHANGED
@@ -92,6 +92,57 @@ function createSchema(db) {
92
92
  );
93
93
  CREATE INDEX IF NOT EXISTS idx_fc_filepath ON file_contents(filepath);
94
94
  CREATE INDEX IF NOT EXISTS idx_fc_hash ON file_contents(content_hash);
95
+ CREATE INDEX IF NOT EXISTS idx_fc_indexed ON file_contents(indexed_at);
96
+
97
+ -- Diffs între versiuni consecutive (pentru get_recent)
98
+ CREATE TABLE IF NOT EXISTS file_diffs (
99
+ filepath TEXT PRIMARY KEY,
100
+ prev_hash TEXT NOT NULL,
101
+ diff_text TEXT NOT NULL,
102
+ changed_at INTEGER DEFAULT (unixepoch())
103
+ );
104
+ CREATE INDEX IF NOT EXISTS idx_fd_changed ON file_diffs(changed_at);
105
+
106
+ -- Experiences: logged get_context calls for RL reward system
107
+ CREATE TABLE IF NOT EXISTS experiences (
108
+ id INTEGER PRIMARY KEY,
109
+ query TEXT NOT NULL,
110
+ query_terms TEXT NOT NULL DEFAULT '',
111
+ context_fps TEXT NOT NULL DEFAULT '[]',
112
+ strategy TEXT NOT NULL DEFAULT 'tfidf',
113
+ reward REAL NOT NULL DEFAULT 0.0,
114
+ feedback TEXT,
115
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
116
+ rewarded_at INTEGER
117
+ );
118
+
119
+ CREATE VIRTUAL TABLE IF NOT EXISTS experiences_fts USING fts5(
120
+ query,
121
+ feedback,
122
+ content='experiences',
123
+ content_rowid='id',
124
+ tokenize='porter unicode61'
125
+ );
126
+
127
+ CREATE TRIGGER IF NOT EXISTS experiences_ai AFTER INSERT ON experiences BEGIN
128
+ INSERT INTO experiences_fts(rowid, query, feedback)
129
+ VALUES (new.id, new.query, COALESCE(new.feedback, ''));
130
+ END;
131
+
132
+ CREATE TRIGGER IF NOT EXISTS experiences_au AFTER UPDATE ON experiences BEGIN
133
+ INSERT INTO experiences_fts(experiences_fts, rowid, query, feedback)
134
+ VALUES('delete', old.id, old.query, COALESCE(old.feedback, ''));
135
+ INSERT INTO experiences_fts(rowid, query, feedback)
136
+ VALUES (new.id, new.query, COALESCE(new.feedback, ''));
137
+ END;
138
+
139
+ -- Denormalized reward cache per file (updated on every reward/penalize/implicit)
140
+ CREATE TABLE IF NOT EXISTS file_rewards (
141
+ filepath TEXT PRIMARY KEY,
142
+ total_reward REAL NOT NULL DEFAULT 0.0,
143
+ use_count INTEGER NOT NULL DEFAULT 0,
144
+ last_rewarded INTEGER
145
+ );
95
146
  `);
96
147
  }
97
148
  export function prepareStatements(db) {
@@ -107,8 +158,17 @@ export function prepareStatements(db) {
107
158
  compressed_size = excluded.compressed_size,
108
159
  language = excluded.language,
109
160
  indexed_at = unixepoch()`),
110
- getAllFiles: db.prepare("SELECT filepath, content, language, content_hash FROM file_contents"),
161
+ getAllFiles: db.prepare("SELECT filepath, content, language, content_hash, indexed_at FROM file_contents"),
162
+ getRecentFiles: db.prepare("SELECT filepath, language, indexed_at FROM file_contents WHERE indexed_at >= ? ORDER BY indexed_at DESC"),
111
163
  deleteFile: db.prepare("DELETE FROM file_contents WHERE filepath = ?"),
164
+ upsertDiff: db.prepare(`INSERT INTO file_diffs (filepath, prev_hash, diff_text)
165
+ VALUES (?, ?, ?)
166
+ ON CONFLICT(filepath) DO UPDATE SET
167
+ prev_hash = excluded.prev_hash,
168
+ diff_text = excluded.diff_text,
169
+ changed_at = unixepoch()`),
170
+ getDiff: db.prepare("SELECT * FROM file_diffs WHERE filepath = ?"),
171
+ getRecentDiffs: db.prepare("SELECT * FROM file_diffs WHERE changed_at >= ? ORDER BY changed_at DESC"),
112
172
  fileStorageStats: db.prepare("SELECT COUNT(*) as count, SUM(original_size) as total_original, SUM(compressed_size) as total_compressed FROM file_contents"),
113
173
  // entities
114
174
  getEntityByName: db.prepare("SELECT * FROM entities WHERE name = ? COLLATE NOCASE"),
@@ -139,5 +199,29 @@ export function prepareStatements(db) {
139
199
  FROM relations r
140
200
  JOIN entities ef ON r.from_entity = ef.id
141
201
  JOIN entities et ON r.to_entity = et.id`),
202
+ // experiences
203
+ insertExperience: db.prepare(`INSERT INTO experiences (query, query_terms, context_fps, strategy)
204
+ VALUES (?, ?, ?, ?)`),
205
+ updateExperienceReward: db.prepare(`UPDATE experiences SET
206
+ reward = reward + ?,
207
+ rewarded_at = unixepoch(),
208
+ feedback = COALESCE(?, feedback)
209
+ WHERE id = ?`),
210
+ getExperienceById: db.prepare("SELECT * FROM experiences WHERE id = ?"),
211
+ getTopExperiences: db.prepare("SELECT * FROM experiences ORDER BY reward DESC LIMIT ?"),
212
+ searchExperiencesFTS: db.prepare(`SELECT e.* FROM experiences_fts
213
+ JOIN experiences e ON experiences_fts.rowid = e.id
214
+ WHERE experiences_fts MATCH ?
215
+ ORDER BY rank
216
+ LIMIT ?`),
217
+ // file_rewards
218
+ upsertFileReward: db.prepare(`INSERT INTO file_rewards (filepath, total_reward, use_count, last_rewarded)
219
+ VALUES (?, ?, 1, unixepoch())
220
+ ON CONFLICT(filepath) DO UPDATE SET
221
+ total_reward = total_reward + excluded.total_reward,
222
+ use_count = use_count + 1,
223
+ last_rewarded = unixepoch()`),
224
+ getFileRewards: db.prepare("SELECT * FROM file_rewards"),
225
+ getTopFileRewards: db.prepare("SELECT * FROM file_rewards WHERE total_reward > 0 ORDER BY total_reward DESC LIMIT ?"),
142
226
  };
143
227
  }
@@ -0,0 +1,11 @@
1
+ export type QualitySeverity = "high" | "medium" | "low";
2
+ export interface QualityIssue {
3
+ file: string;
4
+ line: number;
5
+ severity: QualitySeverity;
6
+ ruleId: string;
7
+ message: string;
8
+ suggestion: string;
9
+ }
10
+ export declare function analyzeCodeQuality(filepath: string, source: string, lang?: string): QualityIssue[];
11
+ export declare function formatQualityReport(filepath: string, issues: QualityIssue[]): string;