@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
@@ -89,14 +89,14 @@ public class and_all_information_is_valid : Specification
89
89
  #endif
90
90
  ```
91
91
 
92
- (`CommandScenario` event assertions are extension methods keyed by command + event type — `await _scenario.ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId[, predicate])`; seed prior state through `_scenario.Given.ForEventSource(id).Events(...)` or `_scenario.Services`. The mode matters: in legacy mode `Given` materializes an in-memory read-model dictionary only, so constraint preconditions go through `EventScenario`'s `Given`; in `UseDecisionReads()` mode `Given` writes to the scenario's real log and `EventScenario` is unavailable (accessing it throws), so seed everything through `Given`. Check which mode the spec's scenario uses; see `specs.scenarios.csharp.md` and `cratis-application-slice-specifications`.)
92
+ (`CommandScenario` event assertions are extension methods keyed by command + event type — `await _scenario.ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId[, predicate])`; seed prior state through `_scenario.Given.ForEventSource(id).Events(...)` or `_scenario.Services`. The mode matters: in legacy mode `Given` materializes an in-memory read-model dictionary only, so constraint preconditions go through `EventScenario`'s `Given`; in decision mode (selected automatically from the command's read-protection mode and dependencies) `Given` writes to the scenario's real log and `EventScenario` is unavailable (accessing it throws), so seed everything through `Given`. Check which mode the spec's scenario uses; see `specs.scenarios.csharp.md` and `cratis-application-slice-specifications`.)
93
93
 
94
94
  ### What to specify
95
95
 
96
96
  0. **Every contract specification first** — one spec each, named after it, using the contract's example values. Then add the code-derived cases below that the contract lacks and report them as proposals for the contract (the `.play` model when one covers the slice, otherwise the agreed outline); never present them as contract coverage. A `.play` specification maps to a plain-call spec for a pure decision, with a scenario added where the pipeline, validation, constraints or projections matter; never edit, skip or delete a spec derived from the contract to make code pass — change the code or return an edit request.
97
97
  1. **Happy path** — succeeds, correct event(s) appended.
98
98
  2. **Each validation failure** — assert **both** `ShouldNotBeSuccessful()` and `ShouldHaveValidationErrors()`. Do not assert on message strings by default; assert exact text only when that wording is the specified behavior, and name that behavior in the fact. Violate **only** the rule the spec is named after (build the command valid in every other respect), or a neighbouring rule makes it pass.
99
- 3. **Business-rule violations** — each `Result<,>` rejection / guarded decision condition. Plain injection is advisory; for `[ProtectedDecision]` or stream decisions use `UseDecisionReads()` with seeded events and competing appends, not pinned models.
99
+ 3. **Business-rule violations** — each `Result<,>` rejection / guarded decision condition. Plain injection under `Off` is advisory; for read protection `On`, `[ProtectedDecision]` or stream decisions use `new CommandScenario<T>()` with seeded events, `AppendConcurrently` and `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`, not pinned models and not `UseDecisionReads()`.
100
100
  4. **Constraint violations** — `ShouldHaveConstraintViolationFor(name)` via `EventScenario`.
101
101
  5. **Authorization** — `ShouldNotBeAuthorized()` (an unauthorized result has no validation errors).
102
102
  6. **Repeat execution**, when a reactor invokes the command — the same command twice for the same source; specify whether the second is rejected, a no-op, or appends again.
@@ -32,7 +32,7 @@ Pick exactly one type per slice folder — determined by what the slice *does*.
32
32
 
33
33
  | Type | What it does | Contents |
34
34
  | --- | --- | --- |
35
- | **State Change** | Accepts a command, appends events | Command + validator + event(s); optional `[Passive]` for a current read; a rule that must hold under concurrency needs a decision read or constraint (see the `cratis-chronicle-decision-reads` skill, `references/choosing-how-a-rule-holds.md`) |
35
+ | **State Change** | Accepts a command, appends events | Command + validator + event(s); optional `[Passive]` for a current read; a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint (see the `cratis-chronicle-decision-reads` skill, `references/choosing-how-a-rule-holds.md`) |
36
36
  | **State View** | Builds a queryable read model from events | `[ReadModel]` + projection mappings or reducer code + static query method(s) on the record |
37
37
  | **Automation** | Reacts to events, calls external systems / `ICommandPipeline`, or returns follow-up commands/events (including to another stream) | Reactor only |
38
38
  | **Translation** | Takes data from outside our own facts (an external system or another service) and records it as our facts | An adapter at the boundary, often a reactor |
@@ -46,7 +46,7 @@ Commands are imperative intents (`Register`, `Create`); the slice folder is the
46
46
  Tagged **[contract]** (framework-enforced) or **[convention]** (house default). Mechanics and examples live in `vertical-slices.md`.
47
47
 
48
48
  1. **[contract] Model-bound — no controllers.** Commands are `[Command]` records with a public instance `Handle()` (Arc analyzers enforce this); queries are `static` methods on `[ReadModel]` records; projections/constraints/authorization use attributes. Arc generates the HTTP surface. For read-model style follow rule 11's ladder; for constraints use `IConstraint` when attributes cannot express the rule.
49
- 2. **[contract] Command validation & data flow.** Put command rejection in `CommandValidator<T>`, global value invariants in `ConceptValidator<T>`, and fetched/computed handler data in **`Provide()`** (runs after validation/authorization; may short-circuit with `ValidationResult.Error` / `Result<TProvided, ValidationResult>`). Keep `Handle()` focused on event construction. For a state-dependent rule that must hold **under concurrency**, enroll a guarded stream decision (`IStreamReads`/`StreamDecision`) or use `[ProtectedDecision]` with an admitted `DecisionRead<T>`; plain injected models remain advisory. Return a typed rejection via `Result<TEvent, ValidationResult>` where needed; the result alone adds no guard. **Throwing from `Provide()`/`Handle()` is an exception (HTTP 500), not a validation rejection** — throw only for genuinely exceptional conditions, never for normal business rejection.
49
+ 2. **[contract] Command validation & data flow.** Put command rejection in `CommandValidator<T>`, global value invariants in `ConceptValidator<T>`, and fetched/computed handler data in **`Provide()`** (runs after validation/authorization; may short-circuit with `ValidationResult.Error` / `Result<TProvided, ValidationResult>`). Keep `Handle()` focused on event construction. For a state-dependent rule that must hold **under concurrency**, start new code with read protection `On` (`<CratisReadProtection>On</CratisReadProtection>`): put the rule in a `CommandValidator<T>` with the read model as a constructor parameter and return the events from `Handle()`. Use a constraint for uniqueness, a per-stream read model or `IStreamReads`/`StreamDecision` for one stream, an aggregate for a rich lifecycle; `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in. Under `Off` (the default when nothing is set) a validator over a plain read is a hint, not a guard. Return a typed rejection via `Result<TEvent, ValidationResult>` where needed; the result alone adds no guard. **Throwing from `Provide()`/`Handle()` is an exception (HTTP 500), not a validation rejection** — throw only for genuinely exceptional conditions, never for normal business rejection.
50
50
  3. **[contract] Event-source id resolution:** `ICanProvideEventSourceId` first; else the first qualifying property in declaration order — an `EventSourceId`/`EventSourceId<T>`-derived value *or* a `[Key]` property (no priority between them; two candidates is `ARCCHR0002`); else Arc generates one. A value actually used as a Chronicle stream identity derives from `EventSourceId<T>` with the underlying `IComparable` primitive — never `ConceptAs<Guid>` for that stream identity. `NotSet`, `New()`, and primitive→derived-id operators are optional domain/API conveniences, not Chronicle requirements; typed empty/zero values are real specified stream IDs, not `EventSourceId.Unspecified`.
