@cratis/pi 7.56.3 → 7.57.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 (22) hide show
  1. package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +2 -2
  2. package/package/corpus/product-sync.json +7 -5
  3. package/package/corpus/rules/elixir.md +2 -2
  4. package/package/corpus/skills/cratis-chronicle-client-elixir/SKILL.md +64 -87
  5. package/package/corpus/skills/cratis-chronicle-client-elixir/references/append-api.md +10 -0
  6. package/package/corpus/skills/cratis-chronicle-client-elixir/references/captures.md +43 -0
  7. package/package/corpus/skills/cratis-chronicle-client-elixir/references/client-options.md +6 -0
  8. package/package/corpus/skills/cratis-chronicle-client-elixir/references/connections.md +1 -1
  9. package/package/corpus/skills/cratis-chronicle-client-elixir/references/constraints.md +63 -0
  10. package/package/corpus/skills/cratis-chronicle-client-elixir/references/decision-reads.md +65 -0
  11. package/package/corpus/skills/cratis-chronicle-client-elixir/references/discovery-and-generations.md +56 -0
  12. package/package/corpus/skills/cratis-chronicle-client-elixir/references/event-sources.md +1 -1
  13. package/package/corpus/skills/cratis-chronicle-client-elixir/references/idioms-and-inspection.md +2 -2
  14. package/package/corpus/skills/cratis-chronicle-client-elixir/references/observability.md +37 -0
  15. package/package/corpus/skills/cratis-chronicle-client-elixir/references/patterns.md +22 -0
  16. package/package/corpus/skills/cratis-chronicle-client-elixir/references/pitfalls.md +9 -1
  17. package/package/corpus/skills/cratis-chronicle-client-elixir/references/projections.md +28 -0
  18. package/package/corpus/skills/cratis-chronicle-client-elixir/references/read-models.md +41 -0
  19. package/package/corpus/skills/cratis-chronicle-client-elixir/references/testing.md +63 -0
  20. package/package/corpus/skills/cratis-chronicle-client-elixir/references/upgrading.md +18 -0
  21. package/package/corpus/skills/cratis-chronicle-client-elixir/verification.json +17 -4
  22. package/package.json +1 -1
@@ -12,13 +12,13 @@ paths:
12
12
  # Elixir Conventions
13
13
 
14
14
  Use Elixir and OTP idioms, not translated C# or JVM shapes. These conventions
15
- are grounded in `Cratis/Chronicle.Elixir v3.10.2` (`Source/chronicle/mix.exs`,
15
+ are grounded in `Cratis/Chronicle.Elixir v3.13.0` (`Source/chronicle/mix.exs`,
16
16
  `.formatter.exs`, and `lib/chronicle/`). Client-specific APIs belong in the
17
17
  **cratis-chronicle-client-elixir** skill.
18
18
 
19
19
  ## Build and format
20
20
 
21
- - For Chronicle 3.10.x use Elixir 1.18 or later (`~> 1.18`).
21
+ - For Chronicle 3.13.x use Elixir 1.18 or later (`~> 1.18`).
22
22
  - Run `mix compile --warnings-as-errors`, `mix test`, and
23
23
  `mix format --check-formatted` with the project's formatter inputs.
24
24
  - Use two-space indentation and let `mix format` own layout.
@@ -579,17 +579,19 @@
579
579
  }
580
580
  ],
581
581
  "synced": {
582
- "version": "3.10.2",
583
- "date": "2026-10-12",
584
- "by": "Cratis/AI#620"
582
+ "version": "3.13.0",
583
+ "date": "2026-10-15",
584
+ "by": "Cratis/AI#719"
585
585
  },
586
586
  "covers": {
587
587
  "skills": [
588
588
  "cratis-chronicle-client-elixir"
589
589
  ],
590
- "rules": []
590
+ "rules": [
591
+ "elixir.md"
592
+ ]
591
593
  },
592
- "notes": "Initial historical floor recorded on 2026-10-12 by Cratis/AI#619; date records this inventory, not a new product verification."
594
+ "notes": "Synced to the v3.13.0 tag: decision reads, Chronicle.Testing, captures, discovery drift, standalone constraints, children collections, concurrency scopes and observability."
593
595
  },
