@kairos-es/codec 0.0.0 → 1.0.0-alpha.1

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.
package/README.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Schema-first codec with an encryption seam for kairos-es (ADR-0004).
4
4
 
5
+ ## Installation
6
+
7
+ `@kairos-es/core` and `effect` are peer dependencies, so the application owns the
8
+ single shared Effect instance (per [ADR-0003](../../docs/adr)):
9
+
10
+ ```sh
11
+ pnpm add @kairos-es/codec@alpha @kairos-es/core@alpha effect
12
+ ```
13
+
5
14
  The typed-payload to opaque-bytes seam: `event(type, fields, tagsFor)` defines a
6
15
  tagged `Schema` together with the mapper that derives an event's tags from its own
7
16
  payload (ADR-0006); `projection(init).on(def, fold)…build()` builds the slices a
@@ -21,6 +30,42 @@ because it touches none of that package's machinery, and it is deliberately not
21
30
  a decider calls: `runDecider` already folds to head through `buildDecisionModel`,
22
31
  under the condition that read produces.
23
32
 
33
+ `transact(precondition)(body)` is the LAYERED write path, for a decision whose second
34
+ query cannot be written until an earlier read has been folded — the identifier it needs
35
+ is data that fold produced, and better tagging cannot remove the hop. Outside a bracket
36
+ nothing changes and no signature moves. Inside one, every read is recorded wherever and
37
+ by whatever symbol it happened, and the bracket appends what the body returned under the
38
+ union of every query walked, at the EARLIEST head observed. No append condition reaches
39
+ you, so there is none to assemble wrongly — and the familiar `buildDecisionModel`, which
40
+ guards only its own hop outside a bracket, derives from the whole walk inside one.
41
+
42
+ The bracket takes ONE precondition, a `{ tag, evaluate }` pair it evaluates INSIDE the
43
+ ledger so the guard's own reads are part of what guards the write; its result reaches the
44
+ body as a service, so a body that consumes it cannot be run without one. `decideOnly`
45
+ returns a decider's events for the bracket to append. `unsafeAppend(events, reason)` is
46
+ the one sanctioned append under no condition, and the reason is required.
47
+
48
+ ⚠ Three things to know before you use it. **Wrap conflict retry around the whole
49
+ bracket**, never inside it: a retry within one re-records into the ledger already
50
+ open and its floor cannot advance. **`modelAtHead`'s error channel carries
51
+ `TaglessSliceInLedger`**, raised when a slice built without tags would be unioned
52
+ with another — build every slice a layered decision reads with
53
+ `build({ tags: [...] })`.
54
+
55
+ And ⚠⚠ **inside a bracket, read through `modelAtHead` and nothing else.** A read
56
+ that consults a projection, a materialised view or raw SQL contributes no hop, so
57
+ its query is absent from the derived union and its head is absent from the floor:
58
+ the decision rests on data the append is not guarded on, and the bracket LOOKS
59
+ guarded. Nothing will tell you — not the types, not `NoReadsRecorded`, which fires
60
+ only on a bracket that read nothing at all, and no lint rule, which could not see a
61
+ read one call away in a helper and would not run in your tree in any case. If a
62
+ read must consult a view, do it outside the bracket, where its answer is not part
63
+ of what the append is guarded on.
64
+
65
+ The reasoning for all of this is in
66
+ [ADR-0014](../../docs/adr/0014-layered-decision-models-an-ambient-read-ledger-and-a-bracket-that-owns-the-append.md).
67
+ The root README says why a repo-relative link does not resolve for you yet.
68
+
24
69
  `decodeSlices(slices)` is the decode stage `modelAtHead` folds through, and so in turn
25
70
  the one `buildDecisionModel` gets by building on `modelAtHead` — exported so the read
26
71
  side's `ProjectionRunner` shares one implementation over a live `subscribe` stream
@@ -28,9 +73,29 @@ rather than reimplementing it. `makeRegistry(defs)` is the minimal general
28
73
  path, decoding an arbitrary wire event by its runtime type string with an unknown
29
74
  type as a typed `UnknownEventType` (ADR-0005).
