@gmickel/gno 0.16.0 → 0.18.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 +55 -2
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +13 -0
- package/src/cli/commands/models/use.ts +1 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +107 -0
- package/src/config/types.ts +2 -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/llm/nodeLlamaCpp/generation.ts +3 -1
- package/src/llm/registry.ts +1 -0
- package/src/llm/types.ts +2 -0
- package/src/mcp/tools/index.ts +34 -1
- package/src/mcp/tools/query.ts +26 -2
- package/src/mcp/tools/search.ts +10 -0
- package/src/mcp/tools/vsearch.ts +10 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +282 -11
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +273 -70
- package/src/pipeline/intent.ts +152 -0
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +109 -51
- package/src/pipeline/search.ts +58 -4
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +67 -0
- package/src/pipeline/vsearch.ts +121 -10
- 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 +174 -0
- package/src/serve/public/pages/Ask.tsx +378 -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 +561 -120
- package/src/serve/routes/api.ts +247 -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
|
@@ -372,16 +372,18 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
372
372
|
`
|
|
373
373
|
INSERT INTO documents (
|
|
374
374
|
collection, rel_path, source_hash, source_mime, source_ext,
|
|
375
|
-
source_size, source_mtime, docid, uri, title, mirror_hash,
|
|
376
|
-
converter_id, converter_version, language_hint,
|
|
377
|
-
|
|
378
|
-
|
|
375
|
+
source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
|
|
376
|
+
converter_id, converter_version, language_hint, content_type, categories,
|
|
377
|
+
author, frontmatter_date, date_fields, active, indexed_at, last_error_code,
|
|
378
|
+
last_error_message, last_error_at, ingest_version, updated_at
|
|
379
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
|
|
379
380
|
ON CONFLICT(collection, rel_path) DO UPDATE SET
|
|
380
381
|
source_hash = excluded.source_hash,
|
|
381
382
|
source_mime = excluded.source_mime,
|
|
382
383
|
source_ext = excluded.source_ext,
|
|
383
384
|
source_size = excluded.source_size,
|
|
384
385
|
source_mtime = excluded.source_mtime,
|
|
386
|
+
source_ctime = excluded.source_ctime,
|
|
385
387
|
docid = excluded.docid,
|
|
386
388
|
uri = excluded.uri,
|
|
387
389
|
title = excluded.title,
|
|
@@ -389,7 +391,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
389
391
|
converter_id = excluded.converter_id,
|
|
390
392
|
converter_version = excluded.converter_version,
|
|
391
393
|
language_hint = excluded.language_hint,
|
|
394
|
+
content_type = excluded.content_type,
|
|
395
|
+
categories = excluded.categories,
|
|
396
|
+
author = excluded.author,
|
|
397
|
+
frontmatter_date = excluded.frontmatter_date,
|
|
398
|
+
date_fields = excluded.date_fields,
|
|
392
399
|
active = 1,
|
|
400
|
+
indexed_at = datetime('now'),
|
|
393
401
|
last_error_code = excluded.last_error_code,
|
|
394
402
|
last_error_message = excluded.last_error_message,
|
|
395
403
|
last_error_at = excluded.last_error_at,
|
|
@@ -404,6 +412,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
404
412
|
doc.sourceExt,
|
|
405
413
|
doc.sourceSize,
|
|
406
414
|
doc.sourceMtime,
|
|
415
|
+
doc.sourceCtime ?? doc.sourceMtime,
|
|
407
416
|
docid,
|
|
408
417
|
uri,
|
|
409
418
|
doc.title ?? null,
|
|
@@ -411,6 +420,11 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
411
420
|
doc.converterId ?? null,
|
|
412
421
|
doc.converterVersion ?? null,
|
|
413
422
|
doc.languageHint ?? null,
|
|
423
|
+
doc.contentType ?? null,
|
|
424
|
+
doc.categories ? JSON.stringify(doc.categories) : null,
|
|
425
|
+
doc.author ?? null,
|
|
426
|
+
doc.frontmatterDate ?? null,
|
|
427
|
+
doc.dateFields ? JSON.stringify(doc.dateFields) : null,
|
|
414
428
|
doc.lastErrorCode ?? null,
|
|
415
429
|
doc.lastErrorMessage ?? null,
|
|
416
430
|
doc.lastErrorCode ? new Date().toISOString() : null,
|
|
@@ -529,12 +543,74 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
529
543
|
}
|
|
530
544
|
}
|
|
531
545
|
|
|
546
|
+
async getDocumentsByMirrorHashes(
|
|
547
|
+
mirrorHashes: string[],
|
|
548
|
+
options: {
|
|
549
|
+
collection?: string;
|
|
550
|
+
activeOnly?: boolean;
|
|
551
|
+
} = {}
|
|
552
|
+
): Promise<StoreResult<DocumentRow[]>> {
|
|
553
|
+
try {
|
|
554
|
+
if (mirrorHashes.length === 0) {
|
|
555
|
+
return ok([]);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const uniqueHashes = [
|
|
559
|
+
...new Set(mirrorHashes.filter((hash) => hash.trim().length > 0)),
|
|
560
|
+
];
|
|
561
|
+
if (uniqueHashes.length === 0) {
|
|
562
|
+
return ok([]);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const db = this.ensureOpen();
|
|
566
|
+
const rows: DbDocumentRow[] = [];
|
|
567
|
+
|
|
568
|
+
// SQLite SQLITE_LIMIT_VARIABLE_NUMBER defaults to 999.
|
|
569
|
+
// Reserve headroom for optional non-IN parameters.
|
|
570
|
+
const SQL_PARAM_LIMIT = options.collection ? 899 : 900;
|
|
571
|
+
|
|
572
|
+
for (let i = 0; i < uniqueHashes.length; i += SQL_PARAM_LIMIT) {
|
|
573
|
+
const batch = uniqueHashes.slice(i, i + SQL_PARAM_LIMIT);
|
|
574
|
+
const placeholders = batch.map(() => "?").join(",");
|
|
575
|
+
const clauses = [`mirror_hash IN (${placeholders})`];
|
|
576
|
+
const params: string[] = [...batch];
|
|
577
|
+
|
|
578
|
+
if (options.activeOnly ?? true) {
|
|
579
|
+
clauses.push("active = 1");
|
|
580
|
+
}
|
|
581
|
+
if (options.collection) {
|
|
582
|
+
clauses.push("collection = ?");
|
|
583
|
+
params.push(options.collection);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const sql = `SELECT * FROM documents WHERE ${clauses.join(" AND ")} ORDER BY id`;
|
|
587
|
+
rows.push(...db.query<DbDocumentRow, string[]>(sql).all(...params));
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return ok(rows.map(mapDocumentRow));
|
|
591
|
+
} catch (cause) {
|
|
592
|
+
return err(
|
|
593
|
+
"QUERY_FAILED",
|
|
594
|
+
cause instanceof Error
|
|
595
|
+
? cause.message
|
|
596
|
+
: "Failed to get documents by mirror hashes",
|
|
597
|
+
cause
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
532
602
|
async listDocumentsPaginated(options: {
|
|
533
603
|
collection?: string;
|
|
534
604
|
limit: number;
|
|
535
605
|
offset: number;
|
|
536
606
|
tagsAll?: string[];
|
|
537
607
|
tagsAny?: string[];
|
|
608
|
+
since?: string;
|
|
609
|
+
until?: string;
|
|
610
|
+
categories?: string[];
|
|
611
|
+
author?: string;
|
|
612
|
+
sortField?: string;
|
|
613
|
+
sortOrder?: "asc" | "desc";
|
|
538
614
|
}): Promise<StoreResult<{ documents: DocumentRow[]; total: number }>> {
|
|
539
615
|
try {
|
|
540
616
|
const db = this.ensureOpen();
|
|
@@ -549,6 +625,28 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
549
625
|
params.push(collection);
|
|
550
626
|
}
|
|
551
627
|
|
|
628
|
+
if (options.since) {
|
|
629
|
+
conditions.push("d.source_mtime >= ?");
|
|
630
|
+
params.push(options.since);
|
|
631
|
+
}
|
|
632
|
+
if (options.until) {
|
|
633
|
+
conditions.push("d.source_mtime <= ?");
|
|
634
|
+
params.push(options.until);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (options.categories && options.categories.length > 0) {
|
|
638
|
+
const placeholders = options.categories.map(() => "?").join(",");
|
|
639
|
+
conditions.push(
|
|
640
|
+
`(d.content_type IN (${placeholders}) OR EXISTS (SELECT 1 FROM json_each(COALESCE(d.categories, '[]')) jc WHERE jc.value IN (${placeholders})))`
|
|
641
|
+
);
|
|
642
|
+
params.push(...options.categories, ...options.categories);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (options.author) {
|
|
646
|
+
conditions.push("LOWER(COALESCE(d.author, '')) LIKE ?");
|
|
647
|
+
params.push(`%${options.author.toLowerCase()}%`);
|
|
648
|
+
}
|
|
649
|
+
|
|
552
650
|
// tagsAny: document has at least one of these tags (OR)
|
|
553
651
|
if (tagsAny && tagsAny.length > 0) {
|
|
554
652
|
const placeholders = tagsAny.map(() => "?").join(",");
|
|
@@ -570,6 +668,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
570
668
|
|
|
571
669
|
const whereClause = conditions.join(" AND ");
|
|
572
670
|
|
|
671
|
+
// Sort options
|
|
672
|
+
const sortOrder = options.sortOrder === "asc" ? "ASC" : "DESC";
|
|
673
|
+
const sortField = options.sortField ?? "modified";
|
|
674
|
+
const isSafeDateField = /^[a-z0-9_]+$/.test(sortField);
|
|
675
|
+
let orderClause = `d.source_mtime ${sortOrder}`;
|
|
676
|
+
if (sortField !== "modified" && isSafeDateField) {
|
|
677
|
+
orderClause = `COALESCE(json_extract(d.date_fields, '$."${sortField}"'), d.source_mtime) ${sortOrder}`;
|
|
678
|
+
}
|
|
679
|
+
|
|
573
680
|
// Get total count
|
|
574
681
|
// Use COUNT(DISTINCT d.id) to prevent duplicate counting when tag filters match multiple tags
|
|
575
682
|
const countSql = `SELECT COUNT(DISTINCT d.id) as count FROM documents d WHERE ${whereClause}`;
|
|
@@ -580,7 +687,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
580
687
|
|
|
581
688
|
// Get paginated documents
|
|
582
689
|
// Use DISTINCT to prevent duplicate rows when tag filters match multiple tags
|
|
583
|
-
const selectSql = `SELECT DISTINCT d.* FROM documents d WHERE ${whereClause} ORDER BY d.
|
|
690
|
+
const selectSql = `SELECT DISTINCT d.* FROM documents d WHERE ${whereClause} ORDER BY ${orderClause}, d.id ASC LIMIT ? OFFSET ?`;
|
|
584
691
|
const rows = db
|
|
585
692
|
.query<DbDocumentRow, (string | number)[]>(selectSql)
|
|
586
693
|
.all(...params, limit, offset);
|
|
@@ -595,6 +702,43 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
595
702
|
}
|
|
596
703
|
}
|
|
597
704
|
|
|
705
|
+
async getCollectionDateFields(
|
|
706
|
+
collection?: string
|
|
707
|
+
): Promise<StoreResult<string[]>> {
|
|
708
|
+
try {
|
|
709
|
+
const db = this.ensureOpen();
|
|
710
|
+
const conditions: string[] = [
|
|
711
|
+
"d.active = 1",
|
|
712
|
+
"d.date_fields IS NOT NULL",
|
|
713
|
+
];
|
|
714
|
+
const params: string[] = [];
|
|
715
|
+
|
|
716
|
+
if (collection) {
|
|
717
|
+
conditions.push("d.collection = ?");
|
|
718
|
+
params.push(collection);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const sql = `
|
|
722
|
+
SELECT DISTINCT jf.key as field
|
|
723
|
+
FROM documents d
|
|
724
|
+
JOIN json_each(COALESCE(d.date_fields, '{}')) jf
|
|
725
|
+
WHERE ${conditions.join(" AND ")}
|
|
726
|
+
ORDER BY jf.key ASC
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
const rows = db.query<{ field: string }, string[]>(sql).all(...params);
|
|
730
|
+
return ok(rows.map((r) => r.field));
|
|
731
|
+
} catch (cause) {
|
|
732
|
+
return err(
|
|
733
|
+
"QUERY_FAILED",
|
|
734
|
+
cause instanceof Error
|
|
735
|
+
? cause.message
|
|
736
|
+
: "Failed to list collection date fields",
|
|
737
|
+
cause
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
598
742
|
async markInactive(
|
|
599
743
|
collection: string,
|
|
600
744
|
relPaths: string[]
|
|
@@ -828,6 +972,26 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
828
972
|
}
|
|
829
973
|
}
|
|
830
974
|
|
|
975
|
+
if (options.since) {
|
|
976
|
+
tagConditions.push("d.source_mtime >= ?");
|
|
977
|
+
params.push(options.since);
|
|
978
|
+
}
|
|
979
|
+
if (options.until) {
|
|
980
|
+
tagConditions.push("d.source_mtime <= ?");
|
|
981
|
+
params.push(options.until);
|
|
982
|
+
}
|
|
983
|
+
if (options.categories && options.categories.length > 0) {
|
|
984
|
+
const placeholders = options.categories.map(() => "?").join(",");
|
|
985
|
+
tagConditions.push(
|
|
986
|
+
`(d.content_type IN (${placeholders}) OR EXISTS (SELECT 1 FROM json_each(COALESCE(d.categories, '[]')) jc WHERE jc.value IN (${placeholders})))`
|
|
987
|
+
);
|
|
988
|
+
params.push(...options.categories, ...options.categories);
|
|
989
|
+
}
|
|
990
|
+
if (options.author) {
|
|
991
|
+
tagConditions.push("LOWER(COALESCE(d.author, '')) LIKE ?");
|
|
992
|
+
params.push(`%${options.author.toLowerCase()}%`);
|
|
993
|
+
}
|
|
994
|
+
|
|
831
995
|
if (options.collection) {
|
|
832
996
|
params.push(options.collection);
|
|
833
997
|
}
|
|
@@ -850,6 +1014,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
850
1014
|
d.source_mime,
|
|
851
1015
|
d.source_ext,
|
|
852
1016
|
d.source_mtime,
|
|
1017
|
+
d.frontmatter_date,
|
|
853
1018
|
d.source_size,
|
|
854
1019
|
d.source_hash
|
|
855
1020
|
FROM documents_fts fts
|
|
@@ -874,6 +1039,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
874
1039
|
source_mime: string | null;
|
|
875
1040
|
source_ext: string | null;
|
|
876
1041
|
source_mtime: string | null;
|
|
1042
|
+
frontmatter_date: string | null;
|
|
877
1043
|
source_size: number | null;
|
|
878
1044
|
source_hash: string | null;
|
|
879
1045
|
}
|
|
@@ -894,6 +1060,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
894
1060
|
sourceMime: r.source_mime ?? undefined,
|
|
895
1061
|
sourceExt: r.source_ext ?? undefined,
|
|
896
1062
|
sourceMtime: r.source_mtime ?? undefined,
|
|
1063
|
+
frontmatterDate: r.frontmatter_date ?? undefined,
|
|
897
1064
|
sourceSize: r.source_size ?? undefined,
|
|
898
1065
|
sourceHash: r.source_hash ?? undefined,
|
|
899
1066
|
}))
|
|
@@ -2632,6 +2799,7 @@ interface DbDocumentRow {
|
|
|
2632
2799
|
source_ext: string;
|
|
2633
2800
|
source_size: number;
|
|
2634
2801
|
source_mtime: string;
|
|
2802
|
+
source_ctime: string | null;
|
|
2635
2803
|
docid: string;
|
|
2636
2804
|
uri: string;
|
|
2637
2805
|
title: string | null;
|
|
@@ -2639,6 +2807,12 @@ interface DbDocumentRow {
|
|
|
2639
2807
|
converter_id: string | null;
|
|
2640
2808
|
converter_version: string | null;
|
|
2641
2809
|
language_hint: string | null;
|
|
2810
|
+
content_type: string | null;
|
|
2811
|
+
categories: string | null;
|
|
2812
|
+
author: string | null;
|
|
2813
|
+
frontmatter_date: string | null;
|
|
2814
|
+
date_fields: string | null;
|
|
2815
|
+
indexed_at: string | null;
|
|
2642
2816
|
active: number;
|
|
2643
2817
|
ingest_version: number | null;
|
|
2644
2818
|
last_error_code: string | null;
|
|
@@ -2697,6 +2871,38 @@ function mapContextRow(row: DbContextRow): ContextRow {
|
|
|
2697
2871
|
}
|
|
2698
2872
|
|
|
2699
2873
|
function mapDocumentRow(row: DbDocumentRow): DocumentRow {
|
|
2874
|
+
let categories: string[] | null = null;
|
|
2875
|
+
if (row.categories) {
|
|
2876
|
+
try {
|
|
2877
|
+
const parsed = JSON.parse(row.categories);
|
|
2878
|
+
if (Array.isArray(parsed)) {
|
|
2879
|
+
categories = parsed.filter((v): v is string => typeof v === "string");
|
|
2880
|
+
}
|
|
2881
|
+
} catch {
|
|
2882
|
+
categories = null;
|
|
2883
|
+
}
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
let dateFields: Record<string, string> | null = null;
|
|
2887
|
+
if (row.date_fields) {
|
|
2888
|
+
try {
|
|
2889
|
+
const parsed = JSON.parse(row.date_fields);
|
|
2890
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
2891
|
+
const normalized: Record<string, string> = {};
|
|
2892
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
2893
|
+
if (typeof value === "string") {
|
|
2894
|
+
normalized[key] = value;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (Object.keys(normalized).length > 0) {
|
|
2898
|
+
dateFields = normalized;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
} catch {
|
|
2902
|
+
dateFields = null;
|
|
2903
|
+
}
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2700
2906
|
return {
|
|
2701
2907
|
id: row.id,
|
|
2702
2908
|
collection: row.collection,
|
|
@@ -2706,6 +2912,7 @@ function mapDocumentRow(row: DbDocumentRow): DocumentRow {
|
|
|
2706
2912
|
sourceExt: row.source_ext,
|
|
2707
2913
|
sourceSize: row.source_size,
|
|
2708
2914
|
sourceMtime: row.source_mtime,
|
|
2915
|
+
sourceCtime: row.source_ctime,
|
|
2709
2916
|
docid: row.docid,
|
|
2710
2917
|
uri: row.uri,
|
|
2711
2918
|
title: row.title,
|
|
@@ -2713,6 +2920,12 @@ function mapDocumentRow(row: DbDocumentRow): DocumentRow {
|
|
|
2713
2920
|
converterId: row.converter_id,
|
|
2714
2921
|
converterVersion: row.converter_version,
|
|
2715
2922
|
languageHint: row.language_hint,
|
|
2923
|
+
contentType: row.content_type,
|
|
2924
|
+
categories,
|
|
2925
|
+
author: row.author,
|
|
2926
|
+
frontmatterDate: row.frontmatter_date,
|
|
2927
|
+
dateFields,
|
|
2928
|
+
indexedAt: row.indexed_at,
|
|
2716
2929
|
active: row.active === 1,
|
|
2717
2930
|
ingestVersion: row.ingest_version,
|
|
2718
2931
|
lastErrorCode: row.last_error_code,
|
package/src/store/types.ts
CHANGED
|
@@ -95,6 +95,7 @@ export interface DocumentRow {
|
|
|
95
95
|
sourceExt: string;
|
|
96
96
|
sourceSize: number;
|
|
97
97
|
sourceMtime: string;
|
|
98
|
+
sourceCtime?: string | null;
|
|
98
99
|
|
|
99
100
|
// Derived identifiers
|
|
100
101
|
docid: string;
|
|
@@ -106,6 +107,12 @@ export interface DocumentRow {
|
|
|
106
107
|
converterId: string | null;
|
|
107
108
|
converterVersion: string | null;
|
|
108
109
|
languageHint: string | null;
|
|
110
|
+
contentType?: string | null;
|
|
111
|
+
categories?: string[] | null;
|
|
112
|
+
author?: string | null;
|
|
113
|
+
frontmatterDate?: string | null;
|
|
114
|
+
dateFields?: Record<string, string> | null;
|
|
115
|
+
indexedAt?: string | null;
|
|
109
116
|
|
|
110
117
|
// Status
|
|
111
118
|
active: boolean;
|
|
@@ -242,11 +249,17 @@ export interface DocumentInput {
|
|
|
242
249
|
sourceExt: string;
|
|
243
250
|
sourceSize: number;
|
|
244
251
|
sourceMtime: string;
|
|
252
|
+
sourceCtime?: string;
|
|
245
253
|
title?: string;
|
|
246
254
|
mirrorHash?: string;
|
|
247
255
|
converterId?: string;
|
|
248
256
|
converterVersion?: string;
|
|
249
257
|
languageHint?: string;
|
|
258
|
+
contentType?: string;
|
|
259
|
+
categories?: string[];
|
|
260
|
+
author?: string;
|
|
261
|
+
frontmatterDate?: string;
|
|
262
|
+
dateFields?: Record<string, string>;
|
|
250
263
|
lastErrorCode?: string;
|
|
251
264
|
lastErrorMessage?: string;
|
|
252
265
|
/** Ingest schema version for backfill detection */
|
|
@@ -303,6 +316,14 @@ export interface FtsSearchOptions {
|
|
|
303
316
|
tagsAny?: string[];
|
|
304
317
|
/** Filter to docs with ALL of these tags */
|
|
305
318
|
tagsAll?: string[];
|
|
319
|
+
/** Filter by modified time lower bound (ISO 8601) */
|
|
320
|
+
since?: string;
|
|
321
|
+
/** Filter by modified time upper bound (ISO 8601) */
|
|
322
|
+
until?: string;
|
|
323
|
+
/** Filter to docs matching ANY category */
|
|
324
|
+
categories?: string[];
|
|
325
|
+
/** Filter by author field (case-insensitive contains) */
|
|
326
|
+
author?: string;
|
|
306
327
|
}
|
|
307
328
|
|
|
308
329
|
/** Single FTS search result */
|
|
@@ -321,6 +342,7 @@ export interface FtsResult {
|
|
|
321
342
|
sourceMime?: string;
|
|
322
343
|
sourceExt?: string;
|
|
323
344
|
sourceMtime?: string;
|
|
345
|
+
frontmatterDate?: string;
|
|
324
346
|
sourceSize?: number;
|
|
325
347
|
sourceHash?: string;
|
|
326
348
|
}
|
|
@@ -599,6 +621,18 @@ export interface StorePort {
|
|
|
599
621
|
*/
|
|
600
622
|
listDocuments(collection?: string): Promise<StoreResult<DocumentRow[]>>;
|
|
601
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Fetch documents by mirror hashes in batch.
|
|
626
|
+
* Useful for retrieval pipelines to avoid full document scans.
|
|
627
|
+
*/
|
|
628
|
+
getDocumentsByMirrorHashes(
|
|
629
|
+
mirrorHashes: string[],
|
|
630
|
+
options?: {
|
|
631
|
+
collection?: string;
|
|
632
|
+
activeOnly?: boolean;
|
|
633
|
+
}
|
|
634
|
+
): Promise<StoreResult<DocumentRow[]>>;
|
|
635
|
+
|
|
602
636
|
/**
|
|
603
637
|
* List documents with pagination support.
|
|
604
638
|
* Returns documents and total count for efficient browsing.
|
|
@@ -611,6 +645,18 @@ export interface StorePort {
|
|
|
611
645
|
tagsAll?: string[];
|
|
612
646
|
/** Filter to docs having ANY of these tags (OR) */
|
|
613
647
|
tagsAny?: string[];
|
|
648
|
+
/** Filter by modified time lower bound (ISO 8601) */
|
|
649
|
+
since?: string;
|
|
650
|
+
/** Filter by modified time upper bound (ISO 8601) */
|
|
651
|
+
until?: string;
|
|
652
|
+
/** Filter to docs matching ANY category */
|
|
653
|
+
categories?: string[];
|
|
654
|
+
/** Filter by author field (case-insensitive contains) */
|
|
655
|
+
author?: string;
|
|
656
|
+
/** Sort field: "modified" or frontmatter date key */
|
|
657
|
+
sortField?: string;
|
|
658
|
+
/** Sort direction */
|
|
659
|
+
sortOrder?: "asc" | "desc";
|
|
614
660
|
}): Promise<StoreResult<{ documents: DocumentRow[]; total: number }>>;
|
|
615
661
|
|
|
616
662
|
/**
|