@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
package/src/core/snapshot.ts
DELETED
|
@@ -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
|
-
}
|