@sekiban/dcb-runtime 0.2.0 → 0.2.2

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 (44) hide show
  1. package/README.md +32 -1
  2. package/dist/allocator/AllocatorDurableObject.js +25 -2
  3. package/dist/bootstrap/BootstrapCoordinatorDurableObject.d.ts +2 -0
  4. package/dist/bootstrap/BootstrapCoordinatorDurableObject.js +64 -1
  5. package/dist/bootstrap/OperatorBootstrap.d.ts +5 -1
  6. package/dist/bootstrap/OperatorBootstrap.js +17 -10
  7. package/dist/bootstrap/types.d.ts +6 -0
  8. package/dist/cli/OperatorRepairCli.d.ts +6 -1
  9. package/dist/cli/OperatorRepairCli.js +26 -6
  10. package/dist/cloudflare.d.ts +5 -0
  11. package/dist/cloudflare.js +1567 -465
  12. package/dist/commit/CommitWorker.d.ts +19 -0
  13. package/dist/commit/CommitWorker.js +176 -27
  14. package/dist/completeness/IncidentLifecycle.d.ts +75 -0
  15. package/dist/completeness/IncidentLifecycle.js +503 -0
  16. package/dist/completeness/types.d.ts +67 -0
  17. package/dist/cosmos.d.ts +34 -4
  18. package/dist/cosmos.js +214 -49
  19. package/dist/downstream/OutboxDrain.d.ts +4 -1
  20. package/dist/downstream/OutboxDrain.js +24 -2
  21. package/dist/generated/cosmos-layout.d.ts +87 -0
  22. package/dist/generated/cosmos-layout.js +99 -0
  23. package/dist/http/ConfiguredBearer.d.ts +12 -0
  24. package/dist/http/ConfiguredBearer.js +33 -0
  25. package/dist/http/IncidentMaintenance.d.ts +8 -0
  26. package/dist/http/IncidentMaintenance.js +124 -0
  27. package/dist/http/SerializedQueryWorker.js +37 -0
  28. package/dist/index.d.ts +13 -0
  29. package/dist/index.js +1627 -465
  30. package/dist/read/SerializedReadWorker.d.ts +3 -0
  31. package/dist/read/SerializedReadWorker.js +3 -3
  32. package/dist/shard/ShardRotation.d.ts +48 -0
  33. package/dist/shard/ShardRotation.js +120 -0
  34. package/dist/store/CosmosEventStore.js +56 -46
  35. package/dist/store/PostgresEventStore.d.ts +20 -0
  36. package/dist/store/PostgresEventStore.js +53 -0
  37. package/dist/store/provider.d.ts +4 -0
  38. package/dist/tag/TagDurableObject.js +66 -14
  39. package/dist/tag/TagSqlSchema.d.ts +6 -1
  40. package/dist/tag/TagSqlSchema.js +15 -0
  41. package/dist/tag/types.d.ts +9 -0
  42. package/dist/trace/CommitTrace.d.ts +3 -3
  43. package/dist/trace/CommitTraceVerifier.js +1 -1
  44. package/package.json +6 -15
