@gmickel/gno 1.27.1 → 1.28.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 +2 -2
- package/assets/skill/SKILL.md +26 -1
- package/browser-extension/artifacts/{gno-browser-clipper-v1.27.1.zip → gno-browser-clipper-v1.28.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.28.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +2 -2
- package/spec/cli.md +41 -4
- package/spec/db/schema.sql +12 -0
- package/spec/mcp.md +5 -0
- package/spec/output-schemas/ask.schema.json +125 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +94 -1
- package/spec/output-schemas/get.schema.json +94 -0
- package/spec/output-schemas/mcp-job-status.schema.json +28 -0
- package/spec/output-schemas/multi-get.schema.json +128 -0
- package/spec/output-schemas/record-import.schema.json +193 -0
- package/spec/output-schemas/search-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +125 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- package/src/cli/commands/get.ts +12 -2
- package/src/cli/commands/index-cmd.ts +13 -0
- package/src/cli/commands/multi-get.ts +9 -2
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/program.ts +4 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +16 -0
- package/src/converters/adapters/browser-export/adapter.ts +199 -0
- package/src/converters/adapters/browser-export/formats.ts +358 -0
- package/src/converters/adapters/email/adapter.ts +429 -0
- package/src/converters/adapters/email/html.ts +162 -0
- package/src/converters/adapters/email/mime.ts +454 -0
- package/src/converters/adapters/email/parameters.ts +77 -0
- package/src/converters/adapters/ical/adapter.ts +475 -0
- package/src/converters/adapters/ical/recurrence.ts +186 -0
- package/src/converters/adapters/jsonl/adapter.ts +238 -0
- package/src/converters/adapters/jsonl/config.ts +105 -0
- package/src/converters/adapters/shared/html-text.ts +165 -0
- package/src/converters/adapters/shared/record-utils.ts +79 -0
- package/src/converters/adapters/shared/utf8-lines.ts +141 -0
- package/src/converters/adapters/transcript/adapter.ts +295 -0
- package/src/converters/adapters/transcript/json.ts +184 -0
- package/src/converters/adapters/transcript/model.ts +171 -0
- package/src/converters/adapters/transcript/text.ts +58 -0
- package/src/converters/adapters/transcript/timed.ts +152 -0
- package/src/converters/index.ts +11 -1
- package/src/converters/mime.ts +8 -0
- package/src/converters/pipeline.ts +15 -1
- package/src/converters/registry.ts +37 -1
- package/src/converters/types.ts +136 -0
- package/src/core/context-capsule-schema.ts +87 -0
- package/src/core/context-capsule.ts +20 -0
- package/src/core/context-evidence.ts +7 -1
- package/src/core/document-capabilities.ts +12 -0
- package/src/core/project-profile-apply-state.ts +5 -0
- package/src/core/project-profile.ts +4 -0
- package/src/core/record-metadata.ts +49 -0
- package/src/ingestion/record-adapter-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +437 -0
- package/src/ingestion/record-container.ts +688 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +228 -36
- package/src/ingestion/types.ts +69 -1
- package/src/ingestion/walker.ts +31 -11
- package/src/mcp/tools/get.ts +10 -2
- package/src/mcp/tools/multi-get.ts +9 -2
- package/src/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +7 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +11 -4
- package/src/pipeline/types.ts +3 -0
- package/src/pipeline/vsearch.ts +26 -8
- package/src/sdk/documents.ts +13 -5
- package/src/serve/browse-tree.ts +4 -2
- package/src/serve/routes/api.ts +63 -65
- package/src/store/migrations/022-record-export-lineage.ts +42 -0
- package/src/store/migrations/index.ts +2 -0
- package/src/store/sqlite/adapter.ts +114 -7
- package/src/store/types.ts +40 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
package/src/serve/routes/api.ts
CHANGED
|
@@ -93,6 +93,7 @@ import {
|
|
|
93
93
|
ProjectAffinityInputError,
|
|
94
94
|
resolveRemoteProjectAffinity,
|
|
95
95
|
} from "../../core/project-affinity-surface";
|
|
96
|
+
import { projectRecordEvidenceMetadata } from "../../core/record-metadata";
|
|
96
97
|
import {
|
|
97
98
|
retrievalTraceFilters,
|
|
98
99
|
startRetrievalTraceRequest,
|
|
@@ -607,6 +608,7 @@ function readRequestedUriFromUrl(req: Request): string | undefined {
|
|
|
607
608
|
|
|
608
609
|
interface SourceMeta {
|
|
609
610
|
absPath?: string;
|
|
611
|
+
relPath: string;
|
|
610
612
|
mime: string;
|
|
611
613
|
ext: string;
|
|
612
614
|
modifiedAt?: string;
|
|
@@ -625,7 +627,11 @@ function getCollectionByName(
|
|
|
625
627
|
|
|
626
628
|
async function resolveAbsoluteDocPath(
|
|
627
629
|
collections: Config["collections"],
|
|
628
|
-
doc: {
|
|
630
|
+
doc: {
|
|
631
|
+
collection: string;
|
|
632
|
+
relPath: string;
|
|
633
|
+
recordSourcePath?: string | null;
|
|
634
|
+
}
|
|
629
635
|
): Promise<{
|
|
630
636
|
collection: Config["collections"][number];
|
|
631
637
|
fullPath: string;
|
|
@@ -638,7 +644,7 @@ async function resolveAbsoluteDocPath(
|
|
|
638
644
|
const nodePath = await import("node:path"); // no bun equivalent
|
|
639
645
|
let safeRelPath: string;
|
|
640
646
|
try {
|
|
641
|
-
safeRelPath = validateRelPath(doc.relPath);
|
|
647
|
+
safeRelPath = validateRelPath(doc.recordSourcePath ?? doc.relPath);
|
|
642
648
|
} catch {
|
|
643
649
|
return null;
|
|
644
650
|
}
|
|
@@ -763,6 +769,7 @@ async function buildSourceMeta(
|
|
|
763
769
|
doc: {
|
|
764
770
|
collection: string;
|
|
765
771
|
relPath: string;
|
|
772
|
+
recordSourcePath?: string | null;
|
|
766
773
|
sourceMime: string;
|
|
767
774
|
sourceExt: string;
|
|
768
775
|
sourceMtime?: string | null;
|
|
@@ -770,9 +777,14 @@ async function buildSourceMeta(
|
|
|
770
777
|
sourceHash?: string;
|
|
771
778
|
}
|
|
772
779
|
): Promise<SourceMeta> {
|
|
773
|
-
const
|
|
780
|
+
const relPath = doc.recordSourcePath ?? doc.relPath;
|
|
781
|
+
const resolved = await resolveAbsoluteDocPath(collections, {
|
|
782
|
+
collection: doc.collection,
|
|
783
|
+
relPath,
|
|
784
|
+
});
|
|
774
785
|
return {
|
|
775
786
|
absPath: resolved?.fullPath,
|
|
787
|
+
relPath,
|
|
776
788
|
mime: doc.sourceMime,
|
|
777
789
|
ext: doc.sourceExt,
|
|
778
790
|
modifiedAt: doc.sourceMtime ?? undefined,
|
|
@@ -781,6 +793,20 @@ async function buildSourceMeta(
|
|
|
781
793
|
};
|
|
782
794
|
}
|
|
783
795
|
|
|
796
|
+
const capabilitiesForDocument = (
|
|
797
|
+
doc: Pick<
|
|
798
|
+
DocumentRow,
|
|
799
|
+
"sourceExt" | "sourceMime" | "mirrorHash" | "recordKey"
|
|
800
|
+
>,
|
|
801
|
+
contentAvailable = doc.mirrorHash !== null
|
|
802
|
+
) =>
|
|
803
|
+
getDocumentCapabilities({
|
|
804
|
+
sourceExt: doc.sourceExt,
|
|
805
|
+
sourceMime: doc.sourceMime,
|
|
806
|
+
contentAvailable,
|
|
807
|
+
recordKey: doc.recordKey,
|
|
808
|
+
});
|
|
809
|
+
|
|
784
810
|
async function resolveDocumentReference(
|
|
785
811
|
store: Pick<SqliteAdapter, "getDocumentByDocid" | "getDocumentByUri">,
|
|
786
812
|
docId: string,
|
|
@@ -1613,7 +1639,7 @@ export async function handleDocs(
|
|
|
1613
1639
|
uri: doc.uri,
|
|
1614
1640
|
title: doc.title,
|
|
1615
1641
|
collection: doc.collection,
|
|
1616
|
-
relPath: doc.relPath,
|
|
1642
|
+
relPath: doc.recordSourcePath ?? doc.relPath,
|
|
1617
1643
|
sourceExt: doc.sourceExt,
|
|
1618
1644
|
sourceMime: doc.sourceMime,
|
|
1619
1645
|
updatedAt: doc.updatedAt,
|
|
@@ -1790,12 +1816,10 @@ export async function handleDoc(
|
|
|
1790
1816
|
}
|
|
1791
1817
|
|
|
1792
1818
|
const contentAvailable = content !== null;
|
|
1793
|
-
const capabilities =
|
|
1794
|
-
sourceExt: doc.sourceExt,
|
|
1795
|
-
sourceMime: doc.sourceMime,
|
|
1796
|
-
contentAvailable,
|
|
1797
|
-
});
|
|
1819
|
+
const capabilities = capabilitiesForDocument(doc, contentAvailable);
|
|
1798
1820
|
const source = await buildSourceMeta(config.collections, doc);
|
|
1821
|
+
const record = projectRecordEvidenceMetadata(doc);
|
|
1822
|
+
const relPath = doc.recordSourcePath ?? doc.relPath;
|
|
1799
1823
|
|
|
1800
1824
|
const responseData = {
|
|
1801
1825
|
docid: doc.docid,
|
|
@@ -1804,9 +1828,10 @@ export async function handleDoc(
|
|
|
1804
1828
|
content,
|
|
1805
1829
|
contentAvailable,
|
|
1806
1830
|
collection: doc.collection,
|
|
1807
|
-
relPath
|
|
1831
|
+
relPath,
|
|
1808
1832
|
tags,
|
|
1809
1833
|
source,
|
|
1834
|
+
record,
|
|
1810
1835
|
capabilities,
|
|
1811
1836
|
};
|
|
1812
1837
|
const continuationTraceId = request
|
|
@@ -2079,6 +2104,15 @@ export async function handleRenameDoc(
|
|
|
2079
2104
|
}
|
|
2080
2105
|
|
|
2081
2106
|
const doc = docResult.value;
|
|
2107
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2108
|
+
if (!capabilities.editable) {
|
|
2109
|
+
return errorResponse(
|
|
2110
|
+
"READ_ONLY",
|
|
2111
|
+
capabilities.reason ??
|
|
2112
|
+
"This document cannot be renamed in place from GNO.",
|
|
2113
|
+
409
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2082
2116
|
const resolvedDocPath = await resolveAbsoluteDocPath(
|
|
2083
2117
|
ctxHolder.config.collections,
|
|
2084
2118
|
doc
|
|
@@ -2096,20 +2130,6 @@ export async function handleRenameDoc(
|
|
|
2096
2130
|
return errorResponse("FILE_NOT_FOUND", "Source file no longer exists", 404);
|
|
2097
2131
|
}
|
|
2098
2132
|
|
|
2099
|
-
const capabilities = getDocumentCapabilities({
|
|
2100
|
-
sourceExt: doc.sourceExt,
|
|
2101
|
-
sourceMime: doc.sourceMime,
|
|
2102
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2103
|
-
});
|
|
2104
|
-
if (!capabilities.editable) {
|
|
2105
|
-
return errorResponse(
|
|
2106
|
-
"READ_ONLY",
|
|
2107
|
-
capabilities.reason ??
|
|
2108
|
-
"This document cannot be renamed in place from GNO.",
|
|
2109
|
-
409
|
|
2110
|
-
);
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
2133
|
const nodePath = await import("node:path"); // no bun equivalent
|
|
2114
2134
|
const directory = nodePath.dirname(doc.relPath);
|
|
2115
2135
|
const currentExt = nodePath.extname(doc.relPath);
|
|
@@ -2206,11 +2226,7 @@ export async function handleRefactorPlan(
|
|
|
2206
2226
|
}
|
|
2207
2227
|
|
|
2208
2228
|
const doc = docResult.value;
|
|
2209
|
-
const capabilities =
|
|
2210
|
-
sourceExt: doc.sourceExt,
|
|
2211
|
-
sourceMime: doc.sourceMime,
|
|
2212
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2213
|
-
});
|
|
2229
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2214
2230
|
if (!capabilities.editable) {
|
|
2215
2231
|
return errorResponse(
|
|
2216
2232
|
"READ_ONLY",
|
|
@@ -2333,11 +2349,7 @@ export async function handleTrashDoc(
|
|
|
2333
2349
|
}
|
|
2334
2350
|
const { collection, fullPath } = resolvedDocPath;
|
|
2335
2351
|
|
|
2336
|
-
const capabilities =
|
|
2337
|
-
sourceExt: doc.sourceExt,
|
|
2338
|
-
sourceMime: doc.sourceMime,
|
|
2339
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2340
|
-
});
|
|
2352
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2341
2353
|
if (!capabilities.editable) {
|
|
2342
2354
|
return errorResponse(
|
|
2343
2355
|
"READ_ONLY",
|
|
@@ -2432,11 +2444,7 @@ export async function handleMoveDoc(
|
|
|
2432
2444
|
return errorResponse("NOT_FOUND", "Document not found", 404);
|
|
2433
2445
|
}
|
|
2434
2446
|
const doc = docResult.value;
|
|
2435
|
-
const capabilities =
|
|
2436
|
-
sourceExt: doc.sourceExt,
|
|
2437
|
-
sourceMime: doc.sourceMime,
|
|
2438
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2439
|
-
});
|
|
2447
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2440
2448
|
if (!capabilities.editable) {
|
|
2441
2449
|
return errorResponse(
|
|
2442
2450
|
"READ_ONLY",
|
|
@@ -2563,11 +2571,7 @@ export async function handleDuplicateDoc(
|
|
|
2563
2571
|
return errorResponse("NOT_FOUND", "Document not found", 404);
|
|
2564
2572
|
}
|
|
2565
2573
|
const doc = docResult.value;
|
|
2566
|
-
const capabilities =
|
|
2567
|
-
sourceExt: doc.sourceExt,
|
|
2568
|
-
sourceMime: doc.sourceMime,
|
|
2569
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2570
|
-
});
|
|
2574
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2571
2575
|
if (!capabilities.editable) {
|
|
2572
2576
|
return errorResponse(
|
|
2573
2577
|
"READ_ONLY",
|
|
@@ -2837,6 +2841,15 @@ export async function handleUpdateDoc(
|
|
|
2837
2841
|
}
|
|
2838
2842
|
|
|
2839
2843
|
const doc = docResult.value;
|
|
2844
|
+
const capabilities = capabilitiesForDocument(doc);
|
|
2845
|
+
if (hasContent && !capabilities.editable) {
|
|
2846
|
+
return errorResponse(
|
|
2847
|
+
"READ_ONLY",
|
|
2848
|
+
capabilities.reason ??
|
|
2849
|
+
"This document cannot be edited in place. Create an editable markdown copy instead.",
|
|
2850
|
+
409
|
|
2851
|
+
);
|
|
2852
|
+
}
|
|
2840
2853
|
|
|
2841
2854
|
const resolvedDocPath = await resolveAbsoluteDocPath(
|
|
2842
2855
|
ctxHolder.config.collections,
|
|
@@ -2886,20 +2899,6 @@ export async function handleUpdateDoc(
|
|
|
2886
2899
|
}
|
|
2887
2900
|
}
|
|
2888
2901
|
|
|
2889
|
-
const capabilities = getDocumentCapabilities({
|
|
2890
|
-
sourceExt: doc.sourceExt,
|
|
2891
|
-
sourceMime: doc.sourceMime,
|
|
2892
|
-
contentAvailable: doc.mirrorHash !== null,
|
|
2893
|
-
});
|
|
2894
|
-
if (hasContent && !capabilities.editable) {
|
|
2895
|
-
return errorResponse(
|
|
2896
|
-
"READ_ONLY",
|
|
2897
|
-
capabilities.reason ??
|
|
2898
|
-
"This document cannot be edited in place. Create an editable markdown copy instead.",
|
|
2899
|
-
409
|
|
2900
|
-
);
|
|
2901
|
-
}
|
|
2902
|
-
|
|
2903
2902
|
let writeBack: "applied" | "skipped_unsupported" | undefined;
|
|
2904
2903
|
|
|
2905
2904
|
try {
|
|
@@ -3044,11 +3043,7 @@ export async function handleCreateEditableCopy(
|
|
|
3044
3043
|
|
|
3045
3044
|
const doc = docResult.value;
|
|
3046
3045
|
const contentAvailable = doc.mirrorHash !== null;
|
|
3047
|
-
const capabilities =
|
|
3048
|
-
sourceExt: doc.sourceExt,
|
|
3049
|
-
sourceMime: doc.sourceMime,
|
|
3050
|
-
contentAvailable,
|
|
3051
|
-
});
|
|
3046
|
+
const capabilities = capabilitiesForDocument(doc, contentAvailable);
|
|
3052
3047
|
if (capabilities.editable) {
|
|
3053
3048
|
return errorResponse(
|
|
3054
3049
|
"VALIDATION",
|
|
@@ -3094,12 +3089,15 @@ export async function handleCreateEditableCopy(
|
|
|
3094
3089
|
const existingRelPaths = listResult.ok
|
|
3095
3090
|
? listResult.value.map((entry) => entry.relPath)
|
|
3096
3091
|
: [];
|
|
3097
|
-
relPath = deriveEditableCopyRelPath(
|
|
3092
|
+
relPath = deriveEditableCopyRelPath(
|
|
3093
|
+
doc.recordSourcePath ?? doc.relPath,
|
|
3094
|
+
existingRelPaths
|
|
3095
|
+
);
|
|
3098
3096
|
}
|
|
3099
3097
|
|
|
3100
3098
|
const title =
|
|
3101
3099
|
doc.title ??
|
|
3102
|
-
doc.relPath
|
|
3100
|
+
(doc.recordSourcePath ?? doc.relPath)
|
|
3103
3101
|
.split("/")
|
|
3104
3102
|
.pop()
|
|
3105
3103
|
?.replace(/\.[^.]+$/, "") ??
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** Persist bounded logical-record lineage for file/export adapters. */
|
|
2
|
+
import type { Migration } from "./runner";
|
|
3
|
+
|
|
4
|
+
export const migration: Migration = {
|
|
5
|
+
version: 22,
|
|
6
|
+
name: "record_export_lineage",
|
|
7
|
+
|
|
8
|
+
up(db): void {
|
|
9
|
+
const columns = new Set(
|
|
10
|
+
db
|
|
11
|
+
.query<{ name: string }, []>("PRAGMA table_info(documents)")
|
|
12
|
+
.all()
|
|
13
|
+
.map((row) => row.name)
|
|
14
|
+
);
|
|
15
|
+
const additions = [
|
|
16
|
+
["record_key", "TEXT"],
|
|
17
|
+
["record_source_path", "TEXT"],
|
|
18
|
+
["record_source_locator", "TEXT"],
|
|
19
|
+
["record_metadata", "TEXT"],
|
|
20
|
+
["record_anchors", "TEXT"],
|
|
21
|
+
["record_adapter_fingerprint", "TEXT"],
|
|
22
|
+
] as const;
|
|
23
|
+
for (const [name, type] of additions) {
|
|
24
|
+
if (!columns.has(name)) {
|
|
25
|
+
db.exec(`ALTER TABLE documents ADD COLUMN ${name} ${type}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
db.exec(`
|
|
29
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_documents_record_key
|
|
30
|
+
ON documents(collection, record_source_path, record_key)
|
|
31
|
+
WHERE record_source_path IS NOT NULL AND record_key IS NOT NULL;
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_documents_record_source_path
|
|
33
|
+
ON documents(collection, record_source_path)
|
|
34
|
+
WHERE record_source_path IS NOT NULL;
|
|
35
|
+
`);
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
down(db): void {
|
|
39
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_record_key");
|
|
40
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_record_source_path");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
@@ -35,6 +35,7 @@ import { migration as m018 } from "./018-saved-capsule-registration-epoch";
|
|
|
35
35
|
import { migration as m019 } from "./019-saved-capsule-registration-generation";
|
|
36
36
|
import { migration as m020 } from "./020-browser-clipper-security";
|
|
37
37
|
import { migration as m021 } from "./021-multi-context-identity";
|
|
38
|
+
import { migration as m022 } from "./022-record-export-lineage";
|
|
38
39
|
|
|
39
40
|
/** All migrations in order */
|
|
40
41
|
export const migrations = [
|
|
@@ -59,4 +60,5 @@ export const migrations = [
|
|
|
59
60
|
m019,
|
|
60
61
|
m020,
|
|
61
62
|
m021,
|
|
63
|
+
m022,
|
|
62
64
|
];
|
|
@@ -1067,10 +1067,13 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1067
1067
|
collection, rel_path, source_hash, source_mime, source_ext,
|
|
1068
1068
|
source_size, source_mtime, source_ctime, docid, uri, title, mirror_hash,
|
|
1069
1069
|
converter_id, converter_version, language_hint, content_type, categories,
|
|
1070
|
-
content_type_source, author, frontmatter_date, date_fields,
|
|
1070
|
+
content_type_source, author, frontmatter_date, date_fields,
|
|
1071
|
+
record_key, record_source_path, record_source_locator, record_metadata, record_anchors,
|
|
1072
|
+
record_adapter_fingerprint,
|
|
1073
|
+
content_type_rules_fingerprint,
|
|
1071
1074
|
active, indexed_at, last_error_code, last_error_message, last_error_at,
|
|
1072
1075
|
ingest_version, updated_at
|
|
1073
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
|
|
1076
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, ?, ?, ?, datetime('now'))
|
|
1074
1077
|
ON CONFLICT(collection, rel_path) DO UPDATE SET
|
|
1075
1078
|
source_hash = excluded.source_hash,
|
|
1076
1079
|
source_mime = excluded.source_mime,
|
|
@@ -1091,6 +1094,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1091
1094
|
author = excluded.author,
|
|
1092
1095
|
frontmatter_date = excluded.frontmatter_date,
|
|
1093
1096
|
date_fields = excluded.date_fields,
|
|
1097
|
+
record_key = excluded.record_key,
|
|
1098
|
+
record_source_path = excluded.record_source_path,
|
|
1099
|
+
record_source_locator = excluded.record_source_locator,
|
|
1100
|
+
record_metadata = excluded.record_metadata,
|
|
1101
|
+
record_anchors = excluded.record_anchors,
|
|
1102
|
+
record_adapter_fingerprint = excluded.record_adapter_fingerprint,
|
|
1094
1103
|
content_type_rules_fingerprint = excluded.content_type_rules_fingerprint,
|
|
1095
1104
|
active = 1,
|
|
1096
1105
|
indexed_at = datetime('now'),
|
|
@@ -1122,6 +1131,12 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1122
1131
|
doc.author ?? null,
|
|
1123
1132
|
doc.frontmatterDate ?? null,
|
|
1124
1133
|
doc.dateFields ? JSON.stringify(doc.dateFields) : null,
|
|
1134
|
+
doc.recordKey ?? null,
|
|
1135
|
+
doc.recordSourcePath ?? null,
|
|
1136
|
+
doc.recordSourceLocator ?? null,
|
|
1137
|
+
doc.recordMetadata ? JSON.stringify(doc.recordMetadata) : null,
|
|
1138
|
+
doc.recordAnchors ? JSON.stringify(doc.recordAnchors) : null,
|
|
1139
|
+
doc.recordAdapterFingerprint ?? null,
|
|
1125
1140
|
doc.contentTypeRulesFingerprint ?? null,
|
|
1126
1141
|
doc.lastErrorCode ?? null,
|
|
1127
1142
|
doc.lastErrorMessage ?? null,
|
|
@@ -1366,6 +1381,32 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1366
1381
|
}
|
|
1367
1382
|
}
|
|
1368
1383
|
|
|
1384
|
+
async listRecordDocuments(
|
|
1385
|
+
collection: string,
|
|
1386
|
+
sourcePath: string
|
|
1387
|
+
): Promise<StoreResult<DocumentRow[]>> {
|
|
1388
|
+
try {
|
|
1389
|
+
const db = this.ensureOpen();
|
|
1390
|
+
const rows = db
|
|
1391
|
+
.query<DbDocumentRow, [string, string]>(
|
|
1392
|
+
`SELECT * FROM documents
|
|
1393
|
+
WHERE collection = ? AND record_source_path = ?
|
|
1394
|
+
ORDER BY rel_path ASC, id ASC`
|
|
1395
|
+
)
|
|
1396
|
+
.all(collection, sourcePath);
|
|
1397
|
+
|
|
1398
|
+
return ok(rows.map(mapDocumentRow));
|
|
1399
|
+
} catch (cause) {
|
|
1400
|
+
return err(
|
|
1401
|
+
"QUERY_FAILED",
|
|
1402
|
+
cause instanceof Error
|
|
1403
|
+
? cause.message
|
|
1404
|
+
: "Failed to list record documents",
|
|
1405
|
+
cause
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1369
1410
|
async listActiveDocumentsForBrowse(
|
|
1370
1411
|
collection?: string
|
|
1371
1412
|
): Promise<StoreResult<DocumentRow[]>> {
|
|
@@ -1547,17 +1588,18 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
1547
1588
|
const normalizedPathPrefix = pathPrefix
|
|
1548
1589
|
?.replaceAll("\\", "/")
|
|
1549
1590
|
.replace(/^\/+|\/+$/g, "");
|
|
1591
|
+
const browsePathSql = "COALESCE(d.record_source_path, d.rel_path)";
|
|
1550
1592
|
|
|
1551
1593
|
if (normalizedPathPrefix) {
|
|
1552
|
-
conditions.push(
|
|
1594
|
+
conditions.push(`${browsePathSql} LIKE ?`);
|
|
1553
1595
|
params.push(`${normalizedPathPrefix}/%`);
|
|
1554
1596
|
|
|
1555
1597
|
if (directChildrenOnly) {
|
|
1556
|
-
conditions.push(
|
|
1598
|
+
conditions.push(`substr(${browsePathSql}, ?) NOT LIKE '%/%'`);
|
|
1557
1599
|
params.push(normalizedPathPrefix.length + 2);
|
|
1558
1600
|
}
|
|
1559
1601
|
} else if (directChildrenOnly) {
|
|
1560
|
-
conditions.push(
|
|
1602
|
+
conditions.push(`${browsePathSql} NOT LIKE '%/%'`);
|
|
1561
1603
|
}
|
|
1562
1604
|
|
|
1563
1605
|
if (options.since) {
|
|
@@ -2162,7 +2204,7 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2162
2204
|
${options.collection ? "AND collection = ?" : ""}
|
|
2163
2205
|
${
|
|
2164
2206
|
options.relPathPrefix !== undefined
|
|
2165
|
-
? "AND (rel_path = ? OR substr(rel_path, 1, length(?) + 1) = ? || '/')"
|
|
2207
|
+
? "AND (COALESCE(NULLIF(record_source_path, ''), rel_path) = ? OR substr(COALESCE(NULLIF(record_source_path, ''), rel_path), 1, length(?) + 1) = ? || '/')"
|
|
2166
2208
|
: ""
|
|
2167
2209
|
}
|
|
2168
2210
|
)
|
|
@@ -2187,7 +2229,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2187
2229
|
d.source_hash,
|
|
2188
2230
|
d.content_type,
|
|
2189
2231
|
d.content_type_source,
|
|
2190
|
-
d.categories
|
|
2232
|
+
d.categories,
|
|
2233
|
+
d.converter_id,
|
|
2234
|
+
d.converter_version,
|
|
2235
|
+
d.record_key,
|
|
2236
|
+
d.record_source_path,
|
|
2237
|
+
d.record_source_locator,
|
|
2238
|
+
d.record_metadata,
|
|
2239
|
+
d.record_anchors,
|
|
2240
|
+
d.record_adapter_fingerprint
|
|
2191
2241
|
FROM fts_matches fm
|
|
2192
2242
|
JOIN documents d ON d.id = fm.rowid AND d.active = 1
|
|
2193
2243
|
WHERE 1 = 1
|
|
@@ -2215,6 +2265,14 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2215
2265
|
content_type: string | null;
|
|
2216
2266
|
content_type_source: string | null;
|
|
2217
2267
|
categories: string | null;
|
|
2268
|
+
converter_id: string | null;
|
|
2269
|
+
converter_version: string | null;
|
|
2270
|
+
record_key: string | null;
|
|
2271
|
+
record_source_path: string | null;
|
|
2272
|
+
record_source_locator: string | null;
|
|
2273
|
+
record_metadata: string | null;
|
|
2274
|
+
record_anchors: string | null;
|
|
2275
|
+
record_adapter_fingerprint: string | null;
|
|
2218
2276
|
}
|
|
2219
2277
|
|
|
2220
2278
|
const queryParams = [
|
|
@@ -2254,6 +2312,15 @@ export class SqliteAdapter implements StorePort, SqliteDbProvider {
|
|
|
2254
2312
|
contentType: r.content_type ?? undefined,
|
|
2255
2313
|
contentTypeSource: r.content_type_source ?? undefined,
|
|
2256
2314
|
categories: parseCategoriesJson(r.categories) ?? undefined,
|
|
2315
|
+
converterId: r.converter_id ?? undefined,
|
|
2316
|
+
converterVersion: r.converter_version ?? undefined,
|
|
2317
|
+
recordKey: r.record_key ?? undefined,
|
|
2318
|
+
recordSourcePath: r.record_source_path ?? undefined,
|
|
2319
|
+
recordSourceLocator: r.record_source_locator ?? undefined,
|
|
2320
|
+
recordMetadata:
|
|
2321
|
+
parseRecordMetadataJson(r.record_metadata) ?? undefined,
|
|
2322
|
+
recordAnchors: parseRecordAnchorsJson(r.record_anchors) ?? undefined,
|
|
2323
|
+
recordAdapterFingerprint: r.record_adapter_fingerprint ?? undefined,
|
|
2257
2324
|
}))
|
|
2258
2325
|
);
|
|
2259
2326
|
} catch (cause) {
|
|
@@ -5098,6 +5165,12 @@ interface DbDocumentRow {
|
|
|
5098
5165
|
author: string | null;
|
|
5099
5166
|
frontmatter_date: string | null;
|
|
5100
5167
|
date_fields: string | null;
|
|
5168
|
+
record_key: string | null;
|
|
5169
|
+
record_source_path: string | null;
|
|
5170
|
+
record_source_locator: string | null;
|
|
5171
|
+
record_metadata: string | null;
|
|
5172
|
+
record_anchors: string | null;
|
|
5173
|
+
record_adapter_fingerprint: string | null;
|
|
5101
5174
|
content_type_rules_fingerprint: string | null;
|
|
5102
5175
|
indexed_at: string | null;
|
|
5103
5176
|
active: number;
|
|
@@ -5185,6 +5258,34 @@ function parseCategoriesJson(raw: string | null): string[] | null {
|
|
|
5185
5258
|
return null;
|
|
5186
5259
|
}
|
|
5187
5260
|
|
|
5261
|
+
function parseRecordMetadataJson(
|
|
5262
|
+
raw: string | null
|
|
5263
|
+
): DocumentRow["recordMetadata"] {
|
|
5264
|
+
if (!raw) return null;
|
|
5265
|
+
try {
|
|
5266
|
+
const parsed: unknown = JSON.parse(raw);
|
|
5267
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
5268
|
+
? (parsed as NonNullable<DocumentRow["recordMetadata"]>)
|
|
5269
|
+
: null;
|
|
5270
|
+
} catch {
|
|
5271
|
+
return null;
|
|
5272
|
+
}
|
|
5273
|
+
}
|
|
5274
|
+
|
|
5275
|
+
function parseRecordAnchorsJson(
|
|
5276
|
+
raw: string | null
|
|
5277
|
+
): DocumentRow["recordAnchors"] {
|
|
5278
|
+
if (!raw) return null;
|
|
5279
|
+
try {
|
|
5280
|
+
const parsed: unknown = JSON.parse(raw);
|
|
5281
|
+
return Array.isArray(parsed)
|
|
5282
|
+
? (parsed as NonNullable<DocumentRow["recordAnchors"]>)
|
|
5283
|
+
: null;
|
|
5284
|
+
} catch {
|
|
5285
|
+
return null;
|
|
5286
|
+
}
|
|
5287
|
+
}
|
|
5288
|
+
|
|
5188
5289
|
function mapDocumentRow(row: DbDocumentRow): DocumentRow {
|
|
5189
5290
|
const categories = parseCategoriesJson(row.categories);
|
|
5190
5291
|
let dateFields: Record<string, string> | null = null;
|
|
@@ -5230,6 +5331,12 @@ function mapDocumentRow(row: DbDocumentRow): DocumentRow {
|
|
|
5230
5331
|
author: row.author,
|
|
5231
5332
|
frontmatterDate: row.frontmatter_date,
|
|
5232
5333
|
dateFields,
|
|
5334
|
+
recordKey: row.record_key,
|
|
5335
|
+
recordSourcePath: row.record_source_path,
|
|
5336
|
+
recordSourceLocator: row.record_source_locator,
|
|
5337
|
+
recordMetadata: parseRecordMetadataJson(row.record_metadata),
|
|
5338
|
+
recordAnchors: parseRecordAnchorsJson(row.record_anchors),
|
|
5339
|
+
recordAdapterFingerprint: row.record_adapter_fingerprint,
|
|
5233
5340
|
indexedAt: row.indexed_at,
|
|
5234
5341
|
active: row.active === 1,
|
|
5235
5342
|
ingestVersion: row.ingest_version,
|
package/src/store/types.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Collection, Context, FtsTokenizer } from "../config/types";
|
|
9
|
+
import type { RecordAnchor, RecordMetadata } from "../converters/types";
|
|
9
10
|
|
|
10
11
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
12
|
// Error Types
|
|
@@ -113,6 +114,12 @@ export interface DocumentRow {
|
|
|
113
114
|
author?: string | null;
|
|
114
115
|
frontmatterDate?: string | null;
|
|
115
116
|
dateFields?: Record<string, string> | null;
|
|
117
|
+
recordKey?: string | null;
|
|
118
|
+
recordSourcePath?: string | null;
|
|
119
|
+
recordSourceLocator?: string | null;
|
|
120
|
+
recordMetadata?: RecordMetadata | null;
|
|
121
|
+
recordAnchors?: RecordAnchor[] | null;
|
|
122
|
+
recordAdapterFingerprint?: string | null;
|
|
116
123
|
indexedAt?: string | null;
|
|
117
124
|
|
|
118
125
|
// Status
|
|
@@ -132,6 +139,16 @@ export interface DocumentRow {
|
|
|
132
139
|
updatedAt: string;
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
/** Minimal persisted record identity used by export snapshot reconciliation. */
|
|
143
|
+
export interface StoredRecordState {
|
|
144
|
+
recordKey: string;
|
|
145
|
+
sourceHash: string;
|
|
146
|
+
adapterVersion: string;
|
|
147
|
+
adapterFingerprint: string;
|
|
148
|
+
active: boolean;
|
|
149
|
+
relativePath: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
135
152
|
/** Chunk row from DB */
|
|
136
153
|
export interface ChunkRow {
|
|
137
154
|
mirrorHash: string;
|
|
@@ -300,6 +317,12 @@ export interface DocumentInput {
|
|
|
300
317
|
author?: string;
|
|
301
318
|
frontmatterDate?: string;
|
|
302
319
|
dateFields?: Record<string, string>;
|
|
320
|
+
recordKey?: string;
|
|
321
|
+
recordSourcePath?: string;
|
|
322
|
+
recordSourceLocator?: string;
|
|
323
|
+
recordMetadata?: RecordMetadata;
|
|
324
|
+
recordAnchors?: RecordAnchor[];
|
|
325
|
+
recordAdapterFingerprint?: string;
|
|
303
326
|
lastErrorCode?: string;
|
|
304
327
|
lastErrorMessage?: string;
|
|
305
328
|
/** Ingest schema version for backfill detection */
|
|
@@ -571,6 +594,14 @@ export interface FtsResult {
|
|
|
571
594
|
contentType?: string;
|
|
572
595
|
contentTypeSource?: string;
|
|
573
596
|
categories?: string[];
|
|
597
|
+
converterId?: string;
|
|
598
|
+
converterVersion?: string;
|
|
599
|
+
recordKey?: string;
|
|
600
|
+
recordSourcePath?: string;
|
|
601
|
+
recordSourceLocator?: string;
|
|
602
|
+
recordMetadata?: RecordMetadata;
|
|
603
|
+
recordAnchors?: RecordAnchor[];
|
|
604
|
+
recordAdapterFingerprint?: string;
|
|
574
605
|
}
|
|
575
606
|
|
|
576
607
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -1448,6 +1479,15 @@ export interface StorePort {
|
|
|
1448
1479
|
*/
|
|
1449
1480
|
listDocuments(collection?: string): Promise<StoreResult<DocumentRow[]>>;
|
|
1450
1481
|
|
|
1482
|
+
/**
|
|
1483
|
+
* List logical record documents produced from one source container.
|
|
1484
|
+
* Uses the record-source index so targeted syncs do not scan a collection.
|
|
1485
|
+
*/
|
|
1486
|
+
listRecordDocuments(
|
|
1487
|
+
collection: string,
|
|
1488
|
+
sourcePath: string
|
|
1489
|
+
): Promise<StoreResult<DocumentRow[]>>;
|
|
1490
|
+
|
|
1451
1491
|
/**
|
|
1452
1492
|
* Fetch documents by mirror hashes in batch.
|
|
1453
1493
|
* Useful for retrieval pipelines to avoid full document scans.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
8161a2f2b8bb643dc2768c59a60783752b04dfedb0f5a1cf369042f623afaaa0 gno-browser-clipper-v1.27.1.zip
|