51
51
  4. **[contract] Events never carry the event-source id** — it is implicit in the event context.
52
52
  5. **[contract] `[Key]` / `[Subject]` are distinct.** `[Key]` is for event-source/read-model/projection key resolution; `[Subject]` is compliance identity only. Don't put either on an `EventSourceId<T>` value (it already is both); use them only for non-`EventSourceId<T>` values.
@@ -38,8 +38,8 @@ One precise line per load-bearing term, so the same word means the same thing ev
38
38
  - **Observation command** — a command whose input comes from outside the model's own decisions (webhook, poll, sweep, retry, classifier). It compares with current state and appends nothing when nothing changed; looking is not a fact.
39
39
  - **Signal event** — an event appended only to make a reactor do something (`…Requested`, `…Started`, `…Skipped`). Not a domain fact; use a job, state machine, command operation or `ICommandPipeline`.
40
40
  - **Constraint** — `IConstraint`; an **append-time** invariant (uniqueness / concurrency) enforced at the event-store level.
41
- - **DCB (Dynamic Consistency Boundary)** — enforcing a state-dependent rule under concurrency over explicitly enrolled event-state reads: stream decisions/exact scopes or protected, admitted projection decision reads. Plain injected models and `Result<TEvent, ValidationResult>` alone are advisory, not a guard.
42
- - **Consistency boundary** — the scope within which an invariant holds atomically (an event source, a routed stream, or the event state enrolled by an admitted protected decision read).
41
+ - **DCB (Dynamic Consistency Boundary)** — enforcing a state-dependent rule under concurrency over explicitly enrolled event-state reads: read-model parameters under Arc read protection `On`, stream decisions/exact scopes, or protected, admitted projection decision reads. Plain injected models under `Off` (the default) and `Result<TEvent, ValidationResult>` alone are advisory, not a guard.
42
+ - **Consistency boundary** — the scope within which an invariant holds atomically (an event source, a routed stream, or the event state enrolled by an admitted protected read).
43
43
  - **EventForEventSourceId** — self-describing wrapper to append an event to a **specific** (cross-stream) event source, from a command `Handle()` or from a reactor handler (reactor support has shipped since Chronicle 15.35). It carries the event stream type and id, source type, subject, occurred time, tags and causation, so it is also how a reactor sets those explicitly.
44
44
  - **ReactorDelivery** — the identity of one delivery of one event to one reactor partition; declare it as a handler parameter and Chronicle passes it in. Stable across a replay and across recovering a failed partition, so a receipt kept under its `Id` is what makes a side effect survive re-delivery. An identity, not a guarantee — Chronicle does not know whether the effect ran.
45
45
 
@@ -92,6 +92,8 @@ profile includes neither Screenplay nor Stage.
92
92
  | `cratis/engineering/typescript` | TypeScript engineering conventions | `cratis/engineering/core` |
93
93
  | `cratis/engineering/react` | React engineering conventions | `cratis/engineering/core` |
94
94
 
95
+ The `cratis/engineering` profile also carries [issue-assignment.md](../../../rules/issue-assignment.md): when an agent starts work on a numbered issue in a known repository and nobody is assigned, it assigns the current `gh` user.
96
+
95
97
  ### Language Profiles
96
98
 
97
99
  | Profile ID | Description |
@@ -117,10 +117,13 @@ arguments?, paging?, sorting?)`; `Observe(...)` returns `QueryObservation.Initia
117
117
  and buffered `Next()`. Configure principal/policies in `Services`; schemes fail
118
118
  closed without HTTP. Chronicle query `Given` can seed keyed models/events.
119
119
 
120
- For guarded commands use `UseDecisionReads()` **before** seeding events. Given
121
- state, guarded reads and appends share one real in-process log, unlike legacy
122
- mode above. Pinned models cannot supply guards. Use `OnRoute(route)` and
123
- `AppendConcurrently` to prove conflicts; `ShouldHaveCompletedStream` proves
120
+ For guarded commands use plain `new CommandScenario<T>()`: it selects the decision
121
+ mode from the command's read-protection mode and dependencies (`UseDecisionReads()`
122
+ is hidden and unnecessary). Given state, guarded reads and appends share one real
123
+ in-process log, unlike legacy mode above; an `Off` command with no protected
124
+ dependency stays in legacy mode. Pinned models cannot supply guards. Use `OnRoute(route)` and
125
+ `AppendConcurrently` with `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`
126
+ to prove conflicts (there is no `scenario.AppendConditions`); `ShouldHaveCompletedStream` proves
124
127
  completion and `DecisionReadAdmissions.ShouldAdmitDecisionReadsIn(assembly)`