594
596
  {
595
597
  "id": "authproxy",
@@ -11,13 +11,13 @@ paths:
11
11
  # Elixir Conventions
12
12
 
13
13
  Use Elixir and OTP idioms, not translated C# or JVM shapes. These conventions
14
- are grounded in `Cratis/Chronicle.Elixir v3.10.2` (`Source/chronicle/mix.exs`,
14
+ are grounded in `Cratis/Chronicle.Elixir v3.13.0` (`Source/chronicle/mix.exs`,
15
15
  `.formatter.exs`, and `lib/chronicle/`). Client-specific APIs belong in the
16
16
  **cratis-chronicle-client-elixir** skill.
17
17
 
18
18
  ## Build and format
19
19
 
20
- - For Chronicle 3.10.x use Elixir 1.18 or later (`~> 1.18`).
20
+ - For Chronicle 3.13.x use Elixir 1.18 or later (`~> 1.18`).
21
21
  - Run `mix compile --warnings-as-errors`, `mix test`, and
22
22
  `mix format --check-formatted` with the project's formatter inputs.
23
23
  - Use two-space indentation and let `mix format` own layout.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cratis-chronicle-client-elixir
3
- description: Talk to a Chronicle server from an Elixir application with the cratis_chronicle Hex package - Chronicle.Client supervision and connection lifecycle, EventType structs, append errors, event sources and streams, Chronicle.Concept, PII and encrypted secrets, reactors, read models/projections with composite keys, variants and GlobalHandler, and observer inspection. Use when an OTP application appends to or observes a Chronicle event store. Do not use for the .NET, TypeScript, or Kotlin clients.
3
+ description: Talk to a Chronicle server from an Elixir application with the cratis_chronicle Hex package - Chronicle.Client supervision and connection lifecycle, EventType structs, append errors, event sources and streams, Chronicle.Concept, PII and encrypted secrets, reactors, read models/projections with composite keys, children, variants and GlobalHandler, decision reads, in-memory testing, captures, constraints, discovery drift and observer inspection. Use when an OTP application appends to or observes a Chronicle event store. Do not use for the .NET, TypeScript, or Kotlin clients.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -17,23 +17,11 @@ to yours, and everything after that is ordinary OTP.
17
17
 
18
18
  Source: `Cratis/Chronicle.Elixir` (Hex package `cratis_chronicle`). Source paths below are relative to `Source/chronicle/`.
19
19
 
20
- Read [references/upgrading.md](references/upgrading.md) when moving an application from an older client; wire compatibility and corrected behavior both matter.
20
+ Read [references/upgrading.md](references/upgrading.md) when moving an application from an older client. Requires Elixir `~> 1.18`; read [references/product-sources.md](references/product-sources.md) for the package and contracts dependency facts.
21
21
 
22
- Requires Elixir `~> 1.18`. Read [references/product-sources.md](references/product-sources.md) for the package and contracts dependency facts.
22
+ > **A git tag is not a Hex release.** `mix deps.get` fetches only published versions, and the repository's `VERSION` file is a build-time placeholder: never quote it. Use namespaced modules (`Chronicle.Events.EventType`, not `Chronicle.EventType`); older READMEs got this wrong. Maintained snippets live in `Documentation/client-snippets/`.
23
23
 
24
- > **A git tag is not a Hex release.** `mix deps.get` fetches only published versions. The `Chronicle.Concept` macro
25
- > (`use Chronicle.Concept, type: :string`, a single-value concept that carries its
26
- > `pii` classification once, mirroring `ConceptAs<T>`) is the preferred way to
27
- > classify a personal value; a client without it annotates fields with `pii/1,2`.
28
-
29
- Requires `cratis_chronicle` 3.0.0 for `Chronicle.Concept`.
30
-
31
- > **The repository's `VERSION` file is a build-time placeholder** overwritten by the publish workflow. Never quote it.
32
-
33
- > Use namespaced modules (`Chronicle.Events.EventType`, not `Chronicle.EventType`); older READMEs got this wrong. Maintained
34
- > snippets live in `Documentation/client-snippets/`.
35
-
36
- Select the published version from hex.pm rather than treating a guide's range as a pin.
24
+ `Chronicle.Concept` (`use Chronicle.Concept, type: :string`) is the preferred way to classify a personal value once, mirroring `ConceptAs<T>`; it requires `cratis_chronicle` 3.0.0, and an older client annotates fields with `pii/1,2`.
37
25
 
38
26
  ## Adding it
39
27
 
@@ -41,7 +29,7 @@ Select the published version from hex.pm rather than treating a guide's range as
41
29
  # mix.exs
42
30
  defp deps do
43
31
  [
44
- {:cratis_chronicle, "~> 3.10"}
32
+ {:cratis_chronicle, "~> 3.13"}
45
33
  ]
46
34
  end
47
35
  ```
@@ -104,17 +92,14 @@ case Lifecycle.wait_until(Lifecycle.name_for(Chronicle.Client), :registered, 30_
104
92
  end
105
93
  ```
106
94
 
107
- `wait_until(lifecycle, target_phase, timeout \\ 30_000)` is the blocking wait.
108
- `subscribe/1` returns `{:ok, phase, connection_id}` **and** sends it as a message,
109
- which closes the subscribe-after-transition race; `phase/1` is the plain
110
- read.
95
+ `wait_until(lifecycle, target_phase, timeout \\ 30_000)` is the blocking wait;
96
+ `subscribe/1` returns `{:ok, phase, connection_id}` **and** sends it as a message
97
+ (closing the subscribe-after-transition race); `phase/1` is the plain read.
111
98
 
112
- Reconnect never gives up: exponential backoff starts at 1000 ms and caps at
113
- 10000 ms, re-resolving addresses each time. `:retry_attempts` is ignored.
114
- Registration failures leave the client in `:connected`, log
99
+ Reconnect never gives up (backoff 1000 to 10000 ms; `:retry_attempts` is
100
+ ignored). Registration failures leave the client in `:connected`, log
115
101
  `Chronicle registration failed`, and retry every five seconds. Appends before a
116
- channel exists return `{:error, :not_connected}`; read model queries wait up to
117
- 30 seconds for `:registered`.
102
+ channel exists return `{:error, :not_connected}`.
118
103
 
119
104
  ## Authentication and TLS
120
105
 
@@ -154,24 +139,23 @@ defaults: `nil` registers as string, numbers as number, booleans as boolean;
154
139
  list element schemas use the first default element. Nested structs and concepts
155
140
  are descended into (`schemas/json_schema_generator.ex`).
156
141
 
157
- The macro imports constraint macros into your module:
158
- `unique/1`, `unique/2`, `unique_event_type/0`, `unique_event_type/1`,
159
- `remove_constraint/1`, plus `Chronicle.Compliance.pii/1,2` and
160
- `Chronicle.Confidentiality.encrypted/1,2,3`. The
161
- equivalent accumulating module attributes `@unique`, `@unique_event_type`,
162
- `@remove_constraint` also work.
142
+ The macro imports `unique/1,2`, `unique_event_type/0,1`, `remove_constraint/1,2`,
143
+ `closes_stream/1`, `Chronicle.Compliance.pii/1,2` and
144
+ `Chronicle.Confidentiality.encrypted/1,2,3`; the accumulating attributes
145
+ `@unique`, `@unique_event_type`, `@remove_constraint` also work. Read
146
+ [references/constraints.md](references/constraints.md) for their options.
163
147
 
164
148
  **Only top-level event keys are camelCased.** `encode_event/1` transforms them
165
149
  before `Jason.encode!` (`event_sequences/event_log.ex`); nested keys are
166
150
  not transformed. Event structs get no `Jason.Encoder` implementation;
167
- `Chronicle.Concept` does, encoding its wrapped value rather than a wrapper.
151
+ `Chronicle.Concept` does, encoding its wrapped value.
168
152
 
169
153
  Read [references/compliance.md](references/compliance.md) when defining personal data, data subjects, concepts or encrypted operational secrets. `pii` and `encrypted` are not interchangeable and cannot mark the same leaf.
170
154
 
171
155
  Schema evolution is
172
156
  `use Chronicle.Events.Migration, from: {Mod, generation: n}, to: {Mod, generation: n + 1}`
173
- with `upcast/1` and `downcast/1`; the generations must be exactly one apart or it
174
- raises at compile time.
157
+ with `upcast/1` and `downcast/1`; the generations must be exactly one apart. Mark
158
+ the older module with `generation_for:`, not a second `:id`.
175
159
 
176
160
  ## Appending
177
161
 
@@ -183,40 +167,30 @@ case Chronicle.append("account-1", %MyApp.Events.FundsDeposited{amount: 50}) do
183
167
  end
184
168
  ```
185
169
 
186
- `Chronicle.append/3` delegates to `Chronicle.EventSequences.EventLog`
187
- (`lib/chronicle.ex`):
188
-
189
- ```elixir
190
- @spec append(String.t(), struct(), keyword()) :: :ok | {:error, term()}
191
- ```
192
-
170
+ `Chronicle.append/3` delegates to `Chronicle.EventSequences.EventLog`.
193
171
  **The event source id comes first, the event second.** The return is a bare
194
- `:ok`, not a result struct — this client's `append/3` deliberately carries no
195
- sequence number, and `append_and_wait_for_completion/3` exists for when you need
196
- the outcome.
172
+ `:ok`, carrying no sequence number; use `append_with_result/3` or
173
+ `append_and_wait_for_completion/3` when you need the outcome.
197
174
 
198
175
  Read [references/append-api.md](references/append-api.md) when you need an append variant, a read-back function, or an append option such as `:event_stream_type`, `:concurrency_scope` or `:tags`.
199
176
 
200
177
  Requires `cratis_chronicle` 3.2.0 for rejected appends to return `{:error, reason}`; an older client could report a rejection as `:ok`.
201
178
 
202
- Failed appends return `{:error, reason}`. Reasons include `{:constraint_violations, violations}`,
179
+ Failed appends return `{:error, reason}`: `{:constraint_violations, violations}`,
203
180
  `{:concurrency_violations, violations}`, `{:append_errors, errors}`,
204
- `:not_connected`, and `{:incompatible_server, response}` from the first append's
205
- compatibility preflight per channel. Pair the kernel and installed contracts;
206
- do not blindly retry a timed-out write.
207
-
208
- **Match on them.** A bare `:ok = Chronicle.append(...)` raises a `MatchError` on
209
- a constraint violation, which is a fine choice in a script and the wrong one in a
210
- GenServer.
181
+ `{:unknown_event_type, module}`, `:not_connected`, and `{:incompatible_server, response}`
182
+ from the first append's compatibility preflight per channel. Pair the kernel and
183
+ installed contracts; do not blindly retry a timed-out write. **Match on them**: a
184
+ bare `:ok = Chronicle.append(...)` raises a `MatchError` on a violation, fine in a
185
+ script and wrong in a GenServer.
211
186
 
212
187
  Read [references/event-sources.md](references/event-sources.md) when declaring named event sources/streams, deriving concurrency scopes or routing mixed-source batches; definitions have client and kernel requirements stated there.
213
188
 
214
- A unit of work is a process: `Chronicle.begin_unit_of_work/1` starts an `Agent`,
215
- and while one is current for the calling process `append`/`append_many` buffer
216
- instead of sending, until `UnitOfWork.commit/1` or `rollback/1`.
217
- `commit/1` returns `:ok | {:error, reason}`. `Chronicle.with_unit_of_work(fun, opts)`
218
- passes the unit to an arity-one function, commits on return, and raises on commit
219
- failure. Source/stream options apply to buffered appends too.
189
+ A unit of work is a process: `Chronicle.begin_unit_of_work/1` starts an `Agent`;
190
+ while one is current for the calling process, `append`/`append_many` buffer until
191
+ `UnitOfWork.commit/1` (`:ok | {:error, reason}`) or `rollback/1`.
192
+ `Chronicle.with_unit_of_work(fun, opts)` commits on return and raises on commit
193
+ failure.
220
194
 
221
195
  ## Observing
222
196
 
@@ -224,11 +198,8 @@ failure. Source/stream options apply to buffered appends too.
224
198
 
225
199
  Read [references/reactor-example.md](references/reactor-example.md) when you need a worked reactor module.
226
200
 
227
- This follows `Documentation/client-snippets/get-started/common/reactor.md`.
228
-
229
201
  - **`@handles` is an accumulating module attribute** declaring the subscription;
230
- dispatch is then ordinary Elixir pattern matching in `handle/2`. This is the
231
- Elixir answer to the other clients' method-name or parameter-type conventions.
202
+ dispatch is then ordinary Elixir pattern matching in `handle/2`.
232
203
  - The callback is
233
204
  `handle(event :: struct(), context :: map()) :: :ok | {:error, term()} | {:ok, struct() | [struct()]}`.
234
205
  - **Returning `{:ok, event_or_events}` appends those events as a side effect** —
@@ -239,28 +210,27 @@ This follows `Documentation/client-snippets/get-started/common/reactor.md`.
239
210
  - Optional replay callbacks: `on_replay_begin/0`, `on_replay_end/0`,
240
211
  `on_partition_replay_begin/1`, `on_partition_replay_end/1`.
241
212
 
242
- **The reactor context has six keys**: `event_source_id`, `sequence_number`,
243
- `occurred`, `event_source`, `event_store`, `namespace`
244
- (`lib/chronicle/reactors/handler.ex`). `event_source` is the registered
245
- source name or `nil` for legacy appends. The moduledoc mentions `:correlation_id`,
246
- but **it is not in the map**. Reducer context differs: `event_source_id`,
247
- `sequence_number`, `occurred`, `event_source`, `observation_state`
248
- (`reducers/handler.ex`).
213
+ **The context is a `Chronicle.Events.EventContext` struct** (requires 3.11.0; a
214
+ bare map before). Pattern match on keys or use `context.caused_by`. It carries
215
+ `event_source_id`, `sequence_number`, `occurred`, `event_source` (the registered
216
+ source name or `nil`), `event_store`, `namespace`, `correlation_id`, `caused_by`,
217
+ `causation`, `tags`, `subject`, `appended_generation`, `replaying?` and, for
218
+ reactors only, `delivery_id` (stable across replay and failed-partition
219
+ recovery; key a receipt on it). Reducers get the same struct with
220
+ `observation_state` set (`lib/chronicle/events/event_context.ex`).
249
221
 
250
222
  ### Read models and projections
251
223
 
252
- Read [references/reactor-example.md](references/reactor-example.md#read-model-example) for a worked model-bound read model.
253
-
254
- This is the model-bound shape; use typed defaults for its registered schema.
224
+ Read [references/reactor-example.md](references/reactor-example.md#read-model-example) for a worked model-bound read model with typed defaults.
255
225
 
256
- The DSL macros are `from/1`, `from/2`, `join/2`, `removed_with/2`, `from_every/1`.
226
+ The DSL macros are `from/1,2`, `join/2`, `removed_with/2`, `from_every/1`, `children/2,3`.
257
227
  `from/2` options: `:key` (defaults to `"$eventSourceId"`), `:parent_key`, `:set`,
258
228
  `:add`, `:subtract`, `:count`. `join/2` requires `:on`.
259
229
 
260
- Read [references/projections.md](references/projections.md) when choosing `passive`, `not_rewindable`, `event_sequence`, `no_auto_map`, composite/event-context keys or dictionary counters. Snake_case field atoms now map correctly; no camelCase-string workaround is needed.
230
+ Read [references/projections.md](references/projections.md) when choosing `passive`, `not_rewindable`, `event_sequence`, `no_auto_map`, composite/event-context keys, dictionary counters or `children/2,3` collections. Snake_case field atoms now map correctly; no camelCase-string workaround is needed.
261
231
 
262
232
  > **The read model id sent to the kernel is the last module segment** —
263
- > `Module.split() |> List.last()` at `lib/chronicle/read_models/read_model.ex`.
233
+ > `Module.split() |> List.last()` at `read_models/read_model.ex`.
264
234
  > `MyApp.ReadModels.Account` registers as `"Account"`, so two read models with the
265
235
  > same final segment collide. Pass `id:` to disambiguate.
266
236
 
@@ -283,10 +253,8 @@ Querying (`lib/chronicle/read_models.ex`):
283
253
  {:ok, account} = Chronicle.read_model(MyApp.ReadModels.AccountBalance, "account-1")
284
254
  ```
285
255
 
286
- `Chronicle.read_model/3` delegates to `ReadModels.get/3` (`lib/chronicle.ex`,
287
- `read_models.ex`), returning `{:ok, struct() | nil} | {:error, term()}`.
288
- `Chronicle.all/2` delegates to `ReadModels.all/2`
289
- (`lib/chronicle.ex`, `read_models.ex`).
256
+ `Chronicle.read_model/3` returns `{:ok, struct() | nil} | {:error, term()}`; `nil`
257
+ means absent **or not projected yet**.
290
258
 
291
259
  Use `Chronicle.ReadModels.query(MyApp.ReadModels.AccountBalance, page: 0)` for paging:
292
260
  `:page` is zero-based (default `0`), `:page_size` defaults to `50`, and
@@ -294,18 +262,26 @@ Use `Chronicle.ReadModels.query(MyApp.ReadModels.AccountBalance, page: 0)` for p
294
262
  `occurrences/2`, and `definitions/1` expose history, replay containers, and
295
263
  registered definitions.
296
264
 
297
- **Live updates arrive as messages, not as a stream.** `ReadModels.watch/2`
298
- (`read_models.ex`) sends
299
- `{:chronicle_read_model_changed, module, %Changeset{}}` and
300
- `{:chronicle_read_model_watch_error, module, reason}` to the calling process. The receiving process needs a `handle_info` for both.
265
+ **Live updates arrive as messages, not as a stream.** `ReadModels.watch/2` and `subscribe/2` send `{:chronicle_read_model_changed, module, %Changeset{}}` and error messages to the calling process, which needs a `handle_info` for each. Read [references/read-models.md](references/read-models.md) when subscribing, filtering, observing collections, previewing or waiting for a projection.
266
+
267
+ ## Guarding decisions, testing and more
268
+
269
+ Read the reference that matches the task:
270
+
271
+ - [references/decision-reads.md](references/decision-reads.md) when an append depends on state a concurrent append could change: `Chronicle.decide/2`, `DecisionReads`, `ConcurrencyScope` builders, the optimistic default.
272
+ - [references/testing.md](references/testing.md) when writing specs: `Chronicle.Testing.Case`, assertions, the in-memory kernel's limits.
273
+ - [references/constraints.md](references/constraints.md) when declaring uniqueness, standalone constraint modules, `remove_constraint/2`, violation details or closing events.
274
+ - [references/discovery-and-generations.md](references/discovery-and-generations.md) when pinning ids, setting `:drift_policy`, running `mix chronicle.discover`, or handling older event generations.
275
+ - [references/captures.md](references/captures.md) when turning external API, webhook or other-store data into events.
276
+ - [references/observability.md](references/observability.md) and [references/patterns.md](references/patterns.md) for OpenTelemetry/`:telemetry` and `Chronicle.Patterns`.
301
277
 
302
278
  ## Idioms and operational inspection
303
279
 
304
- Read [references/idioms-and-inspection.md](references/idioms-and-inspection.md) when carrying correlation or identity across a `Task`, formatting the DSL, supervising `ReadModels.watch/2`, or removing observers (obtain explicit authority first).
280
+ Read [references/idioms-and-inspection.md](references/idioms-and-inspection.md) when carrying correlation or identity across a `Task`, formatting the DSL, supervising watches, or removing observers (obtain explicit authority first).
305
281
 
306
282
  ## Common pitfalls
307
283
 
308
- Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting discovery, defaults, process context, variant registration or older-version examples.
284
+ Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting discovery, defaults, process context, variants or older-version examples.
309
285
 
310
286
  ## Verify
311
287
 
@@ -319,10 +295,11 @@ Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting disco
319
295
  - Every event type module passes an explicit, stable `id:`.
320
296
  - Every `Chronicle.append/3` call site handles `{:error, _}` as well as `:ok`.
321
297
  - Every reactor's `@handles` list matches the clauses of its `handle/2`.
298
+ - Reactors, reducers and projections that must survive a rename pin their `:id`.
322
299
  - Read model module names have distinct final segments, or explicit ids.
323
300
  - Every variant group has at least one `enters_on` per variant, and every
324
301
  `GlobalHandler` member exists on every variant it targets.
325
302
  - Production logging filters credential-bearing gRPC/Mint crash reports; see
326
303
  the connections reference for limits.
327
304
  - Examples match the APIs and schema defaults at the installed tag.
328
- - `mix compile --warnings-as-errors` and `mix test` are clean.
305
+ - `mix compile --warnings-as-errors` and `mix test` are clean; `mix chronicle.discover --otp-app my_app` lists every expected artifact.
@@ -38,3 +38,13 @@ Read [event-sources.md](event-sources.md) for definitions, stream routing and de
38
38
  `append_response.ex` returns `{:error, {:constraint_violations, violations}}`, `{:error, {:concurrency_violations, violations}}` or `{:error, {:append_errors, errors}}` on rejection. Channel/preflight errors include `{:error, :not_connected}` and `{:error, {:incompatible_server, response}}`. A transport timeout does not prove that no event was stored.
39
39
 
40
40
  `append_and_wait_for_completion/3` has **no many-event variant**. `{:ok, result}` does not imply every observer succeeded: inspect `result.success` and `failed_partitions`. Its wait happens **after** the append (`event_log.ex`), so `{:error, reason}` can follow a stored event if waiting fails. Do not retry the write blindly; reconcile the stored outcome first.
41
+
42
+ ## Structured results, prepared events and unknown types
43
+
44
+ Requires `cratis_chronicle` 3.13.0 for everything in this section.
45
+
46
+ - `append_with_result/3` and `append_many_with_result/3` return `{:ok, %AppendResult{sequence_number: _}}` (batch: every assigned number, in order) or `{:error, %AppendResult{}}` carrying `:constraint_violations` (each with `:constraint_name`, `:message`, `:event_type_id`, `:sequence_number`, `:details`), `:concurrency_violations` (`:event_source_id`, `:expected_sequence_number`, `:actual_sequence_number`) and `:errors`. Use them when you need the assigned sequence number or the structured reason; transport failures stay plain `{:error, reason}`. `AppendResult.wait_for_completion/2` waits for observers.
47
+ - `EventLog.prepare/2` serializes an event once into a `PreparedEvent` bound to the client, namespace and event sequence that prepared it; reuse it with `EventLog.append_prepared/3` and `Streams.verify_content/3`. Appending it through another client, namespace or sequence returns `{:error, :prepared_event_belongs_to_another_sequence}`. Single use is not enforced.
48
+ - **A struct that does not `use Chronicle.Events.EventType` returns `{:error, {:unknown_event_type, module}}`** from every append path, `prepare/2` and `Streams.verify_content/3`; it does not raise. Match it like any other error.
49
+ - A payload over 4 MiB returns `{:error, {:payload_too_large, size, limit}}` before sending. Raise or disable the guard with `config :cratis_chronicle, max_payload_bytes: ...` (or `:infinity`) only when the kernel is configured for larger messages.
50
+ - Reads: `get_for_event_source/3` and `get_from_sequence_number/2` return raw wire events; pass `decode: true` for `Chronicle.Events.AppendedEvent` structs (`event: nil` and `:raw` for an unknown type, `redacted?: true` for a redacted event).
@@ -0,0 +1,43 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Captures
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/captures.ex`, `captures/capture.ex`, `captures/registrar.ex`, and `Documentation/captures.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0.
9
+
10
+ A capture turns data that arrives from outside the event log (an external API, an inbound webhook, another event store's public events) into events appended to this event store. The DSL compiles at build time to Capture Declaration Language (CDL), the text the kernel's `SaveCapture` accepts. Use a capture instead of a hand-written poller when the source data should become facts you own.
11
+
12
+ ```elixir
13
+ # Copyright (c) Cratis. All rights reserved.
14
+ # Licensed under the MIT license. See LICENSE file in the project root for full license information.
15
+
16
+ defmodule MyApp.Captures.InvoiceCapture do
17
+ use Chronicle.Captures.Capture, name: "InvoiceCapture"
18
+
19
+ api_source("InvoicingApi", route: "/invoices", poll: "10m")
20
+
21
+ key("id")
22
+
23
+ append MyApp.Events.InvoiceStatusChanged, when: "status" do
24
+ set(:status, "$.status")
25
+ end
26
+ end
27
+ ```
28
+
29
+ - Exactly one source: `api_source/1,2` (polls a named external service), `webhook_source/1`, or `events_source/1` (a list of registered event type modules from another event store's public events).
30
+ - `key/1` names the property path that identifies and diffs an item; use `"$eventSourceId"` for an events source.
31
+ - `:id` defaults to a UUIDv5 of the module name, so redeploying updates the same kernel-side capture. `:name` defaults to the last module segment. `start: true` makes the registrar start it once saved.
32
+ - `when:` takes a property name, `{:any, [...]}`, `{:all, [...]}`, `{:transition, property, from, to}`, `:added`, `:removed`, or `{:expression, "template"}`. `set/2` maps a target field from `"$.path"` (payload) or `"$context.<property>"`.
33
+ - The DSL also has `nested` and `children` scopes, and a `map do` block with `rename`, `template`, `translate` and `split`.
34
+
35
+ **An appended event type's id must start with an uppercase letter and contain only letters, digits and underscores.** The kernel's CDL parser rejects hyphens, so `use Chronicle.Events.EventType, id: "InvoiceStatusChanged"` works as an `append` target and `id: "invoice-status-changed"` raises `ArgumentError` at compile time. This restriction does not apply to `events_source/1`.
36
+
37
+ **The kernel's capture engine runs a subset of the language**: only `api` and `events` sources, and only root-level `append` rules. `map`, `nested` and `children` compile and validate but are not applied at runtime, and webhook sources cannot carry authentication.
38
+
39
+ ## Registering and managing
40
+
41
+ Captures are discovered with `otp_app:` or listed in the client's `:captures` option. `Chronicle.Captures.Registrar` saves them at `:registered` and again on every reconnect. It skips a capture whose declaration already matches, and warns before saving over a **started** capture whose declaration changed, because saving stops it on the kernel.
42
+
43
+ Manage directly with `Chronicle.Captures.register/1`, `start/1`, `stop/1`, `delete/1`, `list/0`, `validate/1` (a module or raw CDL text) and `captured_events/1`. `start`, `stop` and `delete` also accept a raw capture id; `delete` is destructive, so obtain authority for the specific capture first.
@@ -16,6 +16,12 @@ your app must read its own configuration and pass it in (`lib/chronicle/client.e
16
16
  | `:discover` | `true` |
17
17
  | `:otp_app` | none; falls back to scanning loaded modules |
18
18
  | `:event_types`, `:event_sources`, `:migrations`, `:reactors`, `:reducers`, `:read_models`, `:projections`, `:seeders`, `:webhooks`, `:event_store_subscriptions`, `:global_handlers` | `[]`, merged with discovered |
19
+ | `:constraints`, `:captures`, `:read_model_reactors` | `[]`, merged with discovered where discoverable |
20
+ | `:drift_policy` | `:warn`; `:ignore` or `:fail`. See [discovery-and-generations.md](discovery-and-generations.md) |
21
+ | `:strict_validation` | `false`; `true` raises on event type and migration mistakes instead of logging |
22
+ | `:warn_defaulted_ids` | `true` |
23
+ | `:event_generation_delivery` | `:compatibility`; `:pinned` delivers events at their appended generation |
24
+ | `:concurrency` | none; `[default: :optimistic, check_first_append: bool]`. See [decision-reads.md](decision-reads.md) |
19
25
  | `:default_sink_type_id` | `:mongodb`; accepts `:mongodb`, `:sql`, `:in_memory`, `:none` or a raw string |
20
26
  | `:server_address` | alternative `"host:port"` address (connection option) |
21
27
  | `:skip_tls_validation`, `:load_balancer`, `:grpc_options`, `:reconnect_base_delay`, `:reconnect_max_delay` | forwarded to the connection | — |
@@ -45,7 +45,7 @@ The client uses grpc 1.x and Mint 1.11+, owns its transport, and needs no `GRPC.
45
45
 
46
46
  ## Credential-safe logging is still necessary
47
47
 
48
- Chronicle's own process status redacts credentials, but dependency crash reports can expose gRPC headers, bearer tokens, API keys and Mint HPACK tables. Before starting the client, install the primary Logger filter from the [client crash-report guide](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/connections/crash-reports.md). It matches `Elixir.GRPC.Client`, `Elixir.GRPC.Channel`, and `Elixir.Mint.` report content and replaces reports with a notice.
48
+ Chronicle's own process status redacts credentials, but dependency crash reports can expose gRPC headers, bearer tokens, API keys and Mint HPACK tables. The client itself installs a primary Logger filter (`Chronicle.Connections.ResponseCrashRedaction`) when a connection starts, replacing the Mint response-process termination report, the one shape it can intercept generically, with a one-line notice (requires `cratis_chronicle` 3.13.0). The `ConnectionProcess` and `GRPC.Client.Connection` state can still print credentials, so before starting the client, install the primary Logger filter from the [client crash-report guide](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/connections/crash-reports.md). It matches `Elixir.GRPC.Client`, `Elixir.GRPC.Channel`, and `Elixir.Mint.` report content and replaces reports with a notice.
49
49
 
50
50
  Keep `handle_sasl_reports: false`. This is a safety net, not a guarantee: plain warning strings carrying exit reasons are not covered, and filters do not survive a Logger restart. Restrict production log access and rotate any credential already exposed. Never log connection strings.
51
51
 
@@ -0,0 +1,63 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Constraints
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/events/event_type.ex`, `events/constraint.ex`, `events/constraint_violation.ex`, and `Documentation/constraints.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0 for standalone constraint modules, `remove_constraint/2`, `:scope` and `ConstraintViolation.details/1`.
9
+
10
+ A constraint is checked by the kernel at append time; a violation returns `{:error, {:constraint_violations, violations}}` (or `{:error, %AppendResult{}}` from `append_with_result/3`).
11
+
12
+ ## On the event type
13
+
14
+ `unique/2`, `unique_event_type/1`, `remove_constraint/1,2` and `closes_stream/1` are imported by `use Chronicle.Events.EventType`.
15
+
16
+ | Option | Meaning |
17
+ | --- | --- |
18
+ | `:name` | Constraint name; defaults to the first field (`unique`) or the event type id (`unique_event_type`) |
19
+ | `:mode` | `:per_event_source` (default) keeps one value per event source and frees the old one when replaced; `:per_value` keeps every value until a removal event releases it |
20
+ | `:ignore_casing` | Case-insensitive comparison (`unique` only) |
21
+ | `:message` | A string or a `{Module, :function}` callback invoked with the violation |
22
+ | `:scope` | `:per_event_source_type`, `:per_event_stream_type`, `:per_event_stream_id`, or a list; narrows uniqueness to values seen along those dimensions. Unscoped is global |
23
+ | `:event_sequences` | Restrict to specific event sequences |
24
+
25
+ An unknown option or invalid `:scope` compiles with a warning naming the module and is ignored; an invalid `:mode` raises `ArgumentError` when the definition is built.
26
+
27
+ ## Releasing a held value
28
+
29
+ In `:per_value` mode, `remove_constraint("Name")` releases every value the removal event's event source held. `remove_constraint("Name", properties: [:version_id])` releases only the value the removal event carries, in the order of the constrained properties.
30
+
31
+ ## Standalone constraint modules
32
+
33
+ Use `use Chronicle.Events.Constraint` when one name spans several event types or lives apart from any one event type's module.
34
+
35
+ ```elixir
36
+ # Copyright (c) Cratis. All rights reserved.
37
+ # Licensed under the MIT license. See LICENSE file in the project root for full license information.
38
+
39
+ defmodule MyApp.Constraints.UniqueEmail do
40
+ use Chronicle.Events.Constraint
41
+
42
+ unique name: "UniqueEmail" do
43
+ on MyApp.Events.UserRegistered, [:email]
44
+ on MyApp.Events.UserEmailChanged, [:new_email]
45
+ removed_with MyApp.Events.UserRemoved
46
+ end
47
+ end
48
+ ```
49
+
50
+ - `name:` is **required** on a standalone declaration.
51
+ - `removed_with/1,2` inside `unique/2` releases the value (with `properties:` in `:per_value` mode). After a bare `unique_event_type/1,2` it releases that constraint; calling it before any `unique_event_type` is a compile error.
52
+ - Two `unique_event_type` declarations with the same `:name` merge into one mutually exclusive constraint: at most one event of the named set per event source (`unique_event_type MyApp.Events.PersonErased, name: "PersonTerminal"`).
53
+ - Modules are discovered like event types, or listed in the client's `:constraints` option (and in `start_supervised_client/1` when testing).
54
+
55
+ ## Violation details
56
+
57
+ `Chronicle.Events.ConstraintViolation.details(violation)` maps the kernel's detail keys to atoms: `:property_name`, `:property_value`, `:closed_by`, `:closed_at_sequence_number`; any other key stays a string. A PII property's `:property_value` is its **encrypted** form. Use it in a `:message` callback: `"A project named '#{ConstraintViolation.details(violation)[:property_value]}' already exists."`.
58
+
59
+ ## Closing events
60
+
61
+ `closes_stream event_stream_id_from: :period, reopened_by: [MyApp.Events.PeriodReopened]` on an event type closes that stream scope when the event is appended; later events in the scope are refused. Options: `:dimensions`, `:event_stream_id_from`, `:reopened_by`, `:name`, `:event_sequences`; same-named declarations merge and must agree on dimensions. Registration closes nothing in history. Inspect closed scopes with `Chronicle.EventSequences.Streams`. The in-memory test kernel does not model `closes_stream`.
62
+
63
+ Constraint drift is not detected: the kernel cannot list registered constraints.
@@ -0,0 +1,65 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Decision reads and the dynamic consistency boundary
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/read_models/decision_reads.ex`, `decision_read_refused.ex`, `decision_conflict.ex`, `transactions/unit_of_work.ex`, and `Documentation/dynamic-consistency-boundary.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0.
9
+
10
+ Use a decision read when an append depends on state that a concurrent append could change (a seat can be booked once, a course has capacity). It folds a read model at a key in a fresh session and remembers the exact log position it stopped at; the commit is then rejected by the kernel if a matching event arrived since. An ordinary `Chronicle.read_model/3` followed by `append/3` is **not** a guard: it can silently act on stale state.
11
+
12
+ ## Decide
13
+
14
+ ```elixir
15
+ result =
16
+ Chronicle.decide(fn _unit_of_work ->
17
+ {:ok, read} = Chronicle.ReadModels.DecisionReads.get(MyApp.ReadModels.Seat, "seat-5")
18
+
19
+ if read.instance == nil do
20
+ Chronicle.EventSequences.EventLog.append("seat-5", %MyApp.Events.SeatBooked{by: "alice"})
21
+ end
22
+ end)
23
+
24
+ case result do
25
+ {:ok, _} -> :booked
26
+ {:error, %Chronicle.ReadModels.DecisionConflict{}} -> :retry
27
+ {:error, reason} -> {:error, reason}
28
+ end
29
+ ```
30
+
31
+ - `Chronicle.decide/2` runs the function in a unit of work and commits on return. It returns `{:ok, result}`, or `{:error, %DecisionConflict{}}` when an enrolled decision read conflicted with a concurrent append; nothing from that attempt was written, so re-read and decide again. Other commit failures are plain `{:error, reason}`.
32
+ - Unlike `with_unit_of_work/2`, `decide/2` never raises on a conflict. A concurrency violation without an enrolled decision read stays `{:error, {:concurrency_violations, _}}`.
33
+ - **`DecisionReads.get/2` enrolls the read only inside a unit of work** (`decide/2` or `UnitOfWork.with_unit_of_work/2`). Outside one it returns `{:error, :requires_unit_of_work}`.
34
+ - `DecisionReads.get_detached/2` folds the same way and enrolls nothing; use it for a read you do not intend to guard.
35
+ - A rule across two entities needs one decision read per event source. Enroll both inside the same `decide/2` and append one event to each; the kernel accepts or rejects the whole batch.
36
+ - `get/4` and `get_detached/4` take `(model, event_source_id, event_stream_type, event_stream_id)` to guard one named stream. The default stream (`""` or `"All"`) is refused with `:all_streams_not_supported`.
37
+
38
+ ## Admission
39
+
40
+ `DecisionReads.admit/2` and `admit_for_stream/2` check a model's shape without any I/O and return `{:ok, event_types}` or `{:error, reason}`. `get` and `get_detached` **raise** `Chronicle.ReadModels.DecisionReadRefused` for a refused shape, because that is a programming error. Refusal reasons: `:reducer` (an active reducer), `:join`, `:hierarchy` (`children/2`), `:not_event_log`, `:open_ended_event_types` (`from_every/1`), `:no_event_types`, `:not_event_source_keyed`, `:unsupported_event_type_id`, `:all_streams_not_supported`, `:stream_scope_not_supported`, `:fold_incomplete`.
41
+
42
+ A model with `join/2`, `removed_with_join/2`, `children/2` or `from_every/1` cannot be guarded. A **passive** reducer (`use Chronicle.Reducers.Reducer, model: M, active: false`) can back a decision read because it is folded fresh on the client; an active reducer cannot.
43
+
44
+ ## Explicit concurrency scopes
45
+
46
+ When a plain guard on the tail is enough, use `Chronicle.Events.ConcurrencyScope` (`:concurrency_scope` append option) instead of a read model.
47
+
48
+ | Need | Builder |
49
+ | --- | --- |
50
+ | Guard at the current tail in one call | `{:ok, scope} = ConcurrencyScope.at_tail("account-1", event_stream_type: "Transactions")`; narrowing options: `:event_stream_type`, `:event_stream_id`, `:event_source_type`, `:event_types` |
51
+ | "This can only happen once" | `ConcurrencyScope.expecting_no_matching_event(event_source_id: true, event_types: [MyApp.Events.UserRegistered])` |
52
+ | Build by hand | `12 \|> ConcurrencyScope.new() \|> with_event_source() \|> with_event_stream_type("Transactions") \|> with_event_type(Mod)`; also `with_sequence_number/2`, `with_event_stream_id/2`, `with_event_source_type/2`, `with_event_types/2` |
53
+ | No guard | `ConcurrencyScope.none()` |
54
+
55
+ **Leaving the expected sequence number unset turns the check off.** It does not mean "expect no event"; use `expecting_no_matching_event/1`, which sends the kernel's explicit condition (requires 3.11.0).
56
+
57
+ ## Default optimistic strategy
58
+
59
+ ```elixir
60
+ {Chronicle.Client, ..., concurrency: [default: :optimistic, check_first_append: false]}
61
+ ```
62
+
63
+ By default an append with no explicit or definition-derived scope sends no guard. With `default: :optimistic` the client reads the tail, narrowed the way the append is routed, and expects nothing newer. `check_first_append: true` also guards the first append into an empty narrowing with `expecting_no_matching_event/1`. An explicit or definition-derived scope always wins.
64
+
65
+ Two **unequal** explicit scopes for one event source id in one batch or unit of work fail before sending with `{:error, {:duplicate_concurrency_scope, event_source_id}}`; equal scopes collapse to one.
@@ -0,0 +1,56 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Discovery, artifact identity and event generations
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/discovery/`, `artifacts.ex`, `client.ex`, `events/event_type.ex`, `events/event_types.ex`, `reactors/reactor.ex`, `reducers/reducer.ex`, `lib/mix/tasks/chronicle.discover.ex`, and `Documentation/discovery.md`, `events.md`, `reactors.md`, `reducers.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0.
9
+
10
+ ## The discovery report
11
+
12
+ `Chronicle.Client` builds a `%Chronicle.Discovery.Report{}` at start, logs a one-line summary at `:info` and the whole report at `:debug`. Read it with `Chronicle.Client.discovery_report()` and print it with `Chronicle.Discovery.Report.format/1`. `Chronicle.Artifacts.report(:my_app)` builds the same report without starting a client; `Chronicle.Artifacts.discover/1` still returns its plain map.
13
+
14
+ Run it from the shell with `mix chronicle.discover --otp-app my_app`; add `--connection-string "chronicle://..."` to compare against a kernel (event store `"default"`, namespace `"Default"`) and `--fail-on-drift` to exit `1` on a warning-severity finding. Exit codes: `0` clean, `1` drift found, `2` could not run (no `--otp-app`, or kernel unreachable). `--fail-on-drift` has no effect without `--connection-string`.
15
+
16
+ ## Pin ids that default to a module name
17
+
18
+ A reactor, reducer or declarative projection with no `:id` defaults it from its module name (reactors and reducers: the full module name; projections: the last segment). **Renaming or moving the module then creates a different artifact and silently resets its observation position.** Pin it:
19
+
20
+ ```elixir
21
+ use Chronicle.Reactors.Reactor, id: "notification-reactor"
22
+ use Chronicle.Projections.Projection, id: "account-summary", model: MyApp.ReadModels.AccountSummary
23
+ ```
24
+
25
+ Discovery logs a warning once per module per BEAM node for a defaulted id. `warn_defaulted_ids: false` on `Chronicle.Client` suppresses it; prefer pinning. In-memory test clients skip it.
26
+
27
+ ## Drift policy
28
+
29
+ Before registering, the client compares what it would register with the kernel. The `:drift_policy` client option:
30
+
31
+ | Value | Behavior |
32
+ | --- | --- |
33
+ | `:warn` (default) | Log findings and register anyway |
34
+ | `:fail` | Log findings and refuse to register on a `:warning`-severity finding |
35
+ | `:ignore` | Skip the check and its kernel calls |
36
+
37
+ Findings: `:schema_changed_without_generation` (an event type generation's coarse shape changed: added, removed or retyped field), `:observer_missing_from_code` (always `:info`, never blocks: other clients may own the observer), `:observer_event_types_changed` (a reactor's handled types differ; `:warning`). An indeterminate check (kernel unreachable, exception, 5 s deadline) continues under `:warn` and stops under `:fail`. Read the current findings with `Chronicle.Client.identity_drift()`. The kernel rejects a schema change without a bumped generation itself, regardless of policy.
38
+
39
+ ## Event type generations
40
+
41
+ A module for an **older** generation uses `generation_for:` instead of its own `:id`, so the ids cannot drift apart:
42
+
43
+ ```elixir
44
+ defmodule MyApp.Events.CustomerRegisteredV1 do
45
+ use Chronicle.Events.EventType, generation_for: MyApp.Events.CustomerRegistered, generation: 1
46
+ defstruct customer_id: "", owner_name: ""
47
+ end
48
+ ```
49
+
50
+ `generation_for:` requires an explicit `generation:` and cannot be combined with `:id`. Bridge the generations with `Chronicle.Events.Migration` (exactly one migrator per pair).
51
+
52
+ Mistakes such as two modules for one id and generation, two migrators for a pair, or `generation_for:` naming a module that is not an event type are logged and the client continues. Set `strict_validation: true` on `Chronicle.Client` to raise instead; `Chronicle.Events.EventTypes.validate/2` returns `:ok | {:error, exception}` and `validate!/2` raises.
53
+
54
+ ## Pinned generation delivery
55
+
56
+ By default (`:compatibility`) every event is upgraded to the generation of the module a reactor or reducer handles. For a handler written against an **older** generation, `use Chronicle.Reactors.Reactor, generation_delivery: :pinned` (or the same on a reducer) delivers the event as appended. `context.appended_generation` reports the appended generation (`0` means unknown). The client-wide default is `event_generation_delivery: :pinned`; an artifact's own option wins. A pinned observer that handles two generations of one event type raises `Chronicle.Observers.MultipleGenerationsOfEventTypeInObserver` on start.
@@ -63,4 +63,4 @@ Without an explicit `:concurrency_scope`, the client reads the tail narrowed by
63
63
 
64
64
  The wire carries one scope per event source id. Guarded entries of that id must agree on dimensions and selected source type, stream type and stream id. Unguarded entries never suppress a later guarded entry. Incompatible derived guards fail before any tail read or append; use separate batches, or deliberately supply one explicit scope on an entry for that id.
65
65
 
66
- Reactor and reducer contexts expose `:event_source` as the registered name, or `nil` for events appended without a definition. Their other context keys differ; do not assume the reactor context shape for a reducer.
66
+ Reactor and reducer contexts expose `:event_source` as the registered name, or `nil` for events appended without a definition. Only reactor contexts carry `:delivery_id`.
@@ -25,8 +25,8 @@ client-side. Observer types include `:reactor`, `:projection`, `:reducer`,
25
25
  - **Discovery is compile-time reflection.** The `use` macros generate
26
26
  `__chronicle_*__/1` functions via `__before_compile__`, and discovery is
27
27
  `function_exported?/3` over the application's modules — not runtime scanning.
28
- - **No client-owned telemetry events.** `telemetry` is a direct dependency;
29
- transport failure detection subscribes to grpc events. Logging is `Logger`.
28
+ - **Telemetry is built in.** The client emits OpenTelemetry spans and `:telemetry`
29
+ events; read [observability.md](observability.md). Logging is `Logger`.
30
30
  - **No formatter export.** `.formatter.exs` declares no `locals_without_parens`
31
31
  for `from`, `join`, `removed_with`, `from_every`, `pii`, `subject`, `unique`, or
32
32
  `unique_event_type`, and there is no `import_deps: [:cratis_chronicle]` to
@@ -0,0 +1,37 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Observability: OpenTelemetry and `:telemetry`
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/diagnostics/tracing.ex`, `connections/trace_propagation_interceptor.ex`, and `Documentation/observability.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0.
9
+
10
+ The client is always instrumented and depends only on `opentelemetry_api`. With no OpenTelemetry SDK started in the release, every span and propagation call is a no-op and nothing leaves the process; there is no Chronicle-specific enable switch. To export, add `opentelemetry` and `opentelemetry_exporter` to the application and configure them the standard way (`OTEL_*` variables or `config :opentelemetry`).
11
+
12
+ All spans use the instrumentation scope `Cratis.Chronicle.Client`, shared with the other Chronicle clients.
13
+
14
+ | Span | Kind | Where |
15
+ | --- | --- | --- |
16
+ | `cratis.chronicle.client.event_sequence.append` | Client | `append/3`, `append_with_result/3` |
17
+ | `cratis.chronicle.client.event_sequence.append_many` | Client | `append_many/3` variants, `append_many_for_event_sources/2`, unit-of-work commit |
18
+ | `cratis.chronicle.client.reactor.handle` | Consumer | a reactor delivery |
19
+ | `cratis.chronicle.client.reducer.reduce` | Consumer | a reducer delivery |
20
+
21
+ Attributes: `cratis.event_store.name`, `cratis.event_store.namespace`, `cratis.event_sequence.id`, `cratis.event_type.id` (append spans), `cratis.event.count` (batches), `cratis.observer.id`, `cratis.correlation_id`. **`cratis.event_source.id` is off by default** because an event source id can be sensitive; enable it with `config :cratis_chronicle, trace_event_source_id: true`.
22
+
23
+ A span is marked error when the operation returns `{:error, _}`, and a raised exception is recorded before re-raising. The status description is a fixed classification; messages, reasons and stacktraces are never put on a span because they can carry payloads.
24
+
25
+ ## `:telemetry` events
26
+
27
+ For metrics built on `:telemetry`, the same operations emit spans of `:start`, `:stop` and `:exception` events:
28
+
29
+ - `[:cratis, :chronicle, :client, :append, ...]`
30
+ - `[:cratis, :chronicle, :client, :reactor, :handle, ...]`
31
+ - `[:cratis, :chronicle, :client, :reducer, :reduce, ...]`
32
+
33
+ The `:stop` metadata carries `error: true` when the operation returned `{:error, _}`. Observer lag is a separate event, `[:chronicle, :observer, :lag]`.
34
+
35
+ ## Trace propagation
36
+
37
+ Every outbound gRPC call carries W3C `traceparent`/`tracestate` for the span active in the **calling process**, injected by `Chronicle.Connections.TracePropagationInterceptor` (always installed). A trace continues into the kernel only when the append or query runs inside the active span (for example inside `OpenTelemetry.Tracer.with_span/2`); a `Task` is a different process. The kernel does not store trace context on events, so an observer's span is not linked to the append that caused it. OpenTelemetry metrics instruments are not emitted, only `:telemetry` events.
@@ -0,0 +1,22 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Behavior patterns
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/patterns.ex` and `Documentation/patterns.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0.
9
+
10
+ `Chronicle.Patterns` queries the recurring behavior (combinations of facets such as `Day`, `TimeBucket`, `CommandType`, `AggregateType`) that Chronicle mined from an event store's history for a scope, for example one user. **An empty result is an answer, not a failure**: the scope has no established behavior for that context. Treat it as "do not claim to know".
11
+
12
+ ```elixir
13
+ {:ok, patterns} = Chronicle.Patterns.get_patterns_at("user-42")
14
+ {:ok, patterns} = Chronicle.Patterns.get_patterns_at("user-42", moment, also_constraining: %{Chronicle.Patterns.FacetName.aggregate_type() => "Invoice"})
15
+ action = pattern.facets[Chronicle.Patterns.FacetName.command_type()]
16
+ ```
17
+
18
+ - Default to `get_patterns_at/3` ("what does this scope usually do now"). It derives `Day` and `TimeBucket` from the moment's **own offset**, not UTC, with the rule the kernel mined with; the moment defaults to now.
19
+ - Use `get_patterns/3` and `get_usual_actions/3` only for a hand-built facet map; `get_usual_actions/3` returns at most one answer per action.
20
+ - `get_patterns_for_scope/2` and `get_scopes/1` inspect what has been mined.
21
+ - `Chronicle.Patterns.time_bucket/1` exposes the kernel's part-of-day rule (`:early_morning`, `:morning`, `:midday`, `:afternoon`, `:evening`, `:night`). **Never write a copy**: a slot the mining never used looks exactly like "nothing established".
22
+ - Options on every call: `:client`, `:namespace`, `:minimum_confidence` (0.0 to 1.0) and `:max_results`. The all-patterns query is not exposed.
@@ -16,10 +16,18 @@ Source: `Cratis/Chronicle.Elixir`.
16
16
  | Assuming the namespace default is `"default"` | It is `"Default"`; the moduledoc is wrong |
17
17
  | `:ok = Chronicle.append(...)` in a server | A constraint violation returns `{:error, _}` and raises `MatchError` |
18
18
  | Waiting for `:connected` | Observers attach at `:registered` |
19
- | Reading `:correlation_id` from a reactor context | The map has six keys and that is not one |
19
+ | Reading `:correlation_id` or `:caused_by` from the context of an older client | It was a bare six-key map before the `EventContext` struct; see the upgrading reference |
20
20
  | Two read models with the same final module segment | The default id is the last segment only |
21
21
  | Appending from a `Task` and expecting the correlation id | Ambient context is per process |
22
22
  | Treating quiet observers as "no events" | Keepalive eviction can silence observers while appends still succeed |
23
23
  | Shipping the default TLS behavior | `skip_tls_validation` defaults to `true` |
24
24
  | Forgetting `global_handlers:` or `:otp_app` discovery | Shared mappings are not merged into variants unless registered |
25
25
  | A shared handler mapping a field one variant lacks | `GlobalHandlerPropertyNotOnVariant` at registration, not a silently skipped mapping |
26
+ | `DecisionReads.get/2` outside `Chronicle.decide/2` | Returns `{:error, :requires_unit_of_work}`; it guards nothing |
27
+ | Unset expected sequence number as "expect no event" | It disables the check; use `ConcurrencyScope.expecting_no_matching_event/1` |
28
+ | Passing no `client: client` in a `Chronicle.Testing.Case` spec | The call addresses the default client, not the test's |
29
+ | Trusting a spec that passed on the in-memory kernel | Queries, projections and `closes_stream` return `{:error, {:not_supported_in_memory, rpc}}`; use `Testing.Integration` |
30
+ | Renaming a reactor, reducer or projection module without a pinned `:id` | Its observation position silently resets |
31
+ | Capture event type ids with hyphens | `append/2,3` needs an uppercase-first, letters-digits-underscore id |
32
+ | Nested `children/3` ids reused across read model instances | Nested events can land on the wrong instance; use UUIDs |
33
+ | Reading a read model right after an append | `{:ok, nil}` means absent or not projected yet; use `Projections.wait_for/3` |
@@ -50,3 +50,31 @@ from_every(increment: [event_counts: {:event_context, :type}])
50
50
  ```
51
51
 
52
52
  `decrement:` supports the corresponding dictionary operation. Use typed defaults for numeric and boolean fields on every model: schemas are generated from those defaults, not static Elixir types.
53
+
54
+ ## Child collections
55
+
56
+ Requires `cratis_chronicle` 3.13.0 for the one-line form, `removed_with_join/2`, `clear_with/2` and the compile-time checks below; `children/2,3` itself requires 3.11.0.
57
+
58
+ `children/2,3` declares a list on the parent that grows, changes and shrinks with events. It works in `use Chronicle.ReadModels.ReadModel` and `use Chronicle.Projections.Projection`; declare the list field's element type with `types: [items: {:list, MyApp.ReadModels.BasketItem}]`.
59
+
60
+ ```elixir
61
+ children :items, identified_by: :item_id do
62
+ from MyApp.Events.ItemAdded,
63
+ key: :item_id,
64
+ parent_key: :event_source_id,
65
+ set: [item_id: :item_id, name: :name]
66
+
67
+ removed_with MyApp.Events.ItemRemoved, key: :item_id, parent_key: :event_source_id
68
+ end
69
+ ```
70
+
71
+ A collection that only grows from one event needs no block: `children :entries, from: MyApp.Events.EntryAdded, identified_by: :entry_id, key: :entry_id, set: [entry_id: :entry_id, name: :name]`.
72
+
73
+ - Inside the block `from/2`, `join/2`, `removed_with/2`, `removed_with_join/2`, `clear_with/2`, `from_every/1` and a nested `children/3` take the options they take on the read model. `key` selects the child an event applies to; `parent_key` selects its parent.
74
+ - A `join` in a children block defaults `:on` to `identified_by`; an explicit disagreeing `:on` compiles with a warning. A root `join/2` still requires `:on`.
75
+ - `removed_with_join event, key: :archived_entry_id` removes a child when an event from a **different** event source matches it by property instead of the collection key.
76
+ - `clear_with event, [:field]` (or `clear: [:field]` in a mapping) sets a scalar back to no value. The field must default to `nil`; a field defaulting to `""` is not nullable to the kernel, so clearing writes the type default.
77
+ - **Give nested child collections globally unique identifiers (UUIDs).** In a nested `children/3`, `parent_key` names a child of the collection one level up, and the kernel finds it across every instance of the read model; two instances that both hold `"e-1"` can receive each other's nested events.
78
+ - Compile-time errors cover a `children` without `:identified_by`, a root `join/2` without `:on`, one target property assigned twice by one event, and a `from`/`join` target or `children` name that is not a struct field.
79
+
80
+ A read model document is limited to 4 MiB; give a child collection that can grow without bound its own read model.
@@ -0,0 +1,41 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Querying, observing and previewing read models
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/read_models.ex`, `read_models/watchers.ex`, `read_models/reactor.ex`, `projections.ex`, and `Documentation/read-models.md`, `projections.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0 for everything below except `watch/2`.
9
+
10
+ A read model is eventually consistent: `Chronicle.read_model/3` returns `{:ok, nil}` both for an absent key and for one not projected yet. To read your own write, append with `append_and_wait_for_completion/3` or call `Chronicle.Projections.wait_for/3`.
11
+
12
+ ## Wait for a projection
13
+
14
+ ```elixir
15
+ {:ok, %{sequence_number: sequence}} = Chronicle.EventSequences.EventLog.append_with_result(id, event)
16
+ :ok = Chronicle.Projections.wait_for(MyApp.ReadModels.Account, sequence)
17
+ ```
18
+
19
+ `wait_for/3` polls until the projection has handled that sequence number or returns `{:error, :timeout}`.
20
+
21
+ ## Filter and sort
22
+
23
+ `Chronicle.ReadModels.query/2` accepts `:filter` (all conditions must match; a value means equality, or `{:gt, v}`, `{:gte, v}`, `{:lt, v}`, `{:lte, v}`, `{:ne, v}`, `{:in, [v]}`, `{:contains, v}`) and `:sort` (`{:amount, :desc}`). An unknown field returns `{:error, {:unknown_field, field}}`.
24
+
25
+ **The client applies the filter** by paging through the whole read model, because the kernel contract cannot filter. A read model with more than `:max_scan` instances (default `10_000`) returns `{:error, {:scan_limit_exceeded, max_scan}}` rather than a partial answer. For large data, give the question its own projection.
26
+
27
+ ## Live changes: pick by what you need
28
+
29
+ | Need | Function | Messages the process receives |
30
+ | --- | --- | --- |
31
+ | Changes to a read model, one stream per caller | `watch/2` / `unwatch/1` | `{:chronicle_read_model_changed, module, %Changeset{}}`, `{:chronicle_read_model_watch_error, module, reason}` (the watch has ended; call again) |
32
+ | Same, shared across callers (default) | `subscribe/2` / `unsubscribe/1` | the two above plus `{:chronicle_read_model_watch_ended, module, reason}` and `{:chronicle_read_model_watch_resumed, module}` |
33
+ | The current page of instances, re-sent on change | `observe/2` / `unobserve/1`, or `observe_stream/2` as an Elixir `Stream` of `%QueryResult{}` | `{:chronicle_read_model_instances, module, %{instances: _}}`, `{:chronicle_read_model_observe_error, module, reason}` (observation ended) |
34
+
35
+ `subscribe/2` shares one kernel stream per read model, client, namespace and event sequence and reconnects with backoff (500 ms doubling to 30 s). **Changes made while the stream was down are not replayed**; query again after `:chronicle_read_model_watch_resumed`. `observe/2` accepts `:page_size`, `:filter` and `:sort`; with a filter, `total_count` counts matches within the delivered page only.
36
+
37
+ A `use Chronicle.ReadModels.Reactor, model: M` module reacts to read model changes through optional `on_added/2`, `on_modified/2` and `on_removed/2`, and is registered with the client's `:read_model_reactors` option. It subscribes through `subscribe/2` and re-subscribes after a reconnect.
38
+
39
+ ## Ad-hoc queries and previews
40
+
41
+ `Chronicle.Projections.query/2` sends a Projection Declaration Language string to the kernel and returns the projected entries without registering a projection or defining a module; entries are plain maps unless `:read_model` is given. Bad syntax returns `{:error, {:invalid_declaration, [%{message:, line:, column:}]}}`. `Chronicle.Projections.preview/2` runs an **already registered** projection's declaration over the stored event sequence; previewing over events you supply is not possible.
@@ -0,0 +1,63 @@
1
+ <!-- Copyright (c) Cratis. All rights reserved. -->
2
+ <!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
3
+
4
+ # Testing against an in-memory kernel
5
+
6
+ Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/testing.ex`, `testing/case.ex`, `testing/assertions.ex`, `testing/in_memory_kernel.ex`, `testing/integration.ex`, `testing/projections.ex`, and `Documentation/testing.md`.
7
+
8
+ Requires `cratis_chronicle` 3.13.0. Earlier clients ship no `Chronicle.Testing` modules.
9
+
10
+ ## Default: `Chronicle.Testing.Case`
11
+
12
+ Use the case template for ExUnit specs. It starts a real `Chronicle.Client` against `Chronicle.Testing.InMemoryKernel`, puts the client name in the test context as `:chronicle`, and stops it on exit. Every test gets a unique event store, so it is safe under `async: true`.
13
+
14
+ ```elixir
15
+ # Copyright (c) Cratis. All rights reserved.
16
+ # Licensed under the MIT license. See LICENSE file in the project root for full license information.
17
+
18
+ defmodule MyApp.AccountTest do
19
+ use Chronicle.Testing.Case,
20
+ event_types: [MyApp.Events.AccountOpened],
21
+ reducers: [MyApp.Reducers.AccountReducer]
22
+
23
+ import Chronicle.Testing.Assertions
24
+
25
+ test "opening an account appends AccountOpened", %{chronicle: client} do
26
+ :ok =
27
+ Chronicle.EventSequences.EventLog.append(
28
+ "account-1",
29
+ %MyApp.Events.AccountOpened{owner_name: "Alice"},
30
+ client: client
31
+ )
32
+
33
+ assert_appended(client, MyApp.Events.AccountOpened, event_source_id: "account-1")
34
+ end
35
+ end
36
+ ```
37
+
38
+ **Pass `client: client` on every call.** Without it the call addresses the default `Chronicle.Client` name, not the test's client.
39
+
40
+ `start_supervised_client/1` (outside ExUnit templates; pair with `stop_supervised_client/1`) accepts `:event_types`, `:event_sources`, `:constraints` (standalone constraint modules), `:reactors`, `:reducers`, `:projections`, `:name`, `:event_store` and `:namespace`.
41
+
42
+ ## Waiting and reading
43
+
44
+ An append returns once the event is committed; reactors and reducers run afterward in `Chronicle.Testing.Dispatcher`, synchronously after each committed append. Call `Chronicle.Testing.wait_for_observers(client)` before asserting on their effects; it is a sleep-free barrier that returns `{:error, failures}` when a reactor or reducer raised.
45
+
46
+ - `Chronicle.Testing.read_model(client, ReducerModule, key)` reads the state a **reducer** built, keyed by event source id.
47
+ - `Chronicle.Testing.appended_events(client, event_source_id)` returns decoded `Chronicle.Events.AppendedEvent` structs.
48
+
49
+ ## Assertions
50
+
51
+ `assert_appended/3` and `refute_appended/3` take a matcher: an exact event struct (`==`), an event type module, or a one-arity predicate; options include `:event_source_id` and `:count`. `assert_constraint_violation(result, constraint_name \\ nil)` and `assert_concurrency_violation(result)` match the `{:error, {:constraint_violations, _}}` and `{:error, {:concurrency_violations, _}}` append results.
52
+
53
+ ## What the in-memory kernel does not model
54
+
55
+ Appends, reads, the tail lookups, concurrency scopes (including `expecting_no_matching_event/1`), and `unique/2` and `unique_event_type/1` constraints are modeled. **Everything else returns `{:error, {:not_supported_in_memory, rpc}}`**: `closes_stream` constraints, `ReadModels` queries and `Projections` queries. A spec that passes against it proves client logic, not kernel behavior.
56
+
57
+ `Chronicle.Testing.Projections.apply_event(ReadModel, instance_or_nil, event)` folds one event through the supported subset of a read model's declarations (`from/2` with `:set`, `:add`, `:subtract`, `:count`, `:increment`, `:decrement`, `:clear`, AutoMap, and `removed_with/2`). `join/2`, `children/2`, `from_every/1` and `removed_with_join/2` raise `Chronicle.Testing.UnsupportedProjectionOperation`; test those against a real kernel.
58
+
59
+ ## Real kernel: `Chronicle.Testing.Integration`
60
+
61
+ Tag these specs `@moduletag :integration`. `Chronicle.Testing.Integration.setup_isolated_client(opts, client \\ Chronicle.Client)` starts a real client with a unique event store and namespace, reading `CHRONICLE_INTEGRATION_CONNECTION_STRING` unless `:connection_string` is given, and waits for registration. Use `append_with_result/3` then `Integration.wait_for_observers(result.sequence_number, client: client)`; do not sleep. The stores it creates are not deleted afterward because the kernel has no delete call.
62
+
63
+ `Chronicle` adopts `Chronicle.Behaviour`, so code that only needs to be observed calling the facade can use a Mox mock (`Mox.defmock(Mock, for: Chronicle.Behaviour)`); Mox is not a dependency of the client.
@@ -32,3 +32,21 @@ Rejected appends (including concurrency/constraint failures) return `{:error, re
32
32
  Declared `unique(..., message: ...)` violation messages are preserved on single, batch and transactional appends. Retain the constraint name and diagnostics when handling violations rather than assuming every message came from the kernel.
33
33
 
34
34
  Requires `cratis_chronicle` 3.10.0 and Chronicle kernel 19.30.0 for named event source definitions.
35
+
36
+ ## Behavior changes to review when raising the requirement
37
+
38
+ Requires `cratis_chronicle` 3.13.0 for the behaviors below; each is a difference an older application can observe.
39
+
40
+ | Behavior | Review |
41
+ | --- | --- |
42
+ | Reactor and reducer `context` is a `Chronicle.Events.EventContext` struct | Pattern matches on a bare map keep working; `correlation_id`, `caused_by`, `causation`, `tags`, `subject`, `appended_generation` and `replaying?` are now present |
43
+ | `drift_policy` defaults to `:warn` | Start-up makes extra kernel calls and logs findings; set `:ignore` to skip them, `:fail` to block on a drift warning |
44
+ | Discovery warns once per module for a reactor, reducer or projection with no pinned `:id` | Pin ids before renaming or moving modules; see [discovery-and-generations.md](discovery-and-generations.md) |
45
+ | Appending a struct that is not an event type returns `{:error, {:unknown_event_type, module}}` | Handle it as an ordinary append error |
46
+ | Spans and `:telemetry` events are always emitted | No action without an SDK; see [observability.md](observability.md) |
47
+ | The client installs a Logger filter for Mint response crash reports | Keep your own production filter for the remaining crash shapes |
48
+ | `children/2,3` and projection mistakes are rejected at compile time | Fix declarations that assigned one target twice or named a missing field |
49
+
50
+ Requires `cratis_chronicle` 3.11.0 for the context struct; it was a bare map before.
51
+
52
+ Capabilities covered by these references: decision reads ([decision-reads.md](decision-reads.md)), `Chronicle.Testing` ([testing.md](testing.md)), captures ([captures.md](captures.md)), standalone constraints ([constraints.md](constraints.md)), `Chronicle.Patterns` ([patterns.md](patterns.md)) and read model subscriptions ([read-models.md](read-models.md)). A git tag is not a Hex release: confirm the version is on hex.pm before depending on it.
@@ -4,12 +4,12 @@
4
4
  "fixtures": [],
5
5
  "assertions": [
6
6
  { "kind": "skill-contains", "value": "`Cratis/Chronicle.Elixir`" },
7
- { "kind": "skill-contains", "value": "{:cratis_chronicle, \"~> 3.10\"}" },
7
+ { "kind": "skill-contains", "value": "{:cratis_chronicle, \"~> 3.13\"}" },
8
8
  { "kind": "skill-contains", "value": "ConnectionString.development()" },
9
9
  { "kind": "skill-contains", "value": "`~> 1.18`" },
10
10
  { "kind": "skill-contains", "value": "`nil` registers as string" },
11
- { "kind": "skill-contains", "value": "**The reactor context has six keys**" },
12
- { "kind": "skill-contains", "value": "`sequence_number`, `occurred`, `event_source`, `observation_state`" },
11
+ { "kind": "skill-contains", "value": "**The context is a `Chronicle.Events.EventContext` struct**" },
12
+ { "kind": "skill-contains", "value": "`delivery_id` (stable across replay and failed-partition" },
13
13
  { "kind": "skill-contains", "value": "**Page 1 is the second page**" },
14
14
  { "kind": "skill-contains", "value": "`:retry_attempts` is accepted but ignored", "file": "references/connections.md" },
15
15
  { "kind": "skill-contains", "value": "Client certificates cannot be combined with `disableTls=true`", "file": "references/connections.md" },
@@ -18,6 +18,19 @@
18
18
  { "kind": "skill-contains", "value": "`Chronicle.Events.ConcurrencyScope.none()`", "file": "references/event-sources.md" },
19
19
  { "kind": "skill-contains", "value": "**no many-event variant**", "file": "references/append-api.md" },
20
20
  { "kind": "skill-contains", "value": "`subject/1` is not imported by `EventType`", "file": "references/compliance.md" },
21
- { "kind": "skill-contains", "value": "calendar year, not the ISO week-year", "file": "references/projections.md" }
21
+ { "kind": "skill-contains", "value": "calendar year, not the ISO week-year", "file": "references/projections.md" },
22
+ { "kind": "skill-contains", "value": "Outside one it returns `{:error, :requires_unit_of_work}`", "file": "references/decision-reads.md" },
23
+ { "kind": "skill-contains", "value": "**Leaving the expected sequence number unset turns the check off.**", "file": "references/decision-reads.md" },
24
+ { "kind": "skill-contains", "value": "**Everything else returns `{:error, {:not_supported_in_memory, rpc}}`**", "file": "references/testing.md" },
25
+ { "kind": "skill-contains", "value": "**Pass `client: client` on every call.**", "file": "references/testing.md" },
26
+ { "kind": "skill-contains", "value": "`name:` is **required** on a standalone declaration.", "file": "references/constraints.md" },
27
+ { "kind": "skill-contains", "value": "**Renaming or moving the module then creates a different artifact", "file": "references/discovery-and-generations.md" },
28
+ { "kind": "skill-contains", "value": "`mix chronicle.discover --otp-app my_app`", "file": "references/discovery-and-generations.md" },
29
+ { "kind": "skill-contains", "value": "**An appended event type's id must start with an uppercase letter", "file": "references/captures.md" },
30
+ { "kind": "skill-contains", "value": "**Give nested child collections globally unique identifiers (UUIDs).**", "file": "references/projections.md" },
31
+ { "kind": "skill-contains", "value": "**The client applies the filter**", "file": "references/read-models.md" },
32
+ { "kind": "skill-contains", "value": "**`cratis.event_source.id` is off by default**", "file": "references/observability.md" },
33
+ { "kind": "skill-contains", "value": "**An empty result is an answer, not a failure**", "file": "references/patterns.md" },
34
+ { "kind": "skill-contains", "value": "`{:error, {:unknown_event_type, module}}`", "file": "references/append-api.md" }
22
35
  ]
23
36
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/pi",
3
- "version": "7.56.3",
3
+ "version": "7.57.0",
4
4
  "description": "Configuration-aware Cratis AI integration for Pi",
5
5
  "type": "module",
6
6
  "files": [