@kairos-es/codec 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +46 -0
  3. package/dist/cjs/Serializer.js +34 -0
  4. package/dist/cjs/Serializer.js.map +1 -0
  5. package/dist/cjs/Uuid.js +41 -0
  6. package/dist/cjs/Uuid.js.map +1 -0
  7. package/dist/cjs/decider.js +152 -0
  8. package/dist/cjs/decider.js.map +1 -0
  9. package/dist/cjs/decisionModel.js +155 -0
  10. package/dist/cjs/decisionModel.js.map +1 -0
  11. package/dist/cjs/decode.js +170 -0
  12. package/dist/cjs/decode.js.map +1 -0
  13. package/dist/cjs/deserialiseThenDecode.js +20 -0
  14. package/dist/cjs/deserialiseThenDecode.js.map +1 -0
  15. package/dist/cjs/errors.js +94 -0
  16. package/dist/cjs/errors.js.map +1 -0
  17. package/dist/cjs/event.js +26 -0
  18. package/dist/cjs/event.js.map +1 -0
  19. package/dist/cjs/index.js +112 -0
  20. package/dist/cjs/index.js.map +1 -0
  21. package/dist/cjs/projection.js +118 -0
  22. package/dist/cjs/projection.js.map +1 -0
  23. package/dist/cjs/registry.js +41 -0
  24. package/dist/cjs/registry.js.map +1 -0
  25. package/dist/cjs/toEvent.js +51 -0
  26. package/dist/cjs/toEvent.js.map +1 -0
  27. package/dist/dts/Serializer.d.ts +27 -0
  28. package/dist/dts/Serializer.d.ts.map +1 -0
  29. package/dist/dts/Uuid.d.ts +19 -0
  30. package/dist/dts/Uuid.d.ts.map +1 -0
  31. package/dist/dts/decider.d.ts +157 -0
  32. package/dist/dts/decider.d.ts.map +1 -0
  33. package/dist/dts/decisionModel.d.ts +129 -0
  34. package/dist/dts/decisionModel.d.ts.map +1 -0
  35. package/dist/dts/decode.d.ts +92 -0
  36. package/dist/dts/decode.d.ts.map +1 -0
  37. package/dist/dts/deserialiseThenDecode.d.ts +62 -0
  38. package/dist/dts/deserialiseThenDecode.d.ts.map +1 -0
  39. package/dist/dts/errors.d.ts +79 -0
  40. package/dist/dts/errors.d.ts.map +1 -0
  41. package/dist/dts/event.d.ts +71 -0
  42. package/dist/dts/event.d.ts.map +1 -0
  43. package/dist/dts/index.d.ts +63 -0
  44. package/dist/dts/index.d.ts.map +1 -0
  45. package/dist/dts/projection.d.ts +56 -0
  46. package/dist/dts/projection.d.ts.map +1 -0
  47. package/dist/dts/registry.d.ts +46 -0
  48. package/dist/dts/registry.d.ts.map +1 -0
  49. package/dist/dts/toEvent.d.ts +34 -0
  50. package/dist/dts/toEvent.d.ts.map +1 -0
  51. package/dist/esm/Serializer.js +26 -0
  52. package/dist/esm/Serializer.js.map +1 -0
  53. package/dist/esm/Uuid.js +33 -0
  54. package/dist/esm/Uuid.js.map +1 -0
  55. package/dist/esm/decider.js +143 -0
  56. package/dist/esm/decider.js.map +1 -0
  57. package/dist/esm/decisionModel.js +146 -0
  58. package/dist/esm/decisionModel.js.map +1 -0
  59. package/dist/esm/decode.js +163 -0
  60. package/dist/esm/decode.js.map +1 -0
  61. package/dist/esm/deserialiseThenDecode.js +13 -0
  62. package/dist/esm/deserialiseThenDecode.js.map +1 -0
  63. package/dist/esm/errors.js +85 -0
  64. package/dist/esm/errors.js.map +1 -0
  65. package/dist/esm/event.js +19 -0
  66. package/dist/esm/event.js.map +1 -0
  67. package/dist/esm/index.js +63 -0
  68. package/dist/esm/index.js.map +1 -0
  69. package/dist/esm/package.json +4 -0
  70. package/dist/esm/projection.js +110 -0
  71. package/dist/esm/projection.js.map +1 -0
  72. package/dist/esm/registry.js +34 -0
  73. package/dist/esm/registry.js.map +1 -0
  74. package/dist/esm/toEvent.js +44 -0
  75. package/dist/esm/toEvent.js.map +1 -0
  76. package/package.json +31 -0
  77. package/src/Serializer.ts +37 -0
  78. package/src/Uuid.ts +36 -0
  79. package/src/decider.ts +219 -0
  80. package/src/decisionModel.ts +182 -0
  81. package/src/decode.ts +256 -0
  82. package/src/deserialiseThenDecode.ts +76 -0
  83. package/src/errors.ts +91 -0
  84. package/src/event.ts +90 -0
  85. package/src/index.ts +71 -0
  86. package/src/projection.ts +163 -0
  87. package/src/registry.ts +87 -0
  88. package/src/toEvent.ts +63 -0
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Neverbland
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # @kairos-es/codec
2
+
3
+ Schema-first codec with an encryption seam for kairos-es (ADR-0004).
4
+
5
+ The typed-payload to opaque-bytes seam: `event(type, fields, tagsFor)` defines a
6
+ tagged `Schema` together with the mapper that derives an event's tags from its own
7
+ payload (ADR-0006); `projection(init).on(def, fold)…build()` builds the slices a
8
+ decision model or a read model folds through; `modelAtHead(slices)` reads, decodes
9
+ through each def's `Schema`, and folds purely, returning `{ state, head }`;
10
+ `buildDecisionModel(slices)` is that same read plus the `AppendCondition` a write
11
+ needs; and `toEvent` produces a wire event (encode, serialise, tags derived, clock
12
+ and uuid from services).
13
+
14
+ `modelAtHead` is the read side's third materialisation KIND — checkpoint-free and
15
+ read-time, no table, no runner, nothing maintained — and `head` is its consistency
16
+ contract: it is current as of the position its own read returned, which a maintained
17
+ view's checkpoint necessarily trails. So it is MORE current than any table of the
18
+ same slices, and reaching for a maintained view is about the queries a single fold
19
+ cannot serve, never about freshness. It lives here rather than in `@kairos-es/read`
20
+ because it touches none of that package's machinery, and it is deliberately not what
21
+ a decider calls: `runDecider` already folds to head through `buildDecisionModel`,
22
+ under the condition that read produces.
23
+
24
+ `decodeSlices(slices)` is the decode stage `modelAtHead` folds through, and so in turn
25
+ the one `buildDecisionModel` gets by building on `modelAtHead` — exported so the read
26
+ side's `ProjectionRunner` shares one implementation over a live `subscribe` stream
27
+ rather than reimplementing it. `makeRegistry(defs)` is the minimal general
28
+ path, decoding an arbitrary wire event by its runtime type string with an unknown
29
+ type as a typed `UnknownEventType` (ADR-0005).
30
+
31
+ `Decider` and `runDecider(decider, command)` close the write side's whole command
32
+ cycle over that fold: build the decision model, decide, append under the read's
33
+ own condition. `decider(projections, decide)` builds one with its slice record,
34
+ its error union and its extra requirement all inferred — the command type, on the
35
+ `projections` parameter, is the only annotation a caller writes. They live here,
36
+ beside the `buildDecisionModel` they drive, rather
37
+ than in `@kairos-es/testing` — nothing in them is test apparatus, and that
38
+ package peers `@effect/vitest`, which a production command handler must not pull
39
+ into its module graph. Wrap the call in `core`'s `retryOnConflict` to retry a
40
+ conflict correctly (ADR-0008).
41
+
42
+ Serialisation is a pluggable `Serializer`, JSON by default. Encryption is an
43
+ **application**-authored `Schema` transform on a payload field: the codec owns no
44
+ cipher and imports no `node:crypto`, it only propagates the transform's
45
+ requirement and error channels. Platform-neutral; `@kairos-es/core` and `effect`
46
+ are peer dependencies.
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.SerializerDefault = exports.Serializer = void 0;
7
+ var _effect = require("effect");
8
+ /**
9
+ * The pluggable serialiser: the `Uint8Array` <-> value boundary applied
10
+ * *outside* `Schema`. Encode is `Schema.encode` then `serialise`; decode is
11
+ * `deserialise` then `Schema.decodeUnknown`. Both codec paths read the same
12
+ * `Serializer` from context, so encode and decode cannot mismatch; swapping
13
+ * JSON for msgpack is a different layer with no schema or call-site changes.
14
+ *
15
+ * `deserialise` may throw synchronously (corrupt bytes): wrapping it as a
16
+ * `CodecError` is the *caller's* concern — every decode site wraps it in
17
+ * `Effect.try`. Keeping the service interface plain (no Effect) lets a layer be
18
+ * a trivial `Layer.succeed`.
19
+ */
20
+
21
+ class Serializer extends /*#__PURE__*/_effect.Context.Tag('@kairos-es/codec/Serializer')() {}
22
+ exports.Serializer = Serializer;
23
+ const encoder = /*#__PURE__*/new TextEncoder();
24
+ const decoder = /*#__PURE__*/new TextDecoder();
25
+ /**
26
+ * The shipped default: JSON over UTF-8. `serialise` = `JSON.stringify` then
27
+ * `TextEncoder`; `deserialise` = `TextDecoder` then `JSON.parse` (which throws
28
+ * synchronously on corrupt bytes, wrapped to `CodecError` at the call site).
29
+ */
30
+ const SerializerDefault = exports.SerializerDefault = /*#__PURE__*/_effect.Layer.succeed(Serializer, {
31
+ serialise: v => encoder.encode(JSON.stringify(v)),
32
+ deserialise: b => JSON.parse(decoder.decode(b))
33
+ });
34
+ //# sourceMappingURL=Serializer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Serializer.js","names":["_effect","require","Serializer","Context","Tag","exports","encoder","TextEncoder","decoder","TextDecoder","SerializerDefault","Layer","succeed","serialise","v","encode","JSON","stringify","deserialise","b","parse","decode"],"sources":["../../src/Serializer.ts"],"sourcesContent":[null],"mappings":";;;;;;AAYA,IAAAA,OAAA,GAAAC,OAAA;AAZA;;;;;;;;;;;;;AAcM,MAAOC,UAAW,sBAAQC,eAAO,CAACC,GAAG,CAAC,6BAA6B,CAAC,EAMvE;AAAAC,OAAA,CAAAH,UAAA,GAAAA,UAAA;AAEH,MAAMI,OAAO,gBAAG,IAAIC,WAAW,EAAE;AACjC,MAAMC,OAAO,gBAAG,IAAIC,WAAW,EAAE;AAEjC;;;;;AAKO,MAAMC,iBAAiB,GAAAL,OAAA,CAAAK,iBAAA,gBAA4BC,aAAK,CAACC,OAAO,CACrEV,UAAU,EACV;EACEW,SAAS,EAAGC,CAAC,IAAKR,OAAO,CAACS,MAAM,CAACC,IAAI,CAACC,SAAS,CAACH,CAAC,CAAC,CAAC;EACnDI,WAAW,EAAGC,CAAC,IAAKH,IAAI,CAACI,KAAK,CAACZ,OAAO,CAACa,MAAM,CAACF,CAAC,CAAC;CACjD,CACF","ignoreList":[]}
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.UuidTest = exports.UuidLive = exports.Uuid = void 0;
7
+ var _testing = require("@kairos-es/core/testing");
8
+ var _effect = require("effect");
9
+ /**
10
+ * The uuid service: `toEvent` stamps each wire event's `uuid` from here, keeping
11
+ * `core` RNG-free (and letting suites make events comparable). Overridable per
12
+ * `toEvent` call via `options.uuid`.
13
+ *
14
+ * `UuidLive` uses WebCrypto (`globalThis.crypto.randomUUID`) so the codec stays
15
+ * platform-neutral with no `node:crypto`. `UuidTest` is a deterministic layer
16
+ * whose per-layer counter yields stable RFC-4122 v4-shaped uuids
17
+ * (`00000000-0000-4000-8000-<12 hex>`), so a suite that builds one `UuidTest`
18
+ * layer gets a repeatable sequence.
19
+ */
20
+
21
+ class Uuid extends /*#__PURE__*/_effect.Context.Tag('@kairos-es/codec/Uuid')() {}
22
+ /** Live uuid generation via WebCrypto (no `node:crypto`). */
23
+ exports.Uuid = Uuid;
24
+ const UuidLive = exports.UuidLive = /*#__PURE__*/_effect.Layer.succeed(Uuid, {
25
+ generate: /*#__PURE__*/_effect.Effect.sync(() => globalThis.crypto.randomUUID())
26
+ });
27
+ /**
28
+ * A deterministic uuid layer for suites, wrapping core's shared
29
+ * `makeSequentialUuid` (the single source of truth for the counter scheme).
30
+ * Each layer instance builds a fresh generator, so it owns its own counter and
31
+ * yields a stable v4-shaped sequence `00000000-0000-4000-8000-<12 hex>` starting
32
+ * at `...000000000000`. Generation is wrapped in `Effect.sync` because the
33
+ * generator advances its counter (a side effect on each call).
34
+ */
35
+ const UuidTest = exports.UuidTest = /*#__PURE__*/_effect.Layer.sync(Uuid, () => {
36
+ const next = (0, _testing.makeSequentialUuid)();
37
+ return {
38
+ generate: _effect.Effect.sync(next)
39
+ };
40
+ });
41
+ //# sourceMappingURL=Uuid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Uuid.js","names":["_testing","require","_effect","Uuid","Context","Tag","exports","UuidLive","Layer","succeed","generate","Effect","sync","globalThis","crypto","randomUUID","UuidTest","next","makeSequentialUuid"],"sources":["../../src/Uuid.ts"],"sourcesContent":[null],"mappings":";;;;;;AAWA,IAAAA,QAAA,GAAAC,OAAA;AACA,IAAAC,OAAA,GAAAD,OAAA;AAZA;;;;;;;;;;;;AAcM,MAAOE,IAAK,sBAAQC,eAAO,CAACC,GAAG,CAAC,uBAAuB,CAAC,EAG3D;AAEH;AAAAC,OAAA,CAAAH,IAAA,GAAAA,IAAA;AACO,MAAMI,QAAQ,GAAAD,OAAA,CAAAC,QAAA,gBAAsBC,aAAK,CAACC,OAAO,CAACN,IAAI,EAAE;EAC7DO,QAAQ,eAAEC,cAAM,CAACC,IAAI,CAAC,MAAMC,UAAU,CAACC,MAAM,CAACC,UAAU,EAAE;CAC3D,CAAC;AAEF;;;;;;;;AAQO,MAAMC,QAAQ,GAAAV,OAAA,CAAAU,QAAA,gBAAsBR,aAAK,CAACI,IAAI,CAACT,IAAI,EAAE,MAAK;EAC/D,MAAMc,IAAI,GAAG,IAAAC,2BAAkB,GAAE;EACjC,OAAO;IAAER,QAAQ,EAAEC,cAAM,CAACC,IAAI,CAACK,IAAI;EAAC,CAAE;AACxC,CAAC,CAAC","ignoreList":[]}
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.runDecider = exports.decider = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _effect = require("effect");
9
+ var _decisionModel = require("./decisionModel.js");
10
+ /**
11
+ * A `Decider`: the write side's unit of decision. It pairs the projections a
12
+ * command handler reads (its decision model) with an effectful `decide` step
13
+ * that turns the folded state plus a command into the events to append.
14
+ *
15
+ * This is PRODUCTION code and it lives beside the `buildDecisionModel` it
16
+ * drives. It began in `@kairos-es/testing` because the Given/When/Then surface
17
+ * was its first consumer, but nothing in it is test apparatus: every import
18
+ * resolves to this package or to `@kairos-es/core`, and a test framework is
19
+ * never one of them. That home was a real hazard rather than a tidiness
20
+ * complaint — `@kairos-es/testing` peers `@effect/vitest`, so a command handler
21
+ * typed against `Decider` (as this repo's own worked example is, in `src/` and
22
+ * not `test/`) named a package whose sibling module imports `expect` and `it` at
23
+ * top level, and a handler that actually RAN a command would have
24
+ * runtime-imported `runDecider` from it, loading a test framework into a
25
+ * production module graph. `@kairos-es/read/testing` documents that same hazard
26
+ * as the reason a subpath may add no peer; the difference here is that this is
27
+ * not a testing surface at all, so the fix is a home rather than a rule.
28
+ *
29
+ * `@kairos-es/testing` now imports these two symbols like any other consumer,
30
+ * and NO compatibility re-export is left behind there: nothing is published yet,
31
+ * so a re-export would be permanent clutter bought for no consumer, and it would
32
+ * preserve in that package's public surface exactly the coupling the move
33
+ * exists to remove. That is the argument ADR-0008 made when `retryOnConflict`
34
+ * moved to the package that owns the error it matches, and it holds here for the
35
+ * same reason — a write-side consumer depending on the wrong package is
36
+ * backwards, whichever direction the dependency points.
37
+ *
38
+ * `projections` is a *function of the command* because a DCB decision model is
39
+ * parametrised by the command's own identifiers (e.g. narrow to
40
+ * `course:{command.courseId}`), which keeps the consistency boundary — and the
41
+ * `failIfEventsMatch` guard the read produces — as tight as the decision
42
+ * actually needs. This mirrors dcb-events, where the command handler calls
43
+ * `buildDecisionModel(store, { ...projections built from command.data })`.
44
+ *
45
+ * `runDecider` runs the DCB command cycle against whatever `DcbEventStore` is in
46
+ * context: build the decision model for the command (a real `read` + decode +
47
+ * fold via `buildDecisionModel`), decide, then `append` the new events under the
48
+ * append condition the read produced. That IS the optimistic-concurrency
49
+ * contract, in production and in a scenario alike — which is also why the
50
+ * Given/When/Then surface built on it exercises the real fold and the real store
51
+ * rather than a mock. It is a free function rather than a `Context.Tag` service,
52
+ * so it is still not the "production command-runner service method" a retrying
53
+ * `Layer` would need (ADR-0008): the correct retry remains
54
+ * `retryOnConflict(runDecider(...))`, wrapping the whole cycle so a fresh read
55
+ * re-derives the condition.
56
+ *
57
+ * The `decide` step produces its events effectfully (they are encoded via
58
+ * `toEvent`, so it requires `Serializer | Uuid | R`) and may fail on its own
59
+ * error channel (a domain rule violation, e.g. "course already exists"); the
60
+ * append may fail with `AppendConditionFailed` if a concurrent writer moved the
61
+ * boundary, and decode may fail with `CodecError`. All surface on the error
62
+ * channel, so a caller handles all three the same way — and
63
+ * `@kairos-es/testing`'s `.thenError(tag)` can assert any of them.
64
+ */
65
+
66
+ /**
67
+ * Build a `Decider` with all four of its type arguments INFERRED: the slice
68
+ * record `P` from what `projections` returns, the error union `E` and the extra
69
+ * requirement `R` from what `decide` returns. The command type is the one thing
70
+ * a caller still writes, once, as the annotation on the `projections` parameter
71
+ * — `Command` occurs only in parameter positions, so nothing else could supply
72
+ * it, and it is domain information (the command this decision is parametrised
73
+ * by) rather than plumbing.
74
+ *
75
+ * It adds no capability: the interface stays directly inhabitable and this
76
+ * returns exactly the object literal a caller would have written. What it
77
+ * changes is what an author must SPELL. An object literal annotated
78
+ * `Decider<P, Command, E>` has to state the whole slice record — a
79
+ * `ReturnType<typeof someProjection>` per key, a slice's state type being
80
+ * inferred by `projection().on().build()` and so having no name to write — plus
81
+ * the union of every error its decide branches can fail with. Dropping the
82
+ * annotation instead is not open to the author: without it `decide`'s `state`
83
+ * and `command` parameters have no contextual type and are implicitly `any`. So
84
+ * the cost landed hardest on the worked example, where some thirty lines of type
85
+ * plumbing sat between the reader and the three decisions that file exists to
86
+ * teach.
87
+ *
88
+ * `Eff` captures decide's WHOLE return type as one type parameter instead of
89
+ * inferring `E` and `R` from an `Effect<…, E, Serializer | Uuid | R>` shaped
90
+ * target, and that is the part that had to be got right rather than merely
91
+ * written down. A real `decide` returns a UNION of effects — one `Effect.fail`
92
+ * per rule it enforces, plus the produce path — and TypeScript infers a type
93
+ * parameter from a union source by choosing one constituent's candidate, not by
94
+ * unioning them. Against the shaped target `E` therefore came out as whichever
95
+ * branch won, and every other branch was then rejected against it. Inference to
96
+ * a NAKED type variable takes the union whole, which is why `Eff` is the
97
+ * parameter and `Effect.Effect.Error` / `Effect.Effect.Context` recover the two
98
+ * channels afterwards: those do distribute across a union of effects.
99
+ *
100
+ * The constraint on `Eff` is load-bearing twice over. It is what still refuses a
101
+ * `decide` that could produce an EMPTY batch, the one guarantee the shaped
102
+ * target used to give and the one `append` relies on. It is also what
103
+ * contextually types the produce path's `(event) => [event]` as a one-element
104
+ * TUPLE; unconstrained, that widens to `DcbEvent[]`, which is not a
105
+ * `NonEmptyReadonlyArray` and would fail at the call site.
106
+ *
107
+ * The single `as` is narrow and unavoidable: the declared return is a
108
+ * conditional over an unresolved `Eff`, so while `Eff` is still a type variable
109
+ * the checker cannot relate `(…) => Eff` to `(…) => Effect<…,
110
+ * Effect.Error<Eff>, …>`. The value is the two functions it was handed, so
111
+ * there is nothing for the cast to be wrong about.
112
+ *
113
+ * `R` excludes `Serializer | Uuid` because that is what `R` MEANS on `Decider`:
114
+ * the interface writes decide's requirement as `Serializer | Uuid | R`, so `R`
115
+ * is what the produce side needs BEYOND the codec's own two services. Leaving
116
+ * them in would state the same requirement twice and make an ordinary decider
117
+ * read as though it carried an extra one, though the set `runDecider` ends up
118
+ * asking for is identical either way. The exclusion cannot swallow a caller's
119
+ * own service by accident: a `Context.Tag`'s requirement type carries its key as
120
+ * a literal `Id` field, so only a tag declared under `@kairos-es/codec/Uuid` or
121
+ * `@kairos-es/codec/Serializer` matches — and a tag under that key IS that
122
+ * service, context being keyed by the same string at runtime.
123
+ */
124
+ const decider = (projections, decide) => ({
125
+ projections,
126
+ decide
127
+ });
128
+ /**
129
+ * Run the full DCB command cycle for a decider against the ambient
130
+ * `DcbEventStore`: build the decision model (real read + decode + fold), decide,
131
+ * and append under the read's append condition. Returns the last new position.
132
+ *
133
+ * The requirement is `DcbEventStore | Serializer | Uuid | R |
134
+ * RequirementOf<P>`: `Serializer` enters through both the decode
135
+ * (`buildDecisionModel`) and produce (`toEvent`) sides, `Uuid` through produce,
136
+ * the slices' `RequirementOf<P>` through decode, and `R` through produce. (For a
137
+ * def whose `Schema` carries a requirement, `RequirementOf<P>` and `R` are the
138
+ * same tag.) Any `decide` failure, a decode `CodecError`, or an
139
+ * `AppendConditionFailed` propagates on the error channel unchanged.
140
+ */
141
+ exports.decider = decider;
142
+ const runDecider = (decider, command) => _effect.Effect.gen(function* () {
143
+ const store = yield* _core.DcbEventStore;
144
+ const {
145
+ state,
146
+ appendCondition
147
+ } = yield* (0, _decisionModel.buildDecisionModel)(decider.projections(command));
148
+ const newEvents = yield* decider.decide(state, command);
149
+ return yield* store.append(newEvents, appendCondition);
150
+ });
151
+ exports.runDecider = runDecider;
152
+ //# sourceMappingURL=decider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decider.js","names":["_core","require","_effect","_decisionModel","decider","projections","decide","exports","runDecider","command","Effect","gen","store","DcbEventStore","state","appendCondition","buildDecisionModel","newEvents","append"],"sources":["../../src/decider.ts"],"sourcesContent":[null],"mappings":";;;;;;AA8DA,IAAAA,KAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AAjEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DO,MAAMG,OAAO,GAAGA,CASrBC,WAAoC,EACpCC,MAA2D,MAO1D;EAAED,WAAW;EAAEC;AAAM,CAAE,CAKvB;AAEH;;;;;;;;;;;;;AAAAC,OAAA,CAAAH,OAAA,GAAAA,OAAA;AAaO,MAAMI,UAAU,GAAGA,CAMxBJ,OAAkC,EAClCK,OAAgB,KAMhBC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAClC,MAAM;IAAEC,KAAK;IAAEC;EAAe,CAAE,GAAG,OAAO,IAAAC,iCAAkB,EAC1DZ,OAAO,CAACC,WAAW,CAACI,OAAO,CAAC,CAC7B;EACD,MAAMQ,SAAS,GAAG,OAAOb,OAAO,CAACE,MAAM,CAACQ,KAAK,EAAEL,OAAO,CAAC;EACvD,OAAO,OAAOG,KAAK,CAACM,MAAM,CAACD,SAAS,EAAEF,eAAe,CAAC;AACxD,CAAC,CAAC;AAAAR,OAAA,CAAAC,UAAA,GAAAA,UAAA","ignoreList":[]}
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.modelAtHead = exports.buildDecisionModel = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _effect = require("effect");
9
+ var _decode = require("./decode.js");
10
+ /**
11
+ * Read-and-fold over a slice record: read the composed query, decode each event
12
+ * through the matching def's `Schema` in an effectful `Stream.mapEffect` stage,
13
+ * then fold *purely* through core's `composeProjections`.
14
+ *
15
+ * TWO exports sit over that ONE body, the second expressed over the first.
16
+ * `modelAtHead` stops where the fold does, at `{ state, head }` — a
17
+ * materialisation of the record with no table, no checkpoint and no runner.
18
+ * `buildDecisionModel` adds the only part that is genuinely decision-specific:
19
+ * the `AppendCondition` derived from the same query and that same head (the DCB
20
+ * optimistic-concurrency contract). It is core's old
21
+ * `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
22
+ * because decode needs `Schema` + the `Serializer`.
23
+ *
24
+ * The fold stays pure and in core; only the decode stage is effectful, and that
25
+ * stage lives in `decode.ts` — `decodeSlices` — so the read side folds a live
26
+ * `subscribe` stream through the *same* decode this module folds a bounded
27
+ * `read` stream through.
28
+ *
29
+ * The result requirement is exactly `DcbEventStore | Serializer | R`, where `R`
30
+ * is the union of the record's slices' requirements — inferred with no cast,
31
+ * `R` being carried covariantly by each slice's `schemas` map value type.
32
+ * `Serializer` is not yielded here: `decodeSlices` reads it from context itself,
33
+ * so the requirement arrives through the decode stage rather than through a
34
+ * binding this module owns (see `decode.ts` for why).
35
+ */
36
+
37
+ /**
38
+ * Read the events for the composed slices' query, decode each through its def's
39
+ * `Schema`, fold into per-key state, and return `{ state, head }`: one slice
40
+ * record materialised at read time, with no table, no checkpoint and no runner.
41
+ *
42
+ * ## Why `head` comes back, and not bare `state`
43
+ *
44
+ * `head` is the consistency statement, and without it the state is a figure with
45
+ * no as-of — which is why the as-of is in the name. The fold stops at the
46
+ * position the read returned and is maintained no further, so `head` is the
47
+ * whole of what a caller can say about this state's currency: it is what lets
48
+ * the write path turn it into an `AppendCondition` (see `buildDecisionModel`),
49
+ * and what lets a read-time caller report, cache or compare a state against a
50
+ * position rather than against a wall clock.
51
+ *
52
+ * It is also the inversion worth naming out loud, because intuition runs the
53
+ * other way. A MAINTAINED view is consistent as of its CHECKPOINT, which trails
54
+ * the log's head by however far behind its runner is; this fold is consistent as
55
+ * of the head itself. So of two materialisations of one slice record, the one
56
+ * with no table, no checkpoint and no runner is the MOST current, and the table
57
+ * beside it is the stale one. Reach for a maintained view for the queries a
58
+ * single fold cannot serve — across many identities, ordered, joined, paginated
59
+ * — and never because it is fresher, because it is not.
60
+ *
61
+ * `head` is LOG-GLOBAL: it is the head the store's `read` returned, so it moves
62
+ * for an event no slice here selects. `test/decisionModel.test.ts` pins that
63
+ * behaviour EXPLICITLY, over four appended events one of which the composed query
64
+ * excludes by tag; `test/modelAtHead.test.ts` pins it incidentally, its own head
65
+ * assertion landing on that same excluded event's position. Two guards, one of them
66
+ * deliberate — reach for the explicit one when the property itself is what changes.
67
+ * Log-global neither tightens nor loosens the derived guard — there can be no
68
+ * event matching the query between the query's last match and the log head, so a
69
+ * query-filtered head would refuse exactly the same appends — which is precisely
70
+ * why the store's own figure is the one to carry: one fact about the log, not a
71
+ * per-query derivation, and the honest as-of for a state folded from everything
72
+ * the query selected up to it.
73
+ *
74
+ * ## Why this lives in the codec and not in `@kairos-es/read`
75
+ *
76
+ * In `@kairos-es/read` it would be the only export touching none of that
77
+ * package's machinery: no checkpoint, no `EventLogDurability` (R2 is vacuous
78
+ * where there is no checkpoint to outlive the log), no rung of that package's
79
+ * construction gate, and none of its test apparatus. The vocabulary is already
80
+ * this package's — ADR-0004 names the typed slice path's clients as "a decision
81
+ * model / materialisation / command" — and the body is `buildDecisionModel`'s,
82
+ * which is here for the reason the module doc gives.
83
+ */
84
+ const modelAtHead = projections => _effect.Effect.gen(function* () {
85
+ const store = yield* _core.DcbEventStore;
86
+ // The shared, back-pressured decode stage, built from the SAME record the
87
+ // query below is composed from — which is the whole reason `decodeSlices`
88
+ // takes the slices rather than a pre-merged schema table.
89
+ const decode = (0, _decode.decodeSlices)(projections);
90
+ // The pure fold: core composes the record (slices are AnyProjections).
91
+ const composite = (0, _core.composeProjections)(projections);
92
+ const {
93
+ events,
94
+ head
95
+ } = yield* store.read(composite.query);
96
+ const decoded = decode(events);
97
+ const state = yield* _effect.Stream.runFold(decoded, composite.initialState, (acc, event) => composite.evolve(acc, event));
98
+ return {
99
+ state,
100
+ head
101
+ };
102
+ });
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
+ * `composeProjections` therefore runs TWICE for one command — once inside
117
+ * `modelAtHead` for the read, once here for `failIfEventsMatch` — and that is
118
+ * deliberate rather than waste. It is a pure derivation of the record, so the two
119
+ * queries cannot disagree however either call site changes. The alternative,
120
+ * returning the query from `modelAtHead`, would put a third field on a READ-TIME
121
+ * API that no read-time caller wants, to save one pure merge over a handful of
122
+ * slices; a surface paying for a body's convenience is the worse trade.
123
+ *
124
+ * ## How to attribute the read, the decode and the fold — the rule, stated once
125
+ *
126
+ * Because this function is `Effect.map` over `modelAtHead`, prose about the codec
127
+ * splits into two kinds and only one of them may name this symbol:
128
+ *
129
+ * - Naming this function as the ENTRY POINT a caller invokes, or as the path
130
+ * something is reached THROUGH, is correct — "the `buildDecisionModel` fold", "the
131
+ * write path reaches decode through `buildDecisionModel`". Transitive reach is real
132
+ * reach, and this is the symbol a command actually calls.
133
+ * - Naming it as the function that CONTAINS the read, the decode or the fold is
134
+ * FALSE. All three are `modelAtHead`'s; this body adds an `AppendCondition` and
135
+ * nothing else.
136
+ *
137
+ * Worth writing down because the distinction is invisible from a call site and the
138
+ * mistake is systematic rather than careless: every such claim was TRUE before
139
+ * `modelAtHead` was extracted from here, so a doc written against the older shape
140
+ * reads perfectly and is wrong. The correction is to name `modelAtHead`, never to
141
+ * soften the sentence.
142
+ */
143
+ exports.modelAtHead = modelAtHead;
144
+ const buildDecisionModel = projections => _effect.Effect.map(modelAtHead(projections), ({
145
+ state,
146
+ head
147
+ }) => ({
148
+ state,
149
+ appendCondition: {
150
+ failIfEventsMatch: (0, _core.composeProjections)(projections).query,
151
+ after: head
152
+ }
153
+ }));
154
+ exports.buildDecisionModel = buildDecisionModel;
155
+ //# sourceMappingURL=decisionModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decisionModel.js","names":["_core","require","_effect","_decode","modelAtHead","projections","Effect","gen","store","DcbEventStore","decode","decodeSlices","composite","composeProjections","events","head","read","query","decoded","state","Stream","runFold","initialState","acc","event","evolve","exports","buildDecisionModel","map","appendCondition","failIfEventsMatch","after"],"sources":["../../src/decisionModel.ts"],"sourcesContent":[null],"mappings":";;;;;;AA0BA,IAAAA,KAAA,GAAAC,OAAA;AAQA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AAnCA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+CO,MAAMG,WAAW,GACtBC,WAAc,IAMdC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMC,KAAK,GAAG,OAAOC,mBAAa;EAElC;EACA;EACA;EACA,MAAMC,MAAM,GAAG,IAAAC,oBAAY,EAACN,WAAW,CAAC;EAExC;EACA,MAAMO,SAAS,GAAG,IAAAC,wBAAkB,EAClCR,WAA4C,CAC7C;EACD,MAAM;IAAES,MAAM;IAAEC;EAAI,CAAE,GAAG,OAAOP,KAAK,CAACQ,IAAI,CAACJ,SAAS,CAACK,KAAK,CAAC;EAE3D,MAAMC,OAAO,GAAGR,MAAM,CAACI,MAAM,CAAC;EAE9B,MAAMK,KAAK,GAAI,OAAOC,cAAM,CAACC,OAAO,CAClCH,OAAO,EACPN,SAAS,CAACU,YAAY,EACtB,CAACC,GAAG,EAAEC,KAAK,KAAKZ,SAAS,CAACa,MAAM,CAACF,GAAG,EAAEC,KAAK,CAAC,CACvB;EAEvB,OAAO;IAAEL,KAAK;IAAEJ;EAAI,CAAE;AACxB,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAW,OAAA,CAAAtB,WAAA,GAAAA,WAAA;AAwCO,MAAMuB,kBAAkB,GAC7BtB,WAAc,IASdC,cAAM,CAACsB,GAAG,CAACxB,WAAW,CAACC,WAAW,CAAC,EAAE,CAAC;EAAEc,KAAK;EAAEJ;AAAI,CAAE,MAAM;EACzDI,KAAK;EACLU,eAAe,EAAE;IACfC,iBAAiB,EAAE,IAAAjB,wBAAkB,EACnCR,WAA4C,CAC7C,CAACY,KAAK;IACPc,KAAK,EAAEhB;;CAEV,CAAC,CAAC;AAAAW,OAAA,CAAAC,kBAAA,GAAAA,kBAAA","ignoreList":[]}
@@ -0,0 +1,170 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.decodeSlices = void 0;
7
+ var _effect = require("effect");
8
+ var _deserialiseThenDecode = require("./deserialiseThenDecode.js");
9
+ var _errors = require("./errors.js");
10
+ var _Serializer = require("./Serializer.js");
11
+ /**
12
+ * Merge every slice's `type -> Schema` map into one decode lookup.
13
+ *
14
+ * A record of slices is the unit an application composes, but decode is keyed
15
+ * by the wire event's runtime `type` string, so the per-slice maps have to
16
+ * collapse into one flat table first. A type reachable from two slices is
17
+ * therefore a collision the merge has to rule on, and it rules by REFERENCE
18
+ * EQUALITY: the same `Schema` arriving twice is absorbed, two *different*
19
+ * `Schema`s under one type string are refused with a throw.
20
+ *
21
+ * **Why reference equality is the discriminator.** The legitimate case — the one
22
+ * the old last-writer-wins was written for — is two slices that `.on` the same
23
+ * `EventDef`, which is how one event feeds two views. `event()` builds its
24
+ * `Schema.Struct` once and every def hands out that one object, so those two
25
+ * slices hold the identical reference and whichever entry survives the merge
26
+ * decodes identically; `decode.test.ts` asserts that under both key orderings
27
+ * rather than leaving it as a claim. Nothing weaker is sound and nothing
28
+ * stronger is needed: two distinct defs can be structurally identical today and
29
+ * diverge on the next commit, so structural comparison would license exactly the
30
+ * pairing that is about to go wrong.
31
+ *
32
+ * **Why the other case cannot stay last-writer-wins.** Two distinct defs can
33
+ * share a `type` string — a copied def, or two versions accidentally given one
34
+ * name — and then the surviving schema is whichever slice `Object.keys` visits
35
+ * last, an ordering the record does not express and its author never chose.
36
+ * Every event of that type is then decoded against the wrong def for one of the
37
+ * slices, and where the two shapes overlap that failure is SILENT rather than a
38
+ * `CodecError`: `Schema.decodeUnknown` defaults `onExcessProperty` to `'ignore'`
39
+ * (`effect@3.22`'s `ParseResult.ts` runs its excess-key loop only under
40
+ * `'error'` or `'preserve'`), so a field the other def declared is dropped, not
41
+ * reported, and the fold downstream is handed a payload quietly missing it.
42
+ *
43
+ * **Why a throw, and why here rather than at the read side's wiring gate.**
44
+ * `makeRegistry` already treats a duplicated type as a construction-time defect
45
+ * and throws (ADR-0005, `totality.test.ts`); this is the same mistake reached
46
+ * through the slice path, so it gets the same mechanism and the same opening
47
+ * words rather than a second convention for one rule. It belongs to the codec
48
+ * because BOTH consumers need it and only one of them is on the read side: the
49
+ * write path reaches this through `buildDecisionModel` and never goes near the read
50
+ * side's wiring gate. Neither consumer can be mid-stream when it fires —
51
+ * the write path merges before its first `store.read`, and the read side
52
+ * merges after that gate and before it forks the pipeline — and inside
53
+ * their `Effect.gen`s a throw becomes a DEFECT rather than an escaping exception
54
+ * (`effect@3.22`'s `fiberRuntime.ts` run loop converts a thrown value with
55
+ * `core.die`), which is the classification a mis-wiring already has on both
56
+ * sides of the library.
57
+ *
58
+ * The sentence names BOTH slices. The two halves of this mistake are in
59
+ * different entries of the record, and a message naming only the one that
60
+ * happened to be visited second sends its author to look in the wrong place.
61
+ *
62
+ * The cast re-attaches `R`. Indexing `T` by a plain `string` key erases each
63
+ * slice to the `SliceProjection` *constraint*, whose requirement parameter
64
+ * defaults to `unknown`, so the concrete per-slice `R` is not recoverable from
65
+ * the value side. `RequirementOf<T>` recovers it from the *type* side, and the
66
+ * assertion is sound because a slice's own requirement is always a member of
67
+ * that union. Doing it once here is why no call site needs a cast.
68
+ *
69
+ * Module-private, and deliberately so: see `decodeSlices` for why the merge is
70
+ * not a step a caller is trusted to take.
71
+ */
72
+ const mergeSliceSchemas = projections => {
73
+ const schemas = new Map();
74
+ // Which slice contributed each entry, kept in lockstep with `schemas` purely
75
+ // so the refusal can name both sides of a collision. Its presence is also what
76
+ // the check below reads, rather than a second `schemas.has`, so the narrowing
77
+ // that makes the earlier key a `string` falls out of the check itself.
78
+ const contributedBy = new Map();
79
+ for (const key of Object.keys(projections)) {
80
+ const slice = projections[key];
81
+ for (const [type, schema] of slice.schemas) {
82
+ const owner = contributedBy.get(type);
83
+ if (owner !== undefined && schemas.get(type) !== schema) {
84
+ throw new Error(`duplicate event type '${type}' in slices '${owner}' and '${key}': ` + 'they hold different Schemas for one type, so decode would use ' + 'whichever slice merged last. Two slices may share an event type ' + 'only by sharing one EventDef.');
85
+ }
86
+ schemas.set(type, schema);
87
+ contributedBy.set(type, key);
88
+ }
89
+ }
90
+ return schemas;
91
+ };
92
+ /**
93
+ * The decode stage: look the event's `type` up in `schemas`, run the shared
94
+ * `deserialiseThenDecode` over the wire bytes, and emit a `DecodedEvent` (the
95
+ * sequenced envelope plus its typed payload) for the pure fold downstream. The
96
+ * lookup and the envelope are what this stage adds; the two-step in the middle,
97
+ * and the two `reason` strings it fails with, belong to that helper.
98
+ *
99
+ * Curried on `schemas` so a caller builds the lookup once — per decision model,
100
+ * per read model — and reuses the stage across every stream it drives.
101
+ *
102
+ * `Serializer` is read from context *inside* the stage (once, when the stream
103
+ * starts, via `Stream.unwrap`) rather than being pre-yielded and closed over by
104
+ * the caller. That keeps the serialisation dependency wholly inside this
105
+ * module: a consumer cannot hand in a different serialiser than the one its
106
+ * layer provides, so encode (`toEvent`) and decode provably read the same
107
+ * `Serializer` — the invariant `Serializer.ts` rests on. The obligation is
108
+ * still visible, because `Serializer` surfaces in the returned stream's `R`;
109
+ * it just is not satisfiable by hand.
110
+ *
111
+ * The stage is transparent to the input stream's own `E` and `R`: a bounded
112
+ * `read` stream (`never`, `never`) and a live `subscribe` stream (`never`,
113
+ * `Scope`) both flow through unchanged, gaining only `CodecError` and
114
+ * `Serializer`.
115
+ *
116
+ * Module-private: `decodeSlices` is the public shape. Keeping the schema-map
117
+ * form here rather than inlining it into `decodeSlices` is what makes the
118
+ * "build the lookup once, reuse the stage" property explicit in the types.
119
+ */
120
+ const decodeEvents = schemas => events => _effect.Stream.unwrap(_effect.Effect.map(_Serializer.Serializer, serializer => events.pipe(_effect.Stream.mapEffect(sequenced => {
121
+ const schema = schemas.get(sequenced.event.type);
122
+ if (schema === undefined) {
123
+ return _effect.Effect.fail(new _errors.CodecError({
124
+ reason: `no schema for event type '${sequenced.event.type}'`
125
+ }));
126
+ }
127
+ return (0, _deserialiseThenDecode.deserialiseThenDecode)(serializer, schema, sequenced.event.data).pipe(_effect.Effect.map(payload => ({
128
+ sequenced,
129
+ payload
130
+ })));
131
+ }))));
132
+ /**
133
+ * The public decode stage: give it the slice record, get back a `Stream`
134
+ * combinator that decodes every event those slices can handle.
135
+ *
136
+ * **Why one call rather than two.** The lookup and the stage were briefly two
137
+ * public symbols — merge the record, then hand the map to the stage — and every
138
+ * consumer in the repo composed them in exactly that order and did nothing else
139
+ * with the intermediate map. A two-step protocol with one legal composition is
140
+ * not flexibility, it is a way to get it wrong: a caller can merge the wrong
141
+ * record, or a record that is not the one its `Query` was composed from, and
142
+ * that second mistake is silent. In the read side it would decode a live
143
+ * subscription against schemas the subscription never selects — every event
144
+ * arriving would fail `no schema for event type`, framed as a data problem when
145
+ * it is a wiring one. Taking the slices directly removes the intermediate value
146
+ * a caller could substitute, so the schemas provably come from the same record
147
+ * the query does.
148
+ *
149
+ * **Still curried, deliberately.** The merge is a pure derivation of the slice
150
+ * record, so both consumers build the stage ONCE — per decision model, per read
151
+ * model — and reuse it across every stream they drive. Rebuilding the table per
152
+ * stream (let alone per restart) would only burn allocations, and the currying
153
+ * is what makes "built once" the natural way to call this rather than a
154
+ * discipline.
155
+ *
156
+ * **It can REFUSE the record**, and does so the moment it is called rather than
157
+ * once events are flowing: a type reachable from two slices under two DIFFERENT
158
+ * `Schema`s throws, in the same words `makeRegistry` uses. See
159
+ * `mergeSliceSchemas` for the rule, why reference equality is its discriminator
160
+ * and why a throw is the right mechanism. Both in-repo consumers call this
161
+ * before there is a stream to fail, so inside their `Effect.gen`s the refusal is
162
+ * a defect at construction.
163
+ *
164
+ * `RequirementOf<T>` surfaces in the result's `R`, so a slice whose `Schema`
165
+ * carries a requirement (an application-authored decryption transform, ADR-0004)
166
+ * propagates it to the caller with no cast at the call site.
167
+ */
168
+ const decodeSlices = slices => decodeEvents(mergeSliceSchemas(slices));
169
+ exports.decodeSlices = decodeSlices;
170
+ //# sourceMappingURL=decode.js.map