125
128
  checks admission. See **cratis-application-slice-specifications**,
126
129
  `references/query-and-decision-scenarios.md`, for sources and limitations.
@@ -89,7 +89,7 @@ The command carries input from the caller. `Handle()` is defined directly on the
89
89
  | Reusable value invariant (length, format, range) | `ConceptValidator<T>` on the concept type |
90
90
  | Command-input / cross-field / pre-handler rule (incl. injected read-model/service/identity checks) | `CommandValidator<TCommand>` with `RuleFor(...)` |
91
91
  | Handler needs fetched/computed data before it can build the event | `Provide()` — fetches the data; may validate it and short-circuit |
92
- | State-dependent rule that must hold **under concurrency** | `IStreamReads`/`StreamDecision` for one stream, or `[ProtectedDecision]` + `DecisionRead<T>` for admitted projected state; plain injection is advisory |
92
+ | State-dependent rule that must hold **under concurrency** | Read protection `On`: the read model as a `CommandValidator<T>` constructor (or `Handle()`) parameter; a per-stream read model or `IStreamReads`/`StreamDecision` for one stream; an aggregate for a rich lifecycle. `[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in. Under `Off` (default) injection is a hint |
93
93
  | Uniqueness | Chronicle `[Unique]` / `IConstraint` (race-safe) — not a read-model pre-check |
94
94
  | Genuinely exceptional failure (bug, missing infra) | `throw` a domain exception |
95
95
 
@@ -183,13 +183,21 @@ Three attributes scope an append broad→narrow, and each one *also* (only with
183
183
 
184
184
  Each attribute always tags the appended events (visible in `EventContext`). **Only when the command also carries `concurrency: true`** does each dimension join the server-side `ConcurrencyScope` (plus the event source id), with the optimistic strategy supplying the expected tail sequence number. A command **without** `concurrency: true` contributes no scope of its own, and the append falls to whatever concurrency strategy the event sequence is configured with — by default the optimistic one, scoped to the event source. So the attribute chooses *which dimensions* bound the check, not whether a check happens at all. Reach for it on high-contention state transitions where a stale read would let two commands both "win" — it narrows the boundary so appends that are genuinely independent stop colliding (concurrency failures surface on `CommandResult`/`AppendResult`). Adopt it deliberately per behavior, not as a blanket default.
185
185
 
186
- At Arc v22.75.0, prefer `IStreamReads.ForCommand()` in `Provide()` and return
187
- `StreamDecision.Append(...)` when deciding over one routed stream. For projected
188
- state use `[ProtectedDecision]` + `DecisionRead<T>`; plain injection and keyed
189
- reads are advisory, including in `Handle()`. Protected projections must be
190
- Chronicle-admitted (direct source key, known events; not reducers, joins, children
191
- or passive shapes); validators must be parameterless and certified by Arc.
192
- Exact returned scopes replace automatic `concurrency: true` capture.
186
+ New code sets `<CratisReadProtection>On</CratisReadProtection>` (Arc 22.106.0).
187
+ Under `On`, a read-model parameter of `Handle()` or of a `CommandValidator<T>`
188
+ constructor is read fresh and guards the append; the state rule goes in the
189
+ validator. Two entities: two parameters, `[LookUpBy(nameof(Cmd.Prop))]` on the
190
+ second, `IEnumerable<EventForEventSourceId>` returned. One stream inside a source:
191
+ a per-stream read model (`[ReadModel, EventStream<TSource>("Stream")]`) on a command
192
+ routed with `[EventSource<TSource>("Stream")]` and `[EventStreamId("{Prop}")]`, or
193
+ `IStreamReads.ForCommand()` in `Provide()` returning `StreamDecision.Append(...)`.
194
+ A deliberate advisory read is `[Unprotected("why")]`. Models must be admitted
195
+ (source-keyed projection with a string or Guid `Id`, or an unfiltered reducer
196
+ without constructor dependencies); joins, children and event-property keys are
197
+ refused. Under `Off` (the default) injection and keyed reads are advisory,
198
+ including in `Handle()`; `[ProtectedDecision]` + `DecisionRead<T>` is the older
199
+ per-command opt-in and still works. Exact returned scopes replace automatic
200
+ `concurrency: true` capture. Refusals: [choosing how a rule holds](../../../skills/cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
193
201
 
194
202
  `[EventStreamId("{ReportingScopeId}:{Period}")]` resolves command property values
195
203
  after validation; concepts supply their underlying values. An interface can
@@ -296,7 +304,7 @@ query reference for versioned behavior and composition trade-offs.
296
304
 
297
305
  ### Choosing read-model access in command-side code **[convention]**
298
306
 
299
- Ladder, first that fits: (1) advisory direct read-model injection — only when keyed by the command's own event-source id; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
307
+ Ladder, first that fits: (1) direct read-model injection — guarded under read protection `On`, advisory under `Off`; keyed by the command's own event-source id, or by `[LookUpBy]`; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
300
308
 
301
309
  **[contract] Passive state is not sink state.** `IMongoCollection<T>` cannot
302
310
  compute a passive read model; any leftover documents are not maintained state.
@@ -408,14 +416,15 @@ Redaction is a system-event request that removes payload availability from the e
408
416
 
409
417
  ## Cross-slice patterns **[convention]**
410
418
 
411
- Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs a decision read or constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
419
+ Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
412
420
 
413
421
  **DCB (Dynamic Consistency Boundary):** the boundary is the event state explicitly
414
422
  enrolled in guarded decisions, not every model a command reads. Use stream
415
- reads/exact scopes or `[ProtectedDecision]` + admitted `DecisionRead<T>`;
416
- plain validator/`Provide()`/`Handle()` injection is advisory. Stream-level
423
+ reads/exact scopes or read-model parameters under read protection `On`
424
+ (`[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in);
425
+ validator/`Provide()`/`Handle()` injection under `Off` is advisory. Stream-level
417
426
  `concurrency: true` scopes are another explicit boundary, not protection for an
