@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,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
- }