@cratis/pi 7.56.3 → 7.58.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 (33) hide show
  1. package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +2 -2
  2. package/package/corpus/product-sync.json +11 -9
  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/corpus/skills/cratis-screenplay-automations-and-translations/references/translate-example.md +18 -16
  23. package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +1 -1
  24. package/package/corpus/skills/cratis-screenplay-model-authoring/references/screen-authoring-workflow.md +25 -0
  25. package/package/corpus/skills/cratis-screenplay-read-surface/SKILL.md +1 -1
  26. package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/invoicing-dues-design.md +7 -4
  27. package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/invoicing-dues-example.md +10 -7
  28. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.json +7460 -1159
  29. package/package/corpus/skills/cratis-screenplay-toolchain/references/screenplay-contract.version +1 -1
  30. package/package/corpus/skills/cratis-screenplay-ui-composition/SKILL.md +34 -2
  31. package/package/corpus/skills/cratis-screenplay-ui-composition/references/application-shell.md +301 -0
  32. package/package/corpus/skills/cratis-screenplay-ui-composition/verification.json +109 -6
  33. package/package.json +1 -1
@@ -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
  }
@@ -57,7 +57,7 @@ module Exams
57
57
  then denied
58
58
 
59
59
  slice Translate ImportProctorResults
60
- description "Per external field: reference -> correlation key (our attempt id, echoed); outcome -> translated to our enum; score -> kept; examinerNote -> ignored (free text, may hold personal data, no consumer). Repeated delivery: this example compares outcome only. An unchanged outcome appends nothing, even if the score changed, so it does not implement score corrections. OneResultPerAttempt refuses a second local result. Changed outcomes and score-only corrections need an explicit correction policy (open decision, recorded in STATE.md, not an overwrite) before this is a complete integration."
60
+ description "Per external field: reference -> correlation key (our attempt id, echoed); outcome -> translated to our enum; score -> kept; examinerNote -> ignored (free text, may hold personal data, no consumer). Repeated delivery: the first record of a key appends once (`when added`, Chronicle's first-observation rule); a later delivery of the same key appends nothing, even if the outcome or score changed, so it does not implement corrections. OneResultPerAttempt refuses a second local result. Changed outcomes and score-only corrections need an explicit correction policy (open decision, recorded in STATE.md, not an overwrite) before this is a complete integration."
61
61
  capture ProctorResults
62
62
  source webhook
63
63
  path /webhooks/proctoring/results
@@ -67,21 +67,9 @@ module Exams
67
67
  "P" => passed
68
68
  "F" => failed
69
69
  append ProctorResultReceived
70
- when outcome
70
+ when added
71
71
  outcome = $.outcome
72
72
  score = $.score
73
- reaction PassTranslator
74
- when ProctorResultReceived
75
- outcome
76
- score
77
- produces ExamPassed
78
- score = score
79
- where outcome == "passed"
80
- reaction FailTranslator
81
- when ProctorResultReceived
82
- outcome
83
- produces ExamFailed
84
- where outcome == "failed"
85
73
  event ProctorResultReceived
86
74
  outcome ProctorOutcome
87
75
  score Int
@@ -112,12 +100,26 @@ module Exams
112
100
  outcome = "failed"
113
101
  score = 40
114
102
  then error "Constraint 'OneResultPerAttempt' is violated: the event source already has the constrained event."