418
- arbitrary snapshot. Specify conflicts with `UseDecisionReads()` and actual seeded
427
+ arbitrary snapshot. Specify conflicts with `AppendConcurrently` and actual seeded
419
428
  events, not pinned models.
420
429
 
421
430
  ## Dialogs
@@ -81,6 +81,7 @@
81
81
  "cratis-arc-observable-query-http",
82
82
  "cratis-arc-query-paging",
83
83
  "cratis-arc-react-page",
84
+ "cratis-chronicle-decision-reads",
84
85
  "cratis-chronicle-read-model",
85
86
  "cratis-code-review",
86
87
  "cratis-lens-browser-extension",
@@ -98,7 +99,7 @@
98
99
  "vertical-slices.md"
99
100
  ]
100
101
  },
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
+ "notes": "Whole-product floor unchanged at 22.100.0. Focused read-protection coverage, Arc 22.106.0-22.108.0: ReadProtectionMode Off, Measure and On (MSBuild property, assembly and command attributes, runtime MaximumMode cap), read-model parameters of Handle and validator constructors guarded under On, [LookUpBy], [Unprotected(\"why\")], per-stream read models on routed commands, Measure report, ReadChanged and ReadIncomplete refusals, and CommandScenario selecting decision mode automatically. the caller severity threshold (X-Allowed-Severity, [BlockOnValidationSeverity]) and the Error floor for commands declared On (22.108.0). [ProtectedDecision] and DecisionRead<T> retained as the per-command opt-in. The other 22.101.0-22.108.0 changes were not reviewed. Prior notes: scoped completion and routing validation, generated-target freshness, observable scenarios, asynchronous execution scopes, read-model diagnostics, ordered reactor refusal handling and ICommandExceptionMapper. No new runtime probe."
102
103
  },
103
104
  {
104
105
  "id": "ai",
@@ -322,7 +323,7 @@
322
323
  ],
323
324
  "rules": []
324
325
  },
325
- "notes": "Release-tag delta through 19.65.0: explicit artifacts, collection observation, redelivery scenarios, webhook generation pins, enrollment contributors, seed ownership, reconnect readiness and hosting/storage fixes. Existing self-reduction guidance retained; publication eligibility corrected and forwarded-tag boundary recorded. No new kernel/transport probe."
326
+ "notes": "Whole-product floor unchanged at 19.65.0. Focused read-protection coverage, Chronicle 19.68.0-19.71.x: admitted reducers (unfiltered, no constructor dependencies, 19.68.0), per-stream read models with [EventStream<TSource>], IUnitOfWork.EnforceReadProtection, AllowReads and GetAppendConditions, CHR0062-CHR0064. The other 19.66.0-19.71.x changes were not reviewed. Prior notes: explicit artifacts, collection observation, redelivery scenarios, webhook generation pins, enrollment contributors, seed ownership, reconnect readiness and hosting/storage fixes. No new kernel/transport probe."
326
327
  },
