@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,1773 +0,0 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { DatabaseSync } from "node:sqlite";
3
- import { compareCanonicalStrings } from "./canonical.ts";
4
- import {
5
- type ChangeContract,
6
- normalizeChangeContract,
7
- } from "./change-contract.ts";
8
- import {
9
- type DesignEvidenceResult,
10
- validateEvidenceResult,
11
- } from "./contracts.ts";
12
- import { canonicalJson } from "./run-state.ts";
13
- import { configureSqlite, ensureSqliteSchema } from "./sqlite-schema.ts";
14
- import { prepareStateRoot, type ResolvedStateRoot } from "./state-root.ts";
15
- import { DESIGN_SCHEMA as SCHEMA } from "./storage-schema.ts";
16
-
17
- const SHA256 = /^[a-f0-9]{64}$/u;
18
- const IDENTIFIER = /^[a-z0-9](?:[a-z0-9._:-]{0,126}[a-z0-9])?$/iu;
19
- const ZERO_HASH = "0".repeat(64);
20
- const MAX_PLAN_BYTES = 16 * 1024 * 1024;
21
- const MAX_CONTRACT_BYTES = 256 * 1024;
22
-
23
- type DecisionCategory = "behavior" | "technical";
24
- type DesignGate = "gate-a" | "gate-b";
25
-
26
- interface RunRow {
27
- run_id: string;
28
- root_hash: string;
29
- stage: string;
30
- change_name: string | null;
31
- state: string;
32
- }
33
-
34
- interface FactRow {
35
- sequence: number;
36
- kind: string;
37
- identity: string;
38
- payload_json: string;
39
- canonical_hash: string;
40
- prior_record_hash: string;
41
- record_hash: string;
42
- }
43
-
44
- interface OperationRow {
45
- operation: string;
46
- request_hash: string;
47
- outcome_json: string;
48
- }
49
-
50
- export interface DesignJournalOptions {
51
- now?: () => number;
52
- finalizationLeaseMs?: number;
53
- }
54
-
55
- export interface DesignFinalizationLease {
56
- runId: string;
57
- operationId: string;
58
- token: string;
59
- expiresAt: number;
60
- }
61
-
62
- export interface DesignGateProof {
63
- revision: number;
64
- contractHash: string;
65
- recordHash: string;
66
- }
67
-
68
- export interface DesignEvidenceProjection {
69
- packetId: string;
70
- resultHash: string;
71
- recordHash: string;
72
- sequence: number;
73
- moduleName: string;
74
- scope: string[];
75
- filesRead: string[];
76
- citations: Array<{
77
- path: string;
78
- lineStart: number;
79
- lineEnd: number;
80
- claimHash: string;
81
- }>;
82
- }
83
-
84
- export interface DesignDecisionProjection {
85
- decisionId: string;
86
- category: DecisionCategory;
87
- revision: number;
88
- contractHash: string;
89
- refs: string[];
90
- recordHash: string;
91
- sequence: number;
92
- }
93
-
94
- export interface DesignPlanProjection {
95
- revision: number;
96
- rawSha256: string;
97
- canonicalHash: string;
98
- recordHash: string;
99
- sequence: number;
100
- }
101
-
102
- export interface DesignGateProjection {
103
- current: boolean;
104
- proof?: DesignGateProof;
105
- approvedSequence?: number;
106
- staleReason?:
107
- | "behavior-decision-changed"
108
- | "technical-decision-changed"
109
- | "plan-recompiled"
110
- | "gate-a-reapproved"
111
- | "gate-a-stale"
112
- | "plan-missing";
113
- }
114
-
115
- export interface DesignStatusProjection {
116
- changeContract?: ChangeContract;
117
- runId: string;
118
- change?: string;
119
- evidence: DesignEvidenceProjection[];
120
- decisions: DesignDecisionProjection[];
121
- gates: {
122
- gateA: DesignGateProjection;
123
- gateB: DesignGateProjection;
124
- };
125
- plan: DesignPlanProjection | null;
126
- }
127
-
128
- export interface GateProofVerificationInput {
129
- change: string;
130
- gate: DesignGate;
131
- proof: DesignGateProof;
132
- }
133
-
134
- export interface FinalizedDeliveryVerificationInput {
135
- change: string;
136
- deliveryRevision: number;
137
- receiptHash: string;
138
- gateA: DesignGateProof;
139
- gateB: DesignGateProof;
140
- planCanonicalHash: string;
141
- }
142
-
143
- export type DesignArtifactOperationFacts =
144
- | {
145
- runId: string;
146
- operationId: string;
147
- operation: "write-artifact";
148
- path: string;
149
- bytes: number;
150
- rawSha256: string;
151
- }
152
- | {
153
- runId: string;
154
- operationId: string;
155
- operation: "delete-artifact";
156
- path: string;
157
- };
158
-
159
- export type DesignArtifactOperationOutcome =
160
- | {
161
- operation: "write-artifact";
162
- runId: string;
163
- path: string;
164
- bytes: number;
165
- rawSha256: string;
166
- }
167
- | {
168
- operation: "delete-artifact";
169
- runId: string;
170
- path: string;
171
- deleted: false;
172
- }
173
- | {
174
- operation: "delete-artifact";
175
- runId: string;
176
- path: string;
177
- deleted: true;
178
- bytes: number;
179
- rawSha256: string;
180
- };
181
-
182
- export interface DesignArtifactOperationState {
183
- state: "pending" | "committed";
184
- outcome: DesignArtifactOperationOutcome;
185
- }
186
-
187
- function sha256(value: Uint8Array | string): string {
188
- return createHash("sha256").update(value).digest("hex");
189
- }
190
-
191
- function contractHash(domain: string, value: string): string {
192
- if (
193
- typeof value !== "string" ||
194
- value.trim().length === 0 ||
195
- Buffer.byteLength(value, "utf8") > MAX_CONTRACT_BYTES
196
- ) {
197
- throw new Error("design-transient-input-invalid");
198
- }
199
- return createHash("sha256")
200
- .update(domain)
201
- .update("\0")
202
- .update(value.replace(/\r\n?/gu, "\n").trim())
203
- .digest("hex");
204
- }
205
-
206
- function requireIdentifier(value: string, label: string): void {
207
- if (!IDENTIFIER.test(value)) throw new Error(`invalid-${label}`);
208
- }
209
-
210
- function requireHash(value: string, label: string): void {
211
- if (!SHA256.test(value)) throw new Error(`invalid-${label}`);
212
- }
213
-
214
- function parseRecord(value: string, code: string): Record<string, unknown> {
215
- const parsed = JSON.parse(value) as unknown;
216
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
217
- throw new Error(code);
218
- }
219
- return parsed as Record<string, unknown>;
220
- }
221
-
222
- function hasExactKeys(
223
- value: Record<string, unknown>,
224
- keys: readonly string[],
225
- ): boolean {
226
- const actual = Object.keys(value);
227
- return (
228
- actual.length === keys.length && actual.every((key) => keys.includes(key))
229
- );
230
- }
231
-
232
- function normalizeRefs(refs: readonly string[]): string[] {
233
- if (
234
- !Array.isArray(refs) ||
235
- refs.length > 128 ||
236
- refs.some(
237
- (ref) =>
238
- typeof ref !== "string" ||
239
- ref.length === 0 ||
240
- ref.length > 512 ||
241
- ref.trim() !== ref ||
242
- ref.includes("\0") ||
243
- ref.includes("\n") ||
244
- ref.includes("\r"),
245
- )
246
- ) {
247
- throw new Error("design-decision-refs-invalid");
248
- }
249
- return [...new Set(refs)].sort((left, right) =>
250
- compareCanonicalStrings(left, right),
251
- );
252
- }
253
-
254
- export class DesignJournal {
255
- readonly stateRoot: ResolvedStateRoot;
256
- readonly #database: DatabaseSync;
257
- readonly #now: () => number;
258
- readonly #finalizationLeaseMs: number;
259
- #closed = false;
260
-
261
- private constructor(
262
- stateRoot: ResolvedStateRoot,
263
- database: DatabaseSync,
264
- options: DesignJournalOptions,
265
- ) {
266
- this.stateRoot = stateRoot;
267
- this.#database = database;
268
- this.#now = options.now ?? Date.now;
269
- this.#finalizationLeaseMs = options.finalizationLeaseMs ?? 300_000;
270
- }
271
-
272
- static open(
273
- unresolved: ResolvedStateRoot,
274
- options: DesignJournalOptions = {},
275
- ): DesignJournal {
276
- if (
277
- options.finalizationLeaseMs !== undefined &&
278
- (!Number.isSafeInteger(options.finalizationLeaseMs) ||
279
- options.finalizationLeaseMs < 1)
280
- ) {
281
- throw new Error("design-finalization-lease-invalid");
282
- }
283
- const stateRoot = prepareStateRoot(unresolved);
284
- const database = new DatabaseSync(stateRoot.databasePath);
285
- try {
286
- configureSqlite(database);
287
- ensureSqliteSchema(database, SCHEMA);
288
- return new DesignJournal(stateRoot, database, options);
289
- } catch (error) {
290
- try {
291
- database.close();
292
- } catch {
293
- // The Design journal initialization failure remains authoritative.
294
- }
295
- throw error;
296
- }
297
- }
298
-
299
- #assertOpen(): void {
300
- if (this.#closed) throw new Error("design-journal-closed");
301
- }
302
-
303
- #transaction<T>(work: () => T): T {
304
- this.#assertOpen();
305
- this.#database.exec("BEGIN IMMEDIATE");
306
- try {
307
- const result = work();
308
- this.#database.exec("COMMIT");
309
- return result;
310
- } catch (error) {
311
- try {
312
- this.#database.exec("ROLLBACK");
313
- } catch {
314
- // The original operation or commit failure remains authoritative.
315
- }
316
- throw error;
317
- }
318
- }
319
-
320
- #run(runId: string, mutable: boolean): RunRow {
321
- const row = this.#database
322
- .prepare(
323
- `SELECT run_id, root_hash, stage, change_name, state
324
- FROM runs WHERE run_id = ?`,
325
- )
326
- .get(runId) as RunRow | undefined;
327
- if (
328
- !row ||
329
- row.root_hash !== this.stateRoot.consumerRootHash ||
330
- row.stage !== "abel-design" ||
331
- (mutable && ["completed", "discarded", "rejected"].includes(row.state))
332
- ) {
333
- throw new Error("design-run-invalid");
334
- }
335
- return row;
336
- }
337
-
338
- assertDesignRun(runId: string): void {
339
- this.#assertOpen();
340
- this.#run(runId, true);
341
- }
342
-
343
- operationOutcome(
344
- runId: string,
345
- operationId: string,
346
- operation: string,
347
- ): Record<string, unknown> | undefined {
348
- this.#assertOpen();
349
- this.#run(runId, false);
350
- requireIdentifier(operationId, "design-operation-id");
351
- const row = this.#database
352
- .prepare(
353
- `SELECT operation, outcome_json FROM design_operations
354
- WHERE run_id = ? AND operation_id = ?`,
355
- )
356
- .get(runId, operationId) as
357
- | Pick<OperationRow, "operation" | "outcome_json">
358
- | undefined;
359
- if (!row) return undefined;
360
- if (row.operation !== operation)
361
- throw new Error("design-operation-conflict");
362
- return structuredClone(
363
- parseRecord(row.outcome_json, "design-operation-outcome-invalid"),
364
- );
365
- }
366
-
367
- #artifactOperationRequest(
368
- input: DesignArtifactOperationFacts,
369
- ): Record<string, unknown> {
370
- requireIdentifier(input.operationId, "design-operation-id");
371
- if (
372
- typeof input.path !== "string" ||
373
- input.path.length === 0 ||
374
- input.path.length > 512
375
- ) {
376
- throw new Error("design-artifact-path-invalid");
377
- }
378
- if (input.operation === "write-artifact") {
379
- if (
380
- !Number.isSafeInteger(input.bytes) ||
381
- input.bytes < 0 ||
382
- input.bytes > MAX_PLAN_BYTES
383
- ) {
384
- throw new Error("design-artifact-content-too-large");
385
- }
386
- requireHash(input.rawSha256, "design-artifact-hash");
387
- return {
388
- operation: input.operation,
389
- path: input.path,
390
- bytes: input.bytes,
391
- rawSha256: input.rawSha256,
392
- };
393
- }
394
- return { operation: input.operation, path: input.path };
395
- }
396
-
397
- #validateArtifactOperationOutcome(
398
- input: DesignArtifactOperationFacts,
399
- value: unknown,
400
- ): DesignArtifactOperationOutcome {
401
- if (!value || typeof value !== "object" || Array.isArray(value)) {
402
- throw new Error("design-artifact-operation-outcome-invalid");
403
- }
404
- const outcome = value as Record<string, unknown>;
405
- if (
406
- outcome.operation !== input.operation ||
407
- outcome.runId !== input.runId ||
408
- outcome.path !== input.path
409
- ) {
410
- throw new Error("design-artifact-operation-outcome-invalid");
411
- }
412
- if (input.operation === "write-artifact") {
413
- if (
414
- !hasExactKeys(outcome, [
415
- "operation",
416
- "runId",
417
- "path",
418
- "bytes",
419
- "rawSha256",
420
- ]) ||
421
- outcome.bytes !== input.bytes ||
422
- outcome.rawSha256 !== input.rawSha256
423
- ) {
424
- throw new Error("design-artifact-operation-outcome-invalid");
425
- }
426
- return structuredClone(outcome) as DesignArtifactOperationOutcome;
427
- }
428
- if (outcome.deleted === false) {
429
- if (!hasExactKeys(outcome, ["operation", "runId", "path", "deleted"])) {
430
- throw new Error("design-artifact-operation-outcome-invalid");
431
- }
432
- return structuredClone(outcome) as DesignArtifactOperationOutcome;
433
- }
434
- if (
435
- !hasExactKeys(outcome, [
436
- "operation",
437
- "runId",
438
- "path",
439
- "deleted",
440
- "bytes",
441
- "rawSha256",
442
- ]) ||
443
- outcome.deleted !== true ||
444
- !Number.isSafeInteger(outcome.bytes) ||
445
- Number(outcome.bytes) < 0 ||
446
- Number(outcome.bytes) > MAX_PLAN_BYTES ||
447
- !SHA256.test(String(outcome.rawSha256))
448
- ) {
449
- throw new Error("design-artifact-operation-outcome-invalid");
450
- }
451
- return structuredClone(outcome) as DesignArtifactOperationOutcome;
452
- }
453
-
454
- artifactOperationState(
455
- input: DesignArtifactOperationFacts,
456
- ): DesignArtifactOperationState | undefined {
457
- this.#assertOpen();
458
- this.#run(input.runId, false);
459
- const request = this.#artifactOperationRequest(input);
460
- const row = this.#database
461
- .prepare(
462
- `SELECT operation, request_hash, outcome_json
463
- FROM design_operations WHERE run_id = ? AND operation_id = ?`,
464
- )
465
- .get(input.runId, input.operationId) as OperationRow | undefined;
466
- if (!row) return undefined;
467
- if (
468
- row.operation !== input.operation ||
469
- row.request_hash !== sha256(canonicalJson(request))
470
- ) {
471
- throw new Error("design-operation-conflict");
472
- }
473
- const stored = parseRecord(
474
- row.outcome_json,
475
- "design-artifact-operation-state-invalid",
476
- );
477
- if (stored.state !== "pending" && stored.state !== "committed") {
478
- throw new Error("design-artifact-operation-state-invalid");
479
- }
480
- return {
481
- state: stored.state,
482
- outcome: this.#validateArtifactOperationOutcome(input, stored.outcome),
483
- };
484
- }
485
-
486
- prepareArtifactOperation(
487
- input: DesignArtifactOperationFacts,
488
- lease: DesignFinalizationLease,
489
- outcome: DesignArtifactOperationOutcome,
490
- ): DesignArtifactOperationState {
491
- if (
492
- lease.runId !== input.runId ||
493
- lease.operationId !== input.operationId
494
- ) {
495
- throw new Error("design-finalization-lease-fenced");
496
- }
497
- return this.#transaction(() => {
498
- this.#run(input.runId, true);
499
- this.#assertFinalizationLease(lease);
500
- const existing = this.artifactOperationState(input);
501
- if (existing) return existing;
502
- const request = this.#artifactOperationRequest(input);
503
- const validated = this.#validateArtifactOperationOutcome(input, outcome);
504
- const state: DesignArtifactOperationState = {
505
- state: "pending",
506
- outcome: validated,
507
- };
508
- this.#database
509
- .prepare(
510
- `INSERT INTO design_operations(
511
- run_id, operation_id, operation, request_hash, outcome_json
512
- ) VALUES (?, ?, ?, ?, ?)`,
513
- )
514
- .run(
515
- input.runId,
516
- input.operationId,
517
- input.operation,
518
- sha256(canonicalJson(request)),
519
- canonicalJson(state),
520
- );
521
- return structuredClone(state);
522
- });
523
- }
524
-
525
- commitArtifactOperation(
526
- input: DesignArtifactOperationFacts,
527
- lease: DesignFinalizationLease,
528
- ): DesignArtifactOperationOutcome {
529
- if (
530
- lease.runId !== input.runId ||
531
- lease.operationId !== input.operationId
532
- ) {
533
- throw new Error("design-finalization-lease-fenced");
534
- }
535
- return this.#transaction(() => {
536
- this.#run(input.runId, true);
537
- this.#assertFinalizationLease(lease);
538
- const existing = this.artifactOperationState(input);
539
- if (!existing) throw new Error("design-artifact-operation-unprepared");
540
- if (existing.state === "committed") {
541
- return structuredClone(existing.outcome);
542
- }
543
- this.#database
544
- .prepare(
545
- `UPDATE design_operations SET outcome_json = ?
546
- WHERE run_id = ? AND operation_id = ?`,
547
- )
548
- .run(
549
- canonicalJson({ state: "committed", outcome: existing.outcome }),
550
- input.runId,
551
- input.operationId,
552
- );
553
- return structuredClone(existing.outcome);
554
- });
555
- }
556
-
557
- assertNoPendingArtifactOperations(runId: string): void {
558
- this.#assertOpen();
559
- this.#run(runId, true);
560
- const rows = this.#database
561
- .prepare(
562
- `SELECT outcome_json FROM design_operations
563
- WHERE run_id = ? AND operation IN ('write-artifact', 'delete-artifact')`,
564
- )
565
- .all(runId) as unknown as Array<{ outcome_json: string }>;
566
- for (const row of rows) {
567
- const state = parseRecord(
568
- row.outcome_json,
569
- "design-artifact-operation-state-invalid",
570
- ).state;
571
- if (state !== "pending" && state !== "committed") {
572
- throw new Error("design-artifact-operation-state-invalid");
573
- }
574
- if (state === "pending") {
575
- throw new Error("design-artifact-operation-pending");
576
- }
577
- }
578
- }
579
-
580
- latestFinalizationOutcome(
581
- runId: string,
582
- ): { operationId: string; outcome: Record<string, unknown> } | undefined {
583
- this.#assertOpen();
584
- this.#run(runId, false);
585
- const row = this.#database
586
- .prepare(
587
- `SELECT operation_id, outcome_json FROM design_operations
588
- WHERE run_id = ? AND operation = 'finalize-delivery'
589
- ORDER BY rowid DESC LIMIT 1`,
590
- )
591
- .get(runId) as { operation_id: string; outcome_json: string } | undefined;
592
- return row
593
- ? {
594
- operationId: row.operation_id,
595
- outcome: structuredClone(
596
- parseRecord(
597
- row.outcome_json,
598
- "design-finalization-outcome-invalid",
599
- ),
600
- ),
601
- }
602
- : undefined;
603
- }
604
-
605
- acquireFinalizationLease(input: {
606
- runId: string;
607
- operationId: string;
608
- }): DesignFinalizationLease {
609
- requireIdentifier(input.operationId, "design-operation-id");
610
- return this.#transaction(() => {
611
- this.#run(input.runId, true);
612
- const now = this.#now();
613
- const existing = this.#database
614
- .prepare(
615
- `SELECT operation_id, token, expires_at
616
- FROM design_finalization_leases WHERE run_id = ?`,
617
- )
618
- .get(input.runId) as
619
- | { operation_id: string; token: string; expires_at: number }
620
- | undefined;
621
- if (existing && existing.expires_at > now) {
622
- throw new Error("design-finalization-busy");
623
- }
624
- if (existing) {
625
- this.#database
626
- .prepare("DELETE FROM design_finalization_leases WHERE run_id = ?")
627
- .run(input.runId);
628
- }
629
- const lease: DesignFinalizationLease = {
630
- runId: input.runId,
631
- operationId: input.operationId,
632
- token: randomUUID(),
633
- expiresAt: now + this.#finalizationLeaseMs,
634
- };
635
- this.#database
636
- .prepare(
637
- `INSERT INTO design_finalization_leases(
638
- run_id, operation_id, token, expires_at
639
- ) VALUES (?, ?, ?, ?)`,
640
- )
641
- .run(lease.runId, lease.operationId, lease.token, lease.expiresAt);
642
- return lease;
643
- });
644
- }
645
-
646
- #assertFinalizationLease(lease: DesignFinalizationLease): void {
647
- const row = this.#database
648
- .prepare(
649
- `SELECT operation_id, token, expires_at
650
- FROM design_finalization_leases WHERE run_id = ?`,
651
- )
652
- .get(lease.runId) as
653
- | { operation_id: string; token: string; expires_at: number }
654
- | undefined;
655
- if (
656
- !row ||
657
- row.operation_id !== lease.operationId ||
658
- row.token !== lease.token ||
659
- row.expires_at !== lease.expiresAt ||
660
- row.expires_at <= this.#now()
661
- ) {
662
- throw new Error("design-finalization-lease-fenced");
663
- }
664
- }
665
-
666
- assertFinalizationLease(lease: DesignFinalizationLease): void {
667
- this.#assertOpen();
668
- this.#assertFinalizationLease(lease);
669
- }
670
-
671
- releaseFinalizationLease(lease: DesignFinalizationLease): void {
672
- this.#transaction(() => {
673
- this.#database
674
- .prepare(
675
- `DELETE FROM design_finalization_leases
676
- WHERE run_id = ? AND operation_id = ? AND token = ?`,
677
- )
678
- .run(lease.runId, lease.operationId, lease.token);
679
- });
680
- }
681
-
682
- #facts(runId: string): FactRow[] {
683
- return this.#database
684
- .prepare(
685
- `SELECT sequence, kind, identity, payload_json, canonical_hash,
686
- prior_record_hash, record_hash
687
- FROM design_facts WHERE run_id = ? ORDER BY sequence`,
688
- )
689
- .all(runId) as unknown as FactRow[];
690
- }
691
-
692
- #appendFact(
693
- runId: string,
694
- kind: string,
695
- identity: string,
696
- payload: Record<string, unknown>,
697
- ): FactRow {
698
- const latest = this.#database
699
- .prepare(
700
- `SELECT sequence, record_hash FROM design_facts
701
- WHERE run_id = ? ORDER BY sequence DESC LIMIT 1`,
702
- )
703
- .get(runId) as { sequence: number; record_hash: string } | undefined;
704
- const sequence = (latest?.sequence ?? 0) + 1;
705
- const canonicalHash = sha256(canonicalJson(payload));
706
- const priorRecordHash = latest?.record_hash ?? ZERO_HASH;
707
- const recordHash = sha256(
708
- canonicalJson({
709
- runId,
710
- sequence,
711
- kind,
712
- identity,
713
- canonicalHash,
714
- priorRecordHash,
715
- }),
716
- );
717
- this.#database
718
- .prepare(
719
- `INSERT INTO design_facts(
720
- run_id, sequence, kind, identity, payload_json, canonical_hash,
721
- prior_record_hash, record_hash
722
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
723
- )
724
- .run(
725
- runId,
726
- sequence,
727
- kind,
728
- identity,
729
- canonicalJson(payload),
730
- canonicalHash,
731
- priorRecordHash,
732
- recordHash,
733
- );
734
- return {
735
- sequence,
736
- kind,
737
- identity,
738
- payload_json: canonicalJson(payload),
739
- canonical_hash: canonicalHash,
740
- prior_record_hash: priorRecordHash,
741
- record_hash: recordHash,
742
- };
743
- }
744
-
745
- #operationReplay(
746
- runId: string,
747
- operationId: string,
748
- operation: string,
749
- request: Record<string, unknown>,
750
- ): Record<string, unknown> | undefined {
751
- requireIdentifier(operationId, "design-operation-id");
752
- const requestHash = sha256(canonicalJson(request));
753
- const row = this.#database
754
- .prepare(
755
- `SELECT operation, request_hash, outcome_json
756
- FROM design_operations WHERE run_id = ? AND operation_id = ?`,
757
- )
758
- .get(runId, operationId) as OperationRow | undefined;
759
- if (!row) return undefined;
760
- if (row.operation !== operation || row.request_hash !== requestHash) {
761
- throw new Error("design-operation-conflict");
762
- }
763
- return parseRecord(row.outcome_json, "design-operation-outcome-invalid");
764
- }
765
-
766
- #recordOperation(
767
- runId: string,
768
- operationId: string,
769
- operation: string,
770
- request: Record<string, unknown>,
771
- outcome: Record<string, unknown>,
772
- ): void {
773
- this.#database
774
- .prepare(
775
- `INSERT INTO design_operations(
776
- run_id, operation_id, operation, request_hash, outcome_json
777
- ) VALUES (?, ?, ?, ?, ?)`,
778
- )
779
- .run(
780
- runId,
781
- operationId,
782
- operation,
783
- sha256(canonicalJson(request)),
784
- canonicalJson(outcome),
785
- );
786
- }
787
-
788
- recordEvidence(input: {
789
- runId: string;
790
- evidence: DesignEvidenceResult;
791
- }): DesignEvidenceProjection {
792
- return this.#transaction(() => {
793
- this.#run(input.runId, true);
794
- const validation = validateEvidenceResult(input.evidence);
795
- if (!validation.ok || input.evidence.role !== "design-explorer") {
796
- throw new Error("design-evidence-invalid");
797
- }
798
- const resultHash = sha256(canonicalJson(input.evidence));
799
- const existing = this.#database
800
- .prepare(
801
- `SELECT sequence, payload_json, record_hash
802
- FROM design_facts
803
- WHERE run_id = ? AND kind = 'evidence' AND identity = ?`,
804
- )
805
- .get(input.runId, input.evidence.packet_id) as
806
- | { sequence: number; payload_json: string; record_hash: string }
807
- | undefined;
808
- if (existing) {
809
- const payload = parseRecord(
810
- existing.payload_json,
811
- "design-evidence-record-invalid",
812
- );
813
- if (payload.resultHash !== resultHash) {
814
- throw new Error("design-evidence-conflict");
815
- }
816
- return this.#evidenceProjection(existing, payload);
817
- }
818
- const payload = {
819
- packetId: input.evidence.packet_id,
820
- resultHash,
821
- moduleName: input.evidence.module_name,
822
- scope: [...new Set(input.evidence.scope)].sort(),
823
- filesRead: [...new Set(input.evidence.files_read)].sort(),
824
- citations: input.evidence.evidence
825
- .map((citation) => ({
826
- path: citation.path,
827
- lineStart: citation.line_start,
828
- lineEnd: citation.line_end,
829
- claimHash: sha256(citation.claim),
830
- }))
831
- .sort(
832
- (left, right) =>
833
- compareCanonicalStrings(left.path, right.path) ||
834
- left.lineStart - right.lineStart ||
835
- left.lineEnd - right.lineEnd ||
836
- compareCanonicalStrings(left.claimHash, right.claimHash),
837
- ),
838
- };
839
- const fact = this.#appendFact(
840
- input.runId,
841
- "evidence",
842
- input.evidence.packet_id,
843
- payload,
844
- );
845
- return this.#evidenceProjection(fact, payload);
846
- });
847
- }
848
-
849
- #evidenceProjection(
850
- row: Pick<FactRow, "sequence" | "record_hash">,
851
- payload: Record<string, unknown>,
852
- ): DesignEvidenceProjection {
853
- return {
854
- packetId: String(payload.packetId),
855
- resultHash: String(payload.resultHash),
856
- recordHash: row.record_hash,
857
- sequence: row.sequence,
858
- moduleName: String(payload.moduleName),
859
- scope: structuredClone(payload.scope as string[]),
860
- filesRead: structuredClone(payload.filesRead as string[]),
861
- citations: structuredClone(
862
- payload.citations as DesignEvidenceProjection["citations"],
863
- ),
864
- };
865
- }
866
-
867
- recordDecision(input: {
868
- runId: string;
869
- operationId: string;
870
- decisionId: string;
871
- category: DecisionCategory;
872
- contract: string;
873
- refs: string[];
874
- }): Record<string, unknown> {
875
- requireIdentifier(input.decisionId, "design-decision-id");
876
- const decisionContractHash = contractHash(
877
- "abel-design-decision",
878
- input.contract,
879
- );
880
- if (input.category !== "behavior" && input.category !== "technical") {
881
- throw new Error("design-decision-category-invalid");
882
- }
883
- const refs = normalizeRefs(input.refs);
884
- const request = {
885
- operation: "record-decision",
886
- decisionId: input.decisionId,
887
- category: input.category,
888
- contractHash: decisionContractHash,
889
- refs,
890
- };
891
- return this.#transaction(() => {
892
- this.#run(input.runId, true);
893
- const replay = this.#operationReplay(
894
- input.runId,
895
- input.operationId,
896
- "record-decision",
897
- request,
898
- );
899
- if (replay) return structuredClone(replay);
900
- const prior = this.#latestDecision(input.runId, input.decisionId);
901
- if (prior && prior.category !== input.category) {
902
- throw new Error("design-decision-category-conflict");
903
- }
904
- let decision: DesignDecisionProjection;
905
- if (
906
- prior &&
907
- prior.contractHash === decisionContractHash &&
908
- canonicalJson(prior.refs) === canonicalJson(refs)
909
- ) {
910
- decision = prior;
911
- } else {
912
- const revision = (prior?.revision ?? 0) + 1;
913
- const payload = {
914
- decisionId: input.decisionId,
915
- category: input.category,
916
- revision,
917
- contractHash: decisionContractHash,
918
- refs,
919
- authorityChanged: prior?.contractHash !== decisionContractHash,
920
- };
921
- const fact = this.#appendFact(
922
- input.runId,
923
- "decision",
924
- `${input.decisionId}:${revision}`,
925
- payload,
926
- );
927
- decision = this.#decisionProjection(fact, payload);
928
- }
929
- const outcome = {
930
- operation: "record-decision",
931
- runId: input.runId,
932
- decision,
933
- gates: this.#gateStatus(input.runId),
934
- };
935
- this.#recordOperation(
936
- input.runId,
937
- input.operationId,
938
- "record-decision",
939
- request,
940
- outcome,
941
- );
942
- return structuredClone(outcome);
943
- });
944
- }
945
-
946
- #decisionProjection(
947
- fact: Pick<FactRow, "sequence" | "record_hash">,
948
- payload: Record<string, unknown>,
949
- ): DesignDecisionProjection {
950
- return {
951
- decisionId: String(payload.decisionId),
952
- category: payload.category as DecisionCategory,
953
- revision: Number(payload.revision),
954
- contractHash: String(payload.contractHash),
955
- refs: structuredClone(payload.refs as string[]),
956
- recordHash: fact.record_hash,
957
- sequence: fact.sequence,
958
- };
959
- }
960
-
961
- #latestDecision(
962
- runId: string,
963
- decisionId: string,
964
- ): DesignDecisionProjection | undefined {
965
- const rows = this.#facts(runId).filter((row) => row.kind === "decision");
966
- for (let index = rows.length - 1; index >= 0; index -= 1) {
967
- const row = rows[index];
968
- const payload = parseRecord(
969
- row.payload_json,
970
- "design-decision-record-invalid",
971
- );
972
- if (payload.decisionId === decisionId) {
973
- return this.#decisionProjection(row, payload);
974
- }
975
- }
976
- return undefined;
977
- }
978
-
979
- approveGate(
980
- input:
981
- | {
982
- runId: string;
983
- operationId: string;
984
- gate: "gate-a";
985
- contract: string | ChangeContract;
986
- }
987
- | {
988
- runId: string;
989
- operationId: string;
990
- gate: "gate-b";
991
- },
992
- ): {
993
- operation: "approve-gate";
994
- runId: string;
995
- gate: DesignGate;
996
- proof: DesignGateProof;
997
- } {
998
- if (input.gate !== "gate-a" && input.gate !== "gate-b") {
999
- throw new Error("design-gate-invalid");
1000
- }
1001
- const changeContract =
1002
- input.gate === "gate-a" && typeof input.contract !== "string"
1003
- ? normalizeChangeContract(input.contract)
1004
- : undefined;
1005
- const gateAContractHash =
1006
- input.gate === "gate-a"
1007
- ? contractHash(
1008
- "abel-design-gate-a",
1009
- changeContract
1010
- ? canonicalJson(changeContract)
1011
- : (input.contract as string),
1012
- )
1013
- : undefined;
1014
- return this.#transaction(() => {
1015
- this.#run(input.runId, true);
1016
- const request = {
1017
- operation: "approve-gate",
1018
- gate: input.gate,
1019
- ...(gateAContractHash ? { contractHash: gateAContractHash } : {}),
1020
- };
1021
- const replay = this.#operationReplay(
1022
- input.runId,
1023
- input.operationId,
1024
- "approve-gate",
1025
- request,
1026
- );
1027
- if (replay)
1028
- return structuredClone(replay) as ReturnType<
1029
- DesignJournal["approveGate"]
1030
- >;
1031
- let approvedContractHash = gateAContractHash;
1032
- if (input.gate === "gate-b") {
1033
- const current = this.#gateStatus(input.runId);
1034
- if (!current.gateA.current) throw new Error("design-gate-a-required");
1035
- const plan = this.#currentPlanProjection(input.runId);
1036
- if (!plan) throw new Error("design-plan-required");
1037
- const latestDecisionSequence = this.#latestSubstantiveDecisionSequence(
1038
- input.runId,
1039
- );
1040
- if (
1041
- plan.sequence <= (current.gateA.approvedSequence ?? 0) ||
1042
- (latestDecisionSequence !== undefined &&
1043
- plan.sequence <= latestDecisionSequence)
1044
- ) {
1045
- throw new Error("design-plan-stale");
1046
- }
1047
- approvedContractHash = plan.canonicalHash;
1048
- }
1049
- if (!approvedContractHash)
1050
- throw new Error("design-transient-input-invalid");
1051
- const existing = this.#latestApproval(input.runId, input.gate);
1052
- const currentGate = this.#gateStatus(input.runId)[
1053
- input.gate === "gate-a" ? "gateA" : "gateB"
1054
- ];
1055
- let proof: DesignGateProof;
1056
- if (
1057
- existing &&
1058
- currentGate.current &&
1059
- existing.proof.contractHash === approvedContractHash
1060
- ) {
1061
- proof = existing.proof;
1062
- } else {
1063
- const revision = (existing?.proof.revision ?? 0) + 1;
1064
- const payload = {
1065
- gate: input.gate,
1066
- revision,
1067
- contractHash: approvedContractHash,
1068
- ...(changeContract ? { changeContract } : {}),
1069
- };
1070
- const fact = this.#appendFact(
1071
- input.runId,
1072
- "approval",
1073
- `${input.gate}:${revision}`,
1074
- payload,
1075
- );
1076
- proof = {
1077
- revision,
1078
- contractHash: approvedContractHash,
1079
- recordHash: fact.record_hash,
1080
- };
1081
- }
1082
- const outcome = {
1083
- operation: "approve-gate" as const,
1084
- runId: input.runId,
1085
- gate: input.gate,
1086
- proof,
1087
- };
1088
- this.#recordOperation(
1089
- input.runId,
1090
- input.operationId,
1091
- "approve-gate",
1092
- request,
1093
- outcome,
1094
- );
1095
- return structuredClone(outcome);
1096
- });
1097
- }
1098
-
1099
- #latestApproval(
1100
- runId: string,
1101
- gate: DesignGate,
1102
- ): { sequence: number; proof: DesignGateProof } | undefined {
1103
- const rows = this.#facts(runId).filter((row) => row.kind === "approval");
1104
- for (let index = rows.length - 1; index >= 0; index -= 1) {
1105
- const row = rows[index];
1106
- const payload = parseRecord(
1107
- row.payload_json,
1108
- "design-approval-record-invalid",
1109
- );
1110
- if (payload.gate === gate) {
1111
- return {
1112
- sequence: row.sequence,
1113
- proof: {
1114
- revision: Number(payload.revision),
1115
- contractHash: String(payload.contractHash),
1116
- recordHash: row.record_hash,
1117
- },
1118
- };
1119
- }
1120
- }
1121
- return undefined;
1122
- }
1123
-
1124
- #certifyPlan(runId: string, planHash: string): void {
1125
- const gates = this.#gateStatus(runId);
1126
- if (gates.gateB.current && gates.gateB.proof?.contractHash === planHash)
1127
- return;
1128
- const previous = this.#latestApproval(runId, "gate-b");
1129
- const revision = (previous?.proof.revision ?? 0) + 1;
1130
- this.#appendFact(runId, "approval", `gate-b:${revision}`, {
1131
- gate: "gate-b",
1132
- revision,
1133
- contractHash: planHash,
1134
- authority: "compiler",
1135
- });
1136
- }
1137
-
1138
- recordCompiledPlan(input: {
1139
- runId: string;
1140
- operationId: string;
1141
- bytes: Uint8Array;
1142
- rawSha256: string;
1143
- canonicalHash: string;
1144
- lease?: DesignFinalizationLease;
1145
- certify?: boolean;
1146
- }): Record<string, unknown> {
1147
- requireHash(input.rawSha256, "design-plan-raw-hash");
1148
- requireHash(input.canonicalHash, "design-plan-canonical-hash");
1149
- if (
1150
- !(input.bytes instanceof Uint8Array) ||
1151
- input.bytes.byteLength < 1 ||
1152
- input.bytes.byteLength > MAX_PLAN_BYTES ||
1153
- sha256(input.bytes) !== input.rawSha256
1154
- ) {
1155
- throw new Error("design-plan-bytes-invalid");
1156
- }
1157
- const request = {
1158
- operation: "compile-plan",
1159
- rawSha256: input.rawSha256,
1160
- canonicalHash: input.canonicalHash,
1161
- };
1162
- return this.#transaction(() => {
1163
- this.#run(input.runId, true);
1164
- const replay = this.#operationReplay(
1165
- input.runId,
1166
- input.operationId,
1167
- "compile-plan",
1168
- request,
1169
- );
1170
- if (replay) return structuredClone(replay);
1171
- if (input.lease) {
1172
- if (
1173
- input.lease.runId !== input.runId ||
1174
- input.lease.operationId !== input.operationId
1175
- ) {
1176
- throw new Error("design-finalization-lease-fenced");
1177
- }
1178
- this.#assertFinalizationLease(input.lease);
1179
- }
1180
- if (!this.#gateStatus(input.runId).gateA.current) {
1181
- throw new Error("design-gate-a-required");
1182
- }
1183
- const prior = this.#currentPlanProjection(input.runId);
1184
- const status = this.status(input.runId);
1185
- const latestDecision =
1186
- this.#latestSubstantiveDecisionSequence(input.runId) ?? 0;
1187
- if (
1188
- prior &&
1189
- prior.rawSha256 === input.rawSha256 &&
1190
- prior.canonicalHash === input.canonicalHash &&
1191
- prior.sequence > latestDecision &&
1192
- prior.sequence > (status.gates.gateA.approvedSequence ?? 0)
1193
- ) {
1194
- if (input.certify) this.#certifyPlan(input.runId, input.canonicalHash);
1195
- const outcome = {
1196
- operation: "compile-plan",
1197
- runId: input.runId,
1198
- plan: prior,
1199
- gates: this.#gateStatus(input.runId),
1200
- };
1201
- this.#recordOperation(
1202
- input.runId,
1203
- input.operationId,
1204
- "compile-plan",
1205
- request,
1206
- outcome,
1207
- );
1208
- return structuredClone(outcome);
1209
- }
1210
- const revision = (prior?.revision ?? 0) + 1;
1211
- const payload = {
1212
- revision,
1213
- rawSha256: input.rawSha256,
1214
- canonicalHash: input.canonicalHash,
1215
- };
1216
- const fact = this.#appendFact(
1217
- input.runId,
1218
- "plan",
1219
- String(revision),
1220
- payload,
1221
- );
1222
- this.#database
1223
- .prepare(
1224
- `INSERT INTO design_compiled_plans(
1225
- run_id, revision, fact_sequence, raw_sha256, canonical_hash,
1226
- plan_bytes
1227
- ) VALUES (?, ?, ?, ?, ?, ?)`,
1228
- )
1229
- .run(
1230
- input.runId,
1231
- revision,
1232
- fact.sequence,
1233
- input.rawSha256,
1234
- input.canonicalHash,
1235
- Buffer.from(input.bytes),
1236
- );
1237
- const plan: DesignPlanProjection = {
1238
- revision,
1239
- rawSha256: input.rawSha256,
1240
- canonicalHash: input.canonicalHash,
1241
- recordHash: fact.record_hash,
1242
- sequence: fact.sequence,
1243
- };
1244
- if (input.certify) this.#certifyPlan(input.runId, input.canonicalHash);
1245
- const outcome = {
1246
- operation: "compile-plan",
1247
- runId: input.runId,
1248
- plan,
1249
- gates: this.#gateStatus(input.runId),
1250
- };
1251
- this.#recordOperation(
1252
- input.runId,
1253
- input.operationId,
1254
- "compile-plan",
1255
- request,
1256
- outcome,
1257
- );
1258
- return structuredClone(outcome);
1259
- });
1260
- }
1261
-
1262
- #currentPlanProjection(runId: string): DesignPlanProjection | undefined {
1263
- const row = this.#database
1264
- .prepare(
1265
- `SELECT plans.revision, plans.raw_sha256, plans.canonical_hash,
1266
- facts.sequence, facts.record_hash
1267
- FROM design_compiled_plans AS plans
1268
- JOIN design_facts AS facts
1269
- ON facts.run_id = plans.run_id
1270
- AND facts.sequence = plans.fact_sequence
1271
- WHERE plans.run_id = ? ORDER BY plans.revision DESC LIMIT 1`,
1272
- )
1273
- .get(runId) as
1274
- | {
1275
- revision: number;
1276
- raw_sha256: string;
1277
- canonical_hash: string;
1278
- sequence: number;
1279
- record_hash: string;
1280
- }
1281
- | undefined;
1282
- return row
1283
- ? {
1284
- revision: row.revision,
1285
- rawSha256: row.raw_sha256,
1286
- canonicalHash: row.canonical_hash,
1287
- recordHash: row.record_hash,
1288
- sequence: row.sequence,
1289
- }
1290
- : undefined;
1291
- }
1292
-
1293
- currentCompiledPlan(
1294
- runId: string,
1295
- ): { projection: DesignPlanProjection; bytes: Uint8Array } | undefined {
1296
- this.#assertOpen();
1297
- this.#run(runId, false);
1298
- const row = this.#database
1299
- .prepare(
1300
- `SELECT plan_bytes FROM design_compiled_plans
1301
- WHERE run_id = ? ORDER BY revision DESC LIMIT 1`,
1302
- )
1303
- .get(runId) as { plan_bytes: Uint8Array } | undefined;
1304
- const projection = this.#currentPlanProjection(runId);
1305
- if (!row || !projection) return undefined;
1306
- return { projection, bytes: Uint8Array.from(row.plan_bytes) };
1307
- }
1308
-
1309
- verifyGateProof(input: GateProofVerificationInput): boolean {
1310
- this.#assertOpen();
1311
- if (
1312
- typeof input.change !== "string" ||
1313
- input.change.length === 0 ||
1314
- (input.gate !== "gate-a" && input.gate !== "gate-b") ||
1315
- !Number.isSafeInteger(input.proof.revision) ||
1316
- input.proof.revision < 1 ||
1317
- !SHA256.test(input.proof.contractHash) ||
1318
- !SHA256.test(input.proof.recordHash)
1319
- ) {
1320
- return false;
1321
- }
1322
- const row = this.#database
1323
- .prepare(
1324
- `SELECT facts.run_id, facts.payload_json
1325
- FROM design_facts AS facts
1326
- JOIN runs ON runs.run_id = facts.run_id
1327
- WHERE runs.root_hash = ? AND runs.stage = 'abel-design'
1328
- AND runs.change_name = ? AND facts.kind = 'approval'
1329
- AND facts.record_hash = ?`,
1330
- )
1331
- .get(
1332
- this.stateRoot.consumerRootHash,
1333
- input.change,
1334
- input.proof.recordHash,
1335
- ) as { run_id: string; payload_json: string } | undefined;
1336
- if (!row) return false;
1337
- const payload = parseRecord(
1338
- row.payload_json,
1339
- "design-approval-record-invalid",
1340
- );
1341
- if (
1342
- payload.gate !== input.gate ||
1343
- payload.revision !== input.proof.revision ||
1344
- payload.contractHash !== input.proof.contractHash
1345
- ) {
1346
- return false;
1347
- }
1348
- const status = this.#gateStatus(row.run_id)[
1349
- input.gate === "gate-a" ? "gateA" : "gateB"
1350
- ];
1351
- return (
1352
- status.current &&
1353
- status.proof?.recordHash === input.proof.recordHash &&
1354
- status.proof.revision === input.proof.revision &&
1355
- status.proof.contractHash === input.proof.contractHash
1356
- );
1357
- }
1358
-
1359
- verifyFinalizedDelivery(input: FinalizedDeliveryVerificationInput): boolean {
1360
- this.#assertOpen();
1361
- if (
1362
- typeof input.change !== "string" ||
1363
- input.change.length === 0 ||
1364
- !Number.isSafeInteger(input.deliveryRevision) ||
1365
- input.deliveryRevision < 1 ||
1366
- !SHA256.test(input.receiptHash) ||
1367
- !SHA256.test(input.planCanonicalHash)
1368
- ) {
1369
- return false;
1370
- }
1371
- const rows = this.#database
1372
- .prepare(
1373
- `SELECT facts.payload_json
1374
- FROM design_facts AS facts
1375
- JOIN runs ON runs.run_id = facts.run_id
1376
- WHERE runs.root_hash = ? AND runs.stage = 'abel-design'
1377
- AND runs.change_name = ? AND facts.kind = 'finalization'`,
1378
- )
1379
- .all(this.stateRoot.consumerRootHash, input.change) as unknown as Array<{
1380
- payload_json: string;
1381
- }>;
1382
- return rows.some((row) => {
1383
- const payload = parseRecord(
1384
- row.payload_json,
1385
- "design-finalization-record-invalid",
1386
- );
1387
- return (
1388
- payload.deliveryRevision === input.deliveryRevision &&
1389
- payload.receiptHash === input.receiptHash &&
1390
- payload.planCanonicalHash === input.planCanonicalHash &&
1391
- canonicalJson(payload.gateA) === canonicalJson(input.gateA) &&
1392
- canonicalJson(payload.gateB) === canonicalJson(input.gateB)
1393
- );
1394
- });
1395
- }
1396
-
1397
- nextDeliveryRevision(runId: string): number {
1398
- this.#assertOpen();
1399
- const run = this.#run(runId, true);
1400
- if (!run.change_name) throw new Error("design-change-required");
1401
- const rows = this.#database
1402
- .prepare(
1403
- `SELECT facts.payload_json
1404
- FROM design_facts AS facts
1405
- JOIN runs ON runs.run_id = facts.run_id
1406
- WHERE runs.root_hash = ? AND runs.stage = 'abel-design'
1407
- AND runs.change_name = ? AND facts.kind = 'finalization'`,
1408
- )
1409
- .all(
1410
- this.stateRoot.consumerRootHash,
1411
- run.change_name,
1412
- ) as unknown as Array<{
1413
- payload_json: string;
1414
- }>;
1415
- const maximum = rows.reduce((current, row) => {
1416
- const payload = parseRecord(
1417
- row.payload_json,
1418
- "design-finalization-record-invalid",
1419
- );
1420
- const revision = Number(payload.deliveryRevision);
1421
- if (!Number.isSafeInteger(revision) || revision < 1) {
1422
- throw new Error("design-finalization-record-invalid");
1423
- }
1424
- return Math.max(current, revision);
1425
- }, 0);
1426
- return maximum + 1;
1427
- }
1428
-
1429
- recordFinalization(input: {
1430
- runId: string;
1431
- operationId: string;
1432
- lease: DesignFinalizationLease;
1433
- deliveryRevision: number;
1434
- receiptHash: string;
1435
- gateA: DesignGateProof;
1436
- gateB: DesignGateProof;
1437
- planCanonicalHash: string;
1438
- }): Record<string, unknown> {
1439
- if (
1440
- input.lease.runId !== input.runId ||
1441
- input.lease.operationId !== input.operationId
1442
- ) {
1443
- throw new Error("design-finalization-lease-fenced");
1444
- }
1445
- if (
1446
- !Number.isSafeInteger(input.deliveryRevision) ||
1447
- input.deliveryRevision < 1
1448
- ) {
1449
- throw new Error("design-delivery-revision-invalid");
1450
- }
1451
- for (const [label, value] of [
1452
- ["design-receipt-hash", input.receiptHash],
1453
- ["design-plan-canonical-hash", input.planCanonicalHash],
1454
- ["design-gate-a-contract-hash", input.gateA.contractHash],
1455
- ["design-gate-a-record-hash", input.gateA.recordHash],
1456
- ["design-gate-b-contract-hash", input.gateB.contractHash],
1457
- ["design-gate-b-record-hash", input.gateB.recordHash],
1458
- ] as const) {
1459
- requireHash(value, label);
1460
- }
1461
- const request = {
1462
- operation: "finalize-delivery",
1463
- deliveryRevision: input.deliveryRevision,
1464
- receiptHash: input.receiptHash,
1465
- gateA: input.gateA,
1466
- gateB: input.gateB,
1467
- planCanonicalHash: input.planCanonicalHash,
1468
- };
1469
- return this.#transaction(() => {
1470
- this.#run(input.runId, false);
1471
- const replay = this.#operationReplay(
1472
- input.runId,
1473
- input.operationId,
1474
- "finalize-delivery",
1475
- request,
1476
- );
1477
- if (replay) return structuredClone(replay);
1478
- this.#run(input.runId, true);
1479
- this.#assertFinalizationLease(input.lease);
1480
- const gates = this.#gateStatus(input.runId);
1481
- const plan = this.#currentPlanProjection(input.runId);
1482
- if (
1483
- !gates.gateA.current ||
1484
- !gates.gateB.current ||
1485
- canonicalJson(gates.gateA.proof) !== canonicalJson(input.gateA) ||
1486
- canonicalJson(gates.gateB.proof) !== canonicalJson(input.gateB) ||
1487
- !plan ||
1488
- plan.canonicalHash !== input.planCanonicalHash ||
1489
- input.gateB.contractHash !== input.planCanonicalHash
1490
- ) {
1491
- throw new Error("design-finalization-proof-stale");
1492
- }
1493
- const expectedRevision = this.nextDeliveryRevision(input.runId);
1494
- if (input.deliveryRevision !== expectedRevision) {
1495
- throw new Error("design-delivery-revision-conflict");
1496
- }
1497
- const payload = {
1498
- deliveryRevision: input.deliveryRevision,
1499
- receiptHash: input.receiptHash,
1500
- gateA: input.gateA,
1501
- gateB: input.gateB,
1502
- planCanonicalHash: input.planCanonicalHash,
1503
- };
1504
- this.#appendFact(
1505
- input.runId,
1506
- "finalization",
1507
- String(input.deliveryRevision),
1508
- payload,
1509
- );
1510
- const outcome = {
1511
- operation: "finalize-delivery",
1512
- runId: input.runId,
1513
- state: "completed",
1514
- completed: true,
1515
- deliveryRevision: input.deliveryRevision,
1516
- receiptHash: input.receiptHash,
1517
- };
1518
- this.#recordOperation(
1519
- input.runId,
1520
- input.operationId,
1521
- "finalize-delivery",
1522
- request,
1523
- outcome,
1524
- );
1525
- return structuredClone(outcome);
1526
- });
1527
- }
1528
-
1529
- #latestSubstantiveDecisionSequence(
1530
- runId: string,
1531
- category?: DecisionCategory,
1532
- ): number | undefined {
1533
- return this.#facts(runId)
1534
- .filter((fact) => {
1535
- if (fact.kind !== "decision") return false;
1536
- const decision = parseRecord(
1537
- fact.payload_json,
1538
- "design-decision-record-invalid",
1539
- );
1540
- return (
1541
- decision.authorityChanged !== false &&
1542
- (category === undefined || decision.category === category)
1543
- );
1544
- })
1545
- .at(-1)?.sequence;
1546
- }
1547
-
1548
- #gateStatus(runId: string): DesignStatusProjection["gates"] {
1549
- const latestBehavior = this.#latestSubstantiveDecisionSequence(
1550
- runId,
1551
- "behavior",
1552
- );
1553
- const latestTechnical = this.#latestSubstantiveDecisionSequence(
1554
- runId,
1555
- "technical",
1556
- );
1557
- const plan = this.#currentPlanProjection(runId);
1558
- const gateAApproval = this.#latestApproval(runId, "gate-a");
1559
- const gateBApproval = this.#latestApproval(runId, "gate-b");
1560
- const gateAChanged =
1561
- gateAApproval !== undefined &&
1562
- latestBehavior !== undefined &&
1563
- latestBehavior > gateAApproval.sequence;
1564
- const gateA: DesignGateProjection = gateAApproval
1565
- ? {
1566
- current: !gateAChanged,
1567
- proof: gateAApproval.proof,
1568
- approvedSequence: gateAApproval.sequence,
1569
- ...(gateAChanged
1570
- ? { staleReason: "behavior-decision-changed" as const }
1571
- : {}),
1572
- }
1573
- : { current: false };
1574
-
1575
- let staleReason: DesignGateProjection["staleReason"];
1576
- if (gateBApproval) {
1577
- if (
1578
- latestBehavior !== undefined &&
1579
- latestBehavior > gateBApproval.sequence
1580
- ) {
1581
- staleReason = "behavior-decision-changed";
1582
- } else if (
1583
- latestTechnical !== undefined &&
1584
- latestTechnical > gateBApproval.sequence
1585
- ) {
1586
- staleReason = "technical-decision-changed";
1587
- } else if (
1588
- gateAApproval !== undefined &&
1589
- gateAApproval.sequence > gateBApproval.sequence
1590
- ) {
1591
- staleReason = "gate-a-reapproved";
1592
- } else if (plan && plan.sequence > gateBApproval.sequence) {
1593
- staleReason = "plan-recompiled";
1594
- } else if (!plan) {
1595
- staleReason = "plan-missing";
1596
- } else if (!gateA.current) {
1597
- staleReason = "gate-a-stale";
1598
- } else if (plan.canonicalHash !== gateBApproval.proof.contractHash) {
1599
- staleReason = "plan-recompiled";
1600
- }
1601
- }
1602
- const gateB: DesignGateProjection = gateBApproval
1603
- ? {
1604
- current: staleReason === undefined,
1605
- proof: gateBApproval.proof,
1606
- approvedSequence: gateBApproval.sequence,
1607
- ...(staleReason ? { staleReason } : {}),
1608
- }
1609
- : { current: false };
1610
- return { gateA, gateB };
1611
- }
1612
-
1613
- /** Seed a new revision only from a completed, privately finalized delivery. */
1614
- inheritFinalizedAuthority(runId: string): void {
1615
- this.#transaction(() => {
1616
- const run = this.#run(runId, true);
1617
- if (!run.change_name || this.#facts(runId).length > 0) return;
1618
- const candidates = this.#database
1619
- .prepare(`
1620
- SELECT facts.run_id, facts.payload_json FROM design_facts AS facts
1621
- JOIN runs ON runs.run_id = facts.run_id
1622
- WHERE runs.root_hash = ? AND runs.stage = 'abel-design'
1623
- AND runs.change_name = ? AND runs.state = 'completed'
1624
- AND facts.kind = 'finalization' AND runs.run_id <> ?
1625
- `)
1626
- .all(run.root_hash, run.change_name, runId) as unknown as Array<{
1627
- run_id: string;
1628
- payload_json: string;
1629
- }>;
1630
- const source = candidates
1631
- .map((row) => ({
1632
- runId: row.run_id,
1633
- fact: parseRecord(
1634
- row.payload_json,
1635
- "design-finalization-record-invalid",
1636
- ),
1637
- }))
1638
- .sort(
1639
- (left, right) =>
1640
- Number(right.fact.deliveryRevision) -
1641
- Number(left.fact.deliveryRevision),
1642
- )[0];
1643
- if (!source) return;
1644
- const status = this.status(source.runId);
1645
- const plan = this.currentCompiledPlan(source.runId);
1646
- const gateA = status.gates.gateA.proof;
1647
- const gateB = status.gates.gateB.proof;
1648
- if (
1649
- !plan ||
1650
- !gateA ||
1651
- !gateB ||
1652
- !status.gates.gateA.current ||
1653
- !status.gates.gateB.current ||
1654
- sha256(plan.bytes) !== plan.projection.rawSha256 ||
1655
- !this.verifyFinalizedDelivery({
1656
- change: run.change_name,
1657
- deliveryRevision: Number(source.fact.deliveryRevision),
1658
- receiptHash: String(source.fact.receiptHash),
1659
- gateA,
1660
- gateB,
1661
- planCanonicalHash: plan.projection.canonicalHash,
1662
- })
1663
- ) {
1664
- throw new Error("design-inherited-authority-invalid");
1665
- }
1666
- this.#appendFact(runId, "inheritance", "finalized-authority", {
1667
- sourceRunId: source.runId,
1668
- deliveryRevision: source.fact.deliveryRevision,
1669
- receiptHash: source.fact.receiptHash,
1670
- gateA,
1671
- gateB,
1672
- });
1673
- for (const decision of status.decisions) {
1674
- this.#appendFact(
1675
- runId,
1676
- "decision",
1677
- `${decision.decisionId}:${decision.revision}`,
1678
- {
1679
- decisionId: decision.decisionId,
1680
- category: decision.category,
1681
- revision: decision.revision,
1682
- contractHash: decision.contractHash,
1683
- refs: decision.refs,
1684
- },
1685
- );
1686
- }
1687
- this.#appendFact(runId, "approval", `gate-a:${gateA.revision}`, {
1688
- gate: "gate-a",
1689
- revision: gateA.revision,
1690
- contractHash: gateA.contractHash,
1691
- ...(status.changeContract
1692
- ? { changeContract: status.changeContract }
1693
- : {}),
1694
- });
1695
- const projection = plan.projection;
1696
- const fact = this.#appendFact(
1697
- runId,
1698
- "plan",
1699
- String(projection.revision),
1700
- {
1701
- revision: projection.revision,
1702
- rawSha256: projection.rawSha256,
1703
- canonicalHash: projection.canonicalHash,
1704
- },
1705
- );
1706
- this.#database
1707
- .prepare(`INSERT INTO design_compiled_plans(
1708
- run_id, revision, fact_sequence, raw_sha256, canonical_hash, plan_bytes
1709
- ) VALUES (?, ?, ?, ?, ?, ?)`)
1710
- .run(
1711
- runId,
1712
- projection.revision,
1713
- fact.sequence,
1714
- projection.rawSha256,
1715
- projection.canonicalHash,
1716
- Buffer.from(plan.bytes),
1717
- );
1718
- this.#appendFact(runId, "approval", `gate-b:${gateB.revision}`, {
1719
- gate: "gate-b",
1720
- revision: gateB.revision,
1721
- contractHash: gateB.contractHash,
1722
- });
1723
- });
1724
- }
1725
-
1726
- status(runId: string): DesignStatusProjection {
1727
- this.#assertOpen();
1728
- const run = this.#run(runId, false);
1729
- const facts = this.#facts(runId);
1730
- const evidence = facts
1731
- .filter((fact) => fact.kind === "evidence")
1732
- .map((fact) =>
1733
- this.#evidenceProjection(
1734
- fact,
1735
- parseRecord(fact.payload_json, "design-evidence-record-invalid"),
1736
- ),
1737
- );
1738
- const latestDecisions = new Map<string, DesignDecisionProjection>();
1739
- for (const fact of facts.filter(
1740
- (candidate) => candidate.kind === "decision",
1741
- )) {
1742
- const decision = this.#decisionProjection(
1743
- fact,
1744
- parseRecord(fact.payload_json, "design-decision-record-invalid"),
1745
- );
1746
- latestDecisions.set(decision.decisionId, decision);
1747
- }
1748
- const approved = facts
1749
- .filter((fact) => fact.kind === "approval")
1750
- .map((fact) => parseRecord(fact.payload_json, "design-approval-invalid"))
1751
- .filter((fact) => fact.gate === "gate-a")
1752
- .at(-1);
1753
- return {
1754
- runId,
1755
- ...(run.change_name ? { change: run.change_name } : {}),
1756
- ...(approved?.changeContract
1757
- ? { changeContract: normalizeChangeContract(approved.changeContract) }
1758
- : {}),
1759
- evidence,
1760
- decisions: [...latestDecisions.values()].sort((left, right) =>
1761
- compareCanonicalStrings(left.decisionId, right.decisionId),
1762
- ),
1763
- gates: this.#gateStatus(runId),
1764
- plan: this.#currentPlanProjection(runId) ?? null,
1765
- };
1766
- }
1767
-
1768
- close(): void {
1769
- if (this.#closed) return;
1770
- this.#database.close();
1771
- this.#closed = true;
1772
- }
1773
- }