@@ -24,6 +24,17 @@ export interface CommitWorkerHooks {
24
24
  /** Test-only barrier after entry admission is released and before /allocate. */
25
25
  beforeBootstrapAllocation?(): Promise<void> | void;
26
26
  beforeBootstrapFinalization?(): Promise<void> | void;
27
+ /**
28
+ * SDT-G36 test-only hook invoked after successful `finalize` and before
29
+ * `appendAllTags`. The bootstrap write permit is held across this hook.
30
+ */
31
+ beforeAuthoritativeAppend?(): Promise<void> | void;
32
+ /**
33
+ * SDT-G36 test-only hook invoked after all appends resolved with no
34
+ * pending tags and before result assembly. Lets a test interleave a
35
+ * concurrent append to prove `tagWriteResults` are not re-read from head.
36
+ */
37
+ afterAppendBeforeResponse?(): Promise<void> | void;
27
38
  /** Test-only allocator identity; production uses the canonical allocator identity. */
28
39
  allocatorScopeIdentity?: string;
29
40
  /** Runtime composition value; never sourced from a caller-controlled V1 body. */
@@ -72,6 +83,14 @@ export declare class CommitWorker {
72
83
  */
73
84
  private retiredJournalMilestone;
74
85
  private bootstrapCommand;
86
+ /**
87
+ * SDT-G36 bootstrap write permit. Deliberately untraced: it must not add
88
+ * `sdt.commit/v2` rows or new trace identities. Only a bootstrap barrier,
89
+ * never commit-outcome authority. No JOURNAL, no alarm, no lease expiry.
90
+ */
91
+ private bootstrapStub;
92
+ private bootstrapPermitAcquire;
93
+ private bootstrapPermitRelease;
75
94
  private postJson;
76
95
  private tagRequest;
77
96
  private acquireReservations;
@@ -12,6 +12,25 @@ import { envServiceIdentity, requestServiceIdentity, } from "../service/ServiceI
12
12
  const INITIAL_OWNER_EPOCH = 0;
13
13
  const MAX_WRITE_ATTEMPTS = 2;
14
14
  const OUTCOME_UNDETERMINED_ERROR = "Commit outcome is undetermined; reread tag heads and event/query state before retrying because blind retry may create duplicate events.";
15
+ /** SDT-G36 stable digest of the attempt tuple (tags + event ids + payloads). */
16
+ function commitPermitDigest(allTags, candidates) {
17
+ const normalized = JSON.stringify({
18
+ tags: [...allTags].sort(),
19
+ events: candidates
20
+ .map((candidate) => ({
21
+ eventId: candidate.eventId,
22
+ payload: candidate.payload,
23
+ tags: [...candidate.tags].sort(),
24
+ }))
25
+ .sort((left, right) => (left.eventId < right.eventId ? -1 : left.eventId > right.eventId ? 1 : 0)),
26
+ });
27
+ let hash = 0x811c9dc5;
28
+ for (let index = 0; index < normalized.length; index += 1) {
29
+ hash ^= normalized.charCodeAt(index);
30
+ hash = Math.imul(hash, 0x01000193) >>> 0;
31
+ }
32
+ return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
33
+ }
15
34
  const GLOBAL_ADMISSION_HEADER = "x-sdt-global-admission";
16
35
  /**
17
36
  * The static authority checker proves the manifest bytes; this is the
@@ -440,30 +459,61 @@ export class CommitWorker {
440
459
  // S05 trace identities are compatibility observations only; they cannot
441
460
  // resolve JOURNAL and are removed from the actual dependency path.
442
461
  await this.retiredJournalMilestone("S04", traceState?.scope);
462
+ // SDT-G36: durable bootstrap write permit. Acquired after admit/release
463
+ // and before allocate (before `beforeBootstrapAllocation`), held until
464
+ // every target tag of this attempt is durably resolved. It is only a
465
+ // bootstrap barrier, never commit-outcome authority. No JOURNAL, no
466
+ // alarm, no lease expiry.
467
+ const permitDigest = commitPermitDigest(input.allTags, candidates);
468
+ let permitHeld = false;
469
+ const releasePermit = async () => {
470
+ if (!permitHeld)
471
+ return;
472
+ permitHeld = false;
473
+ await this.bootstrapPermitRelease(attemptId, permitDigest);
474
+ };
475
+ const permit = await this.bootstrapPermitAcquire(attemptId, permitDigest);
476
+ if (!permit.ok) {
477
+ if (permit.code === "bootstrap_permit_digest_mismatch") {
478
+ return responseWithAttempt(error(409, "bootstrap_permit_digest_mismatch", "Bootstrap write permit digest does not match"), attemptId, fault !== undefined);
479
+ }
480
+ return responseWithAttempt(error(409, "bootstrap_command_rejected", "Bootstrap is importing; commit is unavailable"), attemptId, fault !== undefined);
481
+ }
482
+ permitHeld = this.env.BOOTSTRAP !== undefined;
443
483
  const reservations = await this.acquireReservations(input, attemptId, fault, traceState?.scope);
444
484
  await this.retiredJournalMilestone("S05a", traceState?.scope, 0);
445
485
  if (reservations.failure !== undefined) {
446
- return this.finishReservationFailure([...reservations.successes.keys()], attemptId, reservations.failure, fault, traceState?.scope);
486
+ const failure = await this.finishReservationFailure([...reservations.successes.keys()], attemptId, reservations.failure, fault, traceState?.scope);
487
+ if (failure.resolved)
488
+ await releasePermit();
489
+ return failure.response;
447
490
  }
448
491
  if (fault === "after-reservations-before-allocation") {
449
492
  // Boundary 1: every durable prepare fact is force-tombstoned before
450
493
  // returning the intentionally undetermined outcome. There is no
451
494
  // attempt-level Journal recovery record to make this safe later.
452
- await this.cancelReservations([...reservations.successes.keys()], attemptId, fault, traceState?.scope);
495
+ const cleanup = await this.cancelReservations([...reservations.successes.keys()], attemptId, fault, traceState?.scope);
496
+ if (cleanup.failedTags.length === 0)
497
+ await releasePermit();
453
498
  return this.noApplicationOutcome(attemptId, true);
454
499
  }
455
500
  await this.hooks.beforeBootstrapAllocation?.();
456
501
  const allocation = await this.allocate(candidates, attemptId, fault, bootstrapEpoch, traceState?.scope);
457
502
  if (allocation === undefined) {
458
- return this.finishReservationFailure([...reservations.successes.keys()], attemptId, {
503
+ const failure = await this.finishReservationFailure([...reservations.successes.keys()], attemptId, {
459
504
  outcome: "FAILED",
460
505
  failureCause: "allocator-failure",
461
506
  reason: "allocator could not durably allocate the complete vector",
462
507
  }, fault, traceState?.scope);
508
+ if (failure.resolved)
509
+ await releasePermit();
510
+ return failure.response;
463
511
  }
464
512
  const allocatedCandidates = this.withAllocatedSuids(candidates, allocation);
465
513
  if (allocatedCandidates === undefined) {
466
- await this.cancelReservations([...reservations.successes.keys()], attemptId, fault, traceState?.scope);
514
+ const cleanup = await this.cancelReservations([...reservations.successes.keys()], attemptId, fault, traceState?.scope);
515
+ if (cleanup.failedTags.length === 0)
516
+ await releasePermit();
467
517
  return this.noApplicationOutcome(attemptId, fault !== undefined);
468
518
  }
469
519
  // Allocation supplies the exact EventId/SUID pair needed by the durable
@@ -485,7 +535,9 @@ export class CommitWorker {
485
535
  // This retained fault marks the allocation-to-first-append crash
486
536
  // boundary. With no Journal alarm, tags receive the same best-effort
487
537
  // tombstone barrier immediately and retain their own expiry alarm.
488
- await this.cancelReservations(input.allTags, attemptId, fault, traceState?.scope);
538
+ const cleanup = await this.cancelReservations(input.allTags, attemptId, fault, traceState?.scope);
539
+ if (cleanup.failedTags.length === 0)
540
+ await releasePermit();
489
541
  return this.noApplicationOutcome(attemptId, true);
490
542
  }
491
543
  await this.retiredJournalMilestone("S05c", traceState?.scope, 2);
@@ -493,9 +545,14 @@ export class CommitWorker {
493
545
  // This is immediately before the first final authoritative tag mutation.
494
546
  // The same service epoch obtained at admission must still be current.
495
547
  if ((await this.bootstrapCommand("finalize", attemptId, bootstrapEpoch, traceState?.scope, "S10")) === undefined) {
496
- await this.cancelReservations(input.allTags, attemptId, fault, traceState?.scope);
548
+ const cleanup = await this.cancelReservations(input.allTags, attemptId, fault, traceState?.scope);
549
+ if (cleanup.failedTags.length === 0)
550
+ await releasePermit();
497
551
  return responseWithAttempt(error(409, "bootstrap_command_rejected", "Bootstrap fencing epoch changed before commit write"), attemptId, fault !== undefined);
498
552
  }
553
+ // SDT-G36 test-only hook: the write permit is held across this barrier,
554
+ // so `POST /plan` must be 409 while the in-flight commit may still write.
555
+ await this.hooks.beforeAuthoritativeAppend?.();
499
556
  const writes = await this.appendAllTags(input, allocatedCandidates, attemptId, allocation.allocatorLineageId, reservations.successes, fault, traceState?.scope);
500
557
  if (writes.pendingTags.length > 0) {
501
558
  // Cancel is deliberately best effort: a cancellation error cannot
@@ -506,26 +563,36 @@ export class CommitWorker {
506
563
  // /fence/install loop: G44 can discover the incomplete source universe
507
564
  // without a delivery, and G45/G46 retain a readable fenced frontier.
508
565
  if (!await this.installPartialWriteFences(writes.pendingTags, attemptId, traceState?.scope)) {
566
+ // Resolution is not proven: the permit stays held and correctly
567
+ // blocks `plan`. Do not add recovery.
509
568
  return this.noApplicationOutcome(attemptId, true);
510
569
  }
511
570
  if (writes.committedTags.size === 0 &&
512
571
  writes.registrationUnavailableTags.length === writes.pendingTags.length) {
572
+ await releasePermit();
513
573
  return responseWithAttempt(error(503, "partition_registration_unavailable", "Source partition registration is unavailable; retry the commit.", true), attemptId, fault !== undefined);
514
574
  }
575
+ await releasePermit();
515
576
  return this.partialWriteOutcome(input, allocatedCandidates, writes, attemptId, fault !== undefined);
516
577
  }
578
+ // SDT-G36 test seam: a concurrent append here must not change the
579
+ // already-captured `tagWriteResults`.
580
+ await this.hooks.afterAppendBeforeResponse?.();
517
581
  await this.retiredJournalMilestone("S05d", traceState?.scope, 3);
518
582
  if (fault === "sealing-after-cas") {
519
583
  // The all-tags-written/response-lost boundary has no single terminal
520
584
  // Journal response. Durable tag receipts and G44 reconciliation are
521
585
  // now the detection authority.
586
+ await releasePermit();
522
587
  return this.noApplicationOutcome(attemptId, true);
523
588
  }
524
589
  try {
525
- const success = await this.successResponse(input, allocatedCandidates, attemptId, startedAt, fault, traceState?.scope);
590
+ const success = await this.successResponse(input, allocatedCandidates, attemptId, startedAt, fault, traceState?.scope, writes);
591
+ await releasePermit();
526
592
  return responseWithAttempt(withGlobalAdmission(json(success, 200), writes.globalAdmission), attemptId, fault !== undefined);
527
593
  }
528
594
  catch {
595
+ await releasePermit();
529
596
  return responseWithAttempt(error(500, "internal_error", "Committed records could not be read while preparing the response"), attemptId, fault !== undefined);
530
597
  }
531
598
  }
@@ -571,6 +638,55 @@ export class CommitWorker {
571
638
  : await traceScope.span(rowId, {}, invoke);
572
639
  return result.leaseEpoch;
573
640
  }
641
+ /**
642
+ * SDT-G36 bootstrap write permit. Deliberately untraced: it must not add
643
+ * `sdt.commit/v2` rows or new trace identities. Only a bootstrap barrier,
644
+ * never commit-outcome authority. No JOURNAL, no alarm, no lease expiry.
645
+ */
646
+ bootstrapStub() {
647
+ if (this.env.BOOTSTRAP === undefined)
648
+ return undefined;
649
+ return this.env.BOOTSTRAP.get(scopeIdFor(this.env.BOOTSTRAP, {
650
+ serviceId: this.serviceId,
651
+ doClass: "bootstrap",
652
+ identity: "coordinator",
653
+ }));
654
+ }
655
+ async bootstrapPermitAcquire(commandId, digest) {
656
+ const stub = this.bootstrapStub();
657
+ if (stub === undefined)
658
+ return { ok: true };
659
+ try {
660
+ const url = new URL(`https://commit-worker.internal/command/permit`);
661
+ url.searchParams.set("__serviceId", this.serviceId);
662
+ const response = await stub.fetch(new Request(url, {
663
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ commandId, digest }),
664
+ }));
665
+ if (response.ok)
666
+ return { ok: true };
667
+ const body = await response.clone().json().catch(() => undefined);
668
+ return { ok: false, code: typeof body?.code === "string" ? body.code : undefined };
669
+ }
670
+ catch {
671
+ return { ok: false };
672
+ }
673
+ }
674
+ async bootstrapPermitRelease(commandId, digest) {
675
+ const stub = this.bootstrapStub();
676
+ if (stub === undefined)
677
+ return;
678
+ try {
679
+ const url = new URL(`https://commit-worker.internal/command/permit-release`);
680
+ url.searchParams.set("__serviceId", this.serviceId);
681
+ await stub.fetch(new Request(url, {
682
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ commandId, digest }),
683
+ }));
684
+ }
685
+ catch {
686
+ // Best effort: a lost release cannot replace the primary commit outcome.
687
+ // A permit left behind by a crash correctly blocks `plan`.
688
+ }
689
+ }
574
690
  async postJson(stub, path, body, traceScope, rowId, traceOptions = {}) {
575
691
  const invoke = async () => {
576
692
  const response = await stub.fetch(new Request(`https://commit-worker.internal${path}`, {
@@ -730,6 +846,7 @@ export class CommitWorker {
730
846
  let pending = [...input.allTags];
731
847
  const committedTags = new Set();
732
848
  const lastFailureCodeByTag = new Map();
849
+ const tagWriteFacts = new Map();
733
850
  let globalAdmission = "admitted";
734
851
  for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS && pending.length > 0; attempt += 1) {
735
852
  const append = (stageScope) => Promise.allSettled(pending.map(async (tag) => {
@@ -761,14 +878,31 @@ export class CommitWorker {
761
878
  retryIndex: attempt,
762
879
  attemptId,
763
880
  });
764
- const body = response.status === 503
765
- ? await response.clone().json().catch(() => undefined)
766
- : undefined;
881
+ const success = response.status >= 200 && response.status < 300;
882
+ let appendVersion;
883
+ let appendUpdatedAt;
884
+ let code;
885
+ try {
886
+ const parsed = (await response.clone().json());
887
+ if (isObject(parsed)) {
888
+ if (typeof parsed.version === "number" && Number.isSafeInteger(parsed.version))
889
+ appendVersion = parsed.version;
890
+ if (typeof parsed.updatedAt === "string" && parsed.updatedAt.length > 0)
891
+ appendUpdatedAt = parsed.updatedAt;
892
+ if (typeof parsed.code === "string")
893
+ code = parsed.code;
894
+ }
895
+ }
896
+ catch {
897
+ // Callers use the status first; malformed internal bodies are failures.
898
+ }
767
899
  return {
768
900
  tag,
769
- success: response.status >= 200 && response.status < 300,
901
+ success,
770
902
  globalAdmission: readGlobalAdmission(response),
771
- code: isObject(body) && typeof body.code === "string" ? body.code : undefined,
903
+ code,
904
+ appendVersion,
905
+ appendUpdatedAt,
772
906
  };
773
907
  }));
774
908
  const results = traceScope === undefined
@@ -781,6 +915,9 @@ export class CommitWorker {
781
915
  committedTags.add(tag);
782
916
  lastFailureCodeByTag.delete(tag);
783
917
  globalAdmission = mergeGlobalAdmission(globalAdmission, result.value.globalAdmission);
918
+ if (result.value.appendVersion !== undefined && result.value.appendUpdatedAt !== undefined) {
919
+ tagWriteFacts.set(tag, { version: result.value.appendVersion, updatedAt: result.value.appendUpdatedAt });
920
+ }
784
921
  }
785
922
  else {
786
923
  lastFailureCodeByTag.set(tag, result.status === "fulfilled" ? result.value.code : undefined);
@@ -794,6 +931,7 @@ export class CommitWorker {
794
931
  pendingTags: pending,
795
932
  registrationUnavailableTags: pending.filter((tag) => lastFailureCodeByTag.get(tag) === "partition_registration_unavailable"),
796
933
  globalAdmission,
934
+ tagWriteFacts,
797
935
  };
798
936
  }
799
937
  /**
@@ -827,14 +965,17 @@ export class CommitWorker {
827
965
  if (traceScope !== undefined) {
828
966
  await traceScope.span("S17", {}, async () => undefined);
829
967
  }
830
- await this.cancelReservations(tags, attemptId, fault, traceScope);
968
+ const cleanup = await this.cancelReservations(tags, attemptId, fault, traceScope);
831
969
  await this.retiredJournalMilestone("S05e", traceScope, 4);
832
970
  const response = failure.outcome === "REFUSED"
833
971
  ? error(400, "consistency_conflict", "serialized commit was refused by a consistency reservation")
834
972
  : failure.failureCause === "reservation-timeout"
835
973
  ? error(504, "timeout", "serialized commit timed out")
836
974
  : error(500, "internal_error", "serialized commit failed before writing requested records");
837
- return responseWithAttempt(response, attemptId, fault !== undefined);
975
+ return {
976
+ response: responseWithAttempt(response, attemptId, fault !== undefined),
977
+ resolved: cleanup.failedTags.length === 0,
978
+ };
838
979
  }
839
980
  /**
840
981
  * Persist the local partial-write fact for each tag whose append did not
@@ -877,24 +1018,32 @@ export class CommitWorker {
877
1018
  },
878
1019
  }, 500), attemptId, exposeAttempt);
879
1020
  }
880
- async successResponse(input, candidates, attemptId, startedAt, fault, traceScope) {
881
- const readStates = (stageScope) => Promise.all(input.allTags.map(async (tag, memberIndex) => {
1021
+ async successResponse(input, candidates, attemptId, startedAt, fault, traceScope, writes) {
1022
+ // SDT-G36: `tagWriteResults` come from the `/append` bodies captured in
1023
+ // `writes.tagWriteFacts`. The success path never re-reads `/head-facts`
1024
+ // or `/state`. S13/S14 stay as compatibility observations only.
1025
+ const assemble = async (stageScope) => {
882
1026
  if (fault === "tag-state-unavailable") {
883
1027
  throw new Error("Test fault made the committed tag state unavailable");
884
1028
  }
885
- const response = await this.tagRequest(tag, "/head-facts", undefined, stageScope?.fork(), stageScope === undefined ? undefined : "S14", { memberIndex, attemptId });
886
- if (response.status !== 200) {
887
- throw new Error("Committed tag state was unavailable while preparing the response");
888
- }
889
- const body = (await response.json());
890
- if (typeof body.version !== "number" || typeof body.updatedAt !== "string" || typeof body.head !== "string") {
891
- throw new Error("Committed tag state was malformed while preparing the response");
1029
+ const results = [];
1030
+ for (const [memberIndex, tag] of input.allTags.entries()) {
1031
+ const read = () => {
1032
+ const fact = writes?.tagWriteFacts.get(tag);
1033
+ if (fact === undefined) {
1034
+ throw new Error("Committed tag state was unavailable while preparing the response");
1035
+ }
1036
+ return { tag, version: fact.version, writtenAt: fact.updatedAt };
1037
+ };
1038
+ results.push(stageScope === undefined
1039
+ ? read()
1040
+ : await stageScope.fork().span("S14", { tag, memberIndex, attemptId }, async () => read()));
892
1041
  }
893
- return { tag, version: body.version, writtenAt: body.updatedAt };
894
- }));
1042
+ return results;
1043
+ };
895
1044
  const tagWriteResults = traceScope === undefined
896
- ? await readStates()
897
- : await traceScope.span("S13", {}, async (stage) => readStates(stage));
1045
+ ? await assemble()
1046
+ : await traceScope.span("S13", {}, async (stage) => assemble(stage));
898
1047
  return {
899
1048
  writtenEvents: candidates.map((candidate) => ({
900
1049
  payload: encodeBase64Utf8(candidate.payload),
@@ -0,0 +1,75 @@
1
+ import type { IncidentLifecycleProjection, IncidentLifecycleState, IncidentTransitionRecord, IncidentTransitionRequest } from "./types";
2
+ declare const LIFECYCLE_STATES: readonly IncidentLifecycleState[];
3
+ export interface IncidentListFilters {
4
+ readonly state?: IncidentLifecycleState;
5
+ readonly owner?: string;
6
+ readonly unowned?: boolean;
7
+ readonly overdue?: boolean;
8
+ readonly observedAfterClose?: boolean;
9
+ }
10
+ export interface IncidentFinding {
11
+ readonly serviceId: string;
12
+ readonly incidentIdentity: string;
13
+ readonly incidentType: string;
14
+ readonly partitionTag: string | null;
15
+ readonly obligationSequence: number | null;
16
+ readonly eventId: string | null;
17
+ readonly eventDigest: string | null;
18
+ readonly state: "OPEN" | "UNRESOLVED";
19
+ readonly firstObservedAt: number;
20
+ readonly lastObservedAt: number;
21
+ }
22
+ export interface IncidentListItem {
23
+ readonly finding: IncidentFinding;
24
+ readonly lifecycle: IncidentLifecycleProjection;
25
+ readonly overdue: boolean;
26
+ readonly unowned: boolean;
27
+ readonly observedAfterClose: boolean;
28
+ }
29
+ export interface IncidentListResult {
30
+ readonly items: readonly IncidentListItem[];
31
+ readonly summary: Readonly<{
32
+ total: number;
33
+ byState: Readonly<Record<IncidentLifecycleState, number>>;
34
+ unowned: number;
35
+ overdue: number;
36
+ observedAfterClose: number;
37
+ }>;
38
+ }
39
+ export interface IncidentDetailResult {
40
+ readonly finding: IncidentFinding;
41
+ readonly lifecycle: IncidentLifecycleProjection;
42
+ readonly flags: Readonly<{
43
+ overdue: boolean;
44
+ unowned: boolean;
45
+ observedAfterClose: boolean;
46
+ }>;
47
+ readonly transitions: readonly IncidentTransitionRecord[];
48
+ }
49
+ export interface IncidentTransitionResult {
50
+ readonly projection: IncidentLifecycleProjection;
51
+ readonly transition: IncidentTransitionRecord;
52
+ readonly idempotent: boolean;
53
+ }
54
+ export declare class IncidentLifecycleError extends Error {
55
+ readonly code: string;
56
+ readonly status: number;
57
+ constructor(code: string, status: number, message: string);
58
+ }
59
+ export declare function normalizeIncidentIdentity(value: unknown): string;
60
+ export declare class IncidentLifecycle {
61
+ private readonly database;
62
+ private readonly now;
63
+ constructor(database: D1Database, now?: () => number);
64
+ static parseTransition(value: unknown): IncidentTransitionRequest;
65
+ private finding;
66
+ private lifecycle;
67
+ private transitions;
68
+ private closeTime;
69
+ private item;
70
+ list(serviceId: string, filters?: IncidentListFilters, at?: number): Promise<IncidentListResult>;
71
+ detail(serviceId: string, incidentIdentity: string, at?: number): Promise<IncidentDetailResult>;
72
+ transition(serviceId: string, value: unknown, actor: string): Promise<IncidentTransitionResult>;
73
+ private nextProjection;
74
+ }
75
+ export { LIFECYCLE_STATES };