@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,149 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.ts,**/*.tsx"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*.ts"
|
|
5
|
+
- "**/*.tsx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# TypeScript Conventions
|
|
10
|
+
|
|
11
|
+
TypeScript's type system is the primary tool for catching bugs before they reach production. Every rule here pushes toward maximum compiler coverage and self-documenting code. If the types are right, the code almost writes itself.
|
|
12
|
+
|
|
13
|
+
## Enums over Magic Strings
|
|
14
|
+
|
|
15
|
+
String literal unions look concise but provide no refactoring support, no namespace, and no discoverability. Enums give you all three — plus `switch` exhaustiveness checking.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// ✅ Correct — refactorable, discoverable, exhaustive
|
|
19
|
+
export enum SliceType {
|
|
20
|
+
StateChange = 'stateChange',
|
|
21
|
+
StateView = 'stateView',
|
|
22
|
+
Automation = 'automation',
|
|
23
|
+
Translator = 'translator',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ❌ Wrong — no refactoring support, invisible to tooling
|
|
27
|
+
export type SliceType = 'stateChange' | 'stateView' | 'automation' | 'translator';
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- Use enum members everywhere — `switch` cases, comparisons, defaults.
|
|
31
|
+
- Do **not** import enums as `type`; they are values.
|
|
32
|
+
- Export enums from `index.ts` without the `type` keyword.
|
|
33
|
+
|
|
34
|
+
## One Type or Enum per File
|
|
35
|
+
|
|
36
|
+
Each type gets its own file because it makes the codebase navigable — finding `SliceType` means opening `SliceType.ts`, not hunting through `types.ts`. It also keeps diffs clean and makes imports explicit.
|
|
37
|
+
|
|
38
|
+
- Every interface, type alias, and enum lives in **its own file**, named after the type (e.g. `SliceType.ts`).
|
|
39
|
+
- **Never create** `types.ts`, `models.ts`, `interfaces.ts` grab-bag files — they become dumping grounds that grow without limit.
|
|
40
|
+
- Exception: component props interfaces (`*Props`) may live alongside their component `.tsx` file since they are tightly coupled to that component.
|
|
41
|
+
- Aggregate exports through the folder's `index.ts`.
|
|
42
|
+
|
|
43
|
+
## Type Safety
|
|
44
|
+
|
|
45
|
+
`any` disables the compiler — the one tool that catches bugs for free. Every `any` is a hole in the safety net. Use `unknown` and narrow with type guards instead.
|
|
46
|
+
|
|
47
|
+
- Never use `any` — use `unknown`, `Record<string, unknown>`, or proper generic constraints.
|
|
48
|
+
- Prefer `value as unknown as TargetType` over `value as any`.
|
|
49
|
+
- Use `unknown` as default generic parameter instead of `any`.
|
|
50
|
+
- React synthetic events (`React.MouseEvent<Element, MouseEvent>`) and DOM events (`MouseEvent`) are different types — don't mix them.
|
|
51
|
+
- **`int64`/`uint64` Chronicle fields generate as `bigint`** in the TypeScript proxies (not `number`, which silently truncated past `Number.MAX_SAFE_INTEGER`). Sequence numbers, large counters, and 64-bit ids surface as `bigint` — use `bigint` arithmetic, never `Number()`.
|
|
52
|
+
|
|
53
|
+
## User-facing strings (localization)
|
|
54
|
+
|
|
55
|
+
How user-visible text is handled is **product policy, not a Cratis framework rule** (see [general.md](./general.md) — locales belong in a downstream app's own `.cratis/ai/`). Cratis itself has no mandatory i18n layer or `Strings` alias.
|
|
56
|
+
|
|
57
|
+
- If the app has a localization convention (e.g. a translation object behind a `strings`/`Strings` import, or any i18n library), follow it consistently and keep raw string literals to constant, non-user-facing values (CSS class names, `key` props, internal identifiers).
|
|
58
|
+
- If the app ships literal text, literal labels in JSX are fine.
|
|
59
|
+
|
|
60
|
+
Either way, the Cratis-generic rule is only that strings are handled consistently within the app — not a specific file layout or import alias.
|
|
61
|
+
|
|
62
|
+
## Arc Frontend Patterns
|
|
63
|
+
|
|
64
|
+
Arc's proxy generator bridges C# and TypeScript automatically — every `[Command]` and `[ReadModel]` becomes a TypeScript class with `.use()` hooks, `.execute()` methods, and change tracking. This is the foundation of full-stack type safety: change a C# record and the TypeScript proxy updates on the next `dotnet build`.
|
|
65
|
+
|
|
66
|
+
### Commands
|
|
67
|
+
|
|
68
|
+
Auto-generated from C# `[Command]` records. The `.use()` hook returns a tuple: the command instance (with change tracking) and a setter for property values.
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
const [command, setValues] = OpenAccount.use({ name: '', owner: '' });
|
|
72
|
+
await command.execute(); // Sends command to backend, returns CommandResult
|
|
73
|
+
await command.validate(); // Pre-flight validation only, no side effects
|
|
74
|
+
command.hasChanges; // True when any property differs from initial values
|
|
75
|
+
command.revertChanges(); // Reset all properties to initial values
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Queries
|
|
79
|
+
|
|
80
|
+
Auto-generated from C# `[ReadModel]` static query methods. Observable queries (returning `ISubject<T>` on the backend) auto-subscribe via WebSocket — the component re-renders when data changes on the server.
|
|
81
|
+
|
|
82
|
+
```tsx
|
|
83
|
+
const [result, perform] = AllProjects.use();
|
|
84
|
+
// result.data — the query result
|
|
85
|
+
// result.isPerforming — true while loading
|
|
86
|
+
// result.hasData — true when data has arrived
|
|
87
|
+
// result.isSuccess — true when query completed without errors
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Paginated queries:
|
|
91
|
+
```tsx
|
|
92
|
+
const [result, , setPage] = AllProjects.useWithPaging(10);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### CommandScope
|
|
96
|
+
|
|
97
|
+
Wraps multiple command-using components, aggregating their `hasChanges` state and enabling bulk `execute()` and `revertChanges()`. Useful for forms that span multiple components.
|
|
98
|
+
|
|
99
|
+
### CommandForm
|
|
100
|
+
|
|
101
|
+
Declarative form component with built-in field types, validation timing (`validateOn: blur|change|both`), and automatic server-side validation feedback.
|
|
102
|
+
|
|
103
|
+
## Language — American English Only
|
|
104
|
+
|
|
105
|
+
All identifiers, comments, JSDoc, 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.
|
|
106
|
+
|
|
107
|
+
## Variables and Naming
|
|
108
|
+
|
|
109
|
+
- Prefer `const` over `let` over `var` when declaring variables.
|
|
110
|
+
- Never use shortened or abbreviated names for variables, parameters, or properties.
|
|
111
|
+
- Use full descriptive names: `deltaX` not `dx`, `index` not `idx`, `event` not `e`, `previous` not `prev`, `direction` not `dir`, `position` not `pos`, `contextMenu` not `ctx`/`ctxMenu`.
|
|
112
|
+
- The only acceptable short names are well-established domain terms (e.g. `id`, `url`, `min`, `max`).
|
|
113
|
+
|
|
114
|
+
## Imports and Compilation
|
|
115
|
+
|
|
116
|
+
- Never leave unused import statements in the code.
|
|
117
|
+
- Always ensure that the code compiles without warnings — use `yarn compile` to verify (successful runs produce no output).
|
|
118
|
+
- Review each file for lint compliance before finalizing.
|
|
119
|
+
- Never use placeholder or temporary types — use proper types from the start.
|
|
120
|
+
- **Never modify any file inside `node_modules/` or any build cache (e.g. `.vite/deps/`).** These are managed by the package manager and will be overwritten on the next install. If something appears broken in a library, look harder at the application code — especially when other usages of the same library work fine. Fixes belong in application code or upstream in the library's own repo.
|
|
121
|
+
|
|
122
|
+
## Folder Structure
|
|
123
|
+
|
|
124
|
+
- Do not prefix a file, component, type, or symbol with the name of its containing folder or the concept it belongs to. Instead, use folder structure to provide that context.
|
|
125
|
+
- Favor functional folder structure over technical folder structure.
|
|
126
|
+
- Group files by the feature or concept they belong to, not by their technical role.
|
|
127
|
+
- Avoid folders like `components/`, `hooks/`, `utils/`, `types/` at the feature level.
|
|
128
|
+
|
|
129
|
+
## Advanced Type Safety
|
|
130
|
+
|
|
131
|
+
Additional patterns for common tricky scenarios:
|
|
132
|
+
|
|
133
|
+
**Storybook:**
|
|
134
|
+
- Use `React.ComponentType<Record<string, never>>` for components with no props.
|
|
135
|
+
- Always use `as unknown as` when converting component imports to avoid type mismatch errors.
|
|
136
|
+
- Properly type story args — never use `any`.
|
|
137
|
+
|
|
138
|
+
**External libraries with strict generic constraints:**
|
|
139
|
+
- Import necessary types (e.g. `Command` from `@cratis/arc/commands`) rather than asserting to `any`.
|
|
140
|
+
- Use type assertions through `unknown`: `props.command as unknown as Constructor<Command<...>>`.
|
|
141
|
+
- Extract tuple results explicitly rather than destructuring when type assertions are needed.
|
|
142
|
+
- Use proper library types when available; use specific property types (e.g. `{ canvas?: HTMLCanvasElement }`) over `any`.
|
|
143
|
+
|
|
144
|
+
**Dynamic and generic types:**
|
|
145
|
+
- Add type guards for unknown function parameters: `if (typeof accessor !== 'function') return ''`.
|
|
146
|
+
- Type parameters with fallbacks: `function<T = unknown>(accessor: ((obj: T) => unknown) | unknown)`.
|
|
147
|
+
- Cast arrays from `unknown` explicitly: `((obj as Record<string, unknown>).items || []) as string[]`.
|
|
148
|
+
- Use `String(value)` for string conversions in generic contexts.
|
|
149
|
+
- Use explicit Date parameter types: `new Date(value as string | number | Date)`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/*"
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Verification Discipline
|
|
8
|
+
|
|
9
|
+
Verification answers whether changed behavior works. It does not establish who
|
|
10
|
+
authored a file or preserve a chain of evidence about how it arrived.
|
|
11
|
+
|
|
12
|
+
- Run the narrowest relevant build, type check, lint, and specifications.
|
|
13
|
+
- Add a focused specification for every behavior or rejection rule you change.
|
|
14
|
+
- Keep each check deterministic and runnable locally and in CI.
|
|
15
|
+
- Never replace a real behavior check with a checksum, inventory, generated
|
|
16
|
+
receipt, or provenance record.
|
|
17
|
+
- Report failures and skipped checks honestly.
|
|
18
|
+
|
|
19
|
+
For this repository, `Source/Verification` validates corpus structure, profile
|
|
20
|
+
composition, skill scenarios, and native package behavior. `Source/Harness.Setup`
|
|
21
|
+
verifies that repository harness adapters still point to `.cratis/ai`.
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*.cs, **/*.tsx"
|
|
3
|
+
profile: application
|
|
4
|
+
paths:
|
|
5
|
+
- "**/*.cs"
|
|
6
|
+
- "**/*.tsx"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Vertical Slice Architecture
|
|
10
|
+
|
|
11
|
+
> **Application profile.** This is the architecture for *applications built on Cratis*. It does **not** apply inside a Cratis framework repo (Arc/Chronicle/Fundamentals/Components) — those are libraries; see [framework.md](./framework.md).
|
|
12
|
+
|
|
13
|
+
A vertical slice owns a single behavior: the command or query, the events it produces, the projections that build read models, the React component that renders the UI, and the specs that verify it all works. Everything lives together because everything changes together. **The slice is the invariant unit** (command + events + projection + component + specs), created, renamed, and deleted as one.
|
|
14
|
+
|
|
15
|
+
This file is the reference for *what* goes in each part of a slice. Layout, slice types, workflow, and quality gates are in [general.md](./general.md).
|
|
16
|
+
|
|
17
|
+
## Technical stack
|
|
18
|
+
|
|
19
|
+
- .NET / C# (ASP.NET Core) — **Cratis Arc** for CQRS / model-bound commands and queries, **Cratis Chronicle** for event sourcing, MongoDB or EF Core for read models.
|
|
20
|
+
- React + TypeScript (Vite) — Cratis Components (PrimeReact-based) + Arc-generated proxies, MVVM. Vitest + Mocha/Chai/Sinon for frontend specs.
|
|
21
|
+
- xUnit + Cratis.Specifications + NSubstitute for C# specs (the `*Scenario` family — see [specs.md](./specs.md)).
|
|
22
|
+
|
|
23
|
+
## Proxy generation — the build dependency
|
|
24
|
+
|
|
25
|
+
A Debug `dotnet build` generates a typed TypeScript proxy per command and query next to the slice's `.cs` — Debug is the canonical trigger because it carries the fullest, most reliably-emitted PDB debug information the generator uses to place generated files. Until the backend compiles, **no proxy files exist** and frontend code cannot reference them. **Backend → build → frontend, always** — backend and frontend for the same slice cannot be done in parallel. When a subsequent Release build is only there to verify the app compiles in that configuration, skip proxy regeneration with `dotnet build -c Release -p:CratisProxiesOutputPath=` rather than letting it re-run the generator against a different compilation. Never edit a generated proxy (`// @generated by Cratis` header); fix the C# source and rebuild.
|
|
26
|
+
|
|
27
|
+
## Model-bound — no controllers **[contract]**
|
|
28
|
+
|
|
29
|
+
Commands are `[Command]` records with a public instance `Handle()` (Arc analyzers enforce this); queries are `static` methods on `[ReadModel]` records; projections, constraints, and authorization use attributes. Arc generates the HTTP surface — never write an MVC/API controller. Drop to fluent (`IProjectionFor<T>`, `IConstraint`) only when model-bound cannot express the rule.
|
|
30
|
+
|
|
31
|
+
> **[convention]** A single `.cs` per slice and a fixed declaration order are house style for readability — Arc/Chronicle discover by attributes and static methods, not by file or source order. Split the file when a slice grows large or shared concepts move upward.
|
|
32
|
+
|
|
33
|
+
Declaration order (house style): concepts → command + validator (paired) → business rules → constraints → events → read models + queries → projections → reactors. StyleCop SA1201/SA1210 enforce that each `[EventType]` appears **after** the command/validator that constructs it (a [convention] surfaced as a compile warning, not a Chronicle [contract]).
|
|
34
|
+
|
|
35
|
+
A single `<Slice>.cs` therefore contains ALL of: `[Command]` records with `Handle()`, validators, constraints, `[EventType]` records, `[ReadModel]` records with static query methods, projections/reducers, reactors, and slice-specific concepts.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
<Slice>/
|
|
39
|
+
├── <Slice>.cs ← ALL backend artifacts in one file
|
|
40
|
+
├── <Component>.tsx ← React component(s) for the slice
|
|
41
|
+
└── when_<behavior>/ ← specs
|
|
42
|
+
└── and_<scenario>.cs
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**❌ Never split by artifact type** (`Commands/`, `Handlers/`, `Events/` folders) — that is the layered structure the slice replaces.
|
|
46
|
+
|
|
47
|
+
## Concepts
|
|
48
|
+
|
|
49
|
+
Every domain identifier or named value is a typed record — raw `string`/`Guid`/`int` should not appear on commands, events, or read models for domain values. Primitives stay primitive only when the meaning *is* the base type (`bool`, a plain count, `DateOnly`).
|
|
50
|
+
|
|
51
|
+
- **[contract] Value concepts** derive from `ConceptAs<T>`. The base provides value equality and an implicit **concept → `T`** conversion. **Add your own `T` → concept operator** when call-site ergonomics need it — the base does *not* provide that direction.
|
|
52
|
+
- **[contract] Chronicle stream identities** derive from `EventSourceId<T>` with an `IComparable` underlying primitive. Add `NotSet`, `New()`, or a `T` → derived-id operator only when the domain API justifies it; typed empty/zero values are real specified stream IDs. The base conversion surface does not construct every derived domain record automatically.
|
|
53
|
+
|
|
54
|
+
```csharp
|
|
55
|
+
public record AuthorId(Guid Value) : EventSourceId<Guid>(Value)
|
|
56
|
+
{
|
|
57
|
+
public static readonly AuthorId NotSet = new(Guid.Empty);
|
|
58
|
+
public static AuthorId New() => new(Guid.NewGuid());
|
|
59
|
+
public static implicit operator AuthorId(Guid value) => new(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public record AuthorName(string Value) : ConceptAs<string>(Value)
|
|
63
|
+
{
|
|
64
|
+
public static implicit operator AuthorName(string value) => new(value);
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**[convention] Placement:** slice-specific concept → in the slice file; feature-shared → feature folder; module-shared → module folder; app-wide → `Common/`. Scan `Common/` and the feature folder before creating a new concept. See [concepts.md](./concepts.md) for full patterns, or invoke the **add-concept** skill.
|
|
69
|
+
|
|
70
|
+
## Commands
|
|
71
|
+
|
|
72
|
+
The command carries input from the caller. `Handle()` is defined directly on the record — never a separate handler class. For step-by-step creation, invoke the **cratis-command** skill.
|
|
73
|
+
|
|
74
|
+
> This application is event-sourced, so the guidance below assumes a command's `Handle()` returns event(s) that Arc+Chronicle appends. Arc itself is a standalone CQRS framework (see [general.md](./general.md)) — a command may legitimately return a response or `void` and do its work through injected services when a slice isn't event-sourced; the return-shape and "never inject `IEventLog`" rules are the event-sourced default, not universal Arc laws.
|
|
75
|
+
|
|
76
|
+
### The decision matrix — where each rule lives **[contract]**
|
|
77
|
+
|
|
78
|
+
`Handle()` assumes the command is valid and constructs the event(s). Pick the mechanism by what the decision *is*:
|
|
79
|
+
|
|
80
|
+
| Decision | Use |
|
|
81
|
+
| --- | --- |
|
|
82
|
+
| Reusable value invariant (length, format, range) | `ConceptValidator<T>` on the concept type |
|
|
83
|
+
| Command-input / cross-field / pre-handler rule (incl. injected read-model/service/identity checks) | `CommandValidator<TCommand>` with `RuleFor(...)` |
|
|
84
|
+
| Handler needs fetched/computed data before it can build the event | `Provide()` — fetches the data; may validate it and short-circuit |
|
|
85
|
+
| State-dependent rule that must hold **under concurrency** | inject the read model into `Handle()`, return `Result<TEvent, ValidationResult>` |
|
|
86
|
+
| Uniqueness | Chronicle `[Unique]` / `IConstraint` (race-safe) — not a read-model pre-check |
|
|
87
|
+
| Genuinely exceptional failure (bug, missing infra) | `throw` a domain exception |
|
|
88
|
+
|
|
89
|
+
**Do not throw for normal business rejection.** A thrown exception (from `Provide()` or `Handle()`) surfaces as an exception/HTTP 500, *not* a validation result. Recoverable, user-facing rejections are validation: `ValidationResult.Error(...)` via a validator or `Result<,>`. (For step-by-step business-rule placement, invoke the **add-business-rule** skill.)
|
|
90
|
+
|
|
91
|
+
### Causation — what a command records **[contract]**
|
|
92
|
+
|
|
93
|
+
A command's **property values** are recorded on the causation of every event it appends, alongside the command's name, so an event says not only which command produced it but what that command was asked to do. The causation is written into the event log and stays there for as long as the events do — a value recorded there cannot be taken back out by changing code.
|
|
94
|
+
|
|
95
|
+
Two markings keep a value off the chain, and both are honored on the property, the declaring type, the positional record parameter, **and the property's type**:
|
|
96
|
+
|
|
97
|
+
| Marking | For | Also does |
|
|
98
|
+
| --- | --- | --- |
|
|
99
|
+
| `[PII]` | personal data | encrypts it in the event, enrolls it in erasure |
|
|
100
|
+
| `[NotAudited]` | a secret that is not personal data — password, token, API key, card number | nothing else; it only withholds |
|
|
101
|
+
|
|
102
|
+
They are **not interchangeable**: `[PII]` on a password would encrypt it and enroll it in GDPR erasure, which is wrong; `[NotAudited]` on a name does nothing for an erasure request, which is also wrong.
|
|
103
|
+
|
|
104
|
+
**Prefer marking the concept**, exactly as with `[PII]` — mark `ApiKey` once and every command taking one is covered:
|
|
105
|
+
|
|
106
|
+
```csharp
|
|
107
|
+
[NotAudited]
|
|
108
|
+
public record ApiKey(string Value) : ConceptAs<string>(Value);
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`[NotAudited]` on the command type excludes every property at once, which is right when a command exists only to carry secrets. The command is still **named** on the chain either way — what is withheld is the values, never the fact that it ran.
|
|
112
|
+
|
|
113
|
+
`ARCCHR0009` warns on a command property whose *name* reads like a secret and is unmarked. It cannot see a secret whose name does not say so, so a clean build means "nothing obvious was missed", not "no secrets are recorded". For a false positive, **suppress the diagnostic** — marking it `[NotAudited]` would silence the warning by withholding a value you wanted recorded.
|
|
114
|
+
|
|
115
|
+
Values render camel-cased; concepts unwrap to the value they hold; a value that is not set is omitted; a long one is truncated (the causation rides on *every* event the command appends).
|
|
116
|
+
|
|
117
|
+
### `Provide()` — data for `Handle()` **[contract]**
|
|
118
|
+
|
|
119
|
+
`Provide()` runs after authorization and validation, before `Handle()`. Its parameters resolve from DI (like `Handle()`'s); the command instance is `this`. Return one value, or a tuple (Arc binds tuple values to `Handle(...)` parameters by type). Each provided value must be consumed by a `Handle` parameter — an unused one is the **`ARC0005`** analyzer error. Use a **named record** when the provided values form a real domain/snapshot concept; a **tuple** when they are just separate inputs to event construction. It may be sync or async.
|
|
120
|
+
|
|
121
|
+
```csharp
|
|
122
|
+
[Command]
|
|
123
|
+
public record PlaceOrder(OrderId OrderId, CustomerId CustomerId) : ICanProvideEventSourceId
|
|
124
|
+
{
|
|
125
|
+
public EventSourceId GetEventSourceId() => OrderId;
|
|
126
|
+
|
|
127
|
+
public async Task<Result<Customer, ValidationResult>> Provide(IReadModels readModels)
|
|
128
|
+
{
|
|
129
|
+
var customer = await readModels.GetInstanceById<Customer>((EventSourceId)CustomerId);
|
|
130
|
+
return customer is null ? ValidationResult.Error("Customer must exist.") : customer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public OrderPlaced Handle(Customer customer) => new(customer.Name);
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `Provide()` for IO/fetched/computed data a valid command needs (explicit-key read-model lookups, external services, snapshots). It may short-circuit with `ValidationResult` / `AuthorizationResult` / `Result<TProvided, ValidationResult>` when the supplied data is missing or unusable. Keep IO in `Provide()` and the decision in `Handle()`. Do **not** write a pass-through `Provide()` that only wraps a read model `Handle(...)` could take directly, and do **not** duplicate the same rejection in both the validator and `Provide()` — pick one owner.
|
|
138
|
+
|
|
139
|
+
### `Handle()` return shapes **[contract]**
|
|
140
|
+
|
|
141
|
+
Return the event(s) directly — Arc appends them; never inject `IEventLog` to append the primary event. If there is no `await`, return the value directly (no `Task<T>`/`Task.FromResult`).
|
|
142
|
+
|
|
143
|
+
| Return | When |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `TEvent` | one event on the implicit stream |
|
|
146
|
+
| `(EventSourceId, TEvent)` | command decides the event source id (tuple order doesn't matter) |
|
|
147
|
+
| `(TResponse, TEvent)` | a response value plus an event |
|
|
148
|
+
| `IEnumerable<object>` | multiple events; `EventForEventSourceId(id, @event)` wrappers for cross-stream |
|
|
149
|
+
| `Result<TEvent, ValidationResult>` | success event or a typed validation error (concurrency-sensitive rule) |
|
|
150
|
+
| `void` | no event |
|
|
151
|
+
|
|
152
|
+
**How Arc picks the response vs. metadata:** for an `(A, B)` tuple, each element is checked — exactly one element *without* a registered event handler becomes the `CommandResult<...>` response; if all are events, there is no response; **more than one un-handled element throws**. A `Result<TSuccess, TError>` has its inner value unwrapped and processed by these same rules (so `Result<(TId, TEvent), ValidationResult>` is processed as a tuple). A `(TEvent, Subject)` tuple's `Subject` is treated as **append metadata** (not a response) and overrides the resolved compliance subject. A `(TIdConcept, TEvent)` tuple opens a new stream — the id concept is the event source for that event.
|
|
153
|
+
|
|
154
|
+
### Stream metadata & DCB concurrency **[contract]**
|
|
155
|
+
|
|
156
|
+
Three attributes scope an append broad→narrow, and each one *also* (only with `concurrency: true`) contributes its dimension to the server-side concurrency check:
|
|
157
|
+
|
|
158
|
+
| Attribute | Meaning |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `[EventSourceType("...")]` | the overarching concept the event source *is* (`Account`, `Order`) |
|
|
161
|
+
| `[EventStreamType("...")]` | a concrete process **within** that source type (`Onboarding`, `Transactions`) |
|
|
162
|
+
| `[EventStreamId("...")]` | a marker separating **independent streams** within one stream type (`Monthly`, `2026-06`) |
|
|
163
|
+
|
|
164
|
+
Each attribute always tags the appended events (visible in `EventContext`). **Only when the command also carries `concurrency: true`** does each dimension join the server-side `ConcurrencyScope` (plus the event source id), with the optimistic strategy supplying the expected tail sequence number. A command **without** `concurrency: true` contributes no scope of its own, and the append falls to whatever concurrency strategy the event sequence is configured with — by default the optimistic one, scoped to the event source. So the attribute chooses *which dimensions* bound the check, not whether a check happens at all. Reach for it on high-contention state transitions where a stale read would let two commands both "win" — it narrows the boundary so appends that are genuinely independent stop colliding (concurrency failures surface on `CommandResult`/`AppendResult`). Adopt it deliberately per behavior, not as a blanket default.
|
|
165
|
+
|
|
166
|
+
When the stream id is known only at runtime, implement `ICanProvideEventStreamId.GetEventStreamId()` instead of `[EventStreamId]`. ⚠️ Combining `ICanProvideEventStreamId` with a non-null `[EventStreamId]` attribute on the same command throws `AmbiguousEventStreamId` at startup — pick one (or set the attribute value to `null` to defer to the interface).
|
|
167
|
+
|
|
168
|
+
### Event-source id resolution order **[contract]**
|
|
169
|
+
|
|
170
|
+
`ICanProvideEventSourceId.GetEventSourceId()` → a single `EventSourceId`/`EventSourceId<T>`-derived property → a `[Key]` property → else Arc/Chronicle generates one.
|
|
171
|
+
|
|
172
|
+
⚠️ **Injected read models resolve ONLY by the command's resolved event-source id** — not by the read-model type, and not by "the property that looks like its key." If the read model you need is keyed by a **different** value, direct injection silently hands you the **wrong** instance — or, when nothing exists for the command's id, `null` (or for a `[Passive]` projection a **default-valued** instance; see *Existence checks* below) — a correctness bug, not a compile error. The three failure shapes: (1) you need a *referenced other* entity (act on A, check B); (2) the command has **multiple** `EventSourceId<T>`-typed properties → ambiguous resolution (never rely on property order); (3) a create command whose id is generated inside `Handle()`. In all three, read by explicit key: `IReadModels.GetInstanceById<T>((EventSourceId)key)`.
|
|
173
|
+
|
|
174
|
+
### Validators **[contract]**
|
|
175
|
+
|
|
176
|
+
`CommandValidator<TCommand>` (FluentValidation) sits directly below the `[Command]` it validates. **Omit it entirely when there are no rules** — delete an empty subclass. Single-property intrinsic rules belong on `ConceptValidator<T>`, not here. Constructor dependencies (read models, services, identity accessor) are injected and resolved per the command's event-source id.
|
|
177
|
+
|
|
178
|
+
### Authorization **[contract]/[convention]**
|
|
179
|
+
|
|
180
|
+
Apply authorization attributes on the `[Command]` record or query method. **[convention]** never check roles inside `Handle()` with an `if` — express it as an attribute/policy/command-filter at the boundary; command-specific scope rejection belongs in the validator.
|
|
181
|
+
|
|
182
|
+
## Events
|
|
183
|
+
|
|
184
|
+
`[EventType]` records are the schema-bound truth of the system.
|
|
185
|
+
|
|
186
|
+
- **[contract] No arguments for new events** — the type name is the identifier. Use `generation:`/id only when evolving an existing contract (invoke the **event-type-migrations** skill).
|
|
187
|
+
- **[contract] Past-tense, one-purpose names** (`AuthorRegistered`, not `Updated`/`FormSubmitted`) — Chronicle requires past-tense, single-purpose facts. Self-describing without slice context.
|
|
188
|
+
- **[contract] Avoid nullable properties** — Chronicle's analyzer warns on nullable event members; model optional facts as a separate event, or resolve a nullable command input to a non-null sentinel before constructing the event.
|
|
189
|
+
- **[contract] Never carry the event-source id** — it's implicit in the event context.
|
|
190
|
+
- **[convention] No `DateTimeOffset`/`DateTime` properties** that duplicate `EventContext.Occurred` — Chronicle records the wall-clock timestamp of every event there. Read it in projections/reducers via `[SetFromContext<T>]` or fluent `.Set(m => m.X).ToEventContextProperty(c => c.Occurred)`. Use `DateOnly` only for a business date the user explicitly supplies.
|
|
191
|
+
- **[convention] Every `[EventType]` has an XML `<summary>`** (Cratis C# doc convention) recording why the event exists and what each value means.
|
|
192
|
+
- **[contract] `[EventType]` lives in its owning slice file** — referenced from other slices via `using`.
|
|
193
|
+
|
|
194
|
+
**Event-design rules (what is and isn't a fact):**
|
|
195
|
+
|
|
196
|
+
- **Events are not read models** — never append events for totals, counts, statuses, search indexes, or dashboards. Project those from source events into read models.
|
|
197
|
+
- **Never duplicate one fact as an aggregate event *plus* a per-item fanout.** If a command fans out per item (one `RequestSentToPartner` per partner), don't also emit an aggregate event carrying the whole list. Keep the fanout as the single source of truth and project any aggregate from it; pick per-item vs. single-with-list event from how consumers read it — never both.
|
|
198
|
+
- **Command rejection normally appends no event.** Validation and business-rule rejections are command results, not facts. Add a rejection/failed event only when the business must audit or react to that failure later.
|
|
199
|
+
- **One event name = one meaning.** Don't reuse a vague event for several domain facts; if facts can occur, be absent, or have consumers independently, split them into separate event types.
|
|
200
|
+
|
|
201
|
+
```csharp
|
|
202
|
+
/// <summary>Emitted when an author is registered.</summary>
|
|
203
|
+
/// <param name="Name">The author's display name.</param>
|
|
204
|
+
[EventType]
|
|
205
|
+
public record AuthorRegistered(AuthorName Name);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
## Read Models and Queries
|
|
209
|
+
|
|
210
|
+
**[contract]** `[ReadModel]` records expose queries as `public static` methods; dependencies are plain method parameters (no `[FromServices]`). Default to model-bound projection shape; drop to fluent only when needed. For step-by-step creation, invoke the **cratis-readmodel** skill.
|
|
211
|
+
|
|
212
|
+
### Query return shapes **[contract]**
|
|
213
|
+
|
|
214
|
+
Stay within analyzer-supported shapes: the read model, `T?`, a collection/array/`IQueryable<T>` of it, `Task<...>` of those, `IAsyncEnumerable<T>`, or `ISubject<...>` of read model/collection.
|
|
215
|
+
|
|
216
|
+
- **`IQueryable<TReadModel>`** → automatic server-side paging/sorting (frontend `useWithPaging` — see the **query-paging** skill). Use it whenever a list can grow.
|
|
217
|
+
- **`ISubject<...>`** → live/observable queries. Return it directly — **never** `Task<ISubject<...>>`.
|
|
218
|
+
- **[contract] Custom paths use `[Path("...")]`** (`PathAttribute`), not ASP.NET `[Route]`. Reserve `[Route]` for controller endpoints (which this convention avoids).
|
|
219
|
+
|
|
220
|
+
```csharp
|
|
221
|
+
[ReadModel]
|
|
222
|
+
[FromEvent<AuthorRegistered>]
|
|
223
|
+
public record Author(AuthorId Id, AuthorName Name)
|
|
224
|
+
{
|
|
225
|
+
public static IQueryable<Author> AllAuthors(IMongoCollection<Author> collection) => collection.AsQueryable();
|
|
226
|
+
public static Task<Author?> AuthorById(IReadModels readModels, AuthorId id) =>
|
|
227
|
+
readModels.GetInstanceById<Author>((EventSourceId)id);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**[convention] No default values on `[ReadModel]` constructor parameters** — omit `= null`, `= default`, `= default!`, `= 0`, `= false`, `= ""`, `= []`. Chronicle writes the projected value on the first event, so a sentinel default is never correct and hides missing projection wiring. The only permitted defaults are enum variants that represent an explicit initial state and `[SetValue<T>]`-driven `bool` flags. Use `T?` for data genuinely absent before the relevant event fires.
|
|
232
|
+
|
|
233
|
+
**[convention] Read-model boundaries & naming.** Read models are shaped for consumers and command policies, not CRUD-style aggregate DTOs. Split one when it starts mixing unrelated lifecycles — the test is coupling (state that changes for different reasons), not size. The event log is already the canonical history/audit trail, so name a model `AuditLog`/`ActivityLog` only when it genuinely is an audit surface; for curated UI projections prefer names like `Timeline`, `WorkSurface`, `Summary`, `Detail`, or `Policy`. (This is "specialization over reuse" applied to read models.)
|
|
234
|
+
|
|
235
|
+
### Choosing read-model access in command-side code **[convention]**
|
|
236
|
+
|
|
237
|
+
Ladder, first that fits: (1) direct read-model (DCB) injection — only when keyed by the command's own event-source id; (2) a `[Passive]` projection; (3) `IReadModels.GetInstanceById<T>((EventSourceId)key)` for a different/derived key; (4) a materialized projection. A lookup *interface* is justified **only** for a genuine non-key search (e.g. find-by-email) — never to wrap a keyed `GetInstanceById`.
|
|
238
|
+
|
|
239
|
+
### Existence checks — what an absent read model actually resolves to **[contract]**
|
|
240
|
+
|
|
241
|
+
`IReadModels.GetInstanceById<T>` is declared `Task<TReadModel>` (non-nullable) yet hands back `default!` when nothing exists, so the compiler gives callers no signal. What "nothing exists" resolves to depends on the **backing** — *not* on whether the model carries `[RemovedWith<T>]`:
|
|
242
|
+
|
|
243
|
+
| Backing | Never created — no matching event for the key | Removed by a `[RemovedWith<T>]` event |
|
|
244
|
+
| --- | --- | --- |
|
|
245
|
+
| Materialized projection or reducer (**the default**) | `null` | `null` — the sink document is deleted |
|
|
246
|
+
| `[Passive]` **projection** | ⚠️ a **default-valued instance**, never `null` | `null` — the computed state is blanked |
|
|
247
|
+
| `[Passive]` **reducer** | `null` | `null` |
|
|
248
|
+
|
|
249
|
+
`[RemovedWith<T>]` is therefore not the axis: a removed instance is `null` on **every** backing. The single non-`null` case is a **`[Passive]` projection that was never created** — Chronicle computes it on demand and seeds the initial state from the read model's schema, which fills every non-nullable, non-`string` property with its type default. (It answers `null` only when *no* property yields a schema default — all of them nullable, `string`/`object`, or an enum whose `0` is not a declared member — which is why the behavior looks inconsistent between models, not an escape hatch to design for.)
|
|
250
|
+
|
|
251
|
+
⚠️ **This is invisible when a status enum's `0` is a real state** ("the state every stream starts in"): absent becomes byte-identical to freshly-created, so a command reads never-invited as already-invited. **Renumbering the enum from `1` is not the fix** — the value is then dropped from the payload and deserializes back to CLR `0` on the client anyway. Carry an explicit existence flag instead: `[SetValue<FirstEvent>(true)] bool Exists`, one `[SetValue<T>]` per event that can be the first for the stream. That is exactly the `[SetValue<T>]`-driven `bool` exception the no-defaults convention above allows for. An explicit `NotSet`/`Unknown` zero member also works, but only because the property stays present and readable; the flag is the robust form. ⚠️ **A spec suite will not catch this** — `CommandScenario`'s read-model harness answers `null` for an unseeded event-source id, which matches production for every row above *except* the passive projection. Cover the absent case by seeding a default-valued instance explicitly, not only `null`.
|
|
252
|
+
|
|
253
|
+
**Injected-parameter nullability is the required/optional switch:** a nullable `TReadModel?` parameter (on a validator, `Provide()`, or `Handle()`) gets `null` injected for a never-created/removed instance — check `is null`. A **non-nullable** `TReadModel` whose instance does not exist throws **`CannotResolveValidatorDependency`** (non-nullable = "required, must exist"). ⚠️ Both switches ride on the same resolution, so on a **`[Passive]` projection** neither fires for a never-created instance: the nullable parameter receives a default-valued object and the `is null` guard never runs, and the non-nullable one resolves happily. Check the existence flag there, not nullability. Type by-id accessors you write yourself as `Task<T?>` so callers get the compiler signal Chronicle's own signature withholds.
|
|
254
|
+
|
|
255
|
+
## Projections **[contract]**
|
|
256
|
+
|
|
257
|
+
Projections build read models from **events** (never from other read models). Use the ladder — for adding one to an existing model, invoke the **add-projection** skill:
|
|
258
|
+
|
|
259
|
+
1. **Model-bound attributes** on the `[ReadModel]`: `[FromEvent<T>]`, `[SetFrom<T>]`, `[SetFromContext<T>]`, `[ChildrenFrom<T>]`, `[RemovedWith<T>]`, `[Nested]`, `[ClearWith<T>]`, counters, `[FromAll]`.
|
|
260
|
+
2. **Fluent `IProjectionFor<T>`** for joins, composite/constant keys, parent-key extraction, context mapping, `.NotRewindable()`, or supported conditional setters. Declarative metadata only — no DI side effects, no imperative `Define()` body, builder lambdas are member access only.
|
|
261
|
+
3. **Reducer `IReducerFor<T>`** when the model is "current state + event → next state" — branching on prior state, loops, calculations. A valid style, **not** a failure mode; carry an inline justification comment naming the limitation that ruled out a projection.
|
|
262
|
+
|
|
263
|
+
- **[contract] AutoMap is on by default — never call `.AutoMap()`.** Match property names so AutoMap wires them; diverge with `[SetFrom<T>]` / `.Set().To()` only for genuine name differences. Re-enable `.AutoMap()` only inside a scope disabled with `.NoAutoMap()`.
|
|
264
|
+
- ⚠️ **AutoMap runs per *event* across every event the projection references — it does not defer to your explicit setters.** A property sourced with `[SetFrom<A>]` can still be silently overwritten when some *other* referenced event `B` happens to carry an identically named property (AutoMap wires `B`'s value on top). Fence the property with property-level **`[NoAutoMap]`** (which pairs with the `[SetFrom]`, not replaces it) so only your explicit setter writes it. `[SetFrom]` does **not** imply this — leaving AutoMap on is what allows the common create-then-rename pattern (a later event updating a property by name-match). No colliding event property name means no `[NoAutoMap]` needed. (An event subscribed only via an aggregate like `[Count]` doesn't auto-map its other properties, so it can't collide.) The analyzer **`CHR0025`** surfaces this collision as an *informational* heads-up (not a warning, because either resolution — add `[NoAutoMap]`, or accept the update — can be correct).
|
|
265
|
+
- **[contract] `[Nested]`** projects a single nullable child object; place `[FromEvent<T>]` on the nested type (or property-level `[SetFrom<T>]` when the parent shares the event — declaring class-level `[FromEvent<T>]` on both parent and nested type for the same event is a duplicate-registration startup crash).
|
|
266
|
+
- **[contract] Event-sequence source attributes:** class-level `[EventSequence("name")]`, `[EventLog]`, or `[EventStore("name")]` select where the projection reads from (a named sequence, the explicit default log, or another Chronicle event store / inbox). ⚠️ `[FromEventSequence]` is **removed** — use `[EventSequence("name")]`.
|
|
267
|
+
|
|
268
|
+
## Constraints **[contract]**
|
|
269
|
+
|
|
270
|
+
Append-time invariants enforced by the Chronicle Kernel; Arc converts violations to validation errors on command appends.
|
|
271
|
+
|
|
272
|
+
- **`[Unique]`** on an event property (across event sources) or event class (one per source) — the default.
|
|
273
|
+
- **`[RemoveConstraint("name")]`** on a removal event to release a claimed value.
|
|
274
|
+
- **`IConstraint`** for multi-event, differing property names, case-insensitive (`.IgnoreCasing()`), or custom messages. Declarative metadata only — no DI side effects, member-access lambdas.
|
|
275
|
+
|
|
276
|
+
Group multi-event constraints under a shared name constant (`<Module>ConstraintNames`). Treat violations as validation results — never as thrown exceptions.
|
|
277
|
+
|
|
278
|
+
## Reactors **[contract]**
|
|
279
|
+
|
|
280
|
+
`IReactor` is a marker interface; dispatch is by the first parameter type. Reactors live only in Automation/Translation slices. See [reactors.md](./reactors.md) for full rules, or invoke the **add-reactor** skill.
|
|
281
|
+
|
|
282
|
+
```csharp
|
|
283
|
+
public Task AuthorRegistered(AuthorRegistered @event, EventContext context) => ...
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- **Signatures:** the first parameter is the event and drives dispatch; every parameter after it is resolved as a dependency — `EventContext`, a read model, a service, a `ReactorDelivery` — with no limit on how many. Return `Task`, `void`, or a synchronous side-effect return type. Prefer `Task`/async for real side effects, but sync returns are supported.
|
|
287
|
+
- **[convention] `[OnceOnly]`** on any non-idempotent side effect (emails, payments, external writes) — replay re-fires otherwise. It covers **replay only**; a recovered failed partition still re-delivers, so guard that with a `ReactorDelivery` receipt.
|
|
288
|
+
- **Side-effect events:** return them directly as event objects (single `TEvent`, `IEnumerable<object>`, or `Task<...>` thereof) — Chronicle appends them to the triggering event source. To target a different event source/stream, return `EventForEventSourceId(id, @event)` wrappers (the same cross-stream mechanism as a command — see [reactors.md](./reactors.md)); the wrapper is self-describing, so set the stream type, source type or `Subject` on it when you need full metadata control. Don't inject `IEventLog` for the normal path; reserve it for explicit append-result/wait/options/mixed-target workflows.
|
|
289
|
+
- **Commands from a reactor:** inject `ICommandPipeline` and `Execute(command)` (scopeless overload) — runs the full validation/authorization/handler pipeline. Don't reach for `IEventLog` to change state in another slice.
|
|
290
|
+
- **[convention] Stateless, idempotent, read state via Chronicle read models** (`GetInstanceById`), not `IMongoCollection<T>`. Don't throw to "validate" malformed inbound events — that pauses the partition and can quarantine the observer; record an explicit failure/dead-letter event instead.
|
|
291
|
+
|
|
292
|
+
## Compliance (PII)
|
|
293
|
+
|
|
294
|
+
**[contract]** Chronicle encrypts `[PII]`-marked values per subject; decryption is transparent to observers and queries (read models injected into a `CommandValidator<T>` or `Handle()` decrypt transparently under the command's resolved subject before validation/handler logic runs).
|
|
295
|
+
|
|
296
|
+
- Prefer **concept-level `[PII]`** (on the `ConceptAs<T>` type) so the marker travels everywhere automatically; use property-level `[PII]` only when a value is personal in one event context but not everywhere.
|
|
297
|
+
- `[PII]` also keeps the value **off the causation chain** when it is a command property — see [Causation](#causation--what-a-command-records-contract). A secret that is not personal data needs `[NotAudited]` instead; `[PII]` is the wrong tool for a password.
|
|
298
|
+
- **`[PII]` is found at any nesting depth, and may mark a whole value object.** A `[PII]` concept inside a value object is encrypted where it sits (siblings stay readable). Marking the **value object type** (or a property typed as one) with `[PII]` classifies every value it holds — Chronicle pushes the marker down to the individual values, so the document keeps its shape and each value is separately encrypted rather than fused into one blob.
|
|
299
|
+
- Projection-backed read models inherit PII lineage automatically — don't add `[PII]` to their properties. Reducer-backed models: a `[PII]`-concept-typed property is detected automatically; add property-level `[PII]` only for a primitive/non-PII-concept property populated from PII source data.
|
|
300
|
+
- ⚠️ **`[PII]` cannot be applied to `EventSourceId`/`EventSourceId<T>`** — Chronicle throws `PIINotSupportedOnEventSourceId` at runtime because the identifier is the encryption-key lookup. If the identifier itself is sensitive, use a random `Guid`-backed surrogate as the event source id and store the sensitive value in a `[PII]` property.
|
|
301
|
+
- **PII inside a list/array element IS encrypted.** The compliance walk descends into nested objects *and* into array elements, so a `[PII]` concept in an `IReadOnlyList<T>` and a `[PII]` member of an element object are both encrypted per element. `[PII]` on the *list property itself* is different: it blob-encrypts the whole collection as one value, protecting it but making per-element values unqueryable — reach for that only when you never need to query inside the collection. For list-valued PII that must stay queryable, prefer a **person-scoped** read model joined at the query edge.
|
|
302
|
+
- ⚠️ **Do not declare a read-model property named `_subject`** — Chronicle reserves that field in MongoDB for internal key tracking. After right-to-erasure key deletion, `[PII]` properties return an **empty** value (no exception is thrown).
|
|
303
|
+
|
|
304
|
+
### Subject resolution **[contract]**
|
|
305
|
+
|
|
306
|
+
The compliance **subject** is the encryption-key identity. It is resolved first-match-wins:
|
|
307
|
+
|
|
308
|
+
| Priority | Mechanism |
|
|
309
|
+
| --- | --- |
|
|
310
|
+
| 1 | `Subject` as the second tuple element returned from `Handle()` (computed in the handler) |
|
|
311
|
+
| 2 | `ICanProvideSubject.GetSubject()` on the command |
|
|
312
|
+
| 3 | a `Subject`-typed property on the command |
|
|
313
|
+
| 4 | an `EventSourceId<T>` property (the typed identity is the subject — no attribute) |
|
|
314
|
+
| 5 | `[Subject]` on a **non-`EventSourceId<T>`** property (Arc converts via `ToString()`) |
|
|
315
|
+
|
|
316
|
+
`ICanProvideSubject` lives in `Cratis.Chronicle.Events` (alongside `ICanProvideEventSourceId`/`ICanProvideEventStreamId`). `[Subject]` is *not* aggregate identity. A managed read-model document has **one** subject — never mix multiple people's PII in one document; split into person-scoped models.
|
|
317
|
+
|
|
318
|
+
### Redaction **[contract]**
|
|
319
|
+
|
|
320
|
+
Redaction is a system-event request that removes payload availability from the event log: it replaces the selected event payload in place, preserves the original sequence slot and event context, and records an **`EventRedacted`** marker. It is **not domain undo** — if business state must change, emit a domain event before/alongside the compliance operation. Rewind/rebuild replayable observers after redaction, and make reactor/projection code that consumes broad streams tolerate (filter or no-op on) `EventRedacted`.
|
|
321
|
+
|
|
322
|
+
## Cross-slice patterns **[convention]**
|
|
323
|
+
|
|
324
|
+
Cross-slice access is **read-only and goes through Chronicle** — inject another slice's read model (validator/`Provide()`/`Handle()`), read by explicit key with `IReadModels.GetInstanceById`, mark a `[Passive]` projection for command-side decisions, or react to its events. **Never** instantiate or DI another slice's command/handler/service.
|
|
325
|
+
|
|
326
|
+
**DCB (Dynamic Consistency Boundary):** a command's consistency boundary is exactly the state its validator/`Provide()`/`Handle()` reads — determined at runtime, not pre-declared. Pair with `concurrency: true` stream metadata when a stale read could let two commands both "win."
|
|
327
|
+
|
|
328
|
+
## Dialogs
|
|
329
|
+
|
|
330
|
+
Frontend commands run through Cratis dialog wrappers — **never** import `Dialog` from `primereact/dialog`. Use `CommandDialog` (executes a command) or `Dialog` (data collection) from `@cratis/components`. See [dialogs.md](./dialogs.md).
|
|
331
|
+
|
|
332
|
+
## See also
|
|
333
|
+
|
|
334
|
+
- [general.md](./general.md) — layout, slice types, workflow, quality gates.
|
|
335
|
+
- [concepts.md](./concepts.md) — `ConceptAs<T>` / `EventSourceId<T>` full patterns.
|
|
336
|
+
- [reactors.md](./reactors.md) — reactor signatures and side-effect rules.
|
|
337
|
+
- [specs.md](./specs.md) — `CommandScenario` / `EventScenario` / `ReadModelScenario` / `ReactorScenario`.
|
|
338
|
+
- skills: **event-modeling**, **new-vertical-slice**, **scaffold-feature**, **cratis-command**, **cratis-readmodel**, **add-concept**, **add-projection**, **add-reactor**, **add-business-rule**, **event-type-migrations**, **call-command-from-code**, **query-paging**, **cross-cutting-properties**, **multi-tenancy**.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/*"
|
|
3
|
+
description: "Use when fetching data from the web, CI logs, artifact URLs, signed URLs, Azure Blob URLs, or simple API/text responses. Prefer curl in the terminal over webpage fetch tools for raw data retrieval."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Web Fetching
|
|
7
|
+
|
|
8
|
+
- Prefer `curl` in the terminal for raw remote content such as CI logs, artifact downloads, signed URLs, plain-text endpoints, and JSON APIs.
|
|
9
|
+
- Use webpage/content fetch tools only when the goal is to summarize or inspect rendered page content rather than retrieve the exact response body.
|
|
10
|
+
- For expiring, authenticated, or redirecting URLs, default to `curl -L -s` and then pipe to `head`, `grep`, `sed`, or `jq` as needed.
|
|
11
|
+
- When debugging remote responses, keep the raw output in the terminal path and filter locally instead of depending on fetch tools.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/Documentation/**/*.{md,mdx}"
|
|
3
|
+
paths:
|
|
4
|
+
- "**/Documentation/**/*.md"
|
|
5
|
+
- "**/Documentation/**/*.mdx"
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Writing Correct Code Examples (Technical Docs)
|
|
9
|
+
|
|
10
|
+
Documentation code examples are **copied verbatim** by evaluators. A snippet that uses an API that doesn't exist is worse than no snippet — it breaks on first paste and loses trust. An audit of these docs found **~12 fabricated-API bugs** that had passed review and shipped. The discipline below is how you avoid adding the thirteenth.
|
|
11
|
+
|
|
12
|
+
## The rule: verify every framework API against real source — before you write it
|
|
13
|
+
|
|
14
|
+
For each framework type, attribute, method, prop, hook, or import in an example, confirm it exists and has that exact shape in **source**, not in another doc page (the docs themselves had the bugs):
|
|
15
|
+
|
|
16
|
+
- **C# / backend** — grep real usage in a reference application (e.g. Cratis **Studio**) and the product `Source/` trees of the Cratis repos checked out alongside this one (`Arc/Source`, `Chronicle/Source`). For extension methods, find the `public static … (this <Type> …)` signature and note **which type it extends**.
|
|
17
|
+
- **React / Components** — the authoritative prop names are in the compiled type defs of the installed package (`node_modules/@cratis/components/dist/esm/**/*.d.ts`) or the `Components` source `dist`. Real usage: a reference app's `*.tsx`.
|
|
18
|
+
- **Invented *domain* names are fine** (event/concept/command names like `AuthorRegistered`, `BookId`). Only **framework APIs** must be real. Never invent a framework interface, attribute, prop, method, or import path.
|
|
19
|
+
|
|
20
|
+
## Complete and correct
|
|
21
|
+
|
|
22
|
+
- No pseudo-code, no `// ...` elisions that leave the reader guessing, no props/members that don't exist.
|
|
23
|
+
- A snippet a reader pastes should compile (modulo the invented domain types they'd supply).
|
|
24
|
+
|
|
25
|
+
## Verified gotchas (the real APIs — these are the ones docs kept getting wrong)
|
|
26
|
+
|
|
27
|
+
- Commands/queries are **model-bound**: a `[Command]` record with `Handle()` **on the record**, and `[ReadModel]` records with **static** query methods. The marker/handler interfaces `ICommand`, `ICommandHandler<T>`, `IQuery<T>`, `IQueryHandler<T,R>` **do not exist** — never use them.
|
|
28
|
+
- Bootstrap: `ArcApplication.CreateBuilder(args)` (not `ArcApplicationBuilder.CreateBuilder`). `builder.AddCratisArc()` on the builder (`WebApplicationBuilder`/`IHostBuilder`); `app.UseCratisArc()` on the built app and it takes **no args** (the listen URL comes from `ArcOptions.Hosting.ApplicationUrl`).
|
|
29
|
+
- Read the current user inside `Handle()` by injecting **`IHttpContextAccessor`** and reading `HttpContext?.User` (`ClaimsPrincipal`). There is no `CommandContext.User` and no `IUserAccessor` Arc type. In-`Handle` guards return **`Result<TEvent, ValidationResult>`** (success type first, error type second) + `ValidationResult.Error(...)` — there is no `CommandResult.Forbidden`/`Unauthorized` to return.
|
|
30
|
+
- Components: `DataPage` uses the **compound** `DataPage.Columns` / `DataPage.MenuItems`; the detail prop is **`detailsComponent`** (lowercase) — `detailsTitle`/`initialSizes` are **not** props. Import `DataTableForObservableQuery` from `@cratis/components/DataTables` (the root barrel only re-exports namespaces); `DataPage`/`MenuItem` from `@cratis/components/DataPage`. Required props like `emptyMessage`/`title` must be present.
|
|
31
|
+
- Chronicle model-bound projections use property attributes **`[SetFrom<T>]`** / **`[SetValue<T>]`** (and `[FromEvent<T>]` AutoMap) — **not** `static On(event)` methods (that shape does not exist). Retrieve a read model with `eventStore.ReadModels.GetInstanceById<T>(id)`. Assertion signatures depend on the extension receiver: the out-of-process `IChronicleSetupFixture` extension is `ShouldHaveAppendedEvent<TEvent>(sequenceNumber, eventSourceId, validator)`, while Arc's in-process `CommandScenario<TCommand>` extensions use `<TCommand, TEvent>` with the event-source id and optional predicate. Verify the receiver type and its exact extension signature before copying either shape.
|
|
32
|
+
|
|
33
|
+
## Auditing at scale
|
|
34
|
+
|
|
35
|
+
Re-run a snippet-correctness audit periodically — it keeps finding bugs (the list above came from three rounds). Delegate the cross-checking to subagents that compare each snippet to source and report only confirmed discrepancies; **verify each finding against source yourself before fixing**. Consider adopting an automated example tester (**Doc Detective**, **Squidler** — from awesome-docs) that actually runs the snippets, so correctness is enforced by CI rather than by hand. The principle, from jvns's "write good examples by starting with real code": derive examples from working source, don't compose them from memory.
|