327
328
  {
328
329
  "id": "arc-go",
@@ -16,7 +16,7 @@ Enforce a new rule on an existing command. Invoke the **cratis-arc-command-valid
16
16
  - Reusable value invariant → `ConceptValidator<T>`.
17
17
  - Command-input / cross-field / pre-handler rule → `CommandValidator<T>`.
18
18
  - Handler needs fetched data first → `Provide()`.
19
- - State rule that must hold **under concurrency** → enroll a stream decision with `IStreamReads`/`StreamDecision`, or use `[ProtectedDecision]` with an admitted `DecisionRead<T>`; plain read-model injection and `Result<TEvent, ValidationResult>` alone add no guard. Specify the race with `UseDecisionReads()` and seeded events, not pinned models.
19
+ - State rule that must hold **under concurrency** → new code sets read protection `On` (`<CratisReadProtection>On</CratisReadProtection>`): the read model is a `CommandValidator<T>` constructor parameter and `Handle()` returns the events; add `[LookUpBy]` for a second entity, a per-stream read model or `IStreamReads`/`StreamDecision` for one stream. `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in. Under `Off` (the default) a validator over a plain read is a hint, and `Result<TEvent, ValidationResult>` alone adds no guard. Specify the race with `AppendConcurrently` and seeded events, not pinned models.
20
20
  - Uniqueness → `[Unique]` / `IConstraint`.
21
21
 
22
22
  > **Never throw for normal business rejection** — a throw is HTTP 500, not a validation error. Recoverable rejection is a `ValidationResult` / `Result<,>`. Add a spec for the failure case (assert both not-successful and has-validation-errors). The skill carries the detail; don't duplicate it here.
@@ -31,7 +31,7 @@ Pick exactly one type per slice folder — determined by what the slice *does*.
31
31
 
32
32
  | Type | What it does | Contents |
33
33
  | --- | --- | --- |
34
- | **State Change** | Accepts a command, appends events | Command + validator + event(s); optional `[Passive]` for a current read; a rule that must hold under concurrency needs a decision read or constraint (see the `cratis-chronicle-decision-reads` skill, `references/choosing-how-a-rule-holds.md`) |
34
+ | **State Change** | Accepts a command, appends events | Command + validator + event(s); optional `[Passive]` for a current read; a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint (see the `cratis-chronicle-decision-reads` skill, `references/choosing-how-a-rule-holds.md`) |
35
35
  | **State View** | Builds a queryable read model from events | `[ReadModel]` + projection mappings or reducer code + static query method(s) on the record |
36
36
  | **Automation** | Reacts to events, calls external systems / `ICommandPipeline`, or returns follow-up commands/events (including to another stream) | Reactor only |
37
37
  | **Translation** | Takes data from outside our own facts (an external system or another service) and records it as our facts | An adapter at the boundary, often a reactor |
@@ -45,7 +45,7 @@ Commands are imperative intents (`Register`, `Create`); the slice folder is the
45
45
  Tagged **[contract]** (framework-enforced) or **[convention]** (house default). Mechanics and examples live in `vertical-slices.md`.
46
46
 
47
47
  1. **[contract] Model-bound — no controllers.** Commands are `[Command]` records with a public instance `Handle()` (Arc analyzers enforce this); queries are `static` methods on `[ReadModel]` records; projections/constraints/authorization use attributes. Arc generates the HTTP surface. For read-model style follow rule 11's ladder; for constraints use `IConstraint` when attributes cannot express the rule.
48
- 2. **[contract] Command validation & data flow.** Put command rejection in `CommandValidator<T>`, global value invariants in `ConceptValidator<T>`, and fetched/computed handler data in **`Provide()`** (runs after validation/authorization; may short-circuit with `ValidationResult.Error` / `Result<TProvided, ValidationResult>`). Keep `Handle()` focused on event construction. For a state-dependent rule that must hold **under concurrency**, enroll a guarded stream decision (`IStreamReads`/`StreamDecision`) or use `[ProtectedDecision]` with an admitted `DecisionRead<T>`; plain injected models remain advisory. Return a typed rejection via `Result<TEvent, ValidationResult>` where needed; the result alone adds no guard. **Throwing from `Provide()`/`Handle()` is an exception (HTTP 500), not a validation rejection** — throw only for genuinely exceptional conditions, never for normal business rejection.
48
+ 2. **[contract] Command validation & data flow.** Put command rejection in `CommandValidator<T>`, global value invariants in `ConceptValidator<T>`, and fetched/computed handler data in **`Provide()`** (runs after validation/authorization; may short-circuit with `ValidationResult.Error` / `Result<TProvided, ValidationResult>`). Keep `Handle()` focused on event construction. For a state-dependent rule that must hold **under concurrency**, start new code with read protection `On` (`<CratisReadProtection>On</CratisReadProtection>`): put the rule in a `CommandValidator<T>` with the read model as a constructor parameter and return the events from `Handle()`. Use a constraint for uniqueness, a per-stream read model or `IStreamReads`/`StreamDecision` for one stream, an aggregate for a rich lifecycle; `[ProtectedDecision]` with `DecisionRead<T>` is the older per-command opt-in. Under `Off` (the default when nothing is set) a validator over a plain read is a hint, not a guard. Return a typed rejection via `Result<TEvent, ValidationResult>` where needed; the result alone adds no guard. **Throwing from `Provide()`/`Handle()` is an exception (HTTP 500), not a validation rejection** — throw only for genuinely exceptional conditions, never for normal business rejection.
49
49
  3. **[contract] Event-source id resolution:** `ICanProvideEventSourceId` first; else the first qualifying property in declaration order — an `EventSourceId`/`EventSourceId<T>`-derived value *or* a `[Key]` property (no priority between them; two candidates is `ARCCHR0002`); else Arc generates one. A value actually used as a Chronicle stream identity derives from `EventSourceId<T>` with the underlying `IComparable` primitive — never `ConceptAs<Guid>` for that stream identity. `NotSet`, `New()`, and primitive→derived-id operators are optional domain/API conveniences, not Chronicle requirements; typed empty/zero values are real specified stream IDs, not `EventSourceId.Unspecified`.
50
50
  4. **[contract] Events never carry the event-source id** — it is implicit in the event context.
51
51
  5. **[contract] `[Key]` / `[Subject]` are distinct.** `[Key]` is for event-source/read-model/projection key resolution; `[Subject]` is compliance identity only. Don't put either on an `EventSourceId<T>` value (it already is both); use them only for non-`EventSourceId<T>` values.
@@ -37,8 +37,8 @@ One precise line per load-bearing term, so the same word means the same thing ev
37
37
  - **Observation command** — a command whose input comes from outside the model's own decisions (webhook, poll, sweep, retry, classifier). It compares with current state and appends nothing when nothing changed; looking is not a fact.
38
38
  - **Signal event** — an event appended only to make a reactor do something (`…Requested`, `…Started`, `…Skipped`). Not a domain fact; use a job, state machine, command operation or `ICommandPipeline`.
39
39
  - **Constraint** — `IConstraint`; an **append-time** invariant (uniqueness / concurrency) enforced at the event-store level.
40
- - **DCB (Dynamic Consistency Boundary)** — enforcing a state-dependent rule under concurrency over explicitly enrolled event-state reads: stream decisions/exact scopes or protected, admitted projection decision reads. Plain injected models and `Result<TEvent, ValidationResult>` alone are advisory, not a guard.
41
- - **Consistency boundary** — the scope within which an invariant holds atomically (an event source, a routed stream, or the event state enrolled by an admitted protected decision read).
40
+ - **DCB (Dynamic Consistency Boundary)** — enforcing a state-dependent rule under concurrency over explicitly enrolled event-state reads: read-model parameters under Arc read protection `On`, stream decisions/exact scopes, or protected, admitted projection decision reads. Plain injected models under `Off` (the default) and `Result<TEvent, ValidationResult>` alone are advisory, not a guard.
41
+ - **Consistency boundary** — the scope within which an invariant holds atomically (an event source, a routed stream, or the event state enrolled by an admitted protected read).
42
42
  - **EventForEventSourceId** — self-describing wrapper to append an event to a **specific** (cross-stream) event source, from a command `Handle()` or from a reactor handler (reactor support has shipped since Chronicle 15.35). It carries the event stream type and id, source type, subject, occurred time, tags and causation, so it is also how a reactor sets those explicitly.
43
43
  - **ReactorDelivery** — the identity of one delivery of one event to one reactor partition; declare it as a handler parameter and Chronicle passes it in. Stable across a replay and across recovering a failed partition, so a receipt kept under its `Id` is what makes a side effect survive re-delivery. An identity, not a guarantee — Chronicle does not know whether the effect ran.
44
44
 
@@ -0,0 +1,31 @@
1
+ ---
2
+ profile: cratis/engineering
3
+ ---
4
+
5
+ # Claim the issue you start work on
6
+
7
+ When an agent starts work on a GitHub issue, the issue says who is on it. Nobody can
8
+ tell from an unassigned issue that work has begun, so two people (or two agents) pick it
9
+ up, or it looks abandoned while it is being done.
10
+
11
+ - **Applies when** the work is tied to an issue with a number in a known repository: the
12
+ user named it (`#123`, `Cratis/Repo#123`, an issue URL), or the branch, task or handover
13
+ names it. Work with no issue, or an issue whose repository you cannot determine, is
14
+ outside this rule; never guess a number or a repository.
15
+ - **Read the assignees first**, in the issue's own repository:
16
+ `gh issue view <n> --repo <owner>/<repo> --json assignees,state`.
17
+ - **Assign only when nobody is assigned.** Run
18
+ `gh issue edit <n> --repo <owner>/<repo> --add-assignee @me` so the current `gh` user is
19
+ the assignee. `@me` resolves to whoever is authenticated, so it is the user the agent
20
+ works for, not a name you invent.
21
+ - **Never replace or add to an existing assignment.** Someone already on the issue means
22
+ it is claimed; leave it, and mention it if it affects the work.
23
+ - **Do it once, at the start,** before the first edit. Do not re-check on every turn.
24
+ - **Skip a closed issue** and say so; do not reopen or assign it.
25
+ - **If assigning fails** (no permission, `gh` not authenticated, the issue is in a
26
+ repository you cannot write to), say so in one line and continue with the work. A missing
27
+ assignee is never a reason to stop.
28
+
29
+ The user's request to work on the issue is the authority for this assignment; do not ask
30
+ before doing it. It authorizes assigning the issue and nothing else: commenting, labeling
31
+ or closing still follow [pull-requests.md](./pull-requests.md) and the user's request.
@@ -91,6 +91,8 @@ profile includes neither Screenplay nor Stage.
91
91
  | `cratis/engineering/typescript` | TypeScript engineering conventions | `cratis/engineering/core` |
92
92
  | `cratis/engineering/react` | React engineering conventions | `cratis/engineering/core` |
93
93
 
94
+ The `cratis/engineering` profile also carries [issue-assignment.md](./issue-assignment.md): when an agent starts work on a numbered issue in a known repository and nobody is assigned, it assigns the current `gh` user.
95
+
94
96
  ### Language Profiles
95
97
 
96
98
  | Profile ID | Description |
@@ -116,10 +116,13 @@ arguments?, paging?, sorting?)`; `Observe(...)` returns `QueryObservation.Initia
116
116
  and buffered `Next()`. Configure principal/policies in `Services`; schemes fail
