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