@cratis/pi 7.60.1 → 7.62.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/package/corpus/agents/spec-writer.md +2 -2
  2. package/package/corpus/harnesses/copilot/instructions/application-profile.instructions.md +2 -2
  3. package/package/corpus/harnesses/copilot/instructions/glossary.instructions.md +2 -2
  4. package/package/corpus/harnesses/copilot/instructions/profiles.instructions.md +2 -0
  5. package/package/corpus/harnesses/copilot/instructions/specs.scenarios.csharp.instructions.md +7 -4
  6. package/package/corpus/harnesses/copilot/instructions/vertical-slices.instructions.md +22 -13
  7. package/package/corpus/product-sync.json +3 -2
  8. package/package/corpus/prompts/add-business-rule.prompt.md +1 -1
  9. package/package/corpus/rules/application-profile.md +2 -2
  10. package/package/corpus/rules/glossary.md +2 -2
  11. package/package/corpus/rules/issue-assignment.md +31 -0
  12. package/package/corpus/rules/profiles.md +2 -0
  13. package/package/corpus/rules/specs.scenarios.csharp.md +7 -4
  14. package/package/corpus/rules/vertical-slices.md +22 -13
  15. package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +4 -3
  16. package/package/corpus/skills/cratis-application-slice-specifications/references/query-and-decision-scenarios.md +12 -3
  17. package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +12 -7
  18. package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +11 -11
  19. package/package/corpus/skills/cratis-arc-command/SKILL.md +3 -2
  20. package/package/corpus/skills/cratis-arc-command/references/decisions-and-routing.md +95 -51
  21. package/package/corpus/skills/cratis-arc-command/references/fresh-identifiers.md +5 -2
  22. package/package/corpus/skills/cratis-arc-command/references/read-model-injection.md +9 -5
  23. package/package/corpus/skills/cratis-arc-command-validation/SKILL.md +99 -52
  24. package/package/corpus/skills/cratis-chronicle-decision-reads/SKILL.md +37 -15
  25. package/package/corpus/skills/cratis-chronicle-decision-reads/references/admission-and-ownership.md +3 -2
  26. package/package/corpus/skills/cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md +277 -138
  27. package/package/corpus/skills/cratis-chronicle-event-constraints/SKILL.md +4 -4
  28. package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +6 -4
  29. package/package/corpus/skills/cratis-chronicle-read-model/SKILL.md +2 -1
  30. package/package/corpus/skills/cratis-chronicle-reducer/SKILL.md +6 -3
  31. package/package/corpus/skills/cratis-code-review/SKILL.md +6 -3
  32. package/package/corpus/skills/cratis-screenplay-streams-and-consistency/references/consistency-and-concurrency.md +5 -3
  33. package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +3 -2
  34. package/package/corpus/skills/cratis-specifications-csharp/references/application-scenarios.md +1 -1
  35. package/package.json +1 -1
@@ -15,61 +15,100 @@ Source: Arc. These features require `Cratis.Arc.Chronicle`.
15
15
 
16
16
  ## Pick the guarded read
17
17
 