30
75
 
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,
76
+ `Decider` and `runDecider(decider, command)` close a SINGLE-HOP command cycle over
77
+ that fold: build the decision model, decide, append under the read's own condition.
78
+
79
+ ⚠ **`runDecider` and `decideOnly` are two dispositions of one cycle**, and each
80
+ refuses exactly where the other is the answer:
81
+
82
+ | | outside a bracket | inside a bracket |
83
+ | ------------ | ------------------------------ | ----------------------------------- |
84
+ | `runDecider` | ✅ the ordinary single-hop case | ⛔ refused as a defect when it runs |
85
+ | `decideOnly` | ⛔ unrunnable, and `tsc` says so | ✅ the layered case |
86
+
87
+ So the remedy is always one identifier away and you never have to guess which you
88
+ wanted. Neither is expressed over the other — each would have to defeat the other's
89
+ refusal to be.
90
+
91
+ ⚠ *Unrunnable* rather than *unwritable*: a bare `decideOnly(...)` outside a bracket
92
+ compiles, and so does a helper you export over it, because its `ReadLedger`
93
+ requirement propagates like any other unmet one. The error lands where you DISCHARGE
94
+ the context — `Effect.provide`, `runPromise`, or an annotation asking for
95
+ `R = never` — which may be a different file. You are told by the type checker rather
96
+ than by a failing run, which is the point; you are not told at the call.
97
+
98
+ `decider(projections, decide)` builds one with its slice record,
34
99
  its error union and its extra requirement all inferred — the command type, on the
35
100
  `projections` parameter, is the only annotation a caller writes. They live here,
36
101
  beside the `buildDecisionModel` they drive, rather
