@opum-ai/lore 0.1.0 → 0.2.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 (90) hide show
  1. package/README.md +33 -22
  2. package/bin/lore.cjs +35 -7
  3. package/package.json +17 -17
  4. package/src/adapters/backlog.ts +0 -1084
  5. package/src/adapters/git.ts +0 -221
  6. package/src/cli.ts +0 -667
  7. package/src/commands/agent.ts +0 -301
  8. package/src/commands/agents.ts +0 -302
  9. package/src/commands/args.ts +0 -209
  10. package/src/commands/changed.ts +0 -70
  11. package/src/commands/check.ts +0 -1031
  12. package/src/commands/codex-bridge.ts +0 -49
  13. package/src/commands/concurrency.ts +0 -48
  14. package/src/commands/context.ts +0 -292
  15. package/src/commands/discover.ts +0 -89
  16. package/src/commands/explorer.ts +0 -253
  17. package/src/commands/export.ts +0 -93
  18. package/src/commands/fswrite.ts +0 -928
  19. package/src/commands/graph.ts +0 -291
  20. package/src/commands/help.ts +0 -151
  21. package/src/commands/impact.ts +0 -59
  22. package/src/commands/init.ts +0 -583
  23. package/src/commands/instructions.ts +0 -91
  24. package/src/commands/link.ts +0 -929
  25. package/src/commands/new.ts +0 -476
  26. package/src/commands/orphans.ts +0 -457
  27. package/src/commands/path.ts +0 -67
  28. package/src/commands/provenance.ts +0 -68
  29. package/src/commands/query.ts +0 -312
  30. package/src/commands/reconcile-shared.ts +0 -280
  31. package/src/commands/rename.ts +0 -585
  32. package/src/commands/replace.ts +0 -320
  33. package/src/commands/scaffold.ts +0 -346
  34. package/src/commands/schema.ts +0 -293
  35. package/src/commands/snapshot.ts +0 -130
  36. package/src/commands/supersede.ts +0 -400
  37. package/src/commands/sync.ts +0 -371
  38. package/src/commands/tasks.ts +0 -271
  39. package/src/commands/traversal.ts +0 -151
  40. package/src/commands/validate.ts +0 -226
  41. package/src/config.ts +0 -598
  42. package/src/core/agent-bridge.ts +0 -287
  43. package/src/core/agent-context.ts +0 -498
  44. package/src/core/agent-profile.ts +0 -447
  45. package/src/core/bundle.ts +0 -893
  46. package/src/core/check.ts +0 -853
  47. package/src/core/codex-bridge.ts +0 -100
  48. package/src/core/concept.ts +0 -597
  49. package/src/core/consumer-scaffold.ts +0 -433
  50. package/src/core/context.ts +0 -271
  51. package/src/core/explorer-contract.ts +0 -441
  52. package/src/core/explorer-qualification.ts +0 -58
  53. package/src/core/explorer.ts +0 -518
  54. package/src/core/finding.ts +0 -31
  55. package/src/core/graph.ts +0 -201
  56. package/src/core/indexes.ts +0 -436
  57. package/src/core/instructions.ts +0 -209
  58. package/src/core/ladybug-driver.ts +0 -1795
  59. package/src/core/ladybug-lifecycle.ts +0 -1178
  60. package/src/core/ladybug-native.ts +0 -95
  61. package/src/core/ladybug-source.ts +0 -667
  62. package/src/core/links.ts +0 -681
  63. package/src/core/log.ts +0 -253
  64. package/src/core/managed-block.ts +0 -540
  65. package/src/core/manifest.ts +0 -718
  66. package/src/core/order.ts +0 -13
  67. package/src/core/profile.ts +0 -1007
  68. package/src/core/projection.ts +0 -195
  69. package/src/core/query.ts +0 -542
  70. package/src/core/reconcile.ts +0 -236
  71. package/src/core/replace.ts +0 -419
  72. package/src/core/retrieval.ts +0 -213
  73. package/src/core/rewrite.ts +0 -940
  74. package/src/core/scaffold.ts +0 -255
  75. package/src/core/schema.ts +0 -366
  76. package/src/core/snapshot-runtime.ts +0 -52
  77. package/src/core/snapshot-store.ts +0 -287
  78. package/src/core/snapshot.ts +0 -711
  79. package/src/core/template.ts +0 -429
  80. package/src/core/traversal.ts +0 -487
  81. package/src/core/validate.ts +0 -517
  82. package/src/core/workspace-contract.ts +0 -473
  83. package/src/core/workspace-projection.ts +0 -365
  84. package/src/core/workspace-retrieval.ts +0 -196
  85. package/src/core/workspace-source.ts +0 -174
  86. package/src/errors.ts +0 -697
  87. package/src/meta.ts +0 -7
  88. package/src/output.ts +0 -589
  89. package/src/scripts/upstream-backlog-watch.ts +0 -288
  90. package/src/state.ts +0 -390
