@cratis/pi 0.0.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.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
@@ -0,0 +1,429 @@
1
+ ---
2
+ name: cratis-chronicle-client-elixir
3
+ description: Talk to a Chronicle server from an Elixir application with the cratis_chronicle Hex package - putting Chronicle.Client in a supervision tree, connection strings, use Chronicle.Events.EventType structs, Chronicle.append returning ok or error tuples, reactors with the @handles attribute and a handle/2 callback, model-bound read models, and the connection lifecycle phases and keepalive. Use when an OTP application appends to or observes a Chronicle event store. Do not use for the .NET, TypeScript, or Kotlin clients.
4
+ license: MIT
5
+ ---
6
+
7
+ # The Chronicle client for Elixir
8
+
9
+ `cratis_chronicle` is a **standalone client SDK**. It is a library, not a
10
+ framework: it starts no supervision tree of its own, you add `Chronicle.Client`
11
+ to yours, and everything after that is ordinary OTP.
12
+
13
+ ## Verified product sources
14
+
15
+ This skill is verified against `Cratis/Chronicle.Elixir` at tag **`v2.2.0`**,
16
+ which is the version actually published on Hex.
17
+
18
+ | Fact | Value | Source |
19
+ | --- | --- | --- |
20
+ | OTP app / Hex package | `:cratis_chronicle` | `Source/chronicle/mix.exs:14` |
21
+ | Published version | `2.2.0` | hex.pm |
22
+ | Elixir requirement | `~> 1.14` | `Source/chronicle/mix.exs:16` |
23
+ | Contracts dependency | `cratis_chronicle_contracts` resolved to `16.13.4` | `Source/chronicle/mix.lock` |
24
+
25
+ > **The `v2.3.0` git tag was never published to Hex.** `mix deps.get` cannot
26
+ > fetch it. Anything introduced there — notably the `Chronicle.Concept` macro,
27
+ > which does not exist at `v2.2.0` — must not be documented as available.
28
+
29
+ > **The repository's `VERSION` file says `0.0.5` and is a build-time
30
+ > placeholder** overwritten by the publish workflow. Never quote it.
31
+
32
+ > **Do not copy from the repository's root `README.md`.** At `v2.2.0` it uses
33
+ > module names that do not exist — `use Chronicle.EventType`,
34
+ > `use Chronicle.ReadModel`, `use Chronicle.Reactor`, `use Chronicle.Reducer`,
35
+ > `use Chronicle.Seeder` (`README.md:9-13`, `:49`, `:54`). The real modules are
36
+ > namespaced: `Chronicle.Events.EventType`, `Chronicle.ReadModels.ReadModel`,
37
+ > `Chronicle.Reactors.Reactor`, `Chronicle.Reducers.Reducer`,
38
+ > `Chronicle.Seeding.Seeder`. **Copy from `Documentation/client-snippets/`
39
+ > instead** — those are CI-validated and correct at every version.
40
+
41
+ > `Documentation/get-started.md:12` pins `{:cratis_chronicle, "~> 0.1"}`, which
42
+ > does not match the published `2.2.0`. Use a `2.x` requirement.
43
+
44
+ ## Adding it
45
+
46
+ ```elixir
47
+ # mix.exs
48
+ defp deps do
49
+ [
50
+ {:cratis_chronicle, "~> 2.2"}
51
+ ]
52
+ end
53
+ ```
54
+
55
+ The library declares `extra_applications: [:logger, :inets, :ssl]` and **no
56
+ `mod:`** (`Source/chronicle/mix.exs`), so nothing starts on its own. `:inets` and
57
+ `:ssl` back the least-connections load balancer's HTTP probes.
58
+
59
+ ## Starting the client
60
+
61
+ `Chronicle.Client` is a `Supervisor` (`lib/chronicle/client.ex:129`) with
62
+ `start_link(opts \\ [])` (`:139`). Put it in your application's tree — this is
63
+ the CI-validated snippet from
64
+ `Documentation/client-snippets/get-started/console/connect.md`:
65
+
66
+ ```elixir
67
+ defmodule MyApp.Application do
68
+ use Application
69
+
70
+ @impl true
71
+ def start(_type, _args) do
72
+ children = [
73
+ {Chronicle.Client,
74
+ connection_string: "chronicle://localhost:35000",
75
+ event_store: "quickstart",
76
+ otp_app: :my_app}
77
+ ]
78
+
79
+ Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
80
+ end
81
+ end
82
+ ```
83
+
84
+ ### Options
85
+
86
+ All configuration is **child-spec keyword options**. There is no
87
+ `Application.get_env` anywhere in the library and no `config/` directory —
88
+ `config.exs` does nothing for this client. Read the options at
89
+ `lib/chronicle/client.ex`:
90
+
91
+ | Option | Default | Line |
92
+ | --- | --- | --- |
93
+ | `:name` | `Chronicle.Client` | `:140` |
94
+ | `:connection_string` | `ConnectionString.default/0` | — |
95
+ | `:event_store` | `"default"` | `:159` |
96
+ | `:namespace` | `"Default"` — capital D | `:160` |
97
+ | `:discover` | `true` | `:161` |
98
+ | `:otp_app` | none; falls back to scanning loaded modules | `:168` |
99
+ | `:event_types`, `:migrations`, `:reactors`, `:reducers`, `:read_models`, `:projections`, `:seeders`, `:webhooks`, `:event_store_subscriptions` | `[]`, merged with discovered | `:186-197` |
100
+ | `:skip_tls_validation`, `:load_balancer`, `:grpc_options`, `:retry_attempts`, `:reconnect_base_delay`, `:reconnect_max_delay` | forwarded to the connection | — |
101
+
102
+ > The moduledoc at `client.ex:81` says the namespace defaults to `"default"`.
103
+ > The code at `:160` uses `"Default"`. **Trust the code.**
104
+
105
+ **Set `:otp_app`.** With it, discovery is scoped to your application's modules;
106
+ without it the client falls back to scanning every loaded module.
107
+
108
+ Configuration is stashed in `:persistent_term` and read back with
109
+ `Chronicle.Client.config/1` (`client.ex:150`). Note it stores only a subset —
110
+ `read_models`, `projections`, and `migrations` are not retrievable at runtime.
111
+
112
+ ### The supervision tree
113
+
114
+ `Supervisor.init(children, strategy: :rest_for_one)` — `client.ex:308`. The
115
+ strategy is deliberate: the children are ordered connection → session →
116
+ registration → observers, so restarting the connection restarts everything that
117
+ depends on it, and nothing is left holding a dead channel.
118
+
119
+ ### Multiple clients
120
+
121
+ Pass `name:` to `Chronicle.Client` and `client:` to every API call. Every public
122
+ function takes a `:client` option.
123
+
124
+ ## Connection strings
125
+
126
+ `Chronicle.Connections.ConnectionString` accepts `chronicle://` and
127
+ `chronicle+srv://` (`lib/chronicle/connections/connection_string.ex:8`,
128
+ validated at `:342`). Default port is `35_000` (`:83`).
129
+
130
+ | Function | Line |
131
+ | --- | --- |
132
+ | `default/0` — `chronicle://localhost:35000` | `:144` |
133
+ | `development/0` — adds the dev client credentials | `:154` |
134
+ | `parse/1` | `:186` |
135
+
136
+ Query parameters include `apiKey`, `disableTls`, `skipTlsValidation`,
137
+ `certificatePath`, `certificatePassword`, `authPort`, `loadBalancer`,
138
+ `srvNameServer`. A `chronicle+srv://` string accepts only one host and raises
139
+ otherwise (`:197-198`).
140
+
141
+ > **`skip_tls_validation` defaults to `true`** — the struct default at `:112` and
142
+ > the query parse at `:214`. TLS is on but the certificate chain is not checked,
143
+ > because a development kernel serves a self-signed certificate. **A production
144
+ > connection string must carry `?skipTlsValidation=false`.**
145
+
146
+ ## Defining event types
147
+
148
+ ```elixir
149
+ defmodule MyApp.Events.<EventName> do
150
+ @moduledoc "<What happened, in the past tense.>"
151
+
152
+ use Chronicle.Events.EventType, id: "<event-name>"
153
+
154
+ defstruct [:<field>, :<other_field>]
155
+ end
156
+ ```
157
+
158
+ `use Chronicle.Events.EventType` takes `:id` through `Keyword.fetch!`
159
+ (`lib/chronicle/events/event_type.ex:68`) — **the id is required**, unlike the
160
+ .NET, Kotlin, and TypeScript clients where it defaults to the type name. Give it
161
+ a stable, kebab-case string and never change it. `:generation` defaults to `1`
162
+ (`:69`).
163
+
164
+ The macro imports constraint macros into your module (`:79-86`):
165
+ `unique/1`, `unique/2`, `unique_event_type/0`, `unique_event_type/1`,
166
+ `remove_constraint/1`, plus `Chronicle.Compliance.pii/1,2` (`:88`). The
167
+ equivalent accumulating module attributes `@unique`, `@unique_event_type`,
168
+ `@remove_constraint` also work.
169
+
170
+ **Struct fields are sent as camelCase.** `encode_event/1` converts snake_case to
171
+ camelCase and `Jason.encode!`s the result
172
+ (`lib/chronicle/event_sequences/event_log.ex`).
173
+
174
+ > The moduledoc claims Chronicle generates a `Jason.Encoder` implementation
175
+ > automatically. There is no `defimpl Jason.Encoder` in the macro — encoding
176
+ > happens in `encode_event/1`. Do not rely on the claim.
177
+
178
+ Schema evolution is
179
+ `use Chronicle.Events.Migration, from: {Mod, generation: n}, to: {Mod, generation: n + 1}`
180
+ with `upcast/1` and `downcast/1`; the generations must be exactly one apart or it
181
+ raises at compile time.
182
+
183
+ ## Appending
184
+
185
+ ```elixir
186
+ :ok =
187
+ Chronicle.append(book_id, %MyApp.Events.<EventName>{
188
+ <field>: "<value>"
189
+ })
190
+ ```
191
+
192
+ `Chronicle.append/3` delegates to `Chronicle.EventSequences.EventLog`
193
+ (`lib/chronicle.ex:164-165`):
194
+
195
+ ```elixir
196
+ @spec append(String.t(), struct(), keyword()) :: :ok | {:error, term()}
197
+ ```
198
+
199
+ **The event source id comes first, the event second.** The return is a bare
200
+ `:ok`, not a result struct — this client's `append/3` deliberately carries no
201
+ sequence number, and `append_and_wait_for_completion/3` exists for when you need
202
+ the outcome.
203
+
204
+ | Function | Arity | Line in `event_log.ex` |
205
+ | --- | --- | --- |
206
+ | `append/3` | `(event_source_id, event, opts)` | `:99` |
207
+ | `append_many/3` | `(event_source_id, events, opts)` | `:120` |
208
+ | `append_many_for_event_sources/2` | `(events, opts)` | `:155` |
209
+ | `append_and_wait_for_completion/3` | returns `{:ok, %{success: _, failed_partitions: _}}` | `:215` |
210
+ | `get_for_event_source/2` | | `:326` |
211
+ | `get_from_sequence_number/2` | | `:369` |
212
+ | `get_tail_sequence_number/2` | | `:408` |
213
+
214
+ Append options (`event_log.ex:79-94`): `:client`, `:namespace`,
215
+ `:event_sequence_id` (default `"event-log"`), `:event_source_type` (default
216
+ `"Default"`), `:event_stream_type` (default **`"All"`**), `:event_stream_id`
217
+ (default `"Default"`), `:tags`, `:subject`, `:correlation_id`, `:identity`,
218
+ `:causation`, `:concurrency_scope`, `:occurred`.
219
+
220
+ Errors are normalized to one of two shapes:
221
+
222
+ ```elixir
223
+ {:error, {:constraint_violations, violations}}
224
+ {:error, {:append_errors, errors}}
225
+ ```
226
+
227
+ **Match on them.** A bare `:ok = Chronicle.append(...)` raises a `MatchError` on
228
+ a constraint violation, which is a fine choice in a script and the wrong one in a
229
+ GenServer.
230
+
231
+ A unit of work is a process: `Chronicle.begin_unit_of_work/1` starts an `Agent`,
232
+ and while one is current for the calling process `append`/`append_many` buffer
233
+ instead of sending, until `UnitOfWork.commit/1` or `rollback/1`.
234
+
235
+ ## Observing
236
+
237
+ ### Reactors
238
+
239
+ ```elixir
240
+ defmodule MyApp.Reactors.<ReactorName> do
241
+ use Chronicle.Reactors.Reactor
242
+
243
+ alias MyApp.Events.<EventName>
244
+
245
+ @handles <EventName>
246
+
247
+ @impl true
248
+ def handle(%<EventName>{}, %{event_source_id: <id>}) do
249
+ # side effect here
250
+ :ok
251
+ end
252
+ end
253
+ ```
254
+
255
+ That is the CI-validated snippet
256
+ (`Documentation/client-snippets/get-started/common/reactor.md`).
257
+
258
+ - **`@handles` is an accumulating module attribute** declaring the subscription;
259
+ dispatch is then ordinary Elixir pattern matching in `handle/2`. This is the
260
+ Elixir answer to the other clients' method-name or parameter-type conventions.
261
+ - The callback is
262
+ `handle(event :: struct(), context :: map()) :: :ok | {:error, term()} | {:ok, struct() | [struct()]}`.
263
+ - **Returning `{:ok, event_or_events}` appends those events as a side effect** —
264
+ to the same event source unless you return an `EventForEventSourceId`. A failed
265
+ side-effect append makes the whole `handle/2` result an error.
266
+ - The reactor id defaults to `to_string(__MODULE__)`, i.e. `"Elixir.My.Mod"`.
267
+ Pass `id:` for a stable, readable one.
268
+ - Optional replay callbacks: `on_replay_begin/0`, `on_replay_end/0`,
269
+ `on_partition_replay_begin/1`, `on_partition_replay_end/1`.
270
+
271
+ **The context map has exactly five keys** —
272
+ `%{event_source_id, sequence_number, occurred, event_store, namespace}`
273
+ (`lib/chronicle/reactors/handler.ex:393-397`). The reactor moduledoc also
274
+ mentions `:correlation_id`; **it is not in the map**.
275
+
276
+ ### Read models and projections
277
+
278
+ ```elixir
279
+ defmodule MyApp.ReadModels.<ReadModelName> do
280
+ use Chronicle.ReadModels.ReadModel
281
+
282
+ defstruct id: nil, <field>: nil, <flag>: false
283
+
284
+ from MyApp.Events.<EventName>,
285
+ set: [id: :event_source_id, <field>: :<event_field>]
286
+
287
+ from MyApp.Events.<OtherEvent>,
288
+ set: [<flag>: true]
289
+ end
290
+ ```
291
+
292
+ That shape is the CI-validated snippet
293
+ (`Documentation/client-snippets/get-started/common/book-read-model.md`).
294
+
295
+ The DSL macros are `from/1`, `from/2`, `join/2`, `removed_with/2`, `from_every/1`.
296
+ `from/2` options: `:key` (defaults to `"$eventSourceId"`), `:parent_key`, `:set`,
297
+ `:add`, `:subtract`, `:count`.
298
+
299
+ > **The read model id sent to the kernel is the last module segment** —
300
+ > `Module.split() |> List.last()` at `lib/chronicle/read_models/read_model.ex:148`.
301
+ > `MyApp.ReadModels.Account` registers as `"Account"`, so two read models with the
302
+ > same final segment collide. Pass `id:` to disambiguate.
303
+
304
+ A standalone projection is `use Chronicle.Projections.Projection, model: Mod`
305
+ (`:model` is required); a reducer is
306
+ `use Chronicle.Reducers.Reducer, model: Mod` with
307
+ `reduce(event, model_or_nil, context) :: struct()`. **Reducers run in your
308
+ process**, so the reduction is Elixir code you own.
309
+
310
+ Querying (`lib/chronicle/read_models.ex`):
311
+
312
+ ```elixir
313
+ {:ok, books} = Chronicle.all(MyApp.ReadModels.<ReadModelName>)
314
+ {:ok, book} = Chronicle.read_model(MyApp.ReadModels.<ReadModelName>, book_id)
315
+ ```
316
+
317
+ `Chronicle.read_model/3` delegates to `ReadModels.get/3` (`lib/chronicle.ex:211`,
318
+ `read_models.ex:245-246`), returning `{:ok, struct() | nil} | {:error, term()}`.
319
+ `Chronicle.all/2` delegates to `ReadModels.get_instances/2`
320
+ (`lib/chronicle.ex:219`, `read_models.ex:314-315`).
321
+
322
+ **Live updates arrive as messages, not as a stream.** `ReadModels.watch/2`
323
+ (`read_models.ex:593`) sends
324
+ `{:chronicle_read_model_changed, module, %Changeset{}}` and
325
+ `{:chronicle_read_model_watch_error, module, reason}` to the calling process. This
326
+ is the Elixir analogue of the other clients' observable APIs, and it means the
327
+ receiving process must have a `handle_info` for both.
328
+
329
+ ## Connecting is asynchronous — wait for the lifecycle
330
+
331
+ The client connects in the background. `Chronicle.Connections.Lifecycle`
332
+ broadcasts `{:chronicle_lifecycle, phase, connection_id}` with three phases
333
+ (`lib/chronicle/connections/lifecycle.ex:54`):
334
+
335
+ | Phase | Meaning |
336
+ | --- | --- |
337
+ | `:disconnected` | no live session with the kernel |
338
+ | `:connected` | the session handshake completed |
339
+ | `:registered` | the registration coordinator registered the base artifacts |
340
+
341
+ **Wait for `:registered`, never merely `:connected`** — the module's own docs say
342
+ so at `:27-29`, because reducers and reactors are not attached until registration
343
+ finishes.
344
+
345
+ ```elixir
346
+ config = Chronicle.Client.config()
347
+
348
+ case Chronicle.Connections.Lifecycle.wait_until(config.lifecycle, :registered, 30_000) do
349
+ :ok -> :ok
350
+ {:error, :timeout} -> # decide what a not-yet-registered client means for you
351
+ end
352
+ ```
353
+
354
+ `wait_until(lifecycle, target_phase, timeout \\ 30_000)` is at `:141`.
355
+ `subscribe/1` (`:90`) returns the current phase **and** sends it as a message,
356
+ which closes the subscribe-after-transition race; `phase/1` (`:98`) is the plain
357
+ read.
358
+
359
+ ## Keepalive — the failure mode is silence
360
+
361
+ The contract is spelled out in `lib/chronicle/connections/keep_alive.ex:7-17`:
362
+ the kernel pushes a `ConnectionKeepAlive` down the `Connect` server stream once
363
+ per second, and for each one **the client must call back the separate unary
364
+ `ConnectionKeepAlive` RPC** (`answer/2` at `:56`). A client that only consumes the
365
+ stream is evicted once the kernel's `LastSeen` falls more than five seconds
366
+ behind, and the kernel then unsubscribes its observers.
367
+
368
+ The consequence is stated in that same comment and is worth carrying into any
369
+ diagnosis: **reactors and reducers go quiet while the `Connect` stream stays open
370
+ and every append keeps working.** Nothing raises. If observers stop firing but
371
+ appends succeed, look at the connection before you look at the observer.
372
+
373
+ Reconnect is exponential backoff — `:retry_attempts` 5, `:reconnect_base_delay`
374
+ 1000 ms, `:reconnect_max_delay` 10000 ms — re-resolving addresses on every
375
+ attempt.
376
+
377
+ ## Idioms worth knowing
378
+
379
+ - **Process-scoped context.** Correlation id, identity, and causation are held per
380
+ process: `Chronicle.current_correlation_id/0`, `set_correlation_id/1`,
381
+ `clear_correlation_id/0`; `current_identity/0`, `set_identity/1`,
382
+ `clear_identity/0`; `Chronicle.Auditing.CausationManager`. They are picked up
383
+ automatically on append. **A `Task` is a different process** and does not
384
+ inherit them.
385
+ - **Discovery is compile-time reflection.** The `use` macros generate
386
+ `__chronicle_*__/1` functions via `__before_compile__`, and discovery is
387
+ `function_exported?/3` over the application's modules — not runtime scanning.
388
+ - **No telemetry.** The client emits no `:telemetry` events; `telemetry` appears
389
+ only as a transitive dependency of `grpc`. Logging is plain `Logger`.
390
+ - **No formatter export.** `.formatter.exs` declares no `locals_without_parens`
391
+ for `from`, `join`, `removed_with`, `from_every`, `pii`, `subject`, `unique`, or
392
+ `unique_event_type`, and there is no `import_deps: [:cratis_chronicle]` to
393
+ inherit. `mix format` will add parentheses to the DSL.
394
+ - **`ReadModels.watch/2` is unsupervised.** It uses `Task.start/1` and `unwatch/1`
395
+ is a raw `Process.exit(pid, :shutdown)`. Supervise it yourself if it matters.
396
+
397
+ ## Common pitfalls
398
+
399
+ | Pitfall | Why it bites |
400
+ | --- | --- |
401
+ | Copying the root `README.md` | Its module names do not exist at `v2.2.0` |
402
+ | Using `Chronicle.Concept` | It exists only in the unpublished `v2.3.0` tag |
403
+ | `{:cratis_chronicle, "~> 0.1"}` from the docs | Does not match the published `2.2.0` |
404
+ | Quoting the `VERSION` file | It is a `0.0.5` build-time placeholder |
405
+ | Putting configuration in `config.exs` | The client reads no application env; options are child-spec keywords |
406
+ | Omitting `:otp_app` | Discovery falls back to scanning every loaded module |
407
+ | Assuming the namespace default is `"default"` | It is `"Default"`; the moduledoc is wrong |
408
+ | `:ok = Chronicle.append(...)` in a server | A constraint violation returns `{:error, _}` and raises `MatchError` |
409
+ | Waiting for `:connected` | Observers attach at `:registered` |
410
+ | Reading `:correlation_id` from a reactor context | The map has five keys and that is not one |
411
+ | Two read models with the same final module segment | The id is the last segment only |
412
+ | Appending from a `Task` and expecting the correlation id | Ambient context is per process |
413
+ | Treating quiet observers as "no events" | Keepalive eviction silences observers while appends still succeed |
414
+ | Shipping the default TLS behavior | `skip_tls_validation` defaults to `true` |
415
+
416
+ ## Verify
417
+
418
+ - `mix deps.get` resolves `cratis_chronicle` to the `2.x` version you intended.
419
+ - `Chronicle.Client` is in the supervision tree with an explicit `:otp_app`, and
420
+ the tree starts clean.
421
+ - The lifecycle reaches `:registered` before the application claims readiness.
422
+ - A production connection string sets `skipTlsValidation=false`.
423
+ - Every event type module passes an explicit, stable `id:`.
424
+ - Every `Chronicle.append/3` call site handles `{:error, _}` as well as `:ok`.
425
+ - Every reactor's `@handles` list matches the clauses of its `handle/2`.
426
+ - Read model module names have distinct final segments, or explicit ids.
427
+ - Every code example was copied from `Documentation/client-snippets/`, not from
428
+ the README.
429
+ - `mix compile --warnings-as-errors` and `mix test` are clean.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.