@abelxiaoxing/cadence 1.8.4 → 2.0.1

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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +92 -20
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
@@ -1,1437 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import {
3
- chmodSync,
4
- closeSync,
5
- constants,
6
- fsyncSync,
7
- linkSync,
8
- lstatSync,
9
- mkdirSync,
10
- mkdtempSync,
11
- openSync,
12
- readFileSync,
13
- realpathSync,
14
- renameSync,
15
- rmdirSync,
16
- unlinkSync,
17
- writeFileSync,
18
- } from "node:fs";
19
- import { tmpdir } from "node:os";
20
- import path from "node:path";
21
- import { DatabaseSync } from "node:sqlite";
22
- import type { ArtifactStore } from "./artifact-store.ts";
23
- import { compareCanonicalStrings } from "./canonical.ts";
24
- import { isValidRelativePath } from "./contracts.ts";
25
- import { syncDirectory } from "./directory-sync.ts";
26
- import { isExecutionRetained } from "./execution-retention.ts";
27
- import { removePathSync } from "./remove-path.ts";
28
- import { observeSafePath } from "./safe-path.ts";
29
- import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
30
- import { APPLY_SCHEMA } from "./storage-schema.ts";
31
- import type {
32
- WorkspaceEntry,
33
- WorkspaceRevision,
34
- WorkspaceStore,
35
- } from "./workspace-store.ts";
36
-
37
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
38
- const SHA256 = /^[a-f0-9]{64}$/u;
39
-
40
- type TransactionState =
41
- | "prepared"
42
- | "applying"
43
- | "recovering"
44
- | "paused"
45
- | "discarded"
46
- | "completed";
47
- type PendingIntent = "cancel" | "discard";
48
- type StepState = "pending" | "applying" | "applied" | "rolled-back";
49
-
50
- interface TransactionStep {
51
- path: string;
52
- before: WorkspaceEntry;
53
- after: WorkspaceEntry;
54
- state: StepState;
55
- }
56
-
57
- interface TransactionEvent {
58
- sequence: number;
59
- kind: string;
60
- path?: string;
61
- }
62
-
63
- interface TransactionJournal {
64
- transactionId: string;
65
- consumerRoot: string;
66
- baselineRevisionId: string;
67
- finalRevisionId: string;
68
- verificationId: string;
69
- boundPaths: string[];
70
- state: TransactionState;
71
- code?: string;
72
- pendingIntent?: PendingIntent;
73
- rollbackRetained: boolean;
74
- externalPaths: string[];
75
- retainedHashes: string[];
76
- releasedHashes: string[];
77
- createdDirectories: string[];
78
- steps: TransactionStep[];
79
- events: TransactionEvent[];
80
- }
81
-
82
- interface JournalRow {
83
- journal_json: string;
84
- journal_hash: string;
85
- }
86
-
87
- export interface CumulativeVerificationExecutionInput {
88
- root: string;
89
- revisionId: string;
90
- verificationId: string;
91
- signal?: AbortSignal;
92
- }
93
-
94
- export type CumulativeVerificationExecutionResult =
95
- | {
96
- ok: true;
97
- exitCode: 0;
98
- classification: string;
99
- }
100
- | {
101
- ok: false;
102
- kind:
103
- | "artifact"
104
- | "environment"
105
- | "verification"
106
- | "verification-adapter"
107
- | "approval-boundary"
108
- | "cancelled";
109
- code: string;
110
- };
111
-
112
- export interface VerifyCumulativeRevisionInput {
113
- workspaceStore: WorkspaceStore;
114
- revisionId: string;
115
- verificationId: string;
116
- execute: (
117
- input: CumulativeVerificationExecutionInput,
118
- ) =>
119
- | CumulativeVerificationExecutionResult
120
- | Promise<CumulativeVerificationExecutionResult>;
121
- signal?: AbortSignal;
122
- }
123
-
124
- interface CumulativeVerificationFact {
125
- readonly revisionId: string;
126
- readonly verificationId: string;
127
- }
128
-
129
- const verificationFacts = new WeakMap<
130
- object,
131
- CumulativeVerificationFact & { workspaceStore: WorkspaceStore }
132
- >();
133
-
134
- export type CumulativeVerificationResult =
135
- | { ok: true; fact: object; revisionId: string; verificationId: string }
136
- | {
137
- ok: false;
138
- kind:
139
- | "artifact"
140
- | "environment"
141
- | "verification"
142
- | "verification-adapter"
143
- | "approval-boundary"
144
- | "cancelled";
145
- code: string;
146
- verificationId: string;
147
- };
148
-
149
- export interface ApplyTransactionHooks {
150
- beforeFileMutation?: (input: {
151
- transactionId: string;
152
- path: string;
153
- index: number;
154
- }) => void | Promise<void>;
155
- afterFileMutationBeforeSave?: (input: {
156
- transactionId: string;
157
- path: string;
158
- index: number;
159
- }) => void | Promise<void>;
160
- afterFileMutation?: (input: {
161
- transactionId: string;
162
- path: string;
163
- index: number;
164
- }) => void | Promise<void>;
165
- /** Production executors settle their process scope before cancellation returns. */
166
- awaitPostApplySettlement?: boolean;
167
- postApply?: (input: {
168
- transactionId: string;
169
- root: string;
170
- signal: AbortSignal;
171
- }) =>
172
- | { ok: true }
173
- | { ok: false; code: string }
174
- | Promise<{ ok: true } | { ok: false; code: string }>;
175
- }
176
-
177
- export interface ApplyTransactionOptions {
178
- root: string;
179
- artifacts: ArtifactStore;
180
- workspaces: WorkspaceStore;
181
- hooks?: ApplyTransactionHooks;
182
- databasePath?: string;
183
- }
184
-
185
- export interface PrepareApplyInput {
186
- transactionId: string;
187
- consumerRoot: string;
188
- baselineRevisionId: string;
189
- finalRevisionId: string;
190
- boundPaths: string[];
191
- verificationFact: unknown;
192
- }
193
-
194
- export type ApplyTransactionOutcome =
195
- | {
196
- ok: true;
197
- transactionId: string;
198
- state: "prepared" | "completed";
199
- replayed?: true;
200
- }
201
- | {
202
- ok: false;
203
- transactionId: string;
204
- state: "paused" | "discarded";
205
- code: string;
206
- replayed?: true;
207
- };
208
-
209
- function hashBytes(bytes: Uint8Array): string {
210
- return createHash("sha256").update(bytes).digest("hex");
211
- }
212
-
213
- function journalHash(journal: TransactionJournal): string {
214
- return createHash("sha256")
215
- .update("cadence-apply-transaction\0")
216
- .update(JSON.stringify(journal))
217
- .digest("hex");
218
- }
219
-
220
- function requireIdentifier(value: string): void {
221
- if (!IDENTIFIER.test(value)) throw new Error("transaction-id-invalid");
222
- }
223
-
224
- function requireRelative(relative: string): void {
225
- if (!isValidRelativePath(relative) || relative === ".") {
226
- throw new Error("transaction-path-invalid");
227
- }
228
- }
229
-
230
- function ensurePrivateDirectory(directory: string): void {
231
- if (!path.isAbsolute(directory)) throw new Error("transaction-root-invalid");
232
- let cursor = path.resolve(directory);
233
- const missing: string[] = [];
234
- for (;;) {
235
- const stat = lstatSync(cursor, { throwIfNoEntry: false });
236
- if (stat) {
237
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
238
- throw new Error("transaction-root-invalid");
239
- }
240
- break;
241
- }
242
- missing.push(cursor);
243
- const parent = path.dirname(cursor);
244
- if (parent === cursor) throw new Error("transaction-root-invalid");
245
- cursor = parent;
246
- }
247
- for (const target of missing.reverse()) {
248
- mkdirSync(target, { mode: 0o700 });
249
- chmodSync(target, 0o700);
250
- }
251
- chmodSync(directory, 0o700);
252
- }
253
-
254
- function entryEqual(left: WorkspaceEntry, right: WorkspaceEntry): boolean {
255
- if (left.kind !== right.kind) return false;
256
- return (
257
- left.kind === "absent" ||
258
- (right.kind === "file" &&
259
- left.hash === right.hash &&
260
- left.bytes === right.bytes &&
261
- left.mode === right.mode)
262
- );
263
- }
264
-
265
- function normalizedEntry(entry: WorkspaceEntry | undefined): WorkspaceEntry {
266
- return entry ? structuredClone(entry) : { kind: "absent" };
267
- }
268
-
269
- function isContainedBy(parent: string, candidate: string): boolean {
270
- const relative = path.relative(parent, candidate);
271
- return (
272
- relative === "" ||
273
- (!relative.startsWith("..") && !path.isAbsolute(relative))
274
- );
275
- }
276
-
277
- function canonicalConsumerRoot(input: string): string {
278
- const absolute = path.resolve(input);
279
- const stat = lstatSync(absolute, { throwIfNoEntry: false });
280
- if (!stat?.isDirectory() || stat.isSymbolicLink()) {
281
- throw new Error("consumer-root-invalid");
282
- }
283
- return realpathSync.native(absolute);
284
- }
285
-
286
- function currentEntry(
287
- root: string,
288
- relative: string,
289
- ): WorkspaceEntry | "unsafe" {
290
- const observation = observeSafePath(root, relative);
291
- if (observation.kind === "absent") return { kind: "absent" };
292
- if (observation.kind !== "file") return "unsafe";
293
- const absolute = path.join(root, ...relative.split("/"));
294
- const bytes = readFileSync(absolute);
295
- const stat = lstatSync(absolute);
296
- return {
297
- kind: "file",
298
- hash: hashBytes(bytes),
299
- bytes: bytes.byteLength,
300
- mode: stat.mode & 0o777,
301
- };
302
- }
303
-
304
- function mainMatches(
305
- root: string,
306
- relative: string,
307
- expected: WorkspaceEntry,
308
- ): boolean {
309
- const current = currentEntry(root, relative);
310
- return current !== "unsafe" && entryEqual(current, expected);
311
- }
312
-
313
- function fileEntryAt(target: string): WorkspaceEntry | "unsafe" | "absent" {
314
- const stat = lstatSync(target, { throwIfNoEntry: false });
315
- if (!stat) return "absent";
316
- if (!stat.isFile() || stat.isSymbolicLink()) return "unsafe";
317
- const bytes = readFileSync(target);
318
- const confirmed = lstatSync(target, { throwIfNoEntry: false });
319
- if (
320
- !confirmed?.isFile() ||
321
- confirmed.isSymbolicLink() ||
322
- confirmed.dev !== stat.dev ||
323
- confirmed.ino !== stat.ino
324
- ) {
325
- return "unsafe";
326
- }
327
- return {
328
- kind: "file",
329
- hash: hashBytes(bytes),
330
- bytes: bytes.byteLength,
331
- mode: confirmed.mode & 0o777,
332
- };
333
- }
334
-
335
- function restoreClaim(claim: string, target: string): boolean {
336
- try {
337
- linkSync(claim, target);
338
- } catch (error) {
339
- if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
340
- throw error;
341
- }
342
- unlinkSync(claim);
343
- syncDirectory(path.dirname(target));
344
- return true;
345
- }
346
-
347
- async function raceWithAbort<T>(
348
- execution: Promise<T>,
349
- signal: AbortSignal,
350
- ): Promise<T> {
351
- let removeAbort = () => {};
352
- const aborted = new Promise<never>((_resolve, reject) => {
353
- const onAbort = () =>
354
- reject(
355
- signal.reason instanceof Error
356
- ? signal.reason
357
- : new Error("operation-cancelled"),
358
- );
359
- removeAbort = () => signal.removeEventListener("abort", onAbort);
360
- if (signal.aborted) onAbort();
361
- else signal.addEventListener("abort", onAbort, { once: true });
362
- });
363
- try {
364
- return await Promise.race([execution, aborted]);
365
- } finally {
366
- removeAbort();
367
- }
368
- }
369
-
370
- function safeTargetPath(root: string, relative: string): string {
371
- requireRelative(relative);
372
- const segments = relative.split("/");
373
- let parent = root;
374
- for (const segment of segments.slice(0, -1)) {
375
- parent = path.join(parent, segment);
376
- const stat = lstatSync(parent, { throwIfNoEntry: false });
377
- if (!stat) break;
378
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
379
- throw new Error("transaction-target-unsafe");
380
- }
381
- }
382
- return path.join(root, ...segments);
383
- }
384
-
385
- function writeExclusiveFile(
386
- target: string,
387
- bytes: Uint8Array,
388
- mode: number,
389
- ): boolean {
390
- const directory = path.dirname(target);
391
- const temporary = path.join(
392
- directory,
393
- `.${path.basename(target)}.${process.pid}.${randomUUID()}.apply-tmp`,
394
- );
395
- const descriptor = openSync(
396
- temporary,
397
- constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY,
398
- 0o600,
399
- );
400
- try {
401
- writeFileSync(descriptor, bytes);
402
- fsyncSync(descriptor);
403
- } finally {
404
- closeSync(descriptor);
405
- }
406
- try {
407
- chmodSync(temporary, mode);
408
- try {
409
- linkSync(temporary, target);
410
- } catch (error) {
411
- if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
412
- throw error;
413
- }
414
- unlinkSync(temporary);
415
- syncDirectory(directory);
416
- return true;
417
- } catch (error) {
418
- removePathSync(temporary, { force: true });
419
- throw error;
420
- } finally {
421
- removePathSync(temporary, { force: true });
422
- }
423
- }
424
-
425
- function event(
426
- journal: TransactionJournal,
427
- kind: string,
428
- relative?: string,
429
- ): void {
430
- journal.events.push({
431
- sequence: journal.events.length + 1,
432
- kind,
433
- ...(relative ? { path: relative } : {}),
434
- });
435
- }
436
-
437
- function requireRevisionId(value: string): void {
438
- if (!SHA256.test(value)) throw new Error("workspace-revision-id-invalid");
439
- }
440
-
441
- function validateJournal(value: unknown): TransactionJournal {
442
- if (!value || typeof value !== "object" || Array.isArray(value)) {
443
- throw new Error("transaction-journal-invalid");
444
- }
445
- const journal = value as TransactionJournal;
446
- if (
447
- !IDENTIFIER.test(journal.transactionId) ||
448
- !path.isAbsolute(journal.consumerRoot) ||
449
- !SHA256.test(journal.baselineRevisionId) ||
450
- !SHA256.test(journal.finalRevisionId) ||
451
- !IDENTIFIER.test(journal.verificationId) ||
452
- !Array.isArray(journal.boundPaths) ||
453
- ![
454
- "prepared",
455
- "applying",
456
- "recovering",
457
- "paused",
458
- "discarded",
459
- "completed",
460
- ].includes(journal.state) ||
461
- typeof journal.rollbackRetained !== "boolean" ||
462
- !Array.isArray(journal.externalPaths) ||
463
- !Array.isArray(journal.retainedHashes) ||
464
- !Array.isArray(journal.releasedHashes) ||
465
- !Array.isArray(journal.createdDirectories) ||
466
- !Array.isArray(journal.steps) ||
467
- !Array.isArray(journal.events)
468
- ) {
469
- throw new Error("transaction-journal-invalid");
470
- }
471
- if (
472
- new Set(journal.createdDirectories).size !==
473
- journal.createdDirectories.length ||
474
- journal.createdDirectories.some((relative) => {
475
- try {
476
- requireRelative(relative);
477
- return false;
478
- } catch {
479
- return true;
480
- }
481
- })
482
- ) {
483
- throw new Error("transaction-journal-invalid");
484
- }
485
- if (
486
- journal.boundPaths.length === 0 ||
487
- new Set(journal.boundPaths).size !== journal.boundPaths.length ||
488
- journal.boundPaths.some((relative) => {
489
- try {
490
- requireRelative(relative);
491
- return false;
492
- } catch {
493
- return true;
494
- }
495
- })
496
- ) {
497
- throw new Error("transaction-journal-invalid");
498
- }
499
- for (const step of journal.steps) {
500
- requireRelative(step.path);
501
- if (
502
- !["pending", "applying", "applied", "rolled-back"].includes(step.state)
503
- ) {
504
- throw new Error("transaction-journal-invalid");
505
- }
506
- }
507
- if (
508
- journal.steps.length !== journal.boundPaths.length ||
509
- journal.steps.some((step, index) => step.path !== journal.boundPaths[index])
510
- ) {
511
- throw new Error("transaction-journal-invalid");
512
- }
513
- return journal;
514
- }
515
-
516
- function isAncestor(
517
- workspaces: WorkspaceStore,
518
- ancestorId: string,
519
- revision: WorkspaceRevision,
520
- ): boolean {
521
- let cursor: WorkspaceRevision | null = revision;
522
- const visited = new Set<string>();
523
- while (cursor) {
524
- if (cursor.revisionId === ancestorId) return true;
525
- if (visited.has(cursor.revisionId)) {
526
- throw new Error("workspace-revision-cycle");
527
- }
528
- visited.add(cursor.revisionId);
529
- cursor = cursor.parentRevisionId
530
- ? workspaces.getRevision(cursor.parentRevisionId)
531
- : null;
532
- }
533
- return false;
534
- }
535
-
536
- /** Materialize and execute verification without exposing a caller-forgeable fact. */
537
- export async function verifyCumulativeRevision(
538
- input: VerifyCumulativeRevisionInput,
539
- ): Promise<CumulativeVerificationResult> {
540
- requireRevisionId(input.revisionId);
541
- requireIdentifier(input.verificationId);
542
- const revision = input.workspaceStore.getRevision(input.revisionId);
543
- const temporary = mkdtempSync(
544
- path.join(tmpdir(), "cadence-cumulative-verification-"),
545
- );
546
- chmodSync(temporary, 0o700);
547
- try {
548
- await input.workspaceStore.materializeAsync(
549
- revision.revisionId,
550
- temporary,
551
- input.signal,
552
- );
553
- if (input.signal?.aborted) {
554
- return {
555
- ok: false,
556
- kind: "cancelled",
557
- code: "cancelled",
558
- verificationId: input.verificationId,
559
- };
560
- }
561
- const result = await input.execute({
562
- root: temporary,
563
- revisionId: revision.revisionId,
564
- verificationId: input.verificationId,
565
- signal: input.signal,
566
- });
567
- if (!result.ok) {
568
- return {
569
- ok: false,
570
- kind: result.kind,
571
- code: result.code,
572
- verificationId: input.verificationId,
573
- };
574
- }
575
- if (result.exitCode !== 0 || result.classification.length === 0) {
576
- throw new Error("verification-outcome-invalid");
577
- }
578
- const fact = Object.freeze({
579
- revisionId: revision.revisionId,
580
- verificationId: input.verificationId,
581
- });
582
- verificationFacts.set(fact, {
583
- ...fact,
584
- workspaceStore: input.workspaceStore,
585
- });
586
- return {
587
- ok: true,
588
- fact,
589
- revisionId: revision.revisionId,
590
- verificationId: input.verificationId,
591
- };
592
- } finally {
593
- if (!isExecutionRetained(temporary))
594
- removePathSync(temporary, { recursive: true, force: true });
595
- }
596
- }
597
-
598
- /** Journaled, restart-safe final delivery from one immutable revision. */
599
- export class ApplyTransaction {
600
- readonly root: string;
601
- readonly #artifacts: ArtifactStore;
602
- readonly #workspaces: WorkspaceStore;
603
- readonly #hooks: ApplyTransactionHooks;
604
- readonly #database: DatabaseSync;
605
- readonly #active = new Map<
606
- string,
607
- {
608
- controller: AbortController;
609
- promise: Promise<ApplyTransactionOutcome>;
610
- }
611
- >();
612
- #closed = false;
613
-
614
- constructor(options: ApplyTransactionOptions) {
615
- if (!options || typeof options !== "object") {
616
- throw new Error("transaction-options-invalid");
617
- }
618
- this.root = path.resolve(options.root);
619
- ensurePrivateDirectory(this.root);
620
- this.#artifacts = options.artifacts;
621
- this.#workspaces = options.workspaces;
622
- this.#hooks = options.hooks ?? {};
623
- const databasePath = path.resolve(
624
- options.databasePath ?? path.join(this.root, "apply.sqlite3"),
625
- );
626
- if (!isContainedBy(this.root, databasePath)) {
627
- throw new Error("transaction-database-path-invalid");
628
- }
629
- const existing = lstatSync(databasePath, { throwIfNoEntry: false });
630
- if (existing && (!existing.isFile() || existing.isSymbolicLink())) {
631
- throw new Error("transaction-database-path-invalid");
632
- }
633
- this.#database = new DatabaseSync(databasePath);
634
- configureSqlite(this.#database);
635
- ensureSqliteSchema(this.#database, APPLY_SCHEMA);
636
- chmodSync(databasePath, 0o600);
637
- }
638
-
639
- #save(journal: TransactionJournal): void {
640
- if (this.#closed) throw new Error("transaction-store-closed");
641
- const json = JSON.stringify(journal);
642
- const digest = journalHash(journal);
643
- this.#database
644
- .prepare(
645
- `INSERT INTO apply_transaction_journal (
646
- transaction_id, journal_json, journal_hash
647
- ) VALUES (?, ?, ?)
648
- ON CONFLICT(transaction_id) DO UPDATE SET
649
- journal_json = excluded.journal_json,
650
- journal_hash = excluded.journal_hash`,
651
- )
652
- .run(journal.transactionId, json, digest);
653
- }
654
-
655
- #load(transactionId: string): TransactionJournal {
656
- if (this.#closed) throw new Error("transaction-store-closed");
657
- requireIdentifier(transactionId);
658
- const row = this.#database
659
- .prepare(
660
- `SELECT journal_json, journal_hash
661
- FROM apply_transaction_journal WHERE transaction_id = ?`,
662
- )
663
- .get(transactionId) as JournalRow | undefined;
664
- if (!row) throw new Error("transaction-unavailable");
665
- let parsed: unknown;
666
- try {
667
- parsed = JSON.parse(row.journal_json);
668
- } catch {
669
- throw new Error("transaction-journal-invalid");
670
- }
671
- const journal = validateJournal(parsed);
672
- if (journalHash(journal) !== row.journal_hash) {
673
- throw new Error("transaction-journal-integrity-invalid");
674
- }
675
- return structuredClone(journal);
676
- }
677
-
678
- #project(journal: TransactionJournal, replayed = false) {
679
- return {
680
- transactionId: journal.transactionId,
681
- state: journal.state,
682
- ...(journal.code ? { code: journal.code } : {}),
683
- ...(journal.pendingIntent
684
- ? { pendingIntent: journal.pendingIntent }
685
- : {}),
686
- rollbackRetained: journal.rollbackRetained,
687
- externalPaths: [...journal.externalPaths],
688
- steps: journal.steps.map((step) => ({
689
- path: step.path,
690
- state: step.state,
691
- })),
692
- events: journal.events
693
- .filter((entry) => entry.kind === "intent-prepared")
694
- .map((entry) => ({ ...entry })),
695
- ...(replayed ? { replayed: true as const } : {}),
696
- };
697
- }
698
-
699
- #outcome(
700
- journal: TransactionJournal,
701
- replayed = false,
702
- ): ApplyTransactionOutcome {
703
- if (journal.state === "completed" || journal.state === "prepared") {
704
- return {
705
- ok: true,
706
- transactionId: journal.transactionId,
707
- state: journal.state,
708
- ...(replayed ? { replayed: true as const } : {}),
709
- };
710
- }
711
- if (journal.state !== "paused" && journal.state !== "discarded") {
712
- throw new Error("transaction-outcome-nonterminal");
713
- }
714
- return {
715
- ok: false,
716
- transactionId: journal.transactionId,
717
- state: journal.state,
718
- code: journal.code ?? "recovery-required",
719
- ...(replayed ? { replayed: true as const } : {}),
720
- };
721
- }
722
-
723
- #settledOutcome(
724
- journal: TransactionJournal,
725
- ): ApplyTransactionOutcome | undefined {
726
- return journal.state === "completed" ||
727
- journal.state === "paused" ||
728
- journal.state === "discarded"
729
- ? this.#outcome(journal)
730
- : undefined;
731
- }
732
-
733
- async prepare(input: PrepareApplyInput): Promise<ApplyTransactionOutcome> {
734
- requireIdentifier(input.transactionId);
735
- requireRevisionId(input.baselineRevisionId);
736
- requireRevisionId(input.finalRevisionId);
737
- if (!Array.isArray(input.boundPaths) || input.boundPaths.length === 0) {
738
- throw new Error("transaction-bound-paths-invalid");
739
- }
740
- const boundPaths = [...new Set(input.boundPaths)].sort();
741
- if (boundPaths.length !== input.boundPaths.length) {
742
- throw new Error("transaction-bound-paths-invalid");
743
- }
744
- for (const relative of boundPaths) requireRelative(relative);
745
- const fact =
746
- input.verificationFact && typeof input.verificationFact === "object"
747
- ? verificationFacts.get(input.verificationFact)
748
- : undefined;
749
- if (
750
- !fact ||
751
- fact.workspaceStore !== this.#workspaces ||
752
- fact.revisionId !== input.finalRevisionId
753
- ) {
754
- throw new Error("verification-fact-invalid");
755
- }
756
- const consumerRoot = canonicalConsumerRoot(input.consumerRoot);
757
- if (isContainedBy(consumerRoot, this.root)) {
758
- throw new Error("transaction-root-inside-consumer");
759
- }
760
- const existing = this.#database
761
- .prepare(
762
- "SELECT journal_json, journal_hash FROM apply_transaction_journal WHERE transaction_id = ?",
763
- )
764
- .get(input.transactionId) as JournalRow | undefined;
765
- if (existing) {
766
- const journal = this.#load(input.transactionId);
767
- if (
768
- journal.consumerRoot !== consumerRoot ||
769
- journal.baselineRevisionId !== input.baselineRevisionId ||
770
- journal.finalRevisionId !== input.finalRevisionId ||
771
- journal.verificationId !== fact.verificationId ||
772
- JSON.stringify(journal.boundPaths) !== JSON.stringify(boundPaths)
773
- ) {
774
- throw new Error("transaction-id-conflict");
775
- }
776
- return this.#outcome(journal, true);
777
- }
778
-
779
- const baseline = this.#workspaces.getRevision(input.baselineRevisionId);
780
- const finalRevision = this.#workspaces.getRevision(input.finalRevisionId);
781
- if (!isAncestor(this.#workspaces, baseline.revisionId, finalRevision)) {
782
- throw new Error("workspace-revision-base-invalid");
783
- }
784
- for (const relative of boundPaths) {
785
- const expected = normalizedEntry(baseline.entries[relative]);
786
- if (!mainMatches(consumerRoot, relative, expected)) {
787
- return {
788
- ok: false,
789
- transactionId: input.transactionId,
790
- state: "paused",
791
- code: "main-workspace-stale",
792
- };
793
- }
794
- }
795
- const steps = boundPaths.map((relative) => ({
796
- path: relative,
797
- before: normalizedEntry(baseline.entries[relative]),
798
- after: normalizedEntry(finalRevision.entries[relative]),
799
- state: "pending" as const,
800
- }));
801
- const retainedHashes = [
802
- ...new Set(
803
- steps.flatMap((step) =>
804
- [step.before, step.after]
805
- .filter(
806
- (entry): entry is Extract<WorkspaceEntry, { kind: "file" }> =>
807
- entry.kind === "file",
808
- )
809
- .map((entry) => entry.hash),
810
- ),
811
- ),
812
- ].sort();
813
- for (const hash of retainedHashes) this.#artifacts.retain(hash);
814
- const journal: TransactionJournal = {
815
- transactionId: input.transactionId,
816
- consumerRoot,
817
- baselineRevisionId: baseline.revisionId,
818
- finalRevisionId: finalRevision.revisionId,
819
- verificationId: fact.verificationId,
820
- boundPaths,
821
- state: "prepared",
822
- rollbackRetained: true,
823
- externalPaths: [],
824
- retainedHashes,
825
- releasedHashes: [],
826
- createdDirectories: [],
827
- steps,
828
- events: [],
829
- };
830
- event(journal, "intent-prepared");
831
- this.#save(journal);
832
- return this.#outcome(journal);
833
- }
834
-
835
- #ensureTargetParents(journal: TransactionJournal, relative: string): string {
836
- const segments = relative.split("/");
837
- let parent = journal.consumerRoot;
838
- const relativeSegments: string[] = [];
839
- for (const segment of segments.slice(0, -1)) {
840
- parent = path.join(parent, segment);
841
- relativeSegments.push(segment);
842
- const directory = relativeSegments.join("/");
843
- const stat = lstatSync(parent, { throwIfNoEntry: false });
844
- if (stat) {
845
- if (!stat.isDirectory() || stat.isSymbolicLink()) {
846
- throw new Error("transaction-target-unsafe");
847
- }
848
- continue;
849
- }
850
- if (!journal.createdDirectories.includes(directory)) {
851
- journal.createdDirectories.push(directory);
852
- event(journal, "directory-create-intent", directory);
853
- this.#save(journal);
854
- }
855
- try {
856
- mkdirSync(parent, { mode: 0o755 });
857
- } catch (error) {
858
- if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
859
- journal.createdDirectories = journal.createdDirectories.filter(
860
- (candidate) => candidate !== directory,
861
- );
862
- this.#save(journal);
863
- }
864
- const created = lstatSync(parent, { throwIfNoEntry: false });
865
- if (!created?.isDirectory() || created.isSymbolicLink()) {
866
- throw new Error("transaction-target-unsafe");
867
- }
868
- syncDirectory(path.dirname(parent));
869
- if (journal.createdDirectories.includes(directory)) {
870
- event(journal, "directory-created", directory);
871
- this.#save(journal);
872
- }
873
- }
874
- return safeTargetPath(journal.consumerRoot, relative);
875
- }
876
-
877
- #claimPath(journal: TransactionJournal, step: TransactionStep): string {
878
- const target = safeTargetPath(journal.consumerRoot, step.path);
879
- return path.join(
880
- path.dirname(target),
881
- `.${path.basename(target)}.${createHash("sha256")
882
- .update(`${journal.transactionId}\0${step.path}`)
883
- .digest("hex")
884
- .slice(0, 24)}.apply-claim`,
885
- );
886
- }
887
-
888
- #settleClaim(journal: TransactionJournal, step: TransactionStep): boolean {
889
- const target = safeTargetPath(journal.consumerRoot, step.path);
890
- const claim = this.#claimPath(journal, step);
891
- const claimed = fileEntryAt(claim);
892
- if (claimed === "absent") return true;
893
- if (claimed === "unsafe") return false;
894
- const current = currentEntry(journal.consumerRoot, step.path);
895
- if (current !== "unsafe" && current.kind === "absent") {
896
- return restoreClaim(claim, target);
897
- }
898
- if (
899
- current !== "unsafe" &&
900
- (entryEqual(claimed, step.before) || entryEqual(claimed, step.after))
901
- ) {
902
- unlinkSync(claim);
903
- syncDirectory(path.dirname(target));
904
- return true;
905
- }
906
- return false;
907
- }
908
-
909
- #applyEntry(
910
- journal: TransactionJournal,
911
- step: TransactionStep,
912
- expected: WorkspaceEntry,
913
- entry: WorkspaceEntry,
914
- ): boolean {
915
- const target = this.#ensureTargetParents(journal, step.path);
916
- const directory = path.dirname(target);
917
- const claim = this.#claimPath(journal, step);
918
- if (lstatSync(claim, { throwIfNoEntry: false })) {
919
- throw new Error("transaction-claim-unsettled");
920
- }
921
-
922
- if (expected.kind === "file") {
923
- try {
924
- renameSync(target, claim);
925
- } catch (error) {
926
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
927
- throw error;
928
- }
929
- syncDirectory(directory);
930
- const claimed = fileEntryAt(claim);
931
- if (
932
- claimed === "unsafe" ||
933
- claimed === "absent" ||
934
- !entryEqual(claimed, expected)
935
- ) {
936
- if (!restoreClaim(claim, target)) {
937
- throw new Error("transaction-claim-conflict");
938
- }
939
- return false;
940
- }
941
- } else if (lstatSync(target, { throwIfNoEntry: false })) {
942
- return false;
943
- }
944
-
945
- if (entry.kind === "absent") {
946
- if (expected.kind === "file") {
947
- unlinkSync(claim);
948
- syncDirectory(directory);
949
- }
950
- return true;
951
- }
952
- const bytes = this.#artifacts.read(entry.hash);
953
- if (bytes.byteLength !== entry.bytes) {
954
- throw new Error("transaction-artifact-integrity-invalid");
955
- }
956
- const installed = writeExclusiveFile(target, bytes, entry.mode);
957
- if (expected.kind === "file") {
958
- unlinkSync(claim);
959
- syncDirectory(directory);
960
- }
961
- return installed;
962
- }
963
-
964
- async #release(journal: TransactionJournal): Promise<void> {
965
- if (!journal.rollbackRetained) return;
966
- for (const hash of journal.retainedHashes) {
967
- if (journal.releasedHashes.includes(hash)) continue;
968
- this.#artifacts.release(hash);
969
- journal.releasedHashes.push(hash);
970
- this.#save(journal);
971
- }
972
- journal.rollbackRetained = false;
973
- this.#save(journal);
974
- }
975
-
976
- #rollbackCreatedDirectories(
977
- journal: TransactionJournal,
978
- external: Set<string>,
979
- ): void {
980
- const deepestFirst = [...journal.createdDirectories].sort(
981
- (left, right) =>
982
- right.split("/").length - left.split("/").length ||
983
- compareCanonicalStrings(right, left),
984
- );
985
- for (const relative of deepestFirst) {
986
- const observation = observeSafePath(journal.consumerRoot, relative);
987
- if (observation.kind === "absent") continue;
988
- if (observation.kind !== "directory") {
989
- external.add(relative);
990
- continue;
991
- }
992
- const target = path.join(journal.consumerRoot, ...relative.split("/"));
993
- try {
994
- rmdirSync(target);
995
- } catch (error) {
996
- if (
997
- ["ENOTEMPTY", "EEXIST"].includes(
998
- (error as NodeJS.ErrnoException).code ?? "",
999
- )
1000
- ) {
1001
- external.add(relative);
1002
- continue;
1003
- }
1004
- throw error;
1005
- }
1006
- syncDirectory(path.dirname(target));
1007
- event(journal, "directory-rolled-back", relative);
1008
- this.#save(journal);
1009
- }
1010
- }
1011
-
1012
- async #rollback(
1013
- journal: TransactionJournal,
1014
- terminal: {
1015
- state: "paused" | "discarded";
1016
- code: string;
1017
- externalPaths?: string[];
1018
- },
1019
- ): Promise<ApplyTransactionOutcome> {
1020
- journal.state = "recovering";
1021
- event(journal, "rollback-started");
1022
- this.#save(journal);
1023
- const external = new Set(terminal.externalPaths ?? []);
1024
- for (const step of [...journal.steps].reverse()) {
1025
- if (!this.#settleClaim(journal, step)) {
1026
- external.add(step.path);
1027
- continue;
1028
- }
1029
- const current = currentEntry(journal.consumerRoot, step.path);
1030
- if (step.state === "pending") {
1031
- if (current !== "unsafe" && entryEqual(current, step.before)) {
1032
- step.state = "rolled-back";
1033
- this.#save(journal);
1034
- } else {
1035
- external.add(step.path);
1036
- }
1037
- continue;
1038
- }
1039
- if (current !== "unsafe" && entryEqual(current, step.after)) {
1040
- if (!entryEqual(step.before, step.after)) {
1041
- if (!this.#applyEntry(journal, step, step.after, step.before)) {
1042
- external.add(step.path);
1043
- continue;
1044
- }
1045
- }
1046
- step.state = "rolled-back";
1047
- event(journal, "file-rolled-back", step.path);
1048
- this.#save(journal);
1049
- } else if (current !== "unsafe" && entryEqual(current, step.before)) {
1050
- step.state = "rolled-back";
1051
- this.#save(journal);
1052
- } else {
1053
- external.add(step.path);
1054
- }
1055
- }
1056
- this.#rollbackCreatedDirectories(journal, external);
1057
- journal.externalPaths = [...external].sort();
1058
- journal.state = terminal.state;
1059
- journal.code = terminal.code;
1060
- event(journal, "rollback-settled");
1061
- this.#save(journal);
1062
- await this.#release(journal);
1063
- return this.#outcome(journal);
1064
- }
1065
-
1066
- async #driveForward(
1067
- transactionId: string,
1068
- signal?: AbortSignal,
1069
- ): Promise<ApplyTransactionOutcome> {
1070
- let journal = this.#load(transactionId);
1071
- if (journal.pendingIntent) {
1072
- return this.#rollback(journal, {
1073
- state: journal.pendingIntent === "discard" ? "discarded" : "paused",
1074
- code:
1075
- journal.pendingIntent === "discard"
1076
- ? "run-discarded"
1077
- : "operation-cancelled",
1078
- });
1079
- }
1080
- journal.state = journal.state === "recovering" ? "recovering" : "applying";
1081
- event(journal, "apply-started");
1082
- this.#save(journal);
1083
-
1084
- for (const [index, originalStep] of journal.steps.entries()) {
1085
- journal = this.#load(transactionId);
1086
- let step = journal.steps[index] as TransactionStep;
1087
- if (journal.pendingIntent || signal?.aborted) {
1088
- journal.pendingIntent ??= "cancel";
1089
- journal.state = "recovering";
1090
- event(journal, "control-pending");
1091
- this.#save(journal);
1092
- return this.#rollback(journal, {
1093
- state: journal.pendingIntent === "discard" ? "discarded" : "paused",
1094
- code:
1095
- journal.pendingIntent === "discard"
1096
- ? "run-discarded"
1097
- : "operation-cancelled",
1098
- });
1099
- }
1100
- if (!this.#settleClaim(journal, step)) {
1101
- return this.#rollback(journal, {
1102
- state: "paused",
1103
- code: "recovery-required",
1104
- externalPaths: [step.path],
1105
- });
1106
- }
1107
- const current = currentEntry(journal.consumerRoot, step.path);
1108
- if (step.state === "applied") {
1109
- if (current === "unsafe" || !entryEqual(current, step.after)) {
1110
- return this.#rollback(journal, {
1111
- state: "paused",
1112
- code: "recovery-required",
1113
- externalPaths: [step.path],
1114
- });
1115
- }
1116
- continue;
1117
- }
1118
- if (
1119
- step.state === "applying" &&
1120
- current !== "unsafe" &&
1121
- entryEqual(current, step.after)
1122
- ) {
1123
- step.state = "applied";
1124
- event(journal, "file-apply-recovered", step.path);
1125
- this.#save(journal);
1126
- continue;
1127
- }
1128
- if (current === "unsafe" || !entryEqual(current, step.before)) {
1129
- return this.#rollback(journal, {
1130
- state: "paused",
1131
- code: "recovery-required",
1132
- externalPaths: [step.path],
1133
- });
1134
- }
1135
-
1136
- if (entryEqual(step.before, step.after)) {
1137
- step.state = "applied";
1138
- event(journal, "bound-path-checked", step.path);
1139
- this.#save(journal);
1140
- continue;
1141
- }
1142
-
1143
- step.state = "applying";
1144
- event(journal, "file-step-started", step.path);
1145
- this.#save(journal);
1146
- await this.#hooks.beforeFileMutation?.({
1147
- transactionId,
1148
- path: originalStep.path,
1149
- index,
1150
- });
1151
- journal = this.#load(transactionId);
1152
- step = journal.steps[index] as TransactionStep;
1153
- if (journal.pendingIntent || signal?.aborted) {
1154
- journal.pendingIntent ??= "cancel";
1155
- this.#save(journal);
1156
- return this.#rollback(journal, {
1157
- state: journal.pendingIntent === "discard" ? "discarded" : "paused",
1158
- code:
1159
- journal.pendingIntent === "discard"
1160
- ? "run-discarded"
1161
- : "operation-cancelled",
1162
- });
1163
- }
1164
- if (!this.#applyEntry(journal, step, step.before, step.after)) {
1165
- return this.#rollback(journal, {
1166
- state: "paused",
1167
- code: "recovery-required",
1168
- externalPaths: [step.path],
1169
- });
1170
- }
1171
- await this.#hooks.afterFileMutationBeforeSave?.({
1172
- transactionId,
1173
- path: originalStep.path,
1174
- index,
1175
- });
1176
- journal = this.#load(transactionId);
1177
- step = journal.steps[index] as TransactionStep;
1178
- step.state = "applied";
1179
- event(journal, "file-applied", step.path);
1180
- this.#save(journal);
1181
- try {
1182
- await this.#hooks.afterFileMutation?.({
1183
- transactionId,
1184
- path: originalStep.path,
1185
- index,
1186
- });
1187
- } catch (error) {
1188
- journal = this.#load(transactionId);
1189
- const settled = this.#settledOutcome(journal);
1190
- if (settled) return settled;
1191
- if (!journal.pendingIntent && !signal?.aborted) throw error;
1192
- }
1193
- journal = this.#load(transactionId);
1194
- const settled = this.#settledOutcome(journal);
1195
- if (settled) return settled;
1196
- if (journal.pendingIntent || signal?.aborted) {
1197
- journal.pendingIntent ??= "cancel";
1198
- journal.state = "recovering";
1199
- event(journal, "control-pending");
1200
- this.#save(journal);
1201
- return this.#rollback(journal, {
1202
- state: journal.pendingIntent === "discard" ? "discarded" : "paused",
1203
- code:
1204
- journal.pendingIntent === "discard"
1205
- ? "run-discarded"
1206
- : "operation-cancelled",
1207
- });
1208
- }
1209
- const appliedStep = journal.steps[index] as TransactionStep;
1210
- if (
1211
- !mainMatches(journal.consumerRoot, appliedStep.path, appliedStep.after)
1212
- ) {
1213
- return this.#rollback(journal, {
1214
- state: "paused",
1215
- code: "recovery-required",
1216
- externalPaths: [appliedStep.path],
1217
- });
1218
- }
1219
- }
1220
-
1221
- journal = this.#load(transactionId);
1222
- const external = journal.steps
1223
- .filter(
1224
- (step) => !mainMatches(journal.consumerRoot, step.path, step.after),
1225
- )
1226
- .map((step) => step.path);
1227
- if (external.length > 0) {
1228
- return this.#rollback(journal, {
1229
- state: "paused",
1230
- code: "recovery-required",
1231
- externalPaths: external,
1232
- });
1233
- }
1234
- let postApply: { ok: true } | { ok: false; code: string } | undefined;
1235
- try {
1236
- if (this.#hooks.postApply) {
1237
- const isolatedRoot = mkdtempSync(
1238
- path.join(this.root, "post-apply-verification-"),
1239
- );
1240
- try {
1241
- await this.#workspaces.materializeAsync(
1242
- journal.finalRevisionId,
1243
- isolatedRoot,
1244
- signal,
1245
- );
1246
- const execution = Promise.resolve(
1247
- this.#hooks.postApply({
1248
- transactionId,
1249
- root: isolatedRoot,
1250
- signal: signal ?? new AbortController().signal,
1251
- }),
1252
- );
1253
- void execution.catch(() => undefined);
1254
- postApply =
1255
- signal && !this.#hooks.awaitPostApplySettlement
1256
- ? await raceWithAbort(execution, signal)
1257
- : await execution;
1258
- } finally {
1259
- if (!isExecutionRetained(isolatedRoot))
1260
- removePathSync(isolatedRoot, { recursive: true, force: true });
1261
- }
1262
- }
1263
- } catch (error) {
1264
- journal = this.#load(transactionId);
1265
- const settled = this.#settledOutcome(journal);
1266
- if (settled) return settled;
1267
- if (!journal.pendingIntent && !signal?.aborted) throw error;
1268
- }
1269
- if (isExecutionRetained(this.root)) {
1270
- journal = this.#load(transactionId);
1271
- journal.state = "recovering";
1272
- journal.code = "isolation-termination-unconfirmed";
1273
- this.#save(journal);
1274
- return { ok: false, transactionId, state: "paused", code: journal.code };
1275
- }
1276
- journal = this.#load(transactionId);
1277
- const settled = this.#settledOutcome(journal);
1278
- if (settled) return settled;
1279
- if (journal.pendingIntent || signal?.aborted) {
1280
- journal.pendingIntent ??= "cancel";
1281
- journal.state = "recovering";
1282
- event(journal, "control-pending");
1283
- this.#save(journal);
1284
- return this.#rollback(journal, {
1285
- state: journal.pendingIntent === "discard" ? "discarded" : "paused",
1286
- code:
1287
- journal.pendingIntent === "discard"
1288
- ? "run-discarded"
1289
- : "operation-cancelled",
1290
- });
1291
- }
1292
- const postApplyExternal = journal.steps
1293
- .filter(
1294
- (step) => !mainMatches(journal.consumerRoot, step.path, step.after),
1295
- )
1296
- .map((step) => step.path);
1297
- if (postApplyExternal.length > 0) {
1298
- return this.#rollback(journal, {
1299
- state: "paused",
1300
- code: "recovery-required",
1301
- externalPaths: postApplyExternal,
1302
- });
1303
- }
1304
- if (postApply && !postApply.ok) {
1305
- return this.#rollback(journal, {
1306
- state: "paused",
1307
- code: postApply.code,
1308
- });
1309
- }
1310
- journal.state = "completed";
1311
- delete journal.code;
1312
- event(journal, "transaction-completed");
1313
- this.#save(journal);
1314
- await this.#release(journal);
1315
- return this.#outcome(journal);
1316
- }
1317
-
1318
- async apply(
1319
- transactionId: string,
1320
- signal?: AbortSignal,
1321
- ): Promise<ApplyTransactionOutcome> {
1322
- const journal = this.#load(transactionId);
1323
- if (journal.state === "completed") {
1324
- if (journal.rollbackRetained) await this.#release(journal);
1325
- return this.#outcome(journal, true);
1326
- }
1327
- if (journal.state === "paused" || journal.state === "discarded") {
1328
- return this.#outcome(journal, true);
1329
- }
1330
- try {
1331
- return await this.#drive(transactionId, signal);
1332
- } catch (error) {
1333
- const interrupted = this.#load(transactionId);
1334
- if (
1335
- interrupted.state !== "completed" &&
1336
- interrupted.state !== "paused" &&
1337
- interrupted.state !== "discarded"
1338
- ) {
1339
- interrupted.state = "recovering";
1340
- event(interrupted, "apply-interrupted");
1341
- this.#save(interrupted);
1342
- }
1343
- throw error;
1344
- }
1345
- }
1346
-
1347
- async recover(transactionId: string): Promise<ApplyTransactionOutcome> {
1348
- const journal = this.#load(transactionId);
1349
- if (
1350
- journal.state === "completed" ||
1351
- journal.state === "paused" ||
1352
- journal.state === "discarded"
1353
- ) {
1354
- if (journal.rollbackRetained) await this.#release(journal);
1355
- return this.#outcome(journal, true);
1356
- }
1357
- return this.#drive(transactionId);
1358
- }
1359
-
1360
- #drive(
1361
- transactionId: string,
1362
- parentSignal?: AbortSignal,
1363
- ): Promise<ApplyTransactionOutcome> {
1364
- const existing = this.#active.get(transactionId);
1365
- if (existing) return existing.promise;
1366
- if (isExecutionRetained(this.root))
1367
- return Promise.resolve({
1368
- ok: false,
1369
- transactionId,
1370
- state: "paused",
1371
- code: "isolation-termination-unconfirmed",
1372
- });
1373
- const controller = new AbortController();
1374
- const forwardAbort = () => {
1375
- if (!controller.signal.aborted) {
1376
- controller.abort(
1377
- parentSignal?.reason ?? new Error("operation-cancelled"),
1378
- );
1379
- }
1380
- };
1381
- if (parentSignal?.aborted) forwardAbort();
1382
- else parentSignal?.addEventListener("abort", forwardAbort, { once: true });
1383
- let active!: {
1384
- controller: AbortController;
1385
- promise: Promise<ApplyTransactionOutcome>;
1386
- };
1387
- const promise = this.#driveForward(
1388
- transactionId,
1389
- controller.signal,
1390
- ).finally(() => {
1391
- parentSignal?.removeEventListener("abort", forwardAbort);
1392
- if (this.#active.get(transactionId) === active) {
1393
- this.#active.delete(transactionId);
1394
- }
1395
- });
1396
- active = { controller, promise };
1397
- this.#active.set(transactionId, active);
1398
- return active.promise;
1399
- }
1400
-
1401
- requestControl(transactionId: string, intent: PendingIntent) {
1402
- if (intent !== "cancel" && intent !== "discard") {
1403
- throw new Error("transaction-control-invalid");
1404
- }
1405
- const journal = this.#load(transactionId);
1406
- if (journal.state === "completed" || journal.state === "discarded") {
1407
- return this.#project(journal, true);
1408
- }
1409
- journal.pendingIntent = intent;
1410
- journal.state = "recovering";
1411
- event(journal, "control-pending");
1412
- this.#save(journal);
1413
- const active = this.#active.get(transactionId);
1414
- if (active && !active.controller.signal.aborted) {
1415
- active.controller.abort(
1416
- new Error(
1417
- intent === "discard" ? "run-discarded" : "operation-cancelled",
1418
- ),
1419
- );
1420
- }
1421
- return this.#project(journal);
1422
- }
1423
-
1424
- status(transactionId: string) {
1425
- return this.#project(this.#load(transactionId));
1426
- }
1427
-
1428
- close(): void {
1429
- if (this.#closed) return;
1430
- this.#closed = true;
1431
- this.#database.close();
1432
- }
1433
-
1434
- [Symbol.dispose](): void {
1435
- this.close();
1436
- }
1437
- }