117
117
  closed without HTTP. Chronicle query `Given` can seed keyed models/events.
118
118
 
119
- For guarded commands use `UseDecisionReads()` **before** seeding events. Given
120
- state, guarded reads and appends share one real in-process log, unlike legacy
121
- mode above. Pinned models cannot supply guards. Use `OnRoute(route)` and
122
- `AppendConcurrently` to prove conflicts; `ShouldHaveCompletedStream` proves
119
+ For guarded commands use plain `new CommandScenario<T>()`: it selects the decision
120
+ mode from the command's read-protection mode and dependencies (`UseDecisionReads()`
121
+ is hidden and unnecessary). Given state, guarded reads and appends share one real
122
+ in-process log, unlike legacy mode above; an `Off` command with no protected
123
+ dependency stays in legacy mode. Pinned models cannot supply guards. Use `OnRoute(route)` and
124
+ `AppendConcurrently` with `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`
125
+ to prove conflicts (there is no `scenario.AppendConditions`); `ShouldHaveCompletedStream` proves
123
126
  completion and `DecisionReadAdmissions.ShouldAdmitDecisionReadsIn(assembly)`
124
127
  checks admission. See **cratis-application-slice-specifications**,
125
128
  `references/query-and-decision-scenarios.md`, for sources and limitations.
@@ -88,7 +88,7 @@ The command carries input from the caller. `Handle()` is defined directly on the
88
88
  | Reusable value invariant (length, format, range) | `ConceptValidator<T>` on the concept type |
89
89
  | Command-input / cross-field / pre-handler rule (incl. injected read-model/service/identity checks) | `CommandValidator<TCommand>` with `RuleFor(...)` |
90
90
  | Handler needs fetched/computed data before it can build the event | `Provide()` — fetches the data; may validate it and short-circuit |