@@ -135,8 +135,15 @@ const decider = (projections, decide) => ({
135
135
  * (`buildDecisionModel`) and produce (`toEvent`) sides, `Uuid` through produce,
136
136
  * the slices' `RequirementOf<P>` through decode, and `R` through produce. (For a
137
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.
138
+ * same tag.) Any `decide` failure, a decode `CodecError`, an
139
+ * `AppendConditionFailed`, or the read's own `TaglessSliceInLedger` propagates on
140
+ * the error channel unchanged.
141
+ *
142
+ * ⚠ INSIDE a bracket this function is REFUSED, as a defect, at the store's shared
143
+ * append decorator, and `decideOnly` is what a body reaches for instead.
144
+ * `index.ts` states that pair, why each refuses exactly where the other is the
145
+ * answer, and why neither can be expressed over the other. Read it before
146
+ * changing either signature; this file does not restate the argument.
140
147
  */
141
148
  exports.decider = decider;
142
149
  const runDecider = (decider, command) => _effect.Effect.gen(function* () {
@@ -1 +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":[]}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0DO,MAAMG,OAAO,GAAGA,CASrBC,WAAoC,EACpCC,MAA2D,MAO1D;EAAED,WAAW;EAAEC;AAAM,CAAE,CAKvB;AAEH;;;;;;;;;;;;;;;;;;;;AAAAC,OAAA,CAAAH,OAAA,GAAAA,OAAA;AAoBO,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":[]}
@@ -7,6 +7,7 @@ exports.modelAtHead = exports.buildDecisionModel = void 0;
7
7
  var _core = require("@kairos-es/core");
8
8
  var _effect = require("effect");
9
9
  var _decode = require("./decode.js");
10
+ var _readLedger = require("./readLedger.js");
10
11
  /**
11
12
  * Read-and-fold over a slice record: read the composed query, decode each event
12
13
  * through the matching def's `Schema` in an effectful `Stream.mapEffect` stage,
@@ -16,11 +17,18 @@ var _decode = require("./decode.js");
16
17
  * `modelAtHead` stops where the fold does, at `{ state, head }` — a
17
18
  * materialisation of the record with no table, no checkpoint and no runner.
18
19
  * `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
20
+ * the `AppendCondition` (the DCB optimistic-concurrency contract). It is core's
21
+ * old `buildDecisionModel(Record<string, AnyProjection>)` surface, moved here
22
22
  * because decode needs `Schema` + the `Serializer`.
23
23
  *
24
+ * BOTH exports also record into the ambient read ledger when one is in context,
25
+ * which is what makes a decision that walks a chain of identifiers guard on every
26
+ * hop rather than on its last. Recording is `modelAtHead`'s, so it happens
27
+ * whatever symbol performed the read; outside a bracket there is no ledger,
28
+ * nothing is recorded and no behaviour changes. `readLedger.ts` owns the whole
29
+ * argument, including why the recorder is this module rather than a
30
+ * layered-only entry point of its own.
31
+ *
24
32
  * The fold stays pure and in core; only the decode stage is effectful, and that
25
33
  * stage lives in `decode.ts` — `decodeSlices` — so the read side folds a live
26
34
  * `subscribe` stream through the *same* decode this module folds a bounded
@@ -58,6 +66,16 @@ var _decode = require("./decode.js");
58
66
  * single fold cannot serve — across many identities, ordered, joined, paginated
59
67
  * — and never because it is fresher, because it is not.
60
68
  *
69
+ * ⚠ AND INSIDE A BRACKET IT IS NOT THE FLOOR THE APPEND WILL USE. Everything
70
+ * above is a statement about THIS read, and it stays true; what changes is that a
71
+ * bracket guards on the EARLIEST head across every hop it walked, which on any hop
72
+ * after the first is a strictly lower number than the one returned here. So a
73
+ * caller who reads `head` inside a bracket and reasons about what the append will
74
+ * refuse is reasoning from the wrong figure — the guard is wider than this head
75
+ * says. Nothing here is wrong; the trap is assuming the two are the same number,
76
+ * and it is named because it is exactly the assumption that holds outside a
77
+ * bracket and quietly stops holding inside one. `readLedger.ts` owns the floor.
78
+ *
61
79
  * `head` is LOG-GLOBAL: it is the head the store's `read` returned, so it moves
62
80
  * for an event no slice here selects. `test/decisionModel.test.ts` pins that
63
81
  * behaviour EXPLICITLY, over four appended events one of which the composed query
@@ -89,12 +107,28 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
89
107
  const decode = (0, _decode.decodeSlices)(projections);
90
108
  // The pure fold: core composes the record (slices are AnyProjections).
91
109
  const composite = (0, _core.composeProjections)(projections);
110
+ // Both ledger calls below are no-ops outside a bracket and neither touches
111
+ // the requirement channel; `readLedger.ts` owns why recording happens HERE
112
+ // rather than at a layered-only entry point of its own.
113
+ //
114
+ // Refuse an unservable union BEFORE the read is paid for: the question is
115
+ // answerable from the queries alone, so a hop that would kill the append two
116
+ // layers away fails at the read that caused it instead.
117
+ yield* (0, _readLedger.assertServableAddition)(projections);
118
+ // EAGER: the query is filed before a single event is streamed, so a read that
119
+ // opens and never completes still widens the guard. What comes back is the
120
+ // effect that completes the hop; outside a bracket it does nothing.
121
+ const closeHop = yield* (0, _readLedger.recordQuery)(composite.query);
92
122
  const {
93
123
  events,
94
124
  head
95
125
  } = yield* store.read(composite.query);
96
126
  const decoded = decode(events);
97
127
  const state = yield* _effect.Stream.runFold(decoded, composite.initialState, (acc, event) => composite.evolve(acc, event));
128
+ // LAZY: only once the fold has run to completion. A read that failed
129
+ // part-way folded a PREFIX, and a head recorded for it is a claim it did not
130
+ // earn — so the line below is unreachable on that path, deliberately.
131
+ yield* closeHop(head);
98
132
  return {
99
133
  state,
100
134
  head
@@ -113,17 +147,19 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
113
147
  * of a near-duplicate: `modelAtHead` is the shared part, this function is the
114
148
  * condition and nothing else, and each is shorter than the one function was.
115
149
  *
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.
150
+ * Outside a bracket `composeProjections` therefore runs twice for one command —
151
+ * once inside `modelAtHead` for the read, once here for `failIfEventsMatch` — and
152
+ * that is deliberate rather than waste. It is a pure derivation of the record, so
153
+ * the two queries cannot disagree however either call site changes. The
154
+ * alternative, returning the query from `modelAtHead`, would put a third field on
155
+ * a READ-TIME API that no read-time caller wants, to save one pure merge over a
156
+ * handful of slices; a surface paying for a body's convenience is the worse
157
+ * trade. Inside a bracket the second merge is not reached at all: the ledger's
158
+ * own derivation composes every recorded hop instead.
123
159
  *
124
160
  * ## How to attribute the read, the decode and the fold — the rule, stated once
125
161
  *
126
- * Because this function is `Effect.map` over `modelAtHead`, prose about the codec
162
+ * Because this function is expressed OVER `modelAtHead`, prose about the codec
127
163
  * splits into two kinds and only one of them may name this symbol:
128
164
  *
129
165
  * - Naming this function as the ENTRY POINT a caller invokes, or as the path
@@ -141,15 +177,35 @@ const modelAtHead = projections => _effect.Effect.gen(function* () {
141
177
  * soften the sentence.
142
178
  */
143
179
  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
180
+ const buildDecisionModel = projections => _effect.Effect.gen(function* () {
181
+ const {
182
+ state,
183
+ head
184
+ } = yield* modelAtHead(projections);
185
+ // INSIDE A BRACKET the condition comes from the ledger — every query the
186
+ // bracket walked, at the earliest head observed — and not from this one read.
187
+ // That is the change's whole ergonomic claim: the call whose current
188
+ // behaviour is the hazard becomes CORRECT inside a bracket, with nothing at
189
+ // the call site changing beyond entering one. The cost is that one expression
190
+ // means two things depending on ambient context, and that is the trade taken.
191
+ //
192
+ // `modelAtHead` has just recorded a hop, so inside a bracket the derivation
193
+ // cannot be empty; `None` here means there is no ledger at all.
194
+ const ambient = yield* _readLedger.ambientLayeredCondition;
195
+ if (_effect.Option.isSome(ambient)) {
196
+ return {
197
+ state,
198
+ appendCondition: ambient.value
199
+ };
152
200
  }
153
- }));
201
+ // OUTSIDE a bracket this is byte-for-byte what shipped.
202
+ return {
203
+ state,
204
+ appendCondition: {
205
+ failIfEventsMatch: (0, _core.composeProjections)(projections).query,
206
+ after: head
207
+ }
208
+ };
209
+ });
154
210
  exports.buildDecisionModel = buildDecisionModel;
