@abelxiaoxing/cadence 1.8.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. 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
- }