@opum-ai/lore 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/README.md +33 -22
  2. package/bin/lore.cjs +35 -7
  3. package/package.json +17 -17
  4. package/src/adapters/backlog.ts +0 -1084
  5. package/src/adapters/git.ts +0 -221
  6. package/src/cli.ts +0 -667
  7. package/src/commands/agent.ts +0 -301
  8. package/src/commands/agents.ts +0 -302
  9. package/src/commands/args.ts +0 -209
  10. package/src/commands/changed.ts +0 -70
  11. package/src/commands/check.ts +0 -1031
  12. package/src/commands/codex-bridge.ts +0 -49
  13. package/src/commands/concurrency.ts +0 -48
  14. package/src/commands/context.ts +0 -292
  15. package/src/commands/discover.ts +0 -89
  16. package/src/commands/explorer.ts +0 -253
  17. package/src/commands/export.ts +0 -93
  18. package/src/commands/fswrite.ts +0 -928
  19. package/src/commands/graph.ts +0 -291
  20. package/src/commands/help.ts +0 -151
  21. package/src/commands/impact.ts +0 -59
  22. package/src/commands/init.ts +0 -583
  23. package/src/commands/instructions.ts +0 -91
  24. package/src/commands/link.ts +0 -929
  25. package/src/commands/new.ts +0 -476
  26. package/src/commands/orphans.ts +0 -457
  27. package/src/commands/path.ts +0 -67
  28. package/src/commands/provenance.ts +0 -68
  29. package/src/commands/query.ts +0 -312
  30. package/src/commands/reconcile-shared.ts +0 -280
  31. package/src/commands/rename.ts +0 -585
  32. package/src/commands/replace.ts +0 -320
  33. package/src/commands/scaffold.ts +0 -346
  34. package/src/commands/schema.ts +0 -293
  35. package/src/commands/snapshot.ts +0 -130
  36. package/src/commands/supersede.ts +0 -400
  37. package/src/commands/sync.ts +0 -371
  38. package/src/commands/tasks.ts +0 -271
  39. package/src/commands/traversal.ts +0 -151
  40. package/src/commands/validate.ts +0 -226
  41. package/src/config.ts +0 -598
  42. package/src/core/agent-bridge.ts +0 -287
  43. package/src/core/agent-context.ts +0 -498
  44. package/src/core/agent-profile.ts +0 -447
  45. package/src/core/bundle.ts +0 -893
  46. package/src/core/check.ts +0 -853
  47. package/src/core/codex-bridge.ts +0 -100
  48. package/src/core/concept.ts +0 -597
  49. package/src/core/consumer-scaffold.ts +0 -433
  50. package/src/core/context.ts +0 -271
  51. package/src/core/explorer-contract.ts +0 -441
  52. package/src/core/explorer-qualification.ts +0 -58
  53. package/src/core/explorer.ts +0 -518
  54. package/src/core/finding.ts +0 -31
  55. package/src/core/graph.ts +0 -201
  56. package/src/core/indexes.ts +0 -436
  57. package/src/core/instructions.ts +0 -209
  58. package/src/core/ladybug-driver.ts +0 -1795
  59. package/src/core/ladybug-lifecycle.ts +0 -1178
  60. package/src/core/ladybug-native.ts +0 -95
  61. package/src/core/ladybug-source.ts +0 -667
  62. package/src/core/links.ts +0 -681
  63. package/src/core/log.ts +0 -253
  64. package/src/core/managed-block.ts +0 -540
  65. package/src/core/manifest.ts +0 -718
  66. package/src/core/order.ts +0 -13
  67. package/src/core/profile.ts +0 -1007
  68. package/src/core/projection.ts +0 -195
  69. package/src/core/query.ts +0 -542
  70. package/src/core/reconcile.ts +0 -236
  71. package/src/core/replace.ts +0 -419
  72. package/src/core/retrieval.ts +0 -213
  73. package/src/core/rewrite.ts +0 -940
  74. package/src/core/scaffold.ts +0 -255
  75. package/src/core/schema.ts +0 -366
  76. package/src/core/snapshot-runtime.ts +0 -52
  77. package/src/core/snapshot-store.ts +0 -287
  78. package/src/core/snapshot.ts +0 -711
  79. package/src/core/template.ts +0 -429
  80. package/src/core/traversal.ts +0 -487
  81. package/src/core/validate.ts +0 -517
  82. package/src/core/workspace-contract.ts +0 -473
  83. package/src/core/workspace-projection.ts +0 -365
  84. package/src/core/workspace-retrieval.ts +0 -196
  85. package/src/core/workspace-source.ts +0 -174
  86. package/src/errors.ts +0 -697
  87. package/src/meta.ts +0 -7
  88. package/src/output.ts +0 -589
  89. package/src/scripts/upstream-backlog-watch.ts +0 -288
  90. package/src/state.ts +0 -390
