@smthrs/journal 0.0.0-stage → 1.0.0-rc.3

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 (257) hide show
  1. package/CHANGELOG.md +149 -0
  2. package/LICENSE +21 -0
  3. package/README.md +206 -2
  4. package/dist/cjs/CallFact.d.ts +129 -0
  5. package/dist/cjs/CallFact.d.ts.map +1 -0
  6. package/dist/cjs/CallFact.js +106 -0
  7. package/dist/cjs/CallFact.js.map +7 -0
  8. package/dist/cjs/Consensus.d.ts +440 -0
  9. package/dist/cjs/Consensus.d.ts.map +1 -0
  10. package/dist/cjs/Consensus.js +251 -0
  11. package/dist/cjs/Consensus.js.map +7 -0
  12. package/dist/cjs/EngineEvent.d.ts +890 -0
  13. package/dist/cjs/EngineEvent.d.ts.map +1 -0
  14. package/dist/cjs/EngineEvent.js +383 -0
  15. package/dist/cjs/EngineEvent.js.map +7 -0
  16. package/dist/cjs/ExecutionFact.d.ts +216 -0
  17. package/dist/cjs/ExecutionFact.d.ts.map +1 -0
  18. package/dist/cjs/ExecutionFact.js +303 -0
  19. package/dist/cjs/ExecutionFact.js.map +7 -0
  20. package/dist/cjs/Journal.d.ts +617 -0
  21. package/dist/cjs/Journal.d.ts.map +1 -0
  22. package/dist/cjs/Journal.js +190 -0
  23. package/dist/cjs/Journal.js.map +7 -0
  24. package/dist/cjs/JournalEvent.d.ts +421 -0
  25. package/dist/cjs/JournalEvent.d.ts.map +1 -0
  26. package/dist/cjs/JournalEvent.js +281 -0
  27. package/dist/cjs/JournalEvent.js.map +7 -0
  28. package/dist/cjs/JournalGeneration.d.ts +34 -0
  29. package/dist/cjs/JournalGeneration.d.ts.map +1 -0
  30. package/dist/cjs/JournalGeneration.js +72 -0
  31. package/dist/cjs/JournalGeneration.js.map +7 -0
  32. package/dist/cjs/JournalMetrics.d.ts +72 -0
  33. package/dist/cjs/JournalMetrics.d.ts.map +1 -0
  34. package/dist/cjs/JournalMetrics.js +55 -0
  35. package/dist/cjs/JournalMetrics.js.map +7 -0
  36. package/dist/cjs/Migrations.d.ts +42 -0
  37. package/dist/cjs/Migrations.d.ts.map +1 -0
  38. package/dist/cjs/Migrations.js +58 -0
  39. package/dist/cjs/Migrations.js.map +7 -0
  40. package/dist/cjs/OwnerId.d.ts +40 -0
  41. package/dist/cjs/OwnerId.d.ts.map +1 -0
  42. package/dist/cjs/OwnerId.js +40 -0
  43. package/dist/cjs/OwnerId.js.map +7 -0
  44. package/dist/cjs/Projection.d.ts +29 -0
  45. package/dist/cjs/Projection.d.ts.map +1 -0
  46. package/dist/cjs/Projection.js +25 -0
  47. package/dist/cjs/Projection.js.map +7 -0
  48. package/dist/cjs/RedactedLogger.d.ts +121 -0
  49. package/dist/cjs/RedactedLogger.d.ts.map +1 -0
  50. package/dist/cjs/RedactedLogger.js +235 -0
  51. package/dist/cjs/RedactedLogger.js.map +7 -0
  52. package/dist/cjs/Redaction.d.ts +350 -0
  53. package/dist/cjs/Redaction.d.ts.map +1 -0
  54. package/dist/cjs/Redaction.js +928 -0
  55. package/dist/cjs/Redaction.js.map +7 -0
  56. package/dist/cjs/SqlConsensus.d.ts +44 -0
  57. package/dist/cjs/SqlConsensus.d.ts.map +1 -0
  58. package/dist/cjs/SqlConsensus.js +243 -0
  59. package/dist/cjs/SqlConsensus.js.map +7 -0
  60. package/dist/cjs/SqlJournal.d.ts +173 -0
  61. package/dist/cjs/SqlJournal.d.ts.map +1 -0
  62. package/dist/cjs/SqlJournal.js +1568 -0
  63. package/dist/cjs/SqlJournal.js.map +7 -0
  64. package/dist/cjs/StepFact.d.ts +66 -0
  65. package/dist/cjs/StepFact.d.ts.map +1 -0
  66. package/dist/cjs/StepFact.js +68 -0
  67. package/dist/cjs/StepFact.js.map +7 -0
  68. package/dist/cjs/index.d.ts +95 -0
  69. package/dist/cjs/index.d.ts.map +1 -0
  70. package/dist/cjs/index.js +63 -0
  71. package/dist/cjs/index.js.map +7 -0
  72. package/dist/cjs/internal/JournalInternalFault.d.ts +37 -0
  73. package/dist/cjs/internal/JournalInternalFault.d.ts.map +1 -0
  74. package/dist/cjs/internal/JournalInternalFault.js +43 -0
  75. package/dist/cjs/internal/JournalInternalFault.js.map +7 -0
  76. package/dist/cjs/migrations/0001_initial.d.ts +15 -0
  77. package/dist/cjs/migrations/0001_initial.d.ts.map +1 -0
  78. package/dist/cjs/migrations/0001_initial.js +54 -0
  79. package/dist/cjs/migrations/0001_initial.js.map +7 -0
  80. package/dist/cjs/migrations/0002_checkpoints.d.ts +27 -0
  81. package/dist/cjs/migrations/0002_checkpoints.d.ts.map +1 -0
  82. package/dist/cjs/migrations/0002_checkpoints.js +48 -0
  83. package/dist/cjs/migrations/0002_checkpoints.js.map +7 -0
  84. package/dist/cjs/migrations/0003_startup_index.d.ts +15 -0
  85. package/dist/cjs/migrations/0003_startup_index.d.ts.map +1 -0
  86. package/dist/cjs/migrations/0003_startup_index.js +40 -0
  87. package/dist/cjs/migrations/0003_startup_index.js.map +7 -0
  88. package/dist/cjs/migrations/0004_dedup.d.ts +15 -0
  89. package/dist/cjs/migrations/0004_dedup.d.ts.map +1 -0
  90. package/dist/cjs/migrations/0004_dedup.js +57 -0
  91. package/dist/cjs/migrations/0004_dedup.js.map +7 -0
  92. package/dist/cjs/migrations/0005_run_event_type.d.ts +15 -0
  93. package/dist/cjs/migrations/0005_run_event_type.d.ts.map +1 -0
  94. package/dist/cjs/migrations/0005_run_event_type.js +40 -0
  95. package/dist/cjs/migrations/0005_run_event_type.js.map +7 -0
  96. package/dist/cjs/migrations/0006_consensus.d.ts +22 -0
  97. package/dist/cjs/migrations/0006_consensus.d.ts.map +1 -0
  98. package/dist/cjs/migrations/0006_consensus.js +84 -0
  99. package/dist/cjs/migrations/0006_consensus.js.map +7 -0
  100. package/dist/cjs/package.json +1 -0
  101. package/dist/cjs/test/Notifying.d.ts +65 -0
  102. package/dist/cjs/test/Notifying.d.ts.map +1 -0
  103. package/dist/cjs/test/Notifying.js +62 -0
  104. package/dist/cjs/test/Notifying.js.map +7 -0
  105. package/dist/cjs/test/TestJournal.d.ts +43 -0
  106. package/dist/cjs/test/TestJournal.d.ts.map +1 -0
  107. package/dist/cjs/test/TestJournal.js +43 -0
  108. package/dist/cjs/test/TestJournal.js.map +7 -0
  109. package/dist/esm/CallFact.d.ts +129 -0
  110. package/dist/esm/CallFact.d.ts.map +1 -0
  111. package/dist/esm/CallFact.js +96 -0
  112. package/dist/esm/CallFact.js.map +1 -0
  113. package/dist/esm/Consensus.d.ts +440 -0
  114. package/dist/esm/Consensus.d.ts.map +1 -0
  115. package/dist/esm/Consensus.js +454 -0
  116. package/dist/esm/Consensus.js.map +1 -0
  117. package/dist/esm/EngineEvent.d.ts +890 -0
  118. package/dist/esm/EngineEvent.d.ts.map +1 -0
  119. package/dist/esm/EngineEvent.js +614 -0
  120. package/dist/esm/EngineEvent.js.map +1 -0
  121. package/dist/esm/ExecutionFact.d.ts +216 -0
  122. package/dist/esm/ExecutionFact.d.ts.map +1 -0
  123. package/dist/esm/ExecutionFact.js +374 -0
  124. package/dist/esm/ExecutionFact.js.map +1 -0
  125. package/dist/esm/Journal.d.ts +617 -0
  126. package/dist/esm/Journal.d.ts.map +1 -0
  127. package/dist/esm/Journal.js +323 -0
  128. package/dist/esm/Journal.js.map +1 -0
  129. package/dist/esm/JournalEvent.d.ts +421 -0
  130. package/dist/esm/JournalEvent.d.ts.map +1 -0
  131. package/dist/esm/JournalEvent.js +517 -0
  132. package/dist/esm/JournalEvent.js.map +1 -0
  133. package/dist/esm/JournalGeneration.d.ts +34 -0
  134. package/dist/esm/JournalGeneration.d.ts.map +1 -0
  135. package/dist/esm/JournalGeneration.js +65 -0
  136. package/dist/esm/JournalGeneration.js.map +1 -0
  137. package/dist/esm/JournalMetrics.d.ts +72 -0
  138. package/dist/esm/JournalMetrics.d.ts.map +1 -0
  139. package/dist/esm/JournalMetrics.js +79 -0
  140. package/dist/esm/JournalMetrics.js.map +1 -0
  141. package/dist/esm/Migrations.d.ts +42 -0
  142. package/dist/esm/Migrations.d.ts.map +1 -0
  143. package/dist/esm/Migrations.js +59 -0
  144. package/dist/esm/Migrations.js.map +1 -0
  145. package/dist/esm/OwnerId.d.ts +40 -0
  146. package/dist/esm/OwnerId.d.ts.map +1 -0
  147. package/dist/esm/OwnerId.js +33 -0
  148. package/dist/esm/OwnerId.js.map +1 -0
  149. package/dist/esm/Projection.d.ts +29 -0
  150. package/dist/esm/Projection.d.ts.map +1 -0
  151. package/dist/esm/Projection.js +13 -0
  152. package/dist/esm/Projection.js.map +1 -0
  153. package/dist/esm/RedactedLogger.d.ts +121 -0
  154. package/dist/esm/RedactedLogger.d.ts.map +1 -0
  155. package/dist/esm/RedactedLogger.js +445 -0
  156. package/dist/esm/RedactedLogger.js.map +1 -0
  157. package/dist/esm/Redaction.d.ts +350 -0
  158. package/dist/esm/Redaction.d.ts.map +1 -0
  159. package/dist/esm/Redaction.js +1584 -0
  160. package/dist/esm/Redaction.js.map +1 -0
  161. package/dist/esm/SqlConsensus.d.ts +44 -0
  162. package/dist/esm/SqlConsensus.d.ts.map +1 -0
  163. package/dist/esm/SqlConsensus.js +252 -0
  164. package/dist/esm/SqlConsensus.js.map +1 -0
  165. package/dist/esm/SqlJournal.d.ts +173 -0
  166. package/dist/esm/SqlJournal.d.ts.map +1 -0
  167. package/dist/esm/SqlJournal.js +1858 -0
  168. package/dist/esm/SqlJournal.js.map +1 -0
  169. package/dist/esm/StepFact.d.ts +66 -0
  170. package/dist/esm/StepFact.d.ts.map +1 -0
  171. package/dist/esm/StepFact.js +50 -0
  172. package/dist/esm/StepFact.js.map +1 -0
  173. package/dist/esm/index.d.ts +95 -0
  174. package/dist/esm/index.d.ts.map +1 -0
  175. package/dist/esm/index.js +95 -0
  176. package/dist/esm/index.js.map +1 -0
  177. package/dist/esm/internal/JournalInternalFault.d.ts +37 -0
  178. package/dist/esm/internal/JournalInternalFault.d.ts.map +1 -0
  179. package/dist/esm/internal/JournalInternalFault.js +28 -0
  180. package/dist/esm/internal/JournalInternalFault.js.map +1 -0
  181. package/dist/esm/migrations/0001_initial.d.ts +15 -0
  182. package/dist/esm/migrations/0001_initial.d.ts.map +1 -0
  183. package/dist/esm/migrations/0001_initial.js +32 -0
  184. package/dist/esm/migrations/0001_initial.js.map +1 -0
  185. package/dist/esm/migrations/0002_checkpoints.d.ts +27 -0
  186. package/dist/esm/migrations/0002_checkpoints.d.ts.map +1 -0
  187. package/dist/esm/migrations/0002_checkpoints.js +38 -0
  188. package/dist/esm/migrations/0002_checkpoints.js.map +1 -0
  189. package/dist/esm/migrations/0003_startup_index.d.ts +15 -0
  190. package/dist/esm/migrations/0003_startup_index.d.ts.map +1 -0
  191. package/dist/esm/migrations/0003_startup_index.js +18 -0
  192. package/dist/esm/migrations/0003_startup_index.js.map +1 -0
  193. package/dist/esm/migrations/0004_dedup.d.ts +15 -0
  194. package/dist/esm/migrations/0004_dedup.d.ts.map +1 -0
  195. package/dist/esm/migrations/0004_dedup.js +38 -0
  196. package/dist/esm/migrations/0004_dedup.js.map +1 -0
  197. package/dist/esm/migrations/0005_run_event_type.d.ts +15 -0
  198. package/dist/esm/migrations/0005_run_event_type.d.ts.map +1 -0
  199. package/dist/esm/migrations/0005_run_event_type.js +18 -0
  200. package/dist/esm/migrations/0005_run_event_type.js.map +1 -0
  201. package/dist/esm/migrations/0006_consensus.d.ts +22 -0
  202. package/dist/esm/migrations/0006_consensus.d.ts.map +1 -0
  203. package/dist/esm/migrations/0006_consensus.js +69 -0
  204. package/dist/esm/migrations/0006_consensus.js.map +1 -0
  205. package/dist/esm/test/Notifying.d.ts +65 -0
  206. package/dist/esm/test/Notifying.d.ts.map +1 -0
  207. package/dist/esm/test/Notifying.js +62 -0
  208. package/dist/esm/test/Notifying.js.map +1 -0
  209. package/dist/esm/test/TestJournal.d.ts +43 -0
  210. package/dist/esm/test/TestJournal.d.ts.map +1 -0
  211. package/dist/esm/test/TestJournal.js +27 -0
  212. package/dist/esm/test/TestJournal.js.map +1 -0
  213. package/docs/README.md +164 -0
  214. package/docs/api.md +639 -0
  215. package/docs/concepts/compaction.md +157 -0
  216. package/docs/concepts/idempotency.md +127 -0
  217. package/docs/concepts/owner-fence.md +124 -0
  218. package/docs/concepts/redaction.md +218 -0
  219. package/docs/concepts/state-event-authority.md +159 -0
  220. package/docs/concepts/two-channels.md +167 -0
  221. package/docs/guides/commit-state-and-entry.md +114 -0
  222. package/docs/guides/compact-a-run.md +134 -0
  223. package/docs/guides/fold-a-projection.md +106 -0
  224. package/docs/guides/read-a-run.md +139 -0
  225. package/docs/guides/redact-log-output.md +94 -0
  226. package/docs/guides/testing.md +124 -0
  227. package/docs/guides/write-lifecycle-events.md +110 -0
  228. package/docs/installation.md +133 -0
  229. package/docs/quickstart.md +143 -0
  230. package/docs/troubleshooting.md +214 -0
  231. package/package.json +263 -3
  232. package/src/CallFact.ts +117 -0
  233. package/src/Consensus.ts +683 -0
  234. package/src/EngineEvent.ts +823 -0
  235. package/src/ExecutionFact.ts +433 -0
  236. package/src/Journal.ts +688 -0
  237. package/src/JournalEvent.ts +654 -0
  238. package/src/JournalGeneration.ts +77 -0
  239. package/src/JournalMetrics.ts +89 -0
  240. package/src/Migrations.ts +62 -0
  241. package/src/OwnerId.ts +42 -0
  242. package/src/Projection.ts +31 -0
  243. package/src/RedactedLogger.ts +483 -0
  244. package/src/Redaction.ts +1784 -0
  245. package/src/SqlConsensus.ts +344 -0
  246. package/src/SqlJournal.ts +2732 -0
  247. package/src/StepFact.ts +69 -0
  248. package/src/index.ts +109 -0
  249. package/src/internal/JournalInternalFault.ts +40 -0
  250. package/src/migrations/0001_initial.ts +41 -0
  251. package/src/migrations/0002_checkpoints.ts +46 -0
  252. package/src/migrations/0003_startup_index.ts +19 -0
  253. package/src/migrations/0004_dedup.ts +43 -0
  254. package/src/migrations/0005_run_event_type.ts +19 -0
  255. package/src/migrations/0006_consensus.ts +83 -0
  256. package/src/test/Notifying.ts +101 -0
  257. package/src/test/TestJournal.ts +52 -0