103
+
104
+ slice Automation TranslateProctorResults
105
+ description "Our facts from the provider's: the translators react to the external fact the Translate slice records"
106
+ reaction PassTranslator
107
+ when ProctorResultReceived
108
+ outcome
109
+ score
110
+ produces ExamPassed
111
+ score = score
112
+ where outcome == "passed"
113
+ reaction FailTranslator
114
+ when ProctorResultReceived
115
+ outcome
116
+ produces ExamFailed
117
+ where outcome == "failed"
115
118
  ```
116
119
 
117
120
  Notes for the reader:
118
121
 
119
- - A capture, the translator reactions and the events they append stay in one `Translate`
120
- slice. `then` lists every new fact, so `TranslatingAPass` names both the external fact and ours.
122
+ - A capture and the external fact it appends stay in the `Translate` slice; the translator reactions that answer that fact sit in an `Automation` slice beside it (a reaction in the capture's own directionless `Translate` slice is `PLAY0694`). `then` lists every new fact, so `TranslatingAPass` names both the external fact and ours.
121
123
  - The unauthenticated-booking spec needs an explicit `given caller` (empty means no caller);
122
124
  an authorized command exercised without one is PLAY0389 at binding.
123
125
  - Authenticity of the webhook, tenant ownership and a score-correction policy are not in the
@@ -244,7 +244,7 @@ Never invent commands, parameters, syntax nodes or downstream capabilities.
244
244
  | Commands, events, validation and concurrency | `cratis-screenplay-command-surface` |
245
245
  | Projections and reducers | `cratis-screenplay-projections` |
246
246
  | Read models, queries and screens | `cratis-screenplay-read-surface` |
247
- | Layouts, forms, contributions, component bindings, toolbars, outlets, themes and localization | `cratis-screenplay-ui-composition` |
247
+ | Layouts, forms, contributions, navigation regions, slice buttons, template references, sample data, component bindings, toolbars, outlets, themes and localization | `cratis-screenplay-ui-composition` |
248
248
  | Captures, reactions and triggers | `cratis-screenplay-captures-and-reactions` |
249
249
  | Behavioral examples and assertions | `cratis-screenplay-specifications` |
250
250
  | Rendering/running an admitted model | `cratis-stage-rendering-and-sandbox` |
@@ -11,6 +11,7 @@
11
11
  - 3. Review and apply with revision checks
12
12
  - 4. Validate authoring and executable readiness
13
13
  - 5. Run/render using the same configuration
14
+ - Edit the application shell
14
15
  - 6. Use Screenplay-owned fixtures
15
16
  - Replay the workflow in CI without a Screenplay checkout
16
17
  - Edit, render, run and see it in the browser
@@ -148,6 +149,30 @@ represented in the generated plan or runtime diagnostics. A consumer that cannot
148
149
  implement one reports a blocking diagnostic. Silent fallback to the default screen
149
150
  composition is a failure.
150
151
 
152
+ ## Edit the application shell
153
+
154
+ Navigation regions, toolbar buttons, blueprint template references and sample data are typed nodes and follow the same
155
+ open, read, propose, review, apply, reopen loop; the syntax and rules are in
156
+ `cratis-screenplay-ui-composition` (`references/application-shell.md`). The vectors to replay against are the
157
+ `screen-shell-navigation-positive` source case and its rejection vectors in `Cratis.Screenplay.CanonicalCorpus`.
158
+
159
+ 1. `read-ast` with `kind` `NavigationRegionSyntax`, `NavigationItemSyntax`, `ToolbarButtonSyntax`,
160
+ `TemplateAssignmentSyntax`, `ScreenTemplateReferenceSyntax` or `SampleDataPackSyntax`, `includeContent: true`.
161
+ Select the occurrence by its parent: a region belongs to the `LayoutSyntax` or a `ModuleSyntax`, and the same item id
162
+ can appear in several regions.
163
+ 2. `propose-ast` with `replace` (copy the returned node, change `mode`, `items`, `slice`, `version` or a row value),
164
+ `add` to a member the node kind declares (`navigation`, `items`, `children`, `buttons`) or `remove`. A member that
165
+ holds nothing is not in the node JSON; `add` still accepts it.
166
+ 3. Read the proposal's diagnostics before applying. A refusal such as `PLAY0750` (a module item targeting another
167
+ module), `PLAY0762` (a button on a State View slice), `PLAY0722` (a blueprint no profile lists) or `PLAY0714` (an
168
+ unknown active pack) is the answer; change the proposal, not the rule.
169
+ 4. `apply` with both revisions. A `StaleRevision` means the model moved: reopen, re-read the nodes and propose again.
170
+ 5. Switching a module from inherit to override copies the application's items; retarget each copy to the module's own
171
+ features first or the proposal is refused.
172
+
173
+ Requires Screenplay 4.188.0 or later (`screenplay --version`) to `add` the first region, button or sub-item to a member
174
+ the node JSON omits while empty; an older MCP server answers "Unknown typed syntax member".
175
+
151
176
  ## 6. Use Screenplay-owned fixtures
152
177
 
153
178
  The canonical multi-file application belongs to the Screenplay/conformance corpus,
@@ -317,6 +317,6 @@ Read [screen patterns](references/screen-patterns.md) when deciding whether a sc
317
317
  Attribution and licences: `references/provenance.md`.
318
318
 
319
319
  - How events fill the read model: `cratis-screenplay-projections`.
320
- - Layouts, templates, forms, component bindings, toolbars, outlets, contributions, packages and themes: `cratis-screenplay-ui-composition`.
320
+ - Layouts, templates, forms, component bindings, toolbars and slice buttons, outlets, contributions, navigation regions, sample data, packages and themes: `cratis-screenplay-ui-composition`.
321
321
  - Asserting query results: `cratis-screenplay-specifications`.
322
322
  - Deciding which read models exist: `cratis-screenplay-event-modeling`.
@@ -93,15 +93,18 @@ module Invoicing
93
93
  when outcome
94
94
  outcome = $.outcome
95
95
  reason = $.reason
96
+ event PaymentGatewayResultReceived
97
+ outcome GatewayOutcome
98
+ reason String
99
+ event PaymentReceived
100
+
101
+ slice Automation TranslatePaymentGatewayResults
102
+ description "Gives PaymentReceived its producer: the translator reacts to the provider's external fact"
96
103
  reaction ReceivedTranslator
97
104
  when PaymentGatewayResultReceived
98
105
  outcome
99
106
  produces PaymentReceived
100
107
  where outcome == "captured"
101
- event PaymentGatewayResultReceived
102
- outcome GatewayOutcome
103
- reason String
104
- event PaymentReceived
105
108
 
106
109
  slice StateChange SettleOrVoidInvoice
107
110
  description "An invoice ends either settled or voided, never both (SettleOrVoid, enforced at append). NOT enforced in the model today: that the invoice exists and is still open (settling) and that only an open, unpaid invoice may be voided. Stated as `reads OpenInvoice` + `require` (PLAY0271/PLAY0268 at binding; Screenplay#129). Target: Arc [ProtectedDecision] with DecisionRead, or a Chronicle DCB. Requirements to test in the target: settling an unknown invoice is rejected; voiding a paid invoice is rejected; voiding an open invoice succeeds."
@@ -205,9 +205,18 @@ module Invoicing
205
205
  "C" => captured
206
206
  "D" => declined
207
207
  append PaymentGatewayResultReceived
208
- when outcome
208
+ when added
209
209
  outcome = $.outcome
210
210
  reason = $.reason
211
+ event PaymentGatewayResultReceived
212
+ outcome GatewayOutcome
213
+ reason String
214
+ event PaymentReceived
215
+ event PaymentDeclined
216
+ reason String
217
+
218
+ slice Automation TranslatePaymentGatewayResults
219
+ description "Our facts from the provider's: the translators react to the external fact the Translate slice records"
211
220
  reaction ReceivedTranslator
212
221
  when PaymentGatewayResultReceived
213
222
  outcome
@@ -220,12 +229,6 @@ module Invoicing
220
229
  produces PaymentDeclined
221
230
  reason = reason
222
231
  where outcome == "declined"
223
- event PaymentGatewayResultReceived
224
- outcome GatewayOutcome
225
- reason String
226
- event PaymentReceived
227
- event PaymentDeclined
228
- reason String
229
232
 
230
233
  specification TranslatingADeclinedCardResult
231
234
  when capture PaymentGatewayResults