@abelxiaoxing/cadence 1.0.14 → 1.1.1

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