@yugoo-design/dsh-design-knowledge-ladybug 0.0.0-stage → 0.1.0-beta.2

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 (44) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +24 -0
  3. package/README.md +33 -2
  4. package/THIRD_PARTY_NOTICES.md +35 -0
  5. package/lib/dsh-plugin-BPwDtoBY.d.ts +50 -0
  6. package/lib/dsh-plugin-C0TG9tJI.js +1681 -0
  7. package/lib/dsh-plugin.d.ts +2 -0
  8. package/lib/dsh-plugin.js +3 -0
  9. package/lib/embedding-worker.js +80 -0
  10. package/lib/index.d.ts +63 -0
  11. package/lib/index.js +3 -0
  12. package/lib/semantic-index.d.ts +63 -0
  13. package/lib/semantic-index.js +367 -0
  14. package/lib/third-party/ladybug-wasm-core-0.21.1/BUILD-SOURCE.json +112 -0
  15. package/lib/third-party/ladybug-wasm-core-0.21.1/LICENSE +22 -0
  16. package/lib/third-party/ladybug-wasm-core-0.21.1/README.md +43 -0
  17. package/lib/third-party/ladybug-wasm-core-0.21.1/SOURCE.json +37 -0
  18. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/INDEX.json +115 -0
  19. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/fulltext-supplement/CRoaring-4.5.1.LICENSE +236 -0
  20. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/fulltext-supplement/SOURCES.json +70 -0
  21. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/fulltext-supplement/cpp-httplib-0.14.2.LICENSE +22 -0
  22. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/INDEX.json +117 -0
  23. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/antlr4_cypher-lexer.GENERATED.txt +4 -0
  24. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/antlr4_cypher-parser.GENERATED.txt +5 -0
  25. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/fast_float.NOTICE.txt +37 -0
  26. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/httpfs-extension.LICENSE +22 -0
  27. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/httpfs-httplib.NOTICE.txt +7 -0
  28. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/header-notices/roaring_bitmap.NOTICE.txt +56 -0
  29. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/antlr4_runtime/LICENSE +84 -0
  30. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/brotli/LICENSE +19 -0
  31. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/fastpfor/LICENSE +191 -0
  32. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/lz4/LICENSE +24 -0
  33. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/mbedtls/LICENSE +202 -0
  34. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/miniz/LICENSE +22 -0
  35. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/parquet/LICENSE +217 -0
  36. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/re2/LICENSE +27 -0
  37. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/simsimd/LICENSE +201 -0
  38. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/snappy/LICENSE +54 -0
  39. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/thrift/LICENSE +239 -0
  40. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/utf8proc/LICENSE.md +93 -0
  41. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/yyjson/LICENSE +21 -0
  42. package/lib/third-party/ladybug-wasm-core-0.21.1/source-materials/third_party/zstd/LICENSE +30 -0
  43. package/licenses/dsh-distribution.json +36 -0
  44. package/package.json +44 -4
