@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.
Files changed (218) hide show
  1. package/LICENSE +46 -0
  2. package/assets/gui/WELogo-title.svg +11 -0
  3. package/assets/gui/app.js +1326 -0
  4. package/assets/gui/index.html +118 -0
  5. package/assets/gui/local-preview-asset-urls.js +61 -0
  6. package/assets/gui/local-preview-bridge.js +393 -0
  7. package/assets/gui/spatial-template-panel.js +233 -0
  8. package/assets/gui/styles.css +115 -0
  9. package/assets/gui/terrain-render.js +120 -0
  10. package/assets/gui/workspace-status.css +17 -0
  11. package/assets/gui/workspace-status.js +255 -0
  12. package/assets/gui/world-map-globe.js +187 -0
  13. package/assets/gui/world-map-worker.js +62 -0
  14. package/assets/gui/world-map.css +30 -0
  15. package/assets/gui/world-map.js +2466 -0
  16. package/dist/account-model-proxy.d.ts +21 -0
  17. package/dist/account-model-proxy.js +470 -0
  18. package/dist/authoring-browser-runtime.d.ts +7 -0
  19. package/dist/authoring-browser-runtime.js +110 -0
  20. package/dist/authoring-ingestion.d.ts +38 -0
  21. package/dist/authoring-ingestion.js +410 -0
  22. package/dist/authoring-model-connection.d.ts +166 -0
  23. package/dist/authoring-model-connection.js +757 -0
  24. package/dist/chinese-lexical.d.ts +152 -0
  25. package/dist/chinese-lexical.js +555 -0
  26. package/dist/chromium-preview-browser-driver.d.ts +43 -0
  27. package/dist/chromium-preview-browser-driver.js +575 -0
  28. package/dist/cli/download.d.ts +12 -0
  29. package/dist/cli/download.js +50 -0
  30. package/dist/cli/dump.d.ts +32 -0
  31. package/dist/cli/dump.js +91 -0
  32. package/dist/cli/ingest.d.ts +49 -0
  33. package/dist/cli/ingest.js +225 -0
  34. package/dist/cli/review.d.ts +10 -0
  35. package/dist/cli/review.js +44 -0
  36. package/dist/cli/upload.d.ts +57 -0
  37. package/dist/cli/upload.js +112 -0
  38. package/dist/cli-command.d.ts +60 -0
  39. package/dist/cli-command.js +613 -0
  40. package/dist/cli.d.ts +2 -0
  41. package/dist/cli.js +182 -0
  42. package/dist/desktop-authoring-runtime.d.ts +88 -0
  43. package/dist/desktop-authoring-runtime.js +1602 -0
  44. package/dist/desktop-sidecar.d.ts +11 -0
  45. package/dist/desktop-sidecar.js +143 -0
  46. package/dist/document-dump.d.ts +113 -0
  47. package/dist/document-dump.js +431 -0
  48. package/dist/document-graph-extractor.d.ts +125 -0
  49. package/dist/document-graph-extractor.js +588 -0
  50. package/dist/document-ingestion-journal.d.ts +68 -0
  51. package/dist/document-ingestion-journal.js +471 -0
  52. package/dist/document-ingestion.d.ts +183 -0
  53. package/dist/document-ingestion.js +976 -0
  54. package/dist/document-query-path-proof.d.ts +30 -0
  55. package/dist/document-query-path-proof.js +34 -0
  56. package/dist/document-query-path-renderer.d.ts +50 -0
  57. package/dist/document-query-path-renderer.js +75 -0
  58. package/dist/document-query-proof-paths.d.ts +22 -0
  59. package/dist/document-query-proof-paths.js +71 -0
  60. package/dist/document-query.d.ts +97 -0
  61. package/dist/document-query.js +1106 -0
  62. package/dist/document-relation-role.d.ts +14 -0
  63. package/dist/document-relation-role.js +86 -0
  64. package/dist/document-source.d.ts +107 -0
  65. package/dist/document-source.js +255 -0
  66. package/dist/embedding-cache.d.ts +35 -0
  67. package/dist/embedding-cache.js +317 -0
  68. package/dist/frozen-review-local-pipeline.d.ts +88 -0
  69. package/dist/frozen-review-local-pipeline.js +609 -0
  70. package/dist/frozen-review-source-capture.d.ts +15 -0
  71. package/dist/frozen-review-source-capture.js +551 -0
  72. package/dist/frozen-review-wasm-encryptor.d.ts +16 -0
  73. package/dist/frozen-review-wasm-encryptor.js +72 -0
  74. package/dist/frozen-review.d.ts +131 -0
  75. package/dist/frozen-review.js +802 -0
  76. package/dist/graph-traversal.d.ts +23 -0
  77. package/dist/graph-traversal.js +100 -0
  78. package/dist/gui/graph-layout-worker.js +1 -0
  79. package/dist/gui/graph-layout.wasm +0 -0
  80. package/dist/gui/graph.css +1 -0
  81. package/dist/gui/graph.js +76 -0
  82. package/dist/gui/plane-generation.js +112 -0
  83. package/dist/gui/plane-generator.js +310 -0
  84. package/dist/gui/plane-generator_bg.wasm +0 -0
  85. package/dist/gui/procedural-city.js +595 -0
  86. package/dist/gui/road-generation.js +129 -0
  87. package/dist/gui/road-parcels.js +1584 -0
  88. package/dist/gui/score.css +1 -0
  89. package/dist/gui/score.js +54 -0
  90. package/dist/gui/spatial-runtime.js +1231 -0
  91. package/dist/gui/spatial-templates.js +200 -0
  92. package/dist/gui/trigger.css +1 -0
  93. package/dist/gui/trigger.js +142 -0
  94. package/dist/harness-callback-transport.d.ts +17 -0
  95. package/dist/harness-callback-transport.js +128 -0
  96. package/dist/harness-document-ingestion.d.ts +77 -0
  97. package/dist/harness-document-ingestion.js +343 -0
  98. package/dist/index.d.ts +43 -0
  99. package/dist/index.js +43 -0
  100. package/dist/ingestion-source-reconciliation.d.ts +88 -0
  101. package/dist/ingestion-source-reconciliation.js +253 -0
  102. package/dist/ladybug-runtime.d.ts +117 -0
  103. package/dist/ladybug-runtime.js +886 -0
  104. package/dist/local-account-embedding.d.ts +34 -0
  105. package/dist/local-account-embedding.js +294 -0
  106. package/dist/local-gallery.d.ts +35 -0
  107. package/dist/local-gallery.js +227 -0
  108. package/dist/local-gui-auth.d.ts +96 -0
  109. package/dist/local-gui-auth.js +703 -0
  110. package/dist/local-gui-contract.d.ts +53 -0
  111. package/dist/local-gui-contract.js +5 -0
  112. package/dist/local-gui-image-assets.d.ts +17 -0
  113. package/dist/local-gui-image-assets.js +96 -0
  114. package/dist/local-gui-preview-client.d.ts +3 -0
  115. package/dist/local-gui-preview-client.js +111 -0
  116. package/dist/local-gui-score.d.ts +72 -0
  117. package/dist/local-gui-score.js +184 -0
  118. package/dist/local-gui-server.d.ts +10 -0
  119. package/dist/local-gui-server.js +987 -0
  120. package/dist/local-gui-version.d.ts +17 -0
  121. package/dist/local-gui-version.js +147 -0
  122. package/dist/local-gui-workspace.d.ts +14 -0
  123. package/dist/local-gui-workspace.js +171 -0
  124. package/dist/local-model-settings.d.ts +28 -0
  125. package/dist/local-model-settings.js +124 -0
  126. package/dist/local-preview-assets.d.ts +28 -0
  127. package/dist/local-preview-assets.js +124 -0
  128. package/dist/local-preview-bound-resources.d.ts +12 -0
  129. package/dist/local-preview-bound-resources.js +80 -0
  130. package/dist/local-preview-message-store.d.ts +63 -0
  131. package/dist/local-preview-message-store.js +214 -0
  132. package/dist/local-preview-player-read-models.d.ts +47 -0
  133. package/dist/local-preview-player-read-models.js +90 -0
  134. package/dist/local-preview-projections.d.ts +61 -0
  135. package/dist/local-preview-projections.js +117 -0
  136. package/dist/local-preview-runtime.d.ts +105 -0
  137. package/dist/local-preview-runtime.js +553 -0
  138. package/dist/local-preview-session.d.ts +73 -0
  139. package/dist/local-preview-session.js +395 -0
  140. package/dist/local-preview-source.d.ts +40 -0
  141. package/dist/local-preview-source.js +192 -0
  142. package/dist/local-preview-view-events.d.ts +101 -0
  143. package/dist/local-preview-view-events.js +179 -0
  144. package/dist/local-preview-view-state-sqlite.d.ts +24 -0
  145. package/dist/local-preview-view-state-sqlite.js +263 -0
  146. package/dist/local-preview-view-state.d.ts +93 -0
  147. package/dist/local-preview-view-state.js +108 -0
  148. package/dist/local-preview-vite-config.d.ts +6 -0
  149. package/dist/local-preview-vite-config.js +122 -0
  150. package/dist/local-preview-world-effects.d.ts +9 -0
  151. package/dist/local-preview-world-effects.js +55 -0
  152. package/dist/local-preview-worldline-store.d.ts +47 -0
  153. package/dist/local-preview-worldline-store.js +282 -0
  154. package/dist/local-preview-worldlines.d.ts +95 -0
  155. package/dist/local-preview-worldlines.js +136 -0
  156. package/dist/mcp-async-dispatcher.d.ts +3 -0
  157. package/dist/mcp-async-dispatcher.js +129 -0
  158. package/dist/mcp-line-queue.d.ts +14 -0
  159. package/dist/mcp-line-queue.js +184 -0
  160. package/dist/mcp-pipe-internal.d.ts +6 -0
  161. package/dist/mcp-pipe-internal.js +151 -0
  162. package/dist/mcp-pipe-response.d.ts +14 -0
  163. package/dist/mcp-pipe-response.js +50 -0
  164. package/dist/mcp-stdio-response.d.ts +12 -0
  165. package/dist/mcp-stdio-response.js +27 -0
  166. package/dist/mcp.d.ts +27 -0
  167. package/dist/mcp.js +194 -0
  168. package/dist/prepared-embedding-cache.d.ts +38 -0
  169. package/dist/prepared-embedding-cache.js +238 -0
  170. package/dist/preview-browser-adapter.d.ts +53 -0
  171. package/dist/preview-browser-adapter.js +171 -0
  172. package/dist/preview-runtime.d.ts +67 -0
  173. package/dist/preview-runtime.js +460 -0
  174. package/dist/project-auth-transport.d.ts +27 -0
  175. package/dist/project-auth-transport.js +121 -0
  176. package/dist/project-auth.d.ts +51 -0
  177. package/dist/project-auth.js +108 -0
  178. package/dist/project-git-capture-recovery.d.ts +16 -0
  179. package/dist/project-git-capture-recovery.js +347 -0
  180. package/dist/project-git-cli.d.ts +5 -0
  181. package/dist/project-git-cli.js +40 -0
  182. package/dist/project-git-scenario-source.d.ts +10 -0
  183. package/dist/project-git-scenario-source.js +240 -0
  184. package/dist/project-git-view-source.d.ts +13 -0
  185. package/dist/project-git-view-source.js +134 -0
  186. package/dist/project-git.d.ts +97 -0
  187. package/dist/project-git.js +656 -0
  188. package/dist/project-host.d.ts +59 -0
  189. package/dist/project-host.js +471 -0
  190. package/dist/project-writer-lease.d.ts +18 -0
  191. package/dist/project-writer-lease.js +430 -0
  192. package/dist/public-error.d.ts +7 -0
  193. package/dist/public-error.js +61 -0
  194. package/dist/retrieval-benchmark.d.ts +46 -0
  195. package/dist/retrieval-benchmark.js +58 -0
  196. package/dist/safe-project-path.d.ts +8 -0
  197. package/dist/safe-project-path.js +93 -0
  198. package/dist/scenario-writer.d.ts +32 -0
  199. package/dist/scenario-writer.js +191 -0
  200. package/dist/transfer/download.d.ts +159 -0
  201. package/dist/transfer/download.js +598 -0
  202. package/dist/transfer/transport.d.ts +26 -0
  203. package/dist/transfer/transport.js +1 -0
  204. package/dist/transfer/upload.d.ts +162 -0
  205. package/dist/transfer/upload.js +354 -0
  206. package/dist/transfer-types.d.ts +95 -0
  207. package/dist/transfer-types.js +1 -0
  208. package/dist/transfer.d.ts +18 -0
  209. package/dist/transfer.js +273 -0
  210. package/dist/trigger-runtime.d.ts +121 -0
  211. package/dist/trigger-runtime.js +153 -0
  212. package/dist/vendor/wasm-core/asset-hashes.json +5 -0
  213. package/dist/vendor/wasm-core/wasm_core.d.ts +490 -0
  214. package/dist/vendor/wasm-core/wasm_core.js +1 -0
  215. package/dist/vendor/wasm-core/wasm_core_bg.wasm +0 -0
  216. package/dist/view-watcher.d.ts +9 -0
  217. package/dist/view-watcher.js +34 -0
  218. package/package.json +110 -0
