@gmickel/gno 1.27.1 → 1.29.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 +46 -20
- package/assets/skill/SKILL.md +59 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.29.0.zip.sha256 +1 -0
- package/browser-extension/dist/chunk-b2zm0jjd.js +50 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/browser-extension/dist/preview.html +1 -1
- package/browser-extension/dist/service-worker.js +6 -6
- package/package.json +2 -2
- package/spec/cli.md +82 -5
- package/spec/db/schema.sql +31 -0
- package/spec/mcp.md +71 -0
- package/spec/output-schemas/ask.schema.json +156 -1
- package/spec/output-schemas/browser-clip-preview.schema.json +30 -0
- package/spec/output-schemas/collection-egress-check.schema.json +91 -0
- package/spec/output-schemas/collection-egress-policy-set.schema.json +56 -0
- package/spec/output-schemas/collection-egress-policy.schema.json +41 -0
- package/spec/output-schemas/context-capsule-v1.schema.json +183 -3
- package/spec/output-schemas/context-capsule-verification.schema.json +1 -1
- package/spec/output-schemas/egress-audit-management.schema.json +88 -0
- package/spec/output-schemas/get.schema.json +94 -0
- package/spec/output-schemas/mcp-http-error.schema.json +113 -2
- 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/publish-artifact.schema.json +32 -0
- package/spec/output-schemas/record-import.schema.json +223 -0
- package/spec/output-schemas/retrieval-trace-common.schema.json +31 -0
- package/spec/output-schemas/retrieval-trace-export.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-qrels.schema.json +3 -0
- package/spec/output-schemas/retrieval-trace-summary.schema.json +4 -0
- package/spec/output-schemas/search-result.schema.json +94 -0
- package/spec/output-schemas/search-results.schema.json +155 -0
- package/spec/project-profile.schema.json +66 -0
- package/src/app/context-format.ts +1 -1
- package/src/app/context-runtime-contract.ts +16 -7
- package/src/cli/commands/ask.ts +7 -0
- package/src/cli/commands/collection/index.ts +5 -0
- package/src/cli/commands/collection/policy.ts +126 -0
- package/src/cli/commands/context-build.ts +5 -0
- package/src/cli/commands/daemon.ts +45 -3
- package/src/cli/commands/egress-audit.ts +68 -0
- package/src/cli/commands/embed.ts +2 -0
- 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/query.ts +12 -0
- package/src/cli/commands/replay.ts +13 -3
- package/src/cli/commands/shared.ts +28 -0
- package/src/cli/commands/update.ts +5 -0
- package/src/cli/commands/vsearch.ts +3 -1
- package/src/cli/program.ts +127 -0
- package/src/config/index.ts +9 -0
- package/src/config/project-profile.ts +2 -0
- package/src/config/types.ts +66 -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/browser-clip.ts +16 -1
- package/src/core/collection-egress-policy-projection.ts +28 -0
- package/src/core/collection-egress-policy-service.ts +443 -0
- package/src/core/collection-egress-policy-validation.ts +242 -0
- package/src/core/config-mutation.ts +31 -19
- package/src/core/context-capsule-schema.ts +198 -1
- package/src/core/context-capsule-validation.ts +4 -4
- package/src/core/context-capsule-verification.ts +5 -1
- package/src/core/context-capsule.ts +177 -112
- package/src/core/context-evidence.ts +93 -15
- package/src/core/destination-classifier.ts +402 -0
- package/src/core/document-capabilities.ts +12 -0
- package/src/core/egress-audit.ts +239 -0
- package/src/core/egress-authorization.ts +50 -0
- package/src/core/egress-enforcement.ts +264 -0
- package/src/core/egress-policy.ts +440 -0
- package/src/core/egress-provenance.ts +336 -0
- package/src/core/job-manager.ts +37 -0
- package/src/core/network-boundary-inventory.ts +261 -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/core/retrieval-qrels.ts +6 -0
- package/src/core/retrieval-replay.ts +18 -4
- package/src/core/retrieval-trace-export.ts +23 -4
- package/src/core/retrieval-trace-management-helpers.ts +1 -0
- package/src/core/retrieval-trace-management-types.ts +3 -0
- package/src/core/retrieval-trace-management.ts +9 -1
- package/src/core/retrieval-trace-session.ts +94 -0
- package/src/core/retrieval-trace.ts +22 -0
- package/src/ingestion/record-adapter-canonical.ts +433 -0
- package/src/ingestion/record-adapter.ts +448 -0
- package/src/ingestion/record-container.ts +704 -0
- package/src/ingestion/record-path.ts +20 -0
- package/src/ingestion/record-sync.ts +70 -0
- package/src/ingestion/sync.ts +243 -36
- package/src/ingestion/types.ts +71 -1
- package/src/ingestion/walker.ts +31 -11
- package/src/llm/errors.ts +10 -0
- package/src/llm/http-inference.ts +175 -0
- package/src/llm/http-policy.ts +537 -0
- package/src/llm/httpEmbedding.ts +47 -28
- package/src/llm/httpGeneration.ts +31 -18
- package/src/llm/httpRerank.ts +30 -18
- package/src/llm/index.ts +1 -0
- package/src/llm/nodeLlamaCpp/adapter.ts +55 -20
- package/src/llm/pinned-http-connection.ts +177 -0
- package/src/mcp/context.ts +53 -1
- package/src/mcp/http-egress.ts +220 -0
- package/src/mcp/http-security.ts +32 -21
- package/src/mcp/http-session.ts +13 -2
- package/src/mcp/http-transport.ts +96 -6
- package/src/mcp/sync-egress.ts +24 -0
- package/src/mcp/tools/add-collection.ts +4 -0
- package/src/mcp/tools/ask.ts +1 -1
- package/src/mcp/tools/context.ts +9 -2
- package/src/mcp/tools/egress.ts +247 -0
- package/src/mcp/tools/embed.ts +1 -0
- package/src/mcp/tools/get.ts +10 -2
- package/src/mcp/tools/index-cmd.ts +7 -0
- package/src/mcp/tools/index.ts +98 -0
- package/src/mcp/tools/multi-get.ts +9 -2
- package/src/mcp/tools/query.ts +10 -0
- package/src/mcp/tools/remove-collection.ts +4 -0
- package/src/mcp/tools/sync.ts +6 -0
- package/src/mcp/tools/trace.ts +7 -1
- package/src/mcp/tools/vsearch.ts +1 -0
- package/src/mcp/tools/workspace-write.ts +2 -0
- package/src/pipeline/egress-lineage.ts +124 -0
- package/src/pipeline/filters.ts +1 -1
- package/src/pipeline/graph-retrieval.ts +7 -4
- package/src/pipeline/hybrid.ts +21 -4
- package/src/pipeline/result-context.ts +12 -4
- package/src/pipeline/search.ts +23 -4
- package/src/pipeline/types.ts +6 -0
- package/src/pipeline/vsearch.ts +67 -28
- package/src/publish/artifact.ts +31 -3
- package/src/publish/export-service.ts +25 -0
- package/src/sdk/client.ts +119 -2
- package/src/sdk/documents.ts +13 -5
- package/src/sdk/embed.ts +1 -0
- package/src/sdk/types.ts +33 -0
- package/src/serve/browse-tree.ts +4 -2
- package/src/serve/clipper-capture.ts +5 -1
- package/src/serve/closed-json.ts +61 -0
- package/src/serve/config-sync.ts +4 -1
- package/src/serve/context.ts +1 -0
- package/src/serve/doc-events.ts +110 -39
- package/src/serve/public/components/CollectionEgressCheckPanel.tsx +411 -0
- package/src/serve/public/components/CollectionEgressPolicyEditor.tsx +78 -0
- package/src/serve/public/components/CollectionModelDialog.tsx +93 -9
- package/src/serve/public/components/EgressAuditPanel.tsx +278 -0
- package/src/serve/public/globals.built.css +1 -1
- package/src/serve/public/pages/Collections.tsx +3 -0
- package/src/serve/resident-request.ts +80 -3
- package/src/serve/resident-runtime.ts +63 -1
- package/src/serve/routes/api.ts +237 -65
- package/src/serve/routes/clipper.ts +64 -1
- package/src/serve/routes/mcp.ts +6 -0
- package/src/serve/routes/traces.ts +27 -4
- package/src/serve/server.ts +118 -7
- package/src/store/migrations/022-record-export-lineage.ts +42 -0
- package/src/store/migrations/023-collection-egress-policy.ts +61 -0
- package/src/store/migrations/024-egress-derived-lineage.ts +121 -0
- package/src/store/migrations/025-collection-egress-policy-revision.ts +40 -0
- package/src/store/migrations/index.ts +8 -0
- package/src/store/retrieval-trace-codec.ts +15 -0
- package/src/store/sqlite/adapter.ts +284 -14
- package/src/store/sqlite/change-journal-store.ts +41 -2
- package/src/store/sqlite/egress-audit-store.ts +485 -0
- package/src/store/sqlite/retrieval-trace-management-store.ts +47 -6
- package/src/store/sqlite/retrieval-trace-retention.ts +2 -0
- package/src/store/sqlite/retrieval-trace-rows.ts +8 -1
- package/src/store/sqlite/retrieval-trace-store.ts +104 -5
- package/src/store/types.ts +166 -1
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.27.1.zip.sha256 +0 -1
- package/browser-extension/dist/chunk-vn5f663b.js +0 -50
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const RECORD_VIRTUAL_ROOT = ".gno/records";
|
|
2
|
+
|
|
3
|
+
const sha256 = (value: string): string =>
|
|
4
|
+
new Bun.CryptoHasher("sha256").update(value).digest("hex");
|
|
5
|
+
|
|
6
|
+
/** Stable internal path for one logical record inside an export container. */
|
|
7
|
+
export const recordVirtualPath = (
|
|
8
|
+
sourcePath: string,
|
|
9
|
+
recordKey: string
|
|
10
|
+
): string =>
|
|
11
|
+
`${RECORD_VIRTUAL_ROOT}/${sha256(sourcePath).slice(0, 16)}/${recordKey}.md`;
|
|
12
|
+
|
|
13
|
+
/** Physical files may never occupy GNO's virtual-record namespace. */
|
|
14
|
+
export const isRecordVirtualPath = (relativePath: string): boolean => {
|
|
15
|
+
const normalized = relativePath.replaceAll("\\", "/");
|
|
16
|
+
return (
|
|
17
|
+
normalized === RECORD_VIRTUAL_ROOT ||
|
|
18
|
+
normalized.startsWith(`${RECORD_VIRTUAL_ROOT}/`)
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { StoredRecordState } from "../store/types";
|
|
2
|
+
import type { CanonicalRecord, RecordAdapterRunResult } from "./record-adapter";
|
|
3
|
+
|
|
4
|
+
export type RecordSyncAction =
|
|
5
|
+
| { type: "add"; record: CanonicalRecord }
|
|
6
|
+
| { type: "update"; previous: StoredRecordState; record: CanonicalRecord }
|
|
7
|
+
| {
|
|
8
|
+
type: "reactivate";
|
|
9
|
+
previous: StoredRecordState;
|
|
10
|
+
record: CanonicalRecord;
|
|
11
|
+
}
|
|
12
|
+
| { type: "unchanged"; previous: StoredRecordState; record?: CanonicalRecord }
|
|
13
|
+
| { type: "deactivate"; previous: StoredRecordState }
|
|
14
|
+
| { type: "preserve"; previous: StoredRecordState };
|
|
15
|
+
|
|
16
|
+
export interface RecordSyncPlan {
|
|
17
|
+
authoritative: boolean;
|
|
18
|
+
actions: RecordSyncAction[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Pure snapshot reconciliation. Persistence/transactions are intentionally
|
|
23
|
+
* deferred until the virtual-record lineage schema exists.
|
|
24
|
+
*/
|
|
25
|
+
export function reconcileRecordSnapshot(
|
|
26
|
+
priorRecords: readonly StoredRecordState[],
|
|
27
|
+
snapshot: RecordAdapterRunResult
|
|
28
|
+
): RecordSyncPlan {
|
|
29
|
+
const priorByKey = new Map(
|
|
30
|
+
priorRecords.map((record) => [record.recordKey, record])
|
|
31
|
+
);
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
const failed = new Set(snapshot.failedRecordKeys);
|
|
34
|
+
const actions: RecordSyncAction[] = [];
|
|
35
|
+
|
|
36
|
+
for (const record of snapshot.records) {
|
|
37
|
+
seen.add(record.recordKey);
|
|
38
|
+
const previous = priorByKey.get(record.recordKey);
|
|
39
|
+
if (!previous) {
|
|
40
|
+
actions.push({ type: "add", record });
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (!previous.active) {
|
|
44
|
+
actions.push({ type: "reactivate", previous, record });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (
|
|
48
|
+
previous.sourceHash === record.sourceHash &&
|
|
49
|
+
previous.adapterVersion === record.adapterVersion &&
|
|
50
|
+
previous.adapterFingerprint === record.adapterFingerprint
|
|
51
|
+
) {
|
|
52
|
+
actions.push({ type: "unchanged", previous, record });
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
actions.push({ type: "update", previous, record });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const previous of priorRecords) {
|
|
59
|
+
if (seen.has(previous.recordKey)) continue;
|
|
60
|
+
if (failed.has(previous.recordKey) || !snapshot.authoritative) {
|
|
61
|
+
actions.push({ type: "preserve", previous });
|
|
62
|
+
} else if (previous.active) {
|
|
63
|
+
actions.push({ type: "deactivate", previous });
|
|
64
|
+
} else {
|
|
65
|
+
actions.push({ type: "unchanged", previous });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { authoritative: snapshot.authoritative, actions };
|
|
70
|
+
}
|
package/src/ingestion/sync.ts
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
* @module src/ingestion/sync
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
// node:fs/promises for stat (no Bun equivalent for file stats)
|
|
9
|
-
import { stat } from "node:fs/promises";
|
|
8
|
+
// node:fs/promises for realpath/stat (no Bun equivalent for canonical paths or file stats)
|
|
9
|
+
import { realpath, stat } from "node:fs/promises";
|
|
10
10
|
// node:path for join (no Bun path utils)
|
|
11
|
-
import { join } from "node:path";
|
|
11
|
+
import { isAbsolute, join, relative, sep } from "node:path";
|
|
12
12
|
|
|
13
13
|
import type { NormalizedContentTypeRule } from "../config";
|
|
14
14
|
import type { Collection } from "../config/types";
|
|
@@ -16,6 +16,7 @@ import type {
|
|
|
16
16
|
ChunkInput,
|
|
17
17
|
DocEdgeInput,
|
|
18
18
|
DocLinkInput,
|
|
19
|
+
DocumentInput,
|
|
19
20
|
DocumentRow,
|
|
20
21
|
IngestErrorInput,
|
|
21
22
|
StorePort,
|
|
@@ -37,12 +38,14 @@ import {
|
|
|
37
38
|
fingerprintContentTypeMetadataRules,
|
|
38
39
|
resolveContentTypeRule,
|
|
39
40
|
} from "../config";
|
|
41
|
+
import { createJsonlAdapter } from "../converters/adapters/jsonl/adapter";
|
|
42
|
+
import { createTranscriptAdapter } from "../converters/adapters/transcript/adapter";
|
|
40
43
|
import { getDefaultMimeDetector, type MimeDetector } from "../converters/mime";
|
|
41
44
|
import {
|
|
42
45
|
type ConversionPipeline,
|
|
43
46
|
getDefaultPipeline,
|
|
44
47
|
} from "../converters/pipeline";
|
|
45
|
-
import { DEFAULT_LIMITS } from "../converters/types";
|
|
48
|
+
import { DEFAULT_LIMITS, type RecordAdapter } from "../converters/types";
|
|
46
49
|
import {
|
|
47
50
|
diffDocumentStructure,
|
|
48
51
|
extractDocumentStructure,
|
|
@@ -50,6 +53,7 @@ import {
|
|
|
50
53
|
normalizeRelationEdgeType,
|
|
51
54
|
normalizeRelationTarget,
|
|
52
55
|
} from "../core/change-diff";
|
|
56
|
+
import { enforceCollectionEgress } from "../core/egress-enforcement";
|
|
53
57
|
import {
|
|
54
58
|
normalizeMarkdownPath,
|
|
55
59
|
normalizeWikiName,
|
|
@@ -64,6 +68,7 @@ import {
|
|
|
64
68
|
stripFrontmatter,
|
|
65
69
|
} from "./frontmatter";
|
|
66
70
|
import { buildLineOffsets } from "./position";
|
|
71
|
+
import { processRecordContainer } from "./record-container";
|
|
67
72
|
import { getExcludedRanges } from "./strip";
|
|
68
73
|
import { collectionToWalkConfig, DEFAULT_CHUNK_PARAMS } from "./types";
|
|
69
74
|
import { defaultWalker } from "./walker";
|
|
@@ -542,6 +547,44 @@ function mustOk<T>(
|
|
|
542
547
|
return result.value;
|
|
543
548
|
}
|
|
544
549
|
|
|
550
|
+
const preserveDocumentWithError = (
|
|
551
|
+
existing: DocumentRow,
|
|
552
|
+
code: string,
|
|
553
|
+
message: string
|
|
554
|
+
): DocumentInput => ({
|
|
555
|
+
collection: existing.collection,
|
|
556
|
+
relPath: existing.relPath,
|
|
557
|
+
sourceHash: existing.sourceHash,
|
|
558
|
+
sourceMime: existing.sourceMime,
|
|
559
|
+
sourceExt: existing.sourceExt,
|
|
560
|
+
sourceSize: existing.sourceSize,
|
|
561
|
+
sourceMtime: existing.sourceMtime,
|
|
562
|
+
sourceCtime: existing.sourceCtime ?? existing.sourceMtime,
|
|
563
|
+
title: existing.title ?? undefined,
|
|
564
|
+
mirrorHash: existing.mirrorHash ?? undefined,
|
|
565
|
+
converterId: existing.converterId ?? undefined,
|
|
566
|
+
converterVersion: existing.converterVersion ?? undefined,
|
|
567
|
+
languageHint: existing.languageHint ?? undefined,
|
|
568
|
+
contentType: existing.contentType ?? undefined,
|
|
569
|
+
contentTypeSource: existing.contentTypeSource ?? undefined,
|
|
570
|
+
categories: existing.categories ?? undefined,
|
|
571
|
+
author: existing.author ?? undefined,
|
|
572
|
+
frontmatterDate: existing.frontmatterDate ?? undefined,
|
|
573
|
+
dateFields: existing.dateFields ?? undefined,
|
|
574
|
+
recordKey: existing.recordKey ?? undefined,
|
|
575
|
+
recordSourcePath: existing.recordSourcePath ?? undefined,
|
|
576
|
+
recordSourceLocator: existing.recordSourceLocator ?? undefined,
|
|
577
|
+
recordMetadata: existing.recordMetadata ?? undefined,
|
|
578
|
+
recordAnchors: existing.recordAnchors ?? undefined,
|
|
579
|
+
recordAdapterFingerprint: existing.recordAdapterFingerprint ?? undefined,
|
|
580
|
+
lastErrorCode: code,
|
|
581
|
+
lastErrorMessage: message,
|
|
582
|
+
ingestVersion: existing.ingestVersion ?? undefined,
|
|
583
|
+
contentTypeRulesFingerprint:
|
|
584
|
+
existing.contentTypeRulesFingerprint ?? undefined,
|
|
585
|
+
changeJournal: false,
|
|
586
|
+
});
|
|
587
|
+
|
|
545
588
|
/**
|
|
546
589
|
* Simple semaphore for bounded concurrency.
|
|
547
590
|
*/
|
|
@@ -594,6 +637,30 @@ export class SyncService {
|
|
|
594
637
|
this.pipeline = pipeline ?? getDefaultPipeline();
|
|
595
638
|
}
|
|
596
639
|
|
|
640
|
+
private async selectRecordAdapter(
|
|
641
|
+
collection: Collection,
|
|
642
|
+
mime: string,
|
|
643
|
+
ext: string
|
|
644
|
+
): Promise<RecordAdapter | undefined> {
|
|
645
|
+
const transcriptConfig = collection.recordAdapters?.transcript;
|
|
646
|
+
if (transcriptConfig) {
|
|
647
|
+
const adapter = createTranscriptAdapter(transcriptConfig);
|
|
648
|
+
if (adapter.canHandle(mime, ext)) return adapter;
|
|
649
|
+
}
|
|
650
|
+
const jsonlConfig = collection.recordAdapters?.jsonl;
|
|
651
|
+
if (jsonlConfig) {
|
|
652
|
+
const adapter = createJsonlAdapter(jsonlConfig.fieldMapping);
|
|
653
|
+
if (adapter.canHandle(mime, ext)) return adapter;
|
|
654
|
+
}
|
|
655
|
+
const selectDefault = (
|
|
656
|
+
this.pipeline as ConversionPipeline & {
|
|
657
|
+
selectRecordAdapter?: ConversionPipeline["selectRecordAdapter"];
|
|
658
|
+
}
|
|
659
|
+
).selectRecordAdapter;
|
|
660
|
+
if (!selectDefault) return undefined;
|
|
661
|
+
return selectDefault.call(this.pipeline, mime, ext);
|
|
662
|
+
}
|
|
663
|
+
|
|
597
664
|
/**
|
|
598
665
|
* Process a single file through the ingestion pipeline.
|
|
599
666
|
* All store operations are checked and errors are propagated.
|
|
@@ -661,17 +728,57 @@ export class SyncService {
|
|
|
661
728
|
};
|
|
662
729
|
}
|
|
663
730
|
|
|
664
|
-
|
|
731
|
+
const extensionMime = this.mimeDetector.detect(
|
|
732
|
+
entry.relPath,
|
|
733
|
+
new Uint8Array()
|
|
734
|
+
);
|
|
735
|
+
const recordAdapter = await this.selectRecordAdapter(
|
|
736
|
+
collection,
|
|
737
|
+
extensionMime.mime,
|
|
738
|
+
extensionMime.ext
|
|
739
|
+
);
|
|
740
|
+
const contentTypeRules = options.contentTypeRules ?? [];
|
|
741
|
+
const contentTypeRulesFingerprint =
|
|
742
|
+
options.contentTypeRulesFingerprint ??
|
|
743
|
+
fingerprintContentTypeMetadataRules(contentTypeRules);
|
|
744
|
+
if (recordAdapter) {
|
|
745
|
+
return await processRecordContainer({
|
|
746
|
+
adapter: recordAdapter,
|
|
747
|
+
chunker: this.chunker,
|
|
748
|
+
collection,
|
|
749
|
+
contentTypeRules,
|
|
750
|
+
contentTypeRulesFingerprint,
|
|
751
|
+
entry,
|
|
752
|
+
ext: extensionMime.ext,
|
|
753
|
+
extractMetadata: extractDocumentMetadata,
|
|
754
|
+
ingestVersion: INGEST_VERSION,
|
|
755
|
+
mime: extensionMime.mime,
|
|
756
|
+
options,
|
|
757
|
+
sourceCtime,
|
|
758
|
+
sourceMtime,
|
|
759
|
+
sourceSize,
|
|
760
|
+
store,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const sniffBytes = new Uint8Array(
|
|
765
|
+
await Bun.file(entry.absPath).slice(0, 512).arrayBuffer()
|
|
766
|
+
);
|
|
767
|
+
const mime = this.mimeDetector.detect(entry.relPath, sniffBytes);
|
|
768
|
+
|
|
769
|
+
const priorRecordDocuments = mustOk(
|
|
770
|
+
await store.listRecordDocuments(collection.name, entry.relPath),
|
|
771
|
+
"listRecordDocuments",
|
|
772
|
+
{ collection: collection.name, relPath: entry.relPath }
|
|
773
|
+
);
|
|
774
|
+
|
|
775
|
+
// 2. Read byte-oriented files only after record-adapter selection.
|
|
665
776
|
const bytes = await Bun.file(entry.absPath).bytes();
|
|
666
777
|
|
|
667
778
|
// 3. Compute sourceHash
|
|
668
779
|
const hasher = new Bun.CryptoHasher("sha256");
|
|
669
780
|
hasher.update(bytes);
|
|
670
781
|
const sourceHash = hasher.digest("hex");
|
|
671
|
-
const contentTypeRules = options.contentTypeRules ?? [];
|
|
672
|
-
const contentTypeRulesFingerprint =
|
|
673
|
-
options.contentTypeRulesFingerprint ??
|
|
674
|
-
fingerprintContentTypeMetadataRules(contentTypeRules);
|
|
675
782
|
|
|
676
783
|
// 4. Check existing doc for skip/repair decision
|
|
677
784
|
const existingResult = await store.getDocument(
|
|
@@ -686,12 +793,20 @@ export class SyncService {
|
|
|
686
793
|
);
|
|
687
794
|
|
|
688
795
|
if (decision.kind === "skip") {
|
|
796
|
+
const activeRecordPaths = priorRecordDocuments
|
|
797
|
+
.filter((document) => document.active)
|
|
798
|
+
.map((document) => document.relPath);
|
|
799
|
+
if (activeRecordPaths.length > 0) {
|
|
800
|
+
mustOk(
|
|
801
|
+
await store.markInactive(collection.name, activeRecordPaths),
|
|
802
|
+
"markInactive",
|
|
803
|
+
{ collection: collection.name, relPath: entry.relPath }
|
|
804
|
+
);
|
|
805
|
+
return { relPath: entry.relPath, status: "updated" };
|
|
806
|
+
}
|
|
689
807
|
return { relPath: entry.relPath, status: "unchanged" };
|
|
690
808
|
}
|
|
691
809
|
|
|
692
|
-
// 5. Detect MIME (bytes is already Uint8Array from Bun.file().bytes())
|
|
693
|
-
const mime = this.mimeDetector.detect(entry.absPath, bytes);
|
|
694
|
-
|
|
695
810
|
// 6. Convert via pipeline
|
|
696
811
|
const convertResult = await this.pipeline.convert({
|
|
697
812
|
sourcePath: entry.absPath,
|
|
@@ -938,7 +1053,19 @@ export class SyncService {
|
|
|
938
1053
|
linkCount: linkInputs.length,
|
|
939
1054
|
});
|
|
940
1055
|
|
|
941
|
-
const
|
|
1056
|
+
const activeRecordPaths = priorRecordDocuments
|
|
1057
|
+
.filter((document) => document.active)
|
|
1058
|
+
.map((document) => document.relPath);
|
|
1059
|
+
if (activeRecordPaths.length > 0) {
|
|
1060
|
+
mustOk(
|
|
1061
|
+
await store.markInactive(collection.name, activeRecordPaths),
|
|
1062
|
+
"markInactive",
|
|
1063
|
+
{ collection: collection.name, relPath: entry.relPath }
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
const status =
|
|
1068
|
+
existing || priorRecordDocuments.length > 0 ? "updated" : "added";
|
|
942
1069
|
return {
|
|
943
1070
|
relPath: entry.relPath,
|
|
944
1071
|
status,
|
|
@@ -982,22 +1109,10 @@ export class SyncService {
|
|
|
982
1109
|
collection.name,
|
|
983
1110
|
entry.relPath
|
|
984
1111
|
);
|
|
985
|
-
if (existingResult.ok && existingResult.value) {
|
|
986
|
-
await store.upsertDocument(
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
sourceHash: existingResult.value.sourceHash,
|
|
990
|
-
sourceMime: existingResult.value.sourceMime,
|
|
991
|
-
sourceExt: existingResult.value.sourceExt,
|
|
992
|
-
sourceSize: existingResult.value.sourceSize,
|
|
993
|
-
sourceMtime: existingResult.value.sourceMtime,
|
|
994
|
-
sourceCtime:
|
|
995
|
-
existingResult.value.sourceCtime ??
|
|
996
|
-
existingResult.value.sourceMtime,
|
|
997
|
-
lastErrorCode: code,
|
|
998
|
-
lastErrorMessage: message,
|
|
999
|
-
changeJournal: false,
|
|
1000
|
-
});
|
|
1112
|
+
if (existingResult.ok && existingResult.value?.active) {
|
|
1113
|
+
await store.upsertDocument(
|
|
1114
|
+
preserveDocumentWithError(existingResult.value, code, message)
|
|
1115
|
+
);
|
|
1001
1116
|
}
|
|
1002
1117
|
} catch {
|
|
1003
1118
|
// Best-effort error recording
|
|
@@ -1065,6 +1180,20 @@ export class SyncService {
|
|
|
1065
1180
|
let markedInactive = 0;
|
|
1066
1181
|
|
|
1067
1182
|
for (const relPath of relPaths) {
|
|
1183
|
+
const recordDocumentsResult = await store.listRecordDocuments(
|
|
1184
|
+
collection.name,
|
|
1185
|
+
relPath
|
|
1186
|
+
);
|
|
1187
|
+
if (!recordDocumentsResult.ok) {
|
|
1188
|
+
results.push({
|
|
1189
|
+
relPath,
|
|
1190
|
+
status: "error",
|
|
1191
|
+
errorCode: recordDocumentsResult.error.code,
|
|
1192
|
+
errorMessage: recordDocumentsResult.error.message,
|
|
1193
|
+
});
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const recordDocuments = recordDocumentsResult.value;
|
|
1068
1197
|
const existingResult = await store.getDocument(collection.name, relPath);
|
|
1069
1198
|
const existingDoc = existingResult.ok ? existingResult.value : null;
|
|
1070
1199
|
if (existingDoc) {
|
|
@@ -1074,6 +1203,13 @@ export class SyncService {
|
|
|
1074
1203
|
projectionSourceIds
|
|
1075
1204
|
);
|
|
1076
1205
|
}
|
|
1206
|
+
for (const recordDocument of recordDocuments) {
|
|
1207
|
+
await this.collectProjectionSourceIds(
|
|
1208
|
+
store,
|
|
1209
|
+
recordDocument.id,
|
|
1210
|
+
projectionSourceIds
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1077
1213
|
|
|
1078
1214
|
const absPath = join(collection.path, relPath);
|
|
1079
1215
|
let stats: Awaited<ReturnType<typeof stat>>;
|
|
@@ -1094,10 +1230,17 @@ export class SyncService {
|
|
|
1094
1230
|
});
|
|
1095
1231
|
continue;
|
|
1096
1232
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1233
|
+
const activePaths = [
|
|
1234
|
+
...(existingDoc?.active ? [relPath] : []),
|
|
1235
|
+
...recordDocuments
|
|
1236
|
+
.filter((document) => document.active)
|
|
1237
|
+
.map((document) => document.relPath),
|
|
1238
|
+
];
|
|
1239
|
+
if (activePaths.length > 0) {
|
|
1240
|
+
const inactiveResult = await store.markInactive(
|
|
1241
|
+
collection.name,
|
|
1242
|
+
activePaths
|
|
1243
|
+
);
|
|
1101
1244
|
if (!inactiveResult.ok) {
|
|
1102
1245
|
results.push({
|
|
1103
1246
|
relPath,
|
|
@@ -1111,7 +1254,7 @@ export class SyncService {
|
|
|
1111
1254
|
results.push({
|
|
1112
1255
|
relPath,
|
|
1113
1256
|
status: "updated",
|
|
1114
|
-
docid: existingDoc
|
|
1257
|
+
docid: existingDoc?.docid,
|
|
1115
1258
|
});
|
|
1116
1259
|
continue;
|
|
1117
1260
|
}
|
|
@@ -1133,8 +1276,39 @@ export class SyncService {
|
|
|
1133
1276
|
continue;
|
|
1134
1277
|
}
|
|
1135
1278
|
|
|
1279
|
+
let canonicalSourcePath: string;
|
|
1280
|
+
try {
|
|
1281
|
+
const [collectionRoot, sourcePath] = await Promise.all([
|
|
1282
|
+
realpath(collection.path),
|
|
1283
|
+
realpath(absPath),
|
|
1284
|
+
]);
|
|
1285
|
+
const sourceRelative = relative(collectionRoot, sourcePath);
|
|
1286
|
+
if (
|
|
1287
|
+
sourceRelative === ".." ||
|
|
1288
|
+
sourceRelative.startsWith(`..${sep}`) ||
|
|
1289
|
+
isAbsolute(sourceRelative)
|
|
1290
|
+
) {
|
|
1291
|
+
results.push({
|
|
1292
|
+
relPath,
|
|
1293
|
+
status: "error",
|
|
1294
|
+
errorCode: "PATH_OUTSIDE_COLLECTION",
|
|
1295
|
+
errorMessage: "Source path resolves outside the collection root.",
|
|
1296
|
+
});
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
1299
|
+
canonicalSourcePath = sourcePath;
|
|
1300
|
+
} catch {
|
|
1301
|
+
results.push({
|
|
1302
|
+
relPath,
|
|
1303
|
+
status: "error",
|
|
1304
|
+
errorCode: "PATH_UNRESOLVED",
|
|
1305
|
+
errorMessage: "Source path could not be resolved safely.",
|
|
1306
|
+
});
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1136
1310
|
const entry: WalkEntry = {
|
|
1137
|
-
absPath,
|
|
1311
|
+
absPath: canonicalSourcePath,
|
|
1138
1312
|
relPath,
|
|
1139
1313
|
size: stats.size,
|
|
1140
1314
|
mtime: stats.mtime.toISOString(),
|
|
@@ -1157,6 +1331,21 @@ export class SyncService {
|
|
|
1157
1331
|
projectionSourceIds
|
|
1158
1332
|
);
|
|
1159
1333
|
}
|
|
1334
|
+
const currentDocuments = await store.listRecordDocuments(
|
|
1335
|
+
collection.name,
|
|
1336
|
+
relPath
|
|
1337
|
+
);
|
|
1338
|
+
if (currentDocuments.ok) {
|
|
1339
|
+
for (const recordDocument of currentDocuments.value) {
|
|
1340
|
+
if (recordDocument.active) {
|
|
1341
|
+
await this.collectProjectionSourceIds(
|
|
1342
|
+
store,
|
|
1343
|
+
recordDocument.id,
|
|
1344
|
+
projectionSourceIds
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1160
1349
|
}
|
|
1161
1350
|
|
|
1162
1351
|
const errors =
|
|
@@ -1410,10 +1599,24 @@ export class SyncService {
|
|
|
1410
1599
|
|
|
1411
1600
|
// 1. Run preflight commands
|
|
1412
1601
|
if (options.runUpdateCmd !== false && collection.updateCmd) {
|
|
1602
|
+
enforceCollectionEgress({
|
|
1603
|
+
collections: [collection],
|
|
1604
|
+
action: "export",
|
|
1605
|
+
destinationZone: "remote",
|
|
1606
|
+
caller: { authenticated: true, operationAuthorized: true },
|
|
1607
|
+
contentClass: "source",
|
|
1608
|
+
});
|
|
1413
1609
|
await runUpdateCmd(collection.path, collection.updateCmd);
|
|
1414
1610
|
}
|
|
1415
1611
|
|
|
1416
1612
|
if (options.gitPull && (await isGitRepo(collection.path))) {
|
|
1613
|
+
enforceCollectionEgress({
|
|
1614
|
+
collections: [collection],
|
|
1615
|
+
action: "export",
|
|
1616
|
+
destinationZone: "remote",
|
|
1617
|
+
caller: { authenticated: true, operationAuthorized: true },
|
|
1618
|
+
contentClass: "metadata",
|
|
1619
|
+
});
|
|
1417
1620
|
await gitPull(collection.path);
|
|
1418
1621
|
}
|
|
1419
1622
|
|
|
@@ -1600,7 +1803,11 @@ export class SyncService {
|
|
|
1600
1803
|
const existingDocsResult = await store.listDocuments(collection.name);
|
|
1601
1804
|
if (existingDocsResult.ok) {
|
|
1602
1805
|
const missingPaths = existingDocsResult.value
|
|
1603
|
-
.filter(
|
|
1806
|
+
.filter(
|
|
1807
|
+
(document) =>
|
|
1808
|
+
document.active &&
|
|
1809
|
+
!seenPaths.has(document.recordSourcePath ?? document.relPath)
|
|
1810
|
+
)
|
|
1604
1811
|
.map((d) => d.relPath);
|
|
1605
1812
|
|
|
1606
1813
|
if (missingPaths.length > 0) {
|
package/src/ingestion/types.ts
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
|
|
8
8
|
import type { NormalizedContentTypeRule } from "../config";
|
|
9
9
|
import type { Collection } from "../config/types";
|
|
10
|
+
import type {
|
|
11
|
+
RecordAdapterFailure,
|
|
12
|
+
RecordAttachmentInventoryItem,
|
|
13
|
+
} from "../converters/types";
|
|
14
|
+
import type { EgressLineage } from "../core/egress-provenance";
|
|
10
15
|
|
|
11
16
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
12
17
|
// Walker Types
|
|
@@ -32,8 +37,10 @@ export interface WalkConfig {
|
|
|
32
37
|
root: string;
|
|
33
38
|
/** Glob pattern (default: **\/*) */
|
|
34
39
|
pattern: string;
|
|
35
|
-
/** Extension allowlist (empty =
|
|
40
|
+
/** Extension allowlist (empty = supported defaults) */
|
|
36
41
|
include: string[];
|
|
42
|
+
/** Adapter-configured extensions added only to the supported defaults. */
|
|
43
|
+
additionalDefaultExtensions?: string[];
|
|
37
44
|
/** Paths/patterns to exclude */
|
|
38
45
|
exclude: string[];
|
|
39
46
|
/** Max file size in bytes (files larger are skipped) */
|
|
@@ -147,6 +154,34 @@ export type ContentTypeSource =
|
|
|
147
154
|
| "path-ext"
|
|
148
155
|
| "fallback";
|
|
149
156
|
|
|
157
|
+
/** Maximum per-record actions retained in one sync receipt. */
|
|
158
|
+
export const MAX_RECORD_IMPORT_RECEIPT_ITEMS = 1_000;
|
|
159
|
+
|
|
160
|
+
export type RecordImportOutcome =
|
|
161
|
+
| "added"
|
|
162
|
+
| "updated"
|
|
163
|
+
| "reactivated"
|
|
164
|
+
| "unchanged"
|
|
165
|
+
| "deactivated"
|
|
166
|
+
| "preserved";
|
|
167
|
+
|
|
168
|
+
/** Bounded, privacy-safe identity and provenance for one reconciled record. */
|
|
169
|
+
export interface RecordImportItemReceipt {
|
|
170
|
+
outcome: RecordImportOutcome;
|
|
171
|
+
recordKey: string;
|
|
172
|
+
sourceLocator: string;
|
|
173
|
+
sourceHash: string;
|
|
174
|
+
mirrorHash?: string;
|
|
175
|
+
adapterFingerprint: string;
|
|
176
|
+
attachments: RecordAttachmentInventoryItem[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export interface RecordImportWarning {
|
|
180
|
+
code: "PARTIAL_SNAPSHOT";
|
|
181
|
+
message: string;
|
|
182
|
+
retryable: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
/** Per-file sync status */
|
|
151
186
|
export type FileSyncStatus =
|
|
152
187
|
| "added"
|
|
@@ -165,6 +200,30 @@ export interface FileSyncResult {
|
|
|
165
200
|
contentTypeSource?: ContentTypeSource;
|
|
166
201
|
errorCode?: string;
|
|
167
202
|
errorMessage?: string;
|
|
203
|
+
recordImport?: {
|
|
204
|
+
adapterId: string;
|
|
205
|
+
adapterVersion: string;
|
|
206
|
+
adapterFingerprint: string;
|
|
207
|
+
egressLineage?: EgressLineage;
|
|
208
|
+
snapshotState: "complete" | "partial";
|
|
209
|
+
authoritative: boolean;
|
|
210
|
+
stoppedByCap: boolean;
|
|
211
|
+
sourceBytesRead: number;
|
|
212
|
+
records: {
|
|
213
|
+
accepted: number;
|
|
214
|
+
added: number;
|
|
215
|
+
updated: number;
|
|
216
|
+
reactivated: number;
|
|
217
|
+
unchanged: number;
|
|
218
|
+
deactivated: number;
|
|
219
|
+
preserved: number;
|
|
220
|
+
failed: number;
|
|
221
|
+
};
|
|
222
|
+
items: RecordImportItemReceipt[];
|
|
223
|
+
itemsTruncated: number;
|
|
224
|
+
warnings: RecordImportWarning[];
|
|
225
|
+
failures: RecordAdapterFailure[];
|
|
226
|
+
};
|
|
168
227
|
}
|
|
169
228
|
|
|
170
229
|
/** Collection sync summary */
|
|
@@ -221,6 +280,13 @@ export interface LanguageDetectorPort {
|
|
|
221
280
|
// Helper to create WalkConfig from Collection
|
|
222
281
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
223
282
|
|
|
283
|
+
const TRANSCRIPT_EXTENSION_BY_FORMAT = {
|
|
284
|
+
json: ".json",
|
|
285
|
+
srt: ".srt",
|
|
286
|
+
text: ".txt",
|
|
287
|
+
vtt: ".vtt",
|
|
288
|
+
} as const;
|
|
289
|
+
|
|
224
290
|
/**
|
|
225
291
|
* Create WalkConfig from Collection with maxBytes override.
|
|
226
292
|
*/
|
|
@@ -228,10 +294,14 @@ export function collectionToWalkConfig(
|
|
|
228
294
|
collection: Collection,
|
|
229
295
|
maxBytes: number
|
|
230
296
|
): WalkConfig {
|
|
297
|
+
const transcriptFormat = collection.recordAdapters?.transcript?.format;
|
|
231
298
|
return {
|
|
232
299
|
root: collection.path,
|
|
233
300
|
pattern: collection.pattern,
|
|
234
301
|
include: collection.include,
|
|
302
|
+
additionalDefaultExtensions: transcriptFormat
|
|
303
|
+
? [TRANSCRIPT_EXTENSION_BY_FORMAT[transcriptFormat]]
|
|
304
|
+
: [],
|
|
235
305
|
exclude: collection.exclude,
|
|
236
306
|
maxBytes,
|
|
237
307
|
};
|