@cratis/pi 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +43 -0
  2. package/package.json +24 -0
  3. package/skills/cratis-application-react-specifications/LICENSE +2 -0
  4. package/skills/cratis-application-react-specifications/SKILL.md +192 -0
  5. package/skills/cratis-application-slice-specifications/LICENSE +2 -0
  6. package/skills/cratis-application-slice-specifications/SKILL.md +222 -0
  7. package/skills/cratis-arc-authentication-authorization-and-identity/LICENSE +2 -0
  8. package/skills/cratis-arc-authentication-authorization-and-identity/SKILL.md +233 -0
  9. package/skills/cratis-arc-authentication-authorization-and-identity/references/authentication.md +140 -0
  10. package/skills/cratis-arc-authentication-authorization-and-identity/references/authorization.md +112 -0
  11. package/skills/cratis-arc-authentication-authorization-and-identity/references/frontend.md +102 -0
  12. package/skills/cratis-arc-authentication-authorization-and-identity/references/local-development.md +85 -0
  13. package/skills/cratis-arc-authentication-authorization-and-identity/references/tenancy.md +87 -0
  14. package/skills/cratis-arc-command/LICENSE +2 -0
  15. package/skills/cratis-arc-command/SKILL.md +286 -0
  16. package/skills/cratis-arc-command/references/command-result.md +111 -0
  17. package/skills/cratis-arc-command/references/handler-shapes.md +135 -0
  18. package/skills/cratis-arc-command/references/proxy-generation.md +87 -0
  19. package/skills/cratis-arc-command/references/read-model-injection.md +97 -0
  20. package/skills/cratis-arc-command-execution/LICENSE +2 -0
  21. package/skills/cratis-arc-command-execution/SKILL.md +148 -0
  22. package/skills/cratis-arc-command-validation/LICENSE +2 -0
  23. package/skills/cratis-arc-command-validation/SKILL.md +230 -0
  24. package/skills/cratis-arc-ef-core-migration/LICENSE +2 -0
  25. package/skills/cratis-arc-ef-core-migration/SKILL.md +231 -0
  26. package/skills/cratis-arc-observable-query-http/LICENSE +2 -0
  27. package/skills/cratis-arc-observable-query-http/SKILL.md +157 -0
  28. package/skills/cratis-arc-query-paging/LICENSE +2 -0
  29. package/skills/cratis-arc-query-paging/SKILL.md +173 -0
  30. package/skills/cratis-arc-react-page/LICENSE +2 -0
  31. package/skills/cratis-arc-react-page/SKILL.md +291 -0
  32. package/skills/cratis-arc-react-page/references/data-page.md +125 -0
  33. package/skills/cratis-arc-react-page/references/data-tables.md +136 -0
  34. package/skills/cratis-arc-react-page/references/dialogs.md +238 -0
  35. package/skills/cratis-arc-react-page/references/mvvm.md +150 -0
  36. package/skills/cratis-arc-react-page/references/queries-and-commands.md +200 -0
  37. package/skills/cratis-chronicle-cli-operations/LICENSE +2 -0
  38. package/skills/cratis-chronicle-cli-operations/SKILL.md +199 -0
  39. package/skills/cratis-chronicle-client-dotnet/LICENSE +2 -0
  40. package/skills/cratis-chronicle-client-dotnet/SKILL.md +433 -0
  41. package/skills/cratis-chronicle-client-elixir/LICENSE +2 -0
  42. package/skills/cratis-chronicle-client-elixir/SKILL.md +429 -0
  43. package/skills/cratis-chronicle-client-kotlin/LICENSE +2 -0
  44. package/skills/cratis-chronicle-client-kotlin/SKILL.md +498 -0
  45. package/skills/cratis-chronicle-client-typescript/LICENSE +2 -0
  46. package/skills/cratis-chronicle-client-typescript/SKILL.md +396 -0
  47. package/skills/cratis-chronicle-compliance/LICENSE +2 -0
  48. package/skills/cratis-chronicle-compliance/SKILL.md +246 -0
  49. package/skills/cratis-chronicle-event-constraints/LICENSE +2 -0
  50. package/skills/cratis-chronicle-event-constraints/SKILL.md +227 -0
  51. package/skills/cratis-chronicle-event-modeling/LICENSE +2 -0
  52. package/skills/cratis-chronicle-event-modeling/SKILL.md +157 -0
  53. package/skills/cratis-chronicle-event-specifications/LICENSE +2 -0
  54. package/skills/cratis-chronicle-event-specifications/SKILL.md +200 -0
  55. package/skills/cratis-chronicle-event-type-migration/LICENSE +2 -0
  56. package/skills/cratis-chronicle-event-type-migration/SKILL.md +235 -0
  57. package/skills/cratis-chronicle-mcp-inspection/LICENSE +2 -0
  58. package/skills/cratis-chronicle-mcp-inspection/SKILL.md +79 -0
  59. package/skills/cratis-chronicle-mcp-inspection/references/blocked-tools.md +12 -0
  60. package/skills/cratis-chronicle-mcp-inspection/references/observational-tools.md +8 -0
  61. package/skills/cratis-chronicle-multi-tenancy/LICENSE +2 -0
  62. package/skills/cratis-chronicle-multi-tenancy/SKILL.md +198 -0
  63. package/skills/cratis-chronicle-projection/LICENSE +2 -0
  64. package/skills/cratis-chronicle-projection/SKILL.md +162 -0
  65. package/skills/cratis-chronicle-projection/references/fluent-builder.md +126 -0
  66. package/skills/cratis-chronicle-projection/references/model-bound-attributes.md +118 -0
  67. package/skills/cratis-chronicle-reactor/LICENSE +2 -0
  68. package/skills/cratis-chronicle-reactor/SKILL.md +344 -0
  69. package/skills/cratis-chronicle-read-model/LICENSE +2 -0
  70. package/skills/cratis-chronicle-read-model/SKILL.md +193 -0
  71. package/skills/cratis-chronicle-read-model/references/queries.md +142 -0
  72. package/skills/cratis-chronicle-read-model-specifications/LICENSE +2 -0
  73. package/skills/cratis-chronicle-read-model-specifications/SKILL.md +176 -0
  74. package/skills/cratis-chronicle-reducer/LICENSE +2 -0
  75. package/skills/cratis-chronicle-reducer/SKILL.md +173 -0
  76. package/skills/cratis-chronicle-web-workbench/LICENSE +2 -0
  77. package/skills/cratis-chronicle-web-workbench/SKILL.md +177 -0
  78. package/skills/cratis-cli-terminal-workbench/LICENSE +2 -0
  79. package/skills/cratis-cli-terminal-workbench/SKILL.md +156 -0
  80. package/skills/cratis-code-review/LICENSE +2 -0
  81. package/skills/cratis-code-review/SKILL.md +177 -0
  82. package/skills/cratis-components-accessibility/LICENSE +2 -0
  83. package/skills/cratis-components-accessibility/SKILL.md +176 -0
  84. package/skills/cratis-components-schema-editor/LICENSE +2 -0
  85. package/skills/cratis-components-schema-editor/SKILL.md +238 -0
  86. package/skills/cratis-components-stepper-command-dialog/LICENSE +2 -0
  87. package/skills/cratis-components-stepper-command-dialog/SKILL.md +209 -0
  88. package/skills/cratis-components-styling/LICENSE +2 -0
  89. package/skills/cratis-components-styling/SKILL.md +221 -0
  90. package/skills/cratis-components-toolbar/LICENSE +2 -0
  91. package/skills/cratis-components-toolbar/SKILL.md +259 -0
  92. package/skills/cratis-documentation-writing/LICENSE +2 -0
  93. package/skills/cratis-documentation-writing/SKILL.md +121 -0
  94. package/skills/cratis-event-model-diagram/LICENSE +2 -0
  95. package/skills/cratis-event-model-diagram/SKILL.md +142 -0
  96. package/skills/cratis-fundamentals-concept/LICENSE +2 -0
  97. package/skills/cratis-fundamentals-concept/SKILL.md +198 -0
  98. package/skills/cratis-fundamentals-type-discovery/LICENSE +2 -0
  99. package/skills/cratis-fundamentals-type-discovery/SKILL.md +152 -0
  100. package/skills/cratis-governed-release-methodology/LICENSE +2 -0
  101. package/skills/cratis-governed-release-methodology/SKILL.md +297 -0
  102. package/skills/cratis-lens-browser-extension/LICENSE +2 -0
  103. package/skills/cratis-lens-browser-extension/SKILL.md +239 -0
  104. package/skills/cratis-performance-review/LICENSE +2 -0
  105. package/skills/cratis-performance-review/SKILL.md +139 -0
  106. package/skills/cratis-screenplay-model-authoring/LICENSE +2 -0
  107. package/skills/cratis-screenplay-model-authoring/SKILL.md +231 -0
  108. package/skills/cratis-security-review/LICENSE +2 -0
  109. package/skills/cratis-security-review/SKILL.md +141 -0
  110. package/skills/cratis-specification-by-example/LICENSE +2 -0
  111. package/skills/cratis-specification-by-example/SKILL.md +188 -0
  112. package/skills/cratis-specifications-csharp/LICENSE +2 -0
  113. package/skills/cratis-specifications-csharp/SKILL.md +230 -0
  114. package/skills/cratis-specifications-csharp/references/application-scenarios.md +191 -0
  115. package/skills/cratis-specifications-csharp/references/csharp-patterns.md +142 -0
  116. package/skills/cratis-specifications-csharp/references/integration-specs.md +126 -0
  117. package/skills/cratis-specifications-typescript/LICENSE +2 -0
  118. package/skills/cratis-specifications-typescript/SKILL.md +202 -0
  119. package/skills/cratis-specifications-typescript/references/typescript-patterns.md +168 -0
  120. package/skills/cratis-stage-rendering-and-sandbox/LICENSE +2 -0
  121. package/skills/cratis-stage-rendering-and-sandbox/SKILL.md +230 -0
  122. package/skills/cratis-studio-mcp-safety-guidance/LICENSE +2 -0
  123. package/skills/cratis-studio-mcp-safety-guidance/SKILL.md +69 -0
  124. package/skills/cratis-studio-mcp-safety-guidance/references/blocked-tools.md +12 -0
  125. package/skills/cratis-studio-mcp-safety-guidance/references/observational-tools.md +8 -0
