From e98d6515130d0ac1b3addfd355a5b89c09c0ede0 Mon Sep 17 00:00:00 2001 From: cratis-distribution-fixture-bot Date: Sat, 29 Aug 2026 00:00:00 +0200 Subject: [PATCH] Generate public marketplace distribution 0.2.0 Replace the fixture payload with the exact support-free 34-skill polyglot marketplace root while preserving candidate review bundles and the protected control plane. --- .../plugins/marketplace.json | 4 +- .claude-plugin/marketplace.json | 19 + .cursor-plugin/marketplace.json | 18 + .github/plugin/marketplace.json | 19 + LICENSE | 21 + README.md | 50 ++ SHA256SUMS | 189 +++-- canonical/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - claude/.claude-plugin/marketplace.json | 19 - .../plugins/cratis/.claude-plugin/plugin.json | 8 - .../cratis/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - .../plugins/cratis/.codex-plugin/plugin.json | 6 - .../cratis/skills/cratis-example/LICENSE | 2 - .../cratis/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - copilot/.github/plugin/marketplace.json | 19 - copilot/plugins/cratis/plugin.json | 6 - .../cratis/skills/cratis-example/LICENSE | 2 - .../cratis/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - cursor/.cursor-plugin/marketplace.json | 18 - .../plugins/cratis/.cursor-plugin/plugin.json | 6 - .../cratis/skills/cratis-example/LICENSE | 2 - .../cratis/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - distribution-manifest.json | 758 ++++++++++++++---- gemini-extension.json | 5 + gemini/gemini-extension.json | 5 - gemini/skills/cratis-example/LICENSE | 2 - gemini/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - junie/extensions/cratis/extension.json | 4 - .../cratis/skills/cratis-example/LICENSE | 2 - .../cratis/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - kiro/plugin.json | 10 - kiro/skills/cratis-example/LICENSE | 2 - kiro/skills/cratis-example/SKILL.md | 9 - kiro/skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - marketplace-release.json | 39 + package.json | 24 + pi/package/package.json | 18 - pi/package/skills/cratis-example/LICENSE | 2 - pi/package/skills/cratis-example/SKILL.md | 9 - .../skills/cratis-example/assets/example.txt | 2 - .../skills/cratis-example/references/guide.md | 5 - plugin.json | 17 + .../.claude-plugin/plugin.json | 8 + .../.codex-plugin/plugin.json | 6 + plugins/public-cratis-ai/plugin.json | 17 + .../skills/add-business-rule/SKILL.md | 111 +++ .../skills/add-ef-migration/SKILL.md | 168 ++++ .../skills/add-projection/SKILL.md | 103 +++ .../references/CHRONICLE-API.md | 169 ++++ .../skills/add-reactor/SKILL.md | 149 ++++ .../skills/add-reducer/SKILL.md | 61 ++ .../skills/auth-and-identity/SKILL.md | 162 ++++ .../references/authentication.md | 129 +++ .../references/authorization.md | 179 +++++ .../references/backend-identity.md | 379 +++++++++ .../auth-and-identity/references/frontend.md | 184 +++++ .../references/local-development.md | 111 +++ .../skills/call-command-from-code/SKILL.md | 81 ++ .../skills/cratis-command/SKILL.md | 310 +++++++ .../references/command-dialog.md | 138 ++++ .../references/command-result.md | 81 ++ .../cratis-command/references/proxy-setup.md | 71 ++ .../cratis-command/references/validation.md | 104 +++ .../cratis-fundamentals-concept}/LICENSE | 0 .../cratis-fundamentals-concept/SKILL.md | 198 +++++ .../skills/cratis-react-page/SKILL.md | 243 ++++++ .../cratis-react-page/references/data-page.md | 102 +++ .../references/data-table.md | 62 ++ .../cratis-react-page/references/dialogs.md | 198 +++++ .../cratis-react-page/references/mvvm.md | 133 +++ .../skills/cratis-readmodel/SKILL.md | 258 ++++++ .../cratis-readmodel/references/events.md | 99 +++ .../references/projections.md | 155 ++++ .../cratis-readmodel/references/queries.md | 104 +++ .../cratis-readmodel/references/reducers.md | 136 ++++ .../skills/cratis-specs-csharp/SKILL.md | 206 +++++ .../references/csharp-patterns.md | 166 ++++ .../references/integration-specs.md | 101 +++ .../skills/cratis-specs-typescript/SKILL.md | 148 ++++ .../references/typescript-patterns.md | 168 ++++ .../skills/cratis-vertical-slice/SKILL.md | 207 +++++ .../references/concepts.md | 101 +++ .../references/slice-anatomy.md | 255 ++++++ .../references/slice-types.md | 105 +++ .../skills/create-event-model/SKILL.md | 80 ++ .../skills/cross-cutting-properties/SKILL.md | 70 ++ .../skills/diagnose-slice/SKILL.md | 48 ++ .../skills/discover-implementations/SKILL.md | 95 +++ .../skills/event-modeling/SKILL.md | 63 ++ .../skills/event-type-migrations/SKILL.md | 80 ++ .../skills/inspect-running-chronicle/SKILL.md | 71 ++ .../skills/multi-tenancy/SKILL.md | 52 ++ .../skills/new-vertical-slice/SKILL.md | 103 +++ .../new-vertical-slice/references/PATTERNS.md | 344 ++++++++ .../skills/query-paging/SKILL.md | 90 +++ .../skills/review-code/SKILL.md | 86 ++ .../review-code/references/CHECKLISTS.md | 109 +++ .../skills/review-performance/SKILL.md | 55 ++ .../skills/review-security/SKILL.md | 57 ++ .../skills/scaffold-feature/SKILL.md | 82 ++ .../skills/stepper-command-dialog/SKILL.md | 233 ++++++ .../public-cratis-ai/skills/toolbar/SKILL.md | 282 +++++++ .../skills/write-specs-events/SKILL.md | 206 +++++ .../skills/write-specs-frontend/SKILL.md | 93 +++ .../skills/write-specs-readmodels/SKILL.md | 216 +++++ .../skills/write-specs/SKILL.md | 85 ++ .../skills/write-specs/references/EXAMPLES.md | 137 ++++ provenance.json | 642 ++++++++++++++- skills/add-business-rule/SKILL.md | 111 +++ skills/add-ef-migration/SKILL.md | 168 ++++ skills/add-projection/SKILL.md | 103 +++ .../references/CHRONICLE-API.md | 169 ++++ skills/add-reactor/SKILL.md | 149 ++++ skills/add-reducer/SKILL.md | 61 ++ skills/auth-and-identity/SKILL.md | 162 ++++ .../references/authentication.md | 129 +++ .../references/authorization.md | 179 +++++ .../references/backend-identity.md | 379 +++++++++ .../auth-and-identity/references/frontend.md | 184 +++++ .../references/local-development.md | 111 +++ skills/call-command-from-code/SKILL.md | 81 ++ skills/cratis-command/SKILL.md | 310 +++++++ .../references/command-dialog.md | 138 ++++ .../references/command-result.md | 81 ++ .../cratis-command/references/proxy-setup.md | 71 ++ .../cratis-command/references/validation.md | 104 +++ .../cratis-fundamentals-concept}/LICENSE | 0 skills/cratis-fundamentals-concept/SKILL.md | 198 +++++ skills/cratis-react-page/SKILL.md | 243 ++++++ .../cratis-react-page/references/data-page.md | 102 +++ .../references/data-table.md | 62 ++ .../cratis-react-page/references/dialogs.md | 198 +++++ skills/cratis-react-page/references/mvvm.md | 133 +++ skills/cratis-readmodel/SKILL.md | 258 ++++++ skills/cratis-readmodel/references/events.md | 99 +++ .../references/projections.md | 155 ++++ skills/cratis-readmodel/references/queries.md | 104 +++ .../cratis-readmodel/references/reducers.md | 136 ++++ skills/cratis-specs-csharp/SKILL.md | 206 +++++ .../references/csharp-patterns.md | 166 ++++ .../references/integration-specs.md | 101 +++ skills/cratis-specs-typescript/SKILL.md | 148 ++++ .../references/typescript-patterns.md | 168 ++++ skills/cratis-vertical-slice/SKILL.md | 207 +++++ .../references/concepts.md | 101 +++ .../references/slice-anatomy.md | 255 ++++++ .../references/slice-types.md | 105 +++ skills/create-event-model/SKILL.md | 80 ++ skills/cross-cutting-properties/SKILL.md | 70 ++ skills/diagnose-slice/SKILL.md | 48 ++ skills/discover-implementations/SKILL.md | 95 +++ skills/event-modeling/SKILL.md | 63 ++ skills/event-type-migrations/SKILL.md | 80 ++ skills/inspect-running-chronicle/SKILL.md | 71 ++ skills/multi-tenancy/SKILL.md | 52 ++ skills/new-vertical-slice/SKILL.md | 103 +++ .../new-vertical-slice/references/PATTERNS.md | 344 ++++++++ skills/query-paging/SKILL.md | 90 +++ skills/review-code/SKILL.md | 86 ++ skills/review-code/references/CHECKLISTS.md | 109 +++ skills/review-performance/SKILL.md | 55 ++ skills/review-security/SKILL.md | 57 ++ skills/scaffold-feature/SKILL.md | 82 ++ skills/stepper-command-dialog/SKILL.md | 233 ++++++ skills/toolbar/SKILL.md | 282 +++++++ skills/write-specs-events/SKILL.md | 206 +++++ skills/write-specs-frontend/SKILL.md | 93 +++ skills/write-specs-readmodels/SKILL.md | 216 +++++ skills/write-specs/SKILL.md | 85 ++ skills/write-specs/references/EXAMPLES.md | 137 ++++ submissions/cursor.json | 14 + submissions/openai.json | 63 ++ 187 files changed, 18837 insertions(+), 497 deletions(-) rename {codex/.agents => .agents}/plugins/marketplace.json (79%) create mode 100644 .claude-plugin/marketplace.json create mode 100644 .cursor-plugin/marketplace.json create mode 100644 .github/plugin/marketplace.json create mode 100644 LICENSE create mode 100644 README.md delete mode 100644 canonical/skills/cratis-example/SKILL.md delete mode 100644 canonical/skills/cratis-example/assets/example.txt delete mode 100644 canonical/skills/cratis-example/references/guide.md delete mode 100644 claude/.claude-plugin/marketplace.json delete mode 100644 claude/plugins/cratis/.claude-plugin/plugin.json delete mode 100644 claude/plugins/cratis/skills/cratis-example/SKILL.md delete mode 100644 claude/plugins/cratis/skills/cratis-example/assets/example.txt delete mode 100644 claude/plugins/cratis/skills/cratis-example/references/guide.md delete mode 100644 codex/plugins/cratis/.codex-plugin/plugin.json delete mode 100644 codex/plugins/cratis/skills/cratis-example/LICENSE delete mode 100644 codex/plugins/cratis/skills/cratis-example/SKILL.md delete mode 100644 codex/plugins/cratis/skills/cratis-example/assets/example.txt delete mode 100644 codex/plugins/cratis/skills/cratis-example/references/guide.md delete mode 100644 copilot/.github/plugin/marketplace.json delete mode 100644 copilot/plugins/cratis/plugin.json delete mode 100644 copilot/plugins/cratis/skills/cratis-example/LICENSE delete mode 100644 copilot/plugins/cratis/skills/cratis-example/SKILL.md delete mode 100644 copilot/plugins/cratis/skills/cratis-example/assets/example.txt delete mode 100644 copilot/plugins/cratis/skills/cratis-example/references/guide.md delete mode 100644 cursor/.cursor-plugin/marketplace.json delete mode 100644 cursor/plugins/cratis/.cursor-plugin/plugin.json delete mode 100644 cursor/plugins/cratis/skills/cratis-example/LICENSE delete mode 100644 cursor/plugins/cratis/skills/cratis-example/SKILL.md delete mode 100644 cursor/plugins/cratis/skills/cratis-example/assets/example.txt delete mode 100644 cursor/plugins/cratis/skills/cratis-example/references/guide.md create mode 100644 gemini-extension.json delete mode 100644 gemini/gemini-extension.json delete mode 100644 gemini/skills/cratis-example/LICENSE delete mode 100644 gemini/skills/cratis-example/SKILL.md delete mode 100644 gemini/skills/cratis-example/assets/example.txt delete mode 100644 gemini/skills/cratis-example/references/guide.md delete mode 100644 junie/extensions/cratis/extension.json delete mode 100644 junie/extensions/cratis/skills/cratis-example/LICENSE delete mode 100644 junie/extensions/cratis/skills/cratis-example/SKILL.md delete mode 100644 junie/extensions/cratis/skills/cratis-example/assets/example.txt delete mode 100644 junie/extensions/cratis/skills/cratis-example/references/guide.md delete mode 100644 kiro/plugin.json delete mode 100644 kiro/skills/cratis-example/LICENSE delete mode 100644 kiro/skills/cratis-example/SKILL.md delete mode 100644 kiro/skills/cratis-example/assets/example.txt delete mode 100644 kiro/skills/cratis-example/references/guide.md create mode 100644 marketplace-release.json create mode 100644 package.json delete mode 100644 pi/package/package.json delete mode 100644 pi/package/skills/cratis-example/LICENSE delete mode 100644 pi/package/skills/cratis-example/SKILL.md delete mode 100644 pi/package/skills/cratis-example/assets/example.txt delete mode 100644 pi/package/skills/cratis-example/references/guide.md create mode 100644 plugin.json create mode 100644 plugins/public-cratis-ai/.claude-plugin/plugin.json create mode 100644 plugins/public-cratis-ai/.codex-plugin/plugin.json create mode 100644 plugins/public-cratis-ai/plugin.json create mode 100644 plugins/public-cratis-ai/skills/add-business-rule/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/add-projection/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md create mode 100644 plugins/public-cratis-ai/skills/add-reactor/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/add-reducer/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md create mode 100644 plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md create mode 100644 plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-command/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md create mode 100644 plugins/public-cratis-ai/skills/cratis-command/references/command-result.md create mode 100644 plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md create mode 100644 plugins/public-cratis-ai/skills/cratis-command/references/validation.md rename {canonical/skills/cratis-example => plugins/public-cratis-ai/skills/cratis-fundamentals-concept}/LICENSE (100%) create mode 100644 plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md create mode 100644 plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md create mode 100644 plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md create mode 100644 plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md create mode 100644 plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md create mode 100644 plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md create mode 100644 plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md create mode 100644 plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md create mode 100644 plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md create mode 100644 plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md create mode 100644 plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md create mode 100644 plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md create mode 100644 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md create mode 100644 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md create mode 100644 plugins/public-cratis-ai/skills/create-event-model/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/discover-implementations/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/event-modeling/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md create mode 100644 plugins/public-cratis-ai/skills/query-paging/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/review-code/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md create mode 100644 plugins/public-cratis-ai/skills/review-performance/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/review-security/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/toolbar/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/write-specs-events/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/write-specs-frontend/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/write-specs-readmodels/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/write-specs/SKILL.md create mode 100644 plugins/public-cratis-ai/skills/write-specs/references/EXAMPLES.md create mode 100644 skills/add-business-rule/SKILL.md create mode 100644 skills/add-ef-migration/SKILL.md create mode 100644 skills/add-projection/SKILL.md create mode 100644 skills/add-projection/references/CHRONICLE-API.md create mode 100644 skills/add-reactor/SKILL.md create mode 100644 skills/add-reducer/SKILL.md create mode 100644 skills/auth-and-identity/SKILL.md create mode 100644 skills/auth-and-identity/references/authentication.md create mode 100644 skills/auth-and-identity/references/authorization.md create mode 100644 skills/auth-and-identity/references/backend-identity.md create mode 100644 skills/auth-and-identity/references/frontend.md create mode 100644 skills/auth-and-identity/references/local-development.md create mode 100644 skills/call-command-from-code/SKILL.md create mode 100644 skills/cratis-command/SKILL.md create mode 100644 skills/cratis-command/references/command-dialog.md create mode 100644 skills/cratis-command/references/command-result.md create mode 100644 skills/cratis-command/references/proxy-setup.md create mode 100644 skills/cratis-command/references/validation.md rename {claude/plugins/cratis/skills/cratis-example => skills/cratis-fundamentals-concept}/LICENSE (100%) create mode 100644 skills/cratis-fundamentals-concept/SKILL.md create mode 100644 skills/cratis-react-page/SKILL.md create mode 100644 skills/cratis-react-page/references/data-page.md create mode 100644 skills/cratis-react-page/references/data-table.md create mode 100644 skills/cratis-react-page/references/dialogs.md create mode 100644 skills/cratis-react-page/references/mvvm.md create mode 100644 skills/cratis-readmodel/SKILL.md create mode 100644 skills/cratis-readmodel/references/events.md create mode 100644 skills/cratis-readmodel/references/projections.md create mode 100644 skills/cratis-readmodel/references/queries.md create mode 100644 skills/cratis-readmodel/references/reducers.md create mode 100644 skills/cratis-specs-csharp/SKILL.md create mode 100644 skills/cratis-specs-csharp/references/csharp-patterns.md create mode 100644 skills/cratis-specs-csharp/references/integration-specs.md create mode 100644 skills/cratis-specs-typescript/SKILL.md create mode 100644 skills/cratis-specs-typescript/references/typescript-patterns.md create mode 100644 skills/cratis-vertical-slice/SKILL.md create mode 100644 skills/cratis-vertical-slice/references/concepts.md create mode 100644 skills/cratis-vertical-slice/references/slice-anatomy.md create mode 100644 skills/cratis-vertical-slice/references/slice-types.md create mode 100644 skills/create-event-model/SKILL.md create mode 100644 skills/cross-cutting-properties/SKILL.md create mode 100644 skills/diagnose-slice/SKILL.md create mode 100644 skills/discover-implementations/SKILL.md create mode 100644 skills/event-modeling/SKILL.md create mode 100644 skills/event-type-migrations/SKILL.md create mode 100644 skills/inspect-running-chronicle/SKILL.md create mode 100644 skills/multi-tenancy/SKILL.md create mode 100644 skills/new-vertical-slice/SKILL.md create mode 100644 skills/new-vertical-slice/references/PATTERNS.md create mode 100644 skills/query-paging/SKILL.md create mode 100644 skills/review-code/SKILL.md create mode 100644 skills/review-code/references/CHECKLISTS.md create mode 100644 skills/review-performance/SKILL.md create mode 100644 skills/review-security/SKILL.md create mode 100644 skills/scaffold-feature/SKILL.md create mode 100644 skills/stepper-command-dialog/SKILL.md create mode 100644 skills/toolbar/SKILL.md create mode 100644 skills/write-specs-events/SKILL.md create mode 100644 skills/write-specs-frontend/SKILL.md create mode 100644 skills/write-specs-readmodels/SKILL.md create mode 100644 skills/write-specs/SKILL.md create mode 100644 skills/write-specs/references/EXAMPLES.md create mode 100644 submissions/cursor.json create mode 100644 submissions/openai.json diff --git a/codex/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json similarity index 79% rename from codex/.agents/plugins/marketplace.json rename to .agents/plugins/marketplace.json index eab707a..2575c89 100644 --- a/codex/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -5,10 +5,10 @@ }, "plugins": [ { - "name": "cratis", + "name": "public-cratis-ai", "source": { "source": "local", - "path": "./plugins/cratis" + "path": "./plugins/public-cratis-ai" }, "policy": { "installation": "AVAILABLE", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..f5c3412 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "cratis", + "owner": { + "name": "Cratis" + }, + "metadata": { + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0" + }, + "plugins": [ + { + "name": "public-cratis-ai", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0", + "source": "./plugins/public-cratis-ai", + "strict": true + } + ] +} diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json new file mode 100644 index 0000000..68e5c9c --- /dev/null +++ b/.cursor-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "name": "cratis", + "owner": { + "name": "Cratis" + }, + "metadata": { + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0" + }, + "plugins": [ + { + "name": "public-cratis-ai", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0", + "source": "./plugins/public-cratis-ai" + } + ] +} diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json new file mode 100644 index 0000000..f5c3412 --- /dev/null +++ b/.github/plugin/marketplace.json @@ -0,0 +1,19 @@ +{ + "name": "cratis", + "owner": { + "name": "Cratis" + }, + "metadata": { + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0" + }, + "plugins": [ + { + "name": "public-cratis-ai", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "version": "0.2.0", + "source": "./plugins/public-cratis-ai", + "strict": true + } + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e93db77 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Cratis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..44224f9 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Cratis AI + +Passive skills for building event-sourced and CQRS applications with Cratis. + +## Install + +### Claude Code + +```text +/plugin marketplace add Cratis/AI.Distribution +/plugin install public-cratis-ai@cratis +``` + +### OpenAI Codex CLI + +```bash +codex plugin marketplace add Cratis/AI.Distribution --ref v0.2.0 +codex plugin add public-cratis-ai@cratis +``` + +### GitHub Copilot CLI + +```bash +copilot plugin marketplace add Cratis/AI.Distribution +copilot plugin install public-cratis-ai@cratis +``` + +### Gemini CLI + +```bash +gemini extensions install https://github.com/Cratis/AI.Distribution --ref v0.2.0 +``` + +### Kiro + +Choose **Add Custom Power**, then import `https://github.com/Cratis/AI.Distribution`. + +### Pi + +```bash +pi install git:github.com/Cratis/AI.Distribution@v0.2.0 +``` + +### Generic Agent Skills and Agent Plugins + +Use the root `skills/` directory or root `plugin.json` from the tagged repository. + +## Status + +This is an unsupported `0.x` evaluation distribution. Publication, static validation, and package lifecycle checks do not grant a support claim. The source repository's blocked and repository-only exclusions remain excluded. diff --git a/SHA256SUMS b/SHA256SUMS index ec9fb18..d84d5e5 100644 --- a/SHA256SUMS +++ b/SHA256SUMS @@ -1,49 +1,140 @@ -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 canonical/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea canonical/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b canonical/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 canonical/skills/cratis-example/references/guide.md -74c8b56eb3c78e2bd163e9b3504915a8b0ba544149ed78999c7f4b49d225f8f3 claude/.claude-plugin/marketplace.json -c18d81ecb33d521eb7a0750c33c44a82e3094966a0c8196fc6d5f1b6f97eb75f claude/plugins/cratis/.claude-plugin/plugin.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 claude/plugins/cratis/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea claude/plugins/cratis/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b claude/plugins/cratis/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 claude/plugins/cratis/skills/cratis-example/references/guide.md -1cd12d1cb24101c1e49795633f4e3720a078ee96e6c9fc5cc471b4f1a035efc7 codex/.agents/plugins/marketplace.json -1707ca7cd5392454bb6ce6d7e79bb31fbc6711b7f560b9b3acc91acf45048b9b codex/plugins/cratis/.codex-plugin/plugin.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 codex/plugins/cratis/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea codex/plugins/cratis/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b codex/plugins/cratis/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 codex/plugins/cratis/skills/cratis-example/references/guide.md -74c8b56eb3c78e2bd163e9b3504915a8b0ba544149ed78999c7f4b49d225f8f3 copilot/.github/plugin/marketplace.json -c5ab52a726c1ecfd2e7eefc4902ebc9307bd7cd84ef42cf5ee9e0b201b10f1be copilot/plugins/cratis/plugin.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 copilot/plugins/cratis/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea copilot/plugins/cratis/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b copilot/plugins/cratis/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 copilot/plugins/cratis/skills/cratis-example/references/guide.md -cb0867d8796b5e852ad0754050952821745a8e91342ced01bac2120abf51d760 cursor/.cursor-plugin/marketplace.json -1707ca7cd5392454bb6ce6d7e79bb31fbc6711b7f560b9b3acc91acf45048b9b cursor/plugins/cratis/.cursor-plugin/plugin.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 cursor/plugins/cratis/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea cursor/plugins/cratis/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b cursor/plugins/cratis/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 cursor/plugins/cratis/skills/cratis-example/references/guide.md -a9dab3cb24b6c249933ba6d54eda1a7655227feb1e7d3bcbbfe3fa06d0cc7c51 gemini/gemini-extension.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 gemini/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea gemini/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b gemini/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 gemini/skills/cratis-example/references/guide.md -09cac930698e10a4ba68418ea6d436e52fdb5ccd3f6220a439505f417f463bf5 junie/extensions/cratis/extension.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 junie/extensions/cratis/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea junie/extensions/cratis/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b junie/extensions/cratis/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 junie/extensions/cratis/skills/cratis-example/references/guide.md -08a29f403a2542838840961be9269cf4b07fa46755e2dcea46d3b6ccbd15afc9 kiro/plugin.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 kiro/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea kiro/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b kiro/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 kiro/skills/cratis-example/references/guide.md -df55debb3816cf9653145fa8da599e416cc98cd245536167177a9886dea77d97 pi/package/package.json -91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 pi/package/skills/cratis-example/LICENSE -53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea pi/package/skills/cratis-example/SKILL.md -55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b pi/package/skills/cratis-example/assets/example.txt -e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864 pi/package/skills/cratis-example/references/guide.md -cf77e8c34fbbf3d5faedebde2751e6c1f69d8a9a97d160a1ea305a80af705e2f provenance.json +96441cce905ac08d53aa6842f12e400ead370bbe39ccfd05cc0b9143f4cf0ef5 .agents/plugins/marketplace.json +e3a18b82c3e84f50820b4bcb9ad752b44d2777af73a18100084f70f3385e44a8 .claude-plugin/marketplace.json +da448ca1bb5ede9e0fb5348338d60c5607479371c6279d17ce15d6a813aa00ec .cursor-plugin/marketplace.json +e3a18b82c3e84f50820b4bcb9ad752b44d2777af73a18100084f70f3385e44a8 .github/plugin/marketplace.json +8db23da452b8cee0e9aa8d49801000475bbcc30ab4e6e322e28d1146df7230a7 LICENSE +5d7beef5fba80029f16674ad9b2f80144ddddabc594909cbf5febf26868c5894 README.md +421fc89f770174950d14116643783d3446ab793c99ddb05cd9d7ea053d4acf75 gemini-extension.json +cecdd773207c9e88fb377491fdca35ee511f523320009161739d220052296887 marketplace-release.json +97f6394890220eb53eab3ecab2a2a1a652cec4fecbe7e10724054e2d77a06516 package.json +41b5d4892c55581e330a57accdbb2021b7b3f86130f4b1956d48185e09ac5d3a plugin.json +a9a78f1c5719a74c4edafab5f71e31748cecd4502d57b00f94654198afdf8765 plugins/public-cratis-ai/.claude-plugin/plugin.json +5e3610dee0f0d399a32d425551fbcdab81449248e6088a9f321b8239429c9e6b plugins/public-cratis-ai/.codex-plugin/plugin.json +41b5d4892c55581e330a57accdbb2021b7b3f86130f4b1956d48185e09ac5d3a plugins/public-cratis-ai/plugin.json +e94076cb3fc215d2e358aa095be6a0490c04d37de2b4d8f6ac57afe0862e9456 plugins/public-cratis-ai/skills/add-business-rule/SKILL.md +d4e9952cb7042e33ee1c5547e64ebd58a8a6dd837843f72ba4cd002eb3b7e5f0 plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md +e6c53b655a689a043785846b0842e4845397ac7aca706e724c0102dea45558d2 plugins/public-cratis-ai/skills/add-projection/SKILL.md +566a2b8b471623b5c87e872c3983e3fe4cad748fe6e9ad590151c0a6d9c07050 plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md +a0067468efcff0e47f175a06fa3afdb6d5bc2d1f72b7fdb7865d3edcfbe1a2b3 plugins/public-cratis-ai/skills/add-reactor/SKILL.md +7b789a34cad977d818444a3ab9c8e19c22b1cdfea6a27b83e98beecfbf967273 plugins/public-cratis-ai/skills/add-reducer/SKILL.md +91511bb9085ebc2b733d1abc24bafacd295811a100961d01764dcb3326e3bdb3 plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md +594d6a9190a40a0da0c0afeb0c0915e7305f586b6c014f96c04d4d9476dc144e plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md +7875e1a34ee24170aa0bc99cc03954e13c1b3c7e180ff97dde8487cf015f93db plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md +b467a64027d017b8e39821bdcb7f77834350cd1b47625f603bad72a3a317bdf7 plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md +6d9cbfbd50ff9539bb75e50a0fd8c7b930121716602718bb13c87503c6cc73ec plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md +497832d9d5a60fd0f9990dab275c0db0eb593476b3bdf8fd4a05009506579b68 plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md +4fb567fb9b9ca689e52e604b262f25a5172834d3549762df046fd3a9c90f40bb plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md +315c1abd3f833e5d4222172f15635b55eb1775bbdbb79f84c9eb3d19bf12a61f plugins/public-cratis-ai/skills/cratis-command/SKILL.md +8e65ece3f614d5c4224a34bd94ab481d80eca8b64bf8f4cbe07a487f1edb986b plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md +aae44ec2aeff16f40cc4d8996e7d4fc4c4925f0b705e3fedfb87f300bb7f344a plugins/public-cratis-ai/skills/cratis-command/references/command-result.md +d8ebd3270d193ec743d74f80ace55cb57ff0eabbcef41419e9432efa233fa914 plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md +6ef7a29ac797cf16b0c43a34d7e859597638be739d4876b1748958d93179011a plugins/public-cratis-ai/skills/cratis-command/references/validation.md +91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 plugins/public-cratis-ai/skills/cratis-fundamentals-concept/LICENSE +423ab2d27b08e13d3c7c3fbaf9bbc06ea1fb9a13610bdcfdf37398b12d593ec9 plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md +7dfe2eb8a8336f5b3f65af79105d07427b55cc651fac93fa979f42a6878b7f08 plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md +d98695778cda0b5281ff0386165869023314d957cb89bfc100ad6a52fe907407 plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md +6f3cdc12c81f9af67abed46ed4c698eb2a66691e9bed6a04d65d605b7445351e plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md +a646df0fdd1a026e9c424eb823e5104d9482617dd8c6674d3756ce81309dd317 plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md +8d4e85cdfdf179c2676b0d78fd91cca36268b7afbdf3439d4a98204b9cdd99e1 plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md +be6b27a66f3fbeed427e3d18ab2004b919c9898ea3878151209fcff12fefbbdc plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md +4a957b608b4e44b08e8a85705aa9bc1fcddc7f1f3a0a5728c7ea6df75d61b5ef plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md +217c727da14c4755941ff110bc799e23b0cddff151cc0a9ad4803daa141d6fad plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md +9ed01e8e3e0b87e263e217877daf417334d145d2d1b12c28dd072a8b410c81c3 plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md +89635a8aa7ddb7b1d144d07a35c4c93c3fbc75a254424424cad6921a1575f76e plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md +fbde383f9b9fa4197934bf7bb86e349d184f1fa3497224ad42a6ab6b145783e8 plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md +c44ac0f1a3dc3de07adf2c67a3f85e2b40f890b4f16925bbc15abc9a08c946b4 plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md +77b764f686c5561c9e10b10cd7d4c9953149b2e3fa2e623ca410b19d67c39806 plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md +eb01957bcdca52e6ef62b4f4e56091cae11d7f842265da972c3c0ee637d145b7 plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md +4eedccda9f8396efefcc1f8fb74c3582c8d157823080821e9d9b72a2b3f6a0b3 plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md +a5d90be9cfc37ac2a0c7b51a8a69fce815fb744e801ea94619dff96553a41045 plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md +9b22698ee8294568c0564c55f74baefd74f2cb5de99fd1b24eafe015cf305a14 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md +522466e36311425f92dfde6c49fd29b0b0f6064890ecb20da49d18b7b4103ca2 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md +84c25543bd94a9bcce2c0cbaadd4c47f94e69ca9caa090d8203f27fbd7eb67b6 plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md +620e9d13a8e2752098acd8c60b620cdb6df042f9a49c2072edf5eb882c715581 plugins/public-cratis-ai/skills/create-event-model/SKILL.md +633bef916d401d2ecd74358cc57d6a8e5783e57394ff806a33b44970080a7885 plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md +45839e11e7aa7c2b66e93a2b83b2cef964aa2aa8abeb9064a9505c90677e95ea plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md +907a0f4f3d06856b07f10c1465a7bc2c515d8ec27b0cb84ba335a217f7290cf0 plugins/public-cratis-ai/skills/discover-implementations/SKILL.md +4cc24ccf0faf14066dad477aa87732a6e593ef263c9e0f377b1c9be59ed32f27 plugins/public-cratis-ai/skills/event-modeling/SKILL.md +eec56904897163d9261d3ec976595956fa089a717c7854837ef318e75e387412 plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md +d896c80b4a95a3517fbc66b9fc169c9a11b82096448cb1e7ce5a8a9e51d11aa3 plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md +d88b6633208c7ce3dddb629567db8df2fcfda351c4191b326cee7c382f75ed31 plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md +657c4b0d399a3e9d282210db7ff9d539fa5437bfbc40096f6df544aa965d80b3 plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md +5edc50e6056915e19fe16e0abfeb8fdb092f64c42c30b16bac7115ba503d49a4 plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md +8287047a2741bb6648ac14f0f187c0d503d83639ec8e554252fdd228b4861294 plugins/public-cratis-ai/skills/query-paging/SKILL.md +6cd509452e8ec092f8c334ad25687fb524befdef62afa8c7bd1c820377bdf5de plugins/public-cratis-ai/skills/review-code/SKILL.md +0839f00edc122991650e3c7ee022a1fc3a9ba2a839945c0eff31f3d978c6c824 plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md +09b8a20a88cc68c364cff009b4e70a635b209da12f9345a1c1ae701a5ba4adb8 plugins/public-cratis-ai/skills/review-performance/SKILL.md +bb8e4a571f2814fe5b56ae6cb33a75e21b6484d80e7c5d3f340b0f185c11c68f plugins/public-cratis-ai/skills/review-security/SKILL.md +1475435927c072ff27589e380b1fe08ab66c3e94dec7c8be91c7444e0858a4a0 plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md +51316f7744182d1af9e3cb87259afc79a0f81534f813c17b54517f4b7b010b7e plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md +151cfeecaa911d2f16dc35385e644838ae8a61af60b4a0bbab3d2714744161ee plugins/public-cratis-ai/skills/toolbar/SKILL.md +58cf309e9f62c9f0618c72a49ad1707f5dd6c3f691b1d83cc52a22931ee1ac41 plugins/public-cratis-ai/skills/write-specs-events/SKILL.md +025c6f9cd3581a5738130ba4ba16cea15a4a6a615e735d3c53dcd3a28734f208 plugins/public-cratis-ai/skills/write-specs-frontend/SKILL.md +57d0f2511e950edf8b390a2b5c44cd4532a6aa258d78e8bffc7167a6449dc054 plugins/public-cratis-ai/skills/write-specs-readmodels/SKILL.md +e6bc79cd003e97c591dc0c12c7aa727b809f0e7a8ac9e5d05ad7ab6d79dc8c0e plugins/public-cratis-ai/skills/write-specs/SKILL.md +6dc9b60a7a088c2a9860f4518c5256076b3b3256dd763b237aac05558787b03a plugins/public-cratis-ai/skills/write-specs/references/EXAMPLES.md +4af274b470b4837164d1afd4a4b79bc835f5fcd3a3709cc10c2c0132eca06456 provenance.json +e94076cb3fc215d2e358aa095be6a0490c04d37de2b4d8f6ac57afe0862e9456 skills/add-business-rule/SKILL.md +d4e9952cb7042e33ee1c5547e64ebd58a8a6dd837843f72ba4cd002eb3b7e5f0 skills/add-ef-migration/SKILL.md +e6c53b655a689a043785846b0842e4845397ac7aca706e724c0102dea45558d2 skills/add-projection/SKILL.md +566a2b8b471623b5c87e872c3983e3fe4cad748fe6e9ad590151c0a6d9c07050 skills/add-projection/references/CHRONICLE-API.md +a0067468efcff0e47f175a06fa3afdb6d5bc2d1f72b7fdb7865d3edcfbe1a2b3 skills/add-reactor/SKILL.md +7b789a34cad977d818444a3ab9c8e19c22b1cdfea6a27b83e98beecfbf967273 skills/add-reducer/SKILL.md +91511bb9085ebc2b733d1abc24bafacd295811a100961d01764dcb3326e3bdb3 skills/auth-and-identity/SKILL.md +594d6a9190a40a0da0c0afeb0c0915e7305f586b6c014f96c04d4d9476dc144e skills/auth-and-identity/references/authentication.md +7875e1a34ee24170aa0bc99cc03954e13c1b3c7e180ff97dde8487cf015f93db skills/auth-and-identity/references/authorization.md +b467a64027d017b8e39821bdcb7f77834350cd1b47625f603bad72a3a317bdf7 skills/auth-and-identity/references/backend-identity.md +6d9cbfbd50ff9539bb75e50a0fd8c7b930121716602718bb13c87503c6cc73ec skills/auth-and-identity/references/frontend.md +497832d9d5a60fd0f9990dab275c0db0eb593476b3bdf8fd4a05009506579b68 skills/auth-and-identity/references/local-development.md +4fb567fb9b9ca689e52e604b262f25a5172834d3549762df046fd3a9c90f40bb skills/call-command-from-code/SKILL.md +315c1abd3f833e5d4222172f15635b55eb1775bbdbb79f84c9eb3d19bf12a61f skills/cratis-command/SKILL.md +8e65ece3f614d5c4224a34bd94ab481d80eca8b64bf8f4cbe07a487f1edb986b skills/cratis-command/references/command-dialog.md +aae44ec2aeff16f40cc4d8996e7d4fc4c4925f0b705e3fedfb87f300bb7f344a skills/cratis-command/references/command-result.md +d8ebd3270d193ec743d74f80ace55cb57ff0eabbcef41419e9432efa233fa914 skills/cratis-command/references/proxy-setup.md +6ef7a29ac797cf16b0c43a34d7e859597638be739d4876b1748958d93179011a skills/cratis-command/references/validation.md +91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63 skills/cratis-fundamentals-concept/LICENSE +423ab2d27b08e13d3c7c3fbaf9bbc06ea1fb9a13610bdcfdf37398b12d593ec9 skills/cratis-fundamentals-concept/SKILL.md +7dfe2eb8a8336f5b3f65af79105d07427b55cc651fac93fa979f42a6878b7f08 skills/cratis-react-page/SKILL.md +d98695778cda0b5281ff0386165869023314d957cb89bfc100ad6a52fe907407 skills/cratis-react-page/references/data-page.md +6f3cdc12c81f9af67abed46ed4c698eb2a66691e9bed6a04d65d605b7445351e skills/cratis-react-page/references/data-table.md +a646df0fdd1a026e9c424eb823e5104d9482617dd8c6674d3756ce81309dd317 skills/cratis-react-page/references/dialogs.md +8d4e85cdfdf179c2676b0d78fd91cca36268b7afbdf3439d4a98204b9cdd99e1 skills/cratis-react-page/references/mvvm.md +be6b27a66f3fbeed427e3d18ab2004b919c9898ea3878151209fcff12fefbbdc skills/cratis-readmodel/SKILL.md +4a957b608b4e44b08e8a85705aa9bc1fcddc7f1f3a0a5728c7ea6df75d61b5ef skills/cratis-readmodel/references/events.md +217c727da14c4755941ff110bc799e23b0cddff151cc0a9ad4803daa141d6fad skills/cratis-readmodel/references/projections.md +9ed01e8e3e0b87e263e217877daf417334d145d2d1b12c28dd072a8b410c81c3 skills/cratis-readmodel/references/queries.md +89635a8aa7ddb7b1d144d07a35c4c93c3fbc75a254424424cad6921a1575f76e skills/cratis-readmodel/references/reducers.md +fbde383f9b9fa4197934bf7bb86e349d184f1fa3497224ad42a6ab6b145783e8 skills/cratis-specs-csharp/SKILL.md +c44ac0f1a3dc3de07adf2c67a3f85e2b40f890b4f16925bbc15abc9a08c946b4 skills/cratis-specs-csharp/references/csharp-patterns.md +77b764f686c5561c9e10b10cd7d4c9953149b2e3fa2e623ca410b19d67c39806 skills/cratis-specs-csharp/references/integration-specs.md +eb01957bcdca52e6ef62b4f4e56091cae11d7f842265da972c3c0ee637d145b7 skills/cratis-specs-typescript/SKILL.md +4eedccda9f8396efefcc1f8fb74c3582c8d157823080821e9d9b72a2b3f6a0b3 skills/cratis-specs-typescript/references/typescript-patterns.md +a5d90be9cfc37ac2a0c7b51a8a69fce815fb744e801ea94619dff96553a41045 skills/cratis-vertical-slice/SKILL.md +9b22698ee8294568c0564c55f74baefd74f2cb5de99fd1b24eafe015cf305a14 skills/cratis-vertical-slice/references/concepts.md +522466e36311425f92dfde6c49fd29b0b0f6064890ecb20da49d18b7b4103ca2 skills/cratis-vertical-slice/references/slice-anatomy.md +84c25543bd94a9bcce2c0cbaadd4c47f94e69ca9caa090d8203f27fbd7eb67b6 skills/cratis-vertical-slice/references/slice-types.md +620e9d13a8e2752098acd8c60b620cdb6df042f9a49c2072edf5eb882c715581 skills/create-event-model/SKILL.md +633bef916d401d2ecd74358cc57d6a8e5783e57394ff806a33b44970080a7885 skills/cross-cutting-properties/SKILL.md +45839e11e7aa7c2b66e93a2b83b2cef964aa2aa8abeb9064a9505c90677e95ea skills/diagnose-slice/SKILL.md +907a0f4f3d06856b07f10c1465a7bc2c515d8ec27b0cb84ba335a217f7290cf0 skills/discover-implementations/SKILL.md +4cc24ccf0faf14066dad477aa87732a6e593ef263c9e0f377b1c9be59ed32f27 skills/event-modeling/SKILL.md +eec56904897163d9261d3ec976595956fa089a717c7854837ef318e75e387412 skills/event-type-migrations/SKILL.md +d896c80b4a95a3517fbc66b9fc169c9a11b82096448cb1e7ce5a8a9e51d11aa3 skills/inspect-running-chronicle/SKILL.md +d88b6633208c7ce3dddb629567db8df2fcfda351c4191b326cee7c382f75ed31 skills/multi-tenancy/SKILL.md +657c4b0d399a3e9d282210db7ff9d539fa5437bfbc40096f6df544aa965d80b3 skills/new-vertical-slice/SKILL.md +5edc50e6056915e19fe16e0abfeb8fdb092f64c42c30b16bac7115ba503d49a4 skills/new-vertical-slice/references/PATTERNS.md +8287047a2741bb6648ac14f0f187c0d503d83639ec8e554252fdd228b4861294 skills/query-paging/SKILL.md +6cd509452e8ec092f8c334ad25687fb524befdef62afa8c7bd1c820377bdf5de skills/review-code/SKILL.md +0839f00edc122991650e3c7ee022a1fc3a9ba2a839945c0eff31f3d978c6c824 skills/review-code/references/CHECKLISTS.md +09b8a20a88cc68c364cff009b4e70a635b209da12f9345a1c1ae701a5ba4adb8 skills/review-performance/SKILL.md +bb8e4a571f2814fe5b56ae6cb33a75e21b6484d80e7c5d3f340b0f185c11c68f skills/review-security/SKILL.md +1475435927c072ff27589e380b1fe08ab66c3e94dec7c8be91c7444e0858a4a0 skills/scaffold-feature/SKILL.md +51316f7744182d1af9e3cb87259afc79a0f81534f813c17b54517f4b7b010b7e skills/stepper-command-dialog/SKILL.md +151cfeecaa911d2f16dc35385e644838ae8a61af60b4a0bbab3d2714744161ee skills/toolbar/SKILL.md +58cf309e9f62c9f0618c72a49ad1707f5dd6c3f691b1d83cc52a22931ee1ac41 skills/write-specs-events/SKILL.md +025c6f9cd3581a5738130ba4ba16cea15a4a6a615e735d3c53dcd3a28734f208 skills/write-specs-frontend/SKILL.md +57d0f2511e950edf8b390a2b5c44cd4532a6aa258d78e8bffc7167a6449dc054 skills/write-specs-readmodels/SKILL.md +e6bc79cd003e97c591dc0c12c7aa727b809f0e7a8ac9e5d05ad7ab6d79dc8c0e skills/write-specs/SKILL.md +6dc9b60a7a088c2a9860f4518c5256076b3b3256dd763b237aac05558787b03a skills/write-specs/references/EXAMPLES.md +406146ead65d1a4530a8c7d321192d2d3b2e37bb37a78113a9149f33b8410506 submissions/cursor.json +0a6a79f150e7441a97e716fb153636cb6b9140d830a440faabbdb84cf180ee81 submissions/openai.json diff --git a/canonical/skills/cratis-example/SKILL.md b/canonical/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/canonical/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/canonical/skills/cratis-example/assets/example.txt b/canonical/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/canonical/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/canonical/skills/cratis-example/references/guide.md b/canonical/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/canonical/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/claude/.claude-plugin/marketplace.json b/claude/.claude-plugin/marketplace.json deleted file mode 100644 index 9191420..0000000 --- a/claude/.claude-plugin/marketplace.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "cratis", - "owner": { - "name": "Cratis" - }, - "metadata": { - "description": "Cratis skills-only fixture marketplace", - "version": "0.0.0-fixture" - }, - "plugins": [ - { - "name": "cratis", - "description": "Passive Cratis skills fixture.", - "version": "0.0.0-fixture", - "source": "./plugins/cratis", - "strict": true - } - ] -} diff --git a/claude/plugins/cratis/.claude-plugin/plugin.json b/claude/plugins/cratis/.claude-plugin/plugin.json deleted file mode 100644 index f373881..0000000 --- a/claude/plugins/cratis/.claude-plugin/plugin.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture.", - "author": { - "name": "Cratis" - } -} diff --git a/claude/plugins/cratis/skills/cratis-example/SKILL.md b/claude/plugins/cratis/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/claude/plugins/cratis/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/claude/plugins/cratis/skills/cratis-example/assets/example.txt b/claude/plugins/cratis/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/claude/plugins/cratis/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/claude/plugins/cratis/skills/cratis-example/references/guide.md b/claude/plugins/cratis/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/claude/plugins/cratis/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/codex/plugins/cratis/.codex-plugin/plugin.json b/codex/plugins/cratis/.codex-plugin/plugin.json deleted file mode 100644 index 7afe831..0000000 --- a/codex/plugins/cratis/.codex-plugin/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture.", - "skills": "./skills/" -} diff --git a/codex/plugins/cratis/skills/cratis-example/LICENSE b/codex/plugins/cratis/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/codex/plugins/cratis/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/codex/plugins/cratis/skills/cratis-example/SKILL.md b/codex/plugins/cratis/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/codex/plugins/cratis/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/codex/plugins/cratis/skills/cratis-example/assets/example.txt b/codex/plugins/cratis/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/codex/plugins/cratis/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/codex/plugins/cratis/skills/cratis-example/references/guide.md b/codex/plugins/cratis/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/codex/plugins/cratis/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/copilot/.github/plugin/marketplace.json b/copilot/.github/plugin/marketplace.json deleted file mode 100644 index 9191420..0000000 --- a/copilot/.github/plugin/marketplace.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "cratis", - "owner": { - "name": "Cratis" - }, - "metadata": { - "description": "Cratis skills-only fixture marketplace", - "version": "0.0.0-fixture" - }, - "plugins": [ - { - "name": "cratis", - "description": "Passive Cratis skills fixture.", - "version": "0.0.0-fixture", - "source": "./plugins/cratis", - "strict": true - } - ] -} diff --git a/copilot/plugins/cratis/plugin.json b/copilot/plugins/cratis/plugin.json deleted file mode 100644 index 035655c..0000000 --- a/copilot/plugins/cratis/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture.", - "skills": "skills/" -} diff --git a/copilot/plugins/cratis/skills/cratis-example/LICENSE b/copilot/plugins/cratis/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/copilot/plugins/cratis/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/copilot/plugins/cratis/skills/cratis-example/SKILL.md b/copilot/plugins/cratis/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/copilot/plugins/cratis/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/copilot/plugins/cratis/skills/cratis-example/assets/example.txt b/copilot/plugins/cratis/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/copilot/plugins/cratis/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/copilot/plugins/cratis/skills/cratis-example/references/guide.md b/copilot/plugins/cratis/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/copilot/plugins/cratis/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/cursor/.cursor-plugin/marketplace.json b/cursor/.cursor-plugin/marketplace.json deleted file mode 100644 index 1290dc9..0000000 --- a/cursor/.cursor-plugin/marketplace.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "cratis", - "owner": { - "name": "Cratis" - }, - "metadata": { - "description": "Cratis skills-only fixture marketplace", - "version": "0.0.0-fixture" - }, - "plugins": [ - { - "name": "cratis", - "description": "Passive Cratis skills fixture.", - "version": "0.0.0-fixture", - "source": "./plugins/cratis" - } - ] -} diff --git a/cursor/plugins/cratis/.cursor-plugin/plugin.json b/cursor/plugins/cratis/.cursor-plugin/plugin.json deleted file mode 100644 index 7afe831..0000000 --- a/cursor/plugins/cratis/.cursor-plugin/plugin.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture.", - "skills": "./skills/" -} diff --git a/cursor/plugins/cratis/skills/cratis-example/LICENSE b/cursor/plugins/cratis/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/cursor/plugins/cratis/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/cursor/plugins/cratis/skills/cratis-example/SKILL.md b/cursor/plugins/cratis/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/cursor/plugins/cratis/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/cursor/plugins/cratis/skills/cratis-example/assets/example.txt b/cursor/plugins/cratis/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/cursor/plugins/cratis/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/cursor/plugins/cratis/skills/cratis-example/references/guide.md b/cursor/plugins/cratis/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/cursor/plugins/cratis/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/distribution-manifest.json b/distribution-manifest.json index b62aabb..a529e8c 100644 --- a/distribution-manifest.json +++ b/distribution-manifest.json @@ -1,270 +1,728 @@ { "schemaVersion": "1.0.0", - "state": "FIXTURE_ONLY_LOCAL_STAGING", - "version": "0.0.0-fixture", + "state": "PUBLIC_EVALUATION_MARKETPLACE", + "version": "0.2.0", + "profileId": "public-cratis-ai", "generatedTargets": [ - "canonical", + "agent-skills", + "agent-plugin", "claude", "codex", "copilot", "cursor", "gemini", - "junie", "kiro", "pi" ], "files": [ + { + "path": ".agents/plugins/marketplace.json", + "sha256": "96441cce905ac08d53aa6842f12e400ead370bbe39ccfd05cc0b9143f4cf0ef5", + "size": 377 + }, + { + "path": ".claude-plugin/marketplace.json", + "sha256": "e3a18b82c3e84f50820b4bcb9ad752b44d2777af73a18100084f70f3385e44a8", + "size": 447 + }, + { + "path": ".cursor-plugin/marketplace.json", + "sha256": "da448ca1bb5ede9e0fb5348338d60c5607479371c6279d17ce15d6a813aa00ec", + "size": 425 + }, + { + "path": ".github/plugin/marketplace.json", + "sha256": "e3a18b82c3e84f50820b4bcb9ad752b44d2777af73a18100084f70f3385e44a8", + "size": 447 + }, + { + "path": "LICENSE", + "sha256": "8db23da452b8cee0e9aa8d49801000475bbcc30ab4e6e322e28d1146df7230a7", + "size": 1063 + }, + { + "path": "README.md", + "sha256": "5d7beef5fba80029f16674ad9b2f80144ddddabc594909cbf5febf26868c5894", + "size": 1147 + }, { "path": "SHA256SUMS", - "sha256": "bea215850ebfaec6b4f41bae3afad038a7a12fd1292480f653417119e745587c", - "size": 5572 + "sha256": "2331cf63df4ce9b60e3bc4d523bf23d957aae2e0154be9bad4df544429f9e7a7", + "size": 16377 }, { - "path": "canonical/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "gemini-extension.json", + "sha256": "421fc89f770174950d14116643783d3446ab793c99ddb05cd9d7ea053d4acf75", + "size": 143 }, { - "path": "canonical/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "marketplace-release.json", + "sha256": "cecdd773207c9e88fb377491fdca35ee511f523320009161739d220052296887", + "size": 1016 }, { - "path": "canonical/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "package.json", + "sha256": "97f6394890220eb53eab3ecab2a2a1a652cec4fecbe7e10724054e2d77a06516", + "size": 449 }, { - "path": "canonical/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugin.json", + "sha256": "41b5d4892c55581e330a57accdbb2021b7b3f86130f4b1956d48185e09ac5d3a", + "size": 463 }, { - "path": "claude/.claude-plugin/marketplace.json", - "sha256": "74c8b56eb3c78e2bd163e9b3504915a8b0ba544149ed78999c7f4b49d225f8f3", - "size": 377 + "path": "plugins/public-cratis-ai/.claude-plugin/plugin.json", + "sha256": "a9a78f1c5719a74c4edafab5f71e31748cecd4502d57b00f94654198afdf8765", + "size": 183 }, { - "path": "claude/plugins/cratis/.claude-plugin/plugin.json", - "sha256": "c18d81ecb33d521eb7a0750c33c44a82e3094966a0c8196fc6d5f1b6f97eb75f", - "size": 144 + "path": "plugins/public-cratis-ai/.codex-plugin/plugin.json", + "sha256": "5e3610dee0f0d399a32d425551fbcdab81449248e6088a9f321b8239429c9e6b", + "size": 168 }, { - "path": "claude/plugins/cratis/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/plugin.json", + "sha256": "41b5d4892c55581e330a57accdbb2021b7b3f86130f4b1956d48185e09ac5d3a", + "size": 463 }, { - "path": "claude/plugins/cratis/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/add-business-rule/SKILL.md", + "sha256": "e94076cb3fc215d2e358aa095be6a0490c04d37de2b4d8f6ac57afe0862e9456", + "size": 6683 }, { - "path": "claude/plugins/cratis/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md", + "sha256": "d4e9952cb7042e33ee1c5547e64ebd58a8a6dd837843f72ba4cd002eb3b7e5f0", + "size": 5968 }, { - "path": "claude/plugins/cratis/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/add-projection/SKILL.md", + "sha256": "e6c53b655a689a043785846b0842e4845397ac7aca706e724c0102dea45558d2", + "size": 6464 }, { - "path": "codex/.agents/plugins/marketplace.json", - "sha256": "1cd12d1cb24101c1e49795633f4e3720a078ee96e6c9fc5cc471b4f1a035efc7", - "size": 357 + "path": "plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md", + "sha256": "566a2b8b471623b5c87e872c3983e3fe4cad748fe6e9ad590151c0a6d9c07050", + "size": 4849 }, { - "path": "codex/plugins/cratis/.codex-plugin/plugin.json", - "sha256": "1707ca7cd5392454bb6ce6d7e79bb31fbc6711b7f560b9b3acc91acf45048b9b", - "size": 129 + "path": "plugins/public-cratis-ai/skills/add-reactor/SKILL.md", + "sha256": "a0067468efcff0e47f175a06fa3afdb6d5bc2d1f72b7fdb7865d3edcfbe1a2b3", + "size": 6736 }, { - "path": "codex/plugins/cratis/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/skills/add-reducer/SKILL.md", + "sha256": "7b789a34cad977d818444a3ab9c8e19c22b1cdfea6a27b83e98beecfbf967273", + "size": 4321 }, { - "path": "codex/plugins/cratis/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md", + "sha256": "91511bb9085ebc2b733d1abc24bafacd295811a100961d01764dcb3326e3bdb3", + "size": 9900 }, { - "path": "codex/plugins/cratis/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md", + "sha256": "594d6a9190a40a0da0c0afeb0c0915e7305f586b6c014f96c04d4d9476dc144e", + "size": 5014 }, { - "path": "codex/plugins/cratis/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md", + "sha256": "7875e1a34ee24170aa0bc99cc03954e13c1b3c7e180ff97dde8487cf015f93db", + "size": 6061 }, { - "path": "copilot/.github/plugin/marketplace.json", - "sha256": "74c8b56eb3c78e2bd163e9b3504915a8b0ba544149ed78999c7f4b49d225f8f3", - "size": 377 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md", + "sha256": "b467a64027d017b8e39821bdcb7f77834350cd1b47625f603bad72a3a317bdf7", + "size": 14366 }, { - "path": "copilot/plugins/cratis/plugin.json", - "sha256": "c5ab52a726c1ecfd2e7eefc4902ebc9307bd7cd84ef42cf5ee9e0b201b10f1be", - "size": 127 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md", + "sha256": "6d9cbfbd50ff9539bb75e50a0fd8c7b930121716602718bb13c87503c6cc73ec", + "size": 4614 }, { - "path": "copilot/plugins/cratis/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md", + "sha256": "497832d9d5a60fd0f9990dab275c0db0eb593476b3bdf8fd4a05009506579b68", + "size": 4226 }, { - "path": "copilot/plugins/cratis/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md", + "sha256": "4fb567fb9b9ca689e52e604b262f25a5172834d3549762df046fd3a9c90f40bb", + "size": 4269 }, { - "path": "copilot/plugins/cratis/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/cratis-command/SKILL.md", + "sha256": "315c1abd3f833e5d4222172f15635b55eb1775bbdbb79f84c9eb3d19bf12a61f", + "size": 11830 }, { - "path": "copilot/plugins/cratis/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md", + "sha256": "8e65ece3f614d5c4224a34bd94ab481d80eca8b64bf8f4cbe07a487f1edb986b", + "size": 4793 }, { - "path": "cursor/.cursor-plugin/marketplace.json", - "sha256": "cb0867d8796b5e852ad0754050952821745a8e91342ced01bac2120abf51d760", - "size": 355 + "path": "plugins/public-cratis-ai/skills/cratis-command/references/command-result.md", + "sha256": "aae44ec2aeff16f40cc4d8996e7d4fc4c4925f0b705e3fedfb87f300bb7f344a", + "size": 2230 }, { - "path": "cursor/plugins/cratis/.cursor-plugin/plugin.json", - "sha256": "1707ca7cd5392454bb6ce6d7e79bb31fbc6711b7f560b9b3acc91acf45048b9b", - "size": 129 + "path": "plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md", + "sha256": "d8ebd3270d193ec743d74f80ace55cb57ff0eabbcef41419e9432efa233fa914", + "size": 1977 }, { - "path": "cursor/plugins/cratis/skills/cratis-example/LICENSE", + "path": "plugins/public-cratis-ai/skills/cratis-command/references/validation.md", + "sha256": "6ef7a29ac797cf16b0c43a34d7e859597638be739d4876b1748958d93179011a", + "size": 3242 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-fundamentals-concept/LICENSE", "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", "size": 142 }, { - "path": "cursor/plugins/cratis/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md", + "sha256": "423ab2d27b08e13d3c7c3fbaf9bbc06ea1fb9a13610bdcfdf37398b12d593ec9", + "size": 7766 }, { - "path": "cursor/plugins/cratis/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md", + "sha256": "7dfe2eb8a8336f5b3f65af79105d07427b55cc651fac93fa979f42a6878b7f08", + "size": 8833 }, { - "path": "cursor/plugins/cratis/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md", + "sha256": "d98695778cda0b5281ff0386165869023314d957cb89bfc100ad6a52fe907407", + "size": 3165 }, { - "path": "gemini/gemini-extension.json", - "sha256": "a9dab3cb24b6c249933ba6d54eda1a7655227feb1e7d3bcbbfe3fa06d0cc7c51", - "size": 104 + "path": "plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md", + "sha256": "6f3cdc12c81f9af67abed46ed4c698eb2a66691e9bed6a04d65d605b7445351e", + "size": 1770 }, { - "path": "gemini/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md", + "sha256": "a646df0fdd1a026e9c424eb823e5104d9482617dd8c6674d3756ce81309dd317", + "size": 6246 }, { - "path": "gemini/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md", + "sha256": "8d4e85cdfdf179c2676b0d78fd91cca36268b7afbdf3439d4a98204b9cdd99e1", + "size": 3171 }, { - "path": "gemini/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md", + "sha256": "be6b27a66f3fbeed427e3d18ab2004b919c9898ea3878151209fcff12fefbbdc", + "size": 10846 }, { - "path": "gemini/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md", + "sha256": "4a957b608b4e44b08e8a85705aa9bc1fcddc7f1f3a0a5728c7ea6df75d61b5ef", + "size": 3199 }, { - "path": "junie/extensions/cratis/extension.json", - "sha256": "09cac930698e10a4ba68418ea6d436e52fdb5ccd3f6220a439505f417f463bf5", - "size": 74 + "path": "plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md", + "sha256": "217c727da14c4755941ff110bc799e23b0cddff151cc0a9ad4803daa141d6fad", + "size": 5930 }, { - "path": "junie/extensions/cratis/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md", + "sha256": "9ed01e8e3e0b87e263e217877daf417334d145d2d1b12c28dd072a8b410c81c3", + "size": 2655 }, { - "path": "junie/extensions/cratis/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md", + "sha256": "89635a8aa7ddb7b1d144d07a35c4c93c3fbc75a254424424cad6921a1575f76e", + "size": 4163 }, { - "path": "junie/extensions/cratis/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md", + "sha256": "fbde383f9b9fa4197934bf7bb86e349d184f1fa3497224ad42a6ab6b145783e8", + "size": 8458 }, { - "path": "junie/extensions/cratis/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md", + "sha256": "c44ac0f1a3dc3de07adf2c67a3f85e2b40f890b4f16925bbc15abc9a08c946b4", + "size": 4620 }, { - "path": "kiro/plugin.json", - "sha256": "08a29f403a2542838840961be9269cf4b07fa46755e2dcea46d3b6ccbd15afc9", - "size": 239 + "path": "plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md", + "sha256": "77b764f686c5561c9e10b10cd7d4c9953149b2e3fa2e623ca410b19d67c39806", + "size": 3990 }, { - "path": "kiro/skills/cratis-example/LICENSE", - "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", - "size": 142 + "path": "plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md", + "sha256": "eb01957bcdca52e6ef62b4f4e56091cae11d7f842265da972c3c0ee637d145b7", + "size": 4596 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md", + "sha256": "4eedccda9f8396efefcc1f8fb74c3582c8d157823080821e9d9b72a2b3f6a0b3", + "size": 4028 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md", + "sha256": "a5d90be9cfc37ac2a0c7b51a8a69fce815fb744e801ea94619dff96553a41045", + "size": 6807 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md", + "sha256": "9b22698ee8294568c0564c55f74baefd74f2cb5de99fd1b24eafe015cf305a14", + "size": 3133 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md", + "sha256": "522466e36311425f92dfde6c49fd29b0b0f6064890ecb20da49d18b7b4103ca2", + "size": 6664 + }, + { + "path": "plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md", + "sha256": "84c25543bd94a9bcce2c0cbaadd4c47f94e69ca9caa090d8203f27fbd7eb67b6", + "size": 3617 + }, + { + "path": "plugins/public-cratis-ai/skills/create-event-model/SKILL.md", + "sha256": "620e9d13a8e2752098acd8c60b620cdb6df042f9a49c2072edf5eb882c715581", + "size": 5788 + }, + { + "path": "plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md", + "sha256": "633bef916d401d2ecd74358cc57d6a8e5783e57394ff806a33b44970080a7885", + "size": 3921 + }, + { + "path": "plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md", + "sha256": "45839e11e7aa7c2b66e93a2b83b2cef964aa2aa8abeb9064a9505c90677e95ea", + "size": 5189 + }, + { + "path": "plugins/public-cratis-ai/skills/discover-implementations/SKILL.md", + "sha256": "907a0f4f3d06856b07f10c1465a7bc2c515d8ec27b0cb84ba335a217f7290cf0", + "size": 4881 + }, + { + "path": "plugins/public-cratis-ai/skills/event-modeling/SKILL.md", + "sha256": "4cc24ccf0faf14066dad477aa87732a6e593ef263c9e0f377b1c9be59ed32f27", + "size": 6248 }, { - "path": "kiro/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md", + "sha256": "eec56904897163d9261d3ec976595956fa089a717c7854837ef318e75e387412", + "size": 4357 }, { - "path": "kiro/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md", + "sha256": "d896c80b4a95a3517fbc66b9fc169c9a11b82096448cb1e7ce5a8a9e51d11aa3", + "size": 4909 }, { - "path": "kiro/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md", + "sha256": "d88b6633208c7ce3dddb629567db8df2fcfda351c4191b326cee7c382f75ed31", + "size": 3160 }, { - "path": "pi/package/package.json", - "sha256": "df55debb3816cf9653145fa8da599e416cc98cd245536167177a9886dea77d97", - "size": 278 + "path": "plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md", + "sha256": "657c4b0d399a3e9d282210db7ff9d539fa5437bfbc40096f6df544aa965d80b3", + "size": 5459 }, { - "path": "pi/package/skills/cratis-example/LICENSE", + "path": "plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md", + "sha256": "5edc50e6056915e19fe16e0abfeb8fdb092f64c42c30b16bac7115ba503d49a4", + "size": 12808 + }, + { + "path": "plugins/public-cratis-ai/skills/query-paging/SKILL.md", + "sha256": "8287047a2741bb6648ac14f0f187c0d503d83639ec8e554252fdd228b4861294", + "size": 5234 + }, + { + "path": "plugins/public-cratis-ai/skills/review-code/SKILL.md", + "sha256": "6cd509452e8ec092f8c334ad25687fb524befdef62afa8c7bd1c820377bdf5de", + "size": 4585 + }, + { + "path": "plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md", + "sha256": "0839f00edc122991650e3c7ee022a1fc3a9ba2a839945c0eff31f3d978c6c824", + "size": 5413 + }, + { + "path": "plugins/public-cratis-ai/skills/review-performance/SKILL.md", + "sha256": "09b8a20a88cc68c364cff009b4e70a635b209da12f9345a1c1ae701a5ba4adb8", + "size": 2591 + }, + { + "path": "plugins/public-cratis-ai/skills/review-security/SKILL.md", + "sha256": "bb8e4a571f2814fe5b56ae6cb33a75e21b6484d80e7c5d3f340b0f185c11c68f", + "size": 2449 + }, + { + "path": "plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md", + "sha256": "1475435927c072ff27589e380b1fe08ab66c3e94dec7c8be91c7444e0858a4a0", + "size": 2225 + }, + { + "path": "plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md", + "sha256": "51316f7744182d1af9e3cb87259afc79a0f81534f813c17b54517f4b7b010b7e", + "size": 8572 + }, + { + "path": "plugins/public-cratis-ai/skills/toolbar/SKILL.md", + "sha256": "151cfeecaa911d2f16dc35385e644838ae8a61af60b4a0bbab3d2714744161ee", + "size": 10065 + }, + { + "path": "plugins/public-cratis-ai/skills/write-specs-events/SKILL.md", + "sha256": "58cf309e9f62c9f0618c72a49ad1707f5dd6c3f691b1d83cc52a22931ee1ac41", + "size": 6406 + }, + { + "path": "plugins/public-cratis-ai/skills/write-specs-frontend/SKILL.md", + "sha256": "025c6f9cd3581a5738130ba4ba16cea15a4a6a615e735d3c53dcd3a28734f208", + "size": 3997 + }, + { + "path": "plugins/public-cratis-ai/skills/write-specs-readmodels/SKILL.md", + "sha256": "57d0f2511e950edf8b390a2b5c44cd4532a6aa258d78e8bffc7167a6449dc054", + "size": 6380 + }, + { + "path": "plugins/public-cratis-ai/skills/write-specs/SKILL.md", + "sha256": "e6bc79cd003e97c591dc0c12c7aa727b809f0e7a8ac9e5d05ad7ab6d79dc8c0e", + "size": 4785 + }, + { + "path": "plugins/public-cratis-ai/skills/write-specs/references/EXAMPLES.md", + "sha256": "6dc9b60a7a088c2a9860f4518c5256076b3b3256dd763b237aac05558787b03a", + "size": 5514 + }, + { + "path": "provenance.json", + "sha256": "4af274b470b4837164d1afd4a4b79bc835f5fcd3a3709cc10c2c0132eca06456", + "size": 23147 + }, + { + "path": "skills/add-business-rule/SKILL.md", + "sha256": "e94076cb3fc215d2e358aa095be6a0490c04d37de2b4d8f6ac57afe0862e9456", + "size": 6683 + }, + { + "path": "skills/add-ef-migration/SKILL.md", + "sha256": "d4e9952cb7042e33ee1c5547e64ebd58a8a6dd837843f72ba4cd002eb3b7e5f0", + "size": 5968 + }, + { + "path": "skills/add-projection/SKILL.md", + "sha256": "e6c53b655a689a043785846b0842e4845397ac7aca706e724c0102dea45558d2", + "size": 6464 + }, + { + "path": "skills/add-projection/references/CHRONICLE-API.md", + "sha256": "566a2b8b471623b5c87e872c3983e3fe4cad748fe6e9ad590151c0a6d9c07050", + "size": 4849 + }, + { + "path": "skills/add-reactor/SKILL.md", + "sha256": "a0067468efcff0e47f175a06fa3afdb6d5bc2d1f72b7fdb7865d3edcfbe1a2b3", + "size": 6736 + }, + { + "path": "skills/add-reducer/SKILL.md", + "sha256": "7b789a34cad977d818444a3ab9c8e19c22b1cdfea6a27b83e98beecfbf967273", + "size": 4321 + }, + { + "path": "skills/auth-and-identity/SKILL.md", + "sha256": "91511bb9085ebc2b733d1abc24bafacd295811a100961d01764dcb3326e3bdb3", + "size": 9900 + }, + { + "path": "skills/auth-and-identity/references/authentication.md", + "sha256": "594d6a9190a40a0da0c0afeb0c0915e7305f586b6c014f96c04d4d9476dc144e", + "size": 5014 + }, + { + "path": "skills/auth-and-identity/references/authorization.md", + "sha256": "7875e1a34ee24170aa0bc99cc03954e13c1b3c7e180ff97dde8487cf015f93db", + "size": 6061 + }, + { + "path": "skills/auth-and-identity/references/backend-identity.md", + "sha256": "b467a64027d017b8e39821bdcb7f77834350cd1b47625f603bad72a3a317bdf7", + "size": 14366 + }, + { + "path": "skills/auth-and-identity/references/frontend.md", + "sha256": "6d9cbfbd50ff9539bb75e50a0fd8c7b930121716602718bb13c87503c6cc73ec", + "size": 4614 + }, + { + "path": "skills/auth-and-identity/references/local-development.md", + "sha256": "497832d9d5a60fd0f9990dab275c0db0eb593476b3bdf8fd4a05009506579b68", + "size": 4226 + }, + { + "path": "skills/call-command-from-code/SKILL.md", + "sha256": "4fb567fb9b9ca689e52e604b262f25a5172834d3549762df046fd3a9c90f40bb", + "size": 4269 + }, + { + "path": "skills/cratis-command/SKILL.md", + "sha256": "315c1abd3f833e5d4222172f15635b55eb1775bbdbb79f84c9eb3d19bf12a61f", + "size": 11830 + }, + { + "path": "skills/cratis-command/references/command-dialog.md", + "sha256": "8e65ece3f614d5c4224a34bd94ab481d80eca8b64bf8f4cbe07a487f1edb986b", + "size": 4793 + }, + { + "path": "skills/cratis-command/references/command-result.md", + "sha256": "aae44ec2aeff16f40cc4d8996e7d4fc4c4925f0b705e3fedfb87f300bb7f344a", + "size": 2230 + }, + { + "path": "skills/cratis-command/references/proxy-setup.md", + "sha256": "d8ebd3270d193ec743d74f80ace55cb57ff0eabbcef41419e9432efa233fa914", + "size": 1977 + }, + { + "path": "skills/cratis-command/references/validation.md", + "sha256": "6ef7a29ac797cf16b0c43a34d7e859597638be739d4876b1748958d93179011a", + "size": 3242 + }, + { + "path": "skills/cratis-fundamentals-concept/LICENSE", "sha256": "91062c99a13a9ae1cf4097be14c33b031b6b6944c79388753c6ddb0e98339d63", "size": 142 }, { - "path": "pi/package/skills/cratis-example/SKILL.md", - "sha256": "53d2558526ac9e045d678c8474a990113a47dbc829aaf5cf1e9c5fa2e5e707ea", - "size": 246 + "path": "skills/cratis-fundamentals-concept/SKILL.md", + "sha256": "423ab2d27b08e13d3c7c3fbaf9bbc06ea1fb9a13610bdcfdf37398b12d593ec9", + "size": 7766 }, { - "path": "pi/package/skills/cratis-example/assets/example.txt", - "sha256": "55b477cd965ea5b7591cfd935e3a859175b65b28a71fda708bdb52d8e4efc46b", - "size": 79 + "path": "skills/cratis-react-page/SKILL.md", + "sha256": "7dfe2eb8a8336f5b3f65af79105d07427b55cc651fac93fa979f42a6878b7f08", + "size": 8833 }, { - "path": "pi/package/skills/cratis-example/references/guide.md", - "sha256": "e239fea6f77cca9286f70ce18a9f4e84849b9dd7c2e74ecf27314f8167b40864", - "size": 148 + "path": "skills/cratis-react-page/references/data-page.md", + "sha256": "d98695778cda0b5281ff0386165869023314d957cb89bfc100ad6a52fe907407", + "size": 3165 }, { - "path": "provenance.json", - "sha256": "cf77e8c34fbbf3d5faedebde2751e6c1f69d8a9a97d160a1ea305a80af705e2f", - "size": 1231 + "path": "skills/cratis-react-page/references/data-table.md", + "sha256": "6f3cdc12c81f9af67abed46ed4c698eb2a66691e9bed6a04d65d605b7445351e", + "size": 1770 + }, + { + "path": "skills/cratis-react-page/references/dialogs.md", + "sha256": "a646df0fdd1a026e9c424eb823e5104d9482617dd8c6674d3756ce81309dd317", + "size": 6246 + }, + { + "path": "skills/cratis-react-page/references/mvvm.md", + "sha256": "8d4e85cdfdf179c2676b0d78fd91cca36268b7afbdf3439d4a98204b9cdd99e1", + "size": 3171 + }, + { + "path": "skills/cratis-readmodel/SKILL.md", + "sha256": "be6b27a66f3fbeed427e3d18ab2004b919c9898ea3878151209fcff12fefbbdc", + "size": 10846 + }, + { + "path": "skills/cratis-readmodel/references/events.md", + "sha256": "4a957b608b4e44b08e8a85705aa9bc1fcddc7f1f3a0a5728c7ea6df75d61b5ef", + "size": 3199 + }, + { + "path": "skills/cratis-readmodel/references/projections.md", + "sha256": "217c727da14c4755941ff110bc799e23b0cddff151cc0a9ad4803daa141d6fad", + "size": 5930 + }, + { + "path": "skills/cratis-readmodel/references/queries.md", + "sha256": "9ed01e8e3e0b87e263e217877daf417334d145d2d1b12c28dd072a8b410c81c3", + "size": 2655 + }, + { + "path": "skills/cratis-readmodel/references/reducers.md", + "sha256": "89635a8aa7ddb7b1d144d07a35c4c93c3fbc75a254424424cad6921a1575f76e", + "size": 4163 + }, + { + "path": "skills/cratis-specs-csharp/SKILL.md", + "sha256": "fbde383f9b9fa4197934bf7bb86e349d184f1fa3497224ad42a6ab6b145783e8", + "size": 8458 + }, + { + "path": "skills/cratis-specs-csharp/references/csharp-patterns.md", + "sha256": "c44ac0f1a3dc3de07adf2c67a3f85e2b40f890b4f16925bbc15abc9a08c946b4", + "size": 4620 + }, + { + "path": "skills/cratis-specs-csharp/references/integration-specs.md", + "sha256": "77b764f686c5561c9e10b10cd7d4c9953149b2e3fa2e623ca410b19d67c39806", + "size": 3990 + }, + { + "path": "skills/cratis-specs-typescript/SKILL.md", + "sha256": "eb01957bcdca52e6ef62b4f4e56091cae11d7f842265da972c3c0ee637d145b7", + "size": 4596 + }, + { + "path": "skills/cratis-specs-typescript/references/typescript-patterns.md", + "sha256": "4eedccda9f8396efefcc1f8fb74c3582c8d157823080821e9d9b72a2b3f6a0b3", + "size": 4028 + }, + { + "path": "skills/cratis-vertical-slice/SKILL.md", + "sha256": "a5d90be9cfc37ac2a0c7b51a8a69fce815fb744e801ea94619dff96553a41045", + "size": 6807 + }, + { + "path": "skills/cratis-vertical-slice/references/concepts.md", + "sha256": "9b22698ee8294568c0564c55f74baefd74f2cb5de99fd1b24eafe015cf305a14", + "size": 3133 + }, + { + "path": "skills/cratis-vertical-slice/references/slice-anatomy.md", + "sha256": "522466e36311425f92dfde6c49fd29b0b0f6064890ecb20da49d18b7b4103ca2", + "size": 6664 + }, + { + "path": "skills/cratis-vertical-slice/references/slice-types.md", + "sha256": "84c25543bd94a9bcce2c0cbaadd4c47f94e69ca9caa090d8203f27fbd7eb67b6", + "size": 3617 + }, + { + "path": "skills/create-event-model/SKILL.md", + "sha256": "620e9d13a8e2752098acd8c60b620cdb6df042f9a49c2072edf5eb882c715581", + "size": 5788 + }, + { + "path": "skills/cross-cutting-properties/SKILL.md", + "sha256": "633bef916d401d2ecd74358cc57d6a8e5783e57394ff806a33b44970080a7885", + "size": 3921 + }, + { + "path": "skills/diagnose-slice/SKILL.md", + "sha256": "45839e11e7aa7c2b66e93a2b83b2cef964aa2aa8abeb9064a9505c90677e95ea", + "size": 5189 + }, + { + "path": "skills/discover-implementations/SKILL.md", + "sha256": "907a0f4f3d06856b07f10c1465a7bc2c515d8ec27b0cb84ba335a217f7290cf0", + "size": 4881 + }, + { + "path": "skills/event-modeling/SKILL.md", + "sha256": "4cc24ccf0faf14066dad477aa87732a6e593ef263c9e0f377b1c9be59ed32f27", + "size": 6248 + }, + { + "path": "skills/event-type-migrations/SKILL.md", + "sha256": "eec56904897163d9261d3ec976595956fa089a717c7854837ef318e75e387412", + "size": 4357 + }, + { + "path": "skills/inspect-running-chronicle/SKILL.md", + "sha256": "d896c80b4a95a3517fbc66b9fc169c9a11b82096448cb1e7ce5a8a9e51d11aa3", + "size": 4909 + }, + { + "path": "skills/multi-tenancy/SKILL.md", + "sha256": "d88b6633208c7ce3dddb629567db8df2fcfda351c4191b326cee7c382f75ed31", + "size": 3160 + }, + { + "path": "skills/new-vertical-slice/SKILL.md", + "sha256": "657c4b0d399a3e9d282210db7ff9d539fa5437bfbc40096f6df544aa965d80b3", + "size": 5459 + }, + { + "path": "skills/new-vertical-slice/references/PATTERNS.md", + "sha256": "5edc50e6056915e19fe16e0abfeb8fdb092f64c42c30b16bac7115ba503d49a4", + "size": 12808 + }, + { + "path": "skills/query-paging/SKILL.md", + "sha256": "8287047a2741bb6648ac14f0f187c0d503d83639ec8e554252fdd228b4861294", + "size": 5234 + }, + { + "path": "skills/review-code/SKILL.md", + "sha256": "6cd509452e8ec092f8c334ad25687fb524befdef62afa8c7bd1c820377bdf5de", + "size": 4585 + }, + { + "path": "skills/review-code/references/CHECKLISTS.md", + "sha256": "0839f00edc122991650e3c7ee022a1fc3a9ba2a839945c0eff31f3d978c6c824", + "size": 5413 + }, + { + "path": "skills/review-performance/SKILL.md", + "sha256": "09b8a20a88cc68c364cff009b4e70a635b209da12f9345a1c1ae701a5ba4adb8", + "size": 2591 + }, + { + "path": "skills/review-security/SKILL.md", + "sha256": "bb8e4a571f2814fe5b56ae6cb33a75e21b6484d80e7c5d3f340b0f185c11c68f", + "size": 2449 + }, + { + "path": "skills/scaffold-feature/SKILL.md", + "sha256": "1475435927c072ff27589e380b1fe08ab66c3e94dec7c8be91c7444e0858a4a0", + "size": 2225 + }, + { + "path": "skills/stepper-command-dialog/SKILL.md", + "sha256": "51316f7744182d1af9e3cb87259afc79a0f81534f813c17b54517f4b7b010b7e", + "size": 8572 + }, + { + "path": "skills/toolbar/SKILL.md", + "sha256": "151cfeecaa911d2f16dc35385e644838ae8a61af60b4a0bbab3d2714744161ee", + "size": 10065 + }, + { + "path": "skills/write-specs-events/SKILL.md", + "sha256": "58cf309e9f62c9f0618c72a49ad1707f5dd6c3f691b1d83cc52a22931ee1ac41", + "size": 6406 + }, + { + "path": "skills/write-specs-frontend/SKILL.md", + "sha256": "025c6f9cd3581a5738130ba4ba16cea15a4a6a615e735d3c53dcd3a28734f208", + "size": 3997 + }, + { + "path": "skills/write-specs-readmodels/SKILL.md", + "sha256": "57d0f2511e950edf8b390a2b5c44cd4532a6aa258d78e8bffc7167a6449dc054", + "size": 6380 + }, + { + "path": "skills/write-specs/SKILL.md", + "sha256": "e6bc79cd003e97c591dc0c12c7aa727b809f0e7a8ac9e5d05ad7ab6d79dc8c0e", + "size": 4785 + }, + { + "path": "skills/write-specs/references/EXAMPLES.md", + "sha256": "6dc9b60a7a088c2a9860f4518c5256076b3b3256dd763b237aac05558787b03a", + "size": 5514 + }, + { + "path": "submissions/cursor.json", + "sha256": "406146ead65d1a4530a8c7d321192d2d3b2e37bb37a78113a9149f33b8410506", + "size": 507 + }, + { + "path": "submissions/openai.json", + "sha256": "0a6a79f150e7441a97e716fb153636cb6b9140d830a440faabbdb84cf180ee81", + "size": 2846 } ], - "publicationEligible": false, + "publicationEligible": true, + "installationSupported": false, + "supportGranted": false, "promotionEligible": false } diff --git a/gemini-extension.json b/gemini-extension.json new file mode 100644 index 0000000..f042314 --- /dev/null +++ b/gemini-extension.json @@ -0,0 +1,5 @@ +{ + "name": "public-cratis-ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development" +} diff --git a/gemini/gemini-extension.json b/gemini/gemini-extension.json deleted file mode 100644 index b661242..0000000 --- a/gemini/gemini-extension.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture." -} diff --git a/gemini/skills/cratis-example/LICENSE b/gemini/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/gemini/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/gemini/skills/cratis-example/SKILL.md b/gemini/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/gemini/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/gemini/skills/cratis-example/assets/example.txt b/gemini/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/gemini/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/gemini/skills/cratis-example/references/guide.md b/gemini/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/gemini/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/junie/extensions/cratis/extension.json b/junie/extensions/cratis/extension.json deleted file mode 100644 index 3b44812..0000000 --- a/junie/extensions/cratis/extension.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "cratis", - "description": "Passive Cratis skills fixture." -} diff --git a/junie/extensions/cratis/skills/cratis-example/LICENSE b/junie/extensions/cratis/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/junie/extensions/cratis/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/junie/extensions/cratis/skills/cratis-example/SKILL.md b/junie/extensions/cratis/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/junie/extensions/cratis/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/junie/extensions/cratis/skills/cratis-example/assets/example.txt b/junie/extensions/cratis/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/junie/extensions/cratis/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/junie/extensions/cratis/skills/cratis-example/references/guide.md b/junie/extensions/cratis/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/junie/extensions/cratis/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/kiro/plugin.json b/kiro/plugin.json deleted file mode 100644 index 12efde3..0000000 --- a/kiro/plugin.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", - "name": "cratis", - "version": "0.0.0-fixture", - "description": "Passive Cratis skills fixture.", - "author": { - "name": "Cratis" - }, - "license": "MIT" -} diff --git a/kiro/skills/cratis-example/LICENSE b/kiro/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/kiro/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/kiro/skills/cratis-example/SKILL.md b/kiro/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/kiro/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/kiro/skills/cratis-example/assets/example.txt b/kiro/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/kiro/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/kiro/skills/cratis-example/references/guide.md b/kiro/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/kiro/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/marketplace-release.json b/marketplace-release.json new file mode 100644 index 0000000..294d868 --- /dev/null +++ b/marketplace-release.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": "1.0.0", + "schemaPath": "distribution/public-marketplace-release.schema.json", + "schemaSha256": "ab1ca6d88091b81eaa27c29838ca687c23593638fced74cdf8a41224ac434316", + "state": "PUBLIC_EVALUATION_MARKETPLACE", + "version": "0.2.0", + "profileId": "public-cratis-ai", + "packageName": "@cratis/ai", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "sourceCommit": "ba87a907cf45f11cb804d0a9063cb3709cce1b16", + "targetCount": 34, + "skillCount": 34, + "selectedHarnesses": [ + "agent-skills", + "agent-plugin", + "claude", + "codex", + "copilot", + "cursor", + "gemini", + "kiro", + "pi" + ], + "marketplaceChannels": [ + "agent-skills", + "agent-plugin", + "claude-code", + "codex-openai-submission", + "github-copilot", + "cursor-submission", + "gemini-cli", + "kiro", + "pi-git-package" + ], + "installationAvailable": true, + "installationSupported": false, + "supportGranted": false, + "promotionEligible": false +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ba354d8 --- /dev/null +++ b/package.json @@ -0,0 +1,24 @@ +{ + "name": "@cratis/ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "private": false, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/Cratis/AI.Distribution" + }, + "homepage": "https://cratis.io/ai", + "files": [ + "skills" + ], + "keywords": [ + "pi-package", + "cratis" + ], + "pi": { + "skills": [ + "./skills" + ] + } +} diff --git a/pi/package/package.json b/pi/package/package.json deleted file mode 100644 index 620f3e6..0000000 --- a/pi/package/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "@cratis/ai", - "version": "0.0.0-fixture", - "description": "Private passive Cratis skills fixture.", - "private": true, - "license": "MIT", - "files": [ - "skills" - ], - "keywords": [ - "pi-package" - ], - "pi": { - "skills": [ - "./skills" - ] - } -} diff --git a/pi/package/skills/cratis-example/LICENSE b/pi/package/skills/cratis-example/LICENSE deleted file mode 100644 index 63c4768..0000000 --- a/pi/package/skills/cratis-example/LICENSE +++ /dev/null @@ -1,2 +0,0 @@ -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/pi/package/skills/cratis-example/SKILL.md b/pi/package/skills/cratis-example/SKILL.md deleted file mode 100644 index b9b1c6f..0000000 --- a/pi/package/skills/cratis-example/SKILL.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: cratis-example -description: Use this sanitized fixture skill to verify exact public artifact materialization. ---- - -# Cratis example fixture - -Read the [fixture reference](references/guide.md) and the -[fixture asset](assets/example.txt). diff --git a/pi/package/skills/cratis-example/assets/example.txt b/pi/package/skills/cratis-example/assets/example.txt deleted file mode 100644 index a81c0ca..0000000 --- a/pi/package/skills/cratis-example/assets/example.txt +++ /dev/null @@ -1,2 +0,0 @@ -Copyright (c) Cratis. Licensed under the MIT license. -Sanitized fixture asset. diff --git a/pi/package/skills/cratis-example/references/guide.md b/pi/package/skills/cratis-example/references/guide.md deleted file mode 100644 index 25d3106..0000000 --- a/pi/package/skills/cratis-example/references/guide.md +++ /dev/null @@ -1,5 +0,0 @@ - - -# Fixture reference - -This is sanitized public fixture content. diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..14d3e4c --- /dev/null +++ b/plugin.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "public-cratis-ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "author": { + "name": "Cratis", + "url": "https://cratis.io" + }, + "homepage": "https://cratis.io/ai", + "repository": "https://github.com/Cratis/AI.Distribution", + "license": "MIT", + "keywords": [ + "cratis", + "agent-skills" + ] +} diff --git a/plugins/public-cratis-ai/.claude-plugin/plugin.json b/plugins/public-cratis-ai/.claude-plugin/plugin.json new file mode 100644 index 0000000..36e38d9 --- /dev/null +++ b/plugins/public-cratis-ai/.claude-plugin/plugin.json @@ -0,0 +1,8 @@ +{ + "name": "public-cratis-ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "author": { + "name": "Cratis" + } +} diff --git a/plugins/public-cratis-ai/.codex-plugin/plugin.json b/plugins/public-cratis-ai/.codex-plugin/plugin.json new file mode 100644 index 0000000..f109dde --- /dev/null +++ b/plugins/public-cratis-ai/.codex-plugin/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "public-cratis-ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "skills": "./skills/" +} diff --git a/plugins/public-cratis-ai/plugin.json b/plugins/public-cratis-ai/plugin.json new file mode 100644 index 0000000..14d3e4c --- /dev/null +++ b/plugins/public-cratis-ai/plugin.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "public-cratis-ai", + "version": "0.2.0", + "description": "Cratis AI skills for event-sourced and CQRS application development", + "author": { + "name": "Cratis", + "url": "https://cratis.io" + }, + "homepage": "https://cratis.io/ai", + "repository": "https://github.com/Cratis/AI.Distribution", + "license": "MIT", + "keywords": [ + "cratis", + "agent-skills" + ] +} diff --git a/plugins/public-cratis-ai/skills/add-business-rule/SKILL.md b/plugins/public-cratis-ai/skills/add-business-rule/SKILL.md new file mode 100644 index 0000000..d9070c9 --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-business-rule/SKILL.md @@ -0,0 +1,111 @@ +--- +name: add-business-rule +description: Use this skill when asked to add a validation rule, business rule, or uniqueness constraint to an existing command in a Cratis-based project. +--- + +Add a business rule or event-store constraint to an existing command. + +## Choose the right mechanism + +Pick by what the decision *is* — see [vertical-slices.md](../../rules/vertical-slices.md) "The decision matrix": + +| Scenario | Use | +|---|---| +| Reusable value invariant (length, format, range) | `ConceptValidator` on the concept type | +| Command-input / cross-field / pre-handler rule (incl. injected read-model/service checks) | `CommandValidator` with `RuleFor(...)` | +| Handler needs fetched/computed data before it can build the event | `Provide()` — fetch the data; short-circuit with `ValidationResult.Error(...)` if unusable | +| State-dependent rule that must hold **under concurrency** | inject the read model into `Handle()`, return `Result` | +| Single-event uniqueness (most cases) | `[Unique]` attribute on the event type | +| Multi-event uniqueness or needs `RemovedWith` | `IConstraint` | +| Genuinely exceptional failure (bug, missing infra) | `throw` a custom domain exception | + +> **Never throw for normal business rejection.** A thrown exception from `Provide()`/`Handle()` surfaces as `HasExceptions` / HTTP 500 — **not** a validation result. Recoverable, user-facing rejections are validation: return them via a validator, `Provide()`, or `Result`. + +## Business rules via DCB (read model as `Handle()` argument) + +Use when the rule depends on **Chronicle event-sourced state** (current count, accumulated value) and must hold **under concurrency**. The framework injects the current read-model snapshot, resolved by the command's event-source id, before `Handle()` runs. Return a `Result` — success carries the event, failure carries a typed validation error. + +The read model must already exist in the slice (`[ReadModel]` + a model-bound projection). If it doesn't, add it first — see the `add-projection` / `cratis-readmodel` skills. + +```csharp +[Command] +public record AddItemToCart(CartId CartId, ItemId ItemId) +{ + /// Adds the item; rejects when the cart already holds the maximum. + /// The current cart summary (injected by event-source id). + /// The event on success, or a validation error. + public Result Handle(CartSummary cart) => + cart.ItemCount >= 3 + ? ValidationResult.Error("A cart can hold at most 3 items.") + : new ItemAddedToCart(ItemId); +} +``` + +**Key rules:** +- The read-model parameter type must be a `[ReadModel]` in the same slice/feature; the framework resolves the instance by the command's event-source key. To read a read model keyed differently, use `Provide()` with `IReadModels.GetInstanceById((EventSourceId)key)`. +- Return `Result` — never throw for the rejection. +- One parameter per logical read model; multiple reads → multiple parameters. + +## Pre-handler rules — `CommandValidator` + +For rules that don't depend on race-sensitive state, put them in the validator that sits beside the command. Constructor dependencies (read models, services) are injected and resolved by the command's event-source id. + +```csharp +public class TransferFundsValidator : CommandValidator +{ + public TransferFundsValidator() => + RuleFor(c => c.Amount).GreaterThan(0).WithMessage("Amount must be positive."); +} +``` + +Single-property intrinsic rules belong on `ConceptValidator` instead, so they travel with the value everywhere. + +## Event-store constraints — `[Unique]` (preferred) + +For uniqueness on a single event type, adorn the event type or one of its properties with `[Unique]`. No separate constraint class is needed; constraints are discovered automatically. + +```csharp +// Event-type uniqueness — only one of this event per event source +[EventType] +[Unique(message: "A project with this name already exists.")] +public record ProjectRegistered(ProjectName Name); + +// Property uniqueness — the value must be unique across event sources +[EventType] +public record UserRegistered([Unique(name: "UniqueEmail", message: "Email already registered.")] EmailAddress Email, DisplayName Name); + +// Release a claimed value on removal — the name must match the [Unique] name exactly +[EventType] +[RemoveConstraint("UniqueEmail")] +public record UserRemoved; +``` + +Stack multiple `[RemoveConstraint("...")]` attributes on one removal event to release several claimed values at once. To enforce the same property across **multiple** event types, give each its `[Unique(name: "UniqueEmail")]` with the **same name**. + +## Event-store constraints — `IConstraint` (advanced) + +Use `IConstraint` when uniqueness spans event types with different property names, needs `.IgnoreCasing()`, or a `RemovedWith` event must release it. `Define` is **declarative** — member-access lambdas only, no DI or side effects. + +```csharp +public class UniqueProjectName : IConstraint +{ + public void Define(IConstraintBuilder builder) => + builder.Unique(unique => + unique + .On(e => e.Name) + .IgnoreCasing() // case-insensitive — do NOT lowercase inside the lambda + .RemovedWith()); // omit if there is no remove event +} +``` + +- The `.On(e => e.Prop)` lambda is parsed for **member access only** — `e => e.Email.ToLower()` is a pitfall; use `.IgnoreCasing()`. +- `builder.Unique(name:, message:)` enforces "one event of this type per event source" via the fluent builder (distinct from class-level `[Unique]`). +- Treat a constraint violation as a validation result on the command append — never as a thrown exception. + +**Spec the constraint** with `EventScenario`: seed the conflicting state, append again, and assert `ShouldHaveConstraintViolationFor(ConstraintNames.UniqueX)` — **the constraint name, never the message**. Use per-test unique values (`$"{Guid.NewGuid():N}"`) so specs aren't order-dependent. For a release, append the removal event then assert the value can be re-claimed. + +## After adding + +1. Add a spec for the failure case — assert **both** `ShouldNotBeSuccessful()` and `ShouldHaveValidationErrors()` (or `ShouldHaveConstraintViolationFor(name)` for constraints). See the `write-specs` / `write-specs-events` skills. +2. Build clean (Debug and Release) and run the specs. +3. Fix all failures before completing. diff --git a/plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md b/plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md new file mode 100644 index 0000000..8c91fed --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-ef-migration/SKILL.md @@ -0,0 +1,168 @@ +--- +name: add-ef-migration +description: Use this skill when asked to add a database table, column, relationship, or other schema change via Entity Framework Core in a Cratis-based project. +--- + +Add or update an Entity Framework Core schema change with a hand-written migration. + +> **Always read the [efcore.md](../../rules/efcore.md) rule first.** It is the source of truth for project structure, base types, column helpers, and migration conventions. + +## Pre-flight — Understand the project split + +Before writing anything, locate these three projects: + +| Project | What lives there | +|---------|-----------------| +| **Database** | Migrations only — `WellKnownTables.cs` + versioned migration files | +| **Core** | Entities and feature DbContexts (co-located with features) | +| **Infrastructure** | DbContext registration, migration runner, cross-cutting EF setup | + +**Critical:** `Database` must NEVER import from `Core`. Migrations reference only `WellKnownTables` constants and EF migration types. + +## Step 1 — Update or create the entity + +Add, rename, or remove properties on the entity `record` in the **Core** project, co-located with its feature: + +``` +Missions/StartupPhase/ +├── StartupPhase.cs ← entity record +├── StartupPhaseDbContext.cs ← feature DbContext +└── ... +``` + +## Step 2 — Update or create the feature DbContext + +Use the Cratis Arc base types — never inherit directly from `DbContext`: + +- **`ReadOnlyDbContext`** — for all read model / projection contexts (the vast majority) +- **`BaseDbContext`** — only for writable contexts that own state + +```csharp +public class StartupPhaseDbContext(DbContextOptions options) + : ReadOnlyDbContext(options) +{ + public DbSet StartupPhases => Set(); +} +``` + +Create one focused DbContext per feature — never a "god context" with unrelated entities. + +## Step 3 — Add the table name to WellKnownTables + +If this is a new table, add the constant to `Database/WellKnownTables.cs` **before** writing the migration: + +```csharp +public static class WellKnownTables +{ + public const string StartupPhases = "StartupPhases"; +} +``` + +## Step 4 — Write the migration by hand + +Migrations are **hand-written**, not generated by `dotnet ef migrations add`. Place the file in the **Database** project using versioned naming: + +``` +Database/ +├── Missions/ +│ ├── v1_0_0.cs +│ └── v1_1_0.cs +└── WellKnownTables.cs +``` + +Use the `v{major}_{minor}_{patch}.cs` naming pattern. The namespace must match the folder: + +```csharp +namespace Database.Missions; + +public class v1_1_0 : Migration +{ + /// + /// Apply the schema change. + /// + /// The migration builder. + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Description", + table: WellKnownTables.Missions, + type: migrationBuilder.StringColumnType(maxLength: 1000), + nullable: true); + } + + /// + /// Reverse the schema change. + /// + /// The migration builder. + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Description", + table: WellKnownTables.Missions); + } +} +``` + +### Cross-database column helpers (mandatory) + +Always use the Cratis Arc `MigrationBuilder` extension helpers — never raw `table.Column()`: + +| Helper | Use for | +|--------|---------| +| `table.StringColumn(migrationBuilder)` | Text / varchar columns | +| `table.GuidColumn(migrationBuilder)` | UUID / GUID columns | +| `table.NumberColumn(migrationBuilder)` | Integer, long, or numeric columns | +| `table.DateTimeOffsetColumn(migrationBuilder)` | Timestamps with timezone | + +Example `CreateTable`: + +```csharp +migrationBuilder.CreateTable( + name: WellKnownTables.StartupPhases, + columns: table => new + { + Id = table.StringColumn(migrationBuilder, nullable: false), + Title = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false), + ResourceId = table.NumberColumn(migrationBuilder, nullable: true), + DispatchTime = table.DateTimeOffsetColumn(migrationBuilder), + UrgencyId = table.GuidColumn(migrationBuilder) + }, + constraints: table => + { + table.PrimaryKey("PK_StartupPhases", x => x.Id); + }); +``` + +## Step 5 — Register the DbContext (if new) + +In the **Infrastructure** project, ensure the context is registered. Read-only contexts are auto-discovered: + +```csharp +services.AddReadModelDbContextsWithConnectionStringFromAssemblies( + connectionString, + configureOptions, + [Assembly.GetExecutingAssembly()]); +``` + +Writable contexts need explicit registration: + +```csharp +services.AddDbContextWithConnectionString(connectionString); +``` + +## Step 6 — Update specs + +Integration specs using in-memory SQLite pick up schema changes automatically via `context.Database.EnsureCreated()`. If specs break, check that the fixture uses the correct connection string and that the new migration is in the `Database` assembly. + +## Step 7 — Validate + +Run `dotnet build` and `dotnet test`. Fix all failures before completing. + +## Key rules + +- **Never** use `dotnet ef migrations add` — all migrations are hand-written +- **Never** use `dotnet ef database update` — use the custom runner: `await app.ApplyAllMigrations(connectionString)` +- **Never** hardcode a provider (`UseSqlite`, `UseNpgsql`) — use `UseDatabaseFromConnectionString` +- **Never** mutate state directly through a DbContext — all writes flow through Chronicle events and projections +- **Always** use `WellKnownTables` constants — never magic strings for table names +- **Always** use cross-database column helpers — never raw `table.Column()` diff --git a/plugins/public-cratis-ai/skills/add-projection/SKILL.md b/plugins/public-cratis-ai/skills/add-projection/SKILL.md new file mode 100644 index 0000000..e775a3f --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-projection/SKILL.md @@ -0,0 +1,103 @@ +--- +name: add-projection +description: Use this skill when asked to add a Chronicle projection to a Cratis-based project. Favor model-bound projections by default, and only fall back to declarative/fluent `IProjectionFor` projections when model-bound attributes cannot express the behavior cleanly. Enforces the AutoMap-first rule and Chronicle-specific join semantics. +--- + +Add a Chronicle **projection** that populates a read model from events. + +> For **reactors** (automation / translation), see the `add-reactor` skill instead. + +## Projection — Model-Bound (preferred) + +Put projection metadata directly on the read model using attributes. No separate class needed. + +```csharp +[ReadModel] +[FromEvent] // auto-maps all matching property names +public record ( + [Key] Id, // marks the primary key + ) // auto-mapped from SomeEventHappened +{ + public static ISubject>> All(IMongoCollection<> collection) => + collection.Observe(); +} +``` + +**Attribute reference:** +| Attribute | Purpose | +|-----------|---------| +| `[FromEvent]` | Maps event `T` onto the read model; matching property names map automatically (AutoMap is on by default — never call `.AutoMap()`) | +| `[FromEvent(key: nameof(T.Prop))]` | Same, but uses `Prop` as the read model key instead of EventSourceId | +| `[Key]` | Marks the primary key property | +| `[SetFrom(nameof(T.Prop))]` | Explicitly maps one property from event T | +| `[AddFrom(nameof(T.Prop))]` | Adds event property value to the read model property | +| `[SubtractFrom(nameof(T.Prop))]` | Subtracts event property value | +| `[ChildrenFrom(key: nameof(T.Prop))]` | Projects into a nested child collection | +| `[Join(on: nameof(Prop), eventPropertyName: nameof(T.EProp))]` | Joins data from a related event | +| `[RemovedWith]` | Marks the instance as removed when event T is appended | + +**Critical rules:** +- Joins must be on Chronicle **events** — NEVER join on a read model type +- If property names between event and read model match, `[FromEvent]` alone is sufficient +- Child types also support all attributes recursively + +## Projection — Fluent / declarative (fallback for complex cases) + +Use `IProjectionFor` only when the projection logic is too complex for model-bound attributes or would become less clear if forced into attributes. + +```csharp +public class Projection : IProjectionFor<> +{ + public void Define(IProjectionBuilderFor<> builder) => + builder + .From(b => + b.UsingKey(e => e.SomeId)) + .RemovedWith(); +} +``` + +**Critical rules:** +- AutoMap is on by default — just call `.From<>()` directly. Only call `.AutoMap()` if you previously used `.NoAutoMap()`. +- Joins are on Chronicle **events** only — NEVER join on the read model +- There is NO `Identifier` / `ProjectionId` property — do not add one + +## Advanced patterns & startup-crash gotchas + +- **`[Nested]`** projects a single child object onto a nested type. Put `[FromEvent]` on the **nested type** (or use property-level `[SetFrom]` on the parent when the parent already declares the event). `[NoAutoMap]` and explicit `[SetFrom]` work inside the nested type; `[Nested]` can recurse inside a `[ChildrenFrom]` item. + - ⚠️ **Duplicate-`[FromEvent]` crash:** declaring class-level `[FromEvent]` (no `key:`) on **both** the parent and a nested/child type for the **same** event throws a `Key: +N` duplicate-key exception at startup. Fix: keep `[FromEvent]` on the nested type only, or switch the nested type to property-level `[SetFrom]`. + - ⚠️ **Duplicate-`[SetFromContext]` crash:** two properties with `[SetFromContext]` on the same read model crash at startup (`Key: +1`). Merge them or use `[FromEvery]`. +- **`[FromAll]` vs `[FromEvery]`:** `[FromAll]` (class-level) subscribes to **every** event type system-wide (audit/log models — pair with `[NotRewindable]`). `[FromEvery]` is a property-level capture across the events the model **already** declares via `[FromEvent]` (e.g. to stamp `EventContext` data) — it does *not* subscribe to new event types. +- **Constant-key counters:** `[Count(ConstantKey="metrics")]` / `[Increment(ConstantKey=...)]` route all matching events into **one** aggregating document at the constant key (distinct from `.UsingConstantKey("...")` on the fluent builder). +- **Children with different key names:** when child events use different key properties, use the fluent form — `.From(b => b.UsingKey(e => e.Email)).From(b => b.UsingKey(e => e.OriginalEmail))` — which model-bound `[ChildrenFrom]` (single key) can't express. +- **Source selection:** class-level `[EventSequence("name")]`, `[EventLog]`, or `[EventStore("name")]` choose where the projection reads from. ⚠️ `[FromEventSequence]` is **removed** — use `[EventSequence("name")]`. +- **Cross-stream specs:** in `ReadModelScenario`, seed each contributing stream with its own `Given.ForEventSource(...)`. Don't pre-emptively `[Fact(Skip=...)]` a cross-stream assertion — only skip on a reproduced harness gap, with the reason in the skip message. + +## After creating + +Run `dotnet build`. Fix all errors before completing. + +## Appended event metadata and filtering + +Chronicle correlates appended metadata in two different ways: + +- **Projections** select input through event types, joins, and event sequence configuration +- **Reducers and reactors** can additionally filter by appended tags, event source type, and event stream type + +Use append metadata like this: + +```csharp +await eventLog.Append( + EventSourceId.New(), + new OrderPlaced(42m), + eventStreamType: "fulfillment", + eventSourceType: "order", + tags: ["priority"]); +``` + +If you need metadata-based filtering for downstream processing, pair the projection with a reducer or reactor using `[FilterEventsByTag]`, `[EventSourceType]`, or `[EventStreamType]`. Projection `[Tag]` and `[Tags]` attributes label the projection definition; they do not filter appended events. + +For examples, see `Documentation/events/filtering/`. + +--- + +For the full model-bound projection attribute reference and fluent builder API, see [references/CHRONICLE-API.md](references/CHRONICLE-API.md). diff --git a/plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md b/plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md new file mode 100644 index 0000000..a6414e0 --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-projection/references/CHRONICLE-API.md @@ -0,0 +1,169 @@ +# Chronicle Projection API + +--- + +## Model-Bound Projections (preferred) + +Apply attributes directly to read model types. No separate projection class needed. + +### Class-level attributes + +#### `[FromEvent]` +Maps all properties with matching names from event `T` to the read model. +Equivalent to `.From()` in the fluent API — AutoMap is on by default, so you never call `.AutoMap()` explicitly. + +```csharp +[ReadModel] +[FromEvent] +public record Account( + [Key] Guid Id, + string Name, // auto-mapped from AccountOpened.Name + decimal Balance); // auto-mapped from AccountOpened.Balance +``` + +#### `[FromEvent(key: nameof(T.PropertyName))]` +Same as above, but uses the specified event property as the read model key instead of EventSourceId. + +```csharp +[ReadModel] +[FromEvent(key: nameof(OrderPlaced.OrderId))] +public record Order([Key] Guid Id, decimal Total); +``` + +Multiple `[FromEvent]` attributes are supported for different events: + +```csharp +[FromEvent] +[FromEvent] +public record Account([Key] Guid Id, string Name, decimal Balance); +``` + +--- + +### Property-level attributes + +#### `[Key]` +Marks the primary key property. + +#### `[SetFrom]` / `[SetFrom(nameof(T.Prop))]` +Maps a specific property from event `T`. Omit the property name if the names match. + +#### `[SetFrom]` with multiple events +Apply multiple attributes to update the same property from different events. + +```csharp +[SetFrom(nameof(AccountOpened.AccountName))] +[SetFrom(nameof(AccountRenamed.NewName))] +string Name +``` + +#### `[AddFrom(nameof(T.Prop))]` +Adds the event property value to the read model property (accumulates). + +#### `[SubtractFrom(nameof(T.Prop))]` +Subtracts the event property value. + +#### `[ChildrenFrom(key: nameof(T.ChildId))]` +Projects into a nested child collection. The child type also supports all attributes recursively. + +```csharp +public record Order( + [Key] Guid Id, + [ChildrenFrom(key: nameof(LineItemAdded.ItemId))] + IEnumerable Items); + +public record LineItem( + [Key] Guid Id, + string ProductName, // auto-mapped + int Quantity, // auto-mapped + decimal Price); // auto-mapped +``` + +#### `[Join(on: nameof(Prop), eventPropertyName: nameof(T.EProp))]` +Joins data from a related event (keyed by a different entity). + +```csharp +[Join( + on: nameof(CustomerId), + eventPropertyName: nameof(CustomerRegistered.Name))] +string CustomerName +``` + +#### `[RemovedWith]` +Marks the instance removed when event `T` is appended. + +--- + +## Fluent Projections (alternative for complex cases) + +Use `IProjectionFor` when model-bound attributes don't fit the complexity of the projection. + +### Basic structure + +```csharp +public class MyProjection : IProjectionFor +{ + public void Define(IProjectionBuilderFor builder) => + builder + .From(...); // AutoMap is on by default +} +``` + +> **There is NO `Identifier`/`ProjectionId` property** — do not add one. + +--- + +### `.AutoMap()` + +Maps all event properties to read model properties with matching names automatically. +**AutoMap is on by default — you only need to call it explicitly if you previously used `.NoAutoMap()`.** + +--- + +### `.From(Action>)` + +Handles an event type. Builder methods: + +- `.UsingKey(e => e.PropertyOnEvent)` — sets the key from an event property +- `.UsingParentKey(e => e.PropertyOnEvent)` — sets key from parent (child projections) +- `.Set(m => m.Target).To(e => e.Source)` — explicit property mapping +- `.Set(m => m.Prop).ToValue(literal)` — set to constant value +- `.Add(m => m.Counter).With(1)` — increment by fixed value +- `.Subtract(m => m.Counter).With(1)` — decrement by fixed value +- `.Count(m => m.TotalCount)` — increment count by 1 + +--- + +### `.Children(m => m.ChildCollection, childBuilder => ...)` + +Projects into a nested collection. + +```csharp +builder + .Children(m => m.LineItems, child => // AutoMap is on by default + child + .From(b => + b.UsingKey(e => e.LineItemId) + .Set(li => li.Description).To(e => e.Description)) + .RemovedWith()); +``` + +--- + +### `.RemovedWith()` + +Marks the read model as removed when the specified event is appended. + +--- + +### `.Join(Action>)` + +Joins data from a different event. The join is always on the **event**, never on the read model. + +```csharp +builder + .From() // AutoMap is on by default + .Join(b => + b.On(e => e.ProjectId) + .Set(m => m.OwnerName).To(e => e.OwnerName)); +``` diff --git a/plugins/public-cratis-ai/skills/add-reactor/SKILL.md b/plugins/public-cratis-ai/skills/add-reactor/SKILL.md new file mode 100644 index 0000000..05c6a42 --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-reactor/SKILL.md @@ -0,0 +1,149 @@ +--- +name: add-reactor +description: Use this skill when asked to add a Chronicle reactor (automation or translation) to a Cratis-based project. Reactors observe events and produce side effects. +--- + +Add a Chronicle **reactor** that triggers automation or translation logic in response to events. + +> **Always read the [reactors.md](../../rules/reactors.md) rule first.** It is the source of truth for reactor conventions, rules, and patterns. + +## Step 1 — Identify the event(s) + +Determine which event type(s) the reactor needs to observe. These events must already exist or be created as part of the slice. + +## Step 2 — Create the reactor class + +Place the reactor in the appropriate feature folder, co-located with the slice it belongs to. + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MyApp.Projects.Notifications; + +/// +/// Sends a notification when a project is registered. +/// +/// The notification service. +public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor +{ + /// + /// Reacts to events. + /// + /// The event. + /// The event context. + public async Task ProjectRegistered(Registration.ProjectRegistered @event, EventContext context) => + await notifications.Notify($"Project '{@event.Name}' was registered."); +} +``` + +## Step 3 — Follow the critical rules + +- **`IReactor` is a marker interface** — no methods to implement. Method dispatch is by first-parameter event type. +- **Method name** — can be anything descriptive. The name is for readability, not dispatch. +- **`EventContext`** — optional second parameter. Omit if event metadata is not needed. +- **Idempotent** — reactors may be called more than once for the same event. Design accordingly. +- **Use event data directly** — never query the read model back inside the reactor. +- **Return events instead of injecting IEventLog** — to produce new events, return them directly as `Task`, `Task`, or a collection thereof. For commands in other slices, inject `ICommandPipeline`. Avoid injecting `IEventLog` directly. + +## Step 3b — Return side-effect events (alternative to IEventLog) + +Return events directly from a handler method instead of calling `IEventLog.Append`. This keeps reactors free of direct event-store dependencies. + +```csharp +// Return a single event — uses EventSourceId from incoming event, appends to EventLog +public Task BookReserved(BookReserved @event, EventContext context) => + Task.FromResult(new StockDecreased(@event.Isbn, 1)); + +// Return multiple events +public Task> BookReserved(BookReserved @event, EventContext context) => + Task.FromResult>([new StockDecreased(@event.Isbn, 1), new StockLow(@event.Isbn)]); + +// Target a specific event source id with explicit metadata +// (EventSourceId, EventStreamType, EventStreamId, EventSourceType, Subject, Occurred, Causation) +public Task BookReserved(BookReserved @event, EventContext context) => + Task.FromResult(new EventForEventSourceId(@event.WarehouseId, new StockDecreased(@event.Isbn, 1)) + { + EventStreamType = new("warehouse"), // optional — others default sensibly + }); + +// Target multiple event source ids in one transaction +public Task> BookReserved(BookReserved @event, EventContext context) => + Task.FromResult>( + [ + new(@event.WarehouseId, new StockDecreased(@event.Isbn, 1)), + new(@event.Isbn, new StockLow(@event.Isbn)), + ]); +``` + +For **bare event** returns, the append-metadata is resolved from the reactor itself: `[EventStreamType]`, `[EventSourceType]`, `[EventStreamId]` attributes and the `ICanProvideEventSourceId`, `ICanProvideEventStreamId`, `ICanProvideSubject` interfaces. Return an `EventForEventSourceId` only when you need explicit per-event control or to target several event source ids at once. + +## Step 4 — Translation pattern (if applicable) + +If the reactor adapts events from one slice by triggering commands in another, it is a **Translation** reactor: + +```csharp +/// +/// Reacts to reservation events and decreases stock accordingly. +/// +/// The stock keeper service. +/// The command pipeline. +public class StockKeeping(IStockKeeper stockKeeper, ICommandPipeline commandPipeline) : IReactor +{ + /// + /// Handles a event. + /// + /// The event. + /// The event context. + public async Task BookReserved(BookReserved @event, EventContext context) => + await commandPipeline.Execute(new DecreaseStock(@event.Isbn, await stockKeeper.GetStock(@event.Isbn))); +} +``` + +## Step 5 — Filter by appended event metadata (optional) + +If the reactor should only observe a subset of appended events, decorate it with filter attributes that match the metadata used when appending: + +```csharp +using Cratis.Chronicle; +using Cratis.Chronicle.Events; + +[FilterEventsByTag("priority")] +[EventSourceType("order")] +[EventStreamType("fulfillment")] +public class PriorityFulfillmentReactor : IReactor +{ + public Task OrderPlaced(OrderPlaced @event, EventContext context) => Task.CompletedTask; +} +``` + +These attributes correlate directly to the append call: + +```csharp +await eventLog.Append( + EventSourceId.New(), + new OrderPlaced(42m), + eventStreamType: "fulfillment", + eventSourceType: "order", + tags: ["priority"]); +``` + +- `[FilterEventsByTag]` filters by event tags +- `[EventSourceType]` filters by the appended event source type +- `[EventStreamType]` filters by the appended event stream type +- `[Tag]` and `[Tags]` still label the reactor itself; they do not filter events + +For fuller examples, see `Documentation/events/filtering/`. + +## Step 6 — Validate + +Run `dotnet build`. Fix all errors before completing. + +## When to use a Reactor vs a Projection + +| Need | Use | +|------|-----| +| Populate a queryable read model from events | **Projection** (see `add-projection` skill) | +| Trigger side effects, send notifications, call external APIs | **Reactor** | +| Adapt events across slices by triggering commands | **Reactor** (Translation pattern) | +| Both populate a read model AND trigger side effects | **Both** — one projection + one reactor | diff --git a/plugins/public-cratis-ai/skills/add-reducer/SKILL.md b/plugins/public-cratis-ai/skills/add-reducer/SKILL.md new file mode 100644 index 0000000..a3d7efd --- /dev/null +++ b/plugins/public-cratis-ai/skills/add-reducer/SKILL.md @@ -0,0 +1,61 @@ +--- +name: add-reducer +description: Use this skill when a read model genuinely needs an IReducerFor — when model-bound projection attributes and fluent IProjectionFor cannot express the state transition. Reducers are the last-resort escape hatch, not a default. Covers the admission checklist, method signatures, nullable-current handling, passivity, and pitfalls. +--- + +# Adding a Reducer (`IReducerFor`) + +A reducer is the **last-resort** way to build a read model — "current state + event → next state" with real C# control flow. Reach for it only when the projection ladder can't express the shape. A reducer that *could* have been a projection is a projection that was never written, and will be flagged for conversion in review. + +## Admission checklist — all three must be true before writing a reducer + +1. **Model-bound attributes can't express it** — not `[FromEvent]`, `[SetFrom]`, `[SetFromContext]`, `[ChildrenFrom]`, `[RemovedWith]`, `[Nested]`, `[ClearWith]`, counters (`[Increment]`/`[Count]`), or `[FromAll]`. +2. **Fluent `IProjectionFor` can't express it** — not `UsingKey`/`UsingParentKey`/`UsingCompositeKey`/`UsingConstantKey`, `Join`, `.NotRewindable()`, or a supported conditional setter. +3. **The remainder needs real control flow** — branching on prior state, loops, or accumulated-state calculations the declarative APIs don't have. + +If all three hold, write the reducer and **carry an inline justification comment** on the class naming the specific limitation that ruled out a projection: + +```csharp +// Reducer required: next balance depends on prior balance with a per-event cap that +// no counter/projection attribute expresses. +public class AccountBalanceReducer : IReducerFor +{ + public AccountBalance Opened(DebitAccountOpened @event, AccountBalance? current, EventContext context) + => new(0m, context.Occurred); + + public AccountBalance Deposited(FundsDeposited @event, AccountBalance? current, EventContext context) + => (current ?? new(0m, context.Occurred)) with { Balance = (current?.Balance ?? 0m) + @event.Amount }; +} +``` + +## Method signatures + +Each public method's **first parameter** is the event type it handles (dispatch is by type, not name): + +| Signature | Meaning | +|---|---| +| `T On(TEvent @event, T? current)` | next state from current (null on first event) | +| `T On(TEvent @event, T current)` | non-nullable current when the model is always seeded first | +| `+ EventContext context` | add the context as a trailing parameter when you need `Occurred`/metadata | +| `Task On(...)` | async when the computation awaits | +| return `T?`/`null` | treat the event as a no-op for this model | + +- Return the **complete new state** — never mutate `current` in place (use `with { }`). +- `current` is `null` on the first event for a given event source; for a standing model seeded by one event then updated by many, return `null` when the seed state is absent so replay and `ReadModelScenario` ordering survive. +- **Never store mutable state on the reducer class** — it must be stateless. +- **Don't throw for a no-op event** — return `current` unchanged; throwing pauses the reducer's partition. + +## Passivity, filtering, and identity + +- `[Passive]` on the read model makes any reducer for it passive automatically (command-side reads only, not materialized); `[Reducer(isActive: false)]` is the per-reducer form. `[Reducer(id: ..., eventSequence: ...)]` overrides identity/source. +- Filter which events the reducer observes with `[FilterEventsByTag]`, `[EventSourceType]`, `[EventStreamType]` (these filter; `[Tag]`/`[Tags]` only label). + +## Specs + +Use `ReadModelScenario` (it auto-detects reducer vs projection). Drive a sequence of events through `Given.ForEventSource(id).Events(...)` and assert on `_scenario.Instance`. Cover: first-event creation, accumulation across multiple events, and any branch in the reducer. See [specs.scenarios.csharp.md](../../rules/specs.scenarios.csharp.md) and the `write-specs-readmodels` skill. + +## See also + +- [vertical-slices.md](../../rules/vertical-slices.md) — the projection→fluent→reducer ladder. +- `add-projection` — the model-bound and fluent options to exhaust first. +- `cratis-readmodel` — read model + query method basics. diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md b/plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md new file mode 100644 index 0000000..9ed5b35 --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/SKILL.md @@ -0,0 +1,162 @@ +--- +name: auth-and-identity +description: Use this skill for authentication, authorization, or identity in a Cratis Arc project — backend, frontend, or both. Covers identity providers (`IProvideIdentityDetails`), protecting commands/queries with authorization attributes, Microsoft Identity Platform, connecting backend identity to React, multi-tenant identity, and local-dev generated principals. Trigger on auth, login, roles, permissions, identity details, user context, or protecting endpoints. +--- + +# Auth & Identity in Cratis Arc + +This skill covers the full auth and identity stack in a Cratis Arc application. Read the relevant reference files below for detailed API usage. + +> **Read the relevant instruction files first.** This skill references concepts from the core copilot instructions in `.github/copilot-instructions.md`. If you need details on vertical slices, commands, queries, or proxy generation, consult those instructions. + +## Architecture Overview + +Identity and auth in Arc follow a cookie-first, convention-based pattern: + +``` +Frontend (React) Backend (ASP.NET Core) +───────────────── ────────────────────── + app.MapIdentityProvider() + └── useIdentity() hook └── GET /.cratis/me + │ │ + ├─ 1. Read .cratis-identity cookie │ + └─ 2. If no cookie → fetch /.cratis/me │ + ▼ + AuthenticationMiddleware + └── IAuthenticationHandler[] + │ sets HttpContext.User + ▼ + IIdentityProvider.Get() + └── IProvideIdentityDetails.Provide() + │ + ▼ + IdentityProviderResult + → JSON response + → .cratis-identity cookie (base64) + +Authorization: + [Authorize] / [Roles("Admin")] / [AllowAnonymous] + └── AuthorizationEvaluator checks per command/query +``` + +**Key design decisions:** +- The `.cratis-identity` cookie is `HttpOnly=false` so the frontend JavaScript can read it directly — no extra HTTP call needed on page load. +- Identity details are base64-encoded JSON in the cookie, automatically decoded by the frontend `IdentityProvider`. +- Only one `IProvideIdentityDetails` implementation is allowed per application (auto-discovered). If none exists, a default provider grants access to everyone. +- Cratis has its own `[Authorize]`, `[AllowAnonymous]`, and `[Roles]` attributes in `Cratis.Arc.Authorization` — these are distinct from ASP.NET Core's and are evaluated by `AuthorizationEvaluator` in the command and query pipeline. + +--- + +## Decision Tree — Which Reference to Read + +Use this decision tree to determine which reference file(s) to read based on the user's task: + +| User wants to... | Read this reference | +|---|---| +| Add identity details to their app | [references/backend-identity.md](references/backend-identity.md) | +| Customize `IProvideIdentityDetails` (enrich from DB, block users, multi-tenant, preferences) | [references/backend-identity.md](references/backend-identity.md) | +| Modify identity at runtime (stateless selections, `ModifyDetails`) | [references/backend-identity.md](references/backend-identity.md) | +| Use Azure AD / Entra ID / Microsoft Identity | [references/authentication.md](references/authentication.md) | +| Write a custom authentication handler (API key, JWT, etc.) | [references/authentication.md](references/authentication.md) | +| Protect commands or queries with roles | [references/authorization.md](references/authorization.md) | +| Set up `[Authorize]`, `[AllowAnonymous]`, or `[Roles]` | [references/authorization.md](references/authorization.md) | +| Consume identity in React | [references/frontend.md](references/frontend.md) | +| Use identity in MVVM or vanilla TypeScript | [references/frontend.md](references/frontend.md) | +| Test identity locally without Azure | [references/local-development.md](references/local-development.md) | +| Full-stack setup (backend + frontend) | Read all references in order | + +**For full-stack tasks, read in this order:** +1. `references/backend-identity.md` — identity provider and startup +2. `references/authentication.md` — how users are authenticated +3. `references/authorization.md` — protecting commands and queries +4. `references/frontend.md` — consuming identity in the UI +5. `references/local-development.md` — testing without real infrastructure + +--- + +## Quick-Start: Full-Stack Identity Setup + +This is the minimum checklist for an application with identity. Read the reference files for details on each step. + +### Backend + +1. **Details record**: Define a C# record for application-specific user information +2. **Identity provider**: Implement `IProvideIdentityDetails` (auto-discovered, one per app) +3. **Startup**: Call `app.MapIdentityProvider()` to register `GET /.cratis/me` +4. **Authentication**: Add `AddMicrosoftIdentityPlatformIdentityAuthentication()` or implement `IAuthenticationHandler` +5. **Authorization**: Add `[Authorize]` / `[Roles]` / `[AllowAnonymous]` attributes from `Cratis.Arc.Authorization` to commands and queries + +### Frontend + +6. **Provider**: Wrap app root with `` from `@cratis/arc.react/identity` +7. **Hook**: Use `useIdentity()` to access identity anywhere in the component tree +8. **Roles**: Use `identity.isInRole('Admin')` for UI-level role gating + +### Proxy Generation + +9. Using `IProvideIdentityDetails` (generic) enables automatic TypeScript type generation at `dotnet build` time — the generated types can be imported in the frontend for end-to-end type safety. + +--- + +## Critical Rules + +These rules are frequently violated — always enforce them: + +1. **One identity provider per app**: Only one `IProvideIdentityDetails` implementation is allowed. Multiple throws `MultipleIdentityDetailsProvidersFound`. +2. **Use Cratis attributes, not ASP.NET Core's**: `[Authorize]`, `[Roles]`, `[AllowAnonymous]` must come from `Cratis.Arc.Authorization`, not `Microsoft.AspNetCore.Authorization`. +3. **Never combine `[Authorize]` and `[AllowAnonymous]` on the same target**: This throws `AmbiguousAuthorizationLevel`. +4. **Prefer the generic interface**: Use `IProvideIdentityDetails` over `IProvideIdentityDetails` to enable proxy generation. +5. **Auto-discovery**: Both `IProvideIdentityDetails` and `IAuthenticationHandler` implementations are auto-discovered — no DI registration needed. +6. **Frontend role checks are UX, not security**: `isInRole()` on the frontend hides UI elements. The backend `[Roles]` attribute is the actual security boundary. +7. **Build before frontend**: TypeScript proxy types for identity details are generated by `dotnet build`. The backend must compile before the frontend can import them. +8. **Authorize at the boundary, secure by default**: express access with `[Authorize]`/`[Roles]` attributes on the command/query — **never** gate behavior with an `if (identity.IsInRole(...))` inside `Handle()`. For app-wide protection, configure a default-deny fallback policy so a target is protected unless it explicitly opts out with `[AllowAnonymous]`. Cross-cutting auth that spans many commands belongs in an `ICommandFilter`, not duplicated per handler. +9. **Command-specific scope is validation, not an attribute**: a rule like "may only act on resources in your own organization" belongs in the `CommandValidator` (inject the identity and reject with a validation error) — not in `Handle()`, and not expressible by a role attribute alone. +10. **Read the current user from the authenticated principal**: in backend code resolve the current user from `IHttpContextAccessor.User` (the ASP.NET principal the authentication handler populates). Do not invent a bespoke `IIdentityAccessor` abstraction — that is a product-specific wrapper, not part of generic Cratis. + +--- + +## Common Code Patterns + +### Protecting a command with roles + +```csharp +[Command] +[Roles("Admin")] +public record PromoteUser(UserId Id) +{ + public void Handle(IUserService users) => users.Promote(Id); +} +``` + +### Conditionally rendering UI based on roles + +```tsx +const identity = useIdentity(); + +return identity.isInRole('Admin') + ? + : ; +``` + +### Modifying identity at runtime (stateless selections) + +```csharp +public class SetDepartment(IIdentityProvider identityProvider) +{ + public async Task Handle(string department) => + await identityProvider.ModifyDetails( + details => details with { SelectedDepartment = department }); +} +``` + +--- + +## Reference Documentation + +### Skill references (detailed implementation guidance) + +- [Backend Identity Provider](references/backend-identity.md) — `IProvideIdentityDetails`, `IdentityProviderContext`, cookie mechanics, proxy generation, `ModifyDetails` +- [Authentication](references/authentication.md) — `IAuthenticationHandler`, `AuthenticationResult`, Microsoft Identity Platform, combining handlers +- [Authorization](references/authorization.md) — `[Authorize]`, `[Roles]`, `[AllowAnonymous]`, inheritance rules, fallback policies +- [Frontend Identity](references/frontend.md) — React `IdentityProvider`, `useIdentity()`, MVVM, core identity, role checking +- [Local Development](references/local-development.md) — Generating principals, ModHeader, cookie fallback, dev testing \ No newline at end of file diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md b/plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md new file mode 100644 index 0000000..aa9a4ef --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/references/authentication.md @@ -0,0 +1,129 @@ +# Authentication + +This reference covers implementing custom authentication handlers and integrating Microsoft Identity Platform. + +## Authentication Pipeline + +Arc's authentication system is built around `IAuthenticationHandler` in `Cratis.Arc.Authentication`. Handlers are called in sequence until one succeeds or fails: + +1. Each registered handler is called in order +2. If a handler returns `Succeeded` → request is authenticated, pipeline stops +3. If a handler returns `Failed` → request is rejected (401), pipeline stops +4. If a handler returns `Anonymous` → try the next handler +5. If all handlers return `Anonymous` → request is unauthenticated + +## `AuthenticationResult` Outcomes + +| Method | Meaning | When to Use | +|--------|---------|-------------| +| `AuthenticationResult.Anonymous` | Handler doesn't apply | No relevant credentials found — let other handlers try | +| `AuthenticationResult.Succeeded(principal)` | Authentication succeeded | Valid credentials → return a `ClaimsPrincipal` | +| `AuthenticationResult.Failed(reason)` | Authentication explicitly failed | Credentials present but invalid → return 401 | + +## Custom Authentication Handler + +Handlers are **auto-discovered** — implement `IAuthenticationHandler` and Arc registers it. No manual DI wiring needed. + +```csharp +using System.Security.Claims; +using Cratis.Arc.Authentication; +using Cratis.Arc.Http; + +public class ApiKeyAuthenticationHandler(IApiKeyValidator validator) : IAuthenticationHandler +{ + public async Task HandleAuthentication(IHttpRequestContext context) + { + if (!context.Headers.TryGetValue("X-API-Key", out var apiKey)) + return AuthenticationResult.Anonymous; + + if (!await validator.IsValid(apiKey)) + return AuthenticationResult.Failed(new AuthenticationFailureReason("Invalid API key")); + + var claims = new[] + { + new Claim(ClaimTypes.Name, "API Client"), + new Claim(ClaimTypes.AuthenticationMethod, "ApiKey") + }; + + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "ApiKey")); + return AuthenticationResult.Succeeded(principal); + } +} +``` + +### Common Patterns + +**Bearer Token:** +```csharp +public class BearerTokenAuthenticationHandler : IAuthenticationHandler +{ + public async Task HandleAuthentication(IHttpRequestContext context) + { + if (!context.Headers.TryGetValue("Authorization", out var header)) + return AuthenticationResult.Anonymous; + + if (!header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return AuthenticationResult.Anonymous; + + var token = header["Bearer ".Length..].Trim(); + // validate token, build ClaimsPrincipal... + return AuthenticationResult.Succeeded(principal); + } +} +``` + +**Custom Header:** +```csharp +public class CustomHeaderAuthenticationHandler : IAuthenticationHandler +{ + public Task HandleAuthentication(IHttpRequestContext context) + { + if (!context.Headers.TryGetValue("X-User-ID", out var userId)) + return Task.FromResult(AuthenticationResult.Anonymous); + + var claims = new[] { new Claim(ClaimTypes.NameIdentifier, userId) }; + var principal = new ClaimsPrincipal(new ClaimsIdentity(claims, "CustomHeader")); + return Task.FromResult(AuthenticationResult.Succeeded(principal)); + } +} +``` + +### Best Practices + +- **Return `Anonymous` when your handler doesn't apply** — never `Failed` just because your header is missing +- **Provide clear failure reasons** — `AuthenticationFailureReason("API key is expired")` +- **Dependency injection works** — handlers can take constructor dependencies +- **Handle exceptions gracefully** — catch validation errors and return `Failed` with a reason + +## Microsoft Identity Platform + +For Azure-hosted apps using Azure AD / Entra ID, Arc provides built-in support: + +```csharp +builder.Services.AddMicrosoftIdentityPlatformIdentityAuthentication(); +``` + +This registers an ASP.NET Core `AuthenticationHandler` that reads Azure-provided headers: + +| Header | Description | +|--------|-------------| +| `x-ms-client-principal` | Base64-encoded Microsoft Client Principal token | +| `x-ms-client-principal-id` | User's unique ID from Azure AD | +| `x-ms-client-principal-name` | User's display name | + +These headers are automatically set by Azure Container Apps, Web Apps, and Static Web Apps. You also need: + +```csharp +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapIdentityProvider(); +``` + +## Combining Multiple Handlers + +Multiple handlers coexist naturally because `Anonymous` means "I don't handle this request": + +1. **ApiKeyAuthenticationHandler** — if `X-API-Key` present, authenticates or rejects. If absent, returns `Anonymous`. +2. **MicrosoftIdentityPlatformHandler** — checks for `x-ms-client-principal`. If absent, returns `Anonymous`. +3. If all return `Anonymous` → request is unauthenticated. diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md b/plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md new file mode 100644 index 0000000..a866f06 --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/references/authorization.md @@ -0,0 +1,179 @@ +# Authorization + +This reference covers protecting commands and queries with authorization attributes. + +## Attributes + +Arc uses its own authorization attributes from `Cratis.Arc.Authorization` — these are **distinct from ASP.NET Core's** and are evaluated by `AuthorizationEvaluator` in the command/query pipeline. + +| Attribute | Purpose | +|-----------|---------| +| `[Authorize]` | Require authentication. Optionally specify `Roles` or `Policy`. | +| `[Roles("Admin", "Manager")]` | Convenience wrapper — user needs at least **one** of the listed roles. | +| `[AllowAnonymous]` | Bypass authorization. Useful with fallback policies. | + +## On Model-Bound Commands + +```csharp +[Command] +[Roles("Admin", "Editor")] +public record DeleteArticle(ArticleId Id) +{ + public void Handle(IArticleService articles) => articles.Delete(Id); +} +``` + +When authorization fails, `Handle()` is **never called**. Check `CommandResult.IsAuthorized`: + +```csharp +var result = await commandPipeline.Execute(new DeleteArticle(articleId)); +if (!result.IsAuthorized) +{ + // User lacked required role — command was not executed +} +``` + +## On Model-Bound Queries + +Authorization applies at both class and method level: + +```csharp +[ReadModel] +[Authorize] +public record DebitAccount(AccountId Id, AccountName Name, decimal Balance) +{ + [Roles("Admin")] + public static IEnumerable GetAllAccounts( + IMongoCollection collection) => + collection.Find(_ => true).ToList(); + + [Roles("Manager")] + public static IEnumerable GetHighValueAccounts( + IMongoCollection collection) => + collection.Find(a => a.Balance > 50000).ToList(); + + [AllowAnonymous] + public static int GetTotalCount(IMongoCollection collection) => + (int)collection.CountDocuments(_ => true); +} +``` + +## Inheritance Rules + +| Scenario | Result | +|----------|--------| +| `[Authorize]` on type | All methods require authentication | +| `[Roles]` on type | All methods require those roles | +| `[AllowAnonymous]` on type | All methods allow anonymous access | +| Method-level attribute present | **Overrides** type-level attribute | +| Both `[Authorize]` and `[AllowAnonymous]` on same target | **Error** — throws `AmbiguousAuthorizationLevel` | + +Method-level always takes precedence: +- Methods **without** authorization attributes inherit the class-level attribute +- Methods **with** `[Roles(...)]` override the class-level attribute +- Methods **with** `[AllowAnonymous]` completely bypass authorization + +## Fallback Policy (Secure by Default) + +Make all endpoints require authentication unless explicitly opted out: + +```csharp +builder.Services.AddAuthorizationBuilder() + .SetFallbackPolicy(new AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build()); +``` + +With this, use `[AllowAnonymous]` to make specific endpoints public: + +```csharp +[Command] +[AllowAnonymous] +public record GetPublicCatalog() +{ + public Catalog Handle(ICatalogService catalog) => catalog.GetPublic(); +} +``` + +### Default Policy vs Fallback Policy + +| Policy | Applied When | +|--------|-------------| +| **Default Policy** | `[Authorize]` is used without parameters | +| **Fallback Policy** | No authorization attribute is present at all | + +## Policy-Based Authorization + +For complex scenarios: + +```csharp +[Command] +[Authorize(Policy = "RequireElevatedAccess")] +public record PerformSensitiveOperation(string Data) +{ + public void Handle(ISensitiveService service) => service.Execute(Data); +} +``` + +Define the policy in startup: + +```csharp +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("RequireElevatedAccess", policy => + policy.RequireAssertion(context => + context.User.IsInRole("Admin") || + context.User.HasClaim("elevated", "true"))); +}); +``` + +## Custom Authorization Logic in Handlers + +For domain-specific authorization beyond attributes: + +```csharp +[Authorize] +public record UpdateOrder(OrderId Id, string Data) +{ + public async Task Handle( + IOrderRepository orders, CommandContext context) + { + var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value; + var order = await orders.GetById(Id); + + if (order.OwnerId != userId) + return CommandResult.Forbidden(context.CorrelationId, "Can only update own orders"); + + order.Update(Data); + await orders.Save(order); + return CommandResult.Success; + } +} +``` + +## Authorization Results + +| Scenario | HTTP Status | +|----------|-------------| +| Not authenticated | 401 Unauthorized | +| Authenticated but wrong role | 403 Forbidden | + +## Cross-cutting authorization — `ICommandFilter` + +When the same authorization rule must span many commands (e.g. every command under a namespace), don't repeat it per handler — implement an `ICommandFilter` (auto-discovered, runs before the handler): + +```csharp +public class AdminAreaFilter : ICommandFilter +{ + public Task OnExecution(CommandContext context) => + Task.FromResult(context.Type.Namespace?.Contains(".Admin.") == true && !IsAdmin(context) + ? CommandResult.Unauthorized(context.CorrelationId) + : CommandResult.Success(context.CorrelationId)); +} +``` + +Reserve attributes for per-command roles; use `ICommandFilter` for the cross-cutting rule. Command-specific *scope* rejection ("may only act on your own organization") still belongs in the `CommandValidator`. + +## ⚠️ Adding `[Roles]` breaks existing specs + +Adding `[Roles]` to an existing command **breaks all its `.Execute()` specs** — both happy-path and validation-failure. An unauthorized result has `IsSuccess == false`, so `ShouldNotBeSuccessful()` still passes but the result carries **no** validation errors, so `ShouldHaveValidationErrors()` silently flips to failing. Register the identity the authorization evaluator reads into the command scenario's `Services`, and assert auth failures with `ShouldNotBeAuthorized()` — never `ShouldNotBeSuccessful()` alone. diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md b/plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md new file mode 100644 index 0000000..dcd0476 --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/references/backend-identity.md @@ -0,0 +1,379 @@ +# Backend Identity Provider + +This reference covers implementing `IProvideIdentityDetails` to enrich user identity with application-specific details, and what you can use it for. + +## What It Is + +`IProvideIdentityDetails` is the single extension point where you transform raw authentication claims into rich, application-specific user information. The object you return becomes the `details` field in the frontend identity — available via `useIdentity().details` in React. + +Think of it as the bridge between "who the identity provider says this person is" (claims) and "what our application knows about this person" (department, permissions, preferences, tenant, avatar URL, etc.). + +## How It Works + +When a user hits `GET /.cratis/me`, Arc: + +1. Extracts claims from the authenticated `ClaimsPrincipal` (`sub` → `Id`, `Identity.Name` → `Name`, all claims → `Claims`) +2. Builds an `IdentityProviderContext` with user ID, name, and claims +3. Calls your `IProvideIdentityDetails.Provide()` method +4. Serializes the result as JSON and sets the `.cratis-identity` cookie (base64-encoded, `HttpOnly=false` so frontend JS can read it) +5. Returns the result as JSON in the HTTP response + +The `Details` object you return can be **any shape** — it gets serialized as JSON. The frontend deserializes it into the `TDetails` type parameter you specify. + +## Mapping the Endpoint + +In your application startup: + +```csharp +var app = builder.Build(); +app.UseAuthentication(); +app.UseAuthorization(); +app.MapIdentityProvider(); +``` + +This registers `GET /.cratis/me` — the well-known route the frontend calls to get identity information. + +## The Interface + +```csharp +// Base interface — single method +public interface IProvideIdentityDetails +{ + Task Provide(IdentityProviderContext context); +} + +// Generic variant — adds no methods, just captures TDetails for proxy generation +public interface IProvideIdentityDetails : IProvideIdentityDetails + where TDetails : class; +``` + +Always prefer `IProvideIdentityDetails` — the generic type parameter enables automatic TypeScript proxy generation for your details type. + +## Basic Implementation + +The simplest implementation extracts claims and returns a details record: + +```csharp +public record UserDetails(string Department, string Title, bool IsManager); + +public class IdentityDetailsProvider : IProvideIdentityDetails +{ + public Task Provide(IdentityProviderContext context) + { + var department = context.Claims + .FirstOrDefault(c => c.Key == "department").Value ?? "Unknown"; + + var details = new UserDetails(department, "Engineer", false); + return Task.FromResult(new IdentityDetails(true, details)); + } +} +``` + +### Rules + +- Only **one** `IProvideIdentityDetails` implementation is allowed per app. Multiple implementations throw `MultipleIdentityDetailsProvidersFound`. +- If none exists, `DefaultIdentityDetailsProvider` is used (grants access to everyone with empty details). +- Dependency injection works — your provider can take constructor dependencies. +- The provider is registered as **scoped** (per-request), so it can safely use scoped services. +- Use `IProvideIdentityDetails` (generic) to enable automatic TypeScript proxy generation for the details type. Always prefer the generic interface. + +## `IdentityProviderContext` + +This is the input to your `Provide()` method — everything Arc knows about the user from authentication: + +```csharp +public record IdentityProviderContext( + IdentityId Id, + IdentityName Name, + IEnumerable> Claims); +``` + +| Property | Type | Description | +|----------|------|-------------| +| `Id` | `IdentityId` | User's unique ID (extracted from the `sub` claim) | +| `Name` | `IdentityName` | Display name (from `ClaimsPrincipal.Identity.Name`) | +| `Claims` | `IEnumerable>` | All claims from the authentication token as key-value pairs | + +`IdentityId` and `IdentityName` are `ConceptAs` types with `.Empty` sentinels. + +### Working with Claims + +Claims contain everything the authentication handler extracted from the token. Common claim keys: + +| Claim Key | Description | Example Value | +|-----------|-------------|---------------| +| `http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier` | User ID | `abc123` | +| `name` | Display name | `Jane Doe` | +| `email` or `preferred_username` | Email | `jane@contoso.com` | +| `http://schemas.microsoft.com/ws/2008/06/identity/claims/role` | Role | `Admin` | +| Custom claims | Anything your IdP adds | `department`, `tenant_id`, etc. | + +```csharp +// Extract a specific claim +var email = context.Claims.FirstOrDefault(c => c.Key == "preferred_username").Value; + +// Extract all roles +var roles = context.Claims + .Where(c => c.Key == "http://schemas.microsoft.com/ws/2008/06/identity/claims/role") + .Select(c => c.Value); + +// Check if a claim exists +var hasDepartment = context.Claims.Any(c => c.Key == "department"); +``` + +## `IdentityDetails` + +The return type from your `Provide()` method: + +```csharp +public record IdentityDetails(bool IsUserAuthorized, object Details); +``` + +| Property | Type | Description | +|----------|------|-------------| +| `IsUserAuthorized` | `bool` | Whether the user is allowed into the application. `false` → HTTP 403. | +| `Details` | `object` | Application-specific details payload (serialized as JSON) | + +## What You Can Use It For + +### 1. Enriching Identity from a Database + +The most common use case — look up the user in your own database and add application-specific information: + +```csharp +public record UserDetails( + UserId UserId, + string Department, + string Title, + string AvatarUrl, + bool IsManager); + +public class IdentityDetailsProvider(IMongoCollection users) : IProvideIdentityDetails +{ + public async Task Provide(IdentityProviderContext context) + { + var user = await users.Find(u => u.ExternalId == context.Id.Value).FirstOrDefaultAsync(); + + if (user is null) + { + // Unknown user — still allow access, but with empty details + return new IdentityDetails(true, new UserDetails( + UserId.Empty, "Unknown", "Unknown", string.Empty, false)); + } + + var details = new UserDetails( + user.Id, + user.Department, + user.Title, + user.AvatarUrl, + user.IsManager); + + return new IdentityDetails(true, details); + } +} +``` + +### 2. Blocking Unauthorized Users (Application-Level Gate) + +Return `IsUserAuthorized = false` to completely block a user from the application. The endpoint returns HTTP 403 and the frontend never gets identity. This is different from role-based authorization — it's an application-level gate: + +```csharp +public class IdentityDetailsProvider(IMongoCollection allowedUsers) : IProvideIdentityDetails +{ + public async Task Provide(IdentityProviderContext context) + { + var isAllowed = await allowedUsers + .Find(u => u.ExternalId == context.Id.Value) + .AnyAsync(); + + if (!isAllowed) + { + // User is authenticated but not authorized for this application + return new IdentityDetails(false, new UserDetails()); + } + + // ... build details normally + return new IdentityDetails(true, details); + } +} +``` + +Use cases: +- Invite-only applications (only registered users can access) +- Disabled/suspended user accounts +- Tenant-specific access control + +### 3. Multi-Tenant Identity + +Add tenant information to identity so the frontend knows which tenant context the user is in: + +```csharp +public record TenantUserDetails( + TenantId TenantId, + TenantName TenantName, + string Role, + IEnumerable AvailableTenants); + +public class IdentityDetailsProvider(ITenantService tenants) : IProvideIdentityDetails +{ + public async Task Provide(IdentityProviderContext context) + { + var userTenants = await tenants.GetTenantsForUser(context.Id.Value); + var activeTenant = userTenants.FirstOrDefault(); + + if (activeTenant is null) + { + return new IdentityDetails(false, new TenantUserDetails( + TenantId.Empty, TenantName.Empty, string.Empty, [])); + } + + var details = new TenantUserDetails( + activeTenant.Id, + activeTenant.Name, + activeTenant.UserRole, + userTenants.Select(t => t.Id)); + + return new IdentityDetails(true, details); + } +} +``` + +### 4. Enriching From External APIs + +Call external services during identity resolution: + +```csharp +public class IdentityDetailsProvider( + IMongoCollection users, + IGraphApiClient graphClient) : IProvideIdentityDetails +{ + public async Task Provide(IdentityProviderContext context) + { + // Combine data from your database and Microsoft Graph + var user = await users.Find(u => u.ExternalId == context.Id.Value).FirstOrDefaultAsync(); + var graphProfile = await graphClient.GetUserProfile(context.Id.Value); + + var details = new UserDetails( + user?.Department ?? graphProfile.Department, + graphProfile.JobTitle, + graphProfile.Photo); + + return new IdentityDetails(true, details); + } +} +``` + +### 5. Claims-Only Provider (No Database) + +Sometimes all you need is to reshape token claims into a application-friendly structure — no database lookup required: + +```csharp +public record UserDetails(string Email, string Department, IEnumerable Groups); + +public class IdentityDetailsProvider : IProvideIdentityDetails +{ + public Task Provide(IdentityProviderContext context) + { + var email = context.Claims.FirstOrDefault(c => c.Key == "preferred_username").Value ?? string.Empty; + var department = context.Claims.FirstOrDefault(c => c.Key == "department").Value ?? "Unknown"; + var groups = context.Claims + .Where(c => c.Key == "groups") + .Select(c => c.Value); + + return Task.FromResult(new IdentityDetails( + true, + new UserDetails(email, department, groups))); + } +} +``` + +### 6. User Preferences and Settings + +Surface user preferences (theme, language, selected workspace) as identity details so the frontend can use them immediately on load: + +```csharp +public record UserDetails( + string Theme, + string Language, + WorkspaceId ActiveWorkspace); + +public class IdentityDetailsProvider(IMongoCollection preferences) : IProvideIdentityDetails +{ + public async Task Provide(IdentityProviderContext context) + { + var prefs = await preferences + .Find(p => p.UserId == context.Id.Value) + .FirstOrDefaultAsync(); + + var details = new UserDetails( + prefs?.Theme ?? "light", + prefs?.Language ?? "en", + prefs?.ActiveWorkspace ?? WorkspaceId.Empty); + + return new IdentityDetails(true, details); + } +} +``` + +## Modifying Identity at Runtime + +`IIdentityProvider` allows modifying identity details during requests — useful for stateless applications tracking user selections without a database round-trip: + +```csharp +public class SetActiveWorkspace(IIdentityProvider identityProvider) +{ + public async Task Handle(WorkspaceId workspaceId) => + await identityProvider.ModifyDetails( + details => details with { ActiveWorkspace = workspaceId }); +} +``` + +This re-generates the identity from the current context, applies the modifier function to the details, and re-writes the cookie. The frontend will see the updated details on the next `identity.refresh()` call or page load. + +| Method | Description | +|--------|-------------| +| `Get()` / `Get()` | Build the `IdentityProviderResult` from the current HTTP context | +| `SetCookieForHttpResponse(result)` | Write the result to the response as the `.cratis-identity` cookie | +| `ModifyDetails(modifier)` | Read current details, apply modifier, write updated cookie | + +### When to Use `ModifyDetails` vs. Database Updates + +| Approach | Use When | +|----------|----------| +| `ModifyDetails()` | Ephemeral selections (active tenant, selected workspace, UI preferences) that don't need persistence. Updates the cookie in-place. | +| Database update + `identity.refresh()` | Persistent changes (profile updates, role changes). Update the database, then have the frontend call `refresh()` to pick up the new data. | + +## Proxy Generation for Identity Types + +When using the generic `IProvideIdentityDetails`, the proxy generator automatically creates TypeScript types for `TDetails` at build time: + +1. Define a C# record for your details type +2. Implement `IProvideIdentityDetails` +3. Run `dotnet build` — TypeScript interface is generated +4. Import and use the generated type in your frontend + +| Feature | `IProvideIdentityDetails` | `IProvideIdentityDetails` | +|---------|---------------------------|-------------------------------------| +| Runtime behavior | Identical | Identical | +| Proxy generation | No type generated | TypeScript type generated | +| Frontend type safety | Manual typing required | Automatic type safety | + +## Default Behavior (No Provider) + +If you don't implement `IProvideIdentityDetails`, Arc uses `DefaultIdentityDetailsProvider`: + +```csharp +public class DefaultIdentityDetailsProvider : IProvideIdentityDetails +{ + public Task Provide(IdentityProviderContext context) => + Task.FromResult(new IdentityDetails(true, new { })); +} +``` + +This grants access to everyone with empty details. It's useful for apps that only need authentication (roles) without custom details. + +## Multi-Service Considerations + +- **Single service**: Implement `IProvideIdentityDetails` directly +- **Multiple services**: Have your ingress/reverse proxy call each service's `/.cratis/me` and merge results into a single JSON structure for the `.cratis-identity` cookie +- **Dedicated identity service**: Aggregate identity info from various sources in one service diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md b/plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md new file mode 100644 index 0000000..733eb71 --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/references/frontend.md @@ -0,0 +1,184 @@ +# Frontend Identity + +This reference covers consuming identity in React, MVVM, and vanilla TypeScript. + +## How the Frontend Gets Identity + +The frontend uses a **cookie-first** approach: + +1. Check for the `.cratis-identity` cookie (base64-encoded JSON, `HttpOnly=false`) +2. If cookie exists → decode and use it (no HTTP call needed) +3. If no cookie → call `GET /.cratis/me` to fetch identity and set the cookie +4. In development mode, the cookie fallback (`/.cratis/me` call) always works — no ingress simulation needed + +## React + +### IdentityProvider Context + +Wrap your app root with ``: + +```tsx +import { IdentityProvider } from '@cratis/arc.react/identity'; + +export const App = () => ( + + {/* your app */} + +); +``` + +For type-safe details with complex types (e.g., `Guid`), pass a `detailsType` constructor: + +```tsx +import { IdentityProvider } from '@cratis/arc.react/identity'; +import { Guid } from '@cratis/fundamentals'; + +class UserIdentityDetails { + userId: Guid = Guid.empty; + firstName: string = ''; + lastName: string = ''; +} + +export const App = () => ( + + {/* your app */} + +); +``` + +### `useIdentity()` Hook + +Access identity anywhere in your component tree: + +```tsx +import { useIdentity } from '@cratis/arc.react/identity'; + +type UserDetails = { + department: string; + title: string; +}; + +export const UserProfile = () => { + const identity = useIdentity(); + + return ( +
+

{identity.name}

+

Department: {identity.details.department}

+
+ ); +}; +``` + +**With default values** (useful for local development when the cookie might not exist): + +```tsx +const identity = useIdentity({ + department: '[N/A]', + title: '[N/A]' +}); +``` + +**With a constructor for type-safe deserialization** (uses `JsonSerializer.deserializeFromInstance()` under the hood): + +```tsx +const identity = useIdentity(UserIdentityDetails); + +// With default values: +const identity = useIdentity(UserIdentityDetails, { + userId: Guid.empty, + firstName: '[N/A]', + lastName: '[N/A]' +}); +``` + +### Role Checking + +```tsx +const identity = useIdentity(); + +if (identity.isInRole('Admin')) { + // show admin UI +} + +// Or access roles directly +console.log(identity.roles); +``` + +### Refreshing Identity + +When identity changes on the backend (e.g., user granted new roles): + +```tsx +const identity = useIdentity(); +const handleRefresh = () => identity.refresh(); +``` + +This calls `GET /.cratis/me` again and updates both the cookie and context. + +### `IIdentity` Shape + +| Property | Type | Description | +|----------|------|-------------| +| `id` | `string` | Unique ID from identity provider | +| `name` | `string` | Display name | +| `roles` | `string[]` | Assigned roles | +| `details` | `TDetails` | Application-specific details | +| `isSet` | `boolean` | Whether identity has been loaded | +| `isInRole(role)` | `(string) => boolean` | Check role membership | +| `refresh()` | `() => Promise` | Re-fetch identity from backend | + +## MVVM (tsyringe) + +In an MVVM setup, `IIdentityProvider` is automatically registered in the DI container by `Bindings.initialize()`: + +```typescript +import { injectable } from 'tsyringe'; +import { IIdentityProvider } from '@cratis/arc/identity'; + +type UserDetails = { + department: string; +}; + +@injectable() +export class MyViewModel { + constructor(private readonly _identityProvider: IIdentityProvider) {} + + async loadUser() { + const identity = await this._identityProvider.getCurrent(); + console.log(identity.details.department); + } +} +``` + +Requires the [MVVM Context](Documentation/frontend/react.mvvm/mvvm-context.md) to be set up. + +## Vanilla TypeScript (No Framework) + +Use `IdentityProvider` directly: + +```typescript +import { IdentityProvider } from '@cratis/arc/identity'; + +const identity = await IdentityProvider.getCurrent(); +console.log(identity.name); +console.log(identity.isInRole('Admin')); +``` + +## Frontend Role-Based UI Pattern + +Combine `useIdentity()` with authorization attributes on the backend for defense in depth: + +```tsx +const identity = useIdentity(); + +return ( +
+ {identity.isInRole('Admin') && } + {identity.isInRole('Manager') && } + +
+); +``` + +The frontend check is for UX (hiding buttons the user can't use). The backend `[Roles]` attribute is the actual security boundary. diff --git a/plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md b/plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md new file mode 100644 index 0000000..6fed70a --- /dev/null +++ b/plugins/public-cratis-ai/skills/auth-and-identity/references/local-development.md @@ -0,0 +1,111 @@ +# Local Development & Testing + +This reference covers simulating authentication and identity in local development environments without real Azure or identity infrastructure. + +## How Identity Works in Development + +When running locally: + +1. No Azure App Service or ingress injects identity headers +2. The `.cratis-identity` cookie will not be set automatically +3. The frontend cookie reader falls back to calling `GET /.cratis/me` +4. The backend returns a default anonymous identity with empty details + +## Generating a Microsoft Client Principal + +Azure App Service Easy Auth injects identity via the `X-MS-CLIENT-PRINCIPAL` header. You can simulate this locally with a browser extension. + +### Step 1: Build the Principal JSON + +```json +{ + "auth_typ": "aad", + "claims": [ + { "typ": "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", "val": "user-unique-id" }, + { "typ": "name", "val": "Jane Developer" }, + { "typ": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "val": "Admin" }, + { "typ": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "val": "Manager" } + ], + "name_typ": "name", + "role_typ": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" +} +``` + +### Step 2: Base64-Encode It + +**macOS / Linux terminal:** + +```bash +echo -n '{"auth_typ":"aad","claims":[{"typ":"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier","val":"user-unique-id"},{"typ":"name","val":"Jane Developer"},{"typ":"http://schemas.microsoft.com/ws/2008/06/identity/claims/role","val":"Admin"}],"name_typ":"name","role_typ":"http://schemas.microsoft.com/ws/2008/06/identity/claims/role"}' | base64 +``` + +**Browser console:** + +```javascript +btoa(JSON.stringify({ + auth_typ: "aad", + claims: [ + { typ: "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier", val: "user-unique-id" }, + { typ: "name", val: "Jane Developer" }, + { typ: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", val: "Admin" } + ], + name_typ: "name", + role_typ: "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" +})); +``` + +### Step 3: Inject the Header + +Use the [ModHeader](https://modheader.com/) browser extension: + +1. Install ModHeader for Chrome/Edge/Firefox +2. Add a request header: + - **Name**: `X-MS-CLIENT-PRINCIPAL` + - **Value**: the base64 string from Step 2 +3. Reload the page — the backend will authenticate the user as if Azure App Service injected the header + +## Custom Identity Details in Development + +If your `IProvideIdentityDetails` implementation enriches identity from a database, that still runs in development. The principal provides the initial `id`, `name`, and `roles`; your enrichment adds extra fields. + +To test without a database, create a dev-only fallback in your identity provider: + +```csharp +public Task Provide(IdentityProviderContext context) +{ + // In development, if the user ID is unknown, return sensible defaults + var details = await _readModelStore.GetOrDefault(context.Id, new UserDetails + { + Department = "Engineering", + Title = "Developer" + }); + + return details; +} +``` + +## Testing Without ModHeader + +If you prefer not to install a browser extension, you can also set the cookie directly: + +1. Build the identity JSON matching the `.cratis-identity` cookie format +2. Set it via browser console: + +```javascript +document.cookie = '.cratis-identity=' + btoa(JSON.stringify({ + id: 'user-id', + name: 'Jane Developer', + roles: ['Admin'], + details: { department: 'Engineering' } +})) + '; path=/'; +``` + +3. Reload — the frontend will use this cookie directly without calling the backend + +## Important Notes + +- The cookie is `HttpOnly=false` by design — the frontend JavaScript must read it +- The cookie path is `/` +- In production, the cookie is set by the backend middleware; in development, you can set it manually +- The `GET /.cratis/me` endpoint always works — it is the fallback for all environments +- ModHeader header injection simulates the full pipeline (authentication → identity provider → cookie), while direct cookie setting bypasses the backend entirely diff --git a/plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md b/plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md new file mode 100644 index 0000000..7e382f3 --- /dev/null +++ b/plugins/public-cratis-ai/skills/call-command-from-code/SKILL.md @@ -0,0 +1,81 @@ +--- +name: call-command-from-code +description: Execute a Cratis Arc command from backend code via ICommandPipeline — the scopeless and scoped overloads, typed Execute, pre-flight Validate, and reading CommandResult. Use when a reactor, background job, scheduled task, integration test, or backend service needs to run a command without going through HTTP. +--- + +# Call a Command from Code + +Backend code can drive a command through `ICommandPipeline` instead of an HTTP request. The pipeline runs the same authorization → validation → `Provide()` → `Handle()` chain as the HTTP boundary — only the entry point changes. Common callers: a reactor spawning a follow-up command, a background/scheduled job, an integration test harness. + +> Inside `Handle()` wanting to append to a *different stream*, do **not** inject `IEventLog` — return `IEnumerable` with `EventForEventSourceId(targetId, @event)` wrappers (see `vertical-slices.md`). `ICommandPipeline` is for **command-shaped** invocations that need validation/authorization. + +## Steps + +### 1. Inject and execute (scopeless overload) + +```csharp +public class Handler(ICommandPipeline pipeline) : IReactor +{ + [OnceOnly] + public Task Created(Created @event, EventContext context) => + pipeline.Execute(new (@event.SourceId, @event.Name)); +} +``` + +`Execute(command)` is the scopeless overload — the pipeline manages its own DI scope per call. This is the normal case. + +### 2. Scoped overload — only when needed + +`Execute(command, serviceProvider)` reuses the caller's ambient DI scope. Use it only when the handler genuinely depends on resolving something from that scope (e.g. a scoped DB session). Most callers don't need it. + +### 3. Typed result + +```csharp +var result = await pipeline.Execute(new Register(...)); +if (result.IsSuccess) { var token = result.Response; } +``` + +### 4. Pre-flight `Validate` + +`await pipeline.Validate(command)` runs authorization + validation **without** executing `Provide()`/`Handle()` — useful to confirm validity before committing to side effects. Scoped overload available. + +**Validation severity filtering:** both `Execute` and `Validate` take an optional `allowedSeverity` of type `ValidationResultSeverity?` (from `Cratis.Arc.Validation`) — `pipeline.Execute(command, allowedSeverity: ValidationResultSeverity.Warning)`. The levels are `Unknown=0`, `Information=1`, `Warning=2`, `Error=3`; by default only `Error` blocks, and failures at or below `allowedSeverity` are filtered out (so passing `Warning` lets warnings through but still blocks errors). + +### 5. Inspect `CommandResult` by the granular flag + +`IsSuccess` is the AND of the others — check the specific one your caller cares about: + +| Flag | Meaning | +|---|---| +| `IsAuthorized` | identity satisfied the authorization attributes | +| `IsValid` | validators passed (`ValidationResults` carries failures) | +| `HasExceptions` | `Provide()`/`Handle()` threw (`ExceptionMessages`) | +| `CorrelationId` | pipeline-wide id for log/trace correlation | + +### 6. Don't throw for expected failures + +Validation and authorization failures are normal flow — surface them through `CommandResult`, never as exceptions. Reserve exceptions for genuine `Provide()`/handler crashes. + +### 7. Reactors: pair with `[OnceOnly]` + +A reactor calling `Execute` is not idempotent on replay — every replay re-fires the command. Always combine with `[OnceOnly]`. + +## Common pitfalls + +| Pitfall | Why | +|---|---| +| Throwing on `!result.IsValid` instead of surfacing `ValidationResults` | loses structured failure info | +| `Execute` from a reactor without `[OnceOnly]` | replay re-fires the command | +| Defaulting to the scoped overload | most callers don't need it | +| Asserting only `IsSuccess` | can't tell auth vs validation vs exception apart | + +## Quality gate + +- [ ] Build is clean. +- [ ] A reactor that calls `Execute` is decorated with `[OnceOnly]`. +- [ ] The caller checks the granular `CommandResult` flags before treating the result as success. + +## See also + +- `vertical-slices.md` — reactors, `Handle()` return shapes, cross-stream events. +- `add-business-rule` / `auth-and-identity` — the validation/authorization the pipeline runs. diff --git a/plugins/public-cratis-ai/skills/cratis-command/SKILL.md b/plugins/public-cratis-ai/skills/cratis-command/SKILL.md new file mode 100644 index 0000000..19b112a --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-command/SKILL.md @@ -0,0 +1,310 @@ +--- +name: cratis-command +description: Step-by-step guidance for creating a Cratis Arc command — [Command] record, Handle() method, CommandValidator, proxy generation, and React .use() hook with CommandDialog. Use when adding or creating a command, wiring up a form or button to the backend, working with IEventLog, CommandResult, CommandValidator, CommandDialog, or [Command] attribute. +--- + +# Creating a Cratis Command + +A command represents a user action that changes state. In Cratis Arc the path is: + +``` +[Command] record + Handle() → validator → dotnet build → TypeScript proxy → React .use() +``` + +The command record **owns its own handler** — no separate controller class required. Follow the steps in order. Jump to the reference files for deeper detail on any step. + +--- + +## Step 1 — Define the C# command record + +A command is a **record decorated with `[Command]`** that contains its own `Handle()` method. No separate controller is needed. + +```csharp +// Accounts/OpenDebitAccount/OpenDebitAccount.cs — the slice file +namespace MyApp.Accounts.OpenDebitAccount; + +using Cratis.Arc.Commands.ModelBound; +using Cratis.Chronicle.Events; + +[Command] +public record OpenDebitAccount(AccountId AccountId, AccountName Name, OwnerId OwnerId) +{ + public DebitAccountOpened Handle() => + new(Name, OwnerId); // Arc appends the returned event; AccountId is the event source +} + +/// Emitted when a debit account is opened. +[EventType] // NO arguments — never [EventType("some-guid")] +public record DebitAccountOpened(AccountName Name, OwnerId OwnerId); +``` + +**Rules:** +- `[Command]` attribute is **required** — it makes the type discoverable and the analyzer will warn without it +- `Handle()` returns the event (or events) to append — Arc's Chronicle integration automatically appends them; **never inject `IEventLog` to append the primary event** +- `[EventType]` takes **no arguments** — the identifier is generated from the type name +- Name the command as an imperative action — `OpenDebitAccount`, not `OpenDebitAccountCommand` +- All backend artifacts for the slice live in this one file; place it in the slice folder, not an `API/` or `Commands/` folder (see [vertical-slices.md](../../rules/vertical-slices.md)) +- Use concept wrappers for every domain value — **identity** concepts derive from `EventSourceId`, **value** concepts from `ConceptAs`; never raw `Guid`/`string` + +```csharp +// Accounts/AccountId.cs — identity concept derives from EventSourceId +public record AccountId(Guid Value) : EventSourceId(Value) +{ + public static AccountId New() => new(Guid.NewGuid()); + public static implicit operator AccountId(Guid value) => new(value); +} +``` + +### Generating a new ID and returning it + +```csharp +[Command] +public record RegisterEmployee(string FirstName, string LastName, string Department) +{ + // Return (eventSourceId, event) — Arc uses the first element as the event source ID + // and sends it back to the client as CommandResult.response + public (EmployeeId, EmployeeRegistered) Handle() + { + var employeeId = new EmployeeId(Guid.NewGuid()); + return (employeeId, new(FirstName, LastName, Department)); + } +} +``` + +### Appending multiple events + +Return `IEnumerable`. Events never carry the event-source id — for events that belong to **different** streams, wrap each in `EventForEventSourceId(id, @event)`: + +```csharp +[Command] +public record TransferFunds(AccountId FromId, AccountId ToId, Money Amount) +{ + public IEnumerable Handle() => + [ + new EventForEventSourceId(FromId, new FundsWithdrawn(Amount)), + new EventForEventSourceId(ToId, new FundsDeposited(Amount)), + ]; +} +``` + +For multiple events on the **same** event source, return the bare event records. + +--- + +## Step 2 — Fetch data the handler needs with `Provide()` + +When `Handle()` needs fetched or computed data before it can build the event, add a `Provide()` method. It runs after authorization and validation, resolves its parameters from DI, and binds its return value to `Handle(...)` parameters by type. It may short-circuit with a `ValidationResult.Error(...)` when the data is missing or unusable — **do not throw for that**. + +```csharp +[Command] +public record OpenDebitAccount(AccountId AccountId, AccountName Name, OwnerId OwnerId) +{ + public async Task> Provide(IReadModels readModels) + { + var owner = await readModels.GetInstanceById((EventSourceId)OwnerId); + return owner is null ? ValidationResult.Error("Owner must exist.") : owner; + } + + public DebitAccountOpened Handle(Owner owner) => new(Name, owner.Id); +} +``` + +- Keep IO in `Provide()` and event construction in `Handle()`. +- For **uniqueness**, use `[Unique]` / `IConstraint` (race-safe) — not a read-model pre-check or a throwing service. See the `add-business-rule` skill. +- For a concurrency-sensitive state rule, inject the read model into `Handle()` and return `Result` (see `add-business-rule`). + +Arc automatically wraps the return in `CommandResult` / `CommandResult`. See `references/command-result.md`. + +--- + +## Step 3 — Add validation (optional but recommended) + +FluentValidation rules run **on the server** as part of the command pipeline; the proxy generator also extracts them so the same rules run **client-side as pre-flight** validation in `CommandForm`. Put the validator beside the command in the slice file: + +```csharp +public class OpenDebitAccountValidator : CommandValidator +{ + public OpenDebitAccountValidator() + { + RuleFor(c => c.Name) + .NotEmpty().WithMessage("Account name is required") + .MaximumLength(100); + RuleFor(c => c.OwnerId) + .NotEmpty().WithMessage("Owner is required"); + } +} +``` + +- Extends `CommandValidator` (not `AbstractValidator`) — this makes it discoverable automatically +- No registration needed; **omit the validator entirely when there are no rules** +- Arc also creates a `/validate` endpoint automatically; the frontend `command.validate()` calls it without executing the handler +- Single-property intrinsic rules (format, range, required) belong on a `ConceptValidator` for the value concept, so they travel with the value everywhere + +--- + +## Step 4 — Generate the TypeScript proxy + +```bash +dotnet build +``` + +The `Cratis.Arc.ProxyGenerator.Build` MSBuild package runs during the build and writes TypeScript files to the path configured in your `.csproj`: + +```xml + + $(MSBuildThisFileDirectory)../Web/src/api + +``` + +This produces `Web/src/api/Accounts/OpenDebitAccount.ts`. For first-time setup see `references/proxy-setup.md`. + +--- + +## Step 5 — Use the command in React + +### Inline form (full control) + +```tsx +import { OpenDebitAccount } from '../api/Accounts/OpenDebitAccount'; + +export const OpenAccountForm = () => { + const [command, setValues] = OpenDebitAccount.use(); + const [error, setError] = useState(''); + + const handleSubmit = async () => { + const result = await command.execute(); + if (result.isSuccess) { + onSuccess(result.response); // result.response is Guid if you returned one + } else if (!result.isValid) { + setError(result.validationResults[0]?.message ?? 'Validation failed'); + } + }; + + return ( +
+ (command.name = e.target.value)} + placeholder="Account name" + /> + {error &&

{error}

} + +
+ ); +}; +``` + +**Key properties on the command instance:** + +| Property / method | What it does | +| ----------------- | ------------ | +| `command.propName` | Get/set the property value | +| `command.hasChanges` | `true` when any value differs from the initial | +| `command.execute()` | Send the POST, returns `CommandResult` | +| `command.validate()` | Call the validate endpoint (no side effects) | +| `setValues(obj)` | Set multiple properties at once (e.g. from a query result) | + +### With initial values (edit scenario) + +```tsx +const [command] = UpdateAccount.use({ + accountId: account.id, + name: account.name, +}); +``` + +### Using `CommandDialog` (quickest path for modal forms) + +See Step 6 and `references/command-dialog.md`. + +--- + +## Step 6 — Wrap in a CommandDialog (optional) + +For modal dialogs, create a dialog component using `DialogProps` and wire it up with `useDialog`: + +```tsx +import { DialogProps } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm'; +import { OpenDebitAccount } from '../api/Accounts/OpenDebitAccount'; + +// --- Dialog component --- +export const OpenAccountDialog = ({ closeDialog }: DialogProps) => { + return ( + + command={OpenDebitAccount} + title="Open account" + okLabel="Open" + > + value={c => c.name} label="Name" /> + + ); +}; + +// --- Parent component --- +import { useDialog, DialogResult } from '@cratis/arc.react/dialogs'; +import { OpenAccountDialog } from './OpenAccountDialog'; + +export const AccountsPage = () => { + const [OpenAccountDialogWrapper, showOpenAccount] = useDialog(OpenAccountDialog); + + const handleOpen = async () => { + const [result] = await showOpenAccount(); + if (result === DialogResult.Ok) { + // command already executed inside the dialog — refresh your data here + } + }; + + return ( + <> + + + + ); +}; +``` + +**How it works:** +- `useDialog(OpenAccountDialog)` returns a wrapper component and a `show` function +- `showOpenAccount()` opens the dialog; it returns a Promise that resolves when the dialog closes +- `CommandDialog` executes the command when the user confirms; it closes automatically +- `DialogProps` provides `closeDialog` — needed when you want to pass a response back or handle cancel explicitly + +**Edit dialog (pre-populate with existing values):** + +```tsx +interface EditAccountDialogProps extends DialogProps { + accountId: string; + name: string; +} + +export const EditAccountDialog = ({ closeDialog, accountId, name }: EditAccountDialogProps) => { + return ( + + command={UpdateAccount} + title="Edit account" + initialValues={{ accountId }} + currentValues={{ name }} + > + value={c => c.name} label="Name" /> + + ); +}; +``` + +- `initialValues` — sets the change-tracking baseline (e.g. IDs that must be present for the command but are not user-entered) +- `currentValues` — pre-populates the visible field values + +`CommandDialog` calls `onConfirm` only after a successful `command.execute()`, so you don't need to check `isSuccess` yourself. See `references/command-dialog.md` for the full props list. + +--- + +## Reference files + +| File | What's in it | +| ---- | ------------ | +| `references/command-result.md` | Full `CommandResult` shape, error handling patterns | +| `references/command-dialog.md` | `CommandDialog` props, CommandForm fields, edit dialogs | +| `references/validation.md` | FluentValidation, Data Annotations, client-side pre-flight | +| `references/proxy-setup.md` | First-time proxy generator setup | diff --git a/plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md b/plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md new file mode 100644 index 0000000..7239d92 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-command/references/command-dialog.md @@ -0,0 +1,138 @@ +# CommandDialog — Reference + +`CommandDialog` from `@cratis/components/CommandDialog` is a modal dialog that executes a command when the user confirms. + +## How it works + +- It creates a command instance internally from the `command` constructor you pass +- It renders your `CommandForm` fields as children, bound to that instance +- When the user clicks OK, it calls `command.execute()` +- `onConfirm` is called **only if** execution succeeds +- `onCancel` / dismiss closes without executing + +--- + +## Dialog pattern with `useDialog` and `DialogProps` + +```tsx +import { DialogProps, DialogResult, useDialog } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField, NumberField } from '@cratis/components/CommandForm'; +import { CreateProduct } from '../api/Products/CreateProduct'; + +// 1. Define the dialog component +export const CreateProductDialog = ({ closeDialog }: DialogProps) => { + return ( + + command={CreateProduct} + title="Create product" + okLabel="Create" + > + value={c => c.name} title="Name" /> + value={c => c.price} title="Price" /> + + ); +}; + +// 2. Use it from the parent +const [CreateProductDialogWrapper, showCreateProduct] = useDialog(CreateProductDialog); + +const handleCreate = async () => { + const [result] = await showCreateProduct(); + if (result === DialogResult.Ok) { + refreshProducts(); + } +}; + +// 3. Render the wrapper +return ( + <> + + + +); +``` + +`DialogProps` provides `closeDialog` as a prop to the dialog component. `CommandDialog` automatically executes the command on confirm and closes the dialog. + +--- + +## Edit dialog (pre-populate with existing values) + +```tsx +interface EditProductDialogProps extends DialogProps { + product: Product; +} + +export const EditProductDialog = ({ closeDialog, product }: EditProductDialogProps) => { + return ( + + command={UpdateProduct} + title="Edit product" + currentValues={{ name: product.name, price: product.price }} + initialValues={{ productId: product.id }} + > + value={c => c.name} title="Name" /> + value={c => c.price} title="Price" /> + + ); +}; + +// Pass product to the dialog +const [EditDialogWrapper, showEdit] = useDialog(EditProductDialog); +await showEdit({ product: selectedProduct }); +``` + +- `initialValues` sets the baseline for change tracking (values that are not "changes") +- `currentValues` populates the initial field display + +--- + +## CommandForm field components + +All fields come from `@cratis/components/CommandForm`. Always pass the command type as the generic parameter so the `value` accessor is fully typed. + +```tsx +import { + InputTextField, // text input + NumberField, // number input + CheckboxField, // boolean toggle + CalendarField, // date picker + DropdownField, // select from options list + TextAreaField, // multi-line text +} from '@cratis/components/CommandForm'; + + value={c => c.title} title="Title" /> + value={c => c.quantity} title="Qty" min={1} /> + value={c => c.isActive} label="Active" /> + value={c => c.dueDate} title="Due date" /> + + value={c => c.status} + title="Status" + options={statusOptions} + optionLabel="label" + optionValue="value" +/> + value={c => c.notes} title="Notes" rows={3} /> +``` + +The `value` prop takes a function `(commandInstance) => property`. This drives both reading the value and writing it back on change. + +--- + +## CommandDialog props + +| Prop | Required | Purpose | +| ---- | -------- | ------- | +| `command` | ✓ | Command constructor | +| `title` | ✓ | Dialog header text | +| `initialValues` | | Values set as the change-tracking baseline | +| `currentValues` | | Values to pre-populate the fields | +| `onConfirm` | | Called after successful execution | +| `onCancel` | | Called when cancelled/dismissed | +| `okLabel` | | Confirm button text (default: "Ok") | +| `cancelLabel` | | Cancel button text (default: "Cancel") | +| `isValid` | | Extra validity gate combining with field validation | +| `onBeforeExecute` | | Transform command values just before `.execute()` | +| `onFieldChange` | | Callback on every field change | +| `buttons` | | Override button set (`DialogButtons` enum or custom node) | diff --git a/plugins/public-cratis-ai/skills/cratis-command/references/command-result.md b/plugins/public-cratis-ai/skills/cratis-command/references/command-result.md new file mode 100644 index 0000000..394fae8 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-command/references/command-result.md @@ -0,0 +1,81 @@ +# CommandResult — Reference + +Arc wraps every command response in a `CommandResult` envelope. + +## Shape + +```ts +interface CommandResult { + isSuccess: boolean; // true when authorized + valid + no exceptions + isAuthorized: boolean; // false → user lacks permission + isValid: boolean; // false → one or more validators failed + validationResults: ValidationResult[]; + hasExceptions: boolean; // true → unhandled server exception + exceptionMessages: string[]; + exceptionStackTrace: string; + response: T | null; // present when the backend returned a value +} + +interface ValidationResult { + propertyName: string; // camelCase, matches the command property + message: string; + severity: string; // 'Error' | 'Warning' | 'Info' +} +``` + +## Handling all cases + +```tsx +const handleSubmit = async () => { + const result = await command.execute(); + + if (!result.isAuthorized) { + navigate('/login'); + return; + } + + if (!result.isValid) { + // Map errors by property for inline display + const fieldErrors = result.validationResults.reduce((acc, v) => { + acc[v.propertyName] = v.message; + return acc; + }, {} as Record); + setErrors(fieldErrors); + return; + } + + if (result.hasExceptions) { + toast.error(result.exceptionMessages.join('\n')); + return; + } + + onSuccess(result.response); // result.response typed when backend returns a value +}; +``` + +## Accessing the returned value + +If the backend controller returns a value: + +```csharp +[HttpPost] +public async Task CreateOrder([FromBody] CreateOrder command) +{ + var id = Guid.NewGuid(); + await eventLog.Append(id, new OrderCreated(...)); + return id; +} +``` + +Then on the frontend: + +```ts +const result = await createOrder.execute(); +if (result.isSuccess) { + const newId = result.response as string; // Guids come as strings +} +``` + +## Bypassing the CommandResult wrapper + +If you need to return a raw HTTP response (e.g. for file downloads), use `[AspNetResult]` on the controller action. The frontend receives the raw response and the proxy does not include `execute()`. diff --git a/plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md b/plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md new file mode 100644 index 0000000..bf061b0 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-command/references/proxy-setup.md @@ -0,0 +1,71 @@ +# Proxy Generator Setup — Reference + +The proxy generator runs as an MSBuild task during `dotnet build` and produces TypeScript interfaces and classes for every command and query it finds. + +--- + +## Install the NuGet package + +Add to every `.csproj` that contains controllers, commands, or queries: + +```xml + +``` + +## Configure the output path + +```xml + + + $(MSBuildThisFileDirectory)../Web/src/api + +``` + +## Install the frontend package + +```bash +npm install @cratis/arc +``` + +--- + +## Run the generator + +```bash +dotnet build +``` + +The generator will write TypeScript files under the configured output path, mirroring your C# namespace hierarchy as folders: + +``` +Web/src/api/ + Accounts/ + OpenDebitAccount.ts ← POST action → command proxy + AllAccounts.ts ← GET action → query proxy + index.ts + index.ts +``` + +--- + +## Common gotchas + +| Problem | Fix | +| ------- | --- | +| No files generated | Ensure the package is referenced and the project builds cleanly first | +| Wrong folder structure | The folder mirrors the **namespace**, not the file path — adjust your namespace | +| Stale proxies | Run `dotnet clean && dotnet build` to force full regeneration | +| Proxies mixed with hand-written files | Set `true` to prevent the generator deleting everything; or move proxies to a dedicated folder | + +--- + +## Multi-project solutions + +``` +MyApp.API.csproj: + + + ../MyApp.Web/src/api +``` + +Only the project with controllers needs the proxy generator package. Domain and read-model projects do not. diff --git a/plugins/public-cratis-ai/skills/cratis-command/references/validation.md b/plugins/public-cratis-ai/skills/cratis-command/references/validation.md new file mode 100644 index 0000000..2d3f092 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-command/references/validation.md @@ -0,0 +1,104 @@ +# Command Validation — Reference + +Validation in Cratis Arc runs in two places: **client-side** (via the proxy, before the request is sent) and **server-side** (the full pipeline, always). + +--- + +## FluentValidation (recommended) + +```csharp +// Must extend CommandValidator, not AbstractValidator +public class CreateOrderValidator : CommandValidator +{ + public CreateOrderValidator() + { + RuleFor(c => c.CustomerId).NotEmpty().WithMessage("Customer is required"); + RuleFor(c => c.Total).GreaterThan(0).WithMessage("Total must be positive"); + RuleFor(c => c.Items).NotEmpty().WithMessage("Order must have at least one item"); + } +} +``` + +**Why `CommandValidator`?** It marks the class for automatic discovery (no DI registration needed) and allows the proxy generator to extract the rules into TypeScript for client-side pre-flight. + +Rules that can be extracted and run client-side: +- `NotEmpty`, `NotNull` +- `MaximumLength`, `MinimumLength`, `Length` +- `GreaterThan`, `LessThan`, `GreaterThanOrEqualTo`, `LessThanOrEqualTo` +- `Must` with simple predicates +- `EmailAddress` + +Rules that only run server-side (cannot be extracted): +- Validators with injected dependencies (e.g. database uniqueness checks) + +--- + +## Data Annotations + +```csharp +public record CreateOrder( + [Required] Guid CustomerId, + [Range(0.01, double.MaxValue, ErrorMessage = "Total must be positive")] decimal Total, + [Required, MinLength(1)] List Items +); +``` + +Simpler but less flexible than FluentValidation. Rules are enforced server-side and reflected in `CommandResult.validationResults`. + +--- + +## Automatic validate endpoint + +For every `[HttpPost]` command, Arc registers a parallel endpoint: + +- Execute: `POST /api/orders/create` +- Validate: `POST /api/orders/create/validate` + +The validate endpoint runs all authorization and validation filters but **never** calls the handler. No side effects. + +--- + +## Client-side validate() in React + +```tsx +const [command] = CreateOrder.use(); +const [errors, setErrors] = useState>({}); + +// Option A: validate on blur +const handleBlur = async (field: string) => { + const result = await command.validate(); + const fieldError = result.validationResults.find(v => v.propertyName === field); + setErrors(prev => ({ ...prev, [field]: fieldError?.message ?? '' })); +}; + +// Option B: validate proactively as user types +useEffect(() => { + command.validate().then(result => { + setCanSubmit(result.isSuccess); + }); +}, [command.hasChanges]); + +// Option C: validate + conditional execute +const handleSubmit = async () => { + const validation = await command.validate(); + if (!validation.isValid) { + setErrors(validation.validationResults.reduce(...)); + return; + } + await command.execute(); +}; +``` + +--- + +## Showing validation errors per field + +```tsx +const getError = (field: keyof typeof command) => + result.validationResults.find(v => v.propertyName === String(field))?.message; + + +{getError('name') && {getError('name')}} +``` + +`propertyName` in the result is camelCase matching the C# property name (lowercased first letter). diff --git a/canonical/skills/cratis-example/LICENSE b/plugins/public-cratis-ai/skills/cratis-fundamentals-concept/LICENSE similarity index 100% rename from canonical/skills/cratis-example/LICENSE rename to plugins/public-cratis-ai/skills/cratis-fundamentals-concept/LICENSE diff --git a/plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md b/plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md new file mode 100644 index 0000000..7e68ee3 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-fundamentals-concept/SKILL.md @@ -0,0 +1,198 @@ +--- +name: cratis-fundamentals-concept +description: Create strongly typed Cratis domain values with ConceptAs and Chronicle event-source identities with EventSourceId. Use when a C# domain value has meaning beyond its primitive or when an identity is actually used as a Chronicle event-source/stream ID. Do not use for enums, DTO-only transport values, arbitrary non-stream entity IDs, or event schema migration. +license: MIT +--- + +# Cratis domain concepts and event-source identities + +Replace a primitive only when the domain gives it distinct meaning. Keep value +concepts and Chronicle stream identities separate. + +## Verified product sources + +This skill is verified against these exact public releases: + +| Package | Version | Purpose | +| --- | --- | --- | +| `Cratis.Fundamentals` | `7.18.1` | `Cratis.Concepts.ConceptAs` | +| `Cratis.Chronicle` | `16.38.1` | `Cratis.Chronicle.Events.EventSourceId` and `EventSourceId` | + +Reverify product sources before claiming support for another version. + +## Choose the type + +- Derive a name, amount, code, number, or non-stream entity ID from + `ConceptAs`. +- Derive an identity from `EventSourceId` only when that value is actually + passed to Chronicle as the event-source/stream ID. +- Do not use `ConceptAs` for a Chronicle stream identity. +- Do not use `EventSourceId` merely because a value is called an ID. +- Do not wrap an enum. An enum already expresses a closed domain concept. +- Keep DTO-only transport values primitive unless the domain type belongs in the + public contract. + +Both generic bases require an underlying type that implements `IComparable`. + +## Create a value concept + +A value concept contains exactly one wrapped value. Do not add extra properties; +Fundamentals converters assume the concept is a single-value type and additional +state can be lost during serialization. + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Concepts; + +namespace .; + +/// +/// Represents the . +/// +/// The underlying value. +public record ( Value) : + ConceptAs<>(Value); +``` + +`ConceptAs` supplies implicit conversion from the concept to `T`. Add the +reverse conversion only when it improves the domain API: + +```csharp +public static implicit operator ( value) => + new(value); +``` + +Primitive-to-concept conversion is optional; it is not a Fundamentals +requirement. + +### Absence and sentinels + +`ConceptAs` rejects a null wrapped value. Represent absence with a nullable +concept reference such as `?` when absence is valid. + +A `NotSet` or `Empty` value is optional domain policy. Add one only when the +chosen primitive value is impossible or explicitly reserved in that domain. +Do not assume `string.Empty`, `0`, or `Guid.Empty` is universally invalid. + +## Create a Guid-backed Chronicle stream identity + +Use this shape only for an identity actually supplied to Chronicle append/read +operations as the event-source ID. + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Chronicle.Events; + +namespace .; + +/// +/// Represents the event-source identity of a . +/// +/// The underlying Guid value. +public record (Guid Value) : EventSourceId(Value) +{ + /// + /// Creates a new . + /// + /// A new . + public static New() => new(Guid.NewGuid()); + + /// + /// Converts a Guid to a . + /// + public static implicit operator (Guid value) => new(value); +} +``` + +`New()` and the primitive-to-derived conversion are conveniences on this domain +type. `EventSourceId` does not construct an arbitrary derived identity for +you. + +## Create a non-Guid Chronicle stream identity + +Use a factory only when the domain has an authoritative way to create the +underlying value. + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Cratis.Chronicle.Events; + +namespace .; + +/// +/// Represents the event-source identity of a . +/// +/// The underlying value. +public record ( Value) : + EventSourceId<>(Value) +{ + /// + /// Converts the underlying value to a . + /// + public static implicit operator ( value) => + new(value); +} +``` + +The exact `EventSourceId` base supports conversions among `T`, string, +untyped `EventSourceId`, and `EventSourceId`. Those operators do not create +your derived `` from `T`, string, or untyped `EventSourceId`. +Declare only the derived-type conversions your domain API needs. + +String and Guid are the safest round-trip primitives. Chronicle also supports +constructible `ConceptAs` and `ConceptAs` values. Other comparable +values rely on `Convert.ChangeType`; verify round-trip behavior before using +them as stream IDs. + +### Unspecified and sensitive identities + +`EventSourceId.Unspecified` belongs to the untyped string-backed ID. +`Guid.Empty`, `0`, `0L`, and similar typed values become real, specified stream +IDs after conversion; they are not Chronicle's unspecified value. Treat any +sentinel on a typed identity as explicit domain policy, not framework behavior. + +Never use a sensitive natural identifier directly as an event-source ID. +Chronicle cannot encrypt event-source IDs. Use a random surrogate stream ID and +store the sensitive value separately under the approved compliance model. + +## Use the identity with Chronicle + +Pass the typed identity as the append/read event-source ID. Merely declaring an +`EventSourceId` property does not select the event stream. + +Do not add `[Key]` or `[Subject]` to an `EventSourceId`-derived member; +Chronicle analyzer `CHR0026` reports that misuse. Do not add `[PII]` to an +event-source ID; analyzer `CHR0034` rejects it. + +## Placement is an application convention + +In a Cratis application, place the concept with the feature that owns its +meaning rather than in a generic `Concepts/` folder. Put genuinely cross-feature +concepts in `Common/`. Do not introduce a top-level `Features/` wrapper. + +This placement is a Cratis application convention, not a Fundamentals or +Chronicle API requirement. Framework and client repositories follow their own +repository structure. + +## Verify + +- `ConceptAs` and `EventSourceId` use an `IComparable` underlying type. +- A concept contains exactly one wrapped value and no extra properties. +- Enums remain enums. +- Null absence uses a nullable concept reference rather than a null wrapped + value. +- Primitive-to-derived conversions and sentinels exist only when justified by + the domain. +- An `EventSourceId` type represents a real Chronicle stream identity. +- The typed identity is passed explicitly to Chronicle operations. +- No `[Key]`, `[Subject]`, or `[PII]` attribute is placed on the stream identity. +- Sensitive natural identifiers use a surrogate stream ID. +- The file carries the repository license header. +- The project builds and its relevant specifications pass against the verified + package versions. diff --git a/plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md b/plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md new file mode 100644 index 0000000..6c6c44e --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-react-page/SKILL.md @@ -0,0 +1,243 @@ +--- +name: cratis-react-page +description: Step-by-step guidance for building a React page in a Cratis Arc application — DataPage lists, CommandDialog toolbar actions, row selection, details components, observable queries, and MVVM. Use when building or modifying a page that lists/displays data, adding a table, wiring Add/Edit/Delete, or connecting a component to a proxy-generated query (standard or observable). +--- + +## Workflow + +### Step 1 — Prerequisites + +- Backend query and command endpoints must already exist (see `cratis-readmodel` and `cratis-command` skills). +- Run a Debug `dotnet build` on the backend to regenerate proxies before importing them. + +Import `DataPage` (and its `Column`/`MenuItem` helpers) from the **subpath**, not the root barrel: + +```tsx +import { DataPage, MenuItem } from '@cratis/components/DataPage'; +import { Column } from '@cratis/components/DataPage'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { useDialog, DialogProps } from '@cratis/arc.react/dialogs'; +``` + +--- + +### Step 2 — Basic DataPage setup + +`DataPage` combines a toolbar/menu, a data table, and an optional details component. `title`, `query`, `emptyMessage`, and `children` are required; columns are declared compositionally inside `` using PrimeReact ``. + +```tsx +import { DataPage } from '@cratis/components/DataPage'; +import { Column } from '@cratis/components/DataPage'; +import { AllAccounts } from './AllAccounts'; + +export const AccountsPage = () => ( + + + + + + +); +``` + +--- + +### Step 3 — Add menu actions + +Toolbar actions go in ``. `MenuItem` is a PrimeReact menu item (use `command`, not `onClick`); the `disableOnUnselected` flag greys it out until a row is selected. Create a separate dialog component using `DialogProps`, then wire it up with `useDialog`. + +**Dialog component (`CreateAccountDialog.tsx`):** + +```tsx +import { DialogProps } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm'; +import { CreateAccount } from './CreateAccount'; + +export const CreateAccountDialog = ({ closeDialog }: DialogProps) => ( + command={CreateAccount} title="Create Account" okLabel="Create"> + value={c => c.name} title="Account Name" /> + +); +``` + +**Page component:** + +```tsx +import { DataPage, MenuItem } from '@cratis/components/DataPage'; +import { Column } from '@cratis/components/DataPage'; +import { useDialog } from '@cratis/arc.react/dialogs'; +import { CreateAccountDialog } from './CreateAccountDialog'; + +export const AccountsPage = () => { + const [CreateAccountWrapper, showCreateAccount] = useDialog(CreateAccountDialog); + + return ( + <> + + + + + + showCreateAccount()} /> + + + + + ); +}; +``` + +See [dialogs.md](../../rules/dialogs.md) and the `stepper-command-dialog` skill for the full dialog patterns. + +--- + +### Step 4 — Row selection and edit dialog + +Track selection with `selection` + `onSelectionChange`, and supply the row data as props to the edit dialog. + +**Edit dialog (`EditAccountDialog.tsx`):** + +```tsx +import { DialogProps } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm'; +import { EditAccount } from './EditAccount'; + +interface EditAccountDialogProps extends DialogProps { + accountId: string; + name: string; +} + +export const EditAccountDialog = ({ accountId, name }: EditAccountDialogProps) => ( + + command={EditAccount} + title="Edit Account" + okLabel="Save" + initialValues={{ accountId }} + currentValues={{ name }}> + value={c => c.name} title="Account Name" /> + +); +``` + +**Page wiring:** + +```tsx +const [selected, setSelected] = useState(); +const [EditAccountWrapper, showEditAccount] = useDialog(EditAccountDialog); + + { + setSelected(e.value); + if (e.value) showEditAccount({ accountId: e.value.id, name: e.value.name }); + }}> + + + + + +``` + +- `initialValues` sets the change-tracking baseline (e.g. IDs that must be present but aren't user-entered). +- `currentValues` pre-populates the visible field values. + +--- + +### Step 5 — Observable vs standard query + +The **same `query` prop** accepts a standard query (`IQueryFor`) or an observable query (`IObservableQueryFor`) — there is no separate `observableQuery` prop. Pass the observable query proxy and `DataPage` subscribes to live updates automatically: + +```tsx + + + + + +``` + +Observable results push updates automatically; for snapshot data that changes only on user action, pass the standard query and call `onRefresh` after a command succeeds. + +--- + +### Step 6 — Details component (optional) + +`detailsComponent` renders detail for the selected row. It receives `{ item, onRefresh }`: + +```tsx +import { IDetailsComponentProps } from '@cratis/components/DataPage'; + +const AccountDetail = ({ item }: IDetailsComponentProps) => ( +
{item.name}
+); + + + + + + +``` + +--- + +### Step 7 — MVVM view model (for complex pages) + +For pages with complex state or coordination logic, wrap the page in a view model (see [react.md](../../rules/react.md)): + +```tsx +import { withViewModel } from '@cratis/arc.react.mvvm'; +import { injectable } from 'tsyringe'; + +@injectable() +class AccountsViewModel { + selectedAccount?: AccountSummary; + select(account: AccountSummary) { this.selectedAccount = account; } +} + +export const AccountsPage = withViewModel(AccountsViewModel, ({ viewModel }) => ( + viewModel.select(e.value)}> + + + + +)); +``` + +Read `viewModel.property` inside JSX (never destructure observables at the top of the body). See [react.md](../../rules/react.md) for the full MVVM rules. + +--- + +## Quick decision guide + +| Need | Use | +|---|---| +| Read-only list | `DataPage` with a standard `query` | +| Real-time updates | `DataPage` with an observable query passed to the same `query` prop | +| Add / create action | `` + `MenuItem` + `CommandDialog` + `useDialog` | +| Edit selected row | `selection` + `onSelectionChange` + `CommandDialog` + `currentValues`/`initialValues` | +| Detail for selected row | `detailsComponent` prop | +| Complex page logic | `withViewModel` MVVM wrapper | + +## Key DataPage props + +| Prop | Purpose | +|---|---| +| `title` (required) | toolbar title | +| `query` (required) | the query proxy — standard or observable | +| `emptyMessage` (required) | shown when there are no rows | +| `children` (required) | `` + optional `` | +| `queryArguments` | arguments passed to the query | +| `selection` / `onSelectionChange` | controlled single-row selection | +| `detailsComponent` | `React.FC>` rendered for the selected row | +| `globalFilterFields` / `defaultFilters` / `clientFiltering` | filtering | +| `onRefresh` | invoked to re-fetch a standard query | +| `tablePt` / `menubarPt` / `*Unstyled` | PrimeReact pass-through styling | diff --git a/plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md b/plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md new file mode 100644 index 0000000..da25baf --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-react-page/references/data-page.md @@ -0,0 +1,102 @@ +# DataPage — Reference + +`DataPage` (from `@cratis/components`) is the standard full-page layout providing a menubar, data table, and optional detail panel in one component. + +## Import + +```tsx +import { DataPage, MenuItemGroup, MenuItem, Column } from '@cratis/components'; +``` + +## Core props + +| Prop | Type | Description | +| --- | --- | --- | +| `query` | Query or observable query class | Proxy-generated query — standard *or* observable; `DataPage` auto-detects and goes real-time for an observable query | +| `columns` | `Column[]` | Column definitions (see below) | +| `menuItems` | `ReactNode` | Toolbar content (usually ``) | +| `detailPanel` | `(row: T) => ReactNode` | Renders to the right when a row is selected | +| `onRowSelected` | `(row: T) => void` | Callback when user clicks a row | +| `noDataMessage` | `string` | Message when the query returns no rows | +| `queryArgs` | `object` | Arguments forwarded to the query proxy | + +Pass either a standard or observable query to the single `query` prop — `DataPage` auto-detects which; there is no separate `observableQuery` prop. + +## Column definition + +```tsx +type Column = { + header: string; + field: keyof T | ((row: T) => string); + width?: number | string; + sortable?: boolean; +}; +``` + +Example with custom renderer: + +```tsx +columns={[ + { header: 'Name', field: 'name' }, + { header: 'Balance', field: (row) => row.balance.toFixed(2) }, +]} +``` + +## MenuItemGroup / MenuItem + +```tsx + + + + +``` + +Multiple `MenuItemGroup` children create visual separators between groups. + +## Detail panel + +The detail panel receives the currently selected row. It is hidden when no row is selected. + +```tsx + ( + + )} +/> +``` + +## Full example + +```tsx +import { useDialog } from '@cratis/arc.react/dialogs'; +import { CreateAccountDialog } from './CreateAccountDialog'; + +export const AccountsPage = () => { + const [CreateAccountWrapper, showCreateAccount] = useDialog(CreateAccountDialog); + + return ( + <> + `$${r.balance.toFixed(2)}` }, + ]} + menuItems={ + + showCreateAccount()} /> + + } + detailPanel={(row) => } + noDataMessage="No accounts found." + /> + + + ); +}; +``` + +`CreateAccountDialog` is a separate component that receives `closeDialog` via `DialogProps` and renders a `CommandDialog`. See `dialogs.md` for the full dialog pattern. diff --git a/plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md b/plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md new file mode 100644 index 0000000..c2cce21 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-react-page/references/data-table.md @@ -0,0 +1,62 @@ +# Data Tables — Reference + +Use standalone data table components when you need a table without the built-in `DataPage` full-page chrome (e.g., embedded inside another panel or card). + +## DataTableForQuery + +```tsx +import { DataTableForQuery } from '@cratis/components'; +import { AllAccounts } from './queries/AllAccounts'; + + setSelected(row)} +/> +``` + +## DataTableForObservableQuery + +```tsx +import { DataTableForObservableQuery } from '@cratis/components'; +import { AllAccountsLive } from './queries/AllAccountsLive'; + + setSelected(row)} +/> +``` + +## Shared props + +| Prop | Type | Description | +| --- | --- | --- | +| `query` / `query` | Query class | Proxy query (use the appropriate component for type) | +| `columns` | `Column[]` | Column definitions (same shape as DataPage) | +| `onRowSelected` | `(row: T) => void` | Row click callback | +| `selectedRow` | `T \| undefined` | Externally controlled selected row | +| `noDataMessage` | `string` | Message when no rows are returned | +| `queryArgs` | `object` | Arguments forwarded to the query | + +## Column definition + +```ts +type Column = { + header: string; + field: keyof T | ((row: T) => string); + width?: number | string; +}; +``` + +## When to use each component + +| Situation | Component | +| --- | --- | +| Full page with toolbar | `DataPage` | +| Embedded table, standard query | `DataTableForQuery` | +| Embedded table, real-time push | `DataTableForObservableQuery` | +| Inline data (no query) | Custom table (out of scope) | diff --git a/plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md b/plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md new file mode 100644 index 0000000..644c169 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-react-page/references/dialogs.md @@ -0,0 +1,198 @@ +# Dialogs — Reference + +## Core pattern + +Dialogs are **separate components** that receive `closeDialog` as a prop via `DialogProps`. The parent uses `useDialog(DialogComponent)` to get a wrapper and a `show` function. + +```tsx +import { DialogProps } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField } from '@cratis/components/CommandForm'; +import { CreateAccount } from './commands/CreateAccount'; + +// 1. Define the dialog component +export const CreateAccountDialog = ({ closeDialog }: DialogProps) => { + return ( + + command={CreateAccount} + title="Create Account" + okLabel="Create" + > + value={c => c.name} title="Account Name" /> + + ); +}; +``` + +```tsx +// 2. Wire it up in the parent +import { useDialog } from '@cratis/arc.react/dialogs'; +import { CreateAccountDialog } from './CreateAccountDialog'; + +export const AccountsPage = () => { + const [CreateAccountWrapper, showCreateAccount] = useDialog(CreateAccountDialog); + + return ( + <> + + + + ); +}; +``` + +`showCreateAccount()` opens the dialog. `closeDialog` (injected into the dialog component by the framework) closes it. + +--- + +## `useDialog` + +```tsx +import { useDialog } from '@cratis/arc.react/dialogs'; + +const [DialogWrapper, showDialog] = useDialog(MyDialogComponent); +``` + +- `DialogWrapper` — render this once in the JSX tree; it controls visibility +- `showDialog(props?)` — call to open; returns a `Promise<[DialogResult, TResponse?]>` + +```tsx +const [result, response] = await showDialog({ someInitialProp: value }); +if (result === DialogResult.Ok) { + // handle confirmed result +} +``` + +Pass props to `showDialog()` when the dialog needs context from the parent (e.g. a selected row to edit). + +--- + +## Passing props to a dialog + +Define the dialog's props interface extending `DialogProps`: + +```tsx +interface EditAccountDialogProps extends DialogProps { + accountId: string; + name: string; +} + +export const EditAccountDialog = ({ closeDialog, accountId, name }: EditAccountDialogProps) => { + return ( + + command={EditAccount} + title="Edit Account" + initialValues={{ accountId }} + currentValues={{ name }} + > + value={c => c.name} title="Account Name" /> + + ); +}; +``` + +Then in the parent: + +```tsx +const [EditAccountWrapper, showEditAccount] = useDialog(EditAccountDialog); + +// Pass the selected row when opening + showEditAccount({ accountId: row.id, name: row.name })} ... /> + +``` + +--- + +## CommandDialog + +Use for dialogs that execute a command on confirm. Import from `@cratis/components/CommandDialog`. + +```tsx +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputTextField, NumberField } from '@cratis/components/CommandForm'; +``` + +**Key props:** + +| Prop | Purpose | +| --- | --- | +| `command` | Command constructor (proxy-generated class) | +| `title` | Dialog header text | +| `okLabel` | Confirm button text (default: `"Ok"`) | +| `cancelLabel` | Cancel button text (default: `"Cancel"`) | +| `initialValues` | Values set as the change-tracking baseline (e.g. injected IDs) | +| `currentValues` | Values to pre-populate the fields for editing | +| `isValid` | Extra validity gate in addition to field-level validation | +| `onBeforeExecute` | Transform command values just before `.execute()` | + +`CommandDialog` automatically disables the confirm button until all required fields are filled. + +Use `initialValues` for values that must be present but not visible (e.g. a parent entity ID). Do **not** set them in `onBeforeExecute` — they won't be visible to form validation. + +--- + +## CommandForm field components + +All field components come from `@cratis/components/CommandForm`. Pass the command type as the generic parameter so `value` is fully typed. + +```tsx +import { + InputTextField, // text input + NumberField, // number input + CheckboxField, // boolean toggle + CalendarField, // date picker + DropdownField, // select from options list + TextAreaField, // multi-line text +} from '@cratis/components/CommandForm'; + + value={c => c.title} title="Title" /> + value={c => c.quantity} title="Qty" min={1} /> + value={c => c.isActive} label="Active" /> + value={c => c.dueDate} title="Due date" /> + + value={c => c.status} + title="Status" + options={statusOptions} + optionLabel="label" + optionValue="value" +/> + value={c => c.notes} title="Notes" rows={3} /> +``` + +> The shared field label prop is **`title`** (from the base field props), not `label`. `CheckboxField` and `RadioButtonField` additionally accept their own `label` prop. + +The `value` prop takes a function `(commandInstance) => property`. This drives both reading the value and writing it back on change. + +--- + +## Dialog (data-only, no command) + +Use when the dialog collects data and returns it without executing a command. + +```tsx +import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs'; +import { Dialog } from '@cratis/components/Dialogs'; +import { InputText } from 'primereact/inputtext'; +import { useState } from 'react'; + +export const RenameDialog = ({ closeDialog }: DialogProps<{ name: string }>) => { + const [name, setName] = useState(''); + + return ( + 0} + onConfirm={() => closeDialog(DialogResult.Ok, { name })} + onCancel={() => closeDialog(DialogResult.Cancelled)} + > + setName(event.target.value)} + autoFocus + /> + + ); +}; +``` + +Never import `Dialog` from `primereact/dialog` — always use `@cratis/components/Dialogs`. diff --git a/plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md b/plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md new file mode 100644 index 0000000..3ba11ca --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-react-page/references/mvvm.md @@ -0,0 +1,133 @@ +# MVVM — Reference + +The Arc MVVM pattern keeps page logic in plain TypeScript classes (view models) and keeps components purely declarative. + +## When to use MVVM + +- Page has complex coordinated state (selected item, filters, multiple dialogs) +- Logic needs unit-testing independent of React +- You want to share state across child components via injection + +For simple pages, MVVM is optional — use regular hooks directly in the component instead. + +## Setup + +Install packages if not already present: + +``` +npm install @cratis/arc.react.mvvm tsyringe reflect-metadata +``` + +Ensure `tsconfig.json` enables decorators: + +```json +{ + "compilerOptions": { + "experimentalDecorators": true, + "emitDecoratorMetadata": true + } +} +``` + +Import `reflect-metadata` once, at the entry point of your app: + +```tsx +import 'reflect-metadata'; +``` + +## View model class + +```ts +import { injectable } from 'tsyringe'; +import { makeAutoObservable } from 'mobx'; + +@injectable() +export class AccountsViewModel { + selectedAccount?: AccountSummary = undefined; + + constructor() { + makeAutoObservable(this); + } + + selectAccount(account: AccountSummary) { + this.selectedAccount = account; + } +} +``` + +- `@injectable()` — registers the class with tsyringe for DI +- `makeAutoObservable(this)` — makes all fields reactive (MobX) + +## withViewModel + +```tsx +import { withViewModel } from '@cratis/arc.react.mvvm'; + +export const AccountsPage = withViewModel(AccountsViewModel, ({ viewModel }) => { + return ( + viewModel.selectAccount(row)} + detailPanel={() => viewModel.selectedAccount + ? + : null + } + /> + ); +}); +``` + +The view model instance is created once per mount and disposed on unmount. It is the same instance for the whole component tree under `withViewModel`. + +## IHandleProps — reactive props + +When a child component needs to receive a prop and react to its changes, implement `IHandleProps`: + +```ts +import { IHandleProps } from '@cratis/arc.react.mvvm'; + +interface DetailProps { + account: AccountSummary; +} + +@injectable() +export class AccountDetailViewModel implements IHandleProps { + account!: AccountSummary; + + propsChanged(props: DetailProps): void { + this.account = props.account; + } +} +``` + +`propsChanged` is called whenever the parent passes new props, allowing the view model to react. + +## Dependency injection in view models + +Use tsyringe constructor injection. Cratis registers common singletons (e.g., `IEventStore`, query/command types): + +```ts +@injectable() +export class AccountsViewModel { + constructor( + private readonly _eventLog: IEventLog, + ) { + makeAutoObservable(this); + } +} +``` + +## MVVM context + +Wrap the app (or route root) in `` to enable the DI container: + +```tsx +import { MVVM } from '@cratis/arc.react.mvvm'; + + + + +``` + +If you are using ``, it already includes `` internally — do not double-wrap. diff --git a/plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md b/plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md new file mode 100644 index 0000000..0811a79 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-readmodel/SKILL.md @@ -0,0 +1,258 @@ +--- +name: cratis-readmodel +description: Step-by-step guidance for creating a Cratis Chronicle read model from scratch — defining events, choosing between projection and reducer, [ReadModel] record with static query methods, and the generated TypeScript proxy in React. Use when creating a read model, working with [EventType], [ReadModel], IProjectionFor, IReducerFor, observable queries, or deriving state from events. For adding a projection or reactor to an existing read model, use add-projection instead. +--- + +# Creating a Cratis Read Model + +A read model is derived state built from events. The path is: + +``` +[EventType] records → [ReadModel] record + static query methods → projection or reducer → TypeScript proxy → React +``` + +--- + +## Step 1 — Define your events + +Events are the source of truth. Define each as a `record` decorated with `[EventType]`. Name them in **past tense**. + +```csharp +// Accounts/AccountSummary/AccountSummary.cs — events live in the slice file they belong to +using Cratis.Chronicle.Events; + +/// Emitted when a debit account is opened. +[EventType] +public record DebitAccountOpened(AccountName Name, OwnerId OwnerId); + +/// Emitted when a debit account is closed. +[EventType] +public record DebitAccountClosed; + +/// Emitted when funds are deposited. +[EventType] +public record FundsDeposited(Money Amount); + +/// Emitted when funds are withdrawn. +[EventType] +public record FundsWithdrawn(Money Amount); +``` + +Good event design: +- One clear purpose per event — do not mix concerns. +- **Avoid nullable properties** — Chronicle's analyzer warns on them; model an optional fact as a separate event. +- Properties are concept-typed facts (never raw `Guid`/`string`), and never carry the event-source id. + +--- + +## Step 2 — Define the read model record + +Decorate the record with `[ReadModel]` and add **static query methods** directly on it. The proxy generator turns each static method into a TypeScript query class. + +```csharp +// Domain/ReadModels/AccountSummary.cs +using Cratis.Arc.Queries.ModelBound; +using MongoDB.Driver; + +[ReadModel] +public record AccountSummary(AccountId Id, string Name, OwnerId OwnerId, decimal Balance, bool IsClosed) +{ + // Snapshot query — returns current data once + public static async Task> AllAccounts( + IMongoCollection collection) + => await collection.Find(Builders.Filter.Empty).ToListAsync(); + + public static async Task GetAccount( + AccountId id, + IMongoCollection collection) + => await collection.Find(a => a.Id == id).FirstOrDefaultAsync(); + + // Observable query — pushes updates in real time + public static ISubject> ObserveAllAccounts( + IMongoCollection collection) + => collection.Observe(); +} +``` + +**Rules:** +- `[ReadModel]` attribute is **required** for proxy generation and runtime routing +- Static methods must be `public static` and return the record type, a collection of it, or `ISubject` for real-time push +- Do **not** return `Task>` — observable methods must return `ISubject` directly +- Use `ConceptAs` wrappers for all identity fields — never raw `Guid` +- One read model per use case — do not reuse them + +--- + +## Step 3 — Choose: projection or reducer? + +| | Projection | Reducer | +|-| ---------- | ------- | +| **Best for** | Shaped read models with mapping logic, joins, children | Running aggregates: balances, counts, sums | +| **How it works** | Declarative mapping: each event updates specific fields | Receives events one by one and returns the new full state | +| **When to pick** | The read model shape comes mostly from mapping event fields | The state is a function of *accumulating* multiple events | + +For the `AccountSummary` above: use a **projection** for name/owner fields and a **reducer** for balance (a running total). In practice, reducers cover both when the aggregate combines both concerns. + +--- + +## Step 4A — Implement a projection + +```csharp +// In the slice file — fluent projection (drop to this only when model-bound can't express the shape) +using Cratis.Chronicle.Projections; + +public class AccountSummaryProjection : IProjectionFor +{ + public void Define(IProjectionBuilderFor builder) => builder + .From(from => from + .Set(m => m.Balance).WithValue(0m)) // Name/OwnerId map by AutoMap (matching names) + .From(from => from + .Add(m => m.Balance).With(e => e.Amount)) + .From(from => from + .Subtract(m => m.Balance).With(e => e.Amount)) + .From(from => from + .Set(m => m.IsClosed).WithValue(true)); +} +``` + +- **AutoMap is on by default — never call `.AutoMap()`.** Matching property names (e.g. `Name`, `OwnerId`) map automatically from `.From()`; only `.Set().To()` the ones whose names differ. +- Discovered automatically — no registration needed. +- `IProjectionFor` is keyed by **event source ID** by default (the `Id` passed when appending the event). +- Appended `tags`, `eventSourceType`, and `eventStreamType` do not filter projections directly; use reducers or reactors alongside the projection when you need metadata-based filtering +- See `references/projections.md` for joins, auto-mapping, children, composite keys + +### Model-bound shorthand (preferred for simple cases) + +`[FromEvent]` is a **class-level** attribute declaring which event populates the model; property mapping is implicit via AutoMap (matching names) or explicit per-property with `[SetFrom]`. The model needs `[ReadModel]`, and the key is the event-source id (no `[Key]` needed when the id property is the `EventSourceId` identity). + +```csharp +using Cratis.Chronicle.Projections.ModelBound; + +[ReadModel] +[FromEvent] // class-level: this event populates the model +public record AccountInfo( + AccountId Id, // event-source id — no [Key] needed + AccountName Name, // AutoMap wires DebitAccountOpened.Name (matching name) + [SetFrom(nameof(DebitAccountOpened.OwnerName))] OwnerName Owner // only when names differ +); +``` + +- `[FromEvent]` goes on the **class**, not a property. Property-level mapping uses `[SetFrom]`, and only for genuine name differences — **never call `.AutoMap()`**; matching names map automatically. +- Add more `[FromEvent]` attributes to fold in additional events. + +--- + +## Step 4B — Implement a reducer + +Use a reducer when the state is built by accumulating values across events: + +```csharp +// Domain/Reducers/AccountBalanceReducer.cs +using Cratis.Chronicle.Events; +using Cratis.Chronicle.Reducers; + +public class AccountBalanceReducer : IReducerFor +{ + public AccountBalance Opened(DebitAccountOpened @event, AccountBalance? current, EventContext context) + => new(0m, context.Occurred); + + public AccountBalance Deposited(FundsDeposited @event, AccountBalance? current, EventContext context) + => (current ?? new(0m, context.Occurred)) with { Balance = (current?.Balance ?? 0m) + @event.Amount }; + + public AccountBalance Withdrawn(FundsWithdrawn @event, AccountBalance? current, EventContext context) + => (current ?? new(0m, context.Occurred)) with { Balance = (current?.Balance ?? 0m) - @event.Amount }; +} + +public record AccountBalance(decimal Balance, DateTimeOffset LastUpdated); +``` + +- Return the **complete new state** — do not mutate `current` +- `current` is `null` on the first event for a given event source +- `EventContext` provides `Occurred`, `EventSourceId`, `SequenceNumber`, `CorrelationId` +- Discovered automatically — no registration needed +- Add `[FilterEventsByTag]`, `[EventSourceType]`, and `[EventStreamType]` when the reducer should only observe events appended with matching metadata + +--- + +## Step 5 — Expose read model queries + +Query methods live **directly on the `[ReadModel]` record** as static methods (see Step 2). You do **not** need a separate controller or `IReadModels` injection. + +The method name becomes the TypeScript proxy class name — use descriptive names like `AllAccounts`, `GetAccount`, `ObserveAllAccounts`. + +### Snapshot (one-time) queries + +```csharp +[ReadModel] +public record AccountSummary(AccountId Id, string Name, decimal Balance) +{ + public static async Task> AllAccounts( + IMongoCollection collection) + => await collection.Find(_ => true).ToListAsync(); + + public static async Task GetAccount( + AccountId id, + IMongoCollection collection) + => await collection.Find(a => a.Id == id).FirstOrDefaultAsync(); +} +``` + +### Observable (real-time push) queries + +Return `ISubject` to push updates as projection changes land: + +```csharp +[ReadModel] +public record AccountSummary(AccountId Id, string Name, decimal Balance) +{ + public static ISubject> ObserveAllAccounts( + IMongoCollection collection) + => collection.Observe(); + + public static ISubject ObserveAccount( + AccountId id, + IMongoCollection collection) + => collection.Observe(a => a.Id == id); +} +``` + +When the frontend uses an observable query, the query proxy type changes from `QueryFor` to `ObservableQueryFor`. The **same `query` prop** accepts a standard or observable query — there is no separate `observableQuery` prop; `DataPage` auto-detects it and subscribes to live updates. + +--- + +## Step 6 — Build and use in React + +```bash +dotnet build # generates TypeScript proxies +``` + +```tsx +import { AllAccounts } from '../api/Accounts/AllAccounts'; + +export const AccountList = () => { + const [accounts] = AllAccounts.use(); + + if (accounts.isPerforming) return ; + + return ( +
    + {accounts.data.map(a => ( +
  • {a.name} — ${a.balance}
  • + ))} +
+ ); +}; +``` + +For building full pages with filtering, sorting, and command actions — see the `cratis-react-page` skill. + +--- + +## Reference files + +| File | What's in it | +| ---- | ------------ | +| `references/projections.md` | Full builder API: `Set`, `Add`, `Join`, `Children`, `AutoMap`, composite keys | +| `references/reducers.md` | Reducer signatures, async, passive, snapshot behavior | +| `references/events.md` | `[EventType]`, appending, `AppendResult`, tags, constraints | +| `references/queries.md` | Query result shape, observable queries, paging | diff --git a/plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md b/plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md new file mode 100644 index 0000000..4536a01 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-readmodel/references/events.md @@ -0,0 +1,99 @@ +# Events — Reference + +## [EventType] attribute + +```csharp +using Cratis.Chronicle.Events; + +[EventType] +public record OrderPlaced(string CustomerId, decimal Total); +``` + +Every event must be decorated with `[EventType]` — this makes it discoverable and registers its schema with Chronicle. + +--- + +## Good event design + +- **Past tense**: `OrderPlaced`, `UserOnboarded`, `BookReturned` ✓ — not `PlaceOrder`, `OnboardUser` +- **One purpose**: an `AddressChanged` event should only carry address fields, not payment info +- **No nullables** unless the field is genuinely optional (e.g. `string? MiddleName`) +- **Immutable facts**: events represent something that *has happened* — do not use them to encode intent or possibility + +--- + +## Appending an event + +Inject `IEventLog` and call `Append(eventSourceId, eventInstance)`: + +```csharp +public class OrdersController(IEventLog eventLog) : ControllerBase +{ + [HttpPost] + public async Task PlaceOrder([FromBody] PlaceOrder command) + { + var result = await eventLog.Append( + command.OrderId, + new OrderPlaced(command.CustomerId, command.Total)); + + if (!result.IsSuccess) + { + // result.HasConcurrencyViolation — two requests raced + // result.HasConstraintViolations — uniqueness constraint failed + } + } +} +``` + +The first argument is the **event source ID** — the identity the event belongs to (analogous to an aggregate root ID). Projections and reducers are keyed by this ID by default. + +You can also append metadata that downstream reducers and reactors can filter on: + +```csharp +await eventLog.Append( + command.OrderId, + new OrderPlaced(command.CustomerId, command.Total), + eventStreamType: "fulfillment", + eventSourceType: "order", + tags: ["priority"]); +``` + +--- + +## Constraints (uniqueness) + +Enforce uniqueness at append time without application-level checks. For the common single-event case, mark the property `[Unique]`: + +```csharp +[EventType] +public record OrderPlaced([Unique(name: "UniqueOrderNumber", message: "Order number already used.")] OrderNumber OrderNumber); +``` + +For multi-event or `RemovedWith` rules, implement `IConstraint` (declarative `Define`, member-access lambdas only): + +```csharp +public class UniqueOrderNumber : IConstraint +{ + public void Define(IConstraintBuilder builder) => + builder.Unique(unique => unique.On(e => e.OrderNumber)); +} +``` + +Violations surface on the `AppendResult`/`CommandResult` as a constraint violation (assert the constraint **name**, never the message). See the **add-business-rule** skill. + +--- + +## Tags + +```csharp +[EventType] +[Tag("high-value")] +public record LargeOrderPlaced(decimal Total); + +// Or apply at append time: +await eventLog.Append(orderId, new LargeOrderPlaced(2500m), tags: ["priority"]); +``` + +Tags allow reducers and reactors to filter which appended events they handle when you use `[FilterEventsByTag]`. `[Tag]` and `[Tags]` on projections, reducers, and reactors label the observer or event type; they do not filter the observer by themselves. + +See `Documentation/events/filtering/` for tag, event source type, and event stream type filtering examples. diff --git a/plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md b/plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md new file mode 100644 index 0000000..41fe995 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-readmodel/references/projections.md @@ -0,0 +1,155 @@ +# Projections — Reference + +## Model-bound projections (preferred) + +Put projection metadata on the read model first. This is the default choice for Cratis projects because it keeps the read model and its projection behavior together. + +```csharp +using Cratis.Chronicle.Keys; +using Cratis.Chronicle.Projections.ModelBound; + +public record InvoiceInfo( + [Key] Guid Id, + [FromEvent] string Number, + [AddFrom(nameof(LineItemAdded.Price))] decimal RunningTotal, + [SetFromContext(nameof(EventContext.Occurred))] DateTimeOffset? PaidAt); +``` + +For child relationships where later child events arrive on the child event source, set `parentKey` on the child type's class-level `FromEvent`: + +```csharp +public record Invoice( + [Key] Guid Id, + [ChildrenFrom( + key: nameof(LineItemAdded.LineItemId), + identifiedBy: nameof(LineItem.Id), + parentKey: nameof(LineItemAdded.InvoiceId))] + IEnumerable Lines); + +[FromEvent(parentKey: nameof(LineItemRenamed.InvoiceId))] +public record LineItem( + [Key] Guid Id, + string Description); +``` + +### Attribute reference + +| Attribute | Equivalent builder | +| --------- | ------------------ | +| `[Key]` | Default key (event source ID) | +| `[FromEvent]` | `.From()` — maps event T (AutoMap is on by default) | +| `[FromEvent(key: nameof(T.Prop))]` | `.UsingKey(e => e.Prop)` | +| `[FromEvent(parentKey: nameof(T.Prop))]` | `.UsingParentKey(e => e.Prop)` on child projections | +| `[SetFrom(nameof(...))]` | `.Set(...).To(...)` | +| `[AddFrom(nameof(...))]` | `.Add(...).With(...)` | +| `[SubtractFrom(nameof(...))]` | `.Subtract(...).With(...)` | +| `[SetFromContext(nameof(...))]` | `.Set(...).ToEventContextProperty(...)` | +| `[Increment]` | counter increment | +| `[Decrement]` | counter decrement | +| `[RemovedWith]` | `.RemovedWith()` | +| `[Passive]` | `.Passive()` | +| `[NotRewindable]` | `.NotRewindable()` | + +--- + +## Declarative projection builder (`IProjectionFor`) — fallback + +```csharp +public class InvoiceProjection : IProjectionFor +{ + public void Define(IProjectionBuilderFor builder) => builder + .From() // AutoMap is on by default — matching names map automatically + .From(from => from + .Set(m => m.PaidAt).ToEventContextProperty(c => c.Occurred) + .Set(m => m.Status).WithValue(InvoiceStatus.Paid)) + .From(from => from + .Add(m => m.TotalAmount).With(e => e.Price)) + .Join(j => j + .On(m => m.CustomerId) + .Set(m => m.CustomerName).To(e => e.Name)) + .RemovedWith() + .Children(m => m.Lines, cb => cb + .IdentifiedBy(li => li.LineItemId) + .From() + .RemovedWith()); +} +``` + +### Builder method reference + +| Method | Purpose | +| ------ | ------- | +| `.From(cb)` | Handle an event type | +| `.AutoMap()` | On by default for every `.From()` — **do not call it**; only re-enable inside a `.NoAutoMap()` scope | +| `.Set(m => m.Prop).To(e => e.Prop)` | Explicit property mapping | +| `.Set(m => m.Prop).WithValue(val)` | Set a constant | +| `.Set(m => m.Prop).ToEventContextProperty(c => c.X)` | Map from event metadata | +| `.Add(m => m.Prop).With(e => e.X)` | Add (numeric) | +| `.Subtract(m => m.Prop).With(e => e.X)` | Subtract | +| `.Count(m => m.Prop)` | Increment a counter | +| `.Join(j => j.On(key).Set(...))` | Cross-stream join another event | +| `.RemovedWith()` | Delete the read model on this event | +| `.Children(m => m.Coll, cb)` | Manage a child collection | +| `.FromEvery(cb)` | Apply mapping to every event type | +| `.UsingKey(e => e.Prop)` | Override the key (default: event source ID) | +| `.Passive()` | On-demand only, no active observer | +| `.NotRewindable()` | Forward-only, no replay | + +### Event context properties + +```csharp +.ToEventContextProperty(c => c.Occurred) // DateTimeOffset +.ToEventContextProperty(c => c.EventSourceId) // string +.ToEventContextProperty(c => c.SequenceNumber) // long +.ToEventContextProperty(c => c.CorrelationId) // Guid +``` + +### Composite keys + +```csharp +builder.UsingCompositeKey(key => key + .Set(k => k.Year).ToEventContextProperty(c => c.Occurred.Year) + .Set(k => k.Month).ToEventContextProperty(c => c.Occurred.Month)); +``` + +## Reading projected read models + +```csharp +// Inject IReadModels in a controller +[HttpGet("{id}")] +public async Task Get(Guid id) + => await readModels.GetOne(id); + +// Strong consistency (replay events synchronously before returning) +var account = await readModels.GetOneWithImmediateProjection(id); + +// All instances +var all = await readModels.GetAll(); +``` + +--- + +## Appended event metadata and projections + +When you append events, you can set tags, event source type, and event stream type: + +```csharp +await eventLog.Append( + EventSourceId.New(), + new OrderPlaced(42m), + eventStreamType: "fulfillment", + eventSourceType: "order", + tags: ["priority"]); +``` + +Projection definitions do not use `[FilterEventsByTag]`, `[EventSourceType]`, or `[EventStreamType]` as observer filters. Projections choose input through event types, joins, and event sequence selection. + +Use appended metadata when you need: + +- A reducer or reactor alongside the projection to observe only matching events +- Event context values inside projection mappings +- Consistent metadata across downstream observers that react to the same append operation + +`[Tag]` and `[Tags]` on a projection label the projection definition; they do not filter appended events. + +For reducer and reactor filtering examples, see `Documentation/events/filtering/`. diff --git a/plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md b/plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md new file mode 100644 index 0000000..cf78bc3 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-readmodel/references/queries.md @@ -0,0 +1,104 @@ +# Queries — Reference + +## Query endpoint patterns + +### Collection query + +```csharp +[HttpGet] +public IEnumerable AllAccounts() + => collection.Find(_ => true).ToList(); +``` + +### Single item query + +```csharp +[HttpGet("{id}")] +public AccountSummary? GetAccount(Guid id) + => collection.Find(a => a.Id == id).FirstOrDefault(); +``` + +### Filtered query (with proxy parameter) + +```csharp +[HttpGet("search")] +public IEnumerable Search([FromQuery] string? filter) + => collection.Find(a => a.Name.StartsWith(filter ?? string.Empty)).ToList(); +``` + +The `[FromQuery]` parameter is included in the generated TypeScript proxy. + +### Observable (real-time) query + +Return `ISubject` to push data to clients over WebSocket: + +```csharp +[HttpGet("live")] +public ISubject> AllAccountsLive() +{ + var observable = new ClientObservable>(); + observable.OnNext(collection.Find(_ => true).ToList()); + + var changeStream = collection.Watch(); + observable.ClientDisconnected += () => changeStream.Dispose(); + Task.Run(async () => + { + await foreach (var _ in changeStream.ToAsyncEnumerable()) + observable.OnNext(collection.Find(_ => true).ToList()); + }); + + return observable; +} +``` + +The proxy generator produces an `ObservableQuery` TypeScript class for `ISubject` return types. The React hook `useObservableQuery()` is used automatically. + +--- + +## QueryResult shape (frontend) + +```ts +interface QueryResultWithState { + data: T; + isSuccess: boolean; + isAuthorized: boolean; + isValid: boolean; + validationResults: ValidationResult[]; + hasExceptions: boolean; + exceptionMessages: string[]; + paging: { page: number; pageSize: number; totalItems: number; totalPages: number }; + + // React-specific: + hasData: boolean; // non-null and non-empty + isPerforming: boolean; // request in flight +} +``` + +--- + +## React usage + +```tsx +// Standard query — returns [result, requery] +const [accounts, refresh] = AllAccounts.use(); + +// With parameters +const [results] = Search.use({ filter: searchText }); + +// Observable query — returns [result] only (no manual refresh) +const [liveAccounts] = AllAccountsLive.use(); +``` + +For full page layouts with tables and menu actions, see the `cratis-react-page` skill. + +--- + +## Naming conventions + +The **method name** on the controller becomes the TypeScript proxy class name. Make it descriptive. + +| ✅ Good | ❌ Avoid | +| ------- | ------- | +| `AllAccounts` | `Get`, `GetAll`, `List` | +| `AccountsByOwner` | `Query`, `Fetch` | +| `AllAccountsLive` | `Observable`, `Live` | diff --git a/plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md b/plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md new file mode 100644 index 0000000..22e5237 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-readmodel/references/reducers.md @@ -0,0 +1,136 @@ +# Reducers — Reference + +## Signatures + +All public methods with a `[EventType]` record as the first parameter are treated as event handlers. All the following signatures are valid: + +```csharp +public TState Handle(TEvent @event, TState? current, EventContext context) +public TState Handle(TEvent @event, TState? current) +public Task Handle(TEvent @event, TState? current, EventContext context) +public Task Handle(TEvent @event, TState? current) +``` + +Method names are yours to choose — Chronicle matches by the event type parameter. + +--- + +## EventContext properties + +| Property | Type | Description | +| -------- | ---- | ----------- | +| `context.Occurred` | `DateTimeOffset` | When the event was appended | +| `context.EventSourceId` | `EventSourceId` | The aggregate root identifier | +| `context.SequenceNumber` | `EventSequenceNumber` | Position in the event sequence | +| `context.CorrelationId` | `CorrelationId` | Correlation ID for causality tracking | + +--- + +## Full example: shopping cart + +```csharp +public record CartItem(string Sku, int Quantity, decimal UnitPrice); +public record CartState(IReadOnlyList Items, decimal Total, bool IsCheckedOut); + +public class CartReducer : IReducerFor +{ + public CartState Created(CartCreated @event, CartState? current, EventContext context) + => new([], 0m, false); + + public CartState ItemAdded(CartItemAdded @event, CartState? current, EventContext context) + { + var items = (current?.Items ?? []).ToList(); + var existing = items.FirstOrDefault(i => i.Sku == @event.Sku); + if (existing is not null) + { + items.Remove(existing); + items.Add(existing with { Quantity = existing.Quantity + @event.Quantity }); + } + else + { + items.Add(new CartItem(@event.Sku, @event.Quantity, @event.UnitPrice)); + } + var total = items.Sum(i => i.Quantity * i.UnitPrice); + return new CartState(items, total, false); + } + + public CartState ItemRemoved(CartItemRemoved @event, CartState? current, EventContext context) + { + var items = (current?.Items ?? []).Where(i => i.Sku != @event.Sku).ToList(); + return new CartState(items, items.Sum(i => i.Quantity * i.UnitPrice), false); + } + + public CartState CheckedOut(CartCheckedOut @event, CartState? current, EventContext context) + => (current ?? new([], 0m, false)) with { IsCheckedOut = true }; +} +``` + +--- + +## Passive reducers + +A passive reducer is not an active observer — it computes state on demand. Useful for previews or draft calculations: + +```csharp +[Passive] +public class DraftOrderReducer : IReducerFor { ... } +``` + +Call explicitly rather than subscribing automatically: + +```csharp +var state = await readModels.GetOne(orderId); +``` + +--- + +## Reading reducer state + +```csharp +// Single instance +var cart = await readModels.GetOne(cartId); + +// All instances +var allCarts = await readModels.GetAll(); +``` + +--- + +## Filtering reducers by appended event metadata + +Use reducer filters when the reducer should only observe a subset of appended events: + +```csharp +using Cratis.Chronicle; +using Cratis.Chronicle.Events; +using Cratis.Chronicle.Reducers; + +[FilterEventsByTag("priority")] +[EventSourceType("order")] +[EventStreamType("fulfillment")] +public class PriorityOrderReducer : IReducerFor +{ + public PriorityOrderState Ordered(OrderPlaced @event, PriorityOrderState? current, EventContext context) => + new((current?.Count ?? 0) + 1); +} + +public record PriorityOrderState(int Count); +``` + +Match the reducer filters when you append: + +```csharp +await eventLog.Append( + EventSourceId.New(), + new OrderPlaced(42m), + eventStreamType: "fulfillment", + eventSourceType: "order", + tags: ["priority"]); +``` + +- `[FilterEventsByTag]` matches any appended or static event tag +- `[EventSourceType]` matches the appended `eventSourceType` +- `[EventStreamType]` matches the appended `eventStreamType` +- `[Tag]` and `[Tags]` label the reducer; they do not filter events + +For fuller guidance, see `Documentation/events/filtering/`. diff --git a/plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md b/plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md new file mode 100644 index 0000000..8b9499f --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-specs-csharp/SKILL.md @@ -0,0 +1,206 @@ +--- +name: cratis-specs-csharp +description: Step-by-step guidance for writing C# specs in Cratis with BDD Specification by Example — the Establish/Because/should_ pattern, for_/when_/and_ folder hierarchy, reusable given/ contexts, NSubstitute mocking, and the in-process scenario family. Use when writing C# unit or integration specs or structuring the for_/when_/and_ hierarchy. For specs tied to a specific vertical-slice command, write-specs is the focused workflow. +--- + +## Core philosophy + +Specs are **executable documentation** — the folder tree reads like a spec sheet. Favor readability over DRY. Each spec file has: +- One action under test (`Because`) +- One setup (`Establish`) +- One or more focused assertions (`should_*`) + +--- + +## Step 1 — Choose the spec type + +Lead with the in-process **scenario family** (fast, infrastructure-free — the default for slice behavior); reserve out-of-process Chronicle integration specs for host/transport boundaries they can't reach. **Every spec file is wrapped in `#if DEBUG … #endif`.** Full reference: the universal base in [specs.csharp.md](../../rules/specs.csharp.md) and the application `*Scenario` family in [specs.scenarios.csharp.md](../../rules/specs.scenarios.csharp.md). + +| Scenario | Spec type | +| --- | --- | +| State Change slice (command → events) | `CommandScenario` — runs validators + `Provide()` + `Handle()` + appended events | +| State View slice (projection / reducer) | `ReadModelScenario` | +| Constraints / raw append & concurrency semantics | `EventScenario` | +| Automation / Translation (reactor) | `ReactorScenario` | +| Isolated unit logic (no I/O) | Unit spec in `for_/` | +| Host / transport / real-infra boundary (advanced) | out-of-process Chronicle integration spec | +| Complex setup shared across many specs | Reusable context in `given/` | + +--- + +## Step 2 — Create the folder structure + +``` +for_/ +├── given/ +│ ├── all_dependencies.cs ← mocks all deps, inherits Specification +│ └── a_.cs ← creates SUT, inherits all_dependencies +├── when_/ ← behavior with multiple outcomes +│ ├── and_.cs +│ └── with_.cs +└── when_.cs ← single outcome = single file +``` + +Folder/file names read as English sentences: +- `for_Changeset / when_adding_changes / and_there_are_differences` +- `for_AuthorService / when_registering / and_name_already_exists` + +--- + +## Step 3 — Write a spec + +```csharp +// for_KeyHelper/when_combining_parts.cs +namespace MyApp.for_KeyHelper; + +public class when_combining_parts : Specification +{ + object[] _parts; + string _result; + + void Establish() => _parts = ["First", "Second", "Third"]; + + void Because() => _result = KeyHelper.Combine(_parts); + + [Fact] void should_combine_all_parts() => _result.ShouldEqual("First+Second+Third"); + [Fact] void should_not_be_empty() => _result.ShouldNotBeEmpty(); +} +``` + +Rules: +- Inherit `Specification` (from `Cratis.Specifications`) +- `void Establish()` — setup before the action +- `void Because()` — **one** action under test (the thing being specified) +- `[Fact] void should_*()` — one assertion per fact, no blank lines between them +- All fields: `private` (or `protected` in `given/` contexts), `_camelCase` +- All methods can be `async Task` when needed + +--- + +## Step 4 — Add a reusable context (`given/`) + +When multiple specs share the same setup, extract it into a `given/` class: + +```csharp +// for_AuthorService/given/all_dependencies.cs +namespace MyApp.Authors.for_AuthorService.given; + +public class all_dependencies : Specification +{ + protected IEventLog _eventLog; + protected ILogger _logger; + + void Establish() + { + _eventLog = Substitute.For(); + _logger = Substitute.For>(); + } +} +``` + +```csharp +// for_AuthorService/given/an_author_service.cs +namespace MyApp.Authors.for_AuthorService.given; + +public class an_author_service : all_dependencies +{ + protected AuthorService _service; + + void Establish() => _service = new(_eventLog, _logger); +} +``` + +```csharp +// for_AuthorService/when_registering/and_name_is_valid.cs +namespace MyApp.Authors.for_AuthorService.when_registering; + +public class and_name_is_valid : given.an_author_service +{ + void Because() => _service.Register(new AuthorName("John")); + + [Fact] void should_append_event() => + _eventLog.Received(1).Append(Arg.Any(), Arg.Any()); +} +``` + +See `references/csharp-patterns.md` for full NSubstitute patterns and assertion methods. + +--- + +## Step 5 — In-process scenario specs (the default) + +For slice behavior, use the scenario family — it runs the real Arc/Chronicle pipeline in-process. Wrap every file in `#if DEBUG`. + +```csharp +// Authors/Registration/when_registering_an_author/and_all_information_is_valid.cs +#if DEBUG +namespace MyApp.Authors.Registration.when_registering_an_author; + +public class and_all_information_is_valid : Specification +{ + readonly CommandScenario _scenario = new(); + readonly AuthorId _id = AuthorId.New(); + CommandResult _result; + + async Task Because() => _result = await _scenario.Execute(new RegisterAuthor(_id, new AuthorName("Jane Austen"))); + + [Fact] void should_succeed() => _result.ShouldBeSuccessful(); + [Fact] async Task should_have_appended_registered_event() => + await _scenario.ShouldHaveAppendedEvent(_id, e => e.Name == "Jane Austen"); +} +#endif +``` + +`CommandScenario` exposes only `Services`, `Context`, `Execute`, and `Validate` — event assertions are the extension methods `await _scenario.ShouldHaveAppendedEvent(eventSourceId[, predicate])` and `ShouldHaveTailSequenceNumber(...)`. Unhappy-path specs assert **both** `ShouldNotBeSuccessful()` and `ShouldHaveValidationErrors()` (authorization uses `ShouldNotBeAuthorized()`). Seed DCB read-model state by registering it into `_scenario.Services` (substitute `IReadModels`/`GetInstanceById`, or `AddReadModels(...)`) — there is no `Given`/`Events` on a command scenario. See [specs.scenarios.csharp.md](../../rules/specs.scenarios.csharp.md) for `EventScenario`, `ReadModelScenario`, and `ReactorScenario` (which *do* use `Given.ForEventSource(...).Events(...)`). + +## Step 6 — Out-of-process Chronicle integration spec (advanced) + +Reserve this for the host/transport boundary the scenario helpers can't reach. Integration specs live directly inside the slice's `when_/` folder and test the full stack against a real Chronicle event store. + +```csharp +// Authors/Registration/when_registering/and_there_are_no_authors.cs +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(new AuthorName("John Doe"))); + } + + [Fact] void should_be_successful() => Context.Result!.IsSuccess.ShouldBeTrue(); + [Fact] void should_have_appended_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")); +} +``` + +See `references/integration-specs.md` for the full integration spec guide. + +--- + +## What NOT to spec + +- Simple auto-properties (`public AuthorId Id { get; }`) +- Properties returning constructor parameters +- Simple delegation (`public IEnumerable All => _list;`) +- Logging calls +- Trivial null checks + +--- + +## Reference files + +- `references/csharp-patterns.md` — BDD pattern detail, NSubstitute, assertions, exception catching +- `references/integration-specs.md` — Chronicle integration spec structure, helpers, Given diff --git a/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md b/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md new file mode 100644 index 0000000..db9a574 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/csharp-patterns.md @@ -0,0 +1,166 @@ +# C# Spec Patterns — Reference + +## BDD phases + +| Method | Purpose | Notes | +| --- | --- | --- | +| `void Establish()` | Setup — runs before `Because()` | Base-class `Establish` runs first, then derived class | +| `void Because()` | The single action under test | Only in concrete spec files — never in `given/` contexts | +| `[Fact] void should_*()` | One assertion per fact | No blank lines between `should_` methods | +| `void Destroy()` | Teardown after each test | Optional | + +All phases can be `async Task`. + +--- + +## Minimal spec + +```csharp +namespace MyApp.for_KeyHelper; + +public class when_combining_parts : Specification +{ + object[] _parts; + string _result; + + void Establish() => _parts = ["First", "Second", "Third"]; + void Because() => _result = KeyHelper.Combine(_parts); + + [Fact] void should_combine_all_parts() => _result.ShouldEqual("First+Second+Third"); + [Fact] void should_not_be_empty() => _result.ShouldNotBeEmpty(); +} +``` + +--- + +## Reusable context (layered given) + +```csharp +// given/all_dependencies.cs — mock all external deps +public class all_dependencies : Specification +{ + protected IEventStore _eventStore; + protected IReactorInvoker _reactorInvoker; + + void Establish() + { + _eventStore = Substitute.For(); + _reactorInvoker = Substitute.For(); + } +} + +// given/a_reactor_handler.cs — build system under test +public class a_reactor_handler : all_dependencies +{ + protected ReactorHandler _handler; + + void Establish() => _handler = new(_eventStore, _reactorInvoker); +} + +// when_handling/and_event_is_received.cs — concrete spec +public class and_event_is_received : given.a_reactor_handler +{ + void Because() => _handler.Handle(new SomeEvent()); + + [Fact] void should_invoke_reactor() => + _reactorInvoker.Received(1).Invoke(Arg.Any()); +} +``` + +--- + +## NSubstitute patterns + +```csharp +// Create substitutes +_service = Substitute.For(); + +// Return values +_service.GetValue(Arg.Any()).Returns("result"); +_service.GetAsync(Arg.Any()).Returns(Task.FromResult(42)); + +// Argument matchers +Arg.Is(r => r.Id == expectedId && r.Name == expectedName) + +// Verify calls +_service.Received(1).DoSomething(Arg.Any()); +_service.DidNotReceive().DoSomethingElse(); + +// Capture arguments +_service.When(s => s.Process(Arg.Any>())) + .Do(info => _captured = info.Arg>()); + +// Throw from substitute +_handler.Handle(Arg.Any()).Throws(new MyException("fail")); +``` + +--- + +## Assertion extension methods (Cratis.Specifications) + +| Method | Example | +| --- | --- | +| `.ShouldEqual(expected)` | `_result.ShouldEqual(42)` | +| `.ShouldBeTrue()` | `_flag.ShouldBeTrue()` | +| `.ShouldBeFalse()` | `_flag.ShouldBeFalse()` | +| `.ShouldBeNull()` | `_error.ShouldBeNull()` | +| `.ShouldNotBeNull()` | `_value.ShouldNotBeNull()` | +| `.ShouldBeEmpty()` | `_list.ShouldBeEmpty()` | +| `.ShouldNotBeEmpty()` | `_list.ShouldNotBeEmpty()` | +| `.ShouldContain(item)` | `_list.ShouldContain(expected)` | +| `.ShouldNotContain(item)` | `_list.ShouldNotContain(excluded)` | +| `.ShouldContainOnly(items)` | `_list.ShouldContainOnly(expectedItems)` | +| `.ShouldBeOfExactType()` | `_obj.ShouldBeOfExactType()` | +| `.ShouldBeGreaterThan(n)` | `_count.ShouldBeGreaterThan(0)` | +| `.ShouldBeLessThan(n)` | `_count.ShouldBeLessThan(100)` | + +--- + +## Catching exceptions + +```csharp +Exception? _error; + +async Task Because() => _error = await Catch.Exception(_sut.DoSomethingThatThrows); + +[Fact] void should_throw() => _error.ShouldNotBeNull(); +[Fact] void should_not_throw() => _error.ShouldBeNull(); +[Fact] void should_throw_author_not_found() => _error.ShouldBeOfExactType(); +``` + +--- + +## Using statements + +Common usings are provided globally in `GlobalUsings.Specs.cs` — do **not** add them manually: +- `Xunit` +- `NSubstitute` +- `Cratis.Specifications` + +Do **not** add a `using` for the namespace of the system under test. + +--- + +## Multiple outcomes → folder + +``` +// Single outcome → single file +for_MyService/when_processing.cs + +// Multiple outcomes → folder + files +for_MyService/when_processing/ + and_input_is_valid.cs + and_input_is_null.cs + with_empty_collection.cs + without_required_field.cs +``` + +Allowed file name prefixes: `and_*`, `with_*`, `without_*`, `having_*`, `given_*` + +--- + +## Folder naming read as sentences + +`for_AuthorService / when_registering / and_name_already_exists` + +→ "For AuthorService, when registering, and name already exists, it should..." diff --git a/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md b/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md new file mode 100644 index 0000000..0f6ce1a --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-specs-csharp/references/integration-specs.md @@ -0,0 +1,101 @@ +# Chronicle Integration Specs — Reference + +Integration specs test a complete vertical slice end-to-end — from HTTP request through command handling, event appending, constraint checking, and projection — against a real Chronicle event store. If one passes, the entire stack works. + +They live under `when_/` **inside the slice folder** (not in a `for_/` unit folder — there's no isolated unit, the entire slice is under test). + +--- + +## Structure + +```csharp +// Authors/Registration/when_registering/and_there_are_no_authors.cs + +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(new AuthorName("John Doe"))); + } + + [Fact] void should_be_successful() => Context.Result!.IsSuccess.ShouldBeTrue(); + [Fact] void should_have_appended_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")); +} +``` + +--- + +## Spec with preconditions (seed the event store) + +Use `async Task Establish()` to append events before `Because()`: + +```csharp +public class context(ChronicleOutOfProcessFixture fixture) : given.an_http_client(fixture) +{ + public const string ExistingName = "John Doe"; + public CommandResult? Result; + + async Task Establish() => + await EventStore.EventLog.Append(AuthorId.New(), new AuthorRegistered(ExistingName)); + + async Task Because() => + Result = await Client.ExecuteCommand( + "/api/authors/register", + new RegisterAuthor(ExistingName)); +} + +[Fact] void should_not_be_successful() => Context.Result!.IsSuccess.ShouldBeFalse(); +[Fact] void should_not_have_appended_additional_events() => + Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First); +``` + +--- + +## ExecuteCommand overloads + +```csharp +// Command with no typed response (returns CommandResult) +Result = await Client.ExecuteCommand(url, command); + +// Command with typed response (returns CommandResult) +Result = await Client.ExecuteCommand(url, command); +``` + +--- + +## Integration assertion helpers + +| Helper | What it verifies | +| --- | --- | +| `Context.Result!.IsSuccess.ShouldBeTrue()` | Command succeeded | +| `Context.Result!.IsSuccess.ShouldBeFalse()` | Command failed (validation, constraint, etc.) | +| `Context.ShouldHaveTailSequenceNumber(EventSequenceNumber.First)` | Event log has exactly one event (sequence 0) | +| `Context.ShouldHaveTailSequenceNumber(n)` | Event log tail is at sequence `n` | +| `Context.ShouldHaveAppendedEvent(seq, eventSourceId, validator)` | Specific event was appended at sequence with correct values | + +--- + +## Key rules + +- `context` is an **inner public class** inheriting from `given.an_http_client(fixture)` +- Always add `using context = .context;` alias at the top +- `[Collection(ChronicleCollection.Name)]` on the outer class — required for test isolation +- `Establish` seeds preconditions; `Because` executes the command under test +- Declare `Result` as nullable, initialized to `null!` if needed for nullable analysis +- The outer class constructor receives `context` via xUnit constructor injection +- Never mix unit specs and integration specs in the same folder diff --git a/plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md b/plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md new file mode 100644 index 0000000..103317d --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-specs-typescript/SKILL.md @@ -0,0 +1,148 @@ +--- +name: cratis-specs-typescript +description: Step-by-step guidance for writing TypeScript specs in Cratis using BDD-style Specification by Example — the given()/describe/it pattern, for_/when_/ folder hierarchy, reusable context classes, Sinon mocking, and Chai assertions. Use whenever writing TypeScript specs or tests, creating spec files/folders, using the given() helper, mocking with sinon.createStubInstance or sinon.stub, asserting with Chai .should, or understanding how yarn test runs specs. +--- + +## Core philosophy + +Same BDD philosophy as C# specs — specs describe behaviors, not implementations. The `given()` helper + context class mirrors the C# `Specification` base class: setup is separated from the action, each `it()` verifies a single outcome. + +--- + +## Step 1 — Create the folder structure + +``` +for_/ +├── given/ +│ └── a_.ts ← reusable context class +├── when_/ ← behavior with multiple outcomes +│ ├── with_.ts +│ ├── without_.ts +│ └── and_.ts +└── when_.ts ← single outcome = single file +``` + +Example: +``` +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 + └── multiple_events_collection.ts +``` + +--- + +## Step 2 — Write a reusable context class + +```ts +// for_AuthorService/given/an_author_service.ts +import sinon from 'sinon'; +import { AuthorService } from '../../../AuthorService'; + +export class an_author_service { + eventLog: sinon.StubbedInstance; + service: AuthorService; + + constructor() { + this.eventLog = sinon.createStubInstance(EventLog); + this.service = new AuthorService(this.eventLog); + } +} +``` + +Properties are **public** (unlike C# protected fields) — tests access them via `context.propertyName`. + +--- + +## Step 3 — Write a spec using `given()` + +```ts +// for_AuthorService/when_registering/with_valid_name.ts +import { an_author_service } from '../given/an_author_service'; +import { given } from '../../given'; // import from package root + +describe('when registering with valid name', given(an_author_service, context => { + beforeEach(async () => { + await context.service.register('John Doe'); + }); + + it('should append an event', () => { + context.eventLog.append.calledOnce.should.be.true; + }); + + it('should pass the author name', () => { + const call = context.eventLog.append.firstCall; + call.args[1].name.should.equal('John Doe'); + }); +})); +``` + +--- + +## Step 4 — Simple spec (no shared context) + +For behaviors without shared setup: + +```ts +describe('when replacing route parameters', () => { + let result: { route: string; unusedParameters: object }; + + beforeEach(() => { + result = UrlHelpers.replaceRouteParameters('/api/items/{id}', { id: '123' }); + }); + + it('should replace the route parameter', () => { + result.route.should.equal('/api/items/123'); + }); + + it('should remove used parameters', () => { + Object.keys(result.unusedParameters).should.have.lengthOf(0); + }); +}); +``` + +--- + +## Naming conventions + +| Element | Convention | Example | +| --- | --- | --- | +| `describe()` text | Natural language sentence | `'when registering with valid name'` | +| `it()` text | Starts with "should", uses **spaces** | `'should append an event'` | +| Context class | `a_` or `an_` prefix | `an_author_service` | +| Spec file | Descriptive, `with_` / `without_` / `and_` | `with_valid_name.ts` | + +**Always use spaces in `it()` descriptions** — never underscores. + +--- + +## Behavior isolation rule + +Keep one primary behavior per spec file/folder. Do not mix orthogonal behaviors in one spec. + +- Good: separate folders for delta semantics: + - `when_items_are_added_as_delta/and_item_is_identified_by_a_guid.ts` + - `when_items_are_removed_as_delta/and_item_is_identified_by_a_guid.ts` +- Avoid: a single file that validates both add-delta and remove-delta behavior. + +This keeps failures precise and prevents unrelated behavior from becoming tangled in one spec. + +--- + +## Running specs + +```bash +yarn test # from the package root +``` + +--- + +## Reference files + +- `references/typescript-patterns.md` — Chai assertions, Sinon patterns, async specs, full examples diff --git a/plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md b/plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md new file mode 100644 index 0000000..74f0aae --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-specs-typescript/references/typescript-patterns.md @@ -0,0 +1,168 @@ +# TypeScript Spec Patterns — Reference + +## Frameworks + +| Framework | Role | +| --- | --- | +| [Vitest](https://vitest.dev/) | Test runner | +| [Mocha](https://mochajs.org/) | Test structure (`describe`, `it`, `beforeEach`) | +| [Chai](https://www.chaijs.com/) | Assertions — always `.should` fluent interface | +| [SinonJS](https://sinonjs.org/) | Mocking and stubbing | + +--- + +## Chai assertions — always use `.should` + +Never use `expect()`. The `.should` style reads as a natural English sentence. + +```ts +// Equality +value.should.equal(expected); +value.should.deep.equal({ id: 1, name: 'John' }); + +// Booleans +flag.should.be.true; +flag.should.be.false; + +// Null / undefined +value.should.be.null; +value.should.not.be.null; +value.should.be.undefined; +value.should.not.be.undefined; + +// Arrays +array.should.contain(item); +array.should.have.lengthOf(3); +array.should.be.empty; +array.should.not.be.empty; + +// Types +value.should.be.instanceOf(MyClass); + +// Throwing +(() => throwingFn()).should.throw(ErrorType); +``` + +--- + +## Sinon mocking + +```ts +import sinon from 'sinon'; + +// Stub an entire class (all methods become stubs) +const service = sinon.createStubInstance(ConcreteService); + +// Stub a global function +const fetchStub = sinon.stub(globalThis, 'fetch'); +fetchStub.resolves({ ok: true, json: async () => ({ data: 'value' }) }); + +// Configure return values +service.getValue.returns('result'); +service.getAsync.resolves(42); + +// Access call details +service.doSomething.calledOnce.should.be.true; +service.doSomething.calledWith('expected-arg').should.be.true; +service.doSomething.callCount.should.equal(2); + +const firstCall = service.doSomething.firstCall; +firstCall.args[0].should.equal('expected'); + +// Restore stubs after test +afterEach(() => sinon.restore()); +``` + +--- + +## given() helper — full pattern + +The `given()` function instantiates a context class, runs tests with it, and ensures setup is isolated per test. + +```ts +import { given } from '../../given'; // import from package root +import { a_my_service } from '../given/a_my_service'; + +describe('when doing something', given(a_my_service, context => { + let result: string; + + beforeEach(async () => { + result = await context.service.doSomething('input'); + }); + + it('should return expected result', () => { + result.should.equal('expected'); + }); + + it('should call dependency once', () => { + context.dependency.process.calledOnce.should.be.true; + }); +})); +``` + +--- + +## Reusable context class + +```ts +// given/a_my_service.ts +import sinon from 'sinon'; +import { MyService } from '../../../MyService'; + +export class a_my_service { + dependency: sinon.StubbedInstance; + service: MyService; + + constructor() { + this.dependency = sinon.createStubInstance(DependencyClass); + // configure defaults: + this.dependency.getValue.returns('default'); + this.service = new MyService(this.dependency as unknown as IDependency); + } +} +``` + +Properties are **public** — accessed via `context.propertyName` in specs. + +--- + +## Multiple outcomes — folder pattern + +``` +when_processing/ +├── with_valid_input.ts → happy path +├── with_empty_input.ts → edge case +└── without_required_field.ts → failure path +``` + +Each file has its own `describe()` block, its own `beforeEach`, and its own `it()` assertions. + +--- + +## Async specs + +`beforeEach`, `afterEach`, and `it` can all be `async`: + +```ts +describe('when loading data', given(a_loader, context => { + let result: Data[]; + + beforeEach(async () => { + result = await context.loader.load('source'); + }); + + it('should return items', () => { + result.should.have.lengthOf(3); + }); +})); +``` + +--- + +## What NOT to spec + +Same as C#: +- Simple property getters and setters +- Properties that return constructor parameters directly +- Trivial delegation +- Don't write specs whose `describe` starts with "when getting" or "when returning" — these are almost always testing getters, not behavior diff --git a/plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md b/plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md new file mode 100644 index 0000000..6e97a25 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-vertical-slice/SKILL.md @@ -0,0 +1,207 @@ +--- +name: cratis-vertical-slice +description: Explains how vertical feature slices are structured in a Cratis Chronicle + Arc application — folder layout, the single backend .cs file, the four slice types (State Change/View/Automation/Translation), and how features compose slices. Use when asking how slices work, where files go, or which slice type to choose. To actually build a new slice end-to-end, use new-vertical-slice instead. +--- + +## Core principle + +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, and the specs. Everything lives together because everything changes together. + +One feature folder → many slices. +One slice folder → one `.cs` file (all backend) + one `.tsx` file (frontend). + +--- + +## Step 1 — Identify the feature and slice type + +First, name the feature (a domain noun, pluralized) and identify the slice type: + +| Slice type | What it does | Key artifacts | +| --- | --- | --- | +| **State Change** | Mutates system state | Command + events + validators/constraints | +| **State View** | Projects events into queryable data | Read model + projection + queries | +| **Automation** | Reacts to events, makes decisions | Reactor + optional local read models | +| **Translation** | Adapts events between slices | Reactor → triggers commands in own slice | + +--- + +## Step 2 — Create the folder structure + +The feature folder lives directly under the app source root (or under an optional `/` grouping) — there is **no** top-level `Features/` wrapper. + +``` +/ ← feature root (pluralized domain noun) +├── .tsx ← composition page +├── .cs ← shared ConceptAs / EventSourceId types for the feature +└── / ← slice (action or view name) — the invariant unit + ├── .cs ← ALL backend artifacts in ONE file + ├── .tsx ← React component + └── when_/ ← specs + └── and_.cs +``` + +✅ Correct: +``` +Authors/ +├── Authors.tsx +├── AuthorId.cs +├── AuthorName.cs +├── Registration/ +│ ├── Registration.cs ← command + event + constraint + validator +│ ├── AddAuthor.tsx +│ └── when_registering/ +│ └── and_there_are_no_authors.cs +└── Listing/ + ├── Listing.cs ← read model + projection + query + └── Listing.tsx +``` + +❌ Wrong — never split by artifact type: +``` +Authors/ +├── Commands/RegisterAuthor.cs +├── Handlers/RegisterAuthorHandler.cs +├── Events/AuthorRegistered.cs +``` + +**Namespace rule**: the namespace mirrors the folder path under the source root — there is no `Features` segment. +`MyApp.Authors.Registration` (or `MyApp..Authors.Registration` when a module groups the feature). + +--- + +## Step 3 — Write the backend slice file + +All backend artifacts for one slice go in a single `.cs` file. 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. +``` + +For a **State Change** slice, the file contains: +```csharp +[EventType] +public record AuthorRegistered(AuthorName Name); + +public class UniqueAuthorName : IConstraint { ... } + +public class RegisterAuthorValidator : CommandValidator { ... } + +[Command] +public record RegisterAuthor(AuthorName Name) +{ + public (AuthorId, AuthorRegistered) Handle() + { + var authorId = AuthorId.New(); + return (authorId, new(Name)); + } +} +``` + +For a **State View** slice, the file contains: +```csharp +[ReadModel] +[FromEvent] +public record Author( + [Key] AuthorId Id, + AuthorName Name) +{ + public static ISubject> AllAuthors(IMongoCollection collection) => + collection.Observe(); +} +``` + +See `references/slice-anatomy.md` for all artifact patterns. + +--- + +## Step 4 — Define domain concepts + +For every domain value create a concept — one file per concept, in the feature folder (or `Common/` if shared across features). **Identity** concepts derive from `EventSourceId`; **value** concepts from `ConceptAs`. + +```csharp +public record AuthorId(Guid Value) : EventSourceId(Value) +{ + public static readonly AuthorId NotSet = new(Guid.Empty); + public static AuthorId New() => new(Guid.NewGuid()); + public static implicit operator AuthorId(Guid value) => new(value); +} +``` + +`EventSourceId` already supplies the conversions to `Guid`, `EventSourceId`, and `string` — don't redeclare them. + +See `references/concepts.md` for all concept patterns. + +--- + +## Step 5 — Build to generate TypeScript proxies + +```bash +dotnet build +``` + +This generates `.ts` proxy files in the configured ``. The frontend cannot be written until this succeeds — the proxies are the contract. + +--- + +## Step 6 — Write the React component + +```tsx +// Listing.tsx +import { AllAuthors } from '../proxies/Listing'; // auto-generated + +export const Listing = () => { + const [result] = AllAuthors.use(); + return ( + + + + ); +}; +``` + +--- + +## Step 7 — Compose the feature page + +The feature's `.tsx` assembles slices into a page: + +```tsx +// Authors.tsx +import { AddAuthor } from './Registration/AddAuthor'; +import { Listing } from './Listing/Listing'; +import { useDialog } from '@cratis/arc.react/dialogs'; + +export const Authors = () => { + const [AddAuthorDialog, showAddAuthorDialog] = useDialog(AddAuthor); + const menuItems = [{ label: 'Add Author', command: () => showAddAuthorDialog() }]; + return ( + + + + + + ); +}; +``` + +--- + +## Development workflow order + +Work in this exact sequence — TypeScript proxies are generated from C# during `dotnet build`: + +1. Implement the C# slice file (step 3) +2. Write integration specs for state-change slices +3. `dotnet build` — generates TypeScript proxies (step 5) +4. Implement React component(s) (step 6) +5. Register in the feature composition page (step 7) +6. Add/update routes if needed + +--- + +## Reference files + +- `references/slice-anatomy.md` — complete patterns for every artifact type +- `references/slice-types.md` — when to use each slice type with decision guide +- `references/concepts.md` — ConceptAs patterns for all primitive backing types diff --git a/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md new file mode 100644 index 0000000..f138808 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/concepts.md @@ -0,0 +1,101 @@ +# Concepts — Reference + +## What is a Concept? + +A `ConceptAs` wraps a primitive (`Guid`, `string`, `int`, etc.) in a named domain type. The compiler enforces that you cannot pass a `UserId` where an `AuthorId` was expected — both are `Guid` underneath, but they are distinct types. + +**Never use raw primitives in domain models, commands, events, or queries.** + +--- + +## Full canonical pattern — identity (event-source id) + +An **identity** concept (the event-source id of an entity) derives from **`EventSourceId`**, not `ConceptAs`. The base already supplies the conversions to/from `T`, to/from the untyped `EventSourceId`, and to `string`, so Chronicle resolves the key automatically — never hand-write an `EventSourceId` operator. + +```csharp +public record AuthorId(Guid Value) : EventSourceId(Value) +{ + public static readonly AuthorId NotSet = new(Guid.Empty); + + public static AuthorId New() => new(Guid.NewGuid()); + public static implicit operator AuthorId(Guid value) => new(value); +} +``` + +Use `ConceptAs` only for **value** concepts (names, amounts, codes) — see below. + +--- + +## String value concept + +```csharp +public record AuthorName(string Value) : ConceptAs(Value) +{ + public static readonly AuthorName NotSet = new(string.Empty); + + public static implicit operator string(AuthorName name) => name.Value; + public static implicit operator AuthorName(string value) => new(value); +} +``` + +--- + +## Integer value concept + +```csharp +public record PageNumber(int Value) : ConceptAs(Value) +{ + public static readonly PageNumber NotSet = new(0); + + public static implicit operator int(PageNumber p) => p.Value; + public static implicit operator PageNumber(int value) => new(value); +} +``` + +--- + +## Rules + +| Rule | Detail | +| --- | --- | +| Inherit as `record` | Gives value equality and immutability for free | +| `ConceptAs` provides `T → Concept` implicitly | You only need to add the `Concept → T` direction | +| Always add `NotSet` sentinel | Use `Guid.Empty`, `string.Empty`, or `0` — no `null` | +| Add `New()` on Guid identity types | Reads better than `new AuthorId(Guid.NewGuid())` | +| Add `EventSourceId` conversion on identity keys | Enables Chronicle to auto-resolve the event source | +| One concept per file | Named after the concept, e.g. `AuthorId.cs` | + +--- + +## File placement + +| Scope | Location | +| --- | --- | +| Used only within one slice | Inside the slice folder | +| Shared between slices of a feature | Feature root folder (`Authors/AuthorId.cs`) | +| Shared between features | `Common/` (`Common/TenantId.cs`) | + +Never create a standalone `Concepts/` folder — concepts belong near the code that uses them. + +--- + +## In commands and events + +```csharp +// Event uses concepts +[EventType] +public record AuthorRegistered(AuthorName Name); + +// Command uses concepts +[Command] +public record RegisterAuthor(AuthorName Name) +{ + public (AuthorId, AuthorRegistered) Handle() => + (AuthorId.New(), new(Name)); +} + +// Read model uses concepts +[ReadModel] +[FromEvent] +public record Author([Key] AuthorId Id, AuthorName Name); +``` diff --git a/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md new file mode 100644 index 0000000..dc0f647 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-anatomy.md @@ -0,0 +1,255 @@ +# Slice Anatomy — Reference + +All backend artifacts for a slice go in a single `.cs` file. Below are complete patterns for every artifact type. + +--- + +## File header + +Every `.cs` file starts with: + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. +``` + +File-scoped namespace (no extra indentation): + +```csharp +namespace MyApp.Authors.Registration; +``` + +--- + +## Events + +```csharp +[EventType] +public record AuthorRegistered(AuthorName Name); +``` + +Rules: +- `[EventType]` takes **no arguments** — the type name is the identifier +- Past tense: `ItemAddedToCart`, `UserRegistered`, `AddressChangedForPerson` +- No nullable properties — ambiguous events need a second event +- One purpose per event + +--- + +## Commands (model-bound) + +```csharp +[Command] +public record RegisterAuthor(AuthorName Name) +{ + public (AuthorId, AuthorRegistered) Handle() + { + var authorId = AuthorId.New(); + return (authorId, new(Name)); + } +} +``` + +`Handle()` return types: + +| Return | When | +| --- | --- | +| `TEvent Handle()` | Single event, no client result needed | +| `(TResult, TEvent) Handle()` | Return a value to the client + append one event | +| `Result Handle()` | Business rule success/failure path | +| `void Handle()` | Side-effect only, no event | + +Event source resolution (in priority order): +1. Parameter marked with `[Key]` +2. Parameter whose type has implicit conversion to `EventSourceId` +3. Implement `ICanProvideEventSourceId` + +DI in `Handle()`: extra parameters beyond command + read model are resolved from DI automatically. + +--- + +## Business rules via DCB (Dynamic Consistency Boundary) + +Accept a read model parameter in `Handle()` — the framework injects the current projected state: + +```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); + } +} +``` + +--- + +## Validators + +```csharp +public class RegisterAuthorValidator : CommandValidator +{ + public RegisterAuthorValidator() + { + RuleFor(c => c.Name).NotEmpty().WithMessage("Author name is required"); + } +} +``` + +With DI dependencies: + +```csharp +public class MyValidator : CommandValidator +{ + public MyValidator(IMyService service) + { + RuleFor(x => x).MustAsync(async (cmd, ct) => await service.IsValid(cmd)); + } +} +``` + +--- + +## Constraints + +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 (one-per-stream): + +```csharp +public class UniqueUser : IConstraint +{ + public void Define(IConstraintBuilder builder) => + builder.Unique(); +} +``` + +--- + +## Read models (model-bound, preferred) + +```csharp +[ReadModel] +[FromEvent] +public record Author( + [Key] AuthorId Id, + AuthorName Name) +{ + public static ISubject> AllAuthors(IMongoCollection collection) => + collection.Observe(); + + public static Author? ById(IMongoCollection collection, AuthorId id) => + collection.Find(a => a.Id == id).FirstOrDefault(); +} +``` + +Model-bound projection attributes: + +| Attribute | Purpose | +| --- | --- | +| `[Key]` | Read model primary key | +| `[FromEvent]` | Auto-map from event (class-level) | +| `[SetFrom]` | Explicit property mapping | +| `[AddFrom]` / `[SubtractFrom]` | Arithmetic | +| `[Increment]` / `[Decrement]` | ±1 counters | +| `[Count]` | Absolute count | +| `[ChildrenFrom]` | Child collection from event | +| `[Join]` | Join from another event stream | +| `[RemovedWith]` | Remove entry when event occurs | +| `[Passive]` | On-demand only, not actively observed | + +--- + +## Fluent projections (for complex cases) + +```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(); +} +``` + +AutoMap is on by default — just call `.From<>()` directly. Use `.NoAutoMap()` then explicit `.Set()` calls when you need selective mapping. + +**Projections join EVENTS, never read models.** + +--- + +## Reducers + +```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); + } +} +``` + +- `current` is `null` for the first event — always handle initialization +- Keep reducers pure — no side effects, no I/O +- Use `with` expressions on records for state updates + +--- + +## Reactors + +```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 by first-parameter event type +- Method name is descriptive; `EventContext` parameter is optional +- `[OnceOnly]` — skips method during event replay + +--- + +## Integration specs + +Live under `when_/` inside the slice folder: + +```csharp +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(); +} +``` diff --git a/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md new file mode 100644 index 0000000..5e86cea --- /dev/null +++ b/plugins/public-cratis-ai/skills/cratis-vertical-slice/references/slice-types.md @@ -0,0 +1,105 @@ +# Slice Types — Reference + +## The four slice types + +| Type | When to use | Key artifacts | +| --- | --- | --- | +| **State Change** | User action that mutates system state | `[Command]` + `[EventType]` + optional validator/constraint | +| **State View** | Projecting events into queryable read models | `[ReadModel]` + projection/reducer + query methods | +| **Automation** | Reacting to events to make decisions or call external APIs | `IReactor` + optional local read models | +| **Translation** | Adapting events between slices or bounded contexts | `IReactor` → `ICommandPipeline.Execute()` | + +Most features are built from a **State Change slice** paired with a **State View slice**. + +--- + +## State Change slice + +**When**: An action happens (user submits a form, a timer fires, an API is called) that changes state. + +**Contains**: +- `[EventType]` records — the facts that occurred +- `[Command]` record with `Handle()` — validates intent and produces events +- `CommandValidator` — input validation (FluentValidation, exported to TypeScript) +- `IConstraint` — server-side business rules enforced at event-append time + +**Example**: `Authors/Registration/Registration.cs` + +``` +Registration.cs +├── AuthorRegistered (event) +├── UniqueAuthorName (constraint) +├── RegisterAuthorValidator (validator) +└── RegisterAuthor (command with Handle()) +``` + +--- + +## State View slice + +**When**: Data needs to be queried and displayed. Projects the event stream into a read model. + +**Prefer model-bound** (`[ReadModel]` + attribute-based projection) over fluent `IProjectionFor` unless the mapping is too complex for attributes. + +**Contains**: +- `[ReadModel]` record — the query-optimized data shape +- Projection attributes (`[FromEvent]`, `[SetFrom]`, etc.) or `IProjectionFor` +- Static query methods on the record (DI parameters auto-resolved) + +**Example**: `Authors/Listing/Listing.cs` + +``` +Listing.cs +├── Author (read model record) +├── [FromEvent] (projection via attribute) +└── AllAuthors() static query method +``` + +--- + +## Automation slice + +**When**: An event should trigger a side effect automatically — sending an email, calling an external API, making a decision. + +**Contains**: +- `IReactor` implementation — dispatches on event type by first parameter +- Optional local read models for decision state + +```csharp +public class WelcomeEmailSender(IEmailService email) : IReactor +{ + public async Task OnAuthorRegistered(AuthorRegistered @event, EventContext ctx) => + await email.SendWelcome(@event.Name); +} +``` + +--- + +## Translation slice + +**When**: An event from one slice should trigger a command in another slice (event-driven integration). Keeps slices decoupled — neither knows about the other directly. + +**Contains**: +- `IReactor` that listens to source events +- `ICommandPipeline.Execute()` to trigger a command in another slice + +```csharp +public class StockKeeping(ICommandPipeline commandPipeline) : IReactor +{ + public async Task HandleBookReserved(BookReserved @event) => + await commandPipeline.Execute(new DecreaseStock(@event.Isbn)); +} +``` + +--- + +## Decision guide + +``` +User action → State Change slice +Data display → State View slice +Automatic side effect → Automation slice +Cross-slice event reaction → Translation slice +``` + +A typical feature has at least one State Change + one State View. They are completely separate `.cs` files in separate sub-folders. They share events through the feature's namespace — the State View projection references the event type defined in the State Change file. diff --git a/plugins/public-cratis-ai/skills/create-event-model/SKILL.md b/plugins/public-cratis-ai/skills/create-event-model/SKILL.md new file mode 100644 index 0000000..1dd669d --- /dev/null +++ b/plugins/public-cratis-ai/skills/create-event-model/SKILL.md @@ -0,0 +1,80 @@ +--- +name: create-event-model +description: Create and maintain Mermaid eventmodeling diagrams (EventModel.md) for a Cratis module or feature. Use when adding, renaming, moving, or deleting modules/features/slices, commands, events, read models, automations, translations, or cross-module event flows — to keep the diagram in sync with the code. +--- + +# Create Event Model Diagram + +Use this skill when adding a module/feature, or when any slice within it is added, renamed, or removed — the `EventModel.md` for the affected area is updated in the same change. If the event vocabulary/stream boundaries aren't decided yet, use `event-modeling` first; this skill renders an already-chosen model. + +Diagrams use **Mermaid's native `eventmodeling`** diagram type (v11.15+). The full grammar is at ; that page wins when in doubt. + +## What an event model is + +It arranges a module's commands, events, read models, and automations on a left-to-right business-flow timeline — answering *"what happens in this module, and in what order?"* One file per module (or feature), in a fenced ` ```mermaid ` block, alongside the code: `/EventModel.md`; a system-overview at the source root shows only cross-module flows. + +## Mermaid eventmodeling cheat sheet + +**Frame prefix:** `tf` (timeframe — auto-connects to the previous frame) · `rf` (resetframe — breaks the chain; start each independent flow with it). `rf` *replaces* `tf`; `tf N rf Name` is invalid. + +**Frame:** ` `. Number is unique (order of declaration doesn't matter — frames position by reference). Type is one of: + +| Type | Swimlane | Represents | +|---|---|---| +| `ui` | UI / Automation | the persona interacting (persona name only — not a screen name) | +| `pcr` | UI / Automation | a reactor / automation processor | +| `cmd` | Command / Read Model | a `[Command]` record | +| `rmo` | Command / Read Model | a `[ReadModel]` record | +| `evt` | Events | an `[EventType]` record — use the exact, self-describing C# name | + +**Multiple sources (`->>`):** a read model or fan-in reactor fed by several frames references them by **frame number**: `tf 10 rmo Profile ->> 03 ->> 06 ->> 09`. **Namespaces:** a `Module.` prefix (`tf 04 pcr Billing.CreateInvoice`) creates a sub-swimlane — use it for cross-module entities and in the system overview. **Comments:** `%% ── Section ──` (don't use a frame as a section header). + +## Slice type → pattern + +``` +%% ── State Change: Register ────────────── +rf 01 ui +tf 02 cmd Register +tf 03 evt Registered + +%% ── State View: (consumed by ) ── +rf 04 rmo ->> 03 +tf 05 ui + +%% ── Automation: (side-effect only) ── +rf 06 evt Registered +tf 07 pcr %% calls an external service; emits no event + +%% ── Translation: Source.Event -> Target.Reactor ── +rf 08 evt Source.SomethingHappened +tf 09 pcr Target.Reactor +tf 10 evt Target.SomethingElseHappened +``` + +- A State View's `rmo` references the event frames it projects from by number; its consumer-UI frame auto-chains after it. A `[Passive]` read model has no consumer UI — emit only the `rmo … ->>` line with a `%% passive` comment. +- Translation slices are reactor-only (`evt → pcr → evt`, no intermediate `cmd`). If you draw a `cmd` between `pcr` and the result event, it's an **Automation**, not a Translation — reclassify. +- Multiple consumers of one read model: declare each consumer UI as its own `rf` frame with an explicit `->>` back to the `rmo`. + +## Command rules table + +Mermaid eventmodeling has no shape for validation/guards. After the diagram, add a `## Command rules` section: a table with `Command`, `Rules`, `Emits / result`, summarizing validator/`ConceptValidator`/`Provide()`/DCB/authorization rules and no-op/diff behavior in human language. Include commands that emit no event (e.g. response-only parser commands). + +## Process + +1. **Discover** the slices: scan the module for `[Command]` (State Change), `[ReadModel]` without `Handle()` (State View), `IReactor` + `ICommandPipeline` (Automation), `IReactor` returning events / `IEventLog` (Translation). Use exact C# type names. +2. **Order** frames by domain causality (what must happen before what); State Views after the events they project; `rf` only between independent flows, not between sibling events of one flow (`rf`-per-event makes a tall tower). +3. **Write** the diagram + the `## Command rules` table. +4. **Verify** it renders without a syntax-error banner, then reconcile against the source: every slice `.cs` appears, classified by the marker it actually contains. A clean render proves valid Mermaid, not completeness — close gaps against the code, not from memory. + +## Common mistakes + +- **Multi-event State Change:** a command emitting several events chains them with consecutive `tf … evt …` — don't fight the auto-chain with `rf` (one event per row → tall tower). +- **Same event from multiple commands:** each emit-point gets its own `tf evt` frame; don't merge them into one. +- **Multiple consumers of one read model:** the first consumer auto-chains; each additional consumer references the read model explicitly (`rf ui ->> `). +- **Section headers** are Mermaid comments (`%%`), not frames; a screen/persona is the UI lane (``), never the screen name. +- **System overview:** when a flow crosses modules, update three places — the overview diagram, the source module's "Outputs to", and the target module's "Inputs from". + +## See also + +- `event-modeling` — decide the model before drawing it. +- `vertical-slices.md` — slice types and anatomy. diff --git a/plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md b/plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md new file mode 100644 index 0000000..c63e0a9 --- /dev/null +++ b/plugins/public-cratis-ai/skills/cross-cutting-properties/SKILL.md @@ -0,0 +1,70 @@ +--- +name: cross-cutting-properties +description: Attach audit/correlation metadata to every appended Cratis event without polluting event types — via ICanProvideAdditionalEventInformation, plus event tags and the built-in EventContext fields. Use for correlation IDs, tenant/actor context, and other cross-cutting concerns that should travel with events but are not domain payload. +--- + +# Cross-Cutting Event Properties + +Some information must travel with every event — correlation/causation ids, the authenticated actor, tenant context — but adding it as a property to every `[EventType]` would pollute the schemas. Chronicle solves this with `ICanProvideAdditionalEventInformation` (metadata-envelope providers) and event **tags**. + +## First, check the built-in `EventContext` + +Before implementing a provider, see whether what you need is already there (available in reactors/reducers): + +| Property | Description | +|---|---| +| `EventSourceId` | the event source appended to | +| `SequenceNumber` | ordinal within the sequence | +| `Occurred` | wall-clock time at append | +| `CorrelationId` | propagated from the request (or generated) | +| `Causation` | upstream event references | +| `CausedBy` | actor identity (from the configured identity provider) | + +**You don't need a custom provider for actor identity alone** — `CausedBy` already captures it. Reach for a provider only for *additional* fields. + +## `ICanProvideAdditionalEventInformation` + +```csharp +using System.Text.Json.Nodes; + +public class TenantMetadataProvider(IHttpContextAccessor http) : ICanProvideAdditionalEventInformation +{ + // ProvideFor receives the event as a JsonObject and mutates it in place; it returns Task. + public Task ProvideFor(JsonObject @event) + { + @event["tenantId"] = http.HttpContext?.Request.Headers["x-tenant-id"].FirstOrDefault() ?? "Default"; + return Task.CompletedTask; + } +} +``` + +Chronicle discovers providers from DI — register as scoped/singleton. Multiple providers merge; key collisions = last-registered wins. Place the class at a cross-cutting infrastructure location, not inside a slice. The properties land in the event's **metadata envelope**, not the event record — they are not surfaced in `EventContext` on reactive handlers. If a value must influence a projection/reducer, it belongs on the event type (or a dedicated audit event), not in cross-cutting metadata. + +## Tags vs filtering — easy to confuse + +| Attribute | Where | What it does | +|---|---|---| +| `[Tag("analytics", "user-action")]` | on an `[EventType]` | merges static tags into every occurrence at append time; available in `EventContext.Tags`. Does **not** filter. | +| `[FilterEventsByTag("tag")]` | on a reactor/reducer class | restricts which events reach the handler (multiple = OR; combined with `[EventSourceType]`/`[EventStreamType]` = AND). | +| `[Tag]` / `[Tags]` | on a reactor/reducer class | admin-UI label only — **no** effect on delivery. | + +Tags are also used for concurrency scoping. To *filter* by tag you need `[FilterEventsByTag]`, not `[Tag]`. + +## Common pitfalls + +| Pitfall | Why | +|---|---| +| Adding correlation/tenant id to every `[EventType]` | pollutes schemas — use a provider | +| Injecting scoped services into a singleton provider | register the provider scoped, or use `IServiceScopeFactory` | +| Expecting envelope properties to appear in `EventContext` on handlers | they don't — they're metadata only | +| Using a provider for data a projection needs | if the projection needs it, it belongs on the event type | + +## Quality gate + +- [ ] Build is clean; provider is registered in DI (not just implemented). +- [ ] No domain data hidden in cross-cutting properties — infrastructure metadata only. + +## See also + +- `vertical-slices.md` — event types, `EventContext` in reactors/reducers. +- `multi-tenancy` — namespace-per-tenant isolation (a different mechanism from a tenant tag). diff --git a/plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md b/plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md new file mode 100644 index 0000000..da67267 --- /dev/null +++ b/plugins/public-cratis-ai/skills/diagnose-slice/SKILL.md @@ -0,0 +1,48 @@ +--- +name: diagnose-slice +description: Use this skill to diagnose why a Cratis slice misbehaves — read model not updating, TypeScript proxy missing, command rejected unexpectedly, projection/observer quarantined, AutoMap mismatch, reactor side effects not appended, or specs flaking. Symptom → likely cause → the rule/skill that owns the fix. Use when something "isn't working" and the cause isn't obvious. +--- + +Symptom → likely cause → fix for the common ways a Cratis **application** slice misbehaves. Each row routes to the rule/skill that owns the detail — start there. Reproduce a real defect with a spec before and after the fix. + +## Backend / Chronicle + +| Symptom | Likely cause | Fix → | +|---|---|---| +| Read model query returns empty / stale | projection not wired: event→read-model property names differ (AutoMap only maps **matching** names), or a default value masks missing wiring | `add-projection`, `cratis-readmodel`; `[SetFrom]` for name diffs; no default values on `[ReadModel]` props | +| App crashes on startup with a projection error | duplicate `[FromEvent]` for the same event on one read model | `add-projection` (one `[FromEvent]` per event) | +| Command always "not successful" with **no** validation errors | `[Roles]`/authorization: an unauthorized result isn't successful and carries no validation errors | `auth-and-identity`, `cratis-command`; assert `ShouldNotBeAuthorized()` | +| Command rejected unexpectedly | a `CommandValidator`/`ConceptValidator` rule, or `Provide()` short-circuited with `ValidationResult.Error` | `add-business-rule`, `cratis-command` | +| Handler returns HTTP 500 instead of rejecting | threw from `Provide()`/`Handle()` for a *business* rule (that's an exception, not a rejection) | return `Result` — `add-business-rule` | +| Reactor's returned events never appended | returned `EventForEventSourceId` on a Chronicle version before reactor support | return event objects, or `ReactorSideEffect` for another target — `reactors.md` | +| Projection/reactor stops processing ("quarantined") | a handler threw; the partition paused and the observer quarantined (does **not** auto-resume) | read the failure off the server first — **inspect-running-chronicle**; then fix the handler (make it idempotent) and replay — `reactors.md` | +| Duplicate side effect on replay | non-idempotent reactor without `[OnceOnly]` | mark the handler `[OnceOnly]` — `reactors.md` | +| Chronicle analyzer warns on an event property | a nullable event property | model the optional fact as a **separate** event — `vertical-slices.md`, `event-modeling` | +| A read model needs a field from another slice | wrong stream boundary / missing event | re-model (information completeness) — `event-modeling`; never cross-read another read model at runtime | + +## Frontend / proxies + +| Symptom | Likely cause | Fix → | +|---|---|---| +| TS can't find the command/query proxy (`Cannot find module`) | proxies regenerate on a **Debug** build; you only built Release | build Debug — `new-vertical-slice` (backend phase) | +| Edited a generated file and it reverted | generated proxies (`// @generated by Cratis`) are never hand-edited | fix the C# source and rebuild — `general.md` | +| `DataPage` won't show live data | passed a snapshot query, or expected a non-existent `observableQuery` prop | pass the observable query to the single `query` prop (auto-detected) — `cratis-react-page`, `components.md` | +| Dialog/dropdown renders behind an overlay | used raw `primereact/dialog` or `primereact/dropdown` | use the Cratis wrappers from `@cratis/components/*` — `dialogs.md`, `components.md` | + +## Specs + +| Symptom | Likely cause | Fix → | +|---|---|---| +| Spec code leaks into a Release build | spec not wrapped in `#if DEBUG … #endif` | wrap the file — `specs.scenarios.csharp.md` | +| Off-by-one in event-tail assertions | sequence numbers are **zero-based** (tail of one event is `0`) | `specs.scenarios.csharp.md` | +| Order-dependent flake on a uniqueness rule | a hardcoded value collides across tests | use a per-test value (`Guid.NewGuid()`); don't reach for `[Collection]` for collisions — `specs.scenarios.csharp.md` | + +## When the symptom is only visible on a running server + +Everything above is diagnosed from the code. When the read model is stale **in a deployed store** and the same slice behaves locally, the answer is in the server's own state rather than the source — a failed partition carrying the exception that stopped it, an observer that never registered, an event that was never appended. Use **inspect-running-chronicle**; the `cratis` CLI reads all of it. + +The distinction that matters: a failed partition does not retry itself, so what looks like "the projection is slow" is usually "the projection stopped, permanently, with a recorded reason nobody has read". + +## When nothing here fits + +Re-read the owning rule (`vertical-slices.md`, `reactors.md`, `cratis-readmodel`), confirm the build is clean in **both Debug and Release**, and reproduce the symptom with a spec. Don't infer framework behavior from package internals — if the rules/skills don't answer it, ask. diff --git a/plugins/public-cratis-ai/skills/discover-implementations/SKILL.md b/plugins/public-cratis-ai/skills/discover-implementations/SKILL.md new file mode 100644 index 0000000..8253058 --- /dev/null +++ b/plugins/public-cratis-ai/skills/discover-implementations/SKILL.md @@ -0,0 +1,95 @@ +--- +name: discover-implementations +description: Use this skill when asked to wire up a type that needs to enumerate every implementation of an interface (handlers, strategies, filters, validators, formatters, providers) in a Cratis-based C# project. Enforces `IInstancesOf` over `IEnumerable` and removes hand-maintained DI registrations. +--- + +Wire convention-based discovery for a set of implementations behind an abstraction. + +## The pattern in one line + +Inject `IInstancesOf` from `Cratis.Types`. Mark implementations with `[Singleton]`. Delete any `services.AddSingleton()` lines that registered them. + +## When this skill applies + +The consumer of an abstraction needs to iterate, filter, or fan out to **every** registered implementation. Common shapes: + +- `*Handlers`, `*Filters`, `*Validators`, `*Formatters`, `*Strategies`, `*Providers`, `*Resolvers` — anything plural that delegates to a set. +- A dispatcher that picks the right implementation by calling `CanHandle(...)` and forwarding to the matching one. +- A composite that fans a single input out to all implementations and aggregates results. + +## When it does NOT apply + +- The consumer needs **one** implementation chosen at composition time → constructor-inject the concrete interface and let normal DI resolve `IFoo → Foo`. +- The method returns a sequence of values to a caller → `IEnumerable` (or `IReadOnlyList`, etc.) is still the right return type. The rule is only about **injecting** implementations of an abstraction. + +## Step 1 — Confirm the implementations are discoverable + +`IInstancesOf` discovers types by convention from loaded assemblies. The only requirements: + +- Each implementation is a non-abstract `public class`. +- It implements the interface directly (not via another layer that hides it). + +No assembly attribute is needed — the Cratis framework's type discovery picks them up automatically. + +## Step 2 — Mark implementations as singletons (default) + +```csharp +[Singleton] +public class EventResultHandler(IEventTypes eventTypes) : IReactorSideEffectHandler +{ + public bool CanHandle(ReactorContext context, object value) => /* ... */; + public Task Handle(ReactorContext context, object value) => /* ... */; +} +``` + +Add `using Cratis;` if `[Singleton]` is unresolved. + +Skip `[Singleton]` only when the implementation must be transient — i.e. it holds per-call state that cannot be shared. The convention `IFoo → Foo` still applies for transients; do not register them explicitly. + +## Step 3 — Inject `IInstancesOf` in the consumer + +```csharp +using Cratis.Types; + +[Singleton] +public class ReactorSideEffectHandlers(IInstancesOf handlers) : IReactorSideEffectHandlers +{ + public bool CanHandle(ReactorContext context, object value) => + handlers.Any(h => h.CanHandle(context, value)); + + public Task Handle(ReactorContext context, object value) => + handlers.First(h => h.CanHandle(context, value)).Handle(context, value); +} +``` + +`IInstancesOf` implements `IEnumerable` — LINQ works directly on it. Materialize with `.ToArray()` only if you need a stable snapshot (rare). + +## Step 4 — Delete the dead registrations + +Find every line in composition roots and service-collection extensions that registered the implementations or the consumer, and remove them: + +```csharp +// Delete these — IInstancesOf discovers them, [Singleton] registers them +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); +``` + +Use the codebase search tools to find every reference and verify nothing else relies on these registrations. + +## Step 5 — Verify + +1. `dotnet build` — zero warnings, zero errors. +2. Run the relevant specs/integration tests for the affected feature. +3. If a `System.MissingMethodException: Cannot dynamically create an instance of type '...'. Reason: Cannot create an instance of an interface.` appears at runtime, an implementation is missing `[Singleton]` or the interface signature changed — re-check Step 2. + +## Why this matters + +The cost of `services.AddSingleton()` looks zero at the registration line. The hidden cost shows up later: + +- Adding a new implementation in a different folder silently does nothing until someone remembers to register it. +- Removing an implementation leaves a stale registration that fails at startup. +- Spec setups have to duplicate the same registrations to mirror production. +- The composition root grows linearly with the number of implementations — high churn, high merge conflict. + +`IInstancesOf` and `[Singleton]` push that knowledge into the implementation itself. Adding or removing an implementation is a single-file change. diff --git a/plugins/public-cratis-ai/skills/event-modeling/SKILL.md b/plugins/public-cratis-ai/skills/event-modeling/SKILL.md new file mode 100644 index 0000000..b43813c --- /dev/null +++ b/plugins/public-cratis-ai/skills/event-modeling/SKILL.md @@ -0,0 +1,63 @@ +--- +name: event-modeling +description: Design a Cratis event model before writing code — decide stream boundaries, commands, events, read models, automations/translations, compliance subjects, and the spec outline. Use this when behavior, event vocabulary, stream boundaries, or a multi-slice flow is not yet settled, before implementing a slice. +--- + +# Event Modeling + +Use this skill **before writing code** when behavior, event vocabulary, stream boundaries, or a multi-slice flow is not already settled. The output is an implementation brief: which commands exist, which stream each event lands on, which read models consume those events, which automations/translations react, and which specs prove the flow. Afterward, use the `create-event-model` skill to draw or update the Mermaid `EventModel.md` diagram. + +> **Lineage.** Cratis's four slice types and the Given/When/Then-per-slice discipline follow **Event Modeling** (Adam Dymitruk; Martin Dilger, *Understanding Eventsourcing*); this skill applies that method to Cratis. + +Skip this only for mechanical changes where the event types and flow already exist and the request is just wiring or a narrow fix. + +## The brief — decide before implementation + +- **Module / feature / slice name and slice type** for each behavior (State Change / State View / Automation / Translation). +- **Commands:** inputs and the authorization (roles/policy) that gates them. Commands are imperative intents. +- **Events:** past-tense, one-purpose facts. **Decide the event source id for every event** — events never carry their own event-source id as a payload property. **Event properties are non-nullable** (Chronicle's analyzer warns otherwise); model optional facts as *separate* events, not nullable fields. Don't append events for derived/aggregate state — project that from source events. +- **Read models:** their consumers and source events; whether projection-backed, reducer-backed, or `[Passive]` (command-side decision only). +- **Automations / translations:** which events they react to, which side effects need `[OnceOnly]`, and whether they emit follow-up events or run commands via `ICommandPipeline`. +- **Specs:** happy path, validation failures, constraints, projections/reducers, reactor side effects. + +## Information completeness — trace every field to an event + +The core Event Modeling check, run at modeling time (not after the projection misbehaves): + +- **Backward:** for each read model, walk every property back to the event that carries it. A field with no source event is a **missing event or command** — not a nullable column. Resolve it in the model before implementing. +- **Forward:** every event you define should feed at least one read model, automation, or translation. An event nothing consumes is a smell — either a consumer is missing or the event shouldn't exist. + +If a field can only be filled by reaching into another slice's read model, you've found a missing event or a wrong stream boundary — fix the model, don't cross-read at runtime. + +## Compliance modeling (when personal data is involved) + +Decide compliance *before* choosing event/read-model shapes: + +- Prefer **one-subject event streams** for person-level PII. If an event carries PII about a natural person, decide the subject explicitly. +- Use **concept-level `[PII]`** for inherently personal values (names, email, phone, identity-provider subjects, personal notes/feedback). Keep business metadata unmarked. +- The subject defaults to the `EventSourceId` identity — set `[Subject]`/`ICanProvideSubject`/a tuple `Subject` only when the subject is a non-`EventSourceId` value. A managed read-model document has one subject — don't mix multiple people's PII in one document. +- Bearer tokens, magic links, and signed URLs are not durable facts — store keyed hashes / opaque references, not the secret. + +## Output shape + +Write the brief in this order: **(1)** stream boundaries and subjects → **(2)** commands and events → **(3)** read models and consumers → **(4)** automations/translations → **(5)** compliance notes → **(6)** specs. + +If a subject boundary or erasure behavior can't be made person-level without changing product behavior, **stop and surface that trade-off** before implementing. + +## Slice lifecycle — Draft → Ready → Working → Done + +A slice is the unit of work, and it moves through four states. The model (events, commands, read models, UI/screens, specs) lives inside the slice; **anyone — human or agent — can author or update it**, and the lifecycle doesn't care who made the change. + +- **Draft** — being modeled; events/commands/read models/boundaries still in flux. +- **Ready** — the **handoff gate**: the model is *information-complete* (the checks above pass — every read-model field traces to an event, every event has a consumer; commands, authorization, compliance, and the specs outline all decided). A Ready slice can be implemented with **no further modeling decisions**. +- **Working** — an implementer (agent or human) has picked it up and is running the **Implementation Workflow** end-to-end: backend slice file → Debug+Release build → specs → frontend → docs → quality gates. Use the `new-vertical-slice` skill; keep `EventModel.md` in sync via `create-event-model`. +- **Done** — every quality gate is green and the change is shipped (PR merged, CI green — via `ship-changes`). Ready for downstream slices to build on. + +**Marking a slice Ready is the signal to implement it** — the implementer runs the full workflow through to Done rather than stopping at the model, and doesn't batch Ready slices behind one another. (This is the Event Modeling "agent harness" loop: model → Ready → pick up → work → Done → repeat.) This stays subject to the [Collaboration Default](../../rules/general.md) — pause only for a genuine checkpoint, risky change, or a decision the model can't make. + +## See also + +- `create-event-model` — render the chosen model into a Mermaid `EventModel.md`. +- `new-vertical-slice` — implement a Ready slice end-to-end (the Working state). +- `ship-changes` — branch, commit, PR, and merge to reach Done. +- `vertical-slices.md` — slice anatomy that implements the brief. diff --git a/plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md b/plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md new file mode 100644 index 0000000..9effec3 --- /dev/null +++ b/plugins/public-cratis-ai/skills/event-type-migrations/SKILL.md @@ -0,0 +1,80 @@ +--- +name: event-type-migrations +description: Evolve a Cratis Chronicle event schema without breaking replay — add a new generation and an EventTypeMigration so old stored events upcast into the new shape. Use when an event needs a new required property, a renamed property, or a structural change after events of the prior shape already exist. +--- + +# Event Type Migrations + +Chronicle stores events forever. When an event's schema must change, you write a **migration** rather than editing the original record — Chronicle auto-discovers migrations and applies them when reading old events. + +> **You only need this once events of the prior shape exist somewhere you can't regenerate** (a real environment's event log). Before that — in greenfield development with disposable data — rename event types and change schemas freely; a migration just adds dead code that hides the real schema in `git log`. + +## When you need it + +- An `[EventType]` needs a new required property (adding it would break observers reading old events). +- A property is renamed; old events carry the old name. +- The event shape changes structurally. + +> **Never add a nullable value type to an `[EventType]` to represent "absent on old events"** — Chronicle's analyzer warns on nullable event members. Add a migration with a default instead. + +## Generations + +Every `[EventType]` has a **generation** (starts at `1`). Each schema change increments it. Chronicle routes stored events through the migration chain before delivering them to projections/reducers. + +``` +Generation 1 (stored) → Migration 1→2 → Migration 2→3 → Current (Generation 3) +``` + +## Steps + +### 1. Keep the prior record and bump the generation on the new one + +Keep the old shape available to the migration as `TPrevious`, and mark the current record's generation: + +```csharp +[EventType(generation: 2)] +public record OrderPlaced(OrderId OrderId, Currency Currency); // generation 2 (current) +``` + +### 2. Write the migration + +Implement `EventTypeMigration` — `TUpgrade` is the current shape, `TPrevious` the prior. Chronicle extracts the generations, validates they're consecutive, and discovers the migration automatically (no registration). + +`Upcast` and `Downcast` are both `public abstract void` and take an `IEventMigrationBuilder` — you describe the change declaratively through `builder.Properties(...)`, you do not construct the record by hand: + +```csharp +public class OrderPlacedV1ToV2 : EventTypeMigration +{ + public override void Upcast(IEventMigrationBuilder builder) => + builder.Properties(p => p.DefaultValue(_ => _.Currency, Currency.From("NOK"))); // new field's default + + public override void Downcast(IEventMigrationBuilder builder) => + builder.Properties(_ => { }); // map back for any consumers still on gen 1 +} +``` + +The property builder exposes `DefaultValue`, `RenamedFrom`, `Split`, and `Combine` — use them to express the change declaratively. Both `Upcast` and `Downcast` are abstract on the base, so both must be implemented (`Downcast` may be a no-op `builder.Properties(_ => { })` when no consumer needs the gen-1 shape). + +### 3. Chain across generations + +For three generations, write two migrations (`1→2`, `2→3`) — each only knows its adjacent pair; Chronicle chains them. + +## Common pitfalls + +| Pitfall | Why it breaks | +|---|---| +| Editing the stored event record without bumping `generation` | Old events still carry the old schema; Chronicle won't migrate them | +| Adding a nullable value type to handle "missing old data" | Analyzer-flagged anti-pattern; use a migration default | +| A migration that throws on a null/missing old field | Old events may lack fields entirely — null-coalesce / default | +| Splitting one event into two inside `Upcast` | `Upcast` returns one event; model a split as a reactor/command, not a schema migration | + +## Quality gate + +- [ ] Build is clean. +- [ ] Old-generation events upcast to the current shape when replayed through a `ReadModelScenario`. +- [ ] No nullable value types introduced on `[EventType]` records. + +## See also + +- `vertical-slices.md` — event type rules (non-nullable, naming). +- `event-modeling` — deciding when a fact is a new event vs a migration. diff --git a/plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md b/plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md new file mode 100644 index 0000000..fbc8dd6 --- /dev/null +++ b/plugins/public-cratis-ai/skills/inspect-running-chronicle/SKILL.md @@ -0,0 +1,71 @@ +--- +name: inspect-running-chronicle +description: Inspect or operate a running Chronicle server with the cratis CLI - list failed partitions, read why an observer is stuck, replay a partition, browse events, event types, read models, projections and jobs. Use when a question is about the state of a live store rather than the source code, when a projection will not update, when an observer is quarantined, or when checking whether an event was actually appended. Also use when setting the CLI up for a project so agents can reach the store. +--- + +# Inspect a Running Chronicle + +Source code says what *should* happen. When the question is what *is* happening in a live store — a projection that will not move, an observer that stopped, an event you are not sure was appended — read the server instead of the code. The `cratis` CLI is how. + +**Do not guess command names from this file.** The CLI ships its own complete, versioned catalog and that is the authority: + +```bash +cratis llm-context # every command, option and argument as JSON +cratis --help # the same, one group at a time +``` + +This skill covers *when to reach for the CLI and how to read what comes back*. The catalog covers *what to type*. + +## Setting it up in a project + +Once per project, so every agent working it can reach the store: + +```bash +cratis init # detects the AI tools in use and writes CHRONICLE.md + a chronicle-cli skill +cratis init --refresh # re-capture after upgrading the CLI +``` + +Two things worth knowing before running it: + +- The embedded catalog is a **snapshot**, not a live lookup. After a CLI upgrade it still describes the older surface; `cratis init` reports the mismatch and `--refresh` fixes it. +- If the repository's instruction file (`AGENTS.md`, `CLAUDE.md`, `.github/copilot-instructions.md`) is **generated from a shared corpus and propagated** — as it is in every repository consuming this one — pass `--no-context`. Appending to a generated file works until the next sync silently removes it. Add the `@CHRONICLE.md` line to the canonical source instead. + +## Reaching the right server + +Resolution order is `--server` → `CHRONICLE_CONNECTION_STRING` → the active context → `chronicle://localhost:35000`. Prefer a named context over repeating a connection string: + +```bash +cratis context create dev --server chronicle://localhost:35000 +cratis context set dev +``` + +**Be deliberate about which store you are pointed at.** The same commands read production and a local container, and several of them are destructive. Confirm the context before running anything that writes. + +## When to reach for it + +| Question | Where to look | +|---|---| +| Why has this read model stopped updating? | failed partitions for its observer — the error is on the partition, not in the log | +| An observer is "quarantined" — why? | the failed partition's detail, with full stack traces | +| Did this event actually get appended? | the event sequence, filtered by event type or event source | +| What is this event's shape in the store? | the registered event types | +| Is this projection even registered? | the projections list, then its definition | +| Is a replay or migration still running? | the jobs list | + +## Reading what comes back + +- **A failed partition does not retry itself.** It stays failed until something clears it, so a stale value is permanent rather than slow. That distinction is the whole diagnosis: "not arrived yet" and "will never arrive" look identical from the outside. +- **Fix the cause before replaying.** Replaying into an unfixed handler fails the same way and buries the original error under a newer one. +- **Prefer `--output plain` for large listings** (events, event types, read models, projections) — the JSON repeats every field name on every row. Use `--output json` or `json-compact` for `show`/`get` commands where you need the nested structure. +- **`--quiet` prints identifiers only**, which is what you want when piping one command into another. + +## Before you change anything + +Destructive commands — replay, retry, remove, clearing a quarantine — prompt for confirmation in a terminal and take `--yes` in scripts. Reaching for `--yes` to silence a prompt you have not read is how the wrong store gets replayed. + +A failed partition that you have not yet explained is not a thing to clear. Read it, fix the handler, then replay. Clearing it first destroys the evidence and the same failure returns on the next event. + +## Related + +- **diagnose-slice** — symptom → cause → owning rule, for when the defect is in the code rather than the store's state. Start there when the symptom is reproducible locally; start here when it is only visible on a running server. +- **observable-query-curl** — for exercising an application's own observable query endpoints over HTTP, which is a different surface from the store's management API. diff --git a/plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md b/plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md new file mode 100644 index 0000000..74dce04 --- /dev/null +++ b/plugins/public-cratis-ai/skills/multi-tenancy/SKILL.md @@ -0,0 +1,52 @@ +--- +name: multi-tenancy +description: Isolate tenants in a Cratis application with Chronicle namespaces — Arc maps the current tenant to a namespace (TenantNamespaceResolver), and each namespace has its own events, projections, reducers, and read models. Use when one deployment must serve multiple tenants with data isolation. Keep it product-neutral — not every Cratis app is multi-tenant. +--- + +# Multi-Tenancy with Chronicle Namespaces + +Chronicle implements multi-tenancy through **namespaces**: each namespace is a logically separate event store. Events, projections, reducers, and observers run independently per namespace — there is no cross-namespace leakage. **Not every Cratis app is multi-tenant** — adopt this only when one deployment must serve multiple tenants with isolation. + +## Core concept + +| Term | Meaning | +|---|---| +| Namespace | a named isolation boundary in Chronicle (its own event store) | +| Default namespace | `"Default"` — used when none is resolved | + +All appends, projections, and observers are scoped to the resolved namespace. A reactor for `Created` fires once per tenant namespace that has that event — independently. + +## Arc tenancy integration + +When using Arc with Chronicle, tenancy maps to namespaces automatically: Arc's `TenantNamespaceResolver` maps the current tenant id to the Chronicle namespace and falls back to the default namespace when no tenant is set. Enable Arc tenancy in startup so the tenant context resolves before each command/query handler runs; namespace wiring is then automatic — no manual resolver registration needed. + +If you need custom namespace resolution outside Arc tenancy (header, subdomain, JWT claim), Chronicle supports namespace resolvers registered in priority order; the first non-null result wins, else the default namespace is used. + +## Observer and reactor isolation + +Observers (projections, reducers, reactors) are instantiated **per namespace**. Consequences: + +- A `[OnceOnly]` reactor fires once **per event source within each namespace** (i.e. once per tenant), not globally once — see reactors.md. +- Projection rewind affects only the target namespace. +- Each namespace has its own sequence numbers. + +## Common pitfalls + +| Pitfall | Why | +|---|---| +| Storing a tenant id on every event type | the namespace *is* the tenant — events don't need a tenant property | +| No tenant resolution in a multi-tenant deployment | every tenant lands in `"Default"` — no isolation | +| Reading one namespace and writing another in the same request | accidental cross-namespace access is a bug (intentional bridging is a Translation reactor) | +| Expecting `[OnceOnly]` to be globally once | it is per-namespace | + +## Quality gate + +- [ ] Build is clean. +- [ ] Tenant resolution is configured and resolves the expected namespace from test requests. +- [ ] No tenant identifier appears on `[EventType]` records. +- [ ] `[OnceOnly]` reactors are understood to fire per-namespace. + +## See also + +- `cross-cutting-properties` — injecting tenant metadata into event envelopes (distinct from namespace isolation). +- `auth-and-identity` — resolving the current tenant/user. diff --git a/plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md b/plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md new file mode 100644 index 0000000..ff5480e --- /dev/null +++ b/plugins/public-cratis-ai/skills/new-vertical-slice/SKILL.md @@ -0,0 +1,103 @@ +--- +name: new-vertical-slice +description: "Use this skill when asked to implement a new feature, command, query, slice, or screen in a Cratis-based project. Guides the full end-to-end workflow: C# backend → Debug+Release build → specs → React frontend → quality gates." +--- + +Implement a complete vertical slice following this EXACT order. Never skip steps or work on multiple slices in parallel. + +## Step 1 — Identify the slice type + +Choose **one** of: +- **State Change** — a command that mutates state and records events (most common) +- **State View** — a query that reads from a read model +- **Automation** — a background reactor triggered by events +- **Translation** — transforms events into other events + +## Step 2 — Determine the namespace root + +Read `global.json` and existing `.cs` files under the app source root to find the namespace root (e.g. `Studio`, `Library`). Never hard-code it. + +## Step 3 — Create the C# slice file + +Place ALL backend artifacts in a single file in the slice folder: `//.cs` (under the app source root; a `/` grouping above the feature is optional — there is **no** top-level `Features/` wrapper). + +File creation order within the slice: +1. Concept types (if new strongly-typed IDs are needed — see `add-concept` skill) +2. Command `record` with `Handle()` method and optional validation attributes + - If a business rule depends on Chronicle event-sourced state, add the relevant read model as a parameter to `Handle()` — see `add-business-rule` skill (DCB pattern) +3. `CommandValidator` for command-level rejection rules (see `add-business-rule`); `ConceptValidator` for value invariants +4. Constraint class `Constraint` (if needed) +5. Event `record` with `[EventType]` (no arguments, no mutable properties) +6. Read model `record` with `[ReadModel]` and model-bound projection attributes (`[FromEvent]`, `[Key]`, etc.) + - Use fluent `IProjectionFor` only when model-bound attributes don't fit + +**Critical rules:** +- Commands are `record` types with a `Handle()` method directly on them — DO NOT create separate handler classes +- Events use `[EventType]` with NO arguments — never pass a GUID or string +- Projection: prefer model-bound attributes on the read model; if using `IProjectionFor`, AutoMap is on by default — just call `.From<>()` directly +- Namespace mirrors the folder path under the source root: `...` (no `Features` segment — drop any level that isn't present) +- Copyright header on every file: `// Copyright (c) Cratis. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information.` + +## Step 4 — Build + +Run `dotnet build` in **both** Debug and Release. Fix ALL errors and warnings before proceeding — Debug regenerates the TypeScript proxies and compiles `#if DEBUG` spec code; build Release with `-p:CratisProxiesOutputPath=` to skip re-running proxy generation. + +## Step 5 — Write specs (mandatory for every slice type) + +Use the in-process scenario family — `CommandScenario` for State Change, `ReadModelScenario` for State View, `ReactorScenario` for Automation/Translation. For each command, write specs covering: +- Happy path — command succeeds, correct event appended +- Each validation failure (one spec per rule) +- Each business rule violation (one spec per DCB condition in `Handle()` that inspects a read model) +- Each constraint violation + +See `write-specs` skill for the complete spec structure. + +Run `dotnet test`. Fix all failures before proceeding. + +## Step 6 — Implement React component(s) + +Place `.tsx` files in the slice folder `//`. + +- Import the auto-generated command/query proxy from the same folder +- Use `CommandDialog` from `@cratis/components/CommandDialog` for command dialogs +- Use `Dialog` from `@cratis/components/Dialogs` for data-only dialogs — NEVER import from `primereact/dialog` +- Use PrimeReact CSS variables for all colors — never hard-code hex values +- Use full descriptive variable names — never abbreviations (`event` not `e`, `index` not `idx`) +- No `any` types — use `unknown` with type guards + +**Command usage:** +```tsx +const [myCommand] = MyCommand.use(); +const handleSubmit = async () => { + myCommand.propertyName = value; + const result = await myCommand.execute(); + if (result.isSuccess) closeDialog(DialogResult.Ok); +}; +``` + +**Query with paging:** +```tsx +const pageSize = 10; +const [result, , setPage] = MyQuery.useWithPaging(pageSize); +// Use result.data, result.paging.totalItems, result.paging.page +``` + +Write specs for the React surface (view models, helpers, component behavior) with the **write-specs-frontend** skill. + +## Step 7 — Update the composition page + +Open `/.tsx` and add the new component. If a new page is introduced, also update the router and navigation. + +## Step 8 — Quality gates + +All must pass before the slice is considered done: +- `dotnet build` — zero errors/warnings +- `dotnet test` — zero failures +- `yarn lint` — zero errors +- `npx tsc -b` — zero errors +- Public-facing changes (clients, SDKs, public APIs) include associated documentation updates +- `cd Documentation/web && npm run check` passes when documentation is added or changed + +--- + +For complete code patterns for all 4 slice types and frontend examples, see [references/PATTERNS.md](references/PATTERNS.md). diff --git a/plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md b/plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md new file mode 100644 index 0000000..c6e132e --- /dev/null +++ b/plugins/public-cratis-ai/skills/new-vertical-slice/references/PATTERNS.md @@ -0,0 +1,344 @@ +# Vertical Slice Patterns + +## State Change — full example + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MyApp.Projects.Registration; + +// ─── Concepts ─────────────────────────────────────────────────────────────── + +/// +/// Represents the unique identifier of a project. +/// +// Identity concept → derive from EventSourceId (not ConceptAs). The base supplies the +// Guid/EventSourceId/string conversions, so Chronicle resolves the key automatically. +public record ProjectId(Guid Value) : EventSourceId(Value) +{ + /// + /// Gets a sentinel value for an unset identifier. + /// + public static readonly ProjectId NotSet = new(Guid.Empty); + + /// + /// Creates a new with a unique value. + /// + /// A new . + public static ProjectId New() => new(Guid.NewGuid()); + + /// + /// Implicitly converts a to a . + /// + /// The to convert. + public static implicit operator ProjectId(Guid value) => new(value); +} + +/// +/// Represents the name of a project. +/// +public record ProjectName(string Value) : ConceptAs(Value) +{ + /// + /// Gets a sentinel value for an unset name. + /// + public static readonly ProjectName NotSet = new(string.Empty); + + /// + /// Implicitly converts a to a . + /// + /// The string to convert. + public static implicit operator ProjectName(string value) => new(value); + + /// + /// Implicitly extracts the underlying value. + /// + /// The to convert. + public static implicit operator string(ProjectName name) => name.Value; +} + +// ─── Command ──────────────────────────────────────────────────────────────── + +/// +/// Command to register a new project. +/// +[Command] +public record RegisterProject(ProjectId ProjectId, ProjectName Name) +{ + /// + /// Produces a event. + /// + /// The event. + public ProjectRegistered Handle() => new(Name); +} + +// ─── Constraint ───────────────────────────────────────────────────────────── + +/// +/// Prevents two projects from being registered with the same name. +/// +public class UniqueProjectNameConstraint : IConstraint +{ + /// + public void Define(IConstraintBuilder builder) => builder + .Unique(unique => unique.On(e => e.Name)); +} + +// ─── Event ────────────────────────────────────────────────────────────────── + +/// +/// Raised when a new project has been successfully registered. +/// +[EventType] +public record ProjectRegistered(ProjectName Name); +``` + +**Key rules for State Change slices:** +- `[Command]` attribute marks the command record — there is no `ICommand` interface +- `Handle()` RETURNS the event — it never calls `IEventLog` or `eventLog.Append()` +- `[EventType]` has **no arguments** — the event type name is resolved automatically +- The event source ID is resolved in order: `ICanProvideEventSourceId` > `EventSourceId` property > `[Key]` attribute +- Constraints (`IConstraint`) live in the same slice file as the command +- The read model and projection live in the **State View** slice, not here + +--- + +## State View — full example + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MyApp.Projects.Listing; + +// ─── Read Model (model-bound projection — no separate class needed) ────────── + +/// +/// Represents a project in the listing read model. +/// +[ReadModel] +[FromEvent] +public record Project( + [Key] ProjectId Id, + ProjectName Name) +{ + /// + /// Observes all projects in the collection. + /// + /// The to observe. + /// An observable subject of all projects. + public static ISubject> AllProjects(IMongoCollection collection) => + collection.Observe(); +} +``` + +**Key rules for State View slices:** +- The read model is decorated with `[ReadModel]` — needed for the static observable query API +- **Model-bound projection (preferred):** Add `[FromEvent]` at class level for auto-mapping — no separate `IProjectionFor` class needed +- Mark the primary key property with `[Key]` from `Cratis.Chronicle.Keys` +- Use `[SetFrom]` / `[AddFrom]` / `[SubtractFrom]` for explicit property-level mapping +- Use `[ChildrenFrom]` for nested child collections, `[Join]` for cross-event enrichment +- Query methods are **static methods** on the read model record itself +- `Observe()` returns an `ISubject>` for live updates; use `ObserveWithPaging(...)` for paged results + +**When to use fluent `IProjectionFor` instead:** +- Projection logic is too complex for attributes (e.g. conditional branching) +- You prefer to separate projection definition from the read model type + +```csharp +// Fluent alternative — still correct, use for complex cases +public class ProjectProjection : IProjectionFor +{ + public void Define(IProjectionBuilderFor builder) => builder + .From(); // AutoMap is on by default +} +``` + +--- + +## Automation (Reactor) — full example + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MyApp.Projects.Notifications; + +/// +/// Sends a notification when a project is registered. +/// +/// The notification service. +public class ProjectRegisteredNotifier(INotificationService notifications) : IReactor +{ + /// + /// Reacts to events. + /// + /// The event. + /// The event context. + public async Task ProjectRegistered(Registration.ProjectRegistered @event, EventContext context) => + await notifications.Notify($"Project '{@event.Name}' was registered."); +} +``` + +**Key rules:** +- Reactors implement `IReactor` — a marker interface with **no methods** +- The method name MUST match the event type name exactly (by convention) +- Reactors MUST be idempotent — they can be called more than once per event +- Use the event data directly — do not query the read model inside the handler +- To trigger further commands, inject and call `ICommandPipeline` — never use `IEventLog` directly + +--- + +## Translation — full example + +```csharp +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace MyApp.Projects.StockKeeping; + +/// +/// Reacts to reservation events and decreases stock accordingly. +/// +/// The stock keeper service. +/// The command pipeline for executing commands. +public class StockKeeping(IStockKeeper stockKeeper, ICommandPipeline commandPipeline) : IReactor +{ + /// + /// Handles a event. + /// + /// The event. + /// The event context. + public async Task BookReserved(BookReserved @event, EventContext context) => + await commandPipeline.Execute(new DecreaseStock(@event.Isbn, await stockKeeper.GetStock(@event.Isbn))); +} + +/// +/// Command to decrease available stock of a book. +/// +[Command] +public record DecreaseStock(ISBN Isbn, BookStock StockBeforeDecrease) +{ + /// + /// Produces a event. + /// + /// The event. + public StockDecreased Handle() => new(Isbn, StockBeforeDecrease); +} + +/// +/// Raised when the available stock of a book has decreased. +/// +[EventType] +public record StockDecreased(ISBN Isbn, BookStock StockBeforeDecrease); +``` + +--- + +## Frontend — complete component examples + +### Listing component with paging + +```tsx +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Column } from '@cratis/components/DataPage'; +import { DataTable } from 'primereact/datatable'; +import { AllProjects } from './AllProjects'; + +const pageSize = 10; + +export const Listing = () => { + const [result, , setPage] = AllProjects.useWithPaging(pageSize); + + return ( + setPage(event.page ?? 0)} + scrollable scrollHeight="flex" + emptyMessage="No projects found."> + + + ); +}; +``` + +### CommandDialog for state-change commands + +```tsx +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { useState } from 'react'; +import { DialogProps, DialogResult } from '@cratis/arc.react/dialogs'; +import { CommandDialog } from '@cratis/components/CommandDialog'; +import { InputText } from 'primereact/inputtext'; +import { RegisterProject } from './RegisterProject'; + +export const AddProject = ({ closeDialog }: DialogProps) => { + const [name, setName] = useState(''); + + return ( + { + values.name = name; + return values; + }} + onConfirm={() => closeDialog(DialogResult.Ok)} + onCancel={() => closeDialog(DialogResult.Cancelled)}> + + setName(event.target.value)} + autoFocus + /> + + + ); +}; +``` + +### Composition page + +```tsx +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { DialogResult, useDialog } from '@cratis/arc.react/dialogs'; +import { Button } from 'primereact/button'; +import * as mdIcons from 'react-icons/md'; +import { Page } from '@cratis/components/Common'; +import { AddProject } from './Registration/AddProject'; +import { Listing } from './Listing/Listing'; + +export const Projects = () => { + const [AddProjectDialog, showAddProjectDialog] = useDialog(AddProject); + + // PrimeReact 11 removed the standalone Menubar; for a query-backed list + // page prefer `DataPage` + ``, or compose `Button`s + // (content is children in v11) for a custom toolbar. + return ( + + + + + + ); +}; +``` diff --git a/plugins/public-cratis-ai/skills/query-paging/SKILL.md b/plugins/public-cratis-ai/skills/query-paging/SKILL.md new file mode 100644 index 0000000..fe27d13 --- /dev/null +++ b/plugins/public-cratis-ai/skills/query-paging/SKILL.md @@ -0,0 +1,90 @@ +--- +name: query-paging +description: Add server-side paging and sorting to a Cratis read-model query — return IQueryable for one-shot paging, ISubject> for observable + paged results, and consume them from React with useWithPaging. Use when a list query can grow large enough that returning all rows is wasteful, or needs server-side sorting. +--- + +# Query Paging + +Arc applies paging and sorting automatically to any model-bound query that returns `IQueryable`. The HTTP layer reads `?page=`, `?pageSize=`, `?sortby=`, `?sortDirection=` and applies them before the query materializes — you don't write skip/take/sort. If a result set is small and bounded, `IEnumerable` is fine and you don't need this. + +## Steps + +### 1. One-shot paged query — `IQueryable` + +```csharp +[ReadModel] +public record Project(...) +{ + public static IQueryable AllProjects(IMongoCollection collection) => + collection.AsQueryable(); + + // Filtered — apply the predicate before returning; Arc pages on top of it: + public static IQueryable ActiveProjects(IMongoCollection collection) => + collection.AsQueryable().Where(p => !p.IsArchived); + + // Sensible default order when the caller passes no sortby: + public static IQueryable AllByName(IMongoCollection collection) => + collection.AsQueryable().OrderBy(p => p.Name); +} +``` + +### 2. Observable + paged — `ISubject>` + +For live updates *and* paging, return what `Observe` returns: + +```csharp +public static ISubject> AllProjectsLive(IMongoCollection collection) => + collection.Observe(_ => _.Find(p => !p.IsArchived)); +``` + +Each emission carries page metadata alongside the data. Sorting and paging are applied **at the source** and re-applied on every change — the storage `Observe` helpers read the ambient query context themselves rather than going through the query renderer. + +⚠️ **Do not wrap it as `ISubject>`.** No `Observe` overload returns that shape, and the renderer matches on the outer type, so nothing would page it. + +### 3. What does and doesn't page + +| Return type | Paging? | How | +|---|---|---| +| `T`, `T?`, `IEnumerable`, `List`, `T[]` | No | Nothing narrows the result | +| `IQueryable` | **Yes — auto-paged** | The query renderer applies `OrderBy`/`Skip`/`Take` to the queryable | +| `Task>` | **Yes — auto-paged** | The result is awaited first, then rendered as the queryable it unwraps to | +| `ISubject>` | **Yes — paged and sorted** | Not by the renderer. The storage `Observe()` helpers read the ambient query context themselves and apply sorting and paging at the source, re-applying them on every change | +| `ISubject>` | **No — not a shipped shape** | No `Observe` overload returns it, and the renderer matches on the outer type, so it never fires | + +Don't `.ToList()` before returning `IQueryable` (defeats skip/take) and don't hard-code `Take(n)` (conflicts with `pageSize`). + +⚠️ **Returning an already-materialized collection as a queryable — `(await …).AsQueryable()` — pages *correctly* and costs everything.** LINQ-to-objects honours `Skip`/`Take`, so the results are right and the whole set was read to produce them. Page the source, not the answer. + +### 4. Frontend hooks + +```tsx +const [result, perform, setSorting, setPage, setPageSize] = + AllProjects.useWithPaging(25 /* pageSize */, args?, sorting?); +// suspense: AllProjects.useSuspenseWithPaging(25) +// observable + paged: AllProjectsLive.useSuspenseWithPaging(25) +``` + +`result.paging` = `{ page, size, totalItems, totalPages }`. `page` is **zero-based** — show `page + 1` in labels, pass zero-based to `setPage`/`?page=`. All paging hooks support the `.when(condition)` prefix. + +### 5. Spec the data contract + +Paging is the framework's responsibility; the spec covers the data shape — which rows the query selects, and in what order. + +⚠️ **A query method that takes `IMongoCollection` cannot be reached from `ReadModelScenario`.** The scenario materializes read models in memory and exposes no collection, so there is nothing to hand such a method. Spec the projection through the scenario and assert on the materialized instances; if the selection logic itself is worth pinning, keep it in a method that takes what a spec can supply. + +```csharp +void Because() => _result = _scenario.Instances.Values; +[Fact] void should_only_include_active() => _result.All(p => !p.IsArchived).ShouldBeTrue(); +``` + +## Quality gate + +- [ ] Build is clean. +- [ ] Query returns `IQueryable` (or `Task>`) for one-shot paging, or `ISubject>` for an observable paged list. Not `ISubject>` - nothing renders it. +- [ ] A meaningful default sort is applied where the data has a natural order - paging without one is unstable across storage providers, which order differently. +- [ ] The source is paged, not the answer: no `.ToList()`/`.AsQueryable()` that reads everything first. + +## See also + +- `vertical-slices.md` — read-model query return shapes. +- `react.md` — consuming paged queries (`useWithPaging`, `DataPage`). diff --git a/plugins/public-cratis-ai/skills/review-code/SKILL.md b/plugins/public-cratis-ai/skills/review-code/SKILL.md new file mode 100644 index 0000000..b179447 --- /dev/null +++ b/plugins/public-cratis-ai/skills/review-code/SKILL.md @@ -0,0 +1,86 @@ +--- +name: review-code +description: Use this skill when asked to review, check, or validate code in a Cratis-based project. Produces a structured review report with blocking issues and suggestions, checked against all project architecture and style standards. +--- + +Review changed code against all Cratis project standards and produce a structured report. + +## C# Architecture + +- [ ] Each slice is its own folder `///.cs` with all backend artifacts — **no top-level `Features/` wrapper** +- [ ] Commands: `record` type with `Handle()` directly on them — no separate handler classes +- [ ] Business rejection returns a `ValidationResult` / `Result` (or validator) — **never thrown from `Provide()`/`Handle()`** (a throw is HTTP 500, not a validation error) +- [ ] Fetched/computed handler data is in `Provide()`, not inline in `Handle()` +- [ ] Events: `record` type, no mutable/nullable properties, past tense, never carry the event-source id +- [ ] Identity concepts derive from `EventSourceId` (not `ConceptAs`) +- [ ] Projections: AutoMap is on by default — `.AutoMap()` only after `.NoAutoMap()`; projections consume events, never read models +- [ ] Model-bound query custom paths use `[Path("...")]`, not `[Route]` +- [ ] No service locator (`IServiceProvider` not injected); discover implementations with `IInstancesOf`, not `IEnumerable` +- [ ] Namespace matches folder under the app source root (`...`) + +## C# Code Style + +- [ ] File-scoped namespaces; `using` directives alphabetically sorted +- [ ] No unused `using` directives +- [ ] `is null` / `is not null` — never `== null` / `!= null` +- [ ] `var` preferred over explicit types +- [ ] No postfixes: `Async`, `Impl`, `Service` on class names +- [ ] No regions +- [ ] All public types, methods, and properties have multiline XML doc comments +- [ ] `` tags always multiline — never `/// Text` on one line +- [ ] Methods with parameters include `` for each +- [ ] Non-void methods include `` +- [ ] Custom exception types only — never `InvalidOperationException`, `ArgumentException`, etc. +- [ ] Exception XML docs start with "The exception that is thrown when …" +- [ ] Copyright header on every file +- [ ] Strongly-typed Concepts for all domain IDs/values (no raw `Guid`/`string` in domain models) + +## TypeScript Code Style + +- [ ] `const` over `let` over `var` +- [ ] Full descriptive names — never `e`, `idx`, `prev`, `dir`, `pos` +- [ ] No `any` type — `unknown` with type guards +- [ ] No `(x as any)` — use `value as unknown as TargetType` +- [ ] No unused imports +- [ ] Copyright header on every file + +## Component Rules + +- [ ] `CommandDialog` from `@cratis/components/CommandDialog` for command dialogs +- [ ] `Dialog` from `@cratis/components/Dialogs` for data-only dialogs +- [ ] Never imports `Dialog` from `primereact/dialog` directly +- [ ] No hard-coded hex/rgb colors — PrimeReact CSS variables only +- [ ] README.md present for complex component folders with multiple sub-components + +## Performance + +Performance is part of code review, not a separate pass. Flag the common degradations: + +- [ ] Projections don't join on a read model; reactors don't re-query the event log inside a handler — use event data directly +- [ ] New projections can replay all historical events without crashing; events carry no large blobs +- [ ] MongoDB queries filter on indexed fields; lists that can grow return `IQueryable` for server-side paging (never load all rows or hydrate the full collection for a count) +- [ ] No N+1 query pattern; response payloads include only fields the client uses +- [ ] React: large/growing lists page rather than render all rows; no inline object/array literals as props that change identity every render; `useEffect` deps are correct +- [ ] .NET: filter before materializing (no `.ToList()` before `.Where()`); don't enumerate an `IEnumerable` multiple times + +Classify findings: 🔴 measurable degradation at moderate load (fix before merge) · 🟡 degrades under load/scale · 🟢 minor. + +## Output format + +Start with: **Review result: ✅ Approved / ⚠️ Approved with comments / ❌ Changes requested** + +Then list issues: +``` +### + +**[BLOCKING]** Line N: `problematic code` +Because: explanation +Fix: +```corrected code``` +``` + +End with a concise summary of what passed and what must change. + +--- + +For the full expanded checklists across all categories, see [references/CHECKLISTS.md](references/CHECKLISTS.md). diff --git a/plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md b/plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md new file mode 100644 index 0000000..4c62dbe --- /dev/null +++ b/plugins/public-cratis-ai/skills/review-code/references/CHECKLISTS.md @@ -0,0 +1,109 @@ +# Code Review Checklists + +## C# Architecture + +- [ ] Each slice lives in its own folder `//.cs` (under the source root, optional `/` above) — no top-level `Features/` wrapper +- [ ] ALL backend artifacts in one file: command, validator, business rules, event, read model, projection, slice class +- [ ] No separate handler classes — `Handle()` is on the command `record` directly +- [ ] No shared mutable state between commands +- [ ] No service locator (`IServiceProvider` not injected as a dependency) +- [ ] No explicit singleton registration when `[Singleton]` attribute suffices +- [ ] Logging in a separate `*Logging.cs` partial file using `[LoggerMessage]` +- [ ] Namespace mirrors the folder path under the source root: `...` (no `Features` segment) + +## C# Commands + +- [ ] `record` type, not `class` +- [ ] All properties use `init` (immutable) +- [ ] `Handle()` is the single public entry point +- [ ] No unnecessary constructors — primary constructor only +- [ ] Validation attributes on properties (`[Required]`, `[MaxLength]`, etc.) for simple rules +- [ ] Business rules that depend on Chronicle state use a read model parameter in `Handle()` (DCB pattern) + +## C# Events + +- [ ] `record` type with no mutable (or nullable) properties; past tense; never carries the event-source id +- [ ] Decorated with `[EventType]` — **no arguments** for new events (the type name is the identifier; `generation:` only when evolving an existing contract) +- [ ] Has an XML ``; no behavior — data only +- [ ] Properties are domain types (Concepts), not raw primitives like `Guid` or `string` + +## C# Read Models & Projections + +- [ ] Read model is a `record` type +- [ ] Projection: AutoMap is on by default — `.AutoMap()` only needed after `.NoAutoMap()` +- [ ] No joins on the read model — joins are on Chronicle events only +- [ ] `ProjectionId` is a stable GUID string — never changes after first deployment +- [ ] No `ToList()`, `ToArray()`, or mutable collection exposed from public API + +## C# Concepts + +- [ ] Domain IDs/values use `ConceptAs` (see `add-concept` skill) +- [ ] No raw `Guid`, `string`, `int` used where a concept should wrap it +- [ ] Concept has `static readonly NotSet`/`Empty` sentinel +- [ ] Concept has implicit conversion from primitive +- [ ] Concept has `New()` factory if Guid-backed + +## C# Code Style + +- [ ] File-scoped namespace declaration (`namespace Foo.Bar;`) +- [ ] `using` directives alphabetically sorted, no unused ones +- [ ] `is null` / `is not null` — never `== null` / `!= null` +- [ ] `var` preferred over explicit type declarations +- [ ] No postfixes on class names: `Async`, `Impl`, `Service`, `Manager`, `Helper` +- [ ] No regions (`#region`) +- [ ] No built-in exception types: `InvalidOperationException`, `ArgumentException`, etc. +- [ ] All public types, methods, and properties have multiline XML doc comments +- [ ] `` tags always multiline — never `/// Text` on one line +- [ ] Methods with parameters include `` for each parameter +- [ ] Non-void methods include `` documentation +- [ ] Methods that throw include `` documentation +- [ ] Custom exceptions derive from `Exception`, XML doc starts with "The exception that is thrown when …" +- [ ] Copyright header on every file +- [ ] No trailing whitespace or missing newlines at end of file + +## TypeScript Architecture + +- [ ] Components placed in the slice folder (`//`) +- [ ] No `index.ts` barrel just to re-export a single component +- [ ] No technical folder groupings (`hooks/`, `utils/`, `types/`) at feature level +- [ ] Feature folder structure is functional, not technical + +## TypeScript Type Safety + +- [ ] No `any` types — `unknown` with type guards +- [ ] No `(x as any)` — use `value as unknown as TargetType` +- [ ] React synthetic events and DOM events not confused (`React.MouseEvent` vs `MouseEvent`) +- [ ] Generic defaults use `unknown`, not `any` (e.g. ``) +- [ ] No `@ts-ignore` or `@ts-expect-error` without a comment explaining why + +## TypeScript Styling + +- [ ] No hard-coded hex/rgb values — PrimeReact CSS variables (`var(--...)`) only +- [ ] CSS co-located with component (`.css` file in same folder) +- [ ] No `!important` unless justified with a comment + +## TypeScript Code Style + +- [ ] `const` over `let`, `let` over `var` +- [ ] Full descriptive names — never `e`, `evt`, `idx`, `i`, `prev`, `dir`, `pos`, `ctx` +- [ ] No async functions that don't `await` anything +- [ ] No unused variables or imports +- [ ] String enums for all enumerations (not numeric) +- [ ] Copyright header on every file + +## Component Conventions + +- [ ] `CommandDialog` from `@cratis/components/CommandDialog` for command-based dialogs +- [ ] `Dialog` from `@cratis/components/Dialogs` for data-only dialogs +- [ ] Never imports `Dialog` from `primereact/dialog` directly +- [ ] No monolithic components — decomposed into focused sub-components +- [ ] `README.md` exists for component folders with ≥2 sub-components or non-trivial architecture + +## Spec Coverage + +- [ ] Every State Change command has specs +- [ ] Happy path covered +- [ ] Every validation rule has a failure spec +- [ ] Every business rule violation has a spec +- [ ] Every constraint violation has a spec +- [ ] No spec for trivial property getters or constructor passthrough diff --git a/plugins/public-cratis-ai/skills/review-performance/SKILL.md b/plugins/public-cratis-ai/skills/review-performance/SKILL.md new file mode 100644 index 0000000..44d465f --- /dev/null +++ b/plugins/public-cratis-ai/skills/review-performance/SKILL.md @@ -0,0 +1,55 @@ +--- +name: review-performance +description: Use this skill when asked to check for performance issues, inefficiencies, or scalability problems in a Cratis-based project. Covers Chronicle projections, MongoDB query patterns, .NET allocations, and React render overhead. +--- + +Perform a focused performance review of changed code. + +## Chronicle / Event Sourcing + +- [ ] Projections use AutoMap (on by default) — avoids manual mapping cost +- [ ] Projections do NOT join on the read model (forces full re-read) +- [ ] Reactors do NOT re-query the event log inside `On()` — use event data directly +- [ ] No eager loading of entire event sequences without paging/filtering +- [ ] New projections can replay all historical events without crashing +- [ ] Events are small — no large blobs or base64-encoded content embedded + +## MongoDB / Read Models + +- [ ] Queries filter on indexed fields — no unintentional full-collection scans +- [ ] Paged queries use `.Skip()` + `.Take()` — never load all rows +- [ ] No N+1 pattern — single query returns all needed data +- [ ] Read-model records do not embed large nested collections that are never fully iterated + +## ASP.NET Core / Commands & Queries + +- [ ] Query endpoints do not hydrate the full collection when only a count is needed +- [ ] Command validators are synchronous and in-memory — no I/O in validation +- [ ] No `await Task.Run(() => syncWork)` wrapping for naturally async work +- [ ] Response payloads include only fields the client uses — no over-fetching + +## React / TypeScript + +- [ ] `DataTable` uses `lazy` + `paginator` for collections larger than ~20 rows +- [ ] No inline object/array literals passed as props (causes identity change every render) +- [ ] `useEffect` dependencies are correct — no missing deps, no over-broad deps +- [ ] Large-collection components wrapped in `React.memo` or use stable references +- [ ] No `JSON.parse(JSON.stringify(x))` deep cloning + +## General .NET + +- [ ] No LINQ `.ToList()` before `.Where()` — filter before materialising +- [ ] `IEnumerable` not enumerated multiple times — materialise once if needed +- [ ] Large object logging uses `{@obj}` only at `Debug` level + +## Risk classification + +- 🔴 High — measurable degradation at moderate load — must fix before merge +- 🟡 Medium — could degrade under load or at scale +- 🟢 Low — minor inefficiency or style issue + +## Output format + +Start with: **Performance Review: ✅ No issues / ⚠️ Minor findings / ❌ Blocking issues found** + +Group findings by category. End with a summary table showing ✅/⚠️/❌ per category. diff --git a/plugins/public-cratis-ai/skills/review-security/SKILL.md b/plugins/public-cratis-ai/skills/review-security/SKILL.md new file mode 100644 index 0000000..f0873dc --- /dev/null +++ b/plugins/public-cratis-ai/skills/review-security/SKILL.md @@ -0,0 +1,57 @@ +--- +name: review-security +description: Use this skill when asked to perform a security review or security audit of code in a Cratis-based project. Checks for injection, auth/authz, data exposure, secrets, and event-sourcing-specific vulnerabilities. +--- + +Perform a structured security review of changed code. + +## Input Validation & Injection + +- [ ] All command properties validated before use (null, empty, range, format) +- [ ] No raw SQL concatenation — parameterized queries or EF Core only +- [ ] No user-supplied values passed to `Path.Combine`, `File.*`, shell commands, or process args +- [ ] No user-supplied values used as event-store keys without sanitization + +## Authentication & Authorization + +- [ ] All HTTP endpoints decorated with `[Authorize]` or explicitly `[AllowAnonymous]` with justification +- [ ] Tenant isolation enforced — no cross-tenant data accessible without authorization +- [ ] Claims verified before acting on identity-dependent command data + +## Sensitive Data Exposure + +- [ ] No passwords, secrets, API keys, or tokens stored in event properties or read models +- [ ] No PII returned to clients that did not provide it +- [ ] Query results scoped to requesting tenant/user — never return all-tenant data + +## Secrets & Configuration + +- [ ] No secrets in source code, config files, or test fixtures +- [ ] Secrets loaded from environment variables or a secrets manager +- [ ] No hard-coded connection strings in non-test code + +## Event Sourcing Specifics + +- [ ] Events are immutable records — no mutable state in the event store +- [ ] Aggregate/event-store IDs generated server-side, never accepted from untrusted clients +- [ ] Event upcasting logic does not allow injection of unexpected properties +- [ ] Uniqueness constraints cannot be bypassed by concurrent multi-tenant writes + +## Frontend + +- [ ] No user-supplied values in `dangerouslySetInnerHTML` +- [ ] No tokens or secrets in `localStorage` — use `httpOnly` cookies or in-memory state +- [ ] Command DTOs contain only the minimum required fields +- [ ] No client-side access control not also enforced server-side + +## Risk classification + +- 🔴 Critical — must fix before merge +- 🟡 Medium — should fix soon +- 🟢 Low — fix when convenient + +## Output format + +Start with: **Security Review: ✅ No issues / ⚠️ Low-risk findings / ❌ Blocking issues found** + +Group findings by category. End with a summary table showing ✅/⚠️/❌ per category. diff --git a/plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md b/plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md new file mode 100644 index 0000000..6a293ef --- /dev/null +++ b/plugins/public-cratis-ai/skills/scaffold-feature/SKILL.md @@ -0,0 +1,82 @@ +--- +name: scaffold-feature +description: Use this skill when asked to create a new feature, section, or page that does not yet exist in a Cratis-based project. Sets up the folder, composition page, routing, and navigation entry before any slices are added. +--- + +Scaffold a brand-new feature folder with routing and navigation — ready for slices. + +## What to produce + +### 1 — Feature folder + +``` +/ ← directly under the app source root (or under an optional /) — no Features/ wrapper +├── .tsx ← composition page +├── .css ← feature-level styles (can be empty initially) +└── index.ts ← re-exports the composition page +``` + +### 2 — Composition page (`.tsx`) + +```tsx +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +import { Page } from '@cratis/components/Common'; + +export const = () => { + return ( + + {/* Slices will be composed here */} + + ); +}; +``` + +### 3 — `index.ts` + +```typescript +// Copyright (c) Cratis. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +export { } from './'; +``` + +### 4 — Update routing + +Locate the app router (typically `App.tsx` or a `routes.ts` file) and add: + +```tsx +import { } from './'; + +{ path: '', element: < /> } +``` + +### 5 — Update navigation + +Locate the sidebar/navigation configuration and add: + +```tsx +import * as mdIcons from 'react-icons/md'; + +{ + label: '', + icon: mdIcons., + url: '' +} +``` + +## Rules + +- Feature name: PascalCase (e.g. `Projects`, `Invoices`) +- Route path: kebab-case (e.g. `/projects`, `/user-management`) +- Navigation icon: from `react-icons/md` — e.g. `MdFolderOpen`, `MdPeople` +- Copyright header on every file + +## Validation + +Run `yarn lint` and `npx tsc -b`. Fix all errors. Confirm the blank page renders without runtime errors. + +## Next step + +Add slices using the `new-vertical-slice` skill. diff --git a/plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md b/plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md new file mode 100644 index 0000000..0e9133c --- /dev/null +++ b/plugins/public-cratis-ai/skills/stepper-command-dialog/SKILL.md @@ -0,0 +1,233 @@ +--- +name: stepper-command-dialog +description: Step-by-step guidance for building a multi-step wizard dialog (StepperCommandDialog) in a Cratis Arc application. Use whenever a command requires gathering information across multiple steps, implementing a wizard flow, breaking a complex form into named stages, or using StepperCommandDialog, StepperPanel, validateOnInit, or wizard-style navigation. +--- + +# StepperCommandDialog — Wizard Dialogs + +`StepperCommandDialog` organizes a single command form across multiple named steps. Users navigate with **Previous** and **Next** buttons; **Submit** only appears on the last step when every field across all steps is valid. + +Use this instead of `CommandDialog` when: +- The form has too many fields to show at once +- Fields can be grouped into logical stages (e.g. "Contact Info → Project Details → Summary") +- You want guided, linear input with per-step validation feedback +- The operation feels like a wizard or an onboarding flow + +--- + +## Step 1 — Define the command + +A single command collects all fields across all steps. Each step contributes properties to the same command instance. + +```csharp +// Projects/CreateProject/CreateProject.cs — the slice file +[Command] +public record CreateProject(ProjectName Name, EmailAddress Email, Description Description, Money Budget) +{ + public ProjectCreated Handle() => new(Name, Email, Description, Budget); +} +``` + +Run a Debug `dotnet build` to generate the `CreateProject` TypeScript proxy before importing it. + +--- + +## Step 2 — Build the dialog component + +```tsx +import { StepperCommandDialog } from '@cratis/components/CommandDialog'; +import { StepperPanel } from '@cratis/components/CommandDialog'; +import { InputTextField, TextAreaField, NumberField } from '@cratis/components/CommandForm/fields'; +import { DialogResult, useDialogContext } from '@cratis/arc.react/dialogs'; +import { CreateProject } from '../api/Projects/CreateProject'; + +const CreateProjectDialog = () => { + const { closeDialog } = useDialogContext(); + + return ( + + command={CreateProject} + title="Create New Project" + okLabel="Create" + onConfirm={() => closeDialog(DialogResult.Ok)} + onCancel={() => closeDialog(DialogResult.Cancelled)} + > + + + value={c => c.email} + title="Contact Email" + placeholder="Enter contact email" + type="email" + /> + + + + value={c => c.name} + title="Project Name" + placeholder="Enter project name" + /> + + value={c => c.description} + title="Description" + placeholder="Describe the project" + rows={4} + /> + + + + value={c => c.budget} + title="Budget" + placeholder="Enter budget" + /> + + + ); +}; +``` + +**Rules:** +- Each `StepperPanel` takes a `header` string — this is the step label shown in the wizard navigation bar +- All `CommandForm` fields inside any `StepperPanel` are bound to the **same** command instance +- Fields map to command properties via the `value={c => c.propertyName}` accessor +- The `Next` button is disabled while the current step has validation errors +- `Submit` only appears on the **last** step when all fields (across all steps) are valid + +--- + +## Step 3 — Wire the dialog to a parent component + +```tsx +import { useDialog } from '@cratis/arc.react/dialogs'; +import { Button } from 'primereact/button'; + +export const ProjectsPage = () => { + const [CreateProjectDialogWrapper, showCreateProject] = useDialog(CreateProjectDialog); + + return ( + <> +