@@ -0,0 +1,433 @@
1
+ /**
2
+ * Native execution observations, independent of operational ownership leases.
3
+ * @since 1.0.0
4
+ */
5
+
6
+ import * as Schema from "effect/Schema"
7
+ import * as Event from "./JournalEvent.ts"
8
+
9
+ /** Native semantic lifecycle fields recorded by the owning writer.
10
+ * @category schemas
11
+ * @since 1.0.0
12
+ */
13
+ export const Observation = Schema.Struct({
14
+ executionId: Event.RunId,
15
+ flowName: Schema.NonEmptyString,
16
+ status: Schema.Literals(["pending", "running", "suspended", "completed", "failed", "cancelled"]),
17
+ createdAtMs: Event.TimestampMs,
18
+ startedAtMs: Schema.NullOr(Event.TimestampMs),
19
+ finishedAtMs: Schema.NullOr(Event.TimestampMs),
20
+ parentRunId: Schema.NullOr(Event.RunId),
21
+ lineageId: Event.RunId,
22
+ roundOrdinal: Event.NonNegativeQuantity,
23
+ cancelRequestedAtMs: Schema.NullOr(Event.TimestampMs),
24
+ // Additive v1 extension: absent means the producer did not cover tree waits.
25
+ treeVersion: Schema.optional(Schema.Literal(1)),
26
+ parentPolicy: Schema.optional(Schema.Literals(["cancel", "detach"])),
27
+ waiting: Schema.NullOr(Schema.Struct({
28
+ reason: Schema.NonEmptyString,
29
+ wakeAtMs: Schema.NullOr(Event.TimestampMs),
30
+ tokenDigest: Schema.NullOr(Schema.NonEmptyString),
31
+ point: Schema.optional(Schema.NullOr(Schema.String)),
32
+ request: Schema.optional(Schema.NullOr(Schema.Json))
33
+ }))
34
+ })
35
+ /** The value form of a native execution observation.
36
+ * @category models
37
+ * @since 1.0.0
38
+ */
39
+ export type Observation = typeof Observation.Type
40
+
41
+ /** A new observation baseline never invents the preceding history.
42
+ * @category schemas
43
+ * @since 1.0.0
44
+ */
45
+ export const Fact = Schema.Struct({
46
+ version: Schema.Literal(1),
47
+ baseline: Schema.Literals(["created", "legacy"]),
48
+ observation: Observation
49
+ })
50
+
51
+ /** A corrupt/legacy row can still settle, but cannot establish a replay baseline.
52
+ * @category schemas
53
+ * @since 1.0.0
54
+ */
55
+ export const UnavailableFact = Schema.Struct({
56
+ version: Schema.Literal(1),
57
+ unavailable: Schema.Literal("invalid-observation")
58
+ })
59
+
60
+ /** Root identity stays fixed while lifecycle follows its latest trampoline round.
61
+ * @category schemas
62
+ * @since 1.0.0
63
+ */
64
+ export const View = Schema.Struct({
65
+ root: Observation,
66
+ current: Observation,
67
+ /** Redacted display observations; opaque wake tokens are never journal authority. */
68
+ humanWaits: Schema.optional(Schema.Array(Observation))
69
+ })
70
+ /** The value form of a logical execution view.
71
+ * @category models
72
+ * @since 1.0.0
73
+ */
74
+ export type View = typeof View.Type
75
+
76
+ /** Source positions are native run-local sequence plus native journal generation.
77
+ * @category models
78
+ * @since 1.0.0
79
+ */
80
+ export interface Input {
81
+ readonly executionId: string
82
+ readonly generation: number | null
83
+ readonly sequence: number
84
+ readonly eventType: string
85
+ readonly payload: unknown
86
+ readonly gap?: boolean
87
+ }
88
+ /** Honest replay coverage of the selected native root and current round.
89
+ * @category schemas
90
+ * @since 1.0.0
91
+ */
92
+ export const Provenance = Schema.Struct({
93
+ source: Schema.Literals(["events", "legacy-observation", "unverified-observation"]),
94
+ rootExecutionId: Schema.String,
95
+ humanWaits: Schema.optional(Schema.Literals(["events", "legacy-observation", "unverified-observation"])),
96
+ /** Tree coverage starts independently in every native run-local stream. */
97
+ humanWaitSources: Schema.optional(Schema.Array(Schema.Struct({
98
+ executionId: Schema.String,
99
+ generation: Schema.Number,
100
+ baseline: Schema.Literals(["created", "legacy"]),
101
+ fromSequence: Schema.Number,
102
+ throughSequence: Schema.Number
103
+ }))),
104
+ currentExecutionId: Schema.optional(Schema.String),
105
+ generation: Schema.optional(Schema.Number),
106
+ baseline: Schema.optional(Schema.Literals(["created", "legacy"])),
107
+ fromSequence: Schema.optional(Schema.Number),
108
+ throughSequence: Schema.optional(Schema.Number)
109
+ })
110
+
111
+ const record = (value: unknown): Record<string, unknown> =>
112
+ value !== null && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : {}
113
+
114
+ // The journal canonicalizes object key order. Display metadata compares JSON
115
+ // values, so a writer's insertion order cannot turn an equal question into a gap.
116
+ const equalJson = (left: unknown, right: unknown): boolean => {
117
+ if (left === right) return true
118
+ if (left === null || right === null || typeof left !== "object" || typeof right !== "object") return false
119
+ if (Array.isArray(left) || Array.isArray(right)) {
120
+ return Array.isArray(left) && Array.isArray(right) &&
121
+ left.length === right.length && left.every((value, index) => equalJson(value, right[index]))
122
+ }
123
+ const keys = Object.keys(left).sort()
124
+ const other = Object.keys(right).sort()
125
+ return keys.length === other.length && keys.every((key, index) =>
126
+ key === other[index] &&
127
+ equalJson((left as Record<string, unknown>)[key], (right as Record<string, unknown>)[key])
128
+ )
129
+ }
130
+
131
+ /** Compare semantic state only; heartbeat and owner incarnation are operational.
132
+ * @category projections
133
+ * @since 1.0.0
134
+ */
135
+ export const equal = (left: Observation, right: Observation): boolean =>
136
+ left.executionId === right.executionId && left.flowName === right.flowName && left.status === right.status &&
137
+ left.createdAtMs === right.createdAtMs && left.startedAtMs === right.startedAtMs &&
138
+ left.finishedAtMs === right.finishedAtMs &&
139
+ left.parentRunId === right.parentRunId && left.lineageId === right.lineageId &&
140
+ left.roundOrdinal === right.roundOrdinal &&
141
+ left.cancelRequestedAtMs === right.cancelRequestedAtMs &&
142
+ left.treeVersion === right.treeVersion && left.parentPolicy === right.parentPolicy &&
143
+ (left.waiting === null ? right.waiting === null : right.waiting !== null &&
144
+ left.waiting.reason === right.waiting.reason && left.waiting.wakeAtMs === right.waiting.wakeAtMs &&
145
+ left.waiting.tokenDigest === right.waiting.tokenDigest && left.waiting.point === right.waiting.point &&
146
+ equalJson(left.waiting.request, right.waiting.request))
147
+
148
+ /**
149
+ * Fold native facts after a separately authenticated root binding. Sequence gaps
150
+ * alone are not loss: SQL journal rollbacks leave unused sequence reservations.
151
+ * A real omission/generation boundary invalidates coverage until a new complete
152
+ * baseline is recorded. Unknown producer versions never become successful replay.
153
+ * @category projections
154
+ * @since 1.0.0
155
+ */
156
+ export const fold = (inputs: ReadonlyArray<Input>, rootExecutionId: string, observed?: View): {
157
+ readonly view: View | undefined
158
+ readonly provenance: typeof Provenance.Type
159
+ } => {
160
+ interface State {
161
+ generation: number
162
+ sequence: number
163
+ observation?: Observation
164
+ baseline?: "created" | "legacy"
165
+ fromSequence?: number
166
+ treeFromSequence?: number
167
+ treeBaseline?: "created" | "legacy"
168
+ uncovered: boolean
169
+ }
170
+ const states = new Map<string, State>()
171
+ for (const input of inputs) {
172
+ let state = states.get(input.executionId)
173
+ if (input.generation !== null && state !== undefined && input.generation < state.generation) continue
174
+ if (input.gap) {
175
+ if (state === undefined) {
176
+ states.set(input.executionId, {
177
+ generation: input.generation ?? -1,
178
+ sequence: -1,
179
+ uncovered: true
180
+ })
181
+ } else {
182
+ if (input.generation !== null && input.generation > state.generation) {
183
+ state = { generation: input.generation, sequence: -1, uncovered: true }
184
+ states.set(input.executionId, state)
185
+ }
186
+ state.uncovered = true
187
+ }
188
+ continue
189
+ }
190
+ if (input.generation === null) continue
191
+ if (state === undefined || input.generation > state.generation) {
192
+ state = { generation: input.generation, sequence: -1, uncovered: state !== undefined || input.generation > 0 }
193
+ states.set(input.executionId, state)
194
+ }
195
+ if (input.sequence <= state.sequence) continue
196
+ state.sequence = input.sequence
197
+ const payload = record(input.payload)
198
+ const candidate = payload.executionFact
199
+ if (candidate === undefined) {
200
+ // Diagnostic decisions and wake requests are not lifecycle mutations.
201
+ if (
202
+ state.observation !== undefined && input.eventType === "flows.engine.run-decision" &&
203
+ ![
204
+ "wake-scheduled",
205
+ "claim-lost",
206
+ "activation-lost",
207
+ "steal-refused-owner-alive",
208
+ "child-policy-applied",
209
+ "lease-reconfirmed"
210
+ ]
211
+ .includes(String(payload.decision))
212
+ ) {
213
+ state.uncovered = true
214
+ }
215
+ if (state.observation !== undefined && input.eventType === "flows.engine.interrupted") state.uncovered = true
216
+ continue
217
+ }
218
+ if (
219
+ (input.eventType !== "flows.engine.run-decision" && input.eventType !== "flows.engine.interrupted") ||
220
+ !Schema.is(Fact)(candidate) || candidate.observation.executionId !== input.executionId
221
+ ) {
222
+ state.uncovered = true
223
+ continue
224
+ }
225
+ if (state.observation === undefined || state.uncovered) {
226
+ state.baseline = state.uncovered ? "legacy" : candidate.baseline
227
+ state.fromSequence = input.sequence
228
+ }
229
+ const completeTree = candidate.observation.treeVersion === 1 && candidate.observation.parentPolicy !== undefined &&
230
+ (candidate.observation.waiting === null || candidate.observation.waiting.point !== undefined &&
231
+ candidate.observation.waiting.request !== undefined)
232
+ if (completeTree && (state.treeFromSequence === undefined || state.uncovered)) {
233
+ state.treeFromSequence = input.sequence
234
+ state.treeBaseline = state.uncovered || state.observation !== undefined ? "legacy" : candidate.baseline
235
+ } else if (!completeTree) {
236
+ delete state.treeFromSequence
237
+ delete state.treeBaseline
238
+ }
239
+ state.observation = candidate.observation
240
+ state.uncovered = false
241
+ }
242
+ const root = states.get(rootExecutionId)
243
+ let current = root
244
+ if (root?.observation !== undefined) {
245
+ let ordinal = root.observation.roundOrdinal
246
+ for (const state of states.values()) {
247
+ if (
248
+ state.observation?.lineageId === root.observation.lineageId &&
249
+ state.observation.roundOrdinal > ordinal
250
+ ) {
251
+ current = state
252
+ ordinal = state.observation.roundOrdinal
253
+ }
254
+ }
255
+ }
256
+ const treeApplicable = observed?.humanWaits !== undefined || current?.observation?.treeVersion === 1
257
+ const reachable = new Map<string, number>()
258
+ if (current?.observation !== undefined) reachable.set(current.observation.executionId, 0)
259
+ for (let depth = 1; depth <= 64; depth++) {
260
+ let added = false
261
+ for (const [id, state] of states) {
262
+ const observation = state.observation
263
+ if (
264
+ reachable.has(id) || observation === undefined || observation.parentRunId === null ||
265
+ observation.parentPolicy === "detach" || reachable.get(observation.parentRunId) !== depth - 1
266
+ ) continue
267
+ reachable.set(id, depth)
268
+ added = true
269
+ }
270
+ if (!added) break
271
+ }
272
+ const tree = [...reachable].map(([id, depth]) => ({ state: states.get(id)!, depth }))
273
+ tree.sort((a, b) =>
274
+ a.depth - b.depth ||
275
+ a.state.observation!.createdAtMs - b.state.observation!.createdAtMs ||
276
+ // Execution ids are the reachable-map keys, so no two entries share one
277
+ // and the comparator never has to report a tie here.
278
+ (a.state.observation!.executionId < b.state.observation!.executionId ? -1 : 1)
279
+ )
280
+ const humanWaits = tree.flatMap(({ state }) => {
281
+ const observation = state.observation!
282
+ return !["completed", "failed", "cancelled"].includes(observation.status) &&
283
+ observation.waiting?.reason === "approval" && observation.waiting.tokenDigest !== null
284
+ ? [observation] :
285
+ []
286
+ })
287
+ // SQL and memory stores can enumerate an equal set of waits differently.
288
+ // Verify every identity and value without making store order semantic.
289
+ const observedHumanWaits = observed?.humanWaits
290
+ const observedWaits = new Map((observedHumanWaits ?? []).map((wait) => [wait.executionId, wait]))
291
+ const treeCovered = treeApplicable && tree.length > 0 &&
292
+ // A missing/corrupt stream has unknown ancestry and cannot prove absence of a human wait.
293
+ [...states.values()].every((state) => state.observation !== undefined && !state.uncovered) &&
294
+ tree.every(({ state }) =>
295
+ state.observation?.treeVersion === 1 && state.observation.parentPolicy !== undefined &&
296
+ (state.observation.waiting === null || state.observation.waiting.point !== undefined &&
297
+ state.observation.waiting.request !== undefined)
298
+ ) &&
299
+ (observedHumanWaits === undefined || humanWaits.length === observedHumanWaits.length &&
300
+ observedWaits.size === humanWaits.length && humanWaits.every((wait) => {
301
+ const other = observedWaits.get(wait.executionId)
302
+ return other !== undefined && equal(wait, other)
303
+ }))
304
+ const view = root?.observation !== undefined && current?.observation !== undefined
305
+ ? { root: root.observation, current: current.observation, ...(treeApplicable ? { humanWaits } : {}) } :
306
+ undefined
307
+ const covered = view !== undefined && !root?.uncovered && !current?.uncovered &&
308
+ (!treeApplicable || treeCovered) &&
309
+ (observed === undefined || equal(view.root, observed.root) && equal(view.current, observed.current))
310
+ return {
311
+ view: covered ? view : observed,
312
+ provenance: {
313
+ source: covered
314
+ ? "events"
315
+ : root?.observation === undefined && !root?.uncovered
316
+ ? "legacy-observation"
317
+ : "unverified-observation",
318
+ rootExecutionId,
319
+ ...(treeApplicable ?
320
+ {
321
+ humanWaits: treeCovered ?
322
+ "events" as const :
323
+ current?.observation?.treeVersion === 1
324
+ ? "unverified-observation" as const
325
+ : "legacy-observation" as const
326
+ } :
327
+ {}),
328
+ ...(treeCovered ?
329
+ {
330
+ humanWaitSources: tree.map(({ state }) => ({
331
+ executionId: state.observation!.executionId,
332
+ generation: state.generation,
333
+ baseline: state.treeBaseline!,
334
+ fromSequence: state.treeFromSequence!,
335
+ throughSequence: state.sequence
336
+ }))
337
+ } :
338
+ {}),
339
+ ...(current?.observation === undefined ? {} : { currentExecutionId: current.observation.executionId }),
340
+ ...(current === undefined ? {} : { generation: current.generation }),
341
+ ...(current?.baseline === undefined
342
+ ? {}
343
+ : { baseline: root?.baseline === "legacy" ? "legacy" : current.baseline }),
344
+ ...(current?.fromSequence === undefined ? {} : { fromSequence: current.fromSequence }),
345
+ ...(current === undefined ? {} : { throughSequence: current.sequence })
346
+ }
347
+ }
348
+ }
349
+
350
+ /** Control bridge inputs retain their native run-local positions and generation.
351
+ * @category models
352
+ * @since 1.0.0
353
+ */
354
+ export interface BridgeEvent {
355
+ readonly runId?: string | undefined
356
+ readonly kind: string
357
+ readonly payload: unknown
358
+ }
359
+
360
+ /** Interpret only the host's explicit root binding, never an ID naming convention.
361
+ * @category projections
362
+ * @since 1.0.0
363
+ */
364
+ export const foldControl = (events: ReadonlyArray<BridgeEvent>, controlRunId: string, observed?: View) => {
365
+ let root: string | undefined
366
+ let conflicting = false
367
+ const inputs: Array<Input> = []
368
+ for (const event of events) {
369
+ if (event.runId !== controlRunId) continue
370
+ const payload = record(event.payload)
371
+ if (event.kind === "control.engine.bound") {
372
+ if (
373
+ payload.version !== 1 || payload.controlRunId !== controlRunId || typeof payload.executionId !== "string" ||
374
+ payload.executionId.length === 0
375
+ ) {
376
+ conflicting = true
377
+ } else if (root !== undefined && root !== payload.executionId) conflicting = true
378
+ else root = payload.executionId
379
+ }
380
+ if (
381
+ event.kind === "control.engine.event" && payload.version === 1 && typeof payload.executionId === "string" &&
382
+ typeof payload.generation === "number" && Number.isSafeInteger(payload.generation) && payload.generation >= 0 &&
383
+ typeof payload.sequence === "number" && Number.isSafeInteger(payload.sequence) && payload.sequence >= 0 &&
384
+ typeof payload.eventType === "string"
385
+ ) {
386
+ inputs.push({
387
+ executionId: payload.executionId,
388
+ generation: payload.generation,
389
+ sequence: payload.sequence,
390
+ eventType: payload.eventType,
391
+ payload: payload.payload
392
+ })
393
+ }
394
+ if (
395
+ event.kind === "control.engine.event" &&
396
+ !(payload.version === 1 && typeof payload.executionId === "string" &&
397
+ typeof payload.generation === "number" && Number.isSafeInteger(payload.generation) && payload.generation >= 0 &&
398
+ typeof payload.sequence === "number" && Number.isSafeInteger(payload.sequence) && payload.sequence >= 0 &&
399
+ typeof payload.eventType === "string")
400
+ ) {
401
+ if (typeof payload.executionId !== "string") conflicting = true
402
+ else {inputs.push({
403
+ executionId: payload.executionId,
404
+ generation: null,
405
+ sequence: -1,
406
+ eventType: event.kind,
407
+ payload,
408
+ gap: true
409
+ })}
410
+ }
411
+ if (event.kind === "control.engine.projection-gap" && typeof payload.executionId === "string") {
412
+ inputs.push({
413
+ executionId: payload.executionId,
414
+ generation: typeof payload.generation === "number" ? payload.generation : null,
415
+ sequence: -1,
416
+ eventType: event.kind,
417
+ payload,
418
+ gap: true
419
+ })
420
+ }
421
+ }
422
+ if (root === undefined || conflicting || observed !== undefined && observed.root.executionId !== root) {
423
+ return observed === undefined ? undefined : {
424
+ view: observed,
425
+ provenance: {
426
+ source: conflicting || root !== undefined ? "unverified-observation" as const : "legacy-observation" as const,
427
+ rootExecutionId: observed.root.executionId,
428
+ currentExecutionId: observed.current.executionId
429
+ }
430
+ }
431
+ }
432
+ return fold(inputs, root, observed)
433
+ }