18
- Plain injected read models and `IReadModels.GetInstanceById` are advisory,
19
- materialized or passive. A `Result<TEvent, ValidationResult>` rejects a request
20
- but does not enroll a guard. Uniqueness and create-once belong to a Chronicle
21
- constraint, not to any read. For the routing steps, the matrix and the traps read
18
+ Under read protection `On` (`<CratisReadProtection>On</CratisReadProtection>`, or
19
+ `[ReadProtection(ReadProtectionMode.On)]` on a command), a read model that is a
20
+ parameter of `Handle()` or of a `CommandValidator<T>` constructor is read fresh from
21
+ the event log and guards the command's append. `Off` is the default when nothing is
22
+ set; under `Off` and `Measure` those reads are plain, a hint that guards nothing.
23
+ A `Result<TEvent, ValidationResult>` rejects a request but does not enroll a guard.
24
+ Uniqueness and create-once belong to a Chronicle constraint, not to any read. For the
25
+ routing by rule shape, the matrix and the traps read
22
26
  [choosing how a rule holds](../../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
23
27
 
24
- - For one stream, inject `IStreamReads` into `Provide`, call `ForCommand()`, and
25
- return `StreamDecision.Append(...)` from `Handle`. It returns
26
- `EventsWithConcurrencyScopes`, retaining the read route and exact tail. A
27
- competing append conflicts, including two competing first appends.
28
- - For projected state, opt in with `[ProtectedDecision]` in
29
- `Cratis.Arc.Chronicle.ReadModels`, then inject `DecisionRead<T>` into
30
- `Provide`/`Handle` (`Exists`/`Instance`). Command-aware `IDecisionReads.Get<T>`
31
- enrolls reads of other keys. Only Chronicle-admitted direct event-source-keyed
32
- projections with a known event-type set qualify; reducers, joins, children,
33
- open-ended event types and incomplete definitions are refused. Arc certifies parameterless
34
- validators it constructs; constructor dependencies and uncertified instances
35
- are refused: a protected command's discoverable validator needs exactly one
36
- public parameterless constructor, otherwise the command fails with HTTP 500.
37
- The decision model needs an `Id` property, an event-source key and a flat
38
- projection; self-reducing models are refused like reducers. When the command has
39
- two or more key candidates, implement `ICanProvideEventSourceId`: `[Key]` does
40
- not silence `ARCCHR0002`, and Arc silently keys by the first eligible property
41
- in declaration order. Keep all decision state out of validators, including
42
- static members and service locators. `[Unprotected]` on the command opts out; never
43
- combine it with `[ProtectedDecision]`. A parameter-level marker acknowledges
44
- tooling, not a runtime opt-out. `Validate` is advisory, not a commit promise.
45
- - `concurrency: true` on the routing attributes is **not** a guard for the
46
- decision: Arc reads the narrowed tail after `Handle`, so it only widens or
47
- narrows the window the default check covers. Exact returned scopes replace
48
- automatic capture; remove the flags when returning `EventsWithConcurrencyScopes`
49
- (ARCCHR0016). The default check also reads the tail after `Handle`, and the first
50
- append into a scope is unchecked unless `CheckFirstAppendIntoAScope` is set.
28
+ Requires Arc 22.106.0 for read protection. Older Arc has only the per-command
29
+ `[ProtectedDecision]` path below.
30
+
31
+ - **One entity's state.** Put the rule in a validator with the read model as a
32
+ constructor parameter; `Handle()` returns the events. The key is
33
+ `[LookUpBy(nameof(Cmd.Prop))]` on the parameter (`Cratis.Arc.Commands`), else the
34
+ single command property whose type equals the model's `Id` type, else the
35
+ command's event source id. Several matching properties fail with
36
+ `AmbiguousReadModelLookup` (500): add `[LookUpBy]`.
37
+ - **Two entities.** Two read-model parameters, the second with `[LookUpBy]`;
38
+ `Handle()` returns `IEnumerable<EventForEventSourceId>`, one event per entity,
39
+ appended atomically. Use a distinct event type per entity.
40
+ - **One stream inside a source.** A per-stream read model
41
+ (`[ReadModel, EventStream<TSource>("Transactions")]`, `Cratis.Chronicle.ReadModels`)
42
+ as a plain parameter, on a command routed `[EventSource<TSource>("Transactions")]`
43
+ and `[EventStreamId("{Month}")]`; otherwise `ReadModelStreamNotOnCommandRoute`
44
+ (500, analyzer ARCCHR0037). It is folded from that one stream and key-only reads
45
+ are refused. For raw events of a short stream, inject `IStreamReads` into
46
+ `Provide`, call `ForCommand()`, and return `StreamDecision.Append(...)` from
47
+ `Handle` (`EventsWithConcurrencyScopes`); a competing append conflicts, including
48
+ two competing first appends.
49
+ - **Computed key or another stream or source.** Command-aware `IDecisionReads.Get<T>`
50
+ in `Provide()`, including the stream overloads.
51
+ - **Rich lifecycle.** An aggregate root, with `Commit()` returned last. Under `On`,
52
+ an aggregate and a protected read of the same source are refused
53
+ (`ReadAndAggregateOnSameSource`); a different source is fine.
54
+ - **Editing conflict.** Return `EventsWithConcurrencyScopes` built from the revision
55
+ the client sent. `concurrency: true` on the routing attributes is **not** a guard
56
+ for the decision: Arc reads the narrowed tail after `Handle`. Remove the flags
57
+ when returning exact scopes (ARCCHR0016). The default check also reads the tail
58
+ after `Handle`, and the first append into a scope is unchecked unless
59
+ `CheckFirstAppendIntoAScope` is set.
60
+ - **No guard needed.** A hint or a rare violation: mark a deliberate advisory read
61
+ `[Unprotected("why")]` (`Cratis.Arc.Chronicle.ReadModels`; on a method it covers
62
+ the body, not injected parameters), or repair with a reactor.
63
+
64
+ Under `On`, `IReadModels`, `IMongoCollection`, detached reads and immediate appends
65
+ through an injected `IEventLog` are refused; take the model as a parameter and
66
+ return the events. Admitted models: event-source-keyed model-bound or declarative
67
+ projections with a string or Guid `Id`, and unfiltered reducers without constructor
68
+ dependencies except `ILogger<T>` (self-reducing models are exempt). Joins and
69
+ children, event-property keys, Pinned delivery, filtered reducers, reducers with
70
+ dependencies and other event sequences fail with `ReadCannotBeProtected` (500)
71
+ unless marked `[Unprotected("why")]`. Validators with read models must be
72
+ convention-registered (transient).
73
+
74
+ When the command has two or more key candidates, implement `ICanProvideEventSourceId`:
75
+ `[Key]` does not silence `ARCCHR0002`, and Arc silently keys by the first eligible
76
+ property in declaration order.
77
+
78
+ A lost race is HTTP 400 with a validation result of reason `concurrencyViolation`,
79
+ `reasonDetail` `ReadChanged` and `state` `{ readModel, key }`. Arc and Chronicle do
80
+ not re-run the command; the client submits it again. Every other refusal is 500:
81
+ change the code.
82
+
83
+ ### Per-command opt-in: `[ProtectedDecision]`
84
+
85
+ `[ProtectedDecision]` (`Cratis.Arc.Chronicle.ReadModels`) with `DecisionRead<T>`
86
+ (`Exists`/`Instance`) is the older per-command opt-in. It still works and is not
87
+ obsolete; do not describe `On` as making it redundant. Use it for a command in a
88
+ project that is not on `On`. Without `On`, only a parameterless validator runs: a
89
+ discoverable validator needs exactly one public parameterless constructor,
90
+ otherwise the command fails with HTTP 500. Keep decision state out of validators
91
+ there, including static members and service locators. `[Unprotected]` on the
92
+ command opts out; never combine it with `[ProtectedDecision]`
93
+ (`CommandCannotBeBothProtectedAndUnprotected`).
51
94
 
52
95
  Requires Arc 22.81.0 for stream-scoped `IDecisionReads.Get<T>`,
53
96
  `GetDetached<T>` and `AdmitForStream<T>` to participate in command protection,
54
- caching and enrollment. Older Arc refuses these stream reads inside commands;
55
- check the Arc package version before choosing this path. `[Unprotected]` command
56
- reads are unguarded, not refused.
97
+ caching and enrollment. Older Arc refuses these stream reads inside commands.
57
98
 
58
99
  Do not manually append or commit to bypass the owner-capable unit of work.
59
100
  The guard detects every matching event appended after its boundary, whoever wrote
60
101
  it, but it protects only guarded decisions: an unguarded writer deciding on the
61
102
  same facts can still append on stale state, so the rule holds only when every
62
- deciding writer is guarded or the rule is a constraint. Protection does not guard an unguarded writer's own decision (a plain read,
63
- `[Unprotected]` or a direct append), external state, revisions, redactions,
64
- migrations, changes to projection definitions or writes that bypass the kernel's
65
- serialized append path. Protection is per command and
66
- opt-in; an unmarked command keeps its plain reads, and a conflict is returned as a
67
- `ConcurrencyViolation` validation result with no automatic retry. A protected command that
68
- stages no events commits an empty guarded batch: current kernels validate the
69
- enrolled reads (a scope with an actual boundary or expecting no matching event) and write nothing, while a kernel that rejects an empty batch
70
- raises `DecisionReadValidateOnlyNotSupported` (HTTP 500), so cover it with an
71
- integration specification. In contrast, `StreamDecision.Append([])`
72
- returns no scopes: use it only for an identical retry, not a rejected request.
103
+ deciding writer is guarded or the rule is a constraint. Outside the guard:
104
+ `[Unprotected]` reads, `Off` mode, external state, revisions and redactions,
105
+ migrations, changes to projection definitions and writes that bypass the kernel's
106
+ serialized append path. A protected command that stages no events commits an empty
107
+ guarded batch: current kernels validate the enrolled reads and write nothing, while
108
+ a kernel that rejects an empty batch raises `DecisionReadValidateOnlyNotSupported`
109
+ (HTTP 500), so cover it with an integration specification. In contrast,
110
+ `StreamDecision.Append([])` returns no scopes: use it only for an identical retry,
111
+ not a rejected request.
73
112
 
