@toolbaux/guardian 0.1.20 → 0.1.22
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/dist/commands/mcp-serve.js +50 -17
- package/dist/commands/search.js +57 -18
- package/package.json +1 -1
|
@@ -311,30 +311,63 @@ async function search(args) {
|
|
|
311
311
|
// Frontend pages: match path or component
|
|
312
312
|
const pages = (d.frontend_pages || []).filter((p) => p.path?.toLowerCase().includes(q) || p.component?.toLowerCase().includes(q) ||
|
|
313
313
|
p.api_calls?.some((c) => c.toLowerCase().includes(q))).slice(0, 5).map((p) => `${p.path} → ${p.component}`);
|
|
314
|
-
// Functions: search
|
|
314
|
+
// Functions: ranked search across names, literals, calls — capped at 10 to prevent flooding
|
|
315
315
|
const fnHits = [];
|
|
316
316
|
const fi = await loadFuncIntel();
|
|
317
317
|
if (fi) {
|
|
318
|
-
//
|
|
318
|
+
// Build a field map: file → field names (augments fn hits with model fields)
|
|
319
|
+
const fileToFields = new Map();
|
|
320
|
+
for (const m of Object.values(d.model_registry || {})) {
|
|
321
|
+
if (!m.file)
|
|
322
|
+
continue;
|
|
323
|
+
const existing = fileToFields.get(m.file) ?? [];
|
|
324
|
+
fileToFields.set(m.file, [...existing, ...(m.fields ?? [])]);
|
|
325
|
+
}
|
|
326
|
+
const scored = [];
|
|
327
|
+
const seen = new Set();
|
|
328
|
+
for (const fn of (fi.functions ?? [])) {
|
|
329
|
+
const nameNorm = (fn.name ?? "").toLowerCase();
|
|
330
|
+
const fileNorm = (fn.file ?? "").toLowerCase();
|
|
331
|
+
const callsNorm = (fn.calls ?? []).map((c) => c.toLowerCase());
|
|
332
|
+
const litsNorm = [...(fn.stringLiterals ?? []), ...(fn.regexPatterns ?? [])].map((l) => l.toLowerCase());
|
|
333
|
+
let score = 0;
|
|
334
|
+
if (nameNorm === q)
|
|
335
|
+
score = 1.0;
|
|
336
|
+
else if (nameNorm.includes(q))
|
|
337
|
+
score = 0.7;
|
|
338
|
+
else if (callsNorm.some((c) => c.includes(q)))
|
|
339
|
+
score = 0.5;
|
|
340
|
+
else if (litsNorm.some((l) => l.includes(q)))
|
|
341
|
+
score = 0.3;
|
|
342
|
+
else if (fileNorm.includes(q))
|
|
343
|
+
score = 0.2;
|
|
344
|
+
if (score > 0) {
|
|
345
|
+
scored.push({ fn, score });
|
|
346
|
+
seen.add(`${fn.file}:${fn.name}`);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
// Also surface literal_index hits not already captured
|
|
319
350
|
const litIndex = fi.literal_index ?? {};
|
|
320
351
|
for (const [key, hits] of Object.entries(litIndex)) {
|
|
321
|
-
if (key.includes(q))
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
352
|
+
if (!key.includes(q))
|
|
353
|
+
continue;
|
|
354
|
+
for (const h of hits) {
|
|
355
|
+
const uid = `${h.file}:${h.function}`;
|
|
356
|
+
if (seen.has(uid))
|
|
357
|
+
continue;
|
|
358
|
+
seen.add(uid);
|
|
359
|
+
const fn = fi.functions.find((f) => f.file === h.file && f.name === h.function);
|
|
360
|
+
scored.push({ fn: fn ?? { name: h.function, file: h.file, lines: [h.line, h.line], calls: [], stringLiterals: [], regexPatterns: [] }, score: 0.25 });
|
|
325
361
|
}
|
|
326
|
-
if (fnHits.length >= 10)
|
|
327
|
-
break;
|
|
328
362
|
}
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
363
|
+
// Sort by score desc, take top 10
|
|
364
|
+
scored.sort((a, b) => b.score - a.score);
|
|
365
|
+
const projectRoot = process.cwd();
|
|
366
|
+
for (const { fn } of scored.slice(0, 10)) {
|
|
367
|
+
const relFile = fn.file?.startsWith("/") ? require("path").relative(projectRoot, fn.file) : fn.file;
|
|
368
|
+
const fields = fileToFields.get(fn.file) ?? [];
|
|
369
|
+
const fieldSuffix = fields.length > 0 ? ` fields:${fields.slice(0, 5).join(",")}` : "";
|
|
370
|
+
fnHits.push(`${fn.name} [${relFile}:${fn.lines?.[0]}]${fieldSuffix}`);
|
|
338
371
|
}
|
|
339
372
|
}
|
|
340
373
|
return compact({
|
package/dist/commands/search.js
CHANGED
|
@@ -11,6 +11,7 @@ export async function runSearch(options) {
|
|
|
11
11
|
const heatmap = await loadHeatmap(inputDir);
|
|
12
12
|
const funcIntel = await loadFunctionIntelligence(inputDir);
|
|
13
13
|
const types = normalizeTypes(options.types);
|
|
14
|
+
const projectRoot = options.projectRoot ?? process.cwd();
|
|
14
15
|
const matches = searchSnapshots({
|
|
15
16
|
architecture,
|
|
16
17
|
ux,
|
|
@@ -18,6 +19,8 @@ export async function runSearch(options) {
|
|
|
18
19
|
types,
|
|
19
20
|
heatmap,
|
|
20
21
|
funcIntel,
|
|
22
|
+
projectRoot,
|
|
23
|
+
topN: options.topN ?? 10,
|
|
21
24
|
});
|
|
22
25
|
const content = renderSearchMarkdown(options.query, matches);
|
|
23
26
|
if (options.output) {
|
|
@@ -113,7 +116,7 @@ function scoreItem(queryTokens, item) {
|
|
|
113
116
|
return Math.min(1, total / queryTokens.length);
|
|
114
117
|
}
|
|
115
118
|
function searchSnapshots(params) {
|
|
116
|
-
const { architecture, ux, query, types, heatmap, funcIntel } = params;
|
|
119
|
+
const { architecture, ux, query, types, heatmap, funcIntel, projectRoot, topN } = params;
|
|
117
120
|
const queryTokens = tokenize(query);
|
|
118
121
|
const matches = [];
|
|
119
122
|
const pageUsage = buildComponentPageUsage(ux);
|
|
@@ -243,16 +246,23 @@ function searchSnapshots(params) {
|
|
|
243
246
|
}
|
|
244
247
|
if (types.has("functions") && funcIntel) {
|
|
245
248
|
const queryTokens = tokenize(query);
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
text: [...fn.stringLiterals, ...fn.regexPatterns, ...fn.calls, fn.language],
|
|
252
|
-
});
|
|
253
|
-
if (score <= 0)
|
|
249
|
+
const fnMatches = [];
|
|
250
|
+
// Build a map: file → model field names (feature 3 — field augmentation)
|
|
251
|
+
const fileToFields = new Map();
|
|
252
|
+
for (const model of architecture.data_models) {
|
|
253
|
+
if (!model.file)
|
|
254
254
|
continue;
|
|
255
|
-
const
|
|
255
|
+
const existing = fileToFields.get(model.file) ?? [];
|
|
256
|
+
fileToFields.set(model.file, [...existing, ...model.fields]);
|
|
257
|
+
}
|
|
258
|
+
// Helper: relativize a path if it looks absolute
|
|
259
|
+
const relativize = (filePath) => {
|
|
260
|
+
if (!path.isAbsolute(filePath))
|
|
261
|
+
return filePath;
|
|
262
|
+
return path.relative(projectRoot, filePath);
|
|
263
|
+
};
|
|
264
|
+
// Helper: build detail lines for a function hit
|
|
265
|
+
const buildDetail = (fn, relFile) => {
|
|
256
266
|
const detail = [];
|
|
257
267
|
if (fn.stringLiterals.length > 0) {
|
|
258
268
|
detail.push(`Literals: ${formatList(fn.stringLiterals.slice(0, 3).map((l) => `"${l.slice(0, 60)}"`), 3)}`);
|
|
@@ -263,42 +273,71 @@ function searchSnapshots(params) {
|
|
|
263
273
|
if (fn.calls.length > 0) {
|
|
264
274
|
detail.push(`Calls: ${formatList(fn.calls, 5)}`);
|
|
265
275
|
}
|
|
266
|
-
|
|
276
|
+
// Feature 3 — append model field names for the file this function lives in
|
|
277
|
+
const fields = fileToFields.get(fn.file) ?? fileToFields.get(relFile);
|
|
278
|
+
if (fields && fields.length > 0) {
|
|
279
|
+
detail.push(`Model fields: ${formatList(fields.slice(0, 8), 8)}`);
|
|
280
|
+
}
|
|
281
|
+
return detail;
|
|
282
|
+
};
|
|
283
|
+
// 1. Name match — function / theorem name contains a query token
|
|
284
|
+
for (const fn of funcIntel.functions) {
|
|
285
|
+
const score = scoreItem(queryTokens, {
|
|
286
|
+
name: fn.name,
|
|
287
|
+
file: fn.file,
|
|
288
|
+
text: [...fn.stringLiterals, ...fn.regexPatterns, ...fn.calls, fn.language],
|
|
289
|
+
});
|
|
290
|
+
if (score <= 0)
|
|
291
|
+
continue;
|
|
292
|
+
const relFile = relativize(fn.file);
|
|
293
|
+
const lineRange = `${fn.lines[0]}–${fn.lines[1]}`;
|
|
294
|
+
const detail = buildDetail(fn, relFile);
|
|
295
|
+
fnMatches.push({
|
|
267
296
|
type: "functions",
|
|
268
297
|
name: `${fn.name} (${fn.language})`,
|
|
269
298
|
score,
|
|
270
299
|
markdown: [
|
|
271
|
-
`**${fn.name}** · ${
|
|
300
|
+
`**${fn.name}** · ${relFile}:${lineRange} · ${fn.language}${fn.isAsync ? " · async" : ""}`,
|
|
272
301
|
...detail,
|
|
273
302
|
],
|
|
274
303
|
});
|
|
275
304
|
}
|
|
276
305
|
// 2. Literal index match — query token appears in a function's string/regex literals
|
|
277
|
-
// (
|
|
278
|
-
// the function name itself doesn't match)
|
|
306
|
+
// Uses proper scoreItem() ranking instead of hardcoded 0.6 to prevent noise flooding.
|
|
279
307
|
for (const tok of queryTokens) {
|
|
280
308
|
const hits = funcIntel.literal_index[tok.toLowerCase()];
|
|
281
309
|
if (!hits)
|
|
282
310
|
continue;
|
|
283
311
|
for (const hit of hits) {
|
|
284
312
|
// Skip if we already emitted this function via name match above
|
|
285
|
-
if (
|
|
313
|
+
if (fnMatches.some((m) => m.type === "functions" && m.name.startsWith(hit.function + " ("))) {
|
|
286
314
|
continue;
|
|
287
315
|
}
|
|
288
316
|
const fn = funcIntel.functions.find((f) => f.file === hit.file && f.name === hit.function);
|
|
289
317
|
if (!fn)
|
|
290
318
|
continue;
|
|
291
|
-
|
|
319
|
+
const score = scoreItem(queryTokens, {
|
|
320
|
+
name: fn.name,
|
|
321
|
+
file: fn.file,
|
|
322
|
+
text: [...fn.stringLiterals, ...fn.regexPatterns, ...fn.calls, fn.language],
|
|
323
|
+
});
|
|
324
|
+
const relFile = relativize(fn.file);
|
|
325
|
+
const detail = buildDetail(fn, relFile);
|
|
326
|
+
fnMatches.push({
|
|
292
327
|
type: "functions",
|
|
293
328
|
name: `${fn.name} (${fn.language})`,
|
|
294
|
-
score: 0.
|
|
329
|
+
score: Math.max(score, 0.2), // floor at 0.2 so literal hits still surface but rank below name matches
|
|
295
330
|
markdown: [
|
|
296
|
-
`**${fn.name}** · ${
|
|
331
|
+
`**${fn.name}** · ${relFile}:${fn.lines[0]}–${fn.lines[1]} · ${fn.language}`,
|
|
297
332
|
`Matched literal/pattern containing "${tok}"`,
|
|
333
|
+
...detail,
|
|
298
334
|
],
|
|
299
335
|
});
|
|
300
336
|
}
|
|
301
337
|
}
|
|
338
|
+
// Feature 2 — rank by score, cap at topN to prevent context flooding
|
|
339
|
+
fnMatches.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
340
|
+
matches.push(...fnMatches.slice(0, topN));
|
|
302
341
|
}
|
|
303
342
|
return matches.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
304
343
|
}
|
package/package.json
CHANGED