@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.
Files changed (37) hide show
  1. package/README.md +36 -1
  2. package/package.json +4 -1
  3. package/src/cli/commands/ask.ts +9 -0
  4. package/src/cli/commands/query.ts +3 -2
  5. package/src/cli/pager.ts +1 -1
  6. package/src/cli/program.ts +89 -0
  7. package/src/core/links.ts +92 -20
  8. package/src/ingestion/sync.ts +267 -23
  9. package/src/ingestion/types.ts +2 -0
  10. package/src/ingestion/walker.ts +2 -1
  11. package/src/mcp/tools/index.ts +30 -1
  12. package/src/mcp/tools/query.ts +22 -2
  13. package/src/mcp/tools/search.ts +8 -0
  14. package/src/mcp/tools/vsearch.ts +8 -0
  15. package/src/pipeline/answer.ts +324 -7
  16. package/src/pipeline/expansion.ts +243 -7
  17. package/src/pipeline/explain.ts +93 -5
  18. package/src/pipeline/hybrid.ts +240 -57
  19. package/src/pipeline/query-modes.ts +125 -0
  20. package/src/pipeline/rerank.ts +34 -13
  21. package/src/pipeline/search.ts +41 -3
  22. package/src/pipeline/temporal.ts +257 -0
  23. package/src/pipeline/types.ts +58 -0
  24. package/src/pipeline/vsearch.ts +107 -9
  25. package/src/serve/public/app.tsx +1 -3
  26. package/src/serve/public/globals.built.css +2 -2
  27. package/src/serve/public/lib/retrieval-filters.ts +167 -0
  28. package/src/serve/public/pages/Ask.tsx +339 -109
  29. package/src/serve/public/pages/Browse.tsx +71 -5
  30. package/src/serve/public/pages/DocView.tsx +2 -21
  31. package/src/serve/public/pages/Search.tsx +507 -120
  32. package/src/serve/routes/api.ts +202 -2
  33. package/src/store/migrations/006-document-metadata.ts +104 -0
  34. package/src/store/migrations/007-document-date-fields.ts +24 -0
  35. package/src/store/migrations/index.ts +3 -1
  36. package/src/store/sqlite/adapter.ts +218 -5
  37. package/src/store/types.ts +46 -0
@@ -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(queryEmbedding, limit, {
76
- minScore,
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: results.length,
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
- const docs = await store.listDocuments(options.collection);
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 active docs with mirrorHash
336
- const activeDocs = docs.value.filter((d) => d.mirrorHash && d.active);
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,
@@ -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">