74
113
  ## Definition-backed routes and templates
75
114
 
@@ -125,12 +164,17 @@ source id/type and stream route; reactor tuples/collections cannot mix it with e
125
164
 
126
165
  ## Decision specifications
127
166
 
128
- Call `new CommandScenario<T>().UseDecisionReads()` before seeding events.
129
- `Given.ForEventSource(id).OnRoute(route).Events(...)` and
130
- `AppendConcurrently(id, route, ...)` share one real in-process log. Pinned models
131
- cannot supply guards. Assert a real nonempty decision conflicts on the intended
132
- route, and that an independent route is not over-guarded.
133
- `ShouldHaveCompletedStream(type, id)` asserts completion;
167
+ `new CommandScenario<T>()` selects the in-process decision log from the command's
168
+ read-protection mode and dependencies (under `On`, with a per-stream model or
169
+ `[ProtectedDecision]`); `UseDecisionReads()` is hidden and unnecessary. An `Off`
170
+ command with no protected dependency stays in the legacy mode with pinned read
171
+ models, which cannot prove a race. Seed with `Given.ForEventSource(id).Events(...)`,
172
+ `.OnRoute(route).Events(...)`, queue a competing append with
173
+ `AppendConcurrently(id, events...)` (or `AppendConcurrently(id, route, ...)`) and
174
+ assert `result.ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`.
175
+ There is no `scenario.AppendConditions`. Guarded state needs seeded events: protected reads ignore a pinned `.ReadModel(m)` (which only pins an advisory plain read) and `DecisionRead<T>` rejects it. Assert a
176
+ real nonempty decision conflicts on the intended route, and that an independent
177
+ route is not over-guarded. `ShouldHaveCompletedStream(type, id)` asserts completion;
134
178
  `ShouldHaveCompletedStream(ClosedStreamScope)` asserts the exact owner and route;