@@ -1,711 +0,0 @@
1
- /** Storage-neutral retained projection snapshots, bounded comparison, and provenance. */
2
-
3
- import { z } from "zod";
4
- import { LoreError } from "../errors";
5
- import type {
6
- LadybugProjectionSource,
7
- ProjectionConceptRecord,
8
- ProjectionEdgeRecord,
9
- ProjectionTaskRecord,
10
- } from "./ladybug-source";
11
- import { canonicalJson } from "./ladybug-source";
12
- import { compareCodeUnits } from "./order";
13
- import type { WorkspaceRecordProvenance, WorkspaceResultRepository } from "./workspace-contract";
14
- import type { WorkspaceProjection } from "./workspace-projection";
15
-
16
- export const RETAINED_SNAPSHOT_SCHEMA_VERSION = "lore-retained-snapshot/1" as const;
17
- export const CHANGED_RESULT_SCHEMA_VERSION = "lore-changed-result/1" as const;
18
- export const PROVENANCE_RESULT_SCHEMA_VERSION = "lore-provenance-result/1" as const;
19
- export const SNAPSHOT_RETENTION_LIMIT = 16;
20
- export const CHANGED_DEFAULT_LIMIT = 100;
21
- export const CHANGED_MAX_LIMIT = 1_000;
22
- export const CHANGED_FACT_SCAN_LIMIT = 1_000_000;
23
-
24
- export type RetainedFactKind = "concept" | "task" | "edge";
25
- export type RetainedScopeKind = "repository" | "workspace";
26
-
27
- export interface RetainedRepositoryProvenance {
28
- readonly memberId: string | null;
29
- readonly repositoryKey: string;
30
- readonly repositoryScopeKey: string;
31
- readonly bundleKey: string;
32
- readonly bundleId: string;
33
- readonly commitKey: string | null;
34
- readonly gitCommit: string | null;
35
- readonly exportKey: string;
36
- readonly exportDigest: string;
37
- }
38
-
39
- export interface RetainedRecordProvenance extends RetainedRepositoryProvenance {
40
- readonly recordKey: string;
41
- readonly sourceRecordKey: string;
42
- readonly sourceKey: string | null;
43
- readonly sourcePath: string | null;
44
- }
45
-
46
- export interface RetainedFact {
47
- readonly kind: RetainedFactKind;
48
- /** Public concept/task id, or exact edge record key. */
49
- readonly id: string;
50
- readonly recordKey: string;
51
- readonly provenance: RetainedRecordProvenance;
52
- /** Canonical authored/comparison fields. Snapshot provenance is deliberately separate. */
53
- readonly value: Readonly<Record<string, unknown>>;
54
- }
55
-
56
- export interface RetainedSnapshotDescriptor {
57
- readonly schemaVersion: typeof RETAINED_SNAPSHOT_SCHEMA_VERSION;
58
- readonly scopeKind: RetainedScopeKind;
59
- readonly scopeKey: string;
60
- readonly workspaceId: string | null;
61
- readonly snapshotKey: string;
62
- readonly repositories: readonly RetainedRepositoryProvenance[];
63
- readonly counts: Readonly<Record<RetainedFactKind, number>>;
64
- }
65
-
66
- export interface RetainedSnapshot extends RetainedSnapshotDescriptor {
67
- readonly facts: readonly RetainedFact[];
68
- }
69
-
70
- export interface SnapshotChange {
71
- readonly change: "added" | "removed" | "changed";
72
- readonly recordKind: RetainedFactKind;
73
- readonly recordKey: string;
74
- readonly id: string;
75
- readonly relationshipDelta: "added" | "removed" | "changed" | null;
76
- readonly fieldsChanged: readonly string[];
77
- readonly from: RetainedFact | null;
78
- readonly to: RetainedFact | null;
79
- }
80
-
81
- export interface ChangedResult {
82
- readonly schemaVersion: typeof CHANGED_RESULT_SCHEMA_VERSION;
83
- readonly from: RetainedSnapshotDescriptor;
84
- readonly to: RetainedSnapshotDescriptor;
85
- readonly filters: {
86
- readonly repositories: readonly string[];
87
- readonly kinds: readonly RetainedFactKind[];
88
- };
89
- readonly limits: { readonly result: number; readonly factScan: number };
90
- readonly changes: readonly SnapshotChange[];
91
- readonly shown: number;
92
- readonly totalChanges: number;
93
- readonly scanned: number;
94
- readonly truncated: boolean;
95
- readonly complete: boolean;
96
- }
97
-
98
- export interface ProvenanceResult {
99
- readonly schemaVersion: typeof PROVENANCE_RESULT_SCHEMA_VERSION;
100
- readonly snapshot: RetainedSnapshotDescriptor;
101
- readonly fact: RetainedFact;
102
- }
103
-
104
- const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
105
- const commitSchema = z
106
- .string()
107
- .regex(/^[0-9a-f]{40}$/u)
108
- .nullable();
109
- const repositorySchema = z
110
- .object({
111
- memberId: z.string().min(1).nullable(),
112
- repositoryKey: digestSchema,
113
- repositoryScopeKey: digestSchema,
114
- bundleKey: digestSchema,
115
- bundleId: digestSchema,
116
- commitKey: digestSchema.nullable(),
117
- gitCommit: commitSchema,
118
- exportKey: digestSchema,
119
- exportDigest: digestSchema,
120
- })
121
- .strict();
122
- const repositoryPathSchema = z
123
- .string()
124
- .min(1)
125
- .refine(
126
- (path) =>
127
- !path.startsWith("/") &&
128
- !path.includes("\\") &&
129
- path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."),
130
- { message: "source paths must be repository-relative POSIX paths" },
131
- );
132
- const provenanceSchema = repositorySchema
133
- .extend({
134
- recordKey: digestSchema,
135
- sourceRecordKey: z.string().min(1),
136
- sourceKey: digestSchema.nullable(),
137
- sourcePath: repositoryPathSchema.nullable(),
138
- })
139
- .strict();
140
- const conceptValueSchema = z
141
- .object({
142
- id: z.string().min(1),
143
- path: repositoryPathSchema,
144
- type: z.string().min(1),
145
- frontmatter: z.record(z.string(), z.unknown()).refine((value) => !hasSensitiveKey(value), {
146
- message: "retained frontmatter contains a sensitive key",
147
- }),
148
- contentHash: digestSchema,
149
- tokenEstimate: z.number().int().nonnegative(),
150
- })
151
- .strict();
152
- const taskValueSchema = z
153
- .object({
154
- id: z.string().min(1),
155
- title: z.string().min(1),
156
- status: z.string().min(1),
157
- labels: z.array(z.string()),
158
- priority: z.string().nullable(),
159
- ordinal: z.number().int().nonnegative().nullable(),
160
- assignees: z.array(z.string()),
161
- milestone: z.string().nullable(),
162
- parentTaskId: z.string().nullable(),
163
- sourceAdapterVersion: z.string().min(1),
164
- })
165
- .strict();
166
- const edgeValueSchema = z
167
- .object({
168
- from: digestSchema,
169
- to: digestSchema.nullable(),
170
- kind: z.string().min(1),
171
- target: z.string().min(1),
172
- ordinal: z.number().int().nonnegative(),
173
- dangling: z.boolean(),
174
- workspaceFromKind: z.enum(["concept", "task"]).nullable(),
175
- workspaceToKind: z.enum(["concept", "task"]).nullable(),
176
- workspaceLinkKind: z.string().min(1).nullable(),
177
- })
178
- .strict();
179
- const factBaseSchema = z
180
- .object({ id: z.string().min(1), recordKey: digestSchema, provenance: provenanceSchema })
181
- .strict();
182
- const factSchema = z.discriminatedUnion("kind", [
183
- factBaseSchema.extend({ kind: z.literal("concept"), value: conceptValueSchema }).strict(),
184
- factBaseSchema.extend({ kind: z.literal("task"), value: taskValueSchema }).strict(),
185
- factBaseSchema.extend({ kind: z.literal("edge"), value: edgeValueSchema }).strict(),
186
- ]);
187
- const snapshotSchema = z
188
- .object({
189
- schemaVersion: z.literal(RETAINED_SNAPSHOT_SCHEMA_VERSION),
190
- scopeKind: z.enum(["repository", "workspace"]),
191
- scopeKey: digestSchema,
192
- workspaceId: z.string().min(1).nullable(),
193
- snapshotKey: digestSchema,
194
- repositories: z.array(repositorySchema).min(1).max(256),
195
- counts: z
196
- .object({
197
- concept: z.number().int().nonnegative(),
198
- task: z.number().int().nonnegative(),
199
- edge: z.number().int().nonnegative(),
200
- })
201
- .strict(),
202
- facts: z.array(factSchema).max(CHANGED_FACT_SCAN_LIMIT),
203
- })
204
- .strict();
205
-
206
- /** Convert the current validated repository projection into immutable retained facts. */
207
- export function buildRepositoryRetainedSnapshot(source: LadybugProjectionSource): RetainedSnapshot {
208
- const repository: RetainedRepositoryProvenance = {
209
- memberId: null,
210
- repositoryKey: source.repositoryScopeKey,
211
- repositoryScopeKey: source.repositoryScopeKey,
212
- bundleKey: source.manifest.bundle.id,
213
- bundleId: source.manifest.bundle.id,
214
- commitKey: source.commitKey,
215
- gitCommit: source.manifest.bundle.gitCommit,
216
- exportKey: source.exportDigest,
217
- exportDigest: source.exportDigest,
218
- };
219
- const conceptByKey = new Map(source.concepts.map((record) => [record.key, record]));
220
- return finalizeSnapshot({
221
- scopeKind: "repository",
222
- scopeKey: source.repositoryScopeKey,
223
- workspaceId: null,
224
- snapshotKey: source.snapshotKey,
225
- repositories: [repository],
226
- facts: [
227
- ...source.concepts.map((record) => repositoryConcept(record, repository)),
228
- ...source.tasks.map((record) => repositoryTask(record, repository)),
229
- ...source.authoredEdges.map((record) => repositoryEdge(record, repository, conceptByKey.get(record.from))),
230
- ],
231
- });
232
- }
233
-
234
- /** Convert the current complete workspace projection while preserving member provenance. */
235
- export function buildWorkspaceRetainedSnapshot(projection: WorkspaceProjection): RetainedSnapshot {
236
- const repositoryByMember = new Map(
237
- projection.scope.repositories.map((repository) => [repository.memberId, retainedWorkspaceRepository(repository)]),
238
- );
239
- const provenanceByRecord = new Map<string, WorkspaceRecordProvenance>();
240
- for (const provenance of projection.provenanceById.values()) provenanceByRecord.set(provenance.recordKey, provenance);
241
- for (const provenance of projection.taskProvenanceById.values())
242
- provenanceByRecord.set(provenance.recordKey, provenance);
243
- const facts: RetainedFact[] = [];
244
- for (const record of projection.ladybugSource.concepts) {
245
- const provenance = requiredWorkspaceProvenance(provenanceByRecord, record.key);
246
- facts.push(workspaceConcept(record, provenance, requiredRepository(repositoryByMember, provenance.memberId)));
247
- }
248
- for (const record of projection.ladybugSource.tasks) {
249
- const provenance = requiredWorkspaceProvenance(provenanceByRecord, record.key);
250
- facts.push(workspaceTask(record, provenance, requiredRepository(repositoryByMember, provenance.memberId)));
251
- }
252
- for (const record of projection.ladybugSource.authoredEdges) {
253
- const source = requiredWorkspaceProvenance(provenanceByRecord, record.from);
254
- facts.push(workspaceEdge(record, source, requiredRepository(repositoryByMember, source.memberId)));
255
- }
256
- return finalizeSnapshot({
257
- scopeKind: "workspace",
258
- scopeKey: projection.scope.workspaceKey,
259
- workspaceId: projection.scope.workspaceId,
260
- snapshotKey: projection.scope.snapshotKey,
261
- repositories: projection.scope.repositories.map(retainedWorkspaceRepository),
262
- facts,
263
- });
264
- }
265
-
266
- export function parseRetainedSnapshot(value: unknown): RetainedSnapshot {
267
- let parsed: RetainedSnapshot;
268
- try {
269
- parsed = snapshotSchema.parse(value) as RetainedSnapshot;
270
- } catch (cause) {
271
- throw new LoreError(
272
- "validation",
273
- "retained snapshot is malformed or unsupported",
274
- cause instanceof Error ? cause.message : undefined,
275
- );
276
- }
277
- const normalized = finalizeSnapshot(parsed);
278
- assertSnapshotInvariants(normalized);
279
- if (canonicalJson(normalized) !== canonicalJson(parsed)) {
280
- throw new LoreError(
281
- "validation",
282
- "retained snapshot is not canonical",
283
- "re-retain the source with this Lore version",
284
- );
285
- }
286
- return parsed;
287
- }
288
-
289
- export function serializeRetainedSnapshot(value: unknown): string {
290
- return `${canonicalJson(parseRetainedSnapshot(value))}\n`;
291
- }
292
-
293
- export function snapshotDescriptor(snapshot: RetainedSnapshot): RetainedSnapshotDescriptor {
294
- const { facts: _facts, ...descriptor } = snapshot;
295
- return descriptor;
296
- }
297
-
298
- export function compareRetainedSnapshots(
299
- from: RetainedSnapshot,
300
- to: RetainedSnapshot,
301
- options: {
302
- readonly limit?: number;
303
- readonly repositories?: readonly string[];
304
- readonly kinds?: readonly RetainedFactKind[];
305
- } = {},
306
- ): ChangedResult {
307
- if (from.scopeKind !== to.scopeKind || from.scopeKey !== to.scopeKey) {
308
- throw new LoreError(
309
- "conflict",
310
- "retained snapshots belong to different scopes",
311
- "compare snapshots retained for the same repository or workspace",
312
- );
313
- }
314
- const limit = options.limit ?? CHANGED_DEFAULT_LIMIT;
315
- if (!Number.isInteger(limit) || limit < 1 || limit > CHANGED_MAX_LIMIT) {
316
- throw new LoreError("usage", `changed limit must be between 1 and ${CHANGED_MAX_LIMIT}`);
317
- }
318
- const repositories = uniqueSorted(options.repositories ?? []);
319
- const kinds = uniqueKinds(options.kinds ?? ["concept", "task", "edge"]);
320
- const allowedKinds = new Set(kinds);
321
- const allowedRepositories = new Set(repositories);
322
- const selected = (fact: RetainedFact) =>
323
- allowedKinds.has(fact.kind) &&
324
- (allowedRepositories.size === 0 ||
325
- (fact.provenance.memberId !== null && allowedRepositories.has(fact.provenance.memberId)));
326
- const left = from.facts.filter(selected);
327
- const right = to.facts.filter(selected);
328
- const changes: SnapshotChange[] = [];
329
- let totalChanges = 0;
330
- let scanned = 0;
331
- let i = 0;
332
- let j = 0;
333
- while ((i < left.length || j < right.length) && scanned < CHANGED_FACT_SCAN_LIMIT) {
334
- const a = left[i];
335
- const b = right[j];
336
- scanned += 1;
337
- let change: SnapshotChange | null = null;
338
- if (a === undefined) {
339
- change = changeRow("added", null, b as RetainedFact);
340
- j += 1;
341
- } else if (b === undefined) {
342
- change = changeRow("removed", a, null);
343
- i += 1;
344
- } else {
345
- const order = compareFact(a, b);
346
- if (order < 0) {
347
- change = changeRow("removed", a, null);
348
- i += 1;
349
- } else if (order > 0) {
350
- change = changeRow("added", null, b);
351
- j += 1;
352
- } else {
353
- if (canonicalJson(a.value) !== canonicalJson(b.value)) change = changeRow("changed", a, b);
354
- i += 1;
355
- j += 1;
356
- }
357
- }
358
- if (change !== null) {
359
- totalChanges += 1;
360
- if (changes.length < limit) changes.push(change);
361
- }
362
- }
363
- const scanBoundReached = i < left.length || j < right.length;
364
- return {
365
- schemaVersion: CHANGED_RESULT_SCHEMA_VERSION,
366
- from: snapshotDescriptor(from),
367
- to: snapshotDescriptor(to),
368
- filters: { repositories, kinds },
369
- limits: { result: limit, factScan: CHANGED_FACT_SCAN_LIMIT },
370
- changes,
371
- shown: changes.length,
372
- totalChanges,
373
- scanned,
374
- truncated: scanBoundReached || totalChanges > changes.length,
375
- complete: !scanBoundReached,
376
- };
377
- }
378
-
379
- export function findRetainedProvenance(
380
- snapshot: RetainedSnapshot,
381
- input: { readonly id: string; readonly kind: RetainedFactKind; readonly repositories?: readonly string[] },
382
- ): ProvenanceResult {
383
- const repositories = new Set(uniqueSorted(input.repositories ?? []));
384
- const exact = snapshot.facts.filter(
385
- (fact) =>
386
- fact.kind === input.kind &&
387
- (repositories.size === 0 || (fact.provenance.memberId !== null && repositories.has(fact.provenance.memberId))) &&
388
- (fact.id === input.id || fact.recordKey === input.id || fact.provenance.sourceRecordKey === input.id),
389
- );
390
- if (exact.length === 0)
391
- throw new LoreError("not_found", `${input.kind} ${JSON.stringify(input.id)} is not in the retained snapshot`);
392
- if (exact.length > 1) {
393
- throw new LoreError(
394
- "conflict",
395
- `${input.kind} ${JSON.stringify(input.id)} is ambiguous in the retained snapshot`,
396
- "use the exact record key",
397
- );
398
- }
399
- return {
400
- schemaVersion: PROVENANCE_RESULT_SCHEMA_VERSION,
401
- snapshot: snapshotDescriptor(snapshot),
402
- fact: exact[0] as RetainedFact,
403
- };
404
- }
405
-
406
- function repositoryConcept(record: ProjectionConceptRecord, repository: RetainedRepositoryProvenance): RetainedFact {
407
- return fact(
408
- "concept",
409
- record.id,
410
- record.key,
411
- provenance(repository, record.key, record.key, record.path),
412
- conceptValue(record),
413
- );
414
- }
415
-
416
- function repositoryTask(record: ProjectionTaskRecord, repository: RetainedRepositoryProvenance): RetainedFact {
417
- return fact("task", record.id, record.key, provenance(repository, record.key, record.key, null), taskValue(record));
418
- }
419
-
420
- function repositoryEdge(
421
- record: ProjectionEdgeRecord,
422
- repository: RetainedRepositoryProvenance,
423
- source?: ProjectionConceptRecord,
424
- ): RetainedFact {
425
- return fact(
426
- "edge",
427
- record.key,
428
- record.key,
429
- provenance(repository, record.key, record.key, source?.path ?? null),
430
- edgeValue(record),
431
- );
432
- }
433
-
434
- function workspaceConcept(
435
- record: ProjectionConceptRecord,
436
- source: WorkspaceRecordProvenance,
437
- repository: RetainedRepositoryProvenance,
438
- ): RetainedFact {
439
- return fact("concept", record.id, record.key, workspaceProvenance(repository, source), conceptValue(record));
440
- }
441
-
442
- function workspaceTask(
443
- record: ProjectionTaskRecord,
444
- source: WorkspaceRecordProvenance,
445
- repository: RetainedRepositoryProvenance,
446
- ): RetainedFact {
447
- return fact("task", record.id, record.key, workspaceProvenance(repository, source), taskValue(record));
448
- }
449
-
450
- function workspaceEdge(
451
- record: ProjectionEdgeRecord,
452
- source: WorkspaceRecordProvenance,
453
- repository: RetainedRepositoryProvenance,
454
- ): RetainedFact {
455
- return fact(
456
- "edge",
457
- record.key,
458
- record.key,
459
- provenance(repository, record.key, record.workspaceSourceRecordKey ?? record.key, source.sourcePath),
460
- edgeValue(record),
461
- );
462
- }
463
-
464
- function conceptValue(record: ProjectionConceptRecord): Record<string, unknown> {
465
- return {
466
- id: record.id,
467
- path: record.path,
468
- type: record.type,
469
- frontmatter: privacyFiltered(record.frontmatter),
470
- contentHash: record.contentHash,
471
- tokenEstimate: record.tokenEstimate,
472
- };
473
- }
474
-
475
- function taskValue(record: ProjectionTaskRecord): Record<string, unknown> {
476
- return {
477
- id: record.id,
478
- title: record.title,
479
- status: record.status,
480
- labels: record.labels,
481
- priority: record.priority,
482
- ordinal: record.ordinal,
483
- assignees: record.assignees,
484
- milestone: record.milestone,
485
- parentTaskId: record.parentTaskId,
486
- sourceAdapterVersion: record.sourceAdapterVersion,
487
- };
488
- }
489
-
490
- function edgeValue(record: ProjectionEdgeRecord): Record<string, unknown> {
491
- return {
492
- from: record.from,
493
- to: record.to,
494
- kind: record.kind,
495
- target: record.target,
496
- ordinal: record.ordinal,
497
- dangling: record.dangling,
498
- workspaceFromKind: record.workspaceFromKind ?? null,
499
- workspaceToKind: record.workspaceToKind ?? null,
500
- workspaceLinkKind: record.workspaceLinkKind ?? null,
501
- };
502
- }
503
-
504
- function fact(
505
- kind: RetainedFactKind,
506
- id: string,
507
- recordKey: string,
508
- recordProvenance: RetainedRecordProvenance,
509
- value: Record<string, unknown>,
510
- ): RetainedFact {
511
- return { kind, id, recordKey, provenance: recordProvenance, value };
512
- }
513
-
514
- function provenance(
515
- repository: RetainedRepositoryProvenance,
516
- recordKey: string,
517
- sourceRecordKey: string,
518
- sourcePath: string | null,
519
- ): RetainedRecordProvenance {
520
- return { ...repository, recordKey, sourceRecordKey, sourceKey: null, sourcePath };
521
- }
522
-
523
- function workspaceProvenance(
524
- repository: RetainedRepositoryProvenance,
525
- source: WorkspaceRecordProvenance,
526
- ): RetainedRecordProvenance {
527
- return {
528
- ...repository,
529
- recordKey: source.recordKey,
530
- sourceRecordKey: source.sourceRecordKey,
531
- sourceKey: source.sourceKey,
532
- sourcePath: source.sourcePath,
533
- };
534
- }
535
-
536
- function retainedWorkspaceRepository(repository: WorkspaceResultRepository): RetainedRepositoryProvenance {
537
- return { ...repository };
538
- }
539
-
540
- function finalizeSnapshot(
541
- input: Omit<RetainedSnapshot, "schemaVersion" | "counts"> | RetainedSnapshot,
542
- ): RetainedSnapshot {
543
- const facts = [...input.facts].sort(compareFact);
544
- const repositories = [...input.repositories].sort((a, b) => compareCodeUnits(a.memberId ?? "", b.memberId ?? ""));
545
- const counts = {
546
- concept: facts.filter((fact) => fact.kind === "concept").length,
547
- task: facts.filter((fact) => fact.kind === "task").length,
548
- edge: facts.filter((fact) => fact.kind === "edge").length,
549
- };
550
- return { ...input, schemaVersion: RETAINED_SNAPSHOT_SCHEMA_VERSION, repositories, counts, facts };
551
- }
552
-
553
- function assertSnapshotInvariants(snapshot: RetainedSnapshot): void {
554
- if (snapshot.scopeKind === "repository") {
555
- if (
556
- snapshot.workspaceId !== null ||
557
- snapshot.repositories.length !== 1 ||
558
- snapshot.repositories[0]?.memberId !== null
559
- ) {
560
- throw new LoreError("validation", "repository retained snapshot has inconsistent scope evidence");
561
- }
562
- if (snapshot.repositories[0]?.repositoryScopeKey !== snapshot.scopeKey) {
563
- throw new LoreError("validation", "repository retained snapshot scope key disagrees with repository provenance");
564
- }
565
- } else if (
566
- snapshot.workspaceId === null ||
567
- snapshot.repositories.some((repository) => repository.memberId === null)
568
- ) {
569
- throw new LoreError("validation", "workspace retained snapshot has incomplete member evidence");
570
- }
571
-
572
- const repositories = new Map<string, RetainedRepositoryProvenance>();
573
- for (const repository of snapshot.repositories) {
574
- if ((repository.gitCommit === null) !== (repository.commitKey === null)) {
575
- throw new LoreError("validation", "retained repository commit identity is incomplete");
576
- }
577
- const key = repository.memberId ?? repository.repositoryScopeKey;
578
- if (repositories.has(key)) throw new LoreError("validation", `duplicate retained repository ${key}`);
579
- repositories.set(key, repository);
580
- }
581
-
582
- const recordKeys = new Set<string>();
583
- for (const fact of snapshot.facts) {
584
- if (recordKeys.has(fact.recordKey))
585
- throw new LoreError("validation", `duplicate retained record key ${fact.recordKey}`);
586
- recordKeys.add(fact.recordKey);
587
- if (fact.provenance.recordKey !== fact.recordKey) {
588
- throw new LoreError("validation", `retained ${fact.kind} record and provenance keys disagree`);
589
- }
590
- const repository = repositories.get(fact.provenance.memberId ?? fact.provenance.repositoryScopeKey);
591
- if (
592
- repository === undefined ||
593
- canonicalJson(repository) !== canonicalJson(repositoryProvenance(fact.provenance))
594
- ) {
595
- throw new LoreError("validation", `retained ${fact.kind} provenance has no exact repository evidence`);
596
- }
597
- if (fact.kind === "edge" && fact.value.dangling !== (fact.value.to === null)) {
598
- throw new LoreError("validation", `retained edge ${fact.recordKey} has inconsistent dangling state`);
599
- }
600
- }
601
- }
602
-
603
- function repositoryProvenance(provenance: RetainedRecordProvenance): RetainedRepositoryProvenance {
604
- const {
605
- recordKey: _recordKey,
606
- sourceRecordKey: _sourceRecordKey,
607
- sourceKey: _sourceKey,
608
- sourcePath: _sourcePath,
609
- ...repository
610
- } = provenance;
611
- return repository;
612
- }
613
-
614
- function privacyFiltered(value: Readonly<Record<string, unknown>>): Record<string, unknown> {
615
- const filtered: Record<string, unknown> = {};
616
- for (const [key, entry] of Object.entries(value)) {
617
- if (sensitiveKey(key)) continue;
618
- filtered[key] = Array.isArray(entry)
619
- ? entry.map((item) =>
620
- item !== null && typeof item === "object" ? privacyFiltered(item as Record<string, unknown>) : item,
621
- )
622
- : entry !== null && typeof entry === "object"
623
- ? privacyFiltered(entry as Record<string, unknown>)
624
- : entry;
625
- }
626
- return filtered;
627
- }
628
-
629
- function hasSensitiveKey(value: Readonly<Record<string, unknown>>): boolean {
630
- for (const [key, entry] of Object.entries(value)) {
631
- if (sensitiveKey(key)) return true;
632
- if (Array.isArray(entry)) {
633
- if (
634
- entry.some(
635
- (item) => item !== null && typeof item === "object" && hasSensitiveKey(item as Record<string, unknown>),
636
- )
637
- )
638
- return true;
639
- } else if (entry !== null && typeof entry === "object" && hasSensitiveKey(entry as Record<string, unknown>)) {
640
- return true;
641
- }
642
- }
643
- return false;
644
- }
645
-
646
- function sensitiveKey(key: string): boolean {
647
- return /(?:password|credential|secret|api[-_]?key|database(?:path|uri))/iu.test(key);
648
- }
649
-
650
- function compareFact(a: RetainedFact, b: RetainedFact): number {
651
- return compareCodeUnits(a.kind, b.kind) || compareCodeUnits(a.recordKey, b.recordKey);
652
- }
653
-
654
- function changeRow(
655
- change: SnapshotChange["change"],
656
- from: RetainedFact | null,
657
- to: RetainedFact | null,
658
- ): SnapshotChange {
659
- const selected = from ?? (to as RetainedFact);
660
- return {
661
- change,
662
- recordKind: selected.kind,
663
- recordKey: selected.recordKey,
664
- id: selected.id,
665
- relationshipDelta: selected.kind === "edge" ? change : null,
666
- fieldsChanged: change === "changed" ? changedFields(from?.value ?? {}, to?.value ?? {}) : [],
667
- from,
668
- to,
669
- };
670
- }
671
-
672
- function changedFields(from: Readonly<Record<string, unknown>>, to: Readonly<Record<string, unknown>>): string[] {
673
- return [...new Set([...Object.keys(from), ...Object.keys(to)])]
674
- .filter((key) => canonicalJson(from[key]) !== canonicalJson(to[key]))
675
- .sort(compareCodeUnits);
676
- }
677
-
678
- function uniqueSorted(values: readonly string[]): string[] {
679
- const unique = [...new Set(values)];
680
- if (unique.length !== values.length) throw new LoreError("usage", "repository filters must be unique");
681
- return unique.sort(compareCodeUnits);
682
- }
683
-
684
- function uniqueKinds(values: readonly RetainedFactKind[]): RetainedFactKind[] {
685
- const known = new Set<RetainedFactKind>(["concept", "task", "edge"]);
686
- for (const kind of values)
687
- if (!known.has(kind)) throw new LoreError("usage", `unknown retained fact kind ${JSON.stringify(kind)}`);
688
- const unique = [...new Set(values)];
689
- if (unique.length !== values.length) throw new LoreError("usage", "kind filters must be unique");
690
- return unique.sort(compareCodeUnits);
691
- }
692
-
693
- function requiredWorkspaceProvenance(
694
- values: ReadonlyMap<string, WorkspaceRecordProvenance>,
695
- recordKey: string,
696
- ): WorkspaceRecordProvenance {
697
- const value = values.get(recordKey);
698
- if (value === undefined)
699
- throw new LoreError("validation", `workspace retained snapshot is missing provenance for ${recordKey}`);
700
- return value;
701
- }
702
-
703
- function requiredRepository(
704
- values: ReadonlyMap<string, RetainedRepositoryProvenance>,
705
- memberId: string,
706
- ): RetainedRepositoryProvenance {
707
- const value = values.get(memberId);
708
- if (value === undefined)
709
- throw new LoreError("validation", `workspace retained snapshot is missing repository ${memberId}`);
710
- return value;
711
- }