@cratis/pi 0.0.1 → 2.0.2
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/LICENSE +21 -0
- package/README.md +18 -37
- package/package/corpus/agents/backend-developer.md +125 -0
- package/package/corpus/agents/code-reviewer.md +165 -0
- package/package/corpus/agents/coordinator.md +163 -0
- package/package/corpus/agents/frontend-developer.md +246 -0
- package/package/corpus/agents/orchestrator.md +196 -0
- package/package/corpus/agents/performance-reviewer.md +109 -0
- package/package/corpus/agents/planner.md +145 -0
- package/package/corpus/agents/repository-investigation-reviewer.md +45 -0
- package/package/corpus/agents/repository-investigator.md +50 -0
- package/package/corpus/agents/security-reviewer.md +118 -0
- package/package/corpus/agents/slice-implementer.md +59 -0
- package/package/corpus/agents/spec-writer.md +149 -0
- package/package/corpus/harnesses/pi/extensions/cratis-hooks/index.ts +213 -0
- package/package/corpus/harnesses/pi/extensions/cratis-rules/index.ts +27 -0
- package/package/corpus/harnesses/pi/extensions/package.json +4 -0
- package/package/corpus/harnesses/pi/extensions/subagent/agents.ts +167 -0
- package/package/corpus/harnesses/pi/extensions/subagent/index.ts +352 -0
- package/package/corpus/hooks/README.md +434 -0
- package/package/corpus/hooks/agent-stop.md +49 -0
- package/package/corpus/hooks/pre-commit.md +47 -0
- package/package/corpus/hooks/scripts/cratis-guard-writes.sh +87 -0
- package/package/corpus/hooks/scripts/cratis-nuget-pins.txt +11 -0
- package/package/corpus/hooks/scripts/cratis-pattern-scan.sh +197 -0
- package/package/corpus/hooks/scripts/cratis-patterns.json +84 -0
- package/package/corpus/hooks/scripts/cratis-quality-gate.sh +219 -0
- package/package/corpus/hooks/scripts/hook-lib.sh +152 -0
- package/package/corpus/hooks/scripts/quality-gates.json +261 -0
- package/package/corpus/hooks/scripts/type-references-allowlist.txt +71 -0
- package/package/corpus/hooks/scripts/validate-package-imports.sh +166 -0
- package/package/corpus/hooks/scripts/validate-package-subpaths.sh +120 -0
- package/package/corpus/hooks/scripts/validate-type-references.sh +308 -0
- package/package/corpus/hooks/settings.template.json +40 -0
- package/package/corpus/prompts/add-business-rule.prompt.md +22 -0
- package/package/corpus/prompts/add-concept.prompt.md +17 -0
- package/package/corpus/prompts/add-ef-migration.prompt.md +24 -0
- package/package/corpus/prompts/add-projection.prompt.md +20 -0
- package/package/corpus/prompts/add-reactor.prompt.md +22 -0
- package/package/corpus/prompts/add-reducer.prompt.md +20 -0
- package/package/corpus/prompts/audit-hooks.prompt.md +15 -0
- package/package/corpus/prompts/check-doc-drift.prompt.md +21 -0
- package/package/corpus/prompts/code-review.prompt.md +9 -0
- package/package/corpus/prompts/new-feature.prompt.md +9 -0
- package/package/corpus/prompts/new-vertical-slice.prompt.md +18 -0
- package/package/corpus/prompts/review-pr.prompt.md +35 -0
- package/package/corpus/prompts/review-skill.prompt.md +16 -0
- package/package/corpus/prompts/scaffold-feature.prompt.md +16 -0
- package/package/corpus/prompts/ship-changes.prompt.md +20 -0
- package/package/corpus/prompts/verify-ai-setup.prompt.md +19 -0
- package/package/corpus/prompts/write-documentation.prompt.md +21 -0
- package/package/corpus/prompts/write-specs.prompt.md +22 -0
- package/package/corpus/rules/capability-is-not-authority.md +31 -0
- package/package/corpus/rules/code-quality.csharp.md +91 -0
- package/package/corpus/rules/code-quality.md +82 -0
- package/package/corpus/rules/code-quality.typescript.md +89 -0
- package/package/corpus/rules/components.md +207 -0
- package/package/corpus/rules/concepts.md +115 -0
- package/package/corpus/rules/csharp.md +269 -0
- package/package/corpus/rules/dialogs.md +264 -0
- package/package/corpus/rules/documentation-structure-and-formatting.md +148 -0
- package/package/corpus/rules/documentation.md +90 -0
- package/package/corpus/rules/editing-cratis-docs.md +69 -0
- package/package/corpus/rules/efcore.md +235 -0
- package/package/corpus/rules/efcore.specs.md +44 -0
- package/package/corpus/rules/exit-codes-and-wrappers.md +33 -0
- package/package/corpus/rules/framework.md +52 -0
- package/package/corpus/rules/frontend-quality.md +59 -0
- package/package/corpus/rules/frontend-testing.md +126 -0
- package/package/corpus/rules/general.md +305 -0
- package/package/corpus/rules/git-commits.md +138 -0
- package/package/corpus/rules/github-actions.md +92 -0
- package/package/corpus/rules/glossary.md +61 -0
- package/package/corpus/rules/guards-and-fuses.md +45 -0
- package/package/corpus/rules/local-work-artifacts.md +33 -0
- package/package/corpus/rules/managing-ai-rules.md +40 -0
- package/package/corpus/rules/orleans.md +50 -0
- package/package/corpus/rules/pull-requests.md +78 -0
- package/package/corpus/rules/react.md +195 -0
- package/package/corpus/rules/reactors.md +238 -0
- package/package/corpus/rules/rtk.md +37 -0
- package/package/corpus/rules/specs.csharp.md +139 -0
- package/package/corpus/rules/specs.md +132 -0
- package/package/corpus/rules/specs.scenarios.csharp.md +172 -0
- package/package/corpus/rules/specs.typescript.md +139 -0
- package/package/corpus/rules/storybook.md +84 -0
- package/package/corpus/rules/terminal-commands.md +19 -0
- package/package/corpus/rules/typescript.md +149 -0
- package/package/corpus/rules/verification-discipline.md +21 -0
- package/package/corpus/rules/vertical-slices.md +338 -0
- package/package/corpus/rules/web-fetching.md +11 -0
- package/package/corpus/rules/writing-correct-examples.md +35 -0
- package/package/corpus/rules/writing-cratis-docs.md +70 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/SKILL.md +131 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/code-style.md +187 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/domain-philosophy.md +91 -0
- package/package/corpus/skills/cratis-engineering-csharp-conventions/references/exceptions-logging-and-di.md +223 -0
- package/package/corpus/skills/cratis-engineering-decision-record/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-decision-record/SKILL.md +133 -0
- package/package/corpus/skills/cratis-engineering-decision-record/references/record-format.md +107 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/SKILL.md +86 -0
- package/package/corpus/skills/cratis-engineering-docs-authoring/references/site-format.md +46 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/LICENSE +2 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/SKILL.md +130 -0
- package/package/corpus/skills/cratis-engineering-effect-boundaries/references/failure-archetypes.md +133 -0
- package/package/corpus/skills/cratis-fundamentals-concept/verification.json +8 -0
- package/package/profile-catalog.json +821 -0
- package/package.json +35 -13
- package/src/index.ts +80 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-react-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-application-slice-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authentication.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/authorization.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/frontend.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/local-development.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/command-result.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/handler-shapes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/proxy-generation.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command/references/read-model-injection.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-execution/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-command-validation/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-ef-core-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-observable-query-http/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-query-paging/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-page.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/data-tables.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/dialogs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/mvvm.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-arc-react-page/references/queries-and-commands.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-cli-operations/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-dotnet/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-elixir/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-kotlin/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-client-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-compliance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-constraints/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-modeling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-event-type-migration/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-mcp-inspection/references/observational-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-multi-tenancy/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/fluent-builder.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-projection/references/model-bound-attributes.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reactor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model/references/queries.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-read-model-specifications/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-reducer/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-chronicle-web-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-cli-terminal-workbench/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-code-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-accessibility/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-schema-editor/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-stepper-command-dialog/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-styling/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-components-toolbar/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-documentation-writing/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-event-model-diagram/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-concept/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-fundamentals-type-discovery/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-governed-release-methodology/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-lens-browser-extension/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-performance-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-screenplay-model-authoring/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-security-review/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specification-by-example/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/application-scenarios.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/csharp-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-csharp/references/integration-specs.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-specifications-typescript/references/typescript-patterns.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-stage-rendering-and-sandbox/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/LICENSE +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/SKILL.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +0 -0
- /package/{skills → package/corpus/skills}/cratis-studio-mcp-safety-guidance/references/observational-tools.md +0 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.tsx, **/Components/**/*.ts"
|
|
3
|
+
profile: application
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*.tsx"
|
|
6
|
+
- "**/Components/**/*.ts"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# React + Arc + Cratis Components
|
|
10
|
+
|
|
11
|
+
The frontend is React + TypeScript in MVVM style, composed from **Cratis Components** on top of **Arc-generated proxies**. Cratis Components is PrimeReact-based; you reach PrimeReact almost exclusively through the Cratis wrappers. 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
|
+
|
|
13
|
+
## The proxy boundary
|
|
14
|
+
|
|
15
|
+
`dotnet build` (Debug) generates a typed TypeScript proxy per command and query, emitted next to the slice's `.cs`. React imports those proxies and calls their static hooks. **Backend before frontend, always** — the proxies don't exist until the backend compiles. **Never edit a generated proxy** (`// @generated by Cratis` header) — fix the C# source and rebuild.
|
|
16
|
+
|
|
17
|
+
## MVVM
|
|
18
|
+
|
|
19
|
+
Move behavior out of the render function. Two accepted idioms:
|
|
20
|
+
|
|
21
|
+
- **`withViewModel`** from `@cratis/arc.react.mvvm` — for command/query orchestration, long-lived UI state, and workflow components that benefit from observable class state.
|
|
22
|
+
- **A tested pure state module/helper** — for deterministic page/form state that doesn't need MobX. The component stays rendering + event wiring; the module owns derived state, filtering/sorting, wizard steps, command-value derivation, and has BDD specs (see [frontend-testing.md](./frontend-testing.md)).
|
|
23
|
+
|
|
24
|
+
Don't leave complex behavior inline. Trivial presentational leaf components need no view model.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { injectable } from 'tsyringe';
|
|
28
|
+
|
|
29
|
+
@injectable()
|
|
30
|
+
export class <Entity>ListingViewModel {
|
|
31
|
+
selectedId: string | null = null;
|
|
32
|
+
select(id: string) { this.selectedId = id; }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { withViewModel } from '@cratis/arc.react.mvvm';
|
|
38
|
+
|
|
39
|
+
export const Listing = withViewModel(<Entity>ListingViewModel, ({ viewModel }) => (
|
|
40
|
+
/* read viewModel state inside JSX; call viewModel methods */
|
|
41
|
+
));
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
**View-model rules:**
|
|
45
|
+
- Pure TypeScript classes — no JSX, never store React state, never call React hooks (`useIdentity`, `useNavigate`) inside one — inject the Cratis abstraction instead.
|
|
46
|
+
- Fields are auto-observable (`makeAutoObservable` is applied by `withViewModel`); no `@observable` on plain assignments.
|
|
47
|
+
- Inject services via the primary constructor (`@injectable` / tsyringe).
|
|
48
|
+
- A non-trivial view model must be constructible directly in a spec and covered with BDD specs.
|
|
49
|
+
|
|
50
|
+
### When to extract a view model
|
|
51
|
+
|
|
52
|
+
Extract as soon as a component has **any** of: 3+ `useState`, any `useCallback` beyond a trivial inline handler, any `useEffect` that synchronizes state, derived values computed from other state (use a getter), or state shared via prop drilling.
|
|
53
|
+
|
|
54
|
+
### MobX reactivity
|
|
55
|
+
|
|
56
|
+
- **Dereference late** — read `viewModel.property` *inside* JSX; never destructure observables at the top of the component body (the captured value won't track changes).
|
|
57
|
+
- **Computed getters** for derived state (auto-`computed`) — not `useMemo` in the component.
|
|
58
|
+
- **`useCallback`/`useMemo` are unnecessary in `withViewModel` components** — the render is wrapped in `Observer`. Their presence signals state that belongs in the view model. (This does *not* apply to plain functional components using proxy hooks directly. A plain component reaching for `useCallback` to stabilize a handler is a signal that handler belongs in a view model or tested state module.)
|
|
59
|
+
- **Observable arrays into raw children** — passing `viewModel.items` (an observable array) to a *raw non-observer* child needs `viewModel.items.slice()` to materialize a plain array. Uncommon in practice — prefer `DataTableForObservableQuery`, which handles reactivity internally.
|
|
60
|
+
|
|
61
|
+
### Props / route / query params in view models
|
|
62
|
+
|
|
63
|
+
| Need | Mechanism |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Props that change after mount | implement `IHandleProps<TProps>` → `handleProps(props)` (called on initial mount **and** every props change) |
|
|
66
|
+
| Props set once at mount | `@props readonly componentProps: TProps` |
|
|
67
|
+
| Route params | `@params readonly routeParams: RouteParams` — put `@field` on each property |
|
|
68
|
+
| Query-string params | `@queryParams` |
|
|
69
|
+
|
|
70
|
+
`@field` on each route/query-param property is what makes Arc's JSON serializer convert the URL **strings** to the property's real type — **without `@field`, the values stay strings**.
|
|
71
|
+
|
|
72
|
+
### Injectable abstractions (never touch browser/React globals in a view model)
|
|
73
|
+
|
|
74
|
+
| Interface | Package | Replaces |
|
|
75
|
+
|---|---|---|
|
|
76
|
+
| `IMessenger` | `@cratis/arc.react.mvvm/messaging` | cross-component selection (list↔detail) — not React Context |
|
|
77
|
+
| `IDialogs` | `@cratis/arc.react.mvvm/dialogs` | imperative confirmation/busy dialogs from a view model |
|
|
78
|
+
| `IIdentityProvider` | `@cratis/arc/identity` | identity in a view model (not the `useIdentity` hook) |
|
|
79
|
+
| `ILocalStorage`, `INavigation` | `@cratis/arc.react.mvvm/browser` | `localStorage`, URL navigation |
|
|
80
|
+
| `IViewModelDetached` | `@cratis/arc.react.mvvm` | teardown (`detached()`) for subscriptions/timers |
|
|
81
|
+
|
|
82
|
+
These are registered automatically by `withViewModel` and injected via the primary constructor. Concrete usage:
|
|
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()` trickles **down** into nested scopes; bubbling **up** is opt-in with `{ bubble: true }`. Use the default `IMessenger` unless you genuinely need scoping.
|
|
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
|
+
- **`IIdentityProvider`** — `await _identityProvider.getCurrent<TDetails>()`. Use the `useIdentity()` hook only in functional components without a view model.
|
|
87
|
+
- **`ILocalStorage`** — `setItem(key, value)` / `getItem(key)`; **`IViewModelDetached.detached()`** runs on unmount (`detached() { this._subscription?.unsubscribe(); }`).
|
|
88
|
+
|
|
89
|
+
**Decorator prerequisite:** `experimentalDecorators` + `emitDecoratorMetadata` + `reflect-metadata` must be wired (tsconfig + the build) for `@injectable`/`@props`/`@params`/`@field` to work — don't change them.
|
|
90
|
+
|
|
91
|
+
## Queries
|
|
92
|
+
|
|
93
|
+
A typed proxy class is generated per query. Pick the hook:
|
|
94
|
+
|
|
95
|
+
| Hook | When |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `<Query>.use(args?, sorting?)` | standard — returns `[result]`, re-renders on change |
|
|
98
|
+
| `<Query>.useSuspense(...)` | suspense-aware — throws while loading; wrap in `<QueryBoundary>` |
|
|
99
|
+
| `<Query>.when(condition).use(args)` | conditional — only fires when `condition` is true (don't wrap a hook in `if`) |
|
|
100
|
+
| `<Query>.useWithPaging(pageSize, args?, sorting?)` | server-side paging (backend returns `IQueryable<T>`) |
|
|
101
|
+
| `<Query>.useSuspenseWithPaging(pageSize, ...)` | suspense + paging |
|
|
102
|
+
| `<ObservableQuery>.use(...)` | live observable (`ISubject<T>` backend) pushed over SSE/WebSocket |
|
|
103
|
+
| `<ObservableQuery>.useChangeStream(args?, getKey?, ...)` | item-level add/replace/remove deltas |
|
|
104
|
+
|
|
105
|
+
**Return-tuple shapes** (load-bearing — the observable variants have no `perform`):
|
|
106
|
+
|
|
107
|
+
| Hook | Returns |
|
|
108
|
+
|---|---|
|
|
109
|
+
| `<Query>.use(args?, sorting?)` | `[result, perform]` |
|
|
110
|
+
| `<Query>.useSuspense(...)` | `[result, perform, setSorting]` |
|
|
111
|
+
| `<Query>.useWithPaging(pageSize, args?, sorting?)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
112
|
+
| `<Query>.useSuspenseWithPaging(pageSize, ...)` | `[result, perform, setSorting, setPage, setPageSize]` |
|
|
113
|
+
| `<ObservableQuery>.use(...)` | `[result, setSorting]` — **no `perform`** |
|
|
114
|
+
| `<ObservableQuery>.useChangeStream(args?, getKey?, sorting?, isEnabled?)` | `ChangeSet<T> { added, replaced, removed }` |
|
|
115
|
+
|
|
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
|
+
|
|
118
|
+
- **`useChangeStream` `getKey`:** without `getKey`, removed items can't be identified — everything shows up as `added`. `getKey` enables `replaced` detection. Change streams exist only on observable queries returning `IEnumerable<T>`.
|
|
119
|
+
- **Conditional:** `<Query>.when(!!id).use({ id: id ?? '' })` — when `condition` is false no request/subscription is made and the hook returns `QueryResultWithState.empty()` (`hasData: false`, empty `data`); guard with `result.hasData`. `.when()` works with paging, suspense, and observable variants too.
|
|
120
|
+
- **Suspense:** `useSuspense()` throws a promise while loading and a typed error on failure — it **must** render inside a `<QueryBoundary>` (combines `<Suspense>` + `QueryErrorBoundary`) or React surfaces the thrown promise as an unhandled rejection. `useSuspense()` **re-suspends** on `refresh()`/`setSorting()`/`setPage()` (the cache entry is cleared). `<QueryBoundary onError={({ isQueryFailed, isQueryUnauthorized, error, reset }) => ...}>` exposes the failure; `(error as QueryFailed).exceptionMessages` carries diagnostics. In test teardown clear the caches with `clearSuspenseQueryCache()` / `clearSuspenseObservableQueryCache()` (`@cratis/arc.react/queries`).
|
|
121
|
+
- **`QueryScope`** + `useQueryScope()` (`@cratis/arc.react/queries`): `<QueryScope setIsPerforming={...}>` aggregates one `isPerforming` flag across the queries inside it — they register automatically (no manual add); scopes nest (inner registers with the nearest outer).
|
|
122
|
+
|
|
123
|
+
## Commands
|
|
124
|
+
|
|
125
|
+
A typed proxy class is generated per command. For programmatic execution:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
const [command, setCommandValues, clearCommandValues] = Command.use(); // 3-tuple
|
|
129
|
+
command.someField = value; // or setCommandValues({ ... })
|
|
130
|
+
const result = await command.execute();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Inside dialogs, use `CommandDialog` + CommandForm fields rather than driving the command by hand — see [dialogs.md](./dialogs.md).
|
|
134
|
+
|
|
135
|
+
### Read `CommandResult` by the granular flag — not just `isSuccess`
|
|
136
|
+
|
|
137
|
+
| Flag | Meaning | UX response |
|
|
138
|
+
|---|---|---|
|
|
139
|
+
| `isSuccess` | `isAuthorized && isValid && !hasExceptions` | happy path |
|
|
140
|
+
| `isAuthorized` | `[Roles]`/policy rejected | redirect to login / "not allowed" |
|
|
141
|
+
| `isValid` | validation failed — `validationResults` has per-field messages | render inline field errors |
|
|
142
|
+
| `hasExceptions` | `Provide()`/`Handle()` threw — `exceptionMessages` carry diagnostics | generic error toast + log; never show stack traces |
|
|
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 `{ property, message }` — render per-field UI from it, but **never branch on raw `.message` text**. `exceptionMessages`/`exceptionStackTrace` are for logging, never for users.
|
|
145
|
+
|
|
146
|
+
Out-of-dialog execution branches in this order:
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
const result = await command.execute();
|
|
150
|
+
if (!result.isAuthorized) { redirectToLogin(); return; }
|
|
151
|
+
if (!result.isValid) { /* validationResults → inline field errors */ return; }
|
|
152
|
+
if (result.hasExceptions) { toast.error('Something went wrong'); console.error(result.exceptionMessages); return; }
|
|
153
|
+
// happy path — refresh queries, close, etc.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
`toastCommandResult(result, opts)` from `@cratis/components/Notifications` collapses this whole branch into one call (with a `<Toaster />` mounted) — success/not-authorized/validation/exception → the right toast, no stack traces shown.
|
|
157
|
+
|
|
158
|
+
### Command helpers
|
|
159
|
+
|
|
160
|
+
- **`useCommandInstance(Command)`** — read the live reactive command instance for dependent fields (read it; never mutate — mutations go through field bindings).
|
|
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
|
+
- **`initialValues`** = synchronous baseline (also drives change tracking); **`currentValues`** = reactive overlay for async/late-loading values (e.g. from a query).
|
|
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; members tracked automatically (no `addCommand`); 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
|
+
|
|
166
|
+
### `<Arc>` global configuration
|
|
167
|
+
|
|
168
|
+
Top-level provider for microservice name and API base path. Config props (with defaults):
|
|
169
|
+
|
|
170
|
+
| Prop | Default | Notes |
|
|
171
|
+
|---|---|---|
|
|
172
|
+
| `httpHeadersCallback` | — | returns `HeadersInit` merged into every request (bearer/tenant headers) |
|
|
173
|
+
| `queryTransportMethod` | `ServerSentEvents` | SSE is capped at 4 connections on HTTP/1.1 |
|
|
174
|
+
| `observableQueryTransferMode` | `Delta` | `Delta` = server ChangeSet/client diff; `Full` = all items as `added` |
|
|
175
|
+
| `queryConnectionCount` | `1` | hub connection slots |
|
|
176
|
+
| `queryDirectMode` | `false` | direct per-query connection — dev use only |
|
|
177
|
+
|
|
178
|
+
`QueryTransportMethod`/`ObservableQueryTransferMode` import 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
|
+
|
|
180
|
+
## Composition
|
|
181
|
+
|
|
182
|
+
- Single responsibility — if you write a `// Section` comment inside a component, that section is its own component. Parent owns state; children receive props.
|
|
183
|
+
- Single-file component → in the slice folder; multi-file → a folder named after the component with an `index.ts` re-export.
|
|
184
|
+
- A `.tsx` belongs in the slice folder it implements — never one level up. A feature's pass-through layout renders only `<Outlet />`.
|
|
185
|
+
|
|
186
|
+
## Styling default
|
|
187
|
+
|
|
188
|
+
Default to **Cratis Components on PrimeReact theming/tokens** (`var(--surface-*)`, `var(--primary-color)`, `var(--text-color)`, `pt`/unstyled where needed) — see [components.md](./components.md). Tailwind is **not** the Cratis default (it's one supported unstyled path). Never hard-code hex/`rgb()` for chrome.
|
|
189
|
+
|
|
190
|
+
## See also
|
|
191
|
+
|
|
192
|
+
- [components.md](./components.md) — component structure, CSS, PrimeReact tokens, icons, Storybook.
|
|
193
|
+
- [dialogs.md](./dialogs.md) — `CommandDialog` / `Dialog` / `StepperCommandDialog` rules.
|
|
194
|
+
- [frontend-quality.md](./frontend-quality.md) — engineering bar; [frontend-testing.md](./frontend-testing.md) — BDD specs.
|
|
195
|
+
- [typescript.md](./typescript.md) — TS style.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.cs"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.cs"
|
|
5
|
+
profile: application
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Reactor Instructions
|
|
9
|
+
|
|
10
|
+
Reactors are the "if this then that" of event sourcing — they observe events and produce side effects. Unlike projections (which build state), reactors *do things*: send emails, trigger commands in other slices, call external APIs.
|
|
11
|
+
|
|
12
|
+
## IReactor — Marker Interface
|
|
13
|
+
|
|
14
|
+
`IReactor` is a **marker interface** with no methods to implement. Method dispatch is entirely by convention: the first parameter type of each public method determines which event it handles.
|
|
15
|
+
|
|
16
|
+
```csharp
|
|
17
|
+
public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor
|
|
18
|
+
{
|
|
19
|
+
/// <summary>
|
|
20
|
+
/// Reacts to <see cref="ProjectRegistered"/> events by sending a notification.
|
|
21
|
+
/// </summary>
|
|
22
|
+
/// <param name="event">The event.</param>
|
|
23
|
+
/// <param name="context">The event context.</param>
|
|
24
|
+
public async Task ProjectRegistered(ProjectRegistered @event, EventContext context) =>
|
|
25
|
+
await notifications.Notify($"Project '{@event.Name}' was registered.");
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Method Signature
|
|
30
|
+
|
|
31
|
+
```csharp
|
|
32
|
+
public Task MethodName(TEvent @event, EventContext context)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- **First parameter** — the event type. This determines which events the method subscribes to.
|
|
36
|
+
- **Second parameter onward** — dependencies, resolved when the method is invoked: `EventContext`, a read model, or a service. All are optional and there is no limit on how many; see [Taking Dependencies](#taking-dependencies). The analyzer constrains their *kind*, not their count — a primitive, value type or `string` after the first parameter is flagged because it cannot be resolved.
|
|
37
|
+
- **Return type** — `Task` or `void`, or a side-effect type (`TEvent`, `EventForEventSourceId`, or an `IEnumerable<>` of either — or of `object` to mix them) returned directly (sync) or wrapped in `Task<...>` (async). Prefer `Task`/async for real side effects, but synchronous returns are fully supported — there is no "always async" requirement.
|
|
38
|
+
- **Method name** — can be anything descriptive. The name is for readability, not dispatch.
|
|
39
|
+
|
|
40
|
+
## Handling replay differently — `[Replay]`
|
|
41
|
+
|
|
42
|
+
A reactor sees the same event twice for different reasons: as it happens, and again when its observer is replayed. When those call for different work, mark a second handler for the same event type with **`[Replay]`** and it takes over for the duration of the replay.
|
|
43
|
+
|
|
44
|
+
```csharp
|
|
45
|
+
public class OrderNotifications(INotificationService notifications) : IReactor
|
|
46
|
+
{
|
|
47
|
+
public Task OrderPlaced(OrderPlaced @event) => notifications.Notify($"Order {@event.Number} placed.");
|
|
48
|
+
|
|
49
|
+
[Replay]
|
|
50
|
+
public Task OrderPlacedDuringReplay(OrderPlaced @event) => Task.CompletedTask;
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- With a `[Replay]` handler, **only** it runs during replay — the regular handler does not also run.
|
|
55
|
+
- Without one, the regular handler runs during replay exactly as before, so this changes nothing for existing reactors.
|
|
56
|
+
- An event type handled *only* by a `[Replay]` handler is still subscribed to.
|
|
57
|
+
- Reach for **`[OnceOnly]`** instead when the side effect should simply not happen again; use `[Replay]` when replay needs to do something *different* rather than nothing.
|
|
58
|
+
|
|
59
|
+
## Taking Dependencies
|
|
60
|
+
|
|
61
|
+
Beyond the event and `EventContext`, a handler method can take any number of additional parameters as dependencies. Only the first parameter is fixed (it is the event that drives dispatch); every parameter after it is resolved when the method is invoked.
|
|
62
|
+
|
|
63
|
+
```csharp
|
|
64
|
+
public class OrderProcessing(IShippingService shipping) : IReactor
|
|
65
|
+
{
|
|
66
|
+
public async Task OrderPlaced(OrderPlaced @event, EventContext context, Order order, IPricingService pricing)
|
|
67
|
+
{
|
|
68
|
+
// 'order' is the read model, resolved by Chronicle for this event's key.
|
|
69
|
+
// 'pricing' is resolved from the service provider.
|
|
70
|
+
await shipping.Schedule(order, pricing.PriceFor(order));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
How each parameter is resolved:
|
|
76
|
+
|
|
77
|
+
- **`EventContext`** — receives the event context (position independent).
|
|
78
|
+
- **A read model** — a type that has a reducer or a projection (declarative or model-bound). Chronicle resolves it directly — read models never go through the service provider. A **materialized** read model (the default, for projections and reducers alike) is read from its sink, so it is **eventually consistent** — as current as its observer. Mark it **`[Passive]`** when the reactor needs it **strongly consistent**: a passive read model has no sink, so Chronicle computes it on demand from the events at the point the reactor runs.
|
|
79
|
+
- **Any other type** — resolved from the service provider (constructor injection on the reactor itself is still preferred for collaborators used by every method).
|
|
80
|
+
|
|
81
|
+
### Resolving the read model key
|
|
82
|
+
|
|
83
|
+
By default the read model is materialized using the `EventSourceId` from the event context. When the key differs from the event source — for example the event carries the id of a related entity — implement `ICanResolveReadModelKey` on the reactor:
|
|
84
|
+
|
|
85
|
+
```csharp
|
|
86
|
+
public class OrderProcessing : IReactor, ICanResolveReadModelKey
|
|
87
|
+
{
|
|
88
|
+
public ReadModelKey Resolve(object @event, EventContext context) =>
|
|
89
|
+
((OrderLineAdded)@event).OrderId;
|
|
90
|
+
|
|
91
|
+
public Task OrderLineAdded(OrderLineAdded @event, Order order)
|
|
92
|
+
{
|
|
93
|
+
// 'order' was materialized using OrderId rather than the triggering event's source id.
|
|
94
|
+
return Task.CompletedTask;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The resolved key applies to every read model parameter across all of the reactor's handler methods.
|
|
100
|
+
|
|
101
|
+
## Returning Side-Effect Events
|
|
102
|
+
|
|
103
|
+
Instead of taking a dependency on `IEventLog`, reactor handler methods can return events that should be appended automatically. The default target is the event log; the EventSourceId defaults to the one from the triggering event.
|
|
104
|
+
|
|
105
|
+
### Return a single event
|
|
106
|
+
|
|
107
|
+
```csharp
|
|
108
|
+
public Task<SomeEvent> Handle(AnEvent @event, EventContext context) =>
|
|
109
|
+
Task.FromResult(new SomeEvent(@event.Name));
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Return a collection of events
|
|
113
|
+
|
|
114
|
+
```csharp
|
|
115
|
+
public Task<IEnumerable<object>> Handle(AnEvent @event, EventContext context) =>
|
|
116
|
+
Task.FromResult<IEnumerable<object>>([new SomeEvent(), new AnotherEvent()]);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Target a specific event source id via `EventForEventSourceId`
|
|
120
|
+
|
|
121
|
+
Return an `EventForEventSourceId` to append to an explicit `EventSourceId` — for example a different entity than the one that triggered the reactor. It also carries the `EventStreamType`, `EventStreamId`, `EventSourceType`, `Subject`, `Occurred` time, `Tags` and `Causation` per event:
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
public Task<EventForEventSourceId> Handle(AnEvent @event, EventContext context) =>
|
|
125
|
+
Task.FromResult(new EventForEventSourceId(@event.RelatedId, new SomeEvent(@event.Name))
|
|
126
|
+
{
|
|
127
|
+
EventStreamType = new EventStreamType("custom"), // optional
|
|
128
|
+
EventSourceType = new EventSourceType("order"), // optional
|
|
129
|
+
Subject = new Subject(@event.RelatedId), // optional
|
|
130
|
+
});
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Return events for multiple event source ids
|
|
134
|
+
|
|
135
|
+
Return `IEnumerable<EventForEventSourceId>` to append to several event source ids in one transaction:
|
|
136
|
+
|
|
137
|
+
```csharp
|
|
138
|
+
public Task<IEnumerable<EventForEventSourceId>> Handle(AnEvent @event, EventContext context) =>
|
|
139
|
+
Task.FromResult<IEnumerable<EventForEventSourceId>>(
|
|
140
|
+
[
|
|
141
|
+
new(@event.RelatedId, new SomeEvent()),
|
|
142
|
+
new(@event.OtherId, new AnotherEvent())
|
|
143
|
+
]);
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
> Reactor-level metadata (`ICanProvideEventSourceId`, `ICanProvideSubject`, `[EventStreamType]`, …) applies to bare `TEvent` returns. An `EventForEventSourceId` is self-describing, so its own values are used instead. You can mix bare events and `EventForEventSourceId` in a single `IEnumerable<object>` return — each is appended with its respective metadata, all in one transaction.
|
|
147
|
+
|
|
148
|
+
### Cross-stream via `EventForEventSourceId`
|
|
149
|
+
|
|
150
|
+
To append a side-effect event to a **different** event source, return `EventForEventSourceId(id, @event)` (single or `IEnumerable<EventForEventSourceId>`) — the same cross-stream wrapper a command `Handle()` uses. The wrapper is self-describing, so it is also where you set the event stream type and id, source type, `Subject`, occurred time, tags and causation; set only the ones you need and the rest take the append defaults.
|
|
151
|
+
|
|
152
|
+
```csharp
|
|
153
|
+
public Task<IEnumerable<EventForEventSourceId>> Handle(AnEvent @event, EventContext context) =>
|
|
154
|
+
Task.FromResult<IEnumerable<EventForEventSourceId>>(
|
|
155
|
+
[
|
|
156
|
+
new EventForEventSourceId(@event.RelatedId, new SomeEvent())
|
|
157
|
+
]);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
> **Chronicle version note:** reactor side-effect handling of `EventForEventSourceId` wrappers has shipped since Chronicle 15.35.
|
|
161
|
+
|
|
162
|
+
## External event stores (outbox / inbox)
|
|
163
|
+
|
|
164
|
+
Cross-service events route through Chronicle's outbox/inbox rather than a shared log:
|
|
165
|
+
|
|
166
|
+
- The producing service appends its **public contract event** to `EventSequenceId.Outbox`.
|
|
167
|
+
- The consuming observer listens to the implicit inbox sequence for the source store (`inbox-<source-store>`).
|
|
168
|
+
- Chronicle creates/reuses the outbox→inbox subscription from the observer's `[EventStore]` metadata, or from `[EventStore]` on the event type/assembly. Put `[assembly: EventStore("<source-store>")]` in the contracts project when every event there originates from one service; use observer-level `[EventStore("<source-store>")]` to override.
|
|
169
|
+
- An observer-level `[EventStore]` **cannot** be combined with `[EventSequence]`, `[EventLog]`, or `Reactor(eventSequence: ...)`.
|
|
170
|
+
|
|
171
|
+
## Confirming downstream completion — `WaitForCompletion`
|
|
172
|
+
|
|
173
|
+
The default is fire-and-forget. When a caller's correctness depends on all observers (projections, reducers, reactors) having processed an appended event before returning — integration tests, synchronous webhook surfaces — call `WaitForCompletion` (from `Cratis.Chronicle.Observation`) on the `AppendResult`/`AppendManyResult`. It returns an `AppendResultWaitForCompletionResult` carrying `IsSuccess` and `FailedPartitions` (the full set across all affected observers, not just the first to fail). Reach for it deliberately — not as a default.
|
|
174
|
+
|
|
175
|
+
## Critical Rules
|
|
176
|
+
|
|
177
|
+
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
|
+
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 IEventLog** — If the reactor needs to produce new events, return them directly as `Task<TEvent>`, `Task<EventForEventSourceId>`, or a collection thereof. For commands in other slices, inject `ICommandPipeline` and execute a command. Avoid injecting `IEventLog` directly into a reactor.
|
|
180
|
+
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
|
+
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
|
+
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
|
+
7. **No state** — Reactors should be stateless. Inject dependencies via primary constructor, but do not store mutable state on the class.
|
|
184
|
+
|
|
185
|
+
## Slice Types That Use Reactors
|
|
186
|
+
|
|
187
|
+
| Slice type | Pattern |
|
|
188
|
+
|------------|---------|
|
|
189
|
+
| **Automation** | Reacts to events, makes decisions, triggers side effects |
|
|
190
|
+
| **Translation** | Adapts events from one slice/system by triggering commands in another |
|
|
191
|
+
|
|
192
|
+
### Automation Example
|
|
193
|
+
|
|
194
|
+
```csharp
|
|
195
|
+
public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor
|
|
196
|
+
{
|
|
197
|
+
/// <summary>
|
|
198
|
+
/// Sends a notification when a project is registered.
|
|
199
|
+
/// </summary>
|
|
200
|
+
/// <param name="event">The event.</param>
|
|
201
|
+
/// <param name="context">The event context.</param>
|
|
202
|
+
public async Task ProjectRegistered(ProjectRegistered @event, EventContext context) =>
|
|
203
|
+
await notifications.Notify($"Project '{@event.Name}' was registered.");
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Translation Example
|
|
208
|
+
|
|
209
|
+
```csharp
|
|
210
|
+
public class StockKeeping(IStockKeeper stockKeeper, ICommandPipeline commandPipeline) : IReactor
|
|
211
|
+
{
|
|
212
|
+
/// <summary>
|
|
213
|
+
/// Reacts to a book reservation by decreasing stock.
|
|
214
|
+
/// </summary>
|
|
215
|
+
/// <param name="event">The event.</param>
|
|
216
|
+
/// <param name="context">The event context.</param>
|
|
217
|
+
public async Task BookReserved(BookReserved @event, EventContext context) =>
|
|
218
|
+
await commandPipeline.Execute(new DecreaseStock(@event.Isbn, await stockKeeper.GetStock(@event.Isbn)));
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Testing Reactors
|
|
223
|
+
|
|
224
|
+
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:
|
|
225
|
+
|
|
226
|
+
```csharp
|
|
227
|
+
void Establish()
|
|
228
|
+
{
|
|
229
|
+
_notifications = Substitute.For<INotificationService>();
|
|
230
|
+
_scenario = new(new ServiceCollection().AddSingleton(_notifications).BuildServiceProvider());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async Task Because() => await _scenario.Given.ForEventSource(_id).Events(new ProjectRegistered("Acme"));
|
|
234
|
+
|
|
235
|
+
[Fact] async Task should_notify() => await _notifications.Received(1).Notify("Project 'Acme' was registered.");
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
For reactors that return side-effect events, assert the resulting appends through the scenario's event store; for non-event side effects, assert on the mocked services (as above). See [specs.scenarios.csharp.md](./specs.scenarios.csharp.md) for the full `*Scenario` family.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*"
|
|
3
|
+
description: "Use when running any shell command, reading files, or searching code. Route every command rtk supports through rtk (a hook auto-rewrites Bash commands); call rtk read/grep/find directly because the built-in Read/Grep/Glob tools bypass that hook."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Using rtk (Token-Optimized Commands)
|
|
7
|
+
|
|
8
|
+
[rtk](https://github.com/rtk-ai/rtk) (Rust Token Killer) is a CLI proxy that filters and compresses command output *before it reaches the model* — typically **60–90% fewer tokens** on common dev commands, with no loss of the signal you actually need. The default in this corpus is to **run every command rtk supports through rtk**. The savings are real and compound across a session: build, test, lint, git, search, and file-read output are exactly the high-volume, low-signal outputs rtk trims.
|
|
9
|
+
|
|
10
|
+
## How it works — let the hook do its job
|
|
11
|
+
|
|
12
|
+
A `PreToolUse` hook **auto-rewrites Bash commands** to their `rtk` equivalent transparently and at zero token overhead (`git status` → `rtk git status`). For any supported command you do **nothing special** — run it normally and the hook wraps it.
|
|
13
|
+
|
|
14
|
+
- **Never bypass it.** Don't disable the hook, and reserve `rtk proxy <cmd>` for the rare case where you genuinely need the raw, unfiltered output (e.g. debugging what a filter dropped).
|
|
15
|
+
- **Audit coverage** with `rtk gain` (savings so far) and `rtk discover` (commands that slipped past rtk — missed opportunities to close).
|
|
16
|
+
|
|
17
|
+
## What rtk supports (route these through rtk)
|
|
18
|
+
|
|
19
|
+
- **Files** — `ls`, `tree`, `read`, `find`, `grep`, `diff`
|
|
20
|
+
- **Git & GitHub** — `git status/log/diff/add/commit/push/pull`, `gh pr/issue/run …`
|
|
21
|
+
- **Tests** — `dotnet test`, Jest, Vitest, Playwright, pytest, Go, Cargo, RSpec
|
|
22
|
+
- **Build & lint** — `dotnet build`, `tsc`, ESLint, Biome, Prettier, Cargo Clippy, Ruff, golangci-lint, Rubocop
|
|
23
|
+
- **Package managers** — pnpm/npm/yarn, pip, Bundler, Prisma
|
|
24
|
+
- **Cloud & containers** — AWS CLI, Docker, Kubernetes, OpenShift
|
|
25
|
+
|
|
26
|
+
In practice this means the Cratis **quality-gate commands** — `dotnet build`, `dotnet test`, `yarn lint`, `npx tsc -b`, `git`, `gh` — all flow through rtk automatically. Just run them.
|
|
27
|
+
|
|
28
|
+
## The one gap — built-in tools bypass the hook
|
|
29
|
+
|
|
30
|
+
The hook only sees **Bash** commands. Claude Code's built-in `Read`, `Grep`, and `Glob` tools (and the Copilot equivalents) do **not** pass through it, so they are **not** auto-rewritten — and those are some of the most token-heavy operations in a session.
|
|
31
|
+
|
|
32
|
+
- For **bulk reads and broad searches** where output volume is large, call **`rtk read` / `rtk grep` / `rtk find`** from the terminal so the savings are captured. This is a deliberate override of the usual "prefer the built-in file tools" default.
|
|
33
|
+
- Keep the **built-in** `Read`/`Grep`/`Glob` for **small, targeted reads** and when you need exact line references for an edit — rtk filters output, so it serves exploration and volume, not the precise content an `Edit` must match verbatim.
|
|
34
|
+
|
|
35
|
+
## Availability
|
|
36
|
+
|
|
37
|
+
This assumes the `rtk` binary is installed and the hook configured (`rtk init -g`). If `rtk` is **not** on `PATH`, ignore this rule and use the normal tools — never block work on rtk being present.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/for_*/**/*.cs"
|
|
5
|
+
- "**/when_*/**/*.cs"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# How to Write C# Specs
|
|
9
|
+
|
|
10
|
+
Extends the base [specs.md](./specs.md) (folder structure, naming, philosophy) with C#-specific conventions. This file is the **universal foundation** used in **both** profiles — the `Cratis.Specifications` pattern (`Establish → Because → should_`) with NSubstitute.
|
|
11
|
+
|
|
12
|
+
> **Which spec surface?**
|
|
13
|
+
> - **Framework / library code** (Chronicle kernel, Arc pipeline, source generators, Fundamentals): the `Specification` base + NSubstitute in this file is the dominant mode — unit-test the classes under test in isolation. Reach for a `*Scenario` helper (in [specs.scenarios.csharp.md](./specs.scenarios.csharp.md)) **only** when testing the very engine your repo provides — Arc → `CommandScenario`, Chronicle → the event/projection/reactor scenarios. The **write-specs** skill is application-oriented, not for general framework specs.
|
|
14
|
+
> - **Event-sourced applications on Cratis** (commands, projections, reducers, reactors, constraints): use this base **plus** the in-process scenario family — see **[specs.scenarios.csharp.md](./specs.scenarios.csharp.md)** (`profile: application`) and the **write-specs** skill.
|
|
15
|
+
|
|
16
|
+
`Cratis.Specifications` keeps the approach of Machine.Specifications (MSpec): `Establish → Because → should_` maps to "Given → When → Then" and keeps each spec focused on *one setup, one action, one set of assertions*.
|
|
17
|
+
|
|
18
|
+
## Frameworks
|
|
19
|
+
|
|
20
|
+
- [xUnit](https://xunit.net/) for execution.
|
|
21
|
+
- [Cratis.Specifications](https://github.com/Cratis/Specifications) for BDD specification by example — the `Specification` base class discovers `Establish`/`Because`/`Destroy` by convention (no attributes).
|
|
22
|
+
- [NSubstitute](https://nsubstitute.github.io/) for mocking collaborators.
|
|
23
|
+
- Spec projects are named `<Source>.Specs`.
|
|
24
|
+
|
|
25
|
+
## BDD pattern
|
|
26
|
+
|
|
27
|
+
`Establish` sets up the world, `Because` performs the single action under test, `should_*` facts verify individual outcomes. Any method can be `async Task`.
|
|
28
|
+
|
|
29
|
+
```csharp
|
|
30
|
+
public class when_combining_parts : Specification
|
|
31
|
+
{
|
|
32
|
+
object[] _parts;
|
|
33
|
+
string _result;
|
|
34
|
+
|
|
35
|
+
void Establish() => _parts = ["First", "Second", "Third"];
|
|
36
|
+
void Because() => _result = KeyHelper.Combine(_parts);
|
|
37
|
+
|
|
38
|
+
[Fact] void should_combine_all_parts() => _result.ShouldEqual("First+Second+Third");
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Method | Purpose | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| `void Establish()` | setup, before `Because()` | each class in the chain has its own, run base-first — never call `base.Establish()` |
|
|
45
|
+
| `void Because()` | the single action under test | only in concrete spec files, never in a reusable context |
|
|
46
|
+
| `[Fact] void should_*()` | one assertion per fact | use the `ShouldXxx()` extension methods |
|
|
47
|
+
| `void Destroy()` | teardown after each test | |
|
|
48
|
+
|
|
49
|
+
## Reusable layered contexts
|
|
50
|
+
|
|
51
|
+
Layered contexts (`all_dependencies → a_<sut> → when_*`) capture shared setup base-first: the base mocks dependencies, the next builds the system under test, each spec adds only what's unique.
|
|
52
|
+
|
|
53
|
+
```csharp
|
|
54
|
+
// given/all_dependencies.cs — mocks all deps
|
|
55
|
+
public class all_dependencies : Specification
|
|
56
|
+
{
|
|
57
|
+
protected IEventStore _eventStore;
|
|
58
|
+
void Establish() => _eventStore = Substitute.For<IEventStore>();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// given/a_reactor_handler.cs — builds the system under test
|
|
62
|
+
public class a_reactor_handler : all_dependencies
|
|
63
|
+
{
|
|
64
|
+
protected ReactorHandler _handler;
|
|
65
|
+
void Establish() => _handler = new(_eventStore);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Put the action under test only in the concrete spec's `Because()`, never in a reusable context.
|
|
70
|
+
|
|
71
|
+
## NSubstitute patterns
|
|
72
|
+
|
|
73
|
+
```csharp
|
|
74
|
+
_service.GetValue(Arg.Any<string>()).Returns("result");
|
|
75
|
+
Arg.Is<Request>(r => r.Id == expectedId) // matcher
|
|
76
|
+
_service.Received(1).DoSomething(Arg.Any<string>()); // verify
|
|
77
|
+
_service.DidNotReceive().DoSomethingElse(Arg.Any<int>());
|
|
78
|
+
_handler.Handle(Arg.Any<CommandContext>()).Throws(new Exception("fail"));
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Catch exceptions for a unit under test with `Catch.Exception`:
|
|
82
|
+
|
|
83
|
+
```csharp
|
|
84
|
+
async Task Because() => _error = await Catch.Exception(_sut.DoSomething);
|
|
85
|
+
[Fact] void should_not_fail() => _error.ShouldBeNull();
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Assertion extension methods
|
|
89
|
+
|
|
90
|
+
From `Cratis.Specifications`: `.ShouldEqual(expected)`, `.ShouldBeTrue()`, `.ShouldBeFalse()`, `.ShouldBeNull()`, `.ShouldNotBeNull()`, `.ShouldBeEmpty()`, `.ShouldNotBeEmpty()`, `.ShouldContain(item)`, `.ShouldNotContain(item)`, `.ShouldContainOnly(items)`, `.ShouldBeOfExactType<T>()`, `.ShouldBeGreaterThan(n)`, `.ShouldBeLessThan(n)`.
|
|
91
|
+
|
|
92
|
+
## Conventions
|
|
93
|
+
|
|
94
|
+
- Common usings come from `GlobalUsings.Specs.cs` (`Xunit`, `NSubstitute`, `Cratis.Specifications`, …) — don't duplicate them, and don't add a using for the system-under-test namespace.
|
|
95
|
+
- **`using` ordering (SA1210/SA1211):** non-aliased namespaces first, then a blank line, then `using <alias> = …` aliases sorted by alias name. Alias a type whose short name collides with a namespace segment (CS0118) — `using RegisterAuthorCmd = …` style, with a domain-meaningful alias, never a technical `Command`/`Event`/`Component` suffix.
|
|
96
|
+
- Single-statement assertion lambdas use expression-body form (RCS1021) — `[Fact] void should_x() => result.ShouldEqual(...)`, not a `{ … }` block. Don't break long `should_` lines; don't add blank lines between `should_` methods.
|
|
97
|
+
- **Prefer a concept's sentinel over a raw empty primitive.** When a `ConceptAs<T>` / `EventSourceId<T>` value is expected — as an argument, a mock setup, or an assertion — use its `NotSet` (or `Empty` / `New()`) static rather than a `string.Empty` / `Guid.Empty` / `0` that implicitly converts. It states intent and survives a sentinel-value change (see [concepts.md](./concepts.md)). Reserve raw `string.Empty` for genuinely non-concept `string` values (e.g. asserting a decrypted-to-empty JSON payload).
|
|
98
|
+
|
|
99
|
+
## Never sleep — await a fact, not a duration
|
|
100
|
+
|
|
101
|
+
**A spec never waits on the clock for the system under test.** `Thread.Sleep`, a bare `Task.Delay`, or a `SpinWait` before an assertion makes the spec pass because the machine happened to be fast enough, and it silently writes today's latency into the suite: the next fix that shifts timing in the observer/projection core then breaks specs that have nothing to do with it. That coupling — not the bugs being fixed — is the single largest source of regressions in Chronicle, and it is why fixes there keep getting rolled back.
|
|
102
|
+
|
|
103
|
+
Wait on a signal instead:
|
|
104
|
+
|
|
105
|
+
| Waiting for | Await |
|
|
106
|
+
|---|---|
|
|
107
|
+
| observers to catch up with an append | `appendResult.WaitForCompletion()` |
|
|
108
|
+
| client artifacts to be registered with the kernel | `eventStore.WaitForRegistration()` |
|
|
109
|
+
| an observer's state or position | `WaitTillActive` / `WaitTillSubscribed` / `WaitTillReachesEventSequenceNumber` / `WaitForState` (reactors, reducers, projections) |
|
|
110
|
+
| a job | the `JobsWaitHelpers` extensions |
|
|
111
|
+
| anything with no helper yet | build the signal: a `SemaphoreSlim` (or `TaskCompletionSource`) released by the code that observes the event, awaited under a timeout — `Source/Clients/XUnit.Integration/EventAppendCollection.cs` is the shipped example, and it never polls |
|
|
112
|
+
|
|
113
|
+
A **deadline** is not a sleep — every helper above takes a `timeout`, and a timeout is what turns a hang into a named failure. Sleeping *between* re-checks is: a poll loop is a completion signal you have not built yet, so add the signal rather than another `while` + `Task.Delay`.
|
|
114
|
+
|
|
115
|
+
Three delays are not waits at all and stay allowed — say which one it is in a comment: a test double that is **slow on purpose** so the test can observe it mid-flight (`Task.Delay(HandleTime)`), an **infrastructure readiness backoff** between retries of a connect/start that can legitimately fail, and a `Task.Delay(1)` / `Task.Yield()` that **widens an interleaving window** in a concurrency spec.
|
|
116
|
+
|
|
117
|
+
The sleeps that predate this rule are frozen per file in `.github/timing-coupling-baseline.txt`; `.github/workflows/timing-coupling-ratchet.yml` fails a pull request that raises any entry or adds a file. The numbers may only go down.
|
|
118
|
+
|
|
119
|
+
## What NOT to spec
|
|
120
|
+
|
|
121
|
+
Simple properties are compiler-verified. Save spec effort for business logic, coordination between dependencies, and complex transformations.
|
|
122
|
+
|
|
123
|
+
```csharp
|
|
124
|
+
// ❌ Do NOT spec these
|
|
125
|
+
public string TableName => tableName; // returns constructor parameter
|
|
126
|
+
public IEnumerable<Property> Properties => mapper.Properties; // simple delegation
|
|
127
|
+
|
|
128
|
+
// ✅ Spec complex business logic
|
|
129
|
+
public decimal TotalCost => Items.Sum(i => i.Cost * i.Quantity * (1 + i.TaxRate));
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Also don't spec logging or trivial getters.
|
|
133
|
+
|
|
134
|
+
## See also
|
|
135
|
+
|
|
136
|
+
- [specs.md](./specs.md) — folder structure, naming, BDD philosophy (the base this extends).
|
|
137
|
+
- [specs.scenarios.csharp.md](./specs.scenarios.csharp.md) — **application profile**: the in-process scenario family (`CommandScenario` / `EventScenario` / `ReadModelScenario` / `ReactorScenario`) + out-of-process Chronicle integration, for event-sourced apps.
|
|
138
|
+
- [efcore.specs.md](./efcore.specs.md) — `DbContext` specs with SQLite in-memory.
|
|
139
|
+
- [frontend-testing.md](./frontend-testing.md) — TypeScript/React application BDD specs.
|