@@ -0,0 +1,1681 @@
1
+ import { DesignSemanticIndex } from "./semantic-index.js";
2
+ import { createRequire } from "node:module";
3
+ import { createHash, randomInt, randomUUID } from "node:crypto";
4
+ import { copyFile, lstat, mkdir, open, readFile, rename, rm, stat } from "node:fs/promises";
5
+ import { constants, createReadStream } from "node:fs";
6
+ import { dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { gunzipSync, gzipSync } from "node:zlib";
8
+ import { DesignKnowledgeCheckpointConflictError, DesignKnowledgeManager, DesignKnowledgeMutationConflictError, DesignKnowledgeMutationId, DesignKnowledgeProposalDigest, DesignKnowledgeProviderId, DesignKnowledgeRevision, DesignKnowledgeRevisionConflictError, assertDesignKnowledgeDecisionAuthority, designKnowledgeProposalDigest, queryKnowledgeSnapshot } from "@yugoo-design/dsh-design-core";
9
+ import lockfile from "proper-lockfile";
10
+ //#region src/codec.ts
11
+ const LIFECYCLES = /* @__PURE__ */ new Set([
12
+ "observed",
13
+ "proposed",
14
+ "accepted",
15
+ "rejected",
16
+ "superseded"
17
+ ]);
18
+ const PRODUCERS = /* @__PURE__ */ new Set([
19
+ "grounding",
20
+ "reflection",
21
+ "alignment",
22
+ "design-file-projection",
23
+ "human-decision",
24
+ "source-analysis"
25
+ ]);
26
+ function canonicalJson(value) {
27
+ if (value === null || typeof value === "boolean" || typeof value === "string") return JSON.stringify(value);
28
+ if (typeof value === "number") {
29
+ if (!Number.isFinite(value)) throw new TypeError("Design Knowledge values must contain only finite numbers");
30
+ return JSON.stringify(value);
31
+ }
32
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
33
+ if (typeof value !== "object") throw new TypeError("Design Knowledge values must be JSON serializable");
34
+ const record = value;
35
+ return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`;
36
+ }
37
+ /** Stable digest for one provider mutation request. */
38
+ function requestDigest(value) {
39
+ return createHash("sha256").update(canonicalJson(value)).digest("hex");
40
+ }
41
+ /** Stable digest binding mutation content and its full Pin. */
42
+ function mutationDigest(delta) {
43
+ return requestDigest(delta);
44
+ }
45
+ /** Bind one atomic proposal-batch replay to every proposal and its full Pin. */
46
+ function proposalBatchRequestDigest(batch) {
47
+ return requestDigest(batch);
48
+ }
49
+ /** Bind one journal decision replay to its full Pin. */
50
+ function decisionRequestDigest(decision, pin) {
51
+ return requestDigest({
52
+ decision,
53
+ pin
54
+ });
55
+ }
56
+ function parsedRecord(payload, kind) {
57
+ if (typeof payload !== "string") throw new Error(`Ladybug knowledge ${kind} payload is not text`);
58
+ const value = JSON.parse(payload);
59
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`Ladybug knowledge ${kind} payload is not an object`);
60
+ return value;
61
+ }
62
+ function record(value) {
63
+ return value !== null && typeof value === "object" && !Array.isArray(value);
64
+ }
65
+ function validProvenance(value) {
66
+ if (!record(value) || typeof value.producer !== "string" || !PRODUCERS.has(value.producer) || typeof value.checkpoint !== "string" || !Array.isArray(value.evidence)) return false;
67
+ return value.evidence.every((item) => record(item) && typeof item.source === "string" && typeof item.revision === "string" && (item.locator === void 0 || typeof item.locator === "string"));
68
+ }
69
+ function validCommon(value, expectedId) {
70
+ return value.id === expectedId && typeof value.lifecycle === "string" && LIFECYCLES.has(value.lifecycle) && record(value.properties) && validProvenance(value.provenance);
71
+ }
72
+ /** Decode one provider-owned entity payload and reject corrupt durable bytes. */
73
+ function decodeEntity(payload, expectedId) {
74
+ const value = parsedRecord(payload, "entity");
75
+ if (!validCommon(value, expectedId) || typeof value.entityType !== "string" || typeof value.label !== "string") throw new Error(`Ladybug knowledge entity ${JSON.stringify(expectedId)} has corrupt durable content`);
76
+ return value;
77
+ }
78
+ /** Decode one provider-owned relation payload and reject corrupt durable bytes. */
79
+ function decodeRelation(payload, expectedId) {
80
+ const value = parsedRecord(payload, "relation");
81
+ if (!validCommon(value, expectedId) || typeof value.relationType !== "string" || typeof value.from !== "string" || typeof value.to !== "string") throw new Error(`Ladybug knowledge relation ${JSON.stringify(expectedId)} has corrupt durable content`);
82
+ return value;
83
+ }
84
+ /** Encode provider-owned immutable JSON without losing branded ids. */
85
+ function encodeRecord(value) {
86
+ return canonicalJson(value);
87
+ }
88
+ /** Encode a proposal after verifying its public neutral digest. */
89
+ function encodeProposal(value) {
90
+ return {
91
+ payload: canonicalJson(value),
92
+ digest: designKnowledgeProposalDigest(value)
93
+ };
94
+ }
95
+ /** Decode one immutable proposal and verify its stored identity and digest. */
96
+ function decodeProposal(payload, expectedId, expectedDigest) {
97
+ if (typeof payload !== "string") throw new Error("Ladybug knowledge proposal payload is not text");
98
+ const value = JSON.parse(payload);
99
+ if (!record(value) || value.id !== expectedId || typeof value.projectId !== "string" || typeof value.checkpoint !== "string" || typeof value.summary !== "string" || !Array.isArray(value.entities) || !Array.isArray(value.relations)) throw new Error(`Ladybug knowledge proposal ${JSON.stringify(expectedId)} has corrupt durable content`);
100
+ const proposal = value;
101
+ for (const entity of proposal.entities) if (decodeEntity(canonicalJson(entity), entity.id).lifecycle !== "proposed") throw new Error("Ladybug knowledge proposal entity is not proposed");
102
+ for (const relation of proposal.relations) if (decodeRelation(canonicalJson(relation), relation.id).lifecycle !== "proposed") throw new Error("Ladybug knowledge proposal relation is not proposed");
103
+ if (designKnowledgeProposalDigest(proposal) !== expectedDigest) throw new Error(`Ladybug knowledge proposal ${JSON.stringify(expectedId)} digest is corrupt`);
104
+ return proposal;
105
+ }
106
+ /** Decode a durable journal decision. */
107
+ function decodeDecision(payload) {
108
+ if (typeof payload !== "string") throw new Error("Ladybug knowledge decision payload is not text");
109
+ const value = JSON.parse(payload);
110
+ if (!record(value) || typeof value.projectId !== "string" || typeof value.proposalId !== "string" || typeof value.proposalRevision !== "string" || typeof value.proposalDigest !== "string" || ![
111
+ "accepted",
112
+ "rejected",
113
+ "deferred"
114
+ ].includes(String(value.decision)) || typeof value.journal !== "string" || !Number.isSafeInteger(value.journalRevision) || Number(value.journalRevision) < 0 || typeof value.checkpoint !== "string") throw new Error("Ladybug knowledge decision has corrupt durable content");
115
+ return value;
116
+ }
117
+ //#endregion
118
+ //#region src/config.ts
119
+ function natural(value, name, allowZero = false) {
120
+ if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1)) throw new TypeError(`Ladybug knowledge ${name} must be ${allowZero ? "a non-negative" : "a positive"} integer`);
121
+ return value;
122
+ }
123
+ /** Validate and resolve provider configuration before opening durable state. */
124
+ function resolveLadybugKnowledgeConfig(input) {
125
+ if (input.databaseRoot.trim() === "") throw new TypeError("Ladybug knowledge databaseRoot must not be empty");
126
+ const databaseRoot = isAbsolute(input.databaseRoot) ? input.databaseRoot : resolve(input.databaseRoot);
127
+ const writerAttempts = natural(input.writerAttempts ?? 16, "writerAttempts");
128
+ const writerRetryMs = natural(input.writerRetryMs ?? 5, "writerRetryMs", true);
129
+ const projectLockRetries = natural(input.projectLockRetries ?? 8, "projectLockRetries", true);
130
+ const projectLockMinTimeoutMs = natural(input.projectLockMinTimeoutMs ?? 25, "projectLockMinTimeoutMs");
131
+ const projectLockStaleMs = natural(input.projectLockStaleMs ?? 1e4, "projectLockStaleMs");
132
+ const maxQueryLimit = natural(input.maxQueryLimit ?? 1e3, "maxQueryLimit");
133
+ const maxFilterValues = natural(input.maxFilterValues ?? 64, "maxFilterValues");
134
+ const maxTextBytes = natural(input.maxTextBytes ?? 16384, "maxTextBytes");
135
+ const maxImpactDepth = natural(input.maxImpactDepth ?? 8, "maxImpactDepth", true);
136
+ const maxImpactSeeds = natural(input.maxImpactSeeds ?? 128, "maxImpactSeeds");
137
+ const maxArchiveRecords = natural(input.maxArchiveRecords ?? 1e5, "maxArchiveRecords");
138
+ if (projectLockStaleMs < 2e3) throw new TypeError("Ladybug knowledge projectLockStaleMs must be at least 2000");
139
+ return Object.freeze({
140
+ databaseRoot,
141
+ writerAttempts,
142
+ writerRetryMs,
143
+ projectLockRetries,
144
+ projectLockMinTimeoutMs,
145
+ projectLockStaleMs,
146
+ maxQueryLimit,
147
+ maxFilterValues,
148
+ maxTextBytes,
149
+ maxImpactDepth,
150
+ maxImpactSeeds,
151
+ maxArchiveRecords
152
+ });
153
+ }
154
+ //#endregion
155
+ //#region src/durability.ts
156
+ /** Flush a copied file without truncating it. Windows requires a writable handle. */
157
+ async function syncKnowledgeFile(path) {
158
+ const file = await open(path, "r+");
159
+ try {
160
+ await file.sync();
161
+ } finally {
162
+ await file.close();
163
+ }
164
+ }
165
+ /**
166
+ * POSIX directory durability after rename/copy. Node cannot flush a Windows
167
+ * directory handle: preserve file flushes and atomic rename, but do not claim
168
+ * the same directory-entry power-loss guarantee there. Never suppress file I/O
169
+ * errors, including EPERM, as an unsupported-directory workaround.
170
+ */
171
+ async function syncKnowledgeDirectory(path) {
172
+ if (!(await stat(path)).isDirectory()) throw new Error("Knowledge durability path must be a directory");
173
+ if (process.platform === "win32") return;
174
+ const directory = await open(path, "r");
175
+ try {
176
+ await directory.sync();
177
+ } finally {
178
+ await directory.close();
179
+ }
180
+ }
181
+ //#endregion
182
+ //#region src/provider.ts
183
+ const ladybug = createRequire(import.meta.url)("@ladybugdb/wasm-core/nodejs");
184
+ let openProviderCount = 0;
185
+ let runtimeStopped = false;
186
+ const AUTHORITATIVE = /* @__PURE__ */ new Set([
187
+ "accepted",
188
+ "rejected",
189
+ "superseded"
190
+ ]);
191
+ const WRITE_BUSY = "Only one write transaction at a time";
192
+ /** Current on-disk schema accepted by the provider. */
193
+ const LADYBUG_KNOWLEDGE_SCHEMA_VERSION = 2;
194
+ /** The per-project cross-process owner could not be acquired. */
195
+ var LadybugProjectLockError = class extends Error {
196
+ databasePath;
197
+ constructor(databasePath, options) {
198
+ super(`Ladybug Design Knowledge project is already open at ${JSON.stringify(databasePath)}`, options);
199
+ this.databasePath = databasePath;
200
+ this.name = "LadybugProjectLockError";
201
+ }
202
+ };
203
+ /** The bounded serialized-writer acquisition budget was exhausted. */
204
+ var LadybugWriterBusyError = class extends Error {
205
+ attempts;
206
+ constructor(attempts) {
207
+ super(`Ladybug Design Knowledge could not acquire its writer after ${attempts} attempts`);
208
+ this.attempts = attempts;
209
+ this.name = "LadybugWriterBusyError";
210
+ }
211
+ };
212
+ /** Existing durable bytes do not identify the requested project and schema. */
213
+ var LadybugSchemaMismatchError = class extends Error {
214
+ databasePath;
215
+ constructor(databasePath, detail, options) {
216
+ super(`Ladybug Design Knowledge database ${JSON.stringify(databasePath)} ${detail}`, options);
217
+ this.databasePath = databasePath;
218
+ this.name = "LadybugSchemaMismatchError";
219
+ }
220
+ };
221
+ async function fileDigest(path) {
222
+ const hash = createHash("sha256");
223
+ for await (const bytes of createReadStream(path)) hash.update(bytes);
224
+ return hash.digest("hex");
225
+ }
226
+ /** Only known durable-format failures permit discarding a derived index. */
227
+ function isRecoverableLadybugError(error) {
228
+ const chain = [];
229
+ const seen = /* @__PURE__ */ new Set();
230
+ for (let e = error; e instanceof Error && !seen.has(e); e = e.cause) {
231
+ seen.add(e);
232
+ chain.push(e);
233
+ }
234
+ const text = chain.map((e) => e.message).join("\n");
235
+ if (/EACCES|EPERM|ENOSPC|EROFS|EMFILE|ENOMEM|permission|disk.*full|out of memory|already open|identity|project metadata|does not identify/i.test(text)) return false;
236
+ return /memory access out of bounds|different storage version|unsupported storage version|database.*(corrupt|invalid header)|incompatible knowledge schema/i.test(text);
237
+ }
238
+ async function readRecovery(path, projectId) {
239
+ let bytes;
240
+ try {
241
+ if (!(await lstat(path)).isFile()) throw new Error("Invalid Knowledge recovery manifest");
242
+ bytes = await readFile(path, "utf8");
243
+ } catch (error) {
244
+ if (error.code === "ENOENT") return;
245
+ throw error;
246
+ }
247
+ const data = JSON.parse(bytes);
248
+ if (data.format !== 1 || data.projectId !== projectId || !/^[a-f0-9]{8}(-[a-f0-9]{4}){3}-[a-f0-9]{12}$/.test(data.generation) || ![
249
+ "backed-up",
250
+ "building",
251
+ "active",
252
+ "failed"
253
+ ].includes(data.state) || !Number.isSafeInteger(data.baseline) || data.baseline < 1 || data.backup !== void 0 && !new RegExp(`^${databaseName(projectId).replace(".", "\\.")}\\.backup-[a-f0-9-]{36}$`).test(data.backup)) throw new Error("Invalid Knowledge recovery identity");
254
+ return data;
255
+ }
256
+ async function writeRecovery(path, data) {
257
+ const temporary = `${path}.${randomUUID()}.tmp`;
258
+ const file = await open(temporary, "wx", 384);
259
+ try {
260
+ await file.writeFile(JSON.stringify(data));
261
+ await file.sync();
262
+ } finally {
263
+ await file.close();
264
+ }
265
+ await rename(temporary, path);
266
+ await syncKnowledgeDirectory(dirname(path));
267
+ }
268
+ /** Stop the package-global Ladybug worker after every project provider closes. */
269
+ async function shutdownLadybugKnowledgeRuntime() {
270
+ if (openProviderCount !== 0) throw new Error(`cannot stop Ladybug Design Knowledge runtime with ${openProviderCount} open provider(s)`);
271
+ if (runtimeStopped) return;
272
+ runtimeStopped = true;
273
+ await ladybug.close();
274
+ }
275
+ function asNumber(value, name) {
276
+ const number = Number(value);
277
+ if (!Number.isSafeInteger(number) || number < 0) throw new Error(`Ladybug Design Knowledge ${name} is corrupt`);
278
+ return number;
279
+ }
280
+ function asString(value, name) {
281
+ if (typeof value !== "string") throw new Error(`Ladybug Design Knowledge ${name} is corrupt`);
282
+ return value;
283
+ }
284
+ function asBoolean(value, name) {
285
+ if (typeof value !== "boolean") throw new Error(`Ladybug Design Knowledge ${name} is corrupt`);
286
+ return value;
287
+ }
288
+ function parseRevision(value) {
289
+ const match = /^rev:(0|[1-9]\d*)$/u.exec(value);
290
+ if (match === null) throw new Error(`invalid Design Knowledge revision ${JSON.stringify(value)}`);
291
+ return asNumber(match[1], "revision");
292
+ }
293
+ function databaseName(projectId) {
294
+ return `${createHash("sha256").update(projectId).digest("hex")}.lbdb`;
295
+ }
296
+ function uniqueIds(values, kind) {
297
+ const ids = /* @__PURE__ */ new Set();
298
+ for (const value of values) {
299
+ if (ids.has(value.id)) throw new Error(`Ladybug Design Knowledge delta contains duplicate ${kind} ${JSON.stringify(value.id)}`);
300
+ ids.add(value.id);
301
+ }
302
+ }
303
+ function uniqueStrings(values, kind) {
304
+ if (new Set(values).size !== values.length) throw new Error(`Ladybug Design Knowledge delta contains duplicate removed ${kind}`);
305
+ }
306
+ function compareIds(left, right) {
307
+ return left < right ? -1 : left > right ? 1 : 0;
308
+ }
309
+ async function fileExists(path) {
310
+ try {
311
+ await stat(path);
312
+ return true;
313
+ } catch (error) {
314
+ if (error.code === "ENOENT") return false;
315
+ throw error;
316
+ }
317
+ }
318
+ /** Private embedded provider for revisioned Design Knowledge projection. */
319
+ var LadybugDesignKnowledgeProvider = class LadybugDesignKnowledgeProvider {
320
+ recovery;
321
+ #baseline = 0;
322
+ #projectId;
323
+ #config;
324
+ #databasePath;
325
+ #database;
326
+ #connection;
327
+ #releaseLock;
328
+ #closed = false;
329
+ #closing = false;
330
+ #connectionClosed = false;
331
+ #databaseClosed = false;
332
+ #lockReleased = false;
333
+ #runtimeCountReleased = false;
334
+ #closeTail = Promise.resolve();
335
+ #operationTail = Promise.resolve();
336
+ #writeTransaction = false;
337
+ #visibleCache;
338
+ constructor(projectId, config, databasePath, database, connection, releaseLock) {
339
+ this.#projectId = projectId;
340
+ this.#config = config;
341
+ this.#databasePath = databasePath;
342
+ this.#database = database;
343
+ this.#connection = connection;
344
+ this.#releaseLock = releaseLock;
345
+ }
346
+ /** Open one exclusively owned project database and initialize its strict schema. */
347
+ static async open(projectId, input, recovery) {
348
+ if (runtimeStopped) throw new Error("Ladybug Design Knowledge runtime has been stopped");
349
+ if (projectId.trim() === "") throw new TypeError("Ladybug Design Knowledge project id must not be empty");
350
+ const config = resolveLadybugKnowledgeConfig(input);
351
+ await mkdir(config.databaseRoot, { recursive: true });
352
+ const databasePath = join(config.databaseRoot, databaseName(projectId));
353
+ let releaseLock;
354
+ try {
355
+ releaseLock = await lockfile.lock(databasePath, {
356
+ realpath: false,
357
+ stale: config.projectLockStaleMs,
358
+ update: Math.max(1e3, Math.floor(config.projectLockStaleMs / 2)),
359
+ retries: {
360
+ retries: config.projectLockRetries,
361
+ factor: 1,
362
+ minTimeout: config.projectLockMinTimeoutMs,
363
+ maxTimeout: config.projectLockMinTimeoutMs
364
+ }
365
+ });
366
+ } catch (error) {
367
+ throw new LadybugProjectLockError(databasePath, { cause: error });
368
+ }
369
+ const manifestPath = `${databasePath}.recovery.json`;
370
+ let candidate;
371
+ let manifest;
372
+ let backup;
373
+ let backupFiles;
374
+ const candidateRoot = (generation) => join(config.databaseRoot, `${databaseName(projectId)}.generations`, generation);
375
+ const openCandidate = async (generation) => {
376
+ const root = candidateRoot(generation);
377
+ for (const path of [join(config.databaseRoot, `${databaseName(projectId)}.generations`), root]) {
378
+ await mkdir(path, { recursive: true });
379
+ if (!(await lstat(path)).isDirectory()) throw new Error("Invalid Knowledge recovery directory");
380
+ }
381
+ if (await fileExists(join(root, databaseName(projectId) + ".recovery.json"))) throw new Error("Nested Knowledge recovery generation is invalid");
382
+ return this.open(projectId, {
383
+ ...config,
384
+ databaseRoot: root
385
+ });
386
+ };
387
+ const adopt = (provider) => {
388
+ const releaseCandidate = provider.#releaseLock;
389
+ let candidateReleased = false;
390
+ provider.#releaseLock = async () => {
391
+ if (!candidateReleased) {
392
+ await releaseCandidate();
393
+ candidateReleased = true;
394
+ }
395
+ await releaseLock();
396
+ };
397
+ return provider;
398
+ };
399
+ try {
400
+ manifest = await readRecovery(manifestPath, projectId);
401
+ if (manifest?.state === "backed-up") {
402
+ backup = manifest.backup;
403
+ if (!backup || !await fileExists(join(config.databaseRoot, backup, databaseName(projectId)))) throw new Error("Knowledge recovery backup is missing");
404
+ if (!(await lstat(join(config.databaseRoot, backup))).isDirectory()) throw new Error("Invalid Knowledge recovery backup directory");
405
+ backupFiles = manifest.backupFiles;
406
+ if (!backupFiles || !backupFiles[""]) throw new Error("Knowledge recovery backup inventory is missing");
407
+ for (const [suffix, digest] of Object.entries(backupFiles)) {
408
+ if (![
409
+ "",
410
+ ".wal",
411
+ ".shadow"
412
+ ].includes(suffix) || !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid Knowledge recovery backup inventory");
413
+ const path = join(config.databaseRoot, backup, databaseName(projectId) + suffix);
414
+ if (!(await lstat(path)).isFile() || await fileDigest(path) !== digest) throw new Error("Knowledge recovery backup verification failed");
415
+ }
416
+ manifest = void 0;
417
+ }
418
+ if (manifest?.state === "failed") {
419
+ if (!recovery?.retryFailed) throw new Error(typeof manifest.error === "string" ? manifest.error : "知识库自动恢复失败;旧库已保留。请检查运行环境后重试恢复。");
420
+ manifest = {
421
+ ...manifest,
422
+ generation: randomUUID(),
423
+ state: "building",
424
+ baseline: Math.max(manifest.baseline + 1e6, Date.now() * 1024 + randomInt(1024))
425
+ };
426
+ await writeRecovery(manifestPath, manifest);
427
+ }
428
+ if (manifest?.state === "active") try {
429
+ if (!await fileExists(join(candidateRoot(manifest.generation), databaseName(projectId)))) throw new Error("Active Knowledge generation is missing");
430
+ candidate = await openCandidate(manifest.generation);
431
+ if (await candidate.#revisionNumber() < manifest.baseline) throw new Error("Active Knowledge generation is missing its verified revision");
432
+ candidate.recovery = {
433
+ state: "ready",
434
+ generation: manifest.generation,
435
+ message: "图谱已从设计文件恢复,旧知识库已保留。"
436
+ };
437
+ candidate.#baseline = manifest.baseline;
438
+ recovery?.onState?.(candidate.recovery);
439
+ return adopt(candidate);
440
+ } catch (error) {
441
+ if (!recovery || !isRecoverableLadybugError(error) && !error.message.startsWith("Active Knowledge generation is missing")) throw error;
442
+ await candidate?.close();
443
+ candidate = void 0;
444
+ manifest = {
445
+ ...manifest,
446
+ generation: randomUUID(),
447
+ state: "building",
448
+ baseline: Math.max(manifest.baseline + 1e6, Date.now() * 1024 + randomInt(1024))
449
+ };
450
+ await writeRecovery(manifestPath, manifest);
451
+ }
452
+ if (!manifest) {
453
+ if (recovery && !backup && await fileExists(databasePath)) {
454
+ backup = `${databaseName(projectId)}.backup-${randomUUID()}`;
455
+ backupFiles = {};
456
+ const directory = join(config.databaseRoot, backup);
457
+ await mkdir(directory, { mode: 448 });
458
+ for (const suffix of [
459
+ "",
460
+ ".wal",
461
+ ".shadow"
462
+ ]) {
463
+ const source = databasePath + suffix;
464
+ if (!await fileExists(source)) continue;
465
+ if (!(await lstat(source)).isFile()) throw new Error("Invalid Knowledge database file");
466
+ await copyFile(source, join(directory, databaseName(projectId) + suffix), constants.COPYFILE_FICLONE);
467
+ await syncKnowledgeFile(join(directory, databaseName(projectId) + suffix));
468
+ backupFiles[suffix] = await fileDigest(join(directory, databaseName(projectId) + suffix));
469
+ }
470
+ await syncKnowledgeDirectory(directory);
471
+ await writeRecovery(manifestPath, {
472
+ format: 1,
473
+ projectId,
474
+ generation: randomUUID(),
475
+ state: "backed-up",
476
+ baseline: Date.now() * 1024 + randomInt(1024),
477
+ backup,
478
+ backupFiles
479
+ });
480
+ }
481
+ try {
482
+ const healthy = await this.#openLocked(projectId, config, databasePath, releaseLock);
483
+ if (backup) {
484
+ await rm(manifestPath).catch(() => {});
485
+ if (!await fileExists(manifestPath)) await rm(join(config.databaseRoot, backup), {
486
+ recursive: true,
487
+ force: true
488
+ }).catch(() => {});
489
+ }
490
+ return healthy;
491
+ } catch (error) {
492
+ if (!recovery || !isRecoverableLadybugError(error)) throw error;
493
+ manifest = {
494
+ format: 1,
495
+ projectId,
496
+ generation: randomUUID(),
497
+ state: "building",
498
+ baseline: Date.now() * 1024 + randomInt(1024),
499
+ backup,
500
+ backupFiles
501
+ };
502
+ await writeRecovery(manifestPath, manifest);
503
+ }
504
+ }
505
+ if (!recovery) throw new Error("知识库恢复尚未完成,请通过设计系统继续恢复。");
506
+ recovery.onState?.({
507
+ state: "rebuilding",
508
+ generation: manifest.generation,
509
+ message: "旧知识库不可读,正在从设计文件自动恢复;你可以继续设计。"
510
+ });
511
+ candidate = await openCandidate(manifest.generation);
512
+ if (await candidate.#revisionNumber() === 0) await candidate.#rows("MATCH (m:KnowledgeMeta {project: $project}) SET m.revision = $revision;", {
513
+ project: projectId,
514
+ revision: manifest.baseline
515
+ });
516
+ candidate.#baseline = manifest.baseline;
517
+ candidate.recovery = {
518
+ state: "rebuilding",
519
+ generation: manifest.generation,
520
+ message: "正在从设计文件重建图谱"
521
+ };
522
+ for (let attempt = 0;; attempt++) try {
523
+ const validateSource = await recovery.rebuild(candidate);
524
+ const before = await candidate.exportProject(projectId);
525
+ await candidate.close();
526
+ candidate = void 0;
527
+ candidate = await openCandidate(manifest.generation);
528
+ candidate.#baseline = manifest.baseline;
529
+ candidate.recovery = {
530
+ state: "rebuilding",
531
+ generation: manifest.generation,
532
+ message: "正在验证重建图谱"
533
+ };
534
+ const after = await candidate.exportProject(projectId);
535
+ if (canonicalJson(before) !== canonicalJson(after)) throw new Error("Knowledge recovery reopen verification failed");
536
+ await validateSource();
537
+ break;
538
+ } catch (error) {
539
+ if (!candidate || !/checkpoint changed/i.test(String(error.message))) throw error;
540
+ if (attempt >= 2) throw new Error("设计文件持续变化,恢复暂未完成;请稍后重试恢复。", { cause: error });
541
+ }
542
+ await writeRecovery(manifestPath, {
543
+ ...manifest,
544
+ state: "active"
545
+ });
546
+ candidate.recovery = {
547
+ state: "ready",
548
+ generation: manifest.generation,
549
+ message: "图谱已从设计文件恢复,旧知识库已保留。"
550
+ };
551
+ recovery.onState?.(candidate.recovery);
552
+ return adopt(candidate);
553
+ } catch (error) {
554
+ await candidate?.close().catch(() => {});
555
+ if (manifest?.state === "building") await writeRecovery(manifestPath, {
556
+ ...manifest,
557
+ state: "failed",
558
+ error: String(error instanceof Error ? error.message : error).slice(0, 1e3)
559
+ }).catch(() => {});
560
+ recovery?.onState?.({
561
+ state: "error",
562
+ message: error instanceof Error ? error.message : String(error),
563
+ generation: manifest?.generation
564
+ });
565
+ await releaseLock().catch(() => {});
566
+ throw error;
567
+ }
568
+ }
569
+ static async #openLocked(projectId, config, databasePath, releaseLock) {
570
+ let isNew;
571
+ let database;
572
+ let connection;
573
+ try {
574
+ isNew = !await fileExists(databasePath);
575
+ database = new ladybug.Database(databasePath);
576
+ connection = new ladybug.Connection(database);
577
+ await connection.init();
578
+ const provider = new LadybugDesignKnowledgeProvider(projectId, config, databasePath, database, connection, releaseLock);
579
+ await provider.#initializeSchema(isNew);
580
+ openProviderCount += 1;
581
+ return provider;
582
+ } catch (error) {
583
+ await connection?.close().catch(() => {});
584
+ await database?.close().catch(() => {});
585
+ if (isNew === false && !(error instanceof LadybugSchemaMismatchError)) throw new LadybugSchemaMismatchError(databasePath, "cannot be opened as a supported project database", { cause: error });
586
+ throw error;
587
+ }
588
+ }
589
+ /** Close the database, release project ownership, and stop the shared worker when unused. */
590
+ async close() {
591
+ const execution = this.#closeTail.then(() => this.#closeOnce(), () => this.#closeOnce());
592
+ this.#closeTail = execution.then(() => void 0, () => void 0);
593
+ return execution;
594
+ }
595
+ async #closeOnce() {
596
+ if (this.#closed) return;
597
+ this.#closing = true;
598
+ await this.#operationTail;
599
+ this.#visibleCache = void 0;
600
+ let primary;
601
+ for (const cleanup of [
602
+ {
603
+ done: () => this.#connectionClosed,
604
+ mark: () => {
605
+ this.#connectionClosed = true;
606
+ },
607
+ run: () => this.#connection.close()
608
+ },
609
+ {
610
+ done: () => this.#databaseClosed,
611
+ mark: () => {
612
+ this.#databaseClosed = true;
613
+ },
614
+ run: () => this.#database.close()
615
+ },
616
+ {
617
+ done: () => this.#lockReleased,
618
+ mark: () => {
619
+ this.#lockReleased = true;
620
+ },
621
+ run: () => this.#releaseLock()
622
+ }
623
+ ]) {
624
+ if (cleanup.done()) continue;
625
+ try {
626
+ await cleanup.run();
627
+ cleanup.mark();
628
+ } catch (error) {
629
+ primary ??= error;
630
+ }
631
+ }
632
+ if (!this.#runtimeCountReleased) {
633
+ this.#runtimeCountReleased = true;
634
+ openProviderCount -= 1;
635
+ }
636
+ this.#closed = this.#connectionClosed && this.#databaseClosed && this.#lockReleased;
637
+ if (primary !== void 0) throw primary;
638
+ }
639
+ async revision(projectId) {
640
+ return this.#serialized(async () => {
641
+ this.#assertProject(projectId);
642
+ return DesignKnowledgeRevision(`rev:${await this.#revisionNumber()}`);
643
+ });
644
+ }
645
+ async applyDelta(delta) {
646
+ return this.#serialized(() => this.#applyDelta(delta));
647
+ }
648
+ /** Disposable vector cache in the same project DB, outside fact revisions/journal. */
649
+ async readSemanticCache(key) {
650
+ if (!/^[a-f0-9]{64}$/u.test(key)) throw new Error("Invalid semantic cache key");
651
+ return this.#serialized(async () => {
652
+ this.#assertProject(this.#projectId);
653
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS DesignSemanticCacheV1(key STRING, payload STRING, PRIMARY KEY(key));");
654
+ const rows = await this.#rows("MATCH (c:DesignSemanticCacheV1 {key: $key}) RETURN c.payload AS payload;", { key });
655
+ return rows.length ? String(rows[0].payload) : void 0;
656
+ });
657
+ }
658
+ async writeSemanticCache(key, value) {
659
+ if (!/^[a-f0-9]{64}$/u.test(key) || Buffer.byteLength(value) > 25e4) throw new Error("Invalid semantic cache entry");
660
+ await this.#serialized(async () => {
661
+ this.#assertProject(this.#projectId);
662
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS DesignSemanticCacheV1(key STRING, payload STRING, PRIMARY KEY(key));");
663
+ await this.#rows("MERGE (c:DesignSemanticCacheV1 {key: $key}) SET c.payload = $payload;", {
664
+ key,
665
+ payload: value
666
+ });
667
+ });
668
+ }
669
+ async #applyDelta(delta) {
670
+ this.#assertProject(delta.pin.projectId);
671
+ uniqueIds(delta.upsertEntities, "entity");
672
+ uniqueIds(delta.upsertRelations, "relation");
673
+ uniqueStrings(delta.removeEntities, "entity");
674
+ uniqueStrings(delta.removeRelations, "relation");
675
+ if (delta.upsertEntities.some((entity) => delta.removeEntities.includes(entity.id))) throw new Error("Ladybug Design Knowledge delta cannot upsert and remove the same entity");
676
+ if (delta.upsertRelations.some((relation) => delta.removeRelations.includes(relation.id))) throw new Error("Ladybug Design Knowledge delta cannot upsert and remove the same relation");
677
+ if (delta.pin.inputCheckpoint !== delta.pin.observedCheckpoint) throw new DesignKnowledgeCheckpointConflictError(delta.pin.inputCheckpoint, delta.pin.observedCheckpoint);
678
+ const digest = mutationDigest(delta);
679
+ await this.#beginWrite();
680
+ try {
681
+ const replay = await this.#rows("MATCH (m:KnowledgeMutation {id: $id}) RETURN m.digest AS digest, m.revision AS revision;", { id: delta.mutationId });
682
+ if (replay.length > 0) {
683
+ await this.#rollback();
684
+ if (asString(replay[0].digest, "mutation digest") !== digest) throw new DesignKnowledgeMutationConflictError(delta.mutationId);
685
+ return {
686
+ revision: DesignKnowledgeRevision(`rev:${asNumber(replay[0].revision, "mutation revision")}`),
687
+ replayed: true
688
+ };
689
+ }
690
+ const current = await this.#revisionNumber();
691
+ if (parseRevision(delta.pin.expectedRevision) !== current) {
692
+ await this.#rollback();
693
+ throw new DesignKnowledgeRevisionConflictError(delta.pin.expectedRevision, DesignKnowledgeRevision(`rev:${current}`));
694
+ }
695
+ const before = await this.#visibleAt(current);
696
+ this.#validateDelta(delta, before);
697
+ const revision = current + 1;
698
+ for (const entity of delta.upsertEntities) {
699
+ await this.#ensureEntityIdentity(entity.id);
700
+ await this.#rows("CREATE (:KnowledgeEntityVersion {key: $key, entityId: $id, revision: $revision, tombstone: false, payload: $payload});", {
701
+ key: `${entity.id}@${revision}`,
702
+ id: entity.id,
703
+ revision,
704
+ payload: encodeRecord(entity)
705
+ });
706
+ }
707
+ for (const relation of delta.upsertRelations) await this.#rows("MATCH (source:KnowledgeEntityIdentity {id: $from}), (target:KnowledgeEntityIdentity {id: $to}) CREATE (source)-[:KnowledgeRelationVersion {key: $key, relationId: $id, revision: $revision, tombstone: false, payload: $payload}]->(target);", {
708
+ from: relation.from,
709
+ to: relation.to,
710
+ key: `${relation.id}@${revision}`,
711
+ id: relation.id,
712
+ revision,
713
+ payload: encodeRecord(relation)
714
+ });
715
+ for (const id of delta.removeRelations) {
716
+ const relation = before.relations.find((candidate) => candidate.id === id);
717
+ if (relation === void 0) continue;
718
+ await this.#rows("MATCH (source:KnowledgeEntityIdentity {id: $from}), (target:KnowledgeEntityIdentity {id: $to}) CREATE (source)-[:KnowledgeRelationVersion {key: $key, relationId: $id, revision: $revision, tombstone: true, payload: $payload}]->(target);", {
719
+ from: relation.from,
720
+ to: relation.to,
721
+ key: `${id}@${revision}`,
722
+ id,
723
+ revision,
724
+ payload: ""
725
+ });
726
+ }
727
+ for (const id of delta.removeEntities) await this.#rows("CREATE (:KnowledgeEntityVersion {key: $key, entityId: $id, revision: $revision, tombstone: true, payload: $payload});", {
728
+ key: `${id}@${revision}`,
729
+ id,
730
+ revision,
731
+ payload: ""
732
+ });
733
+ await this.#rows("CREATE (:KnowledgeMutation {id: $id, digest: $digest, revision: $revision});", {
734
+ id: delta.mutationId,
735
+ digest,
736
+ revision
737
+ });
738
+ await this.#rows("MATCH (m:KnowledgeMeta {project: $project}) SET m.revision = $revision;", {
739
+ project: this.#projectId,
740
+ revision
741
+ });
742
+ await this.#commit();
743
+ await this.#cacheCommittedSnapshot(revision, before, delta.upsertEntities, delta.upsertRelations, delta.removeEntities, delta.removeRelations);
744
+ return {
745
+ revision: DesignKnowledgeRevision(`rev:${revision}`),
746
+ replayed: false
747
+ };
748
+ } catch (error) {
749
+ await this.#rollback().catch(() => {});
750
+ throw error;
751
+ }
752
+ }
753
+ async query(request) {
754
+ return this.#serialized(async () => {
755
+ this.#assertProject(request.projectId);
756
+ this.#validateQuery(request);
757
+ if (request.offset !== void 0 && (!Number.isSafeInteger(request.offset) || request.offset < 0)) throw new Error("Knowledge query offset must be a non-negative safe integer");
758
+ if ((request.offset ?? 0) > 0 && request.revision === void 0) throw new Error("Knowledge continuation requires a pinned revision");
759
+ const current = await this.#revisionNumber();
760
+ const at = request.revision === void 0 ? current : parseRevision(request.revision);
761
+ if (at > current) throw new DesignKnowledgeRevisionConflictError(request.revision, DesignKnowledgeRevision(`rev:${current}`));
762
+ const visible = await this.#visibleAt(at);
763
+ if (at < this.#baseline) throw new Error("Knowledge revision belongs to an earlier database generation");
764
+ return {
765
+ ...queryKnowledgeSnapshot({
766
+ ...visible,
767
+ revision: DesignKnowledgeRevision(`rev:${at}`)
768
+ }, request),
769
+ ...this.recovery?.generation ? { generation: this.recovery.generation } : {}
770
+ };
771
+ });
772
+ }
773
+ async trace(request) {
774
+ return this.#serialized(async () => {
775
+ this.#assertProject(request.projectId);
776
+ const current = await this.#revisionNumber();
777
+ const at = request.revision === void 0 ? current : parseRevision(request.revision);
778
+ if (at > current) throw new DesignKnowledgeRevisionConflictError(request.revision, DesignKnowledgeRevision(`rev:${current}`));
779
+ const visible = await this.#visibleAt(at);
780
+ const record = request.target === "entity" ? visible.entities.find((value) => value.id === request.entityId) : visible.relations.find((value) => value.id === request.relationId);
781
+ if (record === void 0) throw new Error(`Ladybug Design Knowledge ${request.target} is not visible at rev:${at}`);
782
+ return Object.freeze({
783
+ revision: DesignKnowledgeRevision(`rev:${at}`),
784
+ evidence: Object.freeze([...record.provenance.evidence])
785
+ });
786
+ });
787
+ }
788
+ async impact(request) {
789
+ return this.#serialized(async () => {
790
+ this.#assertProject(request.projectId);
791
+ if (!Number.isSafeInteger(request.depth) || request.depth < 0 || request.depth > this.#config.maxImpactDepth) throw new RangeError(`Ladybug Design Knowledge impact depth must be between 0 and ${this.#config.maxImpactDepth}`);
792
+ if (request.entityIds.length + request.relationIds.length > this.#config.maxImpactSeeds) throw new RangeError(`Ladybug Design Knowledge impact seeds exceed ${this.#config.maxImpactSeeds}`);
793
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > this.#config.maxQueryLimit) throw new RangeError(`Ladybug Design Knowledge impact limit must be between 1 and ${this.#config.maxQueryLimit}`);
794
+ const current = await this.#revisionNumber();
795
+ const at = request.revision === void 0 ? current : parseRevision(request.revision);
796
+ if (at > current) throw new DesignKnowledgeRevisionConflictError(request.revision, DesignKnowledgeRevision(`rev:${current}`));
797
+ const visible = await this.#visibleAt(at);
798
+ const entityIds = new Set(request.entityIds);
799
+ const relationIds = new Set(request.relationIds);
800
+ for (const relation of visible.relations) if (relationIds.has(relation.id)) {
801
+ entityIds.add(relation.from);
802
+ entityIds.add(relation.to);
803
+ }
804
+ for (let depth = 0; depth < request.depth; depth += 1) for (const relation of visible.relations) if (entityIds.has(relation.from) || entityIds.has(relation.to)) {
805
+ relationIds.add(relation.id);
806
+ entityIds.add(relation.from);
807
+ entityIds.add(relation.to);
808
+ }
809
+ return Object.freeze({
810
+ revision: DesignKnowledgeRevision(`rev:${at}`),
811
+ entities: Object.freeze(visible.entities.filter((value) => entityIds.has(value.id)).slice(0, request.limit)),
812
+ relations: Object.freeze(visible.relations.filter((value) => relationIds.has(value.id)).slice(0, request.limit))
813
+ });
814
+ });
815
+ }
816
+ async propose(proposal, pin) {
817
+ const result = await this.proposeBatch({
818
+ mutationId: DesignKnowledgeMutationId(proposal.id),
819
+ proposals: Object.freeze([proposal]),
820
+ pin
821
+ });
822
+ return Object.freeze({
823
+ revision: result.revision,
824
+ replayed: result.replayed
825
+ });
826
+ }
827
+ async proposeBatch(batch) {
828
+ return this.#serialized(async () => {
829
+ const { pin, proposals } = batch;
830
+ this.#assertProject(pin.projectId);
831
+ if (proposals.length === 0) throw new Error("Ladybug Design Knowledge proposal batch must not be empty");
832
+ uniqueIds(proposals, "proposal");
833
+ uniqueIds(proposals.flatMap((proposal) => proposal.entities), "proposal entity");
834
+ uniqueIds(proposals.flatMap((proposal) => proposal.relations), "proposal relation");
835
+ const encoded = proposals.map((proposal) => {
836
+ this.#assertProject(proposal.projectId);
837
+ if (proposal.checkpoint !== pin.inputCheckpoint || pin.inputCheckpoint !== pin.observedCheckpoint) throw new DesignKnowledgeCheckpointConflictError(pin.inputCheckpoint, pin.observedCheckpoint);
838
+ if (proposal.summary.trim() === "" || Buffer.byteLength(proposal.summary, "utf8") > this.#config.maxTextBytes) throw new RangeError("Ladybug Design Knowledge proposal summary is empty or exceeds the text budget");
839
+ const value = encodeProposal(proposal);
840
+ decodeProposal(value.payload, proposal.id, value.digest);
841
+ return Object.freeze({
842
+ proposal,
843
+ ...value
844
+ });
845
+ });
846
+ const replayDigest = proposalBatchRequestDigest(batch);
847
+ await this.#beginWrite();
848
+ try {
849
+ const replay = await this.#rows("MATCH (m:KnowledgeMutation {id: $id}) RETURN m.digest AS digest, m.revision AS revision;", { id: batch.mutationId });
850
+ if (replay.length > 0) {
851
+ await this.#rollback();
852
+ if (replay.length !== 1 || replay[0].digest !== replayDigest) throw new DesignKnowledgeMutationConflictError(batch.mutationId);
853
+ const revision = DesignKnowledgeRevision(`rev:${asNumber(replay[0].revision, "proposal batch revision")}`);
854
+ const receipts = [];
855
+ for (const item of encoded) {
856
+ const rows = await this.#rows("MATCH (p:KnowledgeProposal {id: $id}) RETURN p.digest AS digest, p.revision AS revision, p.payload AS payload;", { id: item.proposal.id });
857
+ if (rows.length !== 1 || rows[0].digest !== item.digest || asNumber(rows[0].revision, "proposal revision") !== parseRevision(revision)) throw new Error(`Ladybug Design Knowledge proposal batch ${JSON.stringify(batch.mutationId)} has corrupt replay state`);
858
+ decodeProposal(rows[0].payload, item.proposal.id, item.digest);
859
+ receipts.push(Object.freeze({
860
+ proposalId: item.proposal.id,
861
+ proposalRevision: revision,
862
+ proposalDigest: DesignKnowledgeProposalDigest(item.digest)
863
+ }));
864
+ }
865
+ return Object.freeze({
866
+ revision,
867
+ replayed: true,
868
+ receipts: Object.freeze(receipts)
869
+ });
870
+ }
871
+ const current = await this.#revisionNumber();
872
+ this.#assertExpected(pin.expectedRevision, current);
873
+ for (const item of encoded) if ((await this.#rows("MATCH (p:KnowledgeProposal {id: $id}) RETURN p.id AS id;", { id: item.proposal.id })).length > 0) throw new DesignKnowledgeMutationConflictError(DesignKnowledgeMutationId(item.proposal.id));
874
+ const before = await this.#visibleAt(current);
875
+ const entities = proposals.flatMap((proposal) => proposal.entities);
876
+ const relations = proposals.flatMap((proposal) => proposal.relations);
877
+ this.#validateDelta({
878
+ mutationId: batch.mutationId,
879
+ pin,
880
+ upsertEntities: entities,
881
+ upsertRelations: relations,
882
+ removeRelations: [],
883
+ removeEntities: []
884
+ }, before);
885
+ const revision = current + 1;
886
+ await this.#appendVersions(entities, relations, [], [], before, revision);
887
+ for (const item of encoded) await this.#rows("CREATE (:KnowledgeProposal {id: $id, digest: $digest, requestDigest: $requestDigest, revision: $revision, payload: $payload});", {
888
+ id: item.proposal.id,
889
+ digest: item.digest,
890
+ requestDigest: replayDigest,
891
+ revision,
892
+ payload: item.payload
893
+ });
894
+ await this.#rows("CREATE (:KnowledgeMutation {id: $id, digest: $digest, revision: $revision});", {
895
+ id: batch.mutationId,
896
+ digest: replayDigest,
897
+ revision
898
+ });
899
+ await this.#advance(revision);
900
+ await this.#commit();
901
+ await this.#cacheCommittedSnapshot(revision, before, entities, relations);
902
+ const brandedRevision = DesignKnowledgeRevision(`rev:${revision}`);
903
+ return Object.freeze({
904
+ revision: brandedRevision,
905
+ replayed: false,
906
+ receipts: Object.freeze(encoded.map((item) => Object.freeze({
907
+ proposalId: item.proposal.id,
908
+ proposalRevision: brandedRevision,
909
+ proposalDigest: DesignKnowledgeProposalDigest(item.digest)
910
+ })))
911
+ });
912
+ } catch (error) {
913
+ await this.#rollback().catch(() => {});
914
+ throw error;
915
+ }
916
+ });
917
+ }
918
+ async applyDecision(decision, pin) {
919
+ return this.#serialized(async () => {
920
+ this.#assertProject(decision.projectId);
921
+ this.#assertProject(pin.projectId);
922
+ if (decision.checkpoint !== pin.inputCheckpoint || pin.inputCheckpoint !== pin.observedCheckpoint) throw new DesignKnowledgeCheckpointConflictError(pin.inputCheckpoint, pin.observedCheckpoint);
923
+ const key = createHash("sha256").update(canonicalJson([decision.journal, decision.journalRevision])).digest("hex");
924
+ const digest = decisionRequestDigest(decision, pin);
925
+ await this.#beginWrite();
926
+ try {
927
+ const replay = await this.#rows("MATCH (d:KnowledgeDecision {key: $key}) RETURN d.requestDigest AS requestDigest, d.revision AS revision, d.payload AS payload;", { key });
928
+ if (replay.length > 0) {
929
+ await this.#rollback();
930
+ if (replay.length !== 1 || replay[0].requestDigest !== digest) throw new DesignKnowledgeMutationConflictError(DesignKnowledgeMutationId(key));
931
+ if (canonicalJson(decodeDecision(replay[0].payload)) !== canonicalJson(decision)) throw new Error("Ladybug Design Knowledge replayed decision payload is corrupt");
932
+ return {
933
+ revision: DesignKnowledgeRevision(`rev:${asNumber(replay[0].revision, "decision revision")}`),
934
+ replayed: true
935
+ };
936
+ }
937
+ const current = await this.#revisionNumber();
938
+ this.#assertExpected(pin.expectedRevision, current);
939
+ const proposals = await this.#rows("MATCH (p:KnowledgeProposal {id: $id}) RETURN p.digest AS digest, p.revision AS revision, p.payload AS payload;", { id: decision.proposalId });
940
+ if (proposals.length !== 1 || proposals[0].digest !== decision.proposalDigest || asNumber(proposals[0].revision, "proposal revision") !== parseRevision(decision.proposalRevision)) throw new Error("Ladybug Design Knowledge decision does not bind a visible immutable proposal");
941
+ const proposal = decodeProposal(proposals[0].payload, decision.proposalId, decision.proposalDigest);
942
+ if ((await this.#rows("MATCH (d:KnowledgeDecision) RETURN d.payload AS payload;", {})).map((row) => decodeDecision(row.payload)).some((value) => value.proposalId === decision.proposalId && value.decision !== "deferred")) throw new Error("Ladybug Design Knowledge proposal already has a terminal decision");
943
+ const revision = current + 1;
944
+ let committedSnapshot;
945
+ if (decision.decision !== "deferred") {
946
+ const lifecycle = decision.decision;
947
+ const evidence = Object.freeze([...proposal.entities.flatMap((value) => value.provenance.evidence), {
948
+ source: decision.journal,
949
+ revision: String(decision.journalRevision)
950
+ }]);
951
+ const provenance = Object.freeze({
952
+ producer: "human-decision",
953
+ checkpoint: decision.checkpoint,
954
+ evidence
955
+ });
956
+ const entities = proposal.entities.map((value) => Object.freeze({
957
+ ...value,
958
+ lifecycle,
959
+ provenance
960
+ }));
961
+ const relations = proposal.relations.map((value) => Object.freeze({
962
+ ...value,
963
+ lifecycle,
964
+ provenance
965
+ }));
966
+ const before = await this.#visibleAt(current);
967
+ assertDesignKnowledgeDecisionAuthority(proposal, decision, before.entities);
968
+ this.#assertNoAuthoritativeOverwrite(entities, relations, before);
969
+ this.#assertEndpointIntegrity(entities, relations, before);
970
+ await this.#appendVersions(entities, relations, [], [], before, revision);
971
+ committedSnapshot = {
972
+ before,
973
+ entities,
974
+ relations
975
+ };
976
+ } else {
977
+ const before = await this.#readVisibleSidecar(current);
978
+ if (before) committedSnapshot = {
979
+ before,
980
+ entities: [],
981
+ relations: []
982
+ };
983
+ }
984
+ await this.#rows("CREATE (:KnowledgeDecision {key: $key, requestDigest: $requestDigest, revision: $revision, payload: $payload});", {
985
+ key,
986
+ requestDigest: digest,
987
+ revision,
988
+ payload: canonicalJson(decision)
989
+ });
990
+ await this.#advance(revision);
991
+ await this.#commit();
992
+ if (committedSnapshot) await this.#cacheCommittedSnapshot(revision, committedSnapshot.before, committedSnapshot.entities, committedSnapshot.relations);
993
+ return {
994
+ revision: DesignKnowledgeRevision(`rev:${revision}`),
995
+ replayed: false
996
+ };
997
+ } catch (error) {
998
+ await this.#rollback().catch(() => {});
999
+ throw error;
1000
+ }
1001
+ });
1002
+ }
1003
+ async exportProject(projectId, requested) {
1004
+ return this.#serialized(async () => {
1005
+ this.#assertProject(projectId);
1006
+ const current = await this.#revisionNumber();
1007
+ const at = requested === void 0 ? current : parseRevision(requested);
1008
+ if (at > current) throw new DesignKnowledgeRevisionConflictError(requested, DesignKnowledgeRevision(`rev:${current}`));
1009
+ const visible = await this.#visibleAt(at);
1010
+ const proposalRows = await this.#rows("MATCH (p:KnowledgeProposal) WHERE p.revision <= $revision RETURN p.id AS id, p.digest AS digest, p.revision AS revision, p.payload AS payload ORDER BY revision, id;", { revision: at });
1011
+ const decisionRows = await this.#rows("MATCH (d:KnowledgeDecision) WHERE d.revision <= $revision RETURN d.revision AS revision, d.payload AS payload ORDER BY revision, d.key;", { revision: at });
1012
+ return Object.freeze({
1013
+ formatVersion: 1,
1014
+ projectId: this.#projectId,
1015
+ revision: DesignKnowledgeRevision(`rev:${at}`),
1016
+ entities: Object.freeze(visible.entities),
1017
+ relations: Object.freeze(visible.relations),
1018
+ proposals: Object.freeze(proposalRows.map((row) => Object.freeze({
1019
+ proposal: decodeProposal(row.payload, asString(row.id, "proposal id"), asString(row.digest, "proposal digest")),
1020
+ proposalRevision: DesignKnowledgeRevision(`rev:${asNumber(row.revision, "proposal revision")}`),
1021
+ proposalDigest: DesignKnowledgeProposalDigest(asString(row.digest, "proposal digest"))
1022
+ }))),
1023
+ decisions: Object.freeze(decisionRows.map((row) => decodeDecision(row.payload)))
1024
+ });
1025
+ });
1026
+ }
1027
+ async importProject(archive) {
1028
+ return this.#serialized(async () => {
1029
+ this.#assertProject(archive.projectId);
1030
+ const validated = this.#validateArchive(archive);
1031
+ await this.#beginWrite();
1032
+ try {
1033
+ const current = await this.#revisionNumber();
1034
+ const occupied = await Promise.all([
1035
+ this.#rows("MATCH (p:KnowledgeProposal) RETURN count(p) AS count;", {}),
1036
+ this.#rows("MATCH (d:KnowledgeDecision) RETURN count(d) AS count;", {}),
1037
+ this.#rows("MATCH (v:KnowledgeEntityVersion) RETURN count(v) AS count;", {}),
1038
+ this.#rows("MATCH ()-[v:KnowledgeRelationVersion]->() RETURN count(v) AS count;", {}),
1039
+ this.#rows("MATCH (m:KnowledgeMutation) RETURN count(m) AS count;", {})
1040
+ ]);
1041
+ if (current !== 0 || occupied.some((rows) => asNumber(rows[0]?.count, "namespace record count") !== 0)) throw new Error("Ladybug Design Knowledge import requires an empty project namespace");
1042
+ let target = 0;
1043
+ const mapping = {};
1044
+ const proposalItems = [...validated.proposals].sort((left, right) => parseRevision(left.proposalRevision) - parseRevision(right.proposalRevision));
1045
+ const proposalEntityIds = new Set(proposalItems.flatMap((value) => value.proposal.entities.map((entity) => entity.id)));
1046
+ const proposalRelationIds = new Set(proposalItems.flatMap((value) => value.proposal.relations.map((relation) => relation.id)));
1047
+ const baseEntities = validated.entities.filter((value) => !proposalEntityIds.has(value.id));
1048
+ const nonProposalRelations = validated.relations.filter((value) => !proposalRelationIds.has(value.id));
1049
+ const baseRelations = nonProposalRelations.filter((value) => !proposalEntityIds.has(value.from) && !proposalEntityIds.has(value.to));
1050
+ const dependentRelations = nonProposalRelations.filter((value) => proposalEntityIds.has(value.from) || proposalEntityIds.has(value.to));
1051
+ for (const entity of validated.entities) await this.#ensureEntityIdentity(entity.id);
1052
+ if (baseEntities.length + baseRelations.length > 0) {
1053
+ target += 1;
1054
+ await this.#appendVersions(baseEntities, baseRelations, [], [], {
1055
+ entities: [],
1056
+ relations: []
1057
+ }, target);
1058
+ await this.#advance(target);
1059
+ }
1060
+ for (let index = 0; index < proposalItems.length;) {
1061
+ const sourceRevision = proposalItems[index].proposalRevision;
1062
+ const group = proposalItems.filter((item) => item.proposalRevision === sourceRevision);
1063
+ target += 1;
1064
+ const checkpoint = group[0].proposal.checkpoint;
1065
+ if (group.some((item) => item.proposal.checkpoint !== checkpoint)) throw new Error("Ladybug Design Knowledge archive proposal batch checkpoints disagree");
1066
+ await this.#appendVersions(group.flatMap((item) => item.proposal.entities), group.flatMap((item) => item.proposal.relations), [], [], await this.#visibleAt(target - 1), target);
1067
+ const importPin = {
1068
+ projectId: archive.projectId,
1069
+ inputCheckpoint: checkpoint,
1070
+ observedCheckpoint: checkpoint,
1071
+ expectedRevision: DesignKnowledgeRevision(`rev:${target - 1}`)
1072
+ };
1073
+ const requestDigest = proposalBatchRequestDigest({
1074
+ mutationId: DesignKnowledgeMutationId(`import:${sourceRevision}`),
1075
+ proposals: group.map((item) => item.proposal),
1076
+ pin: importPin
1077
+ });
1078
+ for (const item of group) await this.#rows("CREATE (:KnowledgeProposal {id: $id, digest: $digest, requestDigest: $requestDigest, revision: $revision, payload: $payload});", {
1079
+ id: item.proposal.id,
1080
+ digest: item.proposalDigest,
1081
+ requestDigest,
1082
+ revision: target,
1083
+ payload: encodeProposal(item.proposal).payload
1084
+ });
1085
+ mapping[sourceRevision] = DesignKnowledgeRevision(`rev:${target}`);
1086
+ await this.#advance(target);
1087
+ index += group.length;
1088
+ }
1089
+ if (dependentRelations.length > 0) {
1090
+ target += 1;
1091
+ await this.#appendVersions([], dependentRelations, [], [], await this.#visibleAt(target - 1), target);
1092
+ await this.#advance(target);
1093
+ }
1094
+ for (const decision of validated.decisions) {
1095
+ target += 1;
1096
+ const item = proposalItems.find((value) => value.proposal.id === decision.proposalId);
1097
+ if (decision.decision !== "deferred") {
1098
+ const lifecycle = decision.decision;
1099
+ const evidence = Object.freeze([...item.proposal.entities.flatMap((value) => value.provenance.evidence), {
1100
+ source: decision.journal,
1101
+ revision: String(decision.journalRevision)
1102
+ }]);
1103
+ const provenance = Object.freeze({
1104
+ producer: "human-decision",
1105
+ checkpoint: decision.checkpoint,
1106
+ evidence
1107
+ });
1108
+ await this.#appendVersions(item.proposal.entities.map((value) => ({
1109
+ ...value,
1110
+ lifecycle,
1111
+ provenance
1112
+ })), item.proposal.relations.map((value) => ({
1113
+ ...value,
1114
+ lifecycle,
1115
+ provenance
1116
+ })), [], [], await this.#visibleAt(target - 1), target);
1117
+ }
1118
+ const remapped = Object.freeze({
1119
+ ...decision,
1120
+ proposalRevision: mapping[decision.proposalRevision]
1121
+ });
1122
+ const key = createHash("sha256").update(canonicalJson([decision.journal, decision.journalRevision])).digest("hex");
1123
+ const pin = {
1124
+ projectId: archive.projectId,
1125
+ inputCheckpoint: decision.checkpoint,
1126
+ observedCheckpoint: decision.checkpoint,
1127
+ expectedRevision: DesignKnowledgeRevision(`rev:${target - 1}`)
1128
+ };
1129
+ await this.#rows("CREATE (:KnowledgeDecision {key: $key, requestDigest: $requestDigest, revision: $revision, payload: $payload});", {
1130
+ key,
1131
+ requestDigest: decisionRequestDigest(remapped, pin),
1132
+ revision: target,
1133
+ payload: canonicalJson(remapped)
1134
+ });
1135
+ await this.#advance(target);
1136
+ }
1137
+ const reconstructed = await this.#visibleAt(target);
1138
+ const changedEntities = validated.entities.filter((value) => {
1139
+ const currentValue = reconstructed.entities.find((candidate) => candidate.id === value.id);
1140
+ return currentValue === void 0 || canonicalJson(currentValue) !== canonicalJson(value);
1141
+ });
1142
+ const changedRelations = validated.relations.filter((value) => {
1143
+ const currentValue = reconstructed.relations.find((candidate) => candidate.id === value.id);
1144
+ return currentValue === void 0 || canonicalJson(currentValue) !== canonicalJson(value);
1145
+ });
1146
+ const finalEntityIds = new Set(validated.entities.map((value) => value.id));
1147
+ const finalRelationIds = new Set(validated.relations.map((value) => value.id));
1148
+ const removeEntities = reconstructed.entities.filter((value) => !finalEntityIds.has(value.id)).map((value) => value.id);
1149
+ const removeRelations = reconstructed.relations.filter((value) => !finalRelationIds.has(value.id)).map((value) => value.id);
1150
+ if (changedEntities.length + changedRelations.length + removeEntities.length + removeRelations.length > 0) {
1151
+ target += 1;
1152
+ await this.#appendVersions(changedEntities, changedRelations, removeRelations, removeEntities, reconstructed, target);
1153
+ await this.#advance(target);
1154
+ }
1155
+ mapping[archive.revision] ??= DesignKnowledgeRevision(`rev:${target}`);
1156
+ await this.#commit();
1157
+ return Object.freeze({
1158
+ revision: DesignKnowledgeRevision(`rev:${target}`),
1159
+ sourceToTargetRevisions: Object.freeze(mapping)
1160
+ });
1161
+ } catch (error) {
1162
+ await this.#rollback().catch(() => {});
1163
+ throw error;
1164
+ }
1165
+ });
1166
+ }
1167
+ async #initializeSchema(isNew) {
1168
+ if (!isNew) {
1169
+ let schema;
1170
+ try {
1171
+ schema = await this.#rows("MATCH (s:KnowledgeSchema) RETURN s.key AS key, s.schemaVersion AS schemaVersion, s.project AS project;", {});
1172
+ } catch (error) {
1173
+ throw new LadybugSchemaMismatchError(this.#databasePath, "has no supported schema identity", { cause: error });
1174
+ }
1175
+ if (schema.length !== 1 || schema[0].key !== "singleton" || schema[0].project !== this.#projectId) throw new LadybugSchemaMismatchError(this.#databasePath, `does not identify schema 2 for project ${JSON.stringify(this.#projectId)}`);
1176
+ if (asNumber(schema[0].schemaVersion, "schema version") !== 2) throw new LadybugSchemaMismatchError(this.#databasePath, "has incompatible Knowledge schema");
1177
+ await this.#assertMetaIdentity();
1178
+ return;
1179
+ }
1180
+ await this.#fixed("CREATE NODE TABLE KnowledgeSchema(key STRING, schemaVersion INT64, project STRING, PRIMARY KEY(key));");
1181
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeMeta(project STRING, revision INT64, PRIMARY KEY(project));");
1182
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeMutation(id STRING, digest STRING, revision INT64, PRIMARY KEY(id));");
1183
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeProposal(id STRING, digest STRING, requestDigest STRING, revision INT64, payload STRING, PRIMARY KEY(id));");
1184
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeDecision(key STRING, requestDigest STRING, revision INT64, payload STRING, PRIMARY KEY(key));");
1185
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeEntityIdentity(id STRING, PRIMARY KEY(id));");
1186
+ await this.#fixed("CREATE NODE TABLE IF NOT EXISTS KnowledgeEntityVersion(key STRING, entityId STRING, revision INT64, tombstone BOOL, payload STRING, PRIMARY KEY(key));");
1187
+ await this.#fixed("CREATE REL TABLE IF NOT EXISTS KnowledgeRelationVersion(FROM KnowledgeEntityIdentity TO KnowledgeEntityIdentity, key STRING, relationId STRING, revision INT64, tombstone BOOL, payload STRING);");
1188
+ await this.#rows("CREATE (:KnowledgeSchema {key: $key, schemaVersion: $schemaVersion, project: $project});", {
1189
+ key: "singleton",
1190
+ schemaVersion: 2,
1191
+ project: this.#projectId
1192
+ });
1193
+ await this.#rows("CREATE (:KnowledgeMeta {project: $project, revision: 0});", { project: this.#projectId });
1194
+ await this.#assertMetaIdentity();
1195
+ }
1196
+ async #assertMetaIdentity() {
1197
+ const rows = await this.#rows("MATCH (m:KnowledgeMeta) RETURN m.project AS project, m.revision AS revision;", {});
1198
+ if (rows.length !== 1 || rows[0].project !== this.#projectId) throw new LadybugSchemaMismatchError(this.#databasePath, `has invalid project metadata for ${JSON.stringify(this.#projectId)}`);
1199
+ asNumber(rows[0].revision, "meta revision");
1200
+ }
1201
+ async #beginWrite() {
1202
+ for (let attempt = 1; attempt <= this.#config.writerAttempts; attempt += 1) try {
1203
+ await this.#fixed("BEGIN TRANSACTION;");
1204
+ this.#visibleCache = void 0;
1205
+ this.#writeTransaction = true;
1206
+ return;
1207
+ } catch (error) {
1208
+ if (!String(error.message).includes(WRITE_BUSY)) throw error;
1209
+ if (attempt < this.#config.writerAttempts) await new Promise((resolve) => setTimeout(resolve, this.#config.writerRetryMs));
1210
+ }
1211
+ throw new LadybugWriterBusyError(this.#config.writerAttempts);
1212
+ }
1213
+ async #commit() {
1214
+ try {
1215
+ await this.#fixed("COMMIT;");
1216
+ } finally {
1217
+ this.#visibleCache = void 0;
1218
+ this.#writeTransaction = false;
1219
+ }
1220
+ }
1221
+ async #rollback() {
1222
+ try {
1223
+ await this.#fixed("ROLLBACK;");
1224
+ } finally {
1225
+ this.#visibleCache = void 0;
1226
+ this.#writeTransaction = false;
1227
+ }
1228
+ }
1229
+ async #revisionNumber() {
1230
+ const rows = await this.#rows("MATCH (m:KnowledgeMeta {project: $project}) RETURN m.revision AS revision;", { project: this.#projectId });
1231
+ if (rows.length !== 1) throw new Error(`Ladybug Design Knowledge meta for ${JSON.stringify(this.#projectId)} is missing or duplicated`);
1232
+ return asNumber(rows[0].revision, "meta revision");
1233
+ }
1234
+ async #visibleAt(revision) {
1235
+ const cached = this.#visibleCache;
1236
+ if (cached?.revision === revision && cached.generation === this.recovery?.generation && cached.baseline === this.#baseline) return structuredClone({
1237
+ entities: cached.entities,
1238
+ relations: cached.relations
1239
+ });
1240
+ const currentRead = revision === await this.#revisionNumber();
1241
+ if (currentRead) {
1242
+ const persisted = await this.#readVisibleSidecar(revision);
1243
+ if (persisted !== void 0) {
1244
+ if (!this.#writeTransaction && persisted.entities.length + persisted.relations.length <= 5e3) this.#visibleCache = {
1245
+ revision,
1246
+ generation: this.recovery?.generation,
1247
+ baseline: this.#baseline,
1248
+ ...structuredClone(persisted)
1249
+ };
1250
+ return persisted;
1251
+ }
1252
+ }
1253
+ const entityRows = await this.#rows("MATCH (v:KnowledgeEntityVersion) WHERE v.revision <= $revision RETURN v.entityId AS id, v.revision AS revision, v.tombstone AS tombstone, v.payload AS payload ORDER BY id, revision DESC;", { revision });
1254
+ const relationRows = await this.#rows("MATCH (:KnowledgeEntityIdentity)-[v:KnowledgeRelationVersion]->(:KnowledgeEntityIdentity) WHERE v.revision <= $revision RETURN v.relationId AS id, v.revision AS revision, v.tombstone AS tombstone, v.payload AS payload ORDER BY id, revision DESC;", { revision });
1255
+ const visible = {
1256
+ entities: this.#fold(entityRows, "entity").map((row) => decodeEntity(row.payload, row.id)),
1257
+ relations: this.#fold(relationRows, "relation").map((row) => decodeRelation(row.payload, row.id))
1258
+ };
1259
+ if (!this.#writeTransaction && visible.entities.length + visible.relations.length <= 5e3) this.#visibleCache = {
1260
+ revision,
1261
+ generation: this.recovery?.generation,
1262
+ baseline: this.#baseline,
1263
+ ...structuredClone(visible)
1264
+ };
1265
+ if (currentRead && !this.#writeTransaction) await this.#writeVisibleSidecar(revision, visible, [entityRows.length, relationRows.length]);
1266
+ return visible;
1267
+ }
1268
+ async #cacheCommittedSnapshot(revision, before, entities, relations, removedEntities = [], removedRelations = []) {
1269
+ try {
1270
+ const entityMap = new Map(before.entities.map((item) => [item.id, item]));
1271
+ const relationMap = new Map(before.relations.map((item) => [item.id, item]));
1272
+ for (const id of removedEntities) entityMap.delete(id);
1273
+ for (const id of removedRelations) relationMap.delete(id);
1274
+ for (const item of entities) entityMap.set(item.id, item);
1275
+ for (const item of relations) relationMap.set(item.id, item);
1276
+ const visible = {
1277
+ entities: [...entityMap.values()].sort((a, b) => compareIds(String(a.id), String(b.id))),
1278
+ relations: [...relationMap.values()].sort((a, b) => compareIds(String(a.id), String(b.id)))
1279
+ };
1280
+ if (visible.entities.length + visible.relations.length <= 5e3) this.#visibleCache = {
1281
+ revision,
1282
+ generation: this.recovery?.generation,
1283
+ baseline: this.#baseline,
1284
+ ...structuredClone(visible)
1285
+ };
1286
+ await this.#writeVisibleSidecar(revision, visible, await this.#versionCounts());
1287
+ } catch {}
1288
+ }
1289
+ #visibleSidecarPath() {
1290
+ return `${this.#databasePath}.visible-v1.gz`;
1291
+ }
1292
+ async #databaseIdentity() {
1293
+ const file = await stat(this.#databasePath);
1294
+ const birth = Number.isFinite(file.birthtimeMs) ? file.birthtimeMs : 0;
1295
+ return `${file.dev}:${file.ino}:${birth}:${file.ino === 0 && birth === 0 ? `${file.size}:${file.mtimeMs}` : ""}`;
1296
+ }
1297
+ async #readVisibleSidecar(revision) {
1298
+ try {
1299
+ const path = this.#visibleSidecarPath();
1300
+ const input = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
1301
+ let compressed;
1302
+ try {
1303
+ const file = await input.stat();
1304
+ if (!file.isFile() || file.size > 16777216) return void 0;
1305
+ compressed = Buffer.alloc(file.size);
1306
+ for (let position = 0; position < compressed.length;) {
1307
+ const { bytesRead } = await input.read(compressed, position, compressed.length - position, position);
1308
+ if (bytesRead === 0) return void 0;
1309
+ position += bytesRead;
1310
+ }
1311
+ } finally {
1312
+ await input.close();
1313
+ }
1314
+ const parsed = JSON.parse(gunzipSync(compressed, { maxOutputLength: 67108864 }).toString("utf8"));
1315
+ if (parsed.schemaVersion !== 1 || parsed.project !== this.#projectId || parsed.database !== await this.#databaseIdentity() || parsed.generation !== (this.recovery?.generation ?? null) || parsed.baseline !== this.#baseline || parsed.revision !== revision || !Array.isArray(parsed.entities) || !Array.isArray(parsed.relations) || !Array.isArray(parsed.versionCounts) || parsed.versionCounts.length !== 2) return void 0;
1316
+ const counts = await this.#versionCounts();
1317
+ if (parsed.versionCounts[0] !== counts[0] || parsed.versionCounts[1] !== counts[1]) return void 0;
1318
+ const content = JSON.stringify({
1319
+ entities: parsed.entities,
1320
+ relations: parsed.relations
1321
+ });
1322
+ if (parsed.digest !== createHash("sha256").update(content).digest("hex")) return void 0;
1323
+ const entities = [];
1324
+ const relations = [];
1325
+ const entityIds = /* @__PURE__ */ new Set();
1326
+ const relationIds = /* @__PURE__ */ new Set();
1327
+ for (const raw of parsed.entities) {
1328
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw) || !("id" in raw) || typeof raw.id !== "string" || entityIds.has(raw.id)) return void 0;
1329
+ const entity = decodeEntity(canonicalJson(raw), raw.id);
1330
+ entityIds.add(entity.id);
1331
+ entities.push(entity);
1332
+ }
1333
+ for (const raw of parsed.relations) {
1334
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw) || !("id" in raw) || typeof raw.id !== "string" || relationIds.has(raw.id)) return void 0;
1335
+ const relation = decodeRelation(canonicalJson(raw), raw.id);
1336
+ if (!entityIds.has(relation.from) || !entityIds.has(relation.to)) return void 0;
1337
+ relationIds.add(relation.id);
1338
+ relations.push(relation);
1339
+ }
1340
+ return {
1341
+ entities,
1342
+ relations
1343
+ };
1344
+ } catch {
1345
+ return;
1346
+ }
1347
+ }
1348
+ async #versionCounts() {
1349
+ const entities = await this.#rows("MATCH (v:KnowledgeEntityVersion) RETURN count(v) AS n;", {});
1350
+ const relations = await this.#rows("MATCH ()-[v:KnowledgeRelationVersion]->() RETURN count(v) AS n;", {});
1351
+ return [asNumber(entities[0]?.n, "entity version count"), asNumber(relations[0]?.n, "relation version count")];
1352
+ }
1353
+ async #writeVisibleSidecar(revision, visible, versionCounts) {
1354
+ let temporary;
1355
+ try {
1356
+ const content = JSON.stringify(visible);
1357
+ if (Buffer.byteLength(content) > 67108864) return;
1358
+ const payload = gzipSync(JSON.stringify({
1359
+ schemaVersion: 1,
1360
+ project: this.#projectId,
1361
+ database: await this.#databaseIdentity(),
1362
+ generation: this.recovery?.generation ?? null,
1363
+ baseline: this.#baseline,
1364
+ revision,
1365
+ versionCounts,
1366
+ digest: createHash("sha256").update(content).digest("hex"),
1367
+ ...visible
1368
+ }));
1369
+ if (payload.byteLength > 16777216) return;
1370
+ temporary = `${this.#visibleSidecarPath()}.${randomUUID()}.tmp`;
1371
+ const output = await open(temporary, "wx", 384);
1372
+ try {
1373
+ await output.writeFile(payload);
1374
+ await output.sync();
1375
+ } finally {
1376
+ await output.close();
1377
+ }
1378
+ await rename(temporary, this.#visibleSidecarPath());
1379
+ temporary = void 0;
1380
+ } catch {} finally {
1381
+ if (temporary !== void 0) await rm(temporary, { force: true }).catch(() => {});
1382
+ }
1383
+ }
1384
+ #fold(rows, kind) {
1385
+ const latest = /* @__PURE__ */ new Map();
1386
+ for (const row of rows) {
1387
+ const value = {
1388
+ id: asString(row.id, `${kind} id`),
1389
+ revision: asNumber(row.revision, `${kind} revision`),
1390
+ tombstone: asBoolean(row.tombstone, `${kind} tombstone`),
1391
+ payload: row.payload
1392
+ };
1393
+ if (!latest.has(value.id)) latest.set(value.id, value);
1394
+ }
1395
+ return [...latest.values()].filter((row) => !row.tombstone).sort((left, right) => compareIds(left.id, right.id));
1396
+ }
1397
+ #validateDelta(delta, before) {
1398
+ for (const record of [...delta.upsertEntities, ...delta.upsertRelations]) if (record.lifecycle !== "observed" && record.lifecycle !== "proposed") throw new Error(`Ladybug Design Knowledge delta cannot write authoritative lifecycle ${JSON.stringify(record.lifecycle)}`);
1399
+ const removeEntities = new Set(delta.removeEntities);
1400
+ const removeRelations = new Set(delta.removeRelations);
1401
+ for (const entity of before.entities) if (AUTHORITATIVE.has(entity.lifecycle) && (removeEntities.has(entity.id) || delta.upsertEntities.some((candidate) => candidate.id === entity.id))) throw new Error(`Ladybug Design Knowledge delta cannot overwrite or remove authoritative entity ${JSON.stringify(entity.id)}`);
1402
+ for (const relation of before.relations) if (AUTHORITATIVE.has(relation.lifecycle) && (removeRelations.has(relation.id) || delta.upsertRelations.some((candidate) => candidate.id === relation.id))) throw new Error(`Ladybug Design Knowledge delta cannot overwrite or remove authoritative relation ${JSON.stringify(relation.id)}`);
1403
+ const entities = new Set(before.entities.map((entity) => entity.id));
1404
+ for (const id of delta.removeEntities) entities.delete(id);
1405
+ for (const entity of delta.upsertEntities) entities.add(entity.id);
1406
+ const relations = new Map(before.relations.map((relation) => [relation.id, relation]));
1407
+ for (const id of delta.removeRelations) relations.delete(id);
1408
+ for (const relation of delta.upsertRelations) relations.set(relation.id, relation);
1409
+ for (const relation of relations.values()) if (!entities.has(relation.from) || !entities.has(relation.to)) throw new Error(`Ladybug Design Knowledge relation ${JSON.stringify(relation.id)} has an endpoint absent from the same revision`);
1410
+ }
1411
+ #assertExpected(expectedRevision, current) {
1412
+ if (parseRevision(expectedRevision) !== current) throw new DesignKnowledgeRevisionConflictError(expectedRevision, DesignKnowledgeRevision(`rev:${current}`));
1413
+ }
1414
+ #assertNoAuthoritativeOverwrite(entities, relations, before) {
1415
+ for (const entity of entities) if (before.entities.some((value) => value.id === entity.id && AUTHORITATIVE.has(value.lifecycle))) throw new Error(`Ladybug Design Knowledge decision cannot overwrite authoritative entity ${JSON.stringify(entity.id)}`);
1416
+ for (const relation of relations) if (before.relations.some((value) => value.id === relation.id && AUTHORITATIVE.has(value.lifecycle))) throw new Error(`Ladybug Design Knowledge decision cannot overwrite authoritative relation ${JSON.stringify(relation.id)}`);
1417
+ }
1418
+ #assertEndpointIntegrity(entities, relations, before) {
1419
+ const visibleEntityIds = new Set(before.entities.map((value) => value.id));
1420
+ for (const entity of entities) visibleEntityIds.add(entity.id);
1421
+ const visibleRelations = new Map(before.relations.map((value) => [value.id, value]));
1422
+ for (const relation of relations) visibleRelations.set(relation.id, relation);
1423
+ for (const relation of visibleRelations.values()) if (!visibleEntityIds.has(relation.from) || !visibleEntityIds.has(relation.to)) throw new Error(`Ladybug Design Knowledge decision relation ${JSON.stringify(relation.id)} has an endpoint absent from the same revision`);
1424
+ }
1425
+ async #advance(revision) {
1426
+ await this.#rows("MATCH (m:KnowledgeMeta {project: $project}) SET m.revision = $revision;", {
1427
+ project: this.#projectId,
1428
+ revision
1429
+ });
1430
+ }
1431
+ async #appendVersions(entities, relations, removeRelations, removeEntities, before, revision) {
1432
+ for (const entity of entities) {
1433
+ await this.#ensureEntityIdentity(entity.id);
1434
+ await this.#rows("CREATE (:KnowledgeEntityVersion {key: $key, entityId: $id, revision: $revision, tombstone: false, payload: $payload});", {
1435
+ key: `${entity.id}@${revision}`,
1436
+ id: entity.id,
1437
+ revision,
1438
+ payload: encodeRecord(entity)
1439
+ });
1440
+ }
1441
+ for (const relation of relations) await this.#rows("MATCH (source:KnowledgeEntityIdentity {id: $from}), (target:KnowledgeEntityIdentity {id: $to}) CREATE (source)-[:KnowledgeRelationVersion {key: $key, relationId: $id, revision: $revision, tombstone: false, payload: $payload}]->(target);", {
1442
+ from: relation.from,
1443
+ to: relation.to,
1444
+ key: `${relation.id}@${revision}`,
1445
+ id: relation.id,
1446
+ revision,
1447
+ payload: encodeRecord(relation)
1448
+ });
1449
+ for (const id of removeRelations) {
1450
+ const relation = before.relations.find((value) => value.id === id);
1451
+ if (relation !== void 0) await this.#rows("MATCH (source:KnowledgeEntityIdentity {id: $from}), (target:KnowledgeEntityIdentity {id: $to}) CREATE (source)-[:KnowledgeRelationVersion {key: $key, relationId: $id, revision: $revision, tombstone: true, payload: $payload}]->(target);", {
1452
+ from: relation.from,
1453
+ to: relation.to,
1454
+ key: `${id}@${revision}`,
1455
+ id,
1456
+ revision,
1457
+ payload: ""
1458
+ });
1459
+ }
1460
+ for (const id of removeEntities) await this.#rows("CREATE (:KnowledgeEntityVersion {key: $key, entityId: $id, revision: $revision, tombstone: true, payload: $payload});", {
1461
+ key: `${id}@${revision}`,
1462
+ id,
1463
+ revision,
1464
+ payload: ""
1465
+ });
1466
+ }
1467
+ #validateArchive(archive) {
1468
+ if (archive.formatVersion !== 1 || archive.projectId !== this.#projectId) throw new Error("Ladybug Design Knowledge archive identity is invalid");
1469
+ const finalRevision = parseRevision(archive.revision);
1470
+ if (archive.entities.length + archive.relations.length + archive.proposals.length + archive.decisions.length > this.#config.maxArchiveRecords) throw new RangeError(`Ladybug Design Knowledge archive exceeds ${this.#config.maxArchiveRecords} records`);
1471
+ uniqueIds(archive.entities, "archive entity");
1472
+ uniqueIds(archive.relations, "archive relation");
1473
+ const entityIds = new Set(archive.entities.map((value) => value.id));
1474
+ for (const entity of archive.entities) decodeEntity(canonicalJson(entity), entity.id);
1475
+ for (const relation of archive.relations) {
1476
+ decodeRelation(canonicalJson(relation), relation.id);
1477
+ if (!entityIds.has(relation.from) || !entityIds.has(relation.to)) throw new Error(`Ladybug Design Knowledge archive relation ${JSON.stringify(relation.id)} has an absent endpoint`);
1478
+ }
1479
+ const proposals = /* @__PURE__ */ new Map();
1480
+ const ownedEntityIds = /* @__PURE__ */ new Set();
1481
+ const ownedRelationIds = /* @__PURE__ */ new Set();
1482
+ for (const item of archive.proposals) {
1483
+ if (item.proposal.projectId !== archive.projectId || item.proposalDigest !== designKnowledgeProposalDigest(item.proposal)) throw new Error("Ladybug Design Knowledge archive proposal digest or project is invalid");
1484
+ const sourceRevision = parseRevision(item.proposalRevision);
1485
+ if (sourceRevision < 1 || sourceRevision > finalRevision) throw new Error("Ladybug Design Knowledge archive proposal revision is invalid");
1486
+ decodeProposal(canonicalJson(item.proposal), item.proposal.id, item.proposalDigest);
1487
+ for (const entity of item.proposal.entities) {
1488
+ if (ownedEntityIds.has(entity.id)) throw new Error("Ladybug Design Knowledge archive proposal entity ownership overlaps");
1489
+ ownedEntityIds.add(entity.id);
1490
+ }
1491
+ for (const relation of item.proposal.relations) {
1492
+ if (ownedRelationIds.has(relation.id)) throw new Error("Ladybug Design Knowledge archive proposal relation ownership overlaps");
1493
+ ownedRelationIds.add(relation.id);
1494
+ }
1495
+ const ids = /* @__PURE__ */ new Set([...entityIds, ...item.proposal.entities.map((value) => value.id)]);
1496
+ for (const relation of item.proposal.relations) if (!ids.has(relation.from) || !ids.has(relation.to)) throw new Error("Ladybug Design Knowledge archive proposal relation has an absent endpoint");
1497
+ if (proposals.has(item.proposal.id)) throw new Error("Ladybug Design Knowledge archive proposal id is duplicated");
1498
+ proposals.set(item.proposal.id, item);
1499
+ }
1500
+ const journalKeys = /* @__PURE__ */ new Set();
1501
+ const terminal = /* @__PURE__ */ new Set();
1502
+ for (const decision of archive.decisions) {
1503
+ const item = proposals.get(decision.proposalId);
1504
+ if (decision.projectId !== archive.projectId || item === void 0 || decision.proposalRevision !== item.proposalRevision || decision.proposalDigest !== item.proposalDigest) throw new Error("Ladybug Design Knowledge archive decision does not bind an archived proposal");
1505
+ const key = canonicalJson([decision.journal, decision.journalRevision]);
1506
+ if (journalKeys.has(key)) throw new Error("Ladybug Design Knowledge archive journal decision is duplicated");
1507
+ journalKeys.add(key);
1508
+ if (terminal.has(decision.proposalId)) throw new Error("Ladybug Design Knowledge archive has a decision after a terminal decision");
1509
+ if (decision.decision !== "deferred") {
1510
+ terminal.add(decision.proposalId);
1511
+ for (const entity of item.proposal.entities) if (archive.entities.find((value) => value.id === entity.id)?.lifecycle !== decision.decision) throw new Error("Ladybug Design Knowledge archive terminal entity lifecycle is inconsistent");
1512
+ for (const relation of item.proposal.relations) if (archive.relations.find((value) => value.id === relation.id)?.lifecycle !== decision.decision) throw new Error("Ladybug Design Knowledge archive terminal relation lifecycle is inconsistent");
1513
+ }
1514
+ }
1515
+ for (const entity of archive.entities) if (AUTHORITATIVE.has(entity.lifecycle)) {
1516
+ const owner = archive.proposals.find((value) => value.proposal.entities.some((candidate) => candidate.id === entity.id));
1517
+ if (owner === void 0 || !archive.decisions.some((value) => value.proposalId === owner.proposal.id && value.decision === entity.lifecycle)) throw new Error("Ladybug Design Knowledge archive authoritative entity lacks its journal decision");
1518
+ }
1519
+ for (const relation of archive.relations) if (AUTHORITATIVE.has(relation.lifecycle)) {
1520
+ const owner = archive.proposals.find((value) => value.proposal.relations.some((candidate) => candidate.id === relation.id));
1521
+ if (owner === void 0 || !archive.decisions.some((value) => value.proposalId === owner.proposal.id && value.decision === relation.lifecycle)) throw new Error("Ladybug Design Knowledge archive authoritative relation lacks its journal decision");
1522
+ }
1523
+ return archive;
1524
+ }
1525
+ #validateQuery(request) {
1526
+ if (request.direction !== void 0 && (![
1527
+ "incoming",
1528
+ "outgoing",
1529
+ "both"
1530
+ ].includes(request.direction) || request.incidentTo === void 0)) throw new TypeError("Knowledge direction requires incidentTo and must be incoming, outgoing or both");
1531
+ if (!Number.isSafeInteger(request.limit) || request.limit < 1 || request.limit > this.#config.maxQueryLimit) throw new RangeError(`Ladybug Design Knowledge query limit must be between 1 and ${this.#config.maxQueryLimit}`);
1532
+ if (request.text !== void 0 && Buffer.byteLength(request.text, "utf8") > this.#config.maxTextBytes) throw new RangeError(`Ladybug Design Knowledge query text exceeds ${this.#config.maxTextBytes} bytes`);
1533
+ for (const [name, values] of [
1534
+ ["entityIds", request.entityIds],
1535
+ ["relationIds", request.relationIds],
1536
+ ["incidentTo", request.incidentTo],
1537
+ ["sourceFiles", request.sourceFiles],
1538
+ ["entityTypes", request.entityTypes],
1539
+ ["ontologyTypes", request.ontologyTypes],
1540
+ ["recordKinds", request.recordKinds],
1541
+ ["identityQualities", request.identityQualities],
1542
+ ["relationTypes", request.relationTypes],
1543
+ ["relationMeanings", request.relationMeanings],
1544
+ ["relationOrigins", request.relationOrigins],
1545
+ ["lifecycles", request.lifecycles]
1546
+ ]) {
1547
+ if (values !== void 0 && (!Array.isArray(values) || values.some((value) => typeof value !== "string"))) throw new TypeError(`Ladybug Design Knowledge query ${name} must be a string array`);
1548
+ if (values !== void 0 && values.length > this.#config.maxFilterValues) throw new RangeError(`Ladybug Design Knowledge query ${name} exceeds ${this.#config.maxFilterValues} values`);
1549
+ }
1550
+ }
1551
+ async #ensureEntityIdentity(id) {
1552
+ if ((await this.#rows("MATCH (e:KnowledgeEntityIdentity {id: $id}) RETURN e.id AS id;", { id })).length === 0) await this.#rows("CREATE (:KnowledgeEntityIdentity {id: $id});", { id });
1553
+ }
1554
+ async #serialized(operation) {
1555
+ const previous = this.#operationTail;
1556
+ let release;
1557
+ this.#operationTail = new Promise((resolve) => {
1558
+ release = resolve;
1559
+ });
1560
+ await previous;
1561
+ try {
1562
+ return await operation();
1563
+ } finally {
1564
+ release();
1565
+ }
1566
+ }
1567
+ async #fixed(statement) {
1568
+ const result = await this.#connection.query(statement);
1569
+ try {
1570
+ if (!result.isSuccess()) throw new Error(await result.getErrorMessage());
1571
+ } finally {
1572
+ await result.close();
1573
+ }
1574
+ }
1575
+ async #rows(statement, parameters) {
1576
+ const prepared = await this.#connection.prepare(statement);
1577
+ try {
1578
+ if (!prepared.isSuccess()) throw new Error(await prepared.getErrorMessage());
1579
+ const result = await this.#connection.execute(prepared, parameters);
1580
+ try {
1581
+ if (!result.isSuccess()) throw new Error(await result.getErrorMessage());
1582
+ return await result.getAllObjects();
1583
+ } finally {
1584
+ await result.close();
1585
+ }
1586
+ } finally {
1587
+ await prepared.close();
1588
+ }
1589
+ }
1590
+ #assertProject(projectId) {
1591
+ if (projectId !== this.#projectId) throw new Error(`Ladybug Design Knowledge provider owns ${JSON.stringify(this.#projectId)}, not ${JSON.stringify(projectId)}`);
1592
+ if (this.#closing) throw new Error("Ladybug Design Knowledge provider is closing or closed");
1593
+ }
1594
+ };
1595
+ //#endregion
1596
+ //#region src/dsh-plugin.ts
1597
+ const name = "dsh-design-knowledge-ladybug";
1598
+ const inject = ["design"];
1599
+ /** Stable provider id persisted in Design Project metadata by this composition. */
1600
+ const LADYBUG_WASM_PROVIDER_ID = DesignKnowledgeProviderId("ladybug-wasm");
1601
+ function projectConfig(location, config) {
1602
+ if (location.provider !== LADYBUG_WASM_PROVIDER_ID) throw new Error(`Ladybug Design Knowledge host cannot open provider ${JSON.stringify(location.provider)}`);
1603
+ return {
1604
+ ...config,
1605
+ databaseRoot: resolve(location.stateRoot, "design-knowledge")
1606
+ };
1607
+ }
1608
+ function aggregate(primary, secondary, message) {
1609
+ if (primary === void 0) return secondary;
1610
+ if (secondary === void 0) return primary;
1611
+ return new AggregateError([primary, secondary], message);
1612
+ }
1613
+ /** Install the default embedded provider without exposing its implementation to consumers. */
1614
+ function apply(ctx, config = {}) {
1615
+ const { embedding, ...databaseConfig } = config;
1616
+ const semantic = new DesignSemanticIndex(embedding);
1617
+ const providers = /* @__PURE__ */ new Map();
1618
+ const recoveryStates = /* @__PURE__ */ new Map();
1619
+ const retries = /* @__PURE__ */ new Set();
1620
+ const design = ctx.design;
1621
+ const { databaseRoot: _validationRoot, ...providerConfig } = resolveLadybugKnowledgeConfig({
1622
+ ...databaseConfig,
1623
+ databaseRoot: resolve(".")
1624
+ });
1625
+ const manager = new DesignKnowledgeManager({ open: async (location) => {
1626
+ const provider = await LadybugDesignKnowledgeProvider.open(location.projectId, projectConfig(location, providerConfig), {
1627
+ retryFailed: retries.delete(location.projectId),
1628
+ rebuild: (candidate) => design.rebuildDerivedKnowledge(location.projectId, candidate),
1629
+ onState: (state) => {
1630
+ recoveryStates.set(location.projectId, state);
1631
+ }
1632
+ });
1633
+ providers.set(location.projectId, provider);
1634
+ return Object.freeze({
1635
+ projectId: location.projectId,
1636
+ service: provider,
1637
+ close: async () => {
1638
+ await design.releaseKnowledgeIndex(location.projectId);
1639
+ semantic.cancelProject(location.projectId);
1640
+ semantic.cancelProject(location.projectId + ":kag");
1641
+ providers.delete(location.projectId);
1642
+ await provider.close();
1643
+ }
1644
+ });
1645
+ } });
1646
+ const unregister = design.registerDesignKnowledgeRuntime({
1647
+ recoveryStatus: (projectId) => recoveryStates.get(projectId),
1648
+ retryRecovery: (projectId) => {
1649
+ retries.add(projectId);
1650
+ },
1651
+ defaultProvider: LADYBUG_WASM_PROVIDER_ID,
1652
+ administration: manager,
1653
+ service: manager.service,
1654
+ semantic: {
1655
+ identity: semantic.publicationIdentity,
1656
+ publicationCache: (projectId) => providers.get(projectId),
1657
+ read: async (projectId, graph) => {
1658
+ const provider = providers.get(projectId);
1659
+ return semantic.read(projectId, graph, provider);
1660
+ },
1661
+ ...semantic.config.provider === "disabled" ? {} : { compute: async (projectId, graph, signal) => semantic.compute(projectId + ":kag", graph, providers.get(projectId), signal) }
1662
+ }
1663
+ });
1664
+ ctx.effect(() => async () => {
1665
+ let failure;
1666
+ await semantic.close();
1667
+ try {
1668
+ await unregister();
1669
+ } catch (error) {
1670
+ failure = error;
1671
+ }
1672
+ try {
1673
+ await manager.shutdown();
1674
+ } catch (error) {
1675
+ failure = aggregate(failure, error, "Ladybug Design Knowledge host disposal failed");
1676
+ }
1677
+ if (failure !== void 0) throw failure;
1678
+ });
1679
+ }
1680
+ //#endregion
1681
+ export { LADYBUG_KNOWLEDGE_SCHEMA_VERSION as a, LadybugSchemaMismatchError as c, shutdownLadybugKnowledgeRuntime as d, resolveLadybugKnowledgeConfig as f, name as i, LadybugWriterBusyError as l, apply as n, LadybugDesignKnowledgeProvider as o, inject as r, LadybugProjectLockError as s, LADYBUG_WASM_PROVIDER_ID as t, isRecoverableLadybugError as u };