@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,1338 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { chmodSync, lstatSync, mkdirSync } from "node:fs";
3
- import path from "node:path";
4
- import { DatabaseSync } from "node:sqlite";
5
- import type { ArtifactStore } from "./artifact-store.ts";
6
- import { compareCanonicalStrings } from "./canonical.ts";
7
- import {
8
- diffWritePaths,
9
- IMPLEMENTATION_PHASES,
10
- type ImplementationPhase,
11
- isValidRelativePath,
12
- } from "./contracts.ts";
13
- import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
14
- import { TASK_SCHEMA } from "./storage-schema.ts";
15
-
16
- export const TASK_LEDGER_LIMITS = {
17
- maxSegmentBytes: 128 * 1024,
18
- maxCandidateBytes: 8 * 1024 * 1024,
19
- maxProjectionBytes: 64 * 1024,
20
- maxObjectiveBytes: 4 * 1024,
21
- maxEventsPerTask: 512,
22
- } as const;
23
-
24
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
25
- const SHA256 = /^[a-f0-9]{64}$/u;
26
- const CLASSIFICATIONS = new Set([
27
- "expected-red",
28
- "expected-green",
29
- "expected-refactor",
30
- ]);
31
- const ZERO_HASH = "0".repeat(64);
32
-
33
- export interface CandidateIdentity {
34
- candidateId: string;
35
- runId: string;
36
- deliveryRevision: number;
37
- taskId: string;
38
- phase: ImplementationPhase;
39
- attemptId: string;
40
- approvedPaths: string[];
41
- isolatedRevisionId: string;
42
- verificationId: string;
43
- routeId: string;
44
- routeFingerprint: string;
45
- }
46
-
47
- interface CandidateRow {
48
- identity_json: string;
49
- state: string;
50
- next_sequence: number;
51
- total_bytes: number;
52
- artifact_hash: string | null;
53
- candidate_hash: string | null;
54
- segment_count: number | null;
55
- paths_json: string | null;
56
- pause_code: string | null;
57
- }
58
-
59
- interface SegmentRow {
60
- sequence: number;
61
- artifact_hash: string;
62
- size_bytes: number;
63
- segment_hash: string;
64
- }
65
-
66
- interface TaskRow {
67
- delivery_revision: number;
68
- boundary_hash: string;
69
- objective: string;
70
- context_refs_json: string;
71
- initial_phase: string;
72
- }
73
-
74
- interface EventRow {
75
- ordinal: number;
76
- event_id: string;
77
- event_json: string;
78
- prior_hash: string;
79
- event_hash: string;
80
- }
81
-
82
- export interface TaskLedgerOptions {
83
- root: string;
84
- artifacts: ArtifactStore;
85
- databasePath?: string;
86
- }
87
-
88
- export interface BeginCandidateInput extends CandidateIdentity {}
89
-
90
- export interface AppendCandidateSegmentInput extends CandidateIdentity {
91
- sequence: number;
92
- bytes: Uint8Array;
93
- segmentHash: string;
94
- }
95
-
96
- export interface SealCandidateInput extends CandidateIdentity {
97
- segmentCount: number;
98
- totalBytes: number;
99
- candidateHash: string;
100
- }
101
-
102
- export type AppendCandidateSegmentResult =
103
- | {
104
- ok: true;
105
- state: "unsealed";
106
- nextSequence: number;
107
- totalBytes: number;
108
- }
109
- | {
110
- ok: false;
111
- state: "paused";
112
- code: string;
113
- limitBytes: number;
114
- replayed?: true;
115
- };
116
-
117
- export type SealCandidateResult =
118
- | {
119
- ok: true;
120
- state: "sealed";
121
- artifactHash: string;
122
- bytes: number;
123
- paths: string[];
124
- replayed?: true;
125
- }
126
- | {
127
- ok: false;
128
- state: "paused";
129
- code: string;
130
- limitBytes: number;
131
- }
132
- | {
133
- ok: false;
134
- state: "unsealed";
135
- code: "candidate-incomplete";
136
- };
137
-
138
- interface TaskContextReference {
139
- kind: "path";
140
- path: string;
141
- hash: string;
142
- }
143
-
144
- export interface OpenTaskInput {
145
- runId: string;
146
- deliveryRevision: number;
147
- taskId: string;
148
- boundaryHash: string;
149
- objective: string;
150
- contextRefs: TaskContextReference[];
151
- initialPhase: ImplementationPhase;
152
- }
153
-
154
- interface PhaseVerifiedEvent {
155
- runId: string;
156
- taskId: string;
157
- eventId: string;
158
- kind: "phase-verified";
159
- phase: ImplementationPhase;
160
- commandId: string;
161
- exitCode: number;
162
- expectedClassification: string;
163
- actualClassification: string;
164
- diagnostic: { kind: "assertion" | "compiler"; id: string };
165
- artifactHash: string;
166
- isolatedRevisionId: string;
167
- outputFacts: Array<
168
- | { path: string; kind: "absent" }
169
- | { path: string; kind: "file"; hash: string; bytes: number }
170
- >;
171
- routeId: string;
172
- routeFingerprint: string;
173
- }
174
-
175
- interface CorrectionEvent {
176
- runId: string;
177
- taskId: string;
178
- eventId: string;
179
- kind: "artifact-correction";
180
- phase: ImplementationPhase;
181
- correctionCategory: "artifact" | "stale" | "verification" | "checkpoint";
182
- safeFailure: { code: string; stage: string };
183
- candidateId: string;
184
- attemptId: string;
185
- routeId: string;
186
- routeFingerprint: string;
187
- }
188
-
189
- interface RepairVerifiedEvent {
190
- runId: string;
191
- taskId: string;
192
- eventId: string;
193
- kind: "repair-verified";
194
- phase: ImplementationPhase;
195
- attempt: number;
196
- commandId: string;
197
- exitCode: 0;
198
- diagnostic: { kind: "assertion" | "compiler"; id: string };
199
- artifactHash: string;
200
- isolatedRevisionId: string;
201
- outputFacts: PhaseVerifiedEvent["outputFacts"];
202
- attribution: "introduced";
203
- failureIdentities: string[];
204
- routeId: string;
205
- routeFingerprint: string;
206
- }
207
-
208
- interface RouteReboundEvent {
209
- runId: string;
210
- taskId: string;
211
- eventId: string;
212
- kind: "route-rebound";
213
- phase: ImplementationPhase;
214
- routeFingerprint: string;
215
- routeId: string;
216
- attemptId: string;
217
- }
218
-
219
- export type VerifiedTaskEvent =
220
- | PhaseVerifiedEvent
221
- | RepairVerifiedEvent
222
- | CorrectionEvent
223
- | RouteReboundEvent;
224
-
225
- function sha256(bytes: Uint8Array | string): string {
226
- return createHash("sha256").update(bytes).digest("hex");
227
- }
228
-
229
- function hashEvent(
230
- priorHash: string,
231
- ordinal: number,
232
- eventJson: string,
233
- ): string {
234
- return createHash("sha256")
235
- .update("cadence-task-ledger-event\0")
236
- .update(priorHash)
237
- .update("\0")
238
- .update(String(ordinal))
239
- .update("\0")
240
- .update(eventJson)
241
- .digest("hex");
242
- }
243
-
244
- function ensurePrivateDirectory(directory: string): void {
245
- if (!path.isAbsolute(directory)) throw new Error("task-ledger-root-invalid");
246
- const absolute = path.resolve(directory);
247
- let cursor = absolute;
248
- const missing: string[] = [];
249
- for (;;) {
250
- const stat = lstatSync(cursor, { throwIfNoEntry: false });
251
- if (stat) {
252
- if (stat.isSymbolicLink() || !stat.isDirectory()) {
253
- throw new Error("task-ledger-root-invalid");
254
- }
255
- break;
256
- }
257
- missing.push(cursor);
258
- const parent = path.dirname(cursor);
259
- if (parent === cursor) throw new Error("task-ledger-root-invalid");
260
- cursor = parent;
261
- }
262
- for (const target of missing.reverse()) {
263
- mkdirSync(target, { mode: 0o700 });
264
- chmodSync(target, 0o700);
265
- }
266
- chmodSync(absolute, 0o700);
267
- }
268
-
269
- function hasExactKeys(value: object, expected: readonly string[]): boolean {
270
- const actual = Object.keys(value).sort();
271
- return (
272
- actual.length === expected.length &&
273
- expected
274
- .slice()
275
- .sort()
276
- .every((key, index) => actual[index] === key)
277
- );
278
- }
279
-
280
- function requireIdentifier(
281
- value: unknown,
282
- code: string,
283
- ): asserts value is string {
284
- if (typeof value !== "string" || !IDENTIFIER.test(value)) {
285
- throw new Error(code);
286
- }
287
- }
288
-
289
- function requireHash(value: unknown, code: string): asserts value is string {
290
- if (typeof value !== "string" || !SHA256.test(value)) {
291
- throw new Error(code);
292
- }
293
- }
294
-
295
- function requirePhase(value: unknown): asserts value is ImplementationPhase {
296
- if (!(IMPLEMENTATION_PHASES as readonly unknown[]).includes(value)) {
297
- throw new Error("ledger-phase-invalid");
298
- }
299
- }
300
-
301
- function normalizePaths(value: unknown): string[] {
302
- if (
303
- !Array.isArray(value) ||
304
- value.length === 0 ||
305
- value.some(
306
- (entry) =>
307
- !isValidRelativePath(entry) ||
308
- entry === "." ||
309
- typeof entry !== "string",
310
- ) ||
311
- new Set(value).size !== value.length
312
- ) {
313
- throw new Error("candidate-approved-paths-invalid");
314
- }
315
- return [...value].sort();
316
- }
317
-
318
- function candidateIdentity(input: CandidateIdentity): CandidateIdentity {
319
- if (
320
- !hasExactKeys(input, [
321
- "candidateId",
322
- "runId",
323
- "deliveryRevision",
324
- "taskId",
325
- "phase",
326
- "attemptId",
327
- "approvedPaths",
328
- "isolatedRevisionId",
329
- "verificationId",
330
- "routeId",
331
- "routeFingerprint",
332
- ]) ||
333
- !Number.isSafeInteger(input.deliveryRevision) ||
334
- input.deliveryRevision < 1
335
- ) {
336
- throw new Error("candidate-identity-invalid");
337
- }
338
- requireIdentifier(input.candidateId, "candidate-identity-invalid");
339
- requireIdentifier(input.runId, "candidate-identity-invalid");
340
- requireIdentifier(input.taskId, "candidate-identity-invalid");
341
- requireIdentifier(input.attemptId, "candidate-identity-invalid");
342
- requireIdentifier(input.verificationId, "candidate-identity-invalid");
343
- requireIdentifier(input.routeId, "candidate-identity-invalid");
344
- requireHash(input.routeFingerprint, "candidate-identity-invalid");
345
- requirePhase(input.phase);
346
- requireHash(input.isolatedRevisionId, "candidate-identity-invalid");
347
- return {
348
- candidateId: input.candidateId,
349
- runId: input.runId,
350
- deliveryRevision: input.deliveryRevision,
351
- taskId: input.taskId,
352
- phase: input.phase,
353
- attemptId: input.attemptId,
354
- approvedPaths: normalizePaths(input.approvedPaths),
355
- isolatedRevisionId: input.isolatedRevisionId,
356
- verificationId: input.verificationId,
357
- routeId: input.routeId,
358
- routeFingerprint: input.routeFingerprint,
359
- };
360
- }
361
-
362
- function identityFromSegment(
363
- input: AppendCandidateSegmentInput,
364
- ): CandidateIdentity {
365
- const {
366
- sequence: _sequence,
367
- bytes: _bytes,
368
- segmentHash: _segmentHash,
369
- ...identity
370
- } = input;
371
- return candidateIdentity(identity);
372
- }
373
-
374
- function identityFromSeal(input: SealCandidateInput): CandidateIdentity {
375
- const {
376
- segmentCount: _segmentCount,
377
- totalBytes: _totalBytes,
378
- candidateHash: _candidateHash,
379
- ...identity
380
- } = input;
381
- return candidateIdentity(identity);
382
- }
383
-
384
- function sameIdentity(rowJson: string, identity: CandidateIdentity): boolean {
385
- return rowJson === JSON.stringify(identity);
386
- }
387
-
388
- function requireTaskKey(runId: unknown, taskId: unknown): void {
389
- requireIdentifier(runId, "ledger-task-identity-invalid");
390
- requireIdentifier(taskId, "ledger-task-identity-invalid");
391
- }
392
-
393
- function normalizeContextRefs(value: unknown): TaskContextReference[] {
394
- if (!Array.isArray(value) || value.length > 128) {
395
- throw new Error("ledger-context-refs-invalid");
396
- }
397
- const refs = value.map((entry) => {
398
- if (
399
- !entry ||
400
- typeof entry !== "object" ||
401
- Array.isArray(entry) ||
402
- !hasExactKeys(entry, ["kind", "path", "hash"]) ||
403
- (entry as TaskContextReference).kind !== "path" ||
404
- !isValidRelativePath((entry as TaskContextReference).path)
405
- ) {
406
- throw new Error("ledger-context-refs-invalid");
407
- }
408
- requireHash(
409
- (entry as TaskContextReference).hash,
410
- "ledger-context-refs-invalid",
411
- );
412
- return { ...(entry as TaskContextReference) };
413
- });
414
- const identities = refs.map((entry) => `${entry.kind}:${entry.path}`);
415
- if (new Set(identities).size !== identities.length) {
416
- throw new Error("ledger-context-refs-invalid");
417
- }
418
- return refs.sort((left, right) =>
419
- compareCanonicalStrings(left.path, right.path),
420
- );
421
- }
422
-
423
- function safeText(value: unknown, code: string, maximum = 256): string {
424
- if (
425
- typeof value !== "string" ||
426
- value.length === 0 ||
427
- value.length > maximum ||
428
- value.includes("\0") ||
429
- /[\r\n]/u.test(value)
430
- ) {
431
- throw new Error(code);
432
- }
433
- return value;
434
- }
435
-
436
- function normalizePhaseEvent(input: PhaseVerifiedEvent): PhaseVerifiedEvent {
437
- if (
438
- !hasExactKeys(input, [
439
- "runId",
440
- "taskId",
441
- "eventId",
442
- "kind",
443
- "phase",
444
- "commandId",
445
- "exitCode",
446
- "expectedClassification",
447
- "actualClassification",
448
- "diagnostic",
449
- "artifactHash",
450
- "isolatedRevisionId",
451
- "outputFacts",
452
- "routeId",
453
- "routeFingerprint",
454
- ]) ||
455
- !Number.isSafeInteger(input.exitCode) ||
456
- input.exitCode < 0 ||
457
- !CLASSIFICATIONS.has(input.expectedClassification) ||
458
- !CLASSIFICATIONS.has(input.actualClassification) ||
459
- !input.diagnostic ||
460
- typeof input.diagnostic !== "object" ||
461
- Array.isArray(input.diagnostic) ||
462
- !hasExactKeys(input.diagnostic, ["kind", "id"]) ||
463
- !["assertion", "compiler"].includes(input.diagnostic.kind) ||
464
- !Array.isArray(input.outputFacts) ||
465
- input.outputFacts.length > 128
466
- ) {
467
- throw new Error("ledger-event-field-invalid");
468
- }
469
- requirePhase(input.phase);
470
- safeText(input.commandId, "ledger-event-field-invalid", 128);
471
- safeText(input.diagnostic.id, "ledger-event-field-invalid", 512);
472
- requireHash(input.artifactHash, "ledger-event-field-invalid");
473
- requireHash(input.isolatedRevisionId, "ledger-event-field-invalid");
474
- requireIdentifier(input.routeId, "ledger-event-field-invalid");
475
- requireHash(input.routeFingerprint, "ledger-event-field-invalid");
476
- const outputFacts = input.outputFacts.map((fact) => {
477
- if (
478
- !fact ||
479
- typeof fact !== "object" ||
480
- Array.isArray(fact) ||
481
- !isValidRelativePath(fact.path) ||
482
- (fact.kind !== "absent" && fact.kind !== "file")
483
- ) {
484
- throw new Error("ledger-event-field-invalid");
485
- }
486
- if (fact.kind === "absent") {
487
- if (!hasExactKeys(fact, ["path", "kind"])) {
488
- throw new Error("ledger-event-field-invalid");
489
- }
490
- return { path: fact.path, kind: "absent" as const };
491
- }
492
- if (
493
- !hasExactKeys(fact, ["path", "kind", "hash", "bytes"]) ||
494
- !Number.isSafeInteger(fact.bytes) ||
495
- fact.bytes < 0
496
- ) {
497
- throw new Error("ledger-event-field-invalid");
498
- }
499
- requireHash(fact.hash, "ledger-event-field-invalid");
500
- return { ...fact };
501
- });
502
- return {
503
- ...input,
504
- diagnostic: { ...input.diagnostic },
505
- outputFacts,
506
- };
507
- }
508
-
509
- function normalizeVerifiedEvent(input: VerifiedTaskEvent): VerifiedTaskEvent {
510
- if (!input || typeof input !== "object" || Array.isArray(input)) {
511
- throw new Error("ledger-event-field-invalid");
512
- }
513
- requireTaskKey(input.runId, input.taskId);
514
- requireIdentifier(input.eventId, "ledger-event-field-invalid");
515
- if (input.kind === "phase-verified") return normalizePhaseEvent(input);
516
- if (input.kind === "repair-verified") {
517
- if (
518
- !hasExactKeys(input, [
519
- "runId",
520
- "taskId",
521
- "eventId",
522
- "kind",
523
- "phase",
524
- "attempt",
525
- "commandId",
526
- "exitCode",
527
- "diagnostic",
528
- "artifactHash",
529
- "isolatedRevisionId",
530
- "outputFacts",
531
- "attribution",
532
- "failureIdentities",
533
- "routeId",
534
- "routeFingerprint",
535
- ]) ||
536
- !Number.isSafeInteger(input.attempt) ||
537
- input.attempt < 1 ||
538
- input.attempt > 3 ||
539
- input.exitCode !== 0 ||
540
- input.attribution !== "introduced" ||
541
- !Array.isArray(input.failureIdentities) ||
542
- input.failureIdentities.length === 0 ||
543
- input.failureIdentities.length > 256 ||
544
- input.failureIdentities.some(
545
- (identity) => typeof identity !== "string" || !SHA256.test(identity),
546
- )
547
- ) {
548
- throw new Error("ledger-event-field-invalid");
549
- }
550
- const normalizedPhase = normalizePhaseEvent({
551
- runId: input.runId,
552
- taskId: input.taskId,
553
- eventId: input.eventId,
554
- kind: "phase-verified",
555
- phase: input.phase,
556
- commandId: input.commandId,
557
- exitCode: input.exitCode,
558
- expectedClassification: "expected-green",
559
- actualClassification: "expected-green",
560
- diagnostic: input.diagnostic,
561
- artifactHash: input.artifactHash,
562
- isolatedRevisionId: input.isolatedRevisionId,
563
- outputFacts: input.outputFacts,
564
- routeId: input.routeId,
565
- routeFingerprint: input.routeFingerprint,
566
- });
567
- return {
568
- ...input,
569
- diagnostic: normalizedPhase.diagnostic,
570
- outputFacts: normalizedPhase.outputFacts,
571
- failureIdentities: [...new Set(input.failureIdentities)].sort(),
572
- };
573
- }
574
- if (input.kind === "artifact-correction") {
575
- if (
576
- !hasExactKeys(input, [
577
- "runId",
578
- "taskId",
579
- "eventId",
580
- "kind",
581
- "phase",
582
- "correctionCategory",
583
- "safeFailure",
584
- "candidateId",
585
- "attemptId",
586
- "routeId",
587
- "routeFingerprint",
588
- ]) ||
589
- !["artifact", "stale", "verification", "checkpoint"].includes(
590
- input.correctionCategory,
591
- ) ||
592
- !input.safeFailure ||
593
- typeof input.safeFailure !== "object" ||
594
- !hasExactKeys(input.safeFailure, ["code", "stage"])
595
- ) {
596
- throw new Error("ledger-event-field-invalid");
597
- }
598
- requirePhase(input.phase);
599
- safeText(input.safeFailure.code, "ledger-event-field-invalid");
600
- safeText(input.safeFailure.stage, "ledger-event-field-invalid");
601
- requireIdentifier(input.candidateId, "ledger-event-field-invalid");
602
- requireIdentifier(input.attemptId, "ledger-event-field-invalid");
603
- requireIdentifier(input.routeId, "ledger-event-field-invalid");
604
- requireHash(input.routeFingerprint, "ledger-event-field-invalid");
605
- return structuredClone(input);
606
- }
607
- if (input.kind === "route-rebound") {
608
- if (
609
- !hasExactKeys(input, [
610
- "runId",
611
- "taskId",
612
- "eventId",
613
- "kind",
614
- "phase",
615
- "routeFingerprint",
616
- "routeId",
617
- "attemptId",
618
- ])
619
- ) {
620
- throw new Error("ledger-event-field-invalid");
621
- }
622
- requirePhase(input.phase);
623
- requireHash(input.routeFingerprint, "ledger-event-field-invalid");
624
- requireIdentifier(input.routeId, "ledger-event-field-invalid");
625
- requireIdentifier(input.attemptId, "ledger-event-field-invalid");
626
- return { ...input };
627
- }
628
- throw new Error("ledger-event-field-invalid");
629
- }
630
-
631
- const FORBIDDEN_PROJECTION_KEY =
632
- /(?:raw|prompt|transcript|reasoning|model.?output|credential|secret|tool.?history|command.?log)/iu;
633
-
634
- function validateProjectionValue(value: unknown, depth = 0): void {
635
- if (depth > 12) throw new Error("ledger-projection-invalid");
636
- if (
637
- value === null ||
638
- typeof value === "string" ||
639
- typeof value === "number" ||
640
- typeof value === "boolean"
641
- ) {
642
- return;
643
- }
644
- if (Array.isArray(value)) {
645
- if (value.length > 512) throw new Error("ledger-projection-invalid");
646
- for (const entry of value) validateProjectionValue(entry, depth + 1);
647
- return;
648
- }
649
- if (!value || typeof value !== "object") {
650
- throw new Error("ledger-projection-invalid");
651
- }
652
- const entries = Object.entries(value);
653
- if (entries.length > 128) throw new Error("ledger-projection-invalid");
654
- for (const [key, entry] of entries) {
655
- if (FORBIDDEN_PROJECTION_KEY.test(key)) {
656
- throw new Error("ledger-projection-private-field");
657
- }
658
- validateProjectionValue(entry, depth + 1);
659
- }
660
- }
661
-
662
- export function serializeTaskLedgerProjection(value: unknown): string {
663
- validateProjectionValue(value);
664
- const serialized = JSON.stringify(value);
665
- if (Buffer.byteLength(serialized) > TASK_LEDGER_LIMITS.maxProjectionBytes) {
666
- throw new Error("ledger-projection-capacity-exceeded");
667
- }
668
- return serialized;
669
- }
670
-
671
- /** Durable task facts and bounded sealed candidate segments. */
672
- export class TaskLedger {
673
- readonly root: string;
674
- readonly #artifacts: ArtifactStore;
675
- readonly #database: DatabaseSync;
676
- #closed = false;
677
-
678
- constructor(options: TaskLedgerOptions) {
679
- if (!options || typeof options !== "object") {
680
- throw new Error("task-ledger-options-invalid");
681
- }
682
- this.root = path.resolve(options.root);
683
- ensurePrivateDirectory(this.root);
684
- this.#artifacts = options.artifacts;
685
- const databasePath = path.resolve(
686
- options.databasePath ?? path.join(this.root, "task-ledger.sqlite3"),
687
- );
688
- const relative = path.relative(this.root, databasePath);
689
- if (
690
- relative.startsWith("..") ||
691
- path.isAbsolute(relative) ||
692
- databasePath === this.root
693
- ) {
694
- throw new Error("task-ledger-database-invalid");
695
- }
696
- const existing = lstatSync(databasePath, { throwIfNoEntry: false });
697
- if (existing && (!existing.isFile() || existing.isSymbolicLink())) {
698
- throw new Error("task-ledger-database-invalid");
699
- }
700
- this.#database = new DatabaseSync(databasePath);
701
- configureSqlite(this.#database);
702
- ensureSqliteSchema(this.#database, TASK_SCHEMA);
703
- chmodSync(databasePath, 0o600);
704
- }
705
-
706
- #assertOpen(): void {
707
- if (this.#closed) throw new Error("task-ledger-closed");
708
- }
709
-
710
- #candidate(candidateId: string): CandidateRow {
711
- this.#assertOpen();
712
- requireIdentifier(candidateId, "candidate-identity-invalid");
713
- const row = this.#database
714
- .prepare(
715
- `SELECT identity_json, state, next_sequence, total_bytes,
716
- artifact_hash, candidate_hash, segment_count, paths_json,
717
- pause_code
718
- FROM candidates WHERE candidate_id = ?`,
719
- )
720
- .get(candidateId) as CandidateRow | undefined;
721
- if (!row) throw new Error("candidate-unavailable");
722
- return row;
723
- }
724
-
725
- beginCandidate(input: BeginCandidateInput) {
726
- this.#assertOpen();
727
- const identity = candidateIdentity(input);
728
- const json = JSON.stringify(identity);
729
- const existing = this.#database
730
- .prepare(
731
- "SELECT identity_json, state FROM candidates WHERE candidate_id = ?",
732
- )
733
- .get(identity.candidateId) as
734
- | { identity_json: string; state: string }
735
- | undefined;
736
- if (existing) {
737
- if (existing.identity_json !== json) {
738
- throw new Error("candidate-identity-mismatch");
739
- }
740
- return {
741
- ok: true,
742
- candidateId: identity.candidateId,
743
- state: existing.state,
744
- replayed: true,
745
- };
746
- }
747
- this.#database
748
- .prepare(
749
- `INSERT INTO candidates (
750
- candidate_id, identity_json, state, next_sequence, total_bytes
751
- ) VALUES (?, ?, 'unsealed', 0, 0)`,
752
- )
753
- .run(identity.candidateId, json);
754
- return { ok: true, candidateId: identity.candidateId, state: "unsealed" };
755
- }
756
-
757
- appendCandidateSegment(
758
- input: AppendCandidateSegmentInput,
759
- ): AppendCandidateSegmentResult {
760
- this.#assertOpen();
761
- if (
762
- !hasExactKeys(input, [
763
- "candidateId",
764
- "runId",
765
- "deliveryRevision",
766
- "taskId",
767
- "phase",
768
- "attemptId",
769
- "approvedPaths",
770
- "isolatedRevisionId",
771
- "verificationId",
772
- "routeId",
773
- "routeFingerprint",
774
- "sequence",
775
- "bytes",
776
- "segmentHash",
777
- ])
778
- ) {
779
- throw new Error("candidate-segment-invalid");
780
- }
781
- const identity = identityFromSegment(input);
782
- const row = this.#candidate(identity.candidateId);
783
- if (!sameIdentity(row.identity_json, identity)) {
784
- throw new Error("candidate-identity-mismatch");
785
- }
786
- if (row.state === "paused") {
787
- return {
788
- ok: false,
789
- state: "paused",
790
- code: row.pause_code ?? "needs-task-split",
791
- limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
792
- replayed: true,
793
- };
794
- }
795
- if (row.state !== "unsealed") throw new Error("candidate-already-sealed");
796
- if (
797
- !Number.isSafeInteger(input.sequence) ||
798
- input.sequence < 0 ||
799
- input.sequence !== row.next_sequence
800
- ) {
801
- throw new Error("candidate-segment-order-invalid");
802
- }
803
- if (
804
- !(input.bytes instanceof Uint8Array) ||
805
- input.bytes.byteLength === 0 ||
806
- input.bytes.byteLength > TASK_LEDGER_LIMITS.maxSegmentBytes
807
- ) {
808
- throw new Error("candidate-segment-size-invalid");
809
- }
810
- requireHash(input.segmentHash, "candidate-segment-hash-invalid");
811
- const bytes = Buffer.from(input.bytes);
812
- if (sha256(bytes) !== input.segmentHash) {
813
- throw new Error("candidate-segment-hash-invalid");
814
- }
815
- if (
816
- row.total_bytes + bytes.byteLength >
817
- TASK_LEDGER_LIMITS.maxCandidateBytes
818
- ) {
819
- this.#database
820
- .prepare(
821
- "UPDATE candidates SET state = 'paused', pause_code = 'needs-task-split' WHERE candidate_id = ?",
822
- )
823
- .run(identity.candidateId);
824
- return {
825
- ok: false,
826
- state: "paused",
827
- code: "needs-task-split",
828
- limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
829
- };
830
- }
831
- const artifact = this.#artifacts.put(bytes);
832
- this.#artifacts.retain(artifact.hash);
833
- try {
834
- this.#database.exec("BEGIN IMMEDIATE");
835
- this.#database
836
- .prepare(
837
- `INSERT INTO candidate_segments (
838
- candidate_id, sequence, artifact_hash, size_bytes, segment_hash
839
- ) VALUES (?, ?, ?, ?, ?)`,
840
- )
841
- .run(
842
- identity.candidateId,
843
- input.sequence,
844
- artifact.hash,
845
- artifact.bytes,
846
- input.segmentHash,
847
- );
848
- this.#database
849
- .prepare(
850
- `UPDATE candidates
851
- SET next_sequence = next_sequence + 1,
852
- total_bytes = total_bytes + ?
853
- WHERE candidate_id = ?`,
854
- )
855
- .run(artifact.bytes, identity.candidateId);
856
- this.#database.exec("COMMIT");
857
- } catch (error) {
858
- try {
859
- this.#database.exec("ROLLBACK");
860
- } catch {}
861
- this.#artifacts.release(artifact.hash);
862
- throw error;
863
- }
864
- return {
865
- ok: true,
866
- state: "unsealed",
867
- nextSequence: input.sequence + 1,
868
- totalBytes: row.total_bytes + artifact.bytes,
869
- };
870
- }
871
-
872
- #segments(candidateId: string): SegmentRow[] {
873
- return this.#database
874
- .prepare(
875
- `SELECT sequence, artifact_hash, size_bytes, segment_hash
876
- FROM candidate_segments WHERE candidate_id = ? ORDER BY sequence`,
877
- )
878
- .all(candidateId) as unknown as SegmentRow[];
879
- }
880
-
881
- sealCandidate(input: SealCandidateInput): SealCandidateResult {
882
- this.#assertOpen();
883
- if (
884
- !hasExactKeys(input, [
885
- "candidateId",
886
- "runId",
887
- "deliveryRevision",
888
- "taskId",
889
- "phase",
890
- "attemptId",
891
- "approvedPaths",
892
- "isolatedRevisionId",
893
- "verificationId",
894
- "routeId",
895
- "routeFingerprint",
896
- "segmentCount",
897
- "totalBytes",
898
- "candidateHash",
899
- ])
900
- ) {
901
- throw new Error("candidate-seal-invalid");
902
- }
903
- const identity = identityFromSeal(input);
904
- const row = this.#candidate(identity.candidateId);
905
- if (!sameIdentity(row.identity_json, identity)) {
906
- throw new Error("candidate-identity-mismatch");
907
- }
908
- if (
909
- !Number.isSafeInteger(input.segmentCount) ||
910
- input.segmentCount < 1 ||
911
- !Number.isSafeInteger(input.totalBytes) ||
912
- input.totalBytes < 1
913
- ) {
914
- throw new Error("candidate-seal-invalid");
915
- }
916
- requireHash(input.candidateHash, "candidate-seal-hash-invalid");
917
- if (row.state === "sealed") {
918
- if (
919
- row.segment_count !== input.segmentCount ||
920
- row.total_bytes !== input.totalBytes ||
921
- row.candidate_hash !== input.candidateHash
922
- ) {
923
- throw new Error("candidate-seal-conflict");
924
- }
925
- if (!row.artifact_hash) {
926
- throw new Error("candidate-seal-integrity-invalid");
927
- }
928
- return {
929
- ok: true,
930
- state: "sealed",
931
- artifactHash: row.artifact_hash,
932
- bytes: row.total_bytes,
933
- paths: JSON.parse(row.paths_json ?? "[]") as string[],
934
- replayed: true,
935
- };
936
- }
937
- if (row.state === "paused") {
938
- return {
939
- ok: false,
940
- state: "paused",
941
- code: row.pause_code ?? "needs-task-split",
942
- limitBytes: TASK_LEDGER_LIMITS.maxCandidateBytes,
943
- };
944
- }
945
- const segments = this.#segments(identity.candidateId);
946
- if (
947
- segments.length !== input.segmentCount ||
948
- row.next_sequence !== input.segmentCount
949
- ) {
950
- return { ok: false, state: "unsealed", code: "candidate-incomplete" };
951
- }
952
- const buffers = segments.map((segment, index) => {
953
- if (segment.sequence !== index) {
954
- throw new Error("candidate-segment-order-invalid");
955
- }
956
- const bytes = Buffer.from(this.#artifacts.read(segment.artifact_hash));
957
- if (
958
- bytes.byteLength !== segment.size_bytes ||
959
- sha256(bytes) !== segment.segment_hash
960
- ) {
961
- throw new Error("candidate-segment-integrity-invalid");
962
- }
963
- return bytes;
964
- });
965
- const candidate = Buffer.concat(buffers);
966
- if (
967
- candidate.byteLength !== input.totalBytes ||
968
- candidate.byteLength !== row.total_bytes ||
969
- sha256(candidate) !== input.candidateHash
970
- ) {
971
- throw new Error("candidate-seal-hash-invalid");
972
- }
973
- let diff: string;
974
- try {
975
- diff = new TextDecoder("utf-8", { fatal: true }).decode(candidate);
976
- } catch {
977
- throw new Error("candidate-diff-invalid");
978
- }
979
- let paths: string[];
980
- try {
981
- paths = diffWritePaths(diff).paths;
982
- } catch {
983
- throw new Error("candidate-diff-invalid");
984
- }
985
- if (paths.some((relative) => !identity.approvedPaths.includes(relative))) {
986
- throw new Error("candidate-write-set-mismatch");
987
- }
988
- const artifact = this.#artifacts.put(candidate);
989
- this.#artifacts.retain(artifact.hash);
990
- this.#database
991
- .prepare(
992
- `UPDATE candidates
993
- SET state = 'sealed', artifact_hash = ?, candidate_hash = ?,
994
- segment_count = ?, paths_json = ?, pause_code = NULL
995
- WHERE candidate_id = ? AND state = 'unsealed'`,
996
- )
997
- .run(
998
- artifact.hash,
999
- input.candidateHash,
1000
- input.segmentCount,
1001
- JSON.stringify(paths),
1002
- identity.candidateId,
1003
- );
1004
- return {
1005
- ok: true,
1006
- state: "sealed",
1007
- artifactHash: artifact.hash,
1008
- bytes: artifact.bytes,
1009
- paths,
1010
- };
1011
- }
1012
-
1013
- readSealedCandidate(candidateId: string): Uint8Array {
1014
- const row = this.#candidate(candidateId);
1015
- if (row.state !== "sealed" || !row.artifact_hash) {
1016
- throw new Error("candidate-not-sealed");
1017
- }
1018
- const bytes = this.#artifacts.read(row.artifact_hash);
1019
- if (
1020
- bytes.byteLength !== row.total_bytes ||
1021
- sha256(bytes) !== row.candidate_hash
1022
- ) {
1023
- throw new Error("candidate-seal-integrity-invalid");
1024
- }
1025
- return Buffer.from(bytes);
1026
- }
1027
-
1028
- openTask(input: OpenTaskInput) {
1029
- this.#assertOpen();
1030
- if (
1031
- !input ||
1032
- typeof input !== "object" ||
1033
- Array.isArray(input) ||
1034
- !hasExactKeys(input, [
1035
- "runId",
1036
- "deliveryRevision",
1037
- "taskId",
1038
- "boundaryHash",
1039
- "objective",
1040
- "contextRefs",
1041
- "initialPhase",
1042
- ]) ||
1043
- !Number.isSafeInteger(input.deliveryRevision) ||
1044
- input.deliveryRevision < 1 ||
1045
- typeof input.objective !== "string" ||
1046
- input.objective.length === 0 ||
1047
- Buffer.byteLength(input.objective) > TASK_LEDGER_LIMITS.maxObjectiveBytes
1048
- ) {
1049
- throw new Error("ledger-task-invalid");
1050
- }
1051
- requireTaskKey(input.runId, input.taskId);
1052
- requireHash(input.boundaryHash, "ledger-task-invalid");
1053
- requirePhase(input.initialPhase);
1054
- const contextRefs = normalizeContextRefs(input.contextRefs);
1055
- const existing = this.#database
1056
- .prepare(
1057
- `SELECT delivery_revision, boundary_hash, objective,
1058
- context_refs_json, initial_phase
1059
- FROM task_ledger WHERE run_id = ? AND task_id = ?`,
1060
- )
1061
- .get(input.runId, input.taskId) as TaskRow | undefined;
1062
- const expected = {
1063
- delivery_revision: input.deliveryRevision,
1064
- boundary_hash: input.boundaryHash,
1065
- objective: input.objective,
1066
- context_refs_json: JSON.stringify(contextRefs),
1067
- initial_phase: input.initialPhase,
1068
- };
1069
- if (existing) {
1070
- if (JSON.stringify(existing) !== JSON.stringify(expected)) {
1071
- throw new Error("ledger-task-conflict");
1072
- }
1073
- return { accepted: true, replayed: true };
1074
- }
1075
- this.#database
1076
- .prepare(
1077
- `INSERT INTO task_ledger (
1078
- run_id, task_id, delivery_revision, boundary_hash, objective,
1079
- context_refs_json, initial_phase
1080
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
1081
- )
1082
- .run(
1083
- input.runId,
1084
- input.taskId,
1085
- input.deliveryRevision,
1086
- input.boundaryHash,
1087
- input.objective,
1088
- JSON.stringify(contextRefs),
1089
- input.initialPhase,
1090
- );
1091
- return { accepted: true };
1092
- }
1093
-
1094
- #task(runId: string, taskId: string): TaskRow {
1095
- requireTaskKey(runId, taskId);
1096
- const row = this.#database
1097
- .prepare(
1098
- `SELECT delivery_revision, boundary_hash, objective,
1099
- context_refs_json, initial_phase
1100
- FROM task_ledger WHERE run_id = ? AND task_id = ?`,
1101
- )
1102
- .get(runId, taskId) as TaskRow | undefined;
1103
- if (!row) throw new Error("ledger-task-unavailable");
1104
- return row;
1105
- }
1106
-
1107
- #events(runId: string, taskId: string): EventRow[] {
1108
- const rows = this.#database
1109
- .prepare(
1110
- `SELECT ordinal, event_id, event_json, prior_hash, event_hash
1111
- FROM task_events WHERE run_id = ? AND task_id = ? ORDER BY ordinal`,
1112
- )
1113
- .all(runId, taskId) as unknown as EventRow[];
1114
- let prior = ZERO_HASH;
1115
- for (const [index, row] of rows.entries()) {
1116
- if (
1117
- row.ordinal !== index + 1 ||
1118
- row.prior_hash !== prior ||
1119
- row.event_hash !== hashEvent(prior, row.ordinal, row.event_json)
1120
- ) {
1121
- throw new Error("ledger-event-integrity-invalid");
1122
- }
1123
- prior = row.event_hash;
1124
- }
1125
- return rows;
1126
- }
1127
-
1128
- commitVerifiedEvent(input: VerifiedTaskEvent) {
1129
- this.#assertOpen();
1130
- const normalized = normalizeVerifiedEvent(input);
1131
- const task = this.#task(normalized.runId, normalized.taskId);
1132
- const rows = this.#events(normalized.runId, normalized.taskId);
1133
- const existing = rows.find((row) => row.event_id === normalized.eventId);
1134
- const json = JSON.stringify(normalized);
1135
- if (existing) {
1136
- if (existing.event_json !== json)
1137
- throw new Error("ledger-event-conflict");
1138
- return { accepted: true, ordinal: existing.ordinal, replayed: true };
1139
- }
1140
- if (rows.length >= TASK_LEDGER_LIMITS.maxEventsPerTask) {
1141
- throw new Error("ledger-event-capacity-exceeded");
1142
- }
1143
- if (normalized.kind === "phase-verified") {
1144
- if (task.initial_phase === "green" && normalized.phase === "red")
1145
- throw new Error("ledger-phase-order-invalid");
1146
- const completed = rows
1147
- .map((row) => JSON.parse(row.event_json) as VerifiedTaskEvent)
1148
- .filter(
1149
- (event): event is PhaseVerifiedEvent =>
1150
- event.kind === "phase-verified",
1151
- )
1152
- .map((event) => event.phase);
1153
- const required =
1154
- normalized.phase === "green"
1155
- ? task.initial_phase === "green"
1156
- ? undefined
1157
- : "red"
1158
- : normalized.phase === "refactor"
1159
- ? "green"
1160
- : undefined;
1161
- if (required && !completed.includes(required)) {
1162
- throw new Error("ledger-phase-order-invalid");
1163
- }
1164
- if (completed.includes(normalized.phase)) {
1165
- throw new Error("ledger-phase-order-invalid");
1166
- }
1167
- }
1168
- const ordinal = rows.length + 1;
1169
- const priorHash = rows.at(-1)?.event_hash ?? ZERO_HASH;
1170
- const digest = hashEvent(priorHash, ordinal, json);
1171
- this.#database
1172
- .prepare(
1173
- `INSERT INTO task_events (
1174
- run_id, task_id, ordinal, event_id, event_json, prior_hash,
1175
- event_hash
1176
- ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
1177
- )
1178
- .run(
1179
- normalized.runId,
1180
- normalized.taskId,
1181
- ordinal,
1182
- normalized.eventId,
1183
- json,
1184
- priorHash,
1185
- digest,
1186
- );
1187
- return { accepted: true, ordinal };
1188
- }
1189
-
1190
- putDurableFact(factKey: string, fact: unknown): unknown {
1191
- this.#assertOpen();
1192
- requireIdentifier(factKey, "ledger-fact-key-invalid");
1193
- const factJson = serializeTaskLedgerProjection(fact);
1194
- const existing = this.#database
1195
- .prepare("SELECT fact_json FROM durable_facts WHERE fact_key = ?")
1196
- .get(factKey) as { fact_json: string } | undefined;
1197
- if (existing) {
1198
- if (existing.fact_json !== factJson) {
1199
- throw new Error("ledger-fact-conflict");
1200
- }
1201
- return JSON.parse(existing.fact_json) as unknown;
1202
- }
1203
- this.#database
1204
- .prepare("INSERT INTO durable_facts(fact_key, fact_json) VALUES (?, ?)")
1205
- .run(factKey, factJson);
1206
- return JSON.parse(factJson) as unknown;
1207
- }
1208
-
1209
- durableFact(factKey: string): unknown | undefined {
1210
- this.#assertOpen();
1211
- requireIdentifier(factKey, "ledger-fact-key-invalid");
1212
- const row = this.#database
1213
- .prepare("SELECT fact_json FROM durable_facts WHERE fact_key = ?")
1214
- .get(factKey) as { fact_json: string } | undefined;
1215
- return row ? (JSON.parse(row.fact_json) as unknown) : undefined;
1216
- }
1217
-
1218
- replaceDurableFact(
1219
- factKey: string,
1220
- expectedFact: unknown,
1221
- fact: unknown,
1222
- ): unknown {
1223
- this.#assertOpen();
1224
- requireIdentifier(factKey, "ledger-fact-key-invalid");
1225
- const expectedJson = serializeTaskLedgerProjection(expectedFact);
1226
- const factJson = serializeTaskLedgerProjection(fact);
1227
- const result = this.#database
1228
- .prepare(
1229
- "UPDATE durable_facts SET fact_json = ? WHERE fact_key = ? AND fact_json = ?",
1230
- )
1231
- .run(factJson, factKey, expectedJson);
1232
- if (Number(result.changes) !== 1) {
1233
- throw new Error("ledger-fact-conflict");
1234
- }
1235
- return JSON.parse(factJson) as unknown;
1236
- }
1237
-
1238
- commitVerifiedEvents(events: VerifiedTaskEvent[]): void {
1239
- this.#assertOpen();
1240
- if (!Array.isArray(events) || events.length === 0 || events.length > 4) {
1241
- throw new Error("ledger-event-batch-invalid");
1242
- }
1243
- this.#database.exec("BEGIN IMMEDIATE");
1244
- try {
1245
- for (const event of events) this.commitVerifiedEvent(event);
1246
- this.#database.exec("COMMIT");
1247
- } catch (error) {
1248
- this.#database.exec("ROLLBACK");
1249
- throw error;
1250
- }
1251
- }
1252
-
1253
- commitTaskCompletion(input: {
1254
- events: VerifiedTaskEvent[];
1255
- factKey: string;
1256
- fact: unknown;
1257
- }): unknown {
1258
- this.#assertOpen();
1259
- if (
1260
- !input ||
1261
- typeof input !== "object" ||
1262
- Array.isArray(input) ||
1263
- !hasExactKeys(input, ["events", "factKey", "fact"]) ||
1264
- !Array.isArray(input.events) ||
1265
- input.events.length === 0 ||
1266
- input.events.length > 4
1267
- ) {
1268
- throw new Error("ledger-task-completion-invalid");
1269
- }
1270
- requireIdentifier(input.factKey, "ledger-fact-key-invalid");
1271
- serializeTaskLedgerProjection(input.fact);
1272
- this.#database.exec("BEGIN IMMEDIATE");
1273
- try {
1274
- for (const event of input.events) this.commitVerifiedEvent(event);
1275
- const fact = this.putDurableFact(input.factKey, input.fact);
1276
- this.#database.exec("COMMIT");
1277
- return fact;
1278
- } catch (error) {
1279
- this.#database.exec("ROLLBACK");
1280
- throw error;
1281
- }
1282
- }
1283
-
1284
- recordWorkerClaim(_input: Record<string, unknown>) {
1285
- this.#assertOpen();
1286
- return { accepted: false, reason: "worker-claim-untrusted" } as const;
1287
- }
1288
-
1289
- projection(input: {
1290
- runId: string;
1291
- taskId: string;
1292
- nextPhase: ImplementationPhase;
1293
- }) {
1294
- this.#assertOpen();
1295
- if (
1296
- !input ||
1297
- typeof input !== "object" ||
1298
- Array.isArray(input) ||
1299
- !hasExactKeys(input, ["runId", "taskId", "nextPhase"])
1300
- ) {
1301
- throw new Error("ledger-projection-request-invalid");
1302
- }
1303
- requirePhase(input.nextPhase);
1304
- const task = this.#task(input.runId, input.taskId);
1305
- const history = this.#events(input.runId, input.taskId).map((row) => {
1306
- const value = JSON.parse(row.event_json) as VerifiedTaskEvent;
1307
- const {
1308
- runId: _runId,
1309
- taskId: _taskId,
1310
- eventId: _eventId,
1311
- ...fact
1312
- } = value;
1313
- return { ordinal: row.ordinal, ...fact };
1314
- });
1315
- const projection = {
1316
- runId: input.runId,
1317
- taskId: input.taskId,
1318
- deliveryRevision: task.delivery_revision,
1319
- boundaryHash: task.boundary_hash,
1320
- objective: task.objective,
1321
- contextRefs: JSON.parse(task.context_refs_json) as TaskContextReference[],
1322
- currentPhase: input.nextPhase,
1323
- history,
1324
- };
1325
- serializeTaskLedgerProjection(projection);
1326
- return structuredClone(projection);
1327
- }
1328
-
1329
- close(): void {
1330
- if (this.#closed) return;
1331
- this.#closed = true;
1332
- this.#database.close();
1333
- }
1334
-
1335
- [Symbol.dispose](): void {
1336
- this.close();
1337
- }
1338
- }