diff --git a/.github/agents/coordinator.md b/.github/agents/coordinator.md new file mode 100644 index 0000000..f5085f4 --- /dev/null +++ b/.github/agents/coordinator.md @@ -0,0 +1,151 @@ +````chatagent +--- +name: Coordinator +description: > + General-purpose coordinator agent for Cratis-based projects. + Receives a high-level goal, breaks it into parallelisable tasks, + assigns each task to the right specialist agent, tracks progress, + and enforces quality gates before declaring the work done. + Use this agent when a request spans multiple concerns (backend + frontend, + multiple slices, mixed C#/TypeScript work, or requires both implementation + and review). +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - terminalLastCommand +--- + +# Coordinator + +You are the **Coordinator** for Cratis-based projects. +You do NOT write code yourself — you decompose goals into tasks and delegate each task to the right specialist agent. + +Always read and follow: +- `.github/copilot-instructions.md` +- `.github/instructions/vertical-slices.instructions.md` + +--- + +## Available specialist agents + +| Agent | Handles | +|---|---| +| `backend-developer` | C# slice files — commands, events, validators, constraints, projections, reactors | +| `frontend-developer` | React/TypeScript components, composition pages, routing | +| `spec-writer` | Integration specs (C#) and unit specs (TypeScript) | +| `code-reviewer` | Architecture conformance, C# and TypeScript standards, review checklist | +| `security-reviewer` | Security vulnerabilities, injection, auth/authz, data exposure | +| `performance-reviewer` | Chronicle projections, MongoDB query patterns, .NET allocations, React render overhead | + +For vertical slice work, also delegate to the **`planner`** agent when the request involves one or more full slices end-to-end. + +--- + +## Decomposition process + +When you receive a goal: + +1. **Classify the work** — is this a vertical slice implementation, a review, a refactor, a documentation task, or a mix? +2. **Identify components** — list all backend, frontend, spec, and review tasks required. +3. **Identify dependencies** — which tasks block which? (e.g. backend must finish before frontend). +4. **Group into phases** — tasks with no mutual dependencies go in the same phase and can run in parallel. +5. **Assign agents** — pick the right specialist for each task. +6. **Output a plan** — always as a markdown checklist with agent assignments. + +--- + +## Parallelisation rules + +- Tasks in the **same phase** have no mutual dependencies and can be delegated in parallel. +- **Backend before frontend** — TypeScript proxies are generated by `dotnet build`; frontend cannot start until backend is compiled. +- **Specs after backend** — integration specs depend on the slice file existing and compiling. +- **Build is a synchronisation point** — `dotnet build` must succeed before any frontend or spec work begins. +- **Quality gates are last** — code review and security review run after all implementation is complete. +- **Independent features** (no shared events) can have their backends worked on in parallel. + +--- + +## Plan template + +```markdown +## Coordinator Plan: + +### Phase 1 — [can run in parallel] +- [ ] [] +- [ ] [] + +### Phase 2 — (depends on Phase 1) +- [ ] [] + +### Phase 3 — Build +- [ ] Run `dotnet build` — must succeed before any Phase 4 work + +### Phase 4 — [can run in parallel] +- [ ] [] + +### Phase 5 — Quality Gates +- [ ] [code-reviewer] Review all changed files +- [ ] [security-reviewer] Security review of all changed files +``` + +--- + +## Delegation instructions + +When handing off to a specialist agent: + +1. State **exactly which files** need to be created or modified. +2. Provide **all context** the agent needs — feature name, slice name, slice type, existing events, namespace root. +3. State **acceptance criteria** — what "done" looks like for this task. +4. Tell the specialist **which agent to hand back to** when finished. +5. Quote the **relevant instruction file** section that governs the work. + +--- + +## Quality gate criteria + +The work is **not done** until all of the following pass: + +- [ ] `dotnet build` — zero errors, zero warnings +- [ ] `dotnet test` — all specs pass +- [ ] `yarn lint` — zero errors (if frontend present) +- [ ] `npx tsc -b` — zero TypeScript errors (if frontend present) +- [ ] `code-reviewer` finds no blocking issues +- [ ] `security-reviewer` finds no vulnerabilities +- [ ] PR description follows the pull request template + +--- + +## When to delegate to the planner instead + +If the entire goal is one or more vertical slices (full backend-to-frontend), delegate directly to the **`planner`** agent rather than coordinating slice phases yourself. The planner is optimised for slice sequencing. Use the Coordinator for cross-cutting work that involves concerns beyond a single slice pipeline (e.g. shared infrastructure changes + slice implementation, documentation updates + implementation, multi-feature refactors). + +--- + +## Output format + +Always output a plan before starting any delegation: + +```markdown +## Coordinator Plan: + +### Phase 1 — Backend [parallel] +- [ ] [backend-developer] + +### Phase 2 — Build +- [ ] `dotnet build` + +### Phase 3 — Frontend + Specs [parallel] +- [ ] [frontend-developer] +- [ ] [spec-writer] + +### Phase 4 — Quality Gates +- [ ] [code-reviewer] Review all changed files +- [ ] [security-reviewer] Security review +``` + +Then delegate each task in order, respecting phase boundaries. + +```` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index ad016d7..be53fd6 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -16,22 +16,25 @@ When these instructions don't explicitly cover a situation, apply these values t ## General +- When adding new instructions or rules, always place them in the most specific location that applies — the relevant `.instructions.md` file (e.g. `typescript.instructions.md`, `csharp.instructions.md`) or a `SKILL.md` if it relates to a specific workflow. Only add something to `copilot-instructions.md` if it genuinely applies to every file and every context in the project. - Always use American English spelling in all code, comments, and documentation (e.g. "color" not "colour", "behavior" not "behaviour"). - Write clear and concise comments for each function. - Make only high confidence suggestions when reviewing code changes. - Never change global.json unless explicitly asked to. - Never change package.json or package-lock.json files unless explicitly asked to. - Never change NuGet.config files unless explicitly asked to. -- Always ensure that the code compiles without warnings. +- Always ensure that the code compiles without warnings. **Warnings are treated as errors** — a build with warnings is a failing build. - Always ensure that the code passes all tests. - Always ensure that the code adheres to the project's coding standards. - Always ensure that the code is maintainable. - Always reuse the active terminal for commands. - Do not create new terminals unless current one is busy or fails. +- **A task is not complete until the code has been built and every warning and error has been resolved.** Never hand back to the user with outstanding build warnings or errors. ## Development Workflow -- After creating each new file, run `dotnet build` (C#) or `yarn compile` (TypeScript) immediately before proceeding to the next file. Fix all errors as they appear — never accumulate technical debt. +- After creating each new file, run `dotnet build` (C#) or `yarn compile` (TypeScript) immediately before proceeding to the next file. Fix all errors **and warnings** as they appear — never accumulate technical debt. +- Before marking any task done, run a final build (`dotnet build` for C#, `yarn compile` for TypeScript) and confirm it exits with zero errors and zero warnings. If there are warnings or errors, fix them before considering the task complete. - Before adding parameters to interfaces or function signatures, review all usages to ensure the new parameter is needed at every call site. - When modifying imports, audit all occurrences — verify additions are used and removals don't break other files. @@ -46,6 +49,7 @@ These guides contain the full rules, examples, and rationale for each topic. The - [Entity Framework Core Specs](./instructions/efcore.specs.instructions.md) - [Concepts (ConceptAs)](./instructions/concepts.instructions.md) - [Documentation](./instructions/documentation.instructions.md) + - [Git Commits](./instructions/git-commits.instructions.md) - [Pull Requests](./instructions/pull-requests.instructions.md) - [Vertical Slices](./instructions/vertical-slices.instructions.md) - [TypeScript Conventions](./instructions/typescript.instructions.md) diff --git a/.github/hooks/agent-stop.md b/.github/hooks/agent-stop.md index 5c4b667..08916e1 100644 --- a/.github/hooks/agent-stop.md +++ b/.github/hooks/agent-stop.md @@ -24,7 +24,7 @@ When the agent finishes a session, automatically build every affected project in ``` Run from the package root that owns the changed files. -4. **If any build fails**: +4. **If any build fails or any warnings are reported**: - Report the full compiler output. - Fix all errors and warnings before considering the session complete. - Re-run the Release build to confirm it is clean. @@ -34,3 +34,4 @@ When the agent finishes a session, automatically build every affected project in - Always run the Release build — never skip it even if the Debug build already passed earlier in the session. - A session is not complete until `dotnet build -c Release` (and `yarn build` if applicable) exits with code `0` and **zero** warnings. - Treat Release-only warnings (nullable annotations, unused variables stripped by the analyzer, etc.) as errors — fix them. +- **Never** use `/clp:ErrorsOnly` or any flag that suppresses warning output. Warnings that are hidden are warnings that are never fixed. Always let the full diagnostic output through so that zero warnings can be confirmed. diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md index e9526bd..222f76a 100644 --- a/.github/instructions/csharp.instructions.md +++ b/.github/instructions/csharp.instructions.md @@ -19,6 +19,7 @@ These rules exist so that every file in the codebase reads the same way. When fo - Apply code-formatting style defined in `.editorconfig`. - Use file-scoped namespace declarations — one less level of indentation for the entire file. - Use single-line `using` directives, sorted alphabetically. +- Never qualify a type that is already unambiguously in scope via a `using` directive. When two `using` directives introduce conflicting type names, qualify only the conflicting occurrences using the shortest unambiguous path (e.g. `Concepts.Events.Foo` or `Contracts.Events.Foo`) — do not add `using` aliases for every conflicting type. - Insert a blank line before the opening `{` of every code block (`if`, `for`, `foreach`, `try`, `using`, etc.). - Ensure the final `return` statement of a method is on its own line. - Use pattern matching and switch expressions wherever possible — they are more readable and the compiler verifies exhaustiveness. diff --git a/.github/instructions/efcore.instructions.md b/.github/instructions/efcore.instructions.md index da0862e..db82457 100644 --- a/.github/instructions/efcore.instructions.md +++ b/.github/instructions/efcore.instructions.md @@ -4,6 +4,9 @@ applyTo: "**/*.cs" # Entity Framework Core Instructions +> **⚠️ APPLIES ONLY TO PROJECTS USING ENTITY FRAMEWORK CORE** +> If your project does not reference `Microsoft.EntityFrameworkCore` or any EF Core packages, **ignore this entire file**. These rules are irrelevant outside of EF Core contexts. + ## Project Structure Responsibilities are split across three projects: diff --git a/.github/instructions/efcore.specs.instructions.md b/.github/instructions/efcore.specs.instructions.md index d6387dd..d8866af 100644 --- a/.github/instructions/efcore.specs.instructions.md +++ b/.github/instructions/efcore.specs.instructions.md @@ -4,6 +4,9 @@ applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs" # Entity Framework Core Specs +> **⚠️ APPLIES ONLY TO PROJECTS USING ENTITY FRAMEWORK CORE** +> If your project does not reference `Microsoft.EntityFrameworkCore` or any EF Core packages, **ignore this entire file**. These rules are irrelevant outside of EF Core contexts. + EF Core specs verify database interaction logic — migrations, queries, and error handling. They use SQLite in-memory databases, which are fast, isolated, and disposable. This keeps specs independent of any real database server. These specs are for code that interacts directly with `DbContext`. For event-sourcing integration specs (testing commands against Chronicle), use the Chronicle integration spec pattern in [specs.csharp.instructions.md](./specs.csharp.instructions.md) instead. diff --git a/.github/instructions/git-commits.instructions.md b/.github/instructions/git-commits.instructions.md new file mode 100644 index 0000000..62322d9 --- /dev/null +++ b/.github/instructions/git-commits.instructions.md @@ -0,0 +1,94 @@ +--- +applyTo: "**/*" +--- + +# How to Write Git Commits + +Commits are the permanent record of how the codebase evolved. Each commit should tell a clear story: *what* changed and *why*. A reviewer reading `git log --oneline` should understand the arc of the work without opening any diffs. + +## Logical Grouping + +Every commit must be a **single logical unit of work**. Group related changes together; separate unrelated changes into distinct commits. + +### What belongs in one commit + +- A bug fix and the spec that proves it. +- A new file and the changes to existing files needed to integrate it (imports, registrations, wiring). +- A refactor that moves or renames code — only the mechanical transformation, nothing else. +- An interface change together with all implementation updates required to compile. + +### What does NOT belong in one commit + +- A bug fix mixed with an unrelated feature. +- Source code changes mixed with unrelated spec additions for a different area. +- Formatting or style cleanups bundled with behavioral changes. +- Multiple independent fixes or features squashed into a single commit. + +### Deciding where to split + +Ask: "If I needed to revert this commit, would I lose exactly one coherent change?" If reverting would undo two unrelated things, it should be two commits. + +Common split points: +1. **Infrastructure / plumbing first** — interface additions, new types, or schema changes that later commits build on. +2. **Core logic second** — the behavioral change that uses the new infrastructure. +3. **Specs / tests third** — the specs that prove the behavioral change works. Specs may also be combined with the core logic commit when they are tightly coupled (e.g., a TDD red-green cycle or a bug fix with its regression test). +4. **Integration or wiring last** — connecting the new behavior to the rest of the system (DI registration, routing, UI hookup). + +When a task produces both source fixes and new integration specs, prefer separate commits for the source changes and the specs — unless the specs are inseparable from the fix (e.g., a single bug fix + its regression test). + +## Commit Messages + +### Format + +``` + + + +``` + +- **Subject line**: imperative mood, present tense. Start with a verb: `Add`, `Fix`, `Remove`, `Rename`, `Extract`, `Update`, `Support`. +- **No period** at the end of the subject line. +- **72-character limit** on the subject line. If you cannot describe the change in 72 characters, the commit is probably too large — split it. +- **Body**: separated from the subject by a blank line. Explain *why*, not *what* (the diff shows the what). Use bullet points for multi-part changes. + +### Good examples + +``` +Fix duplicate key crash in IdentityStorage.Populate + +The upsert used InsertOne which threw on existing identities. +Replace with ReplaceOne using upsert: true. +``` + +``` +Add type-safe event migration API with expression-based builders + +Introduce EventTypeMigration base class with +typed property builders for Split, Join, Rename, and DefaultValue +operations. Migrators are discovered automatically by convention. +``` + +``` +Add integration specs for observer replay on redaction +``` + +### Bad examples + +- `Fix stuff` — meaningless. +- `WIP` — never commit work-in-progress; stage and commit when the unit is complete. +- `Add files` — says nothing about what or why. +- `Fix bug and add new feature and update docs` — three unrelated things. +- `Changed Observer.Handling.cs` — describes a file, not a behavior. + +## When to Commit + +- **After each logical unit passes the build** — `dotnet build` with zero errors and zero warnings, or `yarn compile` with zero errors. +- **Before starting a different kind of work** — about to switch from fixing a bug to adding a feature? Commit the bug fix first. +- **After completing specs for a change** — if the specs are a separate commit from the source change. +- **Never commit code that does not compile.** Every commit must be a buildable, working state of the codebase. + +## Staging Discipline + +- Use `git add ` rather than `git add .` or `git add -A`. Only stage files that belong to the current logical unit. +- Review `git diff --cached` before committing to verify nothing unrelated was staged. +- If you realize mid-commit that unrelated changes are mixed in, unstage them and commit only the related subset. diff --git a/.github/instructions/orleans.instructions.md b/.github/instructions/orleans.instructions.md index 2b06be4..7efeee3 100644 --- a/.github/instructions/orleans.instructions.md +++ b/.github/instructions/orleans.instructions.md @@ -5,6 +5,9 @@ applyTo: "**/*.cs" # Orleans Conventions +> **⚠️ APPLIES ONLY TO PROJECTS USING MICROSOFT ORLEANS** +> If your project does not reference `Microsoft.Orleans` or any Orleans packages, **ignore this entire file**. These rules are irrelevant outside of Orleans contexts. + These conventions apply to projects that use Microsoft Orleans for distributed grain-based actors. ## General diff --git a/.github/instructions/pull-requests.instructions.md b/.github/instructions/pull-requests.instructions.md index 121b159..5f05328 100644 --- a/.github/instructions/pull-requests.instructions.md +++ b/.github/instructions/pull-requests.instructions.md @@ -16,8 +16,12 @@ PR descriptions serve two purposes: they help reviewers understand the change *n ## Commits -- Write clear, concise commit messages in imperative mood: "Add author registration" not "Added author registration" or "Adding author registration". -- Each commit should represent a logical unit of work. Avoid "WIP" or "fix" commits in the final PR — squash or rebase if needed. +See the full [Git Commits guide](./git-commits.instructions.md) for rules on logical grouping, message format, and staging discipline. + +Quick reminders: +- Imperative mood: "Add author registration" not "Added author registration". +- Each commit = one logical unit of work. No WIP commits in the final PR. +- Never mix unrelated changes in a single commit. ## Labels diff --git a/.github/instructions/specs.instructions.md b/.github/instructions/specs.instructions.md index c34b971..3dac23c 100644 --- a/.github/instructions/specs.instructions.md +++ b/.github/instructions/specs.instructions.md @@ -22,31 +22,60 @@ Specs mirror the source structure and read like a sentence when you trace the pa ``` for_/ ├── given/ -│ ├── all_dependencies ← common DI/mock setup -│ └── a_ ← reusable context -├── when_/ ← folder for behaviors with multiple outcomes -│ ├── given/ ← behavior-specific context (optional) +│ ├── all_dependencies ← common DI/mock setup +│ └── a_ ← reusable context +├── when_/ ← folder for behaviors with multiple outcomes +│ ├── given/ ← behavior-specific context (optional) │ │ └── a_ -│ ├── and_ ← individual spec file -│ ├── with_ -│ └── without_ -└── when_ ← single file for single-outcome behaviors +│ ├── and_.cs ← spec file for one outcome +│ ├── and_/ ← OR a sub-folder when that condition itself has multiple outcomes +│ │ ├── with_.cs +│ │ └── without_.cs +│ ├── with_.cs +│ └── without_.cs +└── when_.cs ← single file for single-outcome behaviors ``` +The `and_`, `with_`, `without_`, `having_`, `given_` prepositions work **both** as file names and as folder names. Use a folder when there are multiple outcomes under that condition; use a file when there is only one. + **Naming conventions — read them as English sentences:** | Element | Pattern | Reads as... | |---|---|---| | Unit folder | `for_` | "For the Changeset..." | | Behavior folder | `when_` | "...when adding changes..." | -| Spec file | Descriptive preposition | "...and there are differences" | +| Condition folder or spec file | Descriptive preposition | "...and there are differences" | | Assertion | `should ` | "...it should return true" | -**Allowed prepositions for spec file/class names:** +**Allowed prepositions for spec file/class names (and sub-folder names):** - `and_*` — additional conditions or compound scenarios - `with_*` / `without_*` — specific data or state present/absent - `having_*` — possession or state-based conditions - `given_*` — precondition scenarios +**Critical naming rule — never embed `when` in a spec file or folder name:** +`when` belongs **only** in `when_` folder names. A spec file, spec class, or non-`when_` folder must **never** contain the word `when` anywhere in it. If the name starts with a preposition (`with_`, `and_`, etc.) but also contains `_when_` somewhere in the middle (e.g. `with_a_registered_migration_when_appending_a_generation_1_event`), you have two "whens" in the sentence — which is always wrong. + +The fix is to fold the context into the `when_` folder name itself, then use preposition files/folders for the outcomes: + +``` +# ❌ Wrong — double when in the sentence path +when_appending_event_with_migrations/ +└── with_a_registered_migration_when_appending_a_generation_1_event.cs + +# ❌ Still wrong — unnecessary extra level when a single flat file suffices +when_appending_event_with_migrations/ +└── and_event_is_generation_1/ + └── with_a_registered_migration.cs + +# ✅ Correct — context baked into the when_ folder; outcomes are flat files +when_appending_event_with_registered_migration/ +├── and_event_is_generation_1.cs +├── and_event_is_generation_2.cs +└── and_event_has_default_value.cs +``` + +A sub-folder under `when_` is only needed when that condition has its **own** multiple outcomes that warrant further breakdown. If there is only one outcome per condition, use a flat file. This applies to **all languages** (C#, TypeScript, etc.). + ## What to Specify The goal is to cover *decisions and transformations* — code where bugs hide. Simple plumbing that the compiler already validates is noise. diff --git a/.github/instructions/terminal-commands.instructions.md b/.github/instructions/terminal-commands.instructions.md new file mode 100644 index 0000000..69bfd7a --- /dev/null +++ b/.github/instructions/terminal-commands.instructions.md @@ -0,0 +1,17 @@ +--- +--- + +# RTK (Rust Token Killer) - Token-Optimized Commands + +## Golden Rule + +**Always prefix commands with `rtk`**. If RTK has a dedicated filter, it uses it. If not, it passes through unchanged. This means RTK is always safe to use. + +**Important**: Even in command chains with `&&`, use `rtk`: +```bash +# ❌ Wrong +git add . && git commit -m "msg" && git push + +# ✅ Correct +rtk git add . && rtk git commit -m "msg" && rtk git push +``` diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index f7e13bf..1328a90 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -156,6 +156,7 @@ Declarative form component with built-in field types, validation timing (`valida - Always ensure that the code compiles without warnings — use `yarn compile` to verify (successful runs produce no output). - Review each file for lint compliance before finalizing. - Never use placeholder or temporary types — use proper types from the start. +- **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. ## Folder Structure diff --git a/.github/instructions/vertical-slices.instructions.md b/.github/instructions/vertical-slices.instructions.md index e983ca2..8cb4ef2 100644 --- a/.github/instructions/vertical-slices.instructions.md +++ b/.github/instructions/vertical-slices.instructions.md @@ -6,41 +6,29 @@ applyTo: "**/Features/**/*.*" A vertical slice contains *everything* for 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 in one folder because everything changes together. -This is the opposite of layered architecture. In a layered project, adding "author registration" means touching `Commands/`, `Handlers/`, `Events/`, `Projections/`, `Models/`, and `Components/` — six folders across two tech stacks. In a vertical slice, it's one folder with one `.cs` file and one `.tsx` file. A developer working on a feature sees its entire scope without navigating elsewhere. - ## Technical Stack -- .NET with C# 13 (ASP.NET Core) -- React + TypeScript (Vite) -- PrimeReact UI component library -- Cratis Arc for CQRS application model -- Cratis Chronicle for event sourcing -- MongoDB for read models -- Vitest + Mocha/Chai/Sinon for TypeScript specs +- .NET with C# 13 (ASP.NET Core) — Cratis Arc for CQRS, Cratis Chronicle for event sourcing, MongoDB for read models +- React + TypeScript (Vite) — PrimeReact UI, Vitest + Mocha/Chai/Sinon for specs - xUnit + Cratis.Specifications + NSubstitute for C# specs ## Core Rules These are non-negotiable because the frameworks rely on them for convention-based discovery and proxy generation: -- **Each vertical slice has its own folder with a single `.cs` file containing ALL backend artifacts.** This keeps cohesion high — you never wonder which file has the event or the validator. It's all in one place. -- **Commands define `Handle()` directly on the record — never create separate handler classes.** Arc discovers `Handle()` by convention; a separate handler class breaks that discovery. -- **`[EventType]` must have NO arguments — the type name is used automatically.** Adding a GUID or string argument is a mistake from other frameworks. -- Complete one slice end-to-end before starting the next. Half-finished slices create confusion and merge conflicts. +- **Each vertical slice has its own folder with a single `.cs` file containing ALL backend artifacts.** +- **Commands define `Handle()` directly on the record — never create separate handler classes.** +- **`[EventType]` must have NO arguments — the type name is used automatically.** +- Complete one slice end-to-end before starting the next. - Drop the `.Features` segment from namespaces (e.g. `MyApp.Projects.Registration` not `MyApp.Features.Projects.Registration`). --- ## Proxy Generation — Build Dependency -Commands and Queries generate TypeScript proxies at build time via `dotnet build`. This creates `.ts` files that the frontend imports (hooks, execute methods, change tracking). Until the backend compiles, **no proxy files exist** and frontend code cannot reference them. - -**This is a hard sequencing constraint:** -1. Backend C# code must be written and compile successfully first. -2. `dotnet build` must complete — this generates the TypeScript proxy files. -3. Only then can frontend React components import and use the generated proxies. +`dotnet build` generates TypeScript proxies. Until the backend compiles, **no proxy files exist** and frontend code cannot reference them. -**When running parallel agents or sub-agents:** backend and frontend work for the same slice **cannot** run in parallel. The backend agent must finish and `dotnet build` must succeed before the frontend agent starts. Independent slices (no shared events) can have their backends worked on in parallel, but each slice's frontend still depends on its own backend build completing first. +**Sequencing constraint:** Backend → `dotnet build` → Frontend. Backend and frontend for the same slice **cannot** run in parallel. --- @@ -50,7 +38,7 @@ Commands and Queries generate TypeScript proxies at build time via `dotnet build |---|---|---| | **State Change** | Mutates system state | Command + events + validators/constraints | | **State View** | Projects events into queryable read models | Read model + projection + queries | -| **Automation** | Reacts to read models, makes decisions | Reactor + local read models | +| **Automation** | Reacts to events, makes decisions | Reactor + local read models | | **Translation** | Adapts events across slices/systems | Reactor → triggers commands in own slice | --- @@ -76,12 +64,10 @@ Features/ Features/Authors/ ├── Authors.tsx ├── AuthorId.cs -├── AuthorName.cs ├── Registration/ │ ├── Registration.cs ← command + event + constraint │ ├── AddAuthor.tsx │ └── when_registering/ -│ ├── and_there_are_no_authors.cs │ └── and_name_already_exists.cs └── Listing/ ├── Listing.cs ← read model + projection + query @@ -93,435 +79,99 @@ Features/Authors/ Features/Authors/ ├── Commands/RegisterAuthor.cs ├── Handlers/RegisterAuthorHandler.cs -├── Events/AuthorRegistered.cs +└── Events/AuthorRegistered.cs ``` --- ## What Goes in a Single Slice File -A single `.cs` file contains ALL of: - -- `[Command]` records with `Handle()` method -- `CommandValidator` (if needed) -- `IConstraint` implementations (if needed) -- `[EventType]` records -- `[ReadModel]` records with static query methods (State View slices) -- `IProjectionFor` or model-bound projection attributes (State View slices) -- `IReducerFor` implementations (if needed) -- `IReactor` implementations (Translation/Automation slices) -- Slice-specific `ConceptAs` types +A single `.cs` contains ALL of: `[Command]` records with `Handle()`, validators, constraints, `[EventType]` records, `[ReadModel]` records with static query methods, projections/reducers, reactors, and slice-specific concepts. --- -## Events - -Events are **facts** — immutable records of things that have already happened. They are the write-side of the system and form a perfect audit trail. Once appended, an event is never modified or deleted; if something needs to be "undone," append a compensating event. +## Events — Rules -Immutable records decorated with `[EventType]` from `Cratis.Chronicle.Events`. +- `[EventType]` takes **no arguments** — the type name is the identifier. +- Past tense naming: `AuthorRegistered`, `BookReserved`, `AddressChanged`. +- Never nullable properties — if something is optional, you need a second event. +- One purpose per event — never multipurpose events with many nullable fields. ```csharp [EventType] public record AuthorRegistered(AuthorName Name); ``` -**Rules:** -- `[EventType]` takes **no arguments** — the type name is the event identifier. -- Use past tense: `ItemAddedToCart`, `UserRegistered`, `AddressChangedForPerson`. Past tense reinforces that events describe what *happened*, not what *should happen*. -- Events are facts — never nullable properties. Nullable properties mean the event's meaning is ambiguous, forcing every observer to add logic to interpret it. If a property is genuinely optional, you need a second event. -- One purpose per event — never multipurpose events. A `UserUpdated` event with 10 nullable fields is a design smell; use `EmailChanged`, `NameChanged`, `AddressChanged` instead. - -**❌ WRONG:** -```csharp -[EventType("ce956ea9-...")] // ❌ No GUID argument -[EventType("AuthorRegistered")] // ❌ No string argument -``` - ---- - -## Commands - -Records decorated with `[Command]` from `Cratis.Arc.Commands.ModelBound`. - -```csharp -[Command] -public record RegisterAuthor(AuthorName Name) -{ - public (AuthorId, AuthorRegistered) Handle() - { - var authorId = AuthorId.New(); - return (authorId, new(Name)); - } -} -``` - -**`Handle()` return types:** -- Single event: `public AuthorRegistered Handle() => new(Name);` -- Tuple (event + result): `public (AuthorId, AuthorRegistered) Handle() => ...` -- `Result`: `public Result Handle(Book book) => ...` -- `void` / `Task` for commands without events - -**Event source resolution (in priority order):** -1. Parameter marked with `[Key]` from `Cratis.Chronicle.Keys` -2. Parameter whose type has implicit conversion to `EventSourceId` -3. Implement `ICanProvideEventSourceId` - -**DI in Handle():** Extra parameters (beyond the command and read model arguments) are resolved from DI automatically. - ---- - -## Business Rules via DCB (Dynamic Consistency Boundary) - -Express business rules by accepting a **read model parameter** in `Handle()`. The framework injects the current projected state before invoking the method. - -```csharp -[Command] -public record ReserveBook(ISBN Isbn, MemberId MemberId) -{ - public Result Handle(Book book) - { - if (book.Available <= 0) - return ValidationResult.Error($"No available copies for {Isbn}"); - - return new BookReserved(Isbn, MemberId); - } -} -``` - -Throw a **custom exception** (never built-in) to signal a violation — the framework converts it to a failed `CommandResult`. - ---- - -## Command Validation - -Use `CommandValidator` from `Cratis.Arc.Commands` (extends FluentValidation): - -```csharp -public class RegisterAuthorValidator : CommandValidator -{ - public RegisterAuthorValidator() - { - RuleFor(c => c.Name).NotEmpty().WithMessage("Author name is required"); - } -} -``` - -Validators with DI dependencies: -```csharp -public class MyValidator : CommandValidator -{ - public MyValidator(IMyService service) - { - RuleFor(x => x).MustAsync(async (cmd, ct) => await service.IsValid(cmd)); - } -} -``` - ---- - -## Constraints - -Server-side rules enforced by Chronicle at event-append time. - -**Unique property across events:** -```csharp -public class UniqueAuthorName : IConstraint -{ - public void Define(IConstraintBuilder builder) => builder - .Unique(_ => _ - .On(e => e.Name) - .On(e => e.Name) - .RemovedWith() - .WithMessage("Author name must be unique")); -} -``` - -**Unique event type per event source:** -```csharp -public class UniqueUser : IConstraint -{ - public void Define(IConstraintBuilder builder) => - builder.Unique(); -} -``` - ---- - -## Read Models & Projections - -Read models are the *read side* of the system — purpose-built views optimized for specific queries. A single events stream can feed many projections, each building a different view of the same data. This is by design: specialized projections are easier to maintain, perform better, and never break unrelated features when one query's needs change. - -### Model-Bound (Preferred — Attributes) - -```csharp -[ReadModel] -[FromEvent] -public record Author( - [Key] AuthorId Id, - AuthorName Name) -{ - public static ISubject> AllAuthors(IMongoCollection collection) => - collection.Observe(); -} -``` - -**Key attributes:** -| Attribute | Purpose | -|---|---| -| `[Key]` | Read model primary key | -| `[FromEvent]` | Convention-based auto-mapping from event | -| `[FromEvent(key: nameof(...))]` | Custom key from event property | -| `[SetFrom]` | Explicit property mapping from event | -| `[AddFrom]` / `[SubtractFrom]` | Arithmetic operations | -| `[Increment]` / `[Decrement]` | ±1 counters | -| `[Count]` | Absolute count | -| `[ChildrenFrom]` | Child collection from event | -| `[Join]` | Join data from another event stream | -| `[RemovedWith]` | Remove entry when event occurs | -| `[Passive]` | On-demand projection, not actively observed | -| `[NotRewindable]` | Forward-only, cannot be replayed | - -### Fluent Projection (Alternative — for complex cases) - -```csharp -public class AuthorProjection : IProjectionFor -{ - public void Define(IProjectionBuilderFor builder) => builder - .From(); -} -``` - -AutoMap is **on by default** — you only need `.AutoMap()` if you previously called `.NoAutoMap()`. Most projections just call `.From<>()` directly. - -**Complex projection with joins:** -```csharp -public class BorrowedBooksProjection : IProjectionFor -{ - public void Define(IProjectionBuilderFor builder) => builder - .From(from => from - .Set(m => m.UserId).To(e => e.UserId) - .Set(m => m.Borrowed).ToEventContextProperty(c => c.Occurred)) - .Join(j => j - .On(m => m.Id) - .Set(m => m.Title).To(e => e.Title)) - .RemovedWith(); -} -``` - -**Projections join EVENTS, never read models.** This is fundamental to event sourcing — projections rebuild state from the event stream, not from other projections. A projection that reads from another read model creates a hidden dependency and breaks replay. - -### Passive Projections - -For read models used only in DCB business rules (not for queries): -```csharp -public class BookProjection : IProjectionFor -{ - public void Define(IProjectionBuilderFor builder) => builder - .Passive() - .From(e => e.Increment(book => book.Available)) - .From(e => e.Decrement(book => book.Available)); -} -``` - --- -## Queries - -Static methods on `[ReadModel]` records. Favor reactive queries (`ISubject`) for real-time updates. - -```csharp -public static ISubject> AllAuthors(IMongoCollection collection) => - collection.Observe(); - -public static Author? ById(IMongoCollection collection, AuthorId id) => - collection.Find(a => a.Id == id).FirstOrDefault(); -``` +## Commands — Rules -Method parameters are DI-resolved. `IMongoCollection` is automatically available for `[ReadModel]` types. - ---- - -## Reducers - -Reducers are the imperative counterpart to projections. Where projections are declarative mappings, reducers give you full control: receive the current state and an event, return the new state. Use them when mapping alone can't express the logic — aggregations, conditional updates, or complex transformations. - -```csharp -public class AccountBalanceReducer : IReducerFor -{ - public AccountBalance OnDepositMade(DepositMade @event, AccountBalance? current, EventContext context) - { - var balance = current?.Balance ?? 0m; - return new AccountBalance(balance + @event.Amount, context.Occurred); - } -} -``` +- `[Command]` record from `Cratis.Arc.Commands.ModelBound`. +- `Handle()` defined directly on the record — no separate handler class. +- `Handle()` returns: single event, tuple `(event, result)`, `Result`, or `void`. +- Event source resolved from: `[Key]` parameter → `EventSourceId`-convertible type → `ICanProvideEventSourceId`. +- Business rules via DCB: accept a read model parameter in `Handle()` — the framework injects current state. -- `current` is `null` for the first event — always handle initialization gracefully. -- Keep reducers pure — no side effects, no database calls, no I/O. They must be safe to replay. -- Use immutable state with `with` expressions on records. +→ For step-by-step command creation, invoke the **`cratis-command`** skill. --- -## Reactors - -Reactors are the "if this then that" of event sourcing — they observe events and produce side effects. Unlike projections (which build state) and reducers (which transform state), reactors *do things*: send emails, trigger commands in other slices, call external APIs. +## Read Models & Projections — Rules -Side-effect observers for Translation and Automation slices. +- Prefer model-bound attributes (`[ReadModel]`, `[FromEvent]`, `[Key]`, etc.) over fluent `IProjectionFor`. +- AutoMap is **on by default** — call `.From()` directly, no `.AutoMap()` call needed. +- Projections join **events**, never read models. +- Query methods are **static** methods on the `[ReadModel]` record. +- Favor reactive queries (`ISubject`) for real-time updates. -```csharp -public class StockKeeping(ICommandPipeline commandPipeline) : IReactor -{ - public async Task HandleBookReserved(BookReserved @event) => - await commandPipeline.Execute(new DecreaseStock(@event.Isbn)); -} -``` - -- `IReactor` is a marker interface — method dispatch is by first-parameter type. -- Method name can be anything descriptive. -- `EventContext` parameter is optional. -- `[OnceOnly]` — method executes only on first processing, skipped during replay. -- If a reactor throws, the failing event source partition pauses until resolved. +→ For step-by-step read model creation, invoke the **`cratis-readmodel`** skill. +→ For adding a projection to an existing model, invoke the **`add-projection`** skill. --- -## Event Seeding +## Concepts — Rules -Provide predefined events at startup. Chronicle deduplicates automatically. +- Prefer `ConceptAs` over raw primitives everywhere in domain models, commands, events, and queries. +- Concepts shared between slices → feature folder. Shared between features → `Features/` root. One file per concept. -```csharp -public class EmployeeSeeding : ICanSeedEvents -{ - public void Seed(IEventSeedingBuilder builder) - { - builder.ForEventSource("employee-1", [ - new PersonHired("John", "Doe", "Engineer"), - new EmployeeAddressSet("123 Main St", "Springfield", "62701", "US") - ]); - } -} -``` - -Use `#if DEBUG` to prevent seeding in production. +→ See [concepts.instructions.md](./concepts.instructions.md) for full rules. +→ For step-by-step concept creation, invoke the **`add-concept`** skill. --- -## Concepts +## Reactors — Rules -- Prefer `ConceptAs` over raw primitives everywhere. -- Concepts shared between slices → feature folder. -- Concepts shared between features → `Features/` root. -- One file per concept. +- `IReactor` is a marker interface — method dispatch is by first-parameter event type. +- Reactors observe events and produce side effects — never use `IEventLog` directly from a reactor. +- If a reactor needs to write new events, execute a command via `ICommandPipeline`. +- Design for idempotency — reactors may be called more than once. -See [Concepts Instructions](./concepts.instructions.md) for full details. +→ See [reactors.instructions.md](./reactors.instructions.md) for full rules. +→ For step-by-step reactor creation, invoke the **`add-reactor`** skill. --- ## Development Workflow -Work **slice-by-slice** in this exact order. The sequence matters — TypeScript proxies are generated from C# during `dotnet build`, so frontend work cannot begin until the backend compiles. - -1. **Backend** — Implement the C# slice file (command/query/projection + events, validators, constraints) -2. **Specs** — Write integration specs for state-change slices -3. **Build** — Run `dotnet build` to generate TypeScript proxies (this step creates the `.ts` files the frontend imports) -4. **Frontend** — Implement React component(s) using the auto-generated proxies -5. **Composition** — Register in the feature's composition page -6. **Routes** — Add/update routing if needed - -### Sub-agent coordination - -When parallel agents or sub-agents are used, steps 1–3 are a **hard prerequisite** for step 4. Backend and frontend for the same slice cannot run in parallel — the frontend agent must wait for `dotnet build` to complete. Independent slices (no shared event types) can have their backend phases worked on in parallel, but each slice's frontend still depends on its own build completing first. - ---- - -## Integration Specs +Work **slice-by-slice** in this exact order: -Specs live under `when_/` folders directly inside the slice folder — no `for_` prefix. +1. **Backend** — implement the C# slice file +2. **Specs** — write integration specs for state-change slices +3. **Build** — run `dotnet build` to generate TypeScript proxies +4. **Frontend** — implement React component(s) using the generated proxies +5. **Composition** — register in the feature's composition page +6. **Routes** — add/update routing if needed -```csharp -using context = MyApp.Authors.Registration.when_registering.and_there_are_no_authors.context; - -namespace MyApp.Authors.Registration.when_registering; - -[Collection(ChronicleCollection.Name)] -public class and_there_are_no_authors(context context) : Given(context) -{ - public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture) - { - public CommandResult? Result; - - async Task Because() - { - Result = await Client.ExecuteCommand( - "/api/authors/register", - new RegisterAuthor("John Doe")); - } - } - - [Fact] void should_be_successful() => Context.Result.IsSuccess.ShouldBeTrue(); - [Fact] void should_have_appended_only_one_event() => Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First); - [Fact] void should_append_author_registered_event() => Context.ShouldHaveAppendedEvent( - EventSequenceNumber.First, Context.Result.Response, - evt => evt.Name.Value.ShouldEqual("John Doe")); -} -``` +→ For end-to-end slice implementation, invoke the **`new-vertical-slice`** skill. +→ For creating a new feature folder, invoke the **`scaffold-feature`** skill. --- -## Frontend - -### Listing View - -```tsx -import { DataTable } from 'primereact/datatable'; -import { Column } from 'primereact/column'; -import { AllAuthors } from './AllAuthors'; - -export const Listing = () => { - const [result] = AllAuthors.use(); - - return ( - - - - ); -}; -``` - -### Composition Page - -```tsx -import { Page } from '../../Components/Common'; -import { AddAuthor } from './Registration/AddAuthor'; -import { Listing } from './Listing/Listing'; -import { useDialog } from '@cratis/arc.react/dialogs'; -import { Menubar } from 'primereact/menubar'; -import { MenuItem } from 'primereact/menuitem'; -import * as mdIcons from 'react-icons/md'; - -export const Authors = () => { - const [AddAuthorDialog, showAddAuthorDialog] = useDialog(AddAuthor); - - const menuItems: MenuItem[] = [ - { - label: 'Add Author', - icon: mdIcons.MdAdd, - command: async () => { await showAddAuthorDialog(); } - } - ]; - - return ( - - - - - - ); -}; -``` - -### Dialogs +## Dialogs — Rules - **Never** import `Dialog` from `primereact/dialog` — use Cratis wrappers. - `CommandDialog` from `@cratis/components/CommandDialog` — for dialogs that execute commands. - `Dialog` from `@cratis/components/Dialogs` — for data collection without commands. -- Do not render manual ` + + + ); +}; +``` + +--- + +## Navigation behavior + +| Step | Footer buttons shown | +|------|---------------------| +| First step | **Next** | +| Middle step | **Previous**, **Next** | +| Last step (any step invalid) | **Previous** | +| Last step (all valid) | **Previous**, **Submit** | + +Cancel is always available via the **×** button in the dialog header. + +--- + +## Validation indicators + +Step number circles in the wizard navigation bar change color to reflect validity: + +- **Red circle** — the step contains at least one field with a validation error +- **Green circle** — the step has been visited (navigated through) and all its fields are valid +- **Default color** — the step has not been visited yet +- **Dimmed** — a step that is not the currently active step + +To trigger validation immediately on open (before the user types anything), pass `validateOnInit`: + +```tsx + +``` + +This is useful when the dialog opens with pre-populated values that may already be invalid. + +--- + +## Customizing step labels + +The `okLabel`, `nextLabel`, and `previousLabel` props override the default button text: + +```tsx + +``` + +--- + +## Pre-populating values (edit wizard) + +Use `currentValues` for fields the user can change and `initialValues` for hidden/fixed fields (e.g. an ID): + +```tsx + +``` + +--- + +## Stepper orientation + +For longer wizards, vertical orientation can be more readable: + +```tsx + + ... + ... + +``` + +--- + +## Props reference + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `command` | `Constructor` | — | **Required.** Command class constructor | +| `title` | `string` | — | **Required.** Dialog header title | +| `children` | `StepperPanel[]` | — | **Required.** Wizard steps | +| `okLabel` | `string` | `'Submit'` | Submit button label (last step) | +| `nextLabel` | `string` | `'Next'` | Next button label | +| `previousLabel` | `string` | `'Previous'` | Previous button label | +| `visible` | `boolean` | `true` | Controls dialog visibility | +| `width` | `string` | `'600px'` | Dialog width | +| `isValid` | `boolean` | — | Extra validity gate combined with form validity | +| `validateOnInit` | `boolean` | — | Run validation on mount to show errors immediately | +| `orientation` | `'horizontal' \| 'vertical'` | `'horizontal'` | Stepper layout direction | +| `linear` | `boolean` | `true` | Require steps to be completed in order | +| `initialValues` | `Partial` | — | Fixed initial values (not shown to user as editable) | +| `currentValues` | `Partial` | — | Pre-populated editable values | +| `onConfirm` | `() => void \| Promise` | — | Called after successful execute | +| `onCancel` | `() => void \| Promise` | — | Called when user dismisses dialog | +| `onBeforeExecute` | `(values: TCommand) => TCommand` | — | Transform values before execution | +| `pt` | `StepperProps['pt']` | — | PrimeReact PassThrough for deep DOM customization | + +--- + +## Common mistakes + +| Mistake | Fix | +|---------|-----| +| Putting a Cancel button in the footer | Don't — the × in the dialog header is the cancel action | +| One step per field | Group related fields; aim for 2–5 fields per step | +| Fields from different steps sharing the same `value` accessor | Each property should appear on exactly one step | +| Forgetting `header` on `StepperPanel` | Always set `header` — it is the navigation label | +| Using `CommandDialog` for a 4+ field form | Consider `StepperCommandDialog` to reduce cognitive load | diff --git a/.github/skills/write-documentation/SKILL.md b/.github/skills/write-documentation/SKILL.md index b1cc792..10c1693 100644 --- a/.github/skills/write-documentation/SKILL.md +++ b/.github/skills/write-documentation/SKILL.md @@ -91,6 +91,7 @@ The project's voice is **direct, practical, and opinionated**. Write like an exp - Use headings, lists, and code blocks to organize content — dense paragraphs lose readers. - Focus on public APIs and features — never internal implementation. - Do not document third-party libraries. +- **American English only.** Always use US spellings: `color` not `colour`, `behavior` not `behaviour`, `customize` not `customise`, `organize` not `organise`, `recognize` not `recognise`, `analyze` not `analyse`, `initialize` not `initialise`. ## Code Examples