@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.
- package/README.md +33 -22
- package/bin/lore.cjs +35 -7
- package/package.json +17 -17
- package/src/adapters/backlog.ts +0 -1084
- package/src/adapters/git.ts +0 -221
- package/src/cli.ts +0 -667
- package/src/commands/agent.ts +0 -301
- package/src/commands/agents.ts +0 -302
- package/src/commands/args.ts +0 -209
- package/src/commands/changed.ts +0 -70
- package/src/commands/check.ts +0 -1031
- package/src/commands/codex-bridge.ts +0 -49
- package/src/commands/concurrency.ts +0 -48
- package/src/commands/context.ts +0 -292
- package/src/commands/discover.ts +0 -89
- package/src/commands/explorer.ts +0 -253
- package/src/commands/export.ts +0 -93
- package/src/commands/fswrite.ts +0 -928
- package/src/commands/graph.ts +0 -291
- package/src/commands/help.ts +0 -151
- package/src/commands/impact.ts +0 -59
- package/src/commands/init.ts +0 -583
- package/src/commands/instructions.ts +0 -91
- package/src/commands/link.ts +0 -929
- package/src/commands/new.ts +0 -476
- package/src/commands/orphans.ts +0 -457
- package/src/commands/path.ts +0 -67
- package/src/commands/provenance.ts +0 -68
- package/src/commands/query.ts +0 -312
- package/src/commands/reconcile-shared.ts +0 -280
- package/src/commands/rename.ts +0 -585
- package/src/commands/replace.ts +0 -320
- package/src/commands/scaffold.ts +0 -346
- package/src/commands/schema.ts +0 -293
- package/src/commands/snapshot.ts +0 -130
- package/src/commands/supersede.ts +0 -400
- package/src/commands/sync.ts +0 -371
- package/src/commands/tasks.ts +0 -271
- package/src/commands/traversal.ts +0 -151
- package/src/commands/validate.ts +0 -226
- package/src/config.ts +0 -598
- package/src/core/agent-bridge.ts +0 -287
- package/src/core/agent-context.ts +0 -498
- package/src/core/agent-profile.ts +0 -447
- package/src/core/bundle.ts +0 -893
- package/src/core/check.ts +0 -853
- package/src/core/codex-bridge.ts +0 -100
- package/src/core/concept.ts +0 -597
- package/src/core/consumer-scaffold.ts +0 -433
- package/src/core/context.ts +0 -271
- package/src/core/explorer-contract.ts +0 -441
- package/src/core/explorer-qualification.ts +0 -58
- package/src/core/explorer.ts +0 -518
- package/src/core/finding.ts +0 -31
- package/src/core/graph.ts +0 -201
- package/src/core/indexes.ts +0 -436
- package/src/core/instructions.ts +0 -209
- package/src/core/ladybug-driver.ts +0 -1795
- package/src/core/ladybug-lifecycle.ts +0 -1178
- package/src/core/ladybug-native.ts +0 -95
- package/src/core/ladybug-source.ts +0 -667
- package/src/core/links.ts +0 -681
- package/src/core/log.ts +0 -253
- package/src/core/managed-block.ts +0 -540
- package/src/core/manifest.ts +0 -718
- package/src/core/order.ts +0 -13
- package/src/core/profile.ts +0 -1007
- package/src/core/projection.ts +0 -195
- package/src/core/query.ts +0 -542
- package/src/core/reconcile.ts +0 -236
- package/src/core/replace.ts +0 -419
- package/src/core/retrieval.ts +0 -213
- package/src/core/rewrite.ts +0 -940
- package/src/core/scaffold.ts +0 -255
- package/src/core/schema.ts +0 -366
- package/src/core/snapshot-runtime.ts +0 -52
- package/src/core/snapshot-store.ts +0 -287
- package/src/core/snapshot.ts +0 -711
- package/src/core/template.ts +0 -429
- package/src/core/traversal.ts +0 -487
- package/src/core/validate.ts +0 -517
- package/src/core/workspace-contract.ts +0 -473
- package/src/core/workspace-projection.ts +0 -365
- package/src/core/workspace-retrieval.ts +0 -196
- package/src/core/workspace-source.ts +0 -174
- package/src/errors.ts +0 -697
- package/src/meta.ts +0 -7
- package/src/output.ts +0 -589
- package/src/scripts/upstream-backlog-watch.ts +0 -288
- 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
|
-
}
|