@world-engines/project-host 0.1.0-alpha.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/LICENSE +46 -0
- package/assets/gui/WELogo-title.svg +11 -0
- package/assets/gui/app.js +1326 -0
- package/assets/gui/index.html +118 -0
- package/assets/gui/local-preview-asset-urls.js +61 -0
- package/assets/gui/local-preview-bridge.js +393 -0
- package/assets/gui/spatial-template-panel.js +233 -0
- package/assets/gui/styles.css +115 -0
- package/assets/gui/terrain-render.js +120 -0
- package/assets/gui/workspace-status.css +17 -0
- package/assets/gui/workspace-status.js +255 -0
- package/assets/gui/world-map-globe.js +187 -0
- package/assets/gui/world-map-worker.js +62 -0
- package/assets/gui/world-map.css +30 -0
- package/assets/gui/world-map.js +2466 -0
- package/dist/account-model-proxy.d.ts +21 -0
- package/dist/account-model-proxy.js +470 -0
- package/dist/authoring-browser-runtime.d.ts +7 -0
- package/dist/authoring-browser-runtime.js +110 -0
- package/dist/authoring-ingestion.d.ts +38 -0
- package/dist/authoring-ingestion.js +410 -0
- package/dist/authoring-model-connection.d.ts +166 -0
- package/dist/authoring-model-connection.js +757 -0
- package/dist/chinese-lexical.d.ts +152 -0
- package/dist/chinese-lexical.js +555 -0
- package/dist/chromium-preview-browser-driver.d.ts +43 -0
- package/dist/chromium-preview-browser-driver.js +575 -0
- package/dist/cli/download.d.ts +12 -0
- package/dist/cli/download.js +50 -0
- package/dist/cli/dump.d.ts +32 -0
- package/dist/cli/dump.js +91 -0
- package/dist/cli/ingest.d.ts +49 -0
- package/dist/cli/ingest.js +225 -0
- package/dist/cli/review.d.ts +10 -0
- package/dist/cli/review.js +44 -0
- package/dist/cli/upload.d.ts +57 -0
- package/dist/cli/upload.js +112 -0
- package/dist/cli-command.d.ts +60 -0
- package/dist/cli-command.js +613 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +182 -0
- package/dist/desktop-authoring-runtime.d.ts +88 -0
- package/dist/desktop-authoring-runtime.js +1602 -0
- package/dist/desktop-sidecar.d.ts +11 -0
- package/dist/desktop-sidecar.js +143 -0
- package/dist/document-dump.d.ts +113 -0
- package/dist/document-dump.js +431 -0
- package/dist/document-graph-extractor.d.ts +125 -0
- package/dist/document-graph-extractor.js +588 -0
- package/dist/document-ingestion-journal.d.ts +68 -0
- package/dist/document-ingestion-journal.js +471 -0
- package/dist/document-ingestion.d.ts +183 -0
- package/dist/document-ingestion.js +976 -0
- package/dist/document-query-path-proof.d.ts +30 -0
- package/dist/document-query-path-proof.js +34 -0
- package/dist/document-query-path-renderer.d.ts +50 -0
- package/dist/document-query-path-renderer.js +75 -0
- package/dist/document-query-proof-paths.d.ts +22 -0
- package/dist/document-query-proof-paths.js +71 -0
- package/dist/document-query.d.ts +97 -0
- package/dist/document-query.js +1106 -0
- package/dist/document-relation-role.d.ts +14 -0
- package/dist/document-relation-role.js +86 -0
- package/dist/document-source.d.ts +107 -0
- package/dist/document-source.js +255 -0
- package/dist/embedding-cache.d.ts +35 -0
- package/dist/embedding-cache.js +317 -0
- package/dist/frozen-review-local-pipeline.d.ts +88 -0
- package/dist/frozen-review-local-pipeline.js +609 -0
- package/dist/frozen-review-source-capture.d.ts +15 -0
- package/dist/frozen-review-source-capture.js +551 -0
- package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
- package/dist/frozen-review-wasm-encryptor.js +72 -0
- package/dist/frozen-review.d.ts +131 -0
- package/dist/frozen-review.js +802 -0
- package/dist/graph-traversal.d.ts +23 -0
- package/dist/graph-traversal.js +100 -0
- package/dist/gui/graph-layout-worker.js +1 -0
- package/dist/gui/graph-layout.wasm +0 -0
- package/dist/gui/graph.css +1 -0
- package/dist/gui/graph.js +76 -0
- package/dist/gui/plane-generation.js +112 -0
- package/dist/gui/plane-generator.js +310 -0
- package/dist/gui/plane-generator_bg.wasm +0 -0
- package/dist/gui/procedural-city.js +595 -0
- package/dist/gui/road-generation.js +129 -0
- package/dist/gui/road-parcels.js +1584 -0
- package/dist/gui/score.css +1 -0
- package/dist/gui/score.js +54 -0
- package/dist/gui/spatial-runtime.js +1231 -0
- package/dist/gui/spatial-templates.js +200 -0
- package/dist/gui/trigger.css +1 -0
- package/dist/gui/trigger.js +142 -0
- package/dist/harness-callback-transport.d.ts +17 -0
- package/dist/harness-callback-transport.js +128 -0
- package/dist/harness-document-ingestion.d.ts +77 -0
- package/dist/harness-document-ingestion.js +343 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.js +43 -0
- package/dist/ingestion-source-reconciliation.d.ts +88 -0
- package/dist/ingestion-source-reconciliation.js +253 -0
- package/dist/ladybug-runtime.d.ts +117 -0
- package/dist/ladybug-runtime.js +886 -0
- package/dist/local-account-embedding.d.ts +34 -0
- package/dist/local-account-embedding.js +294 -0
- package/dist/local-gallery.d.ts +35 -0
- package/dist/local-gallery.js +227 -0
- package/dist/local-gui-auth.d.ts +96 -0
- package/dist/local-gui-auth.js +703 -0
- package/dist/local-gui-contract.d.ts +53 -0
- package/dist/local-gui-contract.js +5 -0
- package/dist/local-gui-image-assets.d.ts +17 -0
- package/dist/local-gui-image-assets.js +96 -0
- package/dist/local-gui-preview-client.d.ts +3 -0
- package/dist/local-gui-preview-client.js +111 -0
- package/dist/local-gui-score.d.ts +72 -0
- package/dist/local-gui-score.js +184 -0
- package/dist/local-gui-server.d.ts +10 -0
- package/dist/local-gui-server.js +987 -0
- package/dist/local-gui-version.d.ts +17 -0
- package/dist/local-gui-version.js +147 -0
- package/dist/local-gui-workspace.d.ts +14 -0
- package/dist/local-gui-workspace.js +171 -0
- package/dist/local-model-settings.d.ts +28 -0
- package/dist/local-model-settings.js +124 -0
- package/dist/local-preview-assets.d.ts +28 -0
- package/dist/local-preview-assets.js +124 -0
- package/dist/local-preview-bound-resources.d.ts +12 -0
- package/dist/local-preview-bound-resources.js +80 -0
- package/dist/local-preview-message-store.d.ts +63 -0
- package/dist/local-preview-message-store.js +214 -0
- package/dist/local-preview-player-read-models.d.ts +47 -0
- package/dist/local-preview-player-read-models.js +90 -0
- package/dist/local-preview-projections.d.ts +61 -0
- package/dist/local-preview-projections.js +117 -0
- package/dist/local-preview-runtime.d.ts +105 -0
- package/dist/local-preview-runtime.js +553 -0
- package/dist/local-preview-session.d.ts +73 -0
- package/dist/local-preview-session.js +395 -0
- package/dist/local-preview-source.d.ts +40 -0
- package/dist/local-preview-source.js +192 -0
- package/dist/local-preview-view-events.d.ts +101 -0
- package/dist/local-preview-view-events.js +179 -0
- package/dist/local-preview-view-state-sqlite.d.ts +24 -0
- package/dist/local-preview-view-state-sqlite.js +263 -0
- package/dist/local-preview-view-state.d.ts +93 -0
- package/dist/local-preview-view-state.js +108 -0
- package/dist/local-preview-vite-config.d.ts +6 -0
- package/dist/local-preview-vite-config.js +122 -0
- package/dist/local-preview-world-effects.d.ts +9 -0
- package/dist/local-preview-world-effects.js +55 -0
- package/dist/local-preview-worldline-store.d.ts +47 -0
- package/dist/local-preview-worldline-store.js +282 -0
- package/dist/local-preview-worldlines.d.ts +95 -0
- package/dist/local-preview-worldlines.js +136 -0
- package/dist/mcp-async-dispatcher.d.ts +3 -0
- package/dist/mcp-async-dispatcher.js +129 -0
- package/dist/mcp-line-queue.d.ts +14 -0
- package/dist/mcp-line-queue.js +184 -0
- package/dist/mcp-pipe-internal.d.ts +6 -0
- package/dist/mcp-pipe-internal.js +151 -0
- package/dist/mcp-pipe-response.d.ts +14 -0
- package/dist/mcp-pipe-response.js +50 -0
- package/dist/mcp-stdio-response.d.ts +12 -0
- package/dist/mcp-stdio-response.js +27 -0
- package/dist/mcp.d.ts +27 -0
- package/dist/mcp.js +194 -0
- package/dist/prepared-embedding-cache.d.ts +38 -0
- package/dist/prepared-embedding-cache.js +238 -0
- package/dist/preview-browser-adapter.d.ts +53 -0
- package/dist/preview-browser-adapter.js +171 -0
- package/dist/preview-runtime.d.ts +67 -0
- package/dist/preview-runtime.js +460 -0
- package/dist/project-auth-transport.d.ts +27 -0
- package/dist/project-auth-transport.js +121 -0
- package/dist/project-auth.d.ts +51 -0
- package/dist/project-auth.js +108 -0
- package/dist/project-git-capture-recovery.d.ts +16 -0
- package/dist/project-git-capture-recovery.js +347 -0
- package/dist/project-git-cli.d.ts +5 -0
- package/dist/project-git-cli.js +40 -0
- package/dist/project-git-scenario-source.d.ts +10 -0
- package/dist/project-git-scenario-source.js +240 -0
- package/dist/project-git-view-source.d.ts +13 -0
- package/dist/project-git-view-source.js +134 -0
- package/dist/project-git.d.ts +97 -0
- package/dist/project-git.js +656 -0
- package/dist/project-host.d.ts +59 -0
- package/dist/project-host.js +471 -0
- package/dist/project-writer-lease.d.ts +18 -0
- package/dist/project-writer-lease.js +430 -0
- package/dist/public-error.d.ts +7 -0
- package/dist/public-error.js +61 -0
- package/dist/retrieval-benchmark.d.ts +46 -0
- package/dist/retrieval-benchmark.js +58 -0
- package/dist/safe-project-path.d.ts +8 -0
- package/dist/safe-project-path.js +93 -0
- package/dist/scenario-writer.d.ts +32 -0
- package/dist/scenario-writer.js +191 -0
- package/dist/transfer/download.d.ts +159 -0
- package/dist/transfer/download.js +598 -0
- package/dist/transfer/transport.d.ts +26 -0
- package/dist/transfer/transport.js +1 -0
- package/dist/transfer/upload.d.ts +162 -0
- package/dist/transfer/upload.js +354 -0
- package/dist/transfer-types.d.ts +95 -0
- package/dist/transfer-types.js +1 -0
- package/dist/transfer.d.ts +18 -0
- package/dist/transfer.js +273 -0
- package/dist/trigger-runtime.d.ts +121 -0
- package/dist/trigger-runtime.js +153 -0
- package/dist/vendor/wasm-core/asset-hashes.json +5 -0
- package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
- package/dist/vendor/wasm-core/wasm_core.js +1 -0
- package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
- package/dist/view-watcher.d.ts +9 -0
- package/dist/view-watcher.js +34 -0
- package/package.json +110 -0
|
@@ -0,0 +1,976 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { AuthoringBridgeError } from "@world-engines/authoring-bridge";
|
|
4
|
+
import { DocumentGraphExtractionError, extractDocumentGraphWindow, } from "./document-graph-extractor.js";
|
|
5
|
+
import { openDocumentIngestionJournal, readDocumentIngestionJournalStatus, } from "./document-ingestion-journal.js";
|
|
6
|
+
import { buildDocumentSource, documentSourceBytes, validateDocumentPlan, } from "./document-source.js";
|
|
7
|
+
import { createCachedEmbeddingPort } from "./embedding-cache.js";
|
|
8
|
+
import { assertSafeExistingProjectPath, safeProjectPath } from "./safe-project-path.js";
|
|
9
|
+
const CONTROL_KIND = "world_document_ingestion_source_control";
|
|
10
|
+
const DOCUMENT_KIND = "world_source_document";
|
|
11
|
+
const CHUNK_KIND = "world_source_chunk";
|
|
12
|
+
const SOURCE_DESCRIPTOR_ID = "__document_source_descriptor__";
|
|
13
|
+
const PARTIAL_ATTEMPT_PREFIX = "__document_partial_attempt__:";
|
|
14
|
+
const RETRY_COMPLETE_PREFIX = "__document_retry_complete__:";
|
|
15
|
+
const HARNESS_ACCEPTANCE_PREFIX = "__document_harness_acceptance__:";
|
|
16
|
+
const MAX_PARTIAL_ATTEMPTS = 3;
|
|
17
|
+
export const DOCUMENT_INGESTION_CHECKPOINT_IDS = Object.freeze({
|
|
18
|
+
sourceDescriptorId: SOURCE_DESCRIPTOR_ID,
|
|
19
|
+
partialAttemptPrefix: PARTIAL_ATTEMPT_PREFIX,
|
|
20
|
+
retryCompletePrefix: RETRY_COMPLETE_PREFIX,
|
|
21
|
+
harnessAcceptancePrefix: HARNESS_ACCEPTANCE_PREFIX,
|
|
22
|
+
maxPartialAttempts: MAX_PARTIAL_ATTEMPTS,
|
|
23
|
+
});
|
|
24
|
+
const GENERIC_ENTITY_LABEL = /^(?:大王|那怪|此怪|妖怪|行者|长老|师父|陛下|公主|国王|夫人|老者|童子|道人|菩萨|佛祖|魔王)$/u;
|
|
25
|
+
const METHOD_PAYLOADS = Object.freeze({
|
|
26
|
+
"legacy-v1": Object.freeze({ version: "legacy-v1", promptVersion: "document-graph-prompt-v1",
|
|
27
|
+
schemaVersion: "document_graph_segment_v1", mergeVersion: "evidenced-alias-union-v1", embeddingPolicy: "immediate" }),
|
|
28
|
+
"deferred-v2": Object.freeze({ version: "deferred-v2", promptVersion: "document-graph-prompt-v1",
|
|
29
|
+
schemaVersion: "document_graph_segment_v1", mergeVersion: "evidenced-alias-union-v1", embeddingPolicy: "deferred_review" }),
|
|
30
|
+
"adaptive-repair-v3": Object.freeze({ version: "adaptive-repair-v3", promptVersion: "document-graph-prompt-v1+adaptive-repair-v3",
|
|
31
|
+
schemaVersion: "document_graph_segment_v1", mergeVersion: "evidenced-alias-union-v1", embeddingPolicy: "deferred_review",
|
|
32
|
+
validatorDiagnosticsVersion: "document-graph-diagnostics-v3", repairPolicyVersion: "bounded-adaptive-repair-v3" }),
|
|
33
|
+
"semantic-negation-v4": Object.freeze({ version: "semantic-negation-v4", promptVersion: "document-graph-prompt-v2+semantic-negation-v4",
|
|
34
|
+
schemaVersion: "document_graph_segment_v1", mergeVersion: "evidenced-alias-union-v1+explicit-negation-edge-v4", embeddingPolicy: "deferred_review",
|
|
35
|
+
validatorDiagnosticsVersion: "document-graph-diagnostics-v4", repairPolicyVersion: "bounded-adaptive-repair-v3" }),
|
|
36
|
+
"harness-v1": Object.freeze({ version: "harness-v1", promptVersion: "document-graph-prompt-v2+harness-v1",
|
|
37
|
+
schemaVersion: "document_graph_segment_v1", mergeVersion: "evidenced-alias-union-v1+explicit-negation-edge-v4", embeddingPolicy: "deferred_review",
|
|
38
|
+
validatorDiagnosticsVersion: "document-graph-diagnostics-v4", repairPolicyVersion: "bounded-harness-repair-v1" }),
|
|
39
|
+
});
|
|
40
|
+
function sha256(value) {
|
|
41
|
+
return createHash("sha256").update(value).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
function stableJson(value) {
|
|
44
|
+
if (value === null || typeof value !== "object")
|
|
45
|
+
return JSON.stringify(value);
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
48
|
+
const record = value;
|
|
49
|
+
return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`).join(",")}}`;
|
|
50
|
+
}
|
|
51
|
+
function ingestionMethod(version) {
|
|
52
|
+
const payload = METHOD_PAYLOADS[version];
|
|
53
|
+
return Object.freeze({ ...payload, sha256: sha256(stableJson(payload)) });
|
|
54
|
+
}
|
|
55
|
+
function parseIngestionMethod(value, fallbackLegacy) {
|
|
56
|
+
if (value === undefined && fallbackLegacy)
|
|
57
|
+
return ingestionMethod("legacy-v1");
|
|
58
|
+
const candidate = record(value, "document_ingestion_descriptor_invalid");
|
|
59
|
+
const version = candidate.version;
|
|
60
|
+
if (version !== "legacy-v1" && version !== "deferred-v2" && version !== "adaptive-repair-v3" && version !== "semantic-negation-v4"
|
|
61
|
+
&& version !== "harness-v1") {
|
|
62
|
+
throw new Error("document_ingestion_descriptor_invalid");
|
|
63
|
+
}
|
|
64
|
+
const expected = ingestionMethod(version);
|
|
65
|
+
if (candidate.sha256 !== expected.sha256 || candidate.prompt_version !== expected.promptVersion
|
|
66
|
+
|| candidate.schema_version !== expected.schemaVersion || candidate.merge_version !== expected.mergeVersion
|
|
67
|
+
|| candidate.embedding_policy !== expected.embeddingPolicy
|
|
68
|
+
|| candidate.validator_diagnostics_version !== expected.validatorDiagnosticsVersion
|
|
69
|
+
|| candidate.repair_policy_version !== expected.repairPolicyVersion)
|
|
70
|
+
throw new Error("document_ingestion_descriptor_identity_mismatch");
|
|
71
|
+
return expected;
|
|
72
|
+
}
|
|
73
|
+
function id(prefix, ...parts) {
|
|
74
|
+
return `${prefix}:${sha256(stableJson(parts))}`;
|
|
75
|
+
}
|
|
76
|
+
function record(value, code) {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
78
|
+
throw new Error(code);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function asJson(value) {
|
|
82
|
+
return JSON.parse(JSON.stringify(value));
|
|
83
|
+
}
|
|
84
|
+
function sameBytes(left, right) {
|
|
85
|
+
return left.byteLength === right.byteLength && left.every((byte, index) => byte === right[index]);
|
|
86
|
+
}
|
|
87
|
+
export async function readDocumentIngestionCheckpoint(projectRoot, operationId, segmentId) {
|
|
88
|
+
const operationKey = sha256(operationId);
|
|
89
|
+
const segmentKey = sha256(segmentId);
|
|
90
|
+
const path = await safeProjectPath(projectRoot, `.worldengine/staging/document-ingestion/${operationKey}/segments/${segmentKey}.json`);
|
|
91
|
+
await assertSafeExistingProjectPath(projectRoot, path);
|
|
92
|
+
const envelope = record(JSON.parse(await readFile(path, "utf8")), "document_ingestion_checkpoint_invalid");
|
|
93
|
+
if (envelope.schema_version !== 1 || envelope.id !== segmentId || typeof envelope.payload_json !== "string"
|
|
94
|
+
|| typeof envelope.payload_sha256 !== "string" || sha256(envelope.payload_json) !== envelope.payload_sha256) {
|
|
95
|
+
throw new Error("document_ingestion_checkpoint_invalid");
|
|
96
|
+
}
|
|
97
|
+
return JSON.parse(envelope.payload_json);
|
|
98
|
+
}
|
|
99
|
+
/** descriptor 是输入元数据,不能算作一个已完成的模型窗口。 */
|
|
100
|
+
export async function readLosslessDocumentStatus(projectRoot, operationId) {
|
|
101
|
+
const status = await readDocumentIngestionJournalStatus(projectRoot, operationId);
|
|
102
|
+
const descriptor = await readStagedDocumentInput(projectRoot, operationId);
|
|
103
|
+
const completedSegmentIds = [];
|
|
104
|
+
for (const id of status.completedSegmentIds) {
|
|
105
|
+
if (id === SOURCE_DESCRIPTOR_ID || id.startsWith(PARTIAL_ATTEMPT_PREFIX) || id.startsWith(HARNESS_ACCEPTANCE_PREFIX))
|
|
106
|
+
continue;
|
|
107
|
+
if (id.startsWith(RETRY_COMPLETE_PREFIX)) {
|
|
108
|
+
const windowId = id.slice(RETRY_COMPLETE_PREFIX.length);
|
|
109
|
+
if (graphSegment(await readDocumentIngestionCheckpoint(projectRoot, operationId, id), windowId).status !== "complete") {
|
|
110
|
+
throw new Error("document_ingestion_checkpoint_invalid");
|
|
111
|
+
}
|
|
112
|
+
completedSegmentIds.push(windowId);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const value = await readDocumentIngestionCheckpoint(projectRoot, operationId, id);
|
|
116
|
+
const candidate = record(value, "document_ingestion_checkpoint_invalid");
|
|
117
|
+
if (candidate.status !== "partial" || status.method === undefined)
|
|
118
|
+
completedSegmentIds.push(id);
|
|
119
|
+
}
|
|
120
|
+
return Object.freeze({ ...status,
|
|
121
|
+
...(descriptor.input.sourcePath === undefined ? {} : { sourcePath: descriptor.input.sourcePath }),
|
|
122
|
+
...(descriptor.input.sourceStoragePath === undefined ? {} : { sourceStoragePath: descriptor.input.sourceStoragePath }),
|
|
123
|
+
completedSegmentIds: Object.freeze([...new Set(completedSegmentIds)].sort()) });
|
|
124
|
+
}
|
|
125
|
+
export async function resumeLosslessDocument(host, operationId, capability) {
|
|
126
|
+
const descriptor = await readStagedDocumentInput(capability.projectRoot, operationId);
|
|
127
|
+
if (capability.sourceOptions !== undefined && stableJson(capability.sourceOptions) !== stableJson(descriptor.sourceOptions)) {
|
|
128
|
+
throw new Error("document_ingestion_descriptor_identity_mismatch");
|
|
129
|
+
}
|
|
130
|
+
if (capability.methodVersion !== undefined && capability.methodVersion !== descriptor.method.version) {
|
|
131
|
+
throw new Error("document_ingestion_descriptor_identity_mismatch");
|
|
132
|
+
}
|
|
133
|
+
return ingestLosslessDocumentWithMethod(host, descriptor.input, { ...capability, ...(descriptor.sourceOptions === undefined ? {} : { sourceOptions: descriptor.sourceOptions }) }, descriptor.method);
|
|
134
|
+
}
|
|
135
|
+
/** 上传流或原文件已释放时,以 operationId 只读恢复 journal 中已校验的不可变原始输入。 */
|
|
136
|
+
export async function readStagedDocumentInput(projectRoot, operationId) {
|
|
137
|
+
if (typeof operationId !== "string" || operationId.length === 0 || operationId.length > 512 || operationId.trim() !== operationId
|
|
138
|
+
|| operationId.normalize("NFC") !== operationId || /[\u0000-\u001f\u007f]/u.test(operationId))
|
|
139
|
+
throw new Error("document_ingestion_operation_id_invalid");
|
|
140
|
+
const operationKey = sha256(operationId);
|
|
141
|
+
const segmentKey = sha256(SOURCE_DESCRIPTOR_ID);
|
|
142
|
+
const path = await safeProjectPath(projectRoot, `.worldengine/staging/document-ingestion/${operationKey}/segments/${segmentKey}.json`);
|
|
143
|
+
await assertSafeExistingProjectPath(projectRoot, path);
|
|
144
|
+
const envelope = record(JSON.parse(await readFile(path, "utf8")), "document_ingestion_descriptor_invalid");
|
|
145
|
+
if (envelope.schema_version !== 1 || envelope.id !== SOURCE_DESCRIPTOR_ID || typeof envelope.payload_json !== "string"
|
|
146
|
+
|| typeof envelope.payload_sha256 !== "string" || sha256(envelope.payload_json) !== envelope.payload_sha256)
|
|
147
|
+
throw new Error("document_ingestion_descriptor_invalid");
|
|
148
|
+
const payload = record(JSON.parse(envelope.payload_json), "document_ingestion_descriptor_invalid");
|
|
149
|
+
const storedInput = record(payload.input, "document_ingestion_descriptor_invalid");
|
|
150
|
+
// 已落盘的旧 checkpoint 只读兼容;新 descriptor 只写 snake_case,不重复保存全文。
|
|
151
|
+
const canonical = payload.schema_version === 1 || payload.schema_version === 2;
|
|
152
|
+
const encodedInput = canonical ? { rawBase64: storedInput.raw_base64, scopeId: storedInput.scope_id,
|
|
153
|
+
sourceId: storedInput.source_id, sourceRevision: storedInput.source_revision, sourcePath: storedInput.source_path,
|
|
154
|
+
sourceStoragePath: storedInput.source_storage_path } : storedInput;
|
|
155
|
+
const schemaVersion = canonical ? payload.schema_version : payload.schemaVersion;
|
|
156
|
+
const storedOperationId = canonical ? payload.operation_id : payload.operationId;
|
|
157
|
+
const planSha256 = canonical ? payload.plan_sha256 : payload.planSha256;
|
|
158
|
+
const sourceOptions = canonical ? payload.source_options : payload.sourceOptions;
|
|
159
|
+
if ((schemaVersion !== 1 && schemaVersion !== 2) || storedOperationId !== operationId || typeof planSha256 !== "string"
|
|
160
|
+
|| typeof encodedInput.rawBase64 !== "string" || typeof encodedInput.scopeId !== "string" || typeof encodedInput.sourceId !== "string"
|
|
161
|
+
|| typeof encodedInput.sourceRevision !== "string" || (encodedInput.sourcePath !== undefined && typeof encodedInput.sourcePath !== "string")
|
|
162
|
+
|| (encodedInput.sourceStoragePath !== undefined && typeof encodedInput.sourceStoragePath !== "string")) {
|
|
163
|
+
throw new Error("document_ingestion_descriptor_invalid");
|
|
164
|
+
}
|
|
165
|
+
const method = parseIngestionMethod(canonical ? payload.method : payload.method, schemaVersion === 1 && payload.method === undefined);
|
|
166
|
+
const bytes = Uint8Array.from(Buffer.from(encodedInput.rawBase64, "base64"));
|
|
167
|
+
const descriptor = Object.freeze({ schemaVersion, operationId,
|
|
168
|
+
input: Object.freeze({ bytes, scopeId: encodedInput.scopeId, sourceId: encodedInput.sourceId, sourceRevision: encodedInput.sourceRevision,
|
|
169
|
+
...(encodedInput.sourcePath === undefined ? {} : { sourcePath: encodedInput.sourcePath }),
|
|
170
|
+
...(encodedInput.sourceStoragePath === undefined ? {} : { sourceStoragePath: encodedInput.sourceStoragePath }) }),
|
|
171
|
+
...(sourceOptions === undefined ? {} : { sourceOptions: record(sourceOptions, "document_ingestion_descriptor_invalid") }),
|
|
172
|
+
planSha256, method });
|
|
173
|
+
const rebuilt = buildDocumentSource({ bytes: descriptor.input.bytes, scopeId: descriptor.input.scopeId, sourceId: descriptor.input.sourceId,
|
|
174
|
+
sourceRevision: descriptor.input.sourceRevision, ...(descriptor.sourceOptions === undefined ? {} : { options: descriptor.sourceOptions }) });
|
|
175
|
+
if (sourcePlan(rebuilt, descriptor.method).sha256 !== descriptor.planSha256 || operationIdentity(rebuilt, descriptor.method) !== operationId) {
|
|
176
|
+
throw new Error("document_ingestion_descriptor_identity_mismatch");
|
|
177
|
+
}
|
|
178
|
+
return descriptor;
|
|
179
|
+
}
|
|
180
|
+
function sourcePlan(source, method) {
|
|
181
|
+
const value = asJson({
|
|
182
|
+
schemaVersion: 1,
|
|
183
|
+
documentId: source.documentId,
|
|
184
|
+
windows: source.windows.map((window) => ({ id: window.id, coreUnitIds: window.coreUnitIds })),
|
|
185
|
+
...(method.version === "legacy-v1" ? {} : { method }),
|
|
186
|
+
});
|
|
187
|
+
return Object.freeze({ sha256: sha256(stableJson(value)), value });
|
|
188
|
+
}
|
|
189
|
+
function operationIdentity(source, method) {
|
|
190
|
+
const sourceIdentity = [source.scopeId, source.sourceId, source.sourceRevision, source.sourceSha256];
|
|
191
|
+
return method.version === "legacy-v1"
|
|
192
|
+
? `document-ingest:${sha256(stableJson(sourceIdentity))}`
|
|
193
|
+
: `document-ingest:v2:${sha256(stableJson([...sourceIdentity, method.sha256]))}`;
|
|
194
|
+
}
|
|
195
|
+
function adaptiveDiagnosticFingerprint(method, windowId, segment) {
|
|
196
|
+
const reasons = Object.keys(segment.diagnostics.rejectionReasons).sort();
|
|
197
|
+
const segmentationIssues = segment.diagnostics.segmentationIssues?.map((issue) => Object.freeze({ ...issue }));
|
|
198
|
+
const fingerprintPayload = { methodSha256: method.sha256, windowId, reasons,
|
|
199
|
+
segmentationIssues: segmentationIssues ?? [] };
|
|
200
|
+
return sha256(stableJson(fingerprintPayload));
|
|
201
|
+
}
|
|
202
|
+
function adaptivePreviousAttempt(method, source, windowId, segment, attempt, priorSegment) {
|
|
203
|
+
if (method.version !== "adaptive-repair-v3" && method.version !== "semantic-negation-v4" && method.version !== "harness-v1")
|
|
204
|
+
return undefined;
|
|
205
|
+
const window = source.windows.find((candidate) => candidate.id === windowId);
|
|
206
|
+
if (window === undefined)
|
|
207
|
+
throw new Error("document_graph_window_unknown");
|
|
208
|
+
const reasons = Object.keys(segment.diagnostics.rejectionReasons).sort();
|
|
209
|
+
const segmentationIssues = segment.diagnostics.segmentationIssues?.map((issue) => Object.freeze({ ...issue }));
|
|
210
|
+
const diagnosticFingerprint = adaptiveDiagnosticFingerprint(method, windowId, segment);
|
|
211
|
+
return Object.freeze({ attempt, diagnosticFingerprint,
|
|
212
|
+
repeatedDiagnosticFingerprint: priorSegment !== undefined && adaptiveDiagnosticFingerprint(method, windowId, priorSegment) === diagnosticFingerprint,
|
|
213
|
+
reasons: Object.freeze(reasons),
|
|
214
|
+
validLocalNodeIds: Object.freeze(segment.nodes.map((node) => node.localId).sort()),
|
|
215
|
+
allowedEvidenceUnitIds: Object.freeze([...window.contextUnitIds, ...window.coreUnitIds]),
|
|
216
|
+
...(segmentationIssues === undefined ? {} : { segmentationIssues: Object.freeze(segmentationIssues) }) });
|
|
217
|
+
}
|
|
218
|
+
function markerId(scopeId, sourceId) {
|
|
219
|
+
return id("world-document-source-control", scopeId, sourceId);
|
|
220
|
+
}
|
|
221
|
+
async function readMarker(host, scopeId, sourceId) {
|
|
222
|
+
const expectedId = markerId(scopeId, sourceId);
|
|
223
|
+
const response = record(await host.dispatchTool({ name: "world_get", input: { ids: [expectedId], include_relations: true } }), "document_ingestion_world_get_invalid");
|
|
224
|
+
if (!Array.isArray(response.entities))
|
|
225
|
+
throw new Error("document_ingestion_world_get_invalid");
|
|
226
|
+
const entity = response.entities.map((value) => record(value, "document_ingestion_marker_invalid"))
|
|
227
|
+
.find((value) => value.id === expectedId);
|
|
228
|
+
if (entity === undefined)
|
|
229
|
+
return null;
|
|
230
|
+
if (entity.kind !== CONTROL_KIND)
|
|
231
|
+
throw new Error("document_ingestion_marker_kind_mismatch");
|
|
232
|
+
const props = record(entity.props, "document_ingestion_marker_invalid");
|
|
233
|
+
if (props.scope_id !== scopeId || props.source_id !== sourceId || typeof props.operation_id !== "string" || typeof props.source_revision !== "string"
|
|
234
|
+
|| typeof props.source_sha256 !== "string" || !Array.isArray(props.managed_entity_ids)
|
|
235
|
+
|| !Array.isArray(props.managed_relations) || (props.source_path !== undefined && typeof props.source_path !== "string")
|
|
236
|
+
|| (props.source_storage_path !== undefined && typeof props.source_storage_path !== "string"))
|
|
237
|
+
throw new Error("document_ingestion_marker_invalid");
|
|
238
|
+
const managedEntityIds = props.managed_entity_ids.map((value) => {
|
|
239
|
+
if (typeof value !== "string")
|
|
240
|
+
throw new Error("document_ingestion_marker_invalid");
|
|
241
|
+
return value;
|
|
242
|
+
});
|
|
243
|
+
const managedRelations = props.managed_relations.map((value) => {
|
|
244
|
+
const relation = record(value, "document_ingestion_marker_invalid");
|
|
245
|
+
if (typeof relation.src !== "string" || typeof relation.dst !== "string" || typeof relation.kind !== "string") {
|
|
246
|
+
throw new Error("document_ingestion_marker_invalid");
|
|
247
|
+
}
|
|
248
|
+
return Object.freeze({ src: relation.src, dst: relation.dst, kind: relation.kind });
|
|
249
|
+
});
|
|
250
|
+
let commitSummary;
|
|
251
|
+
if (props.commit_summary !== undefined) {
|
|
252
|
+
const summary = record(props.commit_summary, "document_ingestion_marker_invalid");
|
|
253
|
+
const embedding = record(summary.embedding_status, "document_ingestion_marker_invalid");
|
|
254
|
+
if ((summary.graph_status !== "partial" && summary.graph_status !== "complete")
|
|
255
|
+
|| !Number.isSafeInteger(summary.processed_windows) || !Number.isSafeInteger(summary.node_count) || !Number.isSafeInteger(summary.edge_count)
|
|
256
|
+
|| (embedding.status !== "pending" && embedding.status !== "complete") || typeof embedding.providerId !== "string" || typeof embedding.modelId !== "string"
|
|
257
|
+
|| !Number.isSafeInteger(embedding.dimensions) || !Number.isSafeInteger(summary.previous_project_revision)
|
|
258
|
+
|| !Number.isSafeInteger(summary.next_project_revision))
|
|
259
|
+
throw new Error("document_ingestion_marker_invalid");
|
|
260
|
+
const embeddingStatus = Object.freeze({ status: embedding.status, providerId: embedding.providerId,
|
|
261
|
+
modelId: embedding.modelId, dimensions: embedding.dimensions,
|
|
262
|
+
hits: Number.isSafeInteger(embedding.hits) ? embedding.hits : 0,
|
|
263
|
+
misses: Number.isSafeInteger(embedding.misses) ? embedding.misses : 0,
|
|
264
|
+
remoteTexts: Number.isSafeInteger(embedding.remoteTexts) ? embedding.remoteTexts : 0,
|
|
265
|
+
writes: Number.isSafeInteger(embedding.writes) ? embedding.writes : 0 });
|
|
266
|
+
commitSummary = Object.freeze({ graphStatus: summary.graph_status, processedWindows: summary.processed_windows,
|
|
267
|
+
nodeCount: summary.node_count, edgeCount: summary.edge_count,
|
|
268
|
+
embeddingStatus,
|
|
269
|
+
previousProjectRevision: summary.previous_project_revision, nextProjectRevision: summary.next_project_revision });
|
|
270
|
+
}
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
id: expectedId, operationId: props.operation_id,
|
|
273
|
+
sourceRevision: props.source_revision,
|
|
274
|
+
sourceSha256: props.source_sha256,
|
|
275
|
+
...(props.source_path === undefined ? {} : { sourcePath: props.source_path }),
|
|
276
|
+
...(props.source_storage_path === undefined ? {} : { sourceStoragePath: props.source_storage_path }),
|
|
277
|
+
managedEntityIds: Object.freeze(managedEntityIds),
|
|
278
|
+
managedRelations: Object.freeze(managedRelations),
|
|
279
|
+
...(commitSummary === undefined ? {} : { commitSummary }),
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
async function readManagedSnapshot(host, marker) {
|
|
283
|
+
if (marker === null || marker.managedEntityIds.length === 0)
|
|
284
|
+
return Object.freeze({
|
|
285
|
+
entities: new Map(),
|
|
286
|
+
externalReferenceEntityIds: new Set(),
|
|
287
|
+
});
|
|
288
|
+
const response = record(await host.dispatchTool({ name: "world_get", input: { ids: marker.managedEntityIds, include_relations: true } }), "document_ingestion_world_get_invalid");
|
|
289
|
+
if (!Array.isArray(response.entities) || !Array.isArray(response.relations))
|
|
290
|
+
throw new Error("document_ingestion_world_get_invalid");
|
|
291
|
+
const entities = new Map();
|
|
292
|
+
for (const value of response.entities) {
|
|
293
|
+
const entity = record(value, "document_ingestion_managed_entity_invalid");
|
|
294
|
+
if (typeof entity.id !== "string" || typeof entity.kind !== "string")
|
|
295
|
+
throw new Error("document_ingestion_managed_entity_invalid");
|
|
296
|
+
entities.set(entity.id, Object.freeze({ id: entity.id, kind: entity.kind, props: record(entity.props, "document_ingestion_managed_entity_invalid") }));
|
|
297
|
+
}
|
|
298
|
+
const ownedRelations = new Set(marker.managedRelations.map(relationKey));
|
|
299
|
+
const managedIds = new Set(marker.managedEntityIds);
|
|
300
|
+
const externalReferenceEntityIds = new Set();
|
|
301
|
+
for (const value of response.relations) {
|
|
302
|
+
const relation = record(value, "document_ingestion_managed_relation_invalid");
|
|
303
|
+
if (typeof relation.src !== "string" || typeof relation.dst !== "string" || typeof relation.kind !== "string")
|
|
304
|
+
throw new Error("document_ingestion_managed_relation_invalid");
|
|
305
|
+
if (ownedRelations.has(relationKey(relation)))
|
|
306
|
+
continue;
|
|
307
|
+
if (managedIds.has(relation.src))
|
|
308
|
+
externalReferenceEntityIds.add(relation.src);
|
|
309
|
+
if (managedIds.has(relation.dst))
|
|
310
|
+
externalReferenceEntityIds.add(relation.dst);
|
|
311
|
+
}
|
|
312
|
+
return Object.freeze({ entities, externalReferenceEntityIds });
|
|
313
|
+
}
|
|
314
|
+
export function graphSegment(value, windowId) {
|
|
315
|
+
const candidate = record(value, "document_ingestion_checkpoint_invalid");
|
|
316
|
+
if (candidate.windowId !== windowId || (candidate.status !== "complete" && candidate.status !== "partial")
|
|
317
|
+
|| !Array.isArray(candidate.segments) || !Array.isArray(candidate.nodes) || !Array.isArray(candidate.edges)
|
|
318
|
+
|| typeof candidate.diagnostics !== "object" || candidate.diagnostics === null) {
|
|
319
|
+
throw new Error("document_ingestion_checkpoint_invalid");
|
|
320
|
+
}
|
|
321
|
+
return candidate;
|
|
322
|
+
}
|
|
323
|
+
function errorCode(error) {
|
|
324
|
+
const candidate = error;
|
|
325
|
+
const code = typeof candidate.code === "string" && /^[a-z0-9_:-]{1,128}$/u.test(candidate.code)
|
|
326
|
+
? candidate.code
|
|
327
|
+
: error instanceof Error && /^[a-z0-9_:-]{1,128}$/u.test(error.message) ? error.message : "document_window_failed";
|
|
328
|
+
return Object.freeze({ code, retryable: candidate.retryable === true });
|
|
329
|
+
}
|
|
330
|
+
class UnionFind {
|
|
331
|
+
parent = new Map();
|
|
332
|
+
add(value) { if (!this.parent.has(value))
|
|
333
|
+
this.parent.set(value, value); }
|
|
334
|
+
find(value) {
|
|
335
|
+
const parent = this.parent.get(value);
|
|
336
|
+
if (parent === undefined)
|
|
337
|
+
throw new Error("document_entity_union_unknown");
|
|
338
|
+
if (parent === value)
|
|
339
|
+
return value;
|
|
340
|
+
const root = this.find(parent);
|
|
341
|
+
this.parent.set(value, root);
|
|
342
|
+
return root;
|
|
343
|
+
}
|
|
344
|
+
union(left, right) {
|
|
345
|
+
const a = this.find(left), b = this.find(right);
|
|
346
|
+
if (a !== b)
|
|
347
|
+
this.parent.set(a.localeCompare(b) <= 0 ? b : a, a.localeCompare(b) <= 0 ? a : b);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function evidencedAliases(candidate) {
|
|
351
|
+
const evidence = candidate.node.statements.map((statement) => statement.evidence.text);
|
|
352
|
+
return new Set(candidate.node.aliases.filter((alias) => evidence.some((text) => text.includes(candidate.node.label) && text.includes(alias))));
|
|
353
|
+
}
|
|
354
|
+
function mergeEntities(source, segments) {
|
|
355
|
+
const candidates = segments.flatMap((segment) => segment.nodes.map((node) => Object.freeze({ key: `${segment.windowId}\0${node.localId}`, windowId: segment.windowId, node })));
|
|
356
|
+
const unions = new UnionFind();
|
|
357
|
+
candidates.forEach((candidate) => unions.add(candidate.key));
|
|
358
|
+
const aliases = new Map(candidates.map((candidate) => [candidate.key, evidencedAliases(candidate)]));
|
|
359
|
+
for (let leftIndex = 0; leftIndex < candidates.length; leftIndex += 1) {
|
|
360
|
+
for (let rightIndex = leftIndex + 1; rightIndex < candidates.length; rightIndex += 1) {
|
|
361
|
+
const left = candidates[leftIndex], right = candidates[rightIndex];
|
|
362
|
+
if (left.node.kind !== right.node.kind)
|
|
363
|
+
continue;
|
|
364
|
+
const explicit = aliases.get(left.key).has(right.node.label) || aliases.get(right.key).has(left.node.label)
|
|
365
|
+
|| [...aliases.get(left.key)].some((alias) => aliases.get(right.key).has(alias));
|
|
366
|
+
if (explicit)
|
|
367
|
+
unions.union(left.key, right.key);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
const groups = new Map();
|
|
371
|
+
for (const candidate of candidates) {
|
|
372
|
+
const root = unions.find(candidate.key);
|
|
373
|
+
const group = groups.get(root) ?? [];
|
|
374
|
+
group.push(candidate);
|
|
375
|
+
groups.set(root, group);
|
|
376
|
+
}
|
|
377
|
+
const entities = [...groups.values()].map((group) => {
|
|
378
|
+
const ordered = [...group].sort((a, b) => a.key.localeCompare(b.key));
|
|
379
|
+
const labels = new Set(ordered.flatMap((candidate) => [candidate.node.label, ...candidate.node.aliases]));
|
|
380
|
+
const primary = ordered.map((candidate) => candidate.node.label).sort((a, b) => a.localeCompare(b, "zh-Hans"))[0];
|
|
381
|
+
const identityParts = [source.scopeId, source.sourceId, ordered[0].node.kind, ...[...labels].sort()];
|
|
382
|
+
if (ordered.length === 1 || GENERIC_ENTITY_LABEL.test(primary))
|
|
383
|
+
identityParts.push(...ordered.map((candidate) => candidate.key));
|
|
384
|
+
return Object.freeze({
|
|
385
|
+
id: id("world-document-entity", ...identityParts),
|
|
386
|
+
label: primary,
|
|
387
|
+
kind: ordered[0].node.kind,
|
|
388
|
+
aliases: Object.freeze([...labels].filter((label) => label !== primary).sort((a, b) => a.localeCompare(b, "zh-Hans"))),
|
|
389
|
+
statements: Object.freeze(ordered.flatMap((candidate) => candidate.node.statements)),
|
|
390
|
+
candidates: Object.freeze(ordered),
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
const localToStable = new Map();
|
|
394
|
+
for (const entity of entities)
|
|
395
|
+
for (const candidate of entity.candidates)
|
|
396
|
+
localToStable.set(candidate.key, entity.id);
|
|
397
|
+
return Object.freeze({ entities: Object.freeze(entities), localToStable });
|
|
398
|
+
}
|
|
399
|
+
export function knownDocumentGraphEntities(source, segments) {
|
|
400
|
+
return mergeEntities(source, segments).entities.map((entity) => Object.freeze({
|
|
401
|
+
stableId: entity.id, label: entity.label, kind: entity.kind, aliases: entity.aliases,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
404
|
+
function relationKey(value) {
|
|
405
|
+
return `${value.src}\0${value.dst}\0${value.kind}`;
|
|
406
|
+
}
|
|
407
|
+
/** 旧 immutable checkpoint 只存聚合 quote;装配时由原始 units 补齐精确片段,禁止填补证据间隙。 */
|
|
408
|
+
function documentEvidenceResolver(source) {
|
|
409
|
+
const units = new Map(source.units.map((unit) => [unit.id, unit]));
|
|
410
|
+
const resolved = new Map();
|
|
411
|
+
return (evidence) => {
|
|
412
|
+
const cached = resolved.get(evidence);
|
|
413
|
+
if (cached !== undefined)
|
|
414
|
+
return cached;
|
|
415
|
+
if (!Array.isArray(evidence.unitIds) || evidence.unitIds.length === 0)
|
|
416
|
+
throw new Error("document_ingestion_evidence_units_invalid");
|
|
417
|
+
let previousEnd = -1;
|
|
418
|
+
const seen = new Set();
|
|
419
|
+
const spans = evidence.unitIds.map((unitId) => {
|
|
420
|
+
const unit = units.get(unitId);
|
|
421
|
+
if (unit === undefined || seen.has(unitId) || unit.startOffset < previousEnd)
|
|
422
|
+
throw new Error("document_ingestion_evidence_units_invalid");
|
|
423
|
+
seen.add(unitId);
|
|
424
|
+
previousEnd = unit.endOffset;
|
|
425
|
+
return Object.freeze({ unit_id: unit.id, quote: unit.text, start_offset: unit.startOffset, end_offset: unit.endOffset });
|
|
426
|
+
});
|
|
427
|
+
const quote = spans.map((span) => span.quote).join("");
|
|
428
|
+
if (quote !== evidence.text || spans[0].start_offset !== evidence.startOffset || spans.at(-1).end_offset !== evidence.endOffset) {
|
|
429
|
+
throw new Error("document_ingestion_evidence_source_mismatch");
|
|
430
|
+
}
|
|
431
|
+
const canonical = Object.freeze({ unit_ids: Object.freeze([...evidence.unitIds]), quote,
|
|
432
|
+
start_offset: evidence.startOffset, end_offset: evidence.endOffset, offset_semantics: "envelope_only", spans: Object.freeze(spans) });
|
|
433
|
+
resolved.set(evidence, canonical);
|
|
434
|
+
return canonical;
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async function assemble(source, planSha256, segments, cache, signal, sourcePath, sourceStoragePath) {
|
|
438
|
+
const semanticPlan = Object.freeze({
|
|
439
|
+
schemaVersion: 1,
|
|
440
|
+
documentId: source.documentId,
|
|
441
|
+
segments: Object.freeze(segments.flatMap((segment) => segment.segments.map((item) => Object.freeze({ ...item })))),
|
|
442
|
+
});
|
|
443
|
+
const plan = validateDocumentPlan(source, semanticPlan);
|
|
444
|
+
const resolveEvidence = documentEvidenceResolver(source);
|
|
445
|
+
const evidenceProps = (evidence) => asJson(resolveEvidence(evidence));
|
|
446
|
+
const merged = mergeEntities(source, segments);
|
|
447
|
+
const documentId = id("world-source-document", source.scopeId, source.sourceId, source.sourceRevision, source.sourceSha256);
|
|
448
|
+
const chunkIds = plan.segments.map((chunk) => id("world-source-chunk", source.scopeId, source.sourceId, chunk.startUnitId, chunk.endUnitId));
|
|
449
|
+
const chunkEmbeddingText = plan.segments.map((chunk) => chunk.indexText.normalize("NFC").trim());
|
|
450
|
+
const semanticIndexText = new Map(merged.entities.map((entity) => [entity.id,
|
|
451
|
+
[entity.label, ...entity.aliases, entity.kind].join("\n").normalize("NFC").trim()]));
|
|
452
|
+
const statementNodes = merged.entities.flatMap((entity) => entity.statements.map((statement) => ({
|
|
453
|
+
id: id("world-document-statement", source.scopeId, source.sourceId, entity.id, stableJson(statement)),
|
|
454
|
+
entityId: entity.id,
|
|
455
|
+
statement,
|
|
456
|
+
evidence: resolveEvidence(statement.evidence),
|
|
457
|
+
indexText: [entity.label, statement.assertion, statement.evidence.text].join("\n").normalize("NFC").trim(),
|
|
458
|
+
})));
|
|
459
|
+
const edgeAssertionNodes = segments.flatMap((segment) => segment.edges.map((edge) => {
|
|
460
|
+
const src = merged.localToStable.get(`${segment.windowId}\0${edge.sourceLocalId}`);
|
|
461
|
+
const dst = merged.localToStable.get(`${segment.windowId}\0${edge.targetLocalId}`);
|
|
462
|
+
if (src === undefined || dst === undefined)
|
|
463
|
+
throw new Error("document_ingestion_edge_endpoint_missing");
|
|
464
|
+
return { id: id("world-document-edge-assertion", source.scopeId, source.sourceId, src, dst, stableJson(edge)), src, dst, edge, evidence: resolveEvidence(edge.evidence),
|
|
465
|
+
indexText: [edge.assertion, edge.evidence.text].join("\n").normalize("NFC").trim() };
|
|
466
|
+
}));
|
|
467
|
+
const searchable = Object.freeze([
|
|
468
|
+
...plan.segments.flatMap((_chunk, index) => chunkEmbeddingText[index].length === 0 ? [] : [{ id: chunkIds[index], kind: "chunk", text: chunkEmbeddingText[index] }]),
|
|
469
|
+
...merged.entities.map((entity) => ({
|
|
470
|
+
id: entity.id,
|
|
471
|
+
kind: "entity",
|
|
472
|
+
text: semanticIndexText.get(entity.id),
|
|
473
|
+
})),
|
|
474
|
+
...statementNodes.map((statement) => ({ id: statement.id, kind: "statement", text: statement.indexText })),
|
|
475
|
+
...edgeAssertionNodes.map((assertion) => ({ id: assertion.id, kind: "edge_assertion", text: assertion.indexText })),
|
|
476
|
+
]);
|
|
477
|
+
const vectors = cache === undefined || searchable.length === 0 ? [] : await cache.embed(searchable.map((entry) => entry.text), signal === undefined ? {} : { signal });
|
|
478
|
+
if (cache === undefined && vectors.length !== 0)
|
|
479
|
+
throw new Error("document_ingestion_embedding_unexpected");
|
|
480
|
+
if (cache !== undefined && vectors.length !== searchable.length)
|
|
481
|
+
throw new Error("document_ingestion_embedding_count_invalid");
|
|
482
|
+
const vectorById = new Map(searchable.map((entry, index) => [entry.id, vectors[index]]));
|
|
483
|
+
const embeddingProps = (entityId) => cache === undefined
|
|
484
|
+
? {}
|
|
485
|
+
: { embedding: vectorById.get(entityId), embedding_provider_id: cache.providerId,
|
|
486
|
+
embedding_model_id: cache.modelId, embedding_dimensions: cache.dimensions };
|
|
487
|
+
const entities = [{
|
|
488
|
+
type: "upsert_entity", id: documentId, kind: DOCUMENT_KIND,
|
|
489
|
+
props: asJson({
|
|
490
|
+
scope_id: source.scopeId, source_id: source.sourceId, source_revision: source.sourceRevision,
|
|
491
|
+
source_path: sourcePath ?? null, raw_base64: source.originalBytesBase64, byte_sha256: source.sourceSha256,
|
|
492
|
+
source_storage_path: sourceStoragePath ?? null,
|
|
493
|
+
raw_byte_length: source.byteLength, utf16_length: source.utf16Length, encoding: "utf-8",
|
|
494
|
+
utf8_bom: source.utf8Bom, offset_domain: source.offsetDomain, plan_digest: planSha256,
|
|
495
|
+
source_revision_active: true, retrieval_excluded: true,
|
|
496
|
+
}),
|
|
497
|
+
}];
|
|
498
|
+
for (const [index, chunk] of plan.segments.entries())
|
|
499
|
+
entities.push({
|
|
500
|
+
type: "upsert_entity", id: chunkIds[index], kind: CHUNK_KIND,
|
|
501
|
+
props: asJson({
|
|
502
|
+
scope_id: source.scopeId, source_id: source.sourceId, source_revision: source.sourceRevision,
|
|
503
|
+
document_id: documentId, ordinal: index, title: chunk.title, start_unit_id: chunk.startUnitId,
|
|
504
|
+
end_unit_id: chunk.endUnitId, start_offset: chunk.startOffset, end_offset: chunk.endOffset,
|
|
505
|
+
index_text: chunk.indexText, content_sha256: sha256(new TextEncoder().encode(chunk.indexText)),
|
|
506
|
+
source_revision_active: true,
|
|
507
|
+
...(chunkEmbeddingText[index].length === 0 ? { retrieval_excluded: true } : embeddingProps(chunkIds[index])),
|
|
508
|
+
}),
|
|
509
|
+
});
|
|
510
|
+
for (const entity of merged.entities)
|
|
511
|
+
entities.push({
|
|
512
|
+
type: "upsert_entity", id: entity.id, kind: `world_document_${entity.kind}`,
|
|
513
|
+
props: asJson({
|
|
514
|
+
scope_id: source.scopeId, source_id: source.sourceId, source_revision: source.sourceRevision,
|
|
515
|
+
label: entity.label, aliases: entity.aliases, statements: entity.statements.map((statement) => ({ ...statement, evidence: resolveEvidence(statement.evidence) })),
|
|
516
|
+
index_text: semanticIndexText.get(entity.id), source_revision_active: true, ...embeddingProps(entity.id),
|
|
517
|
+
}),
|
|
518
|
+
});
|
|
519
|
+
for (const item of statementNodes)
|
|
520
|
+
entities.push({ type: "upsert_entity", id: item.id, kind: "world_document_statement", props: asJson({
|
|
521
|
+
scope_id: source.scopeId, source_id: source.sourceId, source_revision: source.sourceRevision, subject_entity_id: item.entityId,
|
|
522
|
+
predicate: item.statement.predicate, assertion: item.statement.assertion, value: item.statement.value,
|
|
523
|
+
qualifiers: item.statement.qualifiers, evidence: item.evidence, index_text: item.indexText,
|
|
524
|
+
source_revision_active: true, ...embeddingProps(item.id),
|
|
525
|
+
}) });
|
|
526
|
+
for (const item of edgeAssertionNodes)
|
|
527
|
+
entities.push({ type: "upsert_entity", id: item.id, kind: "world_document_edge_assertion", props: asJson({
|
|
528
|
+
scope_id: source.scopeId, source_id: source.sourceId, source_revision: source.sourceRevision, source_entity_id: item.src,
|
|
529
|
+
target_entity_id: item.dst, predicate: item.edge.predicate, assertion: item.edge.assertion, qualifiers: item.edge.qualifiers,
|
|
530
|
+
evidence: item.evidence, index_text: item.indexText, source_revision_active: true, ...embeddingProps(item.id),
|
|
531
|
+
}) });
|
|
532
|
+
const relations = new Map();
|
|
533
|
+
const addRelation = (relation) => relations.set(relationKey(relation), relation);
|
|
534
|
+
plan.segments.forEach((_chunk, index) => {
|
|
535
|
+
addRelation({ type: "upsert_relation", src: chunkIds[index], dst: documentId, kind: "part_of", props: asJson({ ordinal: index }) });
|
|
536
|
+
if (index > 0)
|
|
537
|
+
addRelation({ type: "upsert_relation", src: chunkIds[index - 1], dst: chunkIds[index], kind: "next_chunk", props: {} });
|
|
538
|
+
});
|
|
539
|
+
const chunksForEvidence = (evidence) => {
|
|
540
|
+
const spans = resolveEvidence(evidence).spans;
|
|
541
|
+
const ids = plan.segments.flatMap((chunk, index) => spans.some((span) => chunk.startOffset < span.end_offset && chunk.endOffset > span.start_offset) ? [chunkIds[index]] : []);
|
|
542
|
+
if (ids.length === 0)
|
|
543
|
+
throw new Error("document_ingestion_evidence_chunk_missing");
|
|
544
|
+
return ids;
|
|
545
|
+
};
|
|
546
|
+
for (const entity of merged.entities)
|
|
547
|
+
for (const statement of entity.statements) {
|
|
548
|
+
const statementId = id("world-document-statement", source.scopeId, source.sourceId, entity.id, stableJson(statement));
|
|
549
|
+
addRelation({ type: "upsert_relation", src: statementId, dst: entity.id, kind: "describes", props: {} });
|
|
550
|
+
for (const chunkId of chunksForEvidence(statement.evidence)) {
|
|
551
|
+
addRelation({ type: "upsert_relation", src: chunkId, dst: entity.id, kind: "mentions", props: evidenceProps(statement.evidence) });
|
|
552
|
+
addRelation({ type: "upsert_relation", src: chunkId, dst: statementId, kind: "supports", props: evidenceProps(statement.evidence) });
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
for (const segment of segments)
|
|
556
|
+
for (const edge of segment.edges) {
|
|
557
|
+
const src = merged.localToStable.get(`${segment.windowId}\0${edge.sourceLocalId}`);
|
|
558
|
+
const dst = merged.localToStable.get(`${segment.windowId}\0${edge.targetLocalId}`);
|
|
559
|
+
if (src === undefined || dst === undefined)
|
|
560
|
+
throw new Error("document_ingestion_edge_endpoint_missing");
|
|
561
|
+
addRelation({ type: "upsert_relation", src, dst, kind: edge.predicate, props: asJson({ assertion: edge.assertion, qualifiers: edge.qualifiers,
|
|
562
|
+
evidence: evidenceProps(edge.evidence), source_chunk_ids: chunksForEvidence(edge.evidence) }) });
|
|
563
|
+
const assertionId = id("world-document-edge-assertion", source.scopeId, source.sourceId, src, dst, stableJson(edge));
|
|
564
|
+
addRelation({ type: "upsert_relation", src: assertionId, dst: src, kind: "describes_source", props: {} });
|
|
565
|
+
addRelation({ type: "upsert_relation", src: assertionId, dst, kind: "describes_target", props: {} });
|
|
566
|
+
for (const chunkId of chunksForEvidence(edge.evidence)) {
|
|
567
|
+
addRelation({ type: "upsert_relation", src: chunkId, dst: assertionId, kind: "supports", props: evidenceProps(edge.evidence) });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
return Object.freeze({
|
|
571
|
+
operations: Object.freeze([...entities, ...relations.values()]),
|
|
572
|
+
entityIds: Object.freeze(entities.filter((operation) => operation.type === "upsert_entity").map((operation) => operation.id)),
|
|
573
|
+
relations: Object.freeze([...relations.values()].map(({ src, dst, kind }) => Object.freeze({ src, dst, kind }))),
|
|
574
|
+
nodeCount: entities.length,
|
|
575
|
+
edgeCount: relations.size,
|
|
576
|
+
plan,
|
|
577
|
+
embeddingTexts: searchable,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
export async function prepareDocumentGraphEmbedding(projectRoot, operationId) {
|
|
581
|
+
const descriptor = await readStagedDocumentInput(projectRoot, operationId);
|
|
582
|
+
const rawStatus = await readDocumentIngestionJournalStatus(projectRoot, operationId);
|
|
583
|
+
const status = await readLosslessDocumentStatus(projectRoot, operationId);
|
|
584
|
+
if (status.phase !== "committed")
|
|
585
|
+
throw new Error("document_ingestion_graph_not_ready");
|
|
586
|
+
const journalMethod = status.method === undefined ? ingestionMethod("legacy-v1") : status.method;
|
|
587
|
+
if (status.sourceByteSha256 !== sha256(descriptor.input.bytes) || status.planSha256 !== descriptor.planSha256
|
|
588
|
+
|| stableJson(journalMethod) !== stableJson(descriptor.method))
|
|
589
|
+
throw new Error("document_ingestion_descriptor_identity_mismatch");
|
|
590
|
+
const source = buildDocumentSource({ bytes: descriptor.input.bytes, scopeId: descriptor.input.scopeId, sourceId: descriptor.input.sourceId,
|
|
591
|
+
sourceRevision: descriptor.input.sourceRevision, ...(descriptor.sourceOptions === undefined ? {} : { options: descriptor.sourceOptions }) });
|
|
592
|
+
const segments = [];
|
|
593
|
+
for (const window of source.windows) {
|
|
594
|
+
if (!status.completedSegmentIds.includes(window.id))
|
|
595
|
+
throw new Error("document_ingestion_graph_not_ready");
|
|
596
|
+
const retryCompleteId = `${RETRY_COMPLETE_PREFIX}${window.id}`;
|
|
597
|
+
const segmentId = rawStatus.completedSegmentIds.includes(retryCompleteId) ? retryCompleteId : window.id;
|
|
598
|
+
const segment = graphSegment(await readDocumentIngestionCheckpoint(projectRoot, operationId, segmentId), window.id);
|
|
599
|
+
if (segment.status !== "complete")
|
|
600
|
+
throw new Error("document_ingestion_graph_not_ready");
|
|
601
|
+
segments.push(segment);
|
|
602
|
+
}
|
|
603
|
+
const assembled = await assemble(source, descriptor.planSha256, segments, undefined, undefined, descriptor.input.sourcePath, descriptor.input.sourceStoragePath);
|
|
604
|
+
const texts = Object.freeze(assembled.embeddingTexts.map((entry) => Object.freeze({ ...entry })));
|
|
605
|
+
return Object.freeze({
|
|
606
|
+
schemaVersion: 1,
|
|
607
|
+
operationId,
|
|
608
|
+
method: descriptor.method,
|
|
609
|
+
sourceIdentity: Object.freeze({ scopeId: source.scopeId, sourceId: source.sourceId, sourceRevision: source.sourceRevision, sourceSha256: source.sourceSha256 }),
|
|
610
|
+
planSha256: descriptor.planSha256,
|
|
611
|
+
graphSha256: sha256(stableJson({ methodSha256: descriptor.method.sha256, planSha256: descriptor.planSha256, texts })),
|
|
612
|
+
graphStatus: "complete",
|
|
613
|
+
texts,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function emptyEmbeddingStatus(connection) {
|
|
617
|
+
return Object.freeze({ status: "pending", providerId: connection.embedding.providerId, modelId: connection.embedding.modelId,
|
|
618
|
+
dimensions: connection.embedding.dimensions, hits: 0, misses: 0, remoteTexts: 0, writes: 0 });
|
|
619
|
+
}
|
|
620
|
+
function receiptFromStored(value) {
|
|
621
|
+
const candidate = record(value, "document_ingestion_receipt_invalid");
|
|
622
|
+
if (candidate.schemaVersion !== 1 || typeof candidate.operationId !== "string"
|
|
623
|
+
|| (candidate.status !== "committed" && candidate.status !== "already_committed"))
|
|
624
|
+
throw new Error("document_ingestion_receipt_invalid");
|
|
625
|
+
return Object.freeze({ ...candidate, status: "already_committed" });
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* 全文导入的唯一编排入口。模型与 embedding 结果先进入 journal/cache;只有所有窗口
|
|
629
|
+
* 都有已校验 checkpoint 时,才以最初捕获的 revision 做一次原子 world_apply。
|
|
630
|
+
*/
|
|
631
|
+
export async function ingestLosslessDocument(host, input, capability) {
|
|
632
|
+
return ingestLosslessDocumentWithMethod(host, input, capability, ingestionMethod(capability.methodVersion ?? "semantic-negation-v4"));
|
|
633
|
+
}
|
|
634
|
+
async function ingestLosslessDocumentWithMethod(host, input, capability, method) {
|
|
635
|
+
const dependencies = {
|
|
636
|
+
extractWindow: capability.dependencies?.extractWindow ?? extractDocumentGraphWindow,
|
|
637
|
+
openJournal: capability.dependencies?.openJournal ?? openDocumentIngestionJournal,
|
|
638
|
+
createEmbeddingCache: capability.dependencies?.createEmbeddingCache ?? createCachedEmbeddingPort,
|
|
639
|
+
};
|
|
640
|
+
const observed = await host.inspect();
|
|
641
|
+
if (!Number.isSafeInteger(observed.project_revision) || observed.project_revision < 0)
|
|
642
|
+
throw new Error("document_ingestion_revision_invalid");
|
|
643
|
+
const marker = await readMarker(host, input.scopeId, input.sourceId);
|
|
644
|
+
const source = buildDocumentSource({ bytes: input.bytes, sourceId: input.sourceId, sourceRevision: input.sourceRevision, scopeId: input.scopeId,
|
|
645
|
+
...(capability.sourceOptions === undefined ? {} : { options: capability.sourceOptions }) });
|
|
646
|
+
const roundtripVerified = sameBytes(documentSourceBytes(source), input.bytes);
|
|
647
|
+
if (!roundtripVerified)
|
|
648
|
+
throw new Error("document_ingestion_roundtrip_failed");
|
|
649
|
+
if (marker?.sourceRevision === input.sourceRevision && marker.sourceSha256 !== source.sourceSha256) {
|
|
650
|
+
throw new Error("document_ingestion_source_revision_mismatch");
|
|
651
|
+
}
|
|
652
|
+
const extractionPlan = sourcePlan(source, method);
|
|
653
|
+
const modelId = capability.modelConnection.structuredJson.modelId;
|
|
654
|
+
const operationId = operationIdentity(source, method);
|
|
655
|
+
const documentId = id("world-source-document", source.scopeId, source.sourceId, source.sourceRevision, source.sourceSha256);
|
|
656
|
+
const emitProgress = async (progress) => {
|
|
657
|
+
await capability.onProgress?.(Object.freeze({ schemaVersion: 1, operationId, totalWindows: source.windows.length, ...progress }));
|
|
658
|
+
};
|
|
659
|
+
if (marker?.operationId === operationId && marker.sourceRevision === input.sourceRevision && marker.sourceSha256 === source.sourceSha256) {
|
|
660
|
+
if (marker.commitSummary === undefined)
|
|
661
|
+
throw new Error("document_ingestion_marker_commit_summary_missing");
|
|
662
|
+
const recovered = Object.freeze({ schemaVersion: 1, operationId, documentId, status: "already_committed", rawByteSha256: source.sourceSha256,
|
|
663
|
+
rawByteLength: source.byteLength,
|
|
664
|
+
...(marker.sourcePath === undefined ? {} : { sourcePath: marker.sourcePath }),
|
|
665
|
+
...(marker.sourceStoragePath === undefined ? {} : { sourceStoragePath: marker.sourceStoragePath }),
|
|
666
|
+
roundtripVerified, sourceCoverage: Object.freeze({ status: "complete", utf16Length: source.utf16Length,
|
|
667
|
+
unitCount: source.units.length, coveredUnitCount: source.units.length }), graphStatus: marker.commitSummary.graphStatus,
|
|
668
|
+
processedWindows: marker.commitSummary.processedWindows, pendingWindows: Object.freeze([]), failedWindows: Object.freeze([]),
|
|
669
|
+
nodeCount: marker.commitSummary.nodeCount, edgeCount: marker.commitSummary.edgeCount, embeddingStatus: marker.commitSummary.embeddingStatus,
|
|
670
|
+
previousProjectRevision: marker.commitSummary.previousProjectRevision, nextProjectRevision: marker.commitSummary.nextProjectRevision });
|
|
671
|
+
let stagedStatus;
|
|
672
|
+
try {
|
|
673
|
+
stagedStatus = await readLosslessDocumentStatus(capability.projectRoot, operationId);
|
|
674
|
+
}
|
|
675
|
+
catch (error) {
|
|
676
|
+
if (error.code !== "ENOENT")
|
|
677
|
+
throw error;
|
|
678
|
+
}
|
|
679
|
+
if (stagedStatus?.phase === "committing") {
|
|
680
|
+
const recoveryJournal = await dependencies.openJournal({ projectRoot: capability.projectRoot, operationId,
|
|
681
|
+
sourceByteSha256: source.sourceSha256, planSha256: extractionPlan.sha256, modelId: stagedStatus.modelId,
|
|
682
|
+
expectedProjectRevision: marker.commitSummary.previousProjectRevision,
|
|
683
|
+
...(method.version === "legacy-v1" ? {} : { method }) });
|
|
684
|
+
try {
|
|
685
|
+
await recoveryJournal.recordCommitReceipt(asJson(recovered));
|
|
686
|
+
}
|
|
687
|
+
finally {
|
|
688
|
+
await recoveryJournal.close();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
await emitProgress({ event: "phase_changed", phase: "committed", processedWindows: recovered.processedWindows });
|
|
692
|
+
return recovered;
|
|
693
|
+
}
|
|
694
|
+
let expectedProjectRevision = observed.project_revision;
|
|
695
|
+
try {
|
|
696
|
+
expectedProjectRevision = (await readLosslessDocumentStatus(capability.projectRoot, operationId)).expectedProjectRevision;
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
if (error.code !== "ENOENT")
|
|
700
|
+
throw error;
|
|
701
|
+
}
|
|
702
|
+
const journal = await dependencies.openJournal({ projectRoot: capability.projectRoot, operationId,
|
|
703
|
+
sourceByteSha256: source.sourceSha256, planSha256: extractionPlan.sha256, modelId,
|
|
704
|
+
expectedProjectRevision, ...(method.version === "legacy-v1" ? {} : { method }) });
|
|
705
|
+
try {
|
|
706
|
+
let status = await journal.getStatus();
|
|
707
|
+
if (status.phase === "committed") {
|
|
708
|
+
const receipt = Object.freeze({ ...receiptFromStored(status.commitReceipt), documentId });
|
|
709
|
+
await emitProgress({ event: "phase_changed", phase: "committed", processedWindows: receipt.processedWindows });
|
|
710
|
+
return receipt;
|
|
711
|
+
}
|
|
712
|
+
if (observed.project_revision !== expectedProjectRevision) {
|
|
713
|
+
throw Object.assign(new Error("document_ingestion_project_revision_conflict"), { code: "E_STALE_PROJECT_REVISION",
|
|
714
|
+
expected_project_revision: expectedProjectRevision, actual_project_revision: observed.project_revision });
|
|
715
|
+
}
|
|
716
|
+
if (status.phase === "extracting") {
|
|
717
|
+
if (await journal.readSegment(SOURCE_DESCRIPTOR_ID) === null) {
|
|
718
|
+
const descriptor = asJson({ schema_version: method.version === "legacy-v1" ? 1 : 2, operation_id: operationId, input: { raw_base64: source.originalBytesBase64, scope_id: input.scopeId,
|
|
719
|
+
source_id: input.sourceId, source_revision: input.sourceRevision, ...(input.sourcePath === undefined ? {} : { source_path: input.sourcePath }),
|
|
720
|
+
...(input.sourceStoragePath === undefined ? {} : { source_storage_path: input.sourceStoragePath }) },
|
|
721
|
+
...(capability.sourceOptions === undefined ? {} : { source_options: capability.sourceOptions }), plan_sha256: extractionPlan.sha256,
|
|
722
|
+
...(method.version === "legacy-v1" ? {} : { method: { version: method.version, sha256: method.sha256,
|
|
723
|
+
prompt_version: method.promptVersion, schema_version: method.schemaVersion, merge_version: method.mergeVersion,
|
|
724
|
+
embedding_policy: method.embeddingPolicy,
|
|
725
|
+
...(method.validatorDiagnosticsVersion === undefined ? {} : { validator_diagnostics_version: method.validatorDiagnosticsVersion }),
|
|
726
|
+
...(method.repairPolicyVersion === undefined ? {} : { repair_policy_version: method.repairPolicyVersion }) } }) });
|
|
727
|
+
await journal.writeSegment(SOURCE_DESCRIPTOR_ID, descriptor);
|
|
728
|
+
}
|
|
729
|
+
status = await journal.getStatus();
|
|
730
|
+
}
|
|
731
|
+
const completed = new Map();
|
|
732
|
+
const partialCheckpoints = new Map();
|
|
733
|
+
const partialAttemptCounts = new Map();
|
|
734
|
+
let clearedRecoveredFailure = false;
|
|
735
|
+
for (const window of source.windows) {
|
|
736
|
+
const retryCompleteId = `${RETRY_COMPLETE_PREFIX}${window.id}`;
|
|
737
|
+
if (status.completedSegmentIds.includes(retryCompleteId)) {
|
|
738
|
+
const saved = await journal.readSegment(retryCompleteId);
|
|
739
|
+
if (saved === null)
|
|
740
|
+
throw new Error("document_ingestion_checkpoint_missing");
|
|
741
|
+
const segment = graphSegment(saved, window.id);
|
|
742
|
+
if (segment.status !== "complete")
|
|
743
|
+
throw new Error("document_ingestion_checkpoint_invalid");
|
|
744
|
+
completed.set(window.id, segment);
|
|
745
|
+
if (status.failedSegments.some((failure) => failure.id === window.id)) {
|
|
746
|
+
await journal.clearSegmentFailure(window.id);
|
|
747
|
+
clearedRecoveredFailure = true;
|
|
748
|
+
}
|
|
749
|
+
continue;
|
|
750
|
+
}
|
|
751
|
+
if (!status.completedSegmentIds.includes(window.id))
|
|
752
|
+
continue;
|
|
753
|
+
const saved = await journal.readSegment(window.id);
|
|
754
|
+
if (saved === null)
|
|
755
|
+
throw new Error("document_ingestion_checkpoint_missing");
|
|
756
|
+
const segment = graphSegment(saved, window.id);
|
|
757
|
+
if (method.version !== "legacy-v1" && segment.status === "partial")
|
|
758
|
+
partialCheckpoints.set(window.id, segment);
|
|
759
|
+
else
|
|
760
|
+
completed.set(window.id, segment);
|
|
761
|
+
}
|
|
762
|
+
if (method.version !== "legacy-v1")
|
|
763
|
+
for (const window of source.windows) {
|
|
764
|
+
const prefix = `${PARTIAL_ATTEMPT_PREFIX}${window.id}:`;
|
|
765
|
+
const archivedIds = status.completedSegmentIds.filter((id) => id.startsWith(prefix))
|
|
766
|
+
.sort((left, right) => Number(left.slice(prefix.length)) - Number(right.slice(prefix.length)));
|
|
767
|
+
if (archivedIds.length > 0) {
|
|
768
|
+
const latest = await journal.readSegment(archivedIds.at(-1));
|
|
769
|
+
if (latest === null)
|
|
770
|
+
throw new Error("document_ingestion_checkpoint_missing");
|
|
771
|
+
partialCheckpoints.set(window.id, graphSegment(latest, window.id));
|
|
772
|
+
}
|
|
773
|
+
const archivedAttempt = archivedIds.length === 0 ? 0 : Number(archivedIds.at(-1).slice(prefix.length));
|
|
774
|
+
if (!Number.isSafeInteger(archivedAttempt) || archivedAttempt < 0)
|
|
775
|
+
throw new Error("document_ingestion_checkpoint_invalid");
|
|
776
|
+
partialAttemptCounts.set(window.id, Math.max(archivedAttempt, partialCheckpoints.has(window.id) ? 1 : 0));
|
|
777
|
+
}
|
|
778
|
+
if (method.version !== "legacy-v1")
|
|
779
|
+
for (const window of source.windows) {
|
|
780
|
+
const attempts = partialAttemptCounts.get(window.id) ?? 0;
|
|
781
|
+
const failure = status.failedSegments.find((candidate) => candidate.id === window.id);
|
|
782
|
+
if (attempts >= MAX_PARTIAL_ATTEMPTS && failure?.code !== "document_graph_partial_retry_exhausted") {
|
|
783
|
+
await journal.recordSegmentFailure(window.id, { code: "document_graph_partial_retry_exhausted", retryable: false, attempts });
|
|
784
|
+
clearedRecoveredFailure = true;
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
if (clearedRecoveredFailure)
|
|
788
|
+
status = await journal.getStatus();
|
|
789
|
+
const exhausted = new Set(status.failedSegments.filter((failure) => failure.code === "document_graph_partial_retry_exhausted").map((failure) => failure.id));
|
|
790
|
+
const remaining = source.windows.filter((window) => !completed.has(window.id) && !exhausted.has(window.id));
|
|
791
|
+
const requiredPorts = [];
|
|
792
|
+
if (remaining.length > 0)
|
|
793
|
+
requiredPorts.push("structured");
|
|
794
|
+
if (method.embeddingPolicy === "immediate")
|
|
795
|
+
requiredPorts.push("embedding");
|
|
796
|
+
if (requiredPorts.length > 0)
|
|
797
|
+
capability.modelConnection.assertAvailable?.(requiredPorts);
|
|
798
|
+
await emitProgress({ event: "source_staged", phase: status.phase, processedWindows: completed.size });
|
|
799
|
+
if (status.phase === "extracting") {
|
|
800
|
+
const previouslyTruncated = new Set(status.failedSegments.filter((failure) => failure.code === "provider_output_truncated").map((failure) => failure.id));
|
|
801
|
+
const concurrency = capability.concurrency ?? 4;
|
|
802
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 || concurrency > 16)
|
|
803
|
+
throw new Error("document_ingestion_concurrency_invalid");
|
|
804
|
+
for (let offset = 0; offset < remaining.length; offset += concurrency) {
|
|
805
|
+
if (capability.signal?.aborted)
|
|
806
|
+
break;
|
|
807
|
+
const catalog = knownDocumentGraphEntities(source, [...completed.values()]);
|
|
808
|
+
const outcomes = await Promise.allSettled(remaining.slice(offset, offset + concurrency).map(async (window) => {
|
|
809
|
+
let failure;
|
|
810
|
+
const previousPartial = partialCheckpoints.get(window.id);
|
|
811
|
+
const previousAttemptCount = partialAttemptCounts.get(window.id) ?? 0;
|
|
812
|
+
try {
|
|
813
|
+
let priorPartial;
|
|
814
|
+
const adaptiveMethod = method.version === "adaptive-repair-v3" || method.version === "semantic-negation-v4" || method.version === "harness-v1";
|
|
815
|
+
if (adaptiveMethod && previousAttemptCount > 1) {
|
|
816
|
+
const prior = await journal.readSegment(`${PARTIAL_ATTEMPT_PREFIX}${window.id}:${previousAttemptCount - 1}`);
|
|
817
|
+
if (prior !== null)
|
|
818
|
+
priorPartial = graphSegment(prior, window.id);
|
|
819
|
+
}
|
|
820
|
+
const extracted = await dependencies.extractWindow({ window, source, knownEntities: catalog, modelConnection: capability.modelConnection,
|
|
821
|
+
ingestionMethod: method,
|
|
822
|
+
...(adaptiveMethod ? { methodVersion: method.version } : {}),
|
|
823
|
+
...(!adaptiveMethod || previousPartial === undefined ? {} : {
|
|
824
|
+
adaptiveRepair: adaptivePreviousAttempt(method, source, window.id, previousPartial, previousAttemptCount, priorPartial),
|
|
825
|
+
}),
|
|
826
|
+
...(previouslyTruncated.has(window.id) ? { maxOutputTokens: 65_536 } : {}),
|
|
827
|
+
...(capability.signal === undefined ? {} : { signal: capability.signal }) });
|
|
828
|
+
if (method.version !== "legacy-v1" && extracted.status === "partial") {
|
|
829
|
+
const attempts = (partialAttemptCounts.get(window.id) ?? 0) + 1;
|
|
830
|
+
if (previousPartial !== undefined && attempts === 2) {
|
|
831
|
+
await journal.writeSegment(`${PARTIAL_ATTEMPT_PREFIX}${window.id}:1`, asJson(previousPartial));
|
|
832
|
+
}
|
|
833
|
+
await journal.writeSegment(`${PARTIAL_ATTEMPT_PREFIX}${window.id}:${attempts}`, asJson(extracted));
|
|
834
|
+
partialAttemptCounts.set(window.id, attempts);
|
|
835
|
+
failure = Object.freeze({ code: attempts >= MAX_PARTIAL_ATTEMPTS
|
|
836
|
+
? "document_graph_partial_retry_exhausted" : "document_graph_partial", retryable: attempts < MAX_PARTIAL_ATTEMPTS });
|
|
837
|
+
await journal.recordSegmentFailure(window.id, { ...failure, attempts });
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
if (previousPartial !== undefined) {
|
|
841
|
+
if ((partialAttemptCounts.get(window.id) ?? 0) === 1) {
|
|
842
|
+
await journal.writeSegment(`${PARTIAL_ATTEMPT_PREFIX}${window.id}:1`, asJson(previousPartial));
|
|
843
|
+
}
|
|
844
|
+
await journal.writeSegment(`${RETRY_COMPLETE_PREFIX}${window.id}`, asJson(extracted));
|
|
845
|
+
await journal.clearSegmentFailure(window.id);
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
await journal.writeSegment(window.id, asJson(extracted));
|
|
849
|
+
}
|
|
850
|
+
partialCheckpoints.delete(window.id);
|
|
851
|
+
completed.set(window.id, extracted);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
if ((method.version === "adaptive-repair-v3" || method.version === "semantic-negation-v4" || method.version === "harness-v1")
|
|
856
|
+
&& error instanceof DocumentGraphExtractionError) {
|
|
857
|
+
const attempts = previousAttemptCount + 1;
|
|
858
|
+
const diagnosticCheckpoint = Object.freeze({ windowId: window.id, status: "partial", segments: Object.freeze([]),
|
|
859
|
+
nodes: Object.freeze([]), edges: Object.freeze([]), diagnostics: error.diagnostics });
|
|
860
|
+
await journal.writeSegment(`${PARTIAL_ATTEMPT_PREFIX}${window.id}:${attempts}`, asJson(diagnosticCheckpoint));
|
|
861
|
+
partialAttemptCounts.set(window.id, attempts);
|
|
862
|
+
partialCheckpoints.set(window.id, diagnosticCheckpoint);
|
|
863
|
+
failure = Object.freeze({ code: attempts >= MAX_PARTIAL_ATTEMPTS
|
|
864
|
+
? "document_graph_partial_retry_exhausted" : "document_graph_partial", retryable: attempts < MAX_PARTIAL_ATTEMPTS });
|
|
865
|
+
await journal.recordSegmentFailure(window.id, { ...failure, attempts });
|
|
866
|
+
}
|
|
867
|
+
else {
|
|
868
|
+
failure = errorCode(error);
|
|
869
|
+
await journal.recordSegmentFailure(window.id, failure);
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
// consumer 错误不冒充模型失败;当前并发批次全部收敛后再关闭 journal。
|
|
873
|
+
await emitProgress({ event: failure === undefined ? "window_completed" : "window_failed", phase: "extracting",
|
|
874
|
+
processedWindows: completed.size, windowId: window.id, ...(failure === undefined ? {} : { failure: Object.freeze(failure) }) });
|
|
875
|
+
}));
|
|
876
|
+
const failed = outcomes.find((outcome) => outcome.status === "rejected");
|
|
877
|
+
if (failed !== undefined)
|
|
878
|
+
throw failed.reason;
|
|
879
|
+
}
|
|
880
|
+
status = await journal.getStatus();
|
|
881
|
+
}
|
|
882
|
+
const pendingWindows = source.windows.filter((window) => !completed.has(window.id)).map((window) => window.id);
|
|
883
|
+
const failedWindows = status.failedSegments.filter((failure) => pendingWindows.includes(failure.id));
|
|
884
|
+
const sourceCoverage = Object.freeze({ status: "complete", utf16Length: source.utf16Length, unitCount: source.units.length, coveredUnitCount: source.units.length });
|
|
885
|
+
if (pendingWindows.length > 0)
|
|
886
|
+
return Object.freeze({
|
|
887
|
+
schemaVersion: 1, operationId, documentId, status: "staged", rawByteSha256: source.sourceSha256, rawByteLength: source.byteLength,
|
|
888
|
+
...(input.sourcePath === undefined ? {} : { sourcePath: input.sourcePath }),
|
|
889
|
+
...(input.sourceStoragePath === undefined ? {} : { sourceStoragePath: input.sourceStoragePath }),
|
|
890
|
+
roundtripVerified, sourceCoverage, graphStatus: [...partialAttemptCounts.values()].some((attempts) => attempts > 0) || completed.size > 0 ? "partial" : "pending",
|
|
891
|
+
processedWindows: completed.size, pendingWindows: Object.freeze(pendingWindows), failedWindows: Object.freeze(failedWindows),
|
|
892
|
+
nodeCount: 0, edgeCount: 0, embeddingStatus: emptyEmbeddingStatus(capability.modelConnection),
|
|
893
|
+
});
|
|
894
|
+
if (status.phase === "extracting") {
|
|
895
|
+
await journal.markPhase("ready_to_commit");
|
|
896
|
+
status = await journal.getStatus();
|
|
897
|
+
await emitProgress({ event: "phase_changed", phase: status.phase, processedWindows: completed.size });
|
|
898
|
+
}
|
|
899
|
+
const orderedSegments = source.windows.map((window) => completed.get(window.id));
|
|
900
|
+
const cache = method.embeddingPolicy === "immediate"
|
|
901
|
+
? await dependencies.createEmbeddingCache({ projectRoot: capability.projectRoot, embedding: capability.modelConnection.embedding,
|
|
902
|
+
...(capability.embeddingOrigin === undefined ? {} : { origin: capability.embeddingOrigin }) })
|
|
903
|
+
: undefined;
|
|
904
|
+
const managedSnapshot = await readManagedSnapshot(host, marker);
|
|
905
|
+
const assembled = await assemble(source, extractionPlan.sha256, orderedSegments, cache, capability.signal, input.sourcePath, input.sourceStoragePath);
|
|
906
|
+
const currentEntityIds = new Set(assembled.entityIds);
|
|
907
|
+
const currentRelationKeys = new Set(assembled.relations.map(relationKey));
|
|
908
|
+
const deletes = [
|
|
909
|
+
...(marker?.managedRelations ?? []).filter((relation) => !currentRelationKeys.has(relationKey(relation))).map((relation) => ({ type: "delete_relation", ...relation })),
|
|
910
|
+
...(marker?.managedEntityIds ?? []).filter((entityId) => !currentEntityIds.has(entityId) && !managedSnapshot.externalReferenceEntityIds.has(entityId))
|
|
911
|
+
.map((entityId) => ({ type: "delete_entity", id: entityId })),
|
|
912
|
+
];
|
|
913
|
+
const retainedExternalEntities = [...managedSnapshot.externalReferenceEntityIds]
|
|
914
|
+
.filter((entityId) => !currentEntityIds.has(entityId))
|
|
915
|
+
.flatMap((entityId) => {
|
|
916
|
+
const entity = managedSnapshot.entities.get(entityId);
|
|
917
|
+
return entity === undefined ? [] : [{ type: "upsert_entity", id: entity.id, kind: entity.kind,
|
|
918
|
+
props: asJson({ ...entity.props, retrieval_excluded: true, source_revision_active: false }) }];
|
|
919
|
+
});
|
|
920
|
+
const counts = cache?.getCounts() ?? { hits: 0, misses: 0, remoteTexts: 0, writes: 0 };
|
|
921
|
+
const graphStatus = orderedSegments.every((segment) => segment.status === "complete") ? "complete" : "partial";
|
|
922
|
+
const embeddingStatus = Object.freeze({ status: cache === undefined ? "pending" : "complete",
|
|
923
|
+
providerId: cache?.providerId ?? capability.modelConnection.embedding.providerId,
|
|
924
|
+
modelId: cache?.modelId ?? capability.modelConnection.embedding.modelId,
|
|
925
|
+
dimensions: cache?.dimensions ?? capability.modelConnection.embedding.dimensions, ...counts });
|
|
926
|
+
const markerOperation = { type: "upsert_entity", id: markerId(input.scopeId, input.sourceId), kind: CONTROL_KIND, props: asJson({
|
|
927
|
+
scope_id: input.scopeId, source_id: input.sourceId, source_revision: input.sourceRevision, source_sha256: source.sourceSha256,
|
|
928
|
+
...(input.sourcePath === undefined ? {} : { source_path: input.sourcePath }),
|
|
929
|
+
...(input.sourceStoragePath === undefined ? {} : { source_storage_path: input.sourceStoragePath }),
|
|
930
|
+
operation_id: operationId, managed_entity_ids: assembled.entityIds, managed_relations: assembled.relations,
|
|
931
|
+
method_version: method.version, method_sha256: method.sha256,
|
|
932
|
+
commit_summary: { graph_status: graphStatus, processed_windows: source.windows.length, node_count: assembled.nodeCount,
|
|
933
|
+
edge_count: assembled.edgeCount, embedding_status: embeddingStatus, previous_project_revision: expectedProjectRevision,
|
|
934
|
+
next_project_revision: expectedProjectRevision + 1 },
|
|
935
|
+
retrieval_excluded: true,
|
|
936
|
+
}) };
|
|
937
|
+
if (status.phase === "ready_to_commit") {
|
|
938
|
+
await journal.markPhase("committing");
|
|
939
|
+
await emitProgress({ event: "phase_changed", phase: "committing", processedWindows: completed.size });
|
|
940
|
+
}
|
|
941
|
+
const apply = record(await host.dispatchTool({ name: "world_apply", input: { operations: [...deletes, ...retainedExternalEntities, ...assembled.operations, markerOperation] },
|
|
942
|
+
effect_identity: { operation_id: operationId, expected_project_revision: expectedProjectRevision } }), "document_ingestion_apply_receipt_invalid");
|
|
943
|
+
if (apply.schema_version !== 1 || apply.operation_id !== operationId || apply.previous_project_revision !== expectedProjectRevision
|
|
944
|
+
|| !Number.isSafeInteger(apply.next_project_revision) || apply.readback_verified !== true)
|
|
945
|
+
throw new Error("document_ingestion_apply_receipt_invalid");
|
|
946
|
+
const receipt = Object.freeze({
|
|
947
|
+
schemaVersion: 1, operationId, documentId, status: "committed", rawByteSha256: source.sourceSha256, rawByteLength: source.byteLength,
|
|
948
|
+
...(input.sourcePath === undefined ? {} : { sourcePath: input.sourcePath }),
|
|
949
|
+
...(input.sourceStoragePath === undefined ? {} : { sourceStoragePath: input.sourceStoragePath }),
|
|
950
|
+
roundtripVerified, sourceCoverage, graphStatus, processedWindows: source.windows.length, pendingWindows: Object.freeze([]), failedWindows: Object.freeze([]),
|
|
951
|
+
nodeCount: assembled.nodeCount, edgeCount: assembled.edgeCount,
|
|
952
|
+
embeddingStatus,
|
|
953
|
+
previousProjectRevision: expectedProjectRevision, nextProjectRevision: apply.next_project_revision,
|
|
954
|
+
});
|
|
955
|
+
await journal.recordCommitReceipt(asJson(receipt));
|
|
956
|
+
await emitProgress({ event: "phase_changed", phase: "committed", processedWindows: completed.size });
|
|
957
|
+
return receipt;
|
|
958
|
+
}
|
|
959
|
+
finally {
|
|
960
|
+
await journal.close();
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
/** harness 工作流的稳定 adapter:复用唯一 ingestion journal、合并与 CAS commit authority。 */
|
|
964
|
+
export async function ingestHarnessDocument(host, input, capability) {
|
|
965
|
+
return ingestLosslessDocumentWithMethod(host, input, capability, ingestionMethod("harness-v1"));
|
|
966
|
+
}
|
|
967
|
+
/** operation descriptor 是恢复时的唯一 method authority;不得由 CLI 参数原地升级。 */
|
|
968
|
+
export async function resumeHarnessDocument(host, operationId, capability) {
|
|
969
|
+
const descriptor = await readStagedDocumentInput(capability.projectRoot, operationId);
|
|
970
|
+
if (descriptor.method.version !== "harness-v1") {
|
|
971
|
+
throw new AuthoringBridgeError("E_VALIDATION_FAILED", "该 operation 需要对应 legacy provider capability", {
|
|
972
|
+
failure: "E_CAPABILITY_UNAVAILABLE", method_version: descriptor.method.version,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
return ingestLosslessDocumentWithMethod(host, descriptor.input, { ...capability, ...(descriptor.sourceOptions === undefined ? {} : { sourceOptions: descriptor.sourceOptions }) }, descriptor.method);
|
|
976
|
+
}
|