@cratis/pi 2.21.3 → 2.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package/corpus/agents/backend-developer.md +6 -5
- package/package/corpus/agents/code-reviewer.md +7 -7
- package/package/corpus/agents/coordinator.md +5 -4
- package/package/corpus/agents/frontend-developer.md +34 -40
- package/package/corpus/agents/orchestrator.md +5 -4
- package/package/corpus/agents/performance-reviewer.md +5 -4
- package/package/corpus/agents/planner.md +5 -4
- package/package/corpus/agents/repository-investigation-reviewer.md +2 -1
- package/package/corpus/agents/repository-investigator.md +2 -1
- package/package/corpus/agents/security-reviewer.md +5 -4
- package/package/corpus/agents/slice-implementer.md +8 -2
- package/package/corpus/agents/spec-writer.md +6 -4
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +52 -7
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +14 -0
- package/package/corpus/hooks/README.md +29 -27
- package/package/corpus/hooks/agent-stop.md +1 -5
- package/package/corpus/hooks/scripts/cratis-patterns.json +1 -1
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +1 -2
- package/package/corpus/prompts/verify-ai-setup.prompt.md +17 -7
- package/package/corpus/rules/ai-distribution.md +58 -0
- package/package/corpus/rules/components.md +77 -44
- package/package/corpus/rules/dialogs.md +24 -20
- package/package/corpus/rules/efcore.md +35 -187
- package/package/corpus/rules/framework.md +1 -1
- package/package/corpus/rules/frontend-quality.md +1 -1
- package/package/corpus/rules/frontend-testing.md +4 -4
- package/package/corpus/rules/general.md +29 -105
- package/package/corpus/rules/new-repository-intake.md +34 -0
- package/package/corpus/rules/orleans.md +13 -27
- package/package/corpus/rules/react.md +12 -12
- package/package/corpus/rules/reactors.md +28 -5
- package/package/corpus/rules/specs.scenarios.csharp.md +18 -3
- package/package/corpus/rules/specs.typescript.md +1 -1
- package/package/corpus/rules/storybook.md +2 -2
- package/package/corpus/rules/typescript.md +1 -0
- package/package/corpus/rules/vertical-slices.md +20 -15
- package/package/corpus/rules/writing-correct-examples.md +1 -0
- package/package/corpus/skills/cratis-application-react-specifications/SKILL.md +4 -4
- package/package/corpus/skills/cratis-application-slice-specifications/SKILL.md +16 -8
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +6 -4
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +1 -1
- package/package/corpus/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +7 -4
- package/package/corpus/skills/cratis-arc-command/SKILL.md +9 -7
- package/package/corpus/skills/cratis-arc-command/references/command-result.md +1 -1
- package/package/corpus/skills/cratis-arc-command/references/handler-shapes.md +2 -2
- package/package/corpus/skills/cratis-arc-command/references/proxy-generation.md +6 -3
- package/package/corpus/skills/cratis-arc-command/references/read-model-injection.md +1 -1
- package/package/corpus/skills/cratis-arc-command-execution/SKILL.md +12 -2
- package/package/corpus/skills/cratis-arc-command-validation/SKILL.md +5 -3
- package/package/corpus/skills/cratis-arc-ef-core-migration/SKILL.md +7 -4
- package/package/corpus/skills/cratis-arc-observable-query-http/SKILL.md +10 -3
- package/package/corpus/skills/cratis-arc-query-paging/SKILL.md +13 -8
- package/package/corpus/skills/cratis-arc-react-page/SKILL.md +27 -24
- package/package/corpus/skills/cratis-arc-react-page/references/data-page.md +15 -11
- package/package/corpus/skills/cratis-arc-react-page/references/data-tables.md +9 -2
- package/package/corpus/skills/cratis-arc-react-page/references/dialogs.md +11 -9
- package/package/corpus/skills/cratis-arc-react-page/references/queries-and-commands.md +26 -2
- package/package/corpus/skills/cratis-chronicle-cli-operations/SKILL.md +19 -13
- package/package/corpus/skills/cratis-chronicle-client-dotnet/SKILL.md +17 -15
- package/package/corpus/skills/cratis-chronicle-client-elixir/SKILL.md +24 -20
- package/package/corpus/skills/cratis-chronicle-client-kotlin/SKILL.md +26 -15
- package/package/corpus/skills/cratis-chronicle-client-typescript/SKILL.md +15 -11
- package/package/corpus/skills/cratis-chronicle-compliance/SKILL.md +14 -3
- package/package/corpus/skills/cratis-chronicle-event-constraints/SKILL.md +13 -6
- package/package/corpus/skills/cratis-chronicle-event-modeling/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-event-specifications/SKILL.md +11 -5
- package/package/corpus/skills/cratis-chronicle-event-type-migration/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-multi-tenancy/SKILL.md +2 -2
- package/package/corpus/skills/cratis-chronicle-projection/SKILL.md +23 -4
- package/package/corpus/skills/cratis-chronicle-projection/references/fluent-builder.md +1 -1
- package/package/corpus/skills/cratis-chronicle-projection/references/model-bound-attributes.md +2 -2
- package/package/corpus/skills/cratis-chronicle-reactor/SKILL.md +22 -8
- package/package/corpus/skills/cratis-chronicle-read-model/SKILL.md +1 -1
- package/package/corpus/skills/cratis-chronicle-read-model/references/queries.md +1 -1
- package/package/corpus/skills/cratis-chronicle-read-model-specifications/SKILL.md +3 -1
- package/package/corpus/skills/cratis-chronicle-reducer/SKILL.md +1 -1
- package/package/corpus/skills/cratis-chronicle-web-workbench/SKILL.md +19 -11
- package/package/corpus/skills/cratis-cli-terminal-workbench/SKILL.md +11 -6
- package/package/corpus/skills/cratis-code-review/SKILL.md +8 -6
- package/package/corpus/skills/cratis-components-accessibility/SKILL.md +112 -82
- package/package/corpus/skills/cratis-components-schema-editor/SKILL.md +12 -10
- package/package/corpus/skills/cratis-components-stepper-command-dialog/SKILL.md +11 -12
- package/package/corpus/skills/cratis-components-styling/SKILL.md +204 -170
- package/package/corpus/skills/cratis-components-toolbar/SKILL.md +32 -26
- package/package/corpus/skills/cratis-event-model-diagram/SKILL.md +1 -1
- package/package/corpus/skills/cratis-fundamentals-concept/SKILL.md +2 -2
- package/package/corpus/skills/cratis-fundamentals-type-discovery/SKILL.md +2 -2
- package/package/corpus/skills/cratis-lens-browser-extension/SKILL.md +4 -2
- package/package/corpus/skills/cratis-performance-review/SKILL.md +4 -2
- package/package/corpus/skills/cratis-screenplay-model-authoring/SKILL.md +15 -5
- package/package/corpus/skills/cratis-security-review/SKILL.md +20 -3
- package/package/corpus/skills/cratis-specifications-csharp/SKILL.md +3 -3
- package/package/corpus/skills/cratis-specifications-csharp/references/application-scenarios.md +12 -8
- package/package/corpus/skills/cratis-specifications-csharp/references/csharp-patterns.md +1 -1
- package/package/corpus/skills/cratis-specifications-csharp/references/integration-specs.md +7 -3
- package/package/corpus/skills/cratis-specifications-typescript/SKILL.md +6 -3
- package/package/corpus/skills/cratis-specifications-typescript/references/typescript-patterns.md +21 -4
- package/package/corpus/skills/cratis-stage-rendering-and-sandbox/SKILL.md +29 -17
- package/package.json +1 -1
|
@@ -10,41 +10,27 @@ paths:
|
|
|
10
10
|
> **⚠️ APPLIES ONLY TO PROJECTS USING MICROSOFT ORLEANS**
|
|
11
11
|
> If your project does not reference `Microsoft.Orleans` or any Orleans packages, **ignore this entire file**. These rules are irrelevant outside of Orleans contexts.
|
|
12
12
|
|
|
13
|
-
These conventions
|
|
13
|
+
In the Cratis organization, Orleans is the Chronicle kernel's actor runtime (`Source/Kernel/`). These conventions describe how that kernel is shaped, verified against `Cratis/Chronicle` **v18.3.0**; follow them in any Cratis repository that hosts Orleans grains.
|
|
14
14
|
|
|
15
15
|
## General
|
|
16
16
|
|
|
17
|
-
- Do **not** split
|
|
18
|
-
-
|
|
19
|
-
- Do **not** add `[Alias]`
|
|
17
|
+
- Do **not** split grain interfaces into a separate project. Grain interfaces and their implementations live together in `Kernel/Core`; the `Contracts` project holds the gRPC contracts, not grain interfaces.
|
|
18
|
+
- Clustering is chosen by the host, not the grain code: the server uses `UseLocalhostClustering(siloPort, gatewayPort, serviceId, clusterId)` for local development and the MongoDB storage integration switches on `UseMongoDBClustering` (`MongoDBMembershipStrategy.Multiple`) when clustering is enabled. A silo pointed at non-local shared storage while still on localhost clustering is logged as a **warning** at startup (each such node forms its own single-node cluster over the same data); `Cratis__Chronicle__Clustering__Type=MongoDB` on every node is what joins them.
|
|
19
|
+
- Do **not** add `[Alias]` to grain interfaces or grain methods — rely on the default alias Orleans generates. `[Alias(nameof(Type))]` is used only on `[GenerateSerializer]` **state and definition records** in `Kernel/Concepts` (`ReadModelDefinition`, `IndexDefinition`, `ReadModelChangeContext`, `ProjectionFuture`), where a stable serialization alias must survive a rename.
|
|
20
20
|
|
|
21
|
-
##
|
|
21
|
+
## Grain storage providers
|
|
22
22
|
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
-
|
|
23
|
+
- Storage provider names are constants in `WellKnownGrainStorageProviders` (`Kernel/Core`), **one name per concern** — 25 of them at v18.3.0 (`Namespaces`, `EventSequences`, `ObserverDefinitions`, `ObserverState`, `FailedPartitions`, `Jobs`, `JobSteps`, `Recommendations`, `Projections`, `ProjectionsManager`, `ProjectionFutures`, `Reactors`, `Reducers`, `ReducersManager`, `ReadModels`, `ReadModelsManager`, `ReadModelReplayManager`, `Constraints`, `EventStoreSubscriptionsManager`, `EventSeeding`, `Webhooks`, `WebhooksManager`, `DataProtectionKeys`, `PatchManager`, `System`). Reference a provider only through its constant — never a magic string, and there is no "default" provider.
|
|
24
|
+
- Each concern has its own `IGrainStorage` implementation (`JobGrainStorageProvider`, `EventSequencesStorageProvider`, `NamespacesStateStorageProvider`, …) taking the kernel's `IStorage` abstraction — **not** a MongoDB collection. The storage backend (MongoDB, SQL, in-memory) is selected behind `IStorage`, so a grain storage provider never references a database driver.
|
|
25
|
+
- Every provider is registered once, in `StorageProviderExtensions.AddStorageProviders(this ISiloBuilder)`, as a keyed singleton wrapped in `ResilientGrainStorage` (a Polly resilience pipeline configured from `Chronicle:ResilientStorage`). A new concern adds a constant, an `IGrainStorage` over `IStorage`, and one `AddKeyedSingleton(WellKnownGrainStorageProviders.<Name>, CreateResilientStorageFor<…>)` line there — nothing else.
|
|
26
|
+
- Grains declare their state with `[PersistentState(nameof(<StateType>), WellKnownGrainStorageProviders.<Name>)]`.
|
|
26
27
|
|
|
27
28
|
```csharp
|
|
28
|
-
public
|
|
29
|
+
public class JobGrainStorageProvider(IStorage storage) : IGrainStorage
|
|
29
30
|
{
|
|
30
|
-
|
|
31
|
+
// ReadStateAsync / WriteStateAsync / ClearStateAsync over
|
|
32
|
+
// storage.GetEventStore(key.EventStore).GetNamespace(key.Namespace).Jobs
|
|
31
33
|
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
- Annotate grains with `[StorageProvider(ProviderName = WellKnownStorageProviders.Default)]` when they need persistent state.
|
|
35
|
-
|
|
36
|
-
## MongoDB Grain Storage
|
|
37
34
|
|
|
38
|
-
|
|
39
|
-
- Register it as a keyed singleton in the Orleans silo builder:
|
|
40
|
-
|
|
41
|
-
```csharp
|
|
42
|
-
builder.Host.UseOrleans(siloBuilder =>
|
|
43
|
-
{
|
|
44
|
-
siloBuilder.UseLocalhostClustering();
|
|
45
|
-
siloBuilder.Services.AddKeyedSingleton<IGrainStorage>(
|
|
46
|
-
WellKnownStorageProviders.Default,
|
|
47
|
-
(sp, _) => new MongoDbGrainStorage<MyState>(
|
|
48
|
-
sp.GetRequiredService<IMongoCollection<MyState>>()));
|
|
49
|
-
});
|
|
35
|
+
services.AddKeyedSingleton(WellKnownGrainStorageProviders.Jobs, CreateResilientStorageFor<JobGrainStorageProvider>);
|
|
50
36
|
```
|
|
@@ -8,7 +8,7 @@ paths:
|
|
|
8
8
|
|
|
9
9
|
# React + Arc + Cratis Components
|
|
10
10
|
|
|
11
|
-
The frontend is React + TypeScript in MVVM style, composed from **Cratis Components** on top of **Arc-generated proxies**. Cratis Components is
|
|
11
|
+
The frontend is React + TypeScript in MVVM style, composed from **Cratis Components** on top of **Arc-generated proxies**. Cratis Components (4.x) owns its markup, styling contract and behavior — there is no vendor component library underneath to reach for; React Aria is an internal detail you never import. For component structure, styling, and icons see [components.md](./components.md); for dialogs see [dialogs.md](./dialogs.md). This rule covers MVVM, queries, and commands.
|
|
12
12
|
|
|
13
13
|
## The proxy boundary
|
|
14
14
|
|
|
@@ -81,7 +81,7 @@ Extract as soon as a component has **any** of: 3+ `useState`, any `useCallback`
|
|
|
81
81
|
|
|
82
82
|
These are registered automatically by `withViewModel` and injected via the primary constructor. Concrete usage:
|
|
83
83
|
|
|
84
|
-
- **`IMessenger`** — messages are plain classes; `_messenger.publish(new <Entity>Selected(id))` and `messenger.subscribe(<Entity>Selected, ({ id }) => ...)`. For scoped delivery, the messenger from `@cratis/arc/messaging` (distinct from `…mvvm/messaging`) resolves the nearest scope and falls back to root: `publish()`
|
|
84
|
+
- **`IMessenger`** — messages are plain classes; `_messenger.publish(new <Entity>Selected(id))` and `messenger.subscribe(<Entity>Selected, ({ id }) => ...)`. For scoped delivery, the messenger from `@cratis/arc/messaging` (distinct from `…mvvm/messaging`) resolves the nearest scope and falls back to root: a `<MessengerScope>` trickles `publish()` **down** into nested scopes by default (`trickleDownToChildren`, default `true`) and bubbles **up** only when `bubbleToParent` is set (default `false`) — both are props on `MessengerScope` / properties on the messenger, not options on `publish()`. Use the default `IMessenger` unless you genuinely need scoping.
|
|
85
85
|
- **`IDialogs`** — `await _dialogs.showConfirmation(title, message, DialogButtons.YesNo)` returns a `DialogResult`. For user-triggered dialogs that open React command dialogs, use `useDialog` in the component, not `IDialogs`.
|
|
86
86
|
- **`IIdentityProvider`** — `await _identityProvider.getCurrent<TDetails>()`. Use the `useIdentity()` hook only in functional components without a view model.
|
|
87
87
|
- **`ILocalStorage`** — `setItem(key, value)` / `getItem(key)`; **`IViewModelDetached.detached()`** runs on unmount (`detached() { this._subscription?.unsubscribe(); }`).
|
|
@@ -94,7 +94,7 @@ A typed proxy class is generated per query. Pick the hook:
|
|
|
94
94
|
|
|
95
95
|
| Hook | When |
|
|
96
96
|
|---|---|
|
|
97
|
-
| `<Query>.use(args?, sorting?)` | standard — returns `[result]`, re-renders on change |
|
|
97
|
+
| `<Query>.use(args?, sorting?)` | standard — returns `[result, perform, setSorting]`, re-renders on change |
|
|
98
98
|
| `<Query>.useSuspense(...)` | suspense-aware — throws while loading; wrap in `<QueryBoundary>` |
|
|
99
99
|
| `<Query>.when(condition).use(args)` | conditional — only fires when `condition` is true (don't wrap a hook in `if`) |
|
|
100
100
|
| `<Query>.useWithPaging(pageSize, args?, sorting?)` | server-side paging (backend returns `IQueryable<T>`) |
|
|
@@ -106,12 +106,12 @@ A typed proxy class is generated per query. Pick the hook:
|
|
|
106
106
|
|
|
107
107
|
| Hook | Returns |
|
|
108
108
|
|---|---|
|
|
109
|
-
| `<Query>.use(args?, sorting?)` | `[result, perform]` |
|
|
109
|
+
| `<Query>.use(args?, sorting?)` | `[result, perform, setSorting]` |
|
|
110
110
|
| `<Query>.useSuspense(...)` | `[result, perform, setSorting]` |
|
|
111
111
|
| `<Query>.useWithPaging(pageSize, args?, sorting?)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
112
112
|
| `<Query>.useSuspenseWithPaging(pageSize, ...)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
113
|
-
| `<ObservableQuery>.use(...)` | `[result, setSorting]` — **no `perform`** |
|
|
114
|
-
| `<ObservableQuery>.useChangeStream(args?, getKey?, sorting
|
|
113
|
+
| `<ObservableQuery>.use(...)` | `[result, setSorting]` for an enumerable query; `[result]` for a single-instance one — **no `perform`** |
|
|
114
|
+
| `<ObservableQuery>.useChangeStream(args?, getKey?, sorting?)` | `ChangeSet<T> { added, replaced, removed }` |
|
|
115
115
|
|
|
116
116
|
`result.paging` = `{ page, size, totalItems, totalPages }` (zero-based `page`). Read models returning `IQueryable<TReadModel>` get automatic server-side paging/sorting — use it whenever a list can grow.
|
|
117
117
|
|
|
@@ -141,7 +141,7 @@ Inside dialogs, use `CommandDialog` + CommandForm fields rather than driving the
|
|
|
141
141
|
| `isValid` | validation failed — `validationResults` has per-field messages | render inline field errors |
|
|
142
142
|
| `hasExceptions` | `Provide()`/`Handle()` threw — `exceptionMessages` carry diagnostics | generic error toast + log; never show stack traces |
|
|
143
143
|
|
|
144
|
-
`CommandDialog` handles all of this automatically; the flags matter most when executing a command **outside** a dialog. `validate()` returns the same shape with `response == null` (handler did not run). `result.validationResults` is a `ValidationResult[]` of `{
|
|
144
|
+
`CommandDialog` handles all of this automatically; the flags matter most when executing a command **outside** a dialog. `validate()` returns the same shape with `response == null` (handler did not run). `result.validationResults` is a `ValidationResult[]` of `{ severity, message, members: string[], state, reason, reasonDetail? }` — `members` names the affected properties — render per-field UI from it, but **never branch on raw `.message` text**. `exceptionMessages`/`exceptionStackTrace` are for logging, never for users.
|
|
145
145
|
|
|
146
146
|
Out-of-dialog execution branches in this order:
|
|
147
147
|
|
|
@@ -157,11 +157,11 @@ if (result.hasExceptions) { toast.error('Something went wrong'); console.error(r
|
|
|
157
157
|
|
|
158
158
|
### Command helpers
|
|
159
159
|
|
|
160
|
-
- **`useCommandInstance(
|
|
160
|
+
- **`useCommandInstance<TCommand>()`** — inside a `CommandForm`/`CommandDialog`, read the live reactive command instance from context for dependent fields (no argument; the type parameter is the command type) (read it; never mutate — mutations go through field bindings).
|
|
161
161
|
- **`onBeforeExecute`** is a **transformer**: it receives the current values and **must return them** (mutated or not). It runs only on submit — never use it to seed required values (use `initialValues`).
|
|
162
162
|
- **`initialValues`** = synchronous baseline (also drives change tracking); **`currentValues`** = reactive overlay for async/late-loading values (e.g. from a query).
|
|
163
163
|
- **`asCommandFormField<P>(Component, opts)`** — wrap a custom input (rich text, address picker) so it participates in `CommandForm` with validation/error wiring.
|
|
164
|
-
- **`CommandScope`** + `useCommandScope()` (`@cratis/arc.react/commands`) — aggregate state across multiple commands (and queries) on one screen;
|
|
164
|
+
- **`CommandScope`** + `useCommandScope()` (`@cratis/arc.react/commands`) — aggregate state across multiple commands (and queries) on one screen; commands and queries rendered inside the scope register themselves (the `ICommandScope` API also exposes `addCommand`/`addQuery`/`addChildScope` for manual registration); scopes nest (a child reports up to its parent); injectable as `ICommandScope` (nearest enclosing scope). Props: `setHasChanges`, `setIsPerforming`, `onBeforeExecute`, `onSuccess(cmd, result)`, `onFailed`, `onException`, `onUnauthorized`. `ICommandScope` members: `hasChanges`, `isPerforming`, `hasValidationFailures`, `hasExceptions`, `validationFailures`/`exceptions` (this scope), `aggregatedValidationFailures`/`aggregatedExceptions` (scope + children), `execute()` (runs only commands with changes), `revertChanges()`, `parent`.
|
|
165
165
|
|
|
166
166
|
### `<Arc>` global configuration
|
|
167
167
|
|
|
@@ -175,7 +175,7 @@ Top-level provider for microservice name and API base path. Config props (with d
|
|
|
175
175
|
| `queryConnectionCount` | `1` | hub connection slots |
|
|
176
176
|
| `queryDirectMode` | `false` | direct per-query connection — dev use only |
|
|
177
177
|
|
|
178
|
-
`QueryTransportMethod
|
|
178
|
+
`QueryTransportMethod` imports from `@cratis/arc/queries` and `ObservableQueryTransferMode` from `@cratis/arc`; `Arc` from `@cratis/arc.react`. Reach `arc.reconnectQueries()` via `useContext(ArcContext)` and call it after login/logout to re-establish observable connections.
|
|
179
179
|
|
|
180
180
|
## Composition
|
|
181
181
|
|
|
@@ -185,11 +185,11 @@ Top-level provider for microservice name and API base path. Config props (with d
|
|
|
185
185
|
|
|
186
186
|
## Styling default
|
|
187
187
|
|
|
188
|
-
Default to **Cratis Components
|
|
188
|
+
Default to **Cratis Components with the `--cratis-*` tokens** (`var(--cratis-surface-*)`, `var(--cratis-primary-color)`, `var(--cratis-text-color)`; `pt`/`data-cratis-part` when you must reach a part) — see [components.md](./components.md). Tailwind is **not** the Cratis default: it is one supported way to *write* the CSS that maps a product's design onto those tokens. Never hard-code hex/`rgb()` for chrome.
|
|
189
189
|
|
|
190
190
|
## See also
|
|
191
191
|
|
|
192
|
-
- [components.md](./components.md) — component structure, CSS,
|
|
192
|
+
- [components.md](./components.md) — component structure, CSS, `--cratis-*` tokens and parts, icons, Storybook.
|
|
193
193
|
- [dialogs.md](./dialogs.md) — `CommandDialog` / `Dialog` / `StepperCommandDialog` rules.
|
|
194
194
|
- [frontend-quality.md](./frontend-quality.md) — engineering bar; [frontend-testing.md](./frontend-testing.md) — BDD specs.
|
|
195
195
|
- [typescript.md](./typescript.md) — TS style.
|
|
@@ -159,6 +159,23 @@ public Task<IEnumerable<EventForEventSourceId>> Handle(AnEvent @event, EventCont
|
|
|
159
159
|
|
|
160
160
|
> **Chronicle version note:** reactor side-effect handling of `EventForEventSourceId` wrappers has shipped since Chronicle 15.35.
|
|
161
161
|
|
|
162
|
+
### Return a command (Arc + Chronicle)
|
|
163
|
+
|
|
164
|
+
When the follow-up belongs to **another slice**, return its `[Command]` instead of an event — the same idiom as returning events, applied to intent. Arc's Chronicle integration executes a returned command through the full pipeline (validation, authorization, `Handle()`) in its own service scope; a collection is executed in order when **every** element is a command (events and commands do not mix in one return). An unauthorized, invalid or throwing result is a **side-effect failure** — it fails the partition like a failed append, so it is never silently dropped.
|
|
165
|
+
|
|
166
|
+
```csharp
|
|
167
|
+
public class StockKeeping : IReactor
|
|
168
|
+
{
|
|
169
|
+
[OnceOnly]
|
|
170
|
+
public Task<DecreaseStock> BookReserved(BookReserved @event, EventContext context) =>
|
|
171
|
+
Task.FromResult(new DecreaseStock(@event.Isbn, @event.Quantity));
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
- **A reactor runs with no principal.** It is not an HTTP request, so a returned command gated by `[Roles]`/`[Authorize]` is denied. Opt in on the reactor **class** with `[ExecuteCommandsAsSystem("<role>", …)]` (`Cratis.Arc.Chronicle.Reactors`), naming only the roles its returned commands need. The attribute covers **returned** commands only.
|
|
176
|
+
- Mark the handler `[OnceOnly]` — a replay would otherwise execute the command again. Replay exclusion is not exactly-once: recovery re-delivers, so the command itself must be safe to repeat.
|
|
177
|
+
- The imperative alternative — inject `ICommandPipeline` and call `Execute(command)` — remains supported (see the **cratis-arc-command-execution** skill). It needs `[OnceOnly]` (`ARCCHR0006` warns otherwise), its `CommandResult` must be inspected (a denied or invalid result is otherwise discarded), and it needs its own execution context: `[ExecuteCommandsAsSystem]` does not apply to it.
|
|
178
|
+
|
|
162
179
|
## External event stores (outbox / inbox)
|
|
163
180
|
|
|
164
181
|
Cross-service events route through Chronicle's outbox/inbox rather than a shared log:
|
|
@@ -176,11 +193,12 @@ The default is fire-and-forget. When a caller's correctness depends on all obser
|
|
|
176
193
|
|
|
177
194
|
1. **Idempotent** — Reactors may be called more than once for the same event (e.g. during replay or recovery). Design accordingly. For a side effect that must **not** repeat on replay (emails, payments, external writes), mark the handler method `[OnceOnly]` — Chronicle then skips that handler for every event arriving as part of a **replay** (observer rewind, redaction, revision). That is the whole of it: `[OnceOnly]` is replay-exclusion, **not** exactly-once and **not** a per-event-source counter. Recovering a failed partition re-delivers the event as an ordinary observation, so the handler runs again — which is the point of a retry. When the side effect must survive that retry too, take a `ReactorDelivery` parameter alongside the attribute and keep a receipt under its identity, which is stable across the failure and the recovery.
|
|
178
195
|
2. **Use event data directly** — Never query the read model back inside a reactor. The event contains all the information you need.
|
|
179
|
-
3. **Return events instead of injecting
|
|
196
|
+
3. **Return events and commands instead of injecting the plumbing** — If the reactor needs to produce new events, return them directly as `Task<TEvent>`, `Task<EventForEventSourceId>`, or a collection thereof. For intent in another slice, return the `[Command]` (Arc executes it; a denied or invalid result fails the partition) — or inject `ICommandPipeline` and inspect the result. Either way the handler is `[OnceOnly]`. Avoid injecting `IEventLog` directly into a reactor.
|
|
180
197
|
4. **Single responsibility** — Each reactor class should have a focused purpose. Multiple handler methods in one reactor are fine if they serve the same automation concern.
|
|
181
198
|
5. **Failure behavior** — If a reactor throws, *or* a returned side-effect event fails to append (constraint violation, concurrency violation, or error), the failing event-source partition pauses until the issue is resolved. Repeated failures can **quarantine** the observer: once `QuarantineOnFailedPartitionCount`/`QuarantineOnFailedPartitionPercentage` (under `Observers`) is crossed, the observer enters the `Quarantined` state — reminders cancelled, retries stopped, automatic recovery suppressed. **A quarantined observer does NOT auto-resume on reconnect** — an operator must call `ClearObserverQuarantine()`. A periodic watchdog (default 60s, `WatchdogInterval`) re-routes stuck/dead observers but does not rescue quarantined ones. Design for resilience.
|
|
182
199
|
6. **Don't throw to validate malformed inbound events** — reactors are not data-quality validators; invalid payloads must be rejected at the command/append site. When a malformed cross-service fact reaches a consumer, throwing just to reject it pauses the partition and can quarantine the whole observer. Instead append a clear failure/dead-letter event (e.g. `ProvisioningFailed`) or surface it via the operational failure path, and skip partial side effects.
|
|
183
200
|
7. **No state** — Reactors should be stateless. Inject dependencies via primary constructor, but do not store mutable state on the class.
|
|
201
|
+
8. **An unattended, irreversible pass reads the log, not the sink — and carries a fuse.** A reactor that deletes, revokes, bills or notifies *because a read model says something is absent* is trusting a materialized sink that is only as current as its observer: a paused partition, a replay in progress or a lagging projection makes "absent" true of the sink and false of the world. Decide from a **`[Passive]`** read model (computed on demand from the events at the moment the reactor runs) or from the events themselves. And the code shape cannot tell an **empty subject set** ("nothing to consider" — fail-safe) from an **empty qualifying set** ("nothing survived the filter" — fail-destructive: every subject is about to be acted on), so an unattended pass states its expected population, refuses when the count is implausible, and caps how many subjects one run may touch — the same discipline as [guards-and-fuses.md](./guards-and-fuses.md), applied to a reactor.
|
|
184
202
|
|
|
185
203
|
## Slice Types That Use Reactors
|
|
186
204
|
|
|
@@ -207,18 +225,23 @@ public class ProjectRegisteredNotifier(INotificationService notifications) : IRe
|
|
|
207
225
|
### Translation Example
|
|
208
226
|
|
|
209
227
|
```csharp
|
|
210
|
-
|
|
228
|
+
[ExecuteCommandsAsSystem("inventory")]
|
|
229
|
+
public class StockKeeping : IReactor
|
|
211
230
|
{
|
|
212
231
|
/// <summary>
|
|
213
|
-
/// Reacts to a book reservation by decreasing stock.
|
|
232
|
+
/// Reacts to a book reservation by decreasing stock in the inventory slice.
|
|
214
233
|
/// </summary>
|
|
215
234
|
/// <param name="event">The event.</param>
|
|
216
235
|
/// <param name="context">The event context.</param>
|
|
217
|
-
|
|
218
|
-
|
|
236
|
+
/// <returns>The command Arc executes as this reactor's side effect.</returns>
|
|
237
|
+
[OnceOnly]
|
|
238
|
+
public Task<DecreaseStock> BookReserved(BookReserved @event, EventContext context) =>
|
|
239
|
+
Task.FromResult(new DecreaseStock(@event.Isbn, @event.Quantity));
|
|
219
240
|
}
|
|
220
241
|
```
|
|
221
242
|
|
|
243
|
+
The command is returned, not executed: Arc runs it through validation and authorization under the roles the class opts into, and a rejected command fails the partition instead of vanishing. `[OnceOnly]` keeps a replay from decreasing stock twice.
|
|
244
|
+
|
|
222
245
|
## Testing Reactors
|
|
223
246
|
|
|
224
247
|
Use `ReactorScenario<TReactor>` (from `Cratis.Chronicle.Testing.Reactors`) — construct it with an `IServiceProvider` of NSubstitute mocks, fire events through `Given`, and assert on the mocks:
|
|
@@ -51,10 +51,10 @@ public class and_all_information_is_valid : Specification
|
|
|
51
51
|
#endif
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
- `CommandScenario<TCommand>` exposes `Services` (an `IServiceCollection`), `Context`, `Execute(command)`,
|
|
54
|
+
- `CommandScenario<TCommand>` exposes `Services` (an `IServiceCollection`), `Context`, `Execute(command[, ct])`, `Validate(command)`, `LastResult`, `Operations` and the operation assertions `ShouldHaveExecutedOperation<T>()` / `ShouldHaveCompensatedOperation<T>()` / `ShouldHaveNoOperationInvocations()`; with the Chronicle integration it also gains `Given`, `EventScenario`, `EventLog`, `EventSequence` and `AppendedEvents` as extension members. Everything else is configured through `Services` and asserted with extension methods.
|
|
55
55
|
- **Event assertions are extension methods on the scenario** (from the Chronicle testing package), keyed by **command + event** type: `await _scenario.ShouldHaveAppendedEvent<TCommand, TEvent>(eventSourceId)` or the `(eventSourceId, Func<TEvent,bool> predicate)` overload; plus `await _scenario.ShouldHaveTailSequenceNumber<TCommand>(...)`. They return `Task`, so the fact is `async Task`.
|
|
56
56
|
- **`CommandResult` assertions** (Arc extensions): `ShouldBeSuccessful()`, `ShouldNotBeSuccessful()`, `ShouldBeValid()`, `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`/`ShouldNotHaveExceptions()`.
|
|
57
|
-
- **Seed prior state through `_scenario.
|
|
57
|
+
- **Seed prior state through `_scenario.Given` or `_scenario.Services`.** `_scenario.Given.ForEventSource(id).Events(...)` materializes the seeded events into an **in-memory read-model dictionary** the harness answers keyed reads from — it never reaches the event log (see the false-greens list below). To populate the DCB read models the validator/`Provide()`/`Handle()` inject, substitute `IReadModels` and register it (`_scenario.Services.Replace(new ServiceDescriptor(typeof(IReadModels), mock))`, mocking `GetInstanceById(...)`) or register projections with `_scenario.Services.AddReadModels(...)`.
|
|
58
58
|
- **Validator/`Provide()` dependencies:** register them in `_scenario.Services`; Arc testing discovers the concrete validator automatically. When several specs need different injected validator states, test rejected variants by instantiating the validator directly (per-scenario state can be order-sensitive under parallel xUnit).
|
|
59
59
|
- **`Provide()`:** drive it through `CommandScenario` end-to-end; when the handler's decision is pure given provided data, also test `Handle(providedValue)` directly.
|
|
60
60
|
|
|
@@ -69,7 +69,21 @@ Every unhappy-path spec asserts **both**:
|
|
|
69
69
|
|
|
70
70
|
`ShouldNotBeSuccessful()` alone can't tell a validation rejection from an unhandled exception. **Never assert on message strings** — they're presentation text. Authorization failures use `ShouldNotBeAuthorized()` (an unauthorized result has *no* validation errors, so `ShouldHaveValidationErrors()` would silently flip). To exercise a command that carries authorization attributes, register the identity the authorization evaluator reads into `_scenario.Services` (substitute the identity provider your app uses). **Adding `[Roles]` to an existing command breaks its happy-path AND validation-failure `.Execute()` specs** — an unauthorized result is not successful and carries no validation errors; switch those assertions to `ShouldNotBeAuthorized()`.
|
|
71
71
|
|
|
72
|
-
`CommandResult` assertions (from `Cratis.Arc.Testing.Commands`; failures throw `CommandResultAssertionException`): `ShouldBeSuccessful()` (`isAuthorized && isValid && !hasExceptions`), `ShouldNotBeSuccessful()`, `ShouldBeValid()` (no validation errors — does *not* check authz/exceptions), `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`.
|
|
72
|
+
`CommandResult` assertions (from `Cratis.Arc.Testing.Commands`; failures throw `CommandResultAssertionException`): `ShouldBeSuccessful()` (`isAuthorized && isValid && !hasExceptions`), `ShouldNotBeSuccessful()`, `ShouldBeValid()` (no validation errors — does *not* check authz/exceptions), `ShouldHaveValidationErrors()`, `ShouldHaveValidationErrorFor(message)`, `ShouldHaveValidationErrorBecauseOf(ValidationResultReason)`, `ShouldHaveConstraintViolationFor(name)`, `ShouldBeAuthorized()`, `ShouldNotBeAuthorized()`, `ShouldHaveExceptions()`.
|
|
73
|
+
|
|
74
|
+
#### False greens the harness lets through
|
|
75
|
+
|
|
76
|
+
Each of these passes while proving nothing. They are the reason the two-assertion rule above exists.
|
|
77
|
+
|
|
78
|
+
1. **A discarded assertion `Task`.** The `IEventSequence` assertions `ShouldHaveAppendedEvent`/`ShouldHaveTailSequenceNumber` (Chronicle Testing) are genuinely `async`: in a `void [Fact]` that does not `await` them the exception lands on a `Task` nobody observes and the spec passes regardless of behavior, while the compiler's `CS4014` stays silent outside an `async` method. (The same-named `CommandScenario` overloads in Arc's Chronicle.Testing throw synchronously and only *return* a completed `Task`, so those fail even when discarded — but you cannot tell the two apart at the call site.) Make the fact `async Task` and `await` every `Task`-returning `Should*`. Chronicle's analyzer **`CHR0039` (`NonAwaitedAssertion`)** reports any discarded `Task`/`ValueTask`-returning `Should*` call from a Cratis testing package (Chronicle's and Arc's alike).
|
|
79
|
+
2. **A dependency the spec forgot to seed.** When a validator's constructor, `Provide()` or `Handle()` asks for a read model that does not exist for the command's key, the validator is never built and no rule runs — yet the command is rejected (one validation result, reason **`DependencyUnavailable`**, HTTP 400), so `ShouldNotBeSuccessful()` passes and keeps passing with the rule it is named after deleted. `ShouldHaveValidationErrors()` **refuses** a result whose only errors carry that reason and tells you to seed the read model — or, when that rejection *is* the behavior under spec, assert it deliberately with `ShouldHaveValidationErrorBecauseOf(ValidationResultReason.DependencyUnavailable)`. A missing *service* (not a read model) surfaces as an exception instead (`ShouldHaveExceptions()`), and `ShouldNotBeSuccessful()` alone cannot tell the two apart from a genuine rejection.
|
|
80
|
+
3. **`.Validate()` authorizes first.** `Validate(command)` runs the command filters, and authorization filters are ordered before validation. A validate-only spec on a `[Roles]`/`[Authorize]` command with no identity registered is denied for authorization — the rule it is named after never ran. Register the identity, or assert `ShouldNotBeAuthorized()` and say so in the name.
|
|
81
|
+
4. **Constraint preconditions go to the event log, not `CommandScenario.Given`.** `_scenario.Given.ForEventSource(id).Events(...)` on a `CommandScenario` seeds an **in-memory read-model materialization only**; nothing reaches the event log a `[Unique]`/`IConstraint` is enforced against, so a "second registration with the same name" spec passes because the first one was never appended. Seed constraint preconditions through **`EventScenario`**'s `Given`, and assert the violation by constraint **name** (`ShouldHaveConstraintViolationFor`) — the same helper exists on `CommandResult` and on `IAppendResult`.
|
|
82
|
+
5. **`GetInstances<T>` is not harness-backed.** Under `CommandScenario` the read-model harness answers keyed reads (`GetInstanceById`) from its seeded dictionary and forwards `GetInstances<T>`/`GetSnapshotsById`/`Watch` to the real client, which is not there. Code under spec that enumerates a read model needs a fake lookup registered in `Services`, not `Given`.
|
|
83
|
+
6. **One child is not a children spec.** `[ChildrenFrom<T>]` without `key:` identifies children by the **event source id**, so every child event on one stream upserts the same child and a one-child spec stays green with the key wrong. Drive **two** children with distinct keys and assert the count.
|
|
84
|
+
7. **Independently derived expectations.** An expected value computed in the spec with the same expression as the code under spec agrees with it by construction. Write the expected literal.
|
|
85
|
+
|
|
86
|
+
The `ReadModelScenario` and `EventScenario` equivalents of these live in the **cratis-chronicle-read-model-specifications** and **cratis-chronicle-event-specifications** skills.
|
|
73
87
|
|
|
74
88
|
> **Validator-state order-sensitivity:** the command-scenario pipeline can cache enough state to make injected-validator branches order-sensitive when xUnit runs classes in parallel. Use `CommandScenario` for the valid path, test rejected state-variants by **instantiating the validator directly**, and — only for that validator-state case — put the command's specs in a **small xUnit `[Collection]`**. (This is distinct from the unique-value-collision rule below, where `[Collection]` is the *wrong* fix.)
|
|
75
89
|
|
|
@@ -151,6 +165,7 @@ public class and_name_already_exists(context context) : Given<context>(context)
|
|
|
151
165
|
}
|
|
152
166
|
```
|
|
153
167
|
|
|
168
|
+
- `ChronicleCollection` is a project-defined `[CollectionDefinition]` class over `ChronicleOutOfProcessFixture` (Chronicle ships the fixture, not the collection).
|
|
154
169
|
- `ExecuteCommand<TCommand>(url, cmd)` returns `CommandResult<object>?`; `ExecuteCommand<TCommand, TResult>(url, cmd)` returns `CommandResult<TResult>?`.
|
|
155
170
|
- Helpers: `Context.ShouldHaveTailSequenceNumber(n)` (First = 0), `Context.ShouldHaveAppendedEvent<TEvent>(seq, eventSourceId, validator)`.
|
|
156
171
|
|
|
@@ -40,7 +40,7 @@ for_EventsCommandResponseValueHandler/
|
|
|
40
40
|
|
|
41
41
|
## BDD Pattern with `given()` Helper
|
|
42
42
|
|
|
43
|
-
The `given()` function is the TypeScript
|
|
43
|
+
The `given()` function is the TypeScript counterpart of the C# `Specification` base class. It instantiates a context class (the "given") **once when the `describe` is registered**, passes that instance to the suite callback, and leaves the per-behavior action to a `beforeEach` you write. Because the context is shared by every `it()` in the `describe`, keep one action per `describe` and let `it()` blocks only assert — that is what keeps Establish/Because/should consistent across both stacks.
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
46
|
import { an_events_command_response_value_handler } from '../given/an_events_command_response_value_handler';
|
|
@@ -17,7 +17,7 @@ Good candidates: a presentational card/list-item/form-field with multiple visual
|
|
|
17
17
|
|
|
18
18
|
## Preview infrastructure (what you get for free)
|
|
19
19
|
|
|
20
|
-
A configured `.storybook/preview` should wrap every story in `CratisComponentsProvider` (from `@cratis/components
|
|
20
|
+
A configured `.storybook/preview` should wrap every story in `CratisComponentsProvider` (from `@cratis/components`) and import `@cratis/components/tokens`, `@cratis/components/styles` and (for the baseline look) `@cratis/components/theme` — the same three entries the app imports — so components render styled. With that in place:
|
|
21
21
|
|
|
22
22
|
- **Auto prop tables** via `react-docgen-typescript` — a component's `interface` + per-prop JSDoc renders as a Docs prop table, and union props become `select` controls automatically. Keep a JSDoc comment on every prop. **`argTypes` is an override layer** (change control type, group under `table.category`, disable a control) — don't re-list every prop.
|
|
23
23
|
- **Accessibility panel** (`@storybook/addon-a11y`) scans each story with axe-core. Treat new violations as defects.
|
|
@@ -67,7 +67,7 @@ export const Interactive: Story = {
|
|
|
67
67
|
};
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
- **Stateful only** — presentational atoms get no `play:`.
|
|
70
|
+
- **Stateful only** — presentational atoms get no `play:`. Components' own overlay components (dialogs, dropdowns, toasts) are excluded — their behavior is the library's, not yours.
|
|
71
71
|
- Prefer role/structure queries over localized text. `play:` runs in the Interactions panel (documentation/QA); the enforced behavioral gate is the Vitest spec.
|
|
72
72
|
|
|
73
73
|
## Router-dependent components
|
|
@@ -121,6 +121,7 @@ All identifiers, comments, JSDoc, and string literals must use **American Englis
|
|
|
121
121
|
|
|
122
122
|
## Folder Structure
|
|
123
123
|
|
|
124
|
+
- **Never give a `.tsx` the same basename as a `.ts` in the same folder.** The Arc proxy generator emits `<Slice>.ts` beside the slice's C#; a component named `<Slice>.tsx` next to it is **silently dropped from the program**: for one basename TypeScript keeps only the `.ts`, so the component's type errors are never reported and `tsc -b` stays green while the file is broken (reproduced on TypeScript 6.0.3 and 7.0.2 with `include: ["src/**/*"]`). Name the component for what it renders (`Listing.tsx`, `RegisterAuthor.tsx`), never for the slice, and consider a repository spec that fails on any `.tsx` with a sibling `.ts` of the same name.
|
|
124
125
|
- Do not prefix a file, component, type, or symbol with the name of its containing folder or the concept it belongs to. Instead, use folder structure to provide that context.
|
|
125
126
|
- Favor functional folder structure over technical folder structure.
|
|
126
127
|
- Group files by the feature or concept they belong to, not by their technical role.
|
|
@@ -17,12 +17,12 @@ This file is the reference for *what* goes in each part of a slice. Layout, slic
|
|
|
17
17
|
## Technical stack
|
|
18
18
|
|
|
19
19
|
- .NET / C# (ASP.NET Core) — **Cratis Arc** for CQRS / model-bound commands and queries, **Cratis Chronicle** for event sourcing, MongoDB or EF Core for read models.
|
|
20
|
-
- React + TypeScript (Vite) — Cratis Components
|
|
20
|
+
- React + TypeScript (Vite) — Cratis Components 4.x + Arc-generated proxies, MVVM. Vitest + Mocha/Chai/Sinon for frontend specs.
|
|
21
21
|
- xUnit + Cratis.Specifications + NSubstitute for C# specs (the `*Scenario` family — see [specs.md](./specs.md)).
|
|
22
22
|
|
|
23
23
|
## Proxy generation — the build dependency
|
|
24
24
|
|
|
25
|
-
A
|
|
25
|
+
A `dotnet build` with `CratisProxiesOutputPath` set generates a typed TypeScript proxy per command and query next to the slice's `.cs` — the generator target has **no configuration condition**; running it from the Debug build is the house convention (one configuration owns the generated files, so Release never rewrites them differently). The generator reads PDB sequence points only to name output files after their source file, and only when `CratisProxiesUseSourceFileAsOutputFile` is on; Arc's own remark says which types retain that information *can differ* between Debug and Release, which is one more reason to let a single configuration generate. Until the backend compiles, **no proxy files exist** and frontend code cannot reference them. **Backend → build → frontend, always** — backend and frontend for the same slice cannot be done in parallel. When a subsequent Release build is only there to verify the app compiles in that configuration, skip proxy regeneration with `dotnet build -c Release -p:CratisProxiesOutputPath=` rather than letting it re-run the generator against a different compilation. Never edit a generated proxy (`// @generated by Cratis` header); fix the C# source and rebuild.
|
|
26
26
|
|
|
27
27
|
## Model-bound — no controllers **[contract]**
|
|
28
28
|
|
|
@@ -116,7 +116,7 @@ Values render camel-cased; concepts unwrap to the value they hold; a value that
|
|
|
116
116
|
|
|
117
117
|
### `Provide()` — data for `Handle()` **[contract]**
|
|
118
118
|
|
|
119
|
-
`Provide()` runs after authorization and validation, before `Handle()`. Its parameters resolve from DI (like `Handle()`'s); the command instance is `this`. Return one value, or a tuple (Arc binds tuple values to `Handle(...)` parameters by type). Each provided value
|
|
119
|
+
`Provide()` runs after authorization and validation, before `Handle()`. Its parameters resolve from DI (like `Handle()`'s); the command instance is `this`. Return one value, or a tuple (Arc binds tuple values to `Handle(...)` parameters by type). Each provided value should be consumed by a `Handle` parameter — an unused one raises the **`ARC0005`** analyzer warning. Use a **named record** when the provided values form a real domain/snapshot concept; a **tuple** when they are just separate inputs to event construction. It may be sync or async.
|
|
120
120
|
|
|
121
121
|
```csharp
|
|
122
122
|
[Command]
|
|
@@ -163,11 +163,13 @@ Three attributes scope an append broad→narrow, and each one *also* (only with
|
|
|
163
163
|
|
|
164
164
|
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.
|
|
165
165
|
|
|
166
|
-
When the stream id is known only at runtime, implement `ICanProvideEventStreamId.GetEventStreamId()` instead of `[EventStreamId]`. ⚠️ Combining `ICanProvideEventStreamId` with a non-null `[EventStreamId]` attribute on the same command throws `AmbiguousEventStreamId` at startup — pick one (or set the attribute value to `null` to defer to the interface).
|
|
166
|
+
When the stream id is known only at runtime, implement `ICanProvideEventStreamId.GetEventStreamId()` instead of `[EventStreamId]`. ⚠️ Combining `ICanProvideEventStreamId` with a non-null `[EventStreamId]` attribute on the same command throws `AmbiguousEventStreamId` when that command is executed (the check runs while Arc builds the command context, not at startup) — pick one (or set the attribute value to `null` to defer to the interface).
|
|
167
167
|
|
|
168
168
|
### Event-source id resolution order **[contract]**
|
|
169
169
|
|
|
170
|
-
`ICanProvideEventSourceId.GetEventSourceId()`
|
|
170
|
+
`ICanProvideEventSourceId.GetEventSourceId()` first; otherwise the **first property, in declaration order, that qualifies** — an `EventSourceId`/`EventSourceId<T>`-derived property *or* a `[Key]`-marked one (there is no priority between those two kinds; Arc reports two qualifying properties as `ARCCHR0002` and tells you to implement the interface); otherwise Arc generates a new id. When `ICanProvideEventSourceId` is implemented and returns `null` (or throws), the result is `EventSourceId.Unspecified`, **not** a generated id.
|
|
171
|
+
|
|
172
|
+
⚠️ **Two attributes are spelled `[Key]`.** Chronicle resolves the key from `Cratis.Chronicle.Keys.KeyAttribute`; `System.ComponentModel.DataAnnotations.KeyAttribute` compiles, reads identically, and does **nothing** in an application that uses Chronicle — every command gets a fresh generated event source id and every read model keyed by it resolves to nothing. Arc's analyzer **`ARCCHR0008`** reports the wrong one; if a `using` pulls in the DataAnnotations namespace, check which `[Key]` you have.
|
|
171
173
|
|
|
172
174
|
⚠️ **Injected read models resolve ONLY by the command's resolved event-source id** — not by the read-model type, and not by "the property that looks like its key." If the read model you need is keyed by a **different** value, direct injection silently hands you the **wrong** instance — or, when nothing exists for the command's id, `null` (or for a `[Passive]` projection a **default-valued** instance; see *Existence checks* below) — a correctness bug, not a compile error. The three failure shapes: (1) you need a *referenced other* entity (act on A, check B); (2) the command has **multiple** `EventSourceId<T>`-typed properties → ambiguous resolution (never rely on property order); (3) a create command whose id is generated inside `Handle()`. In all three, read by explicit key: `IReadModels.GetInstanceById<T>((EventSourceId)key)`.
|
|
173
175
|
|
|
@@ -197,6 +199,7 @@ Apply authorization attributes on the `[Command]` record or query method. **[con
|
|
|
197
199
|
- **Never duplicate one fact as an aggregate event *plus* a per-item fanout.** If a command fans out per item (one `RequestSentToPartner` per partner), don't also emit an aggregate event carrying the whole list. Keep the fanout as the single source of truth and project any aggregate from it; pick per-item vs. single-with-list event from how consumers read it — never both.
|
|
198
200
|
- **Command rejection normally appends no event.** Validation and business-rule rejections are command results, not facts. Add a rejection/failed event only when the business must audit or react to that failure later.
|
|
199
201
|
- **One event name = one meaning.** Don't reuse a vague event for several domain facts; if facts can occur, be absent, or have consumers independently, split them into separate event types.
|
|
202
|
+
- **Never persist clock-relative state.** "Overdue", "expired", "due in 3 days" are true *now* and false later, and a projection only moves when an event arrives — so a projected `IsOverdue` is wrong the moment the clock crosses the line with no event to flip it. Store the **deadline** (a `DateOnly`/`DateTimeOffset` the business supplied) and compute the comparison at the query or UI edge, refreshing when time alone changes the answer. Only when *crossing* the deadline is itself a business fact (a fee is charged, a reminder goes out) does a scheduled process append an event for it.
|
|
200
203
|
|
|
201
204
|
```csharp
|
|
202
205
|
/// <summary>Emitted when an author is registered.</summary>
|
|
@@ -250,7 +253,7 @@ Ladder, first that fits: (1) direct read-model (DCB) injection — only when key
|
|
|
250
253
|
|
|
251
254
|
⚠️ **This is invisible when a status enum's `0` is a real state** ("the state every stream starts in"): absent becomes byte-identical to freshly-created, so a command reads never-invited as already-invited. **Renumbering the enum from `1` is not the fix** — the value is then dropped from the payload and deserializes back to CLR `0` on the client anyway. Carry an explicit existence flag instead: `[SetValue<FirstEvent>(true)] bool Exists`, one `[SetValue<T>]` per event that can be the first for the stream. That is exactly the `[SetValue<T>]`-driven `bool` exception the no-defaults convention above allows for. An explicit `NotSet`/`Unknown` zero member also works, but only because the property stays present and readable; the flag is the robust form. ⚠️ **A spec suite will not catch this** — `CommandScenario`'s read-model harness answers `null` for an unseeded event-source id, which matches production for every row above *except* the passive projection. Cover the absent case by seeding a default-valued instance explicitly, not only `null`.
|
|
252
255
|
|
|
253
|
-
**Injected-parameter nullability is the required/optional switch:** a nullable `TReadModel?` parameter (on a validator, `Provide()`, or `Handle()`) gets `null` injected for a never-created/removed instance — check `is null`. A **non-nullable** `TReadModel` whose instance does not exist
|
|
256
|
+
**Injected-parameter nullability is the required/optional switch:** a nullable `TReadModel?` parameter (on a validator, `Provide()`, or `Handle()`) gets `null` injected for a never-created/removed instance — check `is null`. A **non-nullable** `TReadModel` whose instance does not exist is **rejected, not crashed**: Arc raises `ReadModelDoesNotExistForCommand`, which surfaces as a validation failure (HTTP 400) with reason `DependencyUnavailable` (non-nullable = "required, must exist"). The server-fault path (`CannotResolveCommandDependency` / `CannotResolveValidatorDependency`, HTTP 500) is reached only when the dependency is *not* a registered read model or no usable key was resolved — the exact contract is in the **cratis-arc-command** skill's `read-model-injection` reference. ⚠️ Both switches ride on the same resolution, so on a **`[Passive]` projection** neither fires for a never-created instance: the nullable parameter receives a default-valued object and the `is null` guard never runs, and the non-nullable one resolves happily. Check the existence flag there, not nullability. Type by-id accessors you write yourself as `Task<T?>` so callers get the compiler signal Chronicle's own signature withholds.
|
|
254
257
|
|
|
255
258
|
## Projections **[contract]**
|
|
256
259
|
|
|
@@ -286,7 +289,7 @@ public Task AuthorRegistered(AuthorRegistered @event, EventContext context) => .
|
|
|
286
289
|
- **Signatures:** the first parameter is the event and drives dispatch; every parameter after it is resolved as a dependency — `EventContext`, a read model, a service, a `ReactorDelivery` — with no limit on how many. Return `Task`, `void`, or a synchronous side-effect return type. Prefer `Task`/async for real side effects, but sync returns are supported.
|
|
287
290
|
- **[convention] `[OnceOnly]`** on any non-idempotent side effect (emails, payments, external writes) — replay re-fires otherwise. It covers **replay only**; a recovered failed partition still re-delivers, so guard that with a `ReactorDelivery` receipt.
|
|
288
291
|
- **Side-effect events:** return them directly as event objects (single `TEvent`, `IEnumerable<object>`, or `Task<...>` thereof) — Chronicle appends them to the triggering event source. To target a different event source/stream, return `EventForEventSourceId(id, @event)` wrappers (the same cross-stream mechanism as a command — see [reactors.md](./reactors.md)); the wrapper is self-describing, so set the stream type, source type or `Subject` on it when you need full metadata control. Don't inject `IEventLog` for the normal path; reserve it for explicit append-result/wait/options/mixed-target workflows.
|
|
289
|
-
- **Commands from a reactor:**
|
|
292
|
+
- **Commands from a reactor:** **return** the `[Command]` (or a collection of commands) from a `[OnceOnly]` handler — Arc executes it through the full validation/authorization/handler pipeline and a denied or invalid result fails the partition rather than disappearing. A reactor has no principal, so a `[Roles]`-gated command needs `[ExecuteCommandsAsSystem("<role>")]` on the reactor class. The imperative `ICommandPipeline.Execute(command)` remains available (`[OnceOnly]`, inspect the `CommandResult`, supply your own execution context — see the **cratis-arc-command-execution** skill). Don't reach for `IEventLog` to change state in another slice. Details and the analyzer: [reactors.md](./reactors.md).
|
|
290
293
|
- **[convention] Stateless, idempotent, read state via Chronicle read models** (`GetInstanceById`), not `IMongoCollection<T>`. Don't throw to "validate" malformed inbound events — that pauses the partition and can quarantine the observer; record an explicit failure/dead-letter event instead.
|
|
291
294
|
|
|
292
295
|
## Compliance (PII)
|
|
@@ -305,13 +308,15 @@ public Task AuthorRegistered(AuthorRegistered @event, EventContext context) => .
|
|
|
305
308
|
|
|
306
309
|
The compliance **subject** is the encryption-key identity. It is resolved first-match-wins:
|
|
307
310
|
|
|
308
|
-
| Priority | Mechanism |
|
|
309
|
-
| --- | --- |
|
|
310
|
-
| 1 | `Subject` as
|
|
311
|
-
| 2 | `ICanProvideSubject.GetSubject()` on the command |
|
|
312
|
-
| 3 | a `Subject`-typed property on the command |
|
|
313
|
-
| 4 |
|
|
314
|
-
| 5 | `
|
|
311
|
+
| Priority | Mechanism | Resolved by |
|
|
312
|
+
| --- | --- | --- |
|
|
313
|
+
| 1 | `Subject` as a tuple element returned from `Handle()` (computed in the handler) | Arc |
|
|
314
|
+
| 2 | `ICanProvideSubject.GetSubject()` on the command | Arc |
|
|
315
|
+
| 3 | a `Subject`-typed property, or a `[Subject]`-marked property, on the **command** (any other type is converted with `ToString()`) | Arc |
|
|
316
|
+
| 4 | a `[Subject]`-marked property on the **event** being appended | Chronicle client |
|
|
317
|
+
| 5 | otherwise the **event source id** of the append — for a command, whatever its event-source-id resolution produced (`ICanProvideEventSourceId` → `EventSourceId<T>` property → `[Key]`) | Chronicle kernel |
|
|
318
|
+
|
|
319
|
+
So a command whose identity is an `EventSourceId<T>` needs no attribute at all: with nothing marked, that identity is the subject. Mark `[Subject]` only when the person is *not* the stream — and never on the `EventSourceId<T>` itself (`CHR0026`).
|
|
315
320
|
|
|
316
321
|
`ICanProvideSubject` lives in `Cratis.Chronicle.Events` (alongside `ICanProvideEventSourceId`/`ICanProvideEventStreamId`). `[Subject]` is *not* aggregate identity. A managed read-model document has **one** subject — never mix multiple people's PII in one document; split into person-scoped models.
|
|
317
322
|
|
|
@@ -327,7 +332,7 @@ Cross-slice access is **read-only and goes through Chronicle** — inject anothe
|
|
|
327
332
|
|
|
328
333
|
## Dialogs
|
|
329
334
|
|
|
330
|
-
Frontend commands run through Cratis
|
|
335
|
+
Frontend commands run through the Cratis dialogs — never a vendor or hand-rolled modal. Use `CommandDialog` (executes a command) from `@cratis/components/CommandDialog` or `Dialog` (data collection) from `@cratis/components/Dialogs`. See [dialogs.md](./dialogs.md).
|
|
331
336
|
|
|
332
337
|
## See also
|
|
333
338
|
|
|
@@ -16,6 +16,7 @@ For each framework type, attribute, method, prop, hook, or import in an example,
|
|
|
16
16
|
- **C# / backend** — grep real usage in a reference application (e.g. Cratis **Studio**) and the product `Source/` trees of the Cratis repos checked out alongside this one (`Arc/Source`, `Chronicle/Source`). For extension methods, find the `public static … (this <Type> …)` signature and note **which type it extends**.
|
|
17
17
|
- **React / Components** — the authoritative prop names are in the compiled type defs of the installed package (`node_modules/@cratis/components/dist/esm/**/*.d.ts`) or the `Components` source `dist`. Real usage: a reference app's `*.tsx`.
|
|
18
18
|
- **Invented *domain* names are fine** (event/concept/command names like `AuthorRegistered`, `BookId`). Only **framework APIs** must be real. Never invent a framework interface, attribute, prop, method, or import path.
|
|
19
|
+
- **Read the source at the version the reader runs, never at a checkout's `HEAD`.** A sibling clone's working tree is whatever someone last checked out; the package the reader installed is a tag. Read with `git show <tag>:<path>` and `git grep <pattern> <tag> -- <path>` — both work without touching the checkout, so a dirty or shared worktree is never a reason to skip the check. Name the repository and the tag in the claim ("`Arc v22.16.0`, `ParameterDependencyResolver.cs:44-62`") so the next reader can re-run it. Where the corpus states a version in a skill's *Verified product sources* table, that tag is the one to read; when a consumer's pin is newer, re-read at theirs. A release note is a reason to look, not evidence that a behavior changed or that a workaround can be retired — retire a workaround only after the original reproduction passes at the new tag.
|
|
19
20
|
|
|
20
21
|
## Complete and correct
|
|
21
22
|
|
|
@@ -57,7 +57,7 @@ Behavior that can regress without TypeScript catching it:
|
|
|
57
57
|
- **Component rendering** only when the markup, a disabled state, or wrapper
|
|
58
58
|
behavior is genuinely the point.
|
|
59
59
|
|
|
60
|
-
Do **not** specify generated Cratis proxies,
|
|
60
|
+
Do **not** specify generated Cratis proxies, Cratis Components or framework internals,
|
|
61
61
|
CSS pixel-perfection, snapshots, or trivial presentational pass-throughs.
|
|
62
62
|
|
|
63
63
|
## View-model specification — the default shape
|
|
@@ -109,9 +109,9 @@ import React from 'react';
|
|
|
109
109
|
import { renderToStaticMarkup } from 'react-dom/server';
|
|
110
110
|
import { describe, beforeEach, it, vi } from 'vitest';
|
|
111
111
|
|
|
112
|
-
vi.mock('
|
|
113
|
-
Dialog: (props: {
|
|
114
|
-
React.createElement('div', null, props.
|
|
112
|
+
vi.mock('@cratis/components/Dialogs', () => ({
|
|
113
|
+
Dialog: (props: { buttons?: React.ReactNode; children?: React.ReactNode }) =>
|
|
114
|
+
React.createElement('div', null, props.children, props.buttons),
|
|
115
115
|
}));
|
|
116
116
|
|
|
117
117
|
describe('when rendered while busy', () => {
|
|
@@ -19,11 +19,13 @@ This skill decides **which scenario** and **what to cover**. The C# mechanics
|
|
|
19
19
|
|
|
20
20
|
| Package | Version | Purpose |
|
|
21
21
|
| --- | --- | --- |
|
|
22
|
-
| `Cratis.Arc.Testing` | `22.
|
|
23
|
-
| `Cratis.Arc.Chronicle.Testing` | `22.
|
|
24
|
-
| `Cratis.Chronicle.Testing` | `
|
|
22
|
+
| `Cratis.Arc.Testing` | `22.16.0` | `CommandScenario<TCommand>` and the `CommandResult` assertions |
|
|
23
|
+
| `Cratis.Arc.Chronicle.Testing` | `22.16.0` | `Given`, `EventLog`, `AppendedEvents` and the appended-event assertions on a command scenario |
|
|
24
|
+
| `Cratis.Chronicle.Testing` | `18.3.0` | `EventScenario`, `ReadModelScenario<T>`, `ReactorScenario<T>` |
|
|
25
25
|
| `Cratis.Specifications.XUnit` | `4.x` | The `Specification` base and the `ShouldXxx` assertions |
|
|
26
26
|
|
|
27
|
+
> Re-verified at the versions above by **symbol and signature**: every type, attribute and member this skill names exists at that tag, and the public surface it describes is unchanged since the previous verification (Chronicle 16.45.x / Arc 22.10.4 — the Chronicle 16→18 client diff is converters, options and doc comments; no type was removed or renamed). Behavior claims were verified at the earlier tag unless a section says otherwise.
|
|
28
|
+
|
|
27
29
|
`Cratis.Testing` is the meta-package that brings `Cratis.Arc.Testing` and
|
|
28
30
|
`Cratis.Arc.Chronicle.Testing` together. Reverify against the owning product
|
|
29
31
|
repository before claiming support for another version.
|
|
@@ -109,9 +111,13 @@ public class and_name_is_unique : Specification
|
|
|
109
111
|
#endif
|
|
110
112
|
```
|
|
111
113
|
|
|
112
|
-
The scenario's own surface is `Services`, `Context`, `Execute(command)
|
|
113
|
-
`Validate(command)`
|
|
114
|
-
|
|
114
|
+
The scenario's own surface is `Services`, `Context`, `Execute(command[, ct])`,
|
|
115
|
+
`Validate(command)` (runs the filters without the handler), `LastResult`,
|
|
116
|
+
`Operations` and the three operation assertions
|
|
117
|
+
(`ShouldHaveExecutedOperation<T>()`, `ShouldHaveCompensatedOperation<T>()`,
|
|
118
|
+
`ShouldHaveNoOperationInvocations()`); it is `IDisposable`/`IAsyncDisposable`.
|
|
119
|
+
Seeding (`Given`), the event log and the event assertions arrive as extension
|
|
120
|
+
members from the Chronicle integration.
|
|
115
121
|
|
|
116
122
|
## Step 5 — Seed prior state through `Given`
|
|
117
123
|
|
|
@@ -186,8 +192,10 @@ keyed by **command and event type**. They return `Task`, so the fact is
|
|
|
186
192
|
|
|
187
193
|
⚠️ On a command scenario the second argument is a **predicate**
|
|
188
194
|
(`Func<TEvent, bool>`), not an assertion callback. The `Action<TEvent>` validator
|
|
189
|
-
|
|
190
|
-
`Cratis.Chronicle.Testing`,
|
|
195
|
+
shape lives on other receivers — the `IEventSequence` extensions in
|
|
196
|
+
`Cratis.Chronicle.Testing`, `AppendedEventWithResult.ShouldHaveEvent<TEvent>(Action<TEvent>?)`,
|
|
197
|
+
and the `IChronicleSetupFixture` extension in `Cratis.Chronicle.XUnit.Integration`
|
|
198
|
+
— never on `CommandScenario`.
|
|
191
199
|
|
|
192
200
|
Sequence numbers are **zero-based**: the first event is `0`, and the tail after a
|
|
193
201
|
single append is `0`, never `1`.
|
|
@@ -15,10 +15,12 @@ told about them. Each has its own extension point.
|
|
|
15
15
|
|
|
16
16
|
| Package | Version | Purpose |
|
|
17
17
|
| --- | --- | --- |
|
|
18
|
-
| `Cratis.Arc.Core` | `22.
|
|
19
|
-
| `Cratis.Arc` | `22.
|
|
20
|
-
| `@cratis/arc` | `22.
|
|
21
|
-
| `@cratis/arc.react` | `22.
|
|
18
|
+
| `Cratis.Arc.Core` | `22.16.0` | `Cratis.Arc.Identity`, `Cratis.Arc.Authorization`, `Cratis.Arc.Authentication`, `Cratis.Arc.Tenancy` |
|
|
19
|
+
| `Cratis.Arc` | `22.16.0` | ASP.NET Core hosting, `AddMicrosoftIdentityPlatformIdentityAuthentication` |
|
|
20
|
+
| `@cratis/arc` | `22.16.0` | `IdentityProvider`, `IIdentity` |
|
|
21
|
+
| `@cratis/arc.react` | `22.16.0` | `IdentityProvider` component, `useIdentity`, `RequireRole` |
|
|
22
|
+
|
|
23
|
+
> Re-verified at the versions above by **symbol and signature**: every type, attribute and member this skill names exists at that tag, and the public surface it describes is unchanged since the previous verification (Chronicle 16.45.x / Arc 22.10.4 — the Chronicle 16→18 client diff is converters, options and doc comments; no type was removed or renamed). Behavior claims were verified at the earlier tag unless a section says otherwise.
|
|
22
24
|
|
|
23
25
|
Reverify before claiming support for another version.
|
|
24
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Authorization
|
|
2
2
|
|
|
3
|
-
Verified against `Cratis.Arc.Core` and `Cratis.Arc` `22.
|
|
3
|
+
Verified against `Cratis.Arc.Core` and `Cratis.Arc` `22.16.0`. Everything below
|
|
4
4
|
is in `Cratis.Arc.Authorization` unless stated.
|
|
5
5
|
|
|
6
6
|
## Default access differs by hosting model
|