@kairos-es/codec 1.0.0-alpha.3 → 1.0.0-alpha.5

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 (143) hide show
  1. package/README.md +27 -2
  2. package/dist/cjs/MetadataSource.js +10 -0
  3. package/dist/cjs/MetadataSource.js.map +1 -0
  4. package/dist/cjs/Serializer.js +3 -3
  5. package/dist/cjs/Serializer.js.map +1 -1
  6. package/dist/cjs/Uuid.js +6 -3
  7. package/dist/cjs/Uuid.js.map +1 -1
  8. package/dist/cjs/decider.js +1 -1
  9. package/dist/cjs/decisionModel.js +1 -1
  10. package/dist/cjs/decisionModel.js.map +1 -1
  11. package/dist/cjs/decode.js +5 -5
  12. package/dist/cjs/decode.js.map +1 -1
  13. package/dist/cjs/deserialiseThenDecode.js +1 -1
  14. package/dist/cjs/deserialiseThenDecode.js.map +1 -1
  15. package/dist/cjs/errors.js +6 -4
  16. package/dist/cjs/errors.js.map +1 -1
  17. package/dist/cjs/event.js.map +1 -1
  18. package/dist/cjs/index.js +7 -0
  19. package/dist/cjs/index.js.map +1 -1
  20. package/dist/cjs/precondition.js +2 -2
  21. package/dist/cjs/precondition.js.map +1 -1
  22. package/dist/cjs/projection.js +9 -10
  23. package/dist/cjs/projection.js.map +1 -1
  24. package/dist/cjs/readLedger.js +20 -5
  25. package/dist/cjs/readLedger.js.map +1 -1
  26. package/dist/cjs/registry.js.map +1 -1
  27. package/dist/cjs/toEvent.js +58 -4
  28. package/dist/cjs/toEvent.js.map +1 -1
  29. package/dist/dts-cjs/MetadataSource.d.ts +8 -0
  30. package/dist/dts-cjs/MetadataSource.d.ts.map +1 -0
  31. package/dist/{dts → dts-cjs}/Serializer.d.ts +3 -3
  32. package/dist/{dts → dts-cjs}/Serializer.d.ts.map +1 -1
  33. package/dist/{dts → dts-cjs}/Uuid.d.ts +6 -3
  34. package/dist/{dts → dts-cjs}/Uuid.d.ts.map +1 -1
  35. package/dist/{dts → dts-cjs}/decider.d.ts +7 -7
  36. package/dist/{dts → dts-cjs}/decider.d.ts.map +1 -1
  37. package/dist/{dts → dts-cjs}/decision.d.ts +2 -2
  38. package/dist/{dts → dts-cjs}/decode.d.ts +3 -3
  39. package/dist/{dts → dts-cjs}/decode.d.ts.map +1 -1
  40. package/dist/{dts → dts-cjs}/deserialiseThenDecode.d.ts +3 -3
  41. package/dist/{dts → dts-cjs}/deserialiseThenDecode.d.ts.map +1 -1
  42. package/dist/{dts → dts-cjs}/errors.d.ts +4 -3
  43. package/dist/dts-cjs/errors.d.ts.map +1 -0
  44. package/dist/{dts → dts-cjs}/event.d.ts +18 -8
  45. package/dist/dts-cjs/event.d.ts.map +1 -0
  46. package/dist/{dts → dts-cjs}/index.d.ts +4 -2
  47. package/dist/dts-cjs/index.d.ts.map +1 -0
  48. package/dist/dts-cjs/package.json +3 -0
  49. package/dist/{dts → dts-cjs}/precondition.d.ts +2 -2
  50. package/dist/{dts → dts-cjs}/projection.d.ts +2 -2
  51. package/dist/dts-cjs/projection.d.ts.map +1 -0
  52. package/dist/{dts → dts-cjs}/readLedger.d.ts +4 -4
  53. package/dist/{dts → dts-cjs}/readLedger.d.ts.map +1 -1
  54. package/dist/{dts → dts-cjs}/toEvent.d.ts +15 -4
  55. package/dist/dts-cjs/toEvent.d.ts.map +1 -0
  56. package/dist/dts-esm/MetadataSource.d.ts +8 -0
  57. package/dist/dts-esm/MetadataSource.d.ts.map +1 -0
  58. package/dist/dts-esm/Serializer.d.ts +27 -0
  59. package/dist/dts-esm/Serializer.d.ts.map +1 -0
  60. package/dist/dts-esm/Uuid.d.ts +22 -0
  61. package/dist/dts-esm/Uuid.d.ts.map +1 -0
  62. package/dist/dts-esm/decider.d.ts +206 -0
  63. package/dist/dts-esm/decider.d.ts.map +1 -0
  64. package/dist/dts-esm/decision.d.ts +161 -0
  65. package/dist/dts-esm/decision.d.ts.map +1 -0
  66. package/dist/dts-esm/decisionModel.d.ts +149 -0
  67. package/dist/dts-esm/decisionModel.d.ts.map +1 -0
  68. package/dist/dts-esm/decode.d.ts +92 -0
  69. package/dist/dts-esm/decode.d.ts.map +1 -0
  70. package/dist/dts-esm/deserialiseThenDecode.d.ts +62 -0
  71. package/dist/dts-esm/deserialiseThenDecode.d.ts.map +1 -0
  72. package/dist/dts-esm/errors.d.ts +80 -0
  73. package/dist/dts-esm/errors.d.ts.map +1 -0
  74. package/dist/dts-esm/event.d.ts +81 -0
  75. package/dist/dts-esm/event.d.ts.map +1 -0
  76. package/dist/dts-esm/index.d.ts +134 -0
  77. package/dist/dts-esm/index.d.ts.map +1 -0
  78. package/dist/dts-esm/package.json +3 -0
  79. package/dist/dts-esm/precondition.d.ts +244 -0
  80. package/dist/dts-esm/precondition.d.ts.map +1 -0
  81. package/dist/dts-esm/projection.d.ts +56 -0
  82. package/dist/dts-esm/projection.d.ts.map +1 -0
  83. package/dist/dts-esm/readLedger.d.ts +263 -0
  84. package/dist/dts-esm/readLedger.d.ts.map +1 -0
  85. package/dist/dts-esm/registry.d.ts +46 -0
  86. package/dist/dts-esm/registry.d.ts.map +1 -0
  87. package/dist/dts-esm/toEvent.d.ts +45 -0
  88. package/dist/dts-esm/toEvent.d.ts.map +1 -0
  89. package/dist/esm/MetadataSource.js +3 -0
  90. package/dist/esm/MetadataSource.js.map +1 -0
  91. package/dist/esm/Serializer.js +3 -3
  92. package/dist/esm/Serializer.js.map +1 -1
  93. package/dist/esm/Uuid.js +6 -3
  94. package/dist/esm/Uuid.js.map +1 -1
  95. package/dist/esm/decider.js +1 -1
  96. package/dist/esm/decisionModel.js +1 -1
  97. package/dist/esm/decisionModel.js.map +1 -1
  98. package/dist/esm/decode.js +5 -5
  99. package/dist/esm/decode.js.map +1 -1
  100. package/dist/esm/deserialiseThenDecode.js +1 -1
  101. package/dist/esm/deserialiseThenDecode.js.map +1 -1
  102. package/dist/esm/errors.js +6 -4
  103. package/dist/esm/errors.js.map +1 -1
  104. package/dist/esm/event.js.map +1 -1
  105. package/dist/esm/index.js +4 -2
  106. package/dist/esm/index.js.map +1 -1
  107. package/dist/esm/precondition.js +2 -2
  108. package/dist/esm/precondition.js.map +1 -1
  109. package/dist/esm/projection.js +9 -10
  110. package/dist/esm/projection.js.map +1 -1
  111. package/dist/esm/readLedger.js +21 -6
  112. package/dist/esm/readLedger.js.map +1 -1
  113. package/dist/esm/registry.js.map +1 -1
  114. package/dist/esm/toEvent.js +58 -5
  115. package/dist/esm/toEvent.js.map +1 -1
  116. package/package.json +12 -7
  117. package/src/MetadataSource.ts +43 -0
  118. package/src/Serializer.ts +4 -4
  119. package/src/Uuid.ts +7 -4
  120. package/src/decider.ts +9 -9
  121. package/src/decision.ts +1 -1
  122. package/src/decisionModel.ts +1 -1
  123. package/src/decode.ts +12 -18
  124. package/src/deserialiseThenDecode.ts +5 -5
  125. package/src/errors.ts +6 -4
  126. package/src/event.ts +19 -9
  127. package/src/index.ts +4 -2
  128. package/src/precondition.ts +3 -3
  129. package/src/projection.ts +15 -20
  130. package/src/readLedger.ts +28 -12
  131. package/src/registry.ts +1 -1
  132. package/src/toEvent.ts +54 -7
  133. package/dist/dts/errors.d.ts.map +0 -1
  134. package/dist/dts/event.d.ts.map +0 -1
  135. package/dist/dts/index.d.ts.map +0 -1
  136. package/dist/dts/projection.d.ts.map +0 -1
  137. package/dist/dts/toEvent.d.ts.map +0 -1
  138. /package/dist/{dts → dts-cjs}/decision.d.ts.map +0 -0
  139. /package/dist/{dts → dts-cjs}/decisionModel.d.ts +0 -0
  140. /package/dist/{dts → dts-cjs}/decisionModel.d.ts.map +0 -0
  141. /package/dist/{dts → dts-cjs}/precondition.d.ts.map +0 -0
  142. /package/dist/{dts → dts-cjs}/registry.d.ts +0 -0
  143. /package/dist/{dts → dts-cjs}/registry.d.ts.map +0 -0
