@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,667 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Deterministic source snapshot for the repository-local Ladybug projection.
|
|
3
|
-
*
|
|
4
|
-
* Export schema 1.0 remains the sole ingestion boundary. This module validates
|
|
5
|
-
* that stream, derives stable public/source identities, and fingerprints the
|
|
6
|
-
* exact repository inputs without writing any repository file.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import { closeSync, lstatSync, openSync, readFileSync, readSync } from "node:fs";
|
|
11
|
-
import { join } from "node:path";
|
|
12
|
-
import { type BacklogAdapter, type BacklogTask, bunBacklogSpawn, createBacklogAdapter } from "../adapters/backlog";
|
|
13
|
-
import { resolveHeadSha } from "../adapters/git";
|
|
14
|
-
import { LoreError, WarningCollector } from "../errors";
|
|
15
|
-
import { VERSION } from "../meta";
|
|
16
|
-
import { loadBundle, walkMarkdown } from "./bundle";
|
|
17
|
-
import { loadProfile, PROFILE_JSON_REL_PATH, PROFILE_REL_PATH } from "./profile";
|
|
18
|
-
import {
|
|
19
|
-
buildProjection,
|
|
20
|
-
PROJECTION_NORMALIZATION_VERSION,
|
|
21
|
-
PROJECTION_SCHEMA_VERSION,
|
|
22
|
-
type Projection,
|
|
23
|
-
type ProjectionRecord,
|
|
24
|
-
projectionStreamHash,
|
|
25
|
-
} from "./projection";
|
|
26
|
-
import { DOCS_DIR } from "./scaffold";
|
|
27
|
-
|
|
28
|
-
export const LADYBUG_INDEX_FORMAT = "ladybug-projection/1";
|
|
29
|
-
export const LADYBUG_INDEX_MAJOR = 1;
|
|
30
|
-
export const LADYBUG_CACHE_REL_ROOT = ".lore/cache/graph/ladybug/1";
|
|
31
|
-
export const LADYBUG_DATABASE_FILENAME = "projection.lbdb";
|
|
32
|
-
export const LADYBUG_CONTROL_FILENAME = "index.json";
|
|
33
|
-
export const LADYBUG_SOURCE_FINGERPRINT_DOMAIN = "ladybug-projection-source/1";
|
|
34
|
-
export const LADYBUG_INPUT_FINGERPRINT_DOMAIN = "ladybug-projection-input/1";
|
|
35
|
-
const BOUNDED_MEMORY_GC_RECORD_INTERVAL = 1024;
|
|
36
|
-
|
|
37
|
-
export interface SourceInventoryEntry {
|
|
38
|
-
readonly path: string;
|
|
39
|
-
readonly byteLength: number;
|
|
40
|
-
readonly byteHash: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ProfileInventoryEntry {
|
|
44
|
-
readonly path: string;
|
|
45
|
-
readonly bytes: string;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export interface LadybugProjectionCounts {
|
|
49
|
-
readonly concepts: number;
|
|
50
|
-
readonly tasks: number;
|
|
51
|
-
readonly authoredEdges: number;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export interface ProjectionManifestRecord extends ProjectionRecord {
|
|
55
|
-
readonly record: "manifest";
|
|
56
|
-
readonly schemaVersion: string;
|
|
57
|
-
readonly bundle: {
|
|
58
|
-
readonly id: string;
|
|
59
|
-
readonly okfVersion: string;
|
|
60
|
-
readonly docsRoot: string;
|
|
61
|
-
readonly gitCommit: string | null;
|
|
62
|
-
};
|
|
63
|
-
readonly exporter: {
|
|
64
|
-
readonly name: string;
|
|
65
|
-
readonly version: string;
|
|
66
|
-
};
|
|
67
|
-
readonly generatedAt: string | null;
|
|
68
|
-
readonly normalizationVersion: string;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export interface ProjectionTrailerRecord extends ProjectionRecord {
|
|
72
|
-
readonly record: "trailer";
|
|
73
|
-
readonly recordCount: number;
|
|
74
|
-
readonly streamHash: string;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface ProjectionConceptRecord extends ProjectionRecord {
|
|
78
|
-
readonly record: "concept";
|
|
79
|
-
readonly key: string;
|
|
80
|
-
readonly id: string;
|
|
81
|
-
readonly path: string;
|
|
82
|
-
readonly type: string;
|
|
83
|
-
readonly frontmatter: Record<string, unknown>;
|
|
84
|
-
readonly body: string;
|
|
85
|
-
readonly contentHash: string;
|
|
86
|
-
readonly tokenEstimate: number;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export interface ProjectionTaskRecord extends ProjectionRecord {
|
|
90
|
-
readonly record: "task";
|
|
91
|
-
readonly key: string;
|
|
92
|
-
readonly id: string;
|
|
93
|
-
readonly title: string;
|
|
94
|
-
readonly status: string;
|
|
95
|
-
readonly labels: readonly string[];
|
|
96
|
-
readonly priority: string | null;
|
|
97
|
-
readonly ordinal: number | null;
|
|
98
|
-
readonly assignees: readonly string[];
|
|
99
|
-
readonly milestone: string | null;
|
|
100
|
-
readonly parentTaskId: string | null;
|
|
101
|
-
readonly sourceAdapterVersion: string;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export interface ProjectionEdgeRecord extends ProjectionRecord {
|
|
105
|
-
readonly record: "edge";
|
|
106
|
-
readonly key: string;
|
|
107
|
-
readonly from: string;
|
|
108
|
-
readonly to: string | null;
|
|
109
|
-
readonly kind: string;
|
|
110
|
-
readonly target: string;
|
|
111
|
-
readonly ordinal: number;
|
|
112
|
-
readonly dangling: boolean;
|
|
113
|
-
/** Workspace-only endpoint metadata; absent repository-local records retain M6 defaults. */
|
|
114
|
-
readonly workspaceFromKind?: "concept" | "task";
|
|
115
|
-
readonly workspaceToKind?: "concept" | "task";
|
|
116
|
-
readonly workspaceLinkKind?: string;
|
|
117
|
-
/** Original member edge key or manifest link id behind a namespaced workspace record. */
|
|
118
|
-
readonly workspaceSourceRecordKey?: string;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface LadybugProjectionSource {
|
|
122
|
-
readonly records: readonly ProjectionRecord[];
|
|
123
|
-
readonly manifest: ProjectionManifestRecord;
|
|
124
|
-
readonly trailer: ProjectionTrailerRecord;
|
|
125
|
-
readonly concepts: readonly ProjectionConceptRecord[];
|
|
126
|
-
readonly tasks: readonly ProjectionTaskRecord[];
|
|
127
|
-
readonly authoredEdges: readonly ProjectionEdgeRecord[];
|
|
128
|
-
readonly inventory: readonly SourceInventoryEntry[];
|
|
129
|
-
readonly profileInventory: readonly ProfileInventoryEntry[];
|
|
130
|
-
readonly repositoryScopeKey: string;
|
|
131
|
-
readonly snapshotKey: string;
|
|
132
|
-
readonly commitKey: string | null;
|
|
133
|
-
readonly exportDigest: string;
|
|
134
|
-
readonly taskSnapshotDigest: string;
|
|
135
|
-
readonly sourceRecordsDigest: string;
|
|
136
|
-
readonly recordKeysDigest: string;
|
|
137
|
-
readonly sourceFingerprint: string;
|
|
138
|
-
/** Cheap exact-input identity used to reuse a generation without reparsing the bundle. */
|
|
139
|
-
readonly inputFingerprint: string;
|
|
140
|
-
/** Stable bundle warnings captured when this generation was fully built. */
|
|
141
|
-
readonly warnings: readonly string[];
|
|
142
|
-
/** True only when warnings came from the complete production source loader. */
|
|
143
|
-
readonly warningsComplete: boolean;
|
|
144
|
-
/** Portable directory segment for the content-addressed generation. */
|
|
145
|
-
readonly generationKey: string;
|
|
146
|
-
readonly ladybugVersion: string;
|
|
147
|
-
readonly ladybugStorageVersion: string;
|
|
148
|
-
readonly counts: LadybugProjectionCounts;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export interface PrepareLadybugProjectionSourceOptions {
|
|
152
|
-
readonly projection: Projection;
|
|
153
|
-
readonly inventory: readonly SourceInventoryEntry[];
|
|
154
|
-
readonly profileInventory: readonly ProfileInventoryEntry[];
|
|
155
|
-
readonly ladybugVersion: string;
|
|
156
|
-
readonly ladybugStorageVersion: string;
|
|
157
|
-
readonly loreVersion?: string;
|
|
158
|
-
readonly warnings?: readonly string[];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export interface LadybugProjectionFreshness {
|
|
162
|
-
readonly inputFingerprint: string;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
export interface LoadLadybugProjectionSourceOptions {
|
|
166
|
-
readonly root: string;
|
|
167
|
-
readonly ladybugVersion: string;
|
|
168
|
-
readonly ladybugStorageVersion: string;
|
|
169
|
-
readonly adapter?: BacklogAdapter;
|
|
170
|
-
readonly resolveGitCommit?: (root: string) => string | null;
|
|
171
|
-
readonly warnings?: WarningCollector;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Read the repository and build one complete preflight source snapshot. The
|
|
176
|
-
* caller invokes this again after acquiring the writer lock and before
|
|
177
|
-
* publication to detect changes made during the native build.
|
|
178
|
-
*/
|
|
179
|
-
export async function loadLadybugProjectionSource(
|
|
180
|
-
options: LoadLadybugProjectionSourceOptions,
|
|
181
|
-
): Promise<LadybugProjectionSource> {
|
|
182
|
-
const adapter = options.adapter ?? createBacklogAdapter(bunBacklogSpawn(undefined, options.root));
|
|
183
|
-
const resolveGitCommit = options.resolveGitCommit ?? resolveHeadSha;
|
|
184
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
185
|
-
// A changed source snapshot abandons this entire attempt. Buffer its
|
|
186
|
-
// advisories too, then merge only the stable attempt so indexed commands
|
|
187
|
-
// cannot emit duplicate warnings that the reference loader would report
|
|
188
|
-
// once.
|
|
189
|
-
const attemptWarnings = options.warnings === undefined ? undefined : new WarningCollector();
|
|
190
|
-
const inventory = readSourceInventory(options.root);
|
|
191
|
-
const profileInventory = readProfileInventory(options.root);
|
|
192
|
-
const profile = loadProfile({ root: options.root });
|
|
193
|
-
const graph = loadBundle(join(options.root, DOCS_DIR), {
|
|
194
|
-
warnings: attemptWarnings,
|
|
195
|
-
profile,
|
|
196
|
-
boundedMemory: true,
|
|
197
|
-
});
|
|
198
|
-
const tasks = await adapter.listTasks();
|
|
199
|
-
const gitCommit = resolveGitCommit(options.root);
|
|
200
|
-
const projection = buildProjection({
|
|
201
|
-
graph,
|
|
202
|
-
tasks,
|
|
203
|
-
docsRoot: DOCS_DIR,
|
|
204
|
-
okfVersion: profile.okfVersion,
|
|
205
|
-
exporterVersion: VERSION,
|
|
206
|
-
gitCommit,
|
|
207
|
-
generatedAt: null,
|
|
208
|
-
materializeJsonl: false,
|
|
209
|
-
});
|
|
210
|
-
const finalInventory = readSourceInventory(options.root);
|
|
211
|
-
const finalProfileInventory = readProfileInventory(options.root);
|
|
212
|
-
const finalGitCommit = resolveGitCommit(options.root);
|
|
213
|
-
if (
|
|
214
|
-
canonicalJson(inventory) === canonicalJson(finalInventory) &&
|
|
215
|
-
canonicalJson(profileInventory) === canonicalJson(finalProfileInventory) &&
|
|
216
|
-
gitCommit === finalGitCommit
|
|
217
|
-
) {
|
|
218
|
-
const source = prepareLadybugProjectionSource({
|
|
219
|
-
projection,
|
|
220
|
-
inventory,
|
|
221
|
-
profileInventory,
|
|
222
|
-
ladybugVersion: options.ladybugVersion,
|
|
223
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
224
|
-
loreVersion: VERSION,
|
|
225
|
-
warnings: attemptWarnings?.list() ?? [],
|
|
226
|
-
});
|
|
227
|
-
if (attemptWarnings !== undefined && options.warnings !== undefined) {
|
|
228
|
-
options.warnings.merge(attemptWarnings);
|
|
229
|
-
}
|
|
230
|
-
return source;
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
throw new LoreError(
|
|
234
|
-
"drift",
|
|
235
|
-
"Ladybug projection source changed while its deterministic snapshot was being read",
|
|
236
|
-
"retry after repository writes settle; no cache generation was published",
|
|
237
|
-
);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
/** Validate export schema 1.0 and derive its complete Ladybug source metadata. */
|
|
241
|
-
export function prepareLadybugProjectionSource(
|
|
242
|
-
options: PrepareLadybugProjectionSourceOptions,
|
|
243
|
-
): LadybugProjectionSource {
|
|
244
|
-
const { manifest, trailer, concepts, tasks, authoredEdges } = validateProjection(options.projection);
|
|
245
|
-
const loreVersion = options.loreVersion ?? manifest.exporter.version;
|
|
246
|
-
if (manifest.exporter.name !== "lore" || manifest.exporter.version !== loreVersion) {
|
|
247
|
-
invalidProjection("exporter metadata does not match the active Lore version");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const repositoryScopeKey = digest(`lore-repository-scope/1\0${manifest.bundle.id}\0${manifest.bundle.docsRoot}`);
|
|
251
|
-
const snapshotKey = digest(`lore-projection-snapshot/1\0${repositoryScopeKey}\0${trailer.streamHash}`);
|
|
252
|
-
const commitKey =
|
|
253
|
-
manifest.bundle.gitCommit === null
|
|
254
|
-
? null
|
|
255
|
-
: digest(`lore-source-commit/1\0${repositoryScopeKey}\0${manifest.bundle.gitCommit}`);
|
|
256
|
-
|
|
257
|
-
const taskRecords = [
|
|
258
|
-
...tasks.map((record) => JSON.stringify(record)),
|
|
259
|
-
...authoredEdges.filter((record) => record.kind === "task").map((record) => JSON.stringify(record)),
|
|
260
|
-
];
|
|
261
|
-
const taskSnapshotDigest = digest(taskRecords.join("\n"));
|
|
262
|
-
const sourceRecordsDigest = digestJsonLines([...concepts, ...tasks, ...authoredEdges], true);
|
|
263
|
-
const recordKeysDigest = digest(
|
|
264
|
-
[...concepts, ...tasks, ...authoredEdges]
|
|
265
|
-
.map((record) => record.key)
|
|
266
|
-
.sort(compareCodeUnits)
|
|
267
|
-
.join("\n"),
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
const inventory = [...options.inventory].sort((a, b) => compareCodeUnits(a.path, b.path));
|
|
271
|
-
const profileInventory = [...options.profileInventory].sort((a, b) => compareCodeUnits(a.path, b.path));
|
|
272
|
-
const taskSnapshot = tasks
|
|
273
|
-
.map((record) => JSON.parse(JSON.stringify(record)) as unknown)
|
|
274
|
-
.sort((a, b) =>
|
|
275
|
-
compareCodeUnits(String((a as { key?: unknown }).key ?? ""), String((b as { key?: unknown }).key ?? "")),
|
|
276
|
-
);
|
|
277
|
-
const fingerprintFacts = {
|
|
278
|
-
indexFormatVersion: LADYBUG_INDEX_FORMAT,
|
|
279
|
-
projectionSchemaVersion: manifest.schemaVersion,
|
|
280
|
-
normalizationVersion: manifest.normalizationVersion,
|
|
281
|
-
loreVersion,
|
|
282
|
-
exporter: manifest.exporter,
|
|
283
|
-
ladybugVersion: options.ladybugVersion,
|
|
284
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
285
|
-
repositoryScopeKey,
|
|
286
|
-
bundleId: manifest.bundle.id,
|
|
287
|
-
gitCommit: manifest.bundle.gitCommit,
|
|
288
|
-
inventory,
|
|
289
|
-
profileInventory,
|
|
290
|
-
taskSnapshot,
|
|
291
|
-
};
|
|
292
|
-
const sourceFingerprint = digest(`${LADYBUG_SOURCE_FINGERPRINT_DOMAIN}\0${canonicalJson(fingerprintFacts)}`);
|
|
293
|
-
const inputFingerprint = projectionInputFingerprint({
|
|
294
|
-
inventory,
|
|
295
|
-
profileInventory,
|
|
296
|
-
gitCommit: manifest.bundle.gitCommit,
|
|
297
|
-
tasks,
|
|
298
|
-
ladybugVersion: options.ladybugVersion,
|
|
299
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
300
|
-
loreVersion,
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
return {
|
|
304
|
-
records: options.projection.records,
|
|
305
|
-
manifest,
|
|
306
|
-
trailer,
|
|
307
|
-
concepts,
|
|
308
|
-
tasks,
|
|
309
|
-
authoredEdges,
|
|
310
|
-
inventory,
|
|
311
|
-
profileInventory,
|
|
312
|
-
repositoryScopeKey,
|
|
313
|
-
snapshotKey,
|
|
314
|
-
commitKey,
|
|
315
|
-
exportDigest: trailer.streamHash,
|
|
316
|
-
taskSnapshotDigest,
|
|
317
|
-
sourceRecordsDigest,
|
|
318
|
-
recordKeysDigest,
|
|
319
|
-
sourceFingerprint,
|
|
320
|
-
inputFingerprint,
|
|
321
|
-
warnings: [...(options.warnings ?? [])],
|
|
322
|
-
warningsComplete: options.warnings !== undefined,
|
|
323
|
-
generationKey: digestHex(sourceFingerprint),
|
|
324
|
-
ladybugVersion: options.ladybugVersion,
|
|
325
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
326
|
-
counts: {
|
|
327
|
-
concepts: concepts.length,
|
|
328
|
-
tasks: tasks.length,
|
|
329
|
-
authoredEdges: authoredEdges.length,
|
|
330
|
-
},
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/** Fingerprint exact repository inputs without parsing Markdown or building a graph. */
|
|
335
|
-
export async function loadLadybugProjectionFreshness(
|
|
336
|
-
options: LoadLadybugProjectionSourceOptions,
|
|
337
|
-
): Promise<LadybugProjectionFreshness> {
|
|
338
|
-
const adapter = options.adapter ?? createBacklogAdapter(bunBacklogSpawn(undefined, options.root));
|
|
339
|
-
const resolveGitCommit = options.resolveGitCommit ?? resolveHeadSha;
|
|
340
|
-
for (let attempt = 0; attempt < 2; attempt++) {
|
|
341
|
-
const inventory = readSourceInventory(options.root);
|
|
342
|
-
const profileInventory = readProfileInventory(options.root);
|
|
343
|
-
const tasks = await adapter.listTasks();
|
|
344
|
-
const gitCommit = resolveGitCommit(options.root);
|
|
345
|
-
const inputFingerprint = projectionInputFingerprint({
|
|
346
|
-
inventory,
|
|
347
|
-
profileInventory,
|
|
348
|
-
gitCommit,
|
|
349
|
-
tasks,
|
|
350
|
-
ladybugVersion: options.ladybugVersion,
|
|
351
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
352
|
-
loreVersion: VERSION,
|
|
353
|
-
});
|
|
354
|
-
if (
|
|
355
|
-
canonicalJson(inventory) === canonicalJson(readSourceInventory(options.root)) &&
|
|
356
|
-
canonicalJson(profileInventory) === canonicalJson(readProfileInventory(options.root)) &&
|
|
357
|
-
gitCommit === resolveGitCommit(options.root)
|
|
358
|
-
) {
|
|
359
|
-
return { inputFingerprint };
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
throw new LoreError(
|
|
363
|
-
"drift",
|
|
364
|
-
"Ladybug projection source changed while its freshness was being checked",
|
|
365
|
-
"retry after repository writes settle; no cache generation was selected",
|
|
366
|
-
);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function projectionInputFingerprint(options: {
|
|
370
|
-
readonly inventory: readonly SourceInventoryEntry[];
|
|
371
|
-
readonly profileInventory: readonly ProfileInventoryEntry[];
|
|
372
|
-
readonly gitCommit: string | null;
|
|
373
|
-
readonly tasks: readonly (BacklogTask | ProjectionTaskRecord)[];
|
|
374
|
-
readonly ladybugVersion: string;
|
|
375
|
-
readonly ladybugStorageVersion: string;
|
|
376
|
-
readonly loreVersion: string;
|
|
377
|
-
}): string {
|
|
378
|
-
const tasks = options.tasks
|
|
379
|
-
.map((task) => ({
|
|
380
|
-
id: task.id,
|
|
381
|
-
title: task.title,
|
|
382
|
-
status: task.status,
|
|
383
|
-
labels: [...task.labels],
|
|
384
|
-
priority: task.priority,
|
|
385
|
-
ordinal: task.ordinal,
|
|
386
|
-
assignees: [...task.assignees],
|
|
387
|
-
milestone: task.milestone,
|
|
388
|
-
parentTaskId: task.parentTaskId,
|
|
389
|
-
sourceAdapterVersion: "backlog-json/1",
|
|
390
|
-
}))
|
|
391
|
-
.sort((a, b) => compareCodeUnits(a.id.toLowerCase(), b.id.toLowerCase()) || compareCodeUnits(a.id, b.id));
|
|
392
|
-
const facts = {
|
|
393
|
-
indexFormatVersion: LADYBUG_INDEX_FORMAT,
|
|
394
|
-
projectionSchemaVersion: PROJECTION_SCHEMA_VERSION,
|
|
395
|
-
normalizationVersion: PROJECTION_NORMALIZATION_VERSION,
|
|
396
|
-
loreVersion: options.loreVersion,
|
|
397
|
-
ladybugVersion: options.ladybugVersion,
|
|
398
|
-
ladybugStorageVersion: options.ladybugStorageVersion,
|
|
399
|
-
gitCommit: options.gitCommit,
|
|
400
|
-
inventory: [...options.inventory].sort((a, b) => compareCodeUnits(a.path, b.path)),
|
|
401
|
-
profileInventory: [...options.profileInventory].sort((a, b) => compareCodeUnits(a.path, b.path)),
|
|
402
|
-
tasks,
|
|
403
|
-
};
|
|
404
|
-
return digest(`${LADYBUG_INPUT_FINGERPRINT_DOMAIN}\0${canonicalJson(facts)}`);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export function readSourceInventory(root: string): SourceInventoryEntry[] {
|
|
408
|
-
const docsRoot = join(root, DOCS_DIR);
|
|
409
|
-
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
410
|
-
return walkMarkdown(docsRoot, undefined).map((relativePath) => {
|
|
411
|
-
const { byteLength, byteHash } = hashFileSync(join(docsRoot, relativePath), buffer);
|
|
412
|
-
return {
|
|
413
|
-
path: `${DOCS_DIR}/${relativePath}`,
|
|
414
|
-
byteLength,
|
|
415
|
-
byteHash,
|
|
416
|
-
};
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function hashFileSync(path: string, buffer: Buffer): { byteLength: number; byteHash: string } {
|
|
421
|
-
const hash = createHash("sha256");
|
|
422
|
-
const descriptor = openSync(path, "r");
|
|
423
|
-
let byteLength = 0;
|
|
424
|
-
try {
|
|
425
|
-
for (;;) {
|
|
426
|
-
const count = readSync(descriptor, buffer, 0, buffer.byteLength, null);
|
|
427
|
-
if (count === 0) break;
|
|
428
|
-
byteLength += count;
|
|
429
|
-
hash.update(buffer.subarray(0, count));
|
|
430
|
-
}
|
|
431
|
-
} finally {
|
|
432
|
-
closeSync(descriptor);
|
|
433
|
-
}
|
|
434
|
-
return { byteLength, byteHash: `sha256:${hash.digest("hex")}` };
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
export function readProfileInventory(root: string): ProfileInventoryEntry[] {
|
|
438
|
-
const entries: ProfileInventoryEntry[] = [];
|
|
439
|
-
for (const path of [PROFILE_REL_PATH, PROFILE_JSON_REL_PATH]) {
|
|
440
|
-
const absolute = join(root, path);
|
|
441
|
-
try {
|
|
442
|
-
const stat = lstatSync(absolute);
|
|
443
|
-
if (stat.isSymbolicLink()) {
|
|
444
|
-
throw new LoreError(
|
|
445
|
-
"denied",
|
|
446
|
-
`refusing to fingerprint symlinked profile ${path}`,
|
|
447
|
-
"replace it with a repository-local regular file",
|
|
448
|
-
{ path },
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
if (stat.isFile()) {
|
|
452
|
-
entries.push({ path, bytes: readFileSync(absolute, "utf8") });
|
|
453
|
-
}
|
|
454
|
-
} catch (cause) {
|
|
455
|
-
if (isErrno(cause, "ENOENT") || isErrno(cause, "ENOTDIR")) continue;
|
|
456
|
-
throw cause;
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return entries;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/** RFC-8259-compatible deterministic JSON with lexicographically sorted keys. */
|
|
463
|
-
export function canonicalJson(value: unknown): string {
|
|
464
|
-
return JSON.stringify(canonicalValue(value));
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export function digest(value: string | Uint8Array): string {
|
|
468
|
-
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
function digestJsonLines(values: readonly unknown[], boundedMemory = false): string {
|
|
472
|
-
const hash = createHash("sha256");
|
|
473
|
-
values.forEach((value, index) => {
|
|
474
|
-
if (index > 0) hash.update("\n");
|
|
475
|
-
hash.update(JSON.stringify(value));
|
|
476
|
-
if (boundedMemory && index > 0 && index % BOUNDED_MEMORY_GC_RECORD_INTERVAL === 0) Bun.gc(true);
|
|
477
|
-
});
|
|
478
|
-
return `sha256:${hash.digest("hex")}`;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export function digestHex(value: string): string {
|
|
482
|
-
const match = /^sha256:([0-9a-f]{64})$/.exec(value);
|
|
483
|
-
if (!match) {
|
|
484
|
-
throw new LoreError("validation", `expected a sha256 digest, received ${JSON.stringify(value)}`);
|
|
485
|
-
}
|
|
486
|
-
return match[1] as string;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function validateProjection(projection: Projection): {
|
|
490
|
-
manifest: ProjectionManifestRecord;
|
|
491
|
-
trailer: ProjectionTrailerRecord;
|
|
492
|
-
concepts: ProjectionConceptRecord[];
|
|
493
|
-
tasks: ProjectionTaskRecord[];
|
|
494
|
-
authoredEdges: ProjectionEdgeRecord[];
|
|
495
|
-
} {
|
|
496
|
-
if (projection.records.length < 2) invalidProjection("stream must contain a manifest and trailer");
|
|
497
|
-
const first = projection.records[0];
|
|
498
|
-
const last = projection.records.at(-1);
|
|
499
|
-
if (!isManifest(first)) invalidProjection("first record must be a valid manifest");
|
|
500
|
-
if (!isTrailer(last)) invalidProjection("last record must be a valid trailer");
|
|
501
|
-
if (first.schemaVersion !== PROJECTION_SCHEMA_VERSION) {
|
|
502
|
-
invalidProjection(`unsupported export schema ${JSON.stringify(first.schemaVersion)}`);
|
|
503
|
-
}
|
|
504
|
-
if (first.normalizationVersion !== PROJECTION_NORMALIZATION_VERSION) {
|
|
505
|
-
invalidProjection(`unsupported normalization version ${JSON.stringify(first.normalizationVersion)}`);
|
|
506
|
-
}
|
|
507
|
-
const semanticRecords = projection.records.slice(0, -1);
|
|
508
|
-
if (last.recordCount !== semanticRecords.length) {
|
|
509
|
-
invalidProjection("trailer recordCount does not match the stream");
|
|
510
|
-
}
|
|
511
|
-
if (last.streamHash !== projectionStreamHash(semanticRecords, projection.jsonl === "")) {
|
|
512
|
-
invalidProjection("trailer streamHash does not match the validated records");
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const concepts: ProjectionConceptRecord[] = [];
|
|
516
|
-
const tasks: ProjectionTaskRecord[] = [];
|
|
517
|
-
const authoredEdges: ProjectionEdgeRecord[] = [];
|
|
518
|
-
const keys = new Set<string>();
|
|
519
|
-
for (const record of projection.records.slice(1, -1)) {
|
|
520
|
-
if (isConcept(record)) concepts.push(record);
|
|
521
|
-
else if (isTask(record)) tasks.push(record);
|
|
522
|
-
else if (isEdge(record)) authoredEdges.push(record);
|
|
523
|
-
else invalidProjection(`unsupported or malformed ${JSON.stringify(record.record)} record`);
|
|
524
|
-
const key = (record as { key: string }).key;
|
|
525
|
-
if (keys.has(key)) invalidProjection(`duplicate record key ${JSON.stringify(key)}`);
|
|
526
|
-
keys.add(key);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const conceptKeys = new Set(concepts.map((record) => record.key));
|
|
530
|
-
const taskKeys = new Set(tasks.map((record) => record.key));
|
|
531
|
-
for (const edge of authoredEdges) {
|
|
532
|
-
const sourceKeys = edge.workspaceFromKind === "task" ? taskKeys : conceptKeys;
|
|
533
|
-
const targetKeys =
|
|
534
|
-
edge.workspaceToKind === "task" || (edge.workspaceToKind === undefined && edge.kind === "task")
|
|
535
|
-
? taskKeys
|
|
536
|
-
: conceptKeys;
|
|
537
|
-
if (!sourceKeys.has(edge.from))
|
|
538
|
-
invalidProjection(`edge ${edge.key} has no ${edge.workspaceFromKind ?? "concept"} source`);
|
|
539
|
-
const targetExists = edge.to !== null && targetKeys.has(edge.to);
|
|
540
|
-
if (edge.dangling !== (edge.to === null) || (edge.to !== null && !targetExists)) {
|
|
541
|
-
invalidProjection(`edge ${edge.key} has inconsistent target/dangling metadata`);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return { manifest: first, trailer: last, concepts, tasks, authoredEdges };
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
function isManifest(record: ProjectionRecord | undefined): record is ProjectionManifestRecord {
|
|
548
|
-
if (record?.record !== "manifest" || !isObject(record.bundle) || !isObject(record.exporter)) return false;
|
|
549
|
-
const bundle = record.bundle;
|
|
550
|
-
const exporter = record.exporter;
|
|
551
|
-
return (
|
|
552
|
-
typeof record.schemaVersion === "string" &&
|
|
553
|
-
typeof record.normalizationVersion === "string" &&
|
|
554
|
-
(record.generatedAt === null || typeof record.generatedAt === "string") &&
|
|
555
|
-
typeof bundle.id === "string" &&
|
|
556
|
-
typeof bundle.okfVersion === "string" &&
|
|
557
|
-
typeof bundle.docsRoot === "string" &&
|
|
558
|
-
(bundle.gitCommit === null || typeof bundle.gitCommit === "string") &&
|
|
559
|
-
typeof exporter.name === "string" &&
|
|
560
|
-
typeof exporter.version === "string"
|
|
561
|
-
);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function isTrailer(record: ProjectionRecord | undefined): record is ProjectionTrailerRecord {
|
|
565
|
-
return (
|
|
566
|
-
record?.record === "trailer" &&
|
|
567
|
-
Number.isSafeInteger(record.recordCount) &&
|
|
568
|
-
Number(record.recordCount) >= 1 &&
|
|
569
|
-
typeof record.streamHash === "string"
|
|
570
|
-
);
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function isConcept(record: ProjectionRecord): record is ProjectionConceptRecord {
|
|
574
|
-
return (
|
|
575
|
-
record.record === "concept" &&
|
|
576
|
-
hasCommonKey(record) &&
|
|
577
|
-
typeof record.id === "string" &&
|
|
578
|
-
typeof record.path === "string" &&
|
|
579
|
-
typeof record.type === "string" &&
|
|
580
|
-
isObject(record.frontmatter) &&
|
|
581
|
-
typeof record.body === "string" &&
|
|
582
|
-
typeof record.contentHash === "string" &&
|
|
583
|
-
Number.isSafeInteger(record.tokenEstimate)
|
|
584
|
-
);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function isTask(record: ProjectionRecord): record is ProjectionTaskRecord {
|
|
588
|
-
return (
|
|
589
|
-
record.record === "task" &&
|
|
590
|
-
hasCommonKey(record) &&
|
|
591
|
-
typeof record.id === "string" &&
|
|
592
|
-
typeof record.title === "string" &&
|
|
593
|
-
typeof record.status === "string" &&
|
|
594
|
-
isStringArray(record.labels) &&
|
|
595
|
-
nullableString(record.priority) &&
|
|
596
|
-
(record.ordinal === null || Number.isSafeInteger(record.ordinal)) &&
|
|
597
|
-
isStringArray(record.assignees) &&
|
|
598
|
-
nullableString(record.milestone) &&
|
|
599
|
-
nullableString(record.parentTaskId) &&
|
|
600
|
-
typeof record.sourceAdapterVersion === "string"
|
|
601
|
-
);
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function isEdge(record: ProjectionRecord): record is ProjectionEdgeRecord {
|
|
605
|
-
return (
|
|
606
|
-
record.record === "edge" &&
|
|
607
|
-
hasCommonKey(record) &&
|
|
608
|
-
typeof record.from === "string" &&
|
|
609
|
-
nullableString(record.to) &&
|
|
610
|
-
typeof record.kind === "string" &&
|
|
611
|
-
typeof record.target === "string" &&
|
|
612
|
-
Number.isSafeInteger(record.ordinal) &&
|
|
613
|
-
typeof record.dangling === "boolean" &&
|
|
614
|
-
(record.workspaceFromKind === undefined ||
|
|
615
|
-
record.workspaceFromKind === "concept" ||
|
|
616
|
-
record.workspaceFromKind === "task") &&
|
|
617
|
-
(record.workspaceToKind === undefined ||
|
|
618
|
-
record.workspaceToKind === "concept" ||
|
|
619
|
-
record.workspaceToKind === "task") &&
|
|
620
|
-
(record.workspaceLinkKind === undefined || typeof record.workspaceLinkKind === "string")
|
|
621
|
-
);
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
function hasCommonKey(record: ProjectionRecord): record is ProjectionRecord & { key: string } {
|
|
625
|
-
return typeof record.key === "string" && record.key !== "";
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
function canonicalValue(value: unknown): unknown {
|
|
629
|
-
if (Array.isArray(value)) return value.map((entry) => canonicalValue(entry));
|
|
630
|
-
if (!isObject(value)) return value;
|
|
631
|
-
const result: Record<string, unknown> = {};
|
|
632
|
-
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
633
|
-
const entry = value[key];
|
|
634
|
-
if (entry !== undefined && typeof entry !== "function" && typeof entry !== "symbol") {
|
|
635
|
-
result[key] = canonicalValue(entry);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
return result;
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function invalidProjection(message: string): never {
|
|
642
|
-
throw new LoreError(
|
|
643
|
-
"validation",
|
|
644
|
-
`Ladybug projection source is invalid: ${message}`,
|
|
645
|
-
`rebuild the source through lore export --schema-version ${PROJECTION_SCHEMA_VERSION}`,
|
|
646
|
-
);
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function nullableString(value: unknown): value is string | null {
|
|
650
|
-
return value === null || typeof value === "string";
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function isStringArray(value: unknown): value is string[] {
|
|
654
|
-
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
function isObject(value: unknown): value is Record<string, unknown> {
|
|
658
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function compareCodeUnits(a: string, b: string): number {
|
|
662
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function isErrno(value: unknown, code: string): boolean {
|
|
666
|
-
return isObject(value) && value.code === code;
|
|
667
|
-
}
|