@cratis/pi 7.56.2 → 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.
- package/package/corpus/harnesses/copilot/instructions/elixir.instructions.md +2 -2
- package/package/corpus/product-sync.json +11 -9
- package/package/corpus/rules/elixir.md +2 -2
- package/package/corpus/skills/cratis-arc-command/SKILL.md +1 -1
- package/package/corpus/skills/cratis-arc-command/references/command-result.md +29 -0
- package/package/corpus/skills/cratis-arc-command/references/fresh-identifiers.md +65 -5
- package/package/corpus/skills/cratis-chronicle-client-elixir/SKILL.md +64 -87
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/append-api.md +10 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/captures.md +43 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/client-options.md +6 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/connections.md +1 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/constraints.md +63 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/decision-reads.md +65 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/discovery-and-generations.md +56 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/event-sources.md +1 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/idioms-and-inspection.md +2 -2
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/observability.md +37 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/patterns.md +22 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/pitfalls.md +9 -1
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/projections.md +28 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/read-models.md +41 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/testing.md +63 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/references/upgrading.md +18 -0
- package/package/corpus/skills/cratis-chronicle-client-elixir/verification.json +17 -4
- package/package/corpus/skills/cratis-chronicle-decision-reads/SKILL.md +11 -0
- package/package/corpus/skills/cratis-screenplay-render-and-gap-fill/SKILL.md +2 -2
- package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/scenario-catalogue.md +1 -1
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +6 -5
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/behavior-walk.md +2 -2
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/sandbox-and-specrunner.md +34 -2
- package/package.json +1 -1
|
@@ -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.
|
package/package/corpus/skills/cratis-chronicle-client-elixir/references/discovery-and-generations.md
ADDED
|
@@ -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.
|
|
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`.
|
package/package/corpus/skills/cratis-chronicle-client-elixir/references/idioms-and-inspection.md
CHANGED
|
@@ -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
|
-
- **
|
|
29
|
-
|
|
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
|
|
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.
|
|
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
|
|
12
|
-
{ "kind": "skill-contains", "value": "`
|
|
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
|
}
|
|
@@ -47,6 +47,17 @@ not a command-side alternative to returned guarded events. Check the client
|
|
|
47
47
|
package version; older custom units throw
|
|
48
48
|
`UnitOfWorkEnrollmentContributorsNotSupported` when attached.
|
|
49
49
|
|
|
50
|
+
Requires Arc 22.100.0 or later with Chronicle client 19.65.2 or later for Arc's
|
|
51
|
+
generated-identifier guard on direct unit-of-work and transactional writes,
|
|
52
|
+
including nested commands. Check `dotnet list package --include-transitive`;
|
|
53
|
+
older Arc needs returned events or an explicit whole-source `BeforeFirst` scope
|
|
54
|
+
for direct writes. A conflict against an enrolled generated guard is reported as
|
|
55
|
+
Arc's `ConflictingConcurrencyScopes`, without identifier or scope values, rather
|
|
56
|
+
than Chronicle's `ConflictingEnrollmentScope`; unrelated enrollment conflicts keep
|
|
57
|
+
the Chronicle failure. Read
|
|
58
|
+
[generated-target freshness](../cratis-arc-command/references/fresh-identifiers.md)
|
|
59
|
+
for the covered APIs, scope lifetime, rollback and immediate-append boundary.
|
|
60
|
+
|
|
50
61
|
## Route the work
|
|
51
62
|
|
|
52
63
|
- For Arc `[ProtectedDecision]`, Arc command/handler shapes and Arc `IStreamReads`,
|
|
@@ -157,8 +157,8 @@ Adapters and non-renderable scope follow the contract rules of `cratis-applicati
|
|
|
157
157
|
- `description` and `documentation` reach rendered code only as XML documentation comments (Stage#178),
|
|
158
158
|
never as behavior. A rule stated only in prose is unenforced: fallback record entry or target requirement,
|
|
159
159
|
never "delivered".
|
|
160
|
-
- The `cratis run` sandbox
|
|
161
|
-
is no evidence for denial behaviour.
|
|
160
|
+
- The `cratis run` sandbox's default engine enforces only simple literal validation rules and no
|
|
161
|
+
authorization; it is no evidence for denial behaviour or for any other rule.
|
|
162
162
|
- Never remove `pii`, `secret`, authorization or rules to get a render through; report the gap.
|
|
163
163
|
- Stop and ask only on a genuine contradiction between model, specs, descriptions and declared
|
|
164
164
|
contracts, after reading the slice fully (`cratis-screenplay-modeling-lifecycle` `references/stop-or-assume.md`).
|
package/package/corpus/skills/cratis-screenplay-scenario-coverage/references/scenario-catalogue.md
CHANGED
|
@@ -102,7 +102,7 @@ name them as unfinished.
|
|
|
102
102
|
matching value. Both are needed. Ownership checks on text, Uuid, Date and DateTime subjects render;
|
|
103
103
|
other subjects are STAGE-ESM-015: note it.
|
|
104
104
|
- Traps: `then error` for a denial; a denial spec whose inputs also break a rule (it still
|
|
105
|
-
denies, but reads ambiguously); trusting a `cratis run` sandbox answer (the default engine
|
|
105
|
+
denies, but reads ambiguously); trusting a `cratis run` sandbox answer (the default engine enforces no authorization;
|
|
106
106
|
the opt-in semantic engine enforces both but reads forgeable caller headers, so it is no security boundary); counting another command's denial.
|
|
107
107
|
|
|
108
108
|
## 4. State violation
|
|
@@ -185,9 +185,10 @@ Silent fallback to a default screen is a failure, not a partial render.
|
|
|
185
185
|
`cratis run [PATH]` (Docker required) starts `cratis/stage` on the folder or file: Stage
|
|
186
186
|
API on `9090`, Chronicle Workbench on `https://localhost:35000`. It is a **partial,
|
|
187
187
|
disposable runtime**, not a generated application. The default engine appends modeled
|
|
188
|
-
`produces` facts and echoes the payload
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
`produces` facts and echoes the payload. It refuses missing required properties and
|
|
189
|
+
simple literal `validate` rules on top-level properties, but enforces no other rule, no
|
|
190
|
+
constraint and no command or query authorization; `Stage__Runtime__Engine=semantic` opts
|
|
191
|
+
in to an engine that does, and refuses what it cannot execute (HTTP 501). The default engine
|
|
191
192
|
also refuses routed models (`STAGE-ESM-030`). Callers are built from unsigned headers, so
|
|
192
193
|
authorization there is not a security boundary.
|
|
193
194
|
|
|
@@ -229,8 +230,8 @@ to disk with no managed staging and no safe stale-file removal; a failure can le
|
|
|
229
230
|
managed output and never a weakened model.
|
|
230
231
|
- No file under `.cratis-render/` is staged; managed files carry no local edits.
|
|
231
232
|
- Stage's plan is deterministic (same bytes and hashes); that is not a review, and Stage never reviews, stages or approves anything.
|
|
232
|
-
- Sandbox expectations account for
|
|
233
|
-
authorization
|
|
233
|
+
- Sandbox expectations account for the rules, constraints, authorization and query
|
|
234
|
+
authorization the default engine does not enforce.
|
|
234
235
|
- Runtime screen smoke coverage uses the same model root or workspace export as
|
|
235
236
|
`cratis render`, names package/image versions, and fails on dropped authored UI
|
|
236
237
|
or silent default composition.
|
package/package/corpus/skills/cratis-stage-rendering-and-sandbox/references/behavior-walk.md
CHANGED
|
@@ -13,8 +13,8 @@ V5 evidence.
|
|
|
13
13
|
- Docker, and the `cratis/stage` image at the tag `cratis run` would use (by default the
|
|
14
14
|
Stage version the CLI renders with).
|
|
15
15
|
- The **semantic engine**. `cratis run` has no `--engine` option and passes no environment
|
|
16
|
-
to the container, so it starts the default EventModel engine, which enforces
|
|
17
|
-
validation
|
|
16
|
+
to the container, so it starts the default EventModel engine, which enforces only
|
|
17
|
+
simple literal validation rules, no authorization, and refuses routed models. Start the same image directly:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
docker run --rm -p 127.0.0.1:9090:9090 -p 127.0.0.1:35000:35000 \
|