135
179
  `DecisionReadAdmissions.ShouldAdmitDecisionReadsIn(assembly)` checks projection
136
180
  admission in assembly-level specs.
@@ -98,8 +98,11 @@ use an appropriate constraint or explicit concurrency scope for them.
98
98
 
99
99
  ## Specify the collision
100
100
 
101
- Use `new CommandScenario<TCommand>().UseDecisionReads()` **before seeding**, then
102
- `PinGeneratedValue(name, id)` and `Given.ForEventSource(id).Events(existingEvent)`.
101
+ Use `new CommandScenario<TCommand>()`; it selects the in-process log from the
102
+ command's read-protection mode and dependencies before seeding. If a seeded
103
+ collision throws `GeneratedIdentifierCollisionRequiresDecisionReads`, the
104
+ scenario stayed in legacy mode: call the compatibility switch `UseDecisionReads()`
105
+ before seeding. Then `PinGeneratedValue(name, id)` and `Given.ForEventSource(id).Events(existingEvent)`.
103
106
  Assert the concurrency reason and collision detail, unchanged append tail and
104
107
  one handler execution. Also cover an empty source receiving two events through
105
108
  the chosen write API, an occupied source whose events belong to another stream,
@@ -7,10 +7,12 @@
7
7
  - Which provider owns a read model type
8
8
 