@@ -0,0 +1,87 @@
1
+ # Tenancy
2
+
3
+ Verified against `Cratis.Arc.Core` `22.10.4`. Types are in `Cratis.Arc.Tenancy`.
4
+
5
+ Arc tenancy is **request-scoped tenant resolution**. It answers "which tenant is
6
+ this request for". It is not Chronicle's tenant namespace isolation, and it does
7
+ not by itself scope any storage.
8
+
9
+ ## Constants
10
+
11
+ ```csharp
12
+ public const string DefaultTenantIdHeader = "x-cratis-tenant-id";
13
+ public const string TenantIdItemKey = "TenantId";
14
+ public const string DefaultFixedTenantId = "development";
15
+ ```
16
+
17
+ ## Options and defaults
18
+
19
+ `ArcOptions.Tenancy` is a `TenancyOptions`:
20
+
21
+ | Property | Default |
22
+ | --- | --- |
23
+ | `ResolverType` | `TenantResolverType.Header` |
24
+ | `HttpHeader` | `x-cratis-tenant-id` |
25
+ | `BaseDomain` | empty |
26
+ | `QueryParameter` | `tenantId` |
27
+ | `ClaimType` | `tenant_id` |
28
+ | `FixedTenantId` | `development` |
29
+ | `DevelopmentTenantId` | an alias property over `FixedTenantId` |
30
+
31
+ `TenantResolverType` is `Header = 0`, `Query = 1`, `Claim = 2`,
32
+ `Development = 3`, `Subdomain = 4`, `Fixed = 5`. **Header is the default.**
33
+
34
+ Configure through the extension methods on `ArcOptions`: `UseHeaderTenancy`,
35
+ `UseQueryTenancy`, `UseClaimTenancy`, `UseDevelopmentTenancy`, `UseFixedTenancy`,
36
+ and `UseSubdomainTenancy(baseDomain, fallbackHeaderName?)`. Options are validated
37
+ by `TenancyOptionsValidator`.
38
+
39
+ ## The resolvers
40
+
41
+ | Resolver | Reads |
42
+ | --- | --- |
43
+ | `HeaderTenantIdResolver` | `Tenancy.HttpHeader` |
44
+ | `QueryTenantIdResolver` | `Tenancy.QueryParameter` |
45
+ | `ClaimTenantIdResolver` | the first claim of `Tenancy.ClaimType` |
46
+ | `SubdomainTenantIdResolver` | the host label in front of `BaseDomain` |
47
+ | `FixedTenantIdResolver` | `Tenancy.FixedTenantId` |
48
+ | `DevelopmentTenantIdResolver` | deprecated alias over `FixedTenantIdResolver` |
49
+
50
+ ⚠️ `SubdomainTenantIdResolver` **falls back to the configured HTTP header** when
51
+ the host does not match. A request that misses the subdomain can therefore still
52
+ name its own tenant through a header it controls. Clear `Tenancy.HttpHeader` if
53
+ that fallback is not wanted.
54
+
55
+ Its suffix is built once from the base domain at construction and never rebuilt.
56
+ Writing `BaseDomain` after construction does not take effect — and would silently
57
+ send every request down the header fallback. `BaseDomainIsNotADomainName` guards
58
+ the configured value.
59
+
60
+ ## Reading the tenant
61
+
62
+ `ITenantIdAccessor.Current` gives the current `TenantId`, cached into an
63
+ `AsyncLocal`. An empty resolution is `TenantId.NotSet`, whose value is the
64
+ literal string `"[NotSet]"`. `TenantId.Default` is `"Default"` and `IsDefault`
65
+ tests for it. `CurrentTenantIdIsNotSet.ThrowIfNotSet(...)` is the guard for code
66
+ that requires a tenant.
67
+
68
+ ⚠️ `NotSet` is a real string, not `null`. A check like
69
+ `string.IsNullOrEmpty(tenantId)` never fires for an unresolved tenant. Compare
70
+ against `TenantId.NotSet`.
71
+
72
+ ## Development listing
73
+
74
+ `ICanProvideTenants` (`Task<IEnumerable<Tenant>> Provide()`) feeds
75
+ `GET /.cratis/tenants`, where `Tenant` is `record (TenantId Id, TenantName Name)`.
76
+ The endpoint is `AllowAnonymous` and mapped unconditionally, so an implementation
77
+ that returns real tenants publishes them. Return development fixtures only.
78
+
79
+ ## Where the boundary is
80
+
81
+ Tenant resolution reads a value the **caller** supplied — a header, a query
82
+ parameter, a claim, or a host. Only the claim resolver reads something the
83
+ authentication step produced. The other four are caller-controlled input.
84
+
85
+ Anything that must be isolated per tenant has to enforce that isolation itself,
86
+ downstream of resolution. Never treat a resolved tenant id as an authorization
87
+ decision.
@@ -0,0 +1,2 @@
1
+ Copyright (c) Cratis. All rights reserved.
2
+ Licensed under the MIT license. See LICENSE file in the project root for full license information.
@@ -0,0 +1,286 @@
1
+ ---
2
+ name: cratis-arc-command
3
+ description: Define a Cratis Arc command — the [Command] record, its Handle() method and return shape, the optional Provide() step, and the generated TypeScript proxy. Use when adding a command, choosing what Handle() should return, deciding which values may reach the causation chain, or wiring a form or button to an Arc backend. Do not use for a validation-only change or merely to execute an existing command.
4
+ license: MIT
5
+ ---
6
+
7
+ # Define an Arc command
8
+
9
+ A command is a record that carries the user's intent and owns its own handler.
10
+ Arc discovers it, runs authorization and validation, calls `Handle()`, and turns
11
+ whatever `Handle()` returns into appended events, a response, or both.
12
+
13
+ ## Verified product sources
14
+
15
+ | Package | Version | Purpose |
16
+ | --- | --- | --- |
17
+ | `Cratis.Arc.Core` | `22.10.4` | `Cratis.Arc.Commands.ModelBound.CommandAttribute`, `CommandResult`, `CommandValidator<T>` |
18
+ | `Cratis.Arc.Chronicle` | `22.10.4` | event append handling, `Cratis.Arc.Chronicle.Commands.NotAuditedAttribute` |
19
+ | `Cratis.Arc.ProxyGenerator.Build` | `22.10.4` | `CratisProxiesOutputPath` MSBuild integration |
20
+ | `Cratis.Fundamentals` | `7.18.2` | `Cratis.Monads.Result<TResult, TError>` |
21
+ | `Cratis.Chronicle` | `16.39.1` | `EventTypeAttribute`, `EventForEventSourceId`, `ICanProvideEventSourceId` |
22
+ | `@cratis/arc` | `22.10.4` | `ICommand`, `CommandResult`, `ValidationResult` |
23
+
24
+ Reverify before claiming support for another version. Arc without Chronicle is a
25
+ supported setup; everything on this page that appends events needs Chronicle.
26
+
27
+ ## Declare the command
28
+
29
+ ```csharp
30
+ // Copyright (c) Cratis. All rights reserved.
31
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
32
+
33
+ using Cratis.Arc.Commands.ModelBound;
34
+ using Cratis.Chronicle.Events;
35
+
36
+ namespace <NamespaceRoot>.<Feature>;
37
+
38
+ [Command]
39
+ public record <ImperativeName>(<ConceptType> <Name>, <ConceptType> <Name>)
40
+ {
41
+ public <EventType> Handle() => new(<Name>);
42
+ }
43
+
44
+ [EventType]
45
+ public record <EventType>(<ConceptType> <Name>);
46
+ ```
47
+
48
+ Rules the framework actually enforces:
49
+
50
+ - `[Command]` is `Cratis.Arc.Commands.ModelBound.CommandAttribute` and applies to
51
+ a class declaration only. A type counts as a command when it carries
52
+ `[Command]` **and** declares a `Handle` method; either one alone is not enough.
53
+ - `Handle` is found by name on the command type itself. `ARC0003` is an error
54
+ when another class defines `Handle` for a command, and `ARC0004` is an error
55
+ when a `[Command]` type has no public instance `Handle`.
56
+ - `ARC0002` warns when a type has `Handle` but no `[Command]`; `ARC0007` warns
57
+ when a command is a `class` rather than a `record`.
58
+ - Name the command as the action — `OpenDebitAccount`, not
59
+ `OpenDebitAccountCommand`.
60
+ - `Handle` parameters are resolved from DI, so ask for services there rather
61
+ than through a constructor.
62
+ - Never inject `IEventLog` into `Handle` to append the command's own events —
63
+ `ARCCHR0007` warns, and it bypasses Arc's append pipeline. Express the append
64
+ through the return type.
65
+
66
+ Use concept types rather than raw primitives for the command's values, and give
67
+ Chronicle-backed identities the `EventSourceId<T>` base — see the
68
+ `cratis-fundamentals-concept` skill for the exact shapes.
69
+
70
+ ## Choose the return shape
71
+
72
+ `Handle()`'s return value is dispatched by its runtime type. The shape decides
73
+ what is appended and what the caller gets back.
74
+
75
+ | Return | What Arc does |
76
+ | --- | --- |
77
+ | A registered event record | Appends it to the command's event source id |
78
+ | `IEnumerable<object>` of registered events | Appends each to the command's event source id |
79
+ | `EventForEventSourceId(id, @event)` | Appends that one event to `id` |
80
+ | A collection mixing events and `EventForEventSourceId` | Appends each to its own target, wrappers to their id and plain events to the command's |
81
+ | A tuple | Each element is dispatched on its own; the one element nothing can handle becomes the response |
82
+ | Anything else | Becomes the response payload |
83
+ | `Result<TEvent, ValidationResult>` | Success appends the event; failure becomes a validation failure |
84
+ | `Task<T>` / `ValueTask<T>` of any of the above | Awaited first, then dispatched |
85
+
86
+ See [handler shapes](references/handler-shapes.md) for the dispatch order, the
87
+ tuple rule, and the exact failure modes.
88
+
89
+ Two consequences worth knowing before writing the first command:
90
+
91
+ - **The tuple is how a create command returns its new id.** An
92
+ `EventSourceId`-derived value in the tuple is not appendable, so it becomes the
93
+ response — and the response, when it is an event-source id value, is also what
94
+ the events in that same tuple are appended to. Returning more than one
95
+ unhandleable element throws `MultipleUnhandledTupleValues`.
96
+ - **Events never carry their own event source.** Cross-stream writes use
97
+ `EventForEventSourceId`; they are not expressed by a property on the event.
98
+
99
+ ```csharp
100
+ [Command]
101
+ public record Register<Thing>(<ConceptType> <Name>)
102
+ {
103
+ public (<ThingId>, <ThingRegistered>) Handle()
104
+ {
105
+ var id = <ThingId>.New();
106
+ return (id, new(<Name>));
107
+ }
108
+ }
109
+ ```
110
+
111
+ ```csharp
112
+ [Command]
113
+ public record Transfer(<AccountId> From, <AccountId> To, <Money> Amount)
114
+ {
115
+ public IEnumerable<object> Handle() =>
116
+ [
117
+ new EventForEventSourceId(From, new <Withdrawn>(Amount)),
118
+ new EventForEventSourceId(To, new <Deposited>(Amount)),
119
+ ];
120
+ }
121
+ ```
122
+
123
+ ## Where the event source id comes from
124
+
125
+ Chronicle resolves the command's event source id in this order:
126
+
127
+ 1. `commandContext.Response`, when the response is an event-source-id value —
128
+ the tuple case above;
129
+ 2. `ICanProvideEventSourceId.GetEventSourceId()` on the command;
130
+ 3. a single `EventSourceId`/`EventSourceId<T>`-derived or `[Key]`-marked
131
+ property on the command;
132
+ 4. otherwise a freshly generated id.
133
+
134
+ A command with more than one candidate property is ambiguous and `ARCCHR0002`
135
+ warns; implement `ICanProvideEventSourceId` to say which value wins. Do not rely
136
+ on property order.
137
+
138
+ Two attributes are spelled `[Key]`. Chronicle reads `Cratis.Chronicle.Keys.KeyAttribute`.
139
+ Marking `System.ComponentModel.DataAnnotations.KeyAttribute` in an application
140
+ that uses Chronicle compiles, resolves nothing, and silently invents a new event
141
+ source id for every command — `ARCCHR0008` reports it.
142
+
143
+ ## Fetch what the handler needs with `Provide()`
144
+
145
+ `Provide` is an optional instance method that runs before `Handle`. Its purpose
146
+ is to keep IO out of `Handle`: it fetches or computes values, and Arc binds them
147
+ to `Handle`'s parameters by type.
148
+
149
+ ```csharp
150
+ [Command]
151
+ public record Open<Thing>(<ThingId> Id, <OwnerId> OwnerId)
152
+ {
153
+ public async Task<Result<<Owner>, ValidationResult>> Provide(IReadModels readModels)
154
+ {
155
+ var owner = await readModels.GetInstanceById<<Owner>>((EventSourceId)OwnerId);
156
+ return owner is null
157
+ ? ValidationResult.Error("Owner must exist.")
158
+ : owner;
159
+ }
160
+
161
+ public <ThingOpened> Handle(<Owner> owner) => new(owner.Id);
162
+ }
163
+ ```
164
+
165
+ - Short-circuit with `ValidationResult.Error(...)`; do not throw for a rejection
166
+ the user can act on. A thrown exception surfaces as `HasExceptions` and HTTP
167
+ 500, not as a validation failure.
168
+ - Every value `Provide` returns must be consumed by a `Handle` parameter.
169
+ `ARC0005` warns otherwise. `ValidationResult`, `AuthorizationResult` and
170
+ `CommandResult` are exempt because they short-circuit rather than feed `Handle`.
171
+ - `Provide` runs after authorization and validation.
172
+
173
+ An Arc read model can also be injected directly into `Provide`, `Handle`, or a
174
+ `CommandValidator<T>` — but only by **the command's own resolved key**. Reading a
175
+ read model keyed by anything else needs an explicit by-id read, and an absent
176
+ instance does not always arrive as `null`. Read
177
+ [read-model injection](references/read-model-injection.md) before relying on a
178
+ directly injected read model.
179
+
180
+ ## Decide what the causation chain may record
181
+
182
+ Every property value of the command is written to the causation of every event
183
+ the command appends, and the event log is immutable. Decide this when the
184
+ property is added.
185
+
186
+ | Marking | Use for | Effect |
187
+ | --- | --- | --- |
188
+ | `[PII]` (Chronicle) | personal data | encrypted in the event and enrolled in erasure; already withheld from causation |
189
+ | `[NotAudited]` (Arc Chronicle) | a secret that is not personal data — password, token, API key | withheld from causation, nothing else |
190
+
191
+ ```csharp
192
+ [Command]
193
+ public record Change<Secret>(
194
+ <UserId> User,
195
+ [property: NotAudited] string Old<Secret>,
196
+ [property: NotAudited] string New<Secret>)
197
+ {
198
+ public <SecretChanged> Handle(I<Hasher> hasher) => new(hasher.Hash(New<Secret>));
199
+ }
200
+ ```
201
+
202
+ `[NotAudited]` applies to a class, struct, property, or parameter. On the type it
203
+ withholds every property at once. Marking the concept type instead makes it
204
+ travel to every command that takes that value. The command is still named on the
205
+ chain either way; only the values are withheld.
206
+
207
+ `ARCCHR0009` warns when a property's *name* reads like a secret and is unmarked.
208
+ It cannot see a secret whose name does not say so, so a clean build means
209
+ "nothing obvious was missed", not "no secrets are recorded". When the name only
210
+ reads like a secret and the value is safe to record, the framework's own guidance
211
+ is to mark it `[NotAudited]` anyway or rename the property — the value is written
212
+ either way, so the reading is all a reviewer has to go on.
213
+
214
+ ## Generate the TypeScript proxy
215
+
216
+ ```xml
217
+ <PackageReference Include="Cratis.Arc.ProxyGenerator.Build" Version="22.10.4" />
218
+
219
+ <PropertyGroup>
220
+ <CratisProxiesOutputPath>$(MSBuildThisFileDirectory)../<Web>/src/api</CratisProxiesOutputPath>
221
+ </PropertyGroup>
222
+ ```
223
+
224
+ `dotnet build` runs the generator after the build, and only when
225
+ `CratisProxiesOutputPath` is set. Output folders mirror the C# namespace, not the
226
+ file path. See [proxy generation](references/proxy-generation.md) for the full
227
+ set of MSBuild knobs and the common failures.
228
+
229
+ ## The generated client contract
230
+
231
+ The generated proxy is a `Command` from `@cratis/arc`. Its members are the same
232
+ whatever renders it:
233
+
234
+ | Member | What it does |
235
+ | --- | --- |
236
+ | `route` | The generated route, including the configured API prefix |
237
+ | `roles` | The roles the command declares; empty when it declares none |
238
+ | `<property>` | Get or set one value; setting raises `propertyChanged` |
239
+ | `hasChanges` | True when any value differs from the initial values |
240
+ | `execute(allowedSeverity?, ignoreWarnings?)` | Sends the command; resolves to `CommandResult` |
241
+ | `validate()` | Runs authorization and validation on the server without the handler |
242
+ | `validateClientSide()` | Runs only the extracted rules locally; never touches the network |
243
+ | `setInitialValues(values)` | Sets the change-tracking baseline |
244
+ | `setInitialValuesFromCurrentValues()` | Rebaselines onto the current values |
245
+ | `revertChanges()` / `clear()` | Restore the baseline / reset everything |
246
+
247
+ Branch on the specific flag, not only on `isSuccess`. The exact `CommandResult`
248
+ and `ValidationResult` shapes are in
249
+ [command result](references/command-result.md) — in particular, a validation
250
+ failure carries `members: string[]` (camelCased) and a numeric `severity`, not a
251
+ `propertyName` string.
252
+
253
+ `validateClientSide()` runs only the rules the generator could extract, so it can
254
+ pass where `execute()` still fails validation — see
255
+ [proxy generation](references/proxy-generation.md) for the exact extractable set.
256
+
257
+ Binding this proxy into a React component — the generated `use()` hook and the
258
+ Cratis Components command dialog and form fields — belongs to the Arc React and
259
+ Components guidance, not to this skill.
260
+
261
+ ## Route near misses
262
+
263
+ - Adding or changing a rule on an existing command: the Arc command validation
264
+ guidance.
265
+ - Executing an existing command from backend code: `cratis-arc-command-execution`.
266
+ - Append-time uniqueness or concurrency constraints: the Chronicle event
267
+ constraints guidance.
268
+ - Choosing the concept or identity type for a value: `cratis-fundamentals-concept`.
269
+
270
+ ## Verify
271
+
272
+ - The command is a `record`, carries `[Command]`, and declares a public instance
273
+ `Handle`.
274
+ - The return shape matches what the command is supposed to do, and any
275
+ cross-stream event is wrapped in `EventForEventSourceId`.
276
+ - At most one tuple element is unhandleable.
277
+ - The event source id resolves from exactly one place; `ARCCHR0002` is silent.
278
+ - `[Key]`, where used, is `Cratis.Chronicle.Keys.KeyAttribute`.
279
+ - No `IEventLog` is injected into `Handle`.
280
+ - Every value `Provide` returns is consumed by `Handle`.
281
+ - Rejections are validation results, never thrown exceptions.
282
+ - Every secret or personal value is marked before it can reach the event log.
283
+ - `dotnet build` is clean in Debug and Release, with no suppressed `ARC*` or
284
+ `ARCCHR*` diagnostic left unjustified.
285
+ - The generated proxy exists at the configured output path and the frontend
286
+ compiles against it.
@@ -0,0 +1,111 @@
1
+ # CommandResult and ValidationResult
2
+
3
+ Verified against `Cratis.Arc.Core` `22.10.4` and `@cratis/arc` `22.10.4`.
4
+
5
+ ## Server side
6
+
7
+ `Cratis.Arc.Commands.CommandResult`:
8
+
9
+ | Member | Type | Notes |
10
+ | --- | --- | --- |
11
+ | `CorrelationId` | `CorrelationId` | Defaults to `CorrelationId.NotSet` |
12
+ | `IsSuccess` | `bool` | Computed: `IsAuthorized && IsValid && !HasExceptions` |
13
+ | `IsAuthorized` | `bool` | Settable; defaults to `true` |
14
+ | `IsValid` | `bool` | Computed: `!ValidationResults.Any()` |
15
+ | `HasExceptions` | `bool` | Computed: `ExceptionMessages.Any()` |
16
+ | `ValidationResults` | `IEnumerable<ValidationResult>` | |
17
+ | `ExceptionMessages` | `IEnumerable<string>` | |
18
+ | `ExceptionStackTrace` | `string` | |
19
+ | `AuthorizationFailureReason` | `string` | |
20
+
21
+ `CommandResult<TResult>` adds a `Response` of `TResult`.
22
+
23
+ `IsValid` and `HasExceptions` are **derived**, not stored. There is no way to
24
+ set them directly — a result is invalid because it carries validation results.
25
+
26
+ Factories: `Success`, `Unauthorized`, `MissingHandler`, `Error`, `InvalidBody`,
27
+ `FromException`.
28
+
29
+ ⚠️ **A failed result carries no response.** The pipeline clears the response once
30
+ every execution scope has completed and the result is not successful, so a
31
+ transaction that rolls back after `Handle` returned a value does not leak that
32
+ value into the error body.
33
+
34
+ ## Severity filtering
35
+
36
+ `ValidationResultSeverity`: `Unknown = 0`, `Information = 1`, `Warning = 2`,
37
+ `Error = 3`.
38
+
39
+ Only *blocking* results survive onto the result. With no `allowedSeverity`, only
40
+ `Error` blocks — information and warnings are filtered out entirely. With an
41
+ `allowedSeverity`, anything strictly greater than it blocks. So passing
42
+ `Warning` keeps errors blocking and lets warnings through.
43
+
44
+ ## Client side
45
+
46
+ `@cratis/arc` `ICommandResult<TResponse = object>`:
47
+
48
+ ```ts
49
+ readonly correlationId: Guid;
50
+ readonly isSuccess: boolean;
51
+ readonly isAuthorized: boolean;
52
+ readonly isValid: boolean;
53
+ readonly hasExceptions: boolean;
54
+ readonly validationResults: ValidationResult[];
55
+ readonly exceptionMessages: string[];
56
+ readonly authorizationFailureReason: string;
57
+ readonly exceptionStackTrace: string;
58
+ readonly response?: TResponse;
59
+ ```
60
+
61
+ `ValidationResult` is a class with these readonly members:
62
+
63
+ ```ts
64
+ severity: ValidationResultSeverity // numeric enum, 0..3
65
+ message: string
66
+ members: string[]
67
+ state: any
68
+ reason: ValidationResultReason // defaults to Rule
69
+ reasonDetail?: string
70
+ ```
71
+
72
+ ⚠️ **There is no `propertyName` and no string severity.** A rule attributes its
73
+ failure to one or more `members`, and `severity` is the numeric
74
+ `ValidationResultSeverity` enum. Code that reads `v.propertyName` or compares
75
+ `v.severity === 'Error'` is reading fields that do not exist.
76
+
77
+ Match a failure to a field through `members`, and branch on `reasonDetail`
78
+ rather than parsing `message` — the message is prose written for a human and is
79
+ free to change, while `reasonDetail` is the identity of the specific thing that
80
+ rejected the command (a constraint name, for instance).
81
+
82
+ ```tsx
83
+ const result = await command.execute();
84
+
85
+ if (!result.isAuthorized) { /* the caller is not allowed to do this */ }
86
+ else if (!result.isValid) {
87
+ const errorFor = (property: string) =>
88
+ result.validationResults.find(v => v.members.includes(property))?.message;
89
+ }
90
+ else if (result.hasExceptions) { /* server fault */ }
91
+ else { /* result.response */ }
92
+ ```
93
+
94
+ `members` is **camelCased on purpose**, so a form can match a server failure to
95
+ the field that caused it without transforming anything. A nested member is a
96
+ dotted path from the graph root — `address.postalCode`. A failure raised by a
97
+ `ConceptValidator<T>` is attributed to the field holding the concept, not to the
98
+ concept's inner `Value`, because TypeScript erases the concept to its primitive
99
+ and names the field the same way.
100
+
101
+ `reason` also distinguishes a genuine rejection from a validator that threw
102
+ while validating hostile or partial input: the latter arrives as a single result
103
+ with a `ValidatorFailed` reason and a generic message, replacing every message
104
+ the validator's author wrote. Do not read the message to tell the two apart.
105
+
106
+ ## Executing with a severity
107
+
108
+ `command.execute(allowedSeverity?, ignoreWarnings?)` mirrors the server-side
109
+ filtering. `command.validate()` runs authorization and validation on the server
110
+ without the handler; `command.validateClientSide()` runs only the extracted
111
+ rules locally and never touches the network.
@@ -0,0 +1,135 @@
1
+ # Handler shapes and how Arc dispatches them
2
+
3
+ Verified against `Cratis.Arc.Core` and `Cratis.Arc.Chronicle` `22.10.4` with
4
+ `Cratis.Chronicle` `16.39.1`.
5
+
6
+ ## The pipeline around `Handle`
7
+
8
+ One command execution runs in this order:
9
+
10
+ 1. resolve the handler — no handler produces a `MissingHandler` result;
11
+ 2. build the command context, including the resolved event source id;
12
+ 3. begin every `ICommandExecutionScope`;
13
+ 4. run the command filters — **authorization filters first**, then the rest,
14
+ stopping at the first blocking verdict;
15
+ 5. resolve `Handle`'s arguments, which is where `Provide()` runs;
16
+ 6. invoke `Handle`;
17
+ 7. dispatch the returned value;
18
+ 8. complete the execution scopes in reverse order.
19
+
20
+ If the final result is not successful, the response is **cleared** before it is
21
+ serialized. A caller never receives both a failure and a payload.
22
+
23
+ Authorization runs before validation deliberately: sorting authorization filters
24
+ first is what stops a validation failure from short-circuiting the chain and
25
+ leaving the authorization verdict at its permissive default.
26
+
27
+ ## Dispatch
28
+
29
+ The returned value is dispatched on its **runtime** type, not on the method's
30
+ declared return type.
31
+
32
+ - `Task<T>` and `ValueTask<T>` are awaited first, then the inner value is
33
+ dispatched. (`ARC0010` warns when a synchronous result is wrapped in a `Task`
34
+ for no reason.)
35
+ - A `Result<TValue, TError>` — `Cratis.Monads.Result<,>` from
36
+ `Cratis.Fundamentals` — is a `OneOf`, so its inner value is unwrapped and
37
+ dispatched. `Result<TEvent, ValidationResult>` therefore appends on success and
38
+ becomes a validation failure on error. Both directions have implicit
39
+ conversions, so `return new SomethingHappened(...)` and
40
+ `return ValidationResult.Error("...")` both compile in the same method.
41
+ - A tuple has every non-null element dispatched independently.
42
+ - Anything no handler claims becomes the response payload.
43
+
44
+ ## What claims an event
45
+
46
+ The Chronicle response-value handlers claim a value only when the event type is
47
+ **registered** — that is, the record carries `[EventType]` and Chronicle's event
48
+ type registry knows it.
49
+
50
+ | Runtime value | Claimed by | Appended to |
51
+ | --- | --- | --- |
52
+ | A registered event record | single-event handler | the command's event source id |
53
+ | `IEnumerable<object>` where every element is a registered event | events handler | the command's event source id |
54
+ | `EventForEventSourceId` | single wrapper handler | the wrapper's own id |
55
+ | A collection containing at least one `EventForEventSourceId`, every other element being a registered event | wrapper-collection handler | each wrapper to its own id, each plain event to the command's |
56
+
57
+ ⚠️ **An unregistered event type is not an error.** No handler claims it, so it
58
+ silently becomes the HTTP response body instead of being appended. A command
59
+ that "runs fine" but appends nothing is almost always a missing `[EventType]`.
60
+
61
+ An empty collection statically typed as `IEnumerable<EventForEventSourceId>` is
62
+ still recognised and appends nothing, rather than being serialized as the
63
+ response.
64
+
65
+ `[EventType]` takes an optional id and generation. Pass neither for a new event:
66
+ the identifier defaults to the type name, and `ARCCHR0004` reports an id that
67
+ merely repeats the type name. An id that *differs* from the type name is the
68
+ supported way to rename the record while stored events keep resolving, and is
69
+ left alone.
70
+
71
+ ## The tuple rule
72
+
73
+ Within a tuple:
74
+
75
+ 1. every element is offered to the handlers;
76
+ 2. the elements nothing can handle are candidates for the response;
77
+ 3. **more than one unhandleable element throws `MultipleUnhandledTupleValues`** —
78
+ Arc cannot decide which one is the response;
79
+ 4. the single unhandleable element is set as the response *before* the
80
+ handleable elements are handled, which is what lets the events see it.
81
+
82
+ That last point is the mechanism behind the create-command idiom: an
83
+ `EventSourceId`/`EventSourceId<T>`-derived value is not appendable, so it becomes
84
+ the response, and `commandContext.GetEventSourceId()` prefers a response that is
85
+ an event-source-id value over the id resolved from the command's properties. The
86
+ events in the same tuple are therefore appended to the id the handler just
87
+ generated, and the client receives it as the response.
88
+
89
+ ```csharp
90
+ public (<ThingId>, <ThingRegistered>) Handle()
91
+ {
92
+ var id = <ThingId>.New();
93
+ return (id, new(<Name>));
94
+ }
95
+ ```
96
+
97
+ Returning two ids, or an id and another plain value, is the failure case.
98
+
99
+ ## Cross-stream appends
100
+
101
+ An event never carries its own event source. To write to another stream, wrap:
102
+
103
+ ```csharp
104
+ public IEnumerable<object> Handle() =>
105
+ [
106
+ new EventForEventSourceId(From, new <Withdrawn>(Amount)),
107
+ new EventForEventSourceId(To, new <Deposited>(Amount)),
108
+ ];
109
+ ```
110
+
111
+ `EventForEventSourceId` also carries optional `Subject`, `EventStreamType` and
112
+ `EventStreamId` init members when the target stream is not the default.
113
+
114
+ Concurrency scope is resolved **per target stream** — one expected tail per
115
+ event source id, not one shared across the streams a cross-stream command writes
116
+ to.
117
+
118
+ Do not reach for `IEventLog` to do this instead. `ARCCHR0007` warns when a
119
+ command's `Handle` injects `IEventLog`, because that bypasses Arc's append
120
+ pipeline along with its correlation and ordering guarantees.
121
+
122
+ ## Failure shapes
123
+
124
+ | Situation | What the caller sees |
125
+ | --- | --- |
126
+ | No handler for the command type | `ExceptionMessages` names the command type |
127
+ | Authorization filter denies | `IsAuthorized` false, `AuthorizationFailureReason` set |
128
+ | Validator or `Provide()` returns `ValidationResult.Error` | `IsValid` false, `ValidationResults` populated |
129
+ | Append rejected by a Chronicle constraint | a validation failure whose `Reason` says a constraint rejected it and whose `ReasonDetail` names it |
130
+ | `Provide()`/`Handle()` throws | `HasExceptions` true — HTTP 500, not a validation failure |
131
+ | Two unhandleable tuple elements | `MultipleUnhandledTupleValues` becomes an exception outcome |
132
+
133
+ Branch on `ValidationResult.ReasonDetail` rather than on `Message`. The message
134
+ is prose written for a human and is free to change; the detail is the identity of
135
+ the specific thing that rejected the command.