@a13xu/lucid 1.4.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -14
- package/build/config.d.ts +37 -0
- package/build/config.js +45 -0
- package/build/database.d.ts +36 -1
- package/build/database.js +85 -1
- package/build/guardian/coding-analyzer.d.ts +11 -0
- package/build/guardian/coding-analyzer.js +393 -0
- package/build/guardian/coding-rules.d.ts +1 -0
- package/build/guardian/coding-rules.js +97 -0
- package/build/index.js +164 -3
- package/build/indexer/ast.d.ts +9 -0
- package/build/indexer/ast.js +158 -0
- package/build/indexer/project.js +21 -13
- package/build/memory/experience.d.ts +11 -0
- package/build/memory/experience.js +85 -0
- package/build/retrieval/context.d.ts +29 -0
- package/build/retrieval/context.js +219 -0
- package/build/retrieval/qdrant.d.ts +16 -0
- package/build/retrieval/qdrant.js +135 -0
- package/build/retrieval/tfidf.d.ts +14 -0
- package/build/retrieval/tfidf.js +64 -0
- package/build/security/alerts.d.ts +44 -0
- package/build/security/alerts.js +228 -0
- package/build/security/env.d.ts +24 -0
- package/build/security/env.js +85 -0
- package/build/security/guard.d.ts +35 -0
- package/build/security/guard.js +133 -0
- package/build/security/ratelimit.d.ts +34 -0
- package/build/security/ratelimit.js +105 -0
- package/build/security/smtp.d.ts +26 -0
- package/build/security/smtp.js +125 -0
- package/build/security/ssrf.d.ts +18 -0
- package/build/security/ssrf.js +109 -0
- package/build/security/waf.d.ts +33 -0
- package/build/security/waf.js +174 -0
- package/build/tools/coding-guard.d.ts +24 -0
- package/build/tools/coding-guard.js +82 -0
- package/build/tools/context.d.ts +39 -0
- package/build/tools/context.js +105 -0
- package/build/tools/init.d.ts +41 -1
- package/build/tools/init.js +124 -22
- package/build/tools/remember.d.ts +4 -4
- package/build/tools/reward.d.ts +29 -0
- package/build/tools/reward.js +154 -0
- package/build/tools/sync.js +15 -0
- package/package.json +9 -2
package/build/index.js
CHANGED
|
@@ -4,6 +4,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { initDatabase, prepareStatements } from "./database.js";
|
|
7
|
+
import { guardRequest, guardOutput, configureGuard } from "./security/guard.js";
|
|
8
|
+
import { allowHost } from "./security/ssrf.js";
|
|
9
|
+
import { loadConfig } from "./config.js";
|
|
7
10
|
import { remember, RememberSchema } from "./tools/remember.js";
|
|
8
11
|
import { relate, RelateSchema } from "./tools/relate.js";
|
|
9
12
|
import { recall, RecallSchema } from "./tools/recall.js";
|
|
@@ -14,15 +17,42 @@ import { handleValidateFile, ValidateFileSchema, handleCheckDrift, CheckDriftSch
|
|
|
14
17
|
import { handleGrepCode, GrepCodeSchema } from "./tools/grep.js";
|
|
15
18
|
import { handleInitProject, InitProjectSchema } from "./tools/init.js";
|
|
16
19
|
import { handleSyncFile, SyncFileSchema, handleSyncProject, SyncProjectSchema, } from "./tools/sync.js";
|
|
20
|
+
import { handleGetContext, GetContextSchema, handleGetRecent, GetRecentSchema, } from "./tools/context.js";
|
|
21
|
+
import { handleReward, RewardSchema, handlePenalize, PenalizeSchema, handleShowRewards, ShowRewardsSchema, } from "./tools/reward.js";
|
|
22
|
+
import { handleGetCodingRules, handleCheckCodeQuality, CheckCodeQualitySchema, } from "./tools/coding-guard.js";
|
|
17
23
|
// ---------------------------------------------------------------------------
|
|
18
24
|
// Init DB
|
|
19
25
|
// ---------------------------------------------------------------------------
|
|
20
26
|
const db = initDatabase();
|
|
21
27
|
const stmts = prepareStatements(db);
|
|
22
28
|
// ---------------------------------------------------------------------------
|
|
29
|
+
// Security guard — initialize from config + env
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
const _appCfg = loadConfig();
|
|
32
|
+
configureGuard(_appCfg.security ?? {});
|
|
33
|
+
// Register Qdrant host in SSRF allowlist if configured
|
|
34
|
+
const _qdrantUrl = process.env["QDRANT_URL"] ?? _appCfg.qdrant?.url;
|
|
35
|
+
if (_qdrantUrl) {
|
|
36
|
+
try {
|
|
37
|
+
allowHost(_qdrantUrl);
|
|
38
|
+
}
|
|
39
|
+
catch { /* ignore invalid URL */ }
|
|
40
|
+
}
|
|
41
|
+
const _embeddingUrl = process.env["EMBEDDING_URL"] ?? _appCfg.qdrant?.embeddingUrl;
|
|
42
|
+
if (_embeddingUrl) {
|
|
43
|
+
try {
|
|
44
|
+
allowHost(_embeddingUrl);
|
|
45
|
+
}
|
|
46
|
+
catch { /* ignore */ }
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// Default embedding endpoint
|
|
50
|
+
allowHost("https://api.openai.com");
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
23
53
|
// MCP Server
|
|
24
54
|
// ---------------------------------------------------------------------------
|
|
25
|
-
const server = new Server({ name: "lucid", version: "1.1
|
|
55
|
+
const server = new Server({ name: "lucid", version: "1.9.1" }, { capabilities: { tools: {} } });
|
|
26
56
|
// ---------------------------------------------------------------------------
|
|
27
57
|
// Tool definitions
|
|
28
58
|
// ---------------------------------------------------------------------------
|
|
@@ -154,6 +184,79 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
154
184
|
required: ["pattern"],
|
|
155
185
|
},
|
|
156
186
|
},
|
|
187
|
+
// ── Context & Token Optimization ─────────────────────────────────────────
|
|
188
|
+
{
|
|
189
|
+
name: "get_context",
|
|
190
|
+
description: "Retrieve the minimal relevant context for a task or query. " +
|
|
191
|
+
"Uses TF-IDF scoring (or Qdrant vector search if configured) to rank files by relevance, " +
|
|
192
|
+
"applies recency boost for recently modified files, and returns skeletons (signatures only) " +
|
|
193
|
+
"for large files to stay within the token budget. " +
|
|
194
|
+
"Configure limits in lucid.config.json. Set QDRANT_URL env var for vector search.",
|
|
195
|
+
inputSchema: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
query: { type: "string", description: "What you are working on or searching for" },
|
|
199
|
+
maxTokens: { type: "number", description: "Total token budget (default 4000)" },
|
|
200
|
+
dirs: { type: "array", items: { type: "string" }, description: "Whitelist directories (e.g. [\"src\", \"backend\"])" },
|
|
201
|
+
recentOnly: { type: "boolean", description: "Only files modified within recentWindowHours" },
|
|
202
|
+
recentHours: { type: "number", description: "Override recent window (hours)" },
|
|
203
|
+
skeletonOnly: { type: "boolean", description: "Always show skeleton (signatures only)" },
|
|
204
|
+
topK: { type: "number", description: "Max files to consider (default 10)" },
|
|
205
|
+
},
|
|
206
|
+
required: ["query"],
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "get_recent",
|
|
211
|
+
description: "Return files modified recently with line-level diffs. " +
|
|
212
|
+
"Shows what changed in each file since the previous sync. " +
|
|
213
|
+
"Useful for catching up after a git pull or resuming a session.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: {
|
|
217
|
+
hours: { type: "number", description: "Look back N hours (default 24)" },
|
|
218
|
+
withDiffs: { type: "boolean", description: "Include line diffs (default true)" },
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
// ── Reward System ────────────────────────────────────────────────────────
|
|
223
|
+
{
|
|
224
|
+
name: "reward",
|
|
225
|
+
description: "Signal that the last get_context() result was helpful (+1 reward). " +
|
|
226
|
+
"The files returned in that context will be ranked higher in future similar queries. " +
|
|
227
|
+
"Call this after a get_context() result led to a correct fix or useful code.",
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: "object",
|
|
230
|
+
properties: {
|
|
231
|
+
note: { type: "string", description: "Optional note about what worked (stored for future reference)" },
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
name: "penalize",
|
|
237
|
+
description: "Signal that the last get_context() result was unhelpful (-1 reward). " +
|
|
238
|
+
"The files returned in that context will be ranked lower in future similar queries. " +
|
|
239
|
+
"Call this after a get_context() result missed important files or was irrelevant.",
|
|
240
|
+
inputSchema: {
|
|
241
|
+
type: "object",
|
|
242
|
+
properties: {
|
|
243
|
+
note: { type: "string", description: "Optional note about what was missing or wrong" },
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
name: "show_rewards",
|
|
249
|
+
description: "Show the top rewarded experiences and most rewarded files. " +
|
|
250
|
+
"Rewards decay exponentially (half-life ~14 days). " +
|
|
251
|
+
"Use this to understand which context queries and files have been most valuable.",
|
|
252
|
+
inputSchema: {
|
|
253
|
+
type: "object",
|
|
254
|
+
properties: {
|
|
255
|
+
query: { type: "string", description: "Filter experiences by query text (optional)" },
|
|
256
|
+
topK: { type: "number", description: "Number of top results to show (default 10)" },
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
},
|
|
157
260
|
// ── Logic Guardian ───────────────────────────────────────────────────────
|
|
158
261
|
{
|
|
159
262
|
name: "validate_file",
|
|
@@ -191,6 +294,34 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
191
294
|
"Call this before marking any implementation task as done.",
|
|
192
295
|
inputSchema: { type: "object", properties: {} },
|
|
193
296
|
},
|
|
297
|
+
// ── Coding Guard ─────────────────────────────────────────────────────────
|
|
298
|
+
{
|
|
299
|
+
name: "coding_rules",
|
|
300
|
+
description: "Get the 25 Golden Rules coding checklist. Covers clarity, naming, single responsibility, " +
|
|
301
|
+
"error handling, frontend component size/reuse/props, singleton rules, library selection, " +
|
|
302
|
+
"and architecture separation. Review before marking any task done.",
|
|
303
|
+
inputSchema: { type: "object", properties: {} },
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: "check_code_quality",
|
|
307
|
+
description: "Analyze a file or code snippet against the 25 Golden Rules. " +
|
|
308
|
+
"Detects: file/function size violations, vague naming, deep nesting, dead code, and — " +
|
|
309
|
+
"for React/Vue component files — inline styles, prop explosion, fetch-in-component, " +
|
|
310
|
+
"direct DOM access, mixed styling systems. " +
|
|
311
|
+
"Complements validate_file (which checks logic correctness).",
|
|
312
|
+
inputSchema: {
|
|
313
|
+
type: "object",
|
|
314
|
+
properties: {
|
|
315
|
+
path: { type: "string", description: "Absolute or relative path to the file to analyze." },
|
|
316
|
+
code: { type: "string", description: "Code snippet to analyze inline." },
|
|
317
|
+
language: {
|
|
318
|
+
type: "string",
|
|
319
|
+
enum: ["python", "javascript", "typescript", "vue", "generic"],
|
|
320
|
+
description: "Language hint. Auto-detected from file extension if path is provided.",
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
},
|
|
194
325
|
],
|
|
195
326
|
}));
|
|
196
327
|
// ---------------------------------------------------------------------------
|
|
@@ -198,6 +329,11 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
198
329
|
// ---------------------------------------------------------------------------
|
|
199
330
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
200
331
|
const { name, arguments: args } = request.params;
|
|
332
|
+
// Security: rate limit + WAF check before any execution
|
|
333
|
+
const guard = guardRequest(name, args);
|
|
334
|
+
if (guard.blocked) {
|
|
335
|
+
return { content: [{ type: "text", text: guard.reason ?? "Request blocked by security guard" }], isError: true };
|
|
336
|
+
}
|
|
201
337
|
try {
|
|
202
338
|
let text;
|
|
203
339
|
switch (name) {
|
|
@@ -222,7 +358,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
222
358
|
break;
|
|
223
359
|
// Init & Sync
|
|
224
360
|
case "init_project":
|
|
225
|
-
text = handleInitProject(stmts, InitProjectSchema.parse(args));
|
|
361
|
+
text = await handleInitProject(stmts, InitProjectSchema.parse(args));
|
|
226
362
|
break;
|
|
227
363
|
case "sync_file":
|
|
228
364
|
text = handleSyncFile(stmts, SyncFileSchema.parse(args));
|
|
@@ -234,6 +370,23 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
234
370
|
case "grep_code":
|
|
235
371
|
text = handleGrepCode(stmts, GrepCodeSchema.parse(args));
|
|
236
372
|
break;
|
|
373
|
+
// Context & Token Optimization
|
|
374
|
+
case "get_context":
|
|
375
|
+
text = await handleGetContext(stmts, GetContextSchema.parse(args));
|
|
376
|
+
break;
|
|
377
|
+
case "get_recent":
|
|
378
|
+
text = handleGetRecent(stmts, GetRecentSchema.parse(args));
|
|
379
|
+
break;
|
|
380
|
+
// Reward System
|
|
381
|
+
case "reward":
|
|
382
|
+
text = handleReward(stmts, RewardSchema.parse(args));
|
|
383
|
+
break;
|
|
384
|
+
case "penalize":
|
|
385
|
+
text = handlePenalize(stmts, PenalizeSchema.parse(args));
|
|
386
|
+
break;
|
|
387
|
+
case "show_rewards":
|
|
388
|
+
text = handleShowRewards(stmts, ShowRewardsSchema.parse(args));
|
|
389
|
+
break;
|
|
237
390
|
// Logic Guardian
|
|
238
391
|
case "validate_file":
|
|
239
392
|
text = handleValidateFile(ValidateFileSchema.parse(args));
|
|
@@ -244,10 +397,18 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
244
397
|
case "get_checklist":
|
|
245
398
|
text = handleGetChecklist();
|
|
246
399
|
break;
|
|
400
|
+
// Coding Guard
|
|
401
|
+
case "coding_rules":
|
|
402
|
+
text = handleGetCodingRules();
|
|
403
|
+
break;
|
|
404
|
+
case "check_code_quality":
|
|
405
|
+
text = handleCheckCodeQuality(CheckCodeQualitySchema.parse(args));
|
|
406
|
+
break;
|
|
247
407
|
default:
|
|
248
408
|
return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
|
|
249
409
|
}
|
|
250
|
-
|
|
410
|
+
// Security: scan output for sensitive data leakage
|
|
411
|
+
return { content: [{ type: "text", text: guardOutput(name, text) }] };
|
|
251
412
|
}
|
|
252
413
|
catch (err) {
|
|
253
414
|
const message = err instanceof z.ZodError
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface Skeleton {
|
|
2
|
+
imports: string[];
|
|
3
|
+
exports: string[];
|
|
4
|
+
todos: string[];
|
|
5
|
+
summary: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function extractSkeleton(source: string, language: string): Skeleton;
|
|
8
|
+
/** Render skeleton as compact text for context assembly. */
|
|
9
|
+
export declare function renderSkeleton(sk: Skeleton, filepath: string): string;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// Structural skeleton extraction — regex-based AST-like parsing
|
|
2
|
+
// Returns only signatures, imports, and TODO comments (no function bodies)
|
|
3
|
+
// Used by get_context when a file exceeds the per-file token budget
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// TypeScript / JavaScript
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
function skeletonTS(source) {
|
|
8
|
+
const lines = source.split("\n");
|
|
9
|
+
const imports = [];
|
|
10
|
+
const exports = [];
|
|
11
|
+
const todos = [];
|
|
12
|
+
let summary = "";
|
|
13
|
+
// Grab first JSDoc comment as summary
|
|
14
|
+
const jsdoc = source.match(/^\/\*\*([\s\S]*?)\*\//m);
|
|
15
|
+
if (jsdoc) {
|
|
16
|
+
summary = jsdoc[1].replace(/\s*\*\s*/g, " ").trim().slice(0, 150);
|
|
17
|
+
}
|
|
18
|
+
let i = 0;
|
|
19
|
+
while (i < lines.length) {
|
|
20
|
+
const line = lines[i];
|
|
21
|
+
const trimmed = line.trim();
|
|
22
|
+
// Imports
|
|
23
|
+
if (/^import\s/.test(trimmed)) {
|
|
24
|
+
// Multi-line import: collect until ';'
|
|
25
|
+
let full = line;
|
|
26
|
+
while (!full.includes(";") && i + 1 < lines.length) {
|
|
27
|
+
i++;
|
|
28
|
+
full += " " + lines[i].trim();
|
|
29
|
+
}
|
|
30
|
+
imports.push(full.replace(/\s+/g, " ").trim());
|
|
31
|
+
i++;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
// Exported declarations
|
|
35
|
+
if (/^export\s/.test(trimmed)) {
|
|
36
|
+
// Grab JSDoc above if present
|
|
37
|
+
let sig = line;
|
|
38
|
+
// If it's a function/class/interface, find the signature (up to first '{' or ';')
|
|
39
|
+
if (/^export\s+(async\s+)?function|^export\s+(abstract\s+)?class|^export\s+interface/.test(trimmed)) {
|
|
40
|
+
let j = i;
|
|
41
|
+
let full = "";
|
|
42
|
+
while (j < lines.length) {
|
|
43
|
+
full += lines[j] + "\n";
|
|
44
|
+
if (lines[j].includes("{") || lines[j].includes(";"))
|
|
45
|
+
break;
|
|
46
|
+
j++;
|
|
47
|
+
}
|
|
48
|
+
// Show only up to opening brace
|
|
49
|
+
sig = full.split("{")[0].replace(/\n/g, " ").replace(/\s+/g, " ").trim() + " { … }";
|
|
50
|
+
}
|
|
51
|
+
else if (/^export\s+(type|interface)\s/.test(trimmed)) {
|
|
52
|
+
// Multi-line type — take first line
|
|
53
|
+
sig = trimmed.split("{")[0].trim() + (trimmed.includes("{") ? " { … }" : "");
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
// const/enum/default — take line
|
|
57
|
+
sig = trimmed.slice(0, 120);
|
|
58
|
+
}
|
|
59
|
+
exports.push(sig);
|
|
60
|
+
i++;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
// TODOs
|
|
64
|
+
if (/\/\/\s*(TODO|FIXME|HACK)/i.test(trimmed)) {
|
|
65
|
+
todos.push(trimmed.slice(0, 100));
|
|
66
|
+
}
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
return { imports, exports, todos, summary };
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
// Python
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
function skeletonPython(source) {
|
|
75
|
+
const lines = source.split("\n");
|
|
76
|
+
const imports = [];
|
|
77
|
+
const exports = [];
|
|
78
|
+
const todos = [];
|
|
79
|
+
let summary = "";
|
|
80
|
+
// Module docstring
|
|
81
|
+
const docMatch = source.match(/^['"]{3}([\s\S]*?)['"]{3}/m);
|
|
82
|
+
if (docMatch)
|
|
83
|
+
summary = docMatch[1].trim().slice(0, 150);
|
|
84
|
+
for (let i = 0; i < lines.length; i++) {
|
|
85
|
+
const line = lines[i];
|
|
86
|
+
const trimmed = line.trim();
|
|
87
|
+
if (trimmed.startsWith("import ") || trimmed.startsWith("from ")) {
|
|
88
|
+
imports.push(trimmed.slice(0, 100));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
// Public function/class/async def at top level (no indent)
|
|
92
|
+
if (/^(def|class|async def)\s+(\w)/.test(trimmed) && !trimmed.startsWith("_")) {
|
|
93
|
+
// Collect signature (may span multiple lines until ':')
|
|
94
|
+
let sig = line;
|
|
95
|
+
let j = i + 1;
|
|
96
|
+
while (!sig.includes(":") && j < lines.length) {
|
|
97
|
+
sig += " " + lines[j].trim();
|
|
98
|
+
j++;
|
|
99
|
+
}
|
|
100
|
+
sig = sig.split(":")[0].replace(/\s+/g, " ").trim() + ":";
|
|
101
|
+
exports.push(sig.slice(0, 120));
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (/^\s*#\s*(TODO|FIXME|HACK)/i.test(line)) {
|
|
105
|
+
todos.push(trimmed.slice(0, 100));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return { imports, exports, todos, summary };
|
|
109
|
+
}
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// Generic (markdown, yaml, json, etc.)
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
function skeletonGeneric(source) {
|
|
114
|
+
const lines = source.split("\n").slice(0, 30);
|
|
115
|
+
const todos = [];
|
|
116
|
+
for (const line of source.split("\n")) {
|
|
117
|
+
if (/(?:\/\/|#)\s*(TODO|FIXME|HACK)/i.test(line)) {
|
|
118
|
+
todos.push(line.trim().slice(0, 100));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
imports: [],
|
|
123
|
+
exports: [],
|
|
124
|
+
todos,
|
|
125
|
+
summary: lines.join("\n").slice(0, 300),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Public
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
export function extractSkeleton(source, language) {
|
|
132
|
+
switch (language) {
|
|
133
|
+
case "typescript":
|
|
134
|
+
case "javascript":
|
|
135
|
+
return skeletonTS(source);
|
|
136
|
+
case "python":
|
|
137
|
+
return skeletonPython(source);
|
|
138
|
+
default:
|
|
139
|
+
return skeletonGeneric(source);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Render skeleton as compact text for context assembly. */
|
|
143
|
+
export function renderSkeleton(sk, filepath) {
|
|
144
|
+
const parts = [`// ${filepath} [skeleton]`];
|
|
145
|
+
if (sk.summary)
|
|
146
|
+
parts.push(`// ${sk.summary}`);
|
|
147
|
+
if (sk.imports.length > 0)
|
|
148
|
+
parts.push(sk.imports.slice(0, 8).join("\n"));
|
|
149
|
+
if (sk.exports.length > 0) {
|
|
150
|
+
parts.push("// — exports —");
|
|
151
|
+
parts.push(sk.exports.join("\n"));
|
|
152
|
+
}
|
|
153
|
+
if (sk.todos.length > 0) {
|
|
154
|
+
parts.push("// — TODOs —");
|
|
155
|
+
parts.push(sk.todos.join("\n"));
|
|
156
|
+
}
|
|
157
|
+
return parts.join("\n\n");
|
|
158
|
+
}
|
package/build/indexer/project.js
CHANGED
|
@@ -183,9 +183,9 @@ function indexLogicGuardianYaml(path, stmts, results) {
|
|
|
183
183
|
}
|
|
184
184
|
// Source file indexing — extrage exporturi, clase, funcții principale
|
|
185
185
|
const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs"]);
|
|
186
|
-
const SKIP_DIRS = new Set(["node_modules", ".git", "build", "dist", "__pycache__", ".next", "venv", ".venv", "target"]);
|
|
187
|
-
const MAX_SOURCE_FILES =
|
|
188
|
-
function indexSourceFile(filepath, projectName, stmts) {
|
|
186
|
+
const SKIP_DIRS = new Set(["node_modules", ".git", "build", "dist", "__pycache__", ".next", "venv", ".venv", "target", ".cache", "coverage", ".nyc_output"]);
|
|
187
|
+
const MAX_SOURCE_FILES = 10_000;
|
|
188
|
+
function indexSourceFile(filepath, rootDir, projectName, stmts) {
|
|
189
189
|
const content = readFile(filepath);
|
|
190
190
|
if (!content)
|
|
191
191
|
return [];
|
|
@@ -206,18 +206,18 @@ function indexSourceFile(filepath, projectName, stmts) {
|
|
|
206
206
|
}
|
|
207
207
|
if (exports.length === 0)
|
|
208
208
|
return [];
|
|
209
|
-
|
|
209
|
+
// Cale relativă față de rădăcina proiectului
|
|
210
|
+
const relPath = filepath.replace(/\\/g, "/").replace(rootDir.replace(/\\/g, "/") + "/", "");
|
|
210
211
|
const obs = [`exports from ${relPath}: ${exports.slice(0, 10).join(", ")}`];
|
|
211
212
|
upsert(stmts, projectName, "project", obs);
|
|
212
213
|
return exports;
|
|
213
214
|
}
|
|
214
215
|
function scanSources(dir, projectName, stmts, results) {
|
|
215
|
-
const
|
|
216
|
-
const scanDir = existsSync(srcDir) ? srcDir : dir;
|
|
216
|
+
const rootDir = dir.replace(/\\/g, "/");
|
|
217
217
|
let fileCount = 0;
|
|
218
218
|
const exportedSymbols = [];
|
|
219
|
-
function walk(d
|
|
220
|
-
if (
|
|
219
|
+
function walk(d) {
|
|
220
|
+
if (fileCount >= MAX_SOURCE_FILES)
|
|
221
221
|
return;
|
|
222
222
|
let entries;
|
|
223
223
|
try {
|
|
@@ -230,18 +230,26 @@ function scanSources(dir, projectName, stmts, results) {
|
|
|
230
230
|
if (SKIP_DIRS.has(entry))
|
|
231
231
|
continue;
|
|
232
232
|
const full = join(d, entry);
|
|
233
|
-
|
|
233
|
+
let stat;
|
|
234
|
+
try {
|
|
235
|
+
stat = statSync(full);
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
234
240
|
if (stat.isDirectory()) {
|
|
235
|
-
walk(full
|
|
241
|
+
walk(full);
|
|
236
242
|
}
|
|
237
|
-
else if (SOURCE_EXTS.has(extname(entry))) {
|
|
238
|
-
const syms = indexSourceFile(full, projectName, stmts);
|
|
243
|
+
else if (SOURCE_EXTS.has(extname(entry).toLowerCase())) {
|
|
244
|
+
const syms = indexSourceFile(full, rootDir, projectName, stmts);
|
|
239
245
|
exportedSymbols.push(...syms);
|
|
240
246
|
fileCount++;
|
|
247
|
+
if (fileCount >= MAX_SOURCE_FILES)
|
|
248
|
+
return;
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
|
-
walk(
|
|
252
|
+
walk(dir);
|
|
245
253
|
if (fileCount > 0) {
|
|
246
254
|
results.push({
|
|
247
255
|
entity: projectName,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Statements } from "../database.js";
|
|
2
|
+
export declare const getLastExperienceId: () => number | null;
|
|
3
|
+
export declare const setLastExperienceId: (id: number) => void;
|
|
4
|
+
export declare function decayedReward(reward: number, rewardedAt: number | null): number;
|
|
5
|
+
export declare function createExperience(query: string, contextFps: string[], strategy: string, stmts: Statements): number;
|
|
6
|
+
export declare function rewardExperience(id: number, delta: number, feedback: string | null, stmts: Statements): {
|
|
7
|
+
query: string;
|
|
8
|
+
fps: string[];
|
|
9
|
+
} | null;
|
|
10
|
+
export declare function implicitRewardFromSync(filepath: string, stmts: Statements): boolean;
|
|
11
|
+
export declare function getFileRewardsMap(stmts: Statements): Map<string, number>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Reward system — lightweight RL from get_context usage signals
|
|
2
|
+
// Sources: explicit reward()/penalize(), implicit sync_file(), temporal decay
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// In-process tracking of the last created experience (server is long-running)
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
let _lastId = null;
|
|
7
|
+
export const getLastExperienceId = () => _lastId;
|
|
8
|
+
export const setLastExperienceId = (id) => { _lastId = id; };
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Exponential decay — half-life ≈ 14 days (λ = ln(2)/14 ≈ 0.0495 ≈ 0.05)
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export function decayedReward(reward, rewardedAt) {
|
|
13
|
+
if (rewardedAt === null || rewardedAt === 0)
|
|
14
|
+
return 0;
|
|
15
|
+
const daysSince = Math.max(0, (Date.now() / 1000 - rewardedAt) / 86400);
|
|
16
|
+
return reward * Math.exp(-0.05 * daysSince);
|
|
17
|
+
}
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Helpers
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
function tokenizeQuery(query) {
|
|
22
|
+
return query.toLowerCase().split(/\s+/).filter((t) => t.length > 1).join(" ");
|
|
23
|
+
}
|
|
24
|
+
function parseFps(contextFps) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(contextFps);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Create experience — called after get_context returns results
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
export function createExperience(query, contextFps, strategy, stmts) {
|
|
36
|
+
const result = stmts.insertExperience.run(query, tokenizeQuery(query), JSON.stringify(contextFps), strategy);
|
|
37
|
+
const id = Number(result.lastInsertRowid);
|
|
38
|
+
setLastExperienceId(id);
|
|
39
|
+
return id;
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Reward or penalize an experience (explicit feedback)
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
export function rewardExperience(id, delta, feedback, stmts) {
|
|
45
|
+
const exp = stmts.getExperienceById.get(id);
|
|
46
|
+
if (!exp)
|
|
47
|
+
return null;
|
|
48
|
+
stmts.updateExperienceReward.run(delta, feedback, id);
|
|
49
|
+
const fps = parseFps(exp.context_fps);
|
|
50
|
+
for (const fp of fps) {
|
|
51
|
+
stmts.upsertFileReward.run(fp, delta);
|
|
52
|
+
}
|
|
53
|
+
return { query: exp.query, fps };
|
|
54
|
+
}
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
// Implicit reward from sync_file — if filepath was in the last context → +0.3
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
const IMPLICIT_DELTA = 0.3;
|
|
59
|
+
export function implicitRewardFromSync(filepath, stmts) {
|
|
60
|
+
const lastId = getLastExperienceId();
|
|
61
|
+
if (lastId === null)
|
|
62
|
+
return false;
|
|
63
|
+
const exp = stmts.getExperienceById.get(lastId);
|
|
64
|
+
if (!exp)
|
|
65
|
+
return false;
|
|
66
|
+
const fps = parseFps(exp.context_fps);
|
|
67
|
+
if (!fps.includes(filepath))
|
|
68
|
+
return false;
|
|
69
|
+
stmts.updateExperienceReward.run(IMPLICIT_DELTA, null, lastId);
|
|
70
|
+
stmts.upsertFileReward.run(filepath, IMPLICIT_DELTA);
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Get file rewards map — for ranking boost in assembleContext()
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
export function getFileRewardsMap(stmts) {
|
|
77
|
+
const rows = stmts.getFileRewards.all();
|
|
78
|
+
const map = new Map();
|
|
79
|
+
for (const row of rows) {
|
|
80
|
+
const d = decayedReward(row.total_reward, row.last_rewarded);
|
|
81
|
+
if (d > 0)
|
|
82
|
+
map.set(row.filepath, d);
|
|
83
|
+
}
|
|
84
|
+
return map;
|
|
85
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Statements } from "../database.js";
|
|
2
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
|
+
export declare function estimateTokens(text: string): number;
|
|
4
|
+
export declare function extractFragments(source: string, query: string, contextLines?: number): string;
|
|
5
|
+
export declare function computeDiff(prev: string, curr: string, maxChanges?: number): string;
|
|
6
|
+
export interface ContextFile {
|
|
7
|
+
filepath: string;
|
|
8
|
+
language: string;
|
|
9
|
+
tokens: number;
|
|
10
|
+
content: string;
|
|
11
|
+
reason: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ContextResult {
|
|
14
|
+
files: ContextFile[];
|
|
15
|
+
totalTokens: number;
|
|
16
|
+
strategy: "qdrant" | "tfidf" | "recent";
|
|
17
|
+
truncated: boolean;
|
|
18
|
+
skippedFiles: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ContextOptions {
|
|
21
|
+
maxTokens?: number;
|
|
22
|
+
maxTokensPerFile?: number;
|
|
23
|
+
dirs?: string[];
|
|
24
|
+
recentOnly?: boolean;
|
|
25
|
+
recentHours?: number;
|
|
26
|
+
skeletonOnly?: boolean;
|
|
27
|
+
topK?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function assembleContext(query: string, stmts: Statements, cfg: ResolvedConfig, opts?: ContextOptions): Promise<ContextResult>;
|