155
211
  //# sourceMappingURL=decisionModel.js.map
@@ -1 +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":[]}
1
+ {"version":3,"file":"decisionModel.js","names":["_core","require","_effect","_decode","_readLedger","modelAtHead","projections","Effect","gen","store","DcbEventStore","decode","decodeSlices","composite","composeProjections","assertServableAddition","closeHop","recordQuery","query","events","head","read","decoded","state","Stream","runFold","initialState","acc","event","evolve","exports","buildDecisionModel","ambient","ambientLayeredCondition","Option","isSome","appendCondition","value","failIfEventsMatch","after"],"sources":["../../src/decisionModel.ts"],"sourcesContent":[null],"mappings":";;;;;;AAiCA,IAAAA,KAAA,GAAAC,OAAA;AAQA,IAAAC,OAAA,GAAAD,OAAA;AACA,IAAAE,OAAA,GAAAF,OAAA;AAGA,IAAAG,WAAA,GAAAH,OAAA;AA7CA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDO,MAAMI,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;EAED;EACA;EACA;EACA;EACA;EACA;EACA;EACA,OAAO,IAAAS,kCAAsB,EAACT,WAA4C,CAAC;EAE3E;EACA;EACA;EACA,MAAMU,QAAQ,GAAG,OAAO,IAAAC,uBAAW,EAACJ,SAAS,CAACK,KAAK,CAAC;EAEpD,MAAM;IAAEC,MAAM;IAAEC;EAAI,CAAE,GAAG,OAAOX,KAAK,CAACY,IAAI,CAACR,SAAS,CAACK,KAAK,CAAC;EAE3D,MAAMI,OAAO,GAAGX,MAAM,CAACQ,MAAM,CAAC;EAE9B,MAAMI,KAAK,GAAI,OAAOC,cAAM,CAACC,OAAO,CAClCH,OAAO,EACPT,SAAS,CAACa,YAAY,EACtB,CAACC,GAAG,EAAEC,KAAK,KAAKf,SAAS,CAACgB,MAAM,CAACF,GAAG,EAAEC,KAAK,CAAC,CACvB;EAEvB;EACA;EACA;EACA,OAAOZ,QAAQ,CAACI,IAAI,CAAC;EAErB,OAAO;IAAEG,KAAK;IAAEH;EAAI,CAAE;AACxB,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAAU,OAAA,CAAAzB,WAAA,GAAAA,WAAA;AA0CO,MAAM0B,kBAAkB,GAC7BzB,WAAc,IASdC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAM;IAAEe,KAAK;IAAEH;EAAI,CAAE,GAAG,OAAOf,WAAW,CAACC,WAAW,CAAC;EAEvD;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA,MAAM0B,OAAO,GAAG,OAAOC,mCAAuB;EAC9C,IAAIC,cAAM,CAACC,MAAM,CAACH,OAAO,CAAC,EAAE;IAC1B,OAAO;MAAET,KAAK;MAAEa,eAAe,EAAEJ,OAAO,CAACK;IAAK,CAAE;EAClD;EAEA;EACA,OAAO;IACLd,KAAK;IACLa,eAAe,EAAE;MACfE,iBAAiB,EAAE,IAAAxB,wBAAkB,EACnCR,WAA4C,CAC7C,CAACY,KAAK;MACPqB,KAAK,EAAEnB;;GAEV;AACH,CAAC,CAAC;AAAAU,OAAA,CAAAC,kBAAA,GAAAA,kBAAA","ignoreList":[]}
package/dist/cjs/index.js CHANGED
@@ -9,6 +9,18 @@ Object.defineProperty(exports, "CodecError", {
9
9
  return _errors.CodecError;
10
10
  }
11
11
  });
