@abelxiaoxing/cadence 1.0.1 → 1.0.3

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.
@@ -1,38 +1,313 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import type { Bound } from "./file-snapshot.ts";
2
+ import { isDeepStrictEqual } from "node:util";
3
+ import type {
4
+ BaselineEntry,
5
+ VerificationContract,
6
+ } from "./candidate-preflight.ts";
7
+ import type { ImplementationPhase } from "./contracts.ts";
8
+ import type { Bound, DirBound, FileBound } from "./file-snapshot.ts";
3
9
  import { snapshotFiles } from "./file-snapshot.ts";
4
10
 
11
+ export interface RetainedCandidateIdentity {
12
+ stage: "abel-implement";
13
+ canonicalRoot: string;
14
+ root: string;
15
+ changeId: string;
16
+ taskId: string;
17
+ originRequestId: string;
18
+ phase: ImplementationPhase;
19
+ launchIndex: 0 | 1;
20
+ }
21
+
22
+ export interface RetainedCandidateFacts extends RetainedCandidateIdentity {
23
+ writeSet: string[];
24
+ approvedDependencies: string[];
25
+ snapshot: Bound;
26
+ }
27
+
5
28
  export interface RetainedResult {
6
29
  id: string;
7
30
  diff: Buffer;
31
+ stage?: RetainedCandidateIdentity["stage"];
32
+ canonicalRoot?: string;
33
+ changeId?: string;
34
+ taskId?: string;
35
+ originRequestId?: string;
36
+ phase?: ImplementationPhase;
37
+ launchIndex?: 0 | 1;
8
38
  writeSet: string[];
39
+ approvedDependencies: string[];
9
40
  root: string;
10
41
  snapshot: Bound;
42
+ baseline: BaselineEntry[];
43
+ verification?: VerificationContract;
44
+ packageManifest?: FileBound;
45
+ lockfile?: FileBound;
46
+ dependencyTarget?: FileBound | DirBound;
47
+ }
48
+
49
+ export type BoundRetainedResult = RetainedResult & RetainedCandidateIdentity;
50
+
51
+ interface RetainCandidateData {
52
+ diff: string;
53
+ writeSet: string[];
54
+ approvedDependencies?: string[];
55
+ root: string;
56
+ snapshot?: Bound;
57
+ baseline?: BaselineEntry[];
58
+ verification?: VerificationContract;
59
+ packageManifest?: FileBound;
60
+ lockfile?: FileBound;
61
+ dependencyTarget?: FileBound | DirBound;
62
+ }
63
+
64
+ type RetainInput = RetainCandidateData & Partial<RetainedCandidateIdentity>;
65
+
66
+ const IDENTITY_KEYS = [
67
+ "stage",
68
+ "canonicalRoot",
69
+ "changeId",
70
+ "taskId",
71
+ "originRequestId",
72
+ "phase",
73
+ "launchIndex",
74
+ ] as const;
75
+
76
+ function hasCompleteIdentity(
77
+ value: RetainedResult | RetainInput,
78
+ ): value is (RetainedResult | RetainInput) & RetainedCandidateIdentity {
79
+ return IDENTITY_KEYS.every((key) => value[key] !== undefined);
80
+ }
81
+
82
+ function hasAnyIdentity(value: RetainedResult | RetainInput): boolean {
83
+ return IDENTITY_KEYS.some((key) => value[key] !== undefined);
84
+ }
85
+
86
+ function cloneIdentity(identity: RetainedCandidateIdentity) {
87
+ return {
88
+ stage: identity.stage,
89
+ canonicalRoot: identity.canonicalRoot,
90
+ changeId: identity.changeId,
91
+ taskId: identity.taskId,
92
+ originRequestId: identity.originRequestId,
93
+ phase: identity.phase,
94
+ launchIndex: identity.launchIndex,
95
+ };
96
+ }
97
+
98
+ function cloneRetained(retained: RetainedResult): RetainedResult {
99
+ const clone: RetainedResult = {
100
+ ...retained,
101
+ diff: Buffer.from(retained.diff),
102
+ writeSet: [...retained.writeSet],
103
+ approvedDependencies: [...retained.approvedDependencies],
104
+ snapshot: structuredClone(retained.snapshot),
105
+ baseline: retained.baseline.map((entry) => ({ ...entry })),
106
+ };
107
+ if (retained.verification) {
108
+ clone.verification = {
109
+ ...retained.verification,
110
+ argv: [...retained.verification.argv],
111
+ };
112
+ }
113
+ if (retained.packageManifest) {
114
+ clone.packageManifest = { ...retained.packageManifest };
115
+ }
116
+ if (retained.lockfile) clone.lockfile = { ...retained.lockfile };
117
+ if (retained.dependencyTarget) {
118
+ clone.dependencyTarget = { ...retained.dependencyTarget };
119
+ }
120
+ return clone;
11
121
  }
