@gmickel/gno 0.16.0 → 0.17.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/README.md +36 -1
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +9 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +89 -0
- package/src/core/links.ts +92 -20
- package/src/ingestion/sync.ts +267 -23
- package/src/ingestion/types.ts +2 -0
- package/src/ingestion/walker.ts +2 -1
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/query.ts +22 -2
- package/src/mcp/tools/search.ts +8 -0
- package/src/mcp/tools/vsearch.ts +8 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +243 -7
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +240 -57
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +34 -13
- package/src/pipeline/search.ts +41 -3
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +58 -0
- package/src/pipeline/vsearch.ts +107 -9
- package/src/serve/public/app.tsx +1 -3
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/lib/retrieval-filters.ts +167 -0
- package/src/serve/public/pages/Ask.tsx +339 -109
- package/src/serve/public/pages/Browse.tsx +71 -5
- package/src/serve/public/pages/DocView.tsx +2 -21
- package/src/serve/public/pages/Search.tsx +507 -120
- package/src/serve/routes/api.ts +202 -2
- package/src/store/migrations/006-document-metadata.ts +104 -0
- package/src/store/migrations/007-document-date-fields.ts +24 -0
- package/src/store/migrations/index.ts +3 -1
- package/src/store/sqlite/adapter.ts +218 -5
- package/src/store/types.ts +46 -0
package/src/pipeline/vsearch.ts
CHANGED
|
@@ -15,6 +15,13 @@ import { err, ok } from "../store/types";
|
|
|
15
15
|
import { createChunkLookup } from "./chunk-lookup";
|
|
16
16
|
import { formatQueryForEmbedding } from "./contextual";
|
|
17
17
|
import { detectQueryLanguage } from "./query-language";
|
|
18
|
+
import {
|
|
19
|
+
resolveRecencyTimestamp,
|
|
20
|
+
isWithinTemporalRange,
|
|
21
|
+
resolveTemporalRange,
|
|
22
|
+
shouldSortByRecency,
|
|
23
|
+
type TemporalRange,
|
|
24
|
+
} from "./temporal";
|
|
18
25
|
|
|
19
26
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
20
27
|
// Score Normalization
|
|
@@ -58,6 +65,13 @@ export async function searchVectorWithEmbedding(
|
|
|
58
65
|
const { store, vectorIndex } = deps;
|
|
59
66
|
const limit = options.limit ?? 20;
|
|
60
67
|
const minScore = options.minScore ?? 0;
|
|
68
|
+
const recencySort = shouldSortByRecency(query);
|
|
69
|
+
const retrievalLimit = recencySort ? limit * 3 : limit;
|
|
70
|
+
const temporalRange = resolveTemporalRange(
|
|
71
|
+
query,
|
|
72
|
+
options.since,
|
|
73
|
+
options.until
|
|
74
|
+
);
|
|
61
75
|
|
|
62
76
|
// Detect query language for metadata (DOES NOT affect retrieval filtering)
|
|
63
77
|
const detection = detectQueryLanguage(query);
|
|
@@ -72,15 +86,20 @@ export async function searchVectorWithEmbedding(
|
|
|
72
86
|
}
|
|
73
87
|
|
|
74
88
|
// Search nearest neighbors
|
|
75
|
-
const searchResult = await vectorIndex.searchNearest(
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
const searchResult = await vectorIndex.searchNearest(
|
|
90
|
+
queryEmbedding,
|
|
91
|
+
retrievalLimit,
|
|
92
|
+
{
|
|
93
|
+
minScore,
|
|
94
|
+
}
|
|
95
|
+
);
|
|
78
96
|
|
|
79
97
|
if (!searchResult.ok) {
|
|
80
98
|
return err("QUERY_FAILED", searchResult.error.message);
|
|
81
99
|
}
|
|
82
100
|
|
|
83
101
|
const vecResults = searchResult.value;
|
|
102
|
+
const uniqueHashes = [...new Set(vecResults.map((v) => v.mirrorHash))];
|
|
84
103
|
|
|
85
104
|
// Get collection paths for absPath resolution
|
|
86
105
|
const collectionsResult = await store.getCollections();
|
|
@@ -96,10 +115,14 @@ export async function searchVectorWithEmbedding(
|
|
|
96
115
|
collection: options.collection,
|
|
97
116
|
tagsAll: options.tagsAll,
|
|
98
117
|
tagsAny: options.tagsAny,
|
|
118
|
+
since: temporalRange.since,
|
|
119
|
+
until: temporalRange.until,
|
|
120
|
+
categories: options.categories,
|
|
121
|
+
author: options.author,
|
|
122
|
+
mirrorHashes: uniqueHashes,
|
|
99
123
|
});
|
|
100
124
|
|
|
101
125
|
// Pre-fetch all chunks in one batch query (eliminates N+1)
|
|
102
|
-
const uniqueHashes = [...new Set(vecResults.map((v) => v.mirrorHash))];
|
|
103
126
|
const chunksMapResult = await store.getChunksBatch(uniqueHashes);
|
|
104
127
|
if (!chunksMapResult.ok) {
|
|
105
128
|
return err("QUERY_FAILED", chunksMapResult.error.message);
|
|
@@ -178,6 +201,7 @@ export async function searchVectorWithEmbedding(
|
|
|
178
201
|
mime: doc.sourceMime,
|
|
179
202
|
ext: doc.sourceExt,
|
|
180
203
|
modifiedAt: doc.sourceMtime,
|
|
204
|
+
documentDate: doc.frontmatterDate ?? undefined,
|
|
181
205
|
sizeBytes: doc.sourceSize,
|
|
182
206
|
sourceHash: doc.sourceHash,
|
|
183
207
|
},
|
|
@@ -223,6 +247,7 @@ export async function searchVectorWithEmbedding(
|
|
|
223
247
|
mime: doc.sourceMime,
|
|
224
248
|
ext: doc.sourceExt,
|
|
225
249
|
modifiedAt: doc.sourceMtime,
|
|
250
|
+
documentDate: doc.frontmatterDate ?? undefined,
|
|
226
251
|
sizeBytes: doc.sourceSize,
|
|
227
252
|
sourceHash: doc.sourceHash,
|
|
228
253
|
},
|
|
@@ -237,15 +262,38 @@ export async function searchVectorWithEmbedding(
|
|
|
237
262
|
}
|
|
238
263
|
}
|
|
239
264
|
|
|
265
|
+
if (recencySort) {
|
|
266
|
+
results.sort((a, b) => {
|
|
267
|
+
const aTs = resolveRecencyTimestamp(
|
|
268
|
+
a.source.documentDate,
|
|
269
|
+
a.source.modifiedAt
|
|
270
|
+
);
|
|
271
|
+
const bTs = resolveRecencyTimestamp(
|
|
272
|
+
b.source.documentDate,
|
|
273
|
+
b.source.modifiedAt
|
|
274
|
+
);
|
|
275
|
+
if (aTs !== bTs) {
|
|
276
|
+
return bTs - aTs;
|
|
277
|
+
}
|
|
278
|
+
return b.score - a.score;
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const finalResults = results.slice(0, limit);
|
|
283
|
+
|
|
240
284
|
return ok({
|
|
241
|
-
results,
|
|
285
|
+
results: finalResults,
|
|
242
286
|
meta: {
|
|
243
287
|
query,
|
|
244
288
|
mode: "vector",
|
|
245
289
|
vectorsUsed: true,
|
|
246
|
-
totalResults:
|
|
290
|
+
totalResults: finalResults.length,
|
|
247
291
|
collection: options.collection,
|
|
248
292
|
lang: options.lang,
|
|
293
|
+
since: temporalRange.since,
|
|
294
|
+
until: temporalRange.until,
|
|
295
|
+
categories: options.categories,
|
|
296
|
+
author: options.author,
|
|
249
297
|
queryLanguage,
|
|
250
298
|
},
|
|
251
299
|
});
|
|
@@ -305,6 +353,7 @@ interface DocumentInfo {
|
|
|
305
353
|
sourceMime: string;
|
|
306
354
|
sourceExt: string;
|
|
307
355
|
sourceMtime: string;
|
|
356
|
+
frontmatterDate?: string | null;
|
|
308
357
|
sourceSize: number;
|
|
309
358
|
mirrorHash: string | null;
|
|
310
359
|
converterId: string | null;
|
|
@@ -319,6 +368,25 @@ interface DocumentMapOptions {
|
|
|
319
368
|
collection?: string;
|
|
320
369
|
tagsAll?: string[];
|
|
321
370
|
tagsAny?: string[];
|
|
371
|
+
since?: string;
|
|
372
|
+
until?: string;
|
|
373
|
+
categories?: string[];
|
|
374
|
+
author?: string;
|
|
375
|
+
mirrorHashes?: string[];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function matchesCategoryFilter(
|
|
379
|
+
doc: { contentType?: string | null; categories?: string[] | null },
|
|
380
|
+
categories?: string[]
|
|
381
|
+
): boolean {
|
|
382
|
+
if (!categories || categories.length === 0) {
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
const allowed = new Set(categories.map((c) => c.toLowerCase()));
|
|
386
|
+
if (doc.contentType && allowed.has(doc.contentType.toLowerCase())) {
|
|
387
|
+
return true;
|
|
388
|
+
}
|
|
389
|
+
return (doc.categories ?? []).some((c) => allowed.has(c.toLowerCase()));
|
|
322
390
|
}
|
|
323
391
|
|
|
324
392
|
async function buildDocumentMap(
|
|
@@ -327,13 +395,29 @@ async function buildDocumentMap(
|
|
|
327
395
|
): Promise<Map<string, DocumentInfo>> {
|
|
328
396
|
const result = new Map<string, DocumentInfo>();
|
|
329
397
|
|
|
330
|
-
|
|
398
|
+
if (options.mirrorHashes && options.mirrorHashes.length === 0) {
|
|
399
|
+
return result;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const docs = options.mirrorHashes
|
|
403
|
+
? await store.getDocumentsByMirrorHashes(options.mirrorHashes, {
|
|
404
|
+
collection: options.collection,
|
|
405
|
+
activeOnly: true,
|
|
406
|
+
})
|
|
407
|
+
: await store.listDocuments(options.collection);
|
|
331
408
|
if (!docs.ok) {
|
|
332
409
|
return result;
|
|
333
410
|
}
|
|
334
411
|
|
|
335
|
-
// Filter
|
|
336
|
-
|
|
412
|
+
// Filter docs with mirrorHash.
|
|
413
|
+
// listDocuments path still needs explicit active filter.
|
|
414
|
+
const activeDocs = options.mirrorHashes
|
|
415
|
+
? docs.value.filter((d) => d.mirrorHash)
|
|
416
|
+
: docs.value.filter((d) => d.mirrorHash && d.active);
|
|
417
|
+
const temporalRange: TemporalRange = {
|
|
418
|
+
since: options.since,
|
|
419
|
+
until: options.until,
|
|
420
|
+
};
|
|
337
421
|
|
|
338
422
|
// Apply tag filters if specified (batch fetch to avoid N+1)
|
|
339
423
|
const needsTagFilter = options.tagsAll?.length || options.tagsAny?.length;
|
|
@@ -370,6 +454,19 @@ async function buildDocumentMap(
|
|
|
370
454
|
}
|
|
371
455
|
|
|
372
456
|
for (const doc of activeDocs) {
|
|
457
|
+
if (!isWithinTemporalRange(doc.sourceMtime, temporalRange)) {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (!matchesCategoryFilter(doc, options.categories)) {
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (
|
|
464
|
+
options.author &&
|
|
465
|
+
!doc.author?.toLowerCase().includes(options.author.toLowerCase())
|
|
466
|
+
) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
373
470
|
// Skip if tag filter excluded this doc
|
|
374
471
|
if (allowedDocIds !== null && !allowedDocIds.has(doc.id)) {
|
|
375
472
|
continue;
|
|
@@ -385,6 +482,7 @@ async function buildDocumentMap(
|
|
|
385
482
|
sourceMime: doc.sourceMime,
|
|
386
483
|
sourceExt: doc.sourceExt,
|
|
387
484
|
sourceMtime: doc.sourceMtime,
|
|
485
|
+
frontmatterDate: doc.frontmatterDate,
|
|
388
486
|
sourceSize: doc.sourceSize,
|
|
389
487
|
mirrorHash: doc.mirrorHash,
|
|
390
488
|
converterId: doc.converterId,
|
package/src/serve/public/app.tsx
CHANGED
|
@@ -58,8 +58,6 @@ function App() {
|
|
|
58
58
|
}
|
|
59
59
|
window.history.pushState({}, "", to);
|
|
60
60
|
setLocation(to);
|
|
61
|
-
// Dispatch event for components that need to react to URL changes
|
|
62
|
-
window.dispatchEvent(new CustomEvent("locationchange", { detail: to }));
|
|
63
61
|
}, []);
|
|
64
62
|
|
|
65
63
|
// Global keyboard shortcuts (single-key, GitHub/Gmail pattern)
|
|
@@ -98,7 +96,7 @@ function App() {
|
|
|
98
96
|
<CaptureModalProvider>
|
|
99
97
|
<div className="flex min-h-screen flex-col">
|
|
100
98
|
<div className="flex-1">
|
|
101
|
-
<Page navigate={navigate} />
|
|
99
|
+
<Page key={location} navigate={navigate} />
|
|
102
100
|
</div>
|
|
103
101
|
<footer className="border-t border-border/50 bg-background/80 py-4 text-center text-muted-foreground text-sm">
|
|
104
102
|
<div className="flex items-center justify-center gap-4">
|