12
+ Object.defineProperty(exports, "NoReadsRecorded", {
13
+ enumerable: true,
14
+ get: function () {
15
+ return _readLedger.NoReadsRecorded;
16
+ }
17
+ });
18
+ Object.defineProperty(exports, "ReadLedger", {
19
+ enumerable: true,
20
+ get: function () {
21
+ return _readLedger.ReadLedger;
22
+ }
23
+ });
12
24
  Object.defineProperty(exports, "Serializer", {
13
25
  enumerable: true,
14
26
  get: function () {
@@ -21,6 +33,12 @@ Object.defineProperty(exports, "SerializerDefault", {
21
33
  return _Serializer.SerializerDefault;
22
34
  }
23
35
  });
36
+ Object.defineProperty(exports, "TaglessSliceInLedger", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _readLedger.TaglessSliceInLedger;
40
+ }
41
+ });
24
42
  Object.defineProperty(exports, "UnknownEventType", {
25
43
  enumerable: true,
26
44
  get: function () {
@@ -51,6 +69,12 @@ Object.defineProperty(exports, "buildDecisionModel", {
51
69
  return _decisionModel.buildDecisionModel;
52
70
  }
53
71
  });
72
+ Object.defineProperty(exports, "decideOnly", {
73
+ enumerable: true,
74
+ get: function () {
75
+ return _precondition.decideOnly;
76
+ }
77
+ });
54
78
  Object.defineProperty(exports, "decider", {
55
79
  enumerable: true,
56
80
  get: function () {
@@ -99,12 +123,26 @@ Object.defineProperty(exports, "toEvent", {
99
123
  return _toEvent.toEvent;
100
124
  }
101
125
  });
126
+ Object.defineProperty(exports, "transact", {
127
+ enumerable: true,
128
+ get: function () {
129
+ return _precondition.transact;
130
+ }
131
+ });
132
+ Object.defineProperty(exports, "unsafeAppend", {
133
+ enumerable: true,
134
+ get: function () {
135
+ return _precondition.unsafeAppend;
136
+ }
137
+ });
102
138
  var _decider = require("./decider.js");
103
139
  var _decisionModel = require("./decisionModel.js");
104
140
  var _decode = require("./decode.js");
105
141
  var _errors = require("./errors.js");
106
142
  var _event = require("./event.js");
143
+ var _precondition = require("./precondition.js");
107
144
  var _projection = require("./projection.js");
145
+ var _readLedger = require("./readLedger.js");
108
146
  var _registry = require("./registry.js");
109
147
  var _Serializer = require("./Serializer.js");
110
148
  var _toEvent = require("./toEvent.js");
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["_decider","require","_decisionModel","_decode","_errors","_event","_projection","_registry","_Serializer","_toEvent","_Uuid"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqDA,IAAAA,QAAA,GAAAC,OAAA;AACA,IAAAC,cAAA,GAAAD,OAAA;AAKA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,MAAA,GAAAJ,OAAA;AACA,IAAAK,WAAA,GAAAL,OAAA;AAKA,IAAAM,SAAA,GAAAN,OAAA;AACA,IAAAO,WAAA,GAAAP,OAAA;AACA,IAAAQ,QAAA,GAAAR,OAAA;AACA,IAAAS,KAAA,GAAAT,OAAA","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["_decider","require","_decisionModel","_decode","_errors","_event","_precondition","_projection","_readLedger","_registry","_Serializer","_toEvent","_Uuid"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsGA,IAAAA,QAAA,GAAAC,OAAA;AACA,IAAAC,cAAA,GAAAD,OAAA;AAKA,IAAAE,OAAA,GAAAF,OAAA;AACA,IAAAG,OAAA,GAAAH,OAAA;AACA,IAAAI,MAAA,GAAAJ,OAAA;AACA,IAAAK,aAAA,GAAAL,OAAA;AAMA,IAAAM,WAAA,GAAAN,OAAA;AAKA,IAAAO,WAAA,GAAAP,OAAA;AAOA,IAAAQ,SAAA,GAAAR,OAAA;AACA,IAAAS,WAAA,GAAAT,OAAA;AACA,IAAAU,QAAA,GAAAV,OAAA;AACA,IAAAW,KAAA,GAAAX,OAAA","ignoreList":[]}
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.unsafeAppend = exports.transact = exports.decideOnly = void 0;
7
+ var _core = require("@kairos-es/core");
8
+ var _effect = require("effect");
9
+ var _decisionModel = require("./decisionModel.js");
10
+ var _readLedger = require("./readLedger.js");
11
+ /**
12
+ * The DCB command cycle MINUS the append: read the decision model, decode, fold,
13
+ * decide, and RETURN the events for the bracket to append.
14
+ *
15
+ * Bracket-only, and the refusal is STATIC — see the section below for the mechanism
16
+ * and, more importantly, for where the error actually lands, which is not where a
17
+ * reader first expects. Forced through anyway, being outside a bracket is a DEFECT
18
+ * rather than an error-channel value: it is not a data-dependent outcome a caller
19
+ * could handle, and the events produced would be dropped with nothing to append them.
20
+ *
21
+ * It is also what makes the nesting refusal liveable. A command runner inside a
22
+ * bracket is refused at the store, and this is the function a builder reaches for
23
+ * instead — one identifier away. A refusal is only affordable when the fix is that
24
+ * close.
25
+ *
26
+ * `modelAtHead` and NOT `buildDecisionModel`: this function must not derive a
27
+ * condition at all. The bracket owns that, and owning it in ONE place is what
28
+ * stops a caller appending under the last hop's condition alone.
29
+ *
30
+ * ## Bracket-only is a REQUIREMENT, so `tsc` refuses it rather than a run
31
+ *
32
+ * `ReadLedger` is named in the requirement channel and `transact` is the only thing
33
+ * that provides it, so a program that calls this outside a bracket cannot be RUN,
34
+ * and `tsc` is what says so — nothing has to execute for a caller to be told.
35
+ *
36
+ * ⚠ Measured, because the obvious stronger reading is wrong: the call site itself
37
+ * COMPILES. A bare `decideOnly(d, cmd)` outside a bracket is fine, and so is an
38
+ * exported helper over it; the requirement just propagates, as any unmet requirement
39
+ * does. What fails is the DISCHARGE — `Effect.provide`, `runPromise`, an annotation
40
+ * asking for `R = never` — possibly in another file. That is still a static refusal
41
+ * and it is the whole gain over the runtime defect this replaced. It is not a
42
+ * squiggle under the mistake.
43
+ *
44
+ * That is a PRESENCE check, and presence is the direction the type system can
45
+ * decide here — the same direction `transact`'s `Exclude<R, Id>` already spends on
46
+ * the precondition's output. ADR-0014's 2026-08-29 amendment carries the rule that
47
+ * follows: look for a presence check before reaching for a defect. This function
48
+ * shipped a defect first, and the check was available all along.
49
+ *
50
+ * ⚠ Two lines carry it and their JOBS differ. The RETURN ANNOTATION is the surface a
51
+ * consumer is judged against, and what the discharge above meets. The
52
+ * `yield* ReadLedger` keeps the requirement in the INFERRED type, so it survives the
53
+ * annotation being edited, and it dies at run time for the audience no type reaches
54
+ * — a JavaScript consumer runs no compile step — rather than handing back events
55
+ * with no bracket to append them. Nothing here READS the ledger; `modelAtHead` does
56
+ * the recording, ambiently, through `Effect.serviceOption`.
57
+ *
58
+ * ⚠ Do not try to write down which check catches which deletion. It was tried twice
59
+ * and was wrong twice, and `test/appendOwnership.test.ts` in the source repository
60
+ * says why the second failure means there should be no third attempt. Plant the
61
+ * mutation instead.
62
+ */
63
+ const decideOnly = (decider, command) => _effect.Effect.gen(function* () {
64
+ yield* _readLedger.ReadLedger;
65
+ const {
66
+ state
67
+ } = yield* (0, _decisionModel.modelAtHead)(decider.projections(command));
68
+ return yield* decider.decide(state, command);
69
+ });
70
+ /**
71
+ * The sanctioned exception to the nesting refusal: append these events under NO
72
+ * condition, and say in the identifier and in a required `reason` why that is
73
+ * deliberate.
74
+ *
75
+ * It covers three cases and no others: deliberate narrowing, a genuinely
76
+ * unconditional creation, and a second append inside one bracket. The `reason` is
77
+ * ceremony on purpose, and ADR-0014 decision 6 owns why a hatch has to cost more
78
+ * than the safe path — that argument is a DECISION and belongs to the record that
79
+ * took it, so it is not restated here. How biome turns a required argument into a
80
+ * review signal is the source repository's `biome-plugins/unsafeAppendReported.grit`.
81
+ *
82
+ * ⚠ It BYPASSES the derived condition rather than composing with it. A caller who
83
+ * genuinely needs *the union, minus one event type* must restate the whole
84
+ * condition here. If that turns up in practice, a composing override is the
85
+ * additive answer and this hatch is not it.
86
+ *
87
+ * `Effect.mapInputContext` over
88
+ * `Context.omit(AppendIsOwned)` is what lets it through the store's ownership
89
+ * refusal — the one route, and `AppendIsOwned.ts` owns why removing the marker is
90
+ * the shape rather than a second tag or a sanctioning flag.
91
+ */
92
+ exports.decideOnly = decideOnly;
93
+ const unsafeAppend = (events,
94
+ // Required by the signature and never read, which IS the mechanism: the value a
95
+ // caller has to write is what a reviewer sees in the diff, and `tsc` refuses the
96
+ // call that omits it. A lint rule asserting the same arity existed and was
97
+ // deleted, on the ground that the type already refuses it — which made that
98
+ // ground load-bearing, so `test/appendOwnership.test.ts` in the source repository
99
+ // pins it. Reading the value here would give it a second job and a second place to
100
+ // be wrong.
101
+ //
102
+ // NOT `_reason`, though it is unused. This name SHIPS — it is what a consumer's
103
+ // editor offers and what the published `.d.ts` carries — and a leading underscore
104
+ // is this ecosystem's way of saying "ignore me", which is the opposite of what a
105
+ // required argument whose whole purpose is to be read by a human should say.
106
+ // biome-ignore lint/correctness/noUnusedFunctionParameters: unread on purpose; this name ships in the .d.ts and an underscore would tell a consumer to ignore it
107
+ reason) => _effect.Effect.flatMap(_core.DcbEventStore, store =>
108
+ // ⛔ DO NOT SIMPLIFY THIS. The annotation reads as "map an empty context to an
109
+ // empty context", and the types say nothing else — but `Context.omit` runs
110
+ // against the RUNTIME context, which inside a bracket holds `AppendIsOwned`,
111
+ // and removing it is the entire reason this function can append at all. That
112
+ // marker is deliberately absent from every signature (it is read through
113
+ // `Effect.serviceOption`, which is what keeps it out of `append`'s requirement
114
+ // channel), so nothing here will fail to compile if this is deleted — what
115
+ // catches it is `test/appendOwnership.test.ts` in the source repository, not the
116
+ // type checker.
117
+ _effect.Effect.mapInputContext(store.append(events), context => _effect.Context.omit(_core.AppendIsOwned)(context)));
118
+ /**
119
+ * Open a bracket over ONE decision: evaluate the precondition inside it, hand its
120
+ * output to the body under the precondition's tag, and append what the body
121
+ * produced under the union of every read either of them performed.
122
+ *
123
+ * Curried so the precondition reads as the thing the command DECLARES and the body
124
+ * reads as the thing it DOES.
125
+ *
126
+ * The ownership marker is provided ALONGSIDE the ledger and to the body only, so
127
+ * any append inside the body — a nested bracket, a command runner, a raw
128
+ * `store.append` — is refused at the store as a defect, while the bracket's own
129
+ * closing append below runs outside that scope and is unaffected. That is why the
130
+ * refusal is on APPENDING rather than on opening a bracket: a command runner
131
+ * appends without opening one, and its events usually match the union the bracket
132
+ * is about to guard on, so the bracket's own close would be refused by a condition
133
+ * its own body invalidated.
134
+ */
135
+ exports.unsafeAppend = unsafeAppend;
136
+ const transact = precondition => body => _effect.Effect.gen(function* () {
137
+ const store = yield* _core.DcbEventStore;
138
+ const ledger = yield* _readLedger.makeReadLedger;
139
+ const events = yield* _effect.Effect.gen(function* () {
140
+ // ORDER IS THE DESIGN. The precondition runs first and runs HERE, with the
141
+ // ledger already in context, so its reads are hops in the very condition
142
+ // that will guard the body's write.
143
+ const proof = yield* precondition.evaluate;
144
+ return yield* body.pipe(_effect.Effect.provideService(precondition.tag, proof));
145
+ }).pipe(
146
+ // ONE layer, not two provisions, and it is the ANNOTATION above that makes
147
+ // that mandatory rather than tidy.
148
+ //
149
+ // TypeScript does not simplify a deferred conditional over an unresolved
150
+ // type variable, so `Exclude<Exclude<X, A>, B>` and
151
+ // `Exclude<X, A | B>` are unrelated types while `X` is still a parameter,
152
+ // and every provision here adds one level of nesting the annotation has to
153
+ // mirror EXACTLY. Both halves measured, each as a `TS2719` at the return
154
+ // annotation: two chained provisions refuse the flat
155
+ // `Exclude<PR, ReadLedger | AppendIsOwned>`, and even with this merge the
156
+ // flat `Exclude<R, Id | ReadLedger | AppendIsOwned>` is refused too,
157
+ // because the precondition's own tag is provided separately and further in.
158
+ // So `PR` is flat and `R` is one deep, and that asymmetry is the pipeline
159
+ // showing through rather than a slip.
160
+ //
161
+ // Merging says what is true anyway, which is why it is the fix rather than
162
+ // a cast: the ledger and the ownership marker have one lifetime and one
163
+ // meaning between them, and that meaning is *you are inside a bracket*.
164
+ _effect.Effect.provide(_effect.Layer.merge(ledger.layer, _effect.Layer.succeed(_core.AppendIsOwned, {
165
+ by: 'transact'
166
+ }))));
167
+ const condition = (0, _readLedger.deriveLayeredCondition)(yield* ledger.hops);
168
+ if (condition === undefined) {
169
+ return yield* new _readLedger.NoReadsRecorded();
170
+ }
171
+ return yield* store.append(events, condition);
172
+ });
173
+ exports.transact = transact;
174
+ //# sourceMappingURL=precondition.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"precondition.js","names":["_core","require","_effect","_decisionModel","_readLedger","decideOnly","decider","command","Effect","gen","ReadLedger","state","modelAtHead","projections","decide","exports","unsafeAppend","events","reason","flatMap","DcbEventStore","store","mapInputContext","append","context","Context","omit","AppendIsOwned","transact","precondition","body","ledger","makeReadLedger","proof","evaluate","pipe","provideService","tag","provide","Layer","merge","layer","succeed","by","condition","deriveLayeredCondition","hops","undefined","NoReadsRecorded"],"sources":["../../src/precondition.ts"],"sourcesContent":[null],"mappings":";;;;;;AAyFA,IAAAA,KAAA,GAAAC,OAAA;AAEA,IAAAC,OAAA,GAAAD,OAAA;AAEA,IAAAE,cAAA,GAAAF,OAAA;AAGA,IAAAG,WAAA,GAAAH,OAAA;AAsCA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDO,MAAMI,UAAU,GAAGA,CAMxBC,OAAkC,EAClCC,OAAgB,KAMhBC,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,OAAOC,sBAAU;EAEjB,MAAM;IAAEC;EAAK,CAAE,GAAG,OAAO,IAAAC,0BAAW,EAACN,OAAO,CAACO,WAAW,CAACN,OAAO,CAAC,CAAC;EAClE,OAAO,OAAOD,OAAO,CAACQ,MAAM,CAACH,KAAK,EAAEJ,OAAO,CAAC;AAC9C,CAAC,CAAC;AAEJ;;;;;;;;;;;;;;;;;;;;;;AAAAQ,OAAA,CAAAV,UAAA,GAAAA,UAAA;AAsBO,MAAMW,YAAY,GAAGA,CAC1BC,MAA2C;AAC3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAC,MAAc,KAEdV,cAAM,CAACW,OAAO,CAACC,mBAAa,EAAGC,KAAK;AAClC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACAb,cAAM,CAACc,eAAe,CACpBD,KAAK,CAACE,MAAM,CAACN,MAAM,CAAC,EACnBO,OAA+B,IAAKC,eAAO,CAACC,IAAI,CAACC,mBAAa,CAAC,CAACH,OAAO,CAAC,CAC1E,CACF;AAEH;;;;;;;;;;;;;;;;;AAAAT,OAAA,CAAAC,YAAA,GAAAA,YAAA;AAiBO,MAAMY,QAAQ,GACGC,YAA+C,IAEnEC,IAA8D,IAQ9DtB,cAAM,CAACC,GAAG,CAAC,aAAS;EAClB,MAAMY,KAAK,GAAG,OAAOD,mBAAa;EAClC,MAAMW,MAAM,GAAG,OAAOC,0BAAc;EAEpC,MAAMf,MAAM,GAAG,OAAOT,cAAM,CAACC,GAAG,CAAC,aAAS;IACxC;IACA;IACA;IACA,MAAMwB,KAAK,GAAG,OAAOJ,YAAY,CAACK,QAAQ;IAC1C,OAAO,OAAOJ,IAAI,CAACK,IAAI,CAAC3B,cAAM,CAAC4B,cAAc,CAACP,YAAY,CAACQ,GAAG,EAAEJ,KAAK,CAAC,CAAC;EACzE,CAAC,CAAC,CAACE,IAAI;EACL;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA3B,cAAM,CAAC8B,OAAO,CACZC,aAAK,CAACC,KAAK,CACTT,MAAM,CAACU,KAAK,EACZF,aAAK,CAACG,OAAO,CAACf,mBAAa,EAAE;IAAEgB,EAAE,EAAE;EAAU,CAAE,CAAC,CACjD,CACF,CACF;EAED,MAAMC,SAAS,GAAG,IAAAC,kCAAsB,EAAC,OAAOd,MAAM,CAACe,IAAI,CAAC;EAC5D,IAAIF,SAAS,KAAKG,SAAS,EAAE;IAC3B,OAAO,OAAO,IAAIC,2BAAe,EAAE;EACrC;EAEA,OAAO,OAAO3B,KAAK,CAACE,MAAM,CAACN,MAAM,EAAE2B,SAAS,CAAC;AAC/C,CAAC,CAAC;AAAA7B,OAAA,CAAAa,QAAA,GAAAA,QAAA","ignoreList":[]}