@@ -0,0 +1,206 @@
1
+ /**
2
+ * A `Decider`: the write side's unit of decision. It pairs the projections a
3
+ * command handler reads (its decision model) with an effectful `decide` step
4
+ * that turns the folded state plus a command into the events to append.
5
+ *
6
+ * This is PRODUCTION code and it lives beside the `buildDecisionModel` it
7
+ * drives. It began in `@kairos-es/testing` because the Given/When/Then surface
8
+ * was its first consumer, but nothing in it is test apparatus: every import
9
+ * resolves to this package or to `@kairos-es/core`, and a test framework is
10
+ * never one of them. That home was a real hazard rather than a tidiness
11
+ * complaint — `@kairos-es/testing` peers `@effect/vitest`, so a command handler
12
+ * typed against `Decider` (as this repo's own worked example is, in `src/` and
13
+ * not `test/`) named a package whose sibling module imports `expect` and `it` at
14
+ * top level, and a handler that actually RAN a command would have
15
+ * runtime-imported `runDecider` from it, loading a test framework into a
16
+ * production module graph. `@kairos-es/read/testing` documents that same hazard
17
+ * as the reason a subpath may add no peer; the difference here is that this is
18
+ * not a testing surface at all, so the fix is a home rather than a rule.
19
+ *
20
+ * `@kairos-es/testing` now imports these two symbols like any other consumer,
21
+ * and NO compatibility re-export is left behind there: nothing is published yet,
22
+ * so a re-export would be permanent clutter bought for no consumer, and it would
23
+ * preserve in that package's public surface exactly the coupling the move
24
+ * exists to remove. That is the argument ADR-0008 made when `retryOnConflict`
25
+ * moved to the package that owns the error it matches, and it holds here for the
26
+ * same reason — a write-side consumer depending on the wrong package is
27
+ * backwards, whichever direction the dependency points.
28
+ *
29
+ * `projections` is a *function of the command* because a DCB decision model is
30
+ * parametrised by the command's own identifiers (e.g. narrow to
31
+ * `course:{command.courseId}`), which keeps the consistency boundary — and the
32
+ * `failIfEventsMatch` guard the read produces — as tight as the decision
33
+ * actually needs. This mirrors dcb-events, where the command handler calls
34
+ * `buildDecisionModel(store, { ...projections built from command.data })`.
35
+ *
36
+ * `runDecider` runs the DCB command cycle against whatever `DcbEventStore` is in
37
+ * context: build the decision model for the command (a real `read` + decode +
38
+ * fold via `buildDecisionModel`), decide, then `append` the new events under the
39
+ * append condition the read produced. That IS the optimistic-concurrency
40
+ * contract, in production and in a scenario alike — which is also why the
41
+ * Given/When/Then surface built on it exercises the real fold and the real store
42
+ * rather than a mock. It is a free function rather than a `Context.Service`,
43
+ * so it is still not the "production command-runner service method" a retrying
44
+ * `Layer` would need (ADR-0008): the correct retry remains
45
+ * `retryOnConflict(runDecider(...))`, wrapping the whole cycle so a fresh read
46
+ * re-derives the condition.
47
+ *
48
+ * The `decide` step produces its events effectfully (they are encoded via
49
+ * `toEvent`, so it requires `Serializer | Uuid | R`) and may fail on its own
50
+ * error channel (a domain rule violation, e.g. "course already exists"); the
51
+ * append may fail with `AppendConditionFailed` if a concurrent writer moved the
52
+ * boundary, and decode may fail with `CodecError`. All surface on the error
53
+ * channel, so a caller handles all three the same way — and
54
+ * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
55
+ *
56
+ * `decide` may also answer `Unchanged` — "already done, nothing to append" — but
57
+ * only where its type says so: `Decider`'s fifth type parameter `O` defaults to
58
+ * the non-empty batch, and `runDecider` returns `DecisionResult<O>`, which is
59
+ * plain `Position` unless `O` admits `Unchanged`. So a decider that cannot say
60
+ * "already done" keeps exactly the result type it always had — an untyped one
61
+ * and one built in a wrapper generic in its success type included — and one
62
+ * that can makes its caller handle `Position | Unchanged`. `runDecider` returns an
63
+ * `Unchanged` without calling `append`, so `append`'s refusal of an empty batch
64
+ * is untouched and never met. `decision.ts` owns why it is a success rather than
65
+ * an error or an empty batch, and why it is recognised by a type id.
66
+ */
67
+ import type { AppendConditionFailed, CompositeState, DcbEvent } from '@kairos-es/core';
68
+ import { DcbEventStore } from '@kairos-es/core';
69
+ import type { Array as Arr } from 'effect';
70
+ import { Effect } from 'effect';
71
+ import { type DecisionOutput, type DecisionResult } from './decision.js';
72
+ import { type RequirementOf } from './decisionModel.js';
73
+ import type { CodecError } from './errors.js';
74
+ import type { SliceProjection } from './projection.js';
75
+ import type { TaglessSliceInLedger } from './readLedger.js';
76
+ import type { Serializer } from './Serializer.js';
77
+ import type { Uuid } from './Uuid.js';
78
+ /**
79
+ * A decision unit: the projections whose folded state a command is decided
80
+ * against, and the `decide` step producing the events to append.
81
+ *
82
+ * `decide` may fail with a domain error `E` (a tagged error is idiomatic, so
83
+ * `@kairos-es/testing`'s `.thenError` can match its `_tag`). It receives the
84
+ * composed decision-model `state` and the `command`, and returns the non-empty
85
+ * batch of new events — effectfully, because each event is produced via
86
+ * `toEvent` (encode -> serialise), which requires `Serializer | Uuid | R`.
87
+ *
88
+ * The projection record is bounded by this package's `SliceProjection` (the
89
+ * schema-backed, state-erased bound for a heterogeneous record); the
90
+ * precise per-key state is recovered by core's shared `CompositeState`. The
91
+ * requirement `R` is the union carried by the record's slices' schemas.
92
+ *
93
+ * `O` is what `decide` may succeed with. It defaults to the non-empty batch, and
94
+ * a decider that can answer "already done" widens it to `DecisionOutput`
95
+ * (`NonEmptyReadonlyArray<DcbEvent> | Unchanged`). It is LAST and DEFAULTED so
96
+ * that every `Decider<P, Command, E, R>` already written means exactly what it
97
+ * meant. Its bound is `DecisionOutput`, which still excludes a possibly-empty
98
+ * array — the widening adds an answer and removes no guarantee.
99
+ */
100
+ export interface Decider<P extends Record<string, SliceProjection>, Command, E = never, R = never, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>> {
101
+ readonly projections: (command: Command) => P;
102
+ readonly decide: (state: CompositeState<P>, command: Command) => Effect.Effect<O, E, Serializer | Uuid | R>;
103
+ }
104
+ /**
105
+ * Build a `Decider` with all four of its type arguments INFERRED: the slice
106
+ * record `P` from what `projections` returns, the error union `E` and the extra
107
+ * requirement `R` from what `decide` returns. The command type is the one thing
108
+ * a caller still writes, once, as the annotation on the `projections` parameter
109
+ * — `Command` occurs only in parameter positions, so nothing else could supply
110
+ * it, and it is domain information (the command this decision is parametrised
111
+ * by) rather than plumbing.
112
+ *
113
+ * It adds no capability: the interface stays directly inhabitable and this
114
+ * returns exactly the object literal a caller would have written. What it
115
+ * changes is what an author must SPELL. An object literal annotated
116
+ * `Decider<P, Command, E>` has to state the whole slice record — a
117
+ * `ReturnType<typeof someProjection>` per key, a slice's state type being
118
+ * inferred by `projection().on().build()` and so having no name to write — plus
119
+ * the union of every error its decide branches can fail with. Dropping the
120
+ * annotation instead is not open to the author: without it `decide`'s `state`
121
+ * and `command` parameters have no contextual type and are implicitly `any`. So
122
+ * the cost landed hardest on the worked example, where some thirty lines of type
123
+ * plumbing sat between the reader and the three decisions that file exists to
124
+ * teach.
125
+ *
126
+ * `Eff` captures decide's WHOLE return type as one type parameter instead of
127
+ * inferring `E` and `R` from an `Effect<…, E, Serializer | Uuid | R>` shaped
128
+ * target, and that is the part that had to be got right rather than merely
129
+ * written down. A real `decide` returns a UNION of effects — one `Effect.fail`
130
+ * per rule it enforces, plus the produce path — and TypeScript infers a type
131
+ * parameter from a union source by choosing one constituent's candidate, not by
132
+ * unioning them. Against the shaped target `E` therefore came out as whichever
133
+ * branch won, and every other branch was then rejected against it. Inference to
134
+ * a NAKED type variable takes the union whole, which is why `Eff` is the
135
+ * parameter and `Effect.Error` / `Effect.Services` recover the two
136
+ * channels afterwards: those do distribute across a union of effects.
137
+ *
138
+ * The constraint on `Eff` is load-bearing twice over. It is what still refuses a
139
+ * `decide` that could produce an EMPTY batch, the one guarantee the shaped
140
+ * target used to give and the one `append` relies on. It is also what
141
+ * contextually types the produce path's `(event) => [event]` as a one-element
142
+ * TUPLE; unconstrained, that widens to `DcbEvent[]`, which is not a
143
+ * `NonEmptyReadonlyArray` and would fail at the call site.
144
+ *
145
+ * The single `as` is narrow and unavoidable: the declared return is a
146
+ * conditional over an unresolved `Eff`, so while `Eff` is still a type variable
147
+ * the checker cannot relate `(…) => Eff` to `(…) => Effect<…,
148
+ * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
149
+ * there is nothing for the cast to be wrong about.
150
+ *
151
+ * The success channel is how a decider opts into `Unchanged` without writing a
152
+ * fifth type argument, and it is why this function is declared TWICE. The first
153
+ * declaration bounds `Eff` by the non-empty batch and returns a `Decider` with
154
+ * the DEFAULT `O`; the second bounds it by `DecisionOutput` and returns one whose
155
+ * `O` is `DecisionOutput`. A `decide` with a branch returning
156
+ * `Decision.unchanged(…)` fails the first bound and lands on the second, and one
157
+ * with no such branch keeps the default. Either way `O` is one of the two a
158
+ * caller ever writes, never the inferred success type passed through: inference
159
+ * is more precise than anyone writes — the produce path's one-element tuple, say
160
+ * — and a `Decider<…, readonly [DcbEvent]>` would not be the
161
+ * `Decider<P, Command, E, R>` its caller spells, though it behaves identically.
162
+ * Asking by ASSIGNABILITY rather than by a conditional over `Eff`'s success is
163
+ * what keeps an untyped (`any`) success, and a success that is a type variable
164
+ * bounded by the batch, on the default; `decision.ts` owns that argument, and
165
+ * the one type query the pair cannot answer as before.
166
+ *
167
+ * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
168
+ * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
169
+ * is what the produce side needs BEYOND the codec's own two services. Leaving
170
+ * them in would state the same requirement twice and make an ordinary decider
171
+ * read as though it carried an extra one, though the set `runDecider` ends up
172
+ * asking for is identical either way. The exclusion cannot swallow a caller's
173
+ * own service by accident: a `Context.Service` class's requirement type carries
174
+ * its key as a literal `key` field, so only a service declared under `@kairos-es/codec/Uuid` or
175
+ * `@kairos-es/codec/Serializer` matches — and a service under that key IS that
176
+ * service, context being keyed by the same string at runtime.
177
+ */
178
+ export declare function decider<P extends Record<string, SliceProjection>, Command, Eff extends Effect.Effect<Arr.NonEmptyReadonlyArray<DcbEvent>, unknown, unknown>>(projections: (command: Command) => P, decide: (state: CompositeState<P>, command: Command) => Eff): Decider<P, Command, Effect.Error<Eff>, Exclude<Effect.Services<Eff>, Serializer | Uuid>>;
179
+ export declare function decider<P extends Record<string, SliceProjection>, Command, Eff extends Effect.Effect<DecisionOutput, unknown, unknown>>(projections: (command: Command) => P, decide: (state: CompositeState<P>, command: Command) => Eff): Decider<P, Command, Effect.Error<Eff>, Exclude<Effect.Services<Eff>, Serializer | Uuid>, DecisionOutput>;
180
+ /**
181
+ * Run the full DCB command cycle for a decider against the ambient
182
+ * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
183
+ * and append under the read's append condition. Returns the last new position —
184
+ * or, when `decide` answered `Unchanged`, that `Unchanged`, having appended
185
+ * nothing. The result type is `DecisionResult<O>`, so it is plain `Position` for
186
+ * every decider that cannot answer `Unchanged`, including one whose `O` is still
187
+ * a type variable bounded by the batch. ONE signature, deliberately:
188
+ * `decision.ts` says why that alias, and not an overload, is what carries it.
189
+ *
190
+ * The requirement is `DcbEventStore | Serializer | Uuid | R |
191
+ * RequirementOf<P>`: `Serializer` enters through both the decode
192
+ * (`buildDecisionModel`) and produce (`toEvent`) sides, `Uuid` through produce,
193
+ * the slices' `RequirementOf<P>` through decode, and `R` through produce. (For a
194
+ * def whose `Schema` carries a requirement, `RequirementOf<P>` and `R` are the
195
+ * same tag.) Any `decide` failure, a decode `CodecError`, an
196
+ * `AppendConditionFailed`, or the read's own `TaglessSliceInLedger` propagates on
197
+ * the error channel unchanged.
198
+ *
199
+ * ⚠ INSIDE a bracket this function is REFUSED, as a defect, at the store's shared
200
+ * append decorator, and `decideOnly` is what a body reaches for instead.
201
+ * `index.ts` states that pair, why each refuses exactly where the other is the
202
+ * answer, and why neither can be expressed over the other. Read it before
203
+ * changing either signature; this file does not restate the argument.
204
+ */
205
+ export declare const runDecider: <P extends Record<string, SliceProjection>, Command, E, R, O extends DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent>>(decider: Decider<P, Command, E, R, O>, command: Command) => Effect.Effect<DecisionResult<O>, E | CodecError | AppendConditionFailed | TaglessSliceInLedger, DcbEventStore | Serializer | Uuid | R | RequirementOf<P>>;
206
+ //# sourceMappingURL=decider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decider.d.ts","sourceRoot":"","sources":["../../src/decider.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAEH,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,QAAQ,EACT,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAC/C,OAAO,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,MAAM,QAAQ,CAAA;AAC1C,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EACL,KAAK,cAAc,EACnB,KAAK,cAAc,EAEpB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAA;AACxE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAC9C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAA;AAElC;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,WAAW,OAAO,CACtB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,GAAG,KAAK,EACT,CAAC,GAAG,KAAK,EACT,CAAC,SAAS,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC;IAE9D,QAAQ,CAAC,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,CAAA;IAC7C,QAAQ,CAAC,MAAM,EAAE,CACf,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EACxB,OAAO,EAAE,OAAO,KACb,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,CAAA;CAChD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,wBAAgB,OAAO,CACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,GAAG,SAAS,MAAM,CAAC,MAAM,CACvB,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EACnC,OAAO,EACP,OAAO,CACR,EAED,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,EACpC,MAAM,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,GAAG,GAC1D,OAAO,CACR,CAAC,EACD,OAAO,EACP,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,CACjD,CAAA;AACD,wBAAgB,OAAO,CACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,GAAG,SAAS,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,EAAE,OAAO,CAAC,EAE3D,WAAW,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,CAAC,EACpC,MAAM,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,KAAK,GAAG,GAC1D,OAAO,CACR,CAAC,EACD,OAAO,EACP,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,EACjB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC,EAChD,cAAc,CACf,CAAA;AAiBD;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,UAAU,GACrB,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACzC,OAAO,EACP,CAAC,EACD,CAAC,EACD,CAAC,SAAS,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,EAE9D,SAAS,OAAO,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EACrC,SAAS,OAAO,KACf,MAAM,CAAC,MAAM,CACd,cAAc,CAAC,CAAC,CAAC,EACjB,CAAC,GAAG,UAAU,GAAG,qBAAqB,GAAG,oBAAoB,EAC7D,aAAa,GAAG,UAAU,GAAG,IAAI,GAAG,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAgBtD,CAAA"}
@@ -0,0 +1,161 @@
1
+ /**
2
+ * `Unchanged`: a decision's answer when there is nothing to do because the work
3
+ * is already done, and the two types that let a decision say so without costing
4
+ * any existing caller its `Position`.
5
+ *
6
+ * ## Why a decision needs a third answer
7
+ *
8
+ * A decision had two answers: a non-empty batch of events to append, or a
9
+ * failure. The idempotent case fitted neither. A webhook redelivered after its
10
+ * first delivery was recorded, or a recorder re-running after a conflict whose
11
+ * winning write already closed the item, has nothing to append — and nothing
12
+ * WRONG has happened, so a failure is the wrong channel: a caller would have to
13
+ * tell "already done" from a genuine refusal by matching a tag, and a retry
14
+ * policy keyed on the error channel would retry a success. An empty batch is not
15
+ * the answer either. `append` refuses one as a defect, and the `Decider`'s
16
+ * `NonEmptyReadonlyArray` bound is what makes that refusal unreachable from a
17
+ * decision, a guarantee `decider.ts` calls load-bearing. So "already done" gets a
18
+ * value of its own, and it is a SUCCESS: an idempotent skip is never retried.
19
+ *
20
+ * ## Why it is recognised by a type id, and never by `instanceof` or a `_tag`
21
+ *
22
+ * This package ships dual ESM/CJS, so one consumer's graph can hold two copies of
23
+ * this module and therefore two distinct `Unchanged` classes. Under `instanceof`,
24
+ * a value built by one copy fails the other's check, and the runner would treat
25
+ * "already done" as a batch and hand it to `append` — the same single point of
26
+ * silent failure `@kairos-es/core` argues against, in its contract store, for
27
+ * `AppendConditionFailed`. That error falls back on its `_tag`, and a tag is
28
+ * enough there because the error channel is the library's own. A DECISION's
29
+ * success channel is not: an application's own union may well carry a member
30
+ * tagged `'Unchanged'`, and a runner that skipped on the bare tag would swallow
31
+ * that value and append nothing, silently. `Symbol.for` gives an identity that is
32
+ * shared across both builds (the global symbol registry is per runtime, not per
33
+ * module copy) and that no application value carries by accident. `isUnchanged`
34
+ * is the one test, and every runner in this package uses it.
35
+ *
36
+ * ## Why the widening is a defaulted type parameter, not a second entry point
37
+ *
38
+ * `Decider` gains a fifth type parameter `O`, defaulting to the non-empty batch.
39
+ * A second entry point would have meant a second `Decider`, a second runner, a
40
+ * second `transact` and a second `decideOnly` — against a surface whose whole
41
+ * argument is that `runDecider` and `decideOnly` are ONE pair.
42
+ *
43
+ * ## Why `DecisionResult` is shaped as it is, and why `decider` is overloaded
44
+ *
45
+ * The promise is that a decider or body that cannot answer `Unchanged` keeps
46
+ * exactly the `Position` result it had. Two kinds of success make that harder
47
+ * than it looks, and both were measured against call shapes that compiled
48
+ * before the widening.
49
+ *
50
+ * - An UNTYPED success. `any` satisfies every question a conditional can ask of
51
+ * it: `Unchanged extends any` holds, and `any extends Unchanged` takes BOTH
52
+ * branches. So a decision nobody typed would read as one that can answer
53
+ * `Unchanged`, and its caller's `(position: Position) => …` would stop
54
+ * compiling. `DecisionResult` therefore checks for `any` FIRST, and answers
55
+ * `Position`.
56
+ * - A GENERIC success. In a wrapper generic in the success type
57
+ * (`<A extends NonEmptyReadonlyArray<DcbEvent>, …>`), a conditional over `A`
58
+ * stays DEFERRED, and a deferred conditional is `Position` only if the checker
59
+ * can see that it must be. It can for one shape and not for the other:
60
+ * `Unchanged extends O ? …` is related through the union of its two branches,
61
+ * which admits `Unchanged`; `O extends Unchanged ? …`, DISTRIBUTIVE over `O`,
62
+ * is related through itself instantiated at `O`'s bound — and at a bound of
63
+ * the non-empty batch that is `Position`. So `DecisionResult` is written the
64
+ * second way, and `runDecider` and `transact` keep ONE signature each, which
65
+ * is what lets `body.pipe(transact(p))` still infer its error and requirement
66
+ * channels; an overloaded step would lose both to `unknown`.
67
+ *
68
+ * `decider(…)` cannot be served by a conditional at all. Its `O` is derived from
69
+ * the SUCCESS of an effect type variable, and `Effect.Success` of a type
70
+ * variable is itself deferred, so a wrapper that copies `decider`'s own generics
71
+ * would get a `Decider` whose `O` nothing can resolve, and a runner result that
72
+ * is not `Position`. So `decider` is declared TWICE, which asks the question by
73
+ * ASSIGNABILITY instead: FIRST for an effect whose success is the non-empty
74
+ * batch, returning a `Decider` with the default `O`, and only then for any
75
+ * `DecisionOutput`, returning one whose `O` is `DecisionOutput`. A type
76
+ * variable bounded by the batch fits the first, and so does `any`; a `decide`
77
+ * with a branch answering `Unchanged` does not, and falls through to the second.
78
+ * Keep that ORDER: the checker takes the first declaration that fits, so
79
+ * swapped, every decider would be widened.
80
+ *
81
+ * Neither reaches a type QUERY. `ReturnType<typeof runDecider>` reads `O` at its
82
+ * bound, which admits `Unchanged`, so its success is widened; instantiated
83
+ * (`typeof runDecider<P, Command, E, R>`), `O` takes its default and the answer
84
+ * is `Position` again. `ReturnType<typeof decider<…>>` reads the LAST
85
+ * declaration, so it is the widened `Decider` whatever effect type it is given;
86
+ * a decider VALUE is typed by the declaration that fitted it.
87
+ *
88
+ * `DecisionOutput` still refuses a possibly-empty array — a `DcbEvent[]` from a
89
+ * `filter`, say — at compile time, because a plain array is neither non-empty nor
90
+ * `Unchanged`. The widening adds an answer; it removes no guarantee.
91
+ */
92
+ import type { DcbEvent, Position } from '@kairos-es/core';
93
+ import { type Array as Arr } from 'effect';
94
+ /**
95
+ * The type id `isUnchanged` recognises. `Symbol.for`, so both halves of the dual
96
+ * build — and any two copies of this package in one graph — agree on it.
97
+ */
98
+ export declare const UnchangedTypeId: unique symbol;
99
+ /** The type of `UnchangedTypeId`. */
100
+ export type UnchangedTypeId = typeof UnchangedTypeId;
101
+ declare const Unchanged_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Readonly<A> & {
102
+ readonly _tag: "Unchanged";
103
+ } & import("effect/Pipeable").Pipeable;
104
+ /**
105
+ * "Nothing to do: this work is already done." A runner that receives it appends
106
+ * nothing and returns it in place of a `Position`.
107
+ *
108
+ * `reason` is for a human reading a trace or a test failure, and nothing in the
109
+ * library reads it. A `Data.TaggedClass`, so two values with the same reason are
110
+ * `Equal` and a test can compare them structurally.
111
+ *
112
+ * ⚠ Test for it with `isUnchanged`, never with `instanceof` or `_tag` — the
113
+ * module documentation says why each of those is wrong.
114
+ */
115
+ export declare class Unchanged extends Unchanged_base<{
116
+ readonly reason?: string;
117
+ }> {
118
+ readonly [UnchangedTypeId]: UnchangedTypeId;
119
+ }
120
+ /**
121
+ * Whether `u` is an `Unchanged`, by its type id. Total over `unknown`, and
122
+ * realm-safe across the dual ESM/CJS build. An application value that merely
123
+ * carries `_tag: 'Unchanged'` answers `false`, deliberately.
124
+ */
125
+ export declare const isUnchanged: (u: unknown) => u is Unchanged;
126
+ /**
127
+ * The constructors a `decide` step reaches for. One member today; it is a
128
+ * namespace object rather than a bare function so the call site reads as what it
129
+ * is, `Decision.unchanged('already dispatched')`.
130
+ */
131
+ export declare const Decision: {
132
+ readonly unchanged: (reason?: string) => Unchanged;
133
+ };
134
+ /**
135
+ * Everything a decision may succeed with: a non-empty batch of events to
136
+ * append, or `Unchanged`. A plain, possibly-empty array is NOT a member, which is
137
+ * what keeps an empty batch unreachable from a decision.
138
+ */
139
+ export type DecisionOutput = Arr.NonEmptyReadonlyArray<DcbEvent> | Unchanged;
140
+ /**
141
+ * What a runner returns for a decision that succeeds with `O`: `Position` unless
142
+ * `O` admits `Unchanged`, and `Position | Unchanged` when it does.
143
+ *
144
+ * Asked of each member of `O` — DISTRIBUTIVE — so a union `O` answers
145
+ * `Position | Unchanged` exactly when one of its members is `Unchanged`, a batch
146
+ * answers `Position` with no `| never` residue, and an `O` that is a type
147
+ * variable bounded by the batch is seen to be `Position`, which is what a
148
+ * wrapper generic in its success type needs. `O` that can ONLY be `Unchanged`
149
+ * answers `Unchanged`, the precise result and assignable wherever
150
+ * `Position | Unchanged` is expected.
151
+ *
152
+ * Two members are checked BEFORE the distribution, and both answer `Position`,
153
+ * the result each had before `Unchanged` existed. `any`, because a distributive
154
+ * conditional over `any` takes both branches and would widen every untyped
155
+ * decision. And `never` — a body that only ever fails — because distributing
156
+ * over `never` answers `never`, which a variable once typed with a `Position`
157
+ * result would no longer accept. The module documentation owns the rest.
158
+ */
159
+ export type DecisionResult<O> = 0 extends 1 & O ? Position : [O] extends [never] ? Position : O extends Unchanged ? Unchanged : Position;
160
+ export {};
161
+ //# sourceMappingURL=decision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decision.d.ts","sourceRoot":"","sources":["../../src/decision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0FG;AACH,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAA;AACzD,OAAO,EAAE,KAAK,KAAK,IAAI,GAAG,EAAmB,MAAM,QAAQ,CAAA;AAE3D;;;GAGG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,MAEpC,CAAA;AAED,qCAAqC;AACrC,MAAM,MAAM,eAAe,GAAG,OAAO,eAAe,CAAA;;;;AAEpD;;;;;;;;;;GAUG;AACH,qBAAa,SAAU,SAAQ,eAA8B;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB,CAAC;IACA,QAAQ,CAAC,CAAC,eAAe,CAAC,EAAE,eAAe,CAAkB;CAC9D;AAED;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,GAAG,OAAO,KAAG,CAAC,IAAI,SACH,CAAA;AAE3C;;;;GAIG;AACH,eAAO,MAAM,QAAQ,EAAE;IACrB,QAAQ,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,EAAE,MAAM,KAAK,SAAS,CAAA;CAGnD,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,GAAG,CAAC,qBAAqB,CAAC,QAAQ,CAAC,GAAG,SAAS,CAAA;AAE5E;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAC3C,QAAQ,GACR,CAAC,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,GACjB,QAAQ,GACR,CAAC,SAAS,SAAS,GACjB,SAAS,GACT,QAAQ,CAAA"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Read-and-fold over a slice record: read the composed query, decode each event
3
+ * through the matching def's `Schema` in an effectful `Stream.mapEffect` stage,
4
+ * then fold *purely* through core's `composeProjections`.
5
+ *
6
+ * TWO exports sit over that ONE body, the second expressed over the first.
7
+ * `modelAtHead` stops where the fold does, at `{ state, head }` — a
8
+ * materialisation of the record with no table, no checkpoint and no runner.
9
+ * `buildDecisionModel` adds the only part that is genuinely decision-specific:
10
+ * the `AppendCondition` (the DCB optimistic-concurrency contract). It is core's
11
+ * old `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
12
+ * because decode needs `Schema` + the `Serializer`.
13
+ *
14
+ * BOTH exports also record into the ambient read ledger when one is in context,
15
+ * which is what makes a decision that walks a chain of identifiers guard on every
16
+ * hop rather than on its last. Recording is `modelAtHead`'s, so it happens
17
+ * whatever symbol performed the read; outside a bracket there is no ledger,
18
+ * nothing is recorded and no behaviour changes. `readLedger.ts` owns the whole
19
+ * argument, including why the recorder is this module rather than a
20
+ * layered-only entry point of its own.
21
+ *
22
+ * The fold stays pure and in core; only the decode stage is effectful, and that
23
+ * stage lives in `decode.ts` — `decodeSlices` — so the read side folds a live
24
+ * `subscribe` stream through the *same* decode this module folds a bounded
25
+ * `read` stream through.
26
+ *
27
+ * The result requirement is exactly `DcbEventStore | Serializer | R`, where `R`
28
+ * is the union of the record's slices' requirements — inferred with no cast,
29
+ * `R` being carried covariantly by each slice's `schemas` map value type.
30
+ * `Serializer` is not yielded here: `decodeSlices` reads it from context itself,
31
+ * so the requirement arrives through the decode stage rather than through a
32
+ * binding this module owns (see `decode.ts` for why).
33
+ */
34
+ import { type AppendCondition, type CompositeState, DcbEventStore, type Position } from '@kairos-es/core';
35
+ import { Effect } from 'effect';
36
+ import type { CodecError } from './errors.js';
37
+ import type { SliceProjection } from './projection.js';
38
+ import { type TaglessSliceInLedger } from './readLedger.js';
39
+ import type { Serializer } from './Serializer.js';
40
+ /** The union of requirements `R` carried by a record of `SliceProjection`s. */
41
+ export type RequirementOf<T extends Record<string, SliceProjection>> = T[keyof T] extends SliceProjection<infer _S, infer R> ? R : never;
42
+ /**
43
+ * Read the events for the composed slices' query, decode each through its def's
44
+ * `Schema`, fold into per-key state, and return `{ state, head }`: one slice
45
+ * record materialised at read time, with no table, no checkpoint and no runner.
46
+ *
47
+ * ## Why `head` comes back, and not bare `state`
48
+ *
49
+ * `head` is the consistency statement, and without it the state is a figure with
50
+ * no as-of — which is why the as-of is in the name. The fold stops at the
51
+ * position the read returned and is maintained no further, so `head` is the
52
+ * whole of what a caller can say about this state's currency: it is what lets
53
+ * the write path turn it into an `AppendCondition` (see `buildDecisionModel`),
54
+ * and what lets a read-time caller report, cache or compare a state against a
55
+ * position rather than against a wall clock.
56
+ *
57
+ * It is also the inversion worth naming out loud, because intuition runs the
58
+ * other way. A MAINTAINED view is consistent as of its CHECKPOINT, which trails
59
+ * the log's head by however far behind its runner is; this fold is consistent as
60
+ * of the head itself. So of two materialisations of one slice record, the one
61
+ * with no table, no checkpoint and no runner is the MOST current, and the table
62
+ * beside it is the stale one. Reach for a maintained view for the queries a
63
+ * single fold cannot serve — across many identities, ordered, joined, paginated
64
+ * — and never because it is fresher, because it is not.
65
+ *
66
+ * ⚠ AND INSIDE A BRACKET IT IS NOT THE FLOOR THE APPEND WILL USE. Everything
67
+ * above is a statement about THIS read, and it stays true; what changes is that a
68
+ * bracket guards on the EARLIEST head across every hop it walked, which on any hop
69
+ * after the first is a strictly lower number than the one returned here. So a
70
+ * caller who reads `head` inside a bracket and reasons about what the append will
71
+ * refuse is reasoning from the wrong figure — the guard is wider than this head
72
+ * says. Nothing here is wrong; the trap is assuming the two are the same number,
73
+ * and it is named because it is exactly the assumption that holds outside a
74
+ * bracket and quietly stops holding inside one. `readLedger.ts` owns the floor.
75
+ *
76
+ * `head` is LOG-GLOBAL: it is the head the store's `read` returned, so it moves
77
+ * for an event no slice here selects. `test/decisionModel.test.ts` pins that
78
+ * behaviour EXPLICITLY, over four appended events one of which the composed query
79
+ * excludes by tag; `test/modelAtHead.test.ts` pins it incidentally, its own head
80
+ * assertion landing on that same excluded event's position. Two guards, one of them
81
+ * deliberate — reach for the explicit one when the property itself is what changes.
82
+ * Log-global neither tightens nor loosens the derived guard — there can be no
83
+ * event matching the query between the query's last match and the log head, so a
84
+ * query-filtered head would refuse exactly the same appends — which is precisely
85
+ * why the store's own figure is the one to carry: one fact about the log, not a
86
+ * per-query derivation, and the honest as-of for a state folded from everything
87
+ * the query selected up to it.
88
+ *
89
+ * ## Why this lives in the codec and not in `@kairos-es/read`
90
+ *
91
+ * In `@kairos-es/read` it would be the only export touching none of that
92
+ * package's machinery: no checkpoint, no `EventLogDurability` (R2 is vacuous
93
+ * where there is no checkpoint to outlive the log), no rung of that package's
94
+ * construction gate, and none of its test apparatus. The vocabulary is already
95
+ * this package's — ADR-0004 names the typed slice path's clients as "a decision
96
+ * model / materialisation / command" — and the body is `buildDecisionModel`'s,
97
+ * which is here for the reason the module doc gives.
98
+ */
99
+ export declare const modelAtHead: <T extends Record<string, SliceProjection>>(projections: T) => Effect.Effect<{
100
+ readonly state: CompositeState<T>;
101
+ readonly head: Position;
102
+ }, CodecError | TaglessSliceInLedger, DcbEventStore | Serializer | RequirementOf<T>>;
103
+ /**
104
+ * The write side's decision model: `modelAtHead` plus the `AppendCondition` that
105
+ * reuses the read query as `failIfEventsMatch` and the read head as `after` (the
106
+ * DCB optimistic-concurrency contract).
107
+ *
108
+ * It is expressed OVER `modelAtHead` rather than beside it, and that is the point
109
+ * of the pair. Two functions reading, decoding and folding a slice record almost
110
+ * identically would need two sets of tests and would be free to drift on the
111
+ * decode stage, on the cast recovering `CompositeState<T>`, or on which record
112
+ * the query is composed from. Re-expressing makes this a decomposition instead
113
+ * of a near-duplicate: `modelAtHead` is the shared part, this function is the
114
+ * condition and nothing else, and each is shorter than the one function was.
115
+ *
116
+ * Outside a bracket `composeProjections` therefore runs twice for one command —
117
+ * once inside `modelAtHead` for the read, once here for `failIfEventsMatch` — and
118
+ * that is deliberate rather than waste. It is a pure derivation of the record, so
119
+ * the two queries cannot disagree however either call site changes. The
120
+ * alternative, returning the query from `modelAtHead`, would put a third field on
121
+ * a READ-TIME API that no read-time caller wants, to save one pure merge over a
122
+ * handful of slices; a surface paying for a body's convenience is the worse
123
+ * trade. Inside a bracket the second merge is not reached at all: the ledger's
124
+ * own derivation composes every recorded hop instead.
125
+ *
126
+ * ## How to attribute the read, the decode and the fold — the rule, stated once
127
+ *
128
+ * Because this function is expressed OVER `modelAtHead`, prose about the codec
129
+ * splits into two kinds and only one of them may name this symbol:
130
+ *
131
+ * - Naming this function as the ENTRY POINT a caller invokes, or as the path
132
+ * something is reached THROUGH, is correct — "the `buildDecisionModel` fold", "the
133
+ * write path reaches decode through `buildDecisionModel`". Transitive reach is real
134
+ * reach, and this is the symbol a command actually calls.
135
+ * - Naming it as the function that CONTAINS the read, the decode or the fold is
136
+ * FALSE. All three are `modelAtHead`'s; this body adds an `AppendCondition` and
137
+ * nothing else.
138
+ *
139
+ * Worth writing down because the distinction is invisible from a call site and the
140
+ * mistake is systematic rather than careless: every such claim was TRUE before
141
+ * `modelAtHead` was extracted from here, so a doc written against the older shape
142
+ * reads perfectly and is wrong. The correction is to name `modelAtHead`, never to
143
+ * soften the sentence.
144
+ */
145
+ export declare const buildDecisionModel: <T extends Record<string, SliceProjection>>(projections: T) => Effect.Effect<{
146
+ readonly state: CompositeState<T>;
147
+ readonly appendCondition: AppendCondition;
148
+ }, CodecError | TaglessSliceInLedger, DcbEventStore | Serializer | RequirementOf<T>>;
149
+ //# sourceMappingURL=decisionModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decisionModel.d.ts","sourceRoot":"","sources":["../../src/decisionModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,cAAc,EAEnB,aAAa,EACb,KAAK,QAAQ,EACd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,MAAM,EAAkB,MAAM,QAAQ,CAAA;AAE/C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AAC1C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAIL,KAAK,oBAAoB,EAC1B,MAAM,cAAc,CAAA;AACrB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAE9C,+EAA+E;AAC/E,MAAM,MAAM,aAAa,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,IACjE,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS,eAAe,CAAC,MAAM,EAAE,EAAE,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;AAEnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwDG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACnE,aAAa,CAAC,KACb,MAAM,CAAC,MAAM,CACd;IAAE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;CAAE,EAC9D,UAAU,GAAG,oBAAoB,EACjC,aAAa,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CA6C3C,CAAA;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,eAAO,MAAM,kBAAkB,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EAC1E,aAAa,CAAC,KACb,MAAM,CAAC,MAAM,CACd;IACE,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC,CAAC,CAAC,CAAA;IACjC,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAA;CAC1C,EACD,UAAU,GAAG,oBAAoB,EACjC,aAAa,GAAG,UAAU,GAAG,aAAa,CAAC,CAAC,CAAC,CA6B3C,CAAA"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The shared decode stage: the wire-bytes -> typed-payload half of the codec
3
+ * seam, lifted out of `buildDecisionModel` into a `Stream` combinator so every
4
+ * consumer of a slice record decodes through *one* implementation.
5
+ *
6
+ * Two consumers, one stage. `modelAtHead` folds a bounded `read` stream into
7
+ * decision state, which is how `buildDecisionModel` gets one; the read side folds
8
+ * an unbounded `subscribe` stream into a materialised view. They differ in
9
+ * everything *downstream* of decode (bounded vs live, a pure fold vs a
10
+ * transactional commit) and in nothing upstream of it — same slice record, same `type -> Schema` lookup, same
11
+ * `Serializer`, same failure taxonomy — so decode is exactly where the seam
12
+ * belongs. Duplicating it would let the two paths drift: a read side with its
13
+ * own copy could silently accept an event the decision path rejects, or reword
14
+ * a `CodecError` reason, and nothing would catch it.
15
+ *
16
+ * Why a `Stream` combinator rather than an `Effect` over a materialised array:
17
+ * decode must stay back-pressured. `Stream.mapEffect` with no `concurrency`
18
+ * option decodes one event at a time, pulling only as fast as the consumer
19
+ * folds, and that is what lets a projection catch up over a backlog far larger
20
+ * than memory.
21
+ *
22
+ * Be precise about what adding `concurrency` here would and would not cost,
23
+ * because the obvious reason is the wrong one. Effect's `mapEffect` is
24
+ * order-PRESERVING even when concurrent — reordering needs the separate
25
+ * `unordered: true` option (verified against `effect@4`'s `Channel.mapEffect`,
26
+ * which takes the ordered path unless that option is set) — so a bare
27
+ * `concurrency` would NOT hand the fold its events out of position order. What
28
+ * it would actually cost is the back-pressure above, and it would run
29
+ * application-authored `Schema` transforms (an ADR-0004 decryption hook, say)
30
+ * concurrently, which their authors have no reason to expect. `unordered: true`
31
+ * is the option that would be an outright correctness bug against an
32
+ * order-sensitive fold. Sequential is the right default for both reasons; the
33
+ * order argument alone does not carry it.
34
+ *
35
+ * Why these failures are values and not defects: corrupt bytes, an absent
36
+ * schema and a `Schema` mismatch are *data* problems an application can
37
+ * legitimately handle (quarantine the event, fail the command, alert), so they
38
+ * belong on the error channel as `CodecError` — see `errors.ts`. The three
39
+ * `reason` strings are observable surface; treat them as fixed, not cosmetic.
40
+ *
41
+ * Two of those three are no longer this module's to reword. `deserialise` and
42
+ * `decode` are fixed in `deserialiseThenDecode`, the two-step this stage shares
43
+ * with the general (registry) path, precisely so that a claim about surface has
44
+ * ONE implementation behind it rather than a copy per path. The absent-schema
45
+ * reason stays here, because the two paths deliberately disagree about what a
46
+ * lookup miss means — `UnknownEventType` there, ADR-0005 — and that helper's doc
47
+ * carries the argument.
48
+ */
49
+ import type { DecodedEvent, SequencedEvent } from '@kairos-es/core';
50
+ import { Stream } from 'effect';
51
+ import type { RequirementOf } from './decisionModel.js';
52
+ import { CodecError } from './errors.js';
53
+ import type { SliceProjection } from './projection.js';
54
+ import { Serializer } from './Serializer.js';
55
+ /**
56
+ * The public decode stage: give it the slice record, get back a `Stream`
57
+ * combinator that decodes every event those slices can handle.
58
+ *
59
+ * **Why one call rather than two.** The lookup and the stage were briefly two
60
+ * public symbols — merge the record, then hand the map to the stage — and every
61
+ * consumer in the repo composed them in exactly that order and did nothing else
62
+ * with the intermediate map. A two-step protocol with one legal composition is
63
+ * not flexibility, it is a way to get it wrong: a caller can merge the wrong
64
+ * record, or a record that is not the one its `Query` was composed from, and
65
+ * that second mistake is silent. In the read side it would decode a live
66
+ * subscription against schemas the subscription never selects — every event
67
+ * arriving would fail `no schema for event type`, framed as a data problem when
68
+ * it is a wiring one. Taking the slices directly removes the intermediate value
69
+ * a caller could substitute, so the schemas provably come from the same record
70
+ * the query does.
71
+ *
72
+ * **Still curried, deliberately.** The merge is a pure derivation of the slice
73
+ * record, so both consumers build the stage ONCE — per decision model, per read
74
+ * model — and reuse it across every stream they drive. Rebuilding the table per
75
+ * stream (let alone per restart) would only burn allocations, and the currying
76
+ * is what makes "built once" the natural way to call this rather than a
77
+ * discipline.
78
+ *
79
+ * **It can REFUSE the record**, and does so the moment it is called rather than
80
+ * once events are flowing: a type reachable from two slices under two DIFFERENT
81
+ * `Schema`s throws, in the same words `makeRegistry` uses. See
82
+ * `mergeSliceSchemas` for the rule, why reference equality is its discriminator
83
+ * and why a throw is the right mechanism. Both in-repo consumers call this
84
+ * before there is a stream to fail, so inside their `Effect.gen`s the refusal is
85
+ * a defect at construction.
86
+ *
87
+ * `RequirementOf<T>` surfaces in the result's `R`, so a slice whose `Schema`
88
+ * carries a requirement (an application-authored decryption transform, ADR-0004)
89
+ * propagates it to the caller with no cast at the call site.
90
+ */
91
+ export declare const decodeSlices: <T extends Record<string, SliceProjection>>(slices: T) => (<E, R>(events: Stream.Stream<SequencedEvent, E, R>) => Stream.Stream<DecodedEvent, E | CodecError, R | RequirementOf<T> | Serializer>);
92
+ //# sourceMappingURL=decode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../../src/decode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AACnE,OAAO,EAAuB,MAAM,EAAE,MAAM,QAAQ,CAAA;AAIpD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAA;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAoJzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,EACpE,QAAQ,CAAC,KACR,CAAC,CAAC,CAAC,EAAE,CAAC,EACP,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,cAAc,EAAE,CAAC,EAAE,CAAC,CAAC,KACxC,MAAM,CAAC,MAAM,CAChB,YAAY,EACZ,CAAC,GAAG,UAAU,EACd,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,UAAU,CAClC,CAA4C,CAAA"}