91
- | State-dependent rule that must hold **under concurrency** | `IStreamReads`/`StreamDecision` for one stream, or `[ProtectedDecision]` + `DecisionRead<T>` for admitted projected state; plain injection is advisory |
91
+ | State-dependent rule that must hold **under concurrency** | Read protection `On`: the read model as a `CommandValidator<T>` constructor (or `Handle()`) parameter; a per-stream read model or `IStreamReads`/`StreamDecision` for one stream; an aggregate for a rich lifecycle. `[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in. Under `Off` (default) injection is a hint |
92
92
  | Uniqueness | Chronicle `[Unique]` / `IConstraint` (race-safe) — not a read-model pre-check |
93
93
  | Genuinely exceptional failure (bug, missing infra) | `throw` a domain exception |
94
94
 
@@ -182,13 +182,21 @@ Three attributes scope an append broad→narrow, and each one *also* (only with
182
182
 
183
183
  Each attribute always tags the appended events (visible in `EventContext`). **Only when the command also carries `concurrency: true`** does each dimension join the server-side `ConcurrencyScope` (plus the event source id), with the optimistic strategy supplying the expected tail sequence number. A command **without** `concurrency: true` contributes no scope of its own, and the append falls to whatever concurrency strategy the event sequence is configured with — by default the optimistic one, scoped to the event source. So the attribute chooses *which dimensions* bound the check, not whether a check happens at all. Reach for it on high-contention state transitions where a stale read would let two commands both "win" — it narrows the boundary so appends that are genuinely independent stop colliding (concurrency failures surface on `CommandResult`/`AppendResult`). Adopt it deliberately per behavior, not as a blanket default.
184
184
 
185
- At Arc v22.75.0, prefer `IStreamReads.ForCommand()` in `Provide()` and return
186
- `StreamDecision.Append(...)` when deciding over one routed stream. For projected
187
- state use `[ProtectedDecision]` + `DecisionRead<T>`; plain injection and keyed
188
- reads are advisory, including in `Handle()`. Protected projections must be
189
- Chronicle-admitted (direct source key, known events; not reducers, joins, children
190
- or passive shapes); validators must be parameterless and certified by Arc.
191
- Exact returned scopes replace automatic `concurrency: true` capture.
185
+ New code sets `<CratisReadProtection>On</CratisReadProtection>` (Arc 22.106.0).
186
+ Under `On`, a read-model parameter of `Handle()` or of a `CommandValidator<T>`
187
+ constructor is read fresh and guards the append; the state rule goes in the
188
+ validator. Two entities: two parameters, `[LookUpBy(nameof(Cmd.Prop))]` on the
189
+ second, `IEnumerable<EventForEventSourceId>` returned. One stream inside a source:
190
+ a per-stream read model (`[ReadModel, EventStream<TSource>("Stream")]`) on a command
191
+ routed with `[EventSource<TSource>("Stream")]` and `[EventStreamId("{Prop}")]`, or
192
+ `IStreamReads.ForCommand()` in `Provide()` returning `StreamDecision.Append(...)`.
193
+ A deliberate advisory read is `[Unprotected("why")]`. Models must be admitted
194
+ (source-keyed projection with a string or Guid `Id`, or an unfiltered reducer
195
+ without constructor dependencies); joins, children and event-property keys are
196
+ refused. Under `Off` (the default) injection and keyed reads are advisory,
197
+ including in `Handle()`; `[ProtectedDecision]` + `DecisionRead<T>` is the older
198
+ per-command opt-in and still works. Exact returned scopes replace automatic
199
+ `concurrency: true` capture. Refusals: [choosing how a rule holds](../skills/cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
192
200
 
193
201
  `[EventStreamId("{ReportingScopeId}:{Period}")]` resolves command property values
194
202
  after validation; concepts supply their underlying values. An interface can
@@ -295,7 +303,7 @@ query reference for versioned behavior and composition trade-offs.
295
303
 
296
304
  ### Choosing read-model access in command-side code **[convention]**
297
305
 
298
- Ladder, first that fits: (1) advisory direct read-model injection — only when keyed by the command's own event-source id; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
306
+ Ladder, first that fits: (1) direct read-model injection — guarded under read protection `On`, advisory under `Off`; keyed by the command's own event-source id, or by `[LookUpBy]`; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
299
307
 
300
308
  **[contract] Passive state is not sink state.** `IMongoCollection<T>` cannot
301
309
  compute a passive read model; any leftover documents are not maintained state.
@@ -407,14 +415,15 @@ Redaction is a system-event request that removes payload availability from the e
407
415
 
408
416
  ## Cross-slice patterns **[convention]**
409
417
 
410
- Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs a decision read or constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
418
+ Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for a current read (a rule that must hold under concurrency needs read protection `On`, a stream decision or a constraint, see the `cratis-chronicle-decision-reads` skill's `references/choosing-how-a-rule-holds.md`), or react to its events. **Never** instantiate or DI another slice's command/handler/service.
411
419
 
412
420
  **DCB (Dynamic Consistency Boundary):** the boundary is the event state explicitly
413
421
  enrolled in guarded decisions, not every model a command reads. Use stream
414
- reads/exact scopes or `[ProtectedDecision]` + admitted `DecisionRead<T>`;
415
- plain validator/`Provide()`/`Handle()` injection is advisory. Stream-level
422
+ reads/exact scopes or read-model parameters under read protection `On`
423
+ (`[ProtectedDecision]` + `DecisionRead<T>` is the older per-command opt-in);
424
+ validator/`Provide()`/`Handle()` injection under `Off` is advisory. Stream-level
416
425
  `concurrency: true` scopes are another explicit boundary, not protection for an
417
- arbitrary snapshot. Specify conflicts with `UseDecisionReads()` and actual seeded
426
+ arbitrary snapshot. Specify conflicts with `AppendConcurrently` and actual seeded
418
427
  events, not pinned models.
419
428
 
420
429
  ## Dialogs
@@ -201,9 +201,10 @@ produces. Pin an instance only when the events would be noise.
201
201
  Both builder methods return `void`; there is nothing to await.
202
202
 
203
203
  That is **legacy** read-model mode: seeded events populate an in-memory model,
204
- not the append log. For protected or stream decisions call `UseDecisionReads()`
205
- before seeding; actual events and decisions then share one in-process log.
206
- Pinned models cannot supply decision guards. Read
204
+ not the append log. A command under read protection `On`, with a per-stream model or
205
+ with `[ProtectedDecision]` selects the decision mode itself (no `UseDecisionReads()`
206
+ call); actual events and decisions then share one in-process log. Pinned models
207
+ cannot supply decision guards. Read
207
208
  [query and decision scenarios](references/query-and-decision-scenarios.md) when
208
209
  specifying query pipelines, routed conflicts, projection admission or completion.
209
210
 
@@ -54,9 +54,18 @@ integration setup: scenario pins do not replace arbitrary stores.
54
54
 
55
55
  ## Guarded decisions
56
56
 
57
- Call `new CommandScenario<TCommand>().UseDecisionReads()` before seeding. This
58
- mode shares **one real in-process event log** among given events, guarded reads,
59
- constraints and returned appends. Seed actual events, not pinned read models.
57
+ Create `new CommandScenario<TCommand>()`. It selects Chronicle's in-process decision
58
+ mode from the command's read-protection mode and dependencies (for example under
59
+ `On`, or with a per-stream read model); do not call `UseDecisionReads()`, which is
60
+ hidden and unnecessary. This mode shares **one real in-process event log** among
61
+ given events, guarded reads, constraints and returned appends. Seed actual events
62
+ (`Given.ForEventSource(id).Events(...)`, or `.OnRoute(route).Events(...)`); protected
63
+ reads fold the log and ignore a pinned `.ReadModel(m)`, which only pins an advisory
64
+ (plain) read. Assert a lost race with
65
+ `result.ShouldHaveValidationErrorBecauseOf(ValidationResultReason.ConcurrencyViolation)`
66
+ after `scenario.AppendConcurrently(id, events...)`. There is no
67
+ `scenario.AppendConditions`. An `Off` command with no protected dependency stays in
68
+ legacy mode.
60
69
  `Given.ForEventSource(id).OnRoute(route).Events(...)` preserves routing;
61
70
  `scenario.EventRoutes.For<TSource>(stream).WithStreamId(...)` creates the route.
62
71
  Queue interference using `AppendConcurrently(id, route, event)` and assert a
@@ -159,11 +159,15 @@ protect plain HTTP snapshots, waits and long polls; older versions leave them un
159
159
 
160
160
  ### Validators are not an authorization boundary
161
161
 
162
- A `[ProtectedDecision]` command runs only certified, parameterless validators and
163
- refuses validators with constructor dependencies. A permission or ownership rule
164
- that depends on state does not belong in a validator (or in an `[Unprotected]`
165
- one): put it in `Provide()`/`Handle()` with `DecisionRead<T>` or `IDecisionReads`,
166
- and see the command-validation and Chronicle guidance.
162
+ Authorization runs before validation, so a caller who may not act never reaches a
163
+ validator. Express who may act through policies, roles and `[Authorize]`, not a
164
+ validator. A state rule ("the order is still open") belongs in a
165
+ `CommandValidator<T>` with the read model as a constructor parameter; under read
166
+ protection `On` that read guards the append, under `Off` (the default) it is a
167
+ hint, and in an `[Unprotected]` command it is advisory. A state-dependent
168
+ permission or ownership rule (this member owns the order) may sit there too, with
169
+ the same limit. Under `[ProtectedDecision]` without `On`, only a parameterless
170
+ validator runs; see the command-validation and Chronicle guidance.
167
171
 
168
172
  Read [authorization](references/authorization.md) for the guest rules, evaluation
169
173
  order, the two baselines, default access under Arc's two hosting models, the
@@ -250,8 +254,9 @@ To run locally with an identity, supply the forwarded headers yourself — see
250
254
  method directly.
251
255
  - Observable queries that serve private or revocable data have an emission guard
252
256
  backed by current session and membership state.
253
- - Permission and ownership rules in `[ProtectedDecision]` commands live in
254
- `Provide()`/`Handle()`, not in validators.
257
+ - State-dependent permission and ownership rules are guarded: read protection `On`
258
+ with the read model in the validator or handler, or `[ProtectedDecision]` commands
259
+ with the rule in `Provide()`/`Handle()`. Under `Off` they are hints.
255
260
  - Discovery endpoints (`/.cratis/commands`, `/.cratis/queries`,
256
261
  `/.cratis/identity-details/schema`, `/.cratis/users`, `/.cratis/tenants`) are
257
262
  tested under Production settings; introspection is restricted or disabled where it
@@ -319,17 +319,17 @@ privileged.
319
319
 
320
320
  ## Protected decisions and validators
321
321
 
322
- A `[ProtectedDecision]` command (`Cratis.Arc.Chronicle.ReadModels`) runs only
323
- certified, parameterless validators and **refuses** a validator whose constructor
324
- takes dependencies (a read model, `DecisionRead<T>`, `IDecisionReads`, any
325
- service). So an authorization or ownership rule put in such a validator — or in an
326
- `[Unprotected]` command's validator — is **not decision-safe**. Put
327
- state-dependent permission and ownership rules in `Provide()` or `Handle()` using
328
- `DecisionRead<T>` or `IDecisionReads`, and route to the validation and Chronicle
329
- guidance. Arc constructs/certifies discoverable parameterless validators;
330
- factory-supplied or altered rules fail closed. Keep static/service-locator state
331
- out of them too: certification is not proof of input purity. Validators that
332
- read decision state are a known limitation.
322
+ Under read protection `On`, the read models in a convention-registered
323
+ `CommandValidator<T>` constructor are read fresh and guard the command's append, so
324
+ a state-dependent permission or ownership rule written there is decision-safe. Under
325
+ `Off` (the default) and `Measure` the same validator reads a plain model: a hint. In
326
+ an `[Unprotected]` command (or a model or method marked `[Unprotected("why")]`) the
327
+ read is advisory, so the rule is **not decision-safe**. A `[ProtectedDecision]`
328
+ command without `On` runs only certified, parameterless validators and refuses a
329
+ validator whose constructor takes dependencies; put such a rule in `Provide()` or
330
+ `Handle()` using `DecisionRead<T>` or `IDecisionReads`. Hand-registered scoped,
331
+ singleton, factory or instance validators are refused under `On`. Keep
332
+ static/service-locator state out of validators: a guard covers reads Arc can see.
333
333
  Source: [protected validator boundary](https://github.com/Cratis/Arc/blob/main/Documentation/backend/csharp/chronicle/read-models/injecting-into-commands.md).
334
334
 
335
335
  ## Analyzer diagnostics
@@ -175,8 +175,9 @@ silently takes the first declared. Implement `ICanProvideEventSourceId`.
175
175
 
176
176
  Read [decisions and routing](references/decisions-and-routing.md) when a command
177
177
  needs guarded state, stream-id templates, definition-backed routes, event tags or
178
- post-commit `CompleteStream`. Plain read-model injection remains advisory;
179
- `Result<,>` and `concurrency: true` are not protection; see
178
+ post-commit `CompleteStream`. A read-model parameter guards the append only under
179
+ read protection `On`; under `Off` (the default) it is advisory. `Result<,>` and
180
+ `concurrency: true` are not protection; see
180
181
  [choosing how a rule holds](../cratis-chronicle-decision-reads/references/choosing-how-a-rule-holds.md).
181
182
  A returned `EventForEventSourceId` route wins over command context.
182
183