From 0c31b0d70971f61e43f329d58c0d247e6aed281c Mon Sep 17 00:00:00 2001 From: Einar Ingebrigtsen Date: Sun, 8 Mar 2026 12:04:56 +0000 Subject: [PATCH 1/2] Bootstrap Copilot sync workflows --- .github/instructions/concepts.instructions.md | 29 --- .github/instructions/csharp.instructions.md | 22 --- .../documentation.instructions.md | 45 ----- .../instructions/efcore.specs.instructions.md | 12 -- .../pull-requests.instructions.md | 9 - .../instructions/specs.csharp.instructions.md | 170 ------------------ .github/instructions/specs.instructions.md | 96 ---------- .../specs.typescript.instructions.md | 151 ---------------- .../propagate-copilot-instructions.yml | 16 ++ .../workflows/sync-copilot-instructions.yml | 16 ++ 10 files changed, 32 insertions(+), 534 deletions(-) delete mode 100644 .github/instructions/concepts.instructions.md delete mode 100644 .github/instructions/csharp.instructions.md delete mode 100644 .github/instructions/documentation.instructions.md delete mode 100644 .github/instructions/efcore.specs.instructions.md delete mode 100644 .github/instructions/pull-requests.instructions.md delete mode 100644 .github/instructions/specs.csharp.instructions.md delete mode 100644 .github/instructions/specs.instructions.md delete mode 100644 .github/instructions/specs.typescript.instructions.md create mode 100644 .github/workflows/propagate-copilot-instructions.yml create mode 100644 .github/workflows/sync-copilot-instructions.yml diff --git a/.github/instructions/concepts.instructions.md b/.github/instructions/concepts.instructions.md deleted file mode 100644 index 338be7e..0000000 --- a/.github/instructions/concepts.instructions.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -applyTo: "**/*.cs" ---- - -# πŸ§ͺ How to use Concepts - -- Don't use primitives like `string`, `int`, `Guid`, etc. directly in your domain models, commands, events, queries, etc. -- Create a concept for the primitive type that encapsulates the value and provides domain-specific behavior. -- Inherit from the appropriate base class provided by Cratis Concepts, such as `ConceptAs` -- `ConceptAs` has a default conversion from `T` to `ConceptAs`. -- When creating a concept, add a implicit conversion operator from `ConceptAs` to `T` for easy extraction of the underlying value. -- Instead of using `null` values, add a static readonly value representing an empty or default state for the concept with appropriate naming for the context (e.g. `Empty`, `NotSet`). -- Don't add Concepts to its own folder, place it in the folder that makes most sense for the context of the concept. - -Example of a concept: - -```csharp -public record AuthorId(Guid Value) : ConceptAs(Value) -{ - public static readonly AuthorId NotSet = new(Guid.Empty); - public static implicit operator AuthorId(Guid value) => new(value); - - // If the concept is used as an identifier for an event source, you can add an implicit conversion to EventSourceId - public static implicit operator EventSourceId(AuthorId id) => new(id.Value.ToString()); - - // For convenience, you can add a method to create a new instance with a new unique value - if needed, makes things cleaner when creating new instances - public static AuthorId New() => new(Guid.NewGuid()); -} -``` diff --git a/.github/instructions/csharp.instructions.md b/.github/instructions/csharp.instructions.md deleted file mode 100644 index 6771936..0000000 --- a/.github/instructions/csharp.instructions.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -applyTo: "**/*.cs" ---- - -# πŸ§ͺ Specific instructions for C# - -## Technology Stack - -- .NET 9 (specified in global.json) -- C# 13 -- ASP.NET Core -- MongoDB.Driver for C# -- Entity Framework Core -- xUnit for testing -- NSubstitute for mocking -- Cratis.Specifications for BDD-style tests - -## Building - -- Don't use the build tasks in Visual Studio. Use `dotnet build` from the command line instead. -- Use `dotnet format` to format code. -- Use `dotnet test` to run tests. diff --git a/.github/instructions/documentation.instructions.md b/.github/instructions/documentation.instructions.md deleted file mode 100644 index 001c5c6..0000000 --- a/.github/instructions/documentation.instructions.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -applyTo: "Documentation/**/*.md" ---- - -# πŸ§ͺ How to Write Documentation - -- All documentation files live in the `Documentation/` folder. -- Use [Markdown](https://www.markdownguide.org/basic-syntax/) for formatting. -- Use [GitHub Flavored Markdown](https://github.github.com/gfm/) for additional features. -- Use [Mermaid](https://mermaid-js.github.io/mermaid/#/) for diagrams. -- Use [PlantUML](https://plantuml.com/) for UML diagrams. -- Site is built using [DocFX](https://dotnet.github.io/docfx/). -- Follow the [DocFX Markdown](https://dotnet.github.io/docfx/markdown/) guidelines for additional syntax. -- Generate and maintain correct [DocFX TOC files](https://dotnet.github.io/docfx/docs/dotnet-yaml-format.html) for navigation. -- Be consistent with formatting and style. -- Use clear and concise language. -- Be concise and to the point. -- Be specific and avoid ambiguity. -- Use examples and code snippets to illustrate concepts. -- Do not add extraneous information. -- Do not document the obvious. -- Do not add documentation for internal or private APIs. -- Do not document internal implementation details. -- Focus on the public APIs and features of the project. -- Do not document third party libraries or tools. -- Use active voice and present tense. -- Use proper grammar and spelling. -- Use a friendly and approachable tone. -- Create focused and well-scoped documents. -- Use consistent terminology throughout the documentation. -- Follow a logical structure and flow. -- Don't be too verbose. -- Use headings, lists, and code blocks to organize content. -- Include links to relevant resources and references. -- Always add an index.md file in each folder to serve as the landing page. -- Maintain the index.md file to include links to all relevant subtopics. -- Use relative links for internal documentation references. -- Ensure all links are valid and up-to-date. -- Use images and diagrams to enhance understanding. -- Emphasize why something is done, not just how. -- Always end the generated markdown with a single empty line inside the file content itself. Never try to add it by running commands like echo or printf β€” it must be part of the markdown text you output. -- Never use shell commands or external tools to modify files after writing them. Everything, including the trailing newline, must be produced as part of the file’s content. -- Every folder should have its own `toc.yml` file to define the structure of the documentation within that folder. -- When linking to a folder in a `toc.yml` file, link to the `toc.yml` file in that folder, not to an `index.md` file. -- Ensure that documentation is accurate according to the public APIs and features of the project. diff --git a/.github/instructions/efcore.specs.instructions.md b/.github/instructions/efcore.specs.instructions.md deleted file mode 100644 index 94476f7..0000000 --- a/.github/instructions/efcore.specs.instructions.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs" ---- - -# How to write Entity Framework Core Specs - -- Use Sqlite in-memory database for specs (tests) that involve `DbContext` operations. This allows for lightweight and fast testing without the need for a full database setup. -- Ensure that the in-memory database is properly configured and disposed of after each test to avoid state leakage between tests. -- When writing specs (tests) for units that leverage a `DbContext` or a derivative of it, remember that methods like `SaveChanges` or `SaveChangesAsync` are virtual and can be mocked. -- Simulating failure on a `DbContext` operation can be achieved by mocking these methods to throw exceptions, allowing you to test error handling and recovery logic in your specs. -- `DbSet` has most of its methods as virtual, which means you can mock them as needed for your tests. -- A `DbContext` needs options passed into its constructor. When mocking with NSubstitute you simply pass the options when substituting it, e.g., `Substitute.For(options)`. diff --git a/.github/instructions/pull-requests.instructions.md b/.github/instructions/pull-requests.instructions.md deleted file mode 100644 index ddabcea..0000000 --- a/.github/instructions/pull-requests.instructions.md +++ /dev/null @@ -1,9 +0,0 @@ -# πŸ§ͺ How to do pull requests - -- Follow the [pull request template](../pull_request_template.md). - - Primarily focus on the Added, Changed, Fixed, Removed, Security, and Deprecated sections. The summary can most likely be omitted. - - The description is used as the GitHub release description so make it nice and descriptive and to the point. -- Ensure your code follows the project's coding standards and guidelines. -- Write clear and concise commit messages. -- Label your pull request appropriately according to semantic versioning for what type of improvement the PR is offering (major, minor, patch). -- Add a link to the issue it addresses on every bullet-point added in parentheses (e.g., (#123)) at the end of the bullet point, if applicable. diff --git a/.github/instructions/specs.csharp.instructions.md b/.github/instructions/specs.csharp.instructions.md deleted file mode 100644 index f80d297..0000000 --- a/.github/instructions/specs.csharp.instructions.md +++ /dev/null @@ -1,170 +0,0 @@ ---- -applyTo: "**/for_*/**/*.cs, **/when_*/**/*.cs" ---- - -# πŸ§ͺ How to Write C# Specs - -Use the base instructions for writing specs can be found in [Specs Instructions](./specs.instructions.md) and -then adapt with the C# specific conventions below. - -## Test Frameworks & Conventions - -- **Frameworks:** - - Uses [Xunit](https://xunit.net/) as test framework and execution. - - Uses [NSubstitute](https://nsubstitute.github.io/) for mocking. - - Uses [Cratis Specifications](https://github.com/Cratis/Specifications/blob/main/README.md) for BDD style specification by example testing. - - Use separate projects for specs, e.g. `Applications.Specs`. - -## Base class - -- At the root of inheritance, `Specification` must be the base class. - -## Test Class Pattern - -- Use BDD-style methods: - - `void Establish()` for setup. - - `void Because()` for the action under test. - - `[Fact] void should_()` for assertions. - - Keep them focused on a single behavior or aspect. - -**Example:** - -```csharp -public class when_adding : Specification -{ - EventsToAppend events; - string @event; - - void Establish() - { - events = []; - @event = "Forty Two"; - } - - void Because() => events.Add(@event); - - [Fact] void should_hold_the_added_event() => events.First().ShouldEqual(@event); -} -``` - -**Example with multiple outcomes:** - -``` -for_EventsCommandResponseValueHandler/ -β”œβ”€β”€ given/ -β”‚ └── an_events_command_response_value_handler.cs -β”œβ”€β”€ when_checking_can_handle/ -β”‚ β”œβ”€β”€ with_valid_events_collection.cs -β”‚ β”œβ”€β”€ with_null_value.cs -β”‚ └── without_event_source_id.cs -└── when_handling/ - β”œβ”€β”€ empty_events_collection.cs - β”œβ”€β”€ single_event_collection.cs - └── multiple_events_collection.cs -``` - -Each test class inherits from `given.an_events_command_response_value_handler`: - -```csharp -public class with_valid_events_collection : given.an_events_command_response_value_handler -{ - IEnumerable _events; - bool _result; - - void Establish() - { - _events = [new TestEvent("Test"), new AnotherTestEvent(42)]; - } - - void Because() => _result = _handler.CanHandle(_commandContext, _events); - - [Fact] void should_return_true() => _result.ShouldBeTrue(); -} -``` - -- Establish method can also be async if it needs to perform tasks that are async. - -## Reusable Context - -- Namespace should include `given` in the name (e.g., `Infrastructure.ReadModels.for_EventsCommandResponseValueHandler.given`) -- Members that are to be reused should be `protected` -- Members are initialized using the `Establish` method, same as regular specs -- Specs inherit from the context by doing `given.a_specific_context`, example: `public class when_performing_a_behavior : given.a_specific_context` -- Remember the inheritance rule: `Specification` must be in the inheritance chain at the root -- Shared variables should be `protected` fields, not properties and they should follow the `_camelCase` naming convention -- **For behaviors with multiple outcomes:** - - The reusable context should be in the unit's `given/` folder (e.g., `for_/given/`) - - All test files within behavior folders (e.g., `when_/`) inherit from this shared context - - This allows consistent setup across all variations of the behavior -- If a specific behavior needs additional setup, create behavior-specific contexts in `when_/given/` folder -- If the system being specified is not constructed in the reusable context, don't keep the variable for it in the context, instead create it in the specific behavior spec -- Reusable contexts can inherit from other reusable contexts to build upon setups -- Use `Establish` method for setup logic in reusable contexts -- When it makes sense, create a root reusable context called `all_dependencies` that sets up all common dependencies for the unit under test that more specific contexts can inherit from -- `Because`method should not be used in reusable contexts, it should be in the specific behavior spec files - -## Async - -- Any of the methods (`Establish`, `Because`, `Cleanup`) can be async if needed. - -## Substitutes - -- Concrete classes can be substituted/mocked, not just interfaces or abstract classes. -- When substituting concrete classes, ensure they have virtual methods or properties to allow mocking behavior. -- Pass constructor parameters as needed when substituting concrete classes. For example, `Substitute.For(param1, param2)`. - -## Test Utilities - -- Use `Substitute.For()` for mocks. -- Cratis Specifications has a set of assertion extension methods, use these - - `.ShouldEqual()` - - `.ShouldBeTrue()` - - `.ShouldBeFalse()` - - `.ShouldBeNull()` - - `.ShouldNotBeNull()` - - `.ShouldBeOfExactType()` - - `.ShouldContain()` - - `.ShouldContainOnly()` - - `.ShouldNotContain()` - - `.ShouldBeEmpty()` - - `.ShouldNotBeEmpty()` - - `.ShouldBeInRange()` - - `.ShouldNotBeInRange()` - - `.ShouldBeGreaterThan()` - - `.ShouldBeGreaterThanOrEqual()` - - `.ShouldBeLessThan()` - - `.ShouldBeLessThanOrEqual()` -- Use custom helpers from `XUnit.Integration` for integration/event-based assertions. - -## Using statements - -- Common usings are provided in `GlobalUsings.Specs.cs` (e.g., `using Xunit;`, `using NSubstitute;`), don't add any using statements already in this file. -- Don't add using statement for namespace of the system under test. - -## Properties and Simple Members - -**Do NOT create specs for:** -- Simple auto-properties (e.g., `public string Name { get; set; }`) -- Properties that return constructor parameters (e.g., `public string TableName => tableName;`) -- Properties that return field values (e.g., `public Key Key => key;`) -- Properties that delegate to other objects without transformation (e.g., `public IEnumerable Properties => mapper.Properties;`) -- Expression-bodied properties that return simple values (e.g., `public Type PropertyName => someValue;`) -- Properties that perform simple null checks or basic validation without complex logic -- Getters that return injected dependencies or configuration values - -**Examples of properties to IGNORE:** -```csharp -// ❌ Do NOT write specs for these -public string TableName => tableName; // Returns constructor parameter -public Key Key => key; // Returns constructor parameter -public IEnumerable Properties => mapper.Properties; // Simple delegation -public bool IsValid => _value is not null; // Simple validation -public ILogger Logger { get; } // Injected dependency -``` - -**Only write specs for properties with complex business logic:** -```csharp -// βœ… These might need specs if they contain complex logic -public decimal TotalCost => Items.Sum(i => i.Cost * i.Quantity * (1 + i.TaxRate)); -public bool CanProcess => ValidateComplexBusinessRules() && CheckExternalConditions(); -``` diff --git a/.github/instructions/specs.instructions.md b/.github/instructions/specs.instructions.md deleted file mode 100644 index 4afdc4b..0000000 --- a/.github/instructions/specs.instructions.md +++ /dev/null @@ -1,96 +0,0 @@ ---- -applyTo: "**/for_*/**/*.*, **/when_*/**/*.*" ---- - -# πŸ§ͺ How to Write Specs - -We call automated tests for specs or specifications based on Specification by Example related to BDD (Behavior Driven Development). -Keep tests focused, isolated, and descriptive! - -## Structure - -- **File/Folder Structure:** - - Organize tests by feature/domain, e.g. `Events/Constraints/for_UniqueConstraintProvider/when_providing`. - - Use descriptive folder and file names: - - `for_/` for the unit under test - - `when_/` for behaviors with multiple outcomes - - Single file `when_` for simple behaviors with single outcomes - - Example: `for_UnitOfWork/when_committing/and_it_has_events_and_append_returns_constraints_and_errors` - -## What to specify - -- Write specs that verify what is promised from the signature of methods, not based on its implementation - we want to catch problems with the implementation. -- **Focus on behaviors (methods that perform actions)**, not simple state (properties that hold or return values). -- **Ignore simple properties** - properties that just return constructor parameters, field values, or delegate to other objects without transformation should not have specs. -- Do not test logging - it's too fragile and has no value. - -## Naming - -- Use clear, descriptive names for test classes and methods. -- Folder: `for_`. -- Class: Single condition - `when_[_and_]`. Use descriptive prepositions that make the condition clear: - - `and_*` for additional conditions or compound scenarios - - `or_*` for alternative outcomes or error paths - - `with_*`, `without_*`, `when_*`, `having_*`, `given_*` for specific contexts - - Example: `when_committing_and_validation_fails`, `when_processing_with_invalid_data` -- Method: `should_` - -## Behaviors - -- Capture the behavior in naming, not just necessarily use names that reflect the code or method name, it should be meaningful as to what the code is doing and trying to accomplish -- **A behavior corresponds to a public method** of the unit under test -- Every public method should be tested separately, never write a single file for an entire unit under test -- Each behavior (public method) should have its own dedicated folder structure when it has multiple outcomes -- For simple behaviors with single outcomes, use a single file: `when_.cs` -- For complex behaviors with multiple aspects/outcomes, use folder structure: `when_/` with multiple test files inside - -## Multiple outcomes of a behavior - -- When a behavior (public method) has multiple outcomes, aspects, or edge cases to test, organize them using folder structure: - - Create a folder named `when_` (e.g., `when_checking_can_handle`, `when_handling`) - - Within this folder, create separate test files for each specific outcome or aspect - - Use descriptive names that clearly indicate the specific condition being tested: - - `with_valid_events_collection` - - `with_null_value` - - `without_event_source_id` - - `empty_events_collection` - - `multiple_events_collection` -- Do not limit naming to `and_*` - use any preposition or descriptor that makes the condition clear: - - `and_*` for additional conditions (e.g., `and_it_has_events`, `and_validation_fails`) - - `or_*` for alternative scenarios (e.g., `or_timeout_occurs`, `or_connection_fails`) - - `with_*` for scenarios with specific data/state (e.g., `with_valid_events_collection`, `with_null_value`) - - `without_*` for scenarios missing data/state (e.g., `without_event_source_id`, `without_permissions`) - - `when_*` for temporal or conditional aspects (e.g., `when_timeout_occurs`, `when_validation_passes`) - - `having_*` for possession/state-based conditions (e.g., `having_multiple_items`, `having_no_data`) - - `given_*` for precondition scenarios (e.g., `given_empty_collection`, `given_invalid_state`) -- Keep a single specific outcome or aspect in each file - don't combine multiple scenarios -- Each test file should inherit from the appropriate reusable context (usually `given.a_`) - -## Reusable Context - -- Context can be encapsulated into reusable contexts that can be leveraged between specs. -- Create a `given` folder within the unit folder (e.g., `for_/given/`) -- Add reusable context classes with descriptive names starting with `a_` or `an_` (e.g., `a_events_command_response_value_handler`) -- Reusable contexts can also be more specific to reflect a certain setup (e.g. `two_queries`, `existing_query`, `non_existing_query`) - -- **For behaviors with multiple outcomes:** - - The reusable context should be in the unit's `given/` folder (e.g., `for_/given/`) - - All test files within behavior folders (e.g., `when_/`) inherit from this shared context - - This allows consistent setup across all variations of the behavior -- If a specific behavior needs additional setup, create behavior-specific contexts in `when_/given/` folder - -## Formatting - -- Don't worry about lines being too long for the `should_` methods - prefer one-line lambda methods for readability of the should statements rather than breaking them into multiple lines. -- With multiple `should_` methods, do not add unnecessary whitespace between them. - -## Properties and Simple Members - -**NEVER write specs for simple properties or getters** - Properties are not behaviors and should not be tested unless they contain complex business logic. - -**Avoid file names starting with:** -- `when_getting_*` (e.g., `when_getting_key.*`, `when_getting_properties.*`) -- `when_returning_*` for simple return values -- Any spec that just verifies a property returns what was passed in the constructor - -Focus specs on **behaviors** (methods that perform actions) rather than **state** (properties that hold or return values). diff --git a/.github/instructions/specs.typescript.instructions.md b/.github/instructions/specs.typescript.instructions.md deleted file mode 100644 index 9405cde..0000000 --- a/.github/instructions/specs.typescript.instructions.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -applyTo: "**/for_*/**/*.ts, **/when_*/**/*.ts" ---- - -# πŸ§ͺ How to Write TypeScript Specs - -Use the base instructions for writing specs can be found in [Specs Instructions](./specs.instructions.md) and -then adapt with the C# specific conventions below. - -## Test Frameworks & Conventions - -- **Frameworks:** - - Uses [Mocha](https://mochajs.org) as test framework and execution. - - Uses [SinonJS](https://sinonjs.org) for mocking. - - Uses [Chai](https://www.chaijs.com) for assertions. - - Uses Vitest for running tests. See [Vitest Documentation](https://vitest.dev/). - - Uses yarn as package manager. - - Tests can be run using `yarn test` from every package. - - Tests are found in alongside the code being tested in folders starting with either `for_`, `when_` or `given_` (for reusable contexts). - -- **File/Folder Structure:** - - Organize tests by feature/domain, e.g. `Events/Constraints/for_UniqueConstraintProvider/when_providing.ts`. - - Use descriptive folder and file names: - - `for_/` for the unit under test - - `when_/` for behaviors with multiple outcomes - - `when_.ts` for simple behaviors with single outcomes - - Example: `for_UnitOfWork/when_committing/and_it_has_events_and_append_returns_constraints_and_errors.ts` - -## Test Class Pattern - -- Use BDD-style methods: - - `void Establish()` for setup. - - `void Because()` for the action under test. - - `[Fact] void should_()` for assertions. - - Keep them focused on a single behavior or aspect. - -**Example:** - -```typescript -describe("when adding", () => { - let events: EventsToAppend; - let event: string; - - beforeEach(() => { - events = []; - event = "Forty Two"; - - events.Add(event); - }); - - it("should hold the added event", () => { - events[0].should.equal(event); - }); -}); -``` - -**Example with multiple outcomes:** - -``` -for_EventsCommandResponseValueHandler/ -β”œβ”€β”€ given/ -β”‚ └── an_events_command_response_value_handler.ts -β”œβ”€β”€ when_checking_can_handle/ -β”‚ β”œβ”€β”€ with_valid_events_collection.ts -β”‚ β”œβ”€β”€ with_null_value.ts -β”‚ └── without_event_source_id.ts -└── when_handling/ - β”œβ”€β”€ empty_events_collection.ts - β”œβ”€β”€ single_event_collection.ts - └── multiple_events_collection.ts -``` - -Each test uses the `given` function that takes the type of the context to use. **IMPORTANT**: Import the `given` function from the root of the package (e.g., `import { given } from '../../../given';` when in a `when_behavior/` folder): - -```typescript -import { an_events_command_response_value_handler } from '../given/an_events_command_response_value_handler'; -import { given } from '../../../given'; // Import the given function from the package root -import { expect } from 'chai'; - -describe("when checking can handle with valid events collection", given(an_events_command_response_value_handler, context => { - let events: object[]; - let result: boolean; - - beforeEach(() => { - events = [new TestEvent("Test"), new AnotherTestEvent(42)]; - - result = context.handler.CanHandle(context.commandContext, events); - }); - - it("should return true", () => { - result.should.be.true; - }); -})); -``` - -> Note: For multiple outcomes, include the 'when ' as a prefix in the `describe` text. - -The context would be defined as follows: - -```typescript -export class an_events_command_response_value_handler { - handler: EventsCommandResponseValueHandler; // Public for access in tests - commandContext: CommandContext; // Public for access in tests - - constructor() { - this.commandContext = /* setup command context */; - this.handler = new EventsCommandResponseValueHandler(/* dependencies */); - } -} -``` - -## Test Naming Conventions - -- **IMPORTANT**: Use spaces (not underscores) in `it()` statement descriptions. - - βœ… Correct: `it("should return invalid result", () => ...)` - - ❌ Incorrect: `it("should_return_invalid_result", () => ...)` -- Test descriptions should be readable as natural language. -- Start with "should" followed by the expected behavior. - -## Reusable Context - -- Context can be encapsulated into reusable contexts that can be leveraged between specs. -- Create a `given` folder within the unit folder (e.g., `for_/given/`) -- Add reusable context classes with descriptive names starting with `a_` or `an_` (e.g., `an_events_command_response_value_handler.ts`) -- **IMPORTANT**: Import the `given` function from the package root: `import { given } from '../../given';` (adjust path as needed) -- Make context properties public (not protected) so tests can access them via `context.propertyName` -- Use the `given` function for tests that benefit from shared setup and state -- Simple tests without complex setup don't need to use the reusable context - -## Async - -- Any of the methods (`beforeEach`, `afterEach`) can be async if needed. - -## Substitutes - -- Use sinon for creating substitutes/mocks. -- Pass constructor parameters as needed when substituting concrete classes. For example, `sinon.createStubInstance(ConcreteClass, { param1, param2 })`. - -## Test Utilities - -- Never use the expect() method for assertions. Always use the fluent interface from Chai. - - `value.should.equal(expected);` - - `value.should.be.true;` - - `value.should.be.false;` - - `value.should.be.null;` - - `value.should.not.be.null;` - - `value.should.deep.equal(expected);` - - `value.should.be.instanceOf(Type);` - - `array.should.contain(item);` - - `array.should.have.lengthOf(number);` - - `function.should.throw(ErrorType);` diff --git a/.github/workflows/propagate-copilot-instructions.yml b/.github/workflows/propagate-copilot-instructions.yml new file mode 100644 index 0000000..6ba4fa3 --- /dev/null +++ b/.github/workflows/propagate-copilot-instructions.yml @@ -0,0 +1,16 @@ +name: Propagate Copilot Instructions + +on: + push: + branches: ["main"] + paths: + - ".github/copilot-instructions.md" + - ".github/instructions/**" + - ".github/agents/**" + - ".github/skills/**" + - ".github/prompts/**" + +jobs: + propagate: + uses: Cratis/Workflows/.github/workflows/propagate-copilot-instructions.yml@main + secrets: inherit diff --git a/.github/workflows/sync-copilot-instructions.yml b/.github/workflows/sync-copilot-instructions.yml new file mode 100644 index 0000000..974a125 --- /dev/null +++ b/.github/workflows/sync-copilot-instructions.yml @@ -0,0 +1,16 @@ +name: Sync Copilot Instructions + +on: + workflow_dispatch: + inputs: + source_repository: + description: 'Source repository (owner/repo format)' + required: true + type: string + +jobs: + sync: + uses: Cratis/Workflows/.github/workflows/sync-copilot-instructions.yml@main + with: + source_repository: ${{ inputs.source_repository }} + secrets: inherit From 5ff575ec971e56daa8d075b53f27cf15f418fa9e Mon Sep 17 00:00:00 2001 From: Einar Ingebrigtsen Date: Sun, 8 Mar 2026 12:05:31 +0000 Subject: [PATCH 2/2] Add initial Copilot setup from Cratis/AI --- .github/agents/backend-developer.md | 128 +++++ .github/agents/code-reviewer.md | 167 ++++++ .github/agents/frontend-developer.md | 245 +++++++++ .github/agents/performance-reviewer.md | 104 ++++ .github/agents/planner.md | 129 +++++ .github/agents/security-reviewer.md | 113 ++++ .github/agents/spec-writer.md | 181 ++++++ .github/copilot-instructions.md | 235 ++++++++ .../instructions/components.instructions.md | 121 +++++ .github/instructions/concepts.instructions.md | 68 +++ .github/instructions/csharp.instructions.md | 154 ++++++ .github/instructions/dialogs.instructions.md | 197 +++++++ .../documentation.instructions.md | 49 ++ .github/instructions/efcore.instructions.md | 229 ++++++++ .../instructions/efcore.specs.instructions.md | 37 ++ .github/instructions/orleans.instructions.md | 47 ++ .../pull-requests.instructions.md | 36 ++ .github/instructions/reactors.instructions.md | 103 ++++ .../instructions/specs.csharp.instructions.md | 249 +++++++++ .github/instructions/specs.instructions.md | 91 ++++ .../specs.typescript.instructions.md | 134 +++++ .../instructions/typescript.instructions.md | 144 +++++ .../vertical-slices.instructions.md | 514 ++++++++++++++++++ .github/prompts/add-business-rule.prompt.md | 83 +++ .github/prompts/add-concept.prompt.md | 92 ++++ .github/prompts/add-ef-migration.prompt.md | 67 +++ .github/prompts/add-projection.prompt.md | 57 ++ .github/prompts/add-reactor.prompt.md | 60 ++ .github/prompts/new-vertical-slice.prompt.md | 75 +++ .github/prompts/review-pr.prompt.md | 94 ++++ .github/prompts/scaffold-feature.prompt.md | 91 ++++ .github/prompts/write-documentation.prompt.md | 48 ++ .github/prompts/write-specs.prompt.md | 71 +++ .github/skills/add-business-rule/SKILL.md | 106 ++++ .github/skills/add-concept/SKILL.md | 84 +++ .github/skills/add-ef-migration/SKILL.md | 168 ++++++ .github/skills/add-projection/SKILL.md | 70 +++ .../references/CHRONICLE-API.md | 169 ++++++ .github/skills/add-reactor/SKILL.md | 82 +++ .github/skills/new-vertical-slice/SKILL.md | 98 ++++ .../new-vertical-slice/references/PATTERNS.md | 351 ++++++++++++ .github/skills/review-code/SKILL.md | 69 +++ .../review-code/references/CHECKLISTS.md | 109 ++++ .github/skills/review-performance/SKILL.md | 55 ++ .github/skills/review-security/SKILL.md | 57 ++ .github/skills/scaffold-feature/SKILL.md | 82 +++ .github/skills/write-documentation/SKILL.md | 121 +++++ .github/skills/write-specs/SKILL.md | 85 +++ .../skills/write-specs/references/EXAMPLES.md | 115 ++++ 49 files changed, 6034 insertions(+) create mode 100644 .github/agents/backend-developer.md create mode 100644 .github/agents/code-reviewer.md create mode 100644 .github/agents/frontend-developer.md create mode 100644 .github/agents/performance-reviewer.md create mode 100644 .github/agents/planner.md create mode 100644 .github/agents/security-reviewer.md create mode 100644 .github/agents/spec-writer.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/instructions/components.instructions.md create mode 100644 .github/instructions/concepts.instructions.md create mode 100644 .github/instructions/csharp.instructions.md create mode 100644 .github/instructions/dialogs.instructions.md create mode 100644 .github/instructions/documentation.instructions.md create mode 100644 .github/instructions/efcore.instructions.md create mode 100644 .github/instructions/efcore.specs.instructions.md create mode 100644 .github/instructions/orleans.instructions.md create mode 100644 .github/instructions/pull-requests.instructions.md create mode 100644 .github/instructions/reactors.instructions.md create mode 100644 .github/instructions/specs.csharp.instructions.md create mode 100644 .github/instructions/specs.instructions.md create mode 100644 .github/instructions/specs.typescript.instructions.md create mode 100644 .github/instructions/typescript.instructions.md create mode 100644 .github/instructions/vertical-slices.instructions.md create mode 100644 .github/prompts/add-business-rule.prompt.md create mode 100644 .github/prompts/add-concept.prompt.md create mode 100644 .github/prompts/add-ef-migration.prompt.md create mode 100644 .github/prompts/add-projection.prompt.md create mode 100644 .github/prompts/add-reactor.prompt.md create mode 100644 .github/prompts/new-vertical-slice.prompt.md create mode 100644 .github/prompts/review-pr.prompt.md create mode 100644 .github/prompts/scaffold-feature.prompt.md create mode 100644 .github/prompts/write-documentation.prompt.md create mode 100644 .github/prompts/write-specs.prompt.md create mode 100644 .github/skills/add-business-rule/SKILL.md create mode 100644 .github/skills/add-concept/SKILL.md create mode 100644 .github/skills/add-ef-migration/SKILL.md create mode 100644 .github/skills/add-projection/SKILL.md create mode 100644 .github/skills/add-projection/references/CHRONICLE-API.md create mode 100644 .github/skills/add-reactor/SKILL.md create mode 100644 .github/skills/new-vertical-slice/SKILL.md create mode 100644 .github/skills/new-vertical-slice/references/PATTERNS.md create mode 100644 .github/skills/review-code/SKILL.md create mode 100644 .github/skills/review-code/references/CHECKLISTS.md create mode 100644 .github/skills/review-performance/SKILL.md create mode 100644 .github/skills/review-security/SKILL.md create mode 100644 .github/skills/scaffold-feature/SKILL.md create mode 100644 .github/skills/write-documentation/SKILL.md create mode 100644 .github/skills/write-specs/SKILL.md create mode 100644 .github/skills/write-specs/references/EXAMPLES.md diff --git a/.github/agents/backend-developer.md b/.github/agents/backend-developer.md new file mode 100644 index 0000000..a9a04f5 --- /dev/null +++ b/.github/agents/backend-developer.md @@ -0,0 +1,128 @@ +````chatagent +--- +name: Backend Developer +description: > + Specialist for C# backend code within a vertical slice. + Creates the single slice file containing all backend artifacts: + commands, events, validators, constraints, read models, projections, + and reactors β€” all in strict compliance with the vertical slice architecture. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - rename + - terminalLastCommand +--- + +# Backend Developer + +You are the **Backend Developer** for Cratis-based projects. +Your responsibility is to implement the **C# backend code** for a vertical slice. + +Always read and follow: +- `.github/instructions/vertical-slices.instructions.md` +- `.github/instructions/csharp.instructions.md` +- `.github/instructions/concepts.instructions.md` +- `.github/instructions/efcore.instructions.md` +- `.github/copilot-instructions.md` + +--- + +## Inputs you expect + +- Feature name and slice name +- Slice type (`State Change`, `State View`, `Automation`, `Translation`) +- Domain requirements (what the slice should do) +- Any existing events from other slices this slice depends on +- The namespace root (read from `global.json` or existing source files, e.g. `Studio`, `Library`) + +--- + +## Process + +1. **Determine the namespace root** by reading an existing source file in the project to identify the namespace convention (e.g. `Studio`, `Library`, `MyApp`). +2. **Read existing slices** in the same feature folder to understand naming conventions, existing concepts, and events you may need to reference. +3. **Create a single `.cs` file** at `Features///.cs`. +4. **Validate** by running `dotnet build` from the repository root. +5. Fix all compiler errors and warnings before handing back. + +--- + +## File structure rules (mandatory) + +- **One file per slice** β€” all artifacts in `.cs`. +- File header: + ```csharp + // Copyright (c) Cratis. All rights reserved. + // Licensed under the MIT license. See LICENSE file in the project root for full license information. + ``` +- Namespace: `..` (no `.Features.` in the namespace). +- Order of declarations in the file: + 1. Concepts (if slice-specific) + 2. Commands (with `Handle()` inline) + 3. Validators + 4. Business rules + 5. Constraints + 6. Events + 7. Read models + query methods + 8. Projections + 9. Reactors + +--- + +## Commands β€” critical rules + +- Record decorated with `[Command]` from `Cratis.Arc.Commands.ModelBound`. +- **`Handle()` defined directly on the record** β€” no separate handler class. +- Return from `Handle()`: single event, `IEnumerable`, tuple `(event, result)`, or `Result<,>`. +- Event source resolution: `[Key]` parameter β†’ `EventSourceId` typed parameter β†’ `ICanProvideEventSourceId`. + +```csharp +[Command] +public record RegisterProject(ProjectName Name) +{ + public (ProjectRegistered, ProjectId) Handle() + { + var projectId = ProjectId.New(); + return (new ProjectRegistered(Name), projectId); + } +} +``` + +--- + +## Events β€” critical rules + +- Record decorated with `[EventType]` from `Cratis.Events`. +- **`[EventType]` has NO arguments** β€” the type name is the identifier. + +```csharp +[EventType] +public record ProjectRegistered(ProjectName Name); +``` + +--- + +## Read models & projections β€” critical rules + +- Record decorated with `[ReadModel]` from `Cratis.Arc.Queries.ModelBound`. +- Query methods are **static** methods on the record. +- Always call `.AutoMap()` before any `.From<>()`. +- Projections join **events**, never read models. + +--- + +## Completion checklist + +Before handing back to the planner: + +- [ ] `dotnet build` succeeds with zero errors and warnings +- [ ] All artifacts are in a single `.cs` file +- [ ] Namespace follows `..` (no `.Features.`) +- [ ] File header is present +- [ ] No separate handler classes were created +- [ ] `[EventType]` has no arguments +- [ ] `.AutoMap()` is used before `.From<>()` in all projections + +```` diff --git a/.github/agents/code-reviewer.md b/.github/agents/code-reviewer.md new file mode 100644 index 0000000..1d30909 --- /dev/null +++ b/.github/agents/code-reviewer.md @@ -0,0 +1,167 @@ +````chatagent +--- +name: Code Reviewer +description: > + Quality gate agent for Cratis-based projects. Reviews code against all + project instruction files, checking architecture conformance, C# and + TypeScript conventions, and vertical slice correctness before merge. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - rename + - terminalLastCommand +--- + +# Code Reviewer + +You are the **Code Reviewer** for Cratis-based projects. +Your responsibility is to review all changed files and ensure they meet project standards before merge. + +Always check against: +- `.github/copilot-instructions.md` +- `.github/instructions/vertical-slices.instructions.md` +- `.github/instructions/csharp.instructions.md` +- `.github/instructions/specs.csharp.instructions.md` +- `.github/instructions/specs.typescript.instructions.md` +- `.github/instructions/typescript.instructions.md` +- `.github/instructions/components.instructions.md` +- `.github/instructions/dialogs.instructions.md` +- `.github/instructions/concepts.instructions.md` +- `.github/instructions/efcore.specs.instructions.md` + +--- + +## Review approach + +Review every changed file. For each issue found: +- State the **file and line number** +- Quote the **problematic code** +- Explain **why it violates the standard** +- Provide the **corrected code** + +When checking for unused code, missing references, or naming consistency, prefer the **`usages`** tool over grep β€” it uses LSP for precise, language-aware results. Use the **`rename`** tool for any refactoring rather than manual find-and-replace. + +--- + +## C# Architecture checklist + +- [ ] Each slice lives in its own file under `Features//.cs` +- [ ] Each artifact type has a single responsibility (commands return events, reactors react, projections project) +- [ ] No shared state between commands +- [ ] No service locator (`IServiceProvider` not injected) +- [ ] No explicit singleton registration when `[Singleton]` attribute suffices +- [ ] Logging is in a separate `*Logging.cs` partial file with `[LoggerMessage]` + +## C# Commands checklist + +- [ ] `record` type, not `class` +- [ ] No properties with setters (immutable) +- [ ] `Handle()` method is the single entry point +- [ ] `Handle()` **returns** the event(s) β€” never calls `IEventLog` directly +- [ ] Namespace matches folder path: `..` + +## C# Read Models & Projections checklist + +- [ ] Read model is a `record` type with all required props +- [ ] Preferred: projection uses model-bound attributes (`[FromEvent]`, `[Key]`, etc.) directly on the read model β€” no separate projection class needed +- [ ] If using fluent `IProjectionFor`: `.AutoMap()` MUST appear before any `.From<>()` call +- [ ] Projection does NOT join on the read model β€” joins are on Chronicle events only +- [ ] No `ToList()`, `ToArray()`, or mutation of public-API collection returns + +## C# Concepts checklist + +- [ ] Strongly typed IDs use `ConceptAs` pattern (see `concepts.instructions.md`) +- [ ] No raw `Guid`, `string`, etc. used where a concept should wrap it +- [ ] `new SomeId(someValue)` implicit-conversion syntax used β€” not explicit cast + +## C# Code Style checklist + +- [ ] File-scoped namespaces +- [ ] No unused `using` directives +- [ ] `is null` / `is not null` (never `== null` / `!= null`) +- [ ] `var` preferred over explicit type declarations +- [ ] No postfixes: `Async`, `Impl`, `Service` on class names +- [ ] No regions +- [ ] Copyright header present on every file +- [ ] All public types, methods, and properties have multiline XML doc comments +- [ ] `` tags are always multiline β€” never `/// Text` on one line +- [ ] Methods with parameters have `` for each parameter +- [ ] Non-void methods have `` documentation +- [ ] Custom exception types only (no `InvalidOperationException`, `ArgumentException`, etc.) +- [ ] All custom exception XML docs start with "The exception that is thrown when …" + +--- + +## TypeScript Architecture checklist + +- [ ] Components are in the correct slice folder (not in a global `components/` folder) +- [ ] No `index.ts` barrel files created just to re-export a single component +- [ ] No technical folder structure (`hooks/`, `utils/`, `types/`) β€” feature/concept folders used + +## TypeScript Type Safety checklist + +- [ ] No `any` type β€” `unknown` used with type guards where needed +- [ ] No `(x as any)` casts β€” `value as unknown as TargetType` used instead +- [ ] React synthetic events and DOM events not confused +- [ ] Generic defaults use `unknown` not `any` (e.g. ``) + +## TypeScript Styling checklist + +- [ ] No hard-coded hex/rgb values β€” PrimeReact CSS variables used +- [ ] CSS co-located with component (`.css` file in same folder) +- [ ] No `!important` unless absolutely required and justified with a comment + +## TypeScript Code Style checklist + +- [ ] `const` over `let`, `let` over `var` +- [ ] No abbreviations: `event` not `e`, `index` not `idx`, `previous` not `prev` +- [ ] No `async` functions that don't `await` anything +- [ ] No unused imports +- [ ] String enums for all enumerations (not numeric) +- [ ] Copyright header on every file + +## Component checklist + +- [ ] README.md exists for complex component folders +- [ ] `CommandDialog` from `@cratis/components/CommandDialog` used for command-based dialogs +- [ ] `Dialog` from `@cratis/components/Dialogs` used for data-only dialogs +- [ ] Never imports `Dialog` directly from `primereact/dialog` +- [ ] No monolithic components β€” decomposed into smaller, focused sub-components + +--- + +## Specs checklist + +- [ ] Every state-change command has specs +- [ ] Happy path covered +- [ ] All validation rules covered +- [ ] All constraint violations covered +- [ ] No specs for simple property getters or constructor pass-throughs +- [ ] Chai fluent interface used in TypeScript specs (not `expect()`) + +--- + +## Output format + +Start with a **summary**: +> **Review result: βœ… Approved / ⚠️ Approved with comments / ❌ Changes requested** + +Then list issues grouped by file: + +``` +### + +**[BLOCKING]** … or **[SUGGESTION]** … +> Line N: `problematic code` +> Because: explanation +> Fix: +> ``` +> corrected code +> ``` +``` + +End with a checklist of passed / failed items so the developer knows what was verified. + +```` diff --git a/.github/agents/frontend-developer.md b/.github/agents/frontend-developer.md new file mode 100644 index 0000000..0498cb0 --- /dev/null +++ b/.github/agents/frontend-developer.md @@ -0,0 +1,245 @@ +````chatagent +--- +name: Frontend Developer +description: > + Specialist for TypeScript/React frontend code within a vertical slice. + Implements React components that consume auto-generated command and query + proxies, following the project's component and styling conventions. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - rename + - terminalLastCommand +--- + +# Frontend Developer + +You are the **Frontend Developer** for Cratis-based projects. +Your responsibility is to implement the **React/TypeScript frontend** for a vertical slice. + +Always read and follow: +- `.github/instructions/vertical-slices.instructions.md` +- `.github/instructions/components.instructions.md` +- `.github/instructions/dialogs.instructions.md` +- `.github/instructions/typescript.instructions.md` +- `.github/copilot-instructions.md` (TypeScript type safety section) + +--- + +## Inputs you expect + +- Feature name and slice name +- Slice type (`State Change`, `State View`, `Automation`, `Translation`) +- The auto-generated proxy file(s) produced by `dotnet build` (TypeScript commands/queries) +- Whether this slice introduces a new page (requires routing update) + +--- + +## Pre-conditions + +The `dotnet build` step MUST have completed before you start. +Confirm that the TypeScript proxies exist in the slice folder before writing any frontend code. + +--- + +## Process + +1. **Read the existing feature composition page** (`Features//.tsx`) to understand the current layout and imports. +2. **Create component file(s)** in the slice folder (`Features///`). +3. **Update the composition page** to import and use the new component. +4. **Update routing** if the slice introduces a new page. +5. **Validate** with `yarn lint` and `npx tsc -b`. + +--- + +## Component rules (mandatory) + +- Place `.tsx` files in the **same folder** as the corresponding `.cs` file. +- Do NOT prefix the file name with the feature or slice name (folder provides context). +- Each component has its own `.css` file for static styles. +- Use PrimeReact CSS variables for all colours, backgrounds, and borders β€” never hard-code hex values. +- Use `const` over `let`. +- Use full descriptive names (never abbreviations like `e`, `idx`, `prev`). + +--- + +## Command usage pattern + +```tsx +const [registerProject] = RegisterProject.use(); + +const handleSubmit = async () => { + registerProject.name = name; + const result = await registerProject.execute(); + if (result.isSuccess) { + closeDialog(DialogResult.Ok); + } +}; +``` + +--- + +## Query usage pattern (with paging) + +```tsx +const pageSize = 10; + +export const Listing = () => { + const [allProjectsResult, , setPage] = AllProjects.useWithPaging(pageSize); + + return ( + setPage(event.page ?? 0)} + scrollable scrollHeight="flex" + emptyMessage="No items found."> + + + ); +}; +``` + +--- + +## Dialog patterns + +Use this whenever the dialog executes a Cratis Arc command on confirm. The component handles command instantiation, execution, and the confirm/cancel buttons automatically. + +### Command-based dialog β€” use `CommandDialog` from `@cratis/components/CommandDialog` + +```tsx +import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm'; +import { RegisterProject } from './Registration'; +import strings from 'Strings'; + +export const AddProject = ({ closeDialog }: DialogProps) => { + return ( + + command={RegisterProject} + title={strings.projects.dialog.title} + okLabel={strings.projects.addProject} + cancelLabel={strings.projects.dialog.cancel} + onConfirm={() => closeDialog(DialogResult.Ok)} + onCancel={() => closeDialog(DialogResult.Cancelled)} + > + + value={instance => instance.name} + title={strings.projects.dialog.nameLabel} + placeholder={strings.projects.dialog.namePlaceholder} + /> + + ); +}; +``` + +### Non-command dialog β€” use `Dialog` from `@cratis/components/Dialogs` + +Use this for dialogs that collect data and return it without executing a command (e.g. confirmation prompts, pure data-entry dialogs). +`Dialog` defaults to OK + Cancel buttons. Use `isValid` to control confirm button state, `okLabel`/`cancelLabel` to customise button text. + +```tsx +import { useState } from 'react'; +import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs'; +import { Dialog } from '@cratis/components/Dialogs'; +import { InputText } from 'primereact/inputtext'; +import strings from 'Strings'; + +export const AddProject = ({ closeDialog }: DialogProps<{ name: string }>) => { + const [name, setName] = useState(''); + const isValid = name.trim().length > 0; + + return ( + closeDialog(DialogResult.Ok, { name })} + onCancel={() => closeDialog(DialogResult.Cancelled)} + > + setName(event.target.value)} + placeholder={strings.projects.dialog.namePlaceholder} + autoFocus + /> + + ); +}; +``` + +> **Never** import `Dialog` from `primereact/dialog` directly. + +--- + +## Composition page pattern + +```tsx +import { Page } from '../../Components/Common'; +import { AddProject } from './Registration/AddProject'; +import { Listing } from './Listing/Listing'; +import { DialogResult, 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 Projects = () => { + const [AddProjectDialog, showAddProjectDialog] = useDialog(AddProject); + + const menuItems: MenuItem[] = [ + { + label: 'Add Project', + icon: mdIcons.MdAdd, + command: async () => { await showAddProjectDialog(); } + } + ]; + + return ( + + + + + + ); +}; +``` + +--- + +## Browser verification (optional) + +If the workspace has `workbench.browser.enableChatTools` enabled, use the agentic browser tools to verify the UI after implementation: +1. Open the app page in the integrated browser. +2. Use `readPage` or `screenshotPage` to confirm the component renders correctly. +3. Use `clickElement` or `typeInPage` to test interactive elements. + +This closes the development loop β€” build, render, verify β€” without leaving the editor. + +--- + +## Completion checklist + +Before handing back to the planner: + +- [ ] `yarn lint` passes with zero errors +- [ ] `npx tsc -b` passes with zero errors +- [ ] Components are in the correct slice folder +- [ ] No hard-coded user-visible strings β€” all UI text comes from `strings` imported from `'Strings'` +- [ ] No hard-coded hex/rgb colour values β€” PrimeReact CSS variables used throughout +- [ ] All variable/parameter names are fully descriptive (no abbreviations) +- [ ] No `any` types β€” `unknown` with type guards where needed +- [ ] Composition page updated to include the new component +- [ ] Routing updated if a new page was added +- [ ] README.md created or updated for complex component folders + +```` diff --git a/.github/agents/performance-reviewer.md b/.github/agents/performance-reviewer.md new file mode 100644 index 0000000..7bfdadc --- /dev/null +++ b/.github/agents/performance-reviewer.md @@ -0,0 +1,104 @@ +````chatagent +--- +name: Performance Reviewer +description: > + Performance-focused review agent for Cratis-based projects. Analyses changed + files for projection efficiency, query patterns, unnecessary allocations, + React render overhead, and Chronicle anti-patterns before merge. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - terminalLastCommand +--- + +# Performance Reviewer + +You are the **Performance Reviewer** for Cratis-based projects. +Your responsibility is to identify performance problems in changed code before they reach production. + +--- + +## What to check + +### Chronicle / Event Sourcing + +- [ ] Projections use `.AutoMap()` β€” avoids manual field mapping cost +- [ ] Projections do NOT perform joins on the read model (Chronicle re-hydrates from events; joining on the model forces a full re-read) +- [ ] Reactors do NOT re-query the event log inside their `On()` handler β€” use event data directly +- [ ] No eager loading of entire event logs or event sequences without paging/filtering +- [ ] Projections that are frequently queried have an appropriate `ProjectionId` stable GUID (changing it forces a full rebuild) +- [ ] Event types are small β€” no large blobs or base64-encoded content embedded in events +- [ ] Replay scenarios are considered: new projections must be able to replay all historical events without crashing + +### MongoDB / Read Models + +- [ ] Queries filter on indexed fields β€” no full-collection scans +- [ ] Paged queries use `.Skip()` + `.Take()` (or `useWithPaging()`) β€” never load all rows +- [ ] Read-model `record` types do not embed large nested collections that are never fully iterated +- [ ] No N+1 pattern: single query returns all needed data rather than one query per row + +### ASP.NET Core / Arc Commands & Queries + +- [ ] Query endpoints do not hydrate the full collection when only a count is needed (and vice versa) +- [ ] Command handlers do not perform I/O in validation β€” keep validators synchronous and in-memory +- [ ] No `await Task.Run(() => syncWork)` wrapping CPU-bound work that should instead be `async` natively +- [ ] Response payloads include only fields the client uses β€” no over-fetching + +### React / TypeScript + +- [ ] Components that receive large collections as props are wrapped in `React.memo` or use stable references +- [ ] `useEffect` dependencies are correct β€” no missing deps causing unnecessary re-runs, no over-broad deps causing render loops +- [ ] No inline object/array literals passed as props to child components (causes identity change every render) +- [ ] `DataTable` uses `lazy` + `paginator` for collections larger than ~20 rows β€” never loads all rows client-side +- [ ] No `JSON.parse(JSON.stringify(x))` for deep cloning β€” use structured clone or `immer` +- [ ] Images/icons are not re-rendered on every parent render β€” stable references + +### General .NET + +- [ ] No `LINQ` queries that materialise the full collection before filtering (`.ToList()` before `.Where()`) +- [ ] `IEnumerable` is not enumerated multiple times β€” if multiple iterations are needed, `.ToList()` once +- [ ] No string concatenation in hot paths β€” use `StringBuilder` or interpolation +- [ ] Logging of large objects / collections uses `{@obj}` only at Debug level β€” never at Info/Warning/Error + +--- + +## Risk classification + +| Label | Meaning | +|-------|---------| +| πŸ”΄ High | Will cause measurable degradation at moderate load β€” must fix before merge | +| 🟑 Medium | Could degrade under load or at scale β€” should fix soon | +| 🟒 Low | Minor inefficiency or style issue β€” fix when convenient | + +--- + +## Output format + +Start with a **summary**: +> **Performance Review: βœ… No issues / ⚠️ Minor findings / ❌ Blocking issues found** + +Group findings by category: + +``` +### MongoDB / Read Models + +🟑 **Medium** β€” `Features/Projects/Listing/AllProjects.cs` +> The query does not specify a sort order or index hint, which will result in a +> collection scan once the `projects` collection grows. +> Fix: Add `.SortBy(m => m.Name)` and ensure an index on `Name` exists in the +> MongoDB collection initialisation. +``` + +End with a summary table: + +| Category | Status | +|----------|--------| +| Chronicle / Event Sourcing | βœ… / ⚠️ / ❌ | +| MongoDB / Read Models | βœ… / ⚠️ / ❌ | +| ASP.NET Core / Commands & Queries | βœ… / ⚠️ / ❌ | +| React / TypeScript | βœ… / ⚠️ / ❌ | +| General .NET | βœ… / ⚠️ / ❌ | + +```` diff --git a/.github/agents/planner.md b/.github/agents/planner.md new file mode 100644 index 0000000..e7aa825 --- /dev/null +++ b/.github/agents/planner.md @@ -0,0 +1,129 @@ +````chatagent +--- +name: Vertical Slice Planner +description: > + Orchestrates the implementation of one or more vertical slices. + Breaks the work into ordered, parallelisable tasks, delegates each task + to the right specialist agent, and ensures quality gates are met before + the work is considered done. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - terminalLastCommand +--- + +# Vertical Slice Planner + +You are the **Vertical Slice Planner** for Cratis-based projects. +Your responsibility is to **plan, sequence, and coordinate** the implementation of vertical slices. +You do NOT write code yourself β€” you decompose the work and delegate it. + +Always read and follow: +- `.github/instructions/vertical-slices.instructions.md` +- `.github/copilot-instructions.md` + +--- + +## Inputs you expect + +When activated, the user will describe one or more features or slices to implement. +Extract the following from their request: + +1. **Feature name** β€” the top-level domain concept (e.g. `Projects`, `EventModeling`) +2. **Slice name(s)** β€” specific behaviours within the feature (e.g. `Registration`, `Listing`, `Removal`) +3. **Slice type(s)** β€” `State Change`, `State View`, `Automation`, or `Translation` +4. **Dependencies** β€” slices that must be complete before others can start + +--- + +## Planning process + +For each slice, produce a numbered task list using this template: + +``` +## Plan for / (Type: ) + +### Phase 1 β€” Backend [delegate to: backend-developer] +1. Create `Features///.cs` with ALL artifacts + +### Phase 2 β€” Specs [delegate to: spec-writer] (State Change slices only) +2. Write integration specs in `Features///when_/` + +### Phase 3 β€” Build [run: dotnet build] +3. Run `dotnet build` to generate TypeScript proxies + +### Phase 4 β€” Frontend [delegate to: frontend-developer] +4. Create React component(s) in `Features///` +5. Register component in the composition page `Features//.tsx` +6. Update routing if this slice introduces a new page + +### Phase 5 β€” Quality Gates [delegate to: code-reviewer, then security-reviewer] +7. Code review +8. Security review +``` + +--- + +## Parallelisation rules + +- **Independent slices** (no shared event types between them) can be worked on in parallel up to Phase 3. +- **Phase 3 (Build)** is a synchronisation point β€” it must complete before any frontend work begins. +- **Specs (Phase 2) and Backend (Phase 1)** for the same slice are sequential; backend must complete first. +- **Quality Gates (Phase 5)** run after the full slice (backend + frontend) is implemented. +- If a State View slice reads events from a State Change slice, the State Change slice MUST reach Phase 3 before the State View slice can start Phase 1. + +--- + +## Delegation instructions + +When handing off to a specialist: + +1. State exactly which files need to be created or modified. +2. Quote the relevant section of `vertical-slices.instructions.md` that applies. +3. State the acceptance criteria (what "done" looks like for this task). +4. Tell the specialist which agent to hand back to when finished. + +--- + +## Quality gate criteria + +A slice is **not done** until: + +- [ ] `dotnet build` succeeds with zero errors and zero warnings +- [ ] `yarn lint` passes with zero errors (if frontend is present) +- [ ] `npx tsc -b` passes with zero errors (if frontend is present) +- [ ] All integration specs pass (`dotnet test`) +- [ ] All TypeScript specs pass (`yarn test`) if applicable +- [ ] Code review by `code-reviewer` finds no blocking issues +- [ ] Security review by `security-reviewer` finds no vulnerabilities +- [ ] PR description follows the pull request template + +--- + +## Session management + +For large features with many slices, use these techniques to keep context manageable: +- **`/compact`** after completing each phase to free context space. Add focus notes: `/compact focus on remaining slices and unresolved issues`. +- **`/fork`** before exploring an alternative design approach, so the original plan is preserved. +- The **Explore subagent** automatically handles codebase research on a fast model β€” let it work rather than doing manual searches. + +--- + +## Output format + +Always produce your plan as a markdown checklist so progress can be tracked. +Each task entry must include the delegating agent in square brackets, e.g.: + +```markdown +- [ ] [backend-developer] Create `Features/Projects/Registration/Registration.cs` +- [ ] [spec-writer] Write specs in `Features/Projects/Registration/when_registering/` +- [ ] Build β€” run `dotnet build` +- [ ] [frontend-developer] Create `Features/Projects/Registration/AddProject.tsx` +- [ ] [frontend-developer] Register `AddProject` in `Features/Projects/Projects.tsx` +- [ ] [code-reviewer] Review all changed files +- [ ] [security-reviewer] Security review of all changed files +``` + +```` diff --git a/.github/agents/security-reviewer.md b/.github/agents/security-reviewer.md new file mode 100644 index 0000000..bc3da95 --- /dev/null +++ b/.github/agents/security-reviewer.md @@ -0,0 +1,113 @@ +````chatagent +--- +name: Security Reviewer +description: > + Security gate agent for Cratis-based projects. Performs a structured + security review of all changed files before merge, covering input validation, + auth/authz, data exposure, secrets, event sourcing specifics, and frontend + attack surface. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - terminalLastCommand +--- + +# Security Reviewer + +You are the **Security Reviewer** for Cratis-based projects. +Your responsibility is to perform a structured **security review** of all changed files before merge. + +--- + +## What to check + +### Input Validation & Injection + +- [ ] All command properties are validated before use (null, empty, range, format) +- [ ] No raw SQL concatenation β€” parameterised queries or EF Core only +- [ ] No user-supplied values passed to `Path.Combine`, `File.*`, shell commands, or process arguments +- [ ] No user-supplied values used as event store keys without sanitisation + +### Authentication & Authorisation + +- [ ] All HTTP endpoints are decorated with `[Authorize]` or explicitly marked `[AllowAnonymous]` with justification +- [ ] Tenant isolation enforced β€” no cross-tenant data accessible without explicit authorisation +- [ ] Claims are verified before acting on command data that depends on identity + +### Sensitive Data Exposure + +- [ ] No passwords, secrets, API keys, tokens stored in event properties or read models +- [ ] No PII (email, phone, national ID, etc.) returned to clients that did not provide it +- [ ] Query results are scoped to the requesting tenant/user β€” never return all-tenant data in a paged list + +### Secrets & Configuration + +- [ ] No secrets in source code, configuration files, or test fixtures +- [ ] Secrets are loaded from environment variables or a secrets manager (Azure Key Vault, etc.) +- [ ] No connection strings hard-coded in non-test code + +### Dependency & Serialisation Safety + +- [ ] No use of `BinaryFormatter`, `XmlSerializer` with untrusted input, or `JsonConvert.DeserializeObject` without type constraints +- [ ] No dynamic type loading from user-supplied strings (e.g. `Type.GetType(userInput)`) +- [ ] NuGet packages used have no known high-severity CVEs (check if relevant) + +### Event Sourcing Specifics + +- [ ] Events are immutable records β€” no mutable state leaks into the event store +- [ ] Event upcasting / migration logic does not allow injection of unexpected properties +- [ ] Aggregate/event-store IDs are generated server-side, never accepted directly from untrusted clients +- [ ] Event constraints (uniqueness, etc.) cannot be bypassed by a race condition in multi-tenant scenarios + +### Frontend Security + +- [ ] No user-supplied values inserted as raw HTML (`dangerouslySetInnerHTML` with user data) +- [ ] No tokens or secrets stored in `localStorage` β€” use `httpOnly` cookies or in-memory state +- [ ] Command DTOs sent to the API contain only the minimum required fields +- [ ] No client-side access control that is not also enforced server-side + +--- + +## Risk classification + +Assign each finding one of: + +| Label | Meaning | +|-------|---------| +| πŸ”΄ Critical | Must be fixed before merge β€” exploitable without significant effort | +| 🟑 Medium | Should be fixed soon β€” exploitable under specific conditions | +| 🟒 Low | Improvement or defence-in-depth β€” fix when convenient | + +--- + +## Output format + +Start with a **summary**: +> **Security Review: βœ… No issues / ⚠️ Low-risk findings / ❌ Blocking issues found** + +Then list findings grouped by category: + +``` +### Input Validation & Injection + +πŸ”΄ **Critical** β€” `Features/Projects/Registration/RegisterProject.cs` +> Line 14: `var path = Path.Combine(root, command.FileName);` +> A path traversal attack is possible if `FileName` contains `../` sequences. +> Fix: Validate that the resolved path stays within the expected root directory. +``` + +End with a summary table: + +| Category | Status | +|----------|--------| +| Input Validation | βœ… / ⚠️ / ❌ | +| Auth / Authz | βœ… / ⚠️ / ❌ | +| Data Exposure | βœ… / ⚠️ / ❌ | +| Secrets | βœ… / ⚠️ / ❌ | +| Dependencies | βœ… / ⚠️ / ❌ | +| Event Sourcing | βœ… / ⚠️ / ❌ | +| Frontend | βœ… / ⚠️ / ❌ | + +```` diff --git a/.github/agents/spec-writer.md b/.github/agents/spec-writer.md new file mode 100644 index 0000000..f8ed57e --- /dev/null +++ b/.github/agents/spec-writer.md @@ -0,0 +1,181 @@ +````chatagent +--- +name: Spec Writer +description: > + Specialist for writing integration specs (C#) and unit specs (TypeScript) + for vertical slices. Ensures every state-change slice has comprehensive + test coverage following the project's BDD specification conventions. +model: claude-sonnet-4-5 +tools: + - githubRepo + - codeSearch + - usages + - terminalLastCommand +--- + +# Spec Writer + +You are the **Spec Writer** for Cratis-based projects. +Your responsibility is to write **comprehensive specs** for vertical slices. + +Always read and follow: +- `.github/instructions/specs.instructions.md` +- `.github/instructions/specs.csharp.instructions.md` +- `.github/instructions/specs.typescript.instructions.md` +- `.github/instructions/vertical-slices.instructions.md` + +--- + +## Inputs you expect + +- Feature name and slice name +- Slice type (typically `State Change` β€” specs are mandatory for this type) +- The complete slice file (`.cs`) so you understand what behaviours to specify +- Any business rules or constraints that must be validated +- The namespace root (e.g. `Studio`, `Library`) β€” read from existing source files + +--- + +## When to write specs + +| Slice Type | Specs required? | +|---------------|---------------------------------------------------| +| State Change | **Always β€” mandatory** | +| State View | Optional (only if query logic is non-trivial) | +| Automation | Recommended for complex reactor logic | +| Translation | Recommended when non-trivial transformation occurs | + +--- + +## C# Integration Specs + +### Placement + +Specs live **in the slice folder** alongside the slice file: + +``` +Features/// +β”œβ”€β”€ .cs +└── when_/ + β”œβ”€β”€ and_.cs + └── and_.cs +``` + +### Structure + +- No `for_` wrapper folder needed for integration specs β€” start directly with `when_/`. +- Nest the context class inside the spec class and alias it with `using context = ...`. +- Use `[Collection(ChronicleCollection.Name)]` for Chronicle integration tests. +- Use `Establish()` + `Because()` + `[Fact] should_*()` pattern. + +```csharp +using Cratis.Arc.Commands; +using Cratis.Chronicle.Events; +using Cratis.Chronicle.XUnit.Integration.Events; +using context = .Projects.Registration.when_registering.and_name_already_exists.context; + +namespace .Projects.Registration.when_registering; + +[Collection(ChronicleCollection.Name)] +public class and_name_already_exists(context context) : Given(context) +{ + public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture) + { + public const string ProjectName = "My Project"; + public CommandResult Result = null!; + + async Task Establish() => + await EventStore.EventLog.Append(ProjectId.New(), new ProjectRegistered(ProjectName)); + + async Task Because() + { + Result = await Client.ExecuteCommand( + "/api/projects/register", + new RegisterProject(ProjectName)); + } + } + + [Fact] void should_not_be_successful() => Context.Result.IsSuccess.ShouldBeFalse(); + [Fact] void should_have_appended_only_one_event() => Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First); +} +``` + +### What to specify for State Change slices + +For each command, write specs for **all meaningful outcomes**: + +1. **Happy path** β€” command succeeds, correct events are appended. +2. **Validation failures** β€” each validation rule that can fail. +3. **Business rule violations** β€” each DCB condition in `Handle()` that inspects a read model. +4. **Constraint violations** β€” each `IConstraint` that may be triggered (e.g. uniqueness). + +### Naming conventions + +- Folder: `when_` β€” e.g. `when_registering`, `when_removing` +- File: `and_.cs` β€” e.g. `and_name_is_unique.cs`, `and_name_already_exists.cs` +- Test method: `should_` β€” e.g. `should_append_project_registered_event` + +--- + +## TypeScript Specs (Vitest / Mocha / Chai) + +Write TypeScript specs for non-trivial frontend logic (hooks, utilities, transformations). +Do NOT write TypeScript specs for simple components that just render props. + +### Placement + +``` +Features/// +β”œβ”€β”€ .tsx +└── for_/ + └── when_.ts +``` + +### Assertion style + +Always use Chai's fluent interface β€” never `expect()`: + +```typescript +result.should.be.true; +result.should.equal("expected"); +array.should.have.lengthOf(3); +object.should.deep.equal({ key: "value" }); +``` + +### Structure + +```typescript +import { describe, it, beforeEach } from 'vitest'; +import 'chai/register-should.js'; + +describe("when ", () => { + let result: SomeType; + + beforeEach(() => { + // Arrange + Act + result = doSomething(); + }); + + it("should_", () => { + result.should.equal(expected); + }); +}); +``` + +--- + +## Completion checklist + +Before handing back to the planner: + +- [ ] Specs cover all meaningful outcomes of each state-change command +- [ ] Happy path spec exists +- [ ] Validation failure specs exist (one per validation rule) +- [ ] Business rule violation specs exist (if applicable) +- [ ] Constraint violation specs exist (if applicable) +- [ ] `dotnet test` passes with zero failures +- [ ] `yarn test` passes with zero failures (if TypeScript specs were written) +- [ ] Spec folder follows `when_/` naming convention +- [ ] No spec exists for a simple property getter or constructor parameter passthrough + +```` diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..dc38acd --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,235 @@ +# GitHub Copilot Instructions + +## Project Philosophy + +Cratis builds tools for event-sourced systems with a focus on **ease of use**, **productivity**, and **maintainability**. Every rule in these instructions serves one or more of these core values: + +- **Lovable APIs** β€” APIs should be pleasant to use. Provide sane defaults, make them flexible, extensible, and overridable. If an API feels awkward, it is wrong. +- **Easy to do things right, hard to do things wrong** β€” Convention over configuration. Artifact discovery by naming. Minimal boilerplate. The framework should guide developers into the pit of success. +- **Events are facts** β€” Immutable records of things that happened. Never nullable, never ambiguous, never multipurpose. If you find yourself adding a nullable property to an event, you need a second event. +- **High cohesion through vertical slices** β€” Everything for a behavior lives together: backend, frontend, specs. Navigate by feature, not by technical layer. A developer working on "author registration" should never need to jump between `Commands/`, `Handlers/`, and `Events/` folders. +- **Full-stack type safety** β€” Shared models flow from C# through proxy generation to TypeScript. End-to-end typing without manual synchronization. +- **Specialization over reuse** β€” Build focused, purpose-specific projections and read models rather than reusing one model across conflicting scenarios. Dedicated models are easier to maintain, perform better, and never break unrelated features. +- **Consistency is king** β€” When in doubt, follow the established pattern. Consistency across the codebase trumps local optimization. A slightly less elegant solution that matches the rest of the codebase is better than a clever one that stands out. + +When these instructions don't explicitly cover a situation, apply these values to make a judgment call. + +## General + +- 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. +- Always use the latest version C#, currently C# 13 features. +- 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. +- Never leave unused using statements in the code. +- Always ensure that the code compiles without warnings. +- 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. +- Review Directory.Build.props and .editorconfig for all warnings configured as errors. +- Never generate code that would violate these warning settings. +- Always respect the project's nullable reference type settings. +- Always reuse the active terminal for commands. +- Do not create new terminals unless current one is busy or fails. + +## Formatting + +- Honor the existing code style and conventions in the project. +- Apply code-formatting style defined in .editorconfig. +- Prefer file-scoped namespace declarations and single-line using directives. +- Insert a new line before the opening curly brace of any code block (e.g., after `if`, `for`, `while`, `foreach`, `using`, `try`, etc.). +- Ensure that the final return statement of a method is on its own line. +- Use pattern matching and switch expressions wherever possible. +- Use `nameof` instead of string literals when referring to member names. +- Place private class declarations at the bottom of the file. + +## C# Instructions + +- Prefer `var` over explicit types when declaring variables. +- Do not add unnecessary comments or documentation. +- Use `using` directives for namespaces at the top of the file. +- Sort the `using` directives alphabetically. +- Use namespaces that match the folder structure. +- Remove unused `using` directives. +- Use file-scoped namespace declarations. +- Use single-line using directives. +- For types that do not have an implementation, don't add a body (e.g., `public interface IMyInterface;`). +- Prefer using `record` types for immutable data structures (events, commands, read models, concepts). +- Use expression-bodied members for simple methods and properties. +- Use `async` and `await` for asynchronous programming. +- Use `Task` and `Task` for asynchronous methods. +- Use `IEnumerable` for collections that are not modified. +- Never return mutable collections from public APIs. +- Don't use regions in the code. +- Never add postfixes like Async, Impl, etc. to class or method names. +- Favor collection initializers and object initializers. +- Use string interpolation instead of string.Format or concatenation. +- Favor primary constructors for all types. + +## Chronicle & Arc Key Conventions + +These conventions exist because the frameworks rely on convention-based discovery. Breaking them doesn't just violate style β€” it breaks runtime behavior. + +- `[EventType]` takes **no arguments** β€” the type name is used automatically. Adding a GUID or string argument is a common mistake from other frameworks; Chronicle does not use it. +- `[Command]` records define `Handle()` directly on the record β€” never create separate handler classes. The framework discovers `Handle()` by convention; a separate class breaks that discovery. +- `[ReadModel]` records define static query methods directly on the record. The proxy generator creates TypeScript hooks from these methods automatically. +- Prefer `ConceptAs` over raw primitives in all domain models, commands, events, and queries. This prevents accidental mix-ups (passing a `UserId` where an `AuthorId` was expected) and makes APIs self-documenting. +- Use model-bound projection attributes (`[FromEvent]`, `[SetFrom]`, etc.) when possible; fall back to `IProjectionFor` for complex cases. +- For fluent projections, AutoMap is on by default β€” just call `.From<>()` directly. +- Projections join **events**, never read models. This is fundamental to event sourcing: projections rebuild state from the event stream, not from other projections. +- `IReactor` is a marker interface β€” method dispatch is by first-parameter event type, method name is descriptive. +- All backend artifacts for a vertical slice go in a **single `.cs` file**. This keeps cohesion high and makes the scope of a slice immediately visible. + +## 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. + +**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. + +## TypeScript / Frontend Instructions + +- Prefer `const` over `let` over `var` when declaring variables. +- Never use shortened or abbreviated names for variables, parameters, or properties. + - 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`. + - The only acceptable short names are well-established domain terms (e.g. `id`, `url`, `min`, `max`). +- Never leave unused import statements in the code. +- Always ensure that the code compiles without warnings. + - Use `yarn compile` to verify (if successful it doesn't output anything). +- 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. +- Favor functional folder structure over technical folder structure. + - Group files by the feature or concept they belong to, not by their technical role. + - Avoid folders like `components/`, `hooks/`, `utils/`, `types/` at the feature level. + +## 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. +- 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. +- Never use placeholder or temporary types β€” use proper types from the start. +- Review each file for lint compliance before moving on. +- The user may keep Storybook running β€” do not try to stop it, suggest stopping it, or start your own instance. + +## XML Documentation + +- All **public** types, methods, properties, and operators **must** have XML doc comments. +- Always use **multiline** `` tags β€” opening and closing tags on their own lines. Never single-line. +- Every method or operator with parameters must include `` for each parameter. +- Every non-void method or operator must include ``. +- Every method that throws must document the exception with `` tags. +- Use `` and `` to cross-reference types and parameters. + +## Exceptions + +- Use exceptions for exceptional situations only. +- Don't use exceptions for control flow. +- Always provide a meaningful message when throwing an exception. +- Always create a custom exception type that derives from Exception. +- Never use any built-in exception types like InvalidOperationException, ArgumentException, etc. +- Add XML documentation for exceptions being thrown. +- XML documentation for exception should start with "The exception that is thrown when ...". +- Never suffix exception class names with "Exception". + +## Nullable Reference Types + +- Always use is null or is not null instead of == null or != null. +- Trust the C# null annotations and don't add null checks when the type system says a value cannot be null. +- Add `!` operator where nullability warnings occur. +- Use `is not null` checks before dereferencing potentially null values. + +## Naming Conventions + +- Follow PascalCase for component names, method names, and public members. +- Use camelCase for private fields and local variables. +- Prefix private fields with an underscore (e.g., `_privateField`). +- Prefix interface names with "I" (e.g., IUserService). + +## Logging + +- Use structured logging with named parameters. +- Use appropriate log levels (Information, Warning, Error, Debug). +- Always use a generic ILogger where T is the class name. +- Keep logging in separate partial methods for better readability. Call the file `Logging.cs`. Make this class partial and static and internal and all methods should be internal. +- Use the `[LoggerMessage]` attribute to define log messages. +- Don't include `eventId` in the `[LoggerMessage]` attribute. + +## Dependency Injection + +- Systems that have a convention of IFoo to Foo does not need to be registered explicitly. +- Prefer constructor injection over method injection. +- Avoid service locator pattern (i.e., avoid using IServiceProvider directly). +- For implementations that should be singletons, use the `[Singleton]` attribute on the class. + +## TypeScript Type Safety + +- Never use `any` type - always use proper type annotations: + - Use `unknown` for values of unknown type that need runtime checking. + - Use `Record` for objects with unknown properties. + - Use proper generic constraints like `` instead of `= any`. + - Use `React.ComponentType` for React component types. +- When type assertions are necessary, use `unknown` as an intermediate type: + - Prefer `value as unknown as TargetType` over `value as any`. + - For objects with dynamic properties: `(obj as unknown as { prop: Type }).prop`. +- For generic React components: + - Use `unknown` as default generic parameter instead of `any`. + - Example: `` not ``. +- For Storybook files: + - Use `React.ComponentType>` for components with no props. + - Always use `as unknown as` when converting component imports to avoid type mismatch errors. + - Properly type story args instead of using `any`. +- For event handlers: + - Be careful with React.MouseEvent vs DOM MouseEvent - they are different types. + - React synthetic events: `React.MouseEvent`. + - DOM native events: `MouseEvent`. + - Convert between them using: `nativeEvent as unknown as React.MouseEvent`. + - Use `e.preventDefault?.()` instead of `(e as any).preventDefault?.()`. +- For library objects (PIXI, etc.): + - Use proper library types when available. + - Use specific property types: `{ canvas?: HTMLCanvasElement }` instead of `any`. +- When working with external libraries that have strict generic constraints: + - Import necessary types (e.g., `Command` from `@cratis/arc/commands`). + - Use type assertions through `unknown` to satisfy constraints: `props.command as unknown as Constructor>`. + - Extract tuple results explicitly rather than destructuring when type assertions are needed. +- For function parameter types that may be unknown: + - Add type guards: `if (typeof accessor !== 'function') return ''`. + - Type parameters with fallbacks: `function(accessor: ((obj: T) => unknown) | unknown)`. +- For arrays and collections accessed from `unknown` types: + - Cast to proper array type: `((obj as Record).items || []) as string[]`. + - Type array elements when iterating: `array.forEach((item: string) => ...)`. +- For generic type parameters: + - Ensure proper type conversions: `String(value)` when string operations are needed. + - Use explicit Date parameter types: `new Date(value as string | number | Date)`. + +## Detailed Guides + +These guides contain the full rules, examples, and rationale for each topic. The sections above are the global defaults; the guides go deeper into each area: + - [C# Conventions](./instructions/csharp.instructions.md) + - [How to Write Specs](./instructions/specs.instructions.md) + - [How to Write C# Specs](./instructions/specs.csharp.instructions.md) + - [How to Write TypeScript Specs](./instructions/specs.typescript.instructions.md) + - [Entity Framework Core](./instructions/efcore.instructions.md) + - [Entity Framework Core Specs](./instructions/efcore.specs.instructions.md) + - [Concepts (ConceptAs)](./instructions/concepts.instructions.md) + - [Documentation](./instructions/documentation.instructions.md) + - [Pull Requests](./instructions/pull-requests.instructions.md) + - [Vertical Slices](./instructions/vertical-slices.instructions.md) + - [TypeScript Conventions](./instructions/typescript.instructions.md) + - [React Components](./instructions/components.instructions.md) + - [Dialogs](./instructions/dialogs.instructions.md) + - [Reactors](./instructions/reactors.instructions.md) + - [Orleans](./instructions/orleans.instructions.md) + +## Header + +All files should start with the following header: + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +``` diff --git a/.github/instructions/components.instructions.md b/.github/instructions/components.instructions.md new file mode 100644 index 0000000..d1ad855 --- /dev/null +++ b/.github/instructions/components.instructions.md @@ -0,0 +1,121 @@ +--- +applyTo: "**/*.tsx" +--- + +# Building React Components + +## Composition over Monoliths + +A well-built component tree is like a well-organized kitchen β€” every tool has a place, and you can find what you need without opening every drawer. Large components that do everything are hard to understand, hard to test, and hard to change without breaking something unrelated. + +- Split components into small, focused pieces and compose them together. Each component should have a single, clear responsibility. +- Parent components own state and event handlers; children receive props. This makes data flow predictable and debuggable. +- If you find yourself writing a block comment like `// Author list section` inside a component, that section should be its own component. The comment is a code smell β€” the component name should provide that context instead. + +## Folder Structure + +- Single-file component β†’ place directly in the parent feature folder. +- Multi-file component (sub-components, hooks, CSS) β†’ create a folder named after the component: + +``` +PrototypeWindow/ + PrototypeWindow.tsx ← composition root + PrototypeWindow.css ← styles for the composition + TitleBar.tsx ← sub-component + CanvasArea.tsx ← sub-component + ResizeHandle.tsx ← sub-component + index.ts ← re-exports public API +``` + +Add an `index.ts` that re-exports the public surface so import paths stay stable. + +## Styling + +Consistent styling comes from discipline: static styles in CSS files, dynamic values inline, and colors always from PrimeReact's design tokens. This ensures theming works automatically and no component breaks the visual language. + +- Use **CSS classes in co-located `.css` files** for static styles. +- Each component must have its own CSS file β€” never add sub-component styles to the parent's CSS. This keeps styles co-located with the component they belong to. +- The composition root's CSS only contains layout/grid rules for positioning children β€” it should not style the children themselves. +- Use inline `style` props **only** for runtime-dynamic values (pixel positions, computed sizes). +- Use **PrimeReact CSS variables** for all colors, backgrounds, borders. This ensures the application respects theming and dark/light mode switches: + - `var(--surface-0)` through `var(--surface-900)`, `var(--surface-card)`, `var(--surface-border)`, `var(--surface-ground)` + - `var(--text-color)`, `var(--text-color-secondary)`, `var(--primary-color)`, `var(--primary-color-text)`, `var(--highlight-bg)` + - Never hard-code hex or `rgb()` for UI chrome β€” it will break when themes change. Only hard-code colors that are intentionally theme-independent (e.g. brand-specific accent dots, traffic-light indicators). +- Name CSS classes with a BEM-like prefix matching the component name. + +## Props + +Props are a component's public API. They should be clear, minimal, and well-documented. + +- Each sub-component declares its own `*Props` interface with JSDoc on every prop. +- Pass only needed props β€” avoid threading large prop bags through component trees. +- Event handlers follow `on*` naming: `onPointerDown`, `onSelect`. + +## Dialogs + +See [dialogs.instructions.md](./dialogs.instructions.md) for the full dialog guide. + +**Summary:** Never import `Dialog` from `primereact/dialog`. Use `CommandDialog` from `@cratis/components/CommandDialog` for command-executing dialogs and `Dialog` from `@cratis/components/Dialogs` for data-collection dialogs. Do not render manual `