@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,473 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Versioned control-plane and identity contract for explicit local workspaces.
|
|
3
|
-
*
|
|
4
|
-
* Repository locators are intentionally confined to the manifest and member
|
|
5
|
-
* observations. They never participate in durable identity or projection
|
|
6
|
-
* records, so moving a checkout does not silently rename its source facts.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { createHash } from "node:crypto";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
import { compareCodeUnits } from "./order";
|
|
12
|
-
|
|
13
|
-
export const WORKSPACE_MANIFEST_SCHEMA_VERSION = "lore-workspace-manifest/1" as const;
|
|
14
|
-
export const WORKSPACE_IDENTITY_SCHEMA_VERSION = "lore-workspace-identity/1" as const;
|
|
15
|
-
|
|
16
|
-
export const WORKSPACE_STORAGE_CONTRACT = Object.freeze({
|
|
17
|
-
projectionRoot: ".lore/cache/workspaces/1/<workspaceKey>/",
|
|
18
|
-
sourceOfTruth: "explicit-manifest-and-validated-exports",
|
|
19
|
-
discoversMachineRepositories: false,
|
|
20
|
-
storesRepositoryLocators: false,
|
|
21
|
-
storesCredentials: false,
|
|
22
|
-
exposesQueryLanguage: false,
|
|
23
|
-
rebuildOnly: true,
|
|
24
|
-
deletionScope: "one-workspace-projection",
|
|
25
|
-
requiresContainmentAndSymlinkChecks: true,
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
export const WORKSPACE_LINK_CONTRACT = Object.freeze({
|
|
29
|
-
repositoryLocalAuthoredLinksRemainLocal: true,
|
|
30
|
-
infersCrossRepositoryLinksFromNames: false,
|
|
31
|
-
explicitCrossRepositoryEndpointsRequired: true,
|
|
32
|
-
conflictingCandidateDisposition: "reject-candidate",
|
|
33
|
-
preservesLastVerifiedProjection: true,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const digestSchema = z.string().regex(/^sha256:[0-9a-f]{64}$/u);
|
|
37
|
-
const commitSchema = z
|
|
38
|
-
.string()
|
|
39
|
-
.regex(/^[0-9a-f]{40}$/u)
|
|
40
|
-
.nullable();
|
|
41
|
-
const stableIdSchema = z
|
|
42
|
-
.string()
|
|
43
|
-
.min(1)
|
|
44
|
-
.max(64)
|
|
45
|
-
.regex(/^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/u);
|
|
46
|
-
const boundedTextSchema = z
|
|
47
|
-
.string()
|
|
48
|
-
.min(1)
|
|
49
|
-
.max(4_096)
|
|
50
|
-
.refine((value) => value === value.trim() && !value.includes("\0"), {
|
|
51
|
-
message: "values must be trimmed and contain no NUL bytes",
|
|
52
|
-
});
|
|
53
|
-
const sourcePathSchema = z
|
|
54
|
-
.string()
|
|
55
|
-
.min(1)
|
|
56
|
-
.max(4_096)
|
|
57
|
-
.refine(
|
|
58
|
-
(path) =>
|
|
59
|
-
!path.startsWith("/") &&
|
|
60
|
-
!path.includes("\\") &&
|
|
61
|
-
path.split("/").every((segment) => segment !== "" && segment !== "." && segment !== ".."),
|
|
62
|
-
{ message: "source paths must be repository-relative POSIX paths" },
|
|
63
|
-
);
|
|
64
|
-
|
|
65
|
-
const workspaceMemberSchema = z
|
|
66
|
-
.object({
|
|
67
|
-
memberId: stableIdSchema,
|
|
68
|
-
locator: boundedTextSchema,
|
|
69
|
-
displayName: z.string().min(1).max(256).nullable(),
|
|
70
|
-
expectedRef: boundedTextSchema.max(1_024).nullable(),
|
|
71
|
-
})
|
|
72
|
-
.strict();
|
|
73
|
-
|
|
74
|
-
const workspaceLinkEndpointSchema = z
|
|
75
|
-
.object({
|
|
76
|
-
memberId: stableIdSchema,
|
|
77
|
-
kind: z.enum(["concept", "task"]),
|
|
78
|
-
id: boundedTextSchema.max(1_024),
|
|
79
|
-
})
|
|
80
|
-
.strict();
|
|
81
|
-
|
|
82
|
-
const workspaceLinkSchema = z
|
|
83
|
-
.object({
|
|
84
|
-
linkId: stableIdSchema,
|
|
85
|
-
kind: boundedTextSchema.max(256),
|
|
86
|
-
from: workspaceLinkEndpointSchema,
|
|
87
|
-
to: workspaceLinkEndpointSchema,
|
|
88
|
-
})
|
|
89
|
-
.strict();
|
|
90
|
-
|
|
91
|
-
export const workspaceManifestSchema = z
|
|
92
|
-
.object({
|
|
93
|
-
schemaVersion: z.literal(WORKSPACE_MANIFEST_SCHEMA_VERSION),
|
|
94
|
-
workspaceId: stableIdSchema,
|
|
95
|
-
members: z.array(workspaceMemberSchema).min(1).max(256),
|
|
96
|
-
links: z.array(workspaceLinkSchema).max(10_000),
|
|
97
|
-
})
|
|
98
|
-
.strict();
|
|
99
|
-
|
|
100
|
-
export type WorkspaceManifest = z.infer<typeof workspaceManifestSchema>;
|
|
101
|
-
export type WorkspaceMember = z.infer<typeof workspaceMemberSchema>;
|
|
102
|
-
export type WorkspaceLink = z.infer<typeof workspaceLinkSchema>;
|
|
103
|
-
|
|
104
|
-
const workspaceSourceSnapshotSchema = z
|
|
105
|
-
.object({
|
|
106
|
-
memberId: stableIdSchema,
|
|
107
|
-
repositoryScopeKey: digestSchema,
|
|
108
|
-
bundleId: digestSchema,
|
|
109
|
-
gitCommit: commitSchema,
|
|
110
|
-
exportDigest: digestSchema,
|
|
111
|
-
})
|
|
112
|
-
.strict();
|
|
113
|
-
|
|
114
|
-
export type WorkspaceSourceSnapshot = z.infer<typeof workspaceSourceSnapshotSchema>;
|
|
115
|
-
|
|
116
|
-
const readyObservationSchema = workspaceSourceSnapshotSchema
|
|
117
|
-
.extend({
|
|
118
|
-
state: z.literal("ready"),
|
|
119
|
-
locator: boundedTextSchema,
|
|
120
|
-
gitRef: boundedTextSchema.max(1_024).nullable(),
|
|
121
|
-
})
|
|
122
|
-
.strict();
|
|
123
|
-
const missingObservationSchema = z
|
|
124
|
-
.object({
|
|
125
|
-
state: z.literal("missing"),
|
|
126
|
-
memberId: stableIdSchema,
|
|
127
|
-
locator: boundedTextSchema,
|
|
128
|
-
gitRef: boundedTextSchema.max(1_024).nullable(),
|
|
129
|
-
})
|
|
130
|
-
.strict();
|
|
131
|
-
const workspaceMemberObservationSchema = z.discriminatedUnion("state", [
|
|
132
|
-
readyObservationSchema,
|
|
133
|
-
missingObservationSchema,
|
|
134
|
-
]);
|
|
135
|
-
|
|
136
|
-
export type WorkspaceMemberObservation = z.infer<typeof workspaceMemberObservationSchema>;
|
|
137
|
-
export type WorkspaceRecordKind = "concept" | "task" | "authored-edge";
|
|
138
|
-
|
|
139
|
-
export interface WorkspaceRepositoryIdentity {
|
|
140
|
-
readonly memberId: string;
|
|
141
|
-
readonly memberKey: string;
|
|
142
|
-
readonly repositoryKey: string;
|
|
143
|
-
readonly bundleKey: string;
|
|
144
|
-
readonly commitKey: string | null;
|
|
145
|
-
readonly exportKey: string;
|
|
146
|
-
readonly source: WorkspaceSourceSnapshot;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export interface WorkspaceLinkIdentity {
|
|
150
|
-
readonly linkId: string;
|
|
151
|
-
readonly workspaceLinkKey: string;
|
|
152
|
-
readonly source: WorkspaceLink;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface WorkspaceIdentity {
|
|
156
|
-
readonly schemaVersion: typeof WORKSPACE_IDENTITY_SCHEMA_VERSION;
|
|
157
|
-
readonly workspaceKey: string;
|
|
158
|
-
readonly snapshotKey: string;
|
|
159
|
-
readonly repositories: readonly WorkspaceRepositoryIdentity[];
|
|
160
|
-
readonly links: readonly WorkspaceLinkIdentity[];
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface WorkspaceRecordIdentity {
|
|
164
|
-
readonly workspaceRecordKey: string;
|
|
165
|
-
readonly workspaceSourceKey: string | null;
|
|
166
|
-
readonly sourceRecordKey: string;
|
|
167
|
-
readonly sourcePath: string | null;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/** Derive the portable workspace storage identity without resolving a manifest locator. */
|
|
171
|
-
export function workspaceKeyForId(workspaceId: string): string {
|
|
172
|
-
stableIdSchema.parse(workspaceId);
|
|
173
|
-
return digest(`lore-workspace/1\0${workspaceId}`);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Public, locator-free scope attached to every workspace command result. */
|
|
177
|
-
export interface WorkspaceResultScope {
|
|
178
|
-
readonly schemaVersion: "lore-workspace-result-scope/1";
|
|
179
|
-
readonly workspaceId: string;
|
|
180
|
-
readonly workspaceKey: string;
|
|
181
|
-
readonly snapshotKey: string;
|
|
182
|
-
readonly repositories: readonly WorkspaceResultRepository[];
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/** One selected repository in a workspace command result. */
|
|
186
|
-
export interface WorkspaceResultRepository {
|
|
187
|
-
readonly memberId: string;
|
|
188
|
-
readonly repositoryKey: string;
|
|
189
|
-
readonly repositoryScopeKey: string;
|
|
190
|
-
readonly bundleKey: string;
|
|
191
|
-
readonly bundleId: string;
|
|
192
|
-
readonly commitKey: string | null;
|
|
193
|
-
readonly gitCommit: string | null;
|
|
194
|
-
readonly exportKey: string;
|
|
195
|
-
readonly exportDigest: string;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Complete namespaced and original provenance for one public workspace record. */
|
|
199
|
-
export interface WorkspaceRecordProvenance extends WorkspaceResultRepository {
|
|
200
|
-
readonly recordKind: WorkspaceRecordKind;
|
|
201
|
-
readonly recordKey: string;
|
|
202
|
-
readonly sourceRecordKey: string;
|
|
203
|
-
readonly sourceKey: string | null;
|
|
204
|
-
readonly sourcePath: string | null;
|
|
205
|
-
readonly sourceId: string;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/** Qualify a repository-local identity for the public workspace id space. */
|
|
209
|
-
export function qualifyWorkspaceId(memberId: string, sourceId: string): string {
|
|
210
|
-
stableIdSchema.parse(memberId);
|
|
211
|
-
if (sourceId.length === 0 || sourceId.includes("\0")) throw contractError("workspace source ids must be non-empty");
|
|
212
|
-
return `${memberId}::${sourceId}`;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
/** Split a public workspace id without guessing across repository members. */
|
|
216
|
-
export function parseQualifiedWorkspaceId(value: string): { memberId: string; sourceId: string } {
|
|
217
|
-
const separator = value.indexOf("::");
|
|
218
|
-
if (separator <= 0 || separator === value.length - 2) {
|
|
219
|
-
throw contractError("workspace ids must use <member-id>::<source-id>");
|
|
220
|
-
}
|
|
221
|
-
const memberId = value.slice(0, separator);
|
|
222
|
-
stableIdSchema.parse(memberId);
|
|
223
|
-
return { memberId, sourceId: value.slice(separator + 2) };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const workspaceRepositoryIdentitySchema = z
|
|
227
|
-
.object({
|
|
228
|
-
memberId: stableIdSchema,
|
|
229
|
-
memberKey: digestSchema,
|
|
230
|
-
repositoryKey: digestSchema,
|
|
231
|
-
bundleKey: digestSchema,
|
|
232
|
-
commitKey: digestSchema.nullable(),
|
|
233
|
-
exportKey: digestSchema,
|
|
234
|
-
source: workspaceSourceSnapshotSchema,
|
|
235
|
-
})
|
|
236
|
-
.strict();
|
|
237
|
-
const workspaceLinkIdentitySchema = z
|
|
238
|
-
.object({
|
|
239
|
-
linkId: stableIdSchema,
|
|
240
|
-
workspaceLinkKey: digestSchema,
|
|
241
|
-
source: workspaceLinkSchema,
|
|
242
|
-
})
|
|
243
|
-
.strict();
|
|
244
|
-
export const workspaceIdentitySchema = z
|
|
245
|
-
.object({
|
|
246
|
-
schemaVersion: z.literal(WORKSPACE_IDENTITY_SCHEMA_VERSION),
|
|
247
|
-
workspaceKey: digestSchema,
|
|
248
|
-
snapshotKey: digestSchema,
|
|
249
|
-
repositories: z.array(workspaceRepositoryIdentitySchema).min(1).max(256),
|
|
250
|
-
links: z.array(workspaceLinkIdentitySchema).max(10_000),
|
|
251
|
-
})
|
|
252
|
-
.strict();
|
|
253
|
-
|
|
254
|
-
export type WorkspaceTransitionKind =
|
|
255
|
-
| "unchanged"
|
|
256
|
-
| "add"
|
|
257
|
-
| "remove"
|
|
258
|
-
| "branch-change"
|
|
259
|
-
| "missing-repository"
|
|
260
|
-
| "renamed-repository"
|
|
261
|
-
| "repository-replaced"
|
|
262
|
-
| "conflicting-link"
|
|
263
|
-
| "stale-snapshot";
|
|
264
|
-
|
|
265
|
-
export interface WorkspaceTransition {
|
|
266
|
-
readonly kind: WorkspaceTransitionKind;
|
|
267
|
-
readonly candidateDisposition: "reuse" | "rebuild" | "reject-candidate";
|
|
268
|
-
readonly servePrevious: boolean;
|
|
269
|
-
readonly removePriorEvidence: boolean;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** Parse a canonical manifest and enforce explicit membership/link invariants. */
|
|
273
|
-
export function parseWorkspaceManifest(value: unknown): WorkspaceManifest {
|
|
274
|
-
const manifest = workspaceManifestSchema.parse(value);
|
|
275
|
-
assertSortedUnique(manifest.members, (member) => member.memberId, "workspace members");
|
|
276
|
-
assertSortedUnique(manifest.links, (link) => link.linkId, "workspace links");
|
|
277
|
-
|
|
278
|
-
const members = new Set(manifest.members.map((member) => member.memberId));
|
|
279
|
-
const linkFingerprints = new Set<string>();
|
|
280
|
-
for (const link of manifest.links) {
|
|
281
|
-
if (!members.has(link.from.memberId)) throw contractError(`unknown link source member ${link.from.memberId}`);
|
|
282
|
-
if (!members.has(link.to.memberId)) throw contractError(`unknown link target member ${link.to.memberId}`);
|
|
283
|
-
const fingerprint = [
|
|
284
|
-
link.from.memberId,
|
|
285
|
-
link.from.kind,
|
|
286
|
-
link.from.id,
|
|
287
|
-
link.kind,
|
|
288
|
-
link.to.memberId,
|
|
289
|
-
link.to.kind,
|
|
290
|
-
link.to.id,
|
|
291
|
-
].join("\0");
|
|
292
|
-
if (linkFingerprints.has(fingerprint)) throw contractError(`duplicate explicit link ${link.linkId}`);
|
|
293
|
-
linkFingerprints.add(fingerprint);
|
|
294
|
-
}
|
|
295
|
-
return manifest;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
/** Serialize a validated manifest as canonical UTF-8 JSON with a trailing newline. */
|
|
299
|
-
export function serializeWorkspaceManifest(value: unknown): string {
|
|
300
|
-
return `${canonicalJson(parseWorkspaceManifest(value))}\n`;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* Build locator-free workspace identities from a complete set of validated
|
|
305
|
-
* repository export observations.
|
|
306
|
-
*/
|
|
307
|
-
export function buildWorkspaceIdentity(manifestValue: unknown, sourceValues: readonly unknown[]): WorkspaceIdentity {
|
|
308
|
-
const manifest = parseWorkspaceManifest(manifestValue);
|
|
309
|
-
const sources = sourceValues.map((value) => workspaceSourceSnapshotSchema.parse(value));
|
|
310
|
-
assertSortedUnique(sources, (source) => source.memberId, "workspace sources");
|
|
311
|
-
const expectedMembers = manifest.members.map((member) => member.memberId);
|
|
312
|
-
const observedMembers = sources.map((source) => source.memberId);
|
|
313
|
-
if (canonicalJson(expectedMembers) !== canonicalJson(observedMembers)) {
|
|
314
|
-
throw contractError("workspace sources must exactly match explicit manifest membership");
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const workspaceKey = workspaceKeyForId(manifest.workspaceId);
|
|
318
|
-
const repositories = sources.map((source): WorkspaceRepositoryIdentity => {
|
|
319
|
-
const memberKey = digest(`lore-workspace-member/1\0${workspaceKey}\0${source.memberId}`);
|
|
320
|
-
const repositoryKey = digest(
|
|
321
|
-
`lore-workspace-repository/1\0${workspaceKey}\0${memberKey}\0${source.repositoryScopeKey}`,
|
|
322
|
-
);
|
|
323
|
-
return {
|
|
324
|
-
memberId: source.memberId,
|
|
325
|
-
memberKey,
|
|
326
|
-
repositoryKey,
|
|
327
|
-
bundleKey: digest(`lore-workspace-bundle/1\0${repositoryKey}\0${source.bundleId}`),
|
|
328
|
-
commitKey:
|
|
329
|
-
source.gitCommit === null ? null : digest(`lore-workspace-commit/1\0${repositoryKey}\0${source.gitCommit}`),
|
|
330
|
-
exportKey: digest(`lore-workspace-export/1\0${repositoryKey}\0${source.exportDigest}`),
|
|
331
|
-
source,
|
|
332
|
-
};
|
|
333
|
-
});
|
|
334
|
-
const links = manifest.links.map(
|
|
335
|
-
(link): WorkspaceLinkIdentity => ({
|
|
336
|
-
linkId: link.linkId,
|
|
337
|
-
workspaceLinkKey: digest(`lore-workspace-link/1\0${workspaceKey}\0${link.linkId}`),
|
|
338
|
-
source: link,
|
|
339
|
-
}),
|
|
340
|
-
);
|
|
341
|
-
const snapshotKey = digest(
|
|
342
|
-
`lore-workspace-snapshot/1\0${workspaceKey}\0${canonicalJson({
|
|
343
|
-
repositories: repositories.map((repository) => ({
|
|
344
|
-
memberKey: repository.memberKey,
|
|
345
|
-
repositoryKey: repository.repositoryKey,
|
|
346
|
-
bundleKey: repository.bundleKey,
|
|
347
|
-
gitCommit: repository.source.gitCommit,
|
|
348
|
-
exportDigest: repository.source.exportDigest,
|
|
349
|
-
})),
|
|
350
|
-
links: links.map((link) => ({
|
|
351
|
-
workspaceLinkKey: link.workspaceLinkKey,
|
|
352
|
-
source: link.source,
|
|
353
|
-
})),
|
|
354
|
-
})}`,
|
|
355
|
-
);
|
|
356
|
-
return { schemaVersion: WORKSPACE_IDENTITY_SCHEMA_VERSION, workspaceKey, snapshotKey, repositories, links };
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
/** Namespace one repository-local source record without reinterpreting its key. */
|
|
360
|
-
export function namespaceWorkspaceRecord(
|
|
361
|
-
repository: WorkspaceRepositoryIdentity,
|
|
362
|
-
kind: WorkspaceRecordKind,
|
|
363
|
-
sourceRecordKeyValue: string,
|
|
364
|
-
sourcePathValue: string | null,
|
|
365
|
-
): WorkspaceRecordIdentity {
|
|
366
|
-
const sourceRecordKey = digestSchema.parse(sourceRecordKeyValue);
|
|
367
|
-
const sourcePath = sourcePathValue === null ? null : sourcePathSchema.parse(sourcePathValue);
|
|
368
|
-
return {
|
|
369
|
-
workspaceRecordKey: digest(`lore-workspace-record/1\0${repository.repositoryKey}\0${kind}\0${sourceRecordKey}`),
|
|
370
|
-
workspaceSourceKey:
|
|
371
|
-
sourcePath === null ? null : digest(`lore-workspace-source/1\0${repository.repositoryKey}\0${sourcePath}`),
|
|
372
|
-
sourceRecordKey,
|
|
373
|
-
sourcePath,
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
/**
|
|
378
|
-
* Classify one explicit member transition. The precedence is part of the
|
|
379
|
-
* public contract so simultaneous locator/source changes have one outcome.
|
|
380
|
-
*/
|
|
381
|
-
export function classifyWorkspaceTransition(input: {
|
|
382
|
-
readonly previous: WorkspaceMemberObservation | null;
|
|
383
|
-
readonly current: WorkspaceMemberObservation | null;
|
|
384
|
-
readonly linkConflict?: boolean;
|
|
385
|
-
}): WorkspaceTransition {
|
|
386
|
-
const previous = input.previous === null ? null : workspaceMemberObservationSchema.parse(input.previous);
|
|
387
|
-
const current = input.current === null ? null : workspaceMemberObservationSchema.parse(input.current);
|
|
388
|
-
if (previous === null && current === null) throw contractError("a transition requires a previous or current member");
|
|
389
|
-
if (previous !== null && current !== null && previous.memberId !== current.memberId) {
|
|
390
|
-
throw contractError("transition observations must use the same member id");
|
|
391
|
-
}
|
|
392
|
-
const hasPrevious = previous?.state === "ready";
|
|
393
|
-
if (input.linkConflict === true) return transition("conflicting-link", "reject-candidate", hasPrevious, false);
|
|
394
|
-
if (current === null) return transition("remove", "rebuild", hasPrevious, true);
|
|
395
|
-
if (current.state === "missing") {
|
|
396
|
-
return transition("missing-repository", "reject-candidate", hasPrevious, false);
|
|
397
|
-
}
|
|
398
|
-
if (previous === null || previous.state === "missing") return transition("add", "rebuild", false, false);
|
|
399
|
-
if (previous.repositoryScopeKey !== current.repositoryScopeKey || previous.bundleId !== current.bundleId) {
|
|
400
|
-
return transition("repository-replaced", "rebuild", true, true);
|
|
401
|
-
}
|
|
402
|
-
if (previous.gitCommit !== current.gitCommit || previous.gitRef !== current.gitRef) {
|
|
403
|
-
return transition("branch-change", "rebuild", true, false);
|
|
404
|
-
}
|
|
405
|
-
if (previous.exportDigest !== current.exportDigest) {
|
|
406
|
-
return transition("stale-snapshot", "rebuild", true, false);
|
|
407
|
-
}
|
|
408
|
-
if (previous.locator !== current.locator) {
|
|
409
|
-
return transition("renamed-repository", "reuse", true, false);
|
|
410
|
-
}
|
|
411
|
-
return transition("unchanged", "reuse", true, false);
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/** Canonical locator-free bytes suitable for projection metadata and evidence. */
|
|
415
|
-
export function serializeWorkspaceIdentity(value: unknown): string {
|
|
416
|
-
const identity = workspaceIdentitySchema.parse(value);
|
|
417
|
-
assertSortedUnique(identity.repositories, (repository) => repository.memberId, "workspace identity repositories");
|
|
418
|
-
assertSortedUnique(identity.links, (link) => link.linkId, "workspace identity links");
|
|
419
|
-
for (const repository of identity.repositories) {
|
|
420
|
-
if (repository.memberId !== repository.source.memberId) {
|
|
421
|
-
throw contractError(`repository identity member differs from source ${repository.memberId}`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
for (const link of identity.links) {
|
|
425
|
-
if (link.linkId !== link.source.linkId) throw contractError(`link identity differs from source ${link.linkId}`);
|
|
426
|
-
}
|
|
427
|
-
return `${canonicalJson(identity)}\n`;
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function transition(
|
|
431
|
-
kind: WorkspaceTransitionKind,
|
|
432
|
-
candidateDisposition: WorkspaceTransition["candidateDisposition"],
|
|
433
|
-
servePrevious: boolean,
|
|
434
|
-
removePriorEvidence: boolean,
|
|
435
|
-
): WorkspaceTransition {
|
|
436
|
-
return { kind, candidateDisposition, servePrevious, removePriorEvidence };
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function assertSortedUnique<T>(records: readonly T[], key: (record: T) => string, label: string): void {
|
|
440
|
-
for (let index = 1; index < records.length; index++) {
|
|
441
|
-
const previous = records[index - 1];
|
|
442
|
-
const current = records[index];
|
|
443
|
-
if (previous === undefined || current === undefined) continue;
|
|
444
|
-
if (compareCodeUnits(key(previous), key(current)) >= 0) {
|
|
445
|
-
throw contractError(`${label} must be unique and sorted by UTF-16 code units`);
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function digest(value: string): string {
|
|
451
|
-
return `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function contractError(message: string): Error {
|
|
455
|
-
return new Error(`invalid ${WORKSPACE_MANIFEST_SCHEMA_VERSION}: ${message}`);
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
function canonicalJson(value: unknown): string {
|
|
459
|
-
return JSON.stringify(canonicalValue(value));
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function canonicalValue(value: unknown): unknown {
|
|
463
|
-
if (Array.isArray(value)) return value.map((entry) => canonicalValue(entry));
|
|
464
|
-
if (value === null || typeof value !== "object") return value;
|
|
465
|
-
const result: Record<string, unknown> = {};
|
|
466
|
-
for (const key of Object.keys(value).sort(compareCodeUnits)) {
|
|
467
|
-
const entry = (value as Record<string, unknown>)[key];
|
|
468
|
-
if (entry !== undefined && typeof entry !== "function" && typeof entry !== "symbol") {
|
|
469
|
-
result[key] = canonicalValue(entry);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
return result;
|
|
473
|
-
}
|