@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
|
@@ -12,13 +12,13 @@ paths:
|
|
|
12
12
|
# Elixir Conventions
|
|
13
13
|
|
|
14
14
|
Use Elixir and OTP idioms, not translated C# or JVM shapes. These conventions
|
|
15
|
-
are grounded in `Cratis/Chronicle.Elixir v3.
|
|
15
|
+
are grounded in `Cratis/Chronicle.Elixir v3.13.0` (`Source/chronicle/mix.exs`,
|
|
16
16
|
`.formatter.exs`, and `lib/chronicle/`). Client-specific APIs belong in the
|
|
17
17
|
**cratis-chronicle-client-elixir** skill.
|
|
18
18
|
|
|
19
19
|
## Build and format
|
|
20
20
|
|
|
21
|
-
- For Chronicle 3.
|
|
21
|
+
- For Chronicle 3.13.x use Elixir 1.18 or later (`~> 1.18`).
|
|
22
22
|
- Run `mix compile --warnings-as-errors`, `mix test`, and
|
|
23
23
|
`mix format --check-formatted` with the project's formatter inputs.
|
|
24
24
|
- Use two-space indentation and let `mix format` own layout.
|
|
@@ -63,9 +63,9 @@
|
|
|
63
63
|
}
|
|
64
64
|
],
|
|
65
65
|
"synced": {
|
|
66
|
-
"version": "22.
|
|
67
|
-
"date": "2026-10-
|
|
68
|
-
"by": "Cratis/AI#
|
|
66
|
+
"version": "22.100.0",
|
|
67
|
+
"date": "2026-10-15",
|
|
68
|
+
"by": "Cratis/AI#710"
|
|
69
69
|
},
|
|
70
70
|
"covers": {
|
|
71
71
|
"skills": [
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"vertical-slices.md"
|
|
99
99
|
]
|
|
100
100
|
},
|
|
101
|
-
"notes": "Release-tag delta through 22.
|
|
101
|
+
"notes": "Release-tag delta through 22.100.0: scoped completion and routing validation, generated-target freshness (including direct unit-of-work writes and nested commands), observable scenarios, asynchronous execution scopes, read-model diagnostics, ordered reactor refusal handling and ICommandExceptionMapper. Existing context generation/self-reduction guidance retained; Screenplay extraction deltas handed to its owning lane. No new runtime probe."
|
|
102
102
|
},
|
|
103
103
|
{
|
|
104
104
|
"id": "ai",
|
|
@@ -579,17 +579,19 @@
|
|
|
579
579
|
}
|
|
580
580
|
],
|
|
581
581
|
"synced": {
|
|
582
|
-
"version": "3.
|
|
583
|
-
"date": "2026-10-
|
|
584
|
-
"by": "Cratis/AI#
|
|
582
|
+
"version": "3.13.0",
|
|
583
|
+
"date": "2026-10-15",
|
|
584
|
+
"by": "Cratis/AI#719"
|
|
585
585
|
},
|
|
586
586
|
"covers": {
|
|
587
587
|
"skills": [
|
|
588
588
|
"cratis-chronicle-client-elixir"
|
|
589
589
|
],
|
|
590
|
-
"rules": [
|
|
590
|
+
"rules": [
|
|
591
|
+
"elixir.md"
|
|
592
|
+
]
|
|
591
593
|
},
|
|
592
|
-
"notes": "
|
|
594
|
+
"notes": "Synced to the v3.13.0 tag: decision reads, Chronicle.Testing, captures, discovery drift, standalone constraints, children collections, concurrency scopes and observability."
|
|
593
595
|
},
|
|
594
596
|
{
|
|
595
597
|
"id": "authproxy",
|
|
@@ -11,13 +11,13 @@ paths:
|
|
|
11
11
|
# Elixir Conventions
|
|
12
12
|
|
|
13
13
|
Use Elixir and OTP idioms, not translated C# or JVM shapes. These conventions
|
|
14
|
-
are grounded in `Cratis/Chronicle.Elixir v3.
|
|
14
|
+
are grounded in `Cratis/Chronicle.Elixir v3.13.0` (`Source/chronicle/mix.exs`,
|
|
15
15
|
`.formatter.exs`, and `lib/chronicle/`). Client-specific APIs belong in the
|
|
16
16
|
**cratis-chronicle-client-elixir** skill.
|
|
17
17
|
|
|
18
18
|
## Build and format
|
|
19
19
|
|
|
20
|
-
- For Chronicle 3.
|
|
20
|
+
- For Chronicle 3.13.x use Elixir 1.18 or later (`~> 1.18`).
|
|
21
21
|
- Run `mix compile --warnings-as-errors`, `mix test`, and
|
|
22
22
|
`mix format --check-formatted` with the project's formatter inputs.
|
|
23
23
|
- Use two-space indentation and let `mix format` own layout.
|
|
@@ -246,7 +246,7 @@ whatever renders it:
|
|
|
246
246
|
|
|
247
247
|
Read [references/client-contract.md](references/client-contract.md) for the generated proxy members.
|
|
248
248
|
|
|
249
|
-
Read `references/command-result.md` when branching on a command result
|
|
249
|
+
Read `references/command-result.md` when branching on a command result, matching validation failures to fields, or mapping integration exceptions. Branch on the specific flag, not only on `isSuccess`. The exact `CommandResult`
|
|
250
250
|
and `ValidationResult` shapes are in
|
|
251
251
|
[command result](references/command-result.md) — in particular, a validation
|
|
252
252
|
failure carries `members: string[]` (camelCased) and a numeric `severity`, not a
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
## Contents
|
|
4
4
|
|
|
5
5
|
- Server side
|
|
6
|
+
- Map integration exceptions
|
|
6
7
|
- Severity filtering
|
|
7
8
|
- Client side
|
|
8
9
|
- Executing with a severity
|
|
@@ -40,6 +41,34 @@ every execution scope has completed and the result is not successful, so a
|
|
|
40
41
|
transaction that rolls back after `Handle` returned a value does not leak that
|
|
41
42
|
value into the error body.
|
|
42
43
|
|
|
44
|
+
## Map integration exceptions
|
|
45
|
+
|
|
46
|
+
Requires Arc 22.100.0 or later for `Cratis.Arc.Commands.ICommandExceptionMapper`.
|
|
47
|
+
Check `dotnet list package --include-transitive`; older Arc has no mapper hook
|
|
48
|
+
and uses `CommandResult.FromException` for unhandled execution exceptions.
|
|
49
|
+
|
|
50
|
+
Implement the interface for an integration's typed failures, not ordinary
|
|
51
|
+
user-correctable rejections (return validation results for those):
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
bool TryMap(CommandContext context, Exception exception,
|
|
55
|
+
[NotNullWhen(true)] out CommandResult? result);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`NotNullWhen` is from `System.Diagnostics.CodeAnalysis`. Return `true` with a
|
|
59
|
+
non-null **failure** preserving `context.CorrelationId`, or `false` with `null`
|
|
60
|
+
to decline. Match exception types/properties rather than parsing messages.
|
|
61
|
+
Implementations are convention-discovered through `IInstancesOf<ICommandExceptionMapper>`
|
|
62
|
+
(`Cratis.Types`) and resolved from the command's service scope. The first accepting
|
|
63
|
+
mapper wins. Mapping runs for exceptions escaping command execution or response
|
|
64
|
+
processing, before execution scopes complete, so a mapped failure still rolls
|
|
65
|
+
back pending work. If no mapper accepts, the pipeline falls back to
|
|
66
|
+
`CommandResult.FromException`; a throwing mapper produces `CommandResult.Error`
|
|
67
|
+
and scopes still complete with
|
|
68
|
+
failure. This hook does not replace validation-only or scope-completion exception
|
|
69
|
+
handling. Specify accepting, declining and throwing cases and assert rollback
|
|
70
|
+
and correlation preservation, not only the message.
|
|
71
|
+
|
|
43
72
|
## Severity filtering
|
|
44
73
|
|
|
45
74
|
`ValidationResultSeverity`: `Unknown = 0`, `Information = 1`, `Warning = 2`,
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
# Fresh generated append targets
|
|
5
5
|
|
|
6
|
+
## Contents
|
|
7
|
+
|
|
8
|
+
- Direct writes and nested commands
|
|
9
|
+
- Opt out deliberately
|
|
10
|
+
- Specify the collision
|
|
11
|
+
|
|
6
12
|
Requires Arc 22.95.0 and Chronicle client 19.59.4 for guarded generated targets; deploy a Chronicle kernel 19.40.1 or later for retry-time emptiness checks. Check package references and the deployed kernel version before relying on the guard; older Arc needs an explicit whole-source `BeforeFirst` scope.
|
|
7
13
|
|
|
8
14
|
A `[GeneratedValue]` parameter defaults to `GeneratedValueFreshness.Guarded`.
|
|
@@ -10,9 +16,12 @@ When an Arc-owned append targets a validated value generated in that invocation,
|
|
|
10
16
|
it requires the **whole event source** to be empty, including other streams and
|
|
11
17
|
event types. Payload-only and response-only generated values add no scope.
|
|
12
18
|
Returned events, tuples, wrappers, `EventsWithConcurrencyScopes` and aggregates
|
|
13
|
-
acquired for that generated identifier are covered.
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
acquired for that generated identifier are covered.
|
|
20
|
+
|
|
21
|
+
Requires Arc 22.100.0 or later and Chronicle client 19.65.2 or later for direct
|
|
22
|
+
unit-of-work writes and redacted enrollment-conflict translation. Check resolved
|
|
23
|
+
versions with `dotnet list package --include-transitive`; older Arc needs returned
|
|
24
|
+
events or an explicit whole-source `BeforeFirst` scope for direct writes.
|
|
16
25
|
|
|
17
26
|
An occupied destination returns `ValidationResultReason.ConcurrencyViolation`
|
|
18
27
|
with `ReasonDetail` `GeneratedIdentifierCollision`. Arc does not rerun `Handle()`.
|
|
@@ -25,6 +34,56 @@ explicit scope, including `ConcurrencyScope.None` or a narrowed scope, fails wit
|
|
|
25
34
|
generated source's returned events contiguous; an interleaved shape that would
|
|
26
35
|
split its guard fails with `GeneratedIdentifierEventsCannotBeSplit`.
|
|
27
36
|
|
|
37
|
+
## Direct writes and nested commands
|
|
38
|
+
|
|
39
|
+
The same guard covers these writes on the **command's actual unit of work**:
|
|
40
|
+
|
|
41
|
+
- `IUnitOfWork.AddEvent`, `AddEventWithNamedTags` and `AddEvents`.
|
|
42
|
+
- `eventLog.Transactional.Append`, `AppendMany`, `AppendWithNamedTags` and
|
|
43
|
+
`AppendManyWithNamedTags`.
|
|
44
|
+
|
|
45
|
+
Returned, aggregate and direct events share one whole-source expectation. An
|
|
46
|
+
occupied generated destination rejects the whole pending unit, including events
|
|
47
|
+
for other sources, with the collision result above. Arbitrary new units and
|
|
48
|
+
external stores are outside this protection.
|
|
49
|
+
|
|
50
|
+
Nested pipeline invocations share the outer unit and leave completion to its
|
|
51
|
+
owner. Guarded captures from every active invocation participate, so a nested
|
|
52
|
+
handler's attribute-derived scope cannot narrow an outer generated guard.
|
|
53
|
+
Propagate nested rejection so the outer command rolls back earlier events.
|
|
54
|
+
An explicit aggregate `Commit()` can still complete the shared unit early.
|
|
55
|
+
|
|
56
|
+
After enrollment, the required scope survives disposal of the invocation's
|
|
57
|
+
contributor; later unscoped writes inherit it. An incompatible supplied scope
|
|
58
|
+
against that recorded generated guard maps to `ConflictingConcurrencyScopes`,
|
|
59
|
+
even if Chronicle rejects it before invoking contributors. Neither collision nor
|
|
60
|
+
scope-conflict messages expose the identifier or scope values. Arc uses typed
|
|
61
|
+
scope properties and recorded guard provenance, not message parsing; unrelated
|
|
62
|
+
enrollment conflicts retain Chronicle's redacted `ConflictingEnrollmentScope`.
|
|
63
|
+
Scope-only `AddEvents` entries preserve existing requirements but do not invoke
|
|
64
|
+
contributors or establish a new generated guard. A completed nested invocation's
|
|
65
|
+
untargeted capture leaves no enrolled requirement.
|
|
66
|
+
|
|
67
|
+
Custom units must support enrollment contributors. A guarded generated parameter
|
|
68
|
+
fails closed with `FreshIdentifierGuardRequiresEnrollmentContributors` if the unit
|
|
69
|
+
cannot register one; no generated parameters or only `Unguarded` parameters do not
|
|
70
|
+
require that support.
|
|
71
|
+
|
|
72
|
+
`AddEvents` checks all destinations before staging that call; a scope conflict
|
|
73
|
+
stages none of its events. Transactional `AppendMany` and
|
|
74
|
+
`AppendManyWithNamedTags` stage per event, so a later rejection can leave earlier
|
|
75
|
+
events pending. Let the exception escape `Handle()` to roll back the unit;
|
|
76
|
+
catching it and succeeding can commit those earlier events.
|
|
77
|
+
|
|
78
|
+
Immediate `eventLog.Append`, `AppendMany` and their named-tag variants bypass
|
|
79
|
+
enrollment, cannot be rolled back and receive **no automatic freshness guard**.
|
|
80
|
+
Prefer returning events. An integration needing direct writes must use the
|
|
81
|
+
command's transactional unit; an immediate caller needs an explicit whole-source
|
|
82
|
+
`BeforeFirst` scope and must inspect the append result, without automatic
|
|
83
|
+
`GeneratedIdentifierCollision` classification.
|
|
84
|
+
|
|
85
|
+
## Opt out deliberately
|
|
86
|
+
|
|
28
87
|
Only when appending to an existing generated destination is intentional, use this
|
|
29
88
|
parameter attribute fragment (imports `Cratis.Arc.Commands.ModelBound` and
|
|
30
89
|
`Cratis.Arc.Commands`):
|
|
@@ -42,8 +101,9 @@ use an appropriate constraint or explicit concurrency scope for them.
|
|
|
42
101
|
Use `new CommandScenario<TCommand>().UseDecisionReads()` **before seeding**, then
|
|
43
102
|
`PinGeneratedValue(name, id)` and `Given.ForEventSource(id).Events(existingEvent)`.
|
|
44
103
|
Assert the concurrency reason and collision detail, unchanged append tail and
|
|
45
|
-
one handler execution. Also cover an empty source receiving two events
|
|
46
|
-
occupied source whose events belong to another stream
|
|
104
|
+
one handler execution. Also cover an empty source receiving two events through
|
|
105
|
+
the chosen write API, an occupied source whose events belong to another stream,
|
|
106
|
+
mixed returned/direct writes, and a nested rejection rolling back the outer unit.
|
|
47
107
|
|
|
48
108
|
Requires Arc 22.95.1 for `GeneratedIdentifierCollisionRequiresDecisionReads`:
|
|
49
109
|
default scenario mode fails fast when given events and a guarded pin share an id.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cratis-chronicle-client-elixir
|
|
3
|
-
description: Talk to a Chronicle server from an Elixir application with the cratis_chronicle Hex package - Chronicle.Client supervision and connection lifecycle, EventType structs, append errors, event sources and streams, Chronicle.Concept, PII and encrypted secrets, reactors, read models/projections with composite keys, variants and GlobalHandler, and observer inspection. Use when an OTP application appends to or observes a Chronicle event store. Do not use for the .NET, TypeScript, or Kotlin clients.
|
|
3
|
+
description: Talk to a Chronicle server from an Elixir application with the cratis_chronicle Hex package - Chronicle.Client supervision and connection lifecycle, EventType structs, append errors, event sources and streams, Chronicle.Concept, PII and encrypted secrets, reactors, read models/projections with composite keys, children, variants and GlobalHandler, decision reads, in-memory testing, captures, constraints, discovery drift and observer inspection. Use when an OTP application appends to or observes a Chronicle event store. Do not use for the .NET, TypeScript, or Kotlin clients.
|
|
4
4
|
license: MIT
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -17,23 +17,11 @@ to yours, and everything after that is ordinary OTP.
|
|
|
17
17
|
|
|
18
18
|
Source: `Cratis/Chronicle.Elixir` (Hex package `cratis_chronicle`). Source paths below are relative to `Source/chronicle/`.
|
|
19
19
|
|
|
20
|
-
Read [references/upgrading.md](references/upgrading.md) when moving an application from an older client
|
|
20
|
+
Read [references/upgrading.md](references/upgrading.md) when moving an application from an older client. Requires Elixir `~> 1.18`; read [references/product-sources.md](references/product-sources.md) for the package and contracts dependency facts.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
> **A git tag is not a Hex release.** `mix deps.get` fetches only published versions, and the repository's `VERSION` file is a build-time placeholder: never quote it. Use namespaced modules (`Chronicle.Events.EventType`, not `Chronicle.EventType`); older READMEs got this wrong. Maintained snippets live in `Documentation/client-snippets/`.
|
|
23
23
|
|
|
24
|
-
|
|
25
|
-
> (`use Chronicle.Concept, type: :string`, a single-value concept that carries its
|
|
26
|
-
> `pii` classification once, mirroring `ConceptAs<T>`) is the preferred way to
|
|
27
|
-
> classify a personal value; a client without it annotates fields with `pii/1,2`.
|
|
28
|
-
|
|
29
|
-
Requires `cratis_chronicle` 3.0.0 for `Chronicle.Concept`.
|
|
30
|
-
|
|
31
|
-
> **The repository's `VERSION` file is a build-time placeholder** overwritten by the publish workflow. Never quote it.
|
|
32
|
-
|
|
33
|
-
> Use namespaced modules (`Chronicle.Events.EventType`, not `Chronicle.EventType`); older READMEs got this wrong. Maintained
|
|
34
|
-
> snippets live in `Documentation/client-snippets/`.
|
|
35
|
-
|
|
36
|
-
Select the published version from hex.pm rather than treating a guide's range as a pin.
|
|
24
|
+
`Chronicle.Concept` (`use Chronicle.Concept, type: :string`) is the preferred way to classify a personal value once, mirroring `ConceptAs<T>`; it requires `cratis_chronicle` 3.0.0, and an older client annotates fields with `pii/1,2`.
|
|
37
25
|
|
|
38
26
|
## Adding it
|
|
39
27
|
|
|
@@ -41,7 +29,7 @@ Select the published version from hex.pm rather than treating a guide's range as
|
|
|
41
29
|
# mix.exs
|
|
42
30
|
defp deps do
|
|
43
31
|
[
|
|
44
|
-
{:cratis_chronicle, "~> 3.
|
|
32
|
+
{:cratis_chronicle, "~> 3.13"}
|
|
45
33
|
]
|
|
46
34
|
end
|
|
47
35
|
```
|
|
@@ -104,17 +92,14 @@ case Lifecycle.wait_until(Lifecycle.name_for(Chronicle.Client), :registered, 30_
|
|
|
104
92
|
end
|
|
105
93
|
```
|
|
106
94
|
|
|
107
|
-
`wait_until(lifecycle, target_phase, timeout \\ 30_000)` is the blocking wait
|
|
108
|
-
`subscribe/1` returns `{:ok, phase, connection_id}` **and** sends it as a message
|
|
109
|
-
|
|
110
|
-
read.
|
|
95
|
+
`wait_until(lifecycle, target_phase, timeout \\ 30_000)` is the blocking wait;
|
|
96
|
+
`subscribe/1` returns `{:ok, phase, connection_id}` **and** sends it as a message
|
|
97
|
+
(closing the subscribe-after-transition race); `phase/1` is the plain read.
|
|
111
98
|
|
|
112
|
-
Reconnect never gives up
|
|
113
|
-
|
|
114
|
-
Registration failures leave the client in `:connected`, log
|
|
99
|
+
Reconnect never gives up (backoff 1000 to 10000 ms; `:retry_attempts` is
|
|
100
|
+
ignored). Registration failures leave the client in `:connected`, log
|
|
115
101
|
`Chronicle registration failed`, and retry every five seconds. Appends before a
|
|
116
|
-
channel exists return `{:error, :not_connected}
|
|
117
|
-
30 seconds for `:registered`.
|
|
102
|
+
channel exists return `{:error, :not_connected}`.
|
|
118
103
|
|
|
119
104
|
## Authentication and TLS
|
|
120
105
|
|
|
@@ -154,24 +139,23 @@ defaults: `nil` registers as string, numbers as number, booleans as boolean;
|
|
|
154
139
|
list element schemas use the first default element. Nested structs and concepts
|
|
155
140
|
are descended into (`schemas/json_schema_generator.ex`).
|
|
156
141
|
|
|
157
|
-
The macro imports
|
|
158
|
-
`
|
|
159
|
-
`
|
|
160
|
-
`
|
|
161
|
-
|
|
162
|
-
`@remove_constraint` also work.
|
|
142
|
+
The macro imports `unique/1,2`, `unique_event_type/0,1`, `remove_constraint/1,2`,
|
|
143
|
+
`closes_stream/1`, `Chronicle.Compliance.pii/1,2` and
|
|
144
|
+
`Chronicle.Confidentiality.encrypted/1,2,3`; the accumulating attributes
|
|
145
|
+
`@unique`, `@unique_event_type`, `@remove_constraint` also work. Read
|
|
146
|
+
[references/constraints.md](references/constraints.md) for their options.
|
|
163
147
|
|
|
164
148
|
**Only top-level event keys are camelCased.** `encode_event/1` transforms them
|
|
165
149
|
before `Jason.encode!` (`event_sequences/event_log.ex`); nested keys are
|
|
166
150
|
not transformed. Event structs get no `Jason.Encoder` implementation;
|
|
167
|
-
`Chronicle.Concept` does, encoding its wrapped value
|
|
151
|
+
`Chronicle.Concept` does, encoding its wrapped value.
|
|
168
152
|
|
|
169
153
|
Read [references/compliance.md](references/compliance.md) when defining personal data, data subjects, concepts or encrypted operational secrets. `pii` and `encrypted` are not interchangeable and cannot mark the same leaf.
|
|
170
154
|
|
|
171
155
|
Schema evolution is
|
|
172
156
|
`use Chronicle.Events.Migration, from: {Mod, generation: n}, to: {Mod, generation: n + 1}`
|
|
173
|
-
with `upcast/1` and `downcast/1`; the generations must be exactly one apart
|
|
174
|
-
|
|
157
|
+
with `upcast/1` and `downcast/1`; the generations must be exactly one apart. Mark
|
|
158
|
+
the older module with `generation_for:`, not a second `:id`.
|
|
175
159
|
|
|
176
160
|
## Appending
|
|
177
161
|
|
|
@@ -183,40 +167,30 @@ case Chronicle.append("account-1", %MyApp.Events.FundsDeposited{amount: 50}) do
|
|
|
183
167
|
end
|
|
184
168
|
```
|
|
185
169
|
|
|
186
|
-
`Chronicle.append/3` delegates to `Chronicle.EventSequences.EventLog
|
|
187
|
-
(`lib/chronicle.ex`):
|
|
188
|
-
|
|
189
|
-
```elixir
|
|
190
|
-
@spec append(String.t(), struct(), keyword()) :: :ok | {:error, term()}
|
|
191
|
-
```
|
|
192
|
-
|
|
170
|
+
`Chronicle.append/3` delegates to `Chronicle.EventSequences.EventLog`.
|
|
193
171
|
**The event source id comes first, the event second.** The return is a bare
|
|
194
|
-
`:ok`,
|
|
195
|
-
|
|
196
|
-
the outcome.
|
|
172
|
+
`:ok`, carrying no sequence number; use `append_with_result/3` or
|
|
173
|
+
`append_and_wait_for_completion/3` when you need the outcome.
|
|
197
174
|
|
|
198
175
|
Read [references/append-api.md](references/append-api.md) when you need an append variant, a read-back function, or an append option such as `:event_stream_type`, `:concurrency_scope` or `:tags`.
|
|
199
176
|
|
|
200
177
|
Requires `cratis_chronicle` 3.2.0 for rejected appends to return `{:error, reason}`; an older client could report a rejection as `:ok`.
|
|
201
178
|
|
|
202
|
-
Failed appends return `{:error, reason}
|
|
179
|
+
Failed appends return `{:error, reason}`: `{:constraint_violations, violations}`,
|
|
203
180
|
`{:concurrency_violations, violations}`, `{:append_errors, errors}`,
|
|
204
|
-
`:not_connected`, and `{:incompatible_server, response}`
|
|
205
|
-
compatibility preflight per channel. Pair the kernel and
|
|
206
|
-
do not blindly retry a timed-out write.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
a constraint violation, which is a fine choice in a script and the wrong one in a
|
|
210
|
-
GenServer.
|
|
181
|
+
`{:unknown_event_type, module}`, `:not_connected`, and `{:incompatible_server, response}`
|
|
182
|
+
from the first append's compatibility preflight per channel. Pair the kernel and
|
|
183
|
+
installed contracts; do not blindly retry a timed-out write. **Match on them**: a
|
|
184
|
+
bare `:ok = Chronicle.append(...)` raises a `MatchError` on a violation, fine in a
|
|
185
|
+
script and wrong in a GenServer.
|
|
211
186
|
|
|
212
187
|
Read [references/event-sources.md](references/event-sources.md) when declaring named event sources/streams, deriving concurrency scopes or routing mixed-source batches; definitions have client and kernel requirements stated there.
|
|
213
188
|
|
|
214
|
-
A unit of work is a process: `Chronicle.begin_unit_of_work/1` starts an `Agent
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
`
|
|
218
|
-
|
|
219
|
-
failure. Source/stream options apply to buffered appends too.
|
|
189
|
+
A unit of work is a process: `Chronicle.begin_unit_of_work/1` starts an `Agent`;
|
|
190
|
+
while one is current for the calling process, `append`/`append_many` buffer until
|
|
191
|
+
`UnitOfWork.commit/1` (`:ok | {:error, reason}`) or `rollback/1`.
|
|
192
|
+
`Chronicle.with_unit_of_work(fun, opts)` commits on return and raises on commit
|
|
193
|
+
failure.
|
|
220
194
|
|
|
221
195
|
## Observing
|
|
222
196
|
|
|
@@ -224,11 +198,8 @@ failure. Source/stream options apply to buffered appends too.
|
|
|
224
198
|
|
|
225
199
|
Read [references/reactor-example.md](references/reactor-example.md) when you need a worked reactor module.
|
|
226
200
|
|
|
227
|
-
This follows `Documentation/client-snippets/get-started/common/reactor.md`.
|
|
228
|
-
|
|
229
201
|
- **`@handles` is an accumulating module attribute** declaring the subscription;
|
|
230
|
-
dispatch is then ordinary Elixir pattern matching in `handle/2`.
|
|
231
|
-
Elixir answer to the other clients' method-name or parameter-type conventions.
|
|
202
|
+
dispatch is then ordinary Elixir pattern matching in `handle/2`.
|
|
232
203
|
- The callback is
|
|
233
204
|
`handle(event :: struct(), context :: map()) :: :ok | {:error, term()} | {:ok, struct() | [struct()]}`.
|
|
234
205
|
- **Returning `{:ok, event_or_events}` appends those events as a side effect** —
|
|
@@ -239,28 +210,27 @@ This follows `Documentation/client-snippets/get-started/common/reactor.md`.
|
|
|
239
210
|
- Optional replay callbacks: `on_replay_begin/0`, `on_replay_end/0`,
|
|
240
211
|
`on_partition_replay_begin/1`, `on_partition_replay_end/1`.
|
|
241
212
|
|
|
242
|
-
**The
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
source name or `nil`
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
213
|
+
**The context is a `Chronicle.Events.EventContext` struct** (requires 3.11.0; a
|
|
214
|
+
bare map before). Pattern match on keys or use `context.caused_by`. It carries
|
|
215
|
+
`event_source_id`, `sequence_number`, `occurred`, `event_source` (the registered
|
|
216
|
+
source name or `nil`), `event_store`, `namespace`, `correlation_id`, `caused_by`,
|
|
217
|
+
`causation`, `tags`, `subject`, `appended_generation`, `replaying?` and, for
|
|
218
|
+
reactors only, `delivery_id` (stable across replay and failed-partition
|
|
219
|
+
recovery; key a receipt on it). Reducers get the same struct with
|
|
220
|
+
`observation_state` set (`lib/chronicle/events/event_context.ex`).
|
|
249
221
|
|
|
250
222
|
### Read models and projections
|
|
251
223
|
|
|
252
|
-
Read [references/reactor-example.md](references/reactor-example.md#read-model-example) for a worked model-bound read model.
|
|
253
|
-
|
|
254
|
-
This is the model-bound shape; use typed defaults for its registered schema.
|
|
224
|
+
Read [references/reactor-example.md](references/reactor-example.md#read-model-example) for a worked model-bound read model with typed defaults.
|
|
255
225
|
|
|
256
|
-
The DSL macros are `from/1
|
|
226
|
+
The DSL macros are `from/1,2`, `join/2`, `removed_with/2`, `from_every/1`, `children/2,3`.
|
|
257
227
|
`from/2` options: `:key` (defaults to `"$eventSourceId"`), `:parent_key`, `:set`,
|
|
258
228
|
`:add`, `:subtract`, `:count`. `join/2` requires `:on`.
|
|
259
229
|
|
|
260
|
-
Read [references/projections.md](references/projections.md) when choosing `passive`, `not_rewindable`, `event_sequence`, `no_auto_map`, composite/event-context keys or
|
|
230
|
+
Read [references/projections.md](references/projections.md) when choosing `passive`, `not_rewindable`, `event_sequence`, `no_auto_map`, composite/event-context keys, dictionary counters or `children/2,3` collections. Snake_case field atoms now map correctly; no camelCase-string workaround is needed.
|
|
261
231
|
|
|
262
232
|
> **The read model id sent to the kernel is the last module segment** —
|
|
263
|
-
> `Module.split() |> List.last()` at `
|
|
233
|
+
> `Module.split() |> List.last()` at `read_models/read_model.ex`.
|
|
264
234
|
> `MyApp.ReadModels.Account` registers as `"Account"`, so two read models with the
|
|
265
235
|
> same final segment collide. Pass `id:` to disambiguate.
|
|
266
236
|
|
|
@@ -283,10 +253,8 @@ Querying (`lib/chronicle/read_models.ex`):
|
|
|
283
253
|
{:ok, account} = Chronicle.read_model(MyApp.ReadModels.AccountBalance, "account-1")
|
|
284
254
|
```
|
|
285
255
|
|
|
286
|
-
`Chronicle.read_model/3`
|
|
287
|
-
|
|
288
|
-
`Chronicle.all/2` delegates to `ReadModels.all/2`
|
|
289
|
-
(`lib/chronicle.ex`, `read_models.ex`).
|
|
256
|
+
`Chronicle.read_model/3` returns `{:ok, struct() | nil} | {:error, term()}`; `nil`
|
|
257
|
+
means absent **or not projected yet**.
|
|
290
258
|
|
|
291
259
|
Use `Chronicle.ReadModels.query(MyApp.ReadModels.AccountBalance, page: 0)` for paging:
|
|
292
260
|
`:page` is zero-based (default `0`), `:page_size` defaults to `50`, and
|
|
@@ -294,18 +262,26 @@ Use `Chronicle.ReadModels.query(MyApp.ReadModels.AccountBalance, page: 0)` for p
|
|
|
294
262
|
`occurrences/2`, and `definitions/1` expose history, replay containers, and
|
|
295
263
|
registered definitions.
|
|
296
264
|
|
|
297
|
-
**Live updates arrive as messages, not as a stream.** `ReadModels.watch/2`
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
265
|
+
**Live updates arrive as messages, not as a stream.** `ReadModels.watch/2` and `subscribe/2` send `{:chronicle_read_model_changed, module, %Changeset{}}` and error messages to the calling process, which needs a `handle_info` for each. Read [references/read-models.md](references/read-models.md) when subscribing, filtering, observing collections, previewing or waiting for a projection.
|
|
266
|
+
|
|
267
|
+
## Guarding decisions, testing and more
|
|
268
|
+
|
|
269
|
+
Read the reference that matches the task:
|
|
270
|
+
|
|
271
|
+
- [references/decision-reads.md](references/decision-reads.md) when an append depends on state a concurrent append could change: `Chronicle.decide/2`, `DecisionReads`, `ConcurrencyScope` builders, the optimistic default.
|
|
272
|
+
- [references/testing.md](references/testing.md) when writing specs: `Chronicle.Testing.Case`, assertions, the in-memory kernel's limits.
|
|
273
|
+
- [references/constraints.md](references/constraints.md) when declaring uniqueness, standalone constraint modules, `remove_constraint/2`, violation details or closing events.
|
|
274
|
+
- [references/discovery-and-generations.md](references/discovery-and-generations.md) when pinning ids, setting `:drift_policy`, running `mix chronicle.discover`, or handling older event generations.
|
|
275
|
+
- [references/captures.md](references/captures.md) when turning external API, webhook or other-store data into events.
|
|
276
|
+
- [references/observability.md](references/observability.md) and [references/patterns.md](references/patterns.md) for OpenTelemetry/`:telemetry` and `Chronicle.Patterns`.
|
|
301
277
|
|
|
302
278
|
## Idioms and operational inspection
|
|
303
279
|
|
|
304
|
-
Read [references/idioms-and-inspection.md](references/idioms-and-inspection.md) when carrying correlation or identity across a `Task`, formatting the DSL, supervising
|
|
280
|
+
Read [references/idioms-and-inspection.md](references/idioms-and-inspection.md) when carrying correlation or identity across a `Task`, formatting the DSL, supervising watches, or removing observers (obtain explicit authority first).
|
|
305
281
|
|
|
306
282
|
## Common pitfalls
|
|
307
283
|
|
|
308
|
-
Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting discovery, defaults, process context,
|
|
284
|
+
Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting discovery, defaults, process context, variants or older-version examples.
|
|
309
285
|
|
|
310
286
|
## Verify
|
|
311
287
|
|
|
@@ -319,10 +295,11 @@ Read [references/pitfalls.md](references/pitfalls.md) when troubleshooting disco
|
|
|
319
295
|
- Every event type module passes an explicit, stable `id:`.
|
|
320
296
|
- Every `Chronicle.append/3` call site handles `{:error, _}` as well as `:ok`.
|
|
321
297
|
- Every reactor's `@handles` list matches the clauses of its `handle/2`.
|
|
298
|
+
- Reactors, reducers and projections that must survive a rename pin their `:id`.
|
|
322
299
|
- Read model module names have distinct final segments, or explicit ids.
|
|
323
300
|
- Every variant group has at least one `enters_on` per variant, and every
|
|
324
301
|
`GlobalHandler` member exists on every variant it targets.
|
|
325
302
|
- Production logging filters credential-bearing gRPC/Mint crash reports; see
|
|
326
303
|
the connections reference for limits.
|
|
327
304
|
- Examples match the APIs and schema defaults at the installed tag.
|
|
328
|
-
- `mix compile --warnings-as-errors` and `mix test` are clean.
|
|
305
|
+
- `mix compile --warnings-as-errors` and `mix test` are clean; `mix chronicle.discover --otp-app my_app` lists every expected artifact.
|
|
@@ -38,3 +38,13 @@ Read [event-sources.md](event-sources.md) for definitions, stream routing and de
|
|
|
38
38
|
`append_response.ex` returns `{:error, {:constraint_violations, violations}}`, `{:error, {:concurrency_violations, violations}}` or `{:error, {:append_errors, errors}}` on rejection. Channel/preflight errors include `{:error, :not_connected}` and `{:error, {:incompatible_server, response}}`. A transport timeout does not prove that no event was stored.
|
|
39
39
|
|
|
40
40
|
`append_and_wait_for_completion/3` has **no many-event variant**. `{:ok, result}` does not imply every observer succeeded: inspect `result.success` and `failed_partitions`. Its wait happens **after** the append (`event_log.ex`), so `{:error, reason}` can follow a stored event if waiting fails. Do not retry the write blindly; reconcile the stored outcome first.
|
|
41
|
+
|
|
42
|
+
## Structured results, prepared events and unknown types
|
|
43
|
+
|
|
44
|
+
Requires `cratis_chronicle` 3.13.0 for everything in this section.
|
|
45
|
+
|
|
46
|
+
- `append_with_result/3` and `append_many_with_result/3` return `{:ok, %AppendResult{sequence_number: _}}` (batch: every assigned number, in order) or `{:error, %AppendResult{}}` carrying `:constraint_violations` (each with `:constraint_name`, `:message`, `:event_type_id`, `:sequence_number`, `:details`), `:concurrency_violations` (`:event_source_id`, `:expected_sequence_number`, `:actual_sequence_number`) and `:errors`. Use them when you need the assigned sequence number or the structured reason; transport failures stay plain `{:error, reason}`. `AppendResult.wait_for_completion/2` waits for observers.
|
|
47
|
+
- `EventLog.prepare/2` serializes an event once into a `PreparedEvent` bound to the client, namespace and event sequence that prepared it; reuse it with `EventLog.append_prepared/3` and `Streams.verify_content/3`. Appending it through another client, namespace or sequence returns `{:error, :prepared_event_belongs_to_another_sequence}`. Single use is not enforced.
|
|
48
|
+
- **A struct that does not `use Chronicle.Events.EventType` returns `{:error, {:unknown_event_type, module}}`** from every append path, `prepare/2` and `Streams.verify_content/3`; it does not raise. Match it like any other error.
|
|
49
|
+
- A payload over 4 MiB returns `{:error, {:payload_too_large, size, limit}}` before sending. Raise or disable the guard with `config :cratis_chronicle, max_payload_bytes: ...` (or `:infinity`) only when the kernel is configured for larger messages.
|
|
50
|
+
- Reads: `get_for_event_source/3` and `get_from_sequence_number/2` return raw wire events; pass `decode: true` for `Chronicle.Events.AppendedEvent` structs (`event: nil` and `:raw` for an unknown type, `redacted?: true` for a redacted event).
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!-- Copyright (c) Cratis. All rights reserved. -->
|
|
2
|
+
<!-- Licensed under the MIT license. See LICENSE file in the project root for full license information. -->
|
|
3
|
+
|
|
4
|
+
# Captures
|
|
5
|
+
|
|
6
|
+
Source: `Cratis/Chronicle.Elixir`: `Source/chronicle/lib/chronicle/captures.ex`, `captures/capture.ex`, `captures/registrar.ex`, and `Documentation/captures.md`.
|
|
7
|
+
|
|
8
|
+
Requires `cratis_chronicle` 3.13.0.
|
|
9
|
+
|
|
10
|
+
A capture turns data that arrives from outside the event log (an external API, an inbound webhook, another event store's public events) into events appended to this event store. The DSL compiles at build time to Capture Declaration Language (CDL), the text the kernel's `SaveCapture` accepts. Use a capture instead of a hand-written poller when the source data should become facts you own.
|
|
11
|
+
|
|
12
|
+
```elixir
|
|
13
|
+
# Copyright (c) Cratis. All rights reserved.
|
|
14
|
+
# Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
15
|
+
|
|
16
|
+
defmodule MyApp.Captures.InvoiceCapture do
|
|
17
|
+
use Chronicle.Captures.Capture, name: "InvoiceCapture"
|
|
18
|
+
|
|
19
|
+
api_source("InvoicingApi", route: "/invoices", poll: "10m")
|
|
20
|
+
|
|
21
|
+
key("id")
|
|
22
|
+
|
|
23
|
+
append MyApp.Events.InvoiceStatusChanged, when: "status" do
|
|
24
|
+
set(:status, "$.status")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- Exactly one source: `api_source/1,2` (polls a named external service), `webhook_source/1`, or `events_source/1` (a list of registered event type modules from another event store's public events).
|
|
30
|
+
- `key/1` names the property path that identifies and diffs an item; use `"$eventSourceId"` for an events source.
|
|
31
|
+
- `:id` defaults to a UUIDv5 of the module name, so redeploying updates the same kernel-side capture. `:name` defaults to the last module segment. `start: true` makes the registrar start it once saved.
|
|
32
|
+
- `when:` takes a property name, `{:any, [...]}`, `{:all, [...]}`, `{:transition, property, from, to}`, `:added`, `:removed`, or `{:expression, "template"}`. `set/2` maps a target field from `"$.path"` (payload) or `"$context.<property>"`.
|
|
33
|
+
- The DSL also has `nested` and `children` scopes, and a `map do` block with `rename`, `template`, `translate` and `split`.
|
|
34
|
+
|
|
35
|
+
**An appended event type's id must start with an uppercase letter and contain only letters, digits and underscores.** The kernel's CDL parser rejects hyphens, so `use Chronicle.Events.EventType, id: "InvoiceStatusChanged"` works as an `append` target and `id: "invoice-status-changed"` raises `ArgumentError` at compile time. This restriction does not apply to `events_source/1`.
|
|
36
|
+
|
|
37
|
+
**The kernel's capture engine runs a subset of the language**: only `api` and `events` sources, and only root-level `append` rules. `map`, `nested` and `children` compile and validate but are not applied at runtime, and webhook sources cannot carry authentication.
|
|
38
|
+
|
|
39
|
+
## Registering and managing
|
|
40
|
+
|
|
41
|
+
Captures are discovered with `otp_app:` or listed in the client's `:captures` option. `Chronicle.Captures.Registrar` saves them at `:registered` and again on every reconnect. It skips a capture whose declaration already matches, and warns before saving over a **started** capture whose declaration changed, because saving stops it on the kernel.
|
|
42
|
+
|
|
43
|
+
Manage directly with `Chronicle.Captures.register/1`, `start/1`, `stop/1`, `delete/1`, `list/0`, `validate/1` (a module or raw CDL text) and `captured_events/1`. `start`, `stop` and `delete` also accept a raw capture id; `delete` is destructive, so obtain authority for the specific capture first.
|
|
@@ -16,6 +16,12 @@ your app must read its own configuration and pass it in (`lib/chronicle/client.e
|
|
|
16
16
|
| `:discover` | `true` |
|
|
17
17
|
| `:otp_app` | none; falls back to scanning loaded modules |
|
|
18
18
|
| `:event_types`, `:event_sources`, `:migrations`, `:reactors`, `:reducers`, `:read_models`, `:projections`, `:seeders`, `:webhooks`, `:event_store_subscriptions`, `:global_handlers` | `[]`, merged with discovered |
|
|
19
|
+
| `:constraints`, `:captures`, `:read_model_reactors` | `[]`, merged with discovered where discoverable |
|
|
20
|
+
| `:drift_policy` | `:warn`; `:ignore` or `:fail`. See [discovery-and-generations.md](discovery-and-generations.md) |
|
|
21
|
+
| `:strict_validation` | `false`; `true` raises on event type and migration mistakes instead of logging |
|
|
22
|
+
| `:warn_defaulted_ids` | `true` |
|
|
23
|
+
| `:event_generation_delivery` | `:compatibility`; `:pinned` delivers events at their appended generation |
|
|
24
|
+
| `:concurrency` | none; `[default: :optimistic, check_first_append: bool]`. See [decision-reads.md](decision-reads.md) |
|
|
19
25
|
| `:default_sink_type_id` | `:mongodb`; accepts `:mongodb`, `:sql`, `:in_memory`, `:none` or a raw string |
|
|
20
26
|
| `:server_address` | alternative `"host:port"` address (connection option) |
|
|
21
27
|
| `:skip_tls_validation`, `:load_balancer`, `:grpc_options`, `:reconnect_base_delay`, `:reconnect_max_delay` | forwarded to the connection | — |
|
|
@@ -45,7 +45,7 @@ The client uses grpc 1.x and Mint 1.11+, owns its transport, and needs no `GRPC.
|
|
|
45
45
|
|
|
46
46
|
## Credential-safe logging is still necessary
|
|
47
47
|
|
|
48
|
-
Chronicle's own process status redacts credentials, but dependency crash reports can expose gRPC headers, bearer tokens, API keys and Mint HPACK tables.
|
|
48
|
+
Chronicle's own process status redacts credentials, but dependency crash reports can expose gRPC headers, bearer tokens, API keys and Mint HPACK tables. The client itself installs a primary Logger filter (`Chronicle.Connections.ResponseCrashRedaction`) when a connection starts, replacing the Mint response-process termination report, the one shape it can intercept generically, with a one-line notice (requires `cratis_chronicle` 3.13.0). The `ConnectionProcess` and `GRPC.Client.Connection` state can still print credentials, so before starting the client, install the primary Logger filter from the [client crash-report guide](https://github.com/Cratis/Chronicle.Elixir/blob/main/Documentation/connections/crash-reports.md). It matches `Elixir.GRPC.Client`, `Elixir.GRPC.Channel`, and `Elixir.Mint.` report content and replaces reports with a notice.
|
|
49
49
|
|
|
50
50
|
Keep `handle_sasl_reports: false`. This is a safety net, not a guarantee: plain warning strings carrying exit reasons are not covered, and filters do not survive a Logger restart. Restrict production log access and rotate any credential already exposed. Never log connection strings.
|
|
51
51
|
|