@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,115 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.cs"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.cs"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Concepts — Strongly Typed Domain Values
|
|
8
|
+
|
|
9
|
+
## Why
|
|
10
|
+
|
|
11
|
+
A primitive such as `Guid` or `string` does not tell the compiler whether a
|
|
12
|
+
value is an `AuthorId`, `UserId`, or `InvoiceNumber`. A Cratis concept gives a
|
|
13
|
+
meaningful value its own type and keeps that meaning in method signatures,
|
|
14
|
+
serialization, validation, events, and read models.
|
|
15
|
+
|
|
16
|
+
Use a concept for a real domain value, not mechanically for every primitive in a
|
|
17
|
+
DTO or framework API. An enum already represents a closed domain concept and
|
|
18
|
+
does not need a `ConceptAs<T>` wrapper.
|
|
19
|
+
|
|
20
|
+
## Product contracts
|
|
21
|
+
|
|
22
|
+
- A value concept derives from `ConceptAs<T>` in `Cratis.Fundamentals`.
|
|
23
|
+
- A value actually used as a Chronicle event-source/stream identity derives from
|
|
24
|
+
`EventSourceId<T>` in `Cratis.Chronicle`.
|
|
25
|
+
- An arbitrary entity ID that does not identify a Chronicle stream remains a
|
|
26
|
+
value concept; do not derive it from `EventSourceId<T>` merely because its name
|
|
27
|
+
ends in `Id`.
|
|
28
|
+
- Both generic bases require an underlying type implementing `IComparable`.
|
|
29
|
+
|
|
30
|
+
## Value concept rules
|
|
31
|
+
|
|
32
|
+
- Use a positional record containing exactly one wrapped value. Fundamentals
|
|
33
|
+
serialization assumes a single-value concept; do not add extra properties.
|
|
34
|
+
- `ConceptAs<T>` supplies concept → `T` conversion.
|
|
35
|
+
- Add `T` → derived concept conversion only when it improves the domain API. It
|
|
36
|
+
is optional, not a framework requirement.
|
|
37
|
+
- `ConceptAs<T>` rejects a null wrapped value. Use a nullable concept reference
|
|
38
|
+
such as `AuthorName?` when absence is valid.
|
|
39
|
+
- `NotSet` or `Empty` is optional domain policy. Add one only when the backing
|
|
40
|
+
value is impossible or explicitly reserved in that domain. Do not assume
|
|
41
|
+
empty string, zero, or `Guid.Empty` is universally invalid.
|
|
42
|
+
- Mark a concept that holds **personal data** `[PII]` and one that holds a
|
|
43
|
+
**secret** `[NotAudited]`. Both markings travel with the type, so marking it
|
|
44
|
+
once covers every command and event that uses it - which is the point of
|
|
45
|
+
having the concept. A command's property values are written to the causation
|
|
46
|
+
chain of every event it appends, so an unmarked `ApiKey` or `AccessToken`
|
|
47
|
+
concept reaches the event log in the clear and stays there. The two are not
|
|
48
|
+
interchangeable: `[PII]` also encrypts and enrolls the value in erasure, which
|
|
49
|
+
is wrong for a password, and `[NotAudited]` does nothing for an erasure
|
|
50
|
+
request, which is wrong for a name.
|
|
51
|
+
|
|
52
|
+
```csharp
|
|
53
|
+
public record AuthorName(string Value) : ConceptAs<string>(Value)
|
|
54
|
+
{
|
|
55
|
+
public static implicit operator AuthorName(string value) => new(value);
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Chronicle stream identity rules
|
|
60
|
+
|
|
61
|
+
```csharp
|
|
62
|
+
public record AuthorId(Guid Value) : EventSourceId<Guid>(Value)
|
|
63
|
+
{
|
|
64
|
+
public static AuthorId New() => new(Guid.NewGuid());
|
|
65
|
+
public static implicit operator AuthorId(Guid value) => new(value);
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `New()`, primitive → derived ID conversion, and sentinels are optional domain
|
|
70
|
+
conveniences; `EventSourceId<T>` does not create an arbitrary derived type.
|
|
71
|
+
- The exact `EventSourceId<T>` base supports conversions among its underlying
|
|
72
|
+
`T`, string, typed ID, and untyped `EventSourceId`, but those operators do not
|
|
73
|
+
construct your derived domain record from every source form.
|
|
74
|
+
- String and Guid are the safest round-trip primitives. Other comparable values
|
|
75
|
+
rely on Chronicle conversion behavior and require focused verification.
|
|
76
|
+
- Pass the identity explicitly to Chronicle append/read operations. Declaring an
|
|
77
|
+
`EventSourceId<T>` property does not select the stream.
|
|
78
|
+
- Do not put `[Key]` or `[Subject]` on an `EventSourceId<T>`-derived member;
|
|
79
|
+
Chronicle analyzer `CHR0026` reports it.
|
|
80
|
+
- Do not put `[PII]` on an event-source ID; analyzer `CHR0034` rejects it.
|
|
81
|
+
Sensitive natural identifiers use a random surrogate stream ID and a separate
|
|
82
|
+
compliance-managed value.
|
|
83
|
+
- `EventSourceId.Unspecified` is the untyped string-backed sentinel. Typed empty
|
|
84
|
+
or zero values convert to real specified stream IDs and are not equivalent to
|
|
85
|
+
`Unspecified`.
|
|
86
|
+
|
|
87
|
+
## Application placement convention
|
|
88
|
+
|
|
89
|
+
In a Cratis application, place a concept with the feature/module that owns its
|
|
90
|
+
meaning rather than in a generic `Concepts/` folder. Put genuinely cross-feature
|
|
91
|
+
concepts in `Common/`. Do not introduce a top-level `Features/` wrapper.
|
|
92
|
+
|
|
93
|
+
This is a Cratis application convention, not a Fundamentals or Chronicle API
|
|
94
|
+
contract. Framework and client repositories follow their own structure.
|
|
95
|
+
|
|
96
|
+
## Promote a value deliberately
|
|
97
|
+
|
|
98
|
+
Promote a value when its meaning or cross-cutting characteristics must travel
|
|
99
|
+
with it—for example validation, compliance classification, or a domain-specific
|
|
100
|
+
format. Reuse an existing shared concept when it already owns that meaning.
|
|
101
|
+
|
|
102
|
+
## Call-site guidance
|
|
103
|
+
|
|
104
|
+
Use the constructors, conversions, factories, or sentinels the domain type
|
|
105
|
+
actually provides. Do not require `NotSet`, `New()`, or a reverse conversion on
|
|
106
|
+
every concept. If a sentinel exists, reference the named sentinel rather than
|
|
107
|
+
reconstructing its backing primitive.
|
|
108
|
+
|
|
109
|
+
## Geospatial values
|
|
110
|
+
|
|
111
|
+
Use GeoJSON types from `Cratis.Geospatial`: `Point` for a location,
|
|
112
|
+
`LineString` for a route, and `Polygon` for an area. Do not use the removed
|
|
113
|
+
experimental `Coordinate` type. Model geospatial absence according to the
|
|
114
|
+
owning domain contract; do not invent a nullable event payload that conflicts
|
|
115
|
+
with Chronicle event rules.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.cs"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.cs"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# C# Conventions
|
|
9
|
+
|
|
10
|
+
The goal is minimal ceremony, maximum clarity. Modern C# (13+) gives us records, primary constructors, pattern matching, and file-scoped namespaces — use them everywhere. The less boilerplate in a file, the faster a reader can understand what it *does*.
|
|
11
|
+
|
|
12
|
+
## Building
|
|
13
|
+
|
|
14
|
+
- Use `dotnet build` from the command line.
|
|
15
|
+
- Use `dotnet format` to format code.
|
|
16
|
+
- Use `dotnet test` to run tests.
|
|
17
|
+
|
|
18
|
+
## Formatting
|
|
19
|
+
|
|
20
|
+
These rules exist so that every file in the codebase reads the same way. When formatting is consistent, code review focuses on logic, not style.
|
|
21
|
+
|
|
22
|
+
- Apply code-formatting style defined in `.editorconfig`.
|
|
23
|
+
- Use file-scoped namespace declarations — one less level of indentation for the entire file.
|
|
24
|
+
- Use single-line `using` directives, sorted alphabetically.
|
|
25
|
+
- Never qualify a type that is already unambiguously in scope via a `using` directive. When two `using` directives introduce conflicting type names, qualify only the conflicting occurrences using the shortest unambiguous path (e.g. `Concepts.Events.Foo` or `Contracts.Events.Foo`) — do not add `using` aliases for every conflicting type.
|
|
26
|
+
- Insert a blank line before the opening `{` of every code block (`if`, `for`, `foreach`, `try`, `using`, etc.).
|
|
27
|
+
- Ensure the final `return` statement of a method is on its own line.
|
|
28
|
+
- Use pattern matching and switch expressions wherever possible — they are more readable and the compiler verifies exhaustiveness.
|
|
29
|
+
- Use `nameof` instead of string literals — it survives refactoring.
|
|
30
|
+
- Place private class declarations at the bottom of the file — public API first, implementation details last.
|
|
31
|
+
|
|
32
|
+
## Language — American English Only
|
|
33
|
+
|
|
34
|
+
All identifiers, comments, XML docs, and string literals must use **American English** spelling (initialize, serialize, behavior, color, organization, center, modeling, dialog, license, judgment, gray). See [general.md](./general.md) for the full guidance.
|
|
35
|
+
|
|
36
|
+
## Naming
|
|
37
|
+
|
|
38
|
+
- PascalCase for type names, method names, and public members.
|
|
39
|
+
- camelCase for private fields and local variables.
|
|
40
|
+
- Prefix private fields with `_` (e.g. `_myField`).
|
|
41
|
+
- Prefix interface names with `I` (e.g. `IMyService`).
|
|
42
|
+
|
|
43
|
+
## Code Style
|
|
44
|
+
|
|
45
|
+
Every rule here reduces noise. `var` avoids redundant type repetition. Expression bodies eliminate braces for trivial members. Primary constructors remove the constructor-plus-field ceremony.
|
|
46
|
+
|
|
47
|
+
- Prefer `var` over explicit types — the right side of the assignment already tells you the type.
|
|
48
|
+
- Use expression-bodied members for simple methods and properties.
|
|
49
|
+
- Favor primary constructors for all types — they eliminate field declarations for injected dependencies.
|
|
50
|
+
- Use string interpolation instead of `string.Format()` or concatenation.
|
|
51
|
+
- Favor collection initializers and object initializers.
|
|
52
|
+
- Use `IEnumerable<T>` for collections that are not modified; never return mutable collections from public APIs.
|
|
53
|
+
- Prefer LINQ (`.Where`, `.Any`, `.Select`, `.FirstOrDefault`) over a `foreach` that filters inside its body with an `if`/`continue` or an early `return` — put the filter in the query so the intent is explicit (`items.Where(predicate)`, `return items.Any(predicate);`). Reserve `foreach` for genuine iteration with side effects. (Filtering inside a loop is what the analyzers flag as a "missed opportunity to use Where".)
|
|
54
|
+
- Don't use regions — they hide code instead of organizing it. If a file needs regions, it needs refactoring.
|
|
55
|
+
- Never add postfixes like `Async`, `Impl`, `Service` to class names — they add noise without information.
|
|
56
|
+
- For types with no implementation body, omit the braces (e.g. `public interface IMyInterface;`).
|
|
57
|
+
- Prefer `record` types for immutable data structures (events, commands, read models, concepts) — they give you value equality, immutability, and concise syntax for free.
|
|
58
|
+
|
|
59
|
+
## Nullable Reference Types
|
|
60
|
+
|
|
61
|
+
Embrace the type system — it is the first line of defense against null-related bugs. When it says something cannot be null, trust it.
|
|
62
|
+
|
|
63
|
+
- Use `is null` / `is not null` — never `== null` / `!= null`.
|
|
64
|
+
- Trust the C# null annotations; don't add defensive null checks when the type system guarantees a value.
|
|
65
|
+
- Add `!` operator where nullability warnings occur and you are certain the value is non-null.
|
|
66
|
+
- Use `is not null` checks before dereferencing potentially null values.
|
|
67
|
+
|
|
68
|
+
## XML Documentation
|
|
69
|
+
|
|
70
|
+
XML doc comments are the public API's first impression. They must be multiline — never cram `<summary>` onto a single line. Every public type, method, property, and operator must have XML docs.
|
|
71
|
+
|
|
72
|
+
- Always use **multiline** `<summary>` tags — opening and closing tags on their own lines:
|
|
73
|
+
|
|
74
|
+
```csharp
|
|
75
|
+
/// <summary>
|
|
76
|
+
/// Represents the unique identifier of a project.
|
|
77
|
+
/// </summary>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
- **Never** use single-line summaries:
|
|
81
|
+
|
|
82
|
+
```csharp
|
|
83
|
+
// ❌ Wrong
|
|
84
|
+
/// <summary>Represents the unique identifier of a project.</summary>
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- Every method or operator with parameters **must** include `<param name="...">` for each parameter.
|
|
88
|
+
- Every method or operator that returns a value (non-void) **must** include `<returns>`.
|
|
89
|
+
- Every method that throws must document the exception with `<exception cref="...">` tags.
|
|
90
|
+
- Use `<see cref="..."/>` and `<paramref name="..."/>` to cross-reference types and parameters.
|
|
91
|
+
- Keep summaries concise and purposeful — only document when it adds understanding beyond the name itself.
|
|
92
|
+
|
|
93
|
+
Example:
|
|
94
|
+
|
|
95
|
+
```csharp
|
|
96
|
+
/// <summary>
|
|
97
|
+
/// Represents an instance of <see cref="ICommandFilters"/>.
|
|
98
|
+
/// </summary>
|
|
99
|
+
/// <param name="filters">The collection of <see cref="ICommandFilter"/> to use for filtering commands.</param>
|
|
100
|
+
[Singleton]
|
|
101
|
+
public class CommandFilters(IInstancesOf<ICommandFilter> filters) : ICommandFilters
|
|
102
|
+
{
|
|
103
|
+
/// <summary>
|
|
104
|
+
/// Filters the command execution through all registered command filters.
|
|
105
|
+
/// </summary>
|
|
106
|
+
/// <param name="context">The <see cref="CommandContext"/> to filter.</param>
|
|
107
|
+
/// <returns>A <see cref="CommandResult"/> representing the aggregated filter outcome.</returns>
|
|
108
|
+
public async Task<CommandResult> OnExecution(CommandContext context)
|
|
109
|
+
{
|
|
110
|
+
// ...
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Exceptions
|
|
116
|
+
|
|
117
|
+
Every exception type in the codebase should communicate *what went wrong* in domain terms. Built-in types like `InvalidOperationException` tell you nothing about the problem — a custom `AuthorAlreadyRegistered` tells you everything.
|
|
118
|
+
|
|
119
|
+
- Use exceptions for exceptional situations only — never for control flow.
|
|
120
|
+
- Always create a custom exception type that derives from `Exception`.
|
|
121
|
+
- Never use built-in exception types (`InvalidOperationException`, `ArgumentException`, etc.).
|
|
122
|
+
- Never suffix exception class names with `Exception` — `AuthorNotFound` reads better than `AuthorNotFoundException`.
|
|
123
|
+
- Always provide a meaningful message when throwing.
|
|
124
|
+
- Add XML doc on the exception type starting with "The exception that is thrown when ...".
|
|
125
|
+
- Never write an empty or silently-swallowing `catch` block. Handle the exception, log it, or let it propagate. When ignoring is genuinely correct, use an exception filter (`catch (SomeException) when (…)`) with a body that states the decision (a comment and/or a fallback) — never a bare `catch { }`.
|
|
126
|
+
|
|
127
|
+
## Dependency Injection
|
|
128
|
+
|
|
129
|
+
The framework discovers and wires dependencies by convention. Explicit registration is the exception, not the rule.
|
|
130
|
+
|
|
131
|
+
- Prefer constructor injection; avoid `IServiceProvider` directly (service locator anti-pattern).
|
|
132
|
+
- For singletons, use the `[Singleton]` attribute — no explicit registration needed.
|
|
133
|
+
- Systems with a convention of `IFoo → Foo` do not need to be registered explicitly.
|
|
134
|
+
- Command/query `Handle()` method parameters are automatically resolved from DI by type.
|
|
135
|
+
|
|
136
|
+
### Service lifetimes — `[Singleton]` is a narrow choice, not the default
|
|
137
|
+
|
|
138
|
+
**Assume every application you build is multi-tenant.** Not "design for it later" — assume it now, even when the deployment ships with a single tenant and no tenant resolution configured. A single-tenant application is a multi-tenant one with one tenant in it, and the code shape that serves both is the same shape. The code shape that serves only one has to be found and rewritten later, from the far side of a data migration, under production. The same reasoning applies to the signed-in user: an application always has one, and a service that remembers *which* one will eventually answer for the wrong person.
|
|
139
|
+
|
|
140
|
+
That gives one rule with two faces:
|
|
141
|
+
|
|
142
|
+
> **A singleton may not depend on anything that belongs to a tenant, a user, or a request.**
|
|
143
|
+
|
|
144
|
+
These resolve **per scope**, and the scope is what carries the tenant — so none of them may be injected into a `[Singleton]`:
|
|
145
|
+
|
|
146
|
+
| Off limits in a singleton | Why |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `IEventStore` — and everything off it: `IEventLog`, `IReadModels`, `IConstraints`, `IEventTypes`, `IProjections`, `IReducers`, `IPII` | resolved for the scope's namespace |
|
|
149
|
+
| `IMongoCollection<T>`, `IMongoDatabase`, `IMongoClient` | the database name is resolved per scope from the current tenant |
|
|
150
|
+
| An EF Core `DbContext` | scoped for the same reason, plus it is not thread-safe |
|
|
151
|
+
| A read model injected directly by key | same scope, same binding |
|
|
152
|
+
| Any held tenant, principal, claims, correlation id, or `HttpContext` **value** | belongs to one request and outlives it in a singleton |
|
|
153
|
+
|
|
154
|
+
A `[Singleton]` taking one of these is a **captive dependency**: the container hands it the *root* scope's instance and keeps it for process lifetime. The root scope has no request, so it resolves no tenant — every read and write goes to the default namespace forever, regardless of who is asking.
|
|
155
|
+
|
|
156
|
+
**It does not throw. It returns nothing.** A query against the wrong namespace hits a database that exists and is empty, so the caller gets an empty collection, a `null` read model, or a default-valued options object, and carries on. The application starts, the pages render, the build is green, and the configuration a tenant spent an afternoon entering is simply not there. It is also invisible while there is only one tenant — every symptom appears on the day a second one arrives.
|
|
157
|
+
|
|
158
|
+
**What to use instead.** Default to the convention (transient), which inherits the resolving scope's tenant for free, or `[Scoped]` when a service must be shared within one request. Reserve `[Singleton]` for things that are genuinely process-wide and hold no tenant-, user-, or request-bound state: `IInstancesOf<T>` aggregators, HTTP client wrappers, `IOptions<T>` readers, pure computation, framework plumbing.
|
|
159
|
+
|
|
160
|
+
When something must be a singleton and still needs data — a hosted service, a dispatcher, a poller — inject `IServiceScopeFactory` and open a scope per unit of work:
|
|
161
|
+
|
|
162
|
+
```csharp
|
|
163
|
+
// ❌ Wrong — IEventStore is scoped; this captures the root scope's default namespace forever.
|
|
164
|
+
[Singleton]
|
|
165
|
+
public class DigestSources(IEventStore eventStore) : IDigestSources
|
|
166
|
+
{
|
|
167
|
+
public Task<DigestConfiguration?> GetCurrent() =>
|
|
168
|
+
eventStore.ReadModels.GetInstanceById<DigestConfiguration>(DigestId.Default);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ✅ Right — a scope per call, so the collaborators bind to the caller's tenant.
|
|
172
|
+
[Singleton]
|
|
173
|
+
public class DigestSources(IServiceScopeFactory scopeFactory) : IDigestSources
|
|
174
|
+
{
|
|
175
|
+
public async Task<DigestConfiguration?> GetCurrent()
|
|
176
|
+
{
|
|
177
|
+
using var scope = scopeFactory.CreateScope();
|
|
178
|
+
var eventStore = scope.ServiceProvider.GetRequiredService<IEventStore>();
|
|
179
|
+
return await eventStore.ReadModels.GetInstanceById<DigestConfiguration>(DigestId.Default);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`IChronicleClient` **is** singleton-safe, and is the right collaborator when a flow knows which namespace it means and has no scope to resolve one from — it names the event store and namespace explicitly: `await chronicleClient.GetEventStore("MyStore", tenantId.Value)`. Naming the namespace is a deliberate, readable statement that this code crosses a tenant boundary; capturing a scoped service is the same crossing made by accident.
|
|
185
|
+
|
|
186
|
+
**The current user is not process-wide either.** Never keep the signed-in user, their principal, claims, roles, or anything derived from them in a singleton. The distinction that matters: *the accessor is fine, the value is not.* `IHttpContextAccessor` is itself a singleton and safe to inject; reading a value out of it once and keeping it is not. A current-user service may be a singleton only when every method reads through the accessor on each call and stores nothing. Anything that derives something per user and wants to keep it holds a cache **keyed by the user**, never a single field.
|
|
187
|
+
|
|
188
|
+
**Off-request work carries its tenant.** Reactors, hosted services, background dispatch and scheduled jobs run with no HTTP request, so there is nothing for a tenant resolver to read. Chronicle observers are themselves instantiated per namespace, but the collaborators they call are not — a reactor that reaches a tenant-blind singleton has left its namespace behind without saying so. Such a flow states its tenant explicitly rather than inheriting whatever the root scope happens to be.
|
|
189
|
+
|
|
190
|
+
**Caching.** A process-wide cache of tenant data is the same bug wearing a performance justification. If a singleton caches, the tenant (and where relevant the user) is part of the key. The same holds for `static` fields: a `static` cache of anything tenant-scoped is shared by every tenant in the process.
|
|
191
|
+
|
|
192
|
+
**Enforce it, do not remember it.** This failure is silent, so review will not reliably catch it. Add an architecture spec that reflects over the assembly, finds every `[Singleton]` whose constructor takes a scope-bound service, and asserts the set is empty. It is a few dozen lines, it runs on every build, and it is the only thing that keeps the rule true a year from now.
|
|
193
|
+
|
|
194
|
+
> .NET's own captive-dependency detection (`ServiceProviderOptions.ValidateScopes`, which Arc deliberately leaves on in Development) exists to catch exactly this. If a singleton in your codebase holds a scoped service and Development startup is not complaining, that path is not being exercised in Development — worth knowing on its own.
|
|
195
|
+
|
|
196
|
+
### Discovering multiple implementations — use `IInstancesOf<T>`, never `IEnumerable<T>`
|
|
197
|
+
|
|
198
|
+
When a type needs every implementation of an abstraction (handlers, strategies, filters, validators, formatters), inject `IInstancesOf<TInterface>` from `Cratis.Types`. The framework discovers and instantiates every implementation by convention — no `services.AddSingleton<TInterface, Impl1>()` calls anywhere.
|
|
199
|
+
|
|
200
|
+
```csharp
|
|
201
|
+
// ❌ Wrong — requires hand-maintained registrations for every implementation.
|
|
202
|
+
// Adding a new IReactorSideEffectHandler somewhere else in the codebase silently
|
|
203
|
+
// does nothing until someone remembers to register it here, and dead registrations
|
|
204
|
+
// linger after types are removed.
|
|
205
|
+
services.AddSingleton<IReactorSideEffectHandler, EventResultHandler>();
|
|
206
|
+
services.AddSingleton<IReactorSideEffectHandler, EventsResultHandler>();
|
|
207
|
+
services.AddSingleton<IReactorSideEffectHandlers, ReactorSideEffectHandlers>();
|
|
208
|
+
|
|
209
|
+
public class ReactorSideEffectHandlers(IEnumerable<IReactorSideEffectHandler> handlers) : IReactorSideEffectHandlers { ... }
|
|
210
|
+
|
|
211
|
+
// ✅ Right — implementations discovered automatically. Mark singletons with [Singleton].
|
|
212
|
+
[Singleton]
|
|
213
|
+
public class EventResultHandler(IEventTypes eventTypes) : IReactorSideEffectHandler { ... }
|
|
214
|
+
|
|
215
|
+
[Singleton]
|
|
216
|
+
public class EventsResultHandler(IEventTypes eventTypes) : IReactorSideEffectHandler { ... }
|
|
217
|
+
|
|
218
|
+
[Singleton]
|
|
219
|
+
public class ReactorSideEffectHandlers(IInstancesOf<IReactorSideEffectHandler> handlers) : IReactorSideEffectHandlers { ... }
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
**Rules:**
|
|
223
|
+
|
|
224
|
+
- Never inject `IEnumerable<TInterface>` to enumerate implementations of an abstraction. That signature only works if every implementation is hand-registered, which defeats convention-based discovery. Use `IInstancesOf<TInterface>` instead.
|
|
225
|
+
- Never write `services.AddSingleton<TInterface, Impl>()` or `services.AddTransient<TInterface, Impl>()` for a type that exists to be discovered. Mark the implementation with `[Singleton]` (for singletons) or rely on the `IFoo → Foo` convention (for transients) and remove the registration line.
|
|
226
|
+
- `IInstancesOf<T>` resolves at the point of access — implementations added later in the assembly become available without touching the consumer or any composition root.
|
|
227
|
+
- `IEnumerable<T>` is still the right type to **return** from a method that yields a sequence of values. The rule applies only when the goal is to enumerate **implementations** of an abstraction.
|
|
228
|
+
|
|
229
|
+
## Logging
|
|
230
|
+
|
|
231
|
+
- Use structured logging with named parameters.
|
|
232
|
+
- Use `ILogger<T>` where `T` is the class name.
|
|
233
|
+
- Keep log messages in a separate `<ClassName>Logging.cs` partial static internal class.
|
|
234
|
+
- Use `[LoggerMessage]` attribute (without `eventId`).
|
|
235
|
+
|
|
236
|
+
## Async
|
|
237
|
+
|
|
238
|
+
- Use `async`/`await` for asynchronous programming.
|
|
239
|
+
- Use `Task` and `Task<T>` for asynchronous methods.
|
|
240
|
+
|
|
241
|
+
## Chronicle & Arc — Key API Types
|
|
242
|
+
|
|
243
|
+
These are the building blocks. Each type has a specific role in the vertical slice architecture — using the right type in the right place means the framework handles discovery, wiring, and proxy generation automatically.
|
|
244
|
+
|
|
245
|
+
| Type | Purpose |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| `ConceptAs<T>` | Strongly-typed domain *value* wrapper (see [concepts.md](./concepts.md)) |
|
|
248
|
+
| `EventSourceId<T>` | Strongly-typed *identity* base — derive event-source ids from this, not `ConceptAs<T>` |
|
|
249
|
+
| `[EventType]` | Marks a record as a Chronicle event — **never** pass arguments for a new event |
|
|
250
|
+
| `[Command]` | Marks a record as a model-bound command — define `Handle()` directly on the record |
|
|
251
|
+
| `[ReadModel]` | Marks a record as a model-bound query — define static query methods on the record |
|
|
252
|
+
| `CommandValidator<T>` | FluentValidation validator for commands |
|
|
253
|
+
| `IProjectionFor<T>` | Fluent projection definition — AutoMap is on by default, never call `.AutoMap()` |
|
|
254
|
+
| `IReducerFor<T>` | Imperative reducer — receives current state, returns new state |
|
|
255
|
+
| `IReactor` | Marker interface for side-effect observers — method dispatch by event type parameter |
|
|
256
|
+
| `IConstraint` | Constraint definition — enforced server-side by Chronicle at append time |
|
|
257
|
+
| `AggregateRoot` | Chronicle aggregate root with `Apply()` and `Commit()` |
|
|
258
|
+
| `ICommandPipeline` | Programmatic command execution from reactors or other code |
|
|
259
|
+
| `EventContext` | Event metadata: `Occurred`, `SequenceNumber`, `CorrelationId`, `EventSourceId`, etc. |
|
|
260
|
+
| `ISubject<T>` | Observable query return type — enables real-time push |
|
|
261
|
+
| `IMongoCollection<T>` | MongoDB collection — use `.Observe()` for reactive queries |
|
|
262
|
+
|
|
263
|
+
**Key conventions:**
|
|
264
|
+
|
|
265
|
+
- Prefer `ConceptAs<T>` over raw primitives in all domain models, commands, events, and queries; derive identity concepts from `EventSourceId<T>`. See [concepts.md](./concepts.md) for details.
|
|
266
|
+
- Projections join **events**, never read models — projections rebuild state from the event stream, not from other projections.
|
|
267
|
+
- For fluent projections, AutoMap is on by default — call `.From<EventType>()` without `.AutoMap()` and without manually mapping every matching property.
|
|
268
|
+
- Use model-bound projection attributes (`[FromEvent<T>]`, `[SetFrom<T>]`, etc.) when possible; fall back to `IProjectionFor<T>` for complex cases.
|
|
269
|
+
- Full slice anatomy lives in [vertical-slices.md](./vertical-slices.md).
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.tsx"
|
|
3
|
+
profile: application
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*.tsx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Using Dialogs
|
|
9
|
+
|
|
10
|
+
The Cratis dialog wrappers handle command execution, validation timing, loading states, and footer buttons consistently. Using PrimeReact's raw `Dialog` bypasses all of this and leads to inconsistent UX.
|
|
11
|
+
|
|
12
|
+
## Choose the Correct Dialog Type
|
|
13
|
+
|
|
14
|
+
- If confirm executes a command, use `CommandDialog` from `@cratis/components/CommandDialog`.
|
|
15
|
+
- If no command is executed on confirm, use `Dialog` from `@cratis/components/Dialogs`.
|
|
16
|
+
- If you are **asking the user to confirm** or **showing that something is in progress**, do not build a dialog at all — raise the host-rendered one through its hook (below).
|
|
17
|
+
- **Never** import `Dialog` from `primereact/dialog` directly.
|
|
18
|
+
|
|
19
|
+
## Confirmations and busy indicators are host-rendered — register once, raise by hook
|
|
20
|
+
|
|
21
|
+
`ConfirmationDialog` and `BusyIndicatorDialog` are **not** instantiated in a slice's JSX. They are
|
|
22
|
+
registered once at the app root and raised from anywhere through a hook, so every confirmation and
|
|
23
|
+
every busy indicator in the application looks and behaves identically:
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
import { DialogComponents } from '@cratis/arc.react/dialogs';
|
|
27
|
+
import { BusyIndicatorDialog, ConfirmationDialog } from '@cratis/components/Dialogs';
|
|
28
|
+
|
|
29
|
+
export const App = () => (
|
|
30
|
+
<DialogComponents confirmation={ConfirmationDialog} busyIndicator={BusyIndicatorDialog}>
|
|
31
|
+
<YourApp />
|
|
32
|
+
</DialogComponents>
|
|
33
|
+
);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { DialogButtons, DialogResult, useConfirmationDialog, useBusyIndicator } from '@cratis/arc.react/dialogs';
|
|
38
|
+
|
|
39
|
+
const [confirm] = useConfirmationDialog();
|
|
40
|
+
const answer = await confirm('Delete this alert?', `"${alert.title}" disappears permanently.`, DialogButtons.YesNo);
|
|
41
|
+
if (answer !== DialogResult.Yes) return;
|
|
42
|
+
|
|
43
|
+
const [showBusy, closeBusy] = useBusyIndicator('Importing', 'This takes a moment.');
|
|
44
|
+
showBusy();
|
|
45
|
+
try { await doTheSlowThing(); } finally { closeBusy(); }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Rules:**
|
|
49
|
+
- Register both in **exactly one** place — the app root. A second registration, or a slice building its own confirm/busy dialog, is how two of them end up looking different.
|
|
50
|
+
- Reach them only via `useConfirmationDialog` / `useBusyIndicator`. Never hand-roll a Yes/No `Dialog`, and never use `window.confirm`.
|
|
51
|
+
- `showConfirm()` resolves to a `DialogResult` — branch on the enum member, never on button text.
|
|
52
|
+
- Always pair `showBusy()` with `closeBusy()` in a `finally`; the busy dialog is deliberately non-dismissible, so a missed close leaves the user stuck.
|
|
53
|
+
- **A busy indicator is a modal for work that blocks the user.** For a quick command behind a button, an in-flight/disabled button (eventual-consistency rule 9) is the better control — a modal that flashes for 200 ms is worse than no modal. Use the busy dialog when the user genuinely cannot proceed.
|
|
54
|
+
- From a view model, use the injectable `IDialogs` abstraction (`@cratis/arc.react.mvvm/dialogs`) rather than the hooks — see [react.md](./react.md).
|
|
55
|
+
|
|
56
|
+
## When Using `CommandDialog`
|
|
57
|
+
|
|
58
|
+
- Pass the command constructor to `command={}`. `CommandDialog` handles instantiation, execution, and confirm/cancel buttons.
|
|
59
|
+
- Use command form fields (`InputTextField`, `TextAreaField`, etc. from `@cratis/components/CommandForm`) for user-input values.
|
|
60
|
+
- `CommandDialog` automatically disables confirm while the command executes.
|
|
61
|
+
- Any value that must be present for the form to be considered valid (i.e. passes `validateRequiredProperties`) must be supplied via `initialValues`, **not** via `onBeforeExecute`.
|
|
62
|
+
- `onBeforeExecute` fires only at execution time — the command is already validated before it runs, so values set there never influence `isValid` and the OK/Submit button will remain permanently disabled.
|
|
63
|
+
- Use `initialValues` for injected context values (e.g. a parent entity id passed as a prop).
|
|
64
|
+
- Use `onBeforeExecute` only for transformations that should not affect form validity (e.g. generated IDs).
|
|
65
|
+
|
|
66
|
+
```tsx
|
|
67
|
+
import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs';
|
|
68
|
+
import { CommandDialog } from '@cratis/components/CommandDialog';
|
|
69
|
+
import { InputTextField } from '@cratis/components/CommandForm';
|
|
70
|
+
import { RegisterProject } from './Registration';
|
|
71
|
+
import { Guid } from '@cratis/fundamentals';
|
|
72
|
+
|
|
73
|
+
export const AddProject = ({ closeDialog }: DialogProps) => {
|
|
74
|
+
return (
|
|
75
|
+
<CommandDialog<RegisterProject>
|
|
76
|
+
command={RegisterProject}
|
|
77
|
+
title="Add Project"
|
|
78
|
+
okLabel="Add"
|
|
79
|
+
cancelLabel="Cancel"
|
|
80
|
+
onBeforeExecute={(values) => {
|
|
81
|
+
values.projectId = Guid.create(); // generated, not user input
|
|
82
|
+
return values;
|
|
83
|
+
}}>
|
|
84
|
+
<InputTextField<RegisterProject>
|
|
85
|
+
value={instance => instance.name}
|
|
86
|
+
title="Project name"
|
|
87
|
+
placeholder="My Project"
|
|
88
|
+
/>
|
|
89
|
+
</CommandDialog>
|
|
90
|
+
);
|
|
91
|
+
};
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
To await the result from the parent:
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
const [AddProjectDialog, showAddProjectDialog] = useDialog(AddProject);
|
|
98
|
+
|
|
99
|
+
const [result] = await showAddProjectDialog();
|
|
100
|
+
if (result === DialogResult.Ok) {
|
|
101
|
+
// Dialog confirmed and command executed successfully
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `onSuccess` vs `onConfirm`
|
|
106
|
+
|
|
107
|
+
- **`onSuccess(response)`** fires only after the command succeeds and receives the typed command response — use it for `closeDialog(DialogResult.Ok, response)`, refreshing a query, or a toast.
|
|
108
|
+
- **`onConfirm()`** receives **no** command result. It is a close gate after successful execution: return `true` to let the wrapper close, `false`/`undefined` to keep it open. **Do not use `onConfirm` as a command-result handler.**
|
|
109
|
+
|
|
110
|
+
### `onBeforeExecute` is a transformer
|
|
111
|
+
|
|
112
|
+
It receives the current command values and **must return them** (mutated or not). **Returning `void` executes the command with `undefined` values.** It runs only on submit — never use it to seed *required* values (validation runs against pre-transform state, so a value seeded here never makes the form valid and the submit button stays permanently disabled). Seed required values via `initialValues`; reserve `onBeforeExecute` for transforms that don't affect validity (e.g. a generated id).
|
|
113
|
+
|
|
114
|
+
### CommandForm fields
|
|
115
|
+
|
|
116
|
+
Use built-in `CommandForm` fields (from `@cratis/components/CommandForm`) for every user-input value — a raw PrimeReact control inside a command dialog bypasses `CommandFormFieldWrapper`, so validation never re-runs and the submit button stays **permanently disabled**. Catalog: `InputTextField`, `PasswordField`, `NumberField`, `DropdownField`, `CheckboxField`, `ToggleSwitchField`, `TextAreaField`, `CalendarField`, `RadioButtonField`, `RadioGroupField`, `ChipsField`, `MultiSelectField`, `ColorPickerField`, `SliderField`, `RatingField`.
|
|
117
|
+
|
|
118
|
+
- The `value={c => c.name}` **accessor lambda doubles as the binding and type-checked field selection** — renaming a command property surfaces a compile error at every binding.
|
|
119
|
+
- **`RadioGroupField<T>`** renders a whole group from data (`options`/`optionLabel`/`optionValue`, `layout='horizontal'|'vertical'`); **`RadioButtonField<T>`** is one component per option (each takes a `buttonValue`). Both infer the value type from the accessor — no `as string` casts.
|
|
120
|
+
- **`asCommandFormField(Component, opts)`** (`asCommandFormField`, `WrappedFieldProps` from `@cratis/arc.react/commands`) wraps a custom input so it participates in `CommandForm` like a built-in. `WrappedFieldProps<T>` gives `{ value, onChange, invalid, required, errors }` (`errors` is `string[]` → `errors.join(', ')`); options are `{ defaultValue, extractValue: e => ... }`.
|
|
121
|
+
- **`useCommandInstance(Command)`** (`@cratis/arc.react/commands`) returns the live reactive instance the form is bound to — **read** it to drive dependent fields (e.g. read `command.country` to choose a `DropdownField`'s options); never mutate (mutations go through field bindings).
|
|
122
|
+
|
|
123
|
+
### Opening dialogs — `useDialog` / `useDialogContext`
|
|
124
|
+
|
|
125
|
+
`useDialog<TResponse, TInput>(Component)` returns `[Wrapper, showFn]`: render `<Wrapper />` in JSX and call `showFn(input)` to open it; it resolves to `[DialogResult, TResponse?]` when the dialog closes. For a new dialog, prefer reading input as **plain typed props** (`<Name>Input`) and obtaining `closeDialog` from **`useDialogContext<TResponse>()`** — rather than declaring a props interface that extends `DialogProps` to thread both input and `closeDialog`. (Existing dialogs that destructure `closeDialog` from `DialogProps` remain valid.) Signal the outcome with `closeDialog(DialogResult.Ok | Cancelled, response?)`.
|
|
126
|
+
|
|
127
|
+
### Multi-step wizards — `StepperCommandDialog`
|
|
128
|
+
|
|
129
|
+
For a command split across named steps use `StepperCommandDialog` (`@cratis/components/CommandDialog`) — see the **stepper-command-dialog** skill. Conditional steps written as `{condition && <StepperPanel/>}` are supported **from 2.7.1**: only the steps that actually render are counted, so Next and Submit appear where the user expects them. ⚠️ On earlier versions a hidden step is still counted — Submit never appears on the real last step and a dead Next takes its place, so check what you are pinned to before relying on this. ⚠️ A `<>…</>` fragment wrapping several panels still counts as **one** step — give each step its own `StepperPanel` child. When a step needs non-CommandForm inputs, or cross-step state is complex, fall back to a manual `Dialog` + PrimeReact `Stepper`.
|
|
130
|
+
|
|
131
|
+
## When Using `Dialog`
|
|
132
|
+
|
|
133
|
+
Use this for dialogs that collect data and return it without executing a command (e.g. confirmation prompts, pure data-entry dialogs). `Dialog` defaults to OK + Cancel buttons. Use `isValid` to control confirm button state, `okLabel`/`cancelLabel` to customize button text.
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { useState } from 'react';
|
|
137
|
+
import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs';
|
|
138
|
+
import { Dialog } from '@cratis/components/Dialogs';
|
|
139
|
+
import { InputText } from 'primereact/inputtext';
|
|
140
|
+
|
|
141
|
+
export const AddProject = ({ closeDialog }: DialogProps<{ name: string }>) => {
|
|
142
|
+
const [name, setName] = useState('');
|
|
143
|
+
const isValid = name.trim().length > 0;
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Dialog
|
|
147
|
+
title="Add Project"
|
|
148
|
+
width='32rem'
|
|
149
|
+
isValid={isValid}
|
|
150
|
+
onConfirm={() => closeDialog(DialogResult.Ok, { name })}
|
|
151
|
+
onCancel={() => closeDialog(DialogResult.Cancelled)}
|
|
152
|
+
>
|
|
153
|
+
<InputText
|
|
154
|
+
value={name}
|
|
155
|
+
onChange={event => setName(event.target.value)}
|
|
156
|
+
autoFocus
|
|
157
|
+
/>
|
|
158
|
+
</Dialog>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## Prefer `DialogButtons` over Custom Button JSX
|
|
164
|
+
|
|
165
|
+
Use the built-in `DialogButtons` enum instead of rendering manual `<Button>` elements in the `buttons` prop:
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { DialogButtons, DialogResult, useDialogContext } from '@cratis/arc.react/dialogs';
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Available Button Sets
|
|
172
|
+
|
|
173
|
+
| `buttons` value | Shows |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `DialogButtons.OkCancel` | Ok + Cancel |
|
|
176
|
+
| `DialogButtons.YesNo` | Yes + No |
|
|
177
|
+
| `DialogButtons.YesNoCancel` | Yes + No + Cancel |
|
|
178
|
+
| `DialogButtons.Ok` | Ok only |
|
|
179
|
+
| `null` | No buttons (content-only dialog) |
|
|
180
|
+
|
|
181
|
+
## Customizing Built-in Buttons
|
|
182
|
+
|
|
183
|
+
Use `okLabel`/`cancelLabel` to rename the buttons, and `isValid` to disable the confirm button:
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
<Dialog
|
|
187
|
+
title="Import Orders"
|
|
188
|
+
visible={true}
|
|
189
|
+
buttons={DialogButtons.OkCancel}
|
|
190
|
+
okLabel="Upload"
|
|
191
|
+
isValid={!!file && !isUploading}
|
|
192
|
+
onConfirm={handleUpload}
|
|
193
|
+
onCancel={() => closeDialog(DialogResult.Cancelled)}
|
|
194
|
+
>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Validation Guard — Keep Dialog Open on Failure
|
|
198
|
+
|
|
199
|
+
When `onConfirm` needs to keep the dialog open (e.g. a command fails), annotate the handler as `Promise<boolean>` and return `false` to block the close. Return `true` to let the Dialog close itself:
|
|
200
|
+
|
|
201
|
+
```tsx
|
|
202
|
+
const handleConfirm = async (): Promise<boolean> => {
|
|
203
|
+
const result = await myCommand.execute();
|
|
204
|
+
if (!result.isSuccess) return false; // dialog stays open
|
|
205
|
+
return true; // dialog closes
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
> **TypeScript note:** Always annotate the function as `Promise<boolean>`. Without it TypeScript infers `Promise<false | void>` which does not satisfy the `ConfirmCallback` type.
|
|
210
|
+
|
|
211
|
+
## Passing Result Data on Confirm
|
|
212
|
+
|
|
213
|
+
When the dialog must return data to its caller (e.g. a postal code lookup result), use `onClose` and call `closeDialog` manually, returning `false` to prevent the Dialog from calling it a second time:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
<Dialog
|
|
217
|
+
title="Confirm Location"
|
|
218
|
+
visible={true}
|
|
219
|
+
buttons={DialogButtons.OkCancel}
|
|
220
|
+
isValid={isValid}
|
|
221
|
+
onClose={(result) => {
|
|
222
|
+
if (result === DialogResult.Ok) {
|
|
223
|
+
closeDialog(DialogResult.Ok, { postalCode, city, latitude, longitude } as MyResult);
|
|
224
|
+
return false; // prevent Dialog from calling closeDialog(Ok) again
|
|
225
|
+
}
|
|
226
|
+
// Cancelled: return undefined so the Dialog calls closeDialog(Cancelled)
|
|
227
|
+
}}
|
|
228
|
+
>
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
## Content-only Dialogs (No Action Buttons)
|
|
232
|
+
|
|
233
|
+
Use `buttons={null}` for dialogs that contain their own internal actions (e.g. a menu + data table) and don't need a confirm/cancel footer:
|
|
234
|
+
|
|
235
|
+
```tsx
|
|
236
|
+
<Dialog
|
|
237
|
+
title="Hubs"
|
|
238
|
+
visible={true}
|
|
239
|
+
width="50vw"
|
|
240
|
+
buttons={null}
|
|
241
|
+
onCancel={() => closeDialog(DialogResult.Cancelled)}
|
|
242
|
+
>
|
|
243
|
+
<Menubar model={menuItems} />
|
|
244
|
+
<Listing configurationId={configurationId} />
|
|
245
|
+
</Dialog>
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
## Props Reference
|
|
249
|
+
|
|
250
|
+
| Prop | Type | Notes |
|
|
251
|
+
|---|---|---|
|
|
252
|
+
| `title` | `string` | Header text (replaces PrimeReact `header`) |
|
|
253
|
+
| `visible` | `boolean` | Controls visibility |
|
|
254
|
+
| `buttons` | `DialogButtons \| ReactNode \| null` | Prefer `DialogButtons` enum; `null` for no footer |
|
|
255
|
+
| `isValid` | `boolean` | Disables the confirm button when `false` |
|
|
256
|
+
| `okLabel` | `string` | Override the Ok/Confirm button label |
|
|
257
|
+
| `cancelLabel` | `string` | Override the Cancel button label |
|
|
258
|
+
| `onConfirm` | `() => boolean \| void \| Promise<boolean> \| Promise<void>` | Called when Ok is clicked; return `false` to keep dialog open, `true` to close |
|
|
259
|
+
| `onCancel` | `() => void \| Promise<void>` | Called when Cancel is clicked |
|
|
260
|
+
| `onClose` | `(result: DialogResult) => boolean \| void \| Promise<...>` | Combined handler for both Ok and Cancel |
|
|
261
|
+
| `width` | `string` | Dialog width (e.g. `'50vw'`) — replaces PrimeReact `style={{ width }}` |
|
|
262
|
+
| `resizable` | `boolean` | Default `false` |
|
|
263
|
+
|
|
264
|
+
`style`, `contentStyle`, and `dismissable` **are** supported. The other v10 PrimeReact Dialog props (`modal`, `dismissableMask`, `draggable`, `footer`, `onHide`) are **not** available — do not use them. (`resizable` is accepted for compatibility but is a no-op in PrimeReact 11.)
|