12
122
 
13
123
  export class ResultStore {
14
124
  private readonly results = new Map<string, RetainedResult>();
125
+ private readonly sealedResults = new Map<string, RetainedResult>();
126
+ private readonly sealedCandidateFacts = new Map<
127
+ string,
128
+ RetainedCandidateFacts
129
+ >();
15
130
 
16
- retain(input: { diff: string; writeSet: string[]; root: string }): string {
131
+ retain(input: RetainInput): string {
132
+ if (hasAnyIdentity(input) && !hasCompleteIdentity(input)) {
133
+ throw new Error("retained candidate identity is incomplete");
134
+ }
17
135
  const id = randomUUID();
18
- this.results.set(id, {
136
+ const retained: RetainedResult = {
19
137
  id,
20
138
  diff: Buffer.from(input.diff, "utf8"),
21
- writeSet: [...new Set(input.writeSet)],
139
+ ...(hasCompleteIdentity(input) ? cloneIdentity(input) : {}),
140
+ writeSet: [...input.writeSet],
141
+ approvedDependencies: [...(input.approvedDependencies ?? [])],
22
142
  root: input.root,
23
- snapshot: snapshotFiles(input.root, input.writeSet),
24
- });
143
+ snapshot: structuredClone(
144
+ input.snapshot ?? snapshotFiles(input.root, input.writeSet),
145
+ ),
146
+ baseline: (input.baseline ?? []).map((entry) => ({ ...entry })),
147
+ ...(input.verification === undefined
148
+ ? {}
149
+ : {
150
+ verification: {
151
+ ...input.verification,
152
+ argv: [...input.verification.argv],
153
+ },
154
+ }),
155
+ ...(input.packageManifest === undefined
156
+ ? {}
157
+ : { packageManifest: { ...input.packageManifest } }),
158
+ ...(input.lockfile === undefined
159
+ ? {}
160
+ : { lockfile: { ...input.lockfile } }),
161
+ ...(input.dependencyTarget === undefined
162
+ ? {}
163
+ : { dependencyTarget: { ...input.dependencyTarget } }),
164
+ };
165
+ this.results.set(id, retained);
166
+ this.sealedResults.set(id, cloneRetained(retained));
167
+ if (hasCompleteIdentity(retained)) {
168
+ this.sealedCandidateFacts.set(id, this.cloneFacts(retained));
169
+ }
25
170
  return id;
26
171
  }
27
172
 
28
173
  get(id: string): RetainedResult | undefined {
29
- return this.results.get(id);
174
+ const retained = this.results.get(id);
175
+ return retained ? cloneRetained(retained) : undefined;
176
+ }
177
+
178
+ hasCandidateIdentity(id: string): boolean {
179
+ const retained = this.results.get(id);
180
+ if (!retained) return false;
181
+ this.assertIntegrity(id, retained);
182
+ return hasAnyIdentity(retained);
183
+ }
184
+
185
+ bindIdentity(
186
+ id: string,
187
+ identity: RetainedCandidateIdentity,
188
+ ): BoundRetainedResult {
189
+ const retained = this.results.get(id);
190
+ if (!retained) throw new Error("retained result not found");
191
+ this.assertIntegrity(id, retained);
192
+ if (hasAnyIdentity(retained)) {
193
+ if (
194
+ !hasCompleteIdentity(retained) ||
195
+ !this.matchesIdentity(retained, identity)
196
+ ) {
197
+ throw new Error("retained candidate identity mismatch");
198
+ }
199
+ return cloneRetained(retained) as BoundRetainedResult;
200
+ }
201
+ if (retained.root !== identity.root) {
202
+ throw new Error("retained candidate identity mismatch");
203
+ }
204
+ Object.assign(retained, cloneIdentity(identity));
205
+ this.sealedResults.set(id, cloneRetained(retained));
206
+ return cloneRetained(retained) as BoundRetainedResult;
207
+ }
208
+
209
+ resolveIdentity(
210
+ id: string,
211
+ expected: RetainedCandidateFacts,
212
+ ): BoundRetainedResult {
213
+ const retained = this.results.get(id);
214
+ if (!retained) throw new Error("retained result not found");
215
+ this.assertIntegrity(id, retained);
216
+ if (
217
+ !hasCompleteIdentity(retained) ||
218
+ !this.matchesIdentity(retained, expected) ||
219
+ !isDeepStrictEqual(retained.writeSet, expected.writeSet) ||
220
+ !isDeepStrictEqual(
221
+ retained.approvedDependencies,
222
+ expected.approvedDependencies,
223
+ )
224
+ ) {
225
+ throw new Error("retained candidate identity mismatch");
226
+ }
227
+ const sealed = this.sealedCandidateFacts.get(id);
228
+ if (sealed) {
229
+ if (
230
+ !this.matchesIdentity(retained, sealed) ||
231
+ !isDeepStrictEqual(retained.writeSet, sealed.writeSet) ||
232
+ !isDeepStrictEqual(
233
+ retained.approvedDependencies,
234
+ sealed.approvedDependencies,
235
+ ) ||
236
+ !isDeepStrictEqual(retained.snapshot, sealed.snapshot)
237
+ ) {
238
+ throw new Error("retained candidate identity mismatch");
239
+ }
240
+ } else {
241
+ if (!isDeepStrictEqual(retained.snapshot, expected.snapshot)) {
242
+ throw new Error("retained candidate identity mismatch");
243
+ }
244
+ this.sealedCandidateFacts.set(id, this.cloneFacts(retained));
245
+ }
246
+ return cloneRetained(retained) as BoundRetainedResult;
247
+ }
248
+
249
+ resolveForApply(id: string): RetainedResult {
250
+ const retained = this.results.get(id);
251
+ if (!retained) throw new Error("retained result not found");
252
+ this.assertIntegrity(id, retained);
253
+ return cloneRetained(retained);
254
+ }
255
+
256
+ private assertIntegrity(id: string, retained: RetainedResult): void {
257
+ const sealed = this.sealedResults.get(id);
258
+ if (sealed && isDeepStrictEqual(retained, sealed)) return;
259
+ const identityChanged =
260
+ sealed !== undefined &&
261
+ (IDENTITY_KEYS.some((key) => retained[key] !== sealed[key]) ||
262
+ retained.root !== sealed.root ||
263
+ !isDeepStrictEqual(retained.writeSet, sealed.writeSet) ||
264
+ !isDeepStrictEqual(
265
+ retained.approvedDependencies,
266
+ sealed.approvedDependencies,
267
+ ) ||
268
+ !isDeepStrictEqual(retained.snapshot, sealed.snapshot));
269
+ throw new Error(
270
+ identityChanged
271
+ ? "retained candidate identity mismatch"
272
+ : "retained candidate integrity mismatch",
273
+ );
30
274
  }
275
+
276
+ private cloneFacts(retained: BoundRetainedResult): RetainedCandidateFacts {
277
+ return {
278
+ ...cloneIdentity(retained),
279
+ root: retained.root,
280
+ writeSet: [...retained.writeSet],
281
+ approvedDependencies: [...retained.approvedDependencies],
282
+ snapshot: structuredClone(retained.snapshot),
283
+ };
284
+ }
285
+
286
+ private matchesIdentity(
287
+ retained: BoundRetainedResult,
288
+ expected: RetainedCandidateIdentity,
289
+ ): boolean {
290
+ return (
291
+ retained.stage === expected.stage &&
292
+ retained.canonicalRoot === expected.canonicalRoot &&
293
+ retained.root === expected.root &&
294
+ retained.changeId === expected.changeId &&
295
+ retained.taskId === expected.taskId &&
296
+ retained.originRequestId === expected.originRequestId &&
297
+ retained.phase === expected.phase &&
298
+ retained.launchIndex === expected.launchIndex
299
+ );
300
+ }
301
+
31
302
  discard(id: string): boolean {
303
+ this.sealedResults.delete(id);
304
+ this.sealedCandidateFacts.delete(id);
32
305
  return this.results.delete(id);
33
306
  }
34
307
  clear(): void {
35
308
  this.results.clear();
309
+ this.sealedResults.clear();
310
+ this.sealedCandidateFacts.clear();
36
311
  }
37
312
  get size(): number {
38
313
  return this.results.size;