@@ -1,1178 +0,0 @@
1
- /**
2
- * Repository-local, content-addressed lifecycle for ladybug-projection/1.
3
- *
4
- * A writer owns one explicit lock, builds only in an isolated staging
5
- * directory, closes/reopens the native database for verification, writes the
6
- * control manifest last, and publishes with one atomic directory rename.
7
- */
8
-
9
- import { createHash, randomBytes } from "node:crypto";
10
- import {
11
- chmodSync,
12
- closeSync,
13
- createReadStream,
14
- existsSync,
15
- fsyncSync,
16
- lstatSync,
17
- mkdirSync,
18
- openSync,
19
- readdirSync,
20
- readFileSync,
21
- realpathSync,
22
- renameSync,
23
- rmSync,
24
- statSync,
25
- unlinkSync,
26
- writeFileSync,
27
- } from "node:fs";
28
- import { hostname } from "node:os";
29
- import { isAbsolute, join, relative, resolve } from "node:path";
30
- import type { BacklogAdapter } from "../adapters/backlog";
31
- import { LoreError, type WarningCollector } from "../errors";
32
- import {
33
- EXPECTED_LADYBUG_STORAGE_VERSION,
34
- EXPECTED_LADYBUG_VERSION,
35
- type LadybugDatabaseVerification,
36
- type LadybugNativeDriver,
37
- type LadybugNativeLoader,
38
- loadLadybugNativeDriver,
39
- } from "./ladybug-native";
40
- import {
41
- canonicalJson,
42
- digest,
43
- LADYBUG_CACHE_REL_ROOT,
44
- LADYBUG_CONTROL_FILENAME,
45
- LADYBUG_DATABASE_FILENAME,
46
- LADYBUG_INDEX_FORMAT,
47
- type LadybugProjectionFreshness,
48
- type LadybugProjectionSource,
49
- loadLadybugProjectionSource,
50
- } from "./ladybug-source";
51
-
52
- const CURRENT_PROCESS_START_IDENTITY = `pid:${process.pid}:started:${Math.floor(
53
- Date.now() - process.uptime() * 1_000,
54
- )}`;
55
-
56
- export type LadybugProjectionClassification = "locked" | "unsupported" | "corrupt" | "rebuildable" | "reusable";
57
-
58
- export type LadybugProjectionOutcome = "reused" | "built" | "unavailable";
59
-
60
- export interface LadybugControlManifest {
61
- readonly indexFormatVersion: string;
62
- readonly projectionSchemaVersion: string;
63
- readonly normalizationVersion: string;
64
- readonly loreVersion: string;
65
- readonly exporterName: string;
66
- readonly exporterVersion: string;
67
- readonly ladybugVersion: string;
68
- readonly ladybugStorageVersion: string;
69
- readonly repositoryScopeKey: string;
70
- readonly snapshotKey: string;
71
- readonly bundleId: string;
72
- readonly okfVersion: string;
73
- readonly docsRoot: string;
74
- readonly gitCommit: string | null;
75
- readonly exportDigest: string;
76
- readonly taskSnapshotDigest: string;
77
- readonly sourceFingerprint: string;
78
- readonly inputFingerprint: string;
79
- readonly warnings: readonly string[];
80
- readonly warningsComplete: boolean;
81
- readonly sourceRecordsDigest: string;
82
- readonly recordKeysDigest: string;
83
- readonly recordCount: number;
84
- readonly counts: {
85
- readonly concepts: number;
86
- readonly tasks: number;
87
- readonly authoredEdges: number;
88
- };
89
- readonly database: {
90
- readonly file: typeof LADYBUG_DATABASE_FILENAME;
91
- readonly byteLength: number;
92
- readonly digest: string;
93
- };
94
- readonly completed: true;
95
- }
96
-
97
- export interface LadybugProjectionGeneration {
98
- readonly root: string;
99
- readonly generationPath: string;
100
- readonly databasePath: string;
101
- readonly controlPath: string;
102
- readonly control: LadybugControlManifest;
103
- readonly verification: LadybugDatabaseVerification;
104
- }
105
-
106
- export interface LadybugProjectionLifecycleResult {
107
- /** The ordered state observed before any permitted recovery/build. */
108
- readonly classification: LadybugProjectionClassification;
109
- readonly outcome: LadybugProjectionOutcome;
110
- readonly source: LadybugProjectionSource;
111
- readonly generation?: LadybugProjectionGeneration;
112
- readonly reason?: string;
113
- }
114
-
115
- export interface LadybugLifecycleHooks {
116
- /** Test seam for a crash after the database is closed but before index.json. */
117
- readonly afterDatabaseClose?: (stagingPath: string) => void | Promise<void>;
118
- /** Test seam for a crash after index.json but before publication. */
119
- readonly afterControlManifest?: (stagingPath: string) => void | Promise<void>;
120
- /** Test seam for a crash after atomic publication is complete and immutable. */
121
- readonly afterPublication?: (generationPath: string) => void | Promise<void>;
122
- /** Test seam for a crash after reconciliation completes but before writer ownership is released. */
123
- readonly beforeLockRelease?: () => void | Promise<void>;
124
- }
125
-
126
- export interface ReconcileLadybugProjectionOptions {
127
- readonly root: string;
128
- /** Repository-relative cache root; defaults to the frozen single-repository M6 path. */
129
- readonly cacheRelRoot?: string;
130
- /** Remove every non-current immutable generation after successful reconciliation. */
131
- readonly retainOnlyCurrent?: boolean;
132
- readonly adapter?: BacklogAdapter;
133
- readonly resolveGitCommit?: (root: string) => string | null;
134
- readonly warnings?: WarningCollector;
135
- readonly loadSource?: () => Promise<LadybugProjectionSource>;
136
- /** Cheap exact-input probe used before the full Markdown/export source loader. */
137
- readonly loadFreshness?: () => Promise<LadybugProjectionFreshness>;
138
- /** Injectable lazy native boundary; never invoked before supported control-manifest preflight. */
139
- readonly loadNativeDriver?: LadybugNativeLoader;
140
- readonly hooks?: LadybugLifecycleHooks;
141
- }
142
-
143
- interface WriterLockRecord {
144
- readonly ownerToken: string;
145
- readonly pid: number;
146
- readonly processStartIdentity: string | null;
147
- readonly hostname: string;
148
- readonly acquiredAt: string;
149
- }
150
-
151
- interface OwnedWriterLock {
152
- readonly path: string;
153
- readonly record: WriterLockRecord;
154
- }
155
-
156
- interface GenerationInspection {
157
- readonly classification: LadybugProjectionClassification;
158
- readonly generation?: LadybugProjectionGeneration;
159
- readonly reason?: string;
160
- }
161
-
162
- class NativeDriverLoadFailure {
163
- constructor(readonly cause: unknown) {}
164
- }
165
-
166
- /**
167
- * Reuse or safely construct the exact generation for the current repository
168
- * source. Unsupported and actively locked states are non-destructive and return
169
- * `unavailable`; callers retain the existing in-memory fallback.
170
- */
171
- export async function reconcileLadybugProjection(
172
- options: ReconcileLadybugProjectionOptions,
173
- ): Promise<LadybugProjectionLifecycleResult> {
174
- const root = canonicalRepositoryRoot(options.root);
175
- const cacheRelRoot = validateCacheRelRoot(options.cacheRelRoot ?? LADYBUG_CACHE_REL_ROOT);
176
- const loadNative = onceNativeDriver(options.loadNativeDriver ?? loadLadybugNativeDriver);
177
- const loadSource =
178
- options.loadSource ??
179
- (() =>
180
- loadLadybugProjectionSource({
181
- root,
182
- ladybugVersion: EXPECTED_LADYBUG_VERSION,
183
- ladybugStorageVersion: EXPECTED_LADYBUG_STORAGE_VERSION,
184
- adapter: options.adapter,
185
- resolveGitCommit: options.resolveGitCommit,
186
- warnings: options.warnings,
187
- }));
188
- const cacheRoot = join(root, cacheRelRoot);
189
- assertExistingCacheLayoutSafe(root, cacheRoot, cacheRelRoot);
190
- const lockState = inspectWriterLock(cacheRoot);
191
- if (options.loadFreshness !== undefined && hasPotentialGeneration(cacheRoot)) {
192
- const freshness = await options.loadFreshness();
193
- const fast = await inspectFreshGeneration(cacheRoot, freshness.inputFingerprint, loadNative);
194
- if (fast.classification === "reusable" && fast.generation !== undefined) {
195
- const source = sourceFromControl(fast.generation.control);
196
- return {
197
- classification: lockState === "active" ? "locked" : "reusable",
198
- outcome: "reused",
199
- source,
200
- generation: fast.generation,
201
- ...(lockState === "active"
202
- ? { reason: "another live writer owns writer.lock; the exact verified generation remains reusable" }
203
- : {}),
204
- };
205
- }
206
- }
207
-
208
- let source = await loadSource();
209
- assertNativeVersion(source);
210
- if (lockState === "active") {
211
- const inspection = await inspectGeneration(cacheRoot, source, loadNative);
212
- if (inspection.classification === "reusable" && inspection.generation !== undefined) {
213
- return {
214
- classification: "locked",
215
- outcome: "reused",
216
- source,
217
- generation: inspection.generation,
218
- reason: "another live writer owns writer.lock; the exact verified generation remains reusable",
219
- };
220
- }
221
- return {
222
- classification: "locked",
223
- outcome: "unavailable",
224
- source,
225
- reason: "another live writer owns writer.lock or stale ownership cannot be proved safely",
226
- };
227
- }
228
-
229
- let inspection = await inspectGeneration(cacheRoot, source, loadNative);
230
- const initialClassification = inspection.classification;
231
- if (lockState === "none" && inspection.classification === "reusable" && inspection.generation !== undefined) {
232
- if (options.retainOnlyCurrent === true) {
233
- ensureCacheLayout(root, cacheRoot, cacheRelRoot);
234
- const cleanupLock = acquireWriterLock(cacheRoot);
235
- if (cleanupLock === null) {
236
- return {
237
- classification: "locked",
238
- outcome: "unavailable",
239
- source,
240
- reason: "current-only cleanup could not acquire writer ownership",
241
- };
242
- }
243
- try {
244
- pruneOtherGenerations(cacheRoot, source.generationKey);
245
- } finally {
246
- releaseWriterLock(cleanupLock);
247
- }
248
- }
249
- return { classification: "reusable", outcome: "reused", source, generation: inspection.generation };
250
- }
251
- if (inspection.classification === "unsupported") {
252
- return {
253
- classification: "unsupported",
254
- outcome: "unavailable",
255
- source,
256
- reason: inspection.reason,
257
- };
258
- }
259
- if (inspection.classification === "locked") {
260
- return {
261
- classification: "locked",
262
- outcome: "unavailable",
263
- source,
264
- reason: inspection.reason,
265
- };
266
- }
267
-
268
- ensureCacheLayout(root, cacheRoot, cacheRelRoot);
269
- const ownedLock = acquireWriterLock(cacheRoot);
270
- if (ownedLock === null) {
271
- inspection = await inspectGeneration(cacheRoot, source, loadNative);
272
- if (inspection.classification === "reusable" && inspection.generation !== undefined) {
273
- return {
274
- classification: "locked",
275
- outcome: "reused",
276
- source,
277
- generation: inspection.generation,
278
- reason: "another writer published the exact generation while ownership was contested",
279
- };
280
- }
281
- return {
282
- classification: "locked",
283
- outcome: "unavailable",
284
- source,
285
- reason: "writer ownership could not be acquired safely",
286
- };
287
- }
288
-
289
- try {
290
- cleanupAbandonedStaging(cacheRoot);
291
- for (let attempt = 0; attempt < 2; attempt++) {
292
- assertNativeVersion(source);
293
- inspection = await inspectGeneration(cacheRoot, source, loadNative);
294
- if (inspection.classification === "reusable" && inspection.generation !== undefined) {
295
- if (options.retainOnlyCurrent === true) pruneOtherGenerations(cacheRoot, source.generationKey);
296
- return {
297
- classification: initialClassification,
298
- outcome: "reused",
299
- source,
300
- generation: inspection.generation,
301
- };
302
- }
303
- if (inspection.classification === "unsupported") {
304
- return {
305
- classification: "unsupported",
306
- outcome: "unavailable",
307
- source,
308
- reason: inspection.reason,
309
- };
310
- }
311
- if (inspection.classification === "locked") {
312
- return {
313
- classification: "locked",
314
- outcome: "unavailable",
315
- source,
316
- reason: inspection.reason,
317
- };
318
- }
319
- if (inspection.classification === "corrupt") {
320
- quarantineGeneration(cacheRoot, source, ownedLock.record.ownerToken);
321
- } else if (inspection.classification === "rebuildable") {
322
- retireRebuildableGeneration(cacheRoot, source, ownedLock.record.ownerToken);
323
- }
324
-
325
- const stagingPath = join(cacheRoot, `.building-${ownedLock.record.ownerToken}-${attempt}`);
326
- assertContained(cacheRoot, stagingPath);
327
- mkdirSync(stagingPath, { mode: 0o700 });
328
- const databasePath = join(stagingPath, LADYBUG_DATABASE_FILENAME);
329
- const native = await loadNative();
330
- Bun.gc(true);
331
- await native.buildLadybugDatabase(databasePath, source);
332
- await options.hooks?.afterDatabaseClose?.(stagingPath);
333
- const verification = await native.verifyLadybugDatabase(databasePath, source);
334
- const database = await hashFile(databasePath);
335
- const control = controlManifest(source, database.byteLength, database.digest);
336
- const controlPath = join(stagingPath, LADYBUG_CONTROL_FILENAME);
337
- writeControlManifestLast(controlPath, control);
338
- await options.hooks?.afterControlManifest?.(stagingPath);
339
-
340
- const finalFreshness = await options.loadFreshness?.();
341
- const finalSource =
342
- finalFreshness === undefined || finalFreshness.inputFingerprint !== source.inputFingerprint
343
- ? await loadSource()
344
- : source;
345
- assertNativeVersion(finalSource);
346
- if (finalSource.sourceFingerprint !== source.sourceFingerprint) {
347
- removeContained(cacheRoot, stagingPath);
348
- source = finalSource;
349
- continue;
350
- }
351
-
352
- const generationPath = generationPathFor(cacheRoot, source);
353
- assertContained(cacheRoot, generationPath);
354
- if (existsSync(generationPath)) {
355
- removeContained(cacheRoot, stagingPath);
356
- const existing = await inspectGeneration(cacheRoot, source, loadNative);
357
- if (existing.classification === "reusable" && existing.generation !== undefined) {
358
- if (options.retainOnlyCurrent === true) pruneOtherGenerations(cacheRoot, source.generationKey);
359
- return {
360
- classification: initialClassification,
361
- outcome: "reused",
362
- source,
363
- generation: existing.generation,
364
- };
365
- }
366
- throw lifecycleError("the content-addressed generation appeared but did not verify");
367
- }
368
- chmodSync(databasePath, 0o444);
369
- renameSync(stagingPath, generationPath);
370
- chmodSync(generationPath, 0o555);
371
- await options.hooks?.afterPublication?.(generationPath);
372
- fsyncDirectory(join(cacheRoot, "generations"));
373
- if (options.retainOnlyCurrent === true) pruneOtherGenerations(cacheRoot, source.generationKey);
374
- return {
375
- classification: initialClassification,
376
- outcome: "built",
377
- source,
378
- generation: {
379
- root: cacheRoot,
380
- generationPath,
381
- databasePath: join(generationPath, LADYBUG_DATABASE_FILENAME),
382
- controlPath: join(generationPath, LADYBUG_CONTROL_FILENAME),
383
- control,
384
- verification,
385
- },
386
- };
387
- }
388
- return {
389
- classification: "rebuildable",
390
- outcome: "unavailable",
391
- source,
392
- reason: "repository inputs changed during both isolated build attempts",
393
- };
394
- } finally {
395
- try {
396
- await options.hooks?.beforeLockRelease?.();
397
- } finally {
398
- releaseWriterLock(ownedLock);
399
- }
400
- }
401
- }
402
-
403
- /**
404
- * Explicitly dispose every derived Ladybug artifact while holding the same
405
- * writer ownership boundary. Repository sources are never removed.
406
- */
407
- export function disposeLadybugProjection(rootInput: string): boolean {
408
- const root = canonicalRepositoryRoot(rootInput);
409
- const cacheRoot = join(root, LADYBUG_CACHE_REL_ROOT);
410
- if (!existsSync(cacheRoot)) return false;
411
- ensureCacheLayout(root, cacheRoot, LADYBUG_CACHE_REL_ROOT);
412
- const lock = acquireWriterLock(cacheRoot);
413
- if (lock === null) {
414
- throw new LoreError(
415
- "conflict",
416
- "cannot dispose the Ladybug projection while another writer owns writer.lock",
417
- "retry after the active writer exits",
418
- );
419
- }
420
- try {
421
- for (const entry of readdirSync(cacheRoot, { withFileTypes: true })) {
422
- if (entry.name === "writer.lock") continue;
423
- const target = join(cacheRoot, entry.name);
424
- assertContained(cacheRoot, target);
425
- removeContained(cacheRoot, target);
426
- }
427
- return true;
428
- } finally {
429
- releaseWriterLock(lock);
430
- }
431
- }
432
-
433
- function hasPotentialGeneration(cacheRoot: string): boolean {
434
- const generationsRoot = join(cacheRoot, "generations");
435
- if (!existsSync(generationsRoot)) return false;
436
- return readdirSync(generationsRoot, { withFileTypes: true }).some(
437
- (entry) => entry.isDirectory() && !entry.isSymbolicLink() && /^[0-9a-f]{64}$/.test(entry.name),
438
- );
439
- }
440
-
441
- async function inspectFreshGeneration(
442
- cacheRoot: string,
443
- inputFingerprint: string,
444
- loadNative: LadybugNativeLoader,
445
- ): Promise<GenerationInspection> {
446
- const generationsRoot = join(cacheRoot, "generations");
447
- if (!existsSync(generationsRoot)) return { classification: "rebuildable", reason: "no generation exists" };
448
- for (const entry of readdirSync(generationsRoot, { withFileTypes: true }).sort((a, b) =>
449
- a.name.localeCompare(b.name),
450
- )) {
451
- if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{64}$/.test(entry.name)) continue;
452
- const generationPath = join(generationsRoot, entry.name);
453
- const controlPath = join(generationPath, LADYBUG_CONTROL_FILENAME);
454
- let parsed: unknown;
455
- try {
456
- parsed = JSON.parse(readFileSync(controlPath, "utf8"));
457
- } catch {
458
- continue;
459
- }
460
- if (!isObject(parsed) || parsed.inputFingerprint !== inputFingerprint || parsed.warningsComplete !== true) continue;
461
- if (!isControlManifest(parsed)) return { classification: "corrupt", reason: "control manifest is incomplete" };
462
- const control = parsed;
463
- if (
464
- control.indexFormatVersion !== LADYBUG_INDEX_FORMAT ||
465
- control.projectionSchemaVersion !== "1.0" ||
466
- control.ladybugVersion !== EXPECTED_LADYBUG_VERSION ||
467
- control.ladybugStorageVersion !== EXPECTED_LADYBUG_STORAGE_VERSION
468
- ) {
469
- return { classification: "rebuildable", reason: "generation compatibility changed" };
470
- }
471
- if (entry.name !== control.sourceFingerprint.replace(/^sha256:/, "")) {
472
- return { classification: "corrupt", reason: "generation path differs from source identity" };
473
- }
474
- const databasePath = join(generationPath, LADYBUG_DATABASE_FILENAME);
475
- try {
476
- const generationStat = lstatSync(generationPath);
477
- const controlStat = lstatSync(controlPath);
478
- const databaseStat = lstatSync(databasePath);
479
- if (
480
- !generationStat.isDirectory() ||
481
- generationStat.isSymbolicLink() ||
482
- !controlStat.isFile() ||
483
- controlStat.isSymbolicLink() ||
484
- !databaseStat.isFile() ||
485
- databaseStat.isSymbolicLink() ||
486
- (generationStat.mode & 0o222) !== 0 ||
487
- (controlStat.mode & 0o222) !== 0 ||
488
- (databaseStat.mode & 0o222) !== 0 ||
489
- databaseStat.size !== control.database.byteLength
490
- ) {
491
- return { classification: "corrupt", reason: "generation files or permissions differ" };
492
- }
493
- if ((await hashFile(databasePath)).digest !== control.database.digest) {
494
- return { classification: "corrupt", reason: "projection database digest differs" };
495
- }
496
- const expected = verificationFromControl(control);
497
- const native = await loadNative();
498
- const verification = await native.verifyLadybugDatabaseMetadata(databasePath, expected);
499
- return {
500
- classification: "reusable",
501
- generation: { root: cacheRoot, generationPath, databasePath, controlPath, control, verification },
502
- };
503
- } catch (cause) {
504
- if (isNativeLockError(cause))
505
- return { classification: "locked", reason: "Ladybug database is unexpectedly locked" };
506
- return { classification: "corrupt", reason: errorMessage(cause) };
507
- }
508
- }
509
- return { classification: "rebuildable", reason: "no generation matches the current input fingerprint" };
510
- }
511
-
512
- function verificationFromControl(control: LadybugControlManifest): LadybugDatabaseVerification {
513
- return {
514
- repositoryScopeKey: control.repositoryScopeKey,
515
- snapshotKey: control.snapshotKey,
516
- sourceFingerprint: control.sourceFingerprint,
517
- exportDigest: control.exportDigest,
518
- taskSnapshotDigest: control.taskSnapshotDigest,
519
- sourceRecordsDigest: control.sourceRecordsDigest,
520
- recordKeysDigest: control.recordKeysDigest,
521
- recordCount: control.recordCount,
522
- conceptCount: control.counts.concepts,
523
- taskCount: control.counts.tasks,
524
- authoredEdgeCount: control.counts.authoredEdges,
525
- };
526
- }
527
-
528
- function sourceFromControl(control: LadybugControlManifest): LadybugProjectionSource {
529
- const manifest: LadybugProjectionSource["manifest"] = {
530
- record: "manifest",
531
- schemaVersion: control.projectionSchemaVersion,
532
- bundle: {
533
- id: control.bundleId,
534
- okfVersion: control.okfVersion,
535
- docsRoot: control.docsRoot,
536
- gitCommit: control.gitCommit,
537
- },
538
- exporter: { name: control.exporterName, version: control.exporterVersion },
539
- generatedAt: null,
540
- normalizationVersion: control.normalizationVersion,
541
- };
542
- const trailer: LadybugProjectionSource["trailer"] = {
543
- record: "trailer",
544
- recordCount: control.recordCount,
545
- streamHash: control.exportDigest,
546
- };
547
- return {
548
- records: [],
549
- manifest,
550
- trailer,
551
- concepts: [],
552
- tasks: [],
553
- authoredEdges: [],
554
- inventory: [],
555
- profileInventory: [],
556
- repositoryScopeKey: control.repositoryScopeKey,
557
- snapshotKey: control.snapshotKey,
558
- commitKey:
559
- control.gitCommit === null
560
- ? null
561
- : digest(`lore-source-commit/1\0${control.repositoryScopeKey}\0${control.gitCommit}`),
562
- exportDigest: control.exportDigest,
563
- taskSnapshotDigest: control.taskSnapshotDigest,
564
- sourceRecordsDigest: control.sourceRecordsDigest,
565
- recordKeysDigest: control.recordKeysDigest,
566
- sourceFingerprint: control.sourceFingerprint,
567
- inputFingerprint: control.inputFingerprint,
568
- warnings: control.warnings,
569
- warningsComplete: control.warningsComplete,
570
- generationKey: control.sourceFingerprint.replace(/^sha256:/, ""),
571
- ladybugVersion: control.ladybugVersion,
572
- ladybugStorageVersion: control.ladybugStorageVersion,
573
- counts: control.counts,
574
- };
575
- }
576
-
577
- async function inspectGeneration(
578
- cacheRoot: string,
579
- source: LadybugProjectionSource,
580
- loadNative: LadybugNativeLoader,
581
- ): Promise<GenerationInspection> {
582
- const generationPath = generationPathFor(cacheRoot, source);
583
- if (!existsSync(generationPath)) {
584
- return { classification: "rebuildable", reason: "the exact content-addressed generation does not exist" };
585
- }
586
- try {
587
- const generationStat = lstatSync(generationPath);
588
- if (!generationStat.isDirectory() || generationStat.isSymbolicLink()) {
589
- return { classification: "corrupt", reason: "generation path is not a real directory" };
590
- }
591
- const controlPath = join(generationPath, LADYBUG_CONTROL_FILENAME);
592
- const databasePath = join(generationPath, LADYBUG_DATABASE_FILENAME);
593
- const controlStat = lstatSync(controlPath);
594
- if (!controlStat.isFile() || controlStat.isSymbolicLink()) {
595
- return { classification: "corrupt", reason: "control manifest is missing or symlinked" };
596
- }
597
- let parsed: unknown;
598
- try {
599
- parsed = JSON.parse(readFileSync(controlPath, "utf8"));
600
- } catch {
601
- return { classification: "corrupt", reason: "control manifest is not parseable JSON" };
602
- }
603
- const compatibility = classifyControlCompatibility(parsed, source);
604
- // Unsupported is ordered before corruption. A newer format may use
605
- // different publication permissions, so preserve it before applying this
606
- // version's immutable-generation checks.
607
- if (compatibility.classification === "unsupported") return compatibility;
608
- if ((generationStat.mode & 0o222) !== 0) {
609
- return { classification: "corrupt", reason: "generation directory is not immutable" };
610
- }
611
- if ((controlStat.mode & 0o222) !== 0) {
612
- return { classification: "corrupt", reason: "control manifest is not immutable" };
613
- }
614
- if (compatibility.classification !== "reusable") return compatibility;
615
- const control = parsed as LadybugControlManifest;
616
- const databaseStat = lstatSync(databasePath);
617
- if (!databaseStat.isFile() || databaseStat.isSymbolicLink()) {
618
- return { classification: "corrupt", reason: "projection database is missing or symlinked" };
619
- }
620
- if ((databaseStat.mode & 0o222) !== 0) {
621
- return { classification: "corrupt", reason: "projection database is not immutable" };
622
- }
623
- if (databaseStat.size !== control.database.byteLength) {
624
- return { classification: "corrupt", reason: "projection database byte length differs" };
625
- }
626
- const actualDigest = (await hashFile(databasePath)).digest;
627
- if (actualDigest !== control.database.digest) {
628
- return { classification: "corrupt", reason: "projection database digest differs" };
629
- }
630
- let native: LadybugNativeDriver;
631
- try {
632
- native = await loadNative();
633
- } catch (cause) {
634
- // Failure to load the addon describes the host/runtime, not the verified
635
- // generation bytes. Let the retrieval resolver fall back without
636
- // quarantining a generation that may remain valid once the runtime is
637
- // repaired.
638
- throw new NativeDriverLoadFailure(cause);
639
- }
640
- try {
641
- const verification = await native.verifyLadybugDatabase(databasePath, source);
642
- return {
643
- classification: "reusable",
644
- generation: { root: cacheRoot, generationPath, databasePath, controlPath, control, verification },
645
- };
646
- } catch (cause) {
647
- if (isNativeLockError(cause)) {
648
- return { classification: "locked", reason: "Ladybug database is unexpectedly locked" };
649
- }
650
- return { classification: "corrupt", reason: errorMessage(cause) };
651
- }
652
- } catch (cause) {
653
- if (cause instanceof NativeDriverLoadFailure) throw cause.cause;
654
- if (isErrno(cause, "ENOENT")) return { classification: "corrupt", reason: "generation is incomplete" };
655
- return { classification: "corrupt", reason: errorMessage(cause) };
656
- }
657
- }
658
-
659
- function classifyControlCompatibility(value: unknown, source: LadybugProjectionSource): GenerationInspection {
660
- if (!isObject(value)) return { classification: "corrupt", reason: "control manifest is not an object" };
661
- const indexFormatVersion = value.indexFormatVersion;
662
- const projectionSchemaVersion = value.projectionSchemaVersion;
663
- const indexMajor = formatMajor(indexFormatVersion, /^ladybug-projection\/(\d+)$/);
664
- const projectionMajor = formatMajor(projectionSchemaVersion, /^(\d+)(?:\.\d+)?$/);
665
- if (
666
- (indexMajor !== null && indexMajor > 1) ||
667
- (projectionMajor !== null && projectionMajor > 1) ||
668
- (typeof indexFormatVersion === "string" && indexMajor === null) ||
669
- (typeof projectionSchemaVersion === "string" && projectionMajor === null)
670
- ) {
671
- return { classification: "unsupported", reason: "control manifest uses a newer or unknown format major" };
672
- }
673
- if (!isControlManifest(value)) {
674
- return { classification: "corrupt", reason: "control manifest is missing required completion metadata" };
675
- }
676
- if (indexMajor !== 1 || projectionMajor !== 1) {
677
- return { classification: "rebuildable", reason: "known older projection formats are rebuild-only" };
678
- }
679
- const compatibilityFacts: Array<[unknown, unknown, string]> = [
680
- [value.indexFormatVersion, LADYBUG_INDEX_FORMAT, "index format"],
681
- [value.projectionSchemaVersion, source.manifest.schemaVersion, "projection schema"],
682
- [value.normalizationVersion, source.manifest.normalizationVersion, "normalization version"],
683
- [value.loreVersion, source.manifest.exporter.version, "Lore version"],
684
- [value.exporterName, source.manifest.exporter.name, "exporter name"],
685
- [value.exporterVersion, source.manifest.exporter.version, "exporter version"],
686
- [value.ladybugVersion, source.ladybugVersion, "Ladybug version"],
687
- [value.ladybugStorageVersion, source.ladybugStorageVersion, "Ladybug storage version"],
688
- ];
689
- const compatibilityMismatch = compatibilityFacts.find(([actual, expected]) => actual !== expected);
690
- if (compatibilityMismatch !== undefined) {
691
- return { classification: "rebuildable", reason: `${compatibilityMismatch[2]} changed` };
692
- }
693
- if (value.sourceFingerprint !== source.sourceFingerprint) {
694
- return { classification: "rebuildable", reason: "source fingerprint changed" };
695
- }
696
- const duplicatedFacts: Array<[unknown, unknown, string]> = [
697
- [value.repositoryScopeKey, source.repositoryScopeKey, "repository scope"],
698
- [value.snapshotKey, source.snapshotKey, "snapshot"],
699
- [value.bundleId, source.manifest.bundle.id, "bundle"],
700
- [value.okfVersion, source.manifest.bundle.okfVersion, "OKF version"],
701
- [value.docsRoot, source.manifest.bundle.docsRoot, "docs root"],
702
- [value.gitCommit, source.manifest.bundle.gitCommit, "Git commit"],
703
- [value.exportDigest, source.exportDigest, "export digest"],
704
- [value.taskSnapshotDigest, source.taskSnapshotDigest, "task snapshot"],
705
- [value.sourceRecordsDigest, source.sourceRecordsDigest, "source records"],
706
- [value.recordKeysDigest, source.recordKeysDigest, "record keys"],
707
- [value.inputFingerprint, source.inputFingerprint, "input fingerprint"],
708
- [value.recordCount, source.trailer.recordCount, "record count"],
709
- [value.counts.concepts, source.counts.concepts, "concept count"],
710
- [value.counts.tasks, source.counts.tasks, "task count"],
711
- [value.counts.authoredEdges, source.counts.authoredEdges, "authored-edge count"],
712
- ];
713
- const mismatch = duplicatedFacts.find(([actual, expected]) => actual !== expected);
714
- return mismatch === undefined
715
- ? { classification: "reusable" }
716
- : { classification: "corrupt", reason: `duplicated ${mismatch[2]} metadata disagrees` };
717
- }
718
-
719
- function controlManifest(
720
- source: LadybugProjectionSource,
721
- databaseByteLength: number,
722
- databaseDigest: string,
723
- ): LadybugControlManifest {
724
- return {
725
- indexFormatVersion: LADYBUG_INDEX_FORMAT,
726
- projectionSchemaVersion: source.manifest.schemaVersion,
727
- normalizationVersion: source.manifest.normalizationVersion,
728
- loreVersion: source.manifest.exporter.version,
729
- exporterName: source.manifest.exporter.name,
730
- exporterVersion: source.manifest.exporter.version,
731
- ladybugVersion: source.ladybugVersion,
732
- ladybugStorageVersion: source.ladybugStorageVersion,
733
- repositoryScopeKey: source.repositoryScopeKey,
734
- snapshotKey: source.snapshotKey,
735
- bundleId: source.manifest.bundle.id,
736
- okfVersion: source.manifest.bundle.okfVersion,
737
- docsRoot: source.manifest.bundle.docsRoot,
738
- gitCommit: source.manifest.bundle.gitCommit,
739
- exportDigest: source.exportDigest,
740
- taskSnapshotDigest: source.taskSnapshotDigest,
741
- sourceFingerprint: source.sourceFingerprint,
742
- inputFingerprint: source.inputFingerprint,
743
- warnings: source.warnings,
744
- warningsComplete: source.warningsComplete,
745
- sourceRecordsDigest: source.sourceRecordsDigest,
746
- recordKeysDigest: source.recordKeysDigest,
747
- recordCount: source.trailer.recordCount,
748
- counts: source.counts,
749
- database: {
750
- file: LADYBUG_DATABASE_FILENAME,
751
- byteLength: databaseByteLength,
752
- digest: databaseDigest,
753
- },
754
- completed: true,
755
- };
756
- }
757
-
758
- function writeControlManifestLast(path: string, control: LadybugControlManifest): void {
759
- const descriptor = openSync(path, "wx", 0o400);
760
- try {
761
- writeFileSync(descriptor, `${canonicalJson(control)}\n`, "utf8");
762
- fsyncSync(descriptor);
763
- } finally {
764
- closeSync(descriptor);
765
- }
766
- fsyncDirectory(resolve(path, ".."));
767
- }
768
-
769
- function ensureCacheLayout(root: string, cacheRoot: string, cacheRelRoot: string): void {
770
- const realRoot = canonicalRepositoryRoot(root);
771
- assertExistingCacheLayoutSafe(realRoot, cacheRoot, cacheRelRoot);
772
- const segments = cacheRelRoot.split("/");
773
- let current = realRoot;
774
- for (const segment of segments) {
775
- current = join(current, segment);
776
- assertContained(realRoot, current);
777
- if (!existsSync(current)) {
778
- try {
779
- mkdirSync(current, { mode: 0o700 });
780
- } catch (cause) {
781
- if (!isErrno(cause, "EEXIST")) throw cause;
782
- }
783
- }
784
- const stat = lstatSync(current);
785
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
786
- throw new LoreError(
787
- "denied",
788
- `refusing Ladybug cache path through non-directory or symlink ${relative(realRoot, current)}`,
789
- "replace it with a repository-local real directory",
790
- );
791
- }
792
- }
793
- if (current !== cacheRoot) throw lifecycleError("resolved cache root does not match the frozen storage path");
794
- const generations = join(cacheRoot, "generations");
795
- if (!existsSync(generations)) {
796
- try {
797
- mkdirSync(generations, { mode: 0o700 });
798
- } catch (cause) {
799
- if (!isErrno(cause, "EEXIST")) throw cause;
800
- }
801
- }
802
- const stat = lstatSync(generations);
803
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
804
- throw new LoreError("denied", "refusing symlinked or non-directory Ladybug generations path");
805
- }
806
- }
807
-
808
- function assertExistingCacheLayoutSafe(root: string, cacheRoot: string, cacheRelRoot: string): void {
809
- const segments = [...cacheRelRoot.split("/"), "generations"];
810
- let current = root;
811
- for (const segment of segments) {
812
- current = join(current, segment);
813
- let stat: ReturnType<typeof lstatSync>;
814
- try {
815
- stat = lstatSync(current);
816
- } catch (cause) {
817
- if (isErrno(cause, "ENOENT") || isErrno(cause, "ENOTDIR")) return;
818
- throw cause;
819
- }
820
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
821
- throw new LoreError(
822
- "denied",
823
- `refusing Ladybug cache path through non-directory or symlink ${relative(root, current)}`,
824
- "replace it with a repository-local real directory",
825
- );
826
- }
827
- }
828
- if (join(root, cacheRelRoot) !== cacheRoot) {
829
- throw lifecycleError("resolved cache root does not match the frozen storage path");
830
- }
831
- }
832
-
833
- function validateCacheRelRoot(value: string): string {
834
- const segments = value.split("/");
835
- if (
836
- value === "" ||
837
- isAbsolute(value) ||
838
- !value.startsWith(".lore/cache/") ||
839
- segments.some((segment) => segment === "" || segment === "." || segment === ".." || segment.includes("\\"))
840
- ) {
841
- throw new LoreError("denied", "Ladybug cache root must be a contained repository-relative .lore/cache path");
842
- }
843
- return value;
844
- }
845
-
846
- function pruneOtherGenerations(cacheRoot: string, keepGenerationKey: string): void {
847
- const generationsRoot = join(cacheRoot, "generations");
848
- for (const entry of readdirSync(generationsRoot, { withFileTypes: true })) {
849
- if (entry.name === keepGenerationKey) continue;
850
- if (!entry.isDirectory() || entry.isSymbolicLink() || !/^[0-9a-f]{64}$/.test(entry.name)) {
851
- throw new LoreError("denied", "refusing current-only cleanup through an unexpected workspace generation entry");
852
- }
853
- removeContained(cacheRoot, join(generationsRoot, entry.name));
854
- }
855
- fsyncDirectory(generationsRoot);
856
- }
857
-
858
- function inspectWriterLock(cacheRoot: string): "none" | "stale" | "active" {
859
- const path = join(cacheRoot, "writer.lock");
860
- if (!existsSync(path)) return "none";
861
- const record = readLock(path);
862
- return record !== null && canProveProcessGone(record) ? "stale" : "active";
863
- }
864
-
865
- function acquireWriterLock(cacheRoot: string): OwnedWriterLock | null {
866
- const path = join(cacheRoot, "writer.lock");
867
- const record: WriterLockRecord = {
868
- ownerToken: randomBytes(16).toString("hex"),
869
- pid: process.pid,
870
- processStartIdentity: processStartIdentity(process.pid),
871
- hostname: hostname(),
872
- acquiredAt: new Date().toISOString(),
873
- };
874
- for (let attempt = 0; attempt < 2; attempt++) {
875
- try {
876
- const descriptor = openSync(path, "wx", 0o600);
877
- try {
878
- writeFileSync(descriptor, `${canonicalJson(record)}\n`, "utf8");
879
- fsyncSync(descriptor);
880
- } finally {
881
- closeSync(descriptor);
882
- }
883
- fsyncDirectory(cacheRoot);
884
- return { path, record };
885
- } catch (cause) {
886
- if (!isErrno(cause, "EEXIST")) throw cause;
887
- const existing = readLock(path);
888
- if (attempt > 0 || existing === null || !canProveProcessGone(existing)) return null;
889
- const stalePath = join(cacheRoot, `.stale-lock-${record.ownerToken}`);
890
- try {
891
- renameSync(path, stalePath);
892
- } catch (renameCause) {
893
- if (isErrno(renameCause, "ENOENT")) continue;
894
- return null;
895
- }
896
- }
897
- }
898
- return null;
899
- }
900
-
901
- function releaseWriterLock(lock: OwnedWriterLock): void {
902
- const current = readLock(lock.path);
903
- if (current?.ownerToken !== lock.record.ownerToken) return;
904
- try {
905
- unlinkSync(lock.path);
906
- fsyncDirectory(resolve(lock.path, ".."));
907
- } catch (cause) {
908
- if (!isErrno(cause, "ENOENT")) throw cause;
909
- }
910
- }
911
-
912
- function readLock(path: string): WriterLockRecord | null {
913
- try {
914
- const stat = lstatSync(path);
915
- if (!stat.isFile() || stat.isSymbolicLink()) return null;
916
- const value = JSON.parse(readFileSync(path, "utf8")) as unknown;
917
- return isWriterLockRecord(value) ? value : null;
918
- } catch {
919
- return null;
920
- }
921
- }
922
-
923
- function canProveProcessGone(record: WriterLockRecord): boolean {
924
- if (
925
- record.hostname !== hostname() ||
926
- record.processStartIdentity === null ||
927
- !new RegExp(`^pid:${record.pid}:started:\\d+$`).test(record.processStartIdentity)
928
- ) {
929
- return false;
930
- }
931
- try {
932
- process.kill(record.pid, 0);
933
- // A live PID is not enough to prove it is the recorded process instance,
934
- // and this portable layer cannot inspect another process's start identity
935
- // without an external command. Conservatively retain ownership.
936
- return false;
937
- } catch (cause) {
938
- return isErrno(cause, "ESRCH");
939
- }
940
- }
941
-
942
- function processStartIdentity(pid: number): string | null {
943
- if (!Number.isSafeInteger(pid) || pid <= 0) return null;
944
- return pid === process.pid ? CURRENT_PROCESS_START_IDENTITY : null;
945
- }
946
-
947
- function quarantineGeneration(cacheRoot: string, source: LadybugProjectionSource, ownerToken: string): void {
948
- const generationPath = generationPathFor(cacheRoot, source);
949
- if (!existsSync(generationPath)) return;
950
- const quarantinePath = join(cacheRoot, `.corrupt-${source.generationKey}-${ownerToken}`);
951
- assertContained(cacheRoot, quarantinePath);
952
- const stat = lstatSync(generationPath);
953
- if (stat.isDirectory() && !stat.isSymbolicLink()) chmodSync(generationPath, 0o700);
954
- renameSync(generationPath, quarantinePath);
955
- fsyncDirectory(join(cacheRoot, "generations"));
956
- fsyncDirectory(cacheRoot);
957
- }
958
-
959
- function retireRebuildableGeneration(cacheRoot: string, source: LadybugProjectionSource, ownerToken: string): void {
960
- const generationPath = generationPathFor(cacheRoot, source);
961
- if (!existsSync(generationPath)) return;
962
- const retiredPath = join(cacheRoot, `.rebuildable-${source.generationKey}-${ownerToken}`);
963
- assertContained(cacheRoot, retiredPath);
964
- const stat = lstatSync(generationPath);
965
- if (stat.isDirectory() && !stat.isSymbolicLink()) chmodSync(generationPath, 0o700);
966
- renameSync(generationPath, retiredPath);
967
- fsyncDirectory(join(cacheRoot, "generations"));
968
- fsyncDirectory(cacheRoot);
969
- }
970
-
971
- function cleanupAbandonedStaging(cacheRoot: string): void {
972
- for (const entry of readdirSync(cacheRoot, { withFileTypes: true })) {
973
- if (!entry.name.startsWith(".building-")) continue;
974
- try {
975
- removeContained(cacheRoot, join(cacheRoot, entry.name));
976
- } catch {
977
- // Cleanup is advisory. A unique owner token keeps the new staging path
978
- // independent, so an undeletable abandoned build must not block it.
979
- }
980
- }
981
- }
982
-
983
- function removeContained(cacheRoot: string, target: string): void {
984
- assertContained(cacheRoot, target);
985
- try {
986
- const stat = lstatSync(target);
987
- if (stat.isDirectory() && !stat.isSymbolicLink()) makeDirectoryTreeWritable(target);
988
- } catch (cause) {
989
- if (isErrno(cause, "ENOENT")) return;
990
- throw cause;
991
- }
992
- rmSync(target, { recursive: true, force: false });
993
- }
994
-
995
- function makeDirectoryTreeWritable(path: string): void {
996
- chmodSync(path, 0o700);
997
- for (const entry of readdirSync(path, { withFileTypes: true })) {
998
- if (entry.isDirectory() && !entry.isSymbolicLink()) {
999
- makeDirectoryTreeWritable(join(path, entry.name));
1000
- }
1001
- }
1002
- }
1003
-
1004
- function generationPathFor(cacheRoot: string, source: LadybugProjectionSource): string {
1005
- return join(cacheRoot, "generations", source.generationKey);
1006
- }
1007
-
1008
- async function hashFile(path: string): Promise<{ byteLength: number; digest: string }> {
1009
- const hash = createHash("sha256");
1010
- let byteLength = 0;
1011
- await new Promise<void>((resolvePromise, rejectPromise) => {
1012
- const stream = createReadStream(path);
1013
- stream.on("data", (chunk: Buffer) => {
1014
- byteLength += chunk.byteLength;
1015
- hash.update(chunk);
1016
- });
1017
- stream.on("error", rejectPromise);
1018
- stream.on("end", resolvePromise);
1019
- });
1020
- const stat = statSync(path);
1021
- if (stat.size !== byteLength) throw lifecycleError("projection database changed while hashing");
1022
- return { byteLength, digest: `sha256:${hash.digest("hex")}` };
1023
- }
1024
-
1025
- function canonicalRepositoryRoot(root: string): string {
1026
- const resolved = resolve(root);
1027
- try {
1028
- return realpathSync(resolved);
1029
- } catch (cause) {
1030
- throw new LoreError(
1031
- "not_found",
1032
- `cannot resolve repository root ${resolved}: ${errorMessage(cause)}`,
1033
- "use an existing repository directory",
1034
- { root: resolved },
1035
- );
1036
- }
1037
- }
1038
-
1039
- function assertContained(root: string, target: string): void {
1040
- const rel = relative(root, target);
1041
- if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) {
1042
- if (rel === "") return;
1043
- throw new LoreError(
1044
- "denied",
1045
- `Ladybug lifecycle path escapes its repository-local cache root: ${target}`,
1046
- "use only the resolved .lore/cache/graph/ladybug/1 path",
1047
- );
1048
- }
1049
- }
1050
-
1051
- function fsyncDirectory(path: string): void {
1052
- let descriptor: number | undefined;
1053
- try {
1054
- descriptor = openSync(path, "r");
1055
- fsyncSync(descriptor);
1056
- } catch (cause) {
1057
- if (!isUnsupportedDirectoryFsyncError(cause)) throw cause;
1058
- } finally {
1059
- if (descriptor !== undefined) closeSync(descriptor);
1060
- }
1061
- }
1062
-
1063
- export function isUnsupportedDirectoryFsyncError(
1064
- cause: unknown,
1065
- platform: NodeJS.Platform = process.platform,
1066
- ): boolean {
1067
- return isErrno(cause, "EINVAL") || isErrno(cause, "ENOTSUP") || (platform === "win32" && isErrno(cause, "EPERM"));
1068
- }
1069
-
1070
- function assertNativeVersion(source: LadybugProjectionSource): void {
1071
- if (
1072
- source.ladybugVersion !== EXPECTED_LADYBUG_VERSION ||
1073
- source.ladybugStorageVersion !== EXPECTED_LADYBUG_STORAGE_VERSION
1074
- ) {
1075
- throw lifecycleError("source fingerprint Ladybug runtime facts differ from the frozen compatibility contract");
1076
- }
1077
- }
1078
-
1079
- function onceNativeDriver(loader: LadybugNativeLoader): LadybugNativeLoader {
1080
- let pending: Promise<LadybugNativeDriver> | undefined;
1081
- return async () => {
1082
- pending ??= loader().then((driver) => {
1083
- if (
1084
- driver.LADYBUG_VERSION !== EXPECTED_LADYBUG_VERSION ||
1085
- driver.LADYBUG_STORAGE_VERSION !== EXPECTED_LADYBUG_STORAGE_VERSION
1086
- ) {
1087
- throw lifecycleError("loaded Ladybug runtime differs from the frozen compatibility contract");
1088
- }
1089
- return driver;
1090
- });
1091
- return pending;
1092
- };
1093
- }
1094
-
1095
- function isControlManifest(value: unknown): value is LadybugControlManifest {
1096
- if (!isObject(value)) return false;
1097
- return (
1098
- typeof value.indexFormatVersion === "string" &&
1099
- typeof value.projectionSchemaVersion === "string" &&
1100
- typeof value.normalizationVersion === "string" &&
1101
- typeof value.loreVersion === "string" &&
1102
- typeof value.exporterName === "string" &&
1103
- typeof value.exporterVersion === "string" &&
1104
- typeof value.ladybugVersion === "string" &&
1105
- typeof value.ladybugStorageVersion === "string" &&
1106
- typeof value.repositoryScopeKey === "string" &&
1107
- typeof value.snapshotKey === "string" &&
1108
- typeof value.bundleId === "string" &&
1109
- typeof value.okfVersion === "string" &&
1110
- typeof value.docsRoot === "string" &&
1111
- (value.gitCommit === null || typeof value.gitCommit === "string") &&
1112
- typeof value.exportDigest === "string" &&
1113
- typeof value.taskSnapshotDigest === "string" &&
1114
- typeof value.sourceFingerprint === "string" &&
1115
- typeof value.inputFingerprint === "string" &&
1116
- Array.isArray(value.warnings) &&
1117
- value.warnings.every((warning) => typeof warning === "string") &&
1118
- typeof value.warningsComplete === "boolean" &&
1119
- typeof value.sourceRecordsDigest === "string" &&
1120
- typeof value.recordKeysDigest === "string" &&
1121
- Number.isSafeInteger(value.recordCount) &&
1122
- Number(value.recordCount) >= 1 &&
1123
- isObject(value.counts) &&
1124
- Number.isSafeInteger(value.counts.concepts) &&
1125
- Number(value.counts.concepts) >= 0 &&
1126
- Number.isSafeInteger(value.counts.tasks) &&
1127
- Number(value.counts.tasks) >= 0 &&
1128
- Number.isSafeInteger(value.counts.authoredEdges) &&
1129
- Number(value.counts.authoredEdges) >= 0 &&
1130
- isObject(value.database) &&
1131
- value.database.file === LADYBUG_DATABASE_FILENAME &&
1132
- Number.isSafeInteger(value.database.byteLength) &&
1133
- Number(value.database.byteLength) >= 0 &&
1134
- typeof value.database.digest === "string" &&
1135
- value.completed === true
1136
- );
1137
- }
1138
-
1139
- function isWriterLockRecord(value: unknown): value is WriterLockRecord {
1140
- return (
1141
- isObject(value) &&
1142
- typeof value.ownerToken === "string" &&
1143
- Number.isSafeInteger(value.pid) &&
1144
- (value.processStartIdentity === null || typeof value.processStartIdentity === "string") &&
1145
- typeof value.hostname === "string" &&
1146
- typeof value.acquiredAt === "string"
1147
- );
1148
- }
1149
-
1150
- function formatMajor(value: unknown, pattern: RegExp): number | null {
1151
- if (typeof value !== "string") return null;
1152
- const match = pattern.exec(value);
1153
- return match === null ? null : Number(match[1]);
1154
- }
1155
-
1156
- function isNativeLockError(cause: unknown): boolean {
1157
- return /could not set lock|database.*lock|locked/i.test(errorMessage(cause));
1158
- }
1159
-
1160
- function lifecycleError(message: string): LoreError {
1161
- return new LoreError(
1162
- "validation",
1163
- `Ladybug projection lifecycle failed: ${message}`,
1164
- "the in-memory projection remains authoritative; inspect or rebuild the disposable local cache",
1165
- );
1166
- }
1167
-
1168
- function errorMessage(value: unknown): string {
1169
- return value instanceof Error ? value.message : String(value);
1170
- }
1171
-
1172
- function isObject(value: unknown): value is Record<string, unknown> {
1173
- return typeof value === "object" && value !== null && !Array.isArray(value);
1174
- }
1175
-
1176
- function isErrno(value: unknown, code: string): boolean {
1177
- return isObject(value) && value.code === code;
1178
- }