@@ -0,0 +1,886 @@
1
+ import { mkdir, open as openFile, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { join } from "node:path";
5
+ import { buildChineseLexicalIndexProfile, buildLegacyChineseLexicalIndexProfileV1, buildLegacyChineseLexicalIndexProfileV2, buildLegacyChineseLexicalIndexProfileV3, CHINESE_LEXICAL_PROFILE_VERSION, LEGACY_CHINESE_LEXICAL_PROFILE_VERSION, LEGACY_CHINESE_LEXICAL_PROFILE_V2_VERSION, LEGACY_CHINESE_LEXICAL_PROFILE_V3_VERSION, lexicalize, lexicalizeQuery, } from "./chinese-lexical.js";
6
+ import { classifyDocumentRelationRole } from "./document-relation-role.js";
7
+ const require = createRequire(import.meta.url);
8
+ const STATE_FILE = "extracted-graph.v1.json";
9
+ const ENTITY_TABLE = "ExtractedEntity";
10
+ const RELATION_TABLE = "ExtractedRelation";
11
+ const VECTOR_INDEX = "extracted_entity_vector_idx";
12
+ const FTS_INDEX = "extracted_entity_fts_idx";
13
+ export const LADYBUG_WORLD_FACT_QUERY_PROFILE_ID = "world-fact-query/zh-v1";
14
+ export const DEFAULT_LADYBUG_MAX_VECTOR_DISTANCE = 0.5;
15
+ /** 仅编码一条查询;corpus 向量由显式准备流程负责,不由该 seam 补齐。 */
16
+ export async function embedLadybugQuery(port, text) {
17
+ nonEmpty(text, "queryText");
18
+ const result = await port.embed([text], { inputKind: "query" });
19
+ invariant(result.length === 1, "embedding provider 必须为每个 query 返回一个向量");
20
+ return validateVector(result[0], port.dimensions, "provider query embedding");
21
+ }
22
+ let ownedWorkerUsers = 0;
23
+ let workerLifecycleTail = Promise.resolve();
24
+ async function serializeWorkerLifecycle(operation) {
25
+ const prior = workerLifecycleTail;
26
+ let release;
27
+ workerLifecycleTail = new Promise((resolve) => { release = resolve; });
28
+ await prior;
29
+ try {
30
+ return await operation();
31
+ }
32
+ finally {
33
+ release();
34
+ }
35
+ }
36
+ async function acquireOwnedWorker(variant) {
37
+ await serializeWorkerLifecycle(async () => {
38
+ if (ownedWorkerUsers === 0)
39
+ await variant.init();
40
+ ownedWorkerUsers += 1;
41
+ });
42
+ }
43
+ async function releaseOwnedWorker(variant) {
44
+ await serializeWorkerLifecycle(async () => {
45
+ invariant(ownedWorkerUsers > 0, "Ladybug worker lifecycle 引用计数无效");
46
+ ownedWorkerUsers -= 1;
47
+ if (ownedWorkerUsers === 0)
48
+ await variant.close();
49
+ });
50
+ }
51
+ function invariant(condition, message) {
52
+ if (!condition)
53
+ throw new Error(message);
54
+ }
55
+ function nonEmpty(value, label) {
56
+ invariant(typeof value === "string" && value.trim().length > 0, `${label} 必须是非空 string`);
57
+ }
58
+ function validateVector(vector, dimensions, label) {
59
+ invariant(Array.isArray(vector), `${label} 必须是 number[]`);
60
+ invariant(vector.length === dimensions, `${label} 维度必须为 ${dimensions}`);
61
+ invariant(vector.every((value) => typeof value === "number" && Number.isFinite(value)), `${label} 必须只包含有限数`);
62
+ invariant(vector.some((value) => value !== 0), `${label} 不得为零向量`);
63
+ return [...vector];
64
+ }
65
+ function validateSource(source) {
66
+ nonEmpty(source.sourceId, "source.sourceId");
67
+ invariant(/^[a-f\d]{64}$/iu.test(source.contentSha256), "source.contentSha256 必须是 SHA-256 hex");
68
+ invariant(Number.isSafeInteger(source.extractedAtMs) && source.extractedAtMs >= 0, "source.extractedAtMs 无效");
69
+ nonEmpty(source.extractor.harness, "source.extractor.harness");
70
+ nonEmpty(source.extractor.contractVersion, "source.extractor.contractVersion");
71
+ }
72
+ function relationKey(relation) {
73
+ return JSON.stringify([relation.src, relation.dst, relation.kind]);
74
+ }
75
+ function sourceKey(source) {
76
+ return JSON.stringify([source.sourceId, source.contentSha256, source.extractor.contractVersion]);
77
+ }
78
+ function scopeFromProps(props) {
79
+ if (typeof props.scope_id === "string" && props.scope_id.trim().length > 0)
80
+ return props.scope_id;
81
+ const fact = props.fact;
82
+ if (typeof fact === "object" && fact !== null && !Array.isArray(fact)) {
83
+ const scopeId = fact.scopeId;
84
+ if (typeof scopeId === "string" && scopeId.trim().length > 0)
85
+ return scopeId;
86
+ }
87
+ return null;
88
+ }
89
+ function entityScope(entity) {
90
+ if (typeof entity.scopeId === "string" && entity.scopeId.trim().length > 0)
91
+ return entity.scopeId;
92
+ return scopeFromProps(entity.props);
93
+ }
94
+ const DERIVED_VECTOR_PROP_KEYS = new Set([
95
+ "embedding",
96
+ "embedding_vector",
97
+ "embedding_model",
98
+ "embedding_provider",
99
+ "embedding_dimensions",
100
+ "embedding_text_hash",
101
+ ]);
102
+ const GRAPH_SNAPSHOT_BATCH_SIZE = 128;
103
+ export function normalizeLadybugGraphSnapshotProps(props) {
104
+ return Object.fromEntries(Object.entries(props).filter(([key]) => !DERIVED_VECTOR_PROP_KEYS.has(key)));
105
+ }
106
+ /** 兼容并行接线中的旧名称;唯一实现仍由 normalizeLadybugGraphSnapshotProps 持有。 */
107
+ export const ladybugGraphSnapshotProps = normalizeLadybugGraphSnapshotProps;
108
+ function edgeSnapshotKey(edge) {
109
+ return JSON.stringify([edge.src, edge.dst, edge.kind]);
110
+ }
111
+ function batches(values, size) {
112
+ const result = [];
113
+ for (let offset = 0; offset < values.length; offset += size)
114
+ result.push(values.slice(offset, offset + size));
115
+ return result;
116
+ }
117
+ function mergeSources(prior, source) {
118
+ const byKey = new Map(prior.map((entry) => [sourceKey(entry), entry]));
119
+ byKey.set(sourceKey(source), source);
120
+ return [...byKey.values()];
121
+ }
122
+ function emptyState(dimensions) {
123
+ return { schemaVersion: 1, dimensions, embeddingIdentity: null, appliedBatchIds: [], appliedBatchDigests: {}, lexicalProfile: null, entities: [], relations: [] };
124
+ }
125
+ function strings(value) {
126
+ if (typeof value === "string" && value.length > 0)
127
+ return [value];
128
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.length > 0) : [];
129
+ }
130
+ function lexicalProfileInputs(entities) {
131
+ const searchable = entities.filter((entity) => !isRetrievalExcluded(entity));
132
+ if (searchable.length === 0)
133
+ return null;
134
+ const entityLabels = [];
135
+ const aliases = [];
136
+ for (const entity of searchable) {
137
+ entityLabels.push(...strings(entity.props.label), ...strings(entity.props.name), ...strings(entity.props.title));
138
+ aliases.push(...strings(entity.props.alias), ...strings(entity.props.aliases), ...strings(entity.props.labels));
139
+ }
140
+ return { texts: searchable.map((entity) => entity.text), entityLabels, aliases };
141
+ }
142
+ function buildLexicalProfile(entities) {
143
+ const input = lexicalProfileInputs(entities);
144
+ return input === null ? null : buildChineseLexicalIndexProfile(input.texts, input);
145
+ }
146
+ function buildLegacyLexicalProfile(entities) {
147
+ const input = lexicalProfileInputs(entities);
148
+ return input === null ? null : buildLegacyChineseLexicalIndexProfileV1(input.texts, input);
149
+ }
150
+ function buildLegacyLexicalProfileV2(entities) {
151
+ const input = lexicalProfileInputs(entities);
152
+ return input === null ? null : buildLegacyChineseLexicalIndexProfileV2(input.texts, input);
153
+ }
154
+ function buildLegacyLexicalProfileV3(entities) {
155
+ const input = lexicalProfileInputs(entities);
156
+ return input === null ? null : buildLegacyChineseLexicalIndexProfileV3(input.texts, input);
157
+ }
158
+ function sameStableValue(left, right) {
159
+ return JSON.stringify(stableValue(left)) === JSON.stringify(stableValue(right));
160
+ }
161
+ function stableValue(value) {
162
+ if (Array.isArray(value))
163
+ return value.map(stableValue);
164
+ if (typeof value === "object" && value !== null) {
165
+ return Object.fromEntries(Object.entries(value)
166
+ .filter(([, entry]) => entry !== undefined)
167
+ .sort(([left], [right]) => left.localeCompare(right))
168
+ .map(([key, entry]) => [key, stableValue(entry)]));
169
+ }
170
+ return value;
171
+ }
172
+ function sha256Json(value) {
173
+ return createHash("sha256").update(JSON.stringify(stableValue(value))).digest("hex");
174
+ }
175
+ function batchContentDigest(batch) {
176
+ return sha256Json(batch);
177
+ }
178
+ function embeddingTextHash(text) {
179
+ return createHash("sha256").update(text, "utf8").digest("hex");
180
+ }
181
+ function isRetrievalExcluded(entity) {
182
+ return entity.props.retrieval_excluded === true;
183
+ }
184
+ function parseState(raw, dimensions) {
185
+ const state = JSON.parse(raw);
186
+ invariant(state.schemaVersion === 1, "Ladybug state schemaVersion 不受支持");
187
+ invariant(state.dimensions === dimensions, `Ladybug state embedding 维度必须为 ${dimensions}`);
188
+ invariant("embeddingIdentity" in state, "Ladybug state 缺少 embedding model identity,必须重新 ingest");
189
+ if (state.embeddingIdentity !== null) {
190
+ nonEmpty(state.embeddingIdentity.providerId, "state embedding providerId");
191
+ nonEmpty(state.embeddingIdentity.modelId, "state embedding modelId");
192
+ invariant(state.embeddingIdentity.queryProfileId === LADYBUG_WORLD_FACT_QUERY_PROFILE_ID, "Ladybug state query profile 不受支持,必须重新 ingest");
193
+ }
194
+ invariant(Array.isArray(state.appliedBatchIds), "Ladybug state appliedBatchIds 无效");
195
+ const appliedBatchDigests = state.appliedBatchDigests ?? {};
196
+ invariant(typeof appliedBatchDigests === "object" && !Array.isArray(appliedBatchDigests), "Ladybug state appliedBatchDigests 无效");
197
+ invariant(Array.isArray(state.entities), "Ladybug state entities 无效");
198
+ invariant(Array.isArray(state.relations), "Ladybug state relations 无效");
199
+ for (const entity of state.entities) {
200
+ nonEmpty(entity.id, "state entity.id");
201
+ nonEmpty(entity.kind, "state entity.kind");
202
+ invariant(typeof entity.text === "string", "state entity.text 必须是 string");
203
+ if (!isRetrievalExcluded(entity))
204
+ nonEmpty(entity.text, "state entity.text");
205
+ if (entity.embedding !== null)
206
+ validateVector(entity.embedding, dimensions, `state entity ${entity.id} embedding`);
207
+ invariant(Array.isArray(entity.sources) && entity.sources.length > 0, `state entity ${entity.id} 缺少 provenance`);
208
+ entity.sources.forEach(validateSource);
209
+ }
210
+ const entities = state.entities.map((entity) => ({
211
+ ...entity,
212
+ scopeId: typeof entity.scopeId === "string" && entity.scopeId.trim().length > 0
213
+ ? entity.scopeId
214
+ : scopeFromProps(entity.props),
215
+ }));
216
+ const rebuiltProfile = buildLexicalProfile(entities);
217
+ const checkpointProfile = state.lexicalProfile;
218
+ if (checkpointProfile !== undefined) {
219
+ if (checkpointProfile === null) {
220
+ invariant(rebuiltProfile === null, "Ladybug lexical profile identity 与当前 Node/ICU/corpus 不一致,必须重建 checkpoint");
221
+ }
222
+ else {
223
+ invariant(typeof checkpointProfile === "object" && !Array.isArray(checkpointProfile), "Ladybug lexical profile 无效");
224
+ const version = checkpointProfile.version;
225
+ if (version === CHINESE_LEXICAL_PROFILE_VERSION) {
226
+ invariant(sameStableValue(checkpointProfile, rebuiltProfile), "Ladybug lexical profile identity 与当前 Node/ICU/corpus 不一致,必须重建 checkpoint");
227
+ }
228
+ else if (version === LEGACY_CHINESE_LEXICAL_PROFILE_VERSION) {
229
+ const rebuiltLegacyProfile = buildLegacyLexicalProfile(entities);
230
+ invariant(sameStableValue(checkpointProfile, rebuiltLegacyProfile), "Ladybug legacy lexical profile identity 与当前 Node/ICU/corpus 不一致,拒绝迁移");
231
+ }
232
+ else if (version === LEGACY_CHINESE_LEXICAL_PROFILE_V2_VERSION) {
233
+ const rebuiltLegacyProfile = buildLegacyLexicalProfileV2(entities);
234
+ invariant(sameStableValue(checkpointProfile, rebuiltLegacyProfile), "Ladybug legacy v2 lexical profile identity 与当前 Node/ICU/corpus 不一致,拒绝迁移");
235
+ }
236
+ else if (version === LEGACY_CHINESE_LEXICAL_PROFILE_V3_VERSION) {
237
+ const rebuiltLegacyProfile = buildLegacyLexicalProfileV3(entities);
238
+ invariant(sameStableValue(checkpointProfile, rebuiltLegacyProfile), "Ladybug legacy v3 lexical profile identity 与当前 Node/ICU/corpus 不一致,拒绝迁移");
239
+ }
240
+ else {
241
+ invariant(false, "Ladybug lexical profile version 不受支持");
242
+ }
243
+ }
244
+ }
245
+ return {
246
+ ...state,
247
+ appliedBatchDigests,
248
+ lexicalProfile: rebuiltProfile,
249
+ entities,
250
+ };
251
+ }
252
+ async function collect(result) {
253
+ if (!result.isSuccess()) {
254
+ const message = await result.getErrorMessage();
255
+ await result.close();
256
+ throw new Error(`LadybugDB statement failed: ${message}`);
257
+ }
258
+ const rows = await result.getAllObjects();
259
+ await result.close();
260
+ return rows;
261
+ }
262
+ async function query(connection, statement) {
263
+ return collect(await connection.query(statement));
264
+ }
265
+ async function execute(connection, statement, params) {
266
+ const prepared = await connection.prepare(statement);
267
+ if (!prepared.isSuccess()) {
268
+ const message = await prepared.getErrorMessage();
269
+ await prepared.close();
270
+ throw new Error(`LadybugDB prepare failed: ${message}`);
271
+ }
272
+ try {
273
+ return await collect(await connection.execute(prepared, params));
274
+ }
275
+ finally {
276
+ await prepared.close();
277
+ }
278
+ }
279
+ async function atomicWrite(path, contents) {
280
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
281
+ try {
282
+ await writeFile(tempPath, contents, { encoding: "utf8", flag: "wx" });
283
+ const handle = await openFile(tempPath, "r+");
284
+ try {
285
+ await handle.sync();
286
+ }
287
+ finally {
288
+ await handle.close();
289
+ }
290
+ await rename(tempPath, path);
291
+ }
292
+ catch (error) {
293
+ await rm(tempPath, { force: true });
294
+ throw error;
295
+ }
296
+ }
297
+ /**
298
+ * LocalProjectHost 可消费的独立 Ladybug runtime。
299
+ * JSON 文件是可重建 projection checkpoint;每次 open 都把它装入真实 LadybugDB 并创建 FTS/HNSW 索引。
300
+ */
301
+ export class LadybugProjectRuntime {
302
+ #statePath;
303
+ #dimensions;
304
+ #embeddingPort;
305
+ #variant;
306
+ #ownsWorkerLifecycle;
307
+ #state;
308
+ #projection;
309
+ #operation = Promise.resolve();
310
+ #closed = false;
311
+ constructor(options) {
312
+ this.#statePath = join(options.directory, STATE_FILE);
313
+ this.#dimensions = options.dimensions;
314
+ this.#embeddingPort = options.embeddingPort;
315
+ this.#variant = options.variant;
316
+ this.#ownsWorkerLifecycle = options.ownsWorkerLifecycle;
317
+ this.#state = emptyState(options.dimensions);
318
+ }
319
+ static async open(options) {
320
+ invariant(Number.isInteger(options.dimensions) && options.dimensions > 0, "dimensions 必须是正整数");
321
+ if (options.embeddingPort !== undefined) {
322
+ invariant(options.embeddingPort.dimensions === options.dimensions, "embedding provider 维度与 runtime 不一致");
323
+ nonEmpty(options.embeddingPort.providerId, "embedding providerId");
324
+ nonEmpty(options.embeddingPort.modelId, "embedding modelId");
325
+ }
326
+ await mkdir(options.directory, { recursive: true });
327
+ const variant = require("@ladybugdb/wasm-core/nodejs");
328
+ const ownsWorkerLifecycle = options.workerLifecycle !== "shared";
329
+ if (ownsWorkerLifecycle)
330
+ await acquireOwnedWorker(variant);
331
+ else
332
+ await variant.init();
333
+ const runtime = new LadybugProjectRuntime({ ...options, variant, ownsWorkerLifecycle });
334
+ let checkpointNeedsRewrite = false;
335
+ try {
336
+ const rawState = await readFile(runtime.#statePath, "utf8");
337
+ const checkpoint = JSON.parse(rawState);
338
+ checkpointNeedsRewrite = !("lexicalProfile" in checkpoint) || !("appliedBatchDigests" in checkpoint)
339
+ || (checkpoint.lexicalProfile !== null && typeof checkpoint.lexicalProfile === "object"
340
+ && (checkpoint.lexicalProfile.version === LEGACY_CHINESE_LEXICAL_PROFILE_VERSION
341
+ || checkpoint.lexicalProfile.version === LEGACY_CHINESE_LEXICAL_PROFILE_V2_VERSION
342
+ || checkpoint.lexicalProfile.version === LEGACY_CHINESE_LEXICAL_PROFILE_V3_VERSION));
343
+ runtime.#state = parseState(rawState, options.dimensions);
344
+ }
345
+ catch (error) {
346
+ if (error.code !== "ENOENT") {
347
+ if (ownsWorkerLifecycle)
348
+ await releaseOwnedWorker(variant);
349
+ throw error;
350
+ }
351
+ }
352
+ try {
353
+ if (runtime.#state.embeddingIdentity !== null && options.embeddingPort !== undefined) {
354
+ invariant(runtime.#state.embeddingIdentity.providerId === options.embeddingPort.providerId, "Ladybug state embedding provider identity 不匹配");
355
+ invariant(runtime.#state.embeddingIdentity.modelId === options.embeddingPort.modelId, "Ladybug state embedding model identity 不匹配");
356
+ }
357
+ runtime.#projection = await runtime.#buildProjection(runtime.#state);
358
+ if (checkpointNeedsRewrite)
359
+ await atomicWrite(runtime.#statePath, `${JSON.stringify(runtime.#state)}\n`);
360
+ return runtime;
361
+ }
362
+ catch (error) {
363
+ await runtime.#projection?.connection.close();
364
+ await runtime.#projection?.db.close();
365
+ runtime.#projection = undefined;
366
+ if (ownsWorkerLifecycle)
367
+ await releaseOwnedWorker(variant);
368
+ throw error;
369
+ }
370
+ }
371
+ async close() {
372
+ if (this.#closed)
373
+ return;
374
+ this.#closed = true;
375
+ await this.#operation;
376
+ const projection = this.#projection;
377
+ this.#projection = undefined;
378
+ await projection?.connection.close();
379
+ await projection?.db.close();
380
+ if (this.#ownsWorkerLifecycle)
381
+ await releaseOwnedWorker(this.#variant);
382
+ }
383
+ async ingest(batch) {
384
+ return this.#serialize(async () => {
385
+ invariant(!this.#closed, "Ladybug runtime 已关闭");
386
+ invariant(batch.schemaVersion === 1, "extraction batch schemaVersion 必须为 1");
387
+ nonEmpty(batch.batchId, "batchId");
388
+ const contentDigest = batchContentDigest(batch);
389
+ const priorDigest = this.#state.appliedBatchDigests[batch.batchId];
390
+ if (priorDigest !== undefined) {
391
+ invariant(priorDigest === contentDigest, `batchId ${batch.batchId} 已用于不同内容`);
392
+ return {
393
+ batchId: batch.batchId,
394
+ entityCount: this.#state.entities.length,
395
+ relationCount: this.#state.relations.length,
396
+ statePath: this.#statePath,
397
+ };
398
+ }
399
+ invariant(!this.#state.appliedBatchIds.includes(batch.batchId), `legacy batchId ${batch.batchId} 缺少内容 digest,不能安全 replay`);
400
+ const providerGenerated = this.#embeddingPort !== undefined
401
+ && batch.entities.some((entity) => entity.embedding === undefined && !isRetrievalExcluded(entity));
402
+ const prepared = await this.#prepareBatch(batch);
403
+ const next = this.#applyBatch(this.#state, prepared, providerGenerated, contentDigest);
404
+ const projection = await this.#buildProjection(next);
405
+ try {
406
+ await atomicWrite(this.#statePath, `${JSON.stringify(next)}\n`);
407
+ }
408
+ catch (error) {
409
+ await projection.connection.close();
410
+ await projection.db.close();
411
+ throw error;
412
+ }
413
+ const prior = this.#projection;
414
+ this.#projection = projection;
415
+ this.#state = next;
416
+ await prior?.connection.close();
417
+ await prior?.db.close();
418
+ return {
419
+ batchId: prepared.batchId,
420
+ entityCount: next.entities.length,
421
+ relationCount: next.relations.length,
422
+ statePath: this.#statePath,
423
+ };
424
+ });
425
+ }
426
+ async recall(request) {
427
+ return this.#serialize(async () => {
428
+ invariant(!this.#closed, "Ladybug runtime 已关闭");
429
+ nonEmpty(request.queryText, "queryText");
430
+ const topK = request.topK ?? 10;
431
+ invariant(Number.isInteger(topK) && topK > 0 && topK <= 100, "topK 必须位于 1..100");
432
+ if (request.scopeId !== undefined)
433
+ nonEmpty(request.scopeId, "scopeId");
434
+ const maxDistance = request.maxVectorDistance ?? DEFAULT_LADYBUG_MAX_VECTOR_DISTANCE;
435
+ invariant(Number.isFinite(maxDistance) && maxDistance >= 0 && maxDistance <= 2, "maxVectorDistance 必须位于 0..2");
436
+ const projection = this.#projection;
437
+ invariant(projection !== undefined, "Ladybug runtime 已关闭");
438
+ let semanticVector;
439
+ if (request.mode !== "keyword") {
440
+ semanticVector = request.queryEmbedding === undefined
441
+ ? await this.#embedQuery(request.queryText)
442
+ : validateVector(request.queryEmbedding, this.#dimensions, "queryEmbedding");
443
+ }
444
+ if (this.#state.entities.length === 0)
445
+ return [];
446
+ if (semanticVector !== undefined) {
447
+ invariant(this.#state.entities.some((entity) => entity.embedding !== null), "semantic/hybrid recall 没有任何真实 entity embedding 可查询");
448
+ }
449
+ const lexicalQuery = request.mode === "semantic" || this.#state.lexicalProfile === null
450
+ ? ""
451
+ : lexicalizeQuery(request.queryText, this.#state.lexicalProfile);
452
+ const keyword = lexicalQuery.length === 0
453
+ ? []
454
+ : await execute(projection.connection, `CALL QUERY_FTS_INDEX('${ENTITY_TABLE}', '${FTS_INDEX}', $query) ` +
455
+ (request.scopeId === undefined ? "" : "WITH node, score WHERE node.scopeId = $scopeId ") +
456
+ "RETURN node.id AS id, score ORDER BY score DESC LIMIT $resultK;", { query: lexicalQuery, scopeId: request.scopeId ?? "", resultK: topK });
457
+ let semantic = [];
458
+ if (semanticVector !== undefined) {
459
+ const candidateK = request.scopeId === undefined
460
+ ? topK
461
+ : this.#state.entities.reduce((count, entity) => count + (entity.embedding === null ? 0 : 1), 0);
462
+ semantic = await execute(projection.connection, `CALL QUERY_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', $query, $candidateK) ` +
463
+ "YIELD node AS entity, distance " +
464
+ (request.scopeId === undefined ? "" : "WITH entity, distance WHERE entity.scopeId = $scopeId ") +
465
+ "RETURN entity.id AS id, distance ORDER BY distance LIMIT $resultK;", { query: semanticVector, candidateK, scopeId: request.scopeId ?? "", resultK: topK });
466
+ }
467
+ const scores = new Map();
468
+ keyword.forEach((row, rank) => {
469
+ const id = String(row.id);
470
+ scores.set(id, { score: 1 / (60 + rank + 1), channels: new Set(["keyword"]) });
471
+ });
472
+ semantic.forEach((row, rank) => {
473
+ const distance = Number(row.distance);
474
+ if (!Number.isFinite(distance) || distance > maxDistance)
475
+ return;
476
+ const id = String(row.id);
477
+ const current = scores.get(id) ?? { score: 0, channels: new Set() };
478
+ current.score += 1 / (60 + rank + 1);
479
+ current.channels.add("semantic");
480
+ scores.set(id, current);
481
+ });
482
+ const byId = new Map(this.#state.entities.map((entity) => [entity.id, entity]));
483
+ return [...scores.entries()]
484
+ .sort((left, right) => right[1].score - left[1].score || left[0].localeCompare(right[0]))
485
+ .slice(0, topK)
486
+ .map(([id, scored]) => {
487
+ const entity = byId.get(id);
488
+ invariant(entity !== undefined, `Ladybug index 返回未知实体 ${id}`);
489
+ return {
490
+ id: entity.id,
491
+ kind: entity.kind,
492
+ scopeId: entity.scopeId,
493
+ text: entity.text,
494
+ props: entity.props,
495
+ score: scored.score,
496
+ channels: [...scored.channels],
497
+ sources: entity.sources,
498
+ };
499
+ });
500
+ });
501
+ }
502
+ async traverse(request) {
503
+ return this.#serialize(async () => {
504
+ invariant(!this.#closed, "Ladybug runtime 已关闭");
505
+ invariant(Array.isArray(request.startIds) && request.startIds.length > 0, "startIds 必须是非空 array");
506
+ invariant(request.startIds.length <= 100, "startIds 最多 100 个");
507
+ const startIds = request.startIds.map((id, index) => {
508
+ nonEmpty(id, `startIds[${index}]`);
509
+ return id;
510
+ });
511
+ invariant(new Set(startIds).size === startIds.length, "startIds 不得重复");
512
+ if (request.scopeId !== undefined)
513
+ nonEmpty(request.scopeId, "scopeId");
514
+ const direction = request.direction ?? "out";
515
+ invariant(direction === "out" || direction === "in" || direction === "both", "direction 必须是 out/in/both");
516
+ const maxHops = request.maxHops ?? 2;
517
+ invariant(Number.isInteger(maxHops) && maxHops >= 1 && maxHops <= 4, "maxHops 必须位于 1..4");
518
+ const maxPaths = request.maxPaths ?? 128;
519
+ invariant(Number.isInteger(maxPaths) && maxPaths >= 1 && maxPaths <= 1_000, "maxPaths 必须位于 1..1000");
520
+ const nodeBudget = request.nodeBudget ?? 256;
521
+ invariant(Number.isInteger(nodeBudget) && nodeBudget >= startIds.length && nodeBudget <= 1_000, "nodeBudget 必须覆盖 startIds 且不超过 1000");
522
+ const relationKinds = new Set(request.relationKinds ?? []);
523
+ invariant(relationKinds.size <= 64, "relationKinds 最多 64 个");
524
+ for (const kind of relationKinds)
525
+ nonEmpty(kind, "relationKinds[]");
526
+ invariant(request.semanticOnly === undefined || typeof request.semanticOnly === "boolean", "semanticOnly 必须是 boolean");
527
+ const semanticOnly = request.semanticOnly ?? false;
528
+ const projection = this.#projection;
529
+ invariant(projection !== undefined, "Ladybug runtime 已关闭");
530
+ for (const id of startIds) {
531
+ const rows = await execute(projection.connection, `MATCH (n:${ENTITY_TABLE} {id:$id}) RETURN n.id AS id, n.scopeId AS scopeId;`, { id });
532
+ invariant(rows.length === 1, `traverse start node 不存在:${id}`);
533
+ if (request.scopeId !== undefined) {
534
+ invariant(rows[0].scopeId === request.scopeId, `traverse start node ${id} 不属于 scope ${request.scopeId}`);
535
+ }
536
+ }
537
+ const visited = new Set(startIds);
538
+ const results = [];
539
+ let frontier = startIds.map((id) => ({ nodeIds: [id], edges: [] }));
540
+ let truncated = false;
541
+ for (let hop = 0; hop < maxHops && frontier.length > 0; hop += 1) {
542
+ const nextFrontier = [];
543
+ for (const path of frontier) {
544
+ const current = path.nodeIds[path.nodeIds.length - 1];
545
+ const candidates = await this.#readAdjacentEdges(current, direction, request.scopeId);
546
+ for (const candidate of candidates) {
547
+ if (relationKinds.size > 0 && !relationKinds.has(candidate.edge.kind))
548
+ continue;
549
+ if (semanticOnly && classifyDocumentRelationRole({ relation: candidate.edge, endpoints: candidate.endpointKinds }) !== "semantic")
550
+ continue;
551
+ if (path.nodeIds.includes(candidate.nextId))
552
+ continue;
553
+ if (!visited.has(candidate.nextId) && visited.size >= nodeBudget) {
554
+ truncated = true;
555
+ continue;
556
+ }
557
+ visited.add(candidate.nextId);
558
+ const nextPath = {
559
+ nodeIds: [...path.nodeIds, candidate.nextId],
560
+ edges: [...path.edges, candidate.edge],
561
+ };
562
+ results.push(nextPath);
563
+ nextFrontier.push(nextPath);
564
+ if (results.length >= maxPaths) {
565
+ truncated = true;
566
+ return { paths: results, visitedNodeIds: [...visited], truncated };
567
+ }
568
+ }
569
+ }
570
+ frontier = nextFrontier;
571
+ }
572
+ return { paths: results, visitedNodeIds: [...visited], truncated };
573
+ });
574
+ }
575
+ async readGraphSnapshot(request = {}) {
576
+ return this.#serialize(async () => {
577
+ invariant(!this.#closed, "Ladybug runtime 已关闭");
578
+ if (request.scopeId !== undefined)
579
+ nonEmpty(request.scopeId, "scopeId");
580
+ const projection = this.#projection;
581
+ invariant(projection !== undefined, "Ladybug runtime 已关闭");
582
+ const nodeCountRows = await execute(projection.connection, `MATCH (n:${ENTITY_TABLE}) ` +
583
+ (request.scopeId === undefined ? "" : "WHERE n.scopeId = $scopeId ") +
584
+ "RETURN count(n) AS count;", { scopeId: request.scopeId ?? "" });
585
+ const edgeCountRows = await execute(projection.connection, `MATCH (a:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(b:${ENTITY_TABLE}) ` +
586
+ (request.scopeId === undefined ? "" : "WHERE a.scopeId = $scopeId AND b.scopeId = $scopeId ") +
587
+ "RETURN count(r) AS count;", { scopeId: request.scopeId ?? "" });
588
+ const expectedNodeCount = Number(nodeCountRows[0]?.count ?? 0);
589
+ const expectedEdgeCount = Number(edgeCountRows[0]?.count ?? 0);
590
+ invariant(Number.isSafeInteger(expectedNodeCount) && expectedNodeCount >= 0, "Ladybug snapshot node count 无效");
591
+ invariant(Number.isSafeInteger(expectedEdgeCount) && expectedEdgeCount >= 0, "Ladybug snapshot edge count 无效");
592
+ const nodeIdentities = await execute(projection.connection, `MATCH (n:${ENTITY_TABLE}) ` +
593
+ (request.scopeId === undefined ? "" : "WHERE n.scopeId = $scopeId ") +
594
+ "RETURN n.id AS id, n.kind AS kind, n.scopeId AS scopeId ORDER BY n.id;", { scopeId: request.scopeId ?? "" });
595
+ const edgeIdentities = await execute(projection.connection, `MATCH (a:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(b:${ENTITY_TABLE}) ` +
596
+ (request.scopeId === undefined ? "" : "WHERE a.scopeId = $scopeId AND b.scopeId = $scopeId ") +
597
+ "RETURN a.id AS src, b.id AS dst, r.kind AS kind ORDER BY a.id, b.id, r.kind;", { scopeId: request.scopeId ?? "" });
598
+ invariant(nodeIdentities.length === expectedNodeCount, `Ladybug snapshot node identity count mismatch: expected ${expectedNodeCount}, got ${nodeIdentities.length}`);
599
+ invariant(edgeIdentities.length === expectedEdgeCount, `Ladybug snapshot edge identity count mismatch: expected ${expectedEdgeCount}, got ${edgeIdentities.length}`);
600
+ const nodeOrder = nodeIdentities.map((row) => ({ id: String(row.id), kind: String(row.kind), scopeId: row.scopeId == null ? null : String(row.scopeId) }));
601
+ invariant(new Set(nodeOrder.map((node) => node.id)).size === nodeOrder.length, "Ladybug snapshot node identity 重复");
602
+ const propsByNodeId = new Map();
603
+ for (const batch of batches(nodeOrder, GRAPH_SNAPSHOT_BATCH_SIZE)) {
604
+ const rows = await execute(projection.connection, `MATCH (n:${ENTITY_TABLE}) WHERE n.id IN $ids RETURN n.id AS id, n.props AS props;`, { ids: batch.map((node) => node.id) });
605
+ invariant(rows.length === batch.length, "Ladybug snapshot node props batch 缺页或包含重复行");
606
+ const expectedIds = new Set(batch.map((node) => node.id));
607
+ for (const row of rows) {
608
+ const id = String(row.id);
609
+ invariant(expectedIds.has(id), `Ladybug snapshot node props 返回额外 id: ${id}`);
610
+ invariant(!propsByNodeId.has(id), `Ladybug snapshot node props 重复 id: ${id}`);
611
+ propsByNodeId.set(id, JSON.parse(String(row.props)));
612
+ }
613
+ }
614
+ const snapshotNodes = nodeOrder.map((node) => {
615
+ const props = propsByNodeId.get(node.id);
616
+ invariant(props !== undefined, `Ladybug snapshot node props 缺少 id: ${node.id}`);
617
+ return { ...node, props: normalizeLadybugGraphSnapshotProps(props) };
618
+ });
619
+ const edgeOrder = edgeIdentities.map((row) => ({ src: String(row.src), dst: String(row.dst), kind: String(row.kind) }));
620
+ invariant(new Set(edgeOrder.map(edgeSnapshotKey)).size === edgeOrder.length, "Ladybug snapshot edge identity 重复");
621
+ const propsByEdgeKey = new Map();
622
+ for (const batch of batches(edgeOrder, GRAPH_SNAPSHOT_BATCH_SIZE)) {
623
+ const params = {};
624
+ const predicates = batch.map((edge, index) => {
625
+ params[`src${index}`] = edge.src;
626
+ params[`dst${index}`] = edge.dst;
627
+ params[`kind${index}`] = edge.kind;
628
+ return `(a.id = $src${index} AND b.id = $dst${index} AND r.kind = $kind${index})`;
629
+ });
630
+ const rows = await execute(projection.connection, `MATCH (a:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(b:${ENTITY_TABLE}) WHERE ${predicates.join(" OR ")} ` +
631
+ "RETURN a.id AS src, b.id AS dst, r.kind AS kind, r.props AS props;", params);
632
+ invariant(rows.length === batch.length, "Ladybug snapshot edge props batch 缺页或包含重复行");
633
+ const expectedKeys = new Set(batch.map(edgeSnapshotKey));
634
+ for (const row of rows) {
635
+ const key = edgeSnapshotKey({ src: String(row.src), dst: String(row.dst), kind: String(row.kind) });
636
+ invariant(expectedKeys.has(key), `Ladybug snapshot edge props 返回额外 key: ${key}`);
637
+ invariant(!propsByEdgeKey.has(key), `Ladybug snapshot edge props 重复 key: ${key}`);
638
+ propsByEdgeKey.set(key, JSON.parse(String(row.props)));
639
+ }
640
+ }
641
+ const snapshotEdges = edgeOrder.map((edge) => {
642
+ const key = edgeSnapshotKey(edge);
643
+ const props = propsByEdgeKey.get(key);
644
+ invariant(props !== undefined, `Ladybug snapshot edge props 缺少 key: ${key}`);
645
+ return { ...edge, props: normalizeLadybugGraphSnapshotProps(props) };
646
+ });
647
+ return {
648
+ nodes: snapshotNodes,
649
+ edges: snapshotEdges,
650
+ nodeCount: expectedNodeCount,
651
+ edgeCount: expectedEdgeCount,
652
+ };
653
+ });
654
+ }
655
+ async #readAdjacentEdges(id, direction, scopeId) {
656
+ const projection = this.#projection;
657
+ invariant(projection !== undefined, "Ladybug runtime 已关闭");
658
+ const rows = [];
659
+ if (direction === "out" || direction === "both") {
660
+ const outgoing = await execute(projection.connection, `MATCH (a:${ENTITY_TABLE} {id:$id})-[r:${RELATION_TABLE}]->(b:${ENTITY_TABLE}) ` +
661
+ (scopeId === undefined ? "" : "WHERE b.scopeId = $scopeId ") +
662
+ "RETURN a.id AS src, b.id AS dst, a.kind AS srcKind, b.kind AS dstKind, b.id AS nextId, r.kind AS kind, r.props AS props, r.sources AS sources ORDER BY r.kind, b.id;", { id, scopeId: scopeId ?? "" });
663
+ rows.push(...outgoing.map((row) => ({ row, nextId: String(row.nextId) })));
664
+ }
665
+ if (direction === "in" || direction === "both") {
666
+ const incoming = await execute(projection.connection, `MATCH (a:${ENTITY_TABLE})-[r:${RELATION_TABLE}]->(b:${ENTITY_TABLE} {id:$id}) ` +
667
+ (scopeId === undefined ? "" : "WHERE a.scopeId = $scopeId ") +
668
+ "RETURN a.id AS src, b.id AS dst, a.kind AS srcKind, b.kind AS dstKind, a.id AS nextId, r.kind AS kind, r.props AS props, r.sources AS sources ORDER BY r.kind, a.id;", { id, scopeId: scopeId ?? "" });
669
+ rows.push(...incoming.map((row) => ({ row, nextId: String(row.nextId) })));
670
+ }
671
+ const seen = new Set();
672
+ const result = [];
673
+ for (const { row, nextId } of rows) {
674
+ const edge = {
675
+ src: String(row.src),
676
+ dst: String(row.dst),
677
+ kind: String(row.kind),
678
+ props: JSON.parse(String(row.props)),
679
+ sources: JSON.parse(String(row.sources)),
680
+ };
681
+ const key = JSON.stringify([nextId, edge.src, edge.dst, edge.kind]);
682
+ if (seen.has(key))
683
+ continue;
684
+ seen.add(key);
685
+ result.push({ nextId, edge, endpointKinds: { srcKind: String(row.srcKind), dstKind: String(row.dstKind) } });
686
+ }
687
+ return result;
688
+ }
689
+ async #embedQuery(text) {
690
+ invariant(this.#embeddingPort !== undefined, "semantic/hybrid recall 需要 queryEmbedding 或真实 embedding provider");
691
+ invariant(this.#state.embeddingIdentity !== null, "持久化 embedding 缺少 provider/model identity,必须显式给出 queryEmbedding 或重新 ingest");
692
+ invariant(this.#state.embeddingIdentity.providerId === this.#embeddingPort.providerId, "query embedding provider 与持久化 entity embedding 不一致");
693
+ invariant(this.#state.embeddingIdentity.modelId === this.#embeddingPort.modelId, "query embedding model 与持久化 entity embedding 不一致");
694
+ return [...await embedLadybugQuery(this.#embeddingPort, text)];
695
+ }
696
+ async #prepareBatch(batch) {
697
+ invariant(batch.schemaVersion === 1, "extraction batch schemaVersion 必须为 1");
698
+ nonEmpty(batch.batchId, "batchId");
699
+ validateSource(batch.source);
700
+ invariant(Array.isArray(batch.entities), "batch.entities 必须是 array");
701
+ invariant(Array.isArray(batch.relations), "batch.relations 必须是 array");
702
+ const ids = new Set();
703
+ const missing = [];
704
+ batch.entities.forEach((entity, index) => {
705
+ nonEmpty(entity.id, `entities[${index}].id`);
706
+ nonEmpty(entity.kind, `entities[${index}].kind`);
707
+ invariant(typeof entity.text === "string", `entities[${index}].text 必须是 string`);
708
+ if (!isRetrievalExcluded(entity))
709
+ nonEmpty(entity.text, `entities[${index}].text`);
710
+ invariant(!ids.has(entity.id), `batch 中实体 id 重复:${entity.id}`);
711
+ ids.add(entity.id);
712
+ if (isRetrievalExcluded(entity)) {
713
+ invariant(entity.embedding === undefined, `retrieval_excluded source_document ${entity.id} 不得携带 embedding`);
714
+ }
715
+ else if (entity.embedding === undefined)
716
+ missing.push(index);
717
+ else
718
+ validateVector(entity.embedding, this.#dimensions, `entities[${index}].embedding`);
719
+ });
720
+ const generated = new Map();
721
+ if (missing.length > 0) {
722
+ if (this.#embeddingPort !== undefined) {
723
+ invariant(!batch.entities.some((entity) => entity.embedding !== undefined), "同一 ingest batch 不得混用 caller-supplied 与 provider-generated embedding");
724
+ }
725
+ if (this.#embeddingPort !== undefined && this.#state.entities.some((entity) => entity.embedding !== null)) {
726
+ invariant(this.#state.embeddingIdentity !== null, "已有 caller-supplied embedding 的 state 不得追加 provider-generated embedding");
727
+ invariant(this.#state.embeddingIdentity.providerId === this.#embeddingPort.providerId, "ingest embedding provider 与持久化 state 不一致");
728
+ invariant(this.#state.embeddingIdentity.modelId === this.#embeddingPort.modelId, "ingest embedding model 与持久化 state 不一致");
729
+ }
730
+ if (this.#embeddingPort !== undefined) {
731
+ const identityMatches = this.#state.embeddingIdentity?.providerId === this.#embeddingPort.providerId
732
+ && this.#state.embeddingIdentity.modelId === this.#embeddingPort.modelId;
733
+ const cached = new Map();
734
+ if (identityMatches) {
735
+ for (const entity of this.#state.entities) {
736
+ if (entity.embedding !== null)
737
+ cached.set(embeddingTextHash(entity.text), entity.embedding);
738
+ }
739
+ }
740
+ const uncachedByHash = new Map();
741
+ for (const index of missing) {
742
+ const text = batch.entities[index].text;
743
+ const hash = embeddingTextHash(text);
744
+ const reused = cached.get(hash);
745
+ if (reused !== undefined) {
746
+ generated.set(index, reused);
747
+ continue;
748
+ }
749
+ const pending = uncachedByHash.get(hash) ?? { text, indexes: [] };
750
+ pending.indexes.push(index);
751
+ uncachedByHash.set(hash, pending);
752
+ }
753
+ const pending = [...uncachedByHash.values()];
754
+ const vectors = pending.length === 0 ? [] : await this.#embeddingPort.embed(pending.map((entry) => entry.text));
755
+ invariant(vectors.length === pending.length, "embedding provider 返回数量与输入不一致");
756
+ pending.forEach((entry, vectorIndex) => {
757
+ const vector = validateVector(vectors[vectorIndex], this.#dimensions, `provider embedding[${vectorIndex}]`);
758
+ entry.indexes.forEach((index) => generated.set(index, vector));
759
+ });
760
+ }
761
+ }
762
+ else if (batch.entities.some((entity) => entity.embedding !== undefined) && this.#state.embeddingIdentity !== null) {
763
+ throw new Error("已绑定 provider/model identity 的 state 不接受来源不明的 caller-supplied embedding");
764
+ }
765
+ const entities = batch.entities.map((entity, index) => ({
766
+ ...entity,
767
+ embedding: entity.embedding === undefined
768
+ ? (this.#embeddingPort === undefined
769
+ ? undefined
770
+ : (isRetrievalExcluded(entity)
771
+ ? undefined
772
+ : validateVector(generated.get(index), this.#dimensions, `provider entities[${index}].embedding`)))
773
+ : validateVector(entity.embedding, this.#dimensions, `entities[${index}].embedding`),
774
+ }));
775
+ const known = new Set([...this.#state.entities.map((entity) => entity.id), ...ids]);
776
+ const relationKeys = new Set();
777
+ batch.relations.forEach((relation, index) => {
778
+ nonEmpty(relation.src, `relations[${index}].src`);
779
+ nonEmpty(relation.dst, `relations[${index}].dst`);
780
+ nonEmpty(relation.kind, `relations[${index}].kind`);
781
+ invariant(known.has(relation.src) && known.has(relation.dst), `relation 两端实体必须存在:${relation.src} -> ${relation.dst}`);
782
+ const key = relationKey(relation);
783
+ invariant(!relationKeys.has(key), `batch 中 relation 重复:${key}`);
784
+ relationKeys.add(key);
785
+ });
786
+ return { ...batch, entities };
787
+ }
788
+ #applyBatch(state, batch, providerGenerated, contentDigest) {
789
+ const entities = new Map(state.entities.map((entity) => [entity.id, entity]));
790
+ for (const entity of batch.entities) {
791
+ const prior = entities.get(entity.id);
792
+ entities.set(entity.id, {
793
+ id: entity.id,
794
+ kind: entity.kind,
795
+ scopeId: entityScope(entity),
796
+ text: entity.text,
797
+ props: entity.props,
798
+ embedding: entity.embedding ?? null,
799
+ sources: mergeSources(prior?.sources ?? [], batch.source),
800
+ });
801
+ }
802
+ const relations = new Map(state.relations.map((relation) => [relationKey(relation), relation]));
803
+ for (const relation of batch.relations) {
804
+ const key = relationKey(relation);
805
+ const prior = relations.get(key);
806
+ relations.set(key, { ...relation, sources: mergeSources(prior?.sources ?? [], batch.source) });
807
+ }
808
+ const nextWithoutProfile = {
809
+ schemaVersion: 1,
810
+ dimensions: this.#dimensions,
811
+ embeddingIdentity: providerGenerated
812
+ ? {
813
+ providerId: this.#embeddingPort.providerId,
814
+ modelId: this.#embeddingPort.modelId,
815
+ queryProfileId: LADYBUG_WORLD_FACT_QUERY_PROFILE_ID,
816
+ }
817
+ : state.embeddingIdentity,
818
+ appliedBatchIds: [...state.appliedBatchIds, batch.batchId],
819
+ appliedBatchDigests: { ...state.appliedBatchDigests, [batch.batchId]: contentDigest },
820
+ lexicalProfile: null,
821
+ entities: [...entities.values()].sort((left, right) => left.id.localeCompare(right.id)),
822
+ relations: [...relations.values()].sort((left, right) => relationKey(left).localeCompare(relationKey(right))),
823
+ };
824
+ return { ...nextWithoutProfile, lexicalProfile: buildLexicalProfile(nextWithoutProfile.entities) };
825
+ }
826
+ async #buildProjection(state) {
827
+ const db = new this.#variant.Database(":memory:");
828
+ await db.init();
829
+ const connection = new this.#variant.Connection(db);
830
+ await connection.init();
831
+ try {
832
+ await query(connection, `CREATE NODE TABLE ${ENTITY_TABLE}(id STRING, kind STRING, scopeId STRING, text STRING, lexicalText STRING, props STRING, sources STRING, embedding FLOAT[${this.#dimensions}], PRIMARY KEY(id));`);
833
+ await query(connection, `CREATE REL TABLE ${RELATION_TABLE}(FROM ${ENTITY_TABLE} TO ${ENTITY_TABLE}, kind STRING, props STRING, sources STRING);`);
834
+ let createEntity;
835
+ let createRelation;
836
+ try {
837
+ if (state.entities.length > 0) {
838
+ createEntity = await connection.prepare(`CREATE (e:${ENTITY_TABLE} {id:$id, kind:$kind, scopeId:$scopeId, text:$text, lexicalText:$lexicalText, props:$props, sources:$sources, embedding:$embedding});`);
839
+ if (!createEntity.isSuccess())
840
+ throw new Error(`LadybugDB prepare failed: ${await createEntity.getErrorMessage()}`);
841
+ }
842
+ if (state.relations.length > 0) {
843
+ createRelation = await connection.prepare(`MATCH (a:${ENTITY_TABLE} {id:$src}), (b:${ENTITY_TABLE} {id:$dst}) CREATE (a)-[:${RELATION_TABLE} {kind:$kind, props:$props, sources:$sources}]->(b);`);
844
+ if (!createRelation.isSuccess())
845
+ throw new Error(`LadybugDB prepare failed: ${await createRelation.getErrorMessage()}`);
846
+ }
847
+ for (const entity of state.entities) {
848
+ invariant(createEntity !== undefined, "Ladybug entity prepared statement 缺失");
849
+ await collect(await connection.execute(createEntity, { id: entity.id, kind: entity.kind, scopeId: entity.scopeId, text: entity.text, lexicalText: isRetrievalExcluded(entity) || state.lexicalProfile === null ? "" : lexicalize(entity.text, state.lexicalProfile), props: JSON.stringify(entity.props), sources: JSON.stringify(entity.sources), embedding: entity.embedding === null ? null : [...entity.embedding] }));
850
+ }
851
+ for (const relation of state.relations) {
852
+ invariant(createRelation !== undefined, "Ladybug relation prepared statement 缺失");
853
+ await collect(await connection.execute(createRelation, { src: relation.src, dst: relation.dst, kind: relation.kind, props: JSON.stringify(relation.props), sources: JSON.stringify(relation.sources) }));
854
+ }
855
+ }
856
+ finally {
857
+ await createRelation?.close();
858
+ await createEntity?.close();
859
+ }
860
+ if (state.lexicalProfile !== null) {
861
+ await query(connection, `CALL CREATE_FTS_INDEX('${ENTITY_TABLE}', '${FTS_INDEX}', ['lexicalText'], stemmer := 'none', tokenizer := 'simple');`);
862
+ }
863
+ if (state.entities.some((entity) => entity.embedding !== null)) {
864
+ await query(connection, `CALL CREATE_VECTOR_INDEX('${ENTITY_TABLE}', '${VECTOR_INDEX}', 'embedding', metric := 'cosine');`);
865
+ }
866
+ return { db, connection };
867
+ }
868
+ catch (error) {
869
+ await connection.close();
870
+ await db.close();
871
+ throw error;
872
+ }
873
+ }
874
+ async #serialize(operation) {
875
+ const prior = this.#operation;
876
+ let release;
877
+ this.#operation = new Promise((resolve) => { release = resolve; });
878
+ await prior;
879
+ try {
880
+ return await operation();
881
+ }
882
+ finally {
883
+ release();
884
+ }
885
+ }
886
+ }