9
9
  Source: [read models in commands](https://github.com/Cratis/Arc/blob/main/Documentation/backend/csharp/chronicle/read-models/injecting-into-commands.md).
10
- Plain models and keyed reads are advisory, not concurrency guards. For protected
11
- reads use [decisions and routing](decisions-and-routing.md): opt into
12
- `[ProtectedDecision]` before taking `DecisionRead<T>`; constructor-injected
13
- validators are refused there.
10
+ Under read protection `Off` (the default) and `Measure`, injected models and keyed
11
+ reads are plain reads, not concurrency guards. Under `On`, a read-model parameter of
12
+ `Handle()` or a validator constructor is guarded; `IReadModels.GetInstanceById` in a
13
+ command is refused, so read another entity's model with a second parameter and
14
+ `[LookUpBy(nameof(Cmd.Prop))]`. See [decisions and routing](decisions-and-routing.md)
15
+ and, for per-command opt-in, `[ProtectedDecision]` with `DecisionRead<T>`.
14
16
 
15
17
  A read model can be injected into a `CommandValidator<T>` constructor, into
16
18
  `Provide()`, or into `Handle()`. Arc resolves it through an
@@ -26,7 +28,9 @@ error. The three shapes it takes:
26
28
  ambiguous;
27
29
  3. the id is generated inside `Handle()` and does not exist yet.
28
30
 
29
- In all three, read by explicit key in `Provide()`:
31
+ In all three, under `Off` read by explicit key in `Provide()`; under `On` use a
32
+ second parameter with `[LookUpBy]`, or `IDecisionReads.Get` in `Provide()` for a
33
+ key computed there:
30
34
 
31
35
  ```csharp
32
36
  var other = await readModels.GetInstanceById<<Other>>((EventSourceId)<OtherId>);
@@ -59,14 +59,15 @@ another version.
59
59
  Which mechanism makes a rule hold under concurrent writers (constraint, protected
60
60
  decision, stream decision, aggregate, scope, reactor repair) is routed in
61
61
  [choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
62
- A validator is never that mechanism.
62
+ A validator holds a state rule only when the project is on read protection `On`;
63
+ under `Off` (the default) it is a hint, never the mechanism.
63
64
 
64
65
  | The rule is… | Put it in |
65
66
  | --- | --- |
66
67
  | A reusable value invariant — length, format, range | `ConceptValidator<T>` on the concept type |
67
68
  | Command input, cross-field, or a pre-handler check | `CommandValidator<TCommand>` with `RuleFor(...)` |
68
69
  | A dependency on data the handler must fetch first | `Provide()` — fetch, and short-circuit if unusable |
69
- | State-dependent and must hold **under concurrency** | Guard the read with `IStreamReads`/`StreamDecision`, or `[ProtectedDecision]` + `DecisionRead<T>`; return events through Arc |
70
+ | State-dependent and must hold **under concurrency** | Under `On`, a `CommandValidator<T>` with the read model as a constructor parameter (Step 5); or `IStreamReads`/`StreamDecision`; return events through Arc. `[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in |
70
71
  | Uniqueness | A Chronicle constraint, never a command-side read |
71
72
  | A genuine defect or missing infrastructure | `throw` a domain exception |
72
73
 
@@ -119,11 +120,27 @@ public class TransferFundsValidator : CommandValidator<TransferFunds>
119
120
  - The validator is discovered by type — no registration.
120
121
  - Constructor dependencies (read models, services) are injected. A read model
121
122
  injected here resolves the same way as one injected into `Handle()`; see
122
- Step 5 for what "the same way" actually means. A validator that reads state is
123
- **advisory only**: it shows the state at validation time and guards nothing, so
124
- use it for a friendly early message, never as the rule. A `[ProtectedDecision]`
125
- command refuses a discoverable validator unless it has exactly one public
126
- parameterless constructor (otherwise HTTP 500), so keep state rules out of it.
123
+ Step 5. **State rules ("may this proceed") belong here**, with the read model as a
124
+ constructor parameter. Under read protection `On` that read is fresh from the log
125
+ and guards the append: if an event of the model's types for that key arrives
126
+ after the read, nothing from the command is written. Under `Off` (the default
127
+ when nothing is set) and `Measure` the same validator reads a plain model: a hint
128
+ with a friendly message that guards nothing. Under `On` the validator must be
129
+ convention-registered (transient); hand-registered scoped, singleton, factory or
130
+ instance validators are refused. Under `[ProtectedDecision]` without `On`, a
131
+ discoverable validator needs exactly one public parameterless constructor
132
+ (otherwise HTTP 500).
133
+ - **The caller can lower the severity threshold.** Model-bound commands accept
134
+ `X-Allowed-Severity` (`allowedSeverity`). Under `Off` and `Measure`, a caller
135
+ sending `3` (Error) removes Error results from validators, command filters and
136
+ `Provide()`, so `Handle()` runs and a validator's state rule can be skipped
137
+ unless the command declares
138
+ `[BlockOnValidationSeverity(ValidationResultSeverity.Error)]`
139
+ (`Cratis.Arc.Commands.ModelBound`). Requires Arc 22.108.0 for the `On` floor: a
140
+ command declared `On` blocks `Error` and `Unknown` whatever the caller sends; the
141
+ caller can relax only `Warning` and `Information`. Lowering
142
+ `ReadProtection:MaximumMode` at runtime does not remove the floor, and an explicit
143
+ attribute wins. Severity is not authorization.
127
144
  - **Omit the validator entirely when there are no rules.** An empty subclass is
128
145
  noise the reader has to check.
129
146
  - Single-property intrinsic rules belong on `ConceptValidator<T>` instead, so
@@ -159,53 +176,77 @@ public async Task<Result<Customer, ValidationResult>> Provide(IReadModels readMo
159
176
 
160
177
  ## Step 5 — Guard a concurrency-sensitive decision
161
178
 
162
- Plain read-model injection or `IReadModels.GetInstanceById` is **advisory**, even
163
- inside `Handle()` returning `Result<,>`: it enrolls no guard against a competing
164
- append. Choose the first boundary that fits:
165
-
166
- 1. One routed stream: `Provide(IStreamReads reads) => reads.ForCommand()` returns
167
- `Task<StreamDecision>`; `Handle(StreamDecision decision)` returns
168
- `decision.Append(...)` (`EventsWithConcurrencyScopes`), guarding the read tail.
169
- 2. Projected state: mark the command `[ProtectedDecision]` and take
170
- `DecisionRead<T>` (`.Exists`/`.Instance`) or command-aware `IDecisionReads`.
171
- Only Chronicle-admitted, direct event-source-keyed projections with known
172
- event types qualify; reducers (including self-reducing models), joins, children and open-ended event
173
- types are refused. The model needs an `Id` property.
174
- Only parameterless validators constructed/certified by Arc run. Constructor
175
- dependencies are refused; static/service-locator decision state is still your
176
- responsibility. Command-level `[Unprotected]` opts out, not into safety.
177
- 3. Stream-level automatic scopes: `concurrency: true` on routing attributes
178
- (`EventStreamId`, `EventStreamType`, `EventSourceType`). It is **not** a guard
179
- for the decision: like the default check it reads the tail after `Handle`.
180
- Exact returned scopes take precedence; do not combine automatic capture with
181
- exact scopes. A guard detects every matching event after its boundary, whoever
182
- wrote it, but it protects only guarded decisions, so every writer that decides
183
- on the rule needs one, or the rule must be a constraint.
184
-
185
- `ARCCHR0011/0012` flag unprotected reads and immediate append bypasses. Return
186
- events, never append them directly. Read
179
+ Set read protection `On` for new code: `<CratisReadProtection>On</CratisReadProtection>`
180
+ in the project (or `[ReadProtection(ReadProtectionMode.On)]` on one command). Under
181
+ `On`, the read-model parameters of `Handle()` and of the validator constructor are
182
+ the guard, so the state rule is a validator and `Handle()` returns the events.
183
+ Requires Arc 22.106.0. Under `Off` (the default) a plain read guards nothing, even
184
+ inside `Handle()` returning `Result<,>`. Choose the first boundary that fits:
185
+
186
+ 1. A unique value or create-once: a Chronicle constraint, not a read.
187
+ 2. One entity's state: a read-model parameter in the validator (this step's example).
188
+ Several entities: one parameter each, with `[LookUpBy(nameof(Command.Prop))]` on
189
+ a parameter whose key is not the command's event source id, and `Handle()`
190
+ returning `IEnumerable<EventForEventSourceId>` with a distinct event type per
191
+ entity.
192
+ 3. A limit inside one stream: a per-stream read model (`[EventStream<TSource>("…")]`)
193
+ on a command routed `[EventSource<TSource>("…")]` + `[EventStreamId("{Prop}")]`,
194
+ or `Provide(IStreamReads reads) => reads.ForCommand()` returning `StreamDecision`.
195
+ 4. An editing conflict: exact returned scopes from the revision the client sent.
196
+ `concurrency: true` on routing attributes is **not** a guard: like the default
197
+ check it reads the tail after `Handle`.
198
+ 5. Rich lifecycle: an aggregate; do not also read the same source.
199
+
200
+ A read under `On` is refused, with HTTP 500, when it cannot be guarded exactly:
201
+ `IReadModels`, `IMongoCollection`, detached reads, an injected `IEventLog.Append`,
202
+ and models that are joins, event-property keyed, filtered or dependency-bearing
203
+ reducers, Pinned or on another sequence (`ReadCannotBeProtected`; mark a deliberate
204
+ advisory read `[Unprotected("why")]`). Several command properties with the model's
205
+ `Id` type are `AmbiguousReadModelLookup`: add `[LookUpBy]`. A lost race is HTTP 400
206
+ `concurrencyViolation` (`ReadChanged`): the client resubmits; Arc does not re-run
207
+ the command. `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command
208
+ opt-in; it still works and is not obsolete. A guard protects only guarded
209
+ decisions: every writer that decides on the rule needs one, or the rule must be a
210
+ constraint.
211
+
212
+ `ARCCHR0011` (Info, `Off` only) and `ARCCHR0012` flag plain reads and immediate
213
+ append bypasses. Return events, never append them directly. Read
187
214
  [protected and stream decisions](../cratis-arc-command/references/decisions-and-routing.md)
188
- when choosing the guard. This handler excerpt assumes `CartSummary` is an
189
- admitted projection; domain types are the slice's existing types.
215
+ and
216
+ [choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md)
217
+ when choosing the guard. The example assumes `CartSummary` is a model-bound
218
+ projection with a `CartId Id`; domain types are the slice's existing types.
190
219
 
191
220
  ```csharp
192
- using Cratis.Monads;
193
- using Cratis.Arc.Chronicle.ReadModels;
221
+ using Cratis.Arc.Commands;
222
+ using Cratis.Arc.Commands.ModelBound;
223
+ using FluentValidation;
194
224
 
195
225
  [Command]
196
- [ProtectedDecision]
197
226
  public record AddItemToCart(CartId CartId, ItemId ItemId)
198
227
  {
199
- /// <summary>Adds the item; rejects when the cart already holds the maximum.</summary>
200
- /// <param name="cart">The current cart summary, resolved by the command's event-source id.</param>
201
- /// <returns>The event on success, or a validation error.</returns>
202
- public Result<ItemAddedToCart, ValidationResult> Handle(DecisionRead<CartSummary> cart) =>
203
- !cart.Exists
204
- ? ValidationResult.Error("Cart must exist.")
205
- : cart.Instance.ItemCount >= 3
206
- ? ValidationResult.Error("A cart can hold at most 3 items.")
207
- : new ItemAddedToCart(ItemId);
228
+ /// <summary>Adds the item.</summary>
229
+ /// <returns>The event.</returns>
230
+ public ItemAddedToCart Handle() => new(ItemId);
208
231
  }
232
+
233
+ public class AddItemToCartValidator : CommandValidator<AddItemToCart>
234
+ {
235
+ public AddItemToCartValidator(CartSummary? cart) =>
236
+ RuleFor(c => c.CartId)
237
+ .Must(_ => cart is { ItemCount: < 3 })
238
+ .WithMessage("The cart must exist and hold fewer than 3 items.");
239
+ }
240
+ ```
241
+
242
+ Use `Result<TEvent, ValidationResult>` from `Handle()` only when the rule needs a value
243
+ computed there:
244
+
245
+ ```csharp
246
+ public Result<ItemAddedToCart, ValidationResult> Handle(CartSummary? cart) =>
247
+ cart is not { ItemCount: < 3 }
248
+ ? ValidationResult.Error("A cart can hold at most 3 items.")
249
+ : new ItemAddedToCart(ItemId);
209
250
  ```
210
251
 
211
252
  - `Result<TSuccess, TError>` is `Cratis.Monads.Result<,>` from Cratis
@@ -214,6 +255,8 @@ public record AddItemToCart(CartId CartId, ItemId ItemId)
214
255
  statements above compile without wrapping.
215
256
  - **Return the error; never throw it.**
216
257
  - One parameter per logical read model; several reads mean several parameters.
258
+ - Keep the fetch for a *computed* key in `Provide()` with `IDecisionReads.Get`;
259
+ under `On`, `IReadModels` there is refused.
217
260
 
218
261
  ⚠️ **Two things the resolution does *not* do.** It does not key off the
219
262
  `[ReadModel]` attribute — a read model becomes injectable because a Chronicle
@@ -245,9 +288,13 @@ where absence is *not* `null`.
245
288
  ## Step 6 — Specify the rejection
246
289
 
247
290
  Every rule gets a specification of its failure case, using
248
- `CommandScenario<TCommand>`. For protected reads, call `UseDecisionReads()`
249
- before seeding actual events; pinned read models cannot supply a guard. Queue a
250
- competing append on the relevant route and assert the conflict. Assert **both** that the command did not succeed
291
+ `CommandScenario<TCommand>`. For guarded reads, `new CommandScenario<T>()`
292
+ selects the in-process log from the command's mode and dependencies (do not call
293
+ `UseDecisionReads()`). Seed real events with `Given.ForEventSource(id).Events(...)`
294
+ (pinned read models cannot supply a guard), queue a competing
295
+ `AppendConcurrently(id, events...)` and assert
296
+ `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`.
297
+ For the ordinary rejection, assert **both** that the command did not succeed
251
298
  and that it failed *as a validation*:
252
299
 
253
300
  ```csharp
@@ -273,9 +320,9 @@ the fact. See
273
320
  - **The rule never fires.** The read-model type has no backing Chronicle
274
321
  projection, reducer, `DbSet<T>`, or MongoDB collection, so nothing registered
275
322
  it for command-side resolution.
276
- - **Two requests both win.** A plain read was mistaken for a protected decision;
277
- moving it to `Handle()` or returning `Result<,>` does not guard it. Enroll a
278
- stream/projection decision read, or use an append-time uniqueness constraint.
323
+ - **Two requests both win.** The project is on `Off` (the default) or `Measure`, so
324
+ the validator read is a hint; moving it to `Handle()` or returning `Result<,>`
325
+ does not guard it. Set `On`, or use a stream decision or a constraint.
279
326
  - **`ARC0005` on build.** `Provide()` returns a value no `Handle()` parameter
280
327
  consumes.
281
328
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: cratis-chronicle-decision-reads
3
- description: Guard a state-dependent Chronicle decision with IDecisionReads, projection admission, ambient unit-of-work enrollment and commit conflict handling, and choose which mechanism makes a write-side rule hold (constraint, decision read, stream decision, aggregate, explicit scope, reactor repair or a plain read). Use when an append must remain valid for the facts read, when a rule must hold under concurrent writers, when choosing whole-source versus stream-scoped reads, or diagnosing DecisionReadRefused and scope conflicts. Do not use for ordinary query reads, append-time uniqueness alone, or defining an Arc command's ProtectedDecision API.
3
+ description: Choose which mechanism makes a write-side rule hold (constraint, Arc read protection with read-model parameters and validators, per-stream read model, stream decision, aggregate, explicit scope, reactor repair or an advisory read), and guard a Chronicle decision directly with IDecisionReads, projection admission, ambient unit-of-work enrollment and commit conflict handling. Use when an append must remain valid for the facts read, when a rule must hold under concurrent writers, when choosing whole-source versus stream-scoped reads, or diagnosing ReadChanged, DecisionReadRefused and scope conflicts. Do not use for ordinary query reads or append-time uniqueness alone; Arc command shapes belong to cratis-arc-command.
4
4
  license: MIT
5
5
  ---
6
6
 
@@ -17,9 +17,22 @@ consistency boundary (DCB), not authorization or a replacement for unique constr
17
17
 
18
18
  Read [choosing how a rule holds](references/choosing-how-a-rule-holds.md) first
19
19
  when the question is which mechanism to use, or before writing code that reads
20
- state and then appends. It carries the routing steps, the matrix with what not to
21
- use, and the traps. A plain read, a validator read, `Result<,>` and
22
- `concurrency: true` are never the guard.
20
+ state and then appends. It carries the routing by rule shape, the matrix with what
21
+ not to use, the refusals, rollout with Measure, testing and the traps.
22
+
23
+ In an Arc application new code sets read protection `On`
24
+ (`<CratisReadProtection>On</CratisReadProtection>`); `Off` is the default when
25
+ nothing is set. Under `On` the read model is a parameter of `Handle()` or of the
26
+ `CommandValidator<T>` constructor, and the read guards the append; under `Off` and
27
+ `Measure` a validator over a plain read is a hint that guards nothing. A plain read under
28
+ `Off`, `Result<,>` and `concurrency: true` are never the guard.
29
+ `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in; it
30
+ still works and is not obsolete.
31
+
32
+ Requires Arc 22.106.0 (read protection) with Chronicle client and kernel 19.71.0.
33
+ Check `dotnet list package --include-transitive`; older Arc has only the
34
+ per-command `[ProtectedDecision]` path, and the choosing reference names where it
35
+ differs.
23
36
 
24
37
  ## Verified product sources
25
38
 
@@ -60,10 +73,12 @@ for the covered APIs, scope lifetime, rollback and immediate-append boundary.
60
73
 
61
74
  ## Route the work
62
75
 
63
- - For Arc `[ProtectedDecision]`, Arc command/handler shapes and Arc `IStreamReads`,
64
- use **cratis-arc-command** and **cratis-arc-command-validation**; do not recreate
65
- their unit-of-work ownership in a handler. The Chronicle `DecisionRead<T>`
66
- below is not a license to invent an Arc wrapper shape.
76
+ - For Arc read protection, `[LookUpBy]`, `[Unprotected]`, `[ProtectedDecision]`,
77
+ command/handler shapes and Arc `IStreamReads`, use **cratis-arc-command** and
78
+ **cratis-arc-command-validation**; do not recreate their unit-of-work ownership in
79
+ a handler. The Chronicle `IDecisionReads` below is for code outside an Arc
80
+ command (endpoints, services, `Provide()` for a computed key), not a license to
81
+ invent an Arc wrapper shape.
67
82
  - For a unique value claimed atomically or a closed stream, use
68
83
  **cratis-chronicle-event-constraints**. A decision read guards one key's
69
84
  history, not a value across keys: it is the wrong tool for uniqueness.
@@ -81,8 +96,12 @@ for the covered APIs, scope lifetime, rollback and immediate-append boundary.
81
96
  `Admit<T>()` / `AdmitForStream<T>()` check shape **without I/O**, returning
82
97
  `DecisionReadAdmission(IsAdmitted, Reason)`. The model needs an `Id` property
83
98
  (string, or a Guid in lowercase hyphenated form) and an event-source-keyed flat
84
- projection. Reducers, including self-reducing models, are refused with
85
- `Reducer`; a missing `Id` or an unconvertible key refuses with `KeyConversion`. A successful shape check is not
99
+ projection, or an unfiltered reducer on the event log with no constructor
100
+ dependencies other than `ILogger<T>` (self-reducing models are exempt). Requires
101
+ Chronicle 19.68.0 for reducers. A filtered reducer refuses with `ReducerFilters`,
102
+ one with dependencies with `ReducerDependencies`, an event type at two generations
103
+ with `ReducerGeneration`, and a reducer in a stream-scoped read with
104
+ `StreamScopeNotSupported`. A missing `Id` or an unconvertible key refuses with `KeyConversion`. A successful shape check is not
86
105
  runtime read success: definitions, key conversion and fold completion are also
87
106
  checked while reading.
88
107
 
@@ -112,9 +131,12 @@ projection also needs an explicit stream id, not the default wildcard.
112
131
  `AdmitForStream<T>()` checks this shape; unsupported readers/kernels refuse with
113
132
  `StreamScopeNotSupported`. Do not fall back to an unguarded query on refusal.
114
133
 
115
- Requires Arc 22.81.0 for these stream overloads inside Arc commands to use
116
- command protection, caching and enrollment; older Arc refuses them. Check Arc
117
- as well as the Chronicle client/kernel pair before choosing this path.
134
+ In Arc, prefer a per-stream read model
135
+ (`[ReadModel, EventStream<TSource>("Transactions")]` on a command routed with
136
+ `[EventSource<TSource>("Transactions")]` and `[EventStreamId("{Month}")]`); use
137
+ the stream overloads of `IDecisionReads` in `Provide()` for another stream or
138
+ source. Requires Arc 22.81.0 for the stream overloads inside Arc commands to use
139
+ command protection, caching and enrollment; older Arc refuses them.
118
140
 
119
141
  ## Commit and inspect conflicts
120
142
 
@@ -158,8 +180,8 @@ deciding writer is guarded or the rule is a constraint.
158
180
  - The unit commits in the ASP.NET Core middleware: it commits after the response
159
181
  and reports no concurrency violation. Call
160
182
  `IUnitOfWorkCompletionFeature.CommitAsync()` before responding.
161
- - Protection is opt-in in Arc (`[ProtectedDecision]`), so an unmarked command with
162
- a read model is still unguarded.
183
+ - Arc under `Off` (the default when nothing is set): a command with a read model
184
+ is unguarded unless it carries `[ProtectedDecision]`. Set `On` for new code.
163
185
 
164
186
  ## Verify
165
187
 
@@ -10,12 +10,13 @@ and `UnitOfWorkLifecyclePolicy.cs`.
10
10
 
11
11
  ## Why a shape is refused
12
12
 
13
- Admission requires exactly one supported projection on the event log. It is not
13
+ Admission requires exactly one supported projection, or an unfiltered dependency-free reducer, on the event log. It is not
14
14
  an adapter for arbitrary read-model storage:
15
15
 
16
16
  | Reason | Shape to inspect |
17
17
  | --- | --- |
18
- | Reducer / AmbiguousProjection / NotEventLog | reduced model, several projections, or another sequence |
18
+ | Reducer / AmbiguousProjection / NotEventLog | a model with no admitted definition, several projections, or another sequence |
19
+ | ReducerFilters / ReducerDependencies / ReducerGeneration | a reducer with an event filter, constructor dependencies other than `ILogger<T>` (self-reducing models are exempt), or an event type at two generations |
19
20
  | Join / Hierarchy | joins, children or nested projections |
20
21
  | OpenEndedEventTypes / Derivatives / FromEventProperty | all-events, FromEvery, or event-property routing |
21
22
  | NotEventSourceKeyed / KeyConversion | keys not convertible to the admitted raw source key |