From 55c7bcc2875feb4d93457e42985e65c13daddf1f Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 11:25:11 +0100 Subject: [PATCH 01/19] CONFIG: Bump Solution from SLN to SLNX --- LibPostalClient.sln | 61 -------------------------------------------- LibPostalClient.slnx | 18 +++++++++++++ 2 files changed, 18 insertions(+), 61 deletions(-) delete mode 100644 LibPostalClient.sln create mode 100644 LibPostalClient.slnx diff --git a/LibPostalClient.sln b/LibPostalClient.sln deleted file mode 100644 index 15e7f38..0000000 --- a/LibPostalClient.sln +++ /dev/null @@ -1,61 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.7.34202.233 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHSISL.LibPostalClient", "NHSISL.LibPostalClient\NHSISL.LibPostalClient.csproj", "{8F716F8C-89E2-4B11-8220-234499B00A33}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHSISL.LibPostalClient.Infrastructure", "NHSISL.LibPostalClient.Infrastructure\NHSISL.LibPostalClient.Infrastructure.csproj", "{B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHSISL.LibPostalClient.Tests.Unit", "NHSISL.LibPostalClient.Tests.Unit\NHSISL.LibPostalClient.Tests.Unit.csproj", "{7184373C-6377-4A12-8C05-95C2B87D56F5}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NHSISL.LibPostalClient.Tests.Acceptance", "NHSISL.LibPostalClient.Tests.Acceptance\NHSISL.LibPostalClient.Tests.Acceptance.csproj", "{AB0D7591-3461-4E2B-BA6B-08A0063F9D50}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Acceptance|Any CPU = Acceptance|Any CPU - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - Test|Any CPU = Test|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {8F716F8C-89E2-4B11-8220-234499B00A33}.Acceptance|Any CPU.ActiveCfg = Release|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Acceptance|Any CPU.Build.0 = Release|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Release|Any CPU.ActiveCfg = Release|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Release|Any CPU.Build.0 = Release|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Test|Any CPU.ActiveCfg = Release|Any CPU - {8F716F8C-89E2-4B11-8220-234499B00A33}.Test|Any CPU.Build.0 = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Acceptance|Any CPU.ActiveCfg = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Acceptance|Any CPU.Build.0 = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Release|Any CPU.Build.0 = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Test|Any CPU.ActiveCfg = Release|Any CPU - {B99B5798-18E4-4DD9-8CDB-A08F6B89CA19}.Test|Any CPU.Build.0 = Release|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Acceptance|Any CPU.ActiveCfg = Acceptance|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Acceptance|Any CPU.Build.0 = Acceptance|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Release|Any CPU.Build.0 = Release|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Test|Any CPU.ActiveCfg = Test|Any CPU - {7184373C-6377-4A12-8C05-95C2B87D56F5}.Test|Any CPU.Build.0 = Test|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Acceptance|Any CPU.ActiveCfg = Acceptance|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Acceptance|Any CPU.Build.0 = Acceptance|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Release|Any CPU.Build.0 = Release|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Test|Any CPU.ActiveCfg = Test|Any CPU - {AB0D7591-3461-4E2B-BA6B-08A0063F9D50}.Test|Any CPU.Build.0 = Test|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {48B8B835-3204-4034-8944-F129304C43D1} - EndGlobalSection -EndGlobal diff --git a/LibPostalClient.slnx b/LibPostalClient.slnx new file mode 100644 index 0000000..312a68f --- /dev/null +++ b/LibPostalClient.slnx @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + From b3442e96514d1010c635c7d81f1d47998e4c16d7 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 11:39:49 +0100 Subject: [PATCH 02/19] CONFIG: Bump .net from 8.0 to 10.0 --- .../NHSISL.LibPostalClient.Infrastructure.csproj | 4 ++-- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj b/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj index 3549069..62c6add 100644 --- a/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj +++ b/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 enable enable false @@ -10,7 +10,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 039d6b0..d4dd641 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 disable enable false diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 81f3372..7a24d41 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -1,7 +1,7 @@ - net8.0 + net10.0 disable enable false diff --git a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj index c51da00..910eb90 100644 --- a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj +++ b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj @@ -1,7 +1,7 @@  - net8.0 + net10.0 Library true disable @@ -55,8 +55,8 @@ - - + + From 05c3659c0a7e380a518c04d2801bb2e813eef7e0 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 12:57:45 +0100 Subject: [PATCH 03/19] CONFIG: Bump CompareNETObjects from 4.83.0 to 4.84.0 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index d4dd641..1415c03 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -14,7 +14,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 7a24d41..c38710d 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -10,7 +10,7 @@ - + From 69220d0bc90f29dd1e859f7fb3a1d3b56a5c90e3 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 12:59:27 +0100 Subject: [PATCH 04/19] CONFIG: Bump FluentAssertions from 6.12.1 to 7.2.2 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 1415c03..6281fca 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -16,7 +16,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index c38710d..f4d770b 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -12,7 +12,7 @@ - + From 4de32374c0db2e5504a674c2dd871fdf23da9281 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:01:01 +0100 Subject: [PATCH 05/19] CONFIG: Bump Microsoft.Extensions.Configuration from 8.0.0 to 10.0.8 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 6281fca..7ba5ae5 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -17,7 +17,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index f4d770b..4781925 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -13,7 +13,7 @@ - + From 0b3b41464bc4466e75c123d9e7532456ba27583b Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:04:57 +0100 Subject: [PATCH 06/19] CONFIG: Bump Microsoft.NET.Test.Sdk from 17.11.1 to 18.5.1 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 7ba5ae5..8494b27 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -19,7 +19,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 4781925..0e802f1 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -14,7 +14,7 @@ - + From e519c39d952b5e7c619b8f56c5d561653419a9b6 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:06:07 +0100 Subject: [PATCH 07/19] CONFIG: Bump Microsoft.Extensions.Configuration.Json from 8.0.1 to 10.0.8 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 8494b27..5595a1f 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -20,7 +20,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 0e802f1..f6db596 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -15,7 +15,7 @@ - + From 96ed3c0248458e3300ab93d1c3d1462193cb7034 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:07:31 +0100 Subject: [PATCH 08/19] CONFIG: Bump xunit from 2.9.2 to 2.9.3 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 5595a1f..2b36790 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -23,7 +23,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index f6db596..b03a565 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -18,7 +18,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive From d5aba25893a54b067313ac06b11b10227d02330a Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:08:32 +0100 Subject: [PATCH 09/19] CONFIG: Bump Xeption from 2.8.0 to 2.9.0 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 2b36790..9c8fee3 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -24,7 +24,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index b03a565..1199108 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -19,7 +19,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From a76773176aad3d28032242d86695b666ebbe6ed8 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:10:56 +0100 Subject: [PATCH 10/19] CONFIG: Bump xunit.runner.visualstudio from 2.8.2 to 3.1.5 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 9c8fee3..f4303e4 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -25,7 +25,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 1199108..fbf1f3d 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -20,7 +20,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 928fa1d9310223c80f9254bf5b5188b6eda16a8a Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:12:45 +0100 Subject: [PATCH 11/19] CONFIG: Bump coverlet.collector from 6.0.2 to 10.0.0 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index f4303e4..8fa681c 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -29,7 +29,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index fbf1f3d..4203a99 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -24,7 +24,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all From 243f2f534cc3dc31ac4215b1765870532055ae96 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:14:13 +0100 Subject: [PATCH 12/19] CONFIG: Bump Microsoft.Extensions.DependencyInjection from 9.0.0 to 10.0.8 --- NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj index 910eb90..25e7b18 100644 --- a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj +++ b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj @@ -99,7 +99,7 @@ - + From 8f4aa580a370b26a252ee84c1069a199282c5682 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:15:28 +0100 Subject: [PATCH 13/19] CONFIG: Bump Microsoft.Extensions.Hosting from 8.0.0 to 10.0.8 --- NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj index 25e7b18..0319a8e 100644 --- a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj +++ b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj @@ -100,7 +100,7 @@ - + From 02f7581f6b2402e6951090125634f2d1107e7d4b Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:16:31 +0100 Subject: [PATCH 14/19] CONFIG: Bump Newtonsoft.Json from 13.0.3 to 13.0.4 --- NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj index 0319a8e..a0eb03b 100644 --- a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj +++ b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj @@ -101,7 +101,7 @@ - + From 80d8b051a7a50f3e57bde50e8ce8df156ae5b651 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:19:43 +0100 Subject: [PATCH 15/19] CONFIG: Bump Xeption from 2.8.0 to 2.9.0 --- NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj index a0eb03b..1ab4d34 100644 --- a/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj +++ b/NHSISL.LibPostalClient/NHSISL.LibPostalClient.csproj @@ -102,7 +102,7 @@ - + From 3ed6825329e63b44c5ae4090a778c10c1c4855f0 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:20:52 +0100 Subject: [PATCH 16/19] CONFIG: Bump ADotNet from 3.0.5 to 4.2.0 --- .../NHSISL.LibPostalClient.Infrastructure.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj b/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj index 62c6add..16da118 100644 --- a/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj +++ b/NHSISL.LibPostalClient.Infrastructure/NHSISL.LibPostalClient.Infrastructure.csproj @@ -10,7 +10,7 @@ - + From ae65109084d7b59cf0ab8465373366c72bfbdbb7 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:24:33 +0100 Subject: [PATCH 17/19] CONFIG: Replace CleanMoq 42.42.42 with Moq 4.20.72 --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 8fa681c..690a691 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -21,7 +21,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 4203a99..9c847de 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -16,7 +16,7 @@ - + From 141ef65916805fd1fe8ce791c57eac7986081fe5 Mon Sep 17 00:00:00 2001 From: perirrs Date: Wed, 13 May 2026 13:26:47 +0100 Subject: [PATCH 18/19] CONFIG: Lock FluentAssertions to [7.2.2] --- .../NHSISL.LibPostalClient.Tests.Acceptance.csproj | 2 +- .../NHSISL.LibPostalClient.Tests.Unit.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj index 690a691..20fe057 100644 --- a/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj +++ b/NHSISL.LibPostalClient.Tests.Acceptance/NHSISL.LibPostalClient.Tests.Acceptance.csproj @@ -16,7 +16,7 @@ - + diff --git a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj index 9c847de..7e45138 100644 --- a/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj +++ b/NHSISL.LibPostalClient.Tests.Unit/NHSISL.LibPostalClient.Tests.Unit.csproj @@ -12,7 +12,7 @@ - + From b1bce26665a5b86ddc65ca099aed83be1ff0a7ea Mon Sep 17 00:00:00 2001 From: Raghu Peri Date: Thu, 14 May 2026 09:32:18 +0100 Subject: [PATCH 19/19] CODE RUB: Added the-standard-skills 1 Like reaction. --- .../skills/the-standard-architecture/SKILL.md | 1306 +++++++++++++++ .../contracts/contracts.json | 104 ++ .../examples/bad/example_bad_broker.cs | 103 ++ .../examples/bad/example_bad_service.cs | 63 + .../examples/good/example_broker.cs | 154 ++ .../good/example_foundation_service.cs | 312 ++++ .../examples/good/example_logging_broker.cs | 78 + .../good/example_processing_service.cs | 133 ++ .../the-standard-architecture/manifest.json | 71 + .../rules/rules.json | 60 + .../the-standard-architecture/rules/rules.md | 73 + .../templates/broker_template.cs | 131 ++ .../templates/foundation_service_template.cs | 338 ++++ .../templates/logging_broker_template.cs | 60 + .../templates/processing_service_template.cs | 182 ++ .../validations/anti-patterns.md | 170 ++ .../validations/checklist.md | 93 + .../skills/the-standard-code-csharp/SKILL.md | 966 +++++++++++ .../contracts/contracts.json | 79 + .../examples/bad/example_bad_classes.cs | 61 + .../examples/bad/example_bad_methods.cs | 66 + .../examples/bad/example_bad_variables.cs | 59 + .../examples/good/example_classes.cs | 82 + .../examples/good/example_methods.cs | 88 + .../examples/good/example_variables.cs | 73 + .../the-standard-code-csharp/manifest.json | 58 + .../the-standard-code-csharp/rules/rules.json | 51 + .../the-standard-code-csharp/rules/rules.md | 78 + .../templates/broker_template.cs | 88 + .../templates/service_template.cs | 96 ++ .../validations/anti-patterns.md | 220 +++ .../validations/checklist.md | 116 ++ .agents/skills/the-standard-core/SKILL.md | 280 ++++ .../contracts/contracts.json | 104 ++ .../examples/bad/example_bad_modeling.md | 83 + .../examples/bad/example_bad_purposing.md | 75 + .../examples/good/example_modeling.md | 90 + .../examples/good/example_purposing.md | 52 + .../skills/the-standard-core/manifest.json | 59 + .../skills/the-standard-core/rules/rules.json | 54 + .../skills/the-standard-core/rules/rules.md | 98 ++ .../templates/system_design_template.md | 117 ++ .../validations/anti-patterns.md | 186 ++ .../validations/checklist.md | 110 ++ .agents/skills/the-standard-events/SKILL.md | 157 ++ .../contracts/contracts.json | 158 ++ .../examples/bad/example_bad_event_service.cs | 179 ++ .../example_event_service_orchestration_di.cs | 171 ++ .../good/example_event_service_publish.cs | 361 ++++ .../good/example_event_service_subscribe.cs | 299 ++++ .../skills/the-standard-events/manifest.json | 101 ++ .../the-standard-events/rules/rules.json | 54 + .../skills/the-standard-events/rules/rules.md | 78 + .../brokers/event_broker_template.cs | 131 ++ .../foundations/event_service_template.cs | 186 ++ ...ent_service_test_class_publish_template.cs | 153 ++ .../event_service_test_class_root_template.cs | 56 + ...t_service_test_class_subscribe_template.cs | 154 ++ .../event_service_test_publish_template.cs | 154 ++ .../event_service_test_subscribe_template.cs | 157 ++ .../validations/anti-patterns.md | 346 ++++ .../validations/checklist.md | 118 ++ .../skills/the-standard-practices/SKILL.md | 1490 +++++++++++++++++ .../contracts/contracts.json | 76 + .../examples/bad/example_bad_branches.md | 72 + .../examples/bad/example_bad_commits.md | 79 + .../examples/good/example_branch_names.md | 80 + .../examples/good/example_commit_messages.md | 91 + .../the-standard-practices/manifest.json | 59 + .../the-standard-practices/rules/rules.json | 37 + .../the-standard-practices/rules/rules.md | 74 + .../templates/branch_naming_guide.md | 100 ++ .../templates/pull_request_template.md | 76 + .../validations/anti-patterns.md | 152 ++ .../validations/checklist.md | 67 + .agents/skills/the-standard-testing/SKILL.md | 552 ++++++ .../contracts/contracts.json | 85 + .../examples/bad/example_bad_test.cs | 90 + .../examples/good/example_foundation_test.cs | 107 ++ .../examples/good/example_validation_test.cs | 135 ++ .../skills/the-standard-testing/manifest.json | 88 + .../the-standard-testing/rules/rules.json | 99 ++ .../the-standard-testing/rules/rules.md | 155 ++ ...ler_acceptance_test_class_root_template.cs | 92 + .../controller_acceptance_test_template.cs | 55 + ...ontroller_unit_test_class_root_template.cs | 113 ++ .../unit/controller_unit_test_template.cs | 136 ++ .../foundation_service_test_template.cs | 282 ++++ .../foundation_test_class_root_template.cs | 72 + .../processing_service_test_template.cs | 351 ++++ .../processing_test_class_root_template.cs | 111 ++ .../templates/service_test_template.cs | 274 +++ .../templates/test_class_root_template.cs | 71 + .../validations/anti-patterns.md | 166 ++ .../validations/checklist.md | 89 + .../skills/the-standard-versioning/SKILL.md | 576 +++++++ .../contracts/contracts.json | 125 ++ .../enforcement/enforcement.json | 213 +++ .../the-standard-versioning/manifest.json | 50 + .../the-standard-versioning/rules/rules.json | 28 + .../the-standard-versioning/rules/rules.md | 37 + .../validations/checklist.md | 95 ++ skills-lock.json | 47 + 103 files changed, 16680 insertions(+) create mode 100644 .agents/skills/the-standard-architecture/SKILL.md create mode 100644 .agents/skills/the-standard-architecture/contracts/contracts.json create mode 100644 .agents/skills/the-standard-architecture/examples/bad/example_bad_broker.cs create mode 100644 .agents/skills/the-standard-architecture/examples/bad/example_bad_service.cs create mode 100644 .agents/skills/the-standard-architecture/examples/good/example_broker.cs create mode 100644 .agents/skills/the-standard-architecture/examples/good/example_foundation_service.cs create mode 100644 .agents/skills/the-standard-architecture/examples/good/example_logging_broker.cs create mode 100644 .agents/skills/the-standard-architecture/examples/good/example_processing_service.cs create mode 100644 .agents/skills/the-standard-architecture/manifest.json create mode 100644 .agents/skills/the-standard-architecture/rules/rules.json create mode 100644 .agents/skills/the-standard-architecture/rules/rules.md create mode 100644 .agents/skills/the-standard-architecture/templates/broker_template.cs create mode 100644 .agents/skills/the-standard-architecture/templates/foundation_service_template.cs create mode 100644 .agents/skills/the-standard-architecture/templates/logging_broker_template.cs create mode 100644 .agents/skills/the-standard-architecture/templates/processing_service_template.cs create mode 100644 .agents/skills/the-standard-architecture/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-architecture/validations/checklist.md create mode 100644 .agents/skills/the-standard-code-csharp/SKILL.md create mode 100644 .agents/skills/the-standard-code-csharp/contracts/contracts.json create mode 100644 .agents/skills/the-standard-code-csharp/examples/bad/example_bad_classes.cs create mode 100644 .agents/skills/the-standard-code-csharp/examples/bad/example_bad_methods.cs create mode 100644 .agents/skills/the-standard-code-csharp/examples/bad/example_bad_variables.cs create mode 100644 .agents/skills/the-standard-code-csharp/examples/good/example_classes.cs create mode 100644 .agents/skills/the-standard-code-csharp/examples/good/example_methods.cs create mode 100644 .agents/skills/the-standard-code-csharp/examples/good/example_variables.cs create mode 100644 .agents/skills/the-standard-code-csharp/manifest.json create mode 100644 .agents/skills/the-standard-code-csharp/rules/rules.json create mode 100644 .agents/skills/the-standard-code-csharp/rules/rules.md create mode 100644 .agents/skills/the-standard-code-csharp/templates/broker_template.cs create mode 100644 .agents/skills/the-standard-code-csharp/templates/service_template.cs create mode 100644 .agents/skills/the-standard-code-csharp/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-code-csharp/validations/checklist.md create mode 100644 .agents/skills/the-standard-core/SKILL.md create mode 100644 .agents/skills/the-standard-core/contracts/contracts.json create mode 100644 .agents/skills/the-standard-core/examples/bad/example_bad_modeling.md create mode 100644 .agents/skills/the-standard-core/examples/bad/example_bad_purposing.md create mode 100644 .agents/skills/the-standard-core/examples/good/example_modeling.md create mode 100644 .agents/skills/the-standard-core/examples/good/example_purposing.md create mode 100644 .agents/skills/the-standard-core/manifest.json create mode 100644 .agents/skills/the-standard-core/rules/rules.json create mode 100644 .agents/skills/the-standard-core/rules/rules.md create mode 100644 .agents/skills/the-standard-core/templates/system_design_template.md create mode 100644 .agents/skills/the-standard-core/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-core/validations/checklist.md create mode 100644 .agents/skills/the-standard-events/SKILL.md create mode 100644 .agents/skills/the-standard-events/contracts/contracts.json create mode 100644 .agents/skills/the-standard-events/examples/bad/example_bad_event_service.cs create mode 100644 .agents/skills/the-standard-events/examples/good/example_event_service_orchestration_di.cs create mode 100644 .agents/skills/the-standard-events/examples/good/example_event_service_publish.cs create mode 100644 .agents/skills/the-standard-events/examples/good/example_event_service_subscribe.cs create mode 100644 .agents/skills/the-standard-events/manifest.json create mode 100644 .agents/skills/the-standard-events/rules/rules.json create mode 100644 .agents/skills/the-standard-events/rules/rules.md create mode 100644 .agents/skills/the-standard-events/templates/brokers/event_broker_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_test_class_publish_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_test_class_root_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_test_class_subscribe_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_test_publish_template.cs create mode 100644 .agents/skills/the-standard-events/templates/foundations/event_service_test_subscribe_template.cs create mode 100644 .agents/skills/the-standard-events/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-events/validations/checklist.md create mode 100644 .agents/skills/the-standard-practices/SKILL.md create mode 100644 .agents/skills/the-standard-practices/contracts/contracts.json create mode 100644 .agents/skills/the-standard-practices/examples/bad/example_bad_branches.md create mode 100644 .agents/skills/the-standard-practices/examples/bad/example_bad_commits.md create mode 100644 .agents/skills/the-standard-practices/examples/good/example_branch_names.md create mode 100644 .agents/skills/the-standard-practices/examples/good/example_commit_messages.md create mode 100644 .agents/skills/the-standard-practices/manifest.json create mode 100644 .agents/skills/the-standard-practices/rules/rules.json create mode 100644 .agents/skills/the-standard-practices/rules/rules.md create mode 100644 .agents/skills/the-standard-practices/templates/branch_naming_guide.md create mode 100644 .agents/skills/the-standard-practices/templates/pull_request_template.md create mode 100644 .agents/skills/the-standard-practices/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-practices/validations/checklist.md create mode 100644 .agents/skills/the-standard-testing/SKILL.md create mode 100644 .agents/skills/the-standard-testing/contracts/contracts.json create mode 100644 .agents/skills/the-standard-testing/examples/bad/example_bad_test.cs create mode 100644 .agents/skills/the-standard-testing/examples/good/example_foundation_test.cs create mode 100644 .agents/skills/the-standard-testing/examples/good/example_validation_test.cs create mode 100644 .agents/skills/the-standard-testing/manifest.json create mode 100644 .agents/skills/the-standard-testing/rules/rules.json create mode 100644 .agents/skills/the-standard-testing/rules/rules.md create mode 100644 .agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_class_root_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_class_root_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/foundations/foundation_service_test_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/foundations/foundation_test_class_root_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/processings/processing_service_test_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/processings/processing_test_class_root_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/service_test_template.cs create mode 100644 .agents/skills/the-standard-testing/templates/test_class_root_template.cs create mode 100644 .agents/skills/the-standard-testing/validations/anti-patterns.md create mode 100644 .agents/skills/the-standard-testing/validations/checklist.md create mode 100644 .agents/skills/the-standard-versioning/SKILL.md create mode 100644 .agents/skills/the-standard-versioning/contracts/contracts.json create mode 100644 .agents/skills/the-standard-versioning/enforcement/enforcement.json create mode 100644 .agents/skills/the-standard-versioning/manifest.json create mode 100644 .agents/skills/the-standard-versioning/rules/rules.json create mode 100644 .agents/skills/the-standard-versioning/rules/rules.md create mode 100644 .agents/skills/the-standard-versioning/validations/checklist.md create mode 100644 skills-lock.json diff --git a/.agents/skills/the-standard-architecture/SKILL.md b/.agents/skills/the-standard-architecture/SKILL.md new file mode 100644 index 0000000..947404e --- /dev/null +++ b/.agents/skills/the-standard-architecture/SKILL.md @@ -0,0 +1,1306 @@ +--- +name: The Standard Architecture +description: Enforces Standard-compliant modeling, brokers, services, aggregation, exposers, REST APIs, and UI architecture. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard Architecture + +## What this skill is + +This skill operationalizes the architecture chapters of The Standard. +It governs how systems are decomposed, how responsibilities are assigned, and how dependencies flow. +It includes the architecture of models, brokers, services, aggregation, exposers, communication protocols, APIs, and user interfaces. + +## Explicit coverage map + +This skill explicitly covers: + +- 0.1.2 Modeling + - Data Carrier Models + - Primary Models + - Secondary Models + - Relational Models + - Hybrid Models + - Operational Models + - Integration Models (Brokers) + - Processing Models (Services) + - Exposure Models (Exposers) + - Configuration Models +- 1 Brokers + - Introduction + - On The Map + - Characteristics + - Implements a Local Interface + - No Flow Control + - No Exception Handling + - Own Their Configurations + - Natives from Primitives + - Naming Conventions + - Language + - Up & Sideways + - One Resource, One Broker + - Organization + - Broker Types + - Entity Brokers + - Support Brokers + - Implementation + - Asynchronization Abstraction + - FAQs / Clarifications + - Standardized Provider Abstraction Libraries (SPAL) + - Extensibility + - Configurability + - Distributability + - External Mockability (Cloud-Foreign) + - Local to Global +- 2 Services + - Introduction + - Services Operations + - Validations + - Processing + - Integration + - Services Types + - Validators + - Orchestrators + - Aggregators + - Overall Rules + - Do or Delegate + - Two-Three (Florance Pattern) + - Single Exposure Point + - Same or Primitives I/O Model + - Every Service for Itself + - Flow Forward + - For APIs + - Foundation Services (Broker-Neighboring) + - Introduction + - On The Map + - Characteristics + - Pure-Primitive + - Single Entity Integration + - Business Language + - Responsibilities + - Abstraction + - Validation + - Circuit-Breaking Validations + - Continuous Validations + - Upsertable Exceptions + - Dynamic Rules + - Rules & Validations Collector + - Hybrid Continuous Validations + - Structural Validations + - Logical Validations + - External Validations + - Dependency Validations + - Mapping + - Non-Local Models + - Exception Handling + - Exceptions Mappings + - Localise External Exceptions + - Caragorize Exceptions + - Logging + - Processing Services (Higher-Order Business Logic) + - Introduction + - On The Map + - Characteristics + - Language + - Functions Language + - Pass-Through + - Class-Level Language + - Dependencies + - One-Foundation + - Used-Data-Only Validations + - Responsibilities + - Higher-Order Logic + - Shifters + - Combinations + - Signature Mapping + - Non-Exception Local Models + - Exception Handling + - Unwrap and Localise Foundation Exceptions + - Caragorize Exceptions + - Logging + - Orchestration Services (Complex Higher Order Logic) + - Introduction + - On The Map + - Characteristics + - Language + - Functions Language + - Pass-Through + - Class-Level Language + - Dependencies + - Dependency Balance (Florance Pattern) + - Two-Three + - Full-Normalization + - Semi-Normalization + - No-Normalization + - Meaningful Breakdown + - Contracts + - Physical Contracts + - Virtual Contracts + - Cul-De-Sac + - Responsibilities + - Advanced Logic + - Flow Combinations + - Call Order + - Natural Order + - Enforced Order + - Exception Handling + - Unwrap and Localise Foundation Exceptions + - Caragorize Exceptions + - Logging + - Variations + - Coordination + - Management + - Uber Management + - Unit of Work + - Aggregation Services (The Knot) + - Introduction + - On The Map + - Characteristics + - No Dependency Limitation + - No Order Validation + - Basic Validations + - Pass-Through + - Optionality + - Routine-Level Aggregation + - Pure Dependency Contracts + - Responsibilities + - Abstraction + - Exceptions Aggregation +- 3 Exposers + - Introduction + - Purpose + - Pure Mapping + - Types of Exposure Components + - Communication Protocols + - User Interfaces + - I/O Components + - Single Point of Contact + - Summary +- 3.1 Communication Protocols + - Principles & Rules + - Results Communication + - Error Reports + - RESTful APIs + - Language + - Beyond CRUD Routines + - Similar Verbs + - Route Conventions + - Nouns & Verbs + - Controller Routes + - Routine/Method-Specific Routes + - Plural-Singular-Plural + - Query Parameters & OData + - X-WWW-Form-UrlEncoded Parameters + - Codes & Responses + - Success Codes (2xx) + - User Error Codes (4xx) + - System Error Codes (5xx) + - All Codes + - Single Dependency + - Single Contract + - Organization + - Home Controller +- 3.2 User Interfaces + - Principles & Rules + - Progress (Loading) + - Basic Progress + - Remaining Progress + - Detailed Progress + - Results + - Simple + - Partial Details + - Full Details + - Error Reports + - Informational + - Referential / Implicit Actions + - Actionable + - Single Dependency + - Anatomy + - Bases + - Components + - Containers + - UI Component Types + - Web Applications + - Mobile Applications + - Other Types + - Web Applications + - Anatomy + - Base Component + - Implementation + - Utilization + - Restrictions + - Core Component + - Elements + - Existence + - Property Assignment + - Searching by Id + - General Search + - Properties + - Actions + - Styles + - Actions + - Restrictions + - Pages + - Unobtrusiveness + - Organization + + The following topics are governed by dedicated skills that extend this skill. + See `## Related skills` for activation guidance: + - CulDeSac event brokers, event services, publish/subscribe, DI registration, and startup activation → `the-standard-events` + - Release versioning, file versioning, API versioning, and deprecation → `the-standard-versioning` + +## When to use + +Use this skill for system design, architecture review, decomposition, refactoring, API design, UI architecture, dependency-flow design, or when deciding where behavior belongs. + +## Related skills + +This skill defines the structural rules for the entire system. +When a specific architectural pattern is chosen, a dedicated skill governs its detailed implementation. +Activate the relevant skill as soon as the pattern is identified -- do not wait until implementation is underway. + +| Pattern | Skill | Activate when | +|---|---|---| +| CulDeSac eventing: event broker, event service, publish/subscribe, DI registration, startup activation | `the-standard-events` | An orchestration service needs to publish a domain event or subscribe to one without a synchronous return | +| Release versioning, file versioning, API versioning, deprecation | `the-standard-versioning` | A model, service, or API contract change is introduced, a release is being cut, or existing code must be deprecated | + +## System-wide architecture rules + +0. Every system must be decomposed into purpose, dependency, and exposure. +1. At low level, that means: + - Brokers = dependencies + - Services = purpose + - Exposers = exposure +2. Keep the flow forward: + - Exposer -> Service -> Broker -> External resource +3. Never reverse the flow. +4. Never blur layer boundaries. +5. Never hide architecture behind magical abstractions. + +## Modeling rules + +### Data-carrier models + +0. Primary models are pillars of the system. + - They are physically self-sufficient. +1. Secondary models depend on primary models. +2. Relational models connect primary models and should mainly hold references. +3. Hybrid models are allowed only when a relationship itself must carry state or details. +4. Keep physical models anemic and flat. +5. Use virtual contracts only when orchestration truly needs them. + +### Operational models + +0. Integration models are brokers. +1. Processing models are services. +2. Exposure models are exposers. +3. Configuration models compose the system and manage startup, DI, middleware, or platform-specific configuration. + +## Broker rules + +0. A broker is a liaison between business logic and the outside world. +1. Brokers must implement a local interface. +2. Brokers must contain no business logic. +3. Brokers must contain no flow control. + - No if statements for business decisions. + - No loops for business decisions. + - No switch cases for business decisions. +4. Brokers must not handle exceptions. + - Let native exceptions propagate to broker-neighboring services. +5. Brokers must own their configurations. +6. Brokers may construct native models from primitive inputs. +7. Brokers must speak the language of their technology. + - Storage -> Insert / Select / Update / Delete + - Queue -> Enqueue / Dequeue + - REST -> Get / Post / Put / Delete / custom HTTP method when needed +8. Brokers cannot call other brokers. +9. Brokers cannot depend on services. +10. One resource, one broker. +11. Use support brokers for generic capabilities such as time and logging. +12. Use entity brokers / api brokers for resource- or entity-specific integrations. +13. Prefer partial interfaces and partial classes to organize multi-entity brokers. +14. Prefer generic helper methods in broker root partials so entity partials do not touch native clients directly. +15. Use asynchronous abstractions consistently. +16. Prefer ValueTask in Standard examples and abstractions when that aligns with the implementation profile. +17. Brokers live under Brokers/ and their namespaces. +18. Broker configurations live in appsettings.json or equivalent configuration stores +19. Broker configuration classes live under Brokers/ and their namespaces. + +### Asynchronization Abstraction (§1.5.1) + +Every publicly exposed interface method — on brokers, services, and exposers — must +return `ValueTask` or `ValueTask`, **even if the current implementation does not +internally await anything**. + +This is The Standard §1.5.1: the public contract is uniformly async so callers never +need to change if an implementation later becomes truly asynchronous. + +| Pattern | Verdict | +|---|---| +| `public async ValueTask LogWarningAsync(string message) => this.logger.LogWarning(message);` | **Correct** — async keyword, direct call | +| `public ValueTask> SelectAllStudentsAsync() => ValueTask.FromResult(...)` | **Correct** — ValueTask.FromResult wraps sync result | +| `public async ValueTask> SelectAllStudentsAsync() => await this.SelectAllAsync();` | **Correct** — async/await through generic helper | +| `public IQueryable SelectAllStudents() => this.Students.AsNoTracking();` | **WRONG** — synchronous, no ValueTask | +| `public ValueTask LogWarningAsync(string message) => new ValueTask(Task.Run(() => ...));` | **WRONG** — Task.Run wraps a synchronous call needlessly | + +**Consequence for services:** `CreateAndLog*` helpers must be async too, because +`ILoggingBroker.LogErrorAsync` returns `ValueTask`. Catch blocks must use `throw await`: + +```csharp +// WRONG +private StudentValidationException CreateAndLogValidationException(Xeption exception) +{ + ... + this.loggingBroker.LogError(studentValidationException); // no such sync method + return studentValidationException; +} + +// CORRECT +private async ValueTask CreateAndLogValidationException( + Xeption exception) +{ + var studentValidationException = new StudentValidationException(...); + await this.loggingBroker.LogErrorAsync(studentValidationException); + return studentValidationException; +} + +// CORRECT call site in TryCatch +catch (NullStudentException nullStudentException) +{ + throw await CreateAndLogValidationException(nullStudentException); +} +``` + +### Broker clarifications + +0. Brokers are broader than repositories. +1. Providers are not the same as brokers. +2. Native exceptions may leak through brokers by design; foundation services localize them. +3. Partialization is preferred for multi-entity brokers because configuration ownership stays centralized. +4. Suppress warnings at the project level when truly needed; otherwise fix them. + +### SPAL rules + +0. Standardized Provider Abstraction Libraries must be extensible. +1. They must be configurable. +2. They must be distributable. +3. They must support external mockability for local / airplane-mode operation. +4. They should move from local need to global reusable library when possible. +5. They are subsystems and should themselves follow Brokers / Services / Exposers. + +## Service-wide rules + +0. Services contain business logic. +1. Service operations break into validations, processing, and integration. +2. Service types break into validators, orchestrators, and aggregators. +3. Do or delegate, but not both. +4. Enforce Florance Pattern where applicable. +5. Exposure layers must have a single point of contact with business logic. +6. Services should accept and return the same contract or primitives/aggregations of that contract. + - Methods that has primitive inputs must consider using a contract model count exceed three. +7. Every service validates its own inputs and outputs. +8. Services cannot call other services at the same level. +9. Service methods cannot call other service methods at the same level. + - If shared logic exists, extract it to a private method that both public methods can call. +10. Public APIs cannot call public APIs at the same level. +11. Flow forward only. + +## Foundation service rules + +0. Foundation services are broker-neighboring services. +1. Their purpose is validation, abstraction, mapping, and primitive business language. +2. They must remain pure-primitive. + - No multi-step higher-order business logic. +3. They must integrate with one entity broker only. +4. Support brokers like logging and time are allowed. +5. They must translate technology language to business language. + - Insert -> Add + - Select -> Retrieve + - Update -> Modify + - Delete -> Remove +6. They must wrap logic in TryCatch / exception-noise-cancellation. +7. They must perform validation before dependency calls. +8. They are the last abstraction layer before core business logic. +9. Foundation services live under Services/Foundations. +10. Fondation service models live under Models/Foundations/{Entity Plural}/{Entity}.cs +11. Fondation service exceptionmodels live under Models/Foundations/{Entity Plural}/Exceptions/ + +### Validation rules + +0. Validation order is mandatory: + - Structural + - Logical + - External +1. Circuit-breaking validations exit immediately. +2. Continuous validations accumulate errors, then break the circuit. +3. Use upsertable exceptions for accumulated validation data. +4. Use dynamic rules that include both condition and human-readable message. +5. Use a rule collector to aggregate and then throw. +6. Use hybrid continuous validation for nested models. +7. Validate outgoing data when the current routine uses it. +8. Map native failures into local exception models. + +### Exception rules for foundation services + +0. Localize native exceptions. + - The data dictionary from native exception must be assigned to the localised exception's data dictionary. + - The inner exception of the localised exception must be the native exception when possible. +1. Categorize exceptions into validation, dependency validation, dependency, and service exceptions. +2. Preserve inner localized exceptions when moving upstream. +3. Log at the appropriate severity. +4. Critical dependency failures are infrastructure/configuration failures. +5. Do not leak raw native exception semantics into upstream pure business logic. + +## Processing service rules + +0. Processing services hold higher-order single-entity business logic. +1. They may combine primitive operations from one foundation service. +2. They may use utility brokers. +3. They may not use entity brokers directly. +4. They may depend on one and only one foundation service. +5. Their naming must include the entity and the Processing suffix. +6. They validate only what they use from the input. +7. They may shift outcomes to primitives such as bool or int. +8. They may combine multiple primitive routines into one higher-order routine. +9. They map exceptions from foundation layer to processing-layer exception categories. +10. They must localise and categorise exceptions from the foundation layer. +11. Processing services live under Services/Processings. +10. Processing service virtual models live under Models/Processings/{Entity Plural}/{Entity}.cs +11. Processing service exceptionmodels live under Models/Processings/{Entity Plural}/Exceptions/ + +## Orchestration service rules + +0. Orchestration services combine multi-entity operations. +1. They may depend on foundation services or processing services, but not a mixed set of both for entity/business dependencies. +2. Utility brokers are allowed in addition. +3. Florance Pattern is mandatory. +4. Two-Three rule is mandatory for entity/business service dependencies. +5. If dependencies exceed the rule, normalize. + - Full normalization + - Semi-normalization + - No-normalization only as the last option +6. Every breakdown must be meaningful. +7. Orchestration services may be pass-through when contract purity is preserved. +8. Orchestration services may expose physical contracts or virtual contracts. +9. They are responsible for mapping and branching. +10. They are responsible for calling dependencies in the proper order. +11. Natural order is preferred when dependencies require it. +12. Enforced order must be tested when the call dependency is not naturally encoded in input/output relationships. +13. Cul-De-Sac orchestration is valid for event/listener scenarios. +14. Variants include coordination, management, and uber-management services. +15. Keep a unit-of-work mindset. +16. Prefer eventing when it reduces orchestration complexity safely. +17. They map exceptions from foundation layer or processing layer to orchestration-layer exception categories. +18. They must localise and categorise exceptions from the foundation layer or processing layer. +19. Orchestration services live under Services/Orchestrations. +20. Orchestration service virtual models live under Models/Orchestrations/{Entity Plural}/{Entity}.cs +21. Orchestration service exceptionmodels live under Models/Orchestrations/{Entity Plural}/Exceptions/ +22. When Cul-De-Sac eventing is chosen (rules 13 and 16), activate `the-standard-events` skill -- it governs the event broker, event service, validation, exception handling, DI lifetime, and startup activation for the event layer. + +## Aggregation service rules + +0. Aggregation services are the knot at the border of core business logic. +1. They provide a single exposure point when many same-variation services share the same contract. +2. They do not add business logic. +3. They can have many same-variation dependencies. +4. They do not validate call order. +5. They only perform basic structural validations. +6. They may aggregate by multi-call routine or by pass-through methods. +7. They are optional. +8. Their dependencies must share the same contract family. +9. They must aggregate exceptions the same way orchestration-like services do. +10. They must localise and categorise exceptions the same way orchestration-like services do. +11. Aggregation services lives under Services/Aggregations. +12. Aggregation service exceptionmodels live under Models/Aggregations/{Entity Plural}/Exceptions/ + +## Exposer rules + +0. Exposers are disposable mapping layers. +1. Their purpose is duplex mapping in and out of the core business logic. +2. They are pure mapping only. +3. They may not talk to brokers. +4. They may not contain business logic. +5. They may have one and only one service dependency. +6. That dependency must be a service, not a broker. +7. They must provide a single point of contact. + +### Exposure component types + +0. Communication protocols +1. User interfaces +2. I/O components / background daemons + +## Communication protocol rules + +0. Return core-business results in the protocol’s form. +1. Return error reports faithfully and with standardized codes. +2. Support result communication and error reporting. +3. Keep mapping thin and explicit. + +## RESTful API rules + +0. Controllers speak HTTP verb language. + - Post / Get / Put / Delete +1. Custom verbs are allowed when the business operation goes beyond CRUD and the standard verbs do not fit. +2. Similar verbs are allowed across different routines when names and routes differentiate them. +3. Routes must never contain verbs. +4. Routes must use nouns. +5. Enforce the single-noun principle. +6. Prefer resource intersections to noun-collisions. +7. Controller classes are plural. +8. Controller routes should usually follow api/[controller]. +9. Method-specific routes extend the controller route. +10. Follow plural-singular-plural route patterns for nested resource intersections. +11. Use query parameters and OData where appropriate for queryable reads. +12. x-www-form-urlencoded is allowed for form-style endpoint inputs. +13. Controller methods should not accept more than three parameters; beyond that, design a model. +14. Controllers have one service dependency. +15. Controllers honor a single contract family. +16. Controllers live under Controllers. +17. Every system should have a HomeController heartbeat endpoint. +18. HomeController should not require security and should only indicate aliveness. + +### REST response code rules + +0. Success responses: + - 200 OK for successful GET / PUT / DELETE style outcomes + - 201 Created for successful POSTs + - 202 Accepted for delegated or eventual-consistency submissions +1. User error responses: + - 400 BadRequest for validation and dependency-validation issues when mapped to user-correctable input/domain problems + - 404 NotFound for not-found scenarios + - 409 Conflict for already-exists scenarios + - 423 Locked for locked-resource scenarios + - 424 FailedDependency for invalid-reference scenarios +2. System error responses: + - 500 InternalServerError for dependency or service failures + - 507 InsufficientStorage for internal storage issues when applicable +3. Preserve security boundaries. + - Do not expose internal details from dependency/service failures unless the protocol requires a sanitized representation. + +## User interface rules + +0. UI exposers must map progress, results, and errors. +1. Never fake progress. +2. Choose progress reporting level intentionally: + - Basic progress + - Remaining progress + - Detailed progress +3. Choose results reporting level intentionally: + - Simple + - Partial details + - Full details +4. It is a violation to redirect users after submission with no indication of what happened. +5. Error reports must tell users what happened, why it happened, and the next course of action when possible. +6. Error report types: + - Informational + - Referential / implicit action + - Actionable +7. Translate technical error language into user-appropriate language. +8. UI exposers have one single dependency. +9. UI anatomy must separate: + - Bases + - Components + - Containers +10. Base components wrap native or third-party UI elements. +11. Core components integrate with one and only one view service. +12. Containers/pages orchestrate UI components and routes only. +13. Containers may not contain UI logic. +14. Containers may not use base components directly. +15. Base components may not wrap more than one non-local component when avoidable. +16. Base components do not contain business logic, exception handling, validations, or calculations. +17. Pages are route containers and do not require business logic. +18. Separate markup, code-behind, and style files. +19. Organize UI under Views/Bases, Views/Components, and Views/Pages. +20. Use domain-driven UI organization. + +### Web-application specifics + +0. Base components behave like brokers. +1. Core components behave like service/controller hybrids. +2. Core components are test-driven. +3. Core components are built from: + - Elements + - Styles + - Actions +4. Element testing must cover: + - Existence + - Properties + - Actions +5. Existence testing may use: + - Property assignment + - Searching by id + - General search +6. Styles belong primarily to core components, not bases. +7. Pages compose components and represent routes. +8. Pages typically do not require unit tests. +9. Keep UI unobtrusive; do not place CSS, C#, and markup in the same file. +10. Core components do not call other core components at the same level. +11. One view service corresponds to one core component and one view model. + +## Versioning and breaking changes + +This skill governs *what* the correct structure is at each layer. +When a structural change breaks an existing contract -- a model property added or removed, a service signature changed, an API route or response shape altered -- it is no longer purely an architecture decision. +Activate `the-standard-versioning` skill at that point. + +The versioning skill governs: +- When and how to increment the release version +- How to introduce new model versions under `Vn` subfolders without overwriting earlier versions +- How to introduce new service versions alongside earlier ones +- How to version API routes so earlier consumers are not broken +- How to signal deprecation on models, services, and routes + +The architecture skill and the versioning skill are complementary: +- Architecture answers: is the shape correct? +- Versioning answers: how do we change it safely? + +## Architecture review checklist + +When reviewing or generating architecture, verify all of the following: + +0. Purpose is explicit. +1. Models are scoped to the purpose. +2. Dependencies and exposure are separate from purpose. +3. Broker responsibilities are thin and disposable. +4. Services own business logic. +5. Validation order is correct. +6. Service-layer flow is forward only. +7. Same-level services are not calling each other. +8. Florance Pattern is honored. +9. Exposure layers have single point of contact. +10. APIs honor route and status-code rules. +11. UI honors single dependency, unobtrusiveness, and anatomy rules. +12. Architecture remains readable, autonomous, and rewritable. +13. If Cul-De-Sac eventing is used, `the-standard-events` skill has been activated and its rules are satisfied. +14. If a model, service, or API contract has changed or a release is being cut, `the-standard-versioning` skill has been activated and its rules are satisfied. + +## .NET implementation profile included from the supplied implementation specification + +The following addendum preserves the supplied implementation profile so the skill can enforce both the abstract Standard and the concrete .NET implementation style you attached. + +## 1. Overview + +The Standard is an opinionated software engineering standard authored by Hassan Habib. +It prescribes a **tri-nature** architecture consisting of: + +| Layer | Responsibility | +| -------------- | ------------------------------------------------------------- | +| **Brokers** | Thin abstraction over any external dependency (DB, API, logs) | +| **Services** | All business logic — validation, orchestration, coordination | +| **Exposers** | Entry points that expose services (Controllers, Endpoints) | + +This project currently implements **Brokers** and **Foundation Services** for the +`LegacyUser` entity (storage-based) and the `Person` entity (API-based), as well as +an **API Broker** for sending `Person` entities to an external API. + +--- + +## 2. Project Structure + +``` +RedRhino.Core.Synchronizer/ +├── Brokers/ +│ ├── Apis/ +│ │ ├── IModernApiBroker.cs (partial interface — base) +│ │ ├── IModernApiBroker.Persons.cs (partial interface — entity) +│ │ ├── ModernApiBroker.cs (partial class — base) +│ │ └── ModernApiBroker.Persons.cs (partial class — entity) +│ ├── Loggings/ +│ │ ├── ILoggingBroker.cs +│ │ └── LoggingBroker.cs +│ └── Storages/ +│ ├── IStorageBroker.cs (partial interface — base) +│ ├── IStorageBroker.LegacyUsers.cs (partial interface — entity) +│ ├── StorageBroker.cs (partial class — base) +│ └── StorageBroker.LegacyUsers.cs (partial class — entity) +├── Models/ +│ ├── Foundations/ +│ ├── Persons/ +│ │ ├── Person.cs +│ │ ├── PersonType.cs +│ │ ├── PersonRecordState.cs +│ │ └── Exceptions/ +│ │ ├── NullPersonException.cs +│ │ ├── InvalidPersonException.cs +│ │ ├── AlreadyExistsPersonException.cs +│ │ ├── FailedPersonDependencyException.cs +│ │ ├── FailedPersonServiceException.cs +│ │ ├── PersonValidationException.cs +│ │ ├── PersonDependencyException.cs +│ │ ├── PersonDependencyValidationException.cs +│ │ └── PersonServiceException.cs +│ └── LegacyUsers/ +│ ├── LegacyUser.cs +│ └── Exceptions/ +│ ├── NullLegacyUserException.cs +│ ├── InvalidLegacyUserException.cs +│ ├── AlreadyExistsLegacyUserException.cs +│ ├── FailedStorageLegacyUserDependencyException.cs +│ ├── FailedLegacyUserServiceException.cs +│ ├── LegacyUserValidationException.cs +│ ├── LegacyUserDependencyException.cs +│ ├── LegacyUserDependencyValidationException.cs +│ └── LegacyUserServiceException.cs +├── Services/ +│ └── Foundations/ +│ ├── LegacyUsers/ +│ │ ├── ILegacyUserService.cs +│ │ ├── LegacyUserService.cs (partial — logic) +│ │ ├── LegacyUserService.Validations.cs (partial — validations) +│ │ └── LegacyUserService.Exceptions.cs (partial — exception handling) +│ └── Persons/ +│ ├── IPersonService.cs +│ ├── PersonService.cs (partial — logic) +│ ├── PersonService.Validations.cs (partial — validations) +│ └── PersonService.Exceptions.cs (partial — exception handling) +└── Program.cs + +RedRhino.Core.Synchronizer.Tests.Units/ +└── Services/ + └── Foundations/ + ├── LegacyUsers/ + │ ├── LegacyUserServiceTests.cs (partial — setup & helpers) + │ ├── LegacyUserServiceTests.Logic.{Method}.cs (partial — happy-path tests) + │ ├── LegacyUserServiceTests.Validations.{Method}.cs (partial — validation tests) + │ └── LegacyUserServiceTests.Exceptions.{Method}.cs (partial — exception tests) + └── Persons/ + ├── PersonServiceTests.cs (partial — setup & helpers) + ├── PersonServiceTests.Logic.{Method}.cs (partial — happy-path tests) + ├── PersonServiceTests.Validations.{Method}.cs (partial — validation tests) + └── PersonServiceTests.Exceptions.{Method}.cs (partial — exception tests) +``` + +--- + +## 3. Brokers + +Brokers are **thin wrappers** around external resources. They contain **zero business logic**. + +> **Rule — Generic Helpers:** Every broker base partial **must** expose private generic +> helper methods (e.g., `InsertAsync`, `PostAsync`) that encapsulate the underlying +> client calls. Entity partial files **never** reference the private client member +> (e.g., `this.apiClient`, `this.Entry(...)`) directly — they delegate to the generic +> helpers instead. This keeps entity partials decoupled from the concrete client and +> allows swapping the underlying implementation in a single place. + +### 3.1 Storage Broker + +| Aspect | Implementation | +| -------------------- | -------------------------------------------------------------------------------------------------------- | +| Base class | `EFxceptionsContext` (from the **EFxceptions** library — wraps `DbContext` with meaningful EF exceptions) | +| Interface | `partial interface IStorageBroker` — split per entity | +| Class | `partial class StorageBroker` — split per entity | +| Generic CRUD helpers | Private helpers in base partial: `InsertAsync`, `SelectAllAsync`, `SelectAsync`, `UpdateAsync`, `DeleteAsync` | +| Configuration | Reads `DefaultConnection` from `IConfiguration`; calls `this.Database.Migrate()` at construction | + +**Base partial — `StorageBroker.cs`** + +This file owns: `IConfiguration`, `OnConfiguring`, the constructor (with `Database.Migrate()`), +and private generic CRUD helpers. It owns **nothing else**. + +> **arch-014 — No `DbSet<>` in the base partial.** +> `DbSet Students` lives in `StorageBroker.Students.cs`, not here. +> The base partial must never declare entity-specific members. + +```csharp +public partial class StorageBroker : EFxceptionsContext, IStorageBroker +{ + // No DbSet<> properties here. Each entity partial declares its own. + + private readonly IConfiguration configuration; + + public StorageBroker(IConfiguration configuration) + { + this.configuration = configuration; + + // arch-011: Must be called — applies pending migrations at startup. + // Omitting this means the schema is never applied. + this.Database.Migrate(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + string connectionString = + this.configuration.GetConnectionString("DefaultConnection"); + + optionsBuilder.UseSqlServer(connectionString); + } + + // arch-012: All EF operations live here. Entity partials call these helpers only. + private async ValueTask InsertAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Added; + await this.SaveChangesAsync(); + return entity; + } + + // SelectAllAsync wraps IQueryable in a ValueTask so all entity methods + // follow the uniform async/await pattern. AsNoTracking() is applied here. + private ValueTask> SelectAllAsync() where T : class => + ValueTask.FromResult>(this.Set().AsNoTracking()); + + private async ValueTask SelectAsync(Guid entityId) where T : class => + await this.FindAsync(entityId); + + private async ValueTask UpdateAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Modified; + await this.SaveChangesAsync(); + return entity; + } + + private async ValueTask DeleteAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Deleted; + await this.SaveChangesAsync(); + return entity; + } +} +``` + +**Entity partial — `StorageBroker.Students.cs`** + +> **arch-014:** `DbSet` is declared here — in the entity partial — not in `StorageBroker.cs`. + +```csharp +public partial class StorageBroker +{ + // DbSet belongs in this entity partial file, not in StorageBroker.cs. + public DbSet Students { get; set; } + + public async ValueTask InsertStudentAsync(Student student) => + await this.InsertAsync(student); + + // arch-009: SelectAll* must be async ValueTask>. + // WRONG: public IQueryable SelectAllStudents() => this.Students.AsNoTracking(); + // WRONG: public IQueryable SelectAllStudents() => this.Set(); + // CORRECT: delegate to SelectAllAsync() — never touch DbSet or EF members directly. + public async ValueTask> SelectAllStudentsAsync() => + await this.SelectAllAsync(); + + public async ValueTask SelectStudentByIdAsync(Guid studentId) => + await this.SelectAsync(studentId); + + public async ValueTask UpdateStudentAsync(Student student) => + await this.UpdateAsync(student); + + public async ValueTask DeleteStudentAsync(Student student) => + await this.DeleteAsync(student); +} +``` + +> **Rule:** Each entity gets its own partial file for both the interface and the class. +> +> **Rule — scope before scaffolding (arch-013):** A branch named `BROKERS-student-insert` +> implements **only** `InsertStudentAsync`. If the prompt is ambiguous about which +> operations are needed, the agent must ask before creating the branch or writing any code. +> A single broker branch = a single operation. +> +> **Rule — branch action language (prac-016):** Broker branch actions use infrastructure +> verbs — `insert`, `select-all`, `select-by-id`, `update`, `delete`. Never use business +> verbs (`add`, `retrieve`, `modify`, `remove`) in a `BROKERS` branch name. + +### 3.2 API Broker + +| Aspect | Implementation | +| -------------------- | ------------------------------------------------------------------------------------------------ | +| HTTP client | `IRESTFulApiFactoryClient` (from the **RESTFulSense** library — wraps `HttpClient`) | +| Interface | `partial interface IModernApiBroker` — split per entity | +| Class | `partial class ModernApiBroker` — split per entity | +| Generic HTTP helpers | Private `PostAsync()` on the base partial for reuse across entities | +| Configuration | Reads `ApiConfigurations:Url` from `IConfiguration` to set `HttpClient.BaseAddress` | + +**Base partial — `ModernApiBroker.cs`** + +```csharp +public partial class ModernApiBroker : IModernApiBroker +{ + private readonly IRESTFulApiFactoryClient apiClient; + + public ModernApiBroker(IConfiguration configuration) + { + var httpClient = new HttpClient() + { + BaseAddress = + new Uri(configuration.GetValue("ApiConfigurations:Url")) + }; + + this.apiClient = new RESTFulApiFactoryClient(httpClient); + } + + private async ValueTask PostAsync(string relativeUrl, T content) => + await this.apiClient.PostContentAsync(relativeUrl, content); +} +``` + +**Entity partial — `ModernApiBroker.Persons.cs`** + +```csharp +public partial class ModernApiBroker +{ + private const string PersonsRelativeUrl = "api/persons"; + + public async ValueTask PostPersonAsync(Person person) => + await PostAsync(PersonsRelativeUrl, person); +} +``` + +> **Rule:** Entity partials delegate to the generic helpers (`PostAsync`) — they +> never call `this.apiClient` directly. + +### 3.3 Logging Broker + +A dedicated abstraction over `ILogger`. The broker exposes only **async `ValueTask`** methods +that correspond to standard log levels: + +| Method | Log Level | +| --------------------- | ----------- | +| `LogInformationAsync` | Information | +| `LogTraceAsync` | Trace | +| `LogDebugAsync` | Debug | +| `LogWarningAsync` | Warning | +| `LogErrorAsync` | Error | +| `LogCriticalAsync` | Critical | + +**`LoggingBroker.cs`** + +```csharp +public class LoggingBroker : ILoggingBroker +{ + private readonly ILogger logger; + + public LoggingBroker(ILogger logger) => + this.logger = logger; + + public async ValueTask LogInformationAsync(string message) => + this.logger.LogInformation(message); + + public async ValueTask LogTraceAsync(string message) => + this.logger.LogTrace(message); + + public async ValueTask LogDebugAsync(string message) => + this.logger.LogDebug(message); + + public async ValueTask LogWarningAsync(string message) => + this.logger.LogWarning(message); + + public async ValueTask LogErrorAsync(Exception exception) => + this.logger.LogError(exception, exception.Message); + + public async ValueTask LogCriticalAsync(Exception exception) => + this.logger.LogCritical(exception, exception.Message); +} +``` + +**`ILoggingBroker.cs`** + +```csharp +public interface ILoggingBroker +{ + ValueTask LogInformationAsync(string message); + ValueTask LogTraceAsync(string message); + ValueTask LogDebugAsync(string message); + ValueTask LogWarningAsync(string message); + ValueTask LogErrorAsync(Exception exception); + ValueTask LogCriticalAsync(Exception exception); +} +``` + +> **Rule — async expression body, no Task.Run:** +> Each method uses the `async` keyword and delegates directly to `ILogger`. +> Never wrap the call in `Task.Run()` or `new ValueTask(Task.Run(...))`. +> `ILogger` is synchronous; wrapping it in `Task.Run()` introduces +> unnecessary thread-pool overhead and produces an inefficient heap-allocated `ValueTask`. +> +> **Wrong:** +> ```csharp +> public ValueTask LogWarningAsync(string message) => +> new ValueTask(Task.Run(() => this.logger.LogWarning(message))); +> ``` +> +> **Correct:** +> ```csharp +> public async ValueTask LogWarningAsync(string message) => +> this.logger.LogWarning(message); +> ``` + +> **Rule — DI Registration:** +> The logging broker must be explicitly registered in `Program.cs`. +> `AddLogging()` (or the host builder's default) must also be present so that +> `ILogger` resolves correctly. +> +> ```csharp +> builder.Services.AddLogging(); +> builder.Services.AddTransient(); +> ``` +> +> Omitting either line causes a runtime DI resolution failure. + +--- + +## 4. Models + +### 4.1 Entity Model + +Entity classes are plain POCOs residing under `Models/{Service Type}/{Entity}/`. They contain +**no behavior** — only properties. Domain comments on properties capture validation intent +(e.g., nullable rules, email format, phone format). + +The `LegacyUser` class resides under `Models/Foundations/LegacyUsers/`. +The `Person` class resides under `Models/Foundations/Persons/`. + +### 4.2 Exception Models + +Exception models live in `Models/{Service Type}/{Entity}/Exceptions/` and form a **two-tier exception +hierarchy** per The Standard. + +The `LegacyUser` class resides under `Models/Foundations/LegacyUsers/Exceptions/`. +The `Person` class resides under `Models/Foundations/Persons/Exceptions/`. + +The inner/outer exception hierarchy varies by the **type of broker** the Foundation Service +consumes. Storage-based services (e.g., `LegacyUserService`) handle SQL/EF exceptions, while +API-based services (e.g., `PersonService`) handle RESTFulSense HTTP exceptions. + +#### 4.2.1 Storage-Based Exceptions (LegacyUser) + +##### Inner (Local) Exceptions + +| Exception | Purpose | +| -------------------------------------------- | ------------------------------------------- | +| `NullLegacyUserException` | Entity is `null` | +| `InvalidLegacyUserException` | One or more property-level validation fails | +| `AlreadyExistsLegacyUserException` | Duplicate key detected | +| `FailedStorageLegacyUserDependencyException` | SQL / storage-level failure | +| `FailedLegacyUserServiceException` | Unexpected runtime failure | + +##### Outer (Categorical) Exceptions + +| Exception | Category | Wrapped Inner(s) | +| ----------------------------------------- | ------------------------ | ----------------------------------------------------------------------------- | +| `LegacyUserValidationException` | **Validation** | `NullLegacyUserException`, `InvalidLegacyUserException` | +| `LegacyUserDependencyException` | **Dependency** | `FailedStorageLegacyUserDependencyException` | +| `LegacyUserDependencyValidationException` | **DependencyValidation** | `AlreadyExistsLegacyUserException`, `InvalidLegacyUserException` (from `DbUpdateException`) | +| `LegacyUserServiceException` | **Service** | `FailedLegacyUserServiceException` | + +#### 4.2.2 API-Based Exceptions (Person) + +##### Inner (Local) Exceptions + +| Exception | Purpose | +| --------------------------------- | -------------------------------------------------------------------- | +| `NullPersonException` | Entity is `null` | +| `InvalidPersonException` | One or more property-level validation fails, or `BadRequest` from API| +| `AlreadyExistsPersonException` | `Conflict` (409) from API | +| `FailedPersonDependencyException` | HTTP-level failure (any HTTP error or `HttpRequestException`) | +| `FailedPersonServiceException` | Unexpected runtime failure | + +##### Outer (Categorical) Exceptions + +| Exception | Category | Wrapped Inner(s) | +| ---------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------- | +| `PersonValidationException` | **Validation** | `NullPersonException`, `InvalidPersonException` | +| `PersonDependencyValidationException` | **DependencyValidation** | `InvalidPersonException` (from `BadRequest`), `AlreadyExistsPersonException` (from `Conflict`) | +| `PersonDependencyException` | **Dependency** (Critical) | `FailedPersonDependencyException` (from `Unauthorized`, `Forbidden`, `NotFound`, `UrlNotFound`, `HttpRequestException`) | +| `PersonDependencyException` | **Dependency** (Non-Critical) | `FailedPersonDependencyException` (from `InternalServerError`, `ServiceUnavailable`) | +| `PersonServiceException` | **Service** | `FailedPersonServiceException` | + +All exceptions derive from **`Xeption`** (from the `Xeption` NuGet package), which provides +`UpsertDataList` and `ThrowIfContainsErrors` for aggregated validation data. + +--- + +## 5. Services — Foundations + +A Foundation Service sits directly on top of Brokers and is the **only consumer** of them. + +### 5.1 Partial Class Layout + +The service is split into three partial files: + +| Partial file | Concern | +| ------------------------------------- | --------------------------------------------- | +| `{Entity}Service.cs` | Constructor, DI fields, public business logic | +| `{Entity}Service.Validations.cs` | All `Validate*` and `IsInvalid*` methods | +| `{Entity}Service.Exceptions.cs` | `TryCatch` delegate pattern, `CreateAndLog*` helpers | + +### 5.2 Dependency Injection + +A Foundation Service depends **only** on Brokers — never on other services. + +**Storage-based service (LegacyUserService):** + +```csharp +public partial class LegacyUserService : ILegacyUserService +{ + private readonly IStorageBroker storageBroker; + private readonly ILoggingBroker loggingBroker; + + public LegacyUserService( + IStorageBroker storageBroker, + ILoggingBroker loggingBroker) { ... } +} +``` + +**API-based service (PersonService):** + +```csharp +public partial class PersonService : IPersonService +{ + private readonly IModernApiBroker modernApiBroker; + private readonly ILoggingBroker loggingBroker; + + public PersonService( + IModernApiBroker modernApiBroker, + ILoggingBroker loggingBroker) { ... } +} +``` + +### 5.3 Business Logic — TryCatch Pattern + +Every public method delegates to a `TryCatch` wrapper that catches, wraps, logs, and +re-throws categorised exceptions: + +**Storage-based:** + +```csharp +public ValueTask AddLegacyUserAsync(LegacyUser legacyUser) => +TryCatch(async () => +{ + ValidateLegacyUser(legacyUser); + + return await this.storageBroker.InsertLegacyUserAsync(legacyUser); +}); +``` + +**API-based:** + +```csharp +public ValueTask AddPersonAsync(Person person) => +TryCatch(async () => +{ + ValidatePerson(person); + + return await this.modernApiBroker.PostPersonAsync(person); +}); +``` + +### 5.4 Validations + +Validations use a **dynamic rule + aggregate** pattern: + +```csharp +Validate( + (IsInvalid(legacyUser.Id), nameof(LegacyUser.Id)), + (IsInvalid(legacyUser.UserPK), nameof(LegacyUser.UserPK)), + (IsInvalid(legacyUser.UserName), nameof(LegacyUser.UserName)), + ...); +``` + +Each `IsInvalid` overload returns an anonymous object with `Condition` (bool) and `Message` (string). +The `Validate` method aggregates all failures into a single `InvalidLegacyUserException` via +`UpsertDataList` and calls `ThrowIfContainsErrors()`. + +Custom validators exist for domain-specific rules: + +| Validator | Rule | +| ------------------- | --------------------------------- | +| `IsInvalid(Guid)` | Must not be `Guid.Empty` | +| `IsInvalid(int)` | Must not be `default (0)` | +| `IsInvalid(string)` | Must not be null/empty/whitespace | +| `IsInvalidLob(int)` | Must be greater than 0 | +| `IsInvalidEmail` | Regex-based email format check | + +--- + +## 6. Exception Handling + +The `TryCatch` method in each `{Entity}Service.Exceptions.cs` implements The Standard's +**exception mapping** pattern. The specific catch blocks differ based on the broker type +the service consumes. + +### 6.1 Storage-Based Exception Mapping (LegacyUser) + +``` +Native / External Exception → Inner (Local) Exception → Outer (Categorical) Exception +────────────────────────────────────────────────────────────────────────────────────────────────── +(null input) → NullLegacyUserException → LegacyUserValidationException +(validation rules fail) → InvalidLegacyUserException → LegacyUserValidationException +SqlException → FailedStorage...Exception → LegacyUserDependencyException (Critical) +DuplicateKeyException → AlreadyExists...Exception → LegacyUserDependencyValidationException +DbUpdateException → InvalidLegacy...Exception → LegacyUserDependencyValidationException +Exception (catch-all) → FailedService...Exception → LegacyUserServiceException +``` + +### 6.2 API-Based Exception Mapping (Person) + +For services that call external APIs through RESTFulSense, the exception mapping covers +all HTTP response exceptions. The ordering of catch blocks in `TryCatch` matters — more +specific exceptions must appear before their base classes. + +``` +Native / External Exception → Inner (Local) Exception → Outer (Categorical) Exception Log Level +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +(null input) → NullPersonException → PersonValidationException LogError +(validation rules fail) → InvalidPersonException → PersonValidationException LogError +HttpResponseBadRequestException → InvalidPersonException → PersonDependencyValidationException LogError +HttpResponseConflictException → AlreadyExistsPersonException → PersonDependencyValidationException LogError +HttpResponseUnauthorizedException → FailedPersonDependencyException → PersonDependencyException LogCritical +HttpResponseForbiddenException → FailedPersonDependencyException → PersonDependencyException LogCritical +HttpResponseNotFoundException → FailedPersonDependencyException → PersonDependencyException LogCritical +HttpResponseUrlNotFoundException → FailedPersonDependencyException → PersonDependencyException LogCritical +HttpResponseInternalServerErrorException → FailedPersonDependencyException → PersonDependencyException LogError +HttpResponseServiceUnavailableException → FailedPersonDependencyException → PersonDependencyException LogError +HttpRequestException → FailedPersonDependencyException → PersonDependencyException LogCritical +Exception (catch-all) → FailedPersonServiceException → PersonServiceException LogError +``` + +> **Rule — Critical vs Non-Critical Dependency:** Exceptions that indicate the API endpoint +> is unreachable or inaccessible (`Unauthorized`, `Forbidden`, `NotFound`, `UrlNotFound`, +> `HttpRequestException`) are logged at `LogCriticalAsync` because they signal configuration +> or infrastructure failures. Server-side errors (`InternalServerError`, `ServiceUnavailable`) +> are logged at `LogErrorAsync` because they may be transient. + +Each `CreateAndLog*` helper: + +1. Wraps the inner exception in the categorical outer exception. +2. Logs via the `ILoggingBroker` at the appropriate level (`LogErrorAsync` or `LogCriticalAsync`). +3. Returns the outer exception so `TryCatch` can `throw` it. + +--- + +## 7. Dependency Registration (Exposers) + +All wiring is done in `Program.cs` following The Standard's explicit registration style: + +```csharp +builder.Services.AddDbContext(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +> Brokers and Foundation Services are registered as **Transient**. + +--- + +## 10. Naming Conventions + +| Element | Pattern | Example | +| ------------------ | ------------------------------------------------------------- | ---------------------------------- | +| Broker interface | `I{Resource}Broker` | `IStorageBroker`, `IModernApiBroker`, `ILoggingBroker` | +| Broker class | `{Resource}Broker` | `StorageBroker`, `ModernApiBroker`, `LoggingBroker` | +| Broker method | `{Action}{Entity}Async` | `InsertLegacyUserAsync`, `PostPersonAsync` | +| Service interface | `I{Entity}Service` | `ILegacyUserService` | +| Service class | `{Entity}Service` | `LegacyUserService` | +| Service method | `Add{Entity}Async` | `AddLegacyUserAsync` | +| Inner exception | `{Adjective}{Entity}Exception` | `NullLegacyUserException` | +| Outer exception | `{Entity}{Category}Exception` | `LegacyUserValidationException` | +| Test class | `{Entity}ServiceTests` | `LegacyUserServiceTests` | +| Test method | `Should{Action}Async` / `ShouldThrow{Exception}On{Action}…` | `ShouldAddLegacyUserAsync` | + +--- diff --git a/.agents/skills/the-standard-architecture/contracts/contracts.json b/.agents/skills/the-standard-architecture/contracts/contracts.json new file mode 100644 index 0000000..5978765 --- /dev/null +++ b/.agents/skills/the-standard-architecture/contracts/contracts.json @@ -0,0 +1,104 @@ +{ + "skill": "the-standard-architecture", + "version": "1.0.0", + + "naming": { + "broker_interface": "I{Resource}Broker", + "broker_class": "{Resource}Broker", + "broker_method": "{Action}{Entity}Async (infrastructure verbs: Insert, Select, Update, Delete, Post, Get, Put)", + "service_interface": "I{Entity}Service", + "service_class": "{Entity}Service", + "service_method": "{BusinessVerb}{Entity}Async (business verbs: Add, Retrieve, Modify, Remove)", + "processing_service_interface": "I{Entity}ProcessingService", + "processing_service_class": "{Entity}ProcessingService", + "processing_service_method": "{ProcessVerb}{Entity}Async (process verbs: Ensure, Upsert, TryAdd, TryRemove, Verify)", + "orchestration_service_interface": "I{Entity}OrchestrationService", + "orchestration_service_class": "{Entity}OrchestrationService", + "aggregation_service_interface": "I{Entity}AggregationService", + "aggregation_service_class": "{Entity}AggregationService", + "controller_class": "{Entities}Controller (plural entity name)", + "controller_route": "/api/{entities} (plural lowercase)" + }, + + "layer_dependency_rules": { + "allowed": [ + "Exposer → Service (Foundation, Processing, Orchestration, Aggregation)", + "Foundation Service → Broker", + "Processing Service → Foundation Service", + "Orchestration Service → Processing Service or Foundation Service", + "Aggregation Service → any Service", + "Any Service → Support Broker (Logging, DateTime, Identifier)" + ], + "forbidden": [ + "Foundation Service → Foundation Service (same-layer call)", + "Foundation Service → Processing Service (upward dependency)", + "Foundation Service → Infrastructure directly (must go through broker)", + "Broker → Broker (entity brokers must not call other entity brokers)", + "Exposer → Broker (must always go through a service)", + "Service → Exposer (downward exposure call)", + "Processing Service → multiple Foundation Services (must be exactly one)" + ] + }, + + "broker_rules": { + "max_external_resources_per_broker": 1, + "flow_control_allowed": false, + "exception_handling_allowed": false, + "all_methods_must_be_async": true, + "language": "infrastructure", + "must_implement_interface": true + }, + + "foundation_service_rules": { + "entity_types_handled": 1, + "input_output_type": "same_entity", + "validation_order": ["structural", "logical", "external", "dependency"], + "circuit_breaking": "structural validations must break immediately on null", + "continuous_validation": "collect all field errors before throwing", + "exception_categories": [ + "{Entity}ValidationException", + "{Entity}DependencyValidationException", + "{Entity}CriticalDependencyException", + "{Entity}DependencyException", + "{Entity}ServiceException" + ] + }, + + "processing_service_rules": { + "foundation_service_dependencies": "exactly_one", + "validation": "used-data-only", + "patterns": ["Ensure", "Upsert", "TryAdd", "TryRemove", "Verify"] + }, + + "orchestration_service_rules": { + "dependency_count": "2-3 (Florance Pattern)", + "call_order": "natural or explicitly enforced", + "multi_entity": true + }, + + "aggregation_service_rules": { + "order_validation": false, + "mock_sequence_assertions": false, + "validation_level": "basic structural only" + }, + + "exposer_rules": { + "business_logic": false, + "mapping_only": true, + "single_contact_point_per_entity": true, + "route_convention": "/api/{entities}", + "http_success_codes": { + "POST": 201, + "GET": 200, + "PUT": 200, + "DELETE": 200 + }, + "http_error_codes": { + "ValidationException": 400, + "DependencyValidationException": 400, + "CriticalDependencyException": 500, + "DependencyException": 500, + "ServiceException": 500 + } + } +} diff --git a/.agents/skills/the-standard-architecture/examples/bad/example_bad_broker.cs b/.agents/skills/the-standard-architecture/examples/bad/example_bad_broker.cs new file mode 100644 index 0000000..2e100f4 --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/bad/example_bad_broker.cs @@ -0,0 +1,103 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Non-Standard Broker +// Each violation is annotated with the rule it breaks. +// --------------------------------------------------------------- + +using System; + +namespace MyProject.Brokers +{ + // VIOLATION arch-001: No interface — broker cannot be mocked in tests + // VIOLATION arch-008: Wraps TWO resources (storage AND email) in one broker + public class StudentBroker + { + private readonly AppDbContext dbContext; + private readonly SmtpClient emailClient; + + public StudentBroker(AppDbContext dbContext, SmtpClient emailClient) + { + this.dbContext = dbContext; + this.emailClient = emailClient; + } + + // VIOLATION arch-006: Uses business language (Add) instead of infrastructure (Insert) + // VIOLATION arch-002: Contains flow control (if statement) + // VIOLATION arch-003: Handles exceptions — brokers must never catch exceptions + // VIOLATION arch-009: Not async + public Student AddStudent(Student student) + { + // arch-002 VIOLATION: flow control in broker + if (student == null) + { + throw new ArgumentNullException("student is null"); + } + + try + { + // arch-003 VIOLATION: exception handling in broker + this.dbContext.Students.Add(student); + this.dbContext.SaveChanges(); + + // arch-008 VIOLATION: sending email from an entity broker + this.emailClient.Send(new MailMessage("noreply@school.com", student.Email, + "Welcome", "You have been added.")); + + return student; + } + catch (Exception ex) + { + // arch-003 VIOLATION: catching and rethrowing in broker + throw new Exception($"Failed to add student: {ex.Message}"); + } + } + + // VIOLATION arch-006: Uses business language (GetById) instead of (SelectStudentById) + // VIOLATION arch-004: Reads connection string from environment directly + // instead of owning configuration through injection + public Student GetById(Guid id) + { + // arch-004 VIOLATION: broker does not own its configuration + var connectionString = Environment.GetEnvironmentVariable("DB_CONNECTION"); + + return this.dbContext.Students.Find(id); + } + } +} + +// --------------------------------------------------------------- +// BAD EXAMPLE: Non-Standard Logging Broker +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Loggings +{ + public class LoggingBroker : ILoggingBroker + { + private readonly ILogger logger; + + public LoggingBroker(ILogger logger) => + this.logger = logger; + + // VIOLATION arch-009: Task.Run() wraps a synchronous ILogger call, + // producing a heap-allocated Task and introducing thread-pool overhead + // for no benefit. ILogger is already synchronous — no offloading needed. + // Use `async ValueTask` + direct call instead. + public ValueTask LogWarningAsync(string message) => + new ValueTask(Task.Run(() => this.logger.LogWarning(message))); + + // VIOLATION arch-009: Same Task.Run() anti-pattern on every method. + public ValueTask LogErrorAsync(Exception exception) => + new ValueTask(Task.Run(() => this.logger.LogError(exception, exception.Message))); + + public ValueTask LogCriticalAsync(Exception exception) => + new ValueTask(Task.Run(() => this.logger.LogCritical(exception, exception.Message))); + + public ValueTask LogInformationAsync(string message) => + new ValueTask(Task.Run(() => this.logger.LogInformation(message))); + + public ValueTask LogTraceAsync(string message) => + new ValueTask(Task.Run(() => this.logger.LogTrace(message))); + + public ValueTask LogDebugAsync(string message) => + new ValueTask(Task.Run(() => this.logger.LogDebug(message))); + } +} diff --git a/.agents/skills/the-standard-architecture/examples/bad/example_bad_service.cs b/.agents/skills/the-standard-architecture/examples/bad/example_bad_service.cs new file mode 100644 index 0000000..94b8ed2 --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/bad/example_bad_service.cs @@ -0,0 +1,63 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Non-Standard Service +// Each violation is annotated with the rule it breaks. +// --------------------------------------------------------------- + +namespace MyProject.Services +{ + // VIOLATION arch-021: Handles more than one entity type (Student AND Course) + // VIOLATION arch-022: Uses infrastructure language (Insert, Select) not business language + // VIOLATION arch-080: Calls another foundation-level service directly (CourseService) + public class StudentService + { + private readonly IStorageBroker storageBroker; + private readonly ICourseService courseService; // arch-080 VIOLATION: same-layer call + + public StudentService(IStorageBroker storageBroker, ICourseService courseService) + { + this.storageBroker = storageBroker; + this.courseService = courseService; + } + + // VIOLATION arch-022: Infrastructure language (Insert) instead of business (Add) + // VIOLATION arch-023: No validation before delegating + // VIOLATION arch-024: No null check (structural validation) — no circuit-breaking + public async Task InsertStudent(Student student) + { + // arch-023 VIOLATION: Directly inserts without any validation + return await this.storageBroker.InsertStudentAsync(student); + } + + // VIOLATION arch-025: No logical validation + // VIOLATION arch-028: Does not collect all errors — throws on first failure only + public async Task UpdateStudent(Student student) + { + // arch-024 VIOLATION: No null check + // arch-025 VIOLATION: No logical validation + if (student.Name == null) + { + throw new Exception("Name is null"); // not a Standard exception + } + + // arch-080 VIOLATION: calling peer service from foundation service + var courses = await this.courseService.RetrieveAllCoursesAsync(); + + return await this.storageBroker.UpdateStudentAsync(student); + } + + // VIOLATION arch-029: Raw exception is rethrown without localization or categorization + // VIOLATION arch-030: Exception is not categorized into Standard exception types + public async Task GetStudentById(Guid id) + { + try + { + return await this.storageBroker.SelectStudentByIdAsync(id); + } + catch (Exception ex) + { + // arch-029 VIOLATION: Not localized, not categorized, not logged + throw new Exception($"Database error: {ex.Message}"); + } + } + } +} diff --git a/.agents/skills/the-standard-architecture/examples/good/example_broker.cs b/.agents/skills/the-standard-architecture/examples/good/example_broker.cs new file mode 100644 index 0000000..d7e1ab3 --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/good/example_broker.cs @@ -0,0 +1,154 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Storage Broker +// Demonstrates: local interface, partial class split, generic CRUD helpers in base partial, +// entity partials delegate to helpers only (no direct DbSet/DbContext access), +// Database.Migrate() in constructor, all methods async ValueTask. + +using System; +using System.Linq; +using System.Threading.Tasks; +using EFxceptions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +// --------------------------------------------------------------- +// StorageBroker.cs — base partial +// Owns: IConfiguration, OnConfiguring, constructor + Migrate(), private CRUD helpers. +// Does NOT own: DbSet<> properties — each entity partial declares its own. +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Storages +{ + // arch-001: Implements a local interface + // arch-004: Broker owns its configuration (IConfiguration injected here, not in entity partials) + public partial class StorageBroker : EFxceptionsContext, IStorageBroker + { + // NO DbSet<> here. DbSet Students lives in StorageBroker.Students.cs. + private readonly IConfiguration configuration; + + public StorageBroker(IConfiguration configuration) + { + this.configuration = configuration; + + // arch-004: Broker owns migration — Database.Migrate() runs at construction. + // This must NOT be omitted; omitting it means the schema is never applied. + this.Database.Migrate(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + string connectionString = + this.configuration.GetConnectionString("DefaultConnection"); + + optionsBuilder.UseSqlServer(connectionString); + } + + // Generic CRUD helpers — entity partials NEVER call this.Entry(), this.Set(), + // this.FindAsync(), or this.SaveChangesAsync() directly. They use these helpers only. + // This keeps entity partials decoupled from the concrete EF implementation. + + private async ValueTask InsertAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Added; + await this.SaveChangesAsync(); + + return entity; + } + + // SelectAllAsync: wraps IQueryable in a ValueTask so the entity partial + // can use the uniform `async/await` pattern across all operations. + // AsNoTracking() is applied here — entity partials must not add it themselves. + private ValueTask> SelectAllAsync() where T : class => + ValueTask.FromResult>(this.Set().AsNoTracking()); + + private async ValueTask SelectAsync(Guid entityId) where T : class => + await this.FindAsync(entityId); + + private async ValueTask UpdateAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Modified; + await this.SaveChangesAsync(); + + return entity; + } + + private async ValueTask DeleteAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Deleted; + await this.SaveChangesAsync(); + + return entity; + } + } +} + +// --------------------------------------------------------------- +// StorageBroker.Students.cs — entity partial +// DbSet lives HERE, not in StorageBroker.cs. +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Storages +{ + public partial class StorageBroker + { + // arch-014: DbSet is declared in the entity partial only. + // WRONG: placing DbSet in StorageBroker.cs + // CORRECT: DbSet Students lives in StorageBroker.Students.cs + public DbSet Students { get; set; } + + // arch-006: Infrastructure language (Insert, not Add) + // arch-009: Async ValueTask + // arch-002: No flow control + // arch-003: No exception handling — raw exceptions propagate to service + public async ValueTask InsertStudentAsync(Student student) => + await this.InsertAsync(student); + + // arch-006: Infrastructure language (SelectAll, not RetrieveAll) + // arch-009: Async ValueTask> — NOT synchronous IQueryable + // WRONG: public IQueryable SelectAllStudents() => this.Students.AsNoTracking(); + // WRONG: public IQueryable SelectAllStudents() => this.SelectAll(); + // CORRECT: delegate to SelectAllAsync() helper — do NOT touch this.Students directly + public async ValueTask> SelectAllStudentsAsync() => + await this.SelectAllAsync(); + + // arch-006: Infrastructure language (Select, not Retrieve) + public async ValueTask SelectStudentByIdAsync(Guid studentId) => + await this.SelectAsync(studentId); + + // arch-006: Infrastructure language (Update, not Modify) + public async ValueTask UpdateStudentAsync(Student student) => + await this.UpdateAsync(student); + + // arch-006: Infrastructure language (Delete, not Remove) + public async ValueTask DeleteStudentAsync(Student student) => + await this.DeleteAsync(student); + } +} + +// --------------------------------------------------------------- +// IStorageBroker.Students.cs — entity interface partial +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Storages +{ + // arch-001: Local interface — I{Resource}Broker + public partial interface IStorageBroker + { + ValueTask InsertStudentAsync(Student student); + ValueTask> SelectAllStudentsAsync(); + ValueTask SelectStudentByIdAsync(Guid studentId); + ValueTask UpdateStudentAsync(Student student); + ValueTask DeleteStudentAsync(Student student); + } +} + +// --------------------------------------------------------------- +// Program.cs — DI registration +// --------------------------------------------------------------- +// +// builder.Services.AddDbContext(); +// builder.Services.AddTransient(); diff --git a/.agents/skills/the-standard-architecture/examples/good/example_foundation_service.cs b/.agents/skills/the-standard-architecture/examples/good/example_foundation_service.cs new file mode 100644 index 0000000..ef3f62a --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/good/example_foundation_service.cs @@ -0,0 +1,312 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Foundation Service +// Demonstrates: business language, single entity, validates before delegating, +// circuit-breaking, continuous validation, exception categorization, logging. + +using System; +using System.Threading.Tasks; +using Xeptions; + +namespace MyProject.Services.Foundations.Students +{ + // arch-022: Business language (Add, Retrieve, Modify, Remove) + // arch-020: Pure-primitive — returns Student, takes Student + // arch-021: Single entity (Student only) + public partial class StudentService : IStudentService + { + private readonly IStorageBroker storageBroker; + private readonly ILoggingBroker loggingBroker; + + public StudentService( + IStorageBroker storageBroker, + ILoggingBroker loggingBroker) + { + this.storageBroker = storageBroker; + this.loggingBroker = loggingBroker; + } + + // arch-022: Business language + public ValueTask AddStudentAsync(Student student) => + TryCatch(async () => + { + // arch-023: Validate before delegating + ValidateStudentOnAdd(student); + + return await this.storageBroker.InsertStudentAsync(student); + }); + + // arch-015: Asynchronization Abstraction — method returns ValueTask> + // even though the underlying call is not a network round-trip. The public contract + // must be uniformly async so callers never need to change if the implementation evolves. + public ValueTask> RetrieveAllStudentsAsync() => + TryCatch(async () => + await this.storageBroker.SelectAllStudentsAsync()); + + public ValueTask RetrieveStudentByIdAsync(Guid studentId) => + TryCatch(async () => + { + // arch-024: Circuit-breaking — null id stops immediately + ValidateStudentId(studentId); + + Student maybeStudent = + await this.storageBroker.SelectStudentByIdAsync(studentId); + + // arch-026: External validation — check existence + ValidateStorageStudent(maybeStudent, studentId); + + return maybeStudent; + }); + + public ValueTask ModifyStudentAsync(Student student) => + TryCatch(async () => + { + ValidateStudentOnModify(student); + + Student maybeStudent = + await this.storageBroker.SelectStudentByIdAsync(student.Id); + + // arch-026: External validation + ValidateStorageStudent(maybeStudent, student.Id); + ValidateAgainstStorageStudentOnModify(inputStudent: student, storageStudent: maybeStudent); + + return await this.storageBroker.UpdateStudentAsync(student); + }); + + public ValueTask RemoveStudentByIdAsync(Guid studentId) => + TryCatch(async () => + { + ValidateStudentId(studentId); + + Student maybeStudent = + await this.storageBroker.SelectStudentByIdAsync(studentId); + + ValidateStorageStudent(maybeStudent, studentId); + + return await this.storageBroker.DeleteStudentAsync(maybeStudent); + }); + } +} + +// --------------------------------------------------------------- +// Validations partial (StudentService.Validations.cs) +// --------------------------------------------------------------- + +namespace MyProject.Services.Foundations.Students +{ + public partial class StudentService + { + private void ValidateStudentOnAdd(Student student) + { + // arch-024: Circuit-breaking — null entity stops immediately + ValidateStudentIsNotNull(student); + + // arch-025, arch-028: Continuous validation — collect all field errors + Validate( + (Rule: IsInvalid(student.Id), Parameter: nameof(Student.Id)), + (Rule: IsInvalid(student.Name), Parameter: nameof(Student.Name)), + (Rule: IsInvalid(student.CreatedDate), Parameter: nameof(Student.CreatedDate)), + (Rule: IsInvalid(student.UpdatedDate), Parameter: nameof(Student.UpdatedDate)), + (Rule: IsNotSame(student.CreatedDate, student.UpdatedDate, nameof(Student.UpdatedDate)), + Parameter: nameof(Student.UpdatedDate)), + (Rule: IsNotRecent(student.CreatedDate), Parameter: nameof(Student.CreatedDate))); + } + + private static void ValidateStudentIsNotNull(Student student) + { + if (student is null) + { + throw new NullStudentException(message: "Student is null."); + } + } + + private static dynamic IsInvalid(Guid id) => new + { + Condition = id == Guid.Empty, + Message = "Id is required" + }; + + private static dynamic IsInvalid(string text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static dynamic IsInvalid(DateTimeOffset date) => new + { + Condition = date == default, + Message = "Date is required" + }; + + private static dynamic IsNotSame( + DateTimeOffset firstDate, + DateTimeOffset secondDate, + string secondDateName) => new + { + Condition = firstDate != secondDate, + Message = $"Date is not the same as {secondDateName}" + }; + + private dynamic IsNotRecent(DateTimeOffset date) + { + var (isNotRecent, startDate, endDate) = IsDateNotRecent(date); + + return new + { + Condition = isNotRecent, + Message = $"Date is not recent. Expected a value between {startDate} and {endDate}" + }; + } + + // arch-028: Upsertable continuous validation — collects ALL errors before throwing + private static void Validate(params (dynamic Rule, string Parameter)[] validations) + { + var invalidStudentException = new InvalidStudentException( + message: "Invalid student. Please correct the errors and try again."); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalidStudentException.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalidStudentException.ThrowIfContainsErrors(); + } + } +} + +// --------------------------------------------------------------- +// Exception Handling partial (StudentService.Exceptions.cs) +// --------------------------------------------------------------- + +namespace MyProject.Services.Foundations.Students +{ + public partial class StudentService + { + // arch-029: Catch, localize, categorize, and log all broker exceptions + private delegate ValueTask ReturningStudentFunction(); + + private async ValueTask TryCatch(ReturningStudentFunction returningStudentFunction) + { + try + { + return await returningStudentFunction(); + } + catch (NullStudentException nullStudentException) + { + throw await CreateAndLogValidationException(nullStudentException); + } + catch (InvalidStudentException invalidStudentException) + { + throw await CreateAndLogValidationException(invalidStudentException); + } + catch (NotFoundStudentException notFoundStudentException) + { + throw await CreateAndLogValidationException(notFoundStudentException); + } + catch (DuplicateKeyException duplicateKeyException) + { + var alreadyExistsStudentException = + new AlreadyExistsStudentException( + message: "Student with the same Id already exists.", + innerException: duplicateKeyException); + + // arch-030: DependencyValidationException for conflict errors + throw await CreateAndLogDependencyValidationException(alreadyExistsStudentException); + } + catch (DbUpdateConcurrencyException dbUpdateConcurrencyException) + { + var lockedStudentException = + new LockedStudentException( + message: "Student is locked, please try again.", + innerException: dbUpdateConcurrencyException); + + throw await CreateAndLogDependencyValidationException(lockedStudentException); + } + catch (DbUpdateException dbUpdateException) + { + var failedStudentStorageException = + new FailedStudentStorageException( + message: "Failed student storage error occurred, contact support.", + innerException: dbUpdateException); + + // arch-030: DependencyException for non-critical storage errors + throw await CreateAndLogDependencyException(failedStudentStorageException); + } + catch (Exception serviceException) + { + var failedStudentServiceException = + new FailedStudentServiceException( + message: "Unexpected service error occurred. Contact support.", + innerException: serviceException); + + // arch-030: ServiceException for all unexpected errors + throw await CreateAndLogServiceException(failedStudentServiceException); + } + } + + // arch-015: CreateAndLog* helpers are async because ILoggingBroker.LogErrorAsync + // returns ValueTask. Callers use `throw await CreateAndLog*(...)`. + // WRONG: private StudentValidationException CreateAndLogValidationException(...) + // { this.loggingBroker.LogError(...); } + // CORRECT: async ValueTask + await this.loggingBroker.LogErrorAsync(...) + private async ValueTask CreateAndLogValidationException( + Xeption exception) + { + var studentValidationException = + new StudentValidationException( + message: "Student validation error occurred, please fix the errors and try again.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync(studentValidationException); + + return studentValidationException; + } + + private async ValueTask + CreateAndLogDependencyValidationException(Xeption exception) + { + var studentDependencyValidationException = + new StudentDependencyValidationException( + message: "Student dependency validation error occurred, fix the errors.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync(studentDependencyValidationException); + + return studentDependencyValidationException; + } + + private async ValueTask CreateAndLogDependencyException( + Xeption exception) + { + var studentDependencyException = + new StudentDependencyException( + message: "Student dependency error occurred, contact support.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync(studentDependencyException); + + return studentDependencyException; + } + + private async ValueTask CreateAndLogServiceException( + Xeption exception) + { + var studentServiceException = + new StudentServiceException( + message: "Student service error occurred, contact support.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync(studentServiceException); + + return studentServiceException; + } + } +} diff --git a/.agents/skills/the-standard-architecture/examples/good/example_logging_broker.cs b/.agents/skills/the-standard-architecture/examples/good/example_logging_broker.cs new file mode 100644 index 0000000..3b147ba --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/good/example_logging_broker.cs @@ -0,0 +1,78 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Logging Broker +// Demonstrates: local interface, async ValueTask, direct logger calls (no Task.Run), +// no flow control, no exception handling, registered as Transient. + +using System; +using Microsoft.Extensions.Logging; + +namespace MyProject.Brokers.Loggings +{ + // arch-001: Implements a local interface (ILoggingBroker) + // arch-009: All methods are async ValueTask + // arch-002: No flow control + // arch-003: No exception handling — raw exceptions propagate to the service + // arch-010: Support broker — cross-cutting infrastructure only + public class LoggingBroker : ILoggingBroker + { + private readonly ILogger logger; + + public LoggingBroker(ILogger logger) => + this.logger = logger; + + // CORRECT: async expression-bodied method delegates directly to ILogger. + // DO NOT wrap with Task.Run() — ILogger is synchronous by design. + // Task.Run() introduces thread-pool overhead, can swallow async context, + // and produces a ValueTask backed by a Task unnecessarily. + public async ValueTask LogInformationAsync(string message) => + this.logger.LogInformation(message); + + public async ValueTask LogTraceAsync(string message) => + this.logger.LogTrace(message); + + public async ValueTask LogDebugAsync(string message) => + this.logger.LogDebug(message); + + public async ValueTask LogWarningAsync(string message) => + this.logger.LogWarning(message); + + public async ValueTask LogErrorAsync(Exception exception) => + this.logger.LogError(exception, exception.Message); + + public async ValueTask LogCriticalAsync(Exception exception) => + this.logger.LogCritical(exception, exception.Message); + } +} + +// --------------------------------------------------------------- +// Interface (correct) +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Loggings +{ + // arch-001: Local interface pattern — I{Resource}Broker + public interface ILoggingBroker + { + ValueTask LogInformationAsync(string message); + ValueTask LogTraceAsync(string message); + ValueTask LogDebugAsync(string message); + ValueTask LogWarningAsync(string message); + ValueTask LogErrorAsync(Exception exception); + ValueTask LogCriticalAsync(Exception exception); + } +} + +// --------------------------------------------------------------- +// DI Registration in Program.cs (correct) +// --------------------------------------------------------------- +// +// builder.Services.AddLogging(); // registers ILogger from Microsoft.Extensions.Logging +// builder.Services.AddTransient(); +// +// NOTE: AddLogging() must be called (or AddDefaultLogging() via the host builder) +// so that ILogger is resolvable. Omitting this registration causes +// a runtime DI resolution failure. diff --git a/.agents/skills/the-standard-architecture/examples/good/example_processing_service.cs b/.agents/skills/the-standard-architecture/examples/good/example_processing_service.cs new file mode 100644 index 0000000..cb143fd --- /dev/null +++ b/.agents/skills/the-standard-architecture/examples/good/example_processing_service.cs @@ -0,0 +1,133 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Processing Service +// Demonstrates: one foundation dependency, used-data-only validation, +// higher-order logic (Ensure/Upsert), shifters, exception unwrapping. + +using System; +using System.Threading.Tasks; + +namespace MyProject.Services.Processings.Students +{ + // arch-040: Depends on exactly one foundation service + public partial class StudentProcessingService : IStudentProcessingService + { + private readonly IStudentService studentService; + private readonly ILoggingBroker loggingBroker; + + public StudentProcessingService( + IStudentService studentService, + ILoggingBroker loggingBroker) + { + this.studentService = studentService; + this.loggingBroker = loggingBroker; + } + + // arch-042: Higher-order logic — EnsureStudentExists (retrieve+add combination) + // arch-044: Combination pattern + public ValueTask EnsureStudentExistsAsync(Student student) => + TryCatch(async () => + { + // arch-041: Validate only what this method uses + ValidateStudentOnEnsure(student); + + IQueryable allStudents = + this.studentService.RetrieveAllStudents(); + + bool isStudentExists = allStudents.Any(retrievedStudent => + retrievedStudent.Id == student.Id); + + return isStudentExists switch + { + false => await this.studentService.AddStudentAsync(student), + _ => await this.studentService.RetrieveStudentByIdAsync(student.Id) + }; + }); + + // arch-042: Higher-order logic — UpsertStudent (retrieve+modify or add) + public ValueTask UpsertStudentAsync(Student student) => + TryCatch(async () => + { + ValidateStudentOnUpsert(student); + + IQueryable allStudents = + this.studentService.RetrieveAllStudents(); + + bool isStudentExists = allStudents.Any(retrievedStudent => + retrievedStudent.Id == student.Id); + + return isStudentExists switch + { + true => await this.studentService.ModifyStudentAsync(student), + false => await this.studentService.AddStudentAsync(student) + }; + }); + + // arch-043: Shifter — Student → bool + public ValueTask VerifyStudentExistsAsync(Guid studentId) => + TryCatch(async () => + { + ValidateStudentId(studentId); + + IQueryable allStudents = + this.studentService.RetrieveAllStudents(); + + return allStudents.Any(student => student.Id == studentId); + }); + + // arch-046: Pass-through — no processing logic needed, delegates directly + public ValueTask RetrieveStudentByIdAsync(Guid studentId) => + this.studentService.RetrieveStudentByIdAsync(studentId); + } +} + +// --------------------------------------------------------------- +// Exception Handling partial (StudentProcessingService.Exceptions.cs) +// --------------------------------------------------------------- + +namespace MyProject.Services.Processings.Students +{ + public partial class StudentProcessingService + { + // arch-045: Unwrap foundation exceptions, re-wrap as processing exceptions + private delegate ValueTask ReturningStudentFunction(); + private delegate ValueTask ReturningBoolFunction(); + + private async ValueTask TryCatch(ReturningStudentFunction returningStudentFunction) + { + try + { + return await returningStudentFunction(); + } + catch (StudentValidationException studentValidationException) + { + throw CreateAndLogValidationException(studentValidationException.InnerException as Xeption); + } + catch (StudentDependencyValidationException studentDependencyValidationException) + { + throw CreateAndLogDependencyValidationException( + studentDependencyValidationException.InnerException as Xeption); + } + catch (StudentDependencyException studentDependencyException) + { + throw CreateAndLogDependencyException(studentDependencyException.InnerException as Xeption); + } + catch (StudentServiceException studentServiceException) + { + throw CreateAndLogServiceException(studentServiceException.InnerException as Xeption); + } + catch (Exception serviceException) + { + var failedStudentProcessingServiceException = + new FailedStudentProcessingServiceException( + message: "Failed student processing service error occurred, contact support.", + innerException: serviceException); + + throw CreateAndLogServiceException(failedStudentProcessingServiceException); + } + } + } +} diff --git a/.agents/skills/the-standard-architecture/manifest.json b/.agents/skills/the-standard-architecture/manifest.json new file mode 100644 index 0000000..1ae6208 --- /dev/null +++ b/.agents/skills/the-standard-architecture/manifest.json @@ -0,0 +1,71 @@ +{ + "name": "the-standard-architecture", + "version": "1.0.0", + "description": "Governs how Standard-compliant systems are decomposed into models, brokers, foundation services, processing services, orchestration services, aggregation services, and exposers. Defines dependency flow, validation strategy, exception categorization, and REST API conventions.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.3.0.0", + + "inputs": [ + "A system or feature design that requires architectural decisions", + "A code review request for an existing system component" + ], + + "outputs": [ + "A Standard-compliant system decomposition (brokers, services, exposers)", + "Validation strategy per layer", + "Exception hierarchy for each entity", + "REST API route and HTTP code mapping" + ], + + "dependencies": [ + "the-standard-core" + ], + + "related_skills": [ + { + "skill": "the-standard-events", + "relationship": "extended-by", + "when": "CulDeSac eventing is chosen -- event broker, event service, publish/subscribe, DI registration, and startup activation" + }, + { + "skill": "the-standard-versioning", + "relationship": "extended-by", + "when": "a model, service, or API contract change is introduced, a release is being cut, or existing code must be deprecated" + } + ], + + "activation": { + "trigger": "when designing systems, creating services, reviewing architecture, or writing brokers/controllers", + "note": "Always activate the-standard-core first." + }, + + "validation": { + "required": true, + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_broker.cs", + "examples/good/example_foundation_service.cs", + "examples/good/example_processing_service.cs" + ], + "bad": [ + "examples/bad/example_bad_broker.cs", + "examples/bad/example_bad_service.cs" + ] + }, + "templates": [ + "templates/broker_template.cs", + "templates/foundation_service_template.cs", + "templates/processing_service_template.cs" + ], + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-architecture/rules/rules.json b/.agents/skills/the-standard-architecture/rules/rules.json new file mode 100644 index 0000000..6144da0 --- /dev/null +++ b/.agents/skills/the-standard-architecture/rules/rules.json @@ -0,0 +1,60 @@ +{ + "rules": [ + { "id": "arch-001", "category": "brokers", "description": "Every broker must implement a local interface.", "severity": "error" }, + { "id": "arch-002", "category": "brokers", "description": "Brokers must contain no flow control (no if, switch, for, while).", "severity": "error" }, + { "id": "arch-003", "category": "brokers", "description": "Brokers must not handle exceptions — they propagate raw to the service.", "severity": "error" }, + { "id": "arch-004", "category": "brokers", "description": "Brokers own their own configuration — connection strings and credentials are injected into the broker only.", "severity": "error" }, + { "id": "arch-005", "category": "brokers", "description": "Brokers must use native C# primitives, not framework-specific types, in their method signatures where possible.", "severity": "error" }, + { "id": "arch-006", "category": "brokers", "description": "Brokers use infrastructure language (InsertStudentAsync) not business language (AddStudentAsync).", "severity": "error" }, + { "id": "arch-007", "category": "brokers", "description": "Brokers communicate upward to services and sideways to support brokers. Never to other entity brokers.", "severity": "error" }, + { "id": "arch-008", "category": "brokers", "description": "One broker wraps exactly one external resource.", "severity": "error" }, + { "id": "arch-009", "category": "brokers", "description": "All broker methods must return ValueTask or ValueTask (prefer ValueTask over Task per §1.5.1). This includes SelectAll* — it must return ValueTask>, never synchronous IQueryable.", "severity": "error" }, + { "id": "arch-015", "category": "brokers", "description": "Asynchronization Abstraction (§1.5.1): every publicly exposed interface method — across brokers, services, and exposers — must return ValueTask or ValueTask, even if the current implementation does not internally await anything. The public contract must remain uniformly async so callers never need to change when an implementation evolves from synchronous to asynchronous. Wrapping a synchronous result with ValueTask.FromResult() or using the async keyword on an expression-bodied method are both acceptable.", "severity": "error" }, + { "id": "arch-010", "category": "brokers", "description": "Support brokers (Logging, DateTime, Identifier) provide cross-cutting infrastructure, not entity operations.", "severity": "error" }, + { "id": "arch-011", "category": "brokers", "description": "StorageBroker constructor must call this.Database.Migrate() to apply pending migrations at startup.", "severity": "error" }, + { "id": "arch-012", "category": "brokers", "description": "Entity partials must delegate to private generic helpers (InsertAsync, SelectAllAsync, SelectAsync, UpdateAsync, DeleteAsync) defined in the base partial. Entity partials must never reference this.Entry(), this.Set(), this.FindAsync(), this.SaveChangesAsync(), or DbSet properties directly.", "severity": "error" }, + { "id": "arch-013", "category": "brokers", "description": "Before scaffolding broker operations, the agent must clarify which specific operation(s) are required. A branch scoped to one operation (e.g., BROKERS-student-insert) must implement only that operation — not all CRUD.", "severity": "error" }, + { "id": "arch-014", "category": "brokers", "description": "DbSet properties must be declared in the entity partial file (e.g., StorageBroker.Students.cs), never in the base StorageBroker.cs. The base partial owns only configuration, the constructor, OnConfiguring, and private generic helpers.", "severity": "error" }, + { "id": "arch-020", "category": "foundation-services", "description": "Foundation services are pure-primitive: input and output types are always the same entity type.", "severity": "error" }, + { "id": "arch-021", "category": "foundation-services", "description": "Foundation services integrate with exactly one entity type.", "severity": "error" }, + { "id": "arch-022", "category": "foundation-services", "description": "Foundation services use business language (Add, Retrieve, Modify, Remove) not infrastructure language.", "severity": "error" }, + { "id": "arch-023", "category": "foundation-services", "description": "Foundation services must validate all inputs before delegating to a broker.", "severity": "error" }, + { "id": "arch-024", "category": "foundation-services", "description": "Structural validations (null, empty, default) must run first and break immediately (circuit-breaking).", "severity": "error" }, + { "id": "arch-025", "category": "foundation-services", "description": "Logical validations (business rules) run after structural validations.", "severity": "error" }, + { "id": "arch-026", "category": "foundation-services", "description": "External validations (existence checks) run after logical validations.", "severity": "error" }, + { "id": "arch-027", "category": "foundation-services", "description": "Dependency validations cover broker-specific failure conditions.", "severity": "error" }, + { "id": "arch-028", "category": "foundation-services", "description": "Continuous validations must collect all invalid fields before throwing.", "severity": "error" }, + { "id": "arch-029", "category": "foundation-services", "description": "All exceptions from brokers must be caught, localized into Standard exceptions, categorized, and logged.", "severity": "error" }, + { "id": "arch-030", "category": "foundation-services", "description": "Exception categories: ValidationException, DependencyValidationException, CriticalDependencyException, DependencyException, ServiceException.", "severity": "error" }, + { "id": "arch-031", "category": "foundation-services", "description": "Foundation services must verify no unwanted broker calls occur after every operation.", "severity": "error" }, + { "id": "arch-032", "category": "foundation-services", "description": "Non-local models (external API models) must be mapped to local models before returning.", "severity": "warning" }, + { "id": "arch-040", "category": "processing-services", "description": "Processing services depend on exactly one foundation service.", "severity": "error" }, + { "id": "arch-041", "category": "processing-services", "description": "Processing services validate only the data they actually use.", "severity": "error" }, + { "id": "arch-042", "category": "processing-services", "description": "Processing services implement higher-order logic: Ensure, Upsert, TryAdd, TryRemove.", "severity": "error" }, + { "id": "arch-043", "category": "processing-services", "description": "Processing services implement shifters: transformations from one type to another.", "severity": "error" }, + { "id": "arch-044", "category": "processing-services", "description": "Processing services implement combinations: retrieve+add, retrieve+modify (upsert).", "severity": "error" }, + { "id": "arch-045", "category": "processing-services", "description": "Processing services must unwrap foundation exceptions and re-wrap as processing-level exceptions.", "severity": "error" }, + { "id": "arch-046", "category": "processing-services", "description": "Pass-through methods (no processing logic) are allowed and must delegate directly to the foundation service.", "severity": "error" }, + { "id": "arch-050", "category": "orchestration-services", "description": "Orchestration services must follow the Florance Pattern: 2-3 service dependencies.", "severity": "error" }, + { "id": "arch-051", "category": "orchestration-services", "description": "Orchestration services coordinate multi-entity flows across multiple services.", "severity": "error" }, + { "id": "arch-052", "category": "orchestration-services", "description": "Call order must be enforced explicitly when flow correctness depends on it.", "severity": "error" }, + { "id": "arch-053", "category": "orchestration-services", "description": "Natural order (enforced by input/output dependencies) is preferred over mock-sequence verification.", "severity": "error" }, + { "id": "arch-054", "category": "orchestration-services", "description": "Orchestration services must unwrap and re-wrap exceptions from their dependencies.", "severity": "error" }, + { "id": "arch-055", "category": "orchestration-services", "description": "Orchestration variations: Coordination (2-3 deps), Management (3+ deps), UberManagement (4+), Unit of Work.", "severity": "warning" }, + { "id": "arch-060", "category": "aggregation-services", "description": "Aggregation services must not validate dependency call order.", "severity": "error" }, + { "id": "arch-061", "category": "aggregation-services", "description": "Aggregation services must not use mock-sequence style assertions.", "severity": "error" }, + { "id": "arch-062", "category": "aggregation-services", "description": "Aggregation services perform basic structural validations only.", "severity": "error" }, + { "id": "arch-063", "category": "aggregation-services", "description": "Aggregation services may pass-through to multiple service dependencies without ordering constraints.", "severity": "error" }, + { "id": "arch-064", "category": "aggregation-services", "description": "Aggregation services must aggregate exceptions from all dependencies.", "severity": "error" }, + { "id": "arch-070", "category": "exposers", "description": "Exposers (controllers) contain no business logic — pure mapping only.", "severity": "error" }, + { "id": "arch-071", "category": "exposers", "description": "Each entity has a single exposure point (one controller per entity).", "severity": "error" }, + { "id": "arch-072", "category": "exposers", "description": "REST controller routes follow pattern: /api/[entities] (plural entity name).", "severity": "error" }, + { "id": "arch-073", "category": "exposers", "description": "HTTP success codes: 200 for GET/PUT/DELETE, 201 for POST.", "severity": "error" }, + { "id": "arch-074", "category": "exposers", "description": "Validation errors → 400. Dependency validation → 400. Critical dependency → 500. Dependency → 500. Service → 500.", "severity": "error" }, + { "id": "arch-075", "category": "exposers", "description": "Controllers must not catch exceptions — they map them via middleware or problem-details handler.", "severity": "error" }, + { "id": "arch-080", "category": "general", "description": "Services must never call other services at the same layer (no Foundation→Foundation calls).", "severity": "error" }, + { "id": "arch-081", "category": "general", "description": "Services must never call infrastructure directly — only through brokers.", "severity": "error" }, + { "id": "arch-082", "category": "general", "description": "Dependency flow is strictly: Exposer → Service(s) → Broker(s) → External Resource.", "severity": "error" }, + { "id": "arch-083", "category": "general", "description": "Every service type must be declared and tested independently.", "severity": "error" } + ] +} diff --git a/.agents/skills/the-standard-architecture/rules/rules.md b/.agents/skills/the-standard-architecture/rules/rules.md new file mode 100644 index 0000000..509220b --- /dev/null +++ b/.agents/skills/the-standard-architecture/rules/rules.md @@ -0,0 +1,73 @@ +# The Standard Architecture — Rules + +## BROKERS + +**arch-001** [ERROR] Every broker must implement a local interface (e.g., `IStorageBroker`). +**arch-002** [ERROR] Brokers must contain no flow control (no `if`, `switch`, `for`, `while`). +**arch-003** [ERROR] Brokers must not handle exceptions — they propagate raw to the service. +**arch-004** [ERROR] Brokers own their own configuration — connection strings and credentials are injected into the broker only. +**arch-005** [ERROR] Brokers must use native C# primitives, not framework-specific types, in their method signatures where possible. +**arch-006** [ERROR] Brokers use infrastructure language, not business language (e.g., `InsertStudentAsync` not `AddStudentAsync`). +**arch-007** [ERROR] Brokers communicate upward (to services) and sideways (to support brokers like logging). Never to other entity brokers. +**arch-008** [ERROR] One broker wraps exactly one external resource (one database, one API, one queue). +**arch-009** [ERROR] All broker methods must be asynchronous (return `Task` or `ValueTask`). +**arch-010** [ERROR] Entity brokers handle entity CRUD against a resource. Support brokers (Logging, DateTime, Identifier) provide cross-cutting infrastructure. + +## FOUNDATION SERVICES + +**arch-020** [ERROR] Foundation services are pure-primitive: input and output types are always the same entity type. +**arch-021** [ERROR] Foundation services integrate with exactly one entity type. +**arch-022** [ERROR] Foundation services use business language (Add, Retrieve, Modify, Remove) not infrastructure language (Insert, Select, Update, Delete). +**arch-023** [ERROR] Foundation services must validate all inputs before delegating to a broker. +**arch-024** [ERROR] Structural validations (null, empty, default values) must run first and must break immediately (circuit-breaking). +**arch-025** [ERROR] Logical validations (business rules: date ranges, valid states) run after structural validations. +**arch-026** [ERROR] External validations (existence checks in storage for Modify/Remove) run after logical validations. +**arch-027** [ERROR] Dependency validations cover broker-specific failure conditions. +**arch-028** [ERROR] Continuous validations must collect all invalid fields before throwing, using an upsertable exception data structure. +**arch-029** [ERROR] All exceptions from brokers must be caught, localized into Standard exceptions, categorized, and logged. +**arch-030** [ERROR] Exception categories: ValidationException, DependencyValidationException, CriticalDependencyException, DependencyException, ServiceException. +**arch-031** [ERROR] Foundation services must verify no unwanted broker calls occur after every operation. +**arch-032** [WARNING] Non-local models (external API models) must be mapped to local models before returning from a foundation service. + +## PROCESSING SERVICES + +**arch-040** [ERROR] Processing services depend on exactly one foundation service. +**arch-041** [ERROR] Processing services validate only the data they actually use — no over-validation. +**arch-042** [ERROR] Processing services implement higher-order logic: Ensure, Upsert, TryAdd, TryRemove patterns. +**arch-043** [ERROR] Processing services implement shifters: transformations from one type to another (entity → bool, entity → count). +**arch-044** [ERROR] Processing services implement combinations: retrieve+add, retrieve+modify (upsert). +**arch-045** [ERROR] Processing services must unwrap foundation exceptions and re-wrap as processing-level exceptions. +**arch-046** [ERROR] Pass-through methods (no processing logic) are allowed and must delegate directly to the foundation service. + +## ORCHESTRATION SERVICES + +**arch-050** [ERROR] Orchestration services must follow the Florance Pattern: 2-3 foundation or processing service dependencies. +**arch-051** [ERROR] Orchestration services coordinate multi-entity flows across multiple services. +**arch-052** [ERROR] Call order must be enforced explicitly when flow correctness depends on it. +**arch-053** [ERROR] Natural order (enforced by input/output dependencies) is preferred over mock-sequence verification. +**arch-054** [ERROR] Orchestration services must unwrap and re-wrap exceptions from their dependencies. +**arch-055** [WARNING] Orchestration service variations: Coordination (2-3 deps), Management (3+ deps), UberManagement (4+), Unit of Work. + +## AGGREGATION SERVICES + +**arch-060** [ERROR] Aggregation services must not validate dependency call order. +**arch-061** [ERROR] Aggregation services must not use mock-sequence style assertions. +**arch-062** [ERROR] Aggregation services perform basic structural validations only. +**arch-063** [ERROR] Aggregation services may pass-through to multiple service dependencies without ordering constraints. +**arch-064** [ERROR] Aggregation services must aggregate exceptions from all dependencies. + +## EXPOSERS (CONTROLLERS) + +**arch-070** [ERROR] Exposers (controllers) contain no business logic — pure mapping only. +**arch-071** [ERROR] Each entity has a single exposure point (one controller per entity). +**arch-072** [ERROR] REST controller routes follow pattern: `/api/[entities]` (plural entity name). +**arch-073** [ERROR] HTTP success codes: 200 for GET/PUT/DELETE, 201 for POST. +**arch-074** [ERROR] Validation errors → HTTP 400. Dependency validation → HTTP 400. Critical dependency → HTTP 500. Dependency → HTTP 500. Service → HTTP 500. +**arch-075** [ERROR] Controllers must not catch exceptions — they map them via a middleware or problem-details handler. + +## GENERAL ARCHITECTURE + +**arch-080** [ERROR] Services must never call other services at the same layer (no Foundation → Foundation calls). +**arch-081** [ERROR] Services must never call infrastructure directly — only through brokers. +**arch-082** [ERROR] Dependency flow is strictly: Exposer → Service(s) → Broker(s) → External Resource. +**arch-083** [ERROR] Every service type (Foundation, Processing, Orchestration, Aggregation) must be declared and tested independently. diff --git a/.agents/skills/the-standard-architecture/templates/broker_template.cs b/.agents/skills/the-standard-architecture/templates/broker_template.cs new file mode 100644 index 0000000..ca36f78 --- /dev/null +++ b/.agents/skills/the-standard-architecture/templates/broker_template.cs @@ -0,0 +1,131 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Storage Broker +// Replace [Entity] with the entity name, PascalCase (e.g., Student) +// Replace [Entities] with the plural entity name (e.g., Students) +// Replace [entity] with the entity name, camelCase (e.g., student) +// Replace [Namespace] with your project namespace + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +// --------------------------------------------------------------- +// StorageBroker.cs — base partial +// Owns: configuration, OnConfiguring, constructor + Migrate(), generic CRUD helpers. +// Does NOT own: DbSet<> properties — those live in entity partials. +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial class StorageBroker : EFxceptionsContext, IStorageBroker + { + // NO DbSet<> properties here. Each entity partial declares its own DbSet. + private readonly IConfiguration configuration; + + public StorageBroker(IConfiguration configuration) + { + this.configuration = configuration; + this.Database.Migrate(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + string connectionString = + this.configuration.GetConnectionString("DefaultConnection"); + + optionsBuilder.UseSqlServer(connectionString); + } + + // Generic helpers — used by entity partials; never exposed on the interface + private async ValueTask InsertAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Added; + await this.SaveChangesAsync(); + + return entity; + } + + private ValueTask> SelectAllAsync() where T : class => + ValueTask.FromResult>(this.Set().AsNoTracking()); + + private async ValueTask SelectAsync(Guid entityId) where T : class => + await this.FindAsync(entityId); + + private async ValueTask UpdateAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Modified; + await this.SaveChangesAsync(); + + return entity; + } + + private async ValueTask DeleteAsync(T entity) where T : class + { + this.Entry(entity).State = EntityState.Deleted; + await this.SaveChangesAsync(); + + return entity; + } + } +} + +// --------------------------------------------------------------- +// StorageBroker.[Entities].cs — entity partial +// One file per entity. DbSet<> lives here, not in StorageBroker.cs. +// Delegates to generic helpers only — no direct EF calls. +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial class StorageBroker + { + // DbSet<> is declared in the entity partial, NOT in StorageBroker.cs. + public DbSet<[Entity]> [Entities] { get; set; } + + public async ValueTask<[Entity]> Insert[Entity]Async([Entity] [entity]) => + await this.InsertAsync([entity]); + + public async ValueTask> SelectAll[Entities]Async() => + await this.SelectAllAsync<[Entity]>(); + + public async ValueTask<[Entity]> Select[Entity]ByIdAsync(Guid [entity]Id) => + await this.SelectAsync<[Entity]>([entity]Id); + + public async ValueTask<[Entity]> Update[Entity]Async([Entity] [entity]) => + await this.UpdateAsync([entity]); + + public async ValueTask<[Entity]> Delete[Entity]Async([Entity] [entity]) => + await this.DeleteAsync([entity]); + } +} + +// --------------------------------------------------------------- +// IStorageBroker.cs — base interface partial +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial interface IStorageBroker { } +} + +// --------------------------------------------------------------- +// IStorageBroker.[Entities].cs — entity interface partial +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial interface IStorageBroker + { + ValueTask<[Entity]> Insert[Entity]Async([Entity] [entity]); + ValueTask> SelectAll[Entities]Async(); + ValueTask<[Entity]> Select[Entity]ByIdAsync(Guid [entity]Id); + ValueTask<[Entity]> Update[Entity]Async([Entity] [entity]); + ValueTask<[Entity]> Delete[Entity]Async([Entity] [entity]); + } +} diff --git a/.agents/skills/the-standard-architecture/templates/foundation_service_template.cs b/.agents/skills/the-standard-architecture/templates/foundation_service_template.cs new file mode 100644 index 0000000..38f58fc --- /dev/null +++ b/.agents/skills/the-standard-architecture/templates/foundation_service_template.cs @@ -0,0 +1,338 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Foundation Service +// Replace [Entity] with the entity name (e.g., Student) +// Replace [Namespace] with your project namespace + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xeptions; + +namespace [Namespace].Services.Foundations.[Entities] +{ + // Root file: [Entity]Service.cs + public partial class [Entity]Service : I[Entity]Service + { + private readonly IStorageBroker storageBroker; + private readonly ILoggingBroker loggingBroker; + + public [Entity]Service( + IStorageBroker storageBroker, + ILoggingBroker loggingBroker) + { + this.storageBroker = storageBroker; + this.loggingBroker = loggingBroker; + } + + public ValueTask<[Entity]> Add[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnAdd([entity]); + + return await this.storageBroker.Insert[Entity]Async([entity]); + }); + + // arch-015: Even though SelectAll does not require a network round-trip, + // the method must return ValueTask> to honour the + // Asynchronization Abstraction principle (§1.5.1 of The Standard). + public ValueTask> RetrieveAll[Entities]Async() => + TryCatch(async () => + await this.storageBroker.SelectAll[Entities]Async()); + + public ValueTask<[Entity]> Retrieve[Entity]ByIdAsync(Guid [entity]Id) => + TryCatch(async () => + { + Validate[Entity]Id([entity]Id); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity]Id); + + ValidateStorage[Entity](maybe[Entity], [entity]Id); + + return maybe[Entity]; + }); + + public ValueTask<[Entity]> Modify[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnModify([entity]); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity].Id); + + ValidateStorage[Entity](maybe[Entity], [entity].Id); + ValidateAgainstStorage[Entity]OnModify(inputEntity: [entity], storage[Entity]: maybe[Entity]); + + return await this.storageBroker.Update[Entity]Async([entity]); + }); + + public ValueTask<[Entity]> Remove[Entity]ByIdAsync(Guid [entity]Id) => + TryCatch(async () => + { + Validate[Entity]Id([entity]Id); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity]Id); + + ValidateStorage[Entity](maybe[Entity], [entity]Id); + + return await this.storageBroker.Delete[Entity]Async(maybe[Entity]); + }); + } +} + +// --------------------------------------------------------------- +// [Entity]Service.Validations.cs +// --------------------------------------------------------------- + +namespace [Namespace].Services.Foundations.[Entities] +{ + public partial class [Entity]Service + { + private void Validate[Entity]OnAdd([Entity] [entity]) + { + Validate[Entity]IsNotNull([entity]); + + Validate( + (Rule: IsInvalid([entity].Id), Parameter: nameof([Entity].Id)), + (Rule: IsInvalid([entity].Name), Parameter: nameof([Entity].Name)), + (Rule: IsInvalid([entity].CreatedDate), Parameter: nameof([Entity].CreatedDate)), + (Rule: IsInvalid([entity].UpdatedDate), Parameter: nameof([Entity].UpdatedDate)), + (Rule: IsNotSame( + firstDate: [entity].CreatedDate, + secondDate: [entity].UpdatedDate, + secondDateName: nameof([Entity].UpdatedDate)), + Parameter: nameof([Entity].UpdatedDate)), + (Rule: IsNotRecent([entity].CreatedDate), Parameter: nameof([Entity].CreatedDate))); + } + + private void Validate[Entity]OnModify([Entity] [entity]) + { + Validate[Entity]IsNotNull([entity]); + + Validate( + (Rule: IsInvalid([entity].Id), Parameter: nameof([Entity].Id)), + (Rule: IsInvalid([entity].Name), Parameter: nameof([Entity].Name)), + (Rule: IsInvalid([entity].CreatedDate), Parameter: nameof([Entity].CreatedDate)), + (Rule: IsInvalid([entity].UpdatedDate), Parameter: nameof([Entity].UpdatedDate)), + (Rule: IsSame( + firstDate: [entity].UpdatedDate, + secondDate: [entity].CreatedDate, + secondDateName: nameof([Entity].CreatedDate)), + Parameter: nameof([Entity].UpdatedDate)), + (Rule: IsNotRecent([entity].UpdatedDate), Parameter: nameof([Entity].UpdatedDate))); + } + + private static void Validate[Entity]IsNotNull([Entity] [entity]) + { + if ([entity] is null) + { + throw new Null[Entity]Exception(message: "[Entity] is null."); + } + } + + private static void ValidateStorage[Entity]([Entity] maybe[Entity], Guid [entity]Id) + { + if (maybe[Entity] is null) + { + throw new NotFound[Entity]Exception([entity]Id); + } + } + + private static void Validate[Entity]Id(Guid [entity]Id) + { + Validate((Rule: IsInvalid([entity]Id), Parameter: nameof([Entity].Id))); + } + + private static dynamic IsInvalid(Guid id) => new + { + Condition = id == Guid.Empty, + Message = "Id is required" + }; + + private static dynamic IsInvalid(string text) => new + { + Condition = string.IsNullOrWhiteSpace(text), + Message = "Text is required" + }; + + private static dynamic IsInvalid(DateTimeOffset date) => new + { + Condition = date == default, + Message = "Date is required" + }; + + private static dynamic IsNotSame( + DateTimeOffset firstDate, + DateTimeOffset secondDate, + string secondDateName) => new + { + Condition = firstDate != secondDate, + Message = $"Date is not the same as {secondDateName}" + }; + + private static dynamic IsSame( + DateTimeOffset firstDate, + DateTimeOffset secondDate, + string secondDateName) => new + { + Condition = firstDate == secondDate, + Message = $"Date is the same as {secondDateName}" + }; + + private dynamic IsNotRecent(DateTimeOffset date) + { + var (isNotRecent, startDate, endDate) = IsDateNotRecent(date); + + return new + { + Condition = isNotRecent, + Message = $"Date is not recent. Expected a value between {startDate} and {endDate}" + }; + } + + private static void Validate(params (dynamic Rule, string Parameter)[] validations) + { + var invalid[Entity]Exception = new Invalid[Entity]Exception( + message: "Invalid [entity]. Please correct the errors and try again."); + + foreach ((dynamic rule, string parameter) in validations) + { + if (rule.Condition) + { + invalid[Entity]Exception.UpsertDataList( + key: parameter, + value: rule.Message); + } + } + + invalid[Entity]Exception.ThrowIfContainsErrors(); + } + } +} + +// --------------------------------------------------------------- +// [Entity]Service.Exceptions.cs +// --------------------------------------------------------------- + +namespace [Namespace].Services.Foundations.[Entities] +{ + public partial class [Entity]Service + { + private delegate ValueTask<[Entity]> Returning[Entity]Function(); + + private async ValueTask<[Entity]> TryCatch(Returning[Entity]Function returning[Entity]Function) + { + try + { + return await returning[Entity]Function(); + } + catch (Null[Entity]Exception null[Entity]Exception) + { + throw await CreateAndLogValidationException(null[Entity]Exception); + } + catch (Invalid[Entity]Exception invalid[Entity]Exception) + { + throw await CreateAndLogValidationException(invalid[Entity]Exception); + } + catch (NotFound[Entity]Exception notFound[Entity]Exception) + { + throw await CreateAndLogValidationException(notFound[Entity]Exception); + } + catch (DuplicateKeyException duplicateKeyException) + { + var alreadyExists[Entity]Exception = + new AlreadyExists[Entity]Exception( + message: "[Entity] with the same id already exists.", + innerException: duplicateKeyException); + + throw await CreateAndLogDependencyValidationException(alreadyExists[Entity]Exception); + } + catch (DbUpdateConcurrencyException dbUpdateConcurrencyException) + { + var locked[Entity]Exception = + new Locked[Entity]Exception( + message: "[Entity] record is locked, please try again.", + innerException: dbUpdateConcurrencyException); + + throw await CreateAndLogDependencyValidationException(locked[Entity]Exception); + } + catch (DbUpdateException dbUpdateException) + { + var failed[Entity]StorageException = + new Failed[Entity]StorageException( + message: "Failed [entity] storage error occurred, contact support.", + innerException: dbUpdateException); + + throw await CreateAndLogDependencyException(failed[Entity]StorageException); + } + catch (Exception serviceException) + { + var failed[Entity]ServiceException = + new Failed[Entity]ServiceException( + message: "Unexpected service error occurred. Contact support.", + innerException: serviceException); + + throw await CreateAndLogServiceException(failed[Entity]ServiceException); + } + } + + // arch-015: CreateAndLog* helpers are async because ILoggingBroker.LogErrorAsync + // returns ValueTask. Callers in TryCatch use `throw await CreateAndLog*(...)`. + private async ValueTask<[Entity]ValidationException> CreateAndLogValidationException( + Xeption exception) + { + var [entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync([entity]ValidationException); + + return [entity]ValidationException; + } + + private async ValueTask<[Entity]DependencyValidationException> + CreateAndLogDependencyValidationException(Xeption exception) + { + var [entity]DependencyValidationException = + new [Entity]DependencyValidationException( + message: "[Entity] dependency validation error occurred, fix the errors.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync([entity]DependencyValidationException); + + return [entity]DependencyValidationException; + } + + private async ValueTask<[Entity]DependencyException> CreateAndLogDependencyException( + Xeption exception) + { + var [entity]DependencyException = + new [Entity]DependencyException( + message: "[Entity] dependency error occurred, contact support.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync([entity]DependencyException); + + return [entity]DependencyException; + } + + private async ValueTask<[Entity]ServiceException> CreateAndLogServiceException( + Xeption exception) + { + var [entity]ServiceException = + new [Entity]ServiceException( + message: "[Entity] service error occurred, contact support.", + innerException: exception); + + await this.loggingBroker.LogErrorAsync([entity]ServiceException); + + return [entity]ServiceException; + } + } +} diff --git a/.agents/skills/the-standard-architecture/templates/logging_broker_template.cs b/.agents/skills/the-standard-architecture/templates/logging_broker_template.cs new file mode 100644 index 0000000..1a710f6 --- /dev/null +++ b/.agents/skills/the-standard-architecture/templates/logging_broker_template.cs @@ -0,0 +1,60 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Logging Broker +// Replace [Namespace] with your project namespace +// Live under: Brokers/Loggings/ + +using System; +using Microsoft.Extensions.Logging; + +namespace [Namespace].Brokers.Loggings +{ + public class LoggingBroker : ILoggingBroker + { + private readonly ILogger logger; + + public LoggingBroker(ILogger logger) => + this.logger = logger; + + public async ValueTask LogInformationAsync(string message) => + this.logger.LogInformation(message); + + public async ValueTask LogTraceAsync(string message) => + this.logger.LogTrace(message); + + public async ValueTask LogDebugAsync(string message) => + this.logger.LogDebug(message); + + public async ValueTask LogWarningAsync(string message) => + this.logger.LogWarning(message); + + public async ValueTask LogErrorAsync(Exception exception) => + this.logger.LogError(exception, exception.Message); + + public async ValueTask LogCriticalAsync(Exception exception) => + this.logger.LogCritical(exception, exception.Message); + } +} + +namespace [Namespace].Brokers.Loggings +{ + public interface ILoggingBroker + { + ValueTask LogInformationAsync(string message); + ValueTask LogTraceAsync(string message); + ValueTask LogDebugAsync(string message); + ValueTask LogWarningAsync(string message); + ValueTask LogErrorAsync(Exception exception); + ValueTask LogCriticalAsync(Exception exception); + } +} + +// --------------------------------------------------------------- +// DI Registration — Program.cs +// --------------------------------------------------------------- +// +// builder.Services.AddLogging(); // registers ILogger +// builder.Services.AddTransient(); diff --git a/.agents/skills/the-standard-architecture/templates/processing_service_template.cs b/.agents/skills/the-standard-architecture/templates/processing_service_template.cs new file mode 100644 index 0000000..91bc62c --- /dev/null +++ b/.agents/skills/the-standard-architecture/templates/processing_service_template.cs @@ -0,0 +1,182 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Processing Service +// Replace [Entity] with entity name (e.g., Student) +// Replace [Namespace] with your project namespace + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xeptions; + +namespace [Namespace].Services.Processings.[Entities] +{ + // [Entity]ProcessingService.cs — root file + public partial class [Entity]ProcessingService : I[Entity]ProcessingService + { + private readonly I[Entity]Service [entity]Service; + private readonly ILoggingBroker loggingBroker; + + public [Entity]ProcessingService( + I[Entity]Service [entity]Service, + ILoggingBroker loggingBroker) + { + this.[entity]Service = [entity]Service; + this.loggingBroker = loggingBroker; + } + + // Higher-order logic: EnsureExists + public ValueTask<[Entity]> Ensure[Entity]ExistsAsync([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnEnsure([entity]); + + IQueryable<[Entity]> all[Entities] = this.[entity]Service.RetrieveAll[Entities](); + + bool is[Entity]Exists = all[Entities].Any(retrieved[Entity] => + retrieved[Entity].Id == [entity].Id); + + return is[Entity]Exists switch + { + false => await this.[entity]Service.Add[Entity]Async([entity]), + _ => await this.[entity]Service.Retrieve[Entity]ByIdAsync([entity].Id) + }; + }); + + // Higher-order logic: Upsert + public ValueTask<[Entity]> Upsert[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnUpsert([entity]); + + IQueryable<[Entity]> all[Entities] = this.[entity]Service.RetrieveAll[Entities](); + + bool is[Entity]Exists = all[Entities].Any(retrieved[Entity] => + retrieved[Entity].Id == [entity].Id); + + return is[Entity]Exists switch + { + true => await this.[entity]Service.Modify[Entity]Async([entity]), + false => await this.[entity]Service.Add[Entity]Async([entity]) + }; + }); + + // Shifter: [Entity] → bool + public ValueTask Verify[Entity]ExistsAsync(Guid [entity]Id) => + TryCatch(async () => + { + Validate[Entity]Id([entity]Id); + + IQueryable<[Entity]> all[Entities] = this.[entity]Service.RetrieveAll[Entities](); + + return all[Entities].Any([entity] => [entity].Id == [entity]Id); + }); + + // Pass-through (no processing logic) + public ValueTask<[Entity]> Retrieve[Entity]ByIdAsync(Guid [entity]Id) => + this.[entity]Service.Retrieve[Entity]ByIdAsync([entity]Id); + + // Pass-through (no processing logic) + public ValueTask<[Entity]> Remove[Entity]ByIdAsync(Guid [entity]Id) => + this.[entity]Service.Remove[Entity]ByIdAsync([entity]Id); + } +} + +// --------------------------------------------------------------- +// [Entity]ProcessingService.Exceptions.cs +// --------------------------------------------------------------- + +namespace [Namespace].Services.Processings.[Entities] +{ + public partial class [Entity]ProcessingService + { + private delegate ValueTask<[Entity]> Returning[Entity]Function(); + private delegate ValueTask ReturningBoolFunction(); + + private async ValueTask<[Entity]> TryCatch(Returning[Entity]Function returning[Entity]Function) + { + try + { + return await returning[Entity]Function(); + } + catch ([Entity]ValidationException [entity]ValidationException) + { + throw CreateAndLogValidationException([entity]ValidationException.InnerException as Xeption); + } + catch ([Entity]DependencyValidationException [entity]DependencyValidationException) + { + throw CreateAndLogDependencyValidationException( + [entity]DependencyValidationException.InnerException as Xeption); + } + catch ([Entity]DependencyException [entity]DependencyException) + { + throw CreateAndLogDependencyException([entity]DependencyException.InnerException as Xeption); + } + catch ([Entity]ServiceException [entity]ServiceException) + { + throw CreateAndLogServiceException([entity]ServiceException.InnerException as Xeption); + } + catch (Exception serviceException) + { + var failed[Entity]ProcessingServiceException = + new Failed[Entity]ProcessingServiceException( + message: "Failed [entity] processing service error occurred, contact support.", + innerException: serviceException); + + throw CreateAndLogServiceException(failed[Entity]ProcessingServiceException); + } + } + + private [Entity]ValidationException CreateAndLogValidationException(Xeption exception) + { + var [entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: exception); + + this.loggingBroker.LogError([entity]ValidationException); + + return [entity]ValidationException; + } + + private [Entity]DependencyValidationException CreateAndLogDependencyValidationException( + Xeption exception) + { + var [entity]DependencyValidationException = + new [Entity]DependencyValidationException( + message: "[Entity] dependency validation error occurred, fix the errors.", + innerException: exception); + + this.loggingBroker.LogError([entity]DependencyValidationException); + + return [entity]DependencyValidationException; + } + + private [Entity]DependencyException CreateAndLogDependencyException(Xeption exception) + { + var [entity]DependencyException = + new [Entity]DependencyException( + message: "[Entity] dependency error occurred, contact support.", + innerException: exception); + + this.loggingBroker.LogError([entity]DependencyException); + + return [entity]DependencyException; + } + + private [Entity]ServiceException CreateAndLogServiceException(Xeption exception) + { + var [entity]ServiceException = + new [Entity]ServiceException( + message: "[Entity] service error occurred, contact support.", + innerException: exception); + + this.loggingBroker.LogError([entity]ServiceException); + + return [entity]ServiceException; + } + } +} diff --git a/.agents/skills/the-standard-architecture/validations/anti-patterns.md b/.agents/skills/the-standard-architecture/validations/anti-patterns.md new file mode 100644 index 0000000..b588b30 --- /dev/null +++ b/.agents/skills/the-standard-architecture/validations/anti-patterns.md @@ -0,0 +1,170 @@ +# The Standard Architecture — Anti-Patterns + +--- + +## AP-ARCH-001: Business Logic in a Broker + +**What it is:** A broker contains `if`/`switch`/`for` statements or makes decisions. + +**Example:** +```csharp +public async ValueTask InsertStudentAsync(Student student) +{ + if (student.Name.Length > 100) // VIOLATION: flow control + throw new Exception("Name too long"); + + return await this.dbContext.Students.AddAsync(student); +} +``` + +**Why harmful:** Brokers are translation layers only. Business logic in a broker means it cannot be independently tested, and it contaminates the infrastructure layer with business concerns. + +**How to fix:** Move all validation to the service's validation partial class. + +--- + +## AP-ARCH-002: Exception Handling in a Broker + +**What it is:** A broker catches exceptions and swallows, transforms, or re-throws them. + +**Example:** +```csharp +public async ValueTask InsertStudentAsync(Student student) +{ + try + { + return await this.dbContext.Students.AddAsync(student); + } + catch (Exception ex) // VIOLATION + { + throw new Exception("Database failed", ex); + } +} +``` + +**Why harmful:** Raw exceptions from infrastructure carry the original context (type, message, inner exception) that the service needs to categorize correctly. Wrapping in the broker destroys that context and prevents correct Standard exception mapping. + +**How to fix:** Remove all try/catch from brokers. Let exceptions bubble to the service's `TryCatch` exception handler. + +--- + +## AP-ARCH-003: Foundation Service Handling Multiple Entities + +**What it is:** A single foundation service manages more than one entity type. + +**Example:** +```csharp +public class StudentCourseService +{ + public ValueTask AddStudentAsync(Student student) { ... } + public ValueTask AddCourseAsync(Course course) { ... } +} +``` + +**Why harmful:** Foundation services must be pure-primitive (same entity in/out). Mixing entity types makes validation, exception handling, and testing ambiguous. It violates single responsibility. + +**How to fix:** Create `StudentService` and `CourseService` as separate, independent foundation services. + +--- + +## AP-ARCH-004: Same-Layer Service Calling Another Same-Layer Service + +**What it is:** A foundation service calls another foundation service directly. + +**Example:** +```csharp +public class StudentService +{ + private readonly ICourseService courseService; // VIOLATION + + public async ValueTask AddStudentAsync(Student student) + { + var courses = await this.courseService.RetrieveAllCoursesAsync(); // VIOLATION + ... + } +} +``` + +**Why harmful:** Foundation services must only communicate upward. Same-layer calls create circular dependency risks, make the system harder to test, and violate the strictly layered architecture. + +**How to fix:** If cross-entity coordination is needed, create an orchestration service that depends on both foundation services. + +--- + +## AP-ARCH-005: Business Logic in a Controller + +**What it is:** A controller contains `if`/`switch` logic, calculations, or decisions beyond mapping. + +**Example:** +```csharp +[HttpPost] +public async Task PostStudentAsync(Student student) +{ + if (student.Age < 18) // VIOLATION: business logic in controller + return BadRequest("Student must be 18 or older"); + + var addedStudent = await this.studentService.AddStudentAsync(student); + return Created(addedStudent); +} +``` + +**Why harmful:** Controllers are pure mapping layers. Business rules in controllers are untestable through service unit tests and create duplicate validation that diverges from service-level validation. + +**How to fix:** Move age validation to the foundation service's structural/logical validation. The controller maps the resulting `StudentValidationException` to HTTP 400. + +--- + +## AP-ARCH-006: Not Logging Exceptions in the Service + +**What it is:** A service catches and re-throws exceptions without logging them. + +**Example:** +```csharp +catch (DuplicateKeyException duplicateKeyException) +{ + var alreadyExistsException = new AlreadyExistsStudentException(duplicateKeyException); + throw new StudentDependencyValidationException(alreadyExistsException); + // VIOLATION: no this.loggingBroker.LogError(...) +} +``` + +**Why harmful:** Without logging, production errors are invisible. The Standard requires every caught exception to be logged before being rethrown. + +**How to fix:** Always call `this.loggingBroker.LogError(...)` on the categorized exception before returning it. + +--- + +## AP-ARCH-007: Processing Service with Multiple Foundation Dependencies + +**What it is:** A processing service depends on two or more foundation services. + +**Example:** +```csharp +public class StudentProcessingService +{ + private readonly IStudentService studentService; + private readonly ICourseService courseService; // VIOLATION +} +``` + +**Why harmful:** Processing services must depend on exactly one foundation service. Multi-foundation processing services are orchestration services in disguise and violate the layer contracts. + +**How to fix:** If the logic truly requires both entities, promote this to an orchestration service with the correct naming and exception handling. + +--- + +## AP-ARCH-008: Aggregation Service Asserting Call Order + +**What it is:** Tests for an aggregation service verify the order in which dependencies are called. + +**Example:** +```csharp +// In aggregation service test — VIOLATION +var sequence = new MockSequence(); +mockStudentService.InSequence(sequence).Setup(...); +mockCourseService.InSequence(sequence).Setup(...); +``` + +**Why harmful:** Aggregation services by definition have no ordering contract. Asserting order creates a brittle test that will break if the implementation legitimately reorders calls for performance reasons. + +**How to fix:** Remove sequence assertions. Test only the contract: the correct inputs were passed and the correct output was returned. diff --git a/.agents/skills/the-standard-architecture/validations/checklist.md b/.agents/skills/the-standard-architecture/validations/checklist.md new file mode 100644 index 0000000..ea3cb0e --- /dev/null +++ b/.agents/skills/the-standard-architecture/validations/checklist.md @@ -0,0 +1,93 @@ +# The Standard Architecture — Validation Checklist + +Run this checklist before approving any architecture design or code review. +Each item is binary: PASS or FAIL. + +--- + +## BROKERS + +- [ ] **arch-001** Every broker implements a local interface. +- [ ] **arch-002** No flow control (`if`, `switch`, `for`, `while`) exists inside any broker method. +- [ ] **arch-003** No exception handling exists in any broker — exceptions propagate raw. +- [ ] **arch-004** Broker configuration (connection strings, API keys) is injected through the constructor only. +- [ ] **arch-006** Broker method names use infrastructure language (Insert, Select, Update, Delete, Post, Get, Put). +- [ ] **arch-007** No broker calls another entity broker. +- [ ] **arch-008** Each broker wraps exactly one external resource. +- [ ] **arch-009** All broker methods are async (return `Task` or `ValueTask`). + +--- + +## FOUNDATION SERVICES + +- [ ] **arch-020** Foundation service inputs and outputs are the same entity type. +- [ ] **arch-021** Foundation service interacts with exactly one entity type. +- [ ] **arch-022** Foundation service method names use business language (Add, Retrieve, Modify, Remove). +- [ ] **arch-023** All inputs are validated before delegating to a broker. +- [ ] **arch-024** Structural validations (null, empty, default) run first and break immediately. +- [ ] **arch-025** Logical validations run after structural validations. +- [ ] **arch-026** External validations (existence checks) run after logical validations. +- [ ] **arch-027** Dependency validations cover broker-specific failure conditions. +- [ ] **arch-028** Continuous validations collect all invalid fields before throwing (upsertable exception data). +- [ ] **arch-029** All broker exceptions are caught, localized, categorized, and logged. +- [ ] **arch-030** Exception types follow the five Standard categories: Validation, DependencyValidation, CriticalDependency, Dependency, Service. +- [ ] **arch-031** No unwanted broker calls occur (verified in tests). + +--- + +## PROCESSING SERVICES + +- [ ] **arch-040** Processing service depends on exactly one foundation service. +- [ ] **arch-041** Only fields actually used are validated. +- [ ] **arch-045** Foundation exceptions are unwrapped and re-wrapped as processing exceptions. + +--- + +## ORCHESTRATION SERVICES + +- [ ] **arch-050** Service has 2-3 dependencies (Florance Pattern). +- [ ] **arch-052** Call order is explicitly enforced when flow correctness depends on it. +- [ ] **arch-054** All dependency exceptions are unwrapped and re-wrapped. + +--- + +## AGGREGATION SERVICES + +- [ ] **arch-060** No call order validation exists. +- [ ] **arch-061** No mock-sequence style assertions exist. +- [ ] **arch-062** Only basic structural validations are present. + +--- + +## EXPOSERS + +- [ ] **arch-070** No business logic exists in any controller. +- [ ] **arch-071** Each entity has exactly one controller. +- [ ] **arch-072** Routes follow `/api/[entities]` convention (plural lowercase). +- [ ] **arch-073** Correct HTTP codes: 200 for GET/PUT/DELETE, 201 for POST. +- [ ] **arch-074** Correct error codes: 400 for validation, 500 for dependency/service errors. + +--- + +## GENERAL ARCHITECTURE + +- [ ] **arch-080** No same-layer service calls (no Foundation → Foundation). +- [ ] **arch-081** No service calls infrastructure directly (must use brokers). +- [ ] **arch-082** Dependency flow is strictly Exposer → Service → Broker → External Resource. +- [ ] **arch-083** Every service type is declared and tested independently. + +--- + +## RESULT + +| Layer | PASS / FAIL | +|---|---| +| Brokers | | +| Foundation Services | | +| Processing Services | | +| Orchestration Services | | +| Aggregation Services | | +| Exposers | | +| General Architecture | | + +**Overall: PASS only when every row is PASS.** diff --git a/.agents/skills/the-standard-code-csharp/SKILL.md b/.agents/skills/the-standard-code-csharp/SKILL.md new file mode 100644 index 0000000..c66d8d9 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/SKILL.md @@ -0,0 +1,966 @@ +--- +name: The Standard Code CSharp +description: Enforces the C# naming, organization, method, variable, class, field, instantiation, and comment/documentation rules of The Standard. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard Code CSharp + +## What this skill is + +This skill is the canonical C# coding-style and organization layer for The Standard. +Use it whenever generating, reviewing, refactoring, or evaluating C# code. + +## Explicit coverage map + +This skill explicitly covers all supplied C# style rule files: + +- Files +- Variables +- Methods +- Classes and Interfaces +- Comments and Documentation + +It also preserves the implementation-profile naming conventions supplied in the implementation specification. + +## When to use + +Use this skill for any C# or .NET code, including models, brokers, services, controllers, tests, and support code. + +## Mandatory operating behavior + +0. Follow these rules exactly. +1. Prefer explicit readability over personal taste. +2. Use Standard naming even when team-local habits differ. +3. Reject abbreviations, ambiguous names, noisy wrappers, and inconsistent formatting. +4. Keep methods at level 0 of detail whenever possible. +5. Break down code when chaining or long declarations become cognitively expensive. +6. Preserve property order when instantiating models. +7. Use `this.` for private fields. +8. Maintain partial-file conventions for multi-dimensional classes. + +## Canonical rule set + +## 0 Files +The following are the naming conventions and guidance for naming C# files. + +### 0.0 Naming +File names should follow the PascalCase convention followed by the file extension `.cs`. + +##### Do +```csharp +Student.cs +``` + +##### Also, Do +```csharp +StudentService.cs +``` + +##### Don't +```csharp +student.cs +``` + +##### Also, Don't +```csharp +studentService.cs +``` + +##### Also, Don't +```csharp +Student_Service.cs +``` + +### 0.1 Partial Class Files +Partial class files are files that contain nested classes for a root file. For instance: + +- StudentService.cs + - StudentService.Validations.cs + - StudentService.Exceptions.cs + +Both validations and exceptions are partial classes to display a different aspect of any given class in a multi-dimensional space. + +##### Do +```csharp +StudentService.Validations.cs +``` + +##### Also, Do +```csharp +StudentService.Validations.Add.cs +``` + +##### Don't +```csharp +StudentServiceValidations.cs +``` + +##### Also, Don't +```csharp +StudentService_Validations.cs +``` + +## 0. Variables + +### 0.0 Naming +Variable names should be concise and representative of nature and the quantity of the value it holds or will potentially hold. + +#### 0.0.0 Names +##### Do +```cs +var student = new Student(); +``` +##### Don't +```cs +var s = new Student(); +``` +##### Also, Don't +```cs +var stdnt = new Student(); +``` + +The same rule applies to lambda expressions: +##### Do +```cs +students.Where(student => student ... ); +``` +##### Don't +```cs +students.Where(s => s ... ); +``` +
+ +#### 0.0.1 Plurals +##### Do +```cs +var students = new List(); +``` +##### Don't +```cs +var studentList = new List(); +``` +
+ +#### 0.0.2 Names with Types + +##### Do +```cs +var student = new Student(); +``` +##### Don't +```cs +var studentModel = new Student(); +``` +##### Also, Don't +```cs +var studentObj = new Student(); +``` +
+ +#### 0.0.3 Nulls or Defaults +If a variable value is it's default such as ```0``` for ```int``` or ```null``` for strings and you are not planning on changing that value (for testing purposes for instance) then the name should identify that value. +##### Do +```cs +Student noStudent = null; +``` +##### Don't +```cs +Student student = null; +``` +##### Also, Do +```cs +int noChangeCount = 0; +``` + +##### But, Don't +```cs +int changeCount = 0; +``` +

+ +### 0.1 Declarations +Declaring a variable and instantiating it should indicate the immediate type of the variable, even if the value is to be determined later. +#### 0.1.0 Clear Types +If the right side type is clear, then use ```var``` to declare your variable +##### Do +```cs +var student = new Student(); +``` +##### Don't +```cs +Student student = new Student(); +``` +

+ +#### 0.1.1 Semi-Clear Types +If the right side isn't clear (but known) of the returned value type, then you must explicitly declare your variable with it's type. +##### Do +```cs +Student student = GetStudent(); +``` +##### Don't +```cs +var student = GetStudent(); +``` +
+ +#### 0.1.2 Unclear Types +If the right side isn't clear and unknown (such as an anonymous types) of the returned value type, you may use ```var``` as your variable type. +##### Do +```cs +var student = new +{ + Name = "Hassan", + Score = 100 +}; +``` +

+ +#### 0.1.3 Single-Property Types + +Assign properties directly if you are declaring a type with one property. + +#### Do + +```cs +var inputStudentEvent = new StudentEvent(); +inputStudentEvent.Student = inputProcessedStudent; +``` + +#### Don't + +```cs +var inputStudentEvent = new StudentEvent +{ + Student = inputProcessedStudent +}; +``` + +#### Also, Do +```cs +var studentEvent = new StudentEvent +{ + Student = someStudent, + Date = someDate +} +``` + +#### Don't +```cs +var studentEvent = new StudentEvent(); +studentEvent.Student = someStudent; +studentEvent.Date = someDate; +``` + +### 0.2 Organization + +#### 0.2.0 Breakdown +If a variable declaration exceeds 120 characters, break it down starting from the equal sign. + +##### Do +```cs +List washingtonSchoolsStudentsWithGrades = + await GetAllWashingtonSchoolsStudentsWithTheirGradesAsync(); + +``` +##### Don't +```cs +List washgintonSchoolsStudentsWithGrades = await GetAllWashingtonSchoolsStudentsWithTheirGradesAsync(); +``` +
+ +#### 0.2.1 Multiple Declarations +Declarations that occupy two lines or more should have a new line before and after them to separate them from previous and next variables declarations. + +##### Do +```cs +Student student = GetStudent(); + +List washingtonSchoolsStudentsWithGrades = + await GetAllWashingtonSchoolsStudentsWithTheirGradesAsync(); + +School school = await GetSchoolAsync(); +``` + +##### Don't +```cs +Student student = GetStudent(); +List washgintonSchoolsStudentsWithGrades = + await GetAllWashingtonSchoolsStudentsWithTheirGradesAsync(); +School school = await GetSchoolAsync(); +``` +Also, declarations of variables that are of only one line should have no new lines between them. + +##### Do +```cs +Student student = GetStudent(); +School school = await GetSchoolAsync(); +``` + +##### Don't +```cs +Student student = GetStudent(); + +School school = await GetSchoolAsync(); + +``` +
+ +## 1 Methods + +### 1.0 Naming +Method names should be a summary of what the method is doing, it needs to stay percise and short and representative of the operation with respect to synchrony. + +#### 1.0.0 Verbs +Method names must contain verbs in them to represent the action it performs. +##### Do +```cs +public List GetStudents() +{ + ... +} + +``` +##### Don't +```cs +public List Students() +{ + ... +} +``` +
+ +#### 1.0.1 Asynchronousy +Asynchronous methods should be postfixed by the term ```Async``` such as methods returning ```Task``` or ```ValueTask``` in general. +##### Do +```cs +public async ValueTask> GetStudentsAsync() +{ + ... +} +``` +##### Don't +```cs +public async ValueTask> GetStudents() +{ + ... +} +``` +
+ +#### 1.0.2 Input Parameters +Input parameters should be explicit about what property of an object they will be assigned to, or will be used for any action such as search. +##### Do +```cs +public async ValueTask GetStudentByNameAsync(string studentName) +{ + ... +} +``` +##### Don't +```cs +public async ValueTask GetStudentByNameAsync(string text) +{ + ... +} +``` +##### Also, Don't +```cs +public async ValueTask GetStudentByNameAsync(string name) +{ + ... +} +``` +
+ +#### 1.0.3 Action Parameters +If your method is performing an action with a particular parameter specify it. +##### Do +```cs +public async ValueTask GetStudentByIdAsync(Guid studentId) +{ + ... +} + +``` +##### Don't +```cs +public async ValueTask GetStudentAsync(Guid studentId) +{ + ... +} +``` +
+ +#### 1.0.4 Passing Parameters +When utilizing a method, if the input parameters aliases match the passed in variables in part or in full, then you don't have to use the aliases, otherwise you must specify your values with aliases. + +Assume you have a method: +```csharp +Student GetStudentByNameAsync(string studentName); +``` + +##### Do +```cs +string studentName = "Todd"; +Student student = await GetStudentByNameAsync(studentName); + +``` +##### Also, Do +```cs +Student student = await GetStudentByNameAsync(studentName: "Todd"); +``` + +##### Also, Do +```cs +Student student = await GetStudentByNameAsync(toddName); +``` + +##### Don't +```cs +Student student = await GetStudentByNameAsync("Todd"); +``` + +##### Don't +```cs +Student student = await GetStudentByNameAsync(todd); +``` + +

+ +### 1.1 Organization +In general encapsulate multiple lines of the same logic into their own method, and keep your method at level 0 of details at all times. + +#### 1.1.0 Single/Multiple-Liners + +##### 1.1.0.0 One-Liners +Any method that contains only one line of code should use fat arrows +##### Do +```cs +public List GetStudents() => this.storageBroker.GetStudents(); + +``` +##### Don't +```cs +public List Students() +{ + return this.storageBroker.GetStudents(); +} +``` + +If a one-liner method exceeds the length of 120 characters then break after the fat arrow with an extra tab for the new line. + +##### Do +```cs +public async ValueTask> GetAllWashingtonSchoolsStudentsAsync() => + await this.storageBroker.GetStudentsAsync(); +``` + +##### Don't +```cs +public async ValueTask> GetAllWashingtonSchoolsStudentsAsync() => await this.storageBroker.GetStudentsAsync(); +``` +
+ +##### 1.1.0.1 Multiple-Liners +If a method contains multiple liners separated or connected via chaining it must have a scope. Unless the parameters are going on the next line then a one-liner method with multi-liner params is allowed. + +##### Do +```cs +public Student AddStudent(Student student) +{ + ValidateStudent(student); + + return this.storageBroker.InsertStudent(student); +} +``` + +##### Also, Do +```cs +public Student AddStudent(Student student) +{ + return this.storageBroker.InsertStudent(student) + .WithLogging(); +} +``` + +##### Also, Do +```cs +public Student AddStudent( + Student student) +{ + return this.storageBroker.InsertStudent(student); +} +``` + +##### Don't +```cs +public Student AddStudent(Student student) => + this.storageBroker.InsertStudent(student) + .WithLogging(); +``` + +##### Also, Don't +```cs +public Student AddStudent( + Student student) => + this.storageBroker.InsertStudent(student); +``` + +#### 1.1.1 Returns +For multi-liner methods, take a new line between the method logic and the final return line (if any). +##### Do +```cs +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + + return studentsApiClient.GetStudents(); +} +``` + +##### Don't +```cs +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + return studentsApiClient.GetStudents(); +} +``` +
+ +#### 1.1.2 Multiple Calls +With mutliple method calls, if both calls are less than 120 characters then they may stack unless the final call is a method return, otherwise separate with a new line. +##### Do +```cs +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + List students = studentsApiClient.GetStudents(); + + return students; +} +``` + +##### Don't +```cs +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + + List students = studentsApiClient.GetStudents(); + + return students; +} +``` +##### Also, Do + +```cs +public async ValueTask> GetStudentsAsync() +{ + StudentsClient washingtonSchoolsStudentsApiClient = + await InitializeWashingtonSchoolsStudentsApiClientAsync(); + + List students = studentsApiClient.GetStudents(); + + return students; +} +``` +##### Don't + +```cs +public async ValueTask> GetStudentsAsync() +{ + StudentsClient washingtonSchoolsStudentsApiClient = + await InitializeWashingtonSchoolsStudentsApiClientAsync(); + List students = studentsApiClient.GetStudents(); + + return students; +} +``` +
+ +#### 1.1.3 Declaration +A method declaration should not be longer than 120 characters. +##### Do +```cs +public async ValueTask> GetAllRegisteredWashgintonSchoolsStudentsAsync( + StudentsQuery studentsQuery) +{ + ... +} +``` + +##### Don't +```cs +public async ValueTask> GetAllRegisteredWashgintonSchoolsStudentsAsync(StudentsQuery studentsQuery) +{ + ... +} +``` +
+ +#### 1.1.4 Multiple Parameters +If you are passing multiple parameters, and the length of the method call is over 120 characters, you must break by the parameters, with **one** parameter on each line. +##### Do +```cs +List redmondHighStudents = await QueryAllWashingtonStudentsByScoreAndSchoolAsync( + MinimumScore: 130, + SchoolName: "Redmond High"); +``` + +##### Don't +```cs +List redmondHighStudents = await QueryAllWashingtonStudentsByScoreAndSchoolAsync( + MinimumScore: 130,SchoolName: "Redmond High"); +``` + +#### 1.1.5 Chaining (Uglification/Beautification) +Some methods offer extensions to call other methods. For instance, you can call a `Select()` method after a `Where()` method. And so on until a full query is completed. + +We will follow a process of Uglification Beautification. We uglify our code to beautify our view of a chain methods. Here's some examples: + +##### Do +```csharp + students.Where(student => student.Name is "Elbek") + .Select(student => student.Name) + .ToList(); +``` + +##### Don't +```csharp + students + .Where(student => student.Name is "Elbek") + .Select(student => student.Name) + .ToList(); +``` + +The first approach enforces simplifying and cutting the chaining short as more calls continues to uglify the code like this: + +```csharp + students.SomeMethod(...) + .SomeOtherMethod(...) + .SomeOtherMethod(...) + .SomeOtherMethod(...) + .SomeOtherMethod(...); +``` +The uglification process forces breaking down the chains to smaller lists then processing it. The second approach (no uglification approach) may require additional cognitive resources to distinguish between a new statement and an existing one as follows: + +```csharp + student + .Where(student => student.Name is "Elbek") + .Select(student => student.Name) + .OrderBy(student => student.Name) + .ToList(); + + ProcessStudents(students); +``` + +## 4 Classes + +### 4.0 Naming +Classes that represent services or brokers in a Standard-Compliant architecture should represent the type of class in their naming convention, however that doesn't apply to models. + +#### 4.0.0 Models +##### Do +```cs +class Student { + ... +} +``` +##### Don't +```cs +class StudentModel { + +} +``` +
+ +#### 4.0.1 Services +In a singular fashion, for any class that contains business logic. +##### Do +```cs +class StudentService { + .... +} +``` +##### Don't +```cs +class StudentsService{ + ... +} +``` +##### Also, Don't +```cs +class StudentBusinessLogic { + ... +} +``` +##### Also, Don't +```cs +class StudentBL { + ... +} +``` +
+ +#### 4.0.2 Brokers +In a singular fashion, for any class that is a shim between your services and external resources. +##### Do +```cs +class StudentBroker { + .... +} +``` +##### Don't +```cs +class StudentsBroker { + ... +} +``` +
+ +#### 4.0.3 Controllers +In a plural fashion, to reflect endpoints such as ```/api/students``` to expose your logic via RESTful operations. +##### Do +```cs +class StudentsController { + .... +} +``` +##### Don't +```cs +class StudentController { + ... +} +``` + +

+### 4.1 Fields +A field is a variable of any type that is declared directly in a class or struct. Fields are members of their containing type. + +#### 4.1.0 Naming +Class fields are named in a camel cased fashion. +##### Do +```cs +class StudentsController { + private readonly string studentName; +} +``` +##### Don't +```cs +class StudentController { + private readonly string StudentName; +} +``` +##### Also, Don't +```cs +class StudentController { + private readonly string _studentName; +} +``` +Should follow the same rules for naming as mentioned in the Variables sections. + +
+ +#### 4.1.1 Referencing +When referencing a class private field, use ```this``` keyword to distinguish private class member from a scoped method or constructor level variable. +##### Do +```cs +class StudentsController { + private readonly string studentName; + + public StudentsController(string studentName) { + this.studentName = studentName; + } +} +``` +##### Don't +```cs +class StudentController { + private readonly string _studentName; + + public StudentsController(string studentName) { + _studentName = studentName; + } +} +``` + +

+### 4.2 Instantiations +#### 4.2.0 Input Params Aliases +If the input variables names match to input aliases, then use them, otherwise you must use the aliases, especially with values passed in. + +##### Do +```cs +int score = 150; +string name = "Josh"; + +var student = new Student(name, score); + +``` + +##### Also, Do +```cs +var student = new Student(name: "Josh", score: 150); + +``` + +##### But, Don't +```cs +var student = new Student("Josh", 150); + +``` + +##### Also, Don't +```cs +Student student = new (...); +``` + +#### 4.2.1 Honoring Property Order +When instantiating a class instance - make sure that your property assignment matches the properties order in the class declarations. + +##### Do +```cs +public class Student +{ + public Guid Id {get; set;} + public string Name {get; set;} +} + +var student = new Student +{ + Id = Guid.NewGuid(), + Name = "Elbek" +} +``` + +##### Also, Do +```cs +public class Student +{ + private readonly Guid id; + private readonly string name; + + public Student(Guid id, string name) + { + this.id = id; + this.name = name; + } +} + +var student = new Student (id: Guid.NewGuid(), name: "Elbek"); +``` +##### Don't +```cs +public class Student +{ + public Guid Id {get; set;} + public string Name {get; set;} +} + +var student = new Student +{ + Name = "Elbek", + Id = Guid.NewGuid() +} +``` + +##### Also, Don't +```cs +public class Student +{ + private readonly Guid id; + private readonly string name; + + public Student(string name, Guid id) + { + this.id = id; + this.name = name; + } +} + +var student = new Student (id: Guid.NewGuid(), name: "Elbek"); +``` +##### Also, Don't +```cs +public class Student +{ + private readonly Guid id; + private readonly string name; + + public Student(Guid id, string name) + { + this.id = id; + this.name = name; + } +} + +var student = new Student (name: "Elbek", id: Guid.NewGuid()); +``` + +## 12 Comments + +### 12.0 Introduction +Comments can only be used to explain what code can't. Whether the code is visible or not. + +### 12.1 Copyrights +Comments highlighting copyrights should follow this pattern: + +##### Do +```csharp + // --------------------------------------------------------------- + // Copyright (c) Coalition of the Good-Hearted Engineers + // FREE TO USE TO CONNECT THE WORLD + // --------------------------------------------------------------- +``` + +##### Don't +```csharp + + //---------------------------------------------------------------- + // + // Copyright (C) Coalition of the Good-Hearted Engineers + // + //---------------------------------------------------------------- + +``` + +##### Also, Don't +```csharp + /* + * ============================================================== + * Copyright (c) Coalition of the Good-Hearted Engineers + * FREE TO USE TO CONNECT THE WORLD + * ============================================================== + */ +``` + +### 12.2 Methods +Methods that have code that is not accessible at dev-time, or perform a complex function should contain the following details in their documentation. + +- Purposing +- Incomes +- Outcomes +- Side Effects + +## Implementation-profile naming addendum + +## 10. Naming Conventions + +| Element | Pattern | Example | +| ------------------ | ------------------------------------------------------------- | ---------------------------------- | +| Broker interface | `I{Resource}Broker` | `IStorageBroker`, `IModernApiBroker`, `ILoggingBroker` | +| Broker class | `{Resource}Broker` | `StorageBroker`, `ModernApiBroker`, `LoggingBroker` | +| Broker method | `{Action}{Entity}Async` | `InsertLegacyUserAsync`, `PostPersonAsync` | +| Service interface | `I{Entity}Service` | `ILegacyUserService` | +| Service class | `{Entity}Service` | `LegacyUserService` | +| Service method | `Add{Entity}Async` | `AddLegacyUserAsync` | +| Inner exception | `{Adjective}{Entity}Exception` | `NullLegacyUserException` | +| Outer exception | `{Entity}{Category}Exception` | `LegacyUserValidationException` | +| Test class | `{Entity}ServiceTests` | `LegacyUserServiceTests` | +| Test method | `Should{Action}Async` / `ShouldThrow{Exception}On{Action}…` | `ShouldAddLegacyUserAsync` | + +--- diff --git a/.agents/skills/the-standard-code-csharp/contracts/contracts.json b/.agents/skills/the-standard-code-csharp/contracts/contracts.json new file mode 100644 index 0000000..9188839 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/contracts/contracts.json @@ -0,0 +1,79 @@ +{ + "skill": "the-standard-code-csharp", + "version": "1.0.0", + + "naming": { + "files": { + "pattern": "PascalCase.cs", + "partial_files": "PascalCase.Dimension.cs or PascalCase.Dimension.Method.cs", + "forbidden": ["camelCase.cs", "PascalCase_Dimension.cs", "PascalCaseDimension.cs"] + }, + "variables": { + "style": "camelCase", + "full_descriptive_names": true, + "abbreviations": false, + "type_suffixes": false, + "null_intent": "prefix with 'no': noStudent, noCount", + "collections": "natural plural: students not studentList" + }, + "methods": { + "must_contain_verb": true, + "async_suffix": "Async for all Task/ValueTask-returning methods", + "parameter_naming": "fully qualified with entity context: studentName not name" + }, + "classes": { + "models": "PascalCase, no suffix", + "services": "Singular + Service: StudentService", + "brokers": "Singular + Broker: StorageBroker", + "controllers": "Plural entity + Controller: StudentsController" + }, + "fields": { + "style": "camelCase", + "prefix": "none (no underscore, no m_, no _)", + "reference": "always via this." + } + }, + + "line_length": { + "max_characters": 120, + "on_exceeded": { + "variable_declaration": "break after =", + "method_one_liner": "break after =>", + "method_declaration": "break parameters to next line, one per line", + "method_call": "break arguments to next line, one per line" + } + }, + + "method_style": { + "one_liner": "fat arrow =>", + "multi_liner": "scope body {}", + "chaining": "first call same line as subject, subsequent calls +1 tab indentation", + "return_separation": "blank line before return in multi-line methods" + }, + + "instantiation": { + "literal_args": "named aliases required", + "matching_var_names": "aliases optional", + "target_typed_new": false, + "property_order": "must match class declaration order" + }, + + "declaration_style": { + "clear_type_right_side": "var", + "unclear_type_right_side": "explicit type", + "single_property_object": "construct then assign", + "multi_property_object": "object initializer" + }, + + "blank_line_rules": { + "between_single_line_declarations": "none", + "around_multi_line_declarations": "one blank line before and after", + "before_return_in_multi_liner": "one blank line" + }, + + "copyright": { + "required": true, + "format": "// ---\n// Copyright (c) ...\n// FREE TO USE ...\n// ---", + "forbidden_formats": ["xml copyright", "block comment copyright"] + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_classes.cs b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_classes.cs new file mode 100644 index 0000000..b45578d --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_classes.cs @@ -0,0 +1,61 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Class naming, field, and instantiation violations +// --------------------------------------------------------------- + +namespace Examples.Bad +{ + // cs-060 VIOLATION: Model has type suffix + public class StudentModel { } + public class StudentDTO { } + public class StudentEntity { } + + // cs-061 VIOLATION: Service is plural + public class StudentsService { } + + // cs-061 VIOLATION: Service uses non-standard suffix + public class StudentBusinessLogic { } + public class StudentBL { } + + // cs-062 VIOLATION: Broker is plural + public class StudentsBroker { } + + // cs-063 VIOLATION: Controller is singular + public class StudentController : ControllerBase { } + + public class BadFieldExamples + { + // cs-070 VIOLATION: underscore prefix + private readonly IStorageBroker _storageBroker; + + // cs-070 VIOLATION: PascalCase field + private readonly IStorageBroker StorageBroker; + + public BadFieldExamples(IStorageBroker storageBroker) + { + // cs-072 VIOLATION: No this. prefix for private field assignment + _storageBroker = storageBroker; + } + } + + public class BadInstantiationExamples + { + public void Demonstrate() + { + // cs-080 VIOLATION: Literal without alias + var student = new Student("Josh", 150); + + // cs-082 VIOLATION: Target-typed new + Student student2 = new (...); + + // cs-083 VIOLATION: Instantiation order doesn't match class declaration order + // Student declares: Id, Name, CreatedDate, UpdatedDate + var student3 = new Student + { + Name = "Elbek", // wrong order — Name before Id + Id = Guid.NewGuid(), + UpdatedDate = DateTimeOffset.UtcNow, // wrong order — UpdatedDate before CreatedDate + CreatedDate = DateTimeOffset.UtcNow + }; + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_methods.cs b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_methods.cs new file mode 100644 index 0000000..92b1e3d --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_methods.cs @@ -0,0 +1,66 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Method violations +// Each violation annotated with the rule it breaks. +// --------------------------------------------------------------- + +namespace Examples.Bad +{ + public class BadMethodExamples + { + // cs-040 VIOLATION: No verb in method name + public List Students() => this.storageBroker.GetStudents(); + + // cs-041 VIOLATION: Async method missing Async suffix + public async ValueTask> GetStudents() => + await this.storageBroker.GetStudentsAsync(); + + // cs-042 VIOLATION: Input parameter not fully qualified (text instead of studentName) + public async ValueTask GetStudentByNameAsync(string text) { ... } + + // cs-042 VIOLATION: Input parameter not entity-qualified (name instead of studentName) + public async ValueTask GetStudentByNameAsync(string name) { ... } + + // cs-043 VIOLATION: Action parameter not described (GetStudentAsync instead of GetStudentByIdAsync) + public async ValueTask GetStudentAsync(Guid studentId) { ... } + + // cs-044 VIOLATION: Literal passed without alias + public async Task BadLiteralPass() + { + Student student = await GetStudentByNameAsync("Todd"); // should be studentName: "Todd" + } + + // cs-050 VIOLATION: One-liner using scope body instead of fat arrow + public List GetStudents() + { + return this.storageBroker.GetStudents(); + } + + // cs-051 VIOLATION: Multi-liner using fat arrow + public Student AddStudent(Student student) => + this.storageBroker.InsertStudent(student) + .WithLogging(); + + // cs-054 VIOLATION: No blank line before return + public List GetStudents2() + { + StudentsClient studentsApiClient = InitializeStudentApiClient(); + return studentsApiClient.GetStudents(); // missing blank line before return + } + + // cs-059 VIOLATION: Chaining starts on new line (no uglification) + public void BadChaining() + { + var result = students + .Where(student => student.Name is "Elbek") + .Select(student => student.Name) + .ToList(); + // ^ should start Where on the same line as students + } + + // cs-057 VIOLATION: Method declaration > 120 chars, no line break + public async ValueTask> GetAllRegisteredWashingtonSchoolsStudentsAsync(StudentsQuery studentsQuery) + { + return await this.storageBroker.GetStudentsAsync(studentsQuery); + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_variables.cs b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_variables.cs new file mode 100644 index 0000000..4f4a7d4 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/bad/example_bad_variables.cs @@ -0,0 +1,59 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Variable violations +// Each violation annotated with the rule it breaks. +// --------------------------------------------------------------- + +namespace Examples.Bad +{ + public class BadVariableExamples + { + public void DemonstrateViolations() + { + // cs-010 VIOLATION: single-letter abbreviation + var s = new Student(); + + // cs-010 VIOLATION: abbreviation in lambda + var filtered = students.Where(s => s.IsActive); + + // cs-011 VIOLATION: abbreviated lambda param + var result = students.Where(s => s.Name == "Todd"); + + // cs-012 VIOLATION: type suffix in plural + var studentList = new List(); + + // cs-013 VIOLATION: type suffix in variable name + var studentModel = new Student(); + var studentObj = new Student(); + + // cs-014 VIOLATION: null variable doesn't signal intent + Student student = null; + + // cs-015 VIOLATION: zero value doesn't signal intent + int changeCount = 0; + + // cs-020 VIOLATION: right-side clear but no var + Student student2 = new Student(); + + // cs-021 VIOLATION: right-side unclear but using var + var student3 = GetStudent(); // GetStudent() return type not obvious from var + + // cs-031 VIOLATION: multi-line declaration missing blank lines + Student anotherStudent = GetStudent(); + List washingtonSchoolsStudents = + await GetAllWashingtonSchoolsStudentsAsync(); + School school = await GetSchoolAsync(); + // ^ no blank lines around the multi-line declaration + + // cs-023 VIOLATION: single-property uses initializer (should be post-assign) + var inputStudentEvent = new StudentEvent + { + Student = inputProcessedStudent // should be: var e = new StudentEvent(); e.Student = ...; + }; + + // cs-023 VIOLATION: multi-property uses post-assignment (should use initializer) + var studentEvent = new StudentEvent(); + studentEvent.Student = someStudent; + studentEvent.Date = someDate; + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/good/example_classes.cs b/.agents/skills/the-standard-code-csharp/examples/good/example_classes.cs new file mode 100644 index 0000000..12cd864 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/good/example_classes.cs @@ -0,0 +1,82 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant class naming, fields, and instantiation + +namespace Examples.Good +{ + // cs-060: Model — no type suffix + public class Student + { + public Guid Id { get; set; } + public string Name { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset UpdatedDate { get; set; } + } + + // cs-061: Service — singular + Service suffix + public class StudentService : IStudentService + { + // cs-070: camelCase field name (no underscore, no PascalCase) + private readonly IStorageBroker storageBroker; + private readonly ILoggingBroker loggingBroker; + + public StudentService( + IStorageBroker storageBroker, + ILoggingBroker loggingBroker) + { + // cs-072: Reference with this. + this.storageBroker = storageBroker; + this.loggingBroker = loggingBroker; + } + } + + // cs-062: Broker — singular + Broker suffix + public partial class StorageBroker : IStorageBroker + { + private readonly IConfiguration configuration; + + public StorageBroker(IConfiguration configuration) + { + this.configuration = configuration; + } + } + + // cs-063: Controller — plural + Controller suffix + public class StudentsController : ControllerBase + { + private readonly IStudentService studentService; + + public StudentsController(IStudentService studentService) + { + this.studentService = studentService; + } + } + + // cs-083: Instantiation property order matches class declaration order + public class InstantiationExamples + { + public void Demonstrate() + { + // Student has: Id, Name, CreatedDate, UpdatedDate + // Initializer must follow that order: + var student = new Student + { + Id = Guid.NewGuid(), + Name = "Elbek", + CreatedDate = DateTimeOffset.UtcNow, + UpdatedDate = DateTimeOffset.UtcNow + }; + + // cs-080: Literal arguments require named aliases + var namedStudent = new Student(id: Guid.NewGuid(), name: "Hassan"); + + // cs-081: Variable names match → aliases optional + Guid id = Guid.NewGuid(); + string name = "Hassan"; + var studentFromVars = new Student(id, name); + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/good/example_methods.cs b/.agents/skills/the-standard-code-csharp/examples/good/example_methods.cs new file mode 100644 index 0000000..eaa3fc7 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/good/example_methods.cs @@ -0,0 +1,88 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant method usage + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Examples.Good +{ + public class MethodExamples + { + // cs-040: Contains verb (Get) + // cs-050: One line → fat arrow + public List GetStudents() => + this.storageBroker.GetStudents(); + + // cs-041: Async → Async suffix + // cs-050: One line → fat arrow + public async ValueTask> GetStudentsAsync() => + await this.storageBroker.GetStudentsAsync(); + + // cs-042: Parameter fully qualified (studentName, not name or text) + // cs-043: Action parameter explicit (ByName) + public async ValueTask GetStudentByNameAsync(string studentName) + { + // cs-044: Variable matches alias → no alias needed + return await this.storageBroker.GetStudentByNameAsync(studentName); + } + + // cs-044: Literal → alias required + public async ValueTask GetToddAsync() + { + return await this.storageBroker.GetStudentByNameAsync(studentName: "Todd"); + } + + // cs-051: Multi-line → scope body + // cs-054: Blank line before return + public List GetStudents() + { + StudentsClient studentsApiClient = InitializeStudentApiClient(); + + return studentsApiClient.GetStudents(); + } + + // cs-052: One-liner > 120 chars → break after => + public async ValueTask> GetAllWashingtonSchoolsStudentsAsync() => + await this.storageBroker.GetStudentsAsync(); + + // cs-057, cs-058: Method declaration > 120 chars → break params, one per line + public async ValueTask> GetAllRegisteredWashingtonSchoolsStudentsAsync( + StudentsQuery studentsQuery) + { + return await this.storageBroker.GetStudentsAsync(studentsQuery); + } + + // cs-059: Chaining — first call on same line, subsequent indented one extra tab + public void DemonstrateChaining() + { + var result = students.Where(student => student.Name is "Elbek") + .Select(student => student.Name) + .ToList(); + } + + // cs-055: Multiple calls < 120 chars → may stack + public async ValueTask> GetStudentsAsync() + { + StudentsClient studentsApiClient = InitializeStudentApiClient(); + List students = studentsApiClient.GetStudents(); + + return students; + } + + // cs-056: Call > 120 chars → blank line separation required + public async ValueTask> GetStudentsWithLongNameAsync() + { + StudentsClient washingtonSchoolsStudentsApiClient = + await InitializeWashingtonSchoolsStudentsApiClientAsync(); + + List students = studentsApiClient.GetStudents(); + + return students; + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/examples/good/example_variables.cs b/.agents/skills/the-standard-code-csharp/examples/good/example_variables.cs new file mode 100644 index 0000000..25d7ca7 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/examples/good/example_variables.cs @@ -0,0 +1,73 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant variable usage +// Each section references the rule it demonstrates. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Examples.Good +{ + public class VariableExamples + { + public void DemonstrateNaming() + { + // cs-010: Full descriptive name + var student = new Student(); + + // cs-010: Full name in lambda + var filteredStudents = students.Where(student => student.IsActive); + + // cs-012: Natural plural (not studentList) + var students = new List(); + + // cs-013: No type suffix (not studentModel) + var student = new Student(); + + // cs-014: Null intent signaled + Student noStudent = null; + + // cs-015: Zero value intent signaled + int noChangeCount = 0; + } + + public async Task DemonstrateDeclarations() + { + // cs-020: Right-side clear → var + var student = new Student(); + + // cs-021: Right-side not clear → explicit type + Student student = GetStudent(); + + // cs-023: Single-property — assign after construction + var inputStudentEvent = new StudentEvent(); + inputStudentEvent.Student = inputProcessedStudent; + + // cs-023: Multi-property — use initializer + var studentEvent = new StudentEvent + { + Student = someStudent, + Date = someDate + }; + } + + public async Task DemonstrateOrganization() + { + // cs-031, cs-032: Single-line declarations stack without blank lines + Student student = GetStudent(); + School school = await GetSchoolAsync(); + + // cs-031: Multi-line declaration needs blank lines before AND after + Student anotherStudent = GetStudent(); + + List washingtonSchoolsStudentsWithGrades = + await GetAllWashingtonSchoolsStudentsWithTheirGradesAsync(); + + School anotherSchool = await GetSchoolAsync(); + } + } +} diff --git a/.agents/skills/the-standard-code-csharp/manifest.json b/.agents/skills/the-standard-code-csharp/manifest.json new file mode 100644 index 0000000..f58bc23 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/manifest.json @@ -0,0 +1,58 @@ +{ + "name": "the-standard-code-csharp", + "version": "1.0.0", + "description": "Enforces The Standard's C# coding style rules: file naming, variable naming and organization, method naming and organization, class naming, field conventions, instantiation rules, and comment formats. Applies to all C# and .NET code including models, brokers, services, controllers, and tests.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.3.0.0", + + "inputs": [ + "Any C# source file or code snippet", + "A code review request", + "A code generation request for a C# component" + ], + + "outputs": [ + "Standard-compliant C# code", + "Code review feedback with rule references", + "Corrected code with violations fixed" + ], + + "dependencies": [ + "the-standard-core" + ], + + "activation": { + "trigger": "any C# or .NET code: models, services, brokers, controllers, tests, migrations, startup", + "note": "Always activate the-standard-core first." + }, + + "validation": { + "required": true, + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_variables.cs", + "examples/good/example_methods.cs", + "examples/good/example_classes.cs" + ], + "bad": [ + "examples/bad/example_bad_variables.cs", + "examples/bad/example_bad_methods.cs", + "examples/bad/example_bad_classes.cs" + ] + }, + "templates": [ + "templates/service_template.cs", + "templates/broker_template.cs" + ], + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-code-csharp/rules/rules.json b/.agents/skills/the-standard-code-csharp/rules/rules.json new file mode 100644 index 0000000..60c09ad --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/rules/rules.json @@ -0,0 +1,51 @@ +{ + "rules": [ + { "id": "cs-001", "category": "files", "description": "File names must use PascalCase and end with .cs.", "severity": "error" }, + { "id": "cs-002", "category": "files", "description": "Partial class files must use dot-notation: StudentService.Validations.cs.", "severity": "error" }, + { "id": "cs-003", "category": "files", "description": "Partial class files must NOT use concatenation: StudentServiceValidations.cs is forbidden.", "severity": "error" }, + { "id": "cs-004", "category": "files", "description": "Partial class files must NOT use underscore: StudentService_Validations.cs is forbidden.", "severity": "error" }, + { "id": "cs-010", "category": "variables-naming", "description": "Variable names must be full and descriptive. Single letters and abbreviations are forbidden.", "severity": "error" }, + { "id": "cs-011", "category": "variables-naming", "description": "Lambda parameter names must be full: students.Where(student => ...) not students.Where(s => ...).", "severity": "error" }, + { "id": "cs-012", "category": "variables-naming", "description": "Plural collections use natural plural: students not studentList, studentCollection.", "severity": "error" }, + { "id": "cs-013", "category": "variables-naming", "description": "No type suffix in variable names: student not studentModel, studentObj, studentEntity.", "severity": "error" }, + { "id": "cs-014", "category": "variables-naming", "description": "Variables with null/default values must signal intent: Student noStudent = null not Student student = null.", "severity": "error" }, + { "id": "cs-015", "category": "variables-naming", "description": "Zero-value numeric variables must signal intent: int noChangeCount = 0 not int changeCount = 0.", "severity": "error" }, + { "id": "cs-020", "category": "variables-declarations", "description": "When right-side type is clear (new Student()), use var.", "severity": "error" }, + { "id": "cs-021", "category": "variables-declarations", "description": "When right-side type is NOT clear (method call), declare with explicit type.", "severity": "error" }, + { "id": "cs-022", "category": "variables-declarations", "description": "When right-side type is anonymous, var is required.", "severity": "warning" }, + { "id": "cs-023", "category": "variables-declarations", "description": "Single-property: assign after construction. Multi-property: use initializer.", "severity": "error" }, + { "id": "cs-030", "category": "variables-organization", "description": "Variable declaration exceeding 120 characters must break after the = sign.", "severity": "error" }, + { "id": "cs-031", "category": "variables-organization", "description": "Multi-line declarations must have a blank line before AND after them.", "severity": "error" }, + { "id": "cs-032", "category": "variables-organization", "description": "Single-line declarations must have NO blank lines between consecutive single-line declarations.", "severity": "error" }, + { "id": "cs-040", "category": "methods-naming", "description": "Method names must contain a verb.", "severity": "error" }, + { "id": "cs-041", "category": "methods-naming", "description": "Async methods must end with Async suffix.", "severity": "error" }, + { "id": "cs-042", "category": "methods-naming", "description": "Input parameter names must be fully qualified: studentName not name, text, value.", "severity": "error" }, + { "id": "cs-043", "category": "methods-naming", "description": "Action parameters must identify the property acted on: GetStudentByIdAsync(Guid studentId).", "severity": "error" }, + { "id": "cs-044", "category": "methods-naming", "description": "When passing literals or non-matching variable names, the parameter alias is required.", "severity": "error" }, + { "id": "cs-050", "category": "methods-organization", "description": "Methods with exactly one line must use fat arrow syntax.", "severity": "error" }, + { "id": "cs-051", "category": "methods-organization", "description": "Methods with multiple lines must use a scope body {}.", "severity": "error" }, + { "id": "cs-052", "category": "methods-organization", "description": "Single-liner exceeding 120 chars must break after => with one extra tab indentation.", "severity": "error" }, + { "id": "cs-053", "category": "methods-organization", "description": "Multi-liner methods with chaining must use a scope body (not fat arrow).", "severity": "error" }, + { "id": "cs-054", "category": "methods-organization", "description": "Multi-line methods must have a blank line before the return statement.", "severity": "error" }, + { "id": "cs-055", "category": "methods-organization", "description": "Consecutive calls under 120 chars may stack without blank lines (unless final is return).", "severity": "error" }, + { "id": "cs-056", "category": "methods-organization", "description": "Calls exceeding 120 chars require blank line separation from adjacent calls.", "severity": "error" }, + { "id": "cs-057", "category": "methods-organization", "description": "Method declarations exceeding 120 characters must break parameters onto the next line.", "severity": "error" }, + { "id": "cs-058", "category": "methods-organization", "description": "When parameters are broken onto new lines, each parameter must be on its own line.", "severity": "error" }, + { "id": "cs-059", "category": "methods-organization", "description": "Method chaining: first call on same line as subject, each subsequent call indented one extra tab.", "severity": "error" }, + { "id": "cs-060", "category": "classes-naming", "description": "Model class names carry no type suffix: Student not StudentModel, StudentDTO.", "severity": "error" }, + { "id": "cs-061", "category": "classes-naming", "description": "Service class names are singular PascalCase + Service: StudentService not StudentsService.", "severity": "error" }, + { "id": "cs-062", "category": "classes-naming", "description": "Broker class names are singular PascalCase + Broker: StudentBroker not StudentsBroker.", "severity": "error" }, + { "id": "cs-063", "category": "classes-naming", "description": "Controller class names are plural PascalCase + Controller: StudentsController not StudentController.", "severity": "error" }, + { "id": "cs-070", "category": "classes-fields", "description": "Class fields are named in camelCase: studentName not StudentName, _studentName.", "severity": "error" }, + { "id": "cs-071", "category": "classes-fields", "description": "Class fields follow the same naming rules as variables (descriptive, no abbreviation).", "severity": "error" }, + { "id": "cs-072", "category": "classes-fields", "description": "Private class fields must be referenced using this.: this.studentName = studentName.", "severity": "error" }, + { "id": "cs-080", "category": "classes-instantiation", "description": "When passing literals, named aliases are required: new Student(name: 'Josh') not new Student('Josh').", "severity": "error" }, + { "id": "cs-081", "category": "classes-instantiation", "description": "When variable names match constructor aliases, aliases are not required.", "severity": "error" }, + { "id": "cs-082", "category": "classes-instantiation", "description": "Target-typed new() is forbidden: Student student = new (...) is not allowed.", "severity": "error" }, + { "id": "cs-083", "category": "classes-instantiation", "description": "Instantiation property assignment order must match the class declaration property order.", "severity": "error" }, + { "id": "cs-090", "category": "comments", "description": "Copyright block must use the exact Standard format with // ------ dash lines.", "severity": "error" }, + { "id": "cs-091", "category": "comments", "description": "XML-style copyright comments () are forbidden.", "severity": "error" }, + { "id": "cs-092", "category": "comments", "description": "Block comment copyright (/* ... */) is forbidden.", "severity": "error" }, + { "id": "cs-093", "category": "comments", "description": "Methods inaccessible at dev-time should document: Purpose, Incomes, Outcomes, Side Effects.", "severity": "warning" } + ] +} diff --git a/.agents/skills/the-standard-code-csharp/rules/rules.md b/.agents/skills/the-standard-code-csharp/rules/rules.md new file mode 100644 index 0000000..bbd821a --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/rules/rules.md @@ -0,0 +1,78 @@ +# The Standard Code CSharp — Rules + +## FILES + +**cs-001** [ERROR] File names must use PascalCase and end with `.cs` — e.g., `Student.cs`, `StudentService.cs`. +**cs-002** [ERROR] Partial class files must use dot-notation — e.g., `StudentService.Validations.cs`, `StudentService.Exceptions.cs`. +**cs-003** [ERROR] Partial class files must NOT use concatenation — `StudentServiceValidations.cs` is forbidden. +**cs-004** [ERROR] Partial class files must NOT use underscore — `StudentService_Validations.cs` is forbidden. + +## VARIABLES — NAMING + +**cs-010** [ERROR] Variable names must be full and descriptive. Single letters and abbreviations are forbidden: `s`, `stdnt`, `st`. +**cs-011** [ERROR] Lambda parameter names must be full: `students.Where(student => ...)` not `students.Where(s => ...)`. +**cs-012** [ERROR] Plural collections use natural plural: `students` not `studentList`, `studentCollection`, `studentArray`. +**cs-013** [ERROR] No type suffix in variable names: `student` not `studentModel`, `studentObj`, `studentEntity`. +**cs-014** [ERROR] Variables with null/default values must signal intent: `Student noStudent = null` not `Student student = null`. +**cs-015** [ERROR] Zero-value numeric variables must signal intent: `int noChangeCount = 0` not `int changeCount = 0`. + +## VARIABLES — DECLARATIONS + +**cs-020** [ERROR] When the right-side type is clear (e.g., `new Student()`), use `var`. +**cs-021** [ERROR] When the right-side type is NOT clear (e.g., method call returning concrete type), declare with explicit type. +**cs-022** [WARNING] When the right-side type is anonymous, `var` is required. +**cs-023** [ERROR] Single-property object: assign property after construction (not in initializer). Multi-property: use initializer. + +## VARIABLES — ORGANIZATION + +**cs-030** [ERROR] Variable declaration exceeding 120 characters must break after the `=` sign. +**cs-031** [ERROR] Multi-line declarations must have a blank line before AND after them. +**cs-032** [ERROR] Single-line declarations must have NO blank lines between consecutive single-line declarations. + +## METHODS — NAMING + +**cs-040** [ERROR] Method names must contain a verb: `GetStudents()` not `Students()`. +**cs-041** [ERROR] Async methods must end with `Async` suffix: `GetStudentsAsync()` not `GetStudents()`. +**cs-042** [ERROR] Input parameter names must be fully qualified: `studentName` not `name`, `text`, `value`. +**cs-043** [ERROR] Action parameters must identify what property is acted on: `GetStudentByIdAsync(Guid studentId)` not `GetStudentAsync(Guid studentId)`. +**cs-044** [ERROR] When calling a method with a matching variable name (full or partial), no alias is needed. When passing literals or non-matching names, the alias is required: `GetStudentByNameAsync(studentName: "Todd")` not `GetStudentByNameAsync("Todd")`. + +## METHODS — ORGANIZATION + +**cs-050** [ERROR] Methods with exactly one line of code must use fat arrow syntax: `=> this.storageBroker.GetStudents()`. +**cs-051** [ERROR] Methods with multiple lines of code must use a scope body `{ }`. +**cs-052** [ERROR] Single-liner exceeding 120 characters must break after `=>` with one extra tab indentation. +**cs-053** [ERROR] Multi-liner methods with chaining must use a scope body (not fat arrow). +**cs-054** [ERROR] Multi-line methods must have a blank line between the last logic statement and the `return` statement. +**cs-055** [ERROR] If multiple consecutive calls are under 120 chars, they may stack without blank lines unless the final call is a `return`. +**cs-056** [ERROR] If any call exceeds 120 chars, blank line separation is required between it and adjacent calls. +**cs-057** [ERROR] Method declarations exceeding 120 characters must break parameters onto the next line. +**cs-058** [ERROR] When multiple parameters are broken onto new lines, each parameter must be on its own line. +**cs-059** [ERROR] Method chaining (uglification-beautification): first call on same line as subject, each subsequent call indented by one extra tab. + +## CLASSES — NAMING + +**cs-060** [ERROR] Model class names carry no type suffix: `Student` not `StudentModel`, `StudentDTO`, `StudentEntity`. +**cs-061** [ERROR] Service class names are singular PascalCase + Service: `StudentService` not `StudentsService`, `StudentBL`. +**cs-062** [ERROR] Broker class names are singular PascalCase + Broker: `StudentBroker` not `StudentsBroker`. +**cs-063** [ERROR] Controller class names are plural PascalCase + Controller: `StudentsController` not `StudentController`. + +## CLASSES — FIELDS + +**cs-070** [ERROR] Class fields are named in camelCase: `private readonly string studentName` not `StudentName`, `_studentName`. +**cs-071** [ERROR] Class fields must follow the same naming rules as variables (descriptive, no abbreviation, no type suffix). +**cs-072** [ERROR] Private class fields must be referenced using `this.`: `this.studentName = studentName`. + +## CLASSES — INSTANTIATION + +**cs-080** [ERROR] When passing literals directly, named aliases are required: `new Student(name: "Josh")` not `new Student("Josh")`. +**cs-081** [ERROR] When variable names match constructor parameter aliases, aliases are not required: `new Student(name, score)` is valid if variables are `name` and `score`. +**cs-082** [ERROR] Target-typed `new()` is forbidden: `Student student = new (...)` is not allowed. +**cs-083** [ERROR] Instantiation property assignment order must match the class declaration property order. + +## COMMENTS + +**cs-090** [ERROR] Copyright block must use the exact Standard format: `// ------` dash lines, `// Copyright (c)...`, `// FREE TO USE...`. +**cs-091** [ERROR] XML-style copyright comments (``) are forbidden. +**cs-092** [ERROR] Block comment copyright (`/* ... */`) is forbidden. +**cs-093** [WARNING] Methods whose code is not accessible at dev-time should document: Purpose, Incomes, Outcomes, Side Effects. diff --git a/.agents/skills/the-standard-code-csharp/templates/broker_template.cs b/.agents/skills/the-standard-code-csharp/templates/broker_template.cs new file mode 100644 index 0000000..1bc7fba --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/templates/broker_template.cs @@ -0,0 +1,88 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Broker +// Replace [Entity] / [Entities] / [Resource] / [Namespace] with actual values. + +using System; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; + +namespace [Namespace].Brokers.Storages +{ + // cs-062: Singular + Broker + // StorageBroker partial — main DbContext file + public partial class StorageBroker : DbContext, IStorageBroker + { + // cs-070: camelCase field + private readonly IConfiguration configuration; + + public StorageBroker(IConfiguration configuration) + { + // cs-072: this. + this.configuration = configuration; + Database.Migrate(); + } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + // cs-072: this. + string connectionString = + this.configuration.GetConnectionString("DefaultConnection"); + + optionsBuilder.UseSqlServer(connectionString); + } + } +} + +// --------------------------------------------------------------- +// StorageBroker.[Entity].cs — entity-specific partial +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial class StorageBroker + { + public DbSet<[Entity]> [Entities] { get; set; } + + // cs-040: Verb (Insert) + // cs-041: Async + // cs-042/cs-043: Parameter [entity], method Insert[Entity]Async + // cs-050: One-liner → fat arrow + public async ValueTask<[Entity]> Insert[Entity]Async([Entity] [entity]) => + await this.InsertAsync([entity]); + + // cs-050: One-liner → fat arrow + public IQueryable<[Entity]> SelectAll[Entities]() => + this.SelectAll<[Entity]>(); + + // cs-042/cs-043: Parameter [entity]Id, method Select[Entity]ByIdAsync + public async ValueTask<[Entity]> Select[Entity]ByIdAsync(Guid [entity]Id) => + await this.SelectAsync<[Entity]>([entity]Id); + + public async ValueTask<[Entity]> Update[Entity]Async([Entity] [entity]) => + await this.UpdateAsync([entity]); + + public async ValueTask<[Entity]> Delete[Entity]Async([Entity] [entity]) => + await this.DeleteAsync([entity]); + } +} + +// --------------------------------------------------------------- +// IStorageBroker.cs — partial interface +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Storages +{ + public partial interface IStorageBroker + { + ValueTask<[Entity]> Insert[Entity]Async([Entity] [entity]); + IQueryable<[Entity]> SelectAll[Entities](); + ValueTask<[Entity]> Select[Entity]ByIdAsync(Guid [entity]Id); + ValueTask<[Entity]> Update[Entity]Async([Entity] [entity]); + ValueTask<[Entity]> Delete[Entity]Async([Entity] [entity]); + } +} diff --git a/.agents/skills/the-standard-code-csharp/templates/service_template.cs b/.agents/skills/the-standard-code-csharp/templates/service_template.cs new file mode 100644 index 0000000..a729392 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/templates/service_template.cs @@ -0,0 +1,96 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Standard-compliant Service file skeleton +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// e.g., [Entity]=Student, [Entities]=Students, [Namespace]=MyProject + +using System; +using System.Linq; +using System.Threading.Tasks; +using Xeptions; + +namespace [Namespace].Services.Foundations.[Entities] +{ + // cs-061: Singular + Service + public partial class [Entity]Service : I[Entity]Service + { + // cs-070: camelCase fields + // cs-072: will be referenced with this. + private readonly IStorageBroker storageBroker; + private readonly ILoggingBroker loggingBroker; + + // cs-057/cs-058: Constructor params broken onto next line when > 120 chars + public [Entity]Service( + IStorageBroker storageBroker, + ILoggingBroker loggingBroker) + { + // cs-072: this. reference + this.storageBroker = storageBroker; + this.loggingBroker = loggingBroker; + } + + // cs-040: Verb (Add) + // cs-041: Async suffix + public ValueTask<[Entity]> Add[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnAdd([entity]); + + return await this.storageBroker.Insert[Entity]Async([entity]); + }); + + // cs-050: One-liner → fat arrow (method body is a single expression) + public ValueTask> RetrieveAll[Entities]() => + TryCatch(() => + { + IQueryable<[Entity]> all[Entities] = + this.storageBroker.SelectAll[Entities](); + + return ValueTask.FromResult(all[Entities]); + }); + + // cs-042/cs-043: Parameter [entity]Id, method ByIdAsync + public ValueTask<[Entity]> Retrieve[Entity]ByIdAsync(Guid [entity]Id) => + TryCatch(async () => + { + Validate[Entity]Id([entity]Id); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity]Id); + + ValidateStorage[Entity](maybe[Entity], [entity]Id); + + return maybe[Entity]; + }); + + public ValueTask<[Entity]> Modify[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnModify([entity]); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity].Id); + + ValidateStorage[Entity](maybe[Entity], [entity].Id); + ValidateAgainstStorage[Entity]OnModify([entity], maybe[Entity]); + + return await this.storageBroker.Update[Entity]Async([entity]); + }); + + public ValueTask<[Entity]> Remove[Entity]ByIdAsync(Guid [entity]Id) => + TryCatch(async () => + { + Validate[Entity]Id([entity]Id); + + [Entity] maybe[Entity] = + await this.storageBroker.Select[Entity]ByIdAsync([entity]Id); + + ValidateStorage[Entity](maybe[Entity], [entity]Id); + + return await this.storageBroker.Delete[Entity]Async(maybe[Entity]); + }); + } +} diff --git a/.agents/skills/the-standard-code-csharp/validations/anti-patterns.md b/.agents/skills/the-standard-code-csharp/validations/anti-patterns.md new file mode 100644 index 0000000..6378592 --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/validations/anti-patterns.md @@ -0,0 +1,220 @@ +# The Standard Code CSharp — Anti-Patterns + +--- + +## AP-CS-001: Abbreviated Variable Names + +**What it is:** Using single letters or abbreviations for variable names. + +**Examples:** +```csharp +var s = new Student(); // cs-010 +var stdnt = new Student(); // cs-010 +students.Where(s => s.IsActive); // cs-011 +``` + +**Why harmful:** Abbreviated names make code harder to read, especially in refactoring tools, search, and code review. The Standard enforces Level 0 readability — abbreviations fail that test. + +**How to fix:** +```csharp +var student = new Student(); +students.Where(student => student.IsActive); +``` + +--- + +## AP-CS-002: Type Suffix Pollution + +**What it is:** Adding type information to variable or class names that is already implied. + +**Examples:** +```csharp +var studentModel = new Student(); // cs-013 +var studentList = new List(); // cs-012 +public class StudentModel { } // cs-060 +public class StudentDTO { } // cs-060 +``` + +**Why harmful:** Adds noise. The type is already visible from `new Student()` or `List`. It creates a maintenance burden when the type changes (the variable name also needs updating). + +**How to fix:** +```csharp +var student = new Student(); +var students = new List(); +public class Student { } +``` + +--- + +## AP-CS-003: Null Intent Hidden + +**What it is:** Naming a null-initialized variable as if it has a value. + +**Example:** +```csharp +Student student = null; // cs-014 — looks like it has a value +``` + +**Why harmful:** When a future engineer reads `student`, they expect it to hold a student. The null initialization is hidden. This leads to null-reference bugs that are hard to trace. + +**How to fix:** +```csharp +Student noStudent = null; +``` + +--- + +## AP-CS-004: Fat Arrow for Multi-Line Methods + +**What it is:** Using fat arrow `=>` for methods that contain multiple logical lines. + +**Example:** +```csharp +// cs-051/cs-053 VIOLATION +public Student AddStudent(Student student) => + this.storageBroker.InsertStudent(student) + .WithLogging(); +``` + +**Why harmful:** Fat arrows are for single-expression methods. Using them for multi-step logic obscures the fact that the method does more than one thing. It also prevents adding breakpoints, local variable inspection, and future steps. + +**How to fix:** +```csharp +public Student AddStudent(Student student) +{ + return this.storageBroker.InsertStudent(student) + .WithLogging(); +} +``` + +--- + +## AP-CS-005: Missing Blank Line Before Return + +**What it is:** Placing `return` immediately after the last logic statement without a blank line. + +**Example:** +```csharp +// cs-054 VIOLATION +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + return studentsApiClient.GetStudents(); // missing blank line +} +``` + +**Why harmful:** Makes the method harder to read at a glance. The return is not visually separated from the logic that produces the return value. + +**How to fix:** +```csharp +public List GetStudents() +{ + StudentsClient studentsApiClient = InitializeStudentApiClient(); + + return studentsApiClient.GetStudents(); +} +``` + +--- + +## AP-CS-006: Underscore Field Prefix + +**What it is:** Using `_fieldName` for private class fields. + +**Example:** +```csharp +// cs-070 VIOLATION +private readonly IStorageBroker _storageBroker; + +public StudentService(IStorageBroker storageBroker) +{ + _storageBroker = storageBroker; // cs-072 VIOLATION: no this. +} +``` + +**Why harmful:** Underscore is a C# style from earlier eras. The Standard uses `this.` to distinguish private fields from local variables. Underscores are redundant and visually noisy. + +**How to fix:** +```csharp +private readonly IStorageBroker storageBroker; + +public StudentService(IStorageBroker storageBroker) +{ + this.storageBroker = storageBroker; +} +``` + +--- + +## AP-CS-007: Positional Literal Arguments + +**What it is:** Passing literal values to a constructor or method without named aliases. + +**Example:** +```csharp +// cs-080 VIOLATION +var student = new Student("Josh", 150); +await GetStudentByNameAsync("Todd"); +``` + +**Why harmful:** At the call site, it is impossible to know what `"Josh"` and `150` represent without jumping to the constructor signature. Named aliases document the call in place. + +**How to fix:** +```csharp +var student = new Student(name: "Josh", score: 150); +await GetStudentByNameAsync(studentName: "Todd"); +``` + +--- + +## AP-CS-008: Instantiation Order Mismatch + +**What it is:** Initializing properties in a different order than they are declared in the class. + +**Example:** +```csharp +// cs-083 VIOLATION +// Class declares: Id, Name, CreatedDate, UpdatedDate +var student = new Student +{ + Name = "Elbek", // wrong — Name before Id + Id = Guid.NewGuid(), + UpdatedDate = DateTimeOffset.UtcNow, // wrong — Updated before Created + CreatedDate = DateTimeOffset.UtcNow +}; +``` + +**Why harmful:** Inconsistency between the class definition and instantiation sites creates cognitive overhead. Engineers must constantly verify alignment between two locations. + +**How to fix:** Always match instantiation order exactly to the class declaration order. + +--- + +## AP-CS-009: Non-Standard Copyright Block + +**What it is:** Using XML-style or block-comment copyright. + +**Examples:** +```csharp +// cs-091 VIOLATION +//---------------------------------------------------------------- +// +// Copyright (C) Coalition of the Good-Hearted Engineers +// +//---------------------------------------------------------------- + +// cs-092 VIOLATION +/* ============================================================ + * Copyright (c) Coalition of the Good-Hearted Engineers + * ============================================================ */ +``` + +**Why harmful:** The Standard prescribes a specific copyright format for consistency across all compliant codebases. Non-Standard formats create visual noise and break tooling that scans for Standard-compliant files. + +**How to fix:** +```csharp +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- +``` diff --git a/.agents/skills/the-standard-code-csharp/validations/checklist.md b/.agents/skills/the-standard-code-csharp/validations/checklist.md new file mode 100644 index 0000000..5c3523b --- /dev/null +++ b/.agents/skills/the-standard-code-csharp/validations/checklist.md @@ -0,0 +1,116 @@ +# The Standard Code CSharp — Validation Checklist + +Run this checklist on every C# file before committing or approving. +Each item is binary: PASS or FAIL. + +--- + +## FILES + +- [ ] **cs-001** File name is PascalCase ending in `.cs`. +- [ ] **cs-002** Partial class files use dot-notation (e.g., `StudentService.Validations.cs`). +- [ ] **cs-003** No partial class files use concatenated naming. +- [ ] **cs-004** No partial class files use underscore separation. + +--- + +## VARIABLES — NAMING + +- [ ] **cs-010** All variable names are full and descriptive (no single letters, no abbreviations). +- [ ] **cs-011** All lambda parameters are full names (not `s`, `x`, `e`). +- [ ] **cs-012** Collections use natural plural (no `List`, `Collection`, `Array` suffix). +- [ ] **cs-013** No variable has a type suffix (`Model`, `Obj`, `Entity`, `DTO`). +- [ ] **cs-014** Variables initialized to `null` signal that intent in the name (`noStudent`). +- [ ] **cs-015** Variables initialized to zero signal that intent in the name (`noChangeCount`). + +--- + +## VARIABLES — DECLARATIONS + +- [ ] **cs-020** Clear right-side type uses `var`. +- [ ] **cs-021** Unclear right-side type (method return) uses explicit type declaration. +- [ ] **cs-023** Single-property objects assign post-construction. Multi-property objects use initializer. + +--- + +## VARIABLES — ORGANIZATION + +- [ ] **cs-030** No variable declaration exceeds 120 characters without a break after `=`. +- [ ] **cs-031** Multi-line declarations have blank lines before AND after. +- [ ] **cs-032** Consecutive single-line declarations have no blank lines between them. + +--- + +## METHODS — NAMING + +- [ ] **cs-040** Every method name contains a verb. +- [ ] **cs-041** All async methods end with `Async`. +- [ ] **cs-042** All input parameters are fully qualified with entity context. +- [ ] **cs-043** Action-specific methods identify the property: `ByName`, `ById`. +- [ ] **cs-044** Literals passed as arguments use named aliases. + +--- + +## METHODS — ORGANIZATION + +- [ ] **cs-050** One-line methods use fat arrow `=>`. +- [ ] **cs-051** Multi-line methods use scope body `{ }`. +- [ ] **cs-052** Fat arrow methods exceeding 120 chars break after `=>` with extra tab. +- [ ] **cs-053** Chaining methods use scope body (not fat arrow). +- [ ] **cs-054** Multi-line methods have blank line before `return`. +- [ ] **cs-055** Stacked calls under 120 chars have no blank lines (unless final is return). +- [ ] **cs-056** Calls exceeding 120 chars have blank line separation. +- [ ] **cs-057** Method declarations exceeding 120 chars break parameters to next line. +- [ ] **cs-058** Broken parameters each occupy their own line. +- [ ] **cs-059** Chained calls: first on same line as subject, subsequent calls +1 tab. + +--- + +## CLASSES — NAMING + +- [ ] **cs-060** Model classes have no type suffix. +- [ ] **cs-061** Service classes are singular PascalCase + `Service`. +- [ ] **cs-062** Broker classes are singular PascalCase + `Broker`. +- [ ] **cs-063** Controller classes are plural entity + `Controller`. + +--- + +## CLASSES — FIELDS + +- [ ] **cs-070** All class fields are camelCase (no underscore, no PascalCase). +- [ ] **cs-072** Private fields are always accessed via `this.`. + +--- + +## CLASSES — INSTANTIATION + +- [ ] **cs-080** Literal arguments use named aliases. +- [ ] **cs-082** No target-typed `new()` usage. +- [ ] **cs-083** Initializer property order matches class declaration order. + +--- + +## COMMENTS + +- [ ] **cs-090** Copyright block follows Standard format exactly. +- [ ] **cs-091** No XML-style copyright. +- [ ] **cs-092** No block-comment copyright. + +--- + +## RESULT + +| Category | PASS / FAIL | +|---|---| +| Files | | +| Variables — Naming | | +| Variables — Declarations | | +| Variables — Organization | | +| Methods — Naming | | +| Methods — Organization | | +| Classes — Naming | | +| Classes — Fields | | +| Classes — Instantiation | | +| Comments | | + +**Overall: PASS only when every row is PASS.** diff --git a/.agents/skills/the-standard-core/SKILL.md b/.agents/skills/the-standard-core/SKILL.md new file mode 100644 index 0000000..e844c37 --- /dev/null +++ b/.agents/skills/the-standard-core/SKILL.md @@ -0,0 +1,280 @@ +--- +name: The Standard Core +description: Enforces the theory, purpose, modeling, simulation flow, and non-negotiable core values of The Standard. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard Core + +## What this skill is + +This skill is the governing layer for every other Standard skill. +It defines the theory, operating model, and core values that must shape all design, implementation, testing, review, and planning decisions. + +The Standard is technology-agnostic. +Apply it regardless of language, framework, runtime, or hosting model. +Use C# and .NET examples only as materializations of the theory, not as the theory itself. + +## Explicit coverage map + +This skill explicitly covers the foundational material of The Standard: + +- 0. Introduction +- 0.0 The Theory + - 0.0.0 Introduction + - 0.0.1 Finding Answers + - 0.0.2 Tri-Nature + - 0.0.2.0 Purpose + - 0.0.2.1 Dependency + - 0.0.2.2 Exposure + - 0.0.3 Everything is Connected + - 0.0.4 Fractal Pattern + - 0.0.5 Systems Design & Architecture + - 0.0.6 Conclusion +- 0.1 Purposing, Modeling, and Simulation + - 0.1.0 Introduction + - 0.1.1 Purposing + - 0.1.1.0 Observation + - 0.1.1.1 Articulation + - 0.1.1.2 Solutioning + - 0.1.2 Modeling + - 0.1.2.0 Model Types + - 0.1.2.0.0 Data Carrier Models + - 0.1.2.0.0.0 Primary Models + - 0.1.2.0.0.1 Secondary Models + - 0.1.2.0.0.2 Relational Models + - 0.1.2.0.0.3 Hybrid Models + - 0.1.2.0.1 Operational Models + - 0.1.2.0.1.0 Integration Models (Brokers) + - 0.1.2.0.1.1 Processing Models (Services) + - 0.1.2.0.1.2 Exposure Models (Exposers) + - 0.1.2.0.2 Configuration Models + - 0.1.3 Simulation + - 0.1.4 Summary +- 0.2 Principles + - 0.2.0 People-First + - 0.2.0.0 Simplicity + - 0.2.0.0.0 Excessive Inheritance + - 0.2.0.0.1 Entanglement + - 0.2.0.0.1.0 Horizontal Entanglement + - 0.2.0.0.1.1 Vertical Entanglement + - 0.2.0.0.2 Autonomous Components + - 0.2.0.0.2.0 No Magic + - 0.2.1 Rewritability + - 0.2.2 Mono-Micro + - 0.2.3 Level 0 + - 0.2.4 Open Code + - 0.2.5 Airplane Mode (Cloud-Foreign) + - 0.2.6 No Toasters + - 0.2.7 Pass Forward + - 0.2.8 All-In/All-Out + - 0.2.9 Readability over Optimization + - 0.2.10 Last Day + +## When to use + +Use this skill always when any software-related request is present. +Use it before architecture, before coding, before testing, before planning, and before review. +If another Standard skill applies, activate this one first and let it govern the rest. + +## Identity and theory + +0. Every system is governed by theory, whether explicit or implicit. +1. The Standard is powered by the Tri-Nature theory. +2. Every system must be understood through three lenses: + - Purpose + - Dependency + - Exposure +3. Tri-Nature is fractal. + - The same pattern repeats at system level, sub-system level, service level, validation level, and exposure level. +4. Everything is connected. + - Design every part with awareness that it will become someone else’s dependency or exposure. + +## Mandatory engineering sequence + +Follow this order intentionally: + +0. Purposing +1. Modeling +2. Simulation + +This order is mandatory at initiation. +Iteration is allowed later, but do not skip purpose to jump into code. + +## Purposing rules + +0. Start with observation. + - Identify the real blocker, constraint, or unmet need. +1. Articulate the problem clearly. + - Use words, diagrams, or figures when helpful. + - A well-described problem is halfway solved. +2. Include solutioning in the purpose. + - Do not treat the path to the goal as trivial. + - A solution must also honor readability, configurability, longevity, optimization, and maintainability. +3. Never cut corners to reach the goal. + - Reaching the goal the wrong way is a violation. +4. Always keep purpose visible. + - If the purpose is unclear, stop and clarify before modeling or implementation. + +## Modeling rules + +0. Model only what the purpose requires. +1. Do not attract irrelevant attributes into the model. +2. Treat models as classes that express only the data required for the problem. +3. Prefer the most generic valid name that still fits the problem scope. + +### Data carrier model rules + +0. Primary models are self-sufficient. + - They do not physically depend on another model to exist. +1. Secondary models depend on primary models. + - They usually reference another model or are nested within one. +2. Relational models connect two primary models. + - They exist to materialize many-to-many relationships. + - They should hold references, not unrelated details. +3. Hybrid models are allowed only when necessary. + - They mix relational behavior with additional details about the relationship. + - Prefer purity first; use hybrid models only when the business flow truly requires it. + +### Operational model rules + +0. Integration models are brokers. +1. Processing models are services. +2. Exposure models are exposers. +3. Configuration models are startup, composition, middleware, or entry-point models. + +## Simulation rules + +0. Simulation is how models interact. +1. A model may act on another model. +2. A model may be acted upon by another model. +3. A model may act on itself. +4. Functions, methods, and routines are simulation mechanisms. +5. Keep simulation inside the purpose and the model boundaries. + +## Core principles and non-negotiables + +### People-First + +0. Build for both users and future maintainers. +1. Favor human understanding over cleverness. +2. Favor systems that mainstream engineers can own, evolve, and rewrite. + +### Simplicity + +0. Simplicity is mandatory, not optional. +1. Simplicity creates rewritability. +2. Simplicity favors modular monoliths and clear decomposition. + +#### Excessive Inheritance + +0. Do not use more than one level of inheritance. +1. More than one level is excessive and prohibited, except when vertical versioning of flows absolutely requires it. + +#### Entanglement + +0. Avoid horizontal entanglement. + - No Utils. + - No Commons. + - No Helpers that pretend to simplify everything. + - No shared models across independent flows. + - No shared exceptions or shared validation rules across unrelated flows. +1. Avoid vertical entanglement. + - No local base components unless they are native or external. + - No local inheritance chains that create hidden coupling. +2. Prefer duplication over cross-flow entanglement when duplication preserves autonomy. + +### Autonomous Components + +0. Every component should be self-sufficient. +1. Every component owns its validations, tooling, and utilities in one of its dimensions. +2. Components may depend on injected dependencies, not hidden helpers. +3. Duplication is permitted when it preserves ownership and autonomy. + +### No Magic + +0. What you see is what you get. +1. No hidden routines. +2. No magical extensions that require chasing references. +3. No runtime tricks that make the system hard to understand. +4. Put validation, exception handling, tracing, security, localization, and behavior in plain sight. + +### Rewritability + +0. Every system must be easy to understand, modify, and fully rewrite. +1. A Standard-compliant system should be forkable, clonable, buildable, and testable with minimal surprise. +2. No hidden dependencies. +3. No unknown prerequisites. +4. No injected routines that obscure behavior. + +### Mono-Micro + +0. Build monoliths with a microservice mindset. +1. Every flow should be autonomous enough to be extracted later. +2. Modularize aggressively without splitting prematurely. + +### Level 0 + +0. Code must be understandable by entry-level engineers. +1. Level 0 understanding is the measure of success. +2. If new engineers cannot follow the system, the system is too complex. + +### Open Code + +0. Prefer code, tooling, platforms, and patterns that are visible and accessible. +1. Do not make source proprietary solely for personal or organizational gain. +2. Exceptions exist only when safety, security, or contractual obligations require it. + +### Airplane Mode (Cloud-Foreign) + +0. The system should be runnable locally without mandatory cloud dependency. +1. Develop tooling that bridges cloud resources to local stand-ins. +2. Favor local testing, local development, and mocked external systems. + +### No Toasters + +0. Do not force Standard compliance via style cops or analyzers as the primary mechanism. +1. Teach the Standard person-to-person. +2. Favor conviction and understanding over coercion. +3. AI-assisted coding (including “vibe coding”) is acceptable, + provided a human remains actively involved and retains full responsibility for the final code. +4. AI may be used for code reviews and suggestions, but the final authority for approval + and merge decisions must always rest with a human. + +### Pass Forward + +0. The Standard is to be taught at no cost. +1. Do not profiteer from teaching The Standard. +2. Do not use The Standard to belittle others. +3. Pass it forward freely. + +### All-In / All-Out + +0. The Standard must be embraced fully or rejected fully. +1. Partial adoption is not standardization. +2. Outdated partial adherence is not enough to claim a Standardized system. + +### Readability over Optimization + +0. When in doubt, choose readability. +1. Unreadable “optimal” software is not truly optimum. + +### Last Day + +0. Work every day as if it might be your last day on the project. +1. End each engineering day at a good stopping point. +2. Ensure another engineer can pick up the work seamlessly the next day. +3. Apply this to design, development, documentation, and automation. + +## Mandatory operating behavior for agents + +0. Never skip purpose. +1. Never start with implementation when purpose or models are unclear. +2. Never optimize at the expense of readability. +3. Never recommend hidden shared abstractions as a first move. +4. Never recommend analyzers or “toasters” as a substitute for understanding. +5. Reject vague or chaotic designs. +6. Prefer explicitness, autonomy, and rewritability. +7. Use the language of The Standard consistently. +8. If there is a conflict between generic convention and The Standard, The Standard wins. diff --git a/.agents/skills/the-standard-core/contracts/contracts.json b/.agents/skills/the-standard-core/contracts/contracts.json new file mode 100644 index 0000000..0cea637 --- /dev/null +++ b/.agents/skills/the-standard-core/contracts/contracts.json @@ -0,0 +1,104 @@ +{ + "skill": "the-standard-core", + "version": "1.0.0", + + "engineering_sequence": { + "mandatory_order": ["Purposing", "Modeling", "Simulation"], + "skip_allowed": false, + "iteration_allowed": true, + "note": "Iteration is allowed after initial sequence completes. Skipping is never allowed." + }, + + "model_types": { + "primary": { + "self_sufficient": true, + "external_physical_dependency": false, + "examples": ["Student", "Course", "Product", "Order"] + }, + "secondary": { + "depends_on": "primary", + "examples": ["StudentContact", "OrderLine"] + }, + "relational": { + "connects": "exactly_two_primary_models", + "unrelated_attributes": false, + "examples": ["StudentCourse", "UserRole"] + }, + "hybrid": { + "use_condition": "business_flow_truly_requires_it", + "default": false + }, + "operational": { + "integration": "broker", + "processing": "service", + "exposure": "exposer", + "configuration": "startup_or_middleware" + } + }, + + "forbidden_patterns": [ + { + "pattern": "Utils class", + "reason": "Creates horizontal entanglement", + "rule": "core-052" + }, + { + "pattern": "Commons class", + "reason": "Creates horizontal entanglement", + "rule": "core-052" + }, + { + "pattern": "Helper class", + "reason": "Creates horizontal entanglement", + "rule": "core-052" + }, + { + "pattern": "Shared model across independent flows", + "reason": "Breaks component autonomy", + "rule": "core-053" + }, + { + "pattern": "Local base class with business logic", + "reason": "Creates vertical entanglement", + "rule": "core-054" + }, + { + "pattern": "More than one level of inheritance", + "reason": "Excessive inheritance, prohibited", + "rule": "core-051" + }, + { + "pattern": "Hidden routines or extension methods with magic", + "reason": "Violates No Magic principle", + "rule": "core-070" + }, + { + "pattern": "Style cop / analyzer as primary compliance mechanism", + "reason": "Violates No Toasters principle", + "rule": "core-130" + } + ], + + "principles": { + "people_first": true, + "simplicity": "mandatory", + "autonomous_components": true, + "no_magic": true, + "rewritability": true, + "mono_micro": true, + "level_0": true, + "open_code": true, + "airplane_mode": true, + "no_toasters": true, + "pass_forward": true, + "all_in_all_out": true, + "readability_over_optimization": true, + "last_day": true + }, + + "adoption": { + "partial_adoption_valid": false, + "full_adoption_required": true, + "note": "The Standard must be embraced fully or rejected fully." + } +} diff --git a/.agents/skills/the-standard-core/examples/bad/example_bad_modeling.md b/.agents/skills/the-standard-core/examples/bad/example_bad_modeling.md new file mode 100644 index 0000000..2ffe1bd --- /dev/null +++ b/.agents/skills/the-standard-core/examples/bad/example_bad_modeling.md @@ -0,0 +1,83 @@ +# Bad Example: Modeling Violations + +## Violation 1 — Speculative Attributes (core-030, core-031) + +```csharp +// BAD: StudentModel with attributes that purposing never required +public class StudentModel +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string Email { get; set; } // purposing never mentioned email + public string PhoneNumber { get; set; } // purposing never mentioned phone + public string EmergencyContact { get; set; } // speculative + public string LinkedInProfile { get; set; } // speculative + public bool IsVip { get; set; } // undefined business meaning +} +``` + +**Problems:** +- `email`, `PhoneNumber`, `EmergencyContact`, `LinkedInProfile`, `IsVip` were never identified in purposing. +- `StudentModel` uses the forbidden `Model` suffix (core-032 / naming convention). + +--- + +## Violation 2 — Primary Model With External Dependency (core-033) + +```csharp +// BAD: Student physically depends on School to exist +public class Student +{ + public Guid Id { get; set; } + public string Name { get; set; } + + // Primary model must not physically depend on another model + public School School { get; set; } // VIOLATION: creates tight coupling + public Guid SchoolId { get; set; } +} +``` + +**Problem:** `Student` is a primary model. It must be self-sufficient. +A `Student` should exist independently. The relationship belongs in +a relational or secondary model. + +--- + +## Violation 3 — Relational Model with Unrelated Details (core-035) + +```csharp +// BAD: StudentCourse has unrelated attributes +public class StudentCourse +{ + public Guid StudentId { get; set; } + public Student Student { get; set; } + public Guid CourseId { get; set; } + public Course Course { get; set; } + + // Unrelated details in a relational model — VIOLATION + public string StudentEmail { get; set; } + public string CourseDescription { get; set; } + public string InstructorName { get; set; } +} +``` + +**Problem:** A relational model must only hold references to the two primary models it connects. +Email belongs on `Student`. Description belongs on `Course`. Instructor is a separate entity. + +--- + +## Violation 4 — Shared Utility Model (core-052, core-053) + +```csharp +// BAD: Shared "helpers" violate autonomous components +public static class ModelHelper +{ + public static bool IsValid(Student student) { ... } + public static bool IsValid(Course course) { ... } + public static string GetDisplayName(object entity) { ... } +} +``` + +**Problem:** This is a `Helper` — horizontal entanglement. Each service must +own its own validation logic. Centralizing it here creates coupling between +unrelated flows and breaks autonomous component ownership. diff --git a/.agents/skills/the-standard-core/examples/bad/example_bad_purposing.md b/.agents/skills/the-standard-core/examples/bad/example_bad_purposing.md new file mode 100644 index 0000000..76bd958 --- /dev/null +++ b/.agents/skills/the-standard-core/examples/bad/example_bad_purposing.md @@ -0,0 +1,75 @@ +# Bad Example: Purposing Violations + +## Violation 1 — Skipping Observation (core-020) + +``` +Engineer: "We need a student service. Let me just start with the model." +// Immediately creates Student.cs, StudentService.cs +``` + +**Problem:** No observation. No real blocker identified. The engineer assumed +what was needed rather than observing the actual constraint. + +**Consequence:** Likely builds the wrong thing. Wasted work, rework, or +a feature that does not solve the actual problem. + +--- + +## Violation 2 — Vague Articulation (core-021) + +``` +Engineer: "The system is slow. Make it faster." +// Jumps to adding Redis caching everywhere. +``` + +**Problem:** "Slow" is not an articulated problem. What is slow? For whom? +Under what conditions? What is the acceptable threshold? + +**Consequence:** Adds complexity (caching) to a problem that was never +properly defined. May cache the wrong data or introduce cache invalidation bugs. + +--- + +## Violation 3 — Solutioning Without Constraints (core-022) + +``` +Engineer: "We'll use microservices for this. Each entity gets its own service, + its own database, and its own deployment pipeline." +// No consideration for team size, operational maturity, or readability. +``` + +**Problem:** The solution does not honor longevity, maintainability, or +the Level 0 readability principle. A 3-person team cannot maintain +30 microservices. + +**Consequence:** Over-engineering, operational burden, and unmaintainability. + +--- + +## Violation 4 — Cutting Corners (core-023) + +``` +Engineer: "I know the proper way is to validate inputs first, but the deadline + is tomorrow. I'll just write straight to the database." +``` + +**Problem:** Reaching the goal the wrong way is a violation. +An unvalidated write creates a security vulnerability and data integrity problem. + +**Consequence:** Security breach, corrupt data, future rework that is more +expensive than doing it right the first time. + +--- + +## Violation 5 — Unclear Purpose Proceeding Anyway (core-024) + +``` +Engineer: "I'm not totally sure what this feature should do, but I'll start + coding and we'll figure it out as we go." +``` + +**Problem:** If purpose is unclear, you must stop and clarify. +Code written without purpose will be thrown away or, worse, kept +as a maintenance liability. + +**Consequence:** Technical debt, confusion, and wasted effort. diff --git a/.agents/skills/the-standard-core/examples/good/example_modeling.md b/.agents/skills/the-standard-core/examples/good/example_modeling.md new file mode 100644 index 0000000..2b4ddb4 --- /dev/null +++ b/.agents/skills/the-standard-core/examples/good/example_modeling.md @@ -0,0 +1,90 @@ +# Good Example: Modeling + +## Context + +After purposing, the team models the student enrollment system. + +## Correct Model Design + +### Primary Models (self-sufficient, no external dependencies) + +```csharp +// Student.cs — Primary model. Self-sufficient. +public class Student +{ + public Guid Id { get; set; } + public string Name { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset UpdatedDate { get; set; } +} + +// Course.cs — Primary model. Self-sufficient. +public class Course +{ + public Guid Id { get; set; } + public string Name { get; set; } + public DateTimeOffset CreatedDate { get; set; } + public DateTimeOffset UpdatedDate { get; set; } +} +``` + +**Why correct:** +- `Student` and `Course` carry only what the problem requires. +- No speculative fields (no `PhoneNumber`, `Address` unless purposing identified them). +- No type suffix (`StudentModel` is forbidden). + +--- + +### Relational Model (connects two primary models) + +```csharp +// StudentCourse.cs — Relational model connecting Student ↔ Course +public class StudentCourse +{ + public Guid StudentId { get; set; } + public Student Student { get; set; } + public Guid CourseId { get; set; } + public Course Course { get; set; } +} +``` + +**Why correct:** +- Connects exactly two primary models. +- Contains only references, no unrelated attributes. + +--- + +### Secondary Model (depends on a primary model) + +```csharp +// Enrollment.cs — Secondary model. References Student and Course. +public class Enrollment +{ + public Guid Id { get; set; } + public Guid StudentId { get; set; } + public Student Student { get; set; } + public Guid CourseId { get; set; } + public Course Course { get; set; } + public EnrollmentStatus Status { get; set; } + public DateTimeOffset CreatedDate { get; set; } +} +``` + +**Why correct:** +- Depends on `Student` and `Course` (primary models). +- Carries only enrollment-specific attributes (`Status`, `CreatedDate`). + +--- + +### Operational Models + +``` +IStorageBroker → Integration model (broker) +IStudentService → Processing model (service) +StudentsController → Exposure model (exposer) +Startup / Program.cs → Configuration model +``` + +**Why correct:** +- Each operational model plays exactly one Tri-Nature role. +- No model crosses its designated responsibility. diff --git a/.agents/skills/the-standard-core/examples/good/example_purposing.md b/.agents/skills/the-standard-core/examples/good/example_purposing.md new file mode 100644 index 0000000..ce1d3d5 --- /dev/null +++ b/.agents/skills/the-standard-core/examples/good/example_purposing.md @@ -0,0 +1,52 @@ +# Good Example: Purposing + +## Context + +A team needs to build a student enrollment system for a university. + +## WRONG approach (jumping to code) + +``` +// Immediately starts writing Student model and database tables. +// No problem statement, no observation, no constraints identified. +``` + +**Why this is wrong:** Skips the observation phase. Team has not identified: Who are the users? What problem is being solved? What are the constraints? + +--- + +## CORRECT approach: Full purposing cycle + +### Step 0: Observation + +> "Students cannot register for courses without first verifying their prerequisite completions. The current system requires a staff member to manually check prerequisites, which takes 2-3 days and causes late enrollments." + +**Real blocker identified:** Manual prerequisite verification is a bottleneck. + +### Step 1: Articulation + +> **Problem:** The enrollment system has no automated prerequisite check. This forces manual intervention for every enrollment, creating delays that cause students to miss course deadlines. + +> **Affected parties:** Students (wait 2-3 days), Staff (repetitive manual work), Administrators (enrollment metrics are delayed). + +> **Constraint:** Prerequisite rules are defined per-course and may change each semester. + +### Step 2: Solutioning + +> **Goal:** Build an automated prerequisite verification service that: +> - Retrieves a student's completed courses +> - Compares them against the required prerequisites for the target course +> - Returns an approval or rejection with specific unmet prerequisites listed +> - Is readable, maintainable, and rewritable by any engineer on the team +> - Does not require cloud connectivity to run locally (Airplane Mode) + +### Result + +Purpose is clear. The team can now move to Modeling. + +**What this enables:** +- The Student model needs: Id, Name, CompletedCourses +- The Course model needs: Id, Name, Prerequisites +- The Enrollment model (secondary) needs: StudentId, CourseId, Status +- A Foundation Service for Student, Course, and Enrollment +- A Processing Service for prerequisite checking logic diff --git a/.agents/skills/the-standard-core/manifest.json b/.agents/skills/the-standard-core/manifest.json new file mode 100644 index 0000000..3ce571a --- /dev/null +++ b/.agents/skills/the-standard-core/manifest.json @@ -0,0 +1,59 @@ +{ + "name": "the-standard-core", + "version": "1.0.0", + "description": "The governing layer of The Standard. Defines Tri-Nature theory, the mandatory engineering sequence (Purposing → Modeling → Simulation), core principles, and non-negotiable operating rules that govern all other Standard skills.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.3.0.0", + + "inputs": [ + "A software problem, requirement, or design request" + ], + + "outputs": [ + "A purpose-articulated problem statement", + "A set of Standard-compliant models (Primary, Secondary, Relational, Operational)", + "A simulation plan showing how models interact", + "A validated design that passes the core checklist" + ], + + "dependencies": [], + + "governs": [ + "the-standard-architecture", + "the-standard-code-csharp", + "the-standard-testing", + "the-standard-practices" + ], + + "activation": { + "trigger": "always", + "note": "This skill must be active for every software-related request. It governs all other Standard skills." + }, + + "validation": { + "required": true, + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_purposing.md", + "examples/good/example_modeling.md" + ], + "bad": [ + "examples/bad/example_bad_purposing.md", + "examples/bad/example_bad_modeling.md" + ] + }, + "templates": [ + "templates/system_design_template.md" + ], + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-core/rules/rules.json b/.agents/skills/the-standard-core/rules/rules.json new file mode 100644 index 0000000..9616a75 --- /dev/null +++ b/.agents/skills/the-standard-core/rules/rules.json @@ -0,0 +1,54 @@ +{ + "rules": [ + { "id": "core-001", "category": "theory", "description": "Every system must be understood through three lenses: Purpose, Dependency, Exposure (Tri-Nature).", "severity": "error" }, + { "id": "core-002", "category": "theory", "description": "The Tri-Nature pattern is fractal — it repeats at system, sub-system, service, validation, and exposure level.", "severity": "error" }, + { "id": "core-003", "category": "theory", "description": "Design every part with awareness that it will become someone else's dependency or exposure.", "severity": "warning" }, + { "id": "core-010", "category": "engineering-sequence", "description": "The engineering sequence is fixed: Purposing → Modeling → Simulation. Never skip or reorder.", "severity": "error" }, + { "id": "core-011", "category": "engineering-sequence", "description": "Never start implementation when purpose is unclear.", "severity": "error" }, + { "id": "core-012", "category": "engineering-sequence", "description": "Never start modeling when the problem observation has not been completed.", "severity": "error" }, + { "id": "core-020", "category": "purposing", "description": "Start every design session with observation: identify the real blocker, constraint, or unmet need.", "severity": "error" }, + { "id": "core-021", "category": "purposing", "description": "Articulate the problem clearly before proposing a solution.", "severity": "error" }, + { "id": "core-022", "category": "purposing", "description": "Solutioning must honor: readability, configurability, longevity, optimization, and maintainability.", "severity": "error" }, + { "id": "core-023", "category": "purposing", "description": "Reaching the goal the wrong way is a violation — never cut corners.", "severity": "error" }, + { "id": "core-024", "category": "purposing", "description": "If purpose is unclear, stop and clarify before proceeding to modeling or implementation.", "severity": "error" }, + { "id": "core-030", "category": "modeling", "description": "Model only what the purpose requires — no speculative attributes.", "severity": "error" }, + { "id": "core-031", "category": "modeling", "description": "Do not attract irrelevant attributes into the model.", "severity": "error" }, + { "id": "core-032", "category": "modeling", "description": "Prefer the most generic valid name that still fits the problem scope.", "severity": "error" }, + { "id": "core-033", "category": "modeling", "description": "Primary models must be self-sufficient — they must not physically depend on another model to exist.", "severity": "error" }, + { "id": "core-034", "category": "modeling", "description": "Secondary models must reference or nest within a primary model.", "severity": "error" }, + { "id": "core-035", "category": "modeling", "description": "Relational models connect exactly two primary models; they must not contain unrelated details.", "severity": "error" }, + { "id": "core-036", "category": "modeling", "description": "Hybrid models are allowed only when the business flow truly requires mixing relational + additional details.", "severity": "warning" }, + { "id": "core-037", "category": "modeling", "description": "Integration models = brokers. Processing models = services. Exposure models = exposers. Configuration models = startup/middleware.", "severity": "error" }, + { "id": "core-040", "category": "simulation", "description": "Simulation must stay inside the purpose and model boundaries.", "severity": "error" }, + { "id": "core-041", "category": "simulation", "description": "Functions, methods, and routines are the simulation mechanisms — they must not cross model ownership.", "severity": "error" }, + { "id": "core-050", "category": "simplicity", "description": "Simplicity is mandatory, not optional.", "severity": "error" }, + { "id": "core-051", "category": "simplicity", "description": "Do not use more than one level of inheritance (excessive inheritance is prohibited).", "severity": "error" }, + { "id": "core-052", "category": "simplicity", "description": "No Utils, no Commons, no Helpers — these create horizontal entanglement.", "severity": "error" }, + { "id": "core-053", "category": "simplicity", "description": "No shared models across independent flows.", "severity": "error" }, + { "id": "core-054", "category": "simplicity", "description": "No local base components that create hidden coupling (vertical entanglement).", "severity": "error" }, + { "id": "core-055", "category": "simplicity", "description": "Prefer duplication over cross-flow entanglement when duplication preserves autonomy.", "severity": "warning" }, + { "id": "core-060", "category": "autonomous-components", "description": "Every component must be self-sufficient — it owns its validations, tooling, and utilities.", "severity": "error" }, + { "id": "core-061", "category": "autonomous-components", "description": "Components may depend on injected dependencies, not hidden helpers.", "severity": "error" }, + { "id": "core-062", "category": "autonomous-components", "description": "Duplication is permitted when it preserves ownership and autonomy.", "severity": "warning" }, + { "id": "core-070", "category": "no-magic", "description": "No hidden routines.", "severity": "error" }, + { "id": "core-071", "category": "no-magic", "description": "No magical extensions that require chasing references to understand.", "severity": "error" }, + { "id": "core-072", "category": "no-magic", "description": "No runtime tricks that make the system hard to understand.", "severity": "error" }, + { "id": "core-073", "category": "no-magic", "description": "Validation, exception handling, tracing, security, and localization must be in plain sight.", "severity": "error" }, + { "id": "core-080", "category": "rewritability", "description": "Every system must be easy to understand, modify, and fully rewrite.", "severity": "error" }, + { "id": "core-081", "category": "rewritability", "description": "No hidden dependencies.", "severity": "error" }, + { "id": "core-082", "category": "rewritability", "description": "No unknown prerequisites.", "severity": "error" }, + { "id": "core-083", "category": "rewritability", "description": "No injected routines that obscure behavior.", "severity": "error" }, + { "id": "core-090", "category": "level-0", "description": "Code must be understandable by entry-level engineers.", "severity": "error" }, + { "id": "core-091", "category": "level-0", "description": "If new engineers cannot follow the system, the system is too complex.", "severity": "error" }, + { "id": "core-100", "category": "all-in-all-out", "description": "The Standard must be embraced fully or rejected fully — partial adoption is not standardization.", "severity": "error" }, + { "id": "core-101", "category": "all-in-all-out", "description": "Outdated partial adherence does not constitute a Standardized system.", "severity": "error" }, + { "id": "core-110", "category": "readability", "description": "When in doubt, choose readability over performance.", "severity": "error" }, + { "id": "core-111", "category": "readability", "description": "Unreadable 'optimal' software is not truly optimum.", "severity": "error" }, + { "id": "core-120", "category": "airplane-mode", "description": "The system must be runnable locally without mandatory cloud dependency.", "severity": "error" }, + { "id": "core-121", "category": "airplane-mode", "description": "Develop tooling that bridges cloud resources to local stand-ins.", "severity": "warning" }, + { "id": "core-130", "category": "no-toasters", "description": "Do not enforce Standard compliance via style cops or analyzers as the primary mechanism.", "severity": "error" }, + { "id": "core-131", "category": "no-toasters", "description": "Teach The Standard person-to-person, not through coercion tools.", "severity": "error" }, + { "id": "core-140", "category": "last-day", "description": "Work every day as if it might be your last day on the project.", "severity": "warning" }, + { "id": "core-141", "category": "last-day", "description": "End each engineering day at a good stopping point so another engineer can continue seamlessly.", "severity": "warning" } + ] +} diff --git a/.agents/skills/the-standard-core/rules/rules.md b/.agents/skills/the-standard-core/rules/rules.md new file mode 100644 index 0000000..e9a2e8a --- /dev/null +++ b/.agents/skills/the-standard-core/rules/rules.md @@ -0,0 +1,98 @@ +# The Standard Core — Rules + +## Rule Set + +### THEORY + +**core-001** [ERROR] Every system must be understood through three lenses: Purpose, Dependency, Exposure (Tri-Nature). +**core-002** [ERROR] The Tri-Nature pattern is fractal — it repeats at system, sub-system, service, validation, and exposure level. +**core-003** [WARNING] Design every part with awareness that it will become someone else's dependency or exposure. + +### ENGINEERING SEQUENCE + +**core-010** [ERROR] The engineering sequence is fixed: Purposing → Modeling → Simulation. Never skip or reorder. +**core-011** [ERROR] Never start implementation when purpose is unclear. +**core-012** [ERROR] Never start modeling when the problem observation has not been completed. + +### PURPOSING + +**core-020** [ERROR] Start every design session with observation: identify the real blocker, constraint, or unmet need. +**core-021** [ERROR] Articulate the problem clearly before proposing a solution. +**core-022** [ERROR] Solutioning must honor: readability, configurability, longevity, optimization, and maintainability. +**core-023** [ERROR] Reaching the goal the wrong way is a violation — never cut corners. +**core-024** [ERROR] If purpose is unclear, stop and clarify before proceeding to modeling or implementation. + +### MODELING + +**core-030** [ERROR] Model only what the purpose requires — no speculative attributes. +**core-031** [ERROR] Do not attract irrelevant attributes into the model. +**core-032** [ERROR] Prefer the most generic valid name that still fits the problem scope. +**core-033** [ERROR] Primary models must be self-sufficient — they must not physically depend on another model to exist. +**core-034** [ERROR] Secondary models must reference or nest within a primary model. +**core-035** [ERROR] Relational models connect exactly two primary models; they must not contain unrelated details. +**core-036** [WARNING] Hybrid models are allowed only when the business flow truly requires mixing relational + additional details. +**core-037** [ERROR] Integration models = brokers. Processing models = services. Exposure models = exposers. Configuration models = startup/middleware. + +### SIMULATION + +**core-040** [ERROR] Simulation must stay inside the purpose and model boundaries. +**core-041** [ERROR] Functions, methods, and routines are the simulation mechanisms — they must not cross model ownership. + +### SIMPLICITY + +**core-050** [ERROR] Simplicity is mandatory, not optional. +**core-051** [ERROR] Do not use more than one level of inheritance (excessive inheritance is prohibited). +**core-052** [ERROR] No Utils, no Commons, no Helpers — these create horizontal entanglement. +**core-053** [ERROR] No shared models across independent flows. +**core-054** [ERROR] No local base components that create hidden coupling (vertical entanglement). +**core-055** [WARNING] Prefer duplication over cross-flow entanglement when duplication preserves autonomy. + +### AUTONOMOUS COMPONENTS + +**core-060** [ERROR] Every component must be self-sufficient — it owns its validations, tooling, and utilities. +**core-061** [ERROR] Components may depend on injected dependencies, not hidden helpers. +**core-062** [WARNING] Duplication is permitted when it preserves ownership and autonomy. + +### NO MAGIC + +**core-070** [ERROR] No hidden routines. +**core-071** [ERROR] No magical extensions that require chasing references to understand. +**core-072** [ERROR] No runtime tricks that make the system hard to understand. +**core-073** [ERROR] Validation, exception handling, tracing, security, and localization must be in plain sight. + +### REWRITABILITY + +**core-080** [ERROR] Every system must be easy to understand, modify, and fully rewrite. +**core-081** [ERROR] No hidden dependencies. +**core-082** [ERROR] No unknown prerequisites. +**core-083** [ERROR] No injected routines that obscure behavior. + +### LEVEL 0 + +**core-090** [ERROR] Code must be understandable by entry-level engineers. +**core-091** [ERROR] If new engineers cannot follow the system, the system is too complex. + +### ALL-IN / ALL-OUT + +**core-100** [ERROR] The Standard must be embraced fully or rejected fully — partial adoption is not standardization. +**core-101** [ERROR] Outdated partial adherence does not constitute a Standardized system. + +### READABILITY OVER OPTIMIZATION + +**core-110** [ERROR] When in doubt, choose readability over performance. +**core-111** [ERROR] Unreadable "optimal" software is not truly optimum. + +### AIRPLANE MODE + +**core-120** [ERROR] The system must be runnable locally without mandatory cloud dependency. +**core-121** [WARNING] Develop tooling that bridges cloud resources to local stand-ins. + +### NO TOASTERS + +**core-130** [ERROR] Do not enforce Standard compliance via style cops or analyzers as the primary mechanism. +**core-131** [ERROR] Teach The Standard person-to-person, not through coercion tools. + +### LAST DAY + +**core-140** [WARNING] Work every day as if it might be your last day on the project. +**core-141** [WARNING] End each engineering day at a good stopping point so another engineer can continue seamlessly. diff --git a/.agents/skills/the-standard-core/templates/system_design_template.md b/.agents/skills/the-standard-core/templates/system_design_template.md new file mode 100644 index 0000000..505c6c9 --- /dev/null +++ b/.agents/skills/the-standard-core/templates/system_design_template.md @@ -0,0 +1,117 @@ +# System Design Template (Standard-Compliant) + +Use this template when starting any new system, service, or feature. +Complete every section before writing any code. + +--- + +## 1. PURPOSING + +### 1.1 Observation +> Describe the real blocker, constraint, or unmet need you have observed. +> Be specific. Use facts, not assumptions. + +**Observed problem:** +[Describe the actual, observable problem here] + +**Affected parties:** +- [Who is affected and how] + +**Current state (without solution):** +[What happens today without the solution] + +**Constraints:** +- [List hard constraints: technical, legal, time, team size] + +--- + +### 1.2 Articulation +> Re-state the problem in one clear sentence. + +**Problem statement:** +> [One sentence that completely captures the problem] + +--- + +### 1.3 Solutioning +> Describe the solution path. Include: what will be built, what principles apply. + +**Proposed solution:** +[Describe the approach] + +**Principles honored:** +- [ ] Readability — an entry-level engineer can follow this +- [ ] Configurability — environment-specific values are injectable +- [ ] Longevity — this system can be maintained 2 years from now +- [ ] Optimization — performance is addressed without sacrificing readability +- [ ] Maintainability — another engineer can pick this up on their "last day" +- [ ] Airplane Mode — system runs locally without cloud connectivity + +--- + +## 2. MODELING + +### 2.1 Data Carrier Models + +| Model Name | Type | Justification | +|---|---|---| +| [ModelName] | Primary / Secondary / Relational / Hybrid | [Why this model exists] | + +**Model attributes (only what purposing requires):** + +```csharp +// [ModelName].cs +public class [ModelName] +{ + public Guid Id { get; set; } + // [only attributes identified in purposing] +} +``` + +--- + +### 2.2 Operational Models + +| Model Name | Tri-Nature Role | Responsibility | +|---|---|---| +| [BrokerName]Broker | Integration (Broker) | Wraps [external resource] | +| [Entity]Service | Processing (Service) | [Business logic summary] | +| [Entities]Controller | Exposure (Exposer) | REST API for [entity] | + +--- + +## 3. SIMULATION + +### 3.1 Flow Description +> Describe how the models interact to produce the system's output. + +``` +[Entity Input] + → [Exposer receives request] + → [Service validates and processes] + → [Broker writes/reads from external resource] + → [Response flows back up the chain] +``` + +### 3.2 Methods/Routines Required + +| Method | Owner | Signature | +|---|---|---| +| Add[Entity]Async | [Entity]Service | `ValueTask<[Entity]> Add[Entity]Async([Entity] entity)` | +| Retrieve[Entity]ByIdAsync | [Entity]Service | `ValueTask<[Entity]> Retrieve[Entity]ByIdAsync(Guid entityId)` | +| Modify[Entity]Async | [Entity]Service | `ValueTask<[Entity]> Modify[Entity]Async([Entity] entity)` | +| Remove[Entity]ByIdAsync | [Entity]Service | `ValueTask<[Entity]> Remove[Entity]ByIdAsync(Guid entityId)` | + +--- + +## 4. VALIDATION GATE + +Before writing any code, confirm: + +- [ ] Purpose is documented and agreed upon +- [ ] All models contain only purposing-required attributes +- [ ] No speculative fields have been added +- [ ] Operational model roles are assigned (broker / service / exposer) +- [ ] The Tri-Nature applies to every layer +- [ ] Local execution (airplane mode) is possible +- [ ] An entry-level engineer can understand the design (Level 0) diff --git a/.agents/skills/the-standard-core/validations/anti-patterns.md b/.agents/skills/the-standard-core/validations/anti-patterns.md new file mode 100644 index 0000000..cb16878 --- /dev/null +++ b/.agents/skills/the-standard-core/validations/anti-patterns.md @@ -0,0 +1,186 @@ +# The Standard Core — Anti-Patterns + +Each anti-pattern below includes: what it is, why it is harmful, and how to fix it. + +--- + +## AP-CORE-001: Skipping Purposing + +**What it is:** +An engineer or AI agent starts writing models or code before articulating the problem. + +**Example:** +``` +// No problem statement. Immediately writes Student model. +public class Student { public Guid Id; public string Name; } +``` + +**Why harmful:** +- Builds the wrong thing with confidence. +- Creates rework that is more expensive than the original work. +- Results in features that do not solve the real user problem. + +**How to fix:** +Stop. Write the observation, articulation, and solutioning sections of the design template first. Only proceed when purpose is clear. + +--- + +## AP-CORE-002: Speculative Modeling + +**What it is:** +Adding model attributes or models that were not identified in purposing. + +**Example:** +```csharp +public class Student +{ + public Guid Id { get; set; } + public string Name { get; set; } + public string LinkedInUrl { get; set; } // never identified in purposing + public bool IsPremium { get; set; } // speculative +} +``` + +**Why harmful:** +- Creates noise that future engineers must maintain. +- Leads to validation and database schema complexity for unused fields. +- Pollutes the model's purpose. + +**How to fix:** +Remove all attributes not explicitly required by purposing. If a field is "nice to have", do not add it — wait until purposing requires it. + +--- + +## AP-CORE-003: Horizontal Entanglement (Utils/Helpers) + +**What it is:** +Creating `Utils`, `Helper`, `Common`, or `Shared` classes that multiple flows depend on. + +**Example:** +```csharp +public static class ValidationHelper +{ + public static void ValidateStudent(Student s) { ... } + public static void ValidateCourse(Course c) { ... } +} +``` + +**Why harmful:** +- A change to `ValidationHelper` can break unrelated flows. +- Destroys component autonomy. +- Makes the system impossible to rewrite in isolation. +- A bug in the helper cascades across the entire system. + +**How to fix:** +Each service owns its own validation logic in its own partial class (e.g., `StudentService.Validations.cs`). Duplicate validation logic rather than share it. + +--- + +## AP-CORE-004: Vertical Entanglement (Local Base Classes) + +**What it is:** +Creating local base classes that inject hidden behavior into derived classes. + +**Example:** +```csharp +public abstract class ServiceBase +{ + protected void ValidateEntity(object entity) { ... } + protected void LogError(Exception ex) { ... } +} + +public class StudentService : ServiceBase { ... } +``` + +**Why harmful:** +- Engineers reading `StudentService` cannot understand it without also reading `ServiceBase`. +- Hidden behavior in base classes violates No Magic. +- Creates deep coupling that makes rewriting one service dependent on all others. + +**How to fix:** +Inject dependencies explicitly. Move validation into the service's own partial class. Move logging into an injected `ILoggingBroker`. + +--- + +## AP-CORE-005: Excessive Inheritance (>1 Level) + +**What it is:** +Using inheritance chains deeper than one level. + +**Example:** +```csharp +public class EntityBase { } +public class AuditableEntity : EntityBase { } +public class Student : AuditableEntity { } // 2 levels deep — VIOLATION +``` + +**Why harmful:** +- Deep inheritance chains obscure what a class actually is and does. +- Makes refactoring dangerous (changing a base class ripples unpredictably). +- Violates Level 0 — new engineers cannot follow the inheritance chain without extensive study. + +**How to fix:** +Use composition over inheritance. If `Student` needs audit fields, add them directly to `Student`. Do not share them through inheritance. + +--- + +## AP-CORE-006: Partial Adoption + +**What it is:** +Applying some Standard rules but not others, then claiming the system is Standard-compliant. + +**Example:** +``` +"We use The Standard naming conventions, but we don't do TDD, +and we allow Helper classes because it's faster." +``` + +**Why harmful:** +- Breaks the system-level consistency that makes The Standard valuable. +- Creates confusion for new engineers who expect the full Standard. +- The "partial" system is not rewritable or predictable. + +**How to fix:** +Adopt The Standard fully or do not claim Standard compliance. All-In or All-Out. + +--- + +## AP-CORE-007: Optimization Before Readability + +**What it is:** +Choosing a clever or performant solution that is harder to read, without measuring whether the performance was actually needed. + +**Example:** +```csharp +// "optimized" but unreadable — parallel bit manipulation +int result = (a ^ b) & ~((a ^ b) - 1); +``` + +**Why harmful:** +- Introduces bugs that are hard to find. +- Future engineers cannot maintain or modify the code safely. +- The optimization is often premature and unnecessary. + +**How to fix:** +Write the readable version first. Only optimize when a measured performance problem exists, and only if the optimization does not cross the readability threshold. + +--- + +## AP-CORE-008: Cloud-Mandatory Development + +**What it is:** +Building a system that cannot be run locally without a live cloud connection. + +**Example:** +``` +// All tests require live Azure Storage connection. +// Local development requires VPN + production credentials. +``` + +**Why harmful:** +- Engineers cannot work offline. +- Tests are non-deterministic and slow. +- Onboarding new engineers requires access provisioning before a single test can run. + +**How to fix:** +Use broker abstraction with local stand-ins. Mock external cloud services in tests. Use local emulators (Azurite for Azure Storage, LocalStack for AWS). diff --git a/.agents/skills/the-standard-core/validations/checklist.md b/.agents/skills/the-standard-core/validations/checklist.md new file mode 100644 index 0000000..6648c97 --- /dev/null +++ b/.agents/skills/the-standard-core/validations/checklist.md @@ -0,0 +1,110 @@ +# The Standard Core — Validation Checklist + +Run this checklist before approving any design, model, or architecture decision. +Each item is binary: PASS or FAIL. A single FAIL must be resolved before proceeding. + +--- + +## PURPOSING + +- [ ] **core-020** The real blocker or unmet need has been identified through observation (not assumption). +- [ ] **core-021** The problem has been articulated clearly in writing. +- [ ] **core-022** The solution honors readability, configurability, longevity, optimization, and maintainability. +- [ ] **core-023** No corners have been cut to reach the goal. +- [ ] **core-024** Purpose is fully clear — no ambiguity remains before proceeding. + +--- + +## ENGINEERING SEQUENCE + +- [ ] **core-010** Purposing was completed before modeling began. +- [ ] **core-011** Modeling was completed before simulation/implementation began. +- [ ] **core-012** No implementation code was written while purpose was still unclear. + +--- + +## MODELING + +- [ ] **core-030** Every model attribute was required by the purpose — no speculative fields. +- [ ] **core-031** No irrelevant attributes were added to any model. +- [ ] **core-032** All model names are generic, clear, and problem-scoped (no `Model`, `Object`, `Data` suffixes). +- [ ] **core-033** All primary models are self-sufficient (no physical external model dependency). +- [ ] **core-034** All secondary models correctly reference a primary model. +- [ ] **core-035** All relational models connect exactly two primary models with no unrelated fields. +- [ ] **core-036** Hybrid models are used only when the business flow absolutely requires mixed behavior. +- [ ] **core-037** Every operational model is correctly assigned: broker / service / exposer / configuration. + +--- + +## SIMPLICITY & ENTANGLEMENT + +- [ ] **core-050** No unnecessary complexity was introduced. +- [ ] **core-051** No class uses more than one level of inheritance. +- [ ] **core-052** No `Utils`, `Commons`, or `Helper` classes exist in the codebase. +- [ ] **core-053** No model is shared across independent flows. +- [ ] **core-054** No local base classes with hidden coupling exist. + +--- + +## AUTONOMOUS COMPONENTS + +- [ ] **core-060** Every component owns its validations, tooling, and utilities. +- [ ] **core-061** No component depends on hidden helpers — only on injected dependencies. + +--- + +## NO MAGIC + +- [ ] **core-070** No hidden routines exist. +- [ ] **core-071** No extension methods or magical abstractions that require reference-chasing. +- [ ] **core-072** No runtime tricks that obscure system behavior. +- [ ] **core-073** Validation, exception handling, tracing, and security are in plain sight. + +--- + +## REWRITABILITY + +- [ ] **core-080** The system can be fully rewritten by any engineer with The Standard knowledge. +- [ ] **core-081** No hidden dependencies exist. +- [ ] **core-082** No unknown prerequisites exist. +- [ ] **core-083** No injected routines obscure system behavior. + +--- + +## LEVEL 0 + +- [ ] **core-090** An entry-level engineer can understand the system without mentorship. +- [ ] **core-091** No component is so complex that a new team member cannot follow it. + +--- + +## AIRPLANE MODE + +- [ ] **core-120** The system runs locally without mandatory cloud connectivity. +- [ ] **core-121** Local stand-ins or mocks exist for all cloud resources. + +--- + +## ALL-IN / ALL-OUT + +- [ ] **core-100** All Standard rules are applied — no selective adoption. +- [ ] **core-101** No outdated partial adherence is claimed as compliance. + +--- + +## RESULT + +| Check | PASS / FAIL | +|---|---| +| Purposing | | +| Engineering Sequence | | +| Modeling | | +| Simplicity & Entanglement | | +| Autonomous Components | | +| No Magic | | +| Rewritability | | +| Level 0 | | +| Airplane Mode | | +| All-In / All-Out | | + +**Overall: PASS only if every row above is PASS.** diff --git a/.agents/skills/the-standard-events/SKILL.md b/.agents/skills/the-standard-events/SKILL.md new file mode 100644 index 0000000..772c5ea --- /dev/null +++ b/.agents/skills/the-standard-events/SKILL.md @@ -0,0 +1,157 @@ +--- +name: The Standard Events +description: Enforces Standard event-driven architecture using the CulDeSac pattern, event brokers, foundation event services, validation, exception handling, and event service testing. +the standard version: v2.13.0 +skill version: v0.1.0.0 +--- + +# The Standard Events + +## What this skill is + +This skill governs how events are architected, implemented, and tested under The Standard. +It covers the CulDeSac pattern, event brokers, foundation event services, validation, exception mapping, and test structure for publish and subscribe operations. + +## Explicit coverage map + +This skill explicitly covers: + +- The CulDeSac pattern and why it exists in Standard-compliant systems +- Event broker structure and implementation +- Foundation event service implementation: main, Validations, and Exceptions files +- Publish and SubscribeTo operation contracts and naming conventions +- Validation rules for null entity and null event handler +- Exception mapping: Validation and Service exceptions only (no Dependency exceptions) +- File and partial-class structure for brokers, services, and tests +- Test structure: Logic, Validations, and Exceptions per operation +- Naming conventions enforced across the entire events layer +- Orchestration integration: how publisher and subscriber orchestration services consume the event service +- Dependency injection registration: lifetime requirements for the event broker and event service +- Startup activation: how and where SubscribeTo calls must be invoked at application startup + +## When to use + +Use this skill whenever implementing, reviewing, expanding, or testing event-driven communication between services. +Use it whenever deciding how to publish a domain event, how to subscribe to one, or how to validate and handle failures in an event service. +Use it when determining whether the CulDeSac pattern is appropriate for a given service. + +## The CulDeSac pattern + +The CulDeSac pattern applies when a service needs to send a domain event outward without expecting a synchronous return from any subscriber. +It is a dead-end in the call graph: data flows in one direction, callers are not blocked waiting for a result, and subscribers are free to react independently. + +### Why use events + +0. To decouple services across bounded contexts without creating direct service-to-service dependencies. +1. To allow multiple subscribers to react to a single domain event independently. +2. To avoid creating orchestration services that must know about every downstream consumer. +3. To support fire-and-forget flows where the publisher does not need confirmation from subscribers. +4. To enable downstream services to scale and evolve without affecting the publisher. +5. To prevent any single orchestrator from accumulating too many responsibilities over time. + +## Event service doctrine + +0. Event services are always foundation services. +1. Event services sit at the boundary between the domain model and the event infrastructure. +2. Event services must not depend on other services -- only on the event broker. +3. Event services expose exactly two operations per entity: Publish[Entity]Async and SubscribeTo[Entity]Event. +4. Event services do not inject a logging broker -- they have no logging dependency by design. +5. Event services use two TryCatch delegates to isolate exception handling from business logic. +6. Event services do NOT catch Dependency or CriticalDependency exceptions -- they do not call HTTP or storage APIs. + +## Broker implementation doctrine + +0. The event broker is the only infrastructure dependency of an event service. +1. The event broker wraps any chosen event infrastructure -- no specific event library is mandated. +2. Each entity gets its own event client instance, typed to that entity, declared in the broker constructor. +3. The broker is split into four files: base interface, entity interface partial, base implementation, entity implementation partial. +4. Broker operations are thin pass-throughs to the underlying event infrastructure -- no logic lives in the broker. + +## Orchestration integration + +The event service is consumed exclusively by orchestration services. +Controllers, processing services, and other foundation services must never depend on an event service directly. + +### Publisher side + +An orchestration service that needs to raise a domain event calls `Publish[Entity]Async` on the event service. +The orchestration service owns the composition of the entity and delegates the publish call to the event service. +The publisher orchestration service knows nothing about which services will subscribe -- it only knows it must publish. + +### Subscriber side + +An orchestration service that needs to react to a domain event exposes a method named `SubscribeTo[Entity]Events` (plural). +That method calls `this.[entity]EventService.SubscribeTo[Entity]Event(handler)` where handler is a private async method. +The handler receives the entity and performs the orchestration reaction -- calling foundation services as needed. +Only the subscriber orchestration service knows what to do with the event; the publisher is unaware of subscribers. + +### Naming distinction: plural vs singular + +0. The event service method is always singular: `SubscribeTo[Entity]Event`. +1. The orchestration service wrapper method is always plural: `SubscribeTo[Entity]Events`. +2. The plural wrapper exists because the orchestration service may route the event to multiple sub-operations. +3. The plural wrapper is the method called at startup -- never the singular event service method directly. + +## Dependency injection and startup activation + +### Registration rules + +0. All orchestration services that publish or subscribe must be registered in DI. +1. The required DI lifetime for `IEventBroker` and `EventBroker` depends on the event infrastructure in use. +2. For **in-memory event infrastructure** (such as LeVent): singleton is required for correctness -- client instances hold subscription handler registrations in memory; scoped or transient registrations produce fresh instances with no registered handlers, so events are never delivered. +3. For **external event infrastructure** (such as Azure Service Bus or EventHighway): follow the client library's own lifetime recommendations -- subscriptions live in the external service and survive independently of the client instance, but clients typically require singleton lifetime for connection reuse and to keep message processors alive. +4. `I[Entity]EventService` and `[Entity]EventService` must be registered with a lifetime that matches the broker they depend on -- never register the service with a longer lifetime than the broker. +5. When in doubt, prefer singleton -- it is safe for all known event infrastructure and avoids silent subscription failures. + +### Startup activation rules + +0. Event subscriptions are not self-activating -- they must be explicitly started at application startup. +1. After the DI container is built, every subscribing orchestration service must have its `SubscribeTo[Entity]Events()` method called. +2. The activation call must appear in `Configure()` (Startup.cs style) or its equivalent in a minimal API host setup. +3. In Startup.cs: use `app.ApplicationServices.GetService().SubscribeTo[Entity]Events()`. +4. In minimal API (Program.cs): use `app.Services.GetService().SubscribeTo[Entity]Events()` after `app = builder.Build()`. +5. If the startup activation call is missing, no subscriber will ever receive events -- the subscription is silently never registered. +6. Every subscribing orchestration service must have exactly one startup activation call per subscription. + +## Validation rules for event services + +### Publish validation + +0. Validate the entity is not null before publishing. +1. A null entity must throw NullEventException immediately (circuit-breaking). +2. Null entity validation must not collect further errors -- it breaks immediately. + +### Subscribe validation + +0. Validate the event handler delegate is not null before subscribing. +1. A null handler must throw NullEventHandlerException immediately (circuit-breaking). +2. Null handler validation must not collect further errors -- it breaks immediately. + +## Exception handling rules + +0. All validation failures must produce [Entity]EventValidationException wrapping the inner exception. +1. All unexpected exceptions must be wrapped in FailedEventServiceException, then in [Entity]EventServiceException. +2. FailedEventServiceException must carry the original exception as innerException and its Data collection. +3. Event services do not use DependencyValidationException or DependencyException categories. +4. TryCatch delegates must be used to separate exception handling from core logic. + +## Naming conventions + +0. Interface: I[Entity]EventService. +1. Implementation: [Entity]EventService (internal partial class). +2. Publish operation: Publish[Entity]Async (returns ValueTask, async). +3. Subscribe operation: SubscribeTo[Entity]Event (returns void, synchronous). +4. Subscribe must NEVER be named ListenTo[Entity]Event. +5. Broker interface: IEventBroker (shared), with entity-specific partials in IEventBroker_[Entity].cs. +6. Exception naming: Null[Entity]EventException, Null[Entity]EventHandlerException, Failed[Entity]EventServiceException, [Entity]EventValidationException, [Entity]EventServiceException. + +## Test rules for event services + +0. Test SubscribeTo[Entity]Events happy path first. +1. Test Publish[Entity]Async happy path second. +2. Test validation failures third (null handler, null entity). +3. Test service exceptions fourth (unexpected errors in both operations). +4. Event service tests must NOT declare a loggingBrokerMock -- there is no logging broker. +5. Subscribe handler mocks must use Mock>. +6. Always end every test with eventBrokerMock.VerifyNoOtherCalls(). +7. Use Times.Once for expected calls and Times.Never for calls skipped due to validation failures. diff --git a/.agents/skills/the-standard-events/contracts/contracts.json b/.agents/skills/the-standard-events/contracts/contracts.json new file mode 100644 index 0000000..15579cc --- /dev/null +++ b/.agents/skills/the-standard-events/contracts/contracts.json @@ -0,0 +1,158 @@ +{ + "skill": "the-standard-events", + "version": "1.0.0", + + "cul_de_sac_pattern": { + "definition": "A dead-end event flow where data is published outward without a synchronous return from any subscriber.", + "when_to_apply": "When a service needs to notify other contexts of a domain event without blocking or coupling to the subscriber.", + "prohibited": "Using events as a substitute for synchronous request-response flows." + }, + + "event_service_contract": { + "operations_per_entity": 2, + "publish": { + "signature": "ValueTask Publish[Entity]Async([Entity] [entity])", + "async": true, + "validates": "entity is not null" + }, + "subscribe": { + "signature": "void SubscribeTo[Entity]Event(Func<[Entity], ValueTask> [entity]EventHandler)", + "async": false, + "validates": "handler is not null" + }, + "dependencies": ["IEventBroker"], + "forbidden_dependencies": ["ILoggingBroker", "IStorageBroker", "any other service"] + }, + + "event_broker_contract": { + "interface": "IEventBroker", + "implementation": "EventBroker", + "library": "LeVent", + "per_entity_files": { + "interface_partial": "IEventBroker_[Entity].cs", + "implementation_partial": "EventBroker_[Entity].cs" + }, + "per_entity_operations": { + "publish": "ValueTask Publish[Entity]Async([Entity] [entity], string eventName = null)", + "subscribe": "void SubscribeTo[Entity]Event(Func<[Entity], ValueTask> [entity]EventHandler, string eventName = null)" + }, + "levent_client_property": "ILeVentClient<[Entity]> [Entity]Events" + }, + + "exception_hierarchy": { + "validation_path": { + "null_entity": "NullEventException -> [Entity]EventValidationException", + "null_handler": "NullEventHandlerException -> [Entity]EventValidationException" + }, + "service_path": { + "unexpected_error": "Exception -> FailedEventServiceException -> [Entity]EventServiceException" + }, + "absent_categories": ["DependencyValidationException", "DependencyException", "CriticalDependencyException"] + }, + + "file_structure": { + "service": { + "main": "[Entity]EventService.cs", + "validations": "[Entity]EventService.Validations.cs", + "exceptions": "[Entity]EventService.Exceptions.cs" + }, + "broker": { + "interface_base": "IEventBroker.cs", + "interface_entity": "IEventBroker_[Entity].cs", + "implementation_base": "EventBroker.cs", + "implementation_entity": "EventBroker_[Entity].cs" + }, + "tests": { + "root": "[Entity]EventServiceTests.cs", + "logic_publish": "[Entity]EventServiceTests.Logic.Publish.cs", + "logic_subscribe": "[Entity]EventServiceTests.Logic.Subscribe.cs", + "validations_publish": "[Entity]EventServiceTests.Validations.Publish.cs", + "validations_subscribe": "[Entity]EventServiceTests.Validations.Subscribe.cs", + "exceptions_publish": "[Entity]EventServiceTests.Exceptions.Publish.cs", + "exceptions_subscribe": "[Entity]EventServiceTests.Exceptions.Subscribe.cs" + } + }, + + "naming_conventions": { + "interface": "I[Entity]EventService", + "implementation": "[Entity]EventService", + "publish_method": "Publish[Entity]Async", + "subscribe_method": "SubscribeTo[Entity]Event", + "subscribe_forbidden_name": "ListenTo[Entity]Event", + "exceptions": { + "null_entity": "Null[Entity]EventException", + "null_handler": "Null[Entity]EventHandlerException", + "failed_service": "Failed[Entity]EventServiceException", + "validation_wrapper": "[Entity]EventValidationException", + "service_wrapper": "[Entity]EventServiceException" + } + }, + + "test_order": [ + "ShouldSubscribeTo[Entity]Events", + "ShouldPublish[Entity]Async", + "ShouldThrowValidationExceptionOnSubscribeTo[Entity]EventIfEventHandlerIsNull", + "ShouldThrowValidationExceptionOnPublishIf[Entity]IsNullAsync", + "ShouldThrowServiceExceptionOnSubscribeTo[Entity]EventIfServiceErrorOccurs", + "ShouldThrowServiceExceptionOnPublish[Entity]EventIfServiceErrorOccursAsync" + ], + + "required_verifications_per_test": [ + "exact_broker_calls_with_Times.Once_or_Times.Never", + "eventBrokerMock.VerifyNoOtherCalls()" + ], + + "forbidden": [ + "loggingBrokerMock_in_event_service_tests", + "ListenTo_prefix_on_subscribe_operations", + "DependencyValidationException_in_event_services", + "DependencyException_in_event_services", + "CriticalDependencyException_in_event_services", + "business_logic_in_broker", + "multiple_service_dependencies_in_event_service", + "missing_VerifyNoOtherCalls_on_eventBrokerMock", + "event_broker_lifetime_that_conflicts_with_infrastructure_requirements", + "event_service_lifetime_longer_than_broker_lifetime", + "SubscribeTo_called_inside_controller_or_service_method", + "missing_startup_activation_of_SubscribeTo" + ], + + "orchestration_integration": { + "publisher_orchestration": { + "calls": "Publish[Entity]Async on the event service", + "owns": "entity composition before publishing", + "forbidden": "knowledge of which services subscribe" + }, + "subscriber_orchestration": { + "public_activation_method": "SubscribeTo[Entity]Events (plural)", + "delegates_to": "this.[entity]EventService.SubscribeTo[Entity]Event(handler)", + "handler": "private async ValueTask On[Entity]EventReceivedAsync([Entity] [entity])" + }, + "naming_distinction": { + "event_service_method": "SubscribeTo[Entity]Event (singular)", + "orchestration_activation_method": "SubscribeTo[Entity]Events (plural)" + } + }, + + "dependency_injection": { + "event_broker_lifetime": "depends on infrastructure", + "in_memory_infrastructure_lifetime": "singleton -- required for correctness; subscription handlers are registered in client instances and lost if the instance is not shared", + "external_infrastructure_lifetime": "follow client library recommendations -- typically singleton for connection reuse and message processor lifecycle; subscriptions live in the external service and are not lost when the client is recreated", + "event_service_lifetime": "must match or be shorter than the IEventBroker lifetime", + "preferred_default": "singleton -- safe for all known event infrastructure", + "startup_activation": { + "startup_cs_pattern": "app.ApplicationServices.GetService().SubscribeTo[Entity]Events()", + "minimal_api_pattern": "app.Services.GetService().SubscribeTo[Entity]Events()", + "when": "after DI container is built, in Configure() or after builder.Build()", + "forbidden_locations": ["controller actions", "service methods", "middleware", "constructors"] + } + }, + + "required_libraries": { + "event_infrastructure": "LeVent", + "mocking": "Moq", + "assertions": "FluentAssertions", + "test_framework": "xUnit", + "exception_base": "Xeption" + } +} diff --git a/.agents/skills/the-standard-events/examples/bad/example_bad_event_service.cs b/.agents/skills/the-standard-events/examples/bad/example_bad_event_service.cs new file mode 100644 index 0000000..3e73e69 --- /dev/null +++ b/.agents/skills/the-standard-events/examples/bad/example_bad_event_service.cs @@ -0,0 +1,179 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Non-Standard Event Service Violations +// Each violation annotated with the rule it breaks. +// --------------------------------------------------------------- + +namespace MyProject.Services.Foundations.PostEvents +{ + // events-024 VIOLATION: class is not internal + // events-023 VIOLATION: ILoggingBroker injected into event service + public partial class PostEventService : IPostEventService + { + private readonly IEventBroker eventBroker; + private readonly ILoggingBroker loggingBroker; // events-023 VIOLATION + + public PostEventService(IEventBroker eventBroker, ILoggingBroker loggingBroker) + { + this.eventBroker = eventBroker; + this.loggingBroker = loggingBroker; // events-023 VIOLATION + } + + // events-063 VIOLATION: ListenTo prefix instead of SubscribeTo + // events-022 VIOLATION: Missing TryCatch delegate + public void ListenToPostEvent(Func postEventHandler) + { + // events-031 VIOLATION: No null check on handler before calling broker + this.eventBroker.SubscribeToPostEvent(postEventHandler); + } + + // events-021 VIOLATION: Method is not wrapped in TryCatch delegate + public ValueTask PublishPostAsync(Post post) + { + // events-030 VIOLATION: No null check on entity before publishing + return this.eventBroker.PublishPostEventAsync(post); + } + } +} + +// --------------------------------------------------------------- +// events-016 VIOLATION: Business logic in the broker +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Events +{ + public partial class EventBroker + { + // events-016 VIOLATION: Conditional logic inside broker + public ValueTask PublishPostEventAsync(Post post, string eventName = null) + { + if (post.IsPublished) // VIOLATION: logic in broker, not service + { + return this.PostEvents.PublishEventAsync(post, eventName); + } + + return ValueTask.CompletedTask; + } + } +} + +// --------------------------------------------------------------- +// events-043 VIOLATION: Dependency exception categories in event service +// --------------------------------------------------------------- + +namespace MyProject.Services.Foundations.PostEvents +{ + internal partial class PostEventService + { + private void TryCatch(ReturningNothingFunction returningNothingFunction) + { + try + { + returningNothingFunction(); + } + catch (NullPostEventHandlerException nullPostEventHandlerException) + { + throw CreateAndLogValidationException(nullPostEventHandlerException); + } + // events-043 VIOLATION: Event services do not call HTTP APIs + catch (HttpResponseNotFoundException httpResponseNotFoundException) + { + throw CreateAndLogCriticalDependencyException(httpResponseNotFoundException); + } + // events-043 VIOLATION: Event services do not call storage + catch (SqlException sqlException) + { + throw CreateAndLogCriticalDependencyException(sqlException); + } + catch (Exception exception) + { + var failedPostEventServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + throw CreateAndLogServiceException(failedPostEventServiceException); + } + } + } +} + +// --------------------------------------------------------------- +// events-032 VIOLATION: Wrong exception type for null entity +// --------------------------------------------------------------- + +namespace MyProject.Services.Foundations.PostEvents +{ + internal partial class PostEventService + { + private static void ValidatePostIsNotNull(Post post) + { + if (post is null) + { + // events-032 VIOLATION: Should be NullPostEventException + throw new InvalidPostException(message: "Post is invalid."); + } + } + } +} + +// --------------------------------------------------------------- +// BAD TESTS: Event service test violations +// --------------------------------------------------------------- + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + private readonly Mock eventBrokerMock; + private readonly Mock loggingBrokerMock; // events-074 VIOLATION + private readonly IPostEventService postEventService; + + public PostEventServiceTests() + { + this.eventBrokerMock = new Mock(); + this.loggingBrokerMock = new Mock(); // events-074 VIOLATION + + this.postEventService = new PostEventService( + eventBroker: this.eventBrokerMock.Object, + loggingBroker: this.loggingBrokerMock.Object); // events-023 VIOLATION + } + + [Fact] + public async Task ShouldPublishPostAsync() + { + // given + Post randomPost = CreateRandomPost(); + Post inputPost = randomPost; + + // when + await this.postEventService.PublishPostAsync(inputPost); + + // then + this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(inputPost), + Times.Once); + + // events-076 VIOLATION: Missing VerifyNoOtherCalls + } + + // events-070 VIOLATION: Subscribe test not run before Publish test (order matters) + // events-075 VIOLATION: Handler set as a raw Func, not Mock> + [Fact] + public void ShouldListenToPostEvents() // events-063 VIOLATION: ListenTo naming + { + // given + Func postEventHandler = (post) => ValueTask.CompletedTask; // VIOLATION: not a Mock + + // when + // events-063 VIOLATION: ListenTo instead of SubscribeTo + this.postEventService.ListenToPostEvent(postEventHandler); + + // then + this.eventBrokerMock.Verify(broker => + broker.SubscribeToPostEvent(postEventHandler), + Times.Once); + + // events-076 VIOLATION: Missing VerifyNoOtherCalls + } + } +} diff --git a/.agents/skills/the-standard-events/examples/good/example_event_service_orchestration_di.cs b/.agents/skills/the-standard-events/examples/good/example_event_service_orchestration_di.cs new file mode 100644 index 0000000..b32025d --- /dev/null +++ b/.agents/skills/the-standard-events/examples/good/example_event_service_orchestration_di.cs @@ -0,0 +1,171 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Orchestration Integration, DI Registration, and Startup Activation +// Demonstrates: +// - Publisher orchestration service consuming the event service (Publish[Entity]Async) +// - Subscriber orchestration service consuming the event service (SubscribeTo[Entity]Event) +// - Plural vs singular naming on the orchestration layer +// - Singleton DI registration for broker and event service +// - Startup activation in Startup.cs (ASP.NET Core 3/5) and minimal API (Program.cs) + +// --------------------------------------------------------------- +// File: IStudentEventOrchestrationService.cs +// Publisher orchestration service interface +// events-004: Decouples publisher from any specific subscriber +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.StudentEvents; + +namespace MyProject.Services.Orchestrations.StudentEvents +{ + public interface IStudentEventOrchestrationService + { + ValueTask PublishStudentEventAsync(Student student); + } +} + +// --------------------------------------------------------------- +// File: StudentEventOrchestrationService.cs +// Publisher orchestration service: composes the entity, delegates publish to event service. +// events-003 (indirectly): Orchestration knows only I[Entity]EventService -- no broker. +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.StudentEvents; +using MyProject.Services.Foundations.StudentEvents; +using MyProject.Services.Foundations.Students; + +namespace MyProject.Services.Orchestrations.StudentEvents +{ + public class StudentEventOrchestrationService : IStudentEventOrchestrationService + { + private readonly IStudentEventService studentEventService; + private readonly IStudentService studentService; + + public StudentEventOrchestrationService( + IStudentEventService studentEventService, + IStudentService studentService) + { + this.studentEventService = studentEventService; + this.studentService = studentService; + } + + public async ValueTask PublishStudentEventAsync(Student student) + { + Student addedStudent = + await this.studentService.AddStudentAsync(student); + + // events-021: Delegates to event service Publish[Entity]Async + await this.studentEventService.PublishStudentAsync(addedStudent); + } + } +} + +// --------------------------------------------------------------- +// File: ILibraryAccountOrchestrationService.cs +// Subscriber orchestration service interface. +// events-082: Exposes SubscribeTo[Entity]Events (plural) -- the startup activation method. +// --------------------------------------------------------------- + +namespace MyProject.Services.Orchestrations.LibraryAccounts +{ + public interface ILibraryAccountOrchestrationService + { + // events-082: Plural -- wraps the singular event service SubscribeTo[Entity]Event + void SubscribeToStudentEvents(); + } +} + +// --------------------------------------------------------------- +// File: LibraryAccountOrchestrationService.cs +// Subscriber orchestration service: reacts to student events by creating library accounts. +// events-082: Plural SubscribeToStudentEvents activates the singular event service subscription. +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.StudentEvents; +using MyProject.Services.Foundations.LibraryAccounts; +using MyProject.Services.Foundations.StudentEvents; + +namespace MyProject.Services.Orchestrations.LibraryAccounts +{ + public class LibraryAccountOrchestrationService : ILibraryAccountOrchestrationService + { + private readonly IStudentEventService studentEventService; + private readonly ILibraryAccountService libraryAccountService; + + public LibraryAccountOrchestrationService( + IStudentEventService studentEventService, + ILibraryAccountService libraryAccountService) + { + this.studentEventService = studentEventService; + this.libraryAccountService = libraryAccountService; + } + + // events-082: Public plural method -- called once at startup to activate the subscription + public void SubscribeToStudentEvents() => + // events-063 (singular): Passes a private handler to the event service + this.studentEventService.SubscribeToStudentEvent(OnStudentEventReceivedAsync); + + // Private handler: contains the orchestration reaction logic + private async ValueTask OnStudentEventReceivedAsync(Student student) => + await this.libraryAccountService.AddLibraryAccountAsync(student); + } +} + +// --------------------------------------------------------------- +// DI Registration -- Startup.cs style (ASP.NET Core 3/5) +// --------------------------------------------------------------- + +// In ConfigureServices(IServiceCollection services): + +// events-080: Lifetime must match infrastructure requirements. +// LeVent is in-memory -- singleton is REQUIRED for correctness (subscription state lives in the client instance). +// External infrastructure (e.g., Azure Service Bus) -- follow the client library's guidance (typically singleton +// for connection reuse and to keep message processors alive). +services.AddSingleton(); + +// events-081: Event service lifetime must match or be shorter than the broker it depends on. +services.AddSingleton(); + +// Orchestration services registered to match their broker/event service dependencies. +services.AddSingleton(); +services.AddSingleton(); + +// --------------------------------------------------------------- +// Startup Activation -- Startup.cs style (ASP.NET Core 3/5) +// --------------------------------------------------------------- + +// In Configure(IApplicationBuilder app, IWebHostEnvironment env): + +app.UseRouting(); +app.UseEndpoints(endpoints => endpoints.MapControllers()); + +// events-083: Activate every subscribing orchestration service after DI is built +// events-084: Must appear here in Configure -- never inside a controller or service method +app.ApplicationServices + .GetService() + .SubscribeToStudentEvents(); + +// --------------------------------------------------------------- +// DI Registration -- Program.cs style (.NET 6+ minimal API) +// --------------------------------------------------------------- + +// events-080, events-081: Same infrastructure-driven lifetime requirements apply. +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// events-083: Activate after app is built -- app.Services is available here +// events-084: Top-level Program.cs is the startup equivalent for minimal API +app.Services + .GetService() + .SubscribeToStudentEvents(); diff --git a/.agents/skills/the-standard-events/examples/good/example_event_service_publish.cs b/.agents/skills/the-standard-events/examples/good/example_event_service_publish.cs new file mode 100644 index 0000000..18ea6fd --- /dev/null +++ b/.agents/skills/the-standard-events/examples/good/example_event_service_publish.cs @@ -0,0 +1,361 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Event Service -- Publish Operation +// Demonstrates: service interface, implementation, validation, exceptions, and tests. + +// --------------------------------------------------------------- +// File: IPostEventService.cs +// events-060: Interface named I[Entity]EventService +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.PostEvents; + +namespace MyProject.Services.Foundations.PostEvents +{ + // events-060: I[Entity]EventService interface contract + // events-020: Exactly two operations per entity + internal interface IPostEventService + { + void SubscribeToPostEvent(Func postEventHandler); + ValueTask PublishPostAsync(Post post); + } +} + +// --------------------------------------------------------------- +// File: PostEventService.cs +// events-061: Implementation named [Entity]EventService +// events-024: internal partial class +// events-003: Only IEventBroker injected -- no other dependency +// events-023: No ILoggingBroker injected +// events-025: TryCatch delegates used +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Brokers.Events; +using MyProject.Models.Services.Foundations.PostEvents; + +namespace MyProject.Services.Foundations.PostEvents +{ + internal partial class PostEventService : IPostEventService + { + private readonly IEventBroker eventBroker; + + // events-023: Constructor takes only IEventBroker + public PostEventService(IEventBroker eventBroker) => + this.eventBroker = eventBroker; + + // events-063: SubscribeTo prefix -- not ListenTo + // events-022: Synchronous (void) + public void SubscribeToPostEvent( + Func postEventHandler) => + TryCatch(() => + { + ValidatePostEventHandler(postEventHandler); + this.eventBroker.SubscribeToPostEvent(postEventHandler); + }); + + // events-062: Publish[Entity]Async naming + // events-021: Async (ValueTask) + public ValueTask PublishPostAsync(Post post) => + TryCatch(async () => + { + ValidatePostOnPublish(post); + await this.eventBroker.PublishPostEventAsync(post); + }); + } +} + +// --------------------------------------------------------------- +// File: PostEventService.Validations.cs +// events-030: Publish validates entity not null +// events-031: Subscribe validates handler not null +// events-032: Null entity throws NullPostEventException +// events-033: Null handler throws NullPostEventHandlerException +// events-034: Circuit-breaking -- throw immediately, no collection +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Models.Foundations.PostEvents.Exceptions; + +namespace MyProject.Services.Foundations.PostEvents +{ + internal partial class PostEventService + { + private void ValidatePostEventHandler( + Func postEventHandler) + { + ValidatePostEventHandlerIsNotNull(postEventHandler); + } + + private void ValidatePostOnPublish(Post post) + { + ValidatePostIsNotNull(post); + } + + private static void ValidatePostEventHandlerIsNotNull( + Func postEventHandler) + { + if (postEventHandler is null) + { + // events-033: NullEventHandlerException for null handler + throw new NullPostEventHandlerException( + message: "Post event handler is null."); + } + } + + private static void ValidatePostIsNotNull(Post post) + { + if (post is null) + { + // events-032: NullEventException for null entity + throw new NullPostEventException( + message: "Post status event handler is null."); + } + } + } +} + +// --------------------------------------------------------------- +// File: PostEventService.Exceptions.cs +// events-040: Two TryCatch delegates (void and ValueTask) +// events-041: Unexpected exceptions -> FailedPostEventServiceException +// events-042: Wrapped in PostEventServiceException +// events-043: No Dependency or CriticalDependency exception categories +// events-044: FailedEventServiceException carries innerException and Data +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.PostEvents.Exceptions; +using Xeptions; + +namespace MyProject.Services.Foundations.PostEvents +{ + internal partial class PostEventService + { + // events-040: Sync delegate for SubscribeTo operations + private delegate void ReturningNothingFunction(); + // events-040: Async delegate for Publish operations + private delegate ValueTask ReturningValueTaskFunction(); + + private void TryCatch(ReturningNothingFunction returningNothingFunction) + { + try + { + returningNothingFunction(); + } + catch (NullPostEventHandlerException nullPostEventHandlerException) + { + // events-035: Wrap in [Entity]EventValidationException + throw CreateAndLogValidationException(nullPostEventHandlerException); + } + catch (Exception exception) + { + // events-041, events-044: Wrap in FailedEventServiceException with innerException + Data + var failedPostEventServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + // events-042: Wrap in [Entity]EventServiceException + throw CreateAndLogServiceException(failedPostEventServiceException); + } + } + + private async ValueTask TryCatch(ReturningValueTaskFunction returningValueTaskFunction) + { + try + { + await returningValueTaskFunction(); + } + catch (NullPostEventException nullPostEventException) + { + throw CreateAndLogValidationException(nullPostEventException); + } + catch (Exception exception) + { + var failedPostEventServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + throw CreateAndLogServiceException(failedPostEventServiceException); + } + } + + private PostEventValidationException CreateAndLogValidationException(Xeption exception) + { + var postEventValidationException = new PostEventValidationException( + message: "Post event validation error occurred, please try again.", + innerException: exception); + + return postEventValidationException; + } + + private PostEventServiceException CreateAndLogServiceException(Xeption exception) + { + var postEventServiceException = new PostEventServiceException( + message: "Post event service error occurred, please contact support", + innerException: exception); + + return postEventServiceException; + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Logic.Publish.cs +// events-071: Publish happy path tested +// events-076: VerifyNoOtherCalls at the end +// events-077: Times.Once for expected call +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public async Task ShouldPublishPostAsync() + { + // given + Post randomPost = CreateRandomPost(); + Post inputPost = randomPost; + + // when + await this.postEventService.PublishPostAsync(inputPost); + + // then + // events-077: Times.Once for expected call + this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(inputPost), + Times.Once); + + // events-076: Always VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Validations.Publish.cs +// events-072: Validation failure tested +// events-077: Times.Never for skipped broker call +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Models.Services.Foundations.PostEvents.Exceptions; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public async Task ShouldThrowValidationExceptionOnPublishIfPostIsNullAsync() + { + // given + Post nullPost = null; + + var nullPostEventException = new NullPostEventException( + message: "Post status event handler is null."); + + var expectedPostEventValidationException = new PostEventValidationException( + message: "Post event validation error occurred, please try again.", + innerException: nullPostEventException); + + // when + ValueTask publishPostTask = this.postEventService.PublishPostAsync(nullPost); + + PostEventValidationException actualPostEventValidationException = + await Assert.ThrowsAsync(publishPostTask.AsTask); + + // then + actualPostEventValidationException.Should() + .BeEquivalentTo(expectedPostEventValidationException); + + // events-077: Times.Never -- broker must not be called when validation fails + this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(nullPost), + Times.Never); + + // events-076: VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Exceptions.Publish.cs +// events-073: Service exception tested +// events-076: VerifyNoOtherCalls at the end +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Models.Services.Foundations.PostEvents.Exceptions; +using Xeptions; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public async Task ShouldThrowServiceExceptionOnPublishPostEventIfServiceErrorOccursAsync() + { + // given + Post randomPost = CreateRandomPost(); + Post inputPost = randomPost; + var serviceException = new Exception(); + + var failedPostEventServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expectedPostEventServiceException = new PostEventServiceException( + message: "Post event service error occurred, please contact support", + innerException: failedPostEventServiceException); + + this.eventBrokerMock.Setup(broker => + broker.PublishPostEventAsync(inputPost)) + .Throws(serviceException); + + // when + ValueTask publishPostTask = this.postEventService.PublishPostAsync(inputPost); + + PostEventServiceException actualPostEventServiceException = + await Assert.ThrowsAsync(publishPostTask.AsTask); + + // then + actualPostEventServiceException.Should() + .BeEquivalentTo(expectedPostEventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(inputPost), + Times.Once); + + // events-076: VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/examples/good/example_event_service_subscribe.cs b/.agents/skills/the-standard-events/examples/good/example_event_service_subscribe.cs new file mode 100644 index 0000000..0dd64c4 --- /dev/null +++ b/.agents/skills/the-standard-events/examples/good/example_event_service_subscribe.cs @@ -0,0 +1,299 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Event Service -- Subscribe Operation +// Demonstrates: broker interface, broker implementation, and subscribe tests. + +// --------------------------------------------------------------- +// File: IEventBroker.cs +// events-010: Base interface named IEventBroker +// --------------------------------------------------------------- + +namespace MyProject.Brokers.Events +{ + // events-010: Shared base interface -- empty, extended by entity partials + public partial interface IEventBroker + { } +} + +// --------------------------------------------------------------- +// File: IEventBroker_Post.cs +// events-013: Entity-specific interface partial +// events-015: Exactly Publish and SubscribeTo per entity +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using MyProject.Models.Services.Foundations.PostEvents; + +namespace MyProject.Brokers.Events +{ + public partial interface IEventBroker + { + // events-015: Publish and SubscribeTo -- no other operations + ValueTask PublishPostEventAsync(Post post, string eventName = null); + void SubscribeToPostEvent(Func postEventHandler, string eventName = null); + } +} + +// --------------------------------------------------------------- +// File: EventBroker.cs +// events-011: Implementation named EventBroker +// events-014: LeVentClient instantiated in constructor +// --------------------------------------------------------------- + +using LeVent.Clients; +using MyProject.Models.Services.Foundations.PostEvents; + +namespace MyProject.Brokers.Events +{ + // events-011: EventBroker is a partial class + public partial class EventBroker : IEventBroker + { + public EventBroker() + { + // events-014: Each entity gets its own LeVentClient instance + this.PostEvents = new LeVentClient(); + } + } +} + +// --------------------------------------------------------------- +// File: EventBroker_Post.cs +// events-012: Entity-specific implementation partial +// events-016: Thin pass-through to LeVentClient -- no logic +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using LeVent.Clients; +using MyProject.Models.Services.Foundations.PostEvents; + +namespace MyProject.Brokers.Events +{ + public partial class EventBroker + { + // events-014: LeVentClient property for this entity + public ILeVentClient PostEvents { get; set; } + + // events-016: Broker is a thin pass-through -- no business logic + public ValueTask PublishPostEventAsync(Post post, string eventName = null) => + this.PostEvents.PublishEventAsync(post, eventName); + + public void SubscribeToPostEvent( + Func postEventHandler, string eventName = null) => + this.PostEvents.RegisterEventHandler(postEventHandler, eventName); + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.cs (root) +// events-053: Root contains only mocks, service, and helpers +// events-074: No loggingBrokerMock declared +// --------------------------------------------------------------- + +using System; +using Moq; +using MyProject.Brokers.Events; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Services.Foundations.PostEvents; +using Tynamix.ObjectFiller; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + // events-074: Only eventBrokerMock -- no loggingBrokerMock + private readonly Mock eventBrokerMock; + private readonly IPostEventService postEventService; + + public PostEventServiceTests() + { + this.eventBrokerMock = new Mock(); + + // events-023: Service only receives the event broker + this.postEventService = new PostEventService( + eventBroker: this.eventBrokerMock.Object); + } + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static string GetRandomString() => + new MnemonicString(wordCount: GetRandomNumber()).GetValue(); + + private static Post CreateRandomPost() => + CreatePostFiller(GetRandomDateTimeOffset()).Create(); + + private static Filler CreatePostFiller(DateTimeOffset dateTimeOffset) + { + var filler = new Filler(); + + filler.Setup() + .OnType().Use(dateTimeOffset) + .OnProperty(post => post.Title).Use(GetRandomString()) + .OnProperty(post => post.Content).Use(GetRandomString()); + + return filler; + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Logic.Subscribe.cs +// events-070: Subscribe happy path tested first +// events-075: Handler mock is Mock> +// events-076: VerifyNoOtherCalls at the end +// events-077: Times.Once for expected call +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public void ShouldSubscribeToPostEvents() + { + // given + // events-075: Handler mock typed as Mock> + var postEventHandlerMock = new Mock>(); + + // when + // events-063: Method is SubscribeToPostEvent -- not ListenToPostEvent + this.postEventService.SubscribeToPostEvent(postEventHandlerMock.Object); + + // then + // events-077: Times.Once for the expected broker call + this.eventBrokerMock.Verify(broker => + broker.SubscribeToPostEvent(postEventHandlerMock.Object), + Times.Once); + + // events-076: Always VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Validations.Subscribe.cs +// events-072: Null handler validation failure tested +// events-077: Times.Never -- broker must not be called +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Models.Foundations.PostEvents.Exceptions; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public void ShouldThrowValidationExceptionOnSubscribeToPostEventIfEventHandlerIsNull() + { + // given + Func postEventHandlerMock = null; + + var nullPostEventHandlerException = + new NullPostEventHandlerException(message: "Post event handler is null."); + + var expectedPostEventValidationException = new PostEventValidationException( + message: "Post event validation error occurred, please try again.", + innerException: nullPostEventHandlerException); + + // when + Action subscribeToPostEventAction = () => + this.postEventService.SubscribeToPostEvent(postEventHandlerMock); + + PostEventValidationException actualPostEventValidationException = + Assert.Throws(subscribeToPostEventAction); + + // then + actualPostEventValidationException.Should() + .BeEquivalentTo(expectedPostEventValidationException); + + // events-077: Times.Never -- broker must not be called when validation fails + this.eventBrokerMock.Verify(broker => + broker.SubscribeToPostEvent(postEventHandlerMock), + Times.Never); + + // events-076: VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: PostEventServiceTests.Exceptions.Subscribe.cs +// events-073: Service exception tested for subscribe +// events-076: VerifyNoOtherCalls at the end +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using MyProject.Models.Services.Foundations.PostEvents; +using MyProject.Models.Foundations.PostEvents.Exceptions; +using Xeptions; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.PostEvents +{ + public partial class PostEventServiceTests + { + [Fact] + public void ShouldThrowServiceExceptionOnSubscribeToPostEventIfServiceErrorOccurs() + { + // given + var postEventHandlerMock = new Mock>(); + var serviceException = new Exception(); + + var failedPostServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expectedPostEventServiceException = new PostEventServiceException( + message: "Post event service error occurred, please contact support", + innerException: failedPostServiceException); + + this.eventBrokerMock.Setup(broker => + broker.SubscribeToPostEvent(postEventHandlerMock.Object)) + .Throws(serviceException); + + // when + Action subscribeToPostEventAction = () => + this.postEventService.SubscribeToPostEvent(postEventHandlerMock.Object); + + PostEventServiceException actualPostEventServiceException = + Assert.Throws(subscribeToPostEventAction); + + // then + actualPostEventServiceException.Should() + .BeEquivalentTo(expectedPostEventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.SubscribeToPostEvent(postEventHandlerMock.Object), + Times.Once); + + // events-076: VerifyNoOtherCalls + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/manifest.json b/.agents/skills/the-standard-events/manifest.json new file mode 100644 index 0000000..5e52695 --- /dev/null +++ b/.agents/skills/the-standard-events/manifest.json @@ -0,0 +1,101 @@ +{ + "name": "the-standard-events", + "version": "1.0.0", + "description": "Governs event-driven architecture under The Standard: CulDeSac pattern, LeVent-backed event brokers, foundation event services, validation, exception mapping, file structure, naming conventions, and test structure for publish and subscribe operations.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.1.0.0", + + "inputs": [ + "A request to implement event-driven communication between services", + "An event service or event broker to review", + "A request to determine whether the CulDeSac pattern applies", + "A publish or subscribe operation that needs to be implemented or tested", + "A request to wire up a subscriber orchestration service at application startup", + "A DI registration file (Startup.cs or Program.cs) to review for event broker, service, or startup activation compliance" + ], + + "outputs": [ + "Standard-compliant event broker: IEventBroker, IEventBroker_[Entity].cs, EventBroker.cs, EventBroker_[Entity].cs", + "Standard-compliant event service: main, .Validations.cs, .Exceptions.cs", + "Correct event service test files: root, Logic.Publish, Logic.Subscribe, Validations.Publish, Validations.Subscribe, Exceptions.Publish, Exceptions.Subscribe", + "Publisher orchestration service consuming Publish[Entity]Async", + "Subscriber orchestration service with plural SubscribeTo[Entity]Events activation method", + "Singleton DI registrations for IEventBroker and I[Entity]EventService", + "Startup activation calls in Configure() or minimal API equivalent", + "Feedback on event service violations with rule references", + "Naming and exception hierarchy conforming to the events-xxx rule set" + ], + + "dependencies": [ + "the-standard-core", + "the-standard-architecture" + ], + + "activation": { + "trigger": "implementing events, reviewing event services, adding publish or subscribe operations, determining CulDeSac applicability", + "note": "Always activate the-standard-core and the-standard-architecture first." + }, + + "required_libraries": [ + "LeVent", + "xUnit", + "Moq", + "FluentAssertions", + "Xeption" + ], + + "validation": { + "required": true, + "files": { + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + } + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_event_service_publish.cs", + "examples/good/example_event_service_subscribe.cs", + "examples/good/example_event_service_orchestration_di.cs" + ], + "bad": [ + "examples/bad/example_bad_event_service.cs" + ] + }, + "templates": { + "brokers": [ + "templates/brokers/levent/event_broker_template.cs" + ], + "foundations": [ + "templates/foundations/event_service_template.cs", + "templates/foundations/event_service_test_class_root_template.cs", + "templates/foundations/event_service_test_class_publish_template.cs", + "templates/foundations/event_service_test_class_subscribe_template.cs", + "templates/foundations/event_service_test_publish_template.cs", + "templates/foundations/event_service_test_subscribe_template.cs" + ], + "real_world_examples": [ + "templates/foundations/IProcessedEventService.cs", + "templates/foundations/ProcessedEventService.cs", + "templates/foundations/ProcessedEventService.Validations.cs", + "templates/foundations/ProcessedEventService.Exceptions.cs", + "templates/foundations/ProcessedEventServiceTests.Logic.Publish.cs", + "templates/foundations/ProcessedEventServiceTests.Logic.Listen.cs", + "templates/foundations/ProcessedEventServiceTests.Validations.Publish.cs", + "templates/foundations/ProcessedEventServiceTests.Validations.Listen.cs", + "templates/foundations/ProcessedEventServiceTests.Exceptions.Publish.cs", + "templates/foundations/ProcessedEventServiceTests.Exceptions.Listen.cs" + ] + }, + "validations": { + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-events/rules/rules.json b/.agents/skills/the-standard-events/rules/rules.json new file mode 100644 index 0000000..d1177fa --- /dev/null +++ b/.agents/skills/the-standard-events/rules/rules.json @@ -0,0 +1,54 @@ +{ + "rules": [ + { "id": "events-001", "category": "architecture", "description": "The CulDeSac pattern must be used when a service needs to publish domain events without expecting a return value from subscribers.", "severity": "error" }, + { "id": "events-002", "category": "architecture", "description": "Event services are always foundation services -- they sit at the boundary between the domain and the event infrastructure.", "severity": "error" }, + { "id": "events-003", "category": "architecture", "description": "Event services must not depend on other services -- only on the event broker.", "severity": "error" }, + { "id": "events-004", "category": "architecture", "description": "Events must be used to decouple services across bounded contexts without creating direct service-to-service dependencies.", "severity": "error" }, + { "id": "events-010", "category": "broker", "description": "The event broker interface must be named IEventBroker.", "severity": "error" }, + { "id": "events-011", "category": "broker", "description": "The event broker implementation must be named EventBroker (partial class).", "severity": "error" }, + { "id": "events-012", "category": "broker", "description": "Entity-specific broker implementation members must be defined in a dedicated partial file: EventBroker_[Entity].cs.", "severity": "error" }, + { "id": "events-013", "category": "broker", "description": "Entity-specific broker interface members must be defined in a dedicated partial file: IEventBroker_[Entity].cs.", "severity": "error" }, + { "id": "events-014", "category": "broker", "description": "The broker must expose exactly two operations per entity: Publish[Entity]Async and SubscribeTo[Entity]Event.", "severity": "error" }, + { "id": "events-015", "category": "broker", "description": "Broker operations must be thin pass-throughs to the underlying event infrastructure -- no business logic lives in the broker.", "severity": "error" }, + { "id": "events-020", "category": "service", "description": "Event services must implement exactly two operations per entity: Publish[Entity]Async and SubscribeTo[Entity]Event.", "severity": "error" }, + { "id": "events-021", "category": "service", "description": "Publish[Entity]Async must be async (ValueTask) and validate the entity before calling the broker.", "severity": "error" }, + { "id": "events-022", "category": "service", "description": "SubscribeTo[Entity]Event must be synchronous (void) and validate the handler before calling the broker.", "severity": "error" }, + { "id": "events-023", "category": "service", "description": "Event services must NOT inject a logging broker -- event services have no logging dependency.", "severity": "error" }, + { "id": "events-024", "category": "service", "description": "Event services must be internal partial classes.", "severity": "error" }, + { "id": "events-025", "category": "service", "description": "Event services must use TryCatch delegates to separate exception handling from business logic.", "severity": "error" }, + { "id": "events-030", "category": "validation", "description": "Publish operations must validate the entity is not null before calling the broker.", "severity": "error" }, + { "id": "events-031", "category": "validation", "description": "Subscribe operations must validate the event handler is not null before calling the broker.", "severity": "error" }, + { "id": "events-032", "category": "validation", "description": "Null entity violations must throw NullEventException -- not InvalidEntityException.", "severity": "error" }, + { "id": "events-033", "category": "validation", "description": "Null handler violations must throw NullEventHandlerException.", "severity": "error" }, + { "id": "events-034", "category": "validation", "description": "Null checks on entity and handler are circuit-breaking: throw immediately, do not continue or collect further errors.", "severity": "error" }, + { "id": "events-035", "category": "validation", "description": "Validation exceptions from event services must be wrapped in [Entity]EventValidationException.", "severity": "error" }, + { "id": "events-040", "category": "exceptions", "description": "Event services must use two TryCatch delegates: ReturningNothingFunction (sync) and ReturningValueTaskFunction (async).", "severity": "error" }, + { "id": "events-041", "category": "exceptions", "description": "All unexpected exceptions must be wrapped in FailedEventServiceException before being rethrown.", "severity": "error" }, + { "id": "events-042", "category": "exceptions", "description": "FailedEventServiceException must be wrapped in [Entity]EventServiceException before leaving the service boundary.", "severity": "error" }, + { "id": "events-043", "category": "exceptions", "description": "Event services do NOT catch DependencyValidation, Dependency, or CriticalDependency exceptions -- they do not call HTTP or storage APIs.", "severity": "error" }, + { "id": "events-044", "category": "exceptions", "description": "FailedEventServiceException must carry the original exception as innerException and the original exception's Data collection.", "severity": "error" }, + { "id": "events-050", "category": "file-structure", "description": "The event service must be split into three partial files: main (.cs), .Validations.cs, and .Exceptions.cs.", "severity": "error" }, + { "id": "events-051", "category": "file-structure", "description": "The event broker must be split into four files: IEventBroker.cs, IEventBroker_[Entity].cs, EventBroker.cs, EventBroker_[Entity].cs.", "severity": "error" }, + { "id": "events-052", "category": "file-structure", "description": "Test files must follow the partial split: root, Logic.Publish, Logic.Subscribe, Validations.Publish, Validations.Subscribe, Exceptions.Publish, Exceptions.Subscribe.", "severity": "error" }, + { "id": "events-053", "category": "file-structure", "description": "The root test file must contain only: mock declarations, dependency instantiation, and helper/filler methods.", "severity": "error" }, + { "id": "events-060", "category": "naming", "description": "The event service interface must be named I[Entity]EventService.", "severity": "error" }, + { "id": "events-061", "category": "naming", "description": "The event service implementation must be named [Entity]EventService.", "severity": "error" }, + { "id": "events-062", "category": "naming", "description": "Publish operations must be named Publish[Entity]Async.", "severity": "error" }, + { "id": "events-063", "category": "naming", "description": "Subscribe operations must be named SubscribeTo[Entity]Event.", "severity": "error" }, + { "id": "events-064", "category": "naming", "description": "The model exception namespace must be [Namespace].Models.Foundations.[Entities].Exceptions.", "severity": "error" }, + { "id": "events-065", "category": "naming", "description": "Exception types must follow the naming pattern: Null[Entity]EventException, Null[Entity]EventHandlerException, Failed[Entity]EventServiceException, [Entity]EventValidationException, [Entity]EventServiceException.", "severity": "error" }, + { "id": "events-070", "category": "testing", "description": "Test SubscribeTo[Entity]Events (happy path) first.", "severity": "error" }, + { "id": "events-071", "category": "testing", "description": "Test Publish[Entity]Async (happy path) second.", "severity": "error" }, + { "id": "events-072", "category": "testing", "description": "Test validation failures third (null handler, null entity).", "severity": "error" }, + { "id": "events-073", "category": "testing", "description": "Test service exceptions fourth (unexpected errors in Publish and Subscribe).", "severity": "error" }, + { "id": "events-074", "category": "testing", "description": "Event service tests must NOT declare a loggingBrokerMock -- event services have no logging broker.", "severity": "error" }, + { "id": "events-075", "category": "testing", "description": "Subscribe handler mocks must be declared as Mock>.", "severity": "error" }, + { "id": "events-076", "category": "testing", "description": "Always end every event service test with eventBrokerMock.VerifyNoOtherCalls().", "severity": "error" }, + { "id": "events-077", "category": "testing", "description": "Use Times.Once for expected broker calls and Times.Never for broker calls that must not occur due to validation failures.", "severity": "error" }, + { "id": "events-080", "category": "dependency-injection", "description": "The DI lifetime of IEventBroker and EventBroker must match the requirements of the event infrastructure in use -- singleton is required for in-memory infrastructure (such as LeVent) where subscription state lives in client instances; for external infrastructure (such as Azure Service Bus or EventHighway), follow the client library's lifetime recommendations, which are typically singleton for connection reuse and message processor lifecycle.", "severity": "error" }, + { "id": "events-081", "category": "dependency-injection", "description": "I[Entity]EventService and [Entity]EventService must be registered with a lifetime that matches their IEventBroker dependency -- never register the service with a longer lifetime than the broker.", "severity": "error" }, + { "id": "events-082", "category": "dependency-injection", "description": "Every subscribing orchestration service must expose SubscribeTo[Entity]Events (plural) as the public activation method that wraps the event service's SubscribeTo[Entity]Event (singular).", "severity": "error" }, + { "id": "events-083", "category": "dependency-injection", "description": "SubscribeTo[Entity]Events must be called at application startup via app.ApplicationServices.GetService() (Startup.cs) or app.Services.GetService<>() (minimal API) -- subscriptions are not self-activating.", "severity": "error" }, + { "id": "events-084", "category": "dependency-injection", "description": "Startup activation calls must appear in Configure() or the minimal API equivalent -- never inside a controller action, service method, or middleware.", "severity": "error" } + ] +} diff --git a/.agents/skills/the-standard-events/rules/rules.md b/.agents/skills/the-standard-events/rules/rules.md new file mode 100644 index 0000000..bf26dc5 --- /dev/null +++ b/.agents/skills/the-standard-events/rules/rules.md @@ -0,0 +1,78 @@ +# The Standard Events -- Rules + +## ARCHITECTURE + +**events-001** [ERROR] The CulDeSac pattern must be used when a service needs to publish domain events without expecting a return value from subscribers. +**events-002** [ERROR] Event services are always foundation services -- they sit at the boundary between the domain and the event infrastructure. +**events-003** [ERROR] Event services must not depend on other services -- only on the event broker. +**events-004** [ERROR] Events must be used to decouple services across bounded contexts without creating direct service-to-service dependencies. + +## BROKER + +**events-010** [ERROR] The event broker interface must be named IEventBroker. +**events-011** [ERROR] The event broker implementation must be named EventBroker (partial class). +**events-012** [ERROR] Entity-specific broker implementation members must be defined in a dedicated partial file: EventBroker_[Entity].cs. +**events-013** [ERROR] Entity-specific broker interface members must be defined in a dedicated partial file: IEventBroker_[Entity].cs. +**events-014** [ERROR] The broker must expose exactly two operations per entity: Publish[Entity]Async and SubscribeTo[Entity]Event. +**events-015** [ERROR] Broker operations must be thin pass-throughs to the underlying event infrastructure -- no business logic lives in the broker. + +## SERVICE + +**events-020** [ERROR] Event services must implement exactly two operations per entity: Publish[Entity]Async and SubscribeTo[Entity]Event. +**events-021** [ERROR] Publish[Entity]Async must be async (ValueTask) and validate the entity before calling the broker. +**events-022** [ERROR] SubscribeTo[Entity]Event must be synchronous (void) and validate the handler before calling the broker. +**events-023** [ERROR] Event services must NOT inject a logging broker -- event services have no logging dependency. +**events-024** [ERROR] Event services must be internal partial classes. +**events-025** [ERROR] Event services must use TryCatch delegates to separate exception handling from business logic. + +## VALIDATION + +**events-030** [ERROR] Publish operations must validate the entity is not null before calling the broker. +**events-031** [ERROR] Subscribe operations must validate the event handler is not null before calling the broker. +**events-032** [ERROR] Null entity violations must throw NullEventException -- not InvalidEntityException. +**events-033** [ERROR] Null handler violations must throw NullEventHandlerException. +**events-034** [ERROR] Null checks on entity and handler are circuit-breaking: throw immediately, do not continue or collect further errors. +**events-035** [ERROR] Validation exceptions from event services must be wrapped in [Entity]EventValidationException. + +## EXCEPTIONS + +**events-040** [ERROR] Event services must use two TryCatch delegates: ReturningNothingFunction (sync) and ReturningValueTaskFunction (async). +**events-041** [ERROR] All unexpected exceptions must be wrapped in FailedEventServiceException before being rethrown. +**events-042** [ERROR] FailedEventServiceException must be wrapped in [Entity]EventServiceException before leaving the service boundary. +**events-043** [ERROR] Event services do NOT catch DependencyValidation, Dependency, or CriticalDependency exceptions -- they do not call HTTP or storage APIs. +**events-044** [ERROR] FailedEventServiceException must carry the original exception as innerException and the original exception's Data collection. + +## FILE STRUCTURE + +**events-050** [ERROR] The event service must be split into three partial files: main (.cs), .Validations.cs, and .Exceptions.cs. +**events-051** [ERROR] The event broker must be split into four files: IEventBroker.cs, IEventBroker_[Entity].cs, EventBroker.cs, EventBroker_[Entity].cs. +**events-052** [ERROR] Test files must follow the partial split: root, Logic.Publish, Logic.Subscribe, Validations.Publish, Validations.Subscribe, Exceptions.Publish, Exceptions.Subscribe. +**events-053** [ERROR] The root test file must contain only: mock declarations, dependency instantiation, and helper/filler methods. + +## NAMING + +**events-060** [ERROR] The event service interface must be named I[Entity]EventService. +**events-061** [ERROR] The event service implementation must be named [Entity]EventService. +**events-062** [ERROR] Publish operations must be named Publish[Entity]Async. +**events-063** [ERROR] Subscribe operations must be named SubscribeTo[Entity]Event. +**events-064** [ERROR] The model exception namespace must be [Namespace].Models.Foundations.[Entities].Exceptions. +**events-065** [ERROR] Exception types must follow the naming pattern: Null[Entity]EventException, Null[Entity]EventHandlerException, Failed[Entity]EventServiceException, [Entity]EventValidationException, [Entity]EventServiceException. + +## DEPENDENCY INJECTION + +**events-080** [ERROR] The DI lifetime of IEventBroker and EventBroker must match the requirements of the event infrastructure in use -- singleton is required for in-memory infrastructure (such as LeVent) where subscription state lives in client instances; for external infrastructure (such as Azure Service Bus or EventHighway), follow the client library's lifetime recommendations, which are typically singleton for connection reuse and message processor lifecycle. +**events-081** [ERROR] I[Entity]EventService and [Entity]EventService must be registered with a lifetime that matches their IEventBroker dependency -- never register the service with a longer lifetime than the broker. +**events-082** [ERROR] Every subscribing orchestration service must expose SubscribeTo[Entity]Events (plural) as the public activation method that wraps the event service's SubscribeTo[Entity]Event (singular). +**events-083** [ERROR] SubscribeTo[Entity]Events must be called at application startup via app.ApplicationServices.GetService() (Startup.cs) or app.Services.GetService<>() (minimal API) -- subscriptions are not self-activating. +**events-084** [ERROR] Startup activation calls must appear in Configure() or the minimal API equivalent -- never inside a controller action, service method, or middleware. + +## TESTING + +**events-070** [ERROR] Test SubscribeTo[Entity]Events (happy path) first. +**events-071** [ERROR] Test Publish[Entity]Async (happy path) second. +**events-072** [ERROR] Test validation failures third (null handler, null entity). +**events-073** [ERROR] Test service exceptions fourth (unexpected errors in Publish and Subscribe). +**events-074** [ERROR] Event service tests must NOT declare a loggingBrokerMock -- event services have no logging broker. +**events-075** [ERROR] Subscribe handler mocks must be declared as Mock>. +**events-076** [ERROR] Always end every event service test with eventBrokerMock.VerifyNoOtherCalls(). +**events-077** [ERROR] Use Times.Once for expected broker calls and Times.Never for broker calls that must not occur due to validation failures. diff --git a/.agents/skills/the-standard-events/templates/brokers/event_broker_template.cs b/.agents/skills/the-standard-events/templates/brokers/event_broker_template.cs new file mode 100644 index 0000000..2f7f745 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/brokers/event_broker_template.cs @@ -0,0 +1,131 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Event Broker — Infrastructure-Agnostic Implementation +// Replace [Entity] / [entity] / [Entities] / [Namespace] with actual values. +// Replace [EventClient] / [IEventClient] with your chosen event infrastructure client type. +// Demonstrates: Interface and implementation for event-driven architecture. + +// NOTE: This template is infrastructure-agnostic. You may use any event library that fits your needs. +// Examples: LeVent, EventHighway, Azure Service Bus, RabbitMQ, Kafka, MassTransit, etc. +// The example below uses LeVent, but you can substitute any event infrastructure. + +// --------------------------------------------------------------- +// File: IEventBroker.cs +// Base interface for event broker +// --------------------------------------------------------------- + +namespace [Namespace].Brokers.Events +{ + public partial interface IEventBroker + { } +} + +// --------------------------------------------------------------- +// File: IEventBroker_[Entity].cs +// Entity-specific event operations interface +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Models.Foundations.[Entities]; + +namespace [Namespace].Brokers.Events +{ + public partial interface IEventBroker + { + ValueTask Publish[Entity]Async([Entity] [entity], string eventName = null); + void SubscribeTo[Entity]Event(Func<[Entity], ValueTask> [entity]EventHandler, string eventName = null); + } +} + +// --------------------------------------------------------------- +// File: EventBroker.cs +// Base event broker implementation with constructor +// --------------------------------------------------------------- + +using [Namespace].Models.Foundations.[Entities]; + +// EXAMPLE: Using LeVent as the event infrastructure +// Replace with your chosen event infrastructure library: +using LeVent.Clients; +// using EventHighway; +// using Azure.Messaging.ServiceBus; +// using RabbitMQ.Client; +// using Confluent.Kafka; + +namespace [Namespace].Brokers.Events +{ + public partial class EventBroker : IEventBroker + { + public EventBroker() + { + // EXAMPLE: Initialize LeVent client for this entity + // Replace with your chosen event infrastructure initialization: + this.[Entity]Events = new LeVentClient<[Entity]>(); + + // ALTERNATIVE EXAMPLES (commented out): + // this.[Entity]Events = new EventHighwayClient<[Entity]>(); + // this.[Entity]Events = new ServiceBusClient(connectionString).CreateSender(queueName); + // this.[Entity]Events = new ConnectionFactory().CreateConnection().CreateModel(); + } + } +} + +// --------------------------------------------------------------- +// File: EventBroker_[Entity].cs +// Entity-specific event operations implementation +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Models.Foundations.[Entities]; + +// EXAMPLE: Using LeVent as the event infrastructure +// Replace with your chosen event infrastructure library: +using LeVent.Clients; +// using EventHighway; +// using Azure.Messaging.ServiceBus; +// using RabbitMQ.Client; + +namespace [Namespace].Brokers.Events +{ + public partial class EventBroker + { + // EXAMPLE: LeVent client property + // Replace ILeVentClient<[Entity]> and [EventClient] with your infrastructure's client type: + public ILeVentClient<[Entity]> [Entity]Events { get; set; } + + // ALTERNATIVE EXAMPLES (commented out): + // public IEventHighwayClient<[Entity]> [Entity]Events { get; set; } + // public ServiceBusSender [Entity]Events { get; set; } + // public IModel [Entity]Events { get; set; } + + public ValueTask Publish[Entity]Async([Entity] [entity], string eventName = null) => + this.[Entity]Events.PublishEventAsync([entity], eventName); + + // ALTERNATIVE EXAMPLES (commented out): + // this.[Entity]Events.PublishAsync([entity], eventName); + // this.[Entity]Events.SendMessageAsync(new ServiceBusMessage(JsonSerializer.Serialize([entity]))); + // this.[Entity]Events.BasicPublish( + // exchange: "", + // routingKey: eventName, + // body: Encoding.UTF8.GetBytes(JsonSerializer.Serialize([entity]))); + + public void SubscribeTo[Entity]Event(Func<[Entity], ValueTask> [entity]EventHandler, string eventName = null) => + this.[Entity]Events.RegisterEventHandler([entity]EventHandler, eventName); + + // ALTERNATIVE EXAMPLES (commented out): + // this.[Entity]Events.Subscribe([entity]EventHandler, eventName); + // this.[Entity]Events.OnMessageAsync(async (message) => + // await [entity]EventHandler(JsonSerializer.Deserialize<[Entity]>(message.Body))); + + // var consumer = new EventingBasicConsumer(channel); consumer.Received += async (model, ea) => + // { + // var [entity] = JsonSerializer.Deserialize<[Entity]>(ea.Body.ToArray()); + // await [entity]EventHandler([entity]); + // }; + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_template.cs new file mode 100644 index 0000000..e30b680 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_template.cs @@ -0,0 +1,186 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Foundation Event Service — All three axes +// Replace [Entity] / [entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Main logic, Validations, and Exceptions files. + +// --------------------------------------------------------------- +// File: I[Entity]EventService.cs +// Interface defining event service contract +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Models.Services.Foundations.[Entity]Events; + +namespace [Namespace].Services.Foundations.[Entity]Events +{ + internal interface I[Entity]EventService + { + void SubscribeTo[Entity]Event(Func<[Entity], ValueTask> [entity]EventHandler); + ValueTask Publish[Entity]Async([Entity] [entity]); + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventService.cs +// Main service implementation with TryCatch wrappers +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Brokers.Events; +using [Namespace].Models.Services.Foundations.[Entity]Events; + +namespace [Namespace].Services.Foundations.[Entity]Events +{ + internal partial class [Entity]EventService : I[Entity]EventService + { + private readonly IEventBroker eventBroker; + + public [Entity]EventService(IEventBroker eventBroker) => + this.eventBroker = eventBroker; + + public void SubscribeTo[Entity]Event( + Func<[Entity], ValueTask> [entity]EventHandler) => + TryCatch(() => + { + Validate[Entity]EventHandler([entity]EventHandler); + this.eventBroker.SubscribeTo[Entity]Event([entity]EventHandler); + }); + + public ValueTask Publish[Entity]Async([Entity] [entity]) => + TryCatch(async () => + { + Validate[Entity]OnPublish([entity]); + await this.eventBroker.Publish[Entity]EventAsync([entity]); + }); + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventService.Validations.cs +// Validation logic for event service operations +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; + +namespace [Namespace].Services.Foundations.[Entity]Events +{ + internal partial class [Entity]EventService + { + private void Validate[Entity]EventHandler( + Func<[Entity], ValueTask> [entity]EventHandler) + { + Validate[Entity]EventHandlerIsNotNull([entity]EventHandler); + } + + private void Validate[Entity]OnPublish([Entity] [entity]) + { + Validate[Entity]IsNotNull([entity]); + } + + private static void Validate[Entity]EventHandlerIsNotNull( + Func<[Entity], ValueTask> [entity]EventHandler) + { + if ([entity]EventHandler is null) + { + throw new Null[Entity]EventHandlerException(message: "[Entity] event handler is null."); + } + } + + private static void Validate[Entity]IsNotNull([Entity] [entity]) + { + if ([entity] is null) + { + throw new Null[Entity]EventException(message: "[Entity] status event handler is null."); + } + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventService.Exceptions.cs +// Exception handling and mapping logic +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using [Namespace].Models.Services.Foundations.[Entity]Events.Exceptions; +using Xeptions; + +namespace [Namespace].Services.Foundations.[Entity]Events +{ + internal partial class [Entity]EventService + { + private delegate void ReturningNothingFunction(); + private delegate ValueTask ReturningValueTaskFunction(); + + private void TryCatch(ReturningNothingFunction returningNothingFunction) + { + try + { + returningNothingFunction(); + } + catch (Null[Entity]EventHandlerException null[Entity]EventHandler) + { + throw CreateAndLogValidationException(null[Entity]EventHandler); + } + catch (Exception exception) + { + var failed[Entity]EventServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + throw CreateAndLogServiceException(failed[Entity]EventServiceException); + } + } + + private async ValueTask TryCatch(ReturningValueTaskFunction returningValueTaskFunction) + { + try + { + await returningValueTaskFunction(); + } + catch (Null[Entity]EventException null[Entity]EventException) + { + throw CreateAndLogValidationException(null[Entity]EventException); + } + catch (Exception exception) + { + var failed[Entity]EventServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + throw CreateAndLogServiceException(failed[Entity]EventServiceException); + } + } + + private [Entity]EventValidationException CreateAndLogValidationException(Xeption exception) + { + var [entity]EventValidationException = new [Entity]EventValidationException( + message: "[Entity] event validation error occurred, please try again.", + innerException: exception); + + return [entity]EventValidationException; + } + + private [Entity]EventServiceException CreateAndLogServiceException( + Xeption exception) + { + var [entity]EventServiceException = new [Entity]EventServiceException( + message: "[Entity] event service error occurred, please contact support", + innerException: exception); + + return [entity]EventServiceException; + } + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_publish_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_publish_template.cs new file mode 100644 index 0000000..50edf9e --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_publish_template.cs @@ -0,0 +1,153 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Event Service Tests — Publish Operation +// Replace [Entity] / [entity] / [Namespace] with actual values. +// Demonstrates: Logic, Validations, and Exceptions for Publish operations. + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Logic.Publish.cs +// Happy-path tests for publish operation +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldPublish[Entity]Async() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + + // when + await this.[entity]EventService + .Publish[Entity]Async(input[Entity]); + + // then + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(input[Entity]), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Validations.Publish.cs +// Validation failure tests for publish operation +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldThrowValidationExceptionOnPublishIf[Entity]IsNullAsync() + { + // given + [Entity] null[Entity] = null; + + var null[Entity]EventException = new Null[Entity]EventException( + message: "[Entity] status event handler is null."); + + var expected[Entity]EventValidationException = new [Entity]EventValidationException( + message: "[Entity] event validation error occurred, please try again.", + innerException: null[Entity]EventException); + + // when + ValueTask publish[Entity]Task = + this.[entity]EventService.Publish[Entity]Async(null[Entity]); + + [Entity]EventValidationException actual[Entity]EventValidationException = + await Assert.ThrowsAsync<[Entity]EventValidationException>(publish[Entity]Task.AsTask); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventValidationException); + + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(null[Entity]), + Times.Never); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Exceptions.Publish.cs +// Exception tests for publish operation +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldThrowServiceExceptionOnPublish[Entity]EventIfServiceErrorOccursAsync() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + + var serviceException = new Exception(); + + var failed[Entity]EventServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expected[Entity]EventServiceException = new [Entity]EventServiceException( + message: "[Entity] event service error occurred, please contact support", + innerException: failed[Entity]EventServiceException); + + this.eventBrokerMock.Setup(broker => + broker.Publish[Entity]EventAsync(input[Entity])) + .Throws(serviceException); + + // when + ValueTask subscribeTo[Entity]EventTask = this.[entity]EventService + .Publish[Entity]Async(input[Entity]); + + [Entity]EventServiceException actual[Entity]EventValidationException = + await Assert.ThrowsAsync<[Entity]EventServiceException>( + subscribeTo[Entity]EventTask.AsTask); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(input[Entity]), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_root_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_root_template.cs new file mode 100644 index 0000000..9748962 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_root_template.cs @@ -0,0 +1,56 @@ +// --------------------------------------------------------------- +// Copyright (c) Christo du Toit. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License") +// See License.txt in the project root for license information. +// --------------------------------------------------------------- + +using System; +using Moq; +using [Namespace].Brokers.Events; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Services.Foundations.[Entity]Events; +using Tynamix.ObjectFiller; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity] +EventServiceTests + { + private readonly Mock eventBrokerMock; + private readonly I[Entity]EventService [entity]EventService; + + public [Entity]EventServiceTests() + { + this.eventBrokerMock = new Mock(); + + this.[entity]EventService = new [Entity]EventService( + eventBroker: this.eventBrokerMock.Object); + } + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static string GetRandomString() => + new MnemonicString(wordCount: GetRandomNumber()).GetValue(); + + private static [Entity] CreateRandom[Entity](DateTimeOffset? dateTimeOffset = null) => + Create[Entity]Filler(dateTimeOffset ?? GetRandomDateTimeOffset()).Create(); + + private static Filler<[Entity]> Create[Entity]Filler(DateTimeOffset dateTimeOffset) + { + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(dateTimeOffset) + .OnProperty(borough => borough.Message).Use(GetRandomString()) + .OnProperty(borough => borough.Status).Use(GetRandomString()) + .OnProperty(borough => borough.[Entity]Items).Use(GetRandomNumber()) + .OnProperty(borough => borough.TotalItems).Use(GetRandomNumber()); + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_subscribe_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_subscribe_template.cs new file mode 100644 index 0000000..746a7f0 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_test_class_subscribe_template.cs @@ -0,0 +1,154 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Event Service Tests — Subscribe Operation +// Replace [Entity] / [entity] / [Namespace] with actual values. +// Demonstrates: Logic, Validations, and Exceptions for Subscribe operations. + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Logic.Subscribe.cs +// Happy-path tests for subscribe operation +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldSubscribeTo[Entity]Events() + { + // given + var [entity]EventHandlerMock = + new Mock>(); + + // when + this.[entity]EventService.SubscribeTo[Entity]Event( + [entity]EventHandlerMock.Object); + + // then + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Validations.Subscribe.cs +// Validation failure tests for subscribe operation +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldThrowValidationExceptionOnSubscribeTo[Entity]EventIfEventHandlerIsNull() + { + // given + Func<[Entity], ValueTask> [entity]EventHandlerMock = null; + + var null[Entity]EventHandler = + new Null[Entity]EventHandlerException(message: "[Entity] event handler is null."); + + var expected[Entity]EventValidationException = new [Entity]EventValidationException( + message: "[Entity] event validation error occurred, please try again.", + innerException: null[Entity]EventHandler); + + // when + Action subscribeTo[Entity]EventAction = () => this.[entity]EventService + .SubscribeTo[Entity]Event([entity]EventHandlerMock); + + [Entity]EventValidationException actual[Entity]EventValidationException = + Assert.Throws<[Entity]EventValidationException>(subscribeTo[Entity]EventAction); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventValidationException); + + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock), + Times.Never); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Exceptions.Subscribe.cs +// Exception tests for subscribe operation +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldThrowServiceExceptionOnSubscribeTo[Entity]EventIfServiceErrorOccurs() + { + // given + var [entity]EventHandlerMock = + new Mock>(); + + var serviceException = new Exception(); + + var failed[Entity]ServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expected[Entity]EventServiceException = new [Entity]EventServiceException( + message: "[Entity] event service error occurred, please contact support", + innerException: failed[Entity]ServiceException); + + this.eventBrokerMock.Setup(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object)) + .Throws(serviceException); + + // when + Action subscribeTo[Entity]EventAction = () => this.[entity]EventService + .SubscribeTo[Entity]Event([entity]EventHandlerMock.Object); + + [Entity]EventServiceException actual[Entity]EventValidationException = + Assert.Throws<[Entity]EventServiceException>(subscribeTo[Entity]EventAction); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_test_publish_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_test_publish_template.cs new file mode 100644 index 0000000..98a7141 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_test_publish_template.cs @@ -0,0 +1,154 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Event Service Tests -- Publish Operation (single-file reference) +// Replace [Entity] / [entity] / [Namespace] with actual values. +// For the full multi-file template see: event_service_test_class_publish_template.cs + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Logic.Publish.cs +// events-071: Happy-path test for Publish[Entity]Async +// events-076: VerifyNoOtherCalls on eventBrokerMock +// events-077: Times.Once for expected call +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldPublish[Entity]Async() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + + // when + await this.[entity]EventService.Publish[Entity]Async(input[Entity]); + + // then + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(input[Entity]), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Validations.Publish.cs +// events-072: Null entity validation failure test +// events-077: Times.Never -- broker must not be called +// --------------------------------------------------------------- + +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldThrowValidationExceptionOnPublishIf[Entity]IsNullAsync() + { + // given + [Entity] null[Entity] = null; + + var null[Entity]EventException = new Null[Entity]EventException( + message: "[Entity] status event handler is null."); + + var expected[Entity]EventValidationException = new [Entity]EventValidationException( + message: "[Entity] event validation error occurred, please try again.", + innerException: null[Entity]EventException); + + // when + ValueTask publish[Entity]Task = + this.[entity]EventService.Publish[Entity]Async(null[Entity]); + + [Entity]EventValidationException actual[Entity]EventValidationException = + await Assert.ThrowsAsync<[Entity]EventValidationException>(publish[Entity]Task.AsTask); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventValidationException); + + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(null[Entity]), + Times.Never); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Exceptions.Publish.cs +// events-073: Service exception test for Publish +// events-076: VerifyNoOtherCalls at the end +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public async Task ShouldThrowServiceExceptionOnPublish[Entity]EventIfServiceErrorOccursAsync() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + var serviceException = new Exception(); + + var failed[Entity]EventServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expected[Entity]EventServiceException = new [Entity]EventServiceException( + message: "[Entity] event service error occurred, please contact support", + innerException: failed[Entity]EventServiceException); + + this.eventBrokerMock.Setup(broker => + broker.Publish[Entity]EventAsync(input[Entity])) + .Throws(serviceException); + + // when + ValueTask publish[Entity]Task = + this.[entity]EventService.Publish[Entity]Async(input[Entity]); + + [Entity]EventServiceException actual[Entity]EventServiceException = + await Assert.ThrowsAsync<[Entity]EventServiceException>(publish[Entity]Task.AsTask); + + // then + actual[Entity]EventServiceException.Should() + .BeEquivalentTo(expected[Entity]EventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.Publish[Entity]EventAsync(input[Entity]), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/templates/foundations/event_service_test_subscribe_template.cs b/.agents/skills/the-standard-events/templates/foundations/event_service_test_subscribe_template.cs new file mode 100644 index 0000000..0b9eed3 --- /dev/null +++ b/.agents/skills/the-standard-events/templates/foundations/event_service_test_subscribe_template.cs @@ -0,0 +1,157 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Event Service Tests -- Subscribe Operation (single-file reference) +// Replace [Entity] / [entity] / [Namespace] with actual values. +// For the full multi-file template see: event_service_test_class_subscribe_template.cs + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Logic.Subscribe.cs +// events-070: Happy-path test for SubscribeTo[Entity]Event runs first +// events-075: Handler mock typed as Mock> +// events-076: VerifyNoOtherCalls on eventBrokerMock +// events-077: Times.Once for expected call +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldSubscribeTo[Entity]Events() + { + // given + // events-075: Handler is Mock> + var [entity]EventHandlerMock = new Mock>(); + + // when + // events-063: SubscribeTo -- never ListenTo + this.[entity]EventService.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object); + + // then + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Validations.Subscribe.cs +// events-072: Null handler validation failure test +// events-077: Times.Never -- broker must not be called +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldThrowValidationExceptionOnSubscribeTo[Entity]EventIfEventHandlerIsNull() + { + // given + Func<[Entity], ValueTask> [entity]EventHandlerMock = null; + + var null[Entity]EventHandlerException = + new Null[Entity]EventHandlerException(message: "[Entity] event handler is null."); + + var expected[Entity]EventValidationException = new [Entity]EventValidationException( + message: "[Entity] event validation error occurred, please try again.", + innerException: null[Entity]EventHandlerException); + + // when + Action subscribeTo[Entity]EventAction = () => + this.[entity]EventService.SubscribeTo[Entity]Event([entity]EventHandlerMock); + + [Entity]EventValidationException actual[Entity]EventValidationException = + Assert.Throws<[Entity]EventValidationException>(subscribeTo[Entity]EventAction); + + // then + actual[Entity]EventValidationException.Should() + .BeEquivalentTo(expected[Entity]EventValidationException); + + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock), + Times.Never); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]EventServiceTests.Exceptions.Subscribe.cs +// events-073: Service exception test for Subscribe +// events-076: VerifyNoOtherCalls at the end +// --------------------------------------------------------------- + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using [Namespace].Models.Services.Foundations.[Entity]Events; +using [Namespace].Models.Foundations.[Entity]Events.Exceptions; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entity]Events +{ + public partial class [Entity]EventServiceTests + { + [Fact] + public void ShouldThrowServiceExceptionOnSubscribeTo[Entity]EventIfServiceErrorOccurs() + { + // given + var [entity]EventHandlerMock = new Mock>(); + var serviceException = new Exception(); + + var failed[Entity]ServiceException = new Failed[Entity]EventServiceException( + message: "Failed [entity] event service error occurred, please contact support", + innerException: serviceException as Xeption, + data: serviceException.Data); + + var expected[Entity]EventServiceException = new [Entity]EventServiceException( + message: "[Entity] event service error occurred, please contact support", + innerException: failed[Entity]ServiceException); + + this.eventBrokerMock.Setup(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object)) + .Throws(serviceException); + + // when + Action subscribeTo[Entity]EventAction = () => + this.[entity]EventService.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object); + + [Entity]EventServiceException actual[Entity]EventServiceException = + Assert.Throws<[Entity]EventServiceException>(subscribeTo[Entity]EventAction); + + // then + actual[Entity]EventServiceException.Should() + .BeEquivalentTo(expected[Entity]EventServiceException); + + this.eventBrokerMock.Verify(broker => + broker.SubscribeTo[Entity]Event([entity]EventHandlerMock.Object), + Times.Once); + + this.eventBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-events/validations/anti-patterns.md b/.agents/skills/the-standard-events/validations/anti-patterns.md new file mode 100644 index 0000000..2b9555c --- /dev/null +++ b/.agents/skills/the-standard-events/validations/anti-patterns.md @@ -0,0 +1,346 @@ +# The Standard Events -- Anti-Patterns + +--- + +## AP-EVENTS-001: Using ListenTo Instead of SubscribeTo + +**What it is:** Naming the subscribe operation ListenTo[Entity]Event instead of SubscribeTo[Entity]Event. + +**Example:** +```csharp +// VIOLATION: events-063 -- ListenTo is not the standard name +public void ListenToPostEvent(Func postEventHandler) +{ + this.eventBroker.ListenToPostEvent(postEventHandler); +} +``` + +**Why harmful:** Inconsistency across codebases makes the intent unclear and breaks tooling, templates, and searchability. The Standard mandates SubscribeTo as the canonical prefix for subscribe operations in event services. + +**How to fix:** +```csharp +// events-063: Subscribe is the standard naming convention +public void SubscribeToPostEvent(Func postEventHandler) +{ + this.eventBroker.SubscribeToPostEvent(postEventHandler); +} +``` + +--- + +## AP-EVENTS-002: Injecting a Logging Broker Into an Event Service + +**What it is:** Adding ILoggingBroker as a dependency of an event service. + +**Example:** +```csharp +// VIOLATION: events-023 -- logging broker must not be injected +public PostEventService(IEventBroker eventBroker, ILoggingBroker loggingBroker) +{ + this.eventBroker = eventBroker; + this.loggingBroker = loggingBroker; +} +``` + +**Why harmful:** Event services are intentionally lean. They have no storage or HTTP dependency, and the exception contract (Validation and Service only) does not require logging infrastructure. Adding a logging broker increases coupling, changes the test contract, and breaks the uniformity of the event service pattern. + +**How to fix:** +```csharp +// events-023: Only the event broker is injected +public PostEventService(IEventBroker eventBroker) => + this.eventBroker = eventBroker; +``` + +--- + +## AP-EVENTS-003: Adding Dependency Exception Categories to an Event Service + +**What it is:** Catching HttpResponseException, StorageBrokerException, or any Dependency/CriticalDependency exception in an event service TryCatch block. + +**Example:** +```csharp +// VIOLATION: events-043 -- event services do not call HTTP or storage +catch (HttpResponseNotFoundException httpResponseNotFoundException) +{ + throw CreateAndLogCriticalDependencyException(httpResponseNotFoundException); +} +``` + +**Why harmful:** Event services communicate via an in-memory event bus (LeVent), not via HTTP or storage. Adding Dependency exception categories implies infrastructure calls that do not exist in an event service. This adds dead code, misleads maintainers, and inflates the exception surface. + +**How to fix:** Remove all Dependency and CriticalDependency catch blocks. Keep only validation and catch-all service exception handling: +```csharp +// events-040, events-041, events-042: Only validation and service exceptions +catch (NullPostEventException nullPostEventException) +{ + throw CreateAndLogValidationException(nullPostEventException); +} +catch (Exception exception) +{ + var failedPostEventServiceException = new FailedPostEventServiceException( + message: "Failed post event service error occurred, please contact support", + innerException: exception as Xeption, + data: exception.Data); + + throw CreateAndLogServiceException(failedPostEventServiceException); +} +``` + +--- + +## AP-EVENTS-004: Declaring loggingBrokerMock in Event Service Tests + +**What it is:** Adding a Mock to the test root class of an event service test. + +**Example:** +```csharp +// VIOLATION: events-074 -- no logging broker in event service tests +public partial class PostEventServiceTests +{ + private readonly Mock eventBrokerMock; + private readonly Mock loggingBrokerMock; // VIOLATION + private readonly IPostEventService postEventService; + + public PostEventServiceTests() + { + this.eventBrokerMock = new Mock(); + this.loggingBrokerMock = new Mock(); // VIOLATION + ... + } +} +``` + +**Why harmful:** The event service does not inject a logging broker, so mocking one will never be called. Its presence implies a verification that cannot happen, and it misleads reviewers into thinking logging is expected. VerifyNoOtherCalls on an unused mock also vacuously passes. + +**How to fix:** +```csharp +// events-074: Only the event broker mock is needed +public partial class PostEventServiceTests +{ + private readonly Mock eventBrokerMock; + private readonly IPostEventService postEventService; + + public PostEventServiceTests() + { + this.eventBrokerMock = new Mock(); + + this.postEventService = new PostEventService( + eventBroker: this.eventBrokerMock.Object); + } +} +``` + +--- + +## AP-EVENTS-005: Placing Business Logic Inside the Event Broker + +**What it is:** Adding filtering, transformation, or conditional routing logic inside the event broker methods. + +**Example:** +```csharp +// VIOLATION: events-016 -- broker must be a thin pass-through +public ValueTask PublishPostAsync(Post post, string eventName = null) +{ + if (post.IsActive) // VIOLATION: logic in broker + { + return this.PostEvents.PublishEventAsync(post, eventName); + } + + return ValueTask.CompletedTask; +} +``` + +**Why harmful:** Brokers are infrastructure wrappers. All business logic must live in the service layer. Putting logic in the broker bypasses validation, breaks testability (brokers are not unit-tested for logic), and violates separation of concerns. + +**How to fix:** +```csharp +// events-016: Broker is a thin pass-through to LeVent +public ValueTask PublishPostAsync(Post post, string eventName = null) => + this.PostEvents.PublishEventAsync(post, eventName); +``` + +--- + +## AP-EVENTS-006: Missing VerifyNoOtherCalls in Event Service Tests + +**What it is:** Omitting eventBrokerMock.VerifyNoOtherCalls() at the end of event service tests. + +**Example:** +```csharp +// VIOLATION: events-076 -- missing VerifyNoOtherCalls +this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(inputPost), + Times.Once); + +// MISSING: this.eventBrokerMock.VerifyNoOtherCalls(); +``` + +**Why harmful:** Without VerifyNoOtherCalls(), a future implementation change could add an extra broker call and no test would catch it. Silent regressions are introduced. + +**How to fix:** +```csharp +// events-076: Always end with VerifyNoOtherCalls +this.eventBrokerMock.Verify(broker => + broker.PublishPostEventAsync(inputPost), + Times.Once); + +this.eventBrokerMock.VerifyNoOtherCalls(); +``` + +--- + +## AP-EVENTS-007: Using InvalidEntityException Instead of NullEventException + +**What it is:** Throwing InvalidEntityException or InvalidEventException when the entity passed to Publish is null. + +**Example:** +```csharp +// VIOLATION: events-032 -- wrong exception type for null entity +private static void ValidatePostIsNotNull(Post post) +{ + if (post is null) + { + throw new InvalidPostException(message: "Post is invalid."); // VIOLATION + } +} +``` + +**Why harmful:** Event services have a distinct exception vocabulary. NullEventException signals a circuit-breaking null check, not a validation of individual fields. Using the wrong exception type breaks the exception hierarchy and makes exception-based routing impossible. + +**How to fix:** +```csharp +// events-032: Use NullEventException for null entity in event services +private static void ValidatePostIsNotNull(Post post) +{ + if (post is null) + { + throw new NullPostEventException(message: "Post status event handler is null."); + } +} +``` + +--- + +## AP-EVENTS-008: Missing Startup Activation of SubscribeTo + +**What it is:** Registering an event service and its subscriber orchestration service in DI but never calling `SubscribeTo[Entity]Events()` at application startup. + +**Example:** +```csharp +// VIOLATION: events-083 -- subscription is never activated +// ConfigureServices (Startup.cs) +services.AddSingleton(); +services.AddSingleton(); +services.AddSingleton(); + +// Configure (Startup.cs) -- MISSING startup activation call +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + // MISSING: app.ApplicationServices + // .GetService() + // .SubscribeToStudentEvents(); +} +``` + +**Why harmful:** Without the startup activation call, the subscription delegate is never registered with the event broker. The subscriber orchestration service exists in DI but it is deaf -- it will never receive any events. No error is thrown; the failure is completely silent. + +**How to fix:** +```csharp +// events-083: Activate every subscription at startup +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + + app.ApplicationServices + .GetService() + .SubscribeToStudentEvents(); +} + +// .NET 6+ minimal API equivalent (after app = builder.Build()) +app.Services + .GetService() + .SubscribeToStudentEvents(); +``` + +--- + +## AP-EVENTS-009: Ignoring Infrastructure Lifetime Requirements for Event Broker or Event Service + +**What it is:** Registering `EventBroker` or `[Entity]EventService` with a DI lifetime that conflicts with what the event infrastructure requires. + +### For in-memory infrastructure (e.g., LeVent) + +**Example:** +```csharp +// VIOLATION: events-080 -- LeVent client instances hold subscription state in memory +services.AddScoped(); // VIOLATION +services.AddTransient(); // VIOLATION +``` + +**Why harmful:** `EventBroker` holds `LeVentClient<[Entity]>` instances that maintain subscriber handler registrations in memory. A scoped or transient registration produces a new broker instance per request or per resolution. Publishers and the startup activation call operate on different instances -- subscriptions registered at startup exist on a discarded instance and are never seen by the instance that handles a request. Events appear to publish but are silently delivered to no one. + +**How to fix:** +```csharp +// events-080, events-081: Singleton required -- in-memory state must be shared +services.AddSingleton(); +services.AddSingleton(); +``` + +### For external infrastructure (e.g., Azure Service Bus, EventHighway) + +**Example:** +```csharp +// VIOLATION: events-080 -- new processor instances are created per request and never started +services.AddTransient(); // VIOLATION +``` + +**Why harmful:** Subscriptions for external infrastructure are handled by long-lived message processors (e.g., `ServiceBusProcessor`). A transient or scoped broker creates a new processor instance per resolution. That processor is never started, so no messages are received. The failure mode differs from in-memory infrastructure -- subscriptions survive in the external service, but nothing is listening to deliver them into the application. + +**How to fix:** +```csharp +// events-080, events-081: Follow the client library's lifetime guidance -- typically singleton +// for connection reuse and to keep the message processor alive +services.AddSingleton(); +services.AddSingleton(); +``` + +--- + +## AP-EVENTS-010: Calling SubscribeTo Inside a Controller or Service Method + +**What it is:** Invoking `SubscribeTo[Entity]Events()` inside a controller action, a service method, or any runtime code path rather than at application startup. + +**Example:** +```csharp +// VIOLATION: events-084 -- subscription activated inside a controller action +[HttpPost] +public async ValueTask> PostStudentAsync(Student student) +{ + // VIOLATION: subscription is re-registered on every POST request + this.libraryAccountOrchestrationService.SubscribeToStudentEvents(); + + Student addedStudent = await this.studentOrchestrationService + .AddStudentAsync(student); + + return Ok(addedStudent); +} +``` + +**Why harmful:** Every request re-registers the subscription handler. Most event libraries, including LeVent, allow multiple registrations on the same client -- meaning the handler fires once per registration per event. After ten requests, ten duplicate handlers fire for every event, producing ten duplicate side effects. + +**How to fix:** +```csharp +// events-083, events-084: Activate once at startup -- never at runtime +public void Configure(IApplicationBuilder app, IWebHostEnvironment env) +{ + app.UseRouting(); + app.UseEndpoints(endpoints => endpoints.MapControllers()); + + app.ApplicationServices + .GetService() + .SubscribeToStudentEvents(); +} +``` diff --git a/.agents/skills/the-standard-events/validations/checklist.md b/.agents/skills/the-standard-events/validations/checklist.md new file mode 100644 index 0000000..6bc2389 --- /dev/null +++ b/.agents/skills/the-standard-events/validations/checklist.md @@ -0,0 +1,118 @@ +# The Standard Events -- Validation Checklist + +Run this checklist before committing any event service code or approving a PR. +Each item is binary: PASS or FAIL. + +--- + +## ARCHITECTURE + +- [ ] **events-001** The CulDeSac pattern is applied -- the event flow is one-way with no synchronous return from subscribers. +- [ ] **events-002** The event service is a foundation service -- no higher-level service depends on it as a dependency of a dependency. +- [ ] **events-003** The event service depends only on IEventBroker -- no other service or broker is injected. +- [ ] **events-005** LeVent is the event infrastructure library -- no other library is used. + +--- + +## BROKER STRUCTURE + +- [ ] **events-010** The broker interface is named IEventBroker. +- [ ] **events-011** The broker implementation is named EventBroker. +- [ ] **events-012** Entity-specific implementation members are in EventBroker_[Entity].cs. +- [ ] **events-013** Entity-specific interface members are in IEventBroker_[Entity].cs. +- [ ] **events-014** Each entity has its own LeVentClient<[Entity]> instance in the broker constructor. +- [ ] **events-015** The broker exposes exactly Publish[Entity]Async and SubscribeTo[Entity]Event per entity. +- [ ] **events-016** Broker methods are thin pass-throughs -- no logic exists inside the broker. + +--- + +## SERVICE IMPLEMENTATION + +- [ ] **events-020** The service exposes exactly Publish[Entity]Async and SubscribeTo[Entity]Event. +- [ ] **events-021** Publish[Entity]Async is async (ValueTask) and validates before publishing. +- [ ] **events-022** SubscribeTo[Entity]Event is void (synchronous) and validates before subscribing. +- [ ] **events-023** No ILoggingBroker is injected into the event service. +- [ ] **events-024** The event service is declared as internal partial class. +- [ ] **events-025** TryCatch delegates are used to isolate exception handling. + +--- + +## VALIDATION + +- [ ] **events-030** Publish validates entity is not null before calling the broker. +- [ ] **events-031** Subscribe validates handler is not null before calling the broker. +- [ ] **events-032** Null entity throws NullEventException (not InvalidEntityException). +- [ ] **events-033** Null handler throws NullEventHandlerException. +- [ ] **events-034** Null checks are circuit-breaking -- no error collection, throws immediately. +- [ ] **events-035** Validation exceptions are wrapped in [Entity]EventValidationException. + +--- + +## EXCEPTION HANDLING + +- [ ] **events-040** Two TryCatch delegates exist: ReturningNothingFunction and ReturningValueTaskFunction. +- [ ] **events-041** Unexpected exceptions produce FailedEventServiceException. +- [ ] **events-042** FailedEventServiceException is wrapped in [Entity]EventServiceException. +- [ ] **events-043** No DependencyValidation, Dependency, or CriticalDependency exception categories are present. +- [ ] **events-044** FailedEventServiceException carries the original innerException and Data. + +--- + +## FILE STRUCTURE + +- [ ] **events-050** Service is split into three partials: main, .Validations.cs, .Exceptions.cs. +- [ ] **events-051** Broker is split into four files: IEventBroker.cs, IEventBroker_[Entity].cs, EventBroker.cs, EventBroker_[Entity].cs. +- [ ] **events-052** Tests are split into root, Logic.Publish, Logic.Subscribe, Validations.Publish, Validations.Subscribe, Exceptions.Publish, Exceptions.Subscribe. +- [ ] **events-053** Root test file contains only mock declarations, service instantiation, and helpers. + +--- + +## NAMING + +- [ ] **events-060** Interface is named I[Entity]EventService. +- [ ] **events-061** Implementation is named [Entity]EventService. +- [ ] **events-062** Publish method is named Publish[Entity]Async. +- [ ] **events-063** Subscribe method is named SubscribeTo[Entity]Event -- NOT ListenTo[Entity]Event. +- [ ] **events-064** Exception namespace is [Namespace].Models.Services.Foundations.[Entities].Exceptions. +- [ ] **events-065** All five exception type names follow the required naming pattern. + +--- + +## DEPENDENCY INJECTION + +- [ ] **events-080** IEventBroker and EventBroker are registered with the correct DI lifetime for the event infrastructure in use (singleton for in-memory infrastructure such as LeVent; follow client library guidance for external infrastructure such as Azure Service Bus or EventHighway). +- [ ] **events-081** I[Entity]EventService and [Entity]EventService are registered with a lifetime that matches or is shorter than the IEventBroker lifetime. +- [ ] **events-082** Every subscribing orchestration service exposes SubscribeTo[Entity]Events (plural) as a public activation method. +- [ ] **events-083** SubscribeTo[Entity]Events is called at application startup via app.ApplicationServices.GetService<>() or app.Services.GetService<>() -- not self-activating. +- [ ] **events-084** Startup activation calls appear in Configure() or the minimal API equivalent -- not inside controllers, services, or middleware. + +--- + +## TESTING + +- [ ] **events-070** SubscribeTo[Entity]Events happy path is tested first. +- [ ] **events-071** Publish[Entity]Async happy path is tested second. +- [ ] **events-072** Validation failures (null handler, null entity) are tested third. +- [ ] **events-073** Service exceptions (unexpected errors) are tested fourth. +- [ ] **events-074** No loggingBrokerMock is declared in the test class. +- [ ] **events-075** Handler mock is Mock>. +- [ ] **events-076** Every test ends with eventBrokerMock.VerifyNoOtherCalls(). +- [ ] **events-077** Times.Once used for expected calls, Times.Never for skipped calls. + +--- + +## RESULT + +| Category | PASS / FAIL | +|---|---| +| Architecture | | +| Broker Structure | | +| Service Implementation | | +| Validation | | +| Exception Handling | | +| File Structure | | +| Naming | | +| Dependency Injection | | +| Testing | | + +**Overall: PASS only when every row is PASS.** diff --git a/.agents/skills/the-standard-practices/SKILL.md b/.agents/skills/the-standard-practices/SKILL.md new file mode 100644 index 0000000..39644ec --- /dev/null +++ b/.agents/skills/the-standard-practices/SKILL.md @@ -0,0 +1,1490 @@ +--- +name: The Standard Practices +description: Enforces Standard contribution workflow, branching, categories, commits, pull requests, configuration handling, measurements, contribution points, and ranks. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard Practices + +## What this skill is + +This skill is the team-process, contribution, and project-discipline layer of The Standard. +It governs how work is organized, named, committed, reviewed, configured, measured, and grown over time. + +## Explicit coverage map + +This skill explicitly covers: + +- Code contribution +- Forking / cloning / branching +- Branch naming conventions +- Category system +- Project / solution structure +- Commit strategy +- FAIL / PASS discipline +- Pull request conventions +- Configuration handling +- Environment variables and secrets +- GitHub Actions configuration guidance +- Measurements +- Contribution points +- Average implementation time +- Ranks +- The extended implementation-spec practices section supplied with the project implementation specification + +## When to use + +Use this skill whenever planning work, naming branches, organizing repositories, writing commits, opening pull requests, or configuring environments. + +## Mandatory operating rules + +0. Contributions must be structured. +1. Branches must be named intentionally. +2. Commits must communicate the actual unit of work. +3. TDD work must follow FAIL / PASS discipline. +4. Pull requests must use Standard category naming. +5. Sensitive configuration must not be checked into plain text. +6. Measurements are part of engineering growth and fairness, not vanity. + +## Canonical practices + +# 4. Practices + +Having defined practices for code contributions, branch, commit strategies and project structures is essential for any software development project. +These practices provide a clear and consistent approach to managing the codebase, ensuring that contributions are made in a controlled and organized manner. +This helps to avoid confusion and conflicts, and ensures that the codebase remains stable and maintainable. +Additionally, well-defined practices for code contributions and project structure can help to improve collaboration and communication within the development team, making it easier to track and review changes, and to identify and resolve issues. +Overall, defined practices for code contributions, branching and commit strategies as well as project structures are a key part of any successful software development project. + +Practices requires these main aspects to be considered: + +4. Code Contribution + 1. Forking / Cloning and Branching + 2. Project Structure + 3. Commits + 4. Pull Requests + +The Standard Team is to clearly understand and define each and every aspect of the aforementioned points. + +## 4.1 Code Contribution + +Defining a contribution guideline is crucial for any open-source or collaborative software development project. +It establishes clear rules and expectations for how contributors should submit and review changes, which helps to ensure that all contributions are made in a consistent and organized manner. +Additionally, a contribute guideline can provide guidance on best practices for coding, testing and documentation, which can help to improve the overall quality of the codebase. +It also serves as an educational tool to new contributors, providing them with the necessary information and instructions on how to contribute to the project effectively. +(Furthermore, having a well-defined GitHub contribute guideline can help to attract new contributors and maintain a healthy open-source community.) + +### 4.1.1 Forking and Branching Strategies +A branch strategy for code is essential for maintaining a stable and maintainable codebase. +It allows for multiple developers to work on different features or bug fixes simultaneously, without interfering with each other's work. +Each developer can create their own branch to work on, which can then be merged into the main branch once it has been reviewed and approved. +This helps to avoid conflicts, and ensures that the codebase remains stable and consistent. +Furthermore, a branch strategy enables version control and allows different versions of the codebase to be created and tracked. +This can be useful for rolling back changes if necessary, and for maintaining multiple versions of the software for different environments. +Overall, a branch strategy is a critical aspect of any software development project and is essential for ensuring that the codebase remains stable and maintainable. + +There are several different types of branching strategies that can be used in software development, including: + +1. Gitflow: A popular branching strategy that follows a strict branching model, where development happens in feature branches and is merged into a main development branch. This strategy is good for large projects with many contributors. + +2. Trunk-Based Development: This strategy involves working on the main branch, or trunk, and committing changes directly to it. This is a good strategy for smaller projects with a small number of contributors. + +3. Feature Branching: This strategy involves creating a separate branch for each feature or bug fix, and merging it into the main branch when it is complete. This is a good strategy for larger projects with multiple contributors. + +4. Forking: This strategy involves creating a copy of the repository and working on it separately. This is good for open-source projects where multiple contributors are working on the same codebase. The main advantage of the Forking workflow is that contributions can be integrated without the need for everybody to push to a single central repository. Developers push to their own server-side repositories, and only the project maintainer can push to the official repository. This allows the maintainer to accept commits from any developer without giving them write access to the official codebase. + +5. Release Branching: This strategy involves creating a separate branch for each release, and merging it into the main branch when it is ready to be released. This is a good strategy for projects that have a regular release schedule. + +6. Continuous Integration: This strategy involves merging code changes as soon as they are made, and running automated tests to ensure that the codebase remains stable. This is a good strategy for projects that have a high frequency of changes. + +Ultimately, it's important to choose a branching strategy that fits the needs and constraints of the project, and that can be easily understood and followed by all the contributors. + +#### 4.1.1.1 Open Source Development +As The Standard promotes open-source work, lets look have a quick look at the Forking strategy. + +**How it works** + +The Forking workflow begins with an official public repository stored on a server. When a new developer wants to start working on the project, they do not directly clone the official repository. + +Instead, they fork the official repository to create a copy of it. This new copy serves as their personal public repository. +After they have created their server-side copy, the developer performs a git clone to get a copy of it onto their local machine. +This serves as their private development environment, just like in the other workflows. + +When they're ready to publish a local commit, they push the commit to their own public repository (not the official one). +They then file a pull request with the main repository, which lets the project maintainer know that an update is ready to be integrated. +The pull request can then be reviewed and it also serves as a convenient discussion thread if there are issues with the contributed code. + +The following is a step-by-step example of this workflow: + +1. A developer 'forks' an 'official' server-side repository. This creates their own server-side copy. +2. The new server-side copy is cloned to their local system. +3. A new local feature branch is created. +4. The developer makes changes on the new branch. +5. New commits are created for the changes. +6. The branch gets pushed to the developer's own server-side copy. +7. The developer opens a pull request from the new branch to the 'official' repository. +8. The pull request gets reviewed and build success and tests are verified. At this point the maintainer can either request changes or approve for merge upon which the changes are then merged into the original server-side repository. + + +#### 4.1.1.2 Branch Name Conventions + +For branch names we can apply the following naming convention: `users/[username]/[category]-[entity]-[action]` + +The variables can be substituted: +* `[username]` => your GitHub username +* `[category]` => the category that you are working on +* `[entity]` => the entity that your service / broker uses +* `[action]` => the action — **must match the language of the layer being worked on** + +##### Branch action language by layer + +The `[action]` segment uses the vocabulary of the layer, not a generic description: + +| Layer | Correct actions | Wrong actions | +|---|---|---| +| `BROKERS` | `insert`, `select-all`, `select-by-id`, `update`, `delete` | `add`, `retrieve`, `modify`, `remove` | +| `FOUNDATIONS` | `add`, `retrieve-all`, `retrieve-by-id`, `modify`, `remove` | `insert`, `select`, `update`, `delete` | +| `PROCESSINGS` | `ensure`, `upsert`, `try-add`, `try-remove` | infrastructure verbs | +| `INFRA`, `DATA`, `CONFIG` | `create`, `setup`, `update`, `remove` | (no strict language constraint) | + +**Examples:** + +``` +# Correct — infrastructure verb for a BROKERS branch +users/hassanhabib/BROKERS-student-insert +users/hassanhabib/BROKERS-student-select-all +users/hassanhabib/BROKERS-student-select-by-id +users/hassanhabib/BROKERS-student-update +users/hassanhabib/BROKERS-student-delete + +# Correct — business verb for a FOUNDATIONS branch +users/hassanhabib/FOUNDATIONS-student-add +users/hassanhabib/FOUNDATIONS-student-retrieve-all +users/hassanhabib/FOUNDATIONS-student-modify + +# Wrong — business verb in a BROKERS branch +users/hassanhabib/BROKERS-student-add ← should be 'insert' +users/hassanhabib/BROKERS-student-retrieve ← should be 'select-by-id' or 'select-all' +``` + +##### One operation per branch + +Each branch represents a single operation. When a prompt is ambiguous about scope +(e.g., "build a storage broker for Student"), the agent **must ask which operation(s) +to implement** before creating the branch or writing any code. It must not assume +all CRUD should be scaffolded. + +``` +Ambiguous prompt: "Build a storage broker for Student" +Required response: "Which operation should this branch implement? + insert / select-all / select-by-id / update / delete" +``` + +### 4.1.1.3 Category List + +| Category | Description | +|------------------------|-------------| +| INFRA | Initial project setup, creating the build project / build scripts. | +| MAJOR INFRA | Major updates, removing or adding simple configurations or files. | +| MEDIUM INFRA | Medium updates, removing or adding simple configurations or files. | +| MINOR INFRA | Minor updates, removing or adding simple configurations or files. | +| PROVISIONS | Creating provision project / scripts. | +| RELEASES | Infrastructure work to release software. | +| DATA | Creation of a data model (and its migration when EF is used) | +| MAJOR DATA | Changing existing data model with 5+ property additions/modifications. | +| MEDIUM DATA | Changing existing data model with 3-4 property additions/modifications. | +| MINOR DATA | Changing existing data model with 1-2 property additions/modifications. | +| MIGRATION | Moving or transforming data from one system to another, updating existing data, or generating reports. | +| MAJOR MIGRATION | Major data migration or transformation tasks involving significant data volume or complexity. | +| MEDIUM MIGRATION | Medium complexity data migration or updates involving moderate data volume or effort. | +| MINOR MIGRATION | Minor data migration or simple data updates with low complexity or small data volume. | +| BROKERS | When creating a broker to wrap external libraries, resources, services, or APIs | +| MAJOR BROKERS | Major changes to existing broker (significant functionality changes). | +| MEDIUM BROKERS | Medium changes to existing broker (multiple significant modifications). | +| MINOR BROKERS | Minor changes to existing broker (simple modifications). | +| FOUNDATIONS | When creating a Foundation Service | +| MAJOR FOUNDATIONS | Changing existing foundation service with writing or updating all or to 5+ tests. | +| MEDIUM FOUNDATIONS | Changing existing foundation service with writing or updating to 3-4 tests. | +| MINOR FOUNDATIONS | Changing existing foundation service with writing or updating to 1-2 tests. | +| PROCESSINGS | When creating a Processing Service | +| MAJOR PROCESSINGS | Changing existing processing service with writing or updating all or to 5+ tests. | +| MEDIUM PROCESSINGS | Changing existing processing service with writing or updating to 3-4 tests. | +| MINOR PROCESSINGS | Changing existing processing service with writing or updating to 1-2 tests. | +| ORCHESTRATIONS | When creating an Orchestration Service | +| MAJOR ORCHESTRATIONS | Changing existing orchestration service with writing or updating all or to 5+ tests. | +| MEDIUM ORCHESTRATIONS | Changing existing orchestration service with writing or updating to 3-4 tests. | +| MINOR ORCHESTRATIONS | Changing existing orchestration service with writing or updating to 1-2 tests. | +| COORDINATIONS | When creating a Coordination Service | +| MAJOR COORDINATIONS | Changing existing coordination service with writing or updating all or to 5+ tests. | +| MEDIUM COORDINATIONS | Changing existing coordination service with writing or updating to 3-4 tests. | +| MINOR COORDINATIONS | Changing existing coordination service with writing or updating to 1-2 tests. | +| MANAGEMENTS | When creating a Management Service | +| MAJOR MANAGEMENTS | Changing existing management service with writing or updating all or to 5+ tests. | +| MEDIUM MANAGEMENTS | Changing existing management service with writing or updating to 3-4 tests. | +| MINOR MANAGEMENTS | Changing existing management service with writing or updating to 1-2 tests. | +| AGGREGATIONS | When creating an Aggregation Service | +| MAJOR AGGREGATIONS | Changing existing aggregation service with writing or updating all or to 5+ tests. | +| MEDIUM AGGREGATIONS | Changing existing aggregation service with writing or updating to 3-4 tests. | +| MINOR AGGREGATIONS | Changing existing aggregation service with writing or updating to 1-2 tests. | +| CONTROLLERS | When creating a Controller | +| MAJOR CONTROLLERS | Changing existing controller with writing or updating all or to 5+ tests. | +| MEDIUM CONTROLLERS | Changing existing controller with writing or updating to 3-4 tests. | +| MINOR CONTROLLERS | Changing existing controller with writing or updating to 1-2 tests. | +| CLIENTS | When creating a client on a library that others can use | +| MAJOR CLIENTS | Changing existing client with writing or updating to 5+ tests. | +| MEDIUM CLIENTS | Changing existing client with writing or updating to 3-4 tests. | +| MINOR CLIENTS | Changing existing client with writing or updating to 1-2 tests. | +| PROVIDERS | When creating a provider on a SPAL provider library that others can use | +| EXPOSERS | When creating any other kind of exposer i.e. Program.cs | +| MAJOR EXPOSERS | | +| MEDIUM EXPOSERS | | +| MINOR EXPOSERS | | +| VIEWS | When creating a View Service | +| MAJOR VIEWS | Changing existing view with writing or updating all or to 5+ tests. | +| MEDIUM VIEWS | Changing existing view with writing or updating to 3-4 tests. | +| MINOR VIEWS | Changing existing view with writing or updating to 1-2 tests. | +| BASES | When creating a Frontend Base Component | +| MAJOR BASES | | +| MEDIUM BASES | | +| MINOR BASES | | +| COMPONENTS | When creating a Component | +| MAJOR COMPONENTS | Changing existing component with writing or updating all or to 5+ tests. | +| MEDIUM COMPONENTS | Changing existing component with writing or updating to 3-4 tests. | +| MINOR COMPONENTS | Changing existing component with writing or updating to 1-2 tests. | +| PAGES | When creating a Blazor Page | +| MAJOR PAGES | | +| MEDIUM PAGES | | +| MINOR PAGES | | +| ACCEPTANCE | When writing an Acceptance Test | +| MAJOR ACCEPTANCE | | +| MEDIUM ACCEPTANCE | | +| MINOR ACCEPTANCE | | +| INTEGRATION | When writing an Integration Test | +| MAJOR INTEGRATION | | +| MEDIUM INTEGRATION | | +| MINOR INTEGRATION | | +| CODE RUB | Small change to fix things styling, code formatting, spelling (Not for bug fixes) | +| MAJOR CODE RUB | Significant code cleanup affecting multiple files. | +| MEDIUM CODE RUB | Moderate code cleanup affecting several files. | +| MINOR CODE RUB | Minor code cleanup affecting few files. | +| MINOR FIX | A minor bug fix / change that do not significantly alter the overall functionality of the program. | +| MEDIUM FIX | A fix that has a moderate level of impact on the functionality or performance of a program or system. | +| MAJOR FIX | A major bug fix is a significant repair or correction made to a software program or system that addresses a critical or major issue. | +| DOCUMENTATION | General documentation | +| CONFIG | Any configuration changes i.e. setting up appsettings.json for your various environments. | +| REVIEW | Reviewing submitted work in the context of project standards, and Standard compliance. | +| STANDARD | When you update or introduce a new thing in The Standard | +| DESIGN | Creating documentation has design details for your architecture (High Level Design / Low Level Design) | +| MAJOR DESIGN | Designing 15+ service methods. | +| MEDIUM DESIGN | Designing 10-14 service methods. | +| MINOR DESIGN | Designing <10 service methods. | +| BUSINESS | Creating documentation that outlines your business processes / Standard Operating Procedures. | +| IMPORT | When you are copying code and tests over from another system with no or minor changes like namespaces. Should include the component being imported, format: IMPORT: [COMPONENT]: [Description] | +| STATUS | When updating STATUS information in your design documentation. | +| PLANNING | Planning should occur once per feature and involve collaboration across services. Individual work and planning for themselves doesn't quality as planning. Planning involves multiple engineers with a leader who assigns and distributes the tasks across the team. | +| MAJOR PLANNING | Planning 10+ tasks. | +| MEDIUM PLANNING | Planning 5+ tasks. | +| MINOR PLANNING | Planning <5 tasks. | +| MENTORSHIP | 60+ minute session. | +| MAJOR MENTORSHIP | 60+ minute session. | +| MEDIUM MENTORSHIP | 45 minute session. | +| MINOR MENTORSHIP | 30 minute session. | +| DISCUSSION | When conducting a discussion meeting under the DISCUSSION title, surrounding technical issues. | +| MAJOR DISCUSSION | 60+ minute discussion. | +| MEDIUM DISCUSSION | 45 minute discussion. | +| MINOR DISCUSSION | 30 minute discussion. | + +### 4.1.2 Projects + +Visual Studio utilizes a hierarchical project structure that allows developers to organize their code and resources in a logical and easy-to-navigate manner. +At the top level, you have a solution that is a container for one one more projetcs. +A project is the container for all of the files and resources that make up an application. + + +#### 4.1.2.1 Projects In The Solution + +A typical solution structure would look like this... + +``` + |-- Taarafo.Core (API) + |-- Taarafo.Core.Infrastructure (Console App) + |-- Taarafo.Core.Tests.Acceptance (xUnit Test Project) + |-- Taarafo.Core.Tests.Unit (xUnit Test Project) +``` + +#### 4.1.2.1 Project Structure + +A typical procect folder structure would look like this... + +``` +Taarafo.Core + |-- Brokers + |-- DateTimes + |-- Loggings + |-- Storages + |-- Models + |-- Foundations + |-- Posts + |-- Exceptions + |-- Comments + |-- Exceptions + |-- Migrations + |-- Services + |-- Foundations + |-- Students + |-- Processings + |-- Students + |-- Orchestrations + |-- Students + |-- Controllers +``` + +The above folder structure can also be carried over into the test projects to organsise models +and to group the various tests by Services\\`[Service Type]`\\`[ServiceName]` + + +### 4.1.3 Commits + +The Standard follows a TDD (Test-Driven Development) software development methodology that emphasizes writing tests before writing the actual code. +It is a process of writing a test case, running it and observing it failing, then writing the code to pass the test. +It's an iterative process that helps developers to focus on the requirements, design, and implementation of the code. +When using TDD, developers write test cases using a testing framework such as xUnit or NUnit, and use the framework to run and assert the tests. +The goal is to have a high percentage of test coverage, ensuring that the code is thoroughly tested and any bugs or issues are caught early on in the development process. +This approach helps to ensure that the code is maintainable, easy to understand, and that it meets the requirements of the project. + +When following this TDD process of writing a test case we can use the test name and its outcome to commit the work. +The work done to write the test and observing it failing can be committed as `[Test Name] -> FAIL` e.g. `ShouldAddPostAsync -> FAIL` +and subsequently the work done to make it pass can be committed as `[Test Name] -> PASS` e.g. `ShouldAddPostAsync -> PASS` + +If you are working on any code that does not require testing i.e. DATA, BROKERS, CONTROLLERS, then we would adopt the same naming convention as used for Pull Requests +which uses the category and a description of the work done using this syntax `[CATEGORY]: [Description Of Work Completed]`, where the category is always in CAPS and the description in Pascal Case +e.g. `DATA: Add Student Model` OR `BROKERS: Insert Student` OR `CONTROLLERS: POST Student` (See the category table above for a complete list of categories) + +### 4.1.4 Pull Requests + +When developers have completed their work, they can create a pull request with the main repository, which lets the project maintainer know that an update is ready to be integrated. +The Pull Request (PR) can be created with the name using this syntax `[CATEGORY]: [Description Of Work Completed]`, where the category is always in CAPS and the description in Pascal Case +e.g. `FOUNDATIONS: Add Student` + +The comment section of the PR can also be completed with any additional information that describe the work done and that can be helpfull / relevant to the approver. +e.g. a screenhots of the CONTROLLER actions working as expected showing the various response outcomes like `201 - Created` or `400 - Bad Request` or `424 - Failed Dependency` etc. + +You can also link a pull request to an issue to show that a fix is in progress and to automatically close the issue when the pull request or branch is merged by adding a KEYWORD #ISSUE-NUMBER anywhere in the comment section. +e.g. `Closes #10` or if the PR cover multiple things, then you could do `Closes #10, closes #123` + +If you simply wish to link an issue without a closing action on merge, you can omit the keyword and just add #ISSUE-NUMBER e.g. `#10` + + +### 4.1.5 Configurations + +Configuration settings are values that determine the behavior of a software application. +They can include anything from database connection strings and API keys to feature flags and logging levels. +By using configuration settings, developers can customize the behavior of their application without modifying the code, +making it easier to deploy and manage different environments. + +One popular way of storing configuration settings in .NET applications is by using the `appsettings.json` file. +This file is a JSON-formatted file that contains a set of key-value pairs representing configuration settings. +Developers can easily modify these settings to change the behavior of their application. + +However, not all configuration settings can be safely stored in `appsettings.json` files. +Sensitive data, such as API keys, database passwords, or any other data that could grant +unauthorized access to the application or its resources, should be kept secret. +Storing sensitive data in configuration files, especially in plain text format, can put the application's security at risk. + +It is very important for developers to follow secure coding practices and never store passwords +or other sensitive data in configuration provider code or in plain text configuration files. +Such sensitive data should be stored in a secure location like Azure Key Vault or in environment +variables that are set in a deployment environment. Storing passwords in local settings files is also +not recommended as this can easily lead to scenarios where passwords are accidentally checked into code repositories, +either because a developer has forgotten to exclude the file from version control or as a result of someone changing +it and then inadvertently including the sensitive files, putting the application's security at risk. + +One way of protecting sensitive data is by using **user secrets**. User secrets is a feature in .NET that provides a +convenient way for developers to store and retrieve sensitive data during development. +This data is stored locally on the developer's machine and is not intended to be used in production. +While user secrets can be useful for keeping sensitive data out of source control and easily accessible during development, +it is important to note that they are not compatible with GitHub workflow actions. +Instead, developers should consider using more secure methods for storing secrets, +such as environment variables or storing them in a secure location like `GitHub Action Secrets` or `Azure Key Vault`. +As user secrets is not directly compatible with GitHub build pipelines we will not explore this option further. + +Another way of protecting sensitive data is the use of **environment variables**. +Environment variables are variables that are set in the operating system, which can be accessed by applications at runtime. + +By using both **appsettings.json** and **environment variables**, +developers can separate sensitive data from other configuration settings, +making it easier to manage and secure. Additionally, +this approach enables developers to store their configuration settings securely in a build pipeline, +ensuring that sensitive data is protected throughout the development lifecycle. + +#### 4.1.5.1 appsettings.json + +A .NET app will be automatically load and register `appsettings.json` and `appsettings.{Environment}.json` with the IConfiguration +interface during application startup, if the files is located in the application's root directory. +This means that the key-value pairs defined in the appsettings.json file will be accessible through the IConfiguration object, +allowing developers to easily access and use the configuration settings throughout their application. + +It's worth noting that in addition to the appsettings.json file, there are other configuration providers available in +.NET that allow developers to load configuration settings from various sources, +such as environment variables or command-line arguments. +(Developers can also create their own custom configuration providers to load configuration settings from other sources if needed.) + +Configuration sources are read in the order that their configuration providers are specified. +Order configuration providers in code to suit the priorities for the underlying configuration sources that the app requires. + +A typical sequence of configuration providers is: + +1. appsettings.json +2. appsettings.\{Environment\}.json +3. User secrets +4. Environment variables using the Environment Variables configuration provider. +5. Command-line arguments using the Command-line configuration provider. + +Lets look at some code samples to see how all this works. + +#### 4.1.5.1 Application Settings + +**appsettings.json** +```cs +{ + "MySettings": { + "ApiUrl": "https://api.somesite.com/", + "ApiKey": "1ae4e397-ec3c-4ed7-8280-a17d0e2cbe78", + "OrganisationId": "1bac6df0-cd68-4ce5-9c29-b2beb58201cd" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} +``` + +With the above `appsettings.json` file we can then load our variables like this: +```cs + string apiUrl = configuration["MySettings:ApiUrl"]; + string apiKey = configuration["MySettings:ApiKey"]; + string organisationId = configuration["MySettings:OrganisationId"]; +``` + +#### 4.1.5.2 Environment Variables + +We can add environment variables to a web application by doing this: +```cs +var builder = WebApplication.CreateBuilder(args); +... +builder.Configuration.AddEnvironmentVariables(); +... +var app = builder.Build(); +``` + +or if you want to play with this in a Console App or Unit Test, you can do this: + +```cs +var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables("MYAPP_ACCEPTANCE_"); + +this.configuration = configurationBuilder.Build(); +``` +**NOTE** that we have defined a prefix that environment variables must start with. +This prefix will automatically be removed from the environment variabl names by the configuration builder. +This is a very useful feature as you can now store environment variables with the same name for multiple apps and/or environments. + +Up to this point we will still get the same values if we get our config items as we have not set the values for any environment variables yet. + +In C# .NET, working with environment variables is straightforward. +The Environment class provides several methods for reading and setting environment variables. +For example, to read an environment variable, you can use the `Environment.GetEnvironmentVariable` method, +passing in the name of the variable you want to retrieve. +To set an environment variable, you can use the `Environment.SetEnvironmentVariable` method, providing the name of the variable and its value. +You can also use the command line to set the enviroment variables by running the SETX command. + +For demonstration we will leave the value of `MySettings:ApiUrl` in appsettings.json + +Next we will set the value of `MySettings:ApiKey` through this line of code +```cs +Environment.SetEnvironmentVariable("MYSETTINGS:APIKEY", "3d4a1c55-fcd7-4b34-8536-99c8ae6ae33c"); +``` + +and for the `MySettings:OrganisationId` we will use the command line argument through a console window with administrative priviledges +```cs +setx MYSETTINGS:APIKEY "b2440ae9-cad2-4d70-b138-4a807abe1bb7" +``` +**NOTE** +Visual Studio preloads the environment variables when it starts, and it caches them until the application is closed. +Unlike environment variables set through code, those set using the command line will not be immediately be available due to the preload behaviour. +**You will need to close Visual Studio and re-open it.** + + +Once you have reloaded Visual Studio we can get the values from the configuration again by doing this: +```cs + string apiUrl = configuration["MySettings:ApiUrl"]; + string apiKey = configuration["MySettings:ApiKey"]; + string organisationId = configuration["MySettings:OrganisationId"]; +``` + +You will notice that the ApiUrl is the same as before, but the values have now changed for `MySettings:ApiKey` and `MySettings:OrganisationId` and they are now equal to the values we specified for the environment variables via code and through SETX on the command line. + +You can also remove environment variables through code by setting the value to `null`. +Environment.SetEnvironmentVariable("MYSETTINGS:APIKEY", null); + +OR via the command line you can do +```cs +setx MYSETTINGS:APIKEY /delete +``` + +Or if you prefer a GUI, you can also go to SYSTEM PROPERTES > ENVIRONMENT VARIABLES where you can ADD / REMOVE environment variables. + +#### 4.1.5.3 GitHub Actions Workflow + +To setup your environment variables for use within GitHub we will need to setup repository secrets and add a section at the beginning of your workflow file. + +1. Go to the GitHub repository where you want to use these settings and click on "Settings" in the top navigation bar. + +2. In the left sidebar, click on "Secrets" and then click on "New repository secret" button. + +3. Create three new secrets named "MYAPP_ACCEPTANCE_MYSETTINGS__APIKEY", and "MYAPP_ACCEPTANCE_MYSETTINGS__ORGANISATIONID", +respectively, with the corresponding values for "ApiUrl", "ApiKey", and "OrganisationId" from the given settings. + + **NOTE** + * We have prefixed our environment variables with `MYAPP_ACCEPTANCE_` to match the earlier configuration of `.AddEnvironmentVariables("MYAPP_ACCEPTANCE_");`. + * The other difference is that a colon (:) is not allowed on GitHub Secrets to represent a hierarchy and you have to use a double underscore (__) instead. + +4. To expose these secrets as environment variables in the GitHub Action Workflow, add the following code snippet at the beginning of your workflow file: + +```yaml +env: + API_URL: ${{ secrets.MySettings__ApiUrl }} + API_KEY: ${{ secrets.MySettings__ApiKey }} + ORG_ID: ${{ secrets.MySettings__OrganisationId } +``` + +A full example of the `dotnet.yml` file will look like this: +```yaml +name: MyApplication Build +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + build: + runs-on: windows-latest + env: + ApiKey: ${{ secrets.MYAPP_ACCEPTANCE_MYSETTINGS__APIKEY }} + OrgId: ${{ secrets.MYAPP_ACCEPTANCE_MYSETTINGS__ORGANISATIONID }} + steps: + - name: Pulling Code + uses: actions/checkout@v2 + - name: Installing .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.201 + include-prerelease: false + - name: Restoring Packages + run: dotnet restore + - name: Building Solution + run: dotnet build --no-restore + - name: Running Tests + run: dotnet test --no-build --verbosity normal +``` +## 4.2 Measurements +The Standard promotes ideals of fairness to all. The Standard recognizes that innovative ideas, great contributions and high performance is not exclusive to individuals with certain titles. In order to truly and fairly ensure everyone's contributions are measured fairly, The Standard introduces values and points to contributions at different levels and categories. The acculmalations of these contributions increases the rank of the contributor from one level to the next. + +### 4.2.0 Contribution Points +The following is a list of all types of contributions and the value for each: + +| Title Starts With | Return Value | +|----------------------------|--------------| +| INFRA | 10 | +| MAJOR INFRA | 10 | +| MEDIUM INFRA | 3 | +| MINOR INFRA | 1 | +| DATA | 5 | +| MAJOR DATA | 5 | +| MEDIUM DATA | 3 | +| MINOR DATA | 1 | +| MIGRATION | 20 | +| MAJOR MIGRATION | 20 | +| MEDIUM MIGRATION | 10 | +| MINOR MIGRATION | 5 | +| BROKER | 5 | +| MAJOR BROKER | 5 | +| MEDIUM BROKER | 3 | +| MINOR BROKER | 1 | +| FOUNDATION | 10 | +| MAJOR FOUNDATION | 10 | +| MEDIUM FOUNDATION | 5 | +| MINOR FOUNDATION | 1 | +| PROCESSING | 15 | +| MAJOR PROCESSING | 15 | +| MEDIUM PROCESSING | 10 | +| MINOR PROCESSING | 5 | +| ORCHESTRATION | 20 | +| MAJOR ORCHESTRATION | 20 | +| MEDIUM ORCHESTRATION | 15 | +| MINOR ORCHESTRATION | 10 | +| COORDINATION | 20 | +| MAJOR COORDINATION | 20 | +| MEDIUM COORDINATION | 15 | +| MINOR COORDINATION | 5 | +| MANAGEMENT | 20 | +| MAJOR MANAGEMENT | 20 | +| MEDIUM MANAGEMENT | 15 | +| MINOR MANAGEMENT | 5 | +| AGGREGATION | 10 | +| MAJOR AGGREGATION | 10 | +| MEDIUM AGGREGATION | 5 | +| MINOR AGGREGATION | 1 | +| CONTROLLER | 5 | +| MAJOR CONTROLLER | 5 | +| MEDIUM CONTROLLER | 3 | +| MINOR CONTROLLER | 1 | +| ACCEPTANCE | 10 | +| MAJOR ACCEPTANCE | 10 | +| MEDIUM ACCEPTANCE | 5 | +| MINOR ACCEPTANCE | 3 | +| INTEGRATION | 15 | +| MAJOR INTEGRATION | 15 | +| MEDIUM INTEGRATION | 10 | +| MINOR INTEGRATION | 10 | +| VIEW | 10 | +| MAJOR VIEW | 10 | +| MEDIUM VIEW | 5 | +| MINOR VIEW | 1 | +| BASE | 5 | +| MAJOR BASE | 5 | +| MEDIUM BASE | 3 | +| MINOR BASE | 1 | +| COMPONENT | 15 | +| MAJOR COMPONENT | 15 | +| MEDIUM COMPONENT | 10 | +| MINOR COMPONENT | 5 | +| PAGE | 5 | +| MAJOR PAGE | 5 | +| MEDIUM PAGE | 3 | +| MINOR PAGE | 1 | +| CLIENT | 5 | +| MAJOR CLIENT | 5 | +| MEDIUM CLIENT | 3 | +| MINOR CLIENT | 1 | +| EXPOSER | 5 | +| MAJOR EXPOSER | 5 | +| MEDIUM EXPOSER | 3 | +| MINOR EXPOSER | 1 | +| DESIGN | 20 | +| MAJOR DESIGN | 20 | +| MEDIUM DESIGN | 10 | +| MINOR DESIGN | 5 | +| MAJOR FIX | 20 | +| MEDIUM FIX | 10 | +| MINOR FIX | 5 | +| PLANNING | 15 | +| MAJOR PLANNING | 15 | +| MINOR PLANNING | 10 | +| MINIOR PLANNING | 5 | +| MENTORSHIP | 20 | +| MAJOR MENTORSHIP | 20 | +| MINOR MENTORSHIP | 15 | +| MINIOR MENTORSHIP | 10 | +| DISCUSSION | 15 | +| MAJOR DISCUSSION | 15 | +| MINOR DISCUSSION | 10 | +| MINIOR DISCUSSION | 5 | +| MAJOR CODE RUB | 10 | +| MEDIUM CODE RUB | 5 | +| MINOR CODE RUB | 1 | +| CODE RUB | 1 | +| PROVISION | 10 | +| IMPORT | 5 | +| RELEASE | 10 | +| CONFIG | 5 | +| REVIEW | 1 | +| DOCUMENTATION | 1 | +| STANDARD | 100 | +| BUSINES | 50 | +| STATUS | 1 | +| UNKNOWN | 0 | + +Each one of these categories have been defined above in the previous section. + +### 4.2.1 Average Implementation Time + +The following table provides the **average estimated time** required to implement different layers or components in The Standard architecture. +These times are based on practical experience and are used as a general reference for planning and effort estimation. + +| Layer / Component | Average Time | +|-------------------|--------------| +| Brokers, Base | ~1 hour | +| Foundations, Components | ~3 hours | +| Processing, Orchestration, Coordination, Orchestration Components | ~5 hours | +| Management, Uber Manager | ~3 hours | +| Controllers, Pages, Clients | ~1 hour | + +These estimates represent typical implementation durations for a single service or component, assuming standard complexity and no external dependencies. + +### 4.2.2 Ranks +The following are the ranks based on the life-time accumalation and contribution to The Standard Community and Standard-Compliant projects: + +| POSITION | SCORE | +|---------------------|-----------------| +| SWE I | 10,000 | +| SWE II | 25,000 | +| SR. SWE | 50,000 | +| PRINCIPAL | 100,000 | +| PARTNER/DIRECTOR | 300,000 | +| VP | 500,000 | +| DISTINGUISHED | 1,000,000 | +| THE STANDARD BEARER | 1M + DELEGATION | + +## Implementation-profile practices addendum + +## 12. Practices + +Having defined practices for code contributions, branch, commit strategies and project structures is essential for any software development project. +These practices provide a clear and consistent approach to managing the codebase, ensuring that contributions are made in a controlled and organized manner. +This helps to avoid confusion and conflicts, and ensures that the codebase remains stable and maintainable. +Additionally, well-defined practices for code contributions and project structure can help to improve collaboration and communication within the development team, making it easier to track and review changes, and to identify and resolve issues. +Overall, defined practices for code contributions, branching and commit strategies as well as project structures are a key part of any successful software development project. + +Practices requires these main aspects to be considered: + +4. Code Contribution + 1. Forking / Cloning and Branching + 2. Project Structure + 3. Commits + 4. Pull Requests + +The Standard Team is to clearly understand and define each and every aspect of the aforementioned points. + +### 12.1 Code Contribution + +Defining a contribution guideline is crucial for any open-source or collaborative software development project. +It establishes clear rules and expectations for how contributors should submit and review changes, which helps to ensure that all contributions are made in a consistent and organized manner. +Additionally, a contribute guideline can provide guidance on best practices for coding, testing and documentation, which can help to improve the overall quality of the codebase. +It also serves as an educational tool to new contributors, providing them with the necessary information and instructions on how to contribute to the project effectively. +(Furthermore, having a well-defined GitHub contribute guideline can help to attract new contributors and maintain a healthy open-source community.) + +#### 12.1.1 Forking and Branching Strategies +A branch strategy for code is essential for maintaining a stable and maintainable codebase. +It allows for multiple developers to work on different features or bug fixes simultaneously, without interfering with each other's work. +Each developer can create their own branch to work on, which can then be merged into the main branch once it has been reviewed and approved. +This helps to avoid conflicts, and ensures that the codebase remains stable and consistent. +Furthermore, a branch strategy enables version control and allows different versions of the codebase to be created and tracked. +This can be useful for rolling back changes if necessary, and for maintaining multiple versions of the software for different environments. +Overall, a branch strategy is a critical aspect of any software development project and is essential for ensuring that the codebase remains stable and maintainable. + +There are several different types of branching strategies that can be used in software development, including: + +1. Gitflow: A popular branching strategy that follows a strict branching model, where development happens in feature branches and is merged into a main development branch. This strategy is good for large projects with many contributors. + +2. Trunk-Based Development: This strategy involves working on the main branch, or trunk, and committing changes directly to it. This is a good strategy for smaller projects with a small number of contributors. + +3. Feature Branching: This strategy involves creating a separate branch for each feature or bug fix, and merging it into the main branch when it is complete. This is a good strategy for larger projects with multiple contributors. + +4. Forking: This strategy involves creating a copy of the repository and working on it separately. This is good for open-source projects where multiple contributors are working on the same codebase. The main advantage of the Forking workflow is that contributions can be integrated without the need for everybody to push to a single central repository. Developers push to their own server-side repositories, and only the project maintainer can push to the official repository. This allows the maintainer to accept commits from any developer without giving them write access to the official codebase. + +5. Release Branching: This strategy involves creating a separate branch for each release, and merging it into the main branch when it is ready to be released. This is a good strategy for projects that have a regular release schedule. + +6. Continuous Integration: This strategy involves merging code changes as soon as they are made, and running automated tests to ensure that the codebase remains stable. This is a good strategy for projects that have a high frequency of changes. + +Ultimately, it's important to choose a branching strategy that fits the needs and constraints of the project, and that can be easily understood and followed by all the contributors. + +##### 12.1.1.1 Open Source Development +As The Standard promotes open-source work, lets look have a quick look at the Forking strategy. + +**How it works** + +The Forking workflow begins with an official public repository stored on a server. When a new developer wants to start working on the project, they do not directly clone the official repository. + +Instead, they fork the official repository to create a copy of it. This new copy serves as their personal public repository. +After they have created their server-side copy, the developer performs a git clone to get a copy of it onto their local machine. +This serves as their private development environment, just like in the other workflows. + +When they're ready to publish a local commit, they push the commit to their own public repository (not the official one). +They then file a pull request with the main repository, which lets the project maintainer know that an update is ready to be integrated. +The pull request can then be reviewed and it also serves as a convenient discussion thread if there are issues with the contributed code. + +The following is a step-by-step example of this workflow: + +1. A developer 'forks' an 'official' server-side repository. This creates their own server-side copy. +2. The new server-side copy is cloned to their local system. +3. A new local feature branch is created. +4. The developer makes changes on the new branch. +5. New commits are created for the changes. +6. The branch gets pushed to the developer's own server-side copy. +7. The developer opens a pull request from the new branch to the 'official' repository. +8. The pull request gets reviewed and build success and tests are verified. At this point the maintainer can either request changes or approve for merge upon which the changes are then merged into the original server-side repository. + + +##### 12.1.1.2 Branch Name Conventions + +For branch names we can apply the following naming convention: `users/[username]/[category]-[entity]-[action]` + +The variables can be subsituted +* `[username]` => your github username +* `[category]` => the category that you are working on +* `[entity]` => the entity that your service / broker uses +* `[action]` => the action + +#### 12.1.1.3 Category List + +| Category | Description | +|------------------------|-------------| +| INFRA | Initial project setup, creating the build project / build scripts. | +| MAJOR INFRA | Major updates, removing or adding simple configurations or files. | +| MEDIUM INFRA | Medium updates, removing or adding simple configurations or files. | +| MINOR INFRA | Minor updates, removing or adding simple configurations or files. | +| PROVISIONS | Creating provision project / scripts. | +| RELEASES | Infrastructure work to release software. | +| DATA | Creation of a data model (and its migration when EF is used) | +| MAJOR DATA | Changing existing data model with 5+ property additions/modifications. | +| MEDIUM DATA | Changing existing data model with 3-4 property additions/modifications. | +| MINOR DATA | Changing existing data model with 1-2 property additions/modifications. | +| MIGRATION | Moving or transforming data from one system to another, updating existing data, or generating reports. | +| MAJOR MIGRATION | Major data migration or transformation tasks involving significant data volume or complexity. | +| MEDIUM MIGRATION | Medium complexity data migration or updates involving moderate data volume or effort. | +| MINOR MIGRATION | Minor data migration or simple data updates with low complexity or small data volume. | +| BROKERS | When creating a broker to wrap external libraries, resources, services, or APIs | +| MAJOR BROKERS | Major changes to existing broker (significant functionality changes). | +| MEDIUM BROKERS | Medium changes to existing broker (multiple significant modifications). | +| MINOR BROKERS | Minor changes to existing broker (simple modifications). | +| FOUNDATIONS | When creating a Foundation Service | +| MAJOR FOUNDATIONS | Changing existing foundation service with writing or updating all or to 5+ tests. | +| MEDIUM FOUNDATIONS | Changing existing foundation service with writing or updating to 3-4 tests. | +| MINOR FOUNDATIONS | Changing existing foundation service with writing or updating to 1-2 tests. | +| PROCESSINGS | When creating a Processing Service | +| MAJOR PROCESSINGS | Changing existing processing service with writing or updating all or to 5+ tests. | +| MEDIUM PROCESSINGS | Changing existing processing service with writing or updating to 3-4 tests. | +| MINOR PROCESSINGS | Changing existing processing service with writing or updating to 1-2 tests. | +| ORCHESTRATIONS | When creating an Orchestration Service | +| MAJOR ORCHESTRATIONS | Changing existing orchestration service with writing or updating all or to 5+ tests. | +| MEDIUM ORCHESTRATIONS | Changing existing orchestration service with writing or updating to 3-4 tests. | +| MINOR ORCHESTRATIONS | Changing existing orchestration service with writing or updating to 1-2 tests. | +| COORDINATIONS | When creating a Coordination Service | +| MAJOR COORDINATIONS | Changing existing coordination service with writing or updating all or to 5+ tests. | +| MEDIUM COORDINATIONS | Changing existing coordination service with writing or updating to 3-4 tests. | +| MINOR COORDINATIONS | Changing existing coordination service with writing or updating to 1-2 tests. | +| MANAGEMENTS | When creating a Management Service | +| MAJOR MANAGEMENTS | Changing existing management service with writing or updating all or to 5+ tests. | +| MEDIUM MANAGEMENTS | Changing existing management service with writing or updating to 3-4 tests. | +| MINOR MANAGEMENTS | Changing existing management service with writing or updating to 1-2 tests. | +| AGGREGATIONS | When creating an Aggregation Service | +| MAJOR AGGREGATIONS | Changing existing aggregation service with writing or updating all or to 5+ tests. | +| MEDIUM AGGREGATIONS | Changing existing aggregation service with writing or updating to 3-4 tests. | +| MINOR AGGREGATIONS | Changing existing aggregation service with writing or updating to 1-2 tests. | +| CONTROLLERS | When creating a Controller | +| MAJOR CONTROLLERS | Changing existing controller with writing or updating all or to 5+ tests. | +| MEDIUM CONTROLLERS | Changing existing controller with writing or updating to 3-4 tests. | +| MINOR CONTROLLERS | Changing existing controller with writing or updating to 1-2 tests. | +| CLIENTS | When creating a client on a library that others can use | +| MAJOR CLIENTS | Changing existing client with writing or updating to 5+ tests. | +| MEDIUM CLIENTS | Changing existing client with writing or updating to 3-4 tests. | +| MINOR CLIENTS | Changing existing client with writing or updating to 1-2 tests. | +| PROVIDERS | When creating a provider on a SPAL provider library that others can use | +| EXPOSERS | When creating any other kind of exposer i.e. Program.cs | +| MAJOR EXPOSERS | | +| MEDIUM EXPOSERS | | +| MINOR EXPOSERS | | +| VIEWS | When creating a View Service | +| MAJOR VIEWS | Changing existing view with writing or updating all or to 5+ tests. | +| MEDIUM VIEWS | Changing existing view with writing or updating to 3-4 tests. | +| MINOR VIEWS | Changing existing view with writing or updating to 1-2 tests. | +| BASES | When creating a Frontend Base Component | +| MAJOR BASES | | +| MEDIUM BASES | | +| MINOR BASES | | +| COMPONENTS | When creating a Component | +| MAJOR COMPONENTS | Changing existing component with writing or updating all or to 5+ tests. | +| MEDIUM COMPONENTS | Changing existing component with writing or updating to 3-4 tests. | +| MINOR COMPONENTS | Changing existing component with writing or updating to 1-2 tests. | +| PAGES | When creating a Blazor Page | +| MAJOR PAGES | | +| MEDIUM PAGES | | +| MINOR PAGES | | +| ACCEPTANCE | When writing an Acceptance Test | +| MAJOR ACCEPTANCE | | +| MEDIUM ACCEPTANCE | | +| MINOR ACCEPTANCE | | +| INTEGRATION | When writing an Integration Test | +| MAJOR INTEGRATION | | +| MEDIUM INTEGRATION | | +| MINOR INTEGRATION | | +| CODE RUB | Small change to fix things styling, code formatting, spelling (Not for bug fixes) | +| MAJOR CODE RUB | Significant code cleanup affecting multiple files. | +| MEDIUM CODE RUB | Moderate code cleanup affecting several files. | +| MINOR CODE RUB | Minor code cleanup affecting few files. | +| MINOR FIX | A minor bug fix / change that do not significantly alter the overall functionality of the program. | +| MEDIUM FIX | A fix that has a moderate level of impact on the functionality or performance of a program or system. | +| MAJOR FIX | A major bug fix is a significant repair or correction made to a software program or system that addresses a critical or major issue. | +| DOCUMENTATION | General documentation | +| CONFIG | Any configuration changes i.e. setting up appsettings.json for your various environments. | +| REVIEW | Reviewing submitted work in the context of project standards, and Standard compliance. | +| STANDARD | When you update or introduce a new thing in The Standard | +| DESIGN | Creating documentation has design details for your architecture (High Level Design / Low Level Design) | +| MAJOR DESIGN | Designing 15+ service methods. | +| MEDIUM DESIGN | Designing 10-14 service methods. | +| MINOR DESIGN | Designing <10 service methods. | +| BUSINESS | Creating documentation that outlines your business processes / Standard Operating Procedures. | +| IMPORT | When you are copying code and tests over from another system with no or minor changes like namespaces. Should include the component being imported, format: IMPORT: [COMPONENT]: [Description] | +| STATUS | When updating STATUS information in your design documentation. | +| PLANNING | Planning should occur once per feature and involve collaboration across services. Individual work and planning for themselves doesn't quality as planning. Planning involves multiple engineers with a leader who assigns and distributes the tasks across the team. | +| MAJOR PLANNING | Planning 10+ tasks. | +| MEDIUM PLANNING | Planning 5+ tasks. | +| MINOR PLANNING | Planning <5 tasks. | +| MENTORSHIP | 60+ minute session. | +| MAJOR MENTORSHIP | 60+ minute session. | +| MEDIUM MENTORSHIP | 45 minute session. | +| MINOR MENTORSHIP | 30 minute session. | +| DISCUSSION | When conducting a discussion meeting under the DISCUSSION title, surrounding technical issues. | +| MAJOR DISCUSSION | 60+ minute discussion. | +| MEDIUM DISCUSSION | 45 minute discussion. | +| MINOR DISCUSSION | 30 minute discussion. | + +#### 12.1.2 Projects + +Visual Studio utilizes a hierarchical project structure that allows developers to organize their code and resources in a logical and easy-to-navigate manner. +At the top level, you have a solution that is a container for one one more projetcs. +A project is the container for all of the files and resources that make up an application. + + +##### 12.1.2.1 Projects In The Solution + +A typical solution structure would look like this... + + +``` + |-- Taarafo.Core (API) + |-- Taarafo.Core.Infrastructure (Console App) + |-- Taarafo.Core.Tests.Acceptance (xUnit Test Project) + |-- Taarafo.Core.Tests.Unit (xUnit Test Project) + +``` + +##### 12.1.2.1 Project Structure + +A typical procect folder structure would look like this... + + +``` +Taarafo.Core + |-- Brokers + |-- DateTimes + |-- Loggings + |-- Storages + |-- Models + |-- Foundations + |-- Posts + |-- Exceptions + |-- Comments + |-- Exceptions + |-- Migrations + |-- Services + |-- Foundations + |-- Students + |-- Processings + |-- Students + |-- Orchestrations + |-- Students + |-- Controllers + +``` + +The above folder structure can also be carried over into the test projects to organsise models +and to group the various tests by Services\\`[Service Type]`\\`[ServiceName]` + + +#### 12.1.3 Commits + +The Standard follows a TDD (Test-Driven Development) software development methodology that emphasizes writing tests before writing the actual code. +It is a process of writing a test case, running it and observing it failing, then writing the code to pass the test. +It's an iterative process that helps developers to focus on the requirements, design, and implementation of the code. +When using TDD, developers write test cases using a testing framework such as xUnit or NUnit, and use the framework to run and assert the tests. +The goal is to have a high percentage of test coverage, ensuring that the code is thoroughly tested and any bugs or issues are caught early on in the development process. +This approach helps to ensure that the code is maintainable, easy to understand, and that it meets the requirements of the project. + +When following this TDD process of writing a test case we can use the test name and its outcome to commit the work. +The work done to write the test and observing it failing can be committed as `[Test Name] -> FAIL` e.g. `ShouldAddPostAsync -> FAIL` +and subsequently the work done to make it pass can be committed as `[Test Name] -> PASS` e.g. `ShouldAddPostAsync -> PASS` + +##### 12.1.3.1 FAIL/PASS Commit Discipline + +Each TDD cycle produces exactly **two** commits: + +1. **FAIL commit** — contains the new test (and any new exception model files needed for compilation), but **no** implementation code that would make the test pass. +2. **PASS commit** — contains only the implementation code (e.g., catch block, helper method, validation logic) that makes the previously failing test pass. + +> **Critical Rule:** Before creating a FAIL commit, the test **must** be executed by the test runner and **verified as actually failing**. A test that does not run, does not compile, or passes is **not** a valid FAIL. Similarly, before creating a PASS commit, **all** tests must be executed and **verified as passing** — not just the new one. + +The TDD cycle is: + +``` +1. Write the test → build → run test → verify FAIL → commit "[TestName] -> FAIL" +2. Write the minimum implementation → build → run ALL tests → verify PASS → commit "[TestName] -> PASS" +3. Repeat for the next test +``` + +##### 12.1.3.2 What Goes in Each Commit + +| Commit Type | Includes | Does NOT Include | +|-------------|----------|------------------| +| FAIL | New test method, new exception model classes needed for the test to compile, new `using` directives | Implementation in the service (catch blocks, validation logic, helper methods) | +| PASS | Implementation code in the service (catch blocks, helper methods, validation logic) | New tests (those belong in the next FAIL commit) | + +##### 12.1.3.3 TDD Commit Order for a Foundation Service + +The following is the exact order of FAIL/PASS commit pairs for implementing an `Add{Entity}Async` method: + +``` + 1. ShouldAdd{Entity}Async -> FAIL + 2. ShouldAdd{Entity}Async -> PASS + 3. ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync -> FAIL + 4. ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync -> PASS + 5. ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync -> FAIL + 6. ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync -> PASS + 7. ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync -> FAIL + 8. ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync -> PASS + 9. ShouldThrowDependencyValidationExceptionOnAddIfConflictErrorOccursAndLogItAsync -> FAIL +10. ShouldThrowDependencyValidationExceptionOnAddIfConflictErrorOccursAndLogItAsync -> PASS +11. ShouldThrowCriticalDependencyExceptionOnAddIfUnauthorizedErrorOccursAndLogItAsync -> FAIL +12. ShouldThrowCriticalDependencyExceptionOnAddIfUnauthorizedErrorOccursAndLogItAsync -> PASS +13. ShouldThrowCriticalDependencyExceptionOnAddIfForbiddenErrorOccursAndLogItAsync -> FAIL +14. ShouldThrowCriticalDependencyExceptionOnAddIfForbiddenErrorOccursAndLogItAsync -> PASS +15. ShouldThrowCriticalDependencyExceptionOnAddIfNotFoundErrorOccursAndLogItAsync -> FAIL +16. ShouldThrowCriticalDependencyExceptionOnAddIfNotFoundErrorOccursAndLogItAsync -> PASS +17. ShouldThrowCriticalDependencyExceptionOnAddIfUrlNotFoundErrorOccursAndLogItAsync -> FAIL +18. ShouldThrowCriticalDependencyExceptionOnAddIfUrlNotFoundErrorOccursAndLogItAsync -> PASS +19. ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccursAndLogItAsync -> FAIL +20. ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccursAndLogItAsync -> PASS +21. ShouldThrowDependencyExceptionOnAddIfServiceUnavailableErrorOccursAndLogItAsync -> FAIL +22. ShouldThrowDependencyExceptionOnAddIfServiceUnavailableErrorOccursAndLogItAsync -> PASS +23. ShouldThrowCriticalDependencyExceptionOnAddIfHttpRequestErrorOccursAndLogItAsync -> FAIL +24. ShouldThrowCriticalDependencyExceptionOnAddIfHttpRequestErrorOccursAndLogItAsync -> PASS +25. ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync -> FAIL +26. ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync -> PASS +``` + +After all tests pass, a final configuration commit registers the service in `Program.cs`: + +``` +27. CONFIGURATIONS: Register I{Entity}Service in Program.cs +``` + +For storage-based services, commits 7–24 are replaced with the corresponding SQL/EF exception tests +(`DuplicateKeyException`, `DbUpdateException`, `SqlException`, etc.) following the same FAIL/PASS pattern. + +##### 12.1.3.4 Non-Test Commits + +If you are working on any code that does not require testing i.e. DATA, BROKERS, CONTROLLERS, then we would adopt the same naming convention as used for Pull Requests +which uses the category and a description of the work done using this syntax `[CATEGORY]: [Description Of Work Completed]`, where the category is always in CAPS and the description in Pascal Case +e.g. `DATA: Add Student Model` OR `BROKERS: Insert Student` OR `CONTROLLERS: POST Student` (See the category table above for a complete list of categories) + +#### 12.1.4 Pull Requests + +When developers have completed their work, they can create a pull request with the main repository, which lets the project maintainer know that an update is ready to be integrated. +The Pull Request (PR) can be created with the name using this syntax `[CATEGORY]: [Description Of Work Completed]`, where the category is always in CAPS and the description in Pascal Case +e.g. `FOUNDATIONS: Add Student` + +The comment section of the PR can also be completed with any additional information that describe the work done and that can be helpfull / relevant to the approver. +e.g. a screenhots of the CONTROLLER actions working as expected showing the various response outcomes like `201 - Created` or `400 - Bad Request` or `424 - Failed Dependency` etc. + +You can also link a pull request to an issue to show that a fix is in progress and to automatically close the issue when the pull request or branch is merged by adding a KEYWORD #ISSUE-NUMBER anywhere in the comment section. +e.g. `Closes #10` or if the PR cover multiple things, then you could do `Closes #10, closes #123` + +If you simply wish to link an issue without a closing action on merge, you can omit the keyword and just add #ISSUE-NUMBER e.g. `#10` + + +#### 12.1.5 Configurations + +Configuration settings are values that determine the behavior of a software application. +They can include anything from database connection strings and API keys to feature flags and logging levels. +By using configuration settings, developers can customize the behavior of their application without modifying the code, +making it easier to deploy and manage different environments. + +One popular way of storing configuration settings in .NET applications is by using the `appsettings.json` file. +This file is a JSON-formatted file that contains a set of key-value pairs representing configuration settings. +Developers can easily modify these settings to change the behavior of their application. + +However, not all configuration settings can be safely stored in `appsettings.json` files. +Sensitive data, such as API keys, database passwords, or any other data that could grant +unauthorized access to the application or its resources, should be kept secret. +Storing sensitive data in configuration files, especially in plain text format, can put the application's security at risk. + +It is very important for developers to follow secure coding practices and never store passwords +or other sensitive data in configuration provider code or in plain text configuration files. +Such sensitive data should be stored in a secure location like Azure Key Vault or in environment +variables that are set in a deployment environment. Storing passwords in local settings files is also +not recommended as this can easily lead to scenarios where passwords are accidentally checked into code repositories, +either because a developer has forgotten to exclude the file from version control or as a result of someone changing +it and then inadvertently including the sensitive files, putting the application's security at risk. + +One way of protecting sensitive data is by using **user secrets**. User secrets is a feature in .NET that provides a +convenient way for developers to store and retrieve sensitive data during development. +This data is stored locally on the developer's machine and is not intended to be used in production. +While user secrets can be useful for keeping sensitive data out of source control and easily accessible during development, +it is important to note that they are not compatible with GitHub workflow actions. +Instead, developers should consider using more secure methods for storing secrets, +such as environment variables or storing them in a secure location like `GitHub Action Secrets` or `Azure Key Vault`. +As user secrets is not directly compatible with GitHub build pipelines we will not explore this option further. + +Another way of protecting sensitive data is the use of **environment variables**. +Environment variables are variables that are set in the operating system, which can be accessed by applications at runtime. + +By using both **appsettings.json** and **environment variables**, +developers can separate sensitive data from other configuration settings, +making it easier to manage and secure. Additionally, +this approach enables developers to store their configuration settings securely in a build pipeline, +ensuring that sensitive data is protected throughout the development lifecycle. + +##### 12.1.5.1 appsettings.json + +A .NET app will be automatically load and register `appsettings.json` and `appsettings.{Environment}.json` with the IConfiguration +interface during application startup, if the files is located in the application's root directory. +This means that the key-value pairs defined in the appsettings.json file will be accessible through the IConfiguration object, +allowing developers to easily access and use the configuration settings throughout their application. + +It's worth noting that in addition to the appsettings.json file, there are other configuration providers available in +.NET that allow developers to load configuration settings from various sources, +such as environment variables or command-line arguments. +(Developers can also create their own custom configuration providers to load configuration settings from other sources if needed.) + +Configuration sources are read in the order that their configuration providers are specified. +Order configuration providers in code to suit the priorities for the underlying configuration sources that the app requires. + +A typical sequence of configuration providers is: + +1. appsettings.json +2. appsettings.{Environment}.json +3. User secrets +4. Environment variables using the Environment Variables configuration provider. +5. Command-line arguments using the Command-line configuration provider. + +Lets look at some code samples to see how all this works. + +##### 12.1.5.1 Application Settings + +**appsettings.json** + +``` +{ + "MySettings": { + "ApiUrl": "https://api.somesite.com/", + "ApiKey": "1ae4e397-ec3c-4ed7-8280-a17d0e2cbe78", + "OrganisationId": "1bac6df0-cd68-4ce5-9c29-b2beb58201cd" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} + +``` + +With the above `appsettings.json` file we can then load our variables like this: + +``` + string apiUrl = configuration["MySettings:ApiUrl"]; + string apiKey = configuration["MySettings:ApiKey"]; + string organisationId = configuration["MySettings:OrganisationId"]; + +``` + +##### 12.1.5.2 Environment Variables + +We can add environment variables to a web application by doing this: + +``` +var builder = WebApplication.CreateBuilder(args); +... +builder.Configuration.AddEnvironmentVariables(); +... +var app = builder.Build(); + +``` + +or if you want to play with this in a Console App or Unit Test, you can do this: + + +``` +var configurationBuilder = new ConfigurationBuilder() + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables("MYAPP_ACCEPTANCE_"); + +this.configuration = configurationBuilder.Build(); + +``` +**NOTE** that we have defined a prefix that environment variables must start with. +This prefix will automatically be removed from the environment variabl names by the configuration builder. +This is a very useful feature as you can now store environment variables with the same name for multiple apps and/or environments. + +Up to this point we will still get the same values if we get our config items as we have not set the values for any environment variables yet. + +In C# .NET, working with environment variables is straightforward. +The Environment class provides several methods for reading and setting environment variables. +For example, to read an environment variable, you can use the `Environment.GetEnvironmentVariable` method, +passing in the name of the variable you want to retrieve. +To set an environment variable, you can use the `Environment.SetEnvironmentVariable` method, providing the name of the variable and its value. +You can also use the command line to set the enviroment variables by running the SETX command. + +For demonstration we will leave the value of `MySettings:ApiUrl` in appsettings.json + +Next we will set the value of `MySettings:ApiKey` through this line of code + +``` +Environment.SetEnvironmentVariable("MYSETTINGS:APIKEY", "3d4a1c55-fcd7-4b34-8536-99c8ae6ae33c"); + +``` + +and for the `MySettings:OrganisationId` we will use the command line argument through a console window with administrative priviledges + +``` +setx MYSETTINGS:APIKEY "b2440ae9-cad2-4d70-b138-4a807abe1bb7" + +``` +**NOTE** +Visual Studio preloads the environment variables when it starts, and it caches them until the application is closed. +Unlike environment variables set through code, those set using the command line will not be immediately be available due to the preload behaviour. +**You will need to close Visual Studio and re-open it.** + + +Once you have reloaded Visual Studio we can get the values from the configuration again by doing this: + +``` + string apiUrl = configuration["MySettings:ApiUrl"]; + string apiKey = configuration["MySettings:ApiKey"]; + string organisationId = configuration["MySettings:OrganisationId"]; + +``` + +You will notice that the ApiUrl is the same as before, but the values have now changed for `MySettings:ApiKey` and `MySettings:OrganisationId` and they are now equal to the values we specified for the environment variables via code and through SETX on the command line. + +You can also remove environment variables through code by setting the value to `null`. +Environment.SetEnvironmentVariable("MYSETTINGS:APIKEY", null); + +OR via the command line you can do + +``` +setx MYSETTINGS:APIKEY /delete + +``` + +Or if you prefer a GUI, you can also go to SYSTEM PROPERTES > ENVIRONMENT VARIABLES where you can ADD / REMOVE environment variables. + +##### 12.1.5.3 GitHub Actions Workflow + +To setup your environment variables for use within GitHub we will need to setup repository secrets and add a section at the beginning of your workflow file. + +1. Go to the GitHub repository where you want to use these settings and click on "Settings" in the top navigation bar. + +2. In the left sidebar, click on "Secrets" and then click on "New repository secret" button. + +3. Create three new secrets named "MYAPP_ACCEPTANCE_MYSETTINGS__APIKEY", and "MYAPP_ACCEPTANCE_MYSETTINGS__ORGANISATIONID", +respectively, with the corresponding values for "ApiUrl", "ApiKey", and "OrganisationId" from the given settings. + + **NOTE** + * We have prefixed our environment variables with `MYAPP_ACCEPTANCE_` to match the earlier configuration of `.AddEnvironmentVariables("MYAPP_ACCEPTANCE_");`. + * The other difference is that a colon (:) is not allowed on GitHub Secrets to represent a hierarchy and you have to use a double underscore (__) instead. + +4. To expose these secrets as environment variables in the GitHub Action Workflow, add the following code snippet at the beginning of your workflow file: + + +```yaml +env: + API_URL: ${{ secrets.MySettings__ApiUrl }} + API_KEY: ${{ secrets.MySettings__ApiKey }} + ORG_ID: ${{ secrets.MySettings__OrganisationId } + +``` + +A full example of the `dotnet.yml` file will look like this: + +```yaml +name: MyApplication Build +on: + push: + branches: + - main + pull_request: + branches: + - main +jobs: + build: + runs-on: windows-latest + env: + ApiKey: ${{ secrets.MYAPP_ACCEPTANCE_MYSETTINGS__APIKEY }} + OrgId: ${{ secrets.MYAPP_ACCEPTANCE_MYSETTINGS__ORGANISATIONID }} + steps: + - name: Pulling Code + uses: actions/checkout@v2 + - name: Installing .NET + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 7.0.201 + include-prerelease: false + - name: Restoring Packages + run: dotnet restore + - name: Building Solution + run: dotnet build --no-restore + - name: Running Tests + run: dotnet test --no-build --verbosity normal + +``` +### 12.2 Measurements +The Standard promotes ideals of fairness to all. The Standard recognizes that innovative ideas, great contributions and high performance is not exclusive to individuals with certain titles. In order to truly and fairly ensure everyone's contributions are measured fairly, The Standard introduces values and points to contributions at different levels and categories. The acculmalations of these contributions increases the rank of the contributor from one level to the next. + +#### 12.2.0 Contribution Points +The following is a list of all types of contributions and the value for each: + +| Title Starts With | Return Value | +|----------------------------|--------------| +| INFRA | 10 | +| MAJOR INFRA | 10 | +| MEDIUM INFRA | 3 | +| MINOR INFRA | 1 | +| DATA | 5 | +| MAJOR DATA | 5 | +| MEDIUM DATA | 3 | +| MINOR DATA | 1 | +| MIGRATION | 20 | +| MAJOR MIGRATION | 20 | +| MEDIUM MIGRATION | 10 | +| MINOR MIGRATION | 5 | +| BROKER | 5 | +| MAJOR BROKER | 5 | +| MEDIUM BROKER | 3 | +| MINOR BROKER | 1 | +| FOUNDATION | 10 | +| MAJOR FOUNDATION | 10 | +| MEDIUM FOUNDATION | 5 | +| MINOR FOUNDATION | 1 | +| PROCESSING | 15 | +| MAJOR PROCESSING | 15 | +| MEDIUM PROCESSING | 10 | +| MINOR PROCESSING | 5 | +| ORCHESTRATION | 20 | +| MAJOR ORCHESTRATION | 20 | +| MEDIUM ORCHESTRATION | 15 | +| MINOR ORCHESTRATION | 10 | +| COORDINATION | 20 | +| MAJOR COORDINATION | 20 | +| MEDIUM COORDINATION | 15 | +| MINOR COORDINATION | 5 | +| MANAGEMENT | 20 | +| MAJOR MANAGEMENT | 20 | +| MEDIUM MANAGEMENT | 15 | +| MINOR MANAGEMENT | 5 | +| AGGREGATION | 10 | +| MAJOR AGGREGATION | 10 | +| MEDIUM AGGREGATION | 5 | +| MINOR AGGREGATION | 1 | +| CONTROLLER | 5 | +| MAJOR CONTROLLER | 5 | +| MEDIUM CONTROLLER | 3 | +| MINOR CONTROLLER | 1 | +| ACCEPTANCE | 10 | +| MAJOR ACCEPTANCE | 10 | +| MEDIUM ACCEPTANCE | 5 | +| MINOR ACCEPTANCE | 3 | +| INTEGRATION | 15 | +| MAJOR INTEGRATION | 15 | +| MEDIUM INTEGRATION | 10 | +| MINOR INTEGRATION | 10 | +| VIEW | 10 | +| MAJOR VIEW | 10 | +| MEDIUM VIEW | 5 | +| MINOR VIEW | 1 | +| BASE | 5 | +| MAJOR BASE | 5 | +| MEDIUM BASE | 3 | +| MINOR BASE | 1 | +| COMPONENT | 15 | +| MAJOR COMPONENT | 15 | +| MEDIUM COMPONENT | 10 | +| MINOR COMPONENT | 5 | +| PAGE | 5 | +| MAJOR PAGE | 5 | +| MEDIUM PAGE | 3 | +| MINOR PAGE | 1 | +| CLIENT | 5 | +| MAJOR CLIENT | 5 | +| MEDIUM CLIENT | 3 | +| MINOR CLIENT | 1 | +| EXPOSER | 5 | +| MAJOR EXPOSER | 5 | +| MEDIUM EXPOSER | 3 | +| MINOR EXPOSER | 1 | +| DESIGN | 20 | +| MAJOR DESIGN | 20 | +| MEDIUM DESIGN | 10 | +| MINOR DESIGN | 5 | +| MAJOR FIX | 20 | +| MEDIUM FIX | 10 | +| MINOR FIX | 5 | +| PLANNING | 15 | +| MAJOR PLANNING | 15 | +| MINOR PLANNING | 10 | +| MINIOR PLANNING | 5 | +| MENTORSHIP | 20 | +| MAJOR MENTORSHIP | 20 | +| MINOR MENTORSHIP | 15 | +| MINIOR MENTORSHIP | 10 | +| DISCUSSION | 15 | +| MAJOR DISCUSSION | 15 | +| MINOR DISCUSSION | 10 | +| MINIOR DISCUSSION | 5 | +| MAJOR CODE RUB | 10 | +| MEDIUM CODE RUB | 5 | +| MINOR CODE RUB | 1 | +| CODE RUB | 1 | +| PROVISION | 10 | +| IMPORT | 5 | +| RELEASE | 10 | +| CONFIG | 5 | +| REVIEW | 1 | +| DOCUMENTATION | 1 | +| STANDARD | 100 | +| BUSINES | 50 | +| STATUS | 1 | +| UNKNOWN | 0 | + +Each one of these categories have been defined above in the previous section. + +#### 12.2.1 Average Implementation Time + +The following table provides the **average estimated time** required to implement different layers or components in The Standard architecture. +These times are based on practical experience and are used as a general reference for planning and effort estimation. + +| Layer / Component | Average Time | +|-------------------|--------------| +| Brokers, Base | ~1 hour | +| Foundations, Components | ~3 hours | +| Processing, Orchestration, Coordination, Orchestration Components | ~5 hours | +| Management, Uber Manager | ~3 hours | +| Controllers, Pages, Clients | ~1 hour | + +These estimates represent typical implementation durations for a single service or component, assuming standard complexity and no external dependencies. + +#### 12.2.2 Ranks +The following are the ranks based on the life-time accumalation and contribution to The Standard Community and Standard-Compliant projects: + +| POSITION | SCORE | +|---------------------|------------------| +| SWE I | 10,000 | +| SWE II | 25,000 | +| SR. SWE | 50,000 | +| PRINCIPAL | 100,000 | +| PARTNER/DIRECTOR | 300,000 | +| VP | 500,000 | +| DISTINGUISHED | 1,000,000 | +| THE STANDARD BEARER | 1M + DELEGATION | diff --git a/.agents/skills/the-standard-practices/contracts/contracts.json b/.agents/skills/the-standard-practices/contracts/contracts.json new file mode 100644 index 0000000..43853cf --- /dev/null +++ b/.agents/skills/the-standard-practices/contracts/contracts.json @@ -0,0 +1,76 @@ +{ + "skill": "the-standard-practices", + "version": "1.0.0", + + "branch_naming": { + "pattern": "users/[username]/[CATEGORY]-[entity]-[action]", + "username": "GitHub username of contributor", + "category_case": "UPPER-CASE", + "separator": "/ between username section and category section, - between category/entity/action" + }, + + "commit_discipline": { + "tdd_fail_prefix": "[FAIL]:", + "tdd_pass_prefix": "[PASS]:", + "fail_requires": "test running and verified red by test runner", + "pass_requires": "all relevant tests running and verified green", + "atomicity": "one test per FAIL commit, one implementation unit per PASS commit", + "non_tdd_commits": "no prefix required, but message must be descriptive" + }, + + "categories": { + "infrastructure": ["INFRA", "MAJOR INFRA", "MEDIUM INFRA", "MINOR INFRA", "PROVISIONS", "RELEASES"], + "data": ["DATA", "MAJOR DATA", "MEDIUM DATA", "MINOR DATA", "MIGRATION", "MAJOR MIGRATION", "MEDIUM MIGRATION", "MINOR MIGRATION"], + "brokers": ["BROKERS", "MAJOR BROKERS", "MEDIUM BROKERS", "MINOR BROKERS"], + "services": [ + "FOUNDATIONS", "MAJOR FOUNDATIONS", "MEDIUM FOUNDATIONS", "MINOR FOUNDATIONS", + "PROCESSINGS", "MAJOR PROCESSINGS", "MEDIUM PROCESSINGS", "MINOR PROCESSINGS", + "ORCHESTRATIONS", "MAJOR ORCHESTRATIONS", "MEDIUM ORCHESTRATIONS", "MINOR ORCHESTRATIONS", + "COORDINATIONS", "MAJOR COORDINATIONS", "MEDIUM COORDINATIONS", "MINOR COORDINATIONS", + "MANAGEMENTS", "MAJOR MANAGEMENTS", "MEDIUM MANAGEMENTS", "MINOR MANAGEMENTS", + "AGGREGATIONS", "MAJOR AGGREGATIONS", "MEDIUM AGGREGATIONS", "MINOR AGGREGATIONS" + ], + "exposers": [ + "CONTROLLERS", "MAJOR CONTROLLERS", "MEDIUM CONTROLLERS", "MINOR CONTROLLERS", + "CLIENTS", "MAJOR CLIENTS", "MEDIUM CLIENTS", "MINOR CLIENTS", + "PROVIDERS", + "EXPOSERS", "MAJOR EXPOSERS", "MEDIUM EXPOSERS", "MINOR EXPOSERS" + ], + "ui": [ + "VIEWS", "MAJOR VIEWS", "MEDIUM VIEWS", "MINOR VIEWS", + "COMPONENTS", "MAJOR COMPONENTS", "MEDIUM COMPONENTS", "MINOR COMPONENTS", + "PAGES", "MAJOR PAGES", "MEDIUM PAGES", "MINOR PAGES" + ], + "other": ["FEATURES", "PATCH", "RELEASES"] + }, + + "size_thresholds": { + "MAJOR": "5 or more tests added or modified", + "MEDIUM": "3 to 4 tests added or modified", + "MINOR": "1 to 2 tests added or modified" + }, + + "pull_requests": { + "title_format": "[CATEGORY]: [Entity] - [Description]", + "required_before_merge": ["CI passing", "human approval", "no sensitive data in diff"], + "ai_may": "suggest, review, comment", + "ai_must_not": "approve or merge" + }, + + "configuration": { + "secrets_in_source_control": false, + "environment_specific_values_in_code": false, + "local_config_in_gitignore": true, + "ci_cd_secrets_method": "repository secrets (not plain text in workflow files)" + }, + + "forbidden": [ + "pushing directly to official repository (unless maintainer)", + "committing secrets, API keys, or connection strings", + "WIP commits on main", + "FAIL commits without verified test failure", + "PASS commits without verified full suite pass", + "non-Standard branch names", + "PR titles without category prefix" + ] +} diff --git a/.agents/skills/the-standard-practices/examples/bad/example_bad_branches.md b/.agents/skills/the-standard-practices/examples/bad/example_bad_branches.md new file mode 100644 index 0000000..8e31094 --- /dev/null +++ b/.agents/skills/the-standard-practices/examples/bad/example_bad_branches.md @@ -0,0 +1,72 @@ +# Bad Example: Branch Name Violations + +## Violation 1 — Missing username (prac-010, prac-011) +``` +# BAD: No users/ prefix, no username +FOUNDATIONS-student-add +feature/student-service +``` + +**Fix:** +``` +users/hassanhabib/FOUNDATIONS-student-add +``` + +--- + +## Violation 2 — Non-Standard category (prac-012) +``` +# BAD: Uses non-Standard categories +users/hassanhabib/feature-student-service +users/hassanhabib/bugfix-student-null +users/hassanhabib/refactor-student-validation +``` + +**Fix:** +``` +users/hassanhabib/FOUNDATIONS-student-create +users/hassanhabib/PATCH-student-null-check +users/hassanhabib/MINOR FOUNDATIONS-student-validations-refactor +``` + +--- + +## Violation 3 — No entity or action (prac-013, prac-014) +``` +# BAD: Missing entity and action +users/hassanhabib/FOUNDATIONS +users/hassanhabib/DATA-fix +``` + +**Fix:** +``` +users/hassanhabib/FOUNDATIONS-student-add +users/hassanhabib/MINOR DATA-course-add-credit-hours +``` + +--- + +## Violation 4 — Wrong size suffix (prac-021) +``` +# BAD: Using MAJOR when only 1 test is modified +users/hassanhabib/MAJOR FOUNDATIONS-student-add-null-check +``` + +**Fix:** `MAJOR` requires 5+ tests. For 1 test, use `MINOR`: +``` +users/hassanhabib/MINOR FOUNDATIONS-student-add-null-check +``` + +--- + +## Violation 5 — Camel or lowercase category (prac-012) +``` +# BAD: Category not in expected casing/format +users/hassanhabib/foundations-student-add +users/hassanhabib/Foundation-student-add +``` + +**Fix:** Category must be UPPER-CASE: +``` +users/hassanhabib/FOUNDATIONS-student-add +``` diff --git a/.agents/skills/the-standard-practices/examples/bad/example_bad_commits.md b/.agents/skills/the-standard-practices/examples/bad/example_bad_commits.md new file mode 100644 index 0000000..e6b1b15 --- /dev/null +++ b/.agents/skills/the-standard-practices/examples/bad/example_bad_commits.md @@ -0,0 +1,79 @@ +# Bad Example: Commit Message Violations + +## Violation 1 — No FAIL/PASS prefix (prac-030, prac-031, prac-032) +``` +# BAD: No discipline prefix +git commit -m "add student null check test" +git commit -m "implement null student validation" +``` + +**Fix:** +``` +[FAIL]: ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync +[PASS]: AddStudentAsync — null student validation +``` + +--- + +## Violation 2 — FAIL commit without verified failure (prac-033) +``` +# BAD: Developer wrote test and committed without running it +[FAIL]: ShouldAddStudentAsync +``` +*...but the test was never run. It might be broken, have a typo, or pass immediately.* + +**Fix:** Always run the test before committing a FAIL. Confirm the test runner shows RED. + +--- + +## Violation 3 — PASS commit with failing tests (prac-034) +``` +# BAD: Committed [PASS] but other tests in the suite are failing +[PASS]: AddStudentAsync — invalid student fields validation +``` +*Other tests broke due to the new validation logic, but the developer didn't run the full suite.* + +**Fix:** Run the FULL relevant test suite before any PASS commit. All tests must be GREEN. + +--- + +## Violation 4 — Multiple tests in one FAIL commit (prac-035) +``` +# BAD: Two tests committed in one FAIL commit +[FAIL]: ShouldAddStudentAsync + ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync +``` + +**Fix:** One test per FAIL commit: +``` +[FAIL]: ShouldAddStudentAsync +``` +Then after PASS: +``` +[FAIL]: ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync +``` + +--- + +## Violation 5 — Vague commit messages +``` +# BAD: No context, not machine-readable +git commit -m "fix" +git commit -m "working on student service" +git commit -m "WIP" +``` + +**Fix:** Commit messages must be descriptive and, for TDD work, use FAIL/PASS format. + +--- + +## Violation 6 — Secrets in a commit (prac-003) +``` +# BAD: Committed appsettings.json with real connection string +appsettings.json: "ConnectionStrings": { "Default": "Server=prod.db;Password=real_password" } +``` + +**Fix:** +1. Remove secret from code immediately. +2. Add `appsettings.local.json` to `.gitignore`. +3. Rotate the compromised secret. +4. Use environment variables or Azure Key Vault. diff --git a/.agents/skills/the-standard-practices/examples/good/example_branch_names.md b/.agents/skills/the-standard-practices/examples/good/example_branch_names.md new file mode 100644 index 0000000..dbfe4c0 --- /dev/null +++ b/.agents/skills/the-standard-practices/examples/good/example_branch_names.md @@ -0,0 +1,80 @@ +# Good Example: Branch Names + +## Pattern: `users/[username]/[CATEGORY]-[entity]-[action]` + +--- + +### Creating a Foundation Service for Student +``` +users/hassanhabib/FOUNDATIONS-student-add +``` +- username: `hassanhabib` +- category: `FOUNDATIONS` (creating a new foundation service) +- entity: `student` +- action: `add` + +--- + +### Modifying an existing Foundation Service (5+ tests changed) +``` +users/hassanhabib/MAJOR FOUNDATIONS-student-validations +``` +- username: `hassanhabib` +- category: `MAJOR FOUNDATIONS` (5+ test changes) +- entity: `student` +- action: `validations` + +--- + +### Creating a Data Model with Migration +``` +users/cjdutoit/DATA-course-create +``` +- username: `cjdutoit` +- category: `DATA` (new data model + migration) +- entity: `course` +- action: `create` + +--- + +### Modifying an existing data model (2 property changes) +``` +users/cjdutoit/MINOR DATA-course-add-description +``` +- Size MINOR because only 1-2 property changes. + +--- + +### Creating a REST Controller +``` +users/hassanhabib/CONTROLLERS-students-create +``` +- category: `CONTROLLERS` (new controller) +- entity: `students` (plural because controllers are plural) +- action: `create` + +--- + +### Initial infrastructure setup +``` +users/hassanhabib/INFRA-project-setup +``` +- category: `INFRA` +- entity: `project` +- action: `setup` + +--- + +### Small bug fix (non-TDD) +``` +users/cjdutoit/PATCH-student-name-null-check +``` +- category: `PATCH` (small non-TDD fix) + +--- + +### End-to-end feature +``` +users/hassanhabib/FEATURES-enrollment-complete +``` +- category: `FEATURES` (full feature, all layers) diff --git a/.agents/skills/the-standard-practices/examples/good/example_commit_messages.md b/.agents/skills/the-standard-practices/examples/good/example_commit_messages.md new file mode 100644 index 0000000..f813e9c --- /dev/null +++ b/.agents/skills/the-standard-practices/examples/good/example_commit_messages.md @@ -0,0 +1,91 @@ +# Good Example: Commit Messages (FAIL/PASS Discipline) + +## Pattern for TDD Commits + +``` +[FAIL]: [Test method name] +[PASS]: [Implementation description] +``` + +--- + +## Foundation Service Add — Full Commit Sequence + +### Step 0: Happy path + +``` +[FAIL]: ShouldAddStudentAsync +``` +*Test is written, run, verified to fail (no implementation yet). Committed.* + +``` +[PASS]: AddStudentAsync — happy path implementation +``` +*Minimum implementation to pass `ShouldAddStudentAsync`. All tests pass. Committed.* + +--- + +### Step 1: Null validation + +``` +[FAIL]: ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync +``` +*Null check test written and verified to fail. Committed.* + +``` +[PASS]: AddStudentAsync — null student validation +``` +*`ValidateStudentIsNotNull` added. Test passes. All tests pass. Committed.* + +--- + +### Step 2: Invalid fields validation + +``` +[FAIL]: ShouldThrowValidationExceptionOnAddIfStudentIsInvalidAndLogItAsync +``` +*Invalid fields test written for Id, Name, CreatedDate, UpdatedDate. Verified to fail. Committed.* + +``` +[PASS]: AddStudentAsync — invalid student fields validation +``` +*Continuous validation for all fields added. All tests pass. Committed.* + +--- + +### Step 3: Dependency validation (BadRequest) + +``` +[FAIL]: ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync +``` + +``` +[PASS]: AddStudentAsync — BadRequest dependency validation exception handling +``` + +--- + +## Non-TDD Commit (PATCH) + +``` +PATCH: Fix null reference in student name formatter +``` + +No FAIL/PASS prefix needed for non-TDD patches. + +--- + +## Infrastructure Commit + +``` +INFRA: Initial project setup — solution, projects, CI pipeline +``` + +--- + +## Rules Reminder + +- Every `[FAIL]` commit must be preceded by running the test and seeing RED. +- Every `[PASS]` commit must be preceded by running the FULL suite and seeing GREEN. +- Never combine multiple tests in one FAIL commit. +- Never combine multiple implementation units in one PASS commit. diff --git a/.agents/skills/the-standard-practices/manifest.json b/.agents/skills/the-standard-practices/manifest.json new file mode 100644 index 0000000..4d9475a --- /dev/null +++ b/.agents/skills/the-standard-practices/manifest.json @@ -0,0 +1,59 @@ +{ + "name": "the-standard-practices", + "version": "1.0.0", + "description": "Governs how contributions are structured, branches are named, commits are made (FAIL/PASS TDD discipline), pull requests are formatted, configuration is handled, and the forking workflow is applied for open-source Standard-compliant projects.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.3.0.0", + + "inputs": [ + "A request to name a branch", + "A request to format a commit message", + "A PR review request", + "A question about how to structure a contribution", + "A request to check configuration handling" + ], + + "outputs": [ + "Standard-compliant branch name", + "FAIL/PASS commit message", + "PR title and description following Standard format", + "Configuration guidance that avoids secret leakage" + ], + + "dependencies": [ + "the-standard-core" + ], + + "activation": { + "trigger": "planning work, naming branches, writing commits, creating PRs, configuring environments, reviewing contributions", + "note": "Always activate the-standard-core first." + }, + + "validation": { + "required": true, + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_branch_names.md", + "examples/good/example_commit_messages.md" + ], + "bad": [ + "examples/bad/example_bad_branches.md", + "examples/bad/example_bad_commits.md" + ] + }, + "templates": [ + "templates/pull_request_template.md", + "templates/branch_naming_guide.md" + ], + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-practices/rules/rules.json b/.agents/skills/the-standard-practices/rules/rules.json new file mode 100644 index 0000000..0cf1f45 --- /dev/null +++ b/.agents/skills/the-standard-practices/rules/rules.json @@ -0,0 +1,37 @@ +{ + "rules": [ + { "id": "prac-001", "category": "contributions", "description": "All contributions must use the Standard workflow: fork → branch → commit → PR.", "severity": "error" }, + { "id": "prac-002", "category": "contributions", "description": "No uncommitted WIP reaching main. All contributions must be complete and intentional.", "severity": "error" }, + { "id": "prac-003", "category": "contributions", "description": "Sensitive configuration must never be committed to source control in plain text.", "severity": "error" }, + { "id": "prac-010", "category": "branch-naming", "description": "Branch names must follow: users/[username]/[CATEGORY]-[entity]-[action].", "severity": "error" }, + { "id": "prac-011", "category": "branch-naming", "description": "[username] must be the GitHub username of the contributor.", "severity": "error" }, + { "id": "prac-012", "category": "branch-naming", "description": "[CATEGORY] must be from the Standard category list.", "severity": "error" }, + { "id": "prac-013", "category": "branch-naming", "description": "[entity] must identify the entity or resource being worked on.", "severity": "error" }, + { "id": "prac-014", "category": "branch-naming", "description": "[action] must describe the specific action being performed.", "severity": "error" }, + { "id": "prac-015", "category": "branch-naming", "description": "Branch name parts must be separated by / between users/username/ and the category block.", "severity": "error" }, + { "id": "prac-016", "category": "branch-naming", "description": "Branch [action] must use the language appropriate to the layer: broker branches use infrastructure verbs (insert, select-all, select-by-id, update, delete); service branches use business verbs (add, retrieve-all, retrieve-by-id, modify, remove). Never mix layers — a BROKERS branch must not use 'add' or 'retrieve'.", "severity": "error" }, + { "id": "prac-017", "category": "branch-naming", "description": "One branch represents one operation. When a prompt is ambiguous about scope, the agent must ask which specific operation to implement before creating the branch or generating any code.", "severity": "error" }, + { "id": "prac-020", "category": "categories", "description": "Category must accurately reflect the type of work being done.", "severity": "error" }, + { "id": "prac-021", "category": "categories", "description": "Size suffix reflects test count: MAJOR=5+, MEDIUM=3-4, MINOR=1-2.", "severity": "error" }, + { "id": "prac-022", "category": "categories", "description": "When unsure between MAJOR/MEDIUM/MINOR, choose based on tests added or modified.", "severity": "warning" }, + { "id": "prac-030", "category": "tdd-commits", "description": "Every TDD commit follows FAIL/PASS discipline.", "severity": "error" }, + { "id": "prac-031", "category": "tdd-commits", "description": "FAIL commit: test written and verified to fail. Prefix: [FAIL].", "severity": "error" }, + { "id": "prac-032", "category": "tdd-commits", "description": "PASS commit: implementation written, all tests passing. Prefix: [PASS].", "severity": "error" }, + { "id": "prac-033", "category": "tdd-commits", "description": "Never commit a FAIL without running and confirming the test is red.", "severity": "error" }, + { "id": "prac-034", "category": "tdd-commits", "description": "Never commit a PASS without running the full suite and confirming all green.", "severity": "error" }, + { "id": "prac-035", "category": "tdd-commits", "description": "FAIL and PASS commits must be atomic — one test per FAIL, one implementation unit per PASS.", "severity": "error" }, + { "id": "prac-040", "category": "pull-requests", "description": "PR titles must include the Standard category: [CATEGORY]: [Entity] - [Description].", "severity": "error" }, + { "id": "prac-041", "category": "pull-requests", "description": "PRs must not include sensitive data in the diff.", "severity": "error" }, + { "id": "prac-042", "category": "pull-requests", "description": "PRs must show passing CI (build + tests) before merge.", "severity": "error" }, + { "id": "prac-043", "category": "pull-requests", "description": "A human reviewer must approve before merging. AI may suggest but not merge.", "severity": "error" }, + { "id": "prac-044", "category": "pull-requests", "description": "PRs should be focused on one category — avoid mixing FEATURE + INFRA.", "severity": "warning" }, + { "id": "prac-050", "category": "configuration", "description": "Never hard-code environment-specific values in source code.", "severity": "error" }, + { "id": "prac-051", "category": "configuration", "description": "Use environment variables or secrets management for sensitive values.", "severity": "error" }, + { "id": "prac-052", "category": "configuration", "description": "Local development configuration files must be in .gitignore.", "severity": "error" }, + { "id": "prac-053", "category": "configuration", "description": "GitHub Actions must use repository secrets, not plain text values.", "severity": "error" }, + { "id": "prac-054", "category": "configuration", "description": "Configuration model classes must not contain default hard-coded production values.", "severity": "error" }, + { "id": "prac-060", "category": "forking", "description": "Open-source contributions must use the forking workflow.", "severity": "error" }, + { "id": "prac-061", "category": "forking", "description": "Contributors must never push directly to the official repository (unless maintainer).", "severity": "error" }, + { "id": "prac-062", "category": "forking", "description": "PRs from forks must be reviewed and build-verified before merge.", "severity": "warning" } + ] +} diff --git a/.agents/skills/the-standard-practices/rules/rules.md b/.agents/skills/the-standard-practices/rules/rules.md new file mode 100644 index 0000000..542f4b7 --- /dev/null +++ b/.agents/skills/the-standard-practices/rules/rules.md @@ -0,0 +1,74 @@ +# The Standard Practices — Rules + +## CONTRIBUTIONS + +**prac-001** [ERROR] All contributions must be structured through the Standard workflow: fork → branch → commit → PR. +**prac-002** [ERROR] Contributions must be intentional — no uncommitted work-in-progress, no "WIP" commits reaching main. +**prac-003** [ERROR] Sensitive configuration (secrets, API keys, connection strings) must never be committed to source control in plain text. + +## BRANCH NAMING + +**prac-010** [ERROR] Branch names must follow: `users/[username]/[CATEGORY]-[entity]-[action]`. +**prac-011** [ERROR] `[username]` must be the GitHub username of the contributor. +**prac-012** [ERROR] `[CATEGORY]` must be from the Standard category list (INFRA, DATA, BROKERS, FOUNDATIONS, etc.). +**prac-013** [ERROR] `[entity]` must identify the entity or resource being worked on. +**prac-014** [ERROR] `[action]` must describe the specific action being performed. +**prac-015** [ERROR] Branch name parts must be separated by `/` between `users/username/` and the category block. + +## CATEGORY SELECTION + +**prac-020** [ERROR] Category must accurately reflect the type of work: + - `INFRA` / `MAJOR|MEDIUM|MINOR INFRA`: project setup or configuration changes + - `DATA` / `MAJOR|MEDIUM|MINOR DATA`: data model creation or modification + - `MIGRATION` / `MAJOR|MEDIUM|MINOR MIGRATION`: data transformation or movement + - `BROKERS` / `MAJOR|MEDIUM|MINOR BROKERS`: broker creation or modification + - `FOUNDATIONS` / `MAJOR|MEDIUM|MINOR FOUNDATIONS`: foundation service creation or modification + - `PROCESSINGS` / `MAJOR|MEDIUM|MINOR PROCESSINGS`: processing service creation or modification + - `ORCHESTRATIONS` / `MAJOR|MEDIUM|MINOR ORCHESTRATIONS`: orchestration service creation or modification + - `COORDINATIONS` / `MAJOR|MEDIUM|MINOR COORDINATIONS`: coordination service creation or modification + - `MANAGEMENTS` / `MAJOR|MEDIUM|MINOR MANAGEMENTS`: management service creation or modification + - `AGGREGATIONS` / `MAJOR|MEDIUM|MINOR AGGREGATIONS`: aggregation service creation or modification + - `CONTROLLERS` / `MAJOR|MEDIUM|MINOR CONTROLLERS`: controller creation or modification + - `CLIENTS`: creating a client library + - `PROVIDERS`: creating a SPAL provider + - `EXPOSERS` / `MAJOR|MEDIUM|MINOR EXPOSERS`: other exposer work (Program.cs, etc.) + - `VIEWS` / `MAJOR|MEDIUM|MINOR VIEWS`: view service creation or modification + - `COMPONENTS` / `MAJOR|MEDIUM|MINOR COMPONENTS`: UI component creation or modification + - `PAGES` / `MAJOR|MEDIUM|MINOR PAGES`: page creation or modification + - `FEATURES`: end-to-end feature (all layers) + - `PATCH`: small non-TDD fixes + - `RELEASES`: release infrastructure + +**prac-021** [ERROR] Size suffix must reflect test count: MAJOR = 5+, MEDIUM = 3-4, MINOR = 1-2. +**prac-022** [WARNING] When unsure between MAJOR/MEDIUM/MINOR, choose based on the number of tests added or modified. + +## TDD COMMIT DISCIPLINE + +**prac-030** [ERROR] Every TDD commit follows FAIL/PASS discipline. +**prac-031** [ERROR] FAIL commit prefix: test is written and verified to fail. Commit message: `[FAIL]: ...` +**prac-032** [ERROR] PASS commit prefix: implementation written, all tests passing. Commit message: `[PASS]: ...` +**prac-033** [ERROR] Never commit a FAIL without running the test and confirming it shows red. +**prac-034** [ERROR] Never commit a PASS without running the full relevant suite and confirming all green. +**prac-035** [ERROR] FAIL and PASS commits must be atomic — one test per FAIL, one implementation unit per PASS. + +## PULL REQUESTS + +**prac-040** [ERROR] Pull request titles must include the Standard category: `[CATEGORY]: [Entity] - [Description]`. +**prac-041** [ERROR] Pull requests must not include sensitive data in the diff. +**prac-042** [ERROR] Pull requests must show passing CI (build + tests) before merge. +**prac-043** [ERROR] A human reviewer must approve before merging. AI may suggest but not merge. +**prac-044** [WARNING] Pull requests should be focused on one category of work — avoid mixing FEATURE + INFRA in one PR. + +## CONFIGURATION HANDLING + +**prac-050** [ERROR] Never hard-code environment-specific values (URLs, connection strings, API keys) in source code. +**prac-051** [ERROR] Use environment variables or a secrets management system for all sensitive values. +**prac-052** [ERROR] Local development configuration files (`.env`, `appsettings.local.json`) must be in `.gitignore`. +**prac-053** [ERROR] GitHub Actions CI/CD must use repository secrets, not plain text values in workflow files. +**prac-054** [ERROR] Configuration model classes must not contain default hard-coded production values. + +## FORKING WORKFLOW + +**prac-060** [ERROR] Open-source contributions must use the forking workflow: fork → local clone → branch → PR from fork. +**prac-061** [ERROR] Contributors must never push directly to the official repository unless they are the maintainer. +**prac-062** [WARNING] Pull requests from forks must be reviewed and build-verified before the maintainer merges. diff --git a/.agents/skills/the-standard-practices/templates/branch_naming_guide.md b/.agents/skills/the-standard-practices/templates/branch_naming_guide.md new file mode 100644 index 0000000..f13f3fa --- /dev/null +++ b/.agents/skills/the-standard-practices/templates/branch_naming_guide.md @@ -0,0 +1,100 @@ +# Branch Naming Guide + +## Pattern + +``` +users/[username]/[CATEGORY]-[entity]-[action] +``` + +## Fields + +| Field | Description | Example | +|---|---|---| +| `username` | Your GitHub username | `hassanhabib`, `cjdutoit` | +| `CATEGORY` | The Standard category (UPPER-CASE) | `FOUNDATIONS`, `MAJOR DATA` | +| `entity` | The entity or resource you are working on | `student`, `course`, `enrollment` | +| `action` | The specific action being performed | `add`, `create`, `validations`, `setup` | + +--- + +## Complete Category Reference + +| Category | When to Use | Size Required | +|---|---|---| +| `INFRA` | Initial project setup, build scripts | No | +| `MAJOR INFRA` | Major configuration/infrastructure changes | Yes | +| `MEDIUM INFRA` | Medium configuration changes | Yes | +| `MINOR INFRA` | Minor configuration changes | Yes | +| `PROVISIONS` | Provision scripts | No | +| `RELEASES` | Release infrastructure | No | +| `DATA` | New data model + migration | No | +| `MAJOR DATA` | 5+ property changes to model | Yes | +| `MEDIUM DATA` | 3-4 property changes to model | Yes | +| `MINOR DATA` | 1-2 property changes to model | Yes | +| `MIGRATION` | New data migration/transformation | No | +| `MAJOR/MEDIUM/MINOR MIGRATION` | Modifying existing migration | Yes | +| `BROKERS` | New broker | No | +| `MAJOR/MEDIUM/MINOR BROKERS` | Modifying existing broker (test count) | Yes | +| `FOUNDATIONS` | New foundation service | No | +| `MAJOR FOUNDATIONS` | Modifying foundation service (5+ tests) | Yes | +| `MEDIUM FOUNDATIONS` | Modifying foundation service (3-4 tests) | Yes | +| `MINOR FOUNDATIONS` | Modifying foundation service (1-2 tests) | Yes | +| `PROCESSINGS` | New processing service | No | +| `MAJOR/MEDIUM/MINOR PROCESSINGS` | Modifying processing service | Yes | +| `ORCHESTRATIONS` | New orchestration service | No | +| `MAJOR/MEDIUM/MINOR ORCHESTRATIONS` | Modifying orchestration service | Yes | +| `COORDINATIONS` | New coordination service | No | +| `MAJOR/MEDIUM/MINOR COORDINATIONS` | Modifying coordination service | Yes | +| `MANAGEMENTS` | New management service | No | +| `MAJOR/MEDIUM/MINOR MANAGEMENTS` | Modifying management service | Yes | +| `AGGREGATIONS` | New aggregation service | No | +| `MAJOR/MEDIUM/MINOR AGGREGATIONS` | Modifying aggregation service | Yes | +| `CONTROLLERS` | New controller | No | +| `MAJOR/MEDIUM/MINOR CONTROLLERS` | Modifying controller | Yes | +| `CLIENTS` | Creating a client library | No | +| `MAJOR/MEDIUM/MINOR CLIENTS` | Modifying client library | Yes | +| `PROVIDERS` | Creating a SPAL provider | No | +| `EXPOSERS` | Other exposers (Program.cs) | No | +| `MAJOR/MEDIUM/MINOR EXPOSERS` | Modifying other exposers | Yes | +| `VIEWS` | New view service | No | +| `MAJOR/MEDIUM/MINOR VIEWS` | Modifying view service | Yes | +| `COMPONENTS` | New UI component | No | +| `MAJOR/MEDIUM/MINOR COMPONENTS` | Modifying UI component | Yes | +| `PAGES` | New page | No | +| `MAJOR/MEDIUM/MINOR PAGES` | Modifying page | Yes | +| `FEATURES` | Full end-to-end feature (all layers) | No | +| `PATCH` | Small non-TDD fix | No | +| `RELEASES` | Release artifacts | No | + +## Size Reference + +| Size | Test Count | +|---|---| +| `MAJOR` | 5+ tests added or modified | +| `MEDIUM` | 3-4 tests added or modified | +| `MINOR` | 1-2 tests added or modified | + +## Quick Examples + +```bash +# New foundation service +git checkout -b users/hassanhabib/FOUNDATIONS-student-create + +# Modifying foundation service — 3 tests updated +git checkout -b users/cjdutoit/MEDIUM FOUNDATIONS-student-validations-update + +# New data model +git checkout -b users/hassanhabib/DATA-course-create + +# Adding 1 property to model +git checkout -b users/cjdutoit/MINOR DATA-student-add-email + +# Infrastructure setup +git checkout -b users/hassanhabib/INFRA-project-initial-setup + +# Small bug fix +git checkout -b users/cjdutoit/PATCH-student-date-null-fix + +# Full feature +git checkout -b users/hassanhabib/FEATURES-enrollment-add-complete +``` diff --git a/.agents/skills/the-standard-practices/templates/pull_request_template.md b/.agents/skills/the-standard-practices/templates/pull_request_template.md new file mode 100644 index 0000000..998da89 --- /dev/null +++ b/.agents/skills/the-standard-practices/templates/pull_request_template.md @@ -0,0 +1,76 @@ +# Pull Request Template + +## Title Format +``` +[CATEGORY]: [Entity] - [Short description of the change] +``` + +**Examples:** +``` +FOUNDATIONS: Student - Add foundation service with Add, Retrieve, Modify, Remove +MAJOR FOUNDATIONS: Student - Update validation logic for date fields +MINOR DATA: Course - Add credit hours property +CONTROLLERS: Students - Create REST controller +PATCH: Student - Fix null reference in name formatter +``` + +--- + +## PR Description Template + +### What does this PR do? +[One paragraph describing the change, the purpose it serves, and the layer it touches.] + +### Category +- [ ] INFRA +- [ ] DATA / MIGRATION +- [ ] BROKERS +- [ ] FOUNDATIONS +- [ ] PROCESSINGS +- [ ] ORCHESTRATIONS +- [ ] AGGREGATIONS +- [ ] CONTROLLERS +- [ ] EXPOSERS +- [ ] VIEWS / COMPONENTS / PAGES +- [ ] FEATURES +- [ ] PATCH +- [ ] RELEASES + +### Size +- [ ] MAJOR (5+ tests added/modified) +- [ ] MEDIUM (3-4 tests added/modified) +- [ ] MINOR (1-2 tests added/modified) +- [ ] N/A (INFRA, PATCH, etc.) + +### Branch name follows Standard convention +- [ ] `users/[username]/[CATEGORY]-[entity]-[action]` + +### TDD compliance +- [ ] All FAIL commits have been verified to fail (test runner was red) +- [ ] All PASS commits have been verified to pass (full suite was green) +- [ ] Commit history follows FAIL/PASS alternating pattern + +### No sensitive data +- [ ] No secrets, API keys, or connection strings in the diff +- [ ] `.gitignore` covers local configuration files + +### Quality gates +- [ ] CI build passes +- [ ] All tests pass +- [ ] Code follows The Standard Code CSharp rules +- [ ] Architecture follows The Standard Architecture rules + +### Related issues +Closes #[issue number] + +--- + +## Reviewer Checklist + +- [ ] PR title follows `[CATEGORY]: [Entity] - [Description]` +- [ ] Branch name follows `users/[username]/[CATEGORY]-[entity]-[action]` +- [ ] Commit history shows FAIL/PASS pattern +- [ ] No sensitive data in diff +- [ ] CI is green +- [ ] Code complies with The Standard C# and Architecture rules +- [ ] Tests cover happy path + validation + exception scenarios diff --git a/.agents/skills/the-standard-practices/validations/anti-patterns.md b/.agents/skills/the-standard-practices/validations/anti-patterns.md new file mode 100644 index 0000000..c5bd638 --- /dev/null +++ b/.agents/skills/the-standard-practices/validations/anti-patterns.md @@ -0,0 +1,152 @@ +# The Standard Practices — Anti-Patterns + +--- + +## AP-PRAC-001: Non-Standard Branch Names + +**What it is:** Branches that don't follow `users/[username]/[CATEGORY]-[entity]-[action]`. + +**Examples:** +``` +feature/student-service +bugfix/null-check +student-service-update +main-fix +``` + +**Why harmful:** Non-Standard branch names make it impossible to understand the scope, owner, and category of work at a glance. Tooling, contribution point tracking, and code review workflows depend on the Standard branch pattern. + +**How to fix:** +``` +users/hassanhabib/FOUNDATIONS-student-create +users/cjdutoit/PATCH-student-null-check +``` + +--- + +## AP-PRAC-002: Committing Without FAIL/PASS Verification + +**What it is:** Writing a FAIL commit where the test was never actually run and confirmed as red, or writing a PASS commit where the full suite was never run. + +**Example:** +``` +git commit -m "[FAIL]: ShouldAddStudentAsync" +# Engineer never ran the test — it may have a typo, compile error, or even pass immediately +``` + +**Why harmful:** The entire value of TDD is the red/green cycle. A FAIL commit that was never red means the engineer may have written a test that will never actually validate anything. A PASS commit that was never fully green means regressions go undetected. + +**How to fix:** Before every FAIL commit: run the test, confirm red. Before every PASS commit: run the full suite, confirm green. + +--- + +## AP-PRAC-003: Bundling Multiple Tests in One FAIL Commit + +**What it is:** Committing two or more test implementations in a single FAIL commit. + +**Example:** +``` +[FAIL]: ShouldAddStudentAsync + ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync +``` + +**Why harmful:** Atomic commits enable bisecting. If something breaks, you need to find exactly which test or implementation caused it. A bundled FAIL commit obscures the cause and makes `git bisect` and `git revert` unpredictable. + +**How to fix:** One test per FAIL commit, one implementation unit per PASS commit. + +--- + +## AP-PRAC-004: Sensitive Data Committed to Source Control + +**What it is:** Secrets, connection strings, API keys, or passwords appear in committed files. + +**Example:** +```json +// appsettings.json — VIOLATION +{ + "ConnectionStrings": { + "Default": "Server=prod-db.mycompany.com;User Id=admin;Password=SuperSecret123;" + } +} +``` + +**Why harmful:** Once committed, a secret is in git history forever — even after deletion. Anyone with repo access (or who ever cloned it) can retrieve it. Secrets in CI pipelines are exposed to anyone who can read workflow logs. + +**How to fix:** +1. Immediately rotate the exposed secret. +2. Add the configuration file to `.gitignore`. +3. Use environment variables, Azure Key Vault, AWS Secrets Manager, or GitHub Secrets. +4. Use `appsettings.local.json` (gitignored) for local development. + +--- + +## AP-PRAC-005: Wrong Category Size + +**What it is:** Using MAJOR when only 1-2 tests are changed, or MINOR when 5+ tests are changed. + +**Examples:** +``` +# VIOLATION: MAJOR used for 1 test change +users/hassanhabib/MAJOR FOUNDATIONS-student-add-null-check + +# VIOLATION: MINOR used for 6 test changes +users/cjdutoit/MINOR FOUNDATIONS-student-all-validations +``` + +**Why harmful:** Size categories are used for contribution point calculations and effort estimation. Incorrect sizing distorts measurements and makes contribution tracking meaningless. + +**How to fix:** Count the tests being added or modified: +- 1-2 tests → `MINOR` +- 3-4 tests → `MEDIUM` +- 5+ tests → `MAJOR` + +--- + +## AP-PRAC-006: Non-Category PR Titles + +**What it is:** PR titles that don't include the Standard category prefix. + +**Examples:** +``` +Add student service +Fix null check bug +Update student validations +``` + +**Why harmful:** The PR title is the primary signal for reviewers, contributors, and tooling to understand what category of work is being reviewed. Without it, code reviews become ambiguous and contribution tracking is broken. + +**How to fix:** +``` +FOUNDATIONS: Student - Add foundation service with CRUD operations +PATCH: Student - Fix null reference in name formatter +MEDIUM FOUNDATIONS: Student - Update validation logic +``` + +--- + +## AP-PRAC-007: Pushing Directly to Official Repository + +**What it is:** A non-maintainer contributor pushes directly to the official repository branch instead of using a fork. + +**Why harmful:** Bypasses the PR review process. Code is never reviewed before reaching the official codebase. Breaks the open-source contribution model that The Standard promotes. + +**How to fix:** Fork the repository. Clone your fork. Create a branch on your fork. Open a PR from your fork to the official repository. + +--- + +## AP-PRAC-008: Mixing Categories in One PR + +**What it is:** A single PR that contains INFRA changes, DATA changes, and FOUNDATIONS changes. + +**Example:** +``` +PR: "Add student feature" +Changes: +- Added Student model + migration (DATA) +- Added StorageBroker changes (BROKERS) +- Added StudentService (FOUNDATIONS) +- Added Program.cs registration (INFRA) +``` + +**Why harmful:** Reviewers cannot focus their review on one category. Regressions are harder to isolate. If the PR needs to be reverted, all work is lost together. Contribution points cannot be accurately assigned. + +**How to fix:** Break the work into separate PRs per category. Use feature flags or partial merges if coordination is needed. diff --git a/.agents/skills/the-standard-practices/validations/checklist.md b/.agents/skills/the-standard-practices/validations/checklist.md new file mode 100644 index 0000000..ae06db2 --- /dev/null +++ b/.agents/skills/the-standard-practices/validations/checklist.md @@ -0,0 +1,67 @@ +# The Standard Practices — Validation Checklist + +Run this checklist when reviewing a PR, creating a branch, or making commits. + +--- + +## BRANCH NAMING + +- [ ] **prac-010** Branch name follows `users/[username]/[CATEGORY]-[entity]-[action]`. +- [ ] **prac-011** Username is the contributor's GitHub username. +- [ ] **prac-012** Category is from the Standard category list and is UPPER-CASE. +- [ ] **prac-013** Entity is identified in the branch name. +- [ ] **prac-014** Action is identified in the branch name. +- [ ] **prac-021** Size suffix (MAJOR/MEDIUM/MINOR) reflects actual test count change. + +--- + +## TDD COMMIT DISCIPLINE + +- [ ] **prac-030** TDD commits use FAIL/PASS pattern. +- [ ] **prac-031** Each `[FAIL]` commit contains exactly one failing test. +- [ ] **prac-032** Each `[PASS]` commit contains exactly one implementation unit. +- [ ] **prac-033** FAIL commits were preceded by a verified red test run. +- [ ] **prac-034** PASS commits were preceded by a verified green full suite run. +- [ ] **prac-035** No FAIL commit contains multiple tests. No PASS commit contains multiple unrelated implementation units. + +--- + +## PULL REQUESTS + +- [ ] **prac-040** PR title follows `[CATEGORY]: [Entity] - [Description]`. +- [ ] **prac-041** Diff contains no sensitive data (secrets, API keys, passwords). +- [ ] **prac-042** CI build and all tests pass. +- [ ] **prac-043** A human reviewer has approved (not AI-only approval). +- [ ] **prac-044** PR is focused on one category of work (no mixing). + +--- + +## CONFIGURATION + +- [ ] **prac-050** No environment-specific values hard-coded in source files. +- [ ] **prac-051** Sensitive values are in environment variables or secrets management. +- [ ] **prac-052** Local config files are in `.gitignore`. +- [ ] **prac-053** CI/CD workflows use repository secrets. +- [ ] **prac-054** No hard-coded production defaults in configuration model classes. + +--- + +## FORKING WORKFLOW (Open Source) + +- [ ] **prac-060** Contribution comes from a fork, not a direct branch on the official repo. +- [ ] **prac-061** Contributor did not push directly to the official repository. +- [ ] **prac-062** PR from fork has been reviewed and build is verified before merge. + +--- + +## RESULT + +| Category | PASS / FAIL | +|---|---| +| Branch Naming | | +| TDD Commit Discipline | | +| Pull Requests | | +| Configuration | | +| Forking Workflow | | + +**Overall: PASS only when every row is PASS.** diff --git a/.agents/skills/the-standard-testing/SKILL.md b/.agents/skills/the-standard-testing/SKILL.md new file mode 100644 index 0000000..042496f --- /dev/null +++ b/.agents/skills/the-standard-testing/SKILL.md @@ -0,0 +1,552 @@ +--- +name: The Standard Testing +description: Enforces Standard TDD discipline, validation testing, exception mapping, controller acceptance tests, and UI component testing. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard Testing + +## What this skill is + +This skill governs how The Standard is tested, verified, and proven. +It covers test-driven development, validation strategies, exception mapping, unit test structure, controller tests, and UI component tests. + +## Explicit coverage map + +This skill explicitly covers: + +- Foundation-service implementation and validation/testing patterns from the Services chapter +- Structural, logical, external, and dependency validation testing +- Exception mapping and category testing +- Processing-service testing responsibilities +- Orchestration-service call-order testing +- Aggregation-service testing restrictions +- REST controller unit and acceptance tests +- UI component testing for bases, core components, and pages +- The supplied implementation specification sections on unit testing, partial test organization, conventions, AAA, and test order +- TDD FAIL/PASS discipline relevant to test creation and implementation verification + +## When to use + +Use this skill whenever writing, reviewing, expanding, fixing, or sequencing tests. +Use it whenever deciding what to test first, how to map exceptions in tests, or how to prove a Standard-compliant flow. + +## Core testing doctrine + +0. Follow TDD. +1. Write the failing test first. +2. Verify the test actually fails. +3. Write the minimum implementation required to pass. +4. Verify the full relevant suite passes. +5. Refactor without changing behavior. +6. Repeat. + +## Validation testing rules + +### Validation Source of Truth + +0. Validation rules MUST be inferred from all authoritative sources: + - Foundation service business rules + - Storage-layer configuration (`StorageBroker.[Entity].Configurations.cs`) + - Domain expectations implied by usage + +1. The storage configuration represents **minimum enforced constraints**: + - Required vs optional + - Maximum length + - Minimum length (if configured) + - Precision / scale / format where applicable + +2. Foundation services represent **the enforcement boundary**: + - All constraints that can cause persistence failure MUST be validated before reaching storage + - Validation must prevent database exceptions where deterministically possible + +### Validation Alignment Rules + +0. Foundation validation MUST be **equal to or stricter than** storage constraints. + +1. The following are **ALLOWED (strengthening rules)**: + - Storage: optional → Foundation: required + - Storage: optional → Foundation: constrained (min/max length) + - Storage: max length → Foundation: smaller max length + +2. The following are **NOT ALLOWED (weakening or missing rules)**: + - Storage: required → Foundation: not validated as required + - Storage: max length → Foundation: no length validation + +3. Violations of alignment MUST be treated as: + - A design defect + - A test failure condition + - A review blocker + +### Validation Responsibility Rule + +0. The database MUST NOT be relied upon to enforce: + - Required field validation + - Length validation + - Format validation + +1. The ONLY acceptable database-enforced constraints without prior validation are: + - Foreign key constraints + - Uniqueness / duplicate key constraints + - Concurrency constraints + +2. Any validation that can be performed deterministically in the foundation service MUST be performed there + + +### Validation Test Derivation from Storage Configuration + +0. Storage configuration (`StorageBroker.[Entity].Configurations.cs`) defines **constraints**, not validation behavior. + +1. For every constraint defined in storage configuration, there MUST exist a corresponding validation rule in the foundation service: + - Required fields + - Length constraints (min/max) + - Precision / format constraints where applicable + +2. Validation tests MUST: + - Target the **foundation service validation methods only** + - NOT directly test storage configuration behavior + - Prove that each storage-defined constraint is enforced at the foundation layer + +3. For each property constraint, validation tests MUST focus on constraint violations and boundary breaches: + + - Invalid case (violates constraint) → FAIL + - Required field missing / null + - Value exceeding maximum length + - Value below minimum length (if applicable) + + - Boundary violation cases: + - Just above maximum → FAIL + - Just below minimum → FAIL (if applicable) + +4. Valid scenarios (within acceptable range) are covered by logic tests and MUST NOT be redundantly tested in validation tests. + +5. Missing alignment between storage configuration and foundation validation MUST be treated as: + - A design defect + - A test failure condition + - A review blocker + +6. Automation MAY assist in identifying constraints (e.g., via EF metadata), but: + - Generated tests MUST still validate foundation behavior + - Automation MUST NOT result in tests that validate the database layer directly + +7. All validation tests MUST: + - Follow Standard naming conventions + - Be explicit and intention-revealing + + +### Validation order + +0. Structural validations first. +1. Logical validations second. +2. External validations third. +3. Dependency validations when the external resource is the source of the failure. + +### Circuit-breaking validations + +0. Null checks and other hard-stop guards must break immediately. +1. If continuing would create invalid dereference or meaningless work, stop immediately. + +### Continuous validations + +0. When multiple fields can be invalid independently, collect them before throwing. +1. Use upsertable exception data. +2. Use dynamic rules with condition + message. +3. Use a validations collector routine. +4. Throw once the collector contains errors. + +### Hybrid continuous validations + +0. Validate parent objects before validating child properties. +1. Split nested validation into levels to avoid unintended null-reference failures. + +## Foundation-service test rules + +0. Test the happy path first. +1. Then test structural validations. +2. Then test logical validations. +3. Then test external validations. +4. Then test dependency validations. +5. Then test dependency exceptions. +6. Then test service exceptions. +7. Always verify logging and broker calls. +8. Always verify no unwanted calls occurred. +9. Always keep validation and exception behaviors local and explicit. + +## Processing-service test rules + +0. Test higher-order logic, not primitive broker details. +1. Validate only what the processing service uses. +2. Test shifters. + - Example: object -> bool or object -> count. +3. Test combinations. + - Example: retrieve + add, retrieve + modify, ensure-exists, upsert. +4. Test processing exception mapping from foundation exceptions. + +## Orchestration-service test rules + +0. Test multi-entity flow combinations. +1. Test mapping/branching between contracts when present. +2. Test call order when the flow depends on order. +3. Prefer natural order when inputs/outputs force sequencing. +4. Use explicit order verification when sequencing is not naturally encoded. +5. Verify orchestration-level exception wrapping and unwrapping. +6. Test normalization outcomes indirectly through dependency shape and resulting behavior. + +## Aggregation-service test rules + +0. Do not test dependency call order in aggregation services. +1. Do not use mock-sequence style order assertions for aggregation services. +2. Test only basic structural validations and exposure-level aggregation behavior. +3. Aggregation services may multi-call or pass-through; test the contract and exposure abstraction, not orchestration logic. + +## Controller and protocol test rules + +0. Controllers require unit tests for mapping logic. +1. Unit-test success code mappings. +2. Unit-test validation / dependency / service error mappings. +3. Unit-test security i.e authorization / authentication failure mappings. +3. Acceptance-test every endpoint. +4. Clean up test data after acceptance tests. +5. Emulate external resources not owned by the microservice when running acceptance tests. +6. Integration and end-to-end testing are valid beyond unit + acceptance. + +## UI component test rules + +### Base components + +0. Treat bases as thin wrappers. +1. Test their exposed APIs and wrapper behavior when needed. +2. Do not put business logic into bases. + +### Core components + +0. Core components are test-driven. +1. Test elements. + - Existence + - Properties + - Actions +2. Existence may be tested by property assignment, searching by id, or general search. +3. Test styles when styles are part of the component contract. +4. Test actions that mutate state, create components, or trigger service calls. +5. Core components should integrate with one and only one view service. + +### Pages / containers + +0. Pages are simpler route containers. +1. They generally do not require unit tests. +2. They should not contain business logic. + +## Unit-test conventions from the supplied implementation profile + +0. Mirror partial-class split in tests. +1. Use setup/helpers in the root test file. +2. Split tests into logic, validations, and exceptions files. +3. Use GWT: Given / When / Then. +4. Mock all dependencies. +5. Use readable assertions. +6. Use deep cloning to protect expectation identity. +7. Use randomized data by default. +8. Verify exact dependency calls. +9. End with VerifyNoOtherCalls. +10. Keep naming explicit and scenario-driven. + +## Exact test implementation order for foundation-service add routines + +When implementing an Add{Entity}Async routine under the implementation profile, follow this order: + +0. ShouldAdd{Entity}Async +1. ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync +2. ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync +3. ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync +4. ShouldThrowDependencyValidationExceptionOnAddIfConflictErrorOccursAndLogItAsync +5. ShouldThrowCriticalDependencyExceptionOnAddIfUnauthorizedErrorOccursAndLogItAsync +6. ShouldThrowCriticalDependencyExceptionOnAddIfForbiddenErrorOccursAndLogItAsync +7. ShouldThrowCriticalDependencyExceptionOnAddIfNotFoundErrorOccursAndLogItAsync +8. ShouldThrowCriticalDependencyExceptionOnAddIfUrlNotFoundErrorOccursAndLogItAsync +9. ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccursAndLogItAsync +10. ShouldThrowDependencyExceptionOnAddIfServiceUnavailableErrorOccursAndLogItAsync +11. ShouldThrowCriticalDependencyExceptionOnAddIfHttpRequestErrorOccursAndLogItAsync +12. ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync + +For storage-based services, substitute the storage equivalents such as duplicate-key, DbUpdate, and SQL exceptions. + +## Testing and exception/localization addendum from the supplied implementation profile + +## 8. Unit Testing + +Unit tests follow The Standard's **partial-class + three-axis** approach. + +### 8.1 Test Class Structure + +Each entity's tests mirror the same partial-class split as the service: + +| Partial file | Tests | +| ------------------------------------------------- | ------------------------------------------------------ | +| `{Entity}ServiceTests.cs` | Setup, mocks, helpers (`CreateRandom{Entity}`, etc.) | +| `{Entity}ServiceTests.Logic.{Method}.cs` | Happy-path / success-case tests | +| `{Entity}ServiceTests.Validations.{Method}.cs` | Validation failure tests | +| `{Entity}ServiceTests.Exceptions.{Method}.cs` | Dependency & service exception tests | + +### 8.2 Conventions + +| Convention | Detail | +| -------------------- | ------------------------------------------------------------------------------------------- | +| Mocking | **Moq** — `Mock`, `Mock`, `Mock` | +| Assertions | **FluentAssertions** — `Should().BeEquivalentTo()` | +| Deep cloning | **DeepCloner** — to isolate input/expected/actual objects | +| Data generation | **Tynamix.ObjectFiller** — `Filler<{Entity}>` with custom property setup | +| Exception comparison | `Xeption.SameExceptionAs()` via `SameExceptionAs` expression helper | +| Test naming | `Should{Action}Async` / `ShouldThrow{Exception}On{Action}If{Condition}AndLogItAsync` | +| Verify calls | Every test verifies broker calls (`Times.Once` / `Times.Never`) and ends with `VerifyNoOtherCalls()` | +| Test framework | **xUnit** — `[Fact]` for single cases, `[Theory] [InlineData]` for parameterised cases | + +### 8.3 Test Pattern — GWT (Given / When / Then) + +``` +// given — build input, configure mocks, construct expected exception +// when — invoke the service method +// then — assert result / exception, verify broker interactions +``` + +### 8.4 Test Implementation Order + +Tests **must** be written and committed in the following strict order. This ordering ensures +each category builds upon the prior one: + +1. **Happy Path** — `ShouldAdd{Entity}Async` +2. **Structural Validations** — null entity check (`ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync`) +3. **Logical Validations** — property-level checks using `[Theory] [InlineData]` (`ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync`) +4. **External Dependency Validation Exceptions** — `BadRequest` → `Conflict` +5. **External Critical Dependency Exceptions** — `Unauthorized` → `Forbidden` → `NotFound` → `UrlNotFound` +6. **External Non-Critical Dependency Exceptions** — `InternalServerError` → `ServiceUnavailable` +7. **Transport-Level Exception** — `HttpRequestException` +8. **Catch-All Service Exception** — `Exception` + +For storage-based services, steps 4–7 are replaced with the corresponding SQL/EF exceptions +(`DuplicateKeyException`, `DbUpdateException`, `SqlException`). + +> **Rule — Test Verification Before Commit:** Each FAIL commit must have the test +> **actually running and failing**. Each PASS commit must have **all** tests +> running and passing. Never commit a FAIL without verifying the test runner +> reports a genuine failure. See [Section 12.1.3 — Commits](#1213-commits) for details. + +--- + +## 9. Key Libraries + +| Package | Purpose | +| ---------------------- | ------------------------------------------------- | +| `Xeption` | Enhanced exceptions with data aggregation | +| `EFxceptions` | EF Core wrapper that throws meaningful exceptions | +| `RESTFulSense` | HTTP client wrapper for external API brokers | +| `Moq` | Mock framework for unit tests | +| `FluentAssertions` | Readable assertion syntax | +| `DeepCloner` | Value-based deep cloning of test objects | +| `Tynamix.ObjectFiller` | Random test data generation | +| `xunit` | Unit test framework | + +--- + +## Exception Handling Principles + +### 0. Scope + +These rules govern exception design, localisation, categorisation, propagation, and testing across all service layers. + +--- + +### 1. Localisation (MANDATORY) + +0. External (non-local) exceptions MUST be localised at the boundary (Foundation). +1. Native exceptions (SQL, HTTP, SDK) MUST NOT cross service boundaries. +2. Localisation MUST convert native exceptions into domain-specific exceptions. + +**Data Preservation (MANDATORY)** + +3. ALL relevant data from the external exception MUST be copied to the local exception: + - `Data` dictionary + - Constraint / validation metadata + - Identifiers / keys + +4. The localised exception MUST carry this data so that: + - The **immediate inner exception** contains full validation detail after categorisation. + +--- + +### 2. Categorisation + +0. All exceptions MUST be categorised into one of: + - Validation + - DependencyValidation + - Dependency + - Service + +1. Categorisation defines upstream handling and exposer mapping. + +--- + +### 3. Propagation (Unwrap / Rewrap) + +Each service layer MUST: + +0. Catch downstream exceptions +1. UNWRAP the categorical exception +2. PRESERVE the LOCAL exception +3. REWRAP into its OWN categorical exception + +This prevents leakage of lower-layer concerns and enforces layer contracts. + +--- + +### 4. Inner Exception Preservation (MANDATORY) + +0. The original local exception MUST always be preserved as the inner exception. +1. No layer may discard or replace the local exception. + +This guarantees: +- Traceability +- Correct exposer mapping (e.g. HTTP Conflict / FailedDependency) +- Retention of validation data + +--- + +### 5. Layer Responsibilities + +#### Foundation +0. Localise external exceptions +1. Populate local exception data +2. Categorise into Validation / DependencyValidation / Dependency / Service + +#### Processing +0. MUST ONLY handle categorised exceptions +1. MUST NOT depend on foundation exception types +2. MUST rewrap into processing-level exceptions + +#### Orchestration +0. MUST handle exceptions from all dependencies +1. MUST unify into a single categorical exception per type +2. MUST unwrap and preserve inner exceptions + +--- + +### 6. Catch-All (MANDATORY) + +0. Every service MUST implement: + - `catch (Exception)` +1. MUST map to ServiceException + +This prevents leakage of unknown failures. + +--- + +### 7. Logging + +0. Each layer MUST log BEFORE rethrowing +1. MUST log the categorised exception only + +--- + +### 8. Testing + +#### Orchestration Exception Tests + +0. SHOULD use `[Theory]` +1. MUST cover multiple dependency exception types in a single test +2. MUST avoid duplication + +--- + +### 9. Design Intent + +These rules ensure: + +0. Full abstraction from external systems +1. Stable layer contracts +2. Simplified exposer logic +3. Complete validation visibility at the local exception level + +--- + +## Exception Handling Cross-References (Enforcement) + +### Validation Testing Alignment + +0. All validation tests MUST align with Exception Handling Principles: + - Localisation MUST be verified (no native exceptions exposed) + - Data preservation MUST be verified on local exceptions + - Validation exceptions MUST contain full error details in inner exception + - From processing service layer upwards, validation exceptions and dependency validation exceptions from its dependencies rewrap to [Entity][Layer]DependencyValidationExceptions + - From processing service layer upwards, dependency exceptions and service exceptions from its dependencies rewrap to [Entity][Layer]DependencyExceptions + +1. Validation tests MUST assert: + - Correct local exception type + - Correct categorised exception type + - Inner exception contains validation data (Data dictionary populated) + +--- + +### Foundation Exception Tests (Enforcement) + +0. Tests MUST verify localisation: + - Native exception → Local exception → Categorised exception + +1. Tests MUST verify: + - External exception data is copied to local exception + - Local exception is preserved as inner exception + +2. Tests MUST NOT allow: + - Native exception leakage + - Missing Data dictionary propagation + +--- + +### Processing Exception Tests (Enforcement) + +0. Tests MUST assert: + - Rewrapping into processing-level exception + - Inner exception preservation + +1. Tests SHOULD: + - Use `[Theory]` to test multiple dependency validation exceptions of the same type in one test + - Use `[Theory]` to test multiple dependency exceptions of the same type in one test + - Avoid duplication by testing multiple cases in a single test method + +--- + +### Orchestration Exception Tests (Enforcement) + +0. Tests MUST assert: + - Rewrapping into orchestration-level exception + - Inner exception is preserved (local exception) + - Categorical exception is replaced at orchestration level + +1. Tests SHOULD: + - Use `[Theory]` to test multiple dependency validation exceptions of the same type in one test + - Use `[Theory]` to test multiple dependency exceptions of the same type in one test + - Avoid duplication by testing multiple cases in a single test method + +--- + +### Catch-All Enforcement + +0. Tests MUST verify: + - Unknown exceptions are mapped to ServiceException + - No raw Exception escapes any service layer + +--- + +### Logging Enforcement + +0. Tests MUST verify: + - Logging occurs before exception is thrown + - Logged exception is the categorised exception + +--- + +### Design Integrity Rule + +0. Any violation of exception handling principles MUST be treated as: + - A design defect + - A test failure + - A review blocker diff --git a/.agents/skills/the-standard-testing/contracts/contracts.json b/.agents/skills/the-standard-testing/contracts/contracts.json new file mode 100644 index 0000000..f1bf4b6 --- /dev/null +++ b/.agents/skills/the-standard-testing/contracts/contracts.json @@ -0,0 +1,85 @@ +{ + "skill": "the-standard-testing", + "version": "1.0.0", + + "tdd_cycle": { + "steps": ["FAIL", "PASS", "REFACTOR"], + "fail_commit_requires": "test actually running and failing (verified by test runner)", + "pass_commit_requires": "all relevant tests running and passing", + "refactor_requires": "no behavior change, all tests still passing" + }, + + "test_file_structure": { + "root": "{Entity}ServiceTests.cs", + "logic": "{Entity}ServiceTests.Logic.{Method}.cs", + "validations": "{Entity}ServiceTests.Validations.{Method}.cs", + "exceptions": "{Entity}ServiceTests.Exceptions.{Method}.cs", + "root_contains": ["setup", "mock_declarations", "helper_methods", "filler_configuration"] + }, + + "test_naming": { + "success_case": "Should{Action}Async", + "error_case": "ShouldThrow{ExceptionType}On{Action}If{Condition}AndLogItAsync" + }, + + "validation_test_order": [ + "happy_path", + "structural_validations", + "logical_validations", + "external_validations", + "dependency_validations", + "dependency_exceptions", + "service_exceptions" + ], + + "foundation_add_exact_order": [ + "ShouldAdd{Entity}Async", + "ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync", + "ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync", + "ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync", + "ShouldThrowDependencyValidationExceptionOnAddIfConflictErrorOccursAndLogItAsync", + "ShouldThrowCriticalDependencyExceptionOnAddIfUnauthorizedErrorOccursAndLogItAsync", + "ShouldThrowCriticalDependencyExceptionOnAddIfForbiddenErrorOccursAndLogItAsync", + "ShouldThrowCriticalDependencyExceptionOnAddIfNotFoundErrorOccursAndLogItAsync", + "ShouldThrowCriticalDependencyExceptionOnAddIfUrlNotFoundErrorOccursAndLogItAsync", + "ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccursAndLogItAsync", + "ShouldThrowDependencyExceptionOnAddIfServiceUnavailableErrorOccursAndLogItAsync", + "ShouldThrowCriticalDependencyExceptionOnAddIfHttpRequestErrorOccursAndLogItAsync", + "ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync" + ], + + "required_verifications_per_test": [ + "exact_broker_calls_with_Times.Once_or_Times.Never", + "logging_broker_received_error", + "VerifyNoOtherCalls_on_all_mocks" + ], + + "required_libraries": { + "mocking": "Moq", + "assertions": "FluentAssertions", + "deep_cloning": "DeepCloner", + "random_data": "Tynamix.ObjectFiller", + "exception_comparison": "Xeption.SameExceptionAs", + "test_framework": "xUnit", + "exception_base": "Xeption" + }, + + "patterns": { + "test_style": "GWT (Given / When / Then with inline comments)", + "parameterized_tests": "[Theory] [InlineData]", + "single_tests": "[Fact]", + "random_data": "always use ObjectFiller — never hard-code values", + "test_isolation": "always deep-clone input/expected/actual objects" + }, + + "forbidden": [ + "hard_coded_test_values", + "shared_state_between_input_and_expected", + "missing_VerifyNoOtherCalls", + "missing_logging_verification", + "aggregation_test_with_mock_sequence_order", + "test_written_after_implementation", + "commit_FAIL_without_verified_failure", + "commit_PASS_without_verified_pass" + ] +} diff --git a/.agents/skills/the-standard-testing/examples/bad/example_bad_test.cs b/.agents/skills/the-standard-testing/examples/bad/example_bad_test.cs new file mode 100644 index 0000000..069ccc6 --- /dev/null +++ b/.agents/skills/the-standard-testing/examples/bad/example_bad_test.cs @@ -0,0 +1,90 @@ +// --------------------------------------------------------------- +// BAD EXAMPLE: Non-Standard Test Violations +// Each violation annotated with the rule it breaks. +// --------------------------------------------------------------- + +namespace MyProject.Tests.Bad +{ + public class BadStudentServiceTests + { + // test-001 VIOLATION: Implementation written before test (test doesn't exist first) + // test-002 VIOLATION: Test not verified to fail before FAIL commit + // test-035 VIOLATION: Hard-coded test values instead of randomized data + // test-100 VIOLATION: No GWT pattern comments + // test-104 VIOLATION: Method name doesn't follow Should{Action}Async convention + [Fact] + public async Task TestAddStudent() + { + // Hard-coded data — test-035 VIOLATION + var student = new Student + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + Name = "John" + }; + + // No mock setup — incomplete test + var result = await this.studentService.AddStudentAsync(student); + + // test-102 VIOLATION: Not using FluentAssertions + Assert.NotNull(result); + Assert.Equal(student.Id, result.Id); + + // test-030 VIOLATION: No broker call verification + // test-031 VIOLATION: No VerifyNoOtherCalls() + } + + // test-010 VIOLATION: Error test written before happy path + // test-011 VIOLATION: Structural validation test out of order + [Fact] + public async Task ShouldThrowIfNull() // test-104 VIOLATION: incomplete naming + { + // test-034 VIOLATION: No deep cloning — same reference used for input/expected + var nullStudent = new Student(); // not actually null — wrong test intent + + // test-036 VIOLATION: Using Assert.Equal instead of Xeption.SameExceptionAs + await Assert.ThrowsAsync( // catching base Exception, not specific type + () => this.studentService.AddStudentAsync(nullStudent)); + + // test-032 VIOLATION: Logging not verified + // test-031 VIOLATION: No VerifyNoOtherCalls() + // test-030 VIOLATION: No Times.Never verification for broker + } + + // test-021 VIOLATION: First invalid field throws immediately (not collecting all) + private void BadContinuousValidation(Student student) + { + if (student.Id == Guid.Empty) + throw new Exception("Id is required"); // stops after first field + + if (string.IsNullOrWhiteSpace(student.Name)) + throw new Exception("Name is required"); // never reached if Id fails + + // Should collect ALL errors and throw once at the end + } + + // test-060/test-061 VIOLATION: Aggregation test asserting call order + [Fact] + public async Task ShouldAggregateInOrder() + { + // test-061 VIOLATION: mock sequence in aggregation test + var sequence = new MockSequence(); + this.studentServiceMock.InSequence(sequence).Setup(...); + this.courseServiceMock.InSequence(sequence).Setup(...); + + // Aggregation services have no order contract — this test will be brittle + } + + // test-090/test-093 VIOLATION: Exception tests mixed into Logic file + // (should be in StudentServiceTests.Exceptions.Add.cs) + // test-091 VIOLATION: Helpers defined in a test method, not in root test class + [Fact] + public async Task ShouldThrowDependencyException() + { + // helper inline — should be in root file + Student CreateTestStudent() => new Student { Id = Guid.NewGuid() }; + + var student = CreateTestStudent(); + // ... + } + } +} diff --git a/.agents/skills/the-standard-testing/examples/good/example_foundation_test.cs b/.agents/skills/the-standard-testing/examples/good/example_foundation_test.cs new file mode 100644 index 0000000..cd75d4f --- /dev/null +++ b/.agents/skills/the-standard-testing/examples/good/example_foundation_test.cs @@ -0,0 +1,107 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Foundation Service Test +// File: StudentServiceTests.Logic.Add.cs +// Demonstrates: GWT, fat arrow for setup, randomized data, deep clone, +// exact broker verification, VerifyNoOtherCalls. + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.Students +{ + public partial class StudentServiceTests + { + // test-010: Happy path first + // test-100: GWT pattern + // test-104: Should{Action}Async naming + [Fact] + public async Task ShouldAddStudentAsync() + { + // given + // test-035: Randomized data via ObjectFiller + Student randomStudent = CreateRandomStudent(); + Student inputStudent = randomStudent; + + // test-034: Deep clone to isolate expected from actual + Student storageStudent = inputStudent.DeepClone(); + Student expectedStudent = storageStudent.DeepClone(); + + this.storageBrokerMock.Setup(broker => + broker.InsertStudentAsync(inputStudent)) + .ReturnsAsync(storageStudent); + + // when + Student actualStudent = + await this.studentService.AddStudentAsync(inputStudent); + + // then + // test-102: FluentAssertions + actualStudent.Should().BeEquivalentTo(expectedStudent); + + // test-030: Verify exact broker calls + this.storageBrokerMock.Verify(broker => + broker.InsertStudentAsync(inputStudent), + Times.Once); + + // test-031: No unwanted calls + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// GOOD EXAMPLE: Root test file (StudentServiceTests.cs) +// Contains setup, mocks, and helper methods. +// --------------------------------------------------------------- + +namespace MyProject.Tests.Unit.Services.Foundations.Students +{ + // test-090, test-091: Root file contains setup, mocks, helpers + public partial class StudentServiceTests + { + // test-101: Mock all dependencies using Moq + private readonly Mock storageBrokerMock; + private readonly Mock loggingBrokerMock; + private readonly IStudentService studentService; + + public StudentServiceTests() + { + this.storageBrokerMock = new Mock(); + this.loggingBrokerMock = new Mock(); + + this.studentService = new StudentService( + storageBroker: this.storageBrokerMock.Object, + loggingBroker: this.loggingBrokerMock.Object); + } + + // test-035: Helper using ObjectFiller for random data + private Student CreateRandomStudent() + { + return CreateStudentFiller(dates: GetRandomDateTimeOffset()).Create(); + } + + private static DateTimeOffset GetRandomDateTimeOffset() => + DateTimeOffset.UtcNow.AddDays(GetRandomNegativeNumber()); + + private static int GetRandomNegativeNumber() => + -1 * new IntRange(min: 2, max: 10).GetValue(); + + private static Filler CreateStudentFiller(DateTimeOffset dates) + { + var filler = new Filler(); + + filler.Setup() + .OnType().Use(dates); + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/examples/good/example_validation_test.cs b/.agents/skills/the-standard-testing/examples/good/example_validation_test.cs new file mode 100644 index 0000000..9df1c13 --- /dev/null +++ b/.agents/skills/the-standard-testing/examples/good/example_validation_test.cs @@ -0,0 +1,135 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// GOOD EXAMPLE: Standard-compliant Validation Test +// File: StudentServiceTests.Validations.Add.cs +// Demonstrates: null check (circuit-breaking), invalid fields (continuous), +// exception comparison, no broker calls verification. + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using Xunit; + +namespace MyProject.Tests.Unit.Services.Foundations.Students +{ + public partial class StudentServiceTests + { + // test-011: Structural validation — null entity (circuit-breaking) + // test-104: ShouldThrow{Exception}On{Add}If{Entity}IsNull naming + [Fact] + public async Task ShouldThrowValidationExceptionOnAddIfStudentIsNullAndLogItAsync() + { + // given + Student nullStudent = null; + + var nullStudentException = + new NullStudentException(message: "Student is null."); + + var expectedStudentValidationException = + new StudentValidationException( + message: "Student validation error occurred, fix the errors and try again.", + innerException: nullStudentException); + + // when + ValueTask addStudentTask = + this.studentService.AddStudentAsync(nullStudent); + + StudentValidationException actualStudentValidationException = + await Assert.ThrowsAsync( + addStudentTask.AsTask); + + // then + // test-036: Xeption.SameExceptionAs for exception equality + actualStudentValidationException.Should().BeEquivalentTo( + expectedStudentValidationException); + + // test-032: Verify logging + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedStudentValidationException))), + Times.Once); + + // test-030/test-031: No broker calls when validation fails + this.storageBrokerMock.Verify(broker => + broker.InsertStudentAsync(It.IsAny()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-011: Structural + Logical validation (continuous — all invalid fields collected) + // test-103: [Theory] [InlineData] for parameterized cases + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldThrowValidationExceptionOnAddIfStudentIsInvalidAndLogItAsync( + string invalidStudentName) + { + // given + var invalidStudent = new Student + { + Name = invalidStudentName // triggers invalid name rule + }; + + var invalidStudentException = + new InvalidStudentException( + message: "Invalid student. Please correct the errors and try again."); + + invalidStudentException.AddData( + key: nameof(Student.Id), + values: "Id is required"); + + invalidStudentException.AddData( + key: nameof(Student.Name), + values: "Text is required"); + + invalidStudentException.AddData( + key: nameof(Student.CreatedDate), + values: "Date is required"); + + invalidStudentException.AddData( + key: nameof(Student.UpdatedDate), + values: new[] + { + "Date is required", + $"Date is not the same as {nameof(Student.CreatedDate)}" + }); + + var expectedStudentValidationException = + new StudentValidationException( + message: "Student validation error occurred, fix the errors and try again.", + innerException: invalidStudentException); + + // when + ValueTask addStudentTask = + this.studentService.AddStudentAsync(invalidStudent); + + StudentValidationException actualStudentValidationException = + await Assert.ThrowsAsync( + addStudentTask.AsTask); + + // then + // test-021: Continuous validation — all fields collected + actualStudentValidationException.Should().BeEquivalentTo( + expectedStudentValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expectedStudentValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.InsertStudentAsync(It.IsAny()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-testing/manifest.json b/.agents/skills/the-standard-testing/manifest.json new file mode 100644 index 0000000..349edd0 --- /dev/null +++ b/.agents/skills/the-standard-testing/manifest.json @@ -0,0 +1,88 @@ +{ + "name": "the-standard-testing", + "version": "1.0.0", + "description": "Governs TDD discipline, test file structure, validation testing strategy, exception testing, foundation/processing/orchestration/aggregation service test rules, controller acceptance testing, and UI component testing for Standard-compliant systems.", + "the_standard_version": "v2.13.0", + "skill_version": "v0.3.0.0", + + "inputs": [ + "A service, broker, controller, or UI component that needs tests", + "A test file for review", + "A request to determine what tests should be written next" + ], + + "outputs": [ + "Standard-compliant test files: root + Logic + Validations + Exceptions", + "Correct test implementation order", + "Feedback on test violations with rule references", + "Test code that follows GWT, uses Moq, FluentAssertions, DeepCloner, ObjectFiller" + ], + + "dependencies": [ + "the-standard-core", + "the-standard-architecture" + ], + + "activation": { + "trigger": "writing tests, reviewing tests, deciding test order, verifying TDD compliance", + "note": "Always activate the-standard-core and the-standard-architecture first." + }, + + "required_libraries": [ + "xUnit", + "Moq", + "FluentAssertions", + "DeepCloner", + "Tynamix.ObjectFiller", + "Xeption" + ], + + "validation": { + "required": true, + "files": { + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + } + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "examples": { + "good": [ + "examples/good/example_foundation_test.cs", + "examples/good/example_validation_test.cs" + ], + "bad": [ + "examples/bad/example_bad_test.cs" + ] + }, + "templates": { + "foundations": [ + "templates/foundations/foundation_service_test_template.cs", + "templates/foundations/foundation_test_class_root_template.cs" + ], + "processings": [ + "templates/processings/processing_service_test_template.cs", + "templates/processings/processing_test_class_root_template.cs" + ], + "controllers": { + "unit": [ + "templates/controllers/unit/controller_unit_test_template.cs", + "templates/controllers/unit/controller_unit_test_class_root_template.cs" + ], + "acceptance": [ + "templates/controllers/acceptance/controller_acceptance_test_template.cs", + "templates/controllers/acceptance/controller_acceptance_test_class_root_template.cs" + ] + } + }, + "validations": { + "checklist": "validations/checklist.md", + "anti_patterns": "validations/anti-patterns.md" + }, + "contracts": "contracts/contracts.json" + } +} diff --git a/.agents/skills/the-standard-testing/rules/rules.json b/.agents/skills/the-standard-testing/rules/rules.json new file mode 100644 index 0000000..5dc523b --- /dev/null +++ b/.agents/skills/the-standard-testing/rules/rules.json @@ -0,0 +1,99 @@ +{ + "rules": [ + { "id": "test-001", "category": "tdd", "description": "Write the failing test first. Never write implementation before a failing test exists.", "severity": "error" }, + { "id": "test-002", "category": "tdd", "description": "Verify the test actually fails before committing a FAIL commit.", "severity": "error" }, + { "id": "test-003", "category": "tdd", "description": "Write the minimum implementation required to pass. No more.", "severity": "error" }, + { "id": "test-004", "category": "tdd", "description": "Verify the full relevant test suite passes before committing a PASS commit.", "severity": "error" }, + { "id": "test-005", "category": "tdd", "description": "Refactor without changing behavior. All tests must pass after refactoring.", "severity": "error" }, + { "id": "test-010", "category": "validation-order", "description": "Test happy path first.", "severity": "error" }, + { "id": "test-011", "category": "validation-order", "description": "Test structural validations second (null, empty, whitespace, default).", "severity": "error" }, + { "id": "test-012", "category": "validation-order", "description": "Test logical validations third (business rules).", "severity": "error" }, + { "id": "test-013", "category": "validation-order", "description": "Test external validations fourth (existence checks).", "severity": "error" }, + { "id": "test-014", "category": "validation-order", "description": "Test dependency validations fifth (broker-specific failures).", "severity": "error" }, + { "id": "test-015", "category": "validation-order", "description": "Test dependency exceptions sixth (storage/API errors).", "severity": "error" }, + { "id": "test-016", "category": "validation-order", "description": "Test service exceptions seventh (catch-all unexpected errors).", "severity": "error" }, + { "id": "test-017", "category": "validation-order", "description": "All services must validate their input parameters before executing any business logic.", "severity": "error" }, + { "id": "test-018", "category": "validation-order", "description": "Services must validate output data when it is reused within the same routine.", "severity": "error" }, + { "id": "test-019", "category": "validation-order", "description": "Validation order must be: structural → logical → external → dependency.", "severity": "error" }, + { "id": "test-020", "category": "validation-types", "description": "Structural validations must break immediately (circuit-breaking): null entity throws immediately.", "severity": "error" }, + { "id": "test-021", "category": "validation-types", "description": "Continuous validations must collect ALL invalid fields before throwing.", "severity": "error" }, + { "id": "test-022", "category": "validation-types", "description": "Continuous validations use upsertable exception data (UpsertDataList).", "severity": "error" }, + { "id": "test-023", "category": "validation-types", "description": "Continuous validations use dynamic rules: { Condition = ..., Message = ... }.", "severity": "error" }, + { "id": "test-024", "category": "validation-types", "description": "Hybrid continuous validations validate parent before child properties.", "severity": "error" }, + { "id": "test-025", "category": "validation-types", "description": "Validations must prevent deterministic failures before reaching external dependencies or storage.", "severity": "error" }, + { "id": "test-026", "category": "validation-types", "description": "Validation must not rely on downstream systems (e.g., databases) to enforce constraints.", "severity": "error" }, + { "id": "test-027", "category": "validation-types", "description": "Validation errors must be aggregated and thrown once using a single validation exception.", "severity": "error" }, + { "id": "test-030", "category": "foundation-tests", "description": "Always verify exact broker calls (Times.Once, Times.Never).", "severity": "error" }, + { "id": "test-031", "category": "foundation-tests", "description": "Always end with VerifyNoOtherCalls() on all mocks.", "severity": "error" }, + { "id": "test-032", "category": "foundation-tests", "description": "Always verify the logging broker received the expected error.", "severity": "error" }, + { "id": "test-033", "category": "foundation-tests", "description": "Keep validation and exception behaviors local and explicit.", "severity": "error" }, + { "id": "test-034", "category": "foundation-tests", "description": "Use deep cloning (DeepCloner) to prevent shared state between test objects.", "severity": "error" }, + { "id": "test-035", "category": "foundation-tests", "description": "Use randomized data (Tynamix.ObjectFiller). Never hard-code test values.", "severity": "error" }, + { "id": "test-036", "category": "foundation-tests", "description": "Use Xeption.SameExceptionAs() for exception equality comparison.", "severity": "error" }, + { "id": "test-037", "category": "foundation-tests", "description": "Foundation services are the primary validation boundary and must enforce all deterministic validations.", "severity": "error" }, + { "id": "test-038", "category": "foundation-tests", "description": "Foundation validations must be equal to or stricter than storage constraints.", "severity": "error" }, + { "id": "test-039", "category": "foundation-tests", "description": "Foundation services must validate required fields, length, format, and all persistence constraints.", "severity": "error" }, + { "id": "test-040", "category": "processing-tests", "description": "Test higher-order logic, not primitive broker details.", "severity": "error" }, + { "id": "test-041", "category": "processing-tests", "description": "Only validate fields the processing service actually uses.", "severity": "error" }, + { "id": "test-042", "category": "processing-tests", "description": "Test shifter operations: entity → bool, entity → count.", "severity": "error" }, + { "id": "test-043", "category": "processing-tests", "description": "Test combination operations: retrieve+add (EnsureExists), retrieve+modify (Upsert).", "severity": "error" }, + { "id": "test-044", "category": "processing-tests", "description": "Test exception mapping from foundation to processing exceptions.", "severity": "error" }, + { "id": "test-045", "category": "processing-tests", "description": "Processing services must perform used-data-only validation.", "severity": "error" }, + { "id": "test-046", "category": "processing-tests", "description": "Processing services must validate only data required for their logic.", "severity": "error" }, + { "id": "test-047", "category": "processing-tests", "description": "Processing services must not revalidate full entity constraints handled by foundation services.", "severity": "error" }, + { "id": "test-048", "category": "processing-tests", "description": "Processing services must validate required identifiers and null checks.", "severity": "error" }, + { "id": "test-049", "category": "processing-tests", "description": "Processing services must rely on foundation services for full validation enforcement.", "severity": "error" }, + { "id": "test-050", "category": "orchestration-tests", "description": "Test multi-entity flow combinations.", "severity": "error" }, + { "id": "test-051", "category": "orchestration-tests", "description": "Test call order when flow depends on it.", "severity": "error" }, + { "id": "test-052", "category": "orchestration-tests", "description": "Prefer natural order over mock-sequence style verification.", "severity": "error" }, + { "id": "test-053", "category": "orchestration-tests", "description": "Verify orchestration-level exception wrapping and unwrapping.", "severity": "error" }, + { "id": "test-054", "category": "orchestration-tests", "description": "Orchestration services must perform structural validation only.", "severity": "error" }, + { "id": "test-055", "category": "orchestration-tests", "description": "Orchestration services must validate input existence and required identifiers.", "severity": "error" }, + { "id": "test-056", "category": "orchestration-tests", "description": "Orchestration services must not perform full entity validation.", "severity": "error" }, + { "id": "test-057", "category": "orchestration-tests", "description": "Orchestration services must delegate validation to downstream services.", "severity": "error" }, + { "id": "test-058", "category": "orchestration-tests", "description": "Orchestration services must validate only data required for their logic.", "severity": "error" }, + { "id": "test-060", "category": "aggregation-tests", "description": "Do NOT test dependency call order in aggregation service tests.", "severity": "error" }, + { "id": "test-061", "category": "aggregation-tests", "description": "Do NOT use mock-sequence style order assertions for aggregation services.", "severity": "error" }, + { "id": "test-062", "category": "aggregation-tests", "description": "Test only basic structural validations and exposure-level aggregation behavior.", "severity": "error" }, + { "id": "test-063", "category": "aggregation-tests", "description": "Aggregation services must perform minimal validation.", "severity": "error" }, + { "id": "test-064", "category": "aggregation-tests", "description": "Aggregation services must validate only input existence and required properties.", "severity": "error" }, + { "id": "test-065", "category": "aggregation-tests", "description": "Aggregation services must not perform business or domain validation.", "severity": "error" }, + { "id": "test-070", "category": "controller-tests", "description": "Unit-test every success code mapping.", "severity": "error" }, + { "id": "test-071", "category": "controller-tests", "description": "Unit-test every error code mapping.", "severity": "error" }, + { "id": "test-072", "category": "controller-tests", "description": "Unit-test auth failure mappings.", "severity": "error" }, + { "id": "test-073", "category": "controller-tests", "description": "Acceptance-test every endpoint.", "severity": "error" }, + { "id": "test-074", "category": "controller-tests", "description": "Clean up all test data after acceptance tests.", "severity": "error" }, + { "id": "test-075", "category": "controller-tests", "description": "Emulate external resources not owned by the microservice in acceptance tests.", "severity": "error" }, + { "id": "test-080", "category": "ui-tests", "description": "Core components are test-driven.", "severity": "error" }, + { "id": "test-081", "category": "ui-tests", "description": "Test element existence, properties, and actions for core components.", "severity": "error" }, + { "id": "test-082", "category": "ui-tests", "description": "Test styles when they are part of the component contract.", "severity": "error" }, + { "id": "test-083", "category": "ui-tests", "description": "Core components integrate with exactly one view service.", "severity": "error" }, + { "id": "test-084", "category": "ui-tests", "description": "Base components: test exposed APIs and wrapper behavior.", "severity": "warning" }, + { "id": "test-085", "category": "ui-tests", "description": "Pages generally do not require unit tests.", "severity": "warning" }, + { "id": "test-090", "category": "structure", "description": "Mirror the partial-class split in tests: root + Logic + Validations + Exceptions.", "severity": "error" }, + { "id": "test-091", "category": "structure", "description": "Root test file contains setup, mocks, and helper methods.", "severity": "error" }, + { "id": "test-092", "category": "structure", "description": "Logic file contains happy-path tests.", "severity": "error" }, + { "id": "test-093", "category": "structure", "description": "Validations file contains validation failure tests.", "severity": "error" }, + { "id": "test-094", "category": "structure", "description": "Exceptions file contains dependency/service exception tests.", "severity": "error" }, + { "id": "test-100", "category": "conventions", "description": "Use GWT pattern: Given / When / Then with inline comments.", "severity": "error" }, + { "id": "test-101", "category": "conventions", "description": "Mock all dependencies using Moq.", "severity": "error" }, + { "id": "test-102", "category": "conventions", "description": "Use FluentAssertions for readable assertions.", "severity": "error" }, + { "id": "test-103", "category": "conventions", "description": "Use xUnit: [Fact] for single cases, [Theory][InlineData] for parameterized.", "severity": "error" }, + { "id": "test-104", "category": "conventions", "description": "Test names: Should{Action}Async or ShouldThrow{Exception}On{Action}If{Condition}AndLogItAsync.", "severity": "error" }, + { "id": "test-110", "category": "implementation-order", "description": "Foundation Add test implementation must follow the exact 14-step order.", "severity": "error" }, + { "id": "test-111", "category": "exception-testing", "description": "All external/native exceptions must be localized into custom exceptions before leaving the service boundary.", "severity": "error" }, + { "id": "test-112", "category": "exception-testing", "description": "Localized exceptions must preserve external/native exceptions as the InnerException on the localized exception.", "severity": "error" }, + { "id": "test-113", "category": "exception-testing", "description": "Localized exceptions must carry the Data collection from the original exception.", "severity": "error" }, + { "id": "test-114", "category": "exception-testing", "description": "Services must only catch dependency-level exceptions relevant to their layer.", "severity": "error" }, + { "id": "test-115", "category": "exception-testing", "description": "Catch-all exceptions must be mapped to ServiceException.", "severity": "error" }, + { "id": "test-116", "category": "exception-testing", "description": "Exceptions must be logged before being thrown when logging is applicable.", "severity": "error" }, + { "id": "test-117", "category": "exception-testing", "description": "Exception handling must use a centralized TryCatch pattern per service.", "severity": "error" }, + { "id": "test-118", "category": "validation-exceptions", "description": "Validation failures must result in ValidationException.", "severity": "error" }, + { "id": "test-119", "category": "validation-exceptions", "description": "Validation exceptions must include aggregated error details in the Data dictionary.", "severity": "error" }, + { "id": "test-120", "category": "validation-exceptions", "description": "Validation exceptions must follow localisation and categorisation rules.", "severity": "error" }, + { "id": "test-121", "category": "exception-testing", "description": "From processing service layer upwards, validation exceptions from dependencies must be rewrapped as [Entity][Layer]DependencyValidationException.", "severity": "error" }, + { "id": "test-122", "category": "exception-testing", "description": "From processing service layer upwards, dependency validation exceptions from dependencies must be rewrapped as [Entity][Layer]DependencyValidationException.", "severity": "error" }, + { "id": "test-123", "category": "exception-testing", "description": "From processing service layer upwards, dependency exceptions from dependencies must be rewrapped as [Entity][Layer]DependencyException.", "severity": "error" }, + { "id": "test-124", "category": "exception-testing", "description": "From processing service layer upwards, service exceptions from dependencies must be rewrapped as [Entity][Layer]DependencyException.", "severity": "error" } + ] +} diff --git a/.agents/skills/the-standard-testing/rules/rules.md b/.agents/skills/the-standard-testing/rules/rules.md new file mode 100644 index 0000000..6d9a133 --- /dev/null +++ b/.agents/skills/the-standard-testing/rules/rules.md @@ -0,0 +1,155 @@ +# The Standard Testing — Rules + +## TDD DOCTRINE + +**test-001** [ERROR] Write the failing test first. Never write implementation before a failing test exists. +**test-002** [ERROR] Verify the test actually fails before committing a FAIL commit. +**test-003** [ERROR] Write the minimum implementation required to pass the test. No more. +**test-004** [ERROR] Verify the full relevant test suite passes before committing a PASS commit. +**test-005** [ERROR] Refactor without changing behavior. All tests must still pass after refactoring. + +## VALIDATION TEST ORDER + +**test-010** [ERROR] Test happy path first. +**test-011** [ERROR] Test structural validations second (null, empty, whitespace, default). +**test-012** [ERROR] Test logical validations third (business rules). +**test-013** [ERROR] Test external validations fourth (existence checks). +**test-014** [ERROR] Test dependency validations fifth (broker-specific failures). +**test-015** [ERROR] Test dependency exceptions sixth (storage/API errors). +**test-016** [ERROR] Test service exceptions seventh (catch-all unexpected errors). +**test-017** [ERROR] All services must validate their input parameters before executing any business logic. +**test-018** [ERROR] Services must validate output data when it is reused within the same routine. +**test-019** [ERROR] Validation order must be: structural → logical → external → dependency. + +## VALIDATION TYPES + +**test-020** [ERROR] Structural validations must break immediately (circuit-breaking): null entity → throw immediately, do not continue. +**test-021** [ERROR] Continuous validations must collect ALL invalid fields before throwing — never throw on the first failure when multiple fields can be invalid. +**test-022** [ERROR] Continuous validations use upsertable exception data (`UpsertDataList`). +**test-023** [ERROR] Continuous validations use dynamic rules: `{ Condition = ..., Message = ... }`. +**test-024** [ERROR] Hybrid continuous validations: validate parent object before validating child properties. +**test-025** [ERROR] Validations must prevent deterministic failures before reaching external dependencies or storage. +**test-026** [ERROR] Validation must not rely on downstream systems (e.g., databases) to enforce constraints. +**test-027** [ERROR] Validation errors must be aggregated and thrown once using a single validation exception. + +## FOUNDATION SERVICE TESTS + +**test-030** [ERROR] Always verify exact broker calls (Times.Once, Times.Never). +**test-031** [ERROR] Always end with `VerifyNoOtherCalls()` on all mocks. +**test-032** [ERROR] Always verify the logging broker received the expected error. +**test-033** [ERROR] Keep validation and exception behaviors local and explicit — no shared helpers. +**test-034** [ERROR] Use deep cloning (DeepCloner) to prevent shared state between input, expected, and actual objects. +**test-035** [ERROR] Use randomized data (Tynamix.ObjectFiller) — never use hard-coded test values. +**test-036** [ERROR] Use `Xeption.SameExceptionAs()` for exception equality comparison. +**test-037** [ERROR] Foundation services are the primary validation boundary and must enforce all deterministic validations. +**test-038** [ERROR] Foundation validations must be equal to or stricter than storage constraints. +**test-039** [ERROR] Foundation services must validate required fields, length, format, and all persistence constraints. + +## PROCESSING SERVICE TESTS + +**test-040** [ERROR] Test higher-order logic, not primitive broker details. +**test-041** [ERROR] Only validate fields the processing service actually uses. +**test-042** [ERROR] Test shifter operations: entity → bool, entity → count. +**test-043** [ERROR] Test combination operations: retrieve+add (EnsureExists), retrieve+modify (Upsert). +**test-044** [ERROR] Test exception mapping from foundation exceptions to processing exceptions. +**test-045** [ERROR] Processing services must perform used-data-only validation. +**test-046** [ERROR] Processing services must validate only data required for their logic. +**test-047** [ERROR] Processing services must not revalidate full entity constraints handled by foundation services. +**test-048** [ERROR] Processing services must validate required identifiers and null checks. +**test-049** [ERROR] Processing services must rely on foundation services for full validation enforcement. + +## ORCHESTRATION SERVICE TESTS + +**test-050** [ERROR] Test multi-entity flow combinations. +**test-051** [ERROR] Test call order when the flow depends on it — use explicit sequence or natural order. +**test-052** [ERROR] Prefer natural order (input/output encoding) over mock-sequence style verification. +**test-053** [ERROR] Verify orchestration-level exception wrapping and unwrapping. +**test-054** [ERROR] Orchestration services must perform structural validation only. +**test-055** [ERROR] Orchestration services must validate input existence and required identifiers. +**test-056** [ERROR] Orchestration services must not perform full entity validation. +**test-057** [ERROR] Orchestration services must delegate validation to downstream services. +**test-058** [ERROR] Orchestration services must validate only data required for their logic. + +## AGGREGATION SERVICE TESTS + +**test-060** [ERROR] Do NOT test dependency call order in aggregation service tests. +**test-061** [ERROR] Do NOT use mock-sequence style order assertions for aggregation services. +**test-062** [ERROR] Test only basic structural validations and the exposure-level aggregation behavior. +**test-063** [ERROR] Aggregation services must perform minimal validation. +**test-064** [ERROR] Aggregation services must validate only input existence and required properties. +**test-065** [ERROR] Aggregation services must not perform business or domain validation. + +## CONTROLLER/PROTOCOL TESTS + +**test-070** [ERROR] Unit-test every success code mapping (200 for GET/PUT/DELETE, 201 for POST). +**test-071** [ERROR] Unit-test every error mapping: Validation → 400, DependencyValidation → 400, CriticalDependency → 500, Dependency → 500, Service → 500. +**test-072** [ERROR] Unit-test authorization/authentication failure mappings. +**test-073** [ERROR] Acceptance-test every endpoint. +**test-074** [ERROR] Clean up all test data after acceptance tests run. +**test-075** [ERROR] Emulate external resources not owned by the microservice during acceptance tests. + +## UI COMPONENT TESTS + +**test-080** [ERROR] Core components are test-driven. +**test-081** [ERROR] Test element existence, properties, and actions for core components. +**test-082** [ERROR] Test styles when they are part of the component contract. +**test-083** [ERROR] Core components integrate with exactly one view service. +**test-084** [WARNING] Base components: test exposed APIs and wrapper behavior, not internal implementation. +**test-085** [WARNING] Pages generally do not require unit tests. + +## TEST FILE STRUCTURE + +**test-090** [ERROR] Mirror the partial-class split in tests: root + Logic + Validations + Exceptions files. +**test-091** [ERROR] Root test file (`{Entity}ServiceTests.cs`) contains setup, mocks, and helper methods. +**test-092** [ERROR] `{Entity}ServiceTests.Logic.{Method}.cs` contains happy-path tests. +**test-093** [ERROR] `{Entity}ServiceTests.Validations.{Method}.cs` contains validation failure tests. +**test-094** [ERROR] `{Entity}ServiceTests.Exceptions.{Method}.cs` contains dependency/service exception tests. + +## TEST CONVENTIONS + +**test-100** [ERROR] Use GWT pattern: Given / When / Then with inline comments. +**test-101** [ERROR] Mock all dependencies using Moq. +**test-102** [ERROR] Use FluentAssertions for readable assertions (`Should().BeEquivalentTo()`). +**test-103** [ERROR] Use xUnit: `[Fact]` for single cases, `[Theory] [InlineData]` for parameterized cases. +**test-104** [ERROR] Test method names: `Should{Action}Async` for success cases; `ShouldThrow{Exception}On{Action}If{Condition}AndLogItAsync` for error cases. + +## EXACT IMPLEMENTATION ORDER (Foundation Service Add) + +**test-110** [ERROR] Test implementation order for Add must follow: + 0. `ShouldAdd{Entity}Async` + 1. `ShouldThrowValidationExceptionOnAddIf{Entity}IsNullAndLogItAsync` + 2. `ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalidAndLogItAsync` + 3. `ShouldThrowValidationExceptionOnAddIf{Entity}HasIsInvalidLengthPropertiesAndLogItAsync` (if applicable) + 4. `ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync` + 5. `ShouldThrowDependencyValidationExceptionOnAddIfConflictErrorOccursAndLogItAsync` + 6. `ShouldThrowCriticalDependencyExceptionOnAddIfUnauthorizedErrorOccursAndLogItAsync` + 7. `ShouldThrowCriticalDependencyExceptionOnAddIfForbiddenErrorOccursAndLogItAsync` + 8. `ShouldThrowCriticalDependencyExceptionOnAddIfNotFoundErrorOccursAndLogItAsync` + 9. `ShouldThrowCriticalDependencyExceptionOnAddIfUrlNotFoundErrorOccursAndLogItAsync` + 10. `ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccursAndLogItAsync` + 11. `ShouldThrowDependencyExceptionOnAddIfServiceUnavailableErrorOccursAndLogItAsync` + 12. `ShouldThrowCriticalDependencyExceptionOnAddIfHttpRequestErrorOccursAndLogItAsync` + 13. `ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync` + +**Note:** Similar patterns with layer-appropriate variations apply to other methods (Modify, Remove, Retrieve) and other service layers (Processing, Orchestration, Aggregation). + + +## EXCEPTION TESTING + +**test-111** [ERROR] All external/native exceptions must be localized into custom exceptions before leaving the service boundary. +**test-112** [ERROR] Localized exceptions must preserve external/native exceptions as the InnerException on the localized exception. +**test-113** [ERROR] Localized exceptions must carry the Data collection from the original exception. +**test-114** [ERROR] Services must only catch dependency-level exceptions relevant to their layer. +**test-115** [ERROR] Catch-all exceptions must be mapped to ServiceException. +**test-116** [ERROR] Exceptions must be logged before being thrown when logging is applicable. +**test-117** [ERROR] Exception handling must use a centralized TryCatch pattern per service. +**test-121** [ERROR] From processing service layer upwards, validation exceptions from dependencies must be rewrapped as `[Entity][Layer]DependencyValidationException`. +**test-122** [ERROR] From processing service layer upwards, dependency validation exceptions from dependencies must be rewrapped as `[Entity][Layer]DependencyValidationException`. +**test-123** [ERROR] From processing service layer upwards, dependency exceptions from dependencies must be rewrapped as `[Entity][Layer]DependencyException`. +**test-124** [ERROR] From processing service layer upwards, service exceptions from dependencies must be rewrapped as `[Entity][Layer]DependencyException`. + +## VALIDATION EXCEPTIONS + +**test-118** [ERROR] Validation failures must result in ValidationException. +**test-119** [ERROR] Validation exceptions must include aggregated error details in the Data dictionary. +**test-120** [ERROR] Validation exceptions must follow localisation and categorisation rules. diff --git a/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_class_root_template.cs b/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_class_root_template.cs new file mode 100644 index 0000000..325fff3 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_class_root_template.cs @@ -0,0 +1,92 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Root test class for Controller Acceptance Tests +// File: [Entity]ApiTests.cs +// Replace [Entity] / [Entities] / [Namespace] with actual values. + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using [Namespace].Tests.Acceptance.Brokers; +using [Namespace].Tests.Acceptance.Models.[Entities]; +using Tynamix.ObjectFiller; + +namespace [Namespace].Tests.Acceptance.Apis.[Entities] +{ + // test-073: Acceptance-test every endpoint + [Collection(nameof(ApiTestCollection))] + public partial class [Entity]ApiTests + { + private readonly ApiBroker apiBroker; + + public [Entity]ApiTests(ApiBroker apiBroker) => + this.apiBroker = apiBroker; + + // test-035: Randomized data helper using ObjectFiller + private int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static DateTimeOffset GetRandomDateTime() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static string GetRandomStringWithLengthOf(int length) + { + string result = new MnemonicString(wordCount: 1, wordMinLength: length, wordMaxLength: length).GetValue(); + + return result.Length > length ? result.Substring(0, length) : result; + } + + private static [Entity] Update[Entity]WithRandomValues([Entity] input[Entity]) + { + DateTimeOffset now = DateTimeOffset.UtcNow; + var updated[Entity] = CreateRandom[Entity](); + updated[Entity].Id = input[Entity].Id; + updated[Entity].EntraId = input[Entity].EntraId; + updated[Entity].CreatedDate = input[Entity].CreatedDate; + updated[Entity].CreatedBy = input[Entity].CreatedBy; + updated[Entity].UpdatedDate = now; + + return updated[Entity]; + } + + // test-074: Clean up all test data after acceptance tests + private async ValueTask<[Entity]> PostRandom[Entity]Async() + { + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] created[Entity] = await this.apiBroker.Post[Entity]Async(random[Entity]); + + return created[Entity]; + } + + private async ValueTask> PostRandom[Entities]Async() + { + int randomNumber = GetRandomNumber(); + var random[Entities] = new List<[Entity]>(); + + for (int i = 0; i < randomNumber; i++) + { + random[Entities].Add(await PostRandom[Entity]Async()); + } + + return random[Entities]; + } + + private static [Entity] CreateRandom[Entity]() => + CreateRandom[Entity]Filler().Create(); + + private static Filler<[Entity]> CreateRandom[Entity]Filler() + { + string user = Guid.NewGuid().ToString(); + DateTime now = DateTime.UtcNow; + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(now); + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_template.cs b/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_template.cs new file mode 100644 index 0000000..0551b4d --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/controllers/acceptance/controller_acceptance_test_template.cs @@ -0,0 +1,55 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Controller Acceptance Tests +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Acceptance test for controller endpoints. + +// --------------------------------------------------------------- +// File: [Entity]ApiTests.GetAll.cs +// test-073: Acceptance-test every endpoint +// --------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using [Namespace].Tests.Acceptance.Models.[Entities]; + +namespace [Namespace].Tests.Acceptance.Apis.[Entities] +{ + public partial class [Entity]ApiTests + { + // test-073: Acceptance-test every endpoint + // test-100: GWT pattern with inline comments + [Fact] + public async Task ShouldGetAll[Entities]Async() + { + // given + List<[Entity]> random[Entities] = await PostRandom[Entities]Async(); + List<[Entity]> expected[Entities] = random[Entities]; + + // when + List<[Entity]> actual[Entities] = await this.apiBroker.GetAll[Entities]Async(); + + // then + foreach ([Entity] expected[Entity] in expected[Entities]) + { + [Entity] actual[Entity] = + actual[Entities].Single(approval => approval.Id == expected[Entity].Id); + + // test-102: Use FluentAssertions for readable assertions + actual[Entity].Should().BeEquivalentTo(expected[Entity], options => options + .Excluding(property => property.CreatedBy) + .Excluding(property => property.CreatedDate) + .Excluding(property => property.UpdatedBy) + .Excluding(property => property.UpdatedDate)); + + // test-074: Clean up all test data after acceptance tests + await this.apiBroker.Delete[Entity]ByIdAsync(actual[Entity].Id); + } + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_class_root_template.cs b/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_class_root_template.cs new file mode 100644 index 0000000..c65bbe0 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_class_root_template.cs @@ -0,0 +1,113 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Root test class for a Controller +// File: [Entities]ControllerTests.cs +// Replace [Entity] / [Entities] / [Namespace] with actual values. + +using System; +using System.Linq; +using [Namespace].Models.Foundations.[Entities]; +using [Namespace].Models.Foundations.[Entities].Exceptions; +using [Namespace].Services.Foundations.[Entities]; +using [Namespace].Controllers; +using Moq; +using RESTFulSense.Controllers; +using Tynamix.ObjectFiller; +using Xeptions; + +namespace [Namespace].Tests.Unit.Controllers.[Entities] +{ + // test-090, test-091: Root file — setup, mocks, helpers + public partial class [Entities]ControllerTests : RESTFulController + { + // test-101: Mock all dependencies with Moq + private readonly Mock [entity]ServiceMock; + private readonly [Entities]Controller [entities]Controller; + + public [Entities]ControllerTests() + { + [entity]ServiceMock = new Mock(); + [entities]Controller = new [Entities]Controller([entity]ServiceMock.Object); + } + + // test-071: Unit-test every error mapping + // Validation → 400, DependencyValidation → 400 + public static TheoryData ValidationExceptions() + { + var someInnerException = new Xeption(); + string someMessage = GetRandomString(); + + return new TheoryData + { + new [Entity]ValidationException( + message: someMessage, + innerException: someInnerException), + + new [Entity]DependencyValidationException( + message: someMessage, + innerException: someInnerException) + }; + } + + // test-071: Unit-test every error mapping + // CriticalDependency → 500, Dependency → 500, Service → 500 + public static TheoryData ServerExceptions() + { + var someInnerException = new Xeption(); + string someMessage = GetRandomString(); + + return new TheoryData + { + new [Entity]DependencyException( + message: someMessage, + innerException: someInnerException), + + new [Entity]ServiceException( + message: someMessage, + innerException: someInnerException) + }; + } + + // test-035: Randomized data helper using ObjectFiller + private static string GetRandomString() => + new MnemonicString(wordCount: GetRandomNumber()).GetValue(); + + private static string GetRandomStringWithLengthOf(int length) + { + string result = new MnemonicString(wordCount: 1, wordMinLength: length, wordMaxLength: length).GetValue(); + + return result.Length > length ? result.Substring(0, length) : result; + } + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static [Entity] CreateRandom[Entity]() => + Create[Entity]Filler().Create(); + + private static IQueryable<[Entity]> CreateRandom[Entities]() + { + return Create[Entity]Filler() + .Create(count: GetRandomNumber()) + .AsQueryable(); + } + + private static Filler<[Entity]> Create[Entity]Filler() + { + DateTimeOffset dateTimeOffset = DateTimeOffset.UtcNow; + string user = Guid.NewGuid().ToString(); + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(dateTimeOffset); + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_template.cs b/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_template.cs new file mode 100644 index 0000000..1b67e37 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/controllers/unit/controller_unit_test_template.cs @@ -0,0 +1,136 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Controller Unit Tests — Logic and Security +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Logic (success code mapping) and Security tests. + +// --------------------------------------------------------------- +// File: [Entities]ControllerTests.Logic.Get.cs +// test-070: Unit-test every success code mapping +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Controllers.[Entities] +{ + public partial class [Entities]ControllerTests + { + // test-070: Unit-test every success code mapping (200 for GET/PUT/DELETE, 201 for POST) + // test-100: GWT pattern with inline comments + // test-030: Always verify exact service calls + [Fact] + public async Task ShouldReturnRecordOnGetByIdsAsync() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + Guid inputId = random[Entity].Id; + [Entity] storage[Entity] = random[Entity]; + [Entity] expected[Entity] = storage[Entity].DeepClone(); + + var expectedObjectResult = + new OkObjectResult(expected[Entity]); + + var expectedActionResult = + new ActionResult<[Entity]>(expectedObjectResult); + + [entity]ServiceMock + .Setup(service => service.Retrieve[Entity]ByIdAsync(It.IsAny())) + .ReturnsAsync(storage[Entity]); + + // when + ActionResult<[Entity]> actualActionResult = await [entities]Controller.Get[Entity]ByIdAsync(inputId); + + // then + actualActionResult.ShouldBeEquivalentTo(expectedActionResult); + + // test-030: Verify exact service calls (Times.Once) + [entity]ServiceMock + .Verify(service => service.Retrieve[Entity]ByIdAsync(It.IsAny()), + Times.Once); + + // test-031: Always end with VerifyNoOtherCalls() + [entity]ServiceMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entities]ControllerTests.Security.Get.cs +// test-072: Unit-test authorization/authentication failure mappings +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Controllers.[Entities] +{ + public partial class [Entities]ControllerTests + { + // test-072: Unit-test auth failure mappings + // test-100: GWT pattern with inline comments + [Fact] + public void GetShouldHaveRoleAttributeWithRoles() + { + // Given + var controllerType = typeof([Entities]Controller); + var methodInfo = controllerType.GetMethod("Get[Entity]ByIdAsync"); + Type attributeType = typeof(AuthorizeAttribute); + string attributeProperty = "Roles"; + + List expectedAttributeValues = new List + { + "Administrators,[Entity]Write" + }; + + // When + var methodAttribute = methodInfo? + .GetCustomAttributes(attributeType, inherit: true) + .FirstOrDefault(); + + var controllerAttribute = controllerType + .GetCustomAttributes(attributeType, inherit: true) + .FirstOrDefault(); + + var attribute = methodAttribute ?? controllerAttribute; + + // Then + attribute.Should().NotBeNull(); + + var actualAttributeValue = attributeType + .GetProperty(attributeProperty)? + .GetValue(attribute) as string ?? string.Empty; + + var actualAttributeValues = actualAttributeValue? + .Split(',') + .Select(role => role.Trim()) + .Where(role => !string.IsNullOrEmpty(role)) + .ToList(); + + // test-102: Use FluentAssertions for readable assertions + actualAttributeValues.Should().BeEquivalentTo(expectedAttributeValues); + } + + // test-072: Unit-test auth failure mappings + [Fact] + public void GetShouldNotHaveInvisibleApiAttribute() + { + // Given + var controllerType = typeof([Entities]Controller); + var methodInfo = controllerType.GetMethod("Get[Entity]ByIdAsync"); + Type attributeType = typeof(InvisibleApiAttribute); + + // When + var methodAttribute = methodInfo? + .GetCustomAttributes(attributeType, inherit: true) + .FirstOrDefault(); + + var controllerAttribute = controllerType + .GetCustomAttributes(attributeType, inherit: true) + .FirstOrDefault(); + + var attribute = methodAttribute ?? controllerAttribute; + + // Then + // test-102: Use FluentAssertions for readable assertions + attribute.Should().BeNull(); + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/foundations/foundation_service_test_template.cs b/.agents/skills/the-standard-testing/templates/foundations/foundation_service_test_template.cs new file mode 100644 index 0000000..adbeaa4 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/foundations/foundation_service_test_template.cs @@ -0,0 +1,282 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Foundation Service Tests — All three axes +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Logic, Validations, and Exceptions files. + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Logic.Add.cs +// test-092: Happy-path tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-010: Happy path first + // test-110: Step 0 — ShouldAdd{Entity}Async + [Fact] + public async Task ShouldAdd[Entity]Async() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + [Entity] storage[Entity] = input[Entity].DeepClone(); + [Entity] expected[Entity] = storage[Entity].DeepClone(); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(input[Entity])) + .ReturnsAsync(storage[Entity]); + + // when + [Entity] actual[Entity] = + await this.[entity]Service.Add[Entity]Async(input[Entity]); + + // then + actual[Entity].Should().BeEquivalentTo(expected[Entity]); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(input[Entity]), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Validations.Add.cs +// test-093: Validation failure tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-110: Step 1 — ShouldThrowValidationExceptionOnAddIf{Entity}IsNull + [Fact] + public async Task ShouldThrowValidationExceptionOnAddIf[Entity]IsNullAndLogItAsync() + { + // given + [Entity] null[Entity] = null; + + var null[Entity]Exception = + new Null[Entity]Exception(message: "[Entity] is null."); + + var expected[Entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: null[Entity]Exception); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(null[Entity]); + + [Entity]ValidationException actual[Entity]ValidationException = + await Assert.ThrowsAsync<[Entity]ValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ValidationException.Should() + .BeEquivalentTo(expected[Entity]ValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(It.IsAny<[Entity]>()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-110: Step 2 — ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalid + // test-103: [Theory][InlineData] for parameterized + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldThrowValidationExceptionOnAddIf[Entity]IsInvalidAndLogItAsync( + string invalidText) + { + // given + var invalid[Entity] = new [Entity] + { + Name = invalidText + }; + + var invalid[Entity]Exception = + new Invalid[Entity]Exception( + message: "Invalid [entity]. Please correct the errors and try again."); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].Id), + values: "Id is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].Name), + values: "Text is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].CreatedDate), + values: "Date is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].UpdatedDate), + values: new[] + { + "Date is required", + $"Date is not the same as {nameof([Entity].CreatedDate)}" + }); + + var expected[Entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: invalid[Entity]Exception); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(invalid[Entity]); + + [Entity]ValidationException actual[Entity]ValidationException = + await Assert.ThrowsAsync<[Entity]ValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ValidationException.Should() + .BeEquivalentTo(expected[Entity]ValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(It.IsAny<[Entity]>()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Exceptions.Add.cs +// test-094: Dependency and service exception tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-110: Step 3 — DependencyValidationException (BadRequest) + [Fact] + public async Task ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync() + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + var httpResponseBadRequestException = new HttpResponseBadRequestException(); + + httpResponseBadRequestException.Data.Add( + key: nameof([Entity].Id), + values: "Id is required"); + + //**test-111** [ERROR] All external/native exceptions must be localized. + //**test-112** [ERROR] Localized exceptions must preserve external/native exceptions as the InnerException. + //**test-113** [ERROR] Localized exceptions must carry the Data collection from the original exception. + var invalidPost[Entity]Exception = + new Invalid[Entity]Exception( + message: "Invalid [entity] error occurred, fix errors and try again.", + innerException: httpResponseBadRequestException, + data: httpResponseBadRequestException.Data); + + var expected[Entity]DependencyValidationException = + new [Entity]DependencyValidationException( + message: "[Entity] dependency validation error occurred, fix the errors.", + innerException: invalidPost[Entity]Exception); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(some[Entity])) + .ThrowsAsync(httpResponseBadRequestException); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(some[Entity]); + + [Entity]DependencyValidationException actual[Entity]DependencyValidationException = + await Assert.ThrowsAsync<[Entity]DependencyValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]DependencyValidationException.Should() + .BeEquivalentTo(expected[Entity]DependencyValidationException); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(some[Entity]), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]DependencyValidationException))), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-110: Step 12 — ServiceException (catch-all) + [Fact] + public async Task ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync() + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + var serviceException = new Exception(); + + var failed[Entity]ServiceException = + new Failed[Entity]ServiceException( + message: "Unexpected service error occurred. Contact support.", + innerException: serviceException); + + var expected[Entity]ServiceException = + new [Entity]ServiceException( + message: "[Entity] service error occurred, contact support.", + innerException: failed[Entity]ServiceException); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(some[Entity])) + .ThrowsAsync(serviceException); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(some[Entity]); + + [Entity]ServiceException actual[Entity]ServiceException = + await Assert.ThrowsAsync<[Entity]ServiceException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ServiceException.Should() + .BeEquivalentTo(expected[Entity]ServiceException); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(some[Entity]), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ServiceException))), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/foundations/foundation_test_class_root_template.cs b/.agents/skills/the-standard-testing/templates/foundations/foundation_test_class_root_template.cs new file mode 100644 index 0000000..02fa345 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/foundations/foundation_test_class_root_template.cs @@ -0,0 +1,72 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Root test class for a Foundation Service +// File: [Entity]ServiceTests.cs +// Replace [Entity] / [Entities] / [Namespace] with actual values. + +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Tynamix.ObjectFiller; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + // test-090, test-091: Root file — setup, mocks, helpers + public partial class [Entity]ServiceTests + { + // test-101: Mock all dependencies with Moq + private readonly Mock storageBrokerMock; + private readonly Mock loggingBrokerMock; + private readonly I[Entity]Service [entity]Service; + + public [Entity]ServiceTests() + { + this.storageBrokerMock = new Mock(); + this.loggingBrokerMock = new Mock(); + + this.[entity]Service = new [Entity]Service( + storageBroker: this.storageBrokerMock.Object, + loggingBroker: this.loggingBrokerMock.Object); + } + + // test-035: Randomized data helper using ObjectFiller + private static [Entity] CreateRandom[Entity]() => + Create[Entity]Filler(dateTimeOffset: GetRandomDateTimeOffset()).Create(); + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static int GetRandomNegativeNumber() => + -1 * GetRandomNumber(); + + // test-036: Exception equality expression helper + private static Expression> SameExceptionAs(Exception expectedException) + { + return actualException => + actualException.Message == expectedException.Message + && actualException.InnerException.Message == expectedException.InnerException.Message; + } + + private static Filler<[Entity]> Create[Entity]Filler(DateTimeOffset dateTimeOffset) + { + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(dateTimeOffset); + // Add .OnProperty([entity] => [entity].NavigationProperty).IgnoreIt(); for navigation properties + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/processings/processing_service_test_template.cs b/.agents/skills/the-standard-testing/templates/processings/processing_service_test_template.cs new file mode 100644 index 0000000..5b5c61d --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/processings/processing_service_test_template.cs @@ -0,0 +1,351 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Processing Service Tests — All three axes +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Logic, Validations, and Exceptions files. + +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Force.DeepCloner; +using [Namespace].Models.Foundations.[Entities]; +using [Namespace].Models.Processings.[Entities].Exceptions; +using Moq; +using Xeptions; +using Xunit; + +// --------------------------------------------------------------- +// File: [Entity]ProcessingServiceTests.Logic.cs +// test-092: Happy-path tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Processings.[Entities] +{ + public partial class [Entity]ProcessingServiceTests + { + // test-010: Happy path first + // test-040: Processing service happy path + // test-043: Combination operations (retrieve+add, retrieve+modify) + [Fact] + public async Task ShouldModify[Entity]IfOneExistsAndNotAddAsync() + { + // Given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] storage[Entity] = random[Entity]; + [Entity] modified[Entity] = storage[Entity].DeepClone(); + modified[Entity].UpdatedDate = DateTimeOffset.UtcNow; + [Entity] updated[Entity] = modified[Entity].DeepClone(); + [Entity] expected[Entity] = updated[Entity]; + + this.[entity]ServiceMock.Setup(service => + service.Retrieve[Entity]ByIdAsync(modified[Entity].Id)) + .ReturnsAsync(value: storage[Entity]); + + this.[entity]ServiceMock.Setup(service => + service.Modify[Entity]Async(modified[Entity])) + .ReturnsAsync(value: updated[Entity]); + + // When + [Entity] actual[Entity] = await this.[entity]ProcessingService + .ModifyOrAdd[Entity]Async(modified[Entity]); + + // Then + actual[Entity].Should().BeEquivalentTo(expected[Entity]); + + this.[entity]ServiceMock.Verify(service => + service.Retrieve[Entity]ByIdAsync(modified[Entity].Id), + Times.Once); + + this.[entity]ServiceMock.Verify(service => + service.Modify[Entity]Async(modified[Entity]), + Times.Once); + + this.[entity]ServiceMock.Verify(service => + service.Add[Entity]Async(modified[Entity]), + Times.Never); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-010: Happy path first + // test-040: Processing service happy path + // test-043: Combination operations (retrieve+add, retrieve+modify) + [Fact] + public async Task ShouldAdd[Entity]If[Entity]DoesNotExistsAsync() + { + // Given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + [Entity] storage[Entity] = input[Entity].DeepClone(); + [Entity] expected[Entity] = storage[Entity]; + + this.[entity]ServiceMock.Setup(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id)) + .ReturnsAsync(value: null); + + this.[entity]ServiceMock.Setup(service => + service.Add[Entity]Async(input[Entity])) + .ReturnsAsync(value: storage[Entity]); + + // When + await this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(input[Entity]); + + // Then + this.[entity]ServiceMock.Verify(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id), + Times.Once); + + this.[entity]ServiceMock.Verify(service => + service.Add[Entity]Async(input[Entity]), + Times.Once); + + this.[entity]ServiceMock.Verify(service => + service.Modify[Entity]Async(input[Entity]), + Times.Never); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ProcessingServiceTests.Exceptions.cs +// test-094: Exception mapping tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Processings.[Entities] +{ + public partial class [Entity]ProcessingServiceTests + { + // test-015: Processing layer maps Foundation DependencyValidation → Processing DependencyValidation + // test-044: Processing exception mapping (Foundation → Processing) + [Theory] + [MemberData(nameof(DependencyValidationExceptions))] + public async Task ShouldThrowDependencyValidationExceptionOnModifyOrAddIfErrorOccursAndLogItAsync( + Xeption dependencyValidationException) + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = some[Entity]; + + var expected[Entity]ProcessingDependencyValidationException = + new [Entity]ProcessingDependencyValidationException( + message: "[Entity] processing dependency validation error occurred, please try again.", + innerException: dependencyValidationException.InnerException as Xeption); + + this.[entity]ServiceMock.Setup(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id)) + .ThrowsAsync(dependencyValidationException); + + // when + ValueTask<[Entity]> [entity]ModifyOrAddTask = + this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(input[Entity]); + + [Entity]ProcessingDependencyValidationException actualException = + await Assert.ThrowsAsync<[Entity]ProcessingDependencyValidationException>( + [entity]ModifyOrAddTask.AsTask); + + // then + actualException.Should().BeEquivalentTo(expected[Entity]ProcessingDependencyValidationException); + + this.[entity]ServiceMock.Verify(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is(SameExceptionAs( + expected[Entity]ProcessingDependencyValidationException))), + Times.Once); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-016: Processing layer maps Foundation Dependency → Processing Dependency + // test-044: Processing exception mapping (Foundation → Processing) + [Theory] + [MemberData(nameof(DependencyExceptions))] + public async Task ShouldThrowDependencyExceptionOnModifyOrAddIfDependencyErrorOccursAndLogItAsync( + Xeption dependencyException) + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = some[Entity]; + + var expected[Entity]ProcessingDependencyException = + new [Entity]ProcessingDependencyException( + message: "[Entity] processing dependency error occurred, please try again.", + innerException: dependencyException.InnerException as Xeption); + + this.[entity]ServiceMock.Setup(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id)) + .ThrowsAsync(dependencyException); + + // when + ValueTask<[Entity]> [entity]ModifyOrAddTask = + this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(input[Entity]); + + [Entity]ProcessingDependencyException actualException = + await Assert.ThrowsAsync<[Entity]ProcessingDependencyException>( + [entity]ModifyOrAddTask.AsTask); + + // then + actualException.Should().BeEquivalentTo(expected[Entity]ProcessingDependencyException); + + this.[entity]ServiceMock.Verify(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is(SameExceptionAs( + expected[Entity]ProcessingDependencyException))), + Times.Once); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-017: Processing layer wraps unexpected exceptions in ServiceException + // test-044: Processing exception mapping (Foundation → Processing) + [Fact] + public async Task ShouldThrowServiceExceptionOnModifyOrAddIfServiceErrorOccursAsync() + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = some[Entity]; + + var serviceException = new Exception(); + + var failed[Entity]ProcessingServiceException = + new Failed[Entity]ProcessingServiceException( + message: "Failed [Entity] processing service error occurred, please contact support.", + innerException: serviceException); + + var expected[Entity]ProcessingServiveException = + new [Entity]ProcessingServiceException( + message: "[Entity] processing service error occurred, please contact support.", + innerException: failed[Entity]ProcessingServiceException); + + this.[entity]ServiceMock.Setup(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id)) + .ThrowsAsync(serviceException); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(input[Entity]); + + [Entity]ProcessingServiceException actualException = + await Assert.ThrowsAsync<[Entity]ProcessingServiceException>(add[Entity]Task.AsTask); + + // then + actualException.Should().BeEquivalentTo(expected[Entity]ProcessingServiveException); + + this.[entity]ServiceMock.Verify(service => + service.Retrieve[Entity]ByIdAsync(input[Entity].Id), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is(SameExceptionAs( + expected[Entity]ProcessingServiveException))), + Times.Once); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ProcessingServiceTests.Validations.cs +// test-093: Validation failure tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Processings.[Entities] +{ + public partial class [Entity]ProcessingServiceTests + { + // test-011: Null validation + // test-020: Null checks before property validation + // test-041: Processing service validates null input + [Fact] + public async Task ShouldThrowValidationExceptionsOnModifyOrAddIf[Entity]ProcessingIsNullAndLogItAsync() + { + // given + [Entity] null[Entity] = null; + + var null[Entity]ProcessingException = + new Null[Entity]ProcessingException(message: "[Entity] is null."); + + var expected[Entity]ProcessingValidationException = + new [Entity]ProcessingValidationException( + message: "[Entity] processing validation error occurred, please try again.", + innerException: null[Entity]ProcessingException); + + // when + ValueTask<[Entity]> Add[Entity]Task = + this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(null[Entity]); + + [Entity]ProcessingValidationException actual[Entity]ProcessingValidationException = + await Assert.ThrowsAsync<[Entity]ProcessingValidationException>(Add[Entity]Task.AsTask); + + //then + actual[Entity]ProcessingValidationException.Should() + .BeEquivalentTo(expected[Entity]ProcessingValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is(SameExceptionAs( + expected[Entity]ProcessingValidationException))), + Times.Once); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-012: Property validation + // test-027: Property validation with AddData + // test-041: Processing service validates null input + [Fact] + public async Task ShouldThrowValidationExceptionsOnModifyOrAddIfIdIsInvalidAndLogItAsync() + { + // given + [Entity] invalid[Entity] = new [Entity](); + + var invalidArgument[Entity]ProcessingException = + new InvalidArgument[Entity]ProcessingException( + message: "Invalid argument(s). Please correct the errors and try again."); + + invalidArgument[Entity]ProcessingException.AddData( + key: "Id", + values: "Id is required"); + + var expected[Entity]ProcessingValidationException = + new [Entity]ProcessingValidationException( + message: "[Entity] processing validation error occurred, please try again.", + innerException: invalidArgument[Entity]ProcessingException); + + // when + ValueTask<[Entity]> Retrieve[Entity]Task = + this.[entity]ProcessingService.ModifyOrAdd[Entity]Async(invalid[Entity]); + + [Entity]ProcessingValidationException actual[Entity]ProcessingValidationException = + await Assert.ThrowsAsync<[Entity]ProcessingValidationException>(Retrieve[Entity]Task.AsTask); + + //then + actual[Entity]ProcessingValidationException.Should() + .BeEquivalentTo(expected[Entity]ProcessingValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogErrorAsync(It.Is(SameExceptionAs( + expected[Entity]ProcessingValidationException))), + Times.Once); + + this.[entity]ServiceMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/processings/processing_test_class_root_template.cs b/.agents/skills/the-standard-testing/templates/processings/processing_test_class_root_template.cs new file mode 100644 index 0000000..80093fd --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/processings/processing_test_class_root_template.cs @@ -0,0 +1,111 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Processing Service Test Root Class +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// This is the base class for all processing service tests. + +using System; +using System.Linq; +using System.Linq.Expressions; +using [Namespace].Brokers.Loggings; +using [Namespace].Models.Foundations.[Entities]; +using [Namespace].Models.Foundations.[Entities].Exceptions; +using [Namespace].Services.Foundations.[Entities]; +using [Namespace].Services.Processings.[Entities]; +using Moq; +using Tynamix.ObjectFiller; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Processings.[Entities] +{ + public partial class [Entity]ProcessingServiceTests + { + private readonly Mock [entity]ServiceMock = new Mock(); + private readonly Mock loggingBrokerMock = new Mock(); + private readonly I[Entity]ProcessingService [entity]ProcessingService; + + public [Entity]ProcessingServiceTests() + { + [entity]ProcessingService = new [Entity]ProcessingService( + [entity]Service: [entity]ServiceMock.Object, + loggingBroker: loggingBrokerMock.Object); + } + + public static TheoryData DependencyValidationExceptions() + { + string randomMessage = GetRandomString(); + string exceptionMessage = randomMessage; + var innerException = new Xeption(exceptionMessage); + + innerException.Data.Add( + key: nameof([Entity].Id), + values: "Id is required"); + + return new TheoryData + { + new [Entity]ValidationException( + message: "[Entity] validation errors occurred, please try again.", innerException), + + new [Entity]DependencyValidationException( + message: "[Entity] dependency validation occurred, please try again.", innerException) + }; + } + + public static TheoryData DependencyExceptions() + { + string randomMessage = GetRandomString(); + string exceptionMessage = randomMessage; + var innerException = new Xeption(exceptionMessage); + + return new TheoryData + { + new [Entity]DependencyException( + message: "[Entity] validation errors occurred, please try again.", innerException), + + new [Entity]ServiceException( + message : "[Entity] service error occurred, please contact support.", innerException) + }; + } + + private static Expression> SameExceptionAs(Xeption expectedException) => + actualException => actualException.SameExceptionAs(expectedException); + + private static string GetRandomString() => + new MnemonicString().GetValue(); + + private static string GetRandomString(int length) => + new MnemonicString(wordCount: 1, wordMinLength: length, wordMaxLength: length).GetValue(); + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static [Entity] CreateRandom[Entity]() => + Create[Entity]Filler(dateTimeOffset: GetRandomDateTimeOffset()).Create(); + + private static IQueryable<[Entity]> CreateRandom[Entities]() + { + return Create[Entity]Filler(dateTimeOffset: GetRandomDateTimeOffset()) + .Create(count: GetRandomNumber()) + .AsQueryable(); + } + + private static Filler<[Entity]> Create[Entity]Filler(DateTimeOffset dateTimeOffset) + { + string user = GetRandomString(length: 255).ToString(); + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(dateTimeOffset); + // Add .OnProperty([entity] => [entity].NavigationProperty).IgnoreIt(); for navigation properties + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/service_test_template.cs b/.agents/skills/the-standard-testing/templates/service_test_template.cs new file mode 100644 index 0000000..c5724f4 --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/service_test_template.cs @@ -0,0 +1,274 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Foundation Service Tests — All three axes +// Replace [Entity] / [Entities] / [Namespace] with actual values. +// Demonstrates: Logic, Validations, and Exceptions files. + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Logic.Add.cs +// test-092: Happy-path tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-010: Happy path first + // test-110: Step 0 — ShouldAdd{Entity}Async + [Fact] + public async Task ShouldAdd[Entity]Async() + { + // given + [Entity] random[Entity] = CreateRandom[Entity](); + [Entity] input[Entity] = random[Entity]; + [Entity] storage[Entity] = input[Entity].DeepClone(); + [Entity] expected[Entity] = storage[Entity].DeepClone(); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(input[Entity])) + .ReturnsAsync(storage[Entity]); + + // when + [Entity] actual[Entity] = + await this.[entity]Service.Add[Entity]Async(input[Entity]); + + // then + actual[Entity].Should().BeEquivalentTo(expected[Entity]); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(input[Entity]), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Validations.Add.cs +// test-093: Validation failure tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-110: Step 1 — ShouldThrowValidationExceptionOnAddIf{Entity}IsNull + [Fact] + public async Task ShouldThrowValidationExceptionOnAddIf[Entity]IsNullAndLogItAsync() + { + // given + [Entity] null[Entity] = null; + + var null[Entity]Exception = + new Null[Entity]Exception(message: "[Entity] is null."); + + var expected[Entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: null[Entity]Exception); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(null[Entity]); + + [Entity]ValidationException actual[Entity]ValidationException = + await Assert.ThrowsAsync<[Entity]ValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ValidationException.Should() + .BeEquivalentTo(expected[Entity]ValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(It.IsAny<[Entity]>()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-110: Step 2 — ShouldThrowValidationExceptionOnAddIf{Entity}IsInvalid + // test-103: [Theory][InlineData] for parameterized + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task ShouldThrowValidationExceptionOnAddIf[Entity]IsInvalidAndLogItAsync( + string invalidText) + { + // given + var invalid[Entity] = new [Entity] + { + Name = invalidText + }; + + var invalid[Entity]Exception = + new Invalid[Entity]Exception( + message: "Invalid [entity]. Please correct the errors and try again."); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].Id), + values: "Id is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].Name), + values: "Text is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].CreatedDate), + values: "Date is required"); + + invalid[Entity]Exception.AddData( + key: nameof([Entity].UpdatedDate), + values: new[] + { + "Date is required", + $"Date is not the same as {nameof([Entity].CreatedDate)}" + }); + + var expected[Entity]ValidationException = + new [Entity]ValidationException( + message: "[Entity] validation error occurred, fix the errors and try again.", + innerException: invalid[Entity]Exception); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(invalid[Entity]); + + [Entity]ValidationException actual[Entity]ValidationException = + await Assert.ThrowsAsync<[Entity]ValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ValidationException.Should() + .BeEquivalentTo(expected[Entity]ValidationException); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ValidationException))), + Times.Once); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(It.IsAny<[Entity]>()), + Times.Never); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} + +// --------------------------------------------------------------- +// File: [Entity]ServiceTests.Exceptions.Add.cs +// test-094: Dependency and service exception tests +// --------------------------------------------------------------- + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + public partial class [Entity]ServiceTests + { + // test-110: Step 3 — DependencyValidationException (BadRequest) + [Fact] + public async Task ShouldThrowDependencyValidationExceptionOnAddIfBadRequestErrorOccursAndLogItAsync() + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + var httpResponseBadRequestException = new HttpResponseBadRequestException(); + + var invalidPost[Entity]Exception = + new Invalid[Entity]Exception( + message: "Invalid [entity] error occurred, fix errors and try again.", + innerException: httpResponseBadRequestException); + + var expected[Entity]DependencyValidationException = + new [Entity]DependencyValidationException( + message: "[Entity] dependency validation error occurred, fix the errors.", + innerException: invalidPost[Entity]Exception); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(some[Entity])) + .ThrowsAsync(httpResponseBadRequestException); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(some[Entity]); + + [Entity]DependencyValidationException actual[Entity]DependencyValidationException = + await Assert.ThrowsAsync<[Entity]DependencyValidationException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]DependencyValidationException.Should() + .BeEquivalentTo(expected[Entity]DependencyValidationException); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(some[Entity]), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]DependencyValidationException))), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + + // test-110: Step 12 — ServiceException (catch-all) + [Fact] + public async Task ShouldThrowServiceExceptionOnAddIfServiceErrorOccursAndLogItAsync() + { + // given + [Entity] some[Entity] = CreateRandom[Entity](); + var serviceException = new Exception(); + + var failed[Entity]ServiceException = + new Failed[Entity]ServiceException( + message: "Unexpected service error occurred. Contact support.", + innerException: serviceException); + + var expected[Entity]ServiceException = + new [Entity]ServiceException( + message: "[Entity] service error occurred, contact support.", + innerException: failed[Entity]ServiceException); + + this.storageBrokerMock.Setup(broker => + broker.Insert[Entity]Async(some[Entity])) + .ThrowsAsync(serviceException); + + // when + ValueTask<[Entity]> add[Entity]Task = + this.[entity]Service.Add[Entity]Async(some[Entity]); + + [Entity]ServiceException actual[Entity]ServiceException = + await Assert.ThrowsAsync<[Entity]ServiceException>( + add[Entity]Task.AsTask); + + // then + actual[Entity]ServiceException.Should() + .BeEquivalentTo(expected[Entity]ServiceException); + + this.storageBrokerMock.Verify(broker => + broker.Insert[Entity]Async(some[Entity]), + Times.Once); + + this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs( + expected[Entity]ServiceException))), + Times.Once); + + this.storageBrokerMock.VerifyNoOtherCalls(); + this.loggingBrokerMock.VerifyNoOtherCalls(); + } + } +} diff --git a/.agents/skills/the-standard-testing/templates/test_class_root_template.cs b/.agents/skills/the-standard-testing/templates/test_class_root_template.cs new file mode 100644 index 0000000..99d442f --- /dev/null +++ b/.agents/skills/the-standard-testing/templates/test_class_root_template.cs @@ -0,0 +1,71 @@ +// --------------------------------------------------------------- +// Copyright (c) Coalition of the Good-Hearted Engineers +// FREE TO USE TO CONNECT THE WORLD +// --------------------------------------------------------------- + +// TEMPLATE: Root test class for a Foundation Service +// File: [Entity]ServiceTests.cs +// Replace [Entity] / [Entities] / [Namespace] with actual values. + +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using Tynamix.ObjectFiller; +using Xeptions; +using Xunit; + +namespace [Namespace].Tests.Unit.Services.Foundations.[Entities] +{ + // test-090, test-091: Root file — setup, mocks, helpers + public partial class [Entity]ServiceTests + { + // test-101: Mock all dependencies with Moq + private readonly Mock storageBrokerMock; + private readonly Mock loggingBrokerMock; + private readonly I[Entity]Service [entity]Service; + + public [Entity]ServiceTests() + { + this.storageBrokerMock = new Mock(); + this.loggingBrokerMock = new Mock(); + + this.[entity]Service = new [Entity]Service( + storageBroker: this.storageBrokerMock.Object, + loggingBroker: this.loggingBrokerMock.Object); + } + + // test-035: Randomized data helper using ObjectFiller + private static [Entity] CreateRandom[Entity]() => + Create[Entity]Filler(dateTimeOffset: GetRandomDateTimeOffset()).Create(); + + private static DateTimeOffset GetRandomDateTimeOffset() => + new DateTimeRange(earliestDate: new DateTime()).GetValue(); + + private static int GetRandomNumber() => + new IntRange(min: 2, max: 10).GetValue(); + + private static int GetRandomNegativeNumber() => + -1 * GetRandomNumber(); + + // test-036: Exception equality expression helper + private static Expression> SameExceptionAs(Exception expectedException) + { + return actualException => + actualException.Message == expectedException.Message + && actualException.InnerException.Message == expectedException.InnerException.Message; + } + + private static Filler<[Entity]> Create[Entity]Filler(DateTimeOffset dateTimeOffset) + { + var filler = new Filler<[Entity]>(); + + filler.Setup() + .OnType().Use(dateTimeOffset); + + return filler; + } + } +} diff --git a/.agents/skills/the-standard-testing/validations/anti-patterns.md b/.agents/skills/the-standard-testing/validations/anti-patterns.md new file mode 100644 index 0000000..c48ba17 --- /dev/null +++ b/.agents/skills/the-standard-testing/validations/anti-patterns.md @@ -0,0 +1,166 @@ +# The Standard Testing — Anti-Patterns + +--- + +## AP-TEST-001: Implementation Before Test + +**What it is:** Writing the service implementation before writing a failing test. + +**Example:** +``` +// Day 1: Engineer writes StudentService.AddStudentAsync() +// Day 1: Engineer then writes ShouldAddStudentAsync test +``` + +**Why harmful:** The test is not a design tool — it becomes a post-hoc documentation exercise. The test is likely to pass on the first run (no red phase), meaning the TDD discipline was bypassed. Tests written after implementation often miss edge cases. + +**How to fix:** Write the test first. Run it. Verify it fails (red). Then write the minimum implementation. Verify it passes (green). + +--- + +## AP-TEST-002: Hard-Coded Test Values + +**What it is:** Using literal values in test setup instead of randomized data. + +**Example:** +```csharp +var student = new Student +{ + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), // hard-coded + Name = "John" // hard-coded +}; +``` + +**Why harmful:** Hard-coded values create the illusion of a passing test for only one specific state. The implementation may have logic that only works for "John" or a specific ID. Randomized data catches these hidden assumptions. + +**How to fix:** +```csharp +Student randomStudent = CreateRandomStudent(); // uses ObjectFiller +``` + +--- + +## AP-TEST-003: Missing VerifyNoOtherCalls + +**What it is:** Tests that verify expected calls but do not verify that NO other unexpected calls occurred. + +**Example:** +```csharp +this.storageBrokerMock.Verify(broker => + broker.InsertStudentAsync(inputStudent), Times.Once); +// MISSING: this.storageBrokerMock.VerifyNoOtherCalls(); +``` + +**Why harmful:** Without `VerifyNoOtherCalls()`, a future implementation change could add an extra broker call (e.g., logging to storage, calling another broker), and the test would still pass. This allows silent regressions. + +**How to fix:** Always end every test with `VerifyNoOtherCalls()` on all mocks. + +--- + +## AP-TEST-004: Missing Logging Verification + +**What it is:** Error-case tests that do not verify the logging broker received the exception. + +**Example:** +```csharp +// when +await Assert.ThrowsAsync(...); + +// then +// MISSING: verify logging broker called +this.storageBrokerMock.VerifyNoOtherCalls(); +this.loggingBrokerMock.VerifyNoOtherCalls(); // will pass vacuously if no LogError call expected +``` + +**Why harmful:** Logging is a critical observable behavior in Standard-compliant systems. If logging is broken, production errors become invisible. Without verifying it in tests, a refactor could silently remove logging and no test would catch it. + +**How to fix:** +```csharp +this.loggingBrokerMock.Verify(broker => + broker.LogError(It.Is(SameExceptionAs(expectedStudentValidationException))), + Times.Once); +``` + +--- + +## AP-TEST-005: Shared Object References Between Input and Expected + +**What it is:** Using the same object reference for input, storage response, and expected value. + +**Example:** +```csharp +Student inputStudent = CreateRandomStudent(); +// VIOLATION: input, storage, and expected all point to the same object +this.storageBrokerMock.Setup(b => b.InsertStudentAsync(inputStudent)) + .ReturnsAsync(inputStudent); // same reference + +Student expectedStudent = inputStudent; // same reference + +Student actualStudent = await this.studentService.AddStudentAsync(inputStudent); + +actualStudent.Should().BeEquivalentTo(expectedStudent); // passes vacuously +``` + +**Why harmful:** If the service modifies the input object in-place, all three references see the modification. The test cannot detect incorrect mutations. + +**How to fix:** +```csharp +Student inputStudent = CreateRandomStudent(); +Student storageStudent = inputStudent.DeepClone(); +Student expectedStudent = storageStudent.DeepClone(); +``` + +--- + +## AP-TEST-006: Aggregation Test Asserting Call Order + +**What it is:** Using `MockSequence` or similar mechanisms in aggregation service tests. + +**Example:** +```csharp +// In aggregation service test — VIOLATION +var sequence = new MockSequence(); +studentServiceMock.InSequence(sequence).Setup(s => s.AddStudentAsync(It.IsAny())); +courseServiceMock.InSequence(sequence).Setup(s => s.AddCourseAsync(It.IsAny())); +``` + +**Why harmful:** Aggregation services have no ordering contract. Testing order here creates a brittle test that breaks if the implementation optimizes by reordering calls (e.g., running them in parallel). The test now couples to implementation details rather than contract behavior. + +**How to fix:** Test that the correct service calls were made with the correct arguments. Do not assert order. + +--- + +## AP-TEST-007: Tests Out of Standard Order + +**What it is:** Writing exception tests before validation tests, or validation tests before the happy path. + +**Example:** +``` +// VIOLATION order in file: +ShouldThrowDependencyExceptionOnAddIfInternalServerErrorOccurs // exception test first +ShouldThrowValidationExceptionOnAddIfStudentIsNull // validation test second +ShouldAddStudentAsync // happy path last +``` + +**Why harmful:** The Standard's test order reflects the order of comprehension — you understand what the system does (happy path) before understanding what it guards against (validations) and what can go wrong (exceptions). Reversed order creates confusion and makes code review harder. + +**How to fix:** Follow the exact 13-step order for foundation service tests. Always happy path first. + +--- + +## AP-TEST-008: Continuous Validation Test Stops at First Error + +**What it is:** A validation test that only verifies one field error, not all errors simultaneously. + +**Example:** +```csharp +// Test only expects Id error — misses Name, CreatedDate, UpdatedDate +invalid[Entity]Exception.AddData( + key: nameof(Student.Id), + values: "Id is required"); +// Missing AddData calls for Name, CreatedDate, UpdatedDate +``` + +**Why harmful:** Continuous validation must collect all errors. If the test only verifies one, you cannot tell if the implementation stops after the first error (failing test-021) or collects all errors correctly. + +**How to fix:** Build the expected exception with ALL field errors that should be collected, then assert `BeEquivalentTo` against it. diff --git a/.agents/skills/the-standard-testing/validations/checklist.md b/.agents/skills/the-standard-testing/validations/checklist.md new file mode 100644 index 0000000..4670ab5 --- /dev/null +++ b/.agents/skills/the-standard-testing/validations/checklist.md @@ -0,0 +1,89 @@ +# The Standard Testing — Validation Checklist + +Run this checklist before committing any test code or approving a PR. +Each item is binary: PASS or FAIL. + +--- + +## TDD DISCIPLINE + +- [ ] **test-001** The failing test was written before the implementation. +- [ ] **test-002** The test was verified to actually fail (test runner shows red) before the FAIL commit. +- [ ] **test-003** Only the minimum implementation to pass the test was written. +- [ ] **test-004** The full relevant test suite was verified to pass before the PASS commit. +- [ ] **test-005** Refactoring did not change behavior — all tests still pass. + +--- + +## TEST IMPLEMENTATION ORDER + +- [ ] **test-010** Happy path test is implemented first. +- [ ] **test-011** Structural validation tests come before logical validation tests. +- [ ] **test-012** Logical validation tests come before external validation tests. +- [ ] **test-013** External validation tests come before dependency validation tests. +- [ ] **test-110** For foundation Add operations, the exact 13-step order is followed. + +--- + +## TEST QUALITY + +- [ ] **test-030** Exact broker calls are verified (`Times.Once` / `Times.Never`). +- [ ] **test-031** Every test ends with `VerifyNoOtherCalls()` on all mocks. +- [ ] **test-032** Logging broker verification is present for all error cases. +- [ ] **test-034** Deep cloning is used to isolate input/expected/actual objects. +- [ ] **test-035** All test data is randomized via ObjectFiller — no hard-coded values. +- [ ] **test-036** Exception equality uses `Xeption.SameExceptionAs()`. + +--- + +## VALIDATION TEST COVERAGE + +- [ ] **test-020** Null entity test exists and verifies circuit-breaking (throws immediately). +- [ ] **test-021** Invalid fields test collects ALL errors before asserting (continuous validation). +- [ ] **test-022** Exception data uses `UpsertDataList` / `AddData`. + +--- + +## FILE STRUCTURE + +- [ ] **test-090** Tests mirror the partial-class split of the service: root + Logic + Validations + Exceptions. +- [ ] **test-091** Root test file contains only: constructor, mocks, helper methods, filler configuration. +- [ ] **test-092** Happy-path tests are in the Logic file. +- [ ] **test-093** Validation failure tests are in the Validations file. +- [ ] **test-094** Exception tests are in the Exceptions file. + +--- + +## CONVENTIONS + +- [ ] **test-100** All tests use GWT pattern with `// given`, `// when`, `// then` comments. +- [ ] **test-101** All dependencies are mocked with Moq. +- [ ] **test-102** Assertions use FluentAssertions (`Should().BeEquivalentTo()`). +- [ ] **test-103** Single-case tests use `[Fact]`; parameterized tests use `[Theory][InlineData]`. +- [ ] **test-104** Test names follow `Should{Action}Async` or `ShouldThrow{Exception}On{Action}If{Condition}AndLogItAsync`. + +--- + +## SERVICE-TYPE SPECIFIC + +- [ ] **test-060** No mock-sequence order assertions in aggregation service tests. +- [ ] **test-073** Every controller endpoint has an acceptance test. +- [ ] **test-074** Acceptance test data is cleaned up after each run. +- [ ] **test-080** Core UI components are test-driven. +- [ ] **test-083** Each core component integrates with exactly one view service. + +--- + +## RESULT + +| Category | PASS / FAIL | +|---|---| +| TDD Discipline | | +| Test Implementation Order | | +| Test Quality | | +| Validation Coverage | | +| File Structure | | +| Conventions | | +| Service-Type Specific | | + +**Overall: PASS only when every row is PASS.** diff --git a/.agents/skills/the-standard-versioning/SKILL.md b/.agents/skills/the-standard-versioning/SKILL.md new file mode 100644 index 0000000..14b14a1 --- /dev/null +++ b/.agents/skills/the-standard-versioning/SKILL.md @@ -0,0 +1,576 @@ +--- +name: The Standard Versioning +description: Enforces Standard Versioning discipline. +the standard version: v2.13.0 +skill version: v0.3.0.0 +--- + +# The Standard — Versioning + +## What this skill is + +This skill defines the required versioning approach for: + +- release versioning +- file versioning (models, services, exceptions) +- API versioning +- deprecation signaling +- discovery capabilities + +This skill MUST follow the explicit versioning model defined here and MUST NOT substitute alternative semantic versioning strategies, compatibility theories, or inferred conventions. + +The skill exists to keep version changes: + +- explicit +- structured +- non-destructive +- discoverable +- safe for consumers + +--- + +## When to use + +Use this skill whenever: +- creating a new release version +- introducing a model change that requires versioning +- introducing a service or routine change that requires versioning +- creating new versioned files or folders +- defining API routes for versioned resources +- marking code or APIs as deprecated +- implementing discovery capabilities +- reviewing version-related changes in PRs + +--- + +## Explicit coverage map + +This skill covers these primary areas: + +1. **Release versioning** +2. **Code versioning** +3. **Deprecation** +4. **Discovery capabilities** + +Code versioning in this skill includes: + +- file versioning +- API versioning +- deprecation markers +- discovery capability signaling that helps consumers understand supported behavior + +--- + +## Release Versioning + +### Release Format + +Release versions MUST use the following exact format: + +```text +v1.2.3.4 +``` + +### Segment Meaning + +| Segment | Meaning | +|---|---| +| 1 | Model change | +| 2 | Service or routine change | +| 3 | Bug fix or configuration change | +| 4 | Automated build version | + +These meanings are fixed for this skill and MUST NOT be reinterpreted. + +### Release Increment Rules + +0. A release version MUST remain in the exact `v1.2.3.4` shape. + +1. A **model change** MUST increment segment `1` and MUST reset segments `2`, `3`, and `4` to zero. + +2. A **service or routine change** MUST increment segment `2` and MUST reset segments `3` and `4` to zero. + +3. A **bug fix or configuration change** MUST increment segment `3` and MUST reset segment `4` to zero. + +4. A **new automated build** for the same code MUST increment segment `4` only. + +5. If multiple change types occur together, the **highest-order change MUST win** and all lower segments MUST reset to zero. + +### Release Examples + +Given current version: + +```text +v1.2.3.4 +``` + +Expected next versions: + +- model change → `v2.0.0.0` +- service or routine change → `v1.3.0.0` +- bug fix or configuration change → `v1.2.4.0` +- automated build of the same code → `v1.2.3.5` +- model + service change together → `v2.0.0.0` + +### Release Guidance + +Use the release version to describe **what kind of change happened**, not merely that a change happened. + +That means: + +- a model change is never hidden inside the service segment +- a service change is never hidden inside the bug/config segment +- a build increment is never used to represent code change + +--- + +## File Versioning + +### V0 Convention + +A file without a version folder is treated as **V0**. + +Examples: + +```text +Models/Foundations/Students/Student.cs +Services/Foundations/Students/StudentService.cs +Models/Foundations/Students/Exceptions/ +``` + +This means the starting shape is unversioned on disk, but version zero is still logically assumed. + +### Model Changes + +When a model changes, a new versioned file MUST be created under a version folder that is a subfolder of the original location. + +Example: + +```text +Models/Foundations/Students/V1/StudentV1.cs +``` + +This pattern means: + +- the original V0 model remains where it is +- the new model version lives in a nested `V1` folder +- the version number becomes explicit in both folder and type name + +### Model-Driven Service Changes + +When a model changes, the corresponding service MUST also be versioned under the same model version folder. + +Example: + +```text +Services/Foundations/Students/V1/StudentV1Service.cs +``` + +Related exceptions MUST align with the matching versioned model path. + +Example: + +```text +Models/Foundations/Students/V1/Exceptions/ +``` + +### Service Behavior Versioning + +When the **behavior** of a service changes without any change to the underlying model, the service file is versioned to reflect that behavioral evolution. Behavior changes include modifying validation rules, making a previously optional field required, or altering business logic. + +If the model is still at V0 and only the service behavior changes, the versioned service file is placed at the service location root with a behavior version suffix — no version folder is created: + +```text +Services/Foundations/Students/StudentServiceV1.cs +``` + +If the model has already been versioned (e.g., V1) and the service behavior changes, the versioned service file is placed inside the model's version folder with both the model version and the behavior version in the name: + +```text +Services/Foundations/Students/V1/StudentV1ServiceV1.cs +``` + +### Combined Model and Behavior Changes + +When both the model and the service behavior change together in the same release, only the model version increments. The behavior version resets to V0, implied by its absence from the file name. A new model always means new service behavior by definition, so the behavior version returns to its baseline: + +```text +Services/Foundations/Students/V2/StudentV2Service.cs +``` + +The behavior version suffix is absent, signaling that the behavior version is V0 for this new model version. The cycle can then repeat as further behavior changes are introduced. + +### File Rules + +0. No version folder means **V0 is implied**. + +1. A new version MUST be introduced by creating new versioned files and folders, not by overwriting the earlier implementation. + +2. A model change MUST produce a versioned model file under a `Vn` subfolder. + +3. A model change MUST produce a versioned service file under the matching `Vn` subfolder. A service behavior-only change MUST produce a versioned service file at the service location root using the behavior version as a suffix — no new version folder is created. + +4. Versioned exceptions MUST live beneath the corresponding versioned model path. + +5. Existing earlier-version code MUST remain available unless there is an explicit deprecation/removal strategy outside the introduction of the new version. + +### Naming Convention + +Versioned file/type naming MUST follow this pattern: + +- model → `{Entity}V{n}.cs` +- service with model version only → `{Entity}V{n}Service.cs` +- service with behavior version only → `{Entity}ServiceV{n}.cs` +- service with both model and behavior versions → `{Entity}V{m}ServiceV{n}.cs` + +The behavior version always appears as a **suffix after the service name**. When both a model change and a behavior change occur in the same release, the behavior version resets to V0 and is implied by its absence from the file name. + +| Scenario | Example file name | +|---|---| +| Model V1, no behavior version | `StudentV1Service.cs` | +| No model version, behavior V1 | `StudentServiceV1.cs` | +| Model V1, behavior V1 | `StudentV1ServiceV1.cs` | +| Model V2 introduced (behavior resets to V0) | `StudentV2Service.cs` | + +--- + +## API Versioning + +### Default API Route + +The initial API is exposed without a version prefix: + +```text +api/Students +``` + +This is the route for V0. + +### Model Version Route + +If the model changes, the route MUST surface the model version: + +```text +api/V1/Students +``` + +Here, `V1` tells the consumer that this endpoint represents the version 1 model. + +### Service Behavior Route + +If the model is already at version 1 and the service behavior changes for that model, the route MUST surface both the model version and the behavior version: + +```text +api/V1.1/Students +``` + +In this shape: + +- `1` before the dot denotes the model version +- `1` after the dot denotes the behavior/service version + +This is a **model + behavior pairing** and MUST be interpreted that way. + +If only behaviour changed but the model stayed the same, the new route will be `api/V0.1/Students` — the model version is still 0, but the behavior version has incremented to 1. + +### API Rules + +0. `api/{resource}` denotes V0. + +1. `api/Vn/{resource}` denotes model version `n`. + +2. `api/Vn.m/{resource}` denotes model version `n` with behavior version `m`. + +3. A route version MUST communicate the intended model and behavior pairing. + +4. The API route MUST remain aligned with the versioning story of the underlying model/service pair. + +### API Example Flow + +Starting point: + +```text +api/Students +``` + +After model change: + +```text +api/V1/Students +``` + +After additional service behavior change for the V1 model: + +```text +api/V1.1/Students +``` + +--- + +## Deprecation + +Deprecation MUST be clear enough that consumers can see: + +- that an API is deprecated +- when it is expected to sunset +- where to go for migration guidance + +Consumers MUST be given enough time to act before the deprecated version is sunset. + +### Deprecation Signaling + +A valid approach is to surface deprecation metadata through headers or attributes that emit those headers. + +Example using a controller action attribute: + +```csharp +[ApiController] +[Route("api/[controller]")] +public class SampleController : ControllerBase +{ + [HttpGet] + [DeprecatedApi( + Sunset = "2024-12-31", + Warning = "This API is deprecated. Please migrate to v2.", + Link = "https://example.com/deprecation-info")] + public IActionResult GetSampleData() + { + return Ok(new { message = "Sample data" }); + } +} +``` + +#### Key Libraries + +| Package | Purpose | +| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| `Attrify` | A NuGet library for controlling API visibility and lifecycle (e.g. deprecation) through attributes applied to controllers and actions | + +--- + +#### Code Deprecation + +Code that is no longer maintained or supported SHOULD be marked with: + +```csharp +[Obsolete("This version is deprecated and no longer maintained.")] +``` + +Use the obsolete marker to make deprecation visible to maintainers and consumers at compile time. + +--- + +## Capabilities + +Capabilities are recommended so consumers can discover what is supported in each version before invoking a feature. + +This is especially useful where model versions and service behaviors differ by version or by provider. + +### Capability Goals + +A capabilities surface should help consumers discover: + +- what features are supported +- what model versions are supported +- what behavior versions are supported +- what optional features are unavailable + +### Capability Surface Options + +Capabilities MAY be exposed through: + +- Swagger +- middleware-based metadata +- a bespoke capabilities endpoint +- a provider-specific capabilities endpoint + +### Provider Example + +In a provider-based architecture such as a SPAL provider, each concrete provider can expose a +capabilities endpoint that tells consumers which operations are supported. + +This pattern allows consumers to safely determine whether a provider supports a given operation **before invoking it**, avoiding runtime failures. + +--- + +#### 1. Operation Attribute + +```csharp +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public sealed class StudentOperationAttribute : Attribute +{ + public string OperationName { get; } + + public StudentOperationAttribute(string operationName) => + OperationName = operationName ?? string.Empty; + + public StudentOperationAttribute() => + OperationName = string.Empty; +} +``` + +--- + +#### 2. Capabilities Models + +```csharp +public sealed class ResourceCapabilities +{ + public string ResourceName { get; init; } = string.Empty; + public IReadOnlyCollection SupportedOperations { get; init; } = + Array.Empty(); +} + +public sealed class ProviderCapabilities +{ + public string ProviderName { get; init; } = string.Empty; + public IReadOnlyCollection SupportedResources { get; init; } = + Array.Empty(); +} +``` + +--- + +#### 3. Student Provider Contract + +```csharp +public interface IStudentProvider +{ + string ProviderName { get; } + + ProviderCapabilities Capabilities { get; } + + IStudentResource Students { get; } +} +``` + +--- + +#### 4. Resource Contract + +```csharp +public interface IStudentResource +{ + ValueTask AddAsync(Student student); +} +``` + +--- + +#### 5. Provider Base (Simplified Capability Declaration) + +```csharp +public abstract class StudentProviderBase : IStudentProvider +{ + public abstract string ProviderName { get; } + + public abstract IStudentResource Students { get; } + + public virtual ProviderCapabilities Capabilities => + new ProviderCapabilities + { + ProviderName = ProviderName, + SupportedResources = new[] + { + new ResourceCapabilities + { + ResourceName = "Student", + SupportedOperations = GetSupportedOperations(Students) + } + } + }; + + private static IReadOnlyCollection GetSupportedOperations(object resource) + { + if (resource is null) + return Array.Empty(); + + return resource.GetType() + .GetMethods() + .Select(method => + method.GetCustomAttributes(typeof(StudentOperationAttribute), true) + .FirstOrDefault() as StudentOperationAttribute) + .Where(attribute => attribute is not null) + .Select(attribute => attribute!.OperationName) + .Where(name => !string.IsNullOrWhiteSpace(name)) + .Distinct() + .ToArray(); + } +} +``` + +--- + +#### 6. Example Implementation + +```csharp +public sealed class StudentResource : IStudentResource +{ + [StudentOperation("Add")] + public ValueTask AddAsync(Student student) + { + return ValueTask.FromResult(student); + } +} + +public sealed class StudentProvider : StudentProviderBase +{ + public override string ProviderName => "DefaultStudentProvider"; + public override IStudentResource Students { get; } = new StudentResource(); +} +``` + +--- + +#### 7. Capability Check Extension + +```csharp +public static class StudentProviderCapabilitiesExtensions +{ + public static bool SupportsOperation( + this IStudentProvider provider, + string resourceName, + string operationName) + { + var resource = provider.Capabilities.SupportedResources + .FirstOrDefault(r => r.ResourceName == resourceName); + + if (resource is null) + return false; + + return resource.SupportedOperations.Contains(operationName); + } +} +``` + +--- + +#### 8. Usage + +```csharp +IStudentProvider provider = new StudentProvider(); + +if (!provider.SupportsOperation("Student", "Add")) +{ + // avoid call + return; +} + +await provider.Students.AddAsync(new Student()); +``` + +--- + +#### Key Principle + +Capabilities turn runtime uncertainty into a deterministic check: + +- Without capabilities → call → fail → handle exception +- With capabilities → check → decide → call safely + +This aligns with the Standard principle: + +> Avoid invalid operations rather than reacting to them + + +--- diff --git a/.agents/skills/the-standard-versioning/contracts/contracts.json b/.agents/skills/the-standard-versioning/contracts/contracts.json new file mode 100644 index 0000000..a107d9b --- /dev/null +++ b/.agents/skills/the-standard-versioning/contracts/contracts.json @@ -0,0 +1,125 @@ +{ + "skill": "the-standard-versioning", + "version": "1.0.0", + + "release_version_format": { + "pattern": "v{major}.{minor}.{patch}.{build}", + "example": "v1.2.3.4", + "segments": { + "1_major": "model change", + "2_minor": "service or routine change", + "3_patch": "bug fix or configuration change", + "4_build": "automated build version" + } + }, + + "release_increment_rules": { + "model_change": "increment segment 1, reset segments 2, 3, 4 to zero", + "service_change": "increment segment 2, reset segments 3, 4 to zero", + "bug_fix": "increment segment 3, reset segment 4 to zero", + "build_only": "increment segment 4 only", + "multiple_changes": "highest-order change wins, all lower segments reset to zero" + }, + + "release_increment_examples": { + "from_v1.2.3.4": { + "model_change": "v2.0.0.0", + "service_change": "v1.3.0.0", + "bug_fix": "v1.2.4.0", + "build": "v1.2.3.5", + "model_and_service": "v2.0.0.0" + } + }, + + "file_versioning": { + "V0_convention": "no version folder means V0 is implied", + "versioned_model_path": "Models/Foundations/{Entity}/V{n}/{Entity}V{n}.cs", + "versioned_service_path": "Services/Foundations/{Entity}/V{n}/{Entity}V{n}Service.cs", + "versioned_exceptions_path": "Models/Foundations/{Entity}/V{n}/Exceptions/", + "examples": { + "V0_model": "Models/Foundations/Students/Student.cs", + "V1_model": "Models/Foundations/Students/V1/StudentV1.cs", + "V0_service": "Services/Foundations/Students/StudentService.cs", + "V1_service": "Services/Foundations/Students/V1/StudentV1Service.cs", + "V1_exceptions": "Models/Foundations/Students/V1/Exceptions/" + } + }, + + "file_versioning_rules": { + "additive_only": "new versions are created by adding new files, not overwriting", + "folder_structure": "Vn folders are subfolders of the original location", + "naming_convention": "{Entity}V{n}.cs for models, {Entity}V{n}Service.cs for services", + "exception_alignment": "versioned exceptions live beneath the corresponding versioned model path", + "earlier_versions": "earlier-version code remains available unless explicitly deprecated/removed" + }, + + "api_versioning": { + "V0_route": "api/{Resource}", + "model_version_route": "api/V{n}/{Resource}", + "model_behavior_version_route": "api/V{n}.{m}/{Resource}", + "route_interpretation": { + "n": "model version", + "m": "behavior/service version for that model" + }, + "examples": { + "V0": "api/Students", + "model_V1": "api/V1/Students", + "model_V1_behavior_1": "api/V1.1/Students" + } + }, + + "api_versioning_rules": { + "default_is_V0": "api/{resource} denotes version 0", + "model_version_explicit": "api/Vn/{resource} denotes model version n", + "behavior_version_explicit": "api/Vn.m/{resource} denotes model version n with behavior version m", + "route_alignment": "API route must align with underlying model/service pair versioning" + }, + + "deprecation": { + "required_metadata": [ + "deprecation_notice", + "sunset_date", + "migration_guidance_link" + ], + "code_marker": "[Obsolete(\"message\")]", + "api_marker": "[DeprecatedApi(Sunset = \"YYYY-MM-DD\", Warning = \"message\", Link = \"url\")]", + "consumer_notice_requirement": "consumers MUST be given enough time to act before sunset" + }, + + "capabilities": { + "purpose": "discovery of supported features before invocation", + "surfaces": [ + "Swagger", + "middleware-based metadata", + "bespoke capabilities endpoint", + "provider-specific capabilities endpoint" + ], + "consumer_benefits": [ + "discover supported features", + "discover supported model versions", + "discover supported behavior versions", + "discover optional features unavailability", + "avoid unsupported calls", + "avoid circuit-breaking exceptions" + ] + }, + + "operating_principles": [ + "follow explicit versioning rules exactly as defined", + "do not replace with inferred semantic versioning practices", + "prefer additive versioned structure over destructive replacement", + "keep routes and files discoverable", + "make deprecation visible and actionable", + "surface capabilities where version differences matter to consumers" + ], + + "forbidden": [ + "hiding model changes inside service segment", + "hiding service changes inside bug/config segment", + "using build increment to represent code change", + "overwriting earlier versions instead of creating new versioned files", + "weakening storage constraints at foundation layer", + "sunsetting APIs without adequate consumer notice period", + "removing earlier versions without explicit deprecation strategy" + ] +} \ No newline at end of file diff --git a/.agents/skills/the-standard-versioning/enforcement/enforcement.json b/.agents/skills/the-standard-versioning/enforcement/enforcement.json new file mode 100644 index 0000000..a99a6ae --- /dev/null +++ b/.agents/skills/the-standard-versioning/enforcement/enforcement.json @@ -0,0 +1,213 @@ +{ + "skill": "the-standard-versioning", + "enforcement_version": "1.0.0", + "description": "Automated validation hooks for versioning rule enforcement", + + "release_version_validators": { + "format_validator": { + "rule": "version-001", + "pattern": "^v\\d+\\.\\d+\\.\\d+\\.\\d+$", + "description": "Validates release version matches v1.2.3.4 format", + "severity": "critical", + "automation": "git tag validation hook" + }, + "segment_increment_validator": { + "rules": ["version-002", "version-003", "version-004", "version-005", "version-006"], + "description": "Validates segment increment follows versioning rules", + "severity": "critical", + "automation": "CI/CD pipeline validation before tag creation", + "checks": [ + "model change: segment 1 incremented, segments 2-4 are zero", + "service change: segment 2 incremented, segments 3-4 are zero", + "bug fix: segment 3 incremented, segment 4 is zero", + "build only: only segment 4 incremented", + "multiple changes: highest-order wins" + ] + } + }, + + "file_structure_validators": { + "V0_convention_validator": { + "rule": "version-010", + "description": "Validates V0 files exist without version folder", + "severity": "warning", + "automation": "pre-commit hook", + "checks": [ + "Models/Foundations/{Entity}/{Entity}.cs exists for V0", + "Services/Foundations/{Entity}/{Entity}Service.cs exists for V0" + ] + }, + "versioned_file_location_validator": { + "rules": ["version-011", "version-012", "version-013", "version-015", "version-016"], + "description": "Validates versioned files are in correct location", + "severity": "critical", + "automation": "pre-commit hook", + "checks": [ + "versioned model: Models/.../V{n}/{Entity}V{n}.cs", + "model-driven versioned service: Services/.../V{n}/{Entity}V{n}Service.cs", + "behavior-only versioned service: Services/.../{Entity}ServiceV{n}.cs (no version folder)", + "model+behavior versioned service: Services/.../V{m}/{Entity}V{m}ServiceV{n}.cs", + "versioned exceptions: Models/.../V{n}/Exceptions/" + ] + }, + "file_naming_validator": { + "rules": ["version-015", "version-016", "version-017"], + "description": "Validates versioned file naming convention for all scenarios", + "severity": "critical", + "automation": "pre-commit hook", + "checks": [ + "model files match pattern: {Entity}V{n}.cs", + "service with model version only matches pattern: {Entity}V{n}Service.cs", + "service with behavior version only matches pattern: {Entity}ServiceV{n}.cs (no version folder)", + "service with model and behavior versions matches pattern: {Entity}V{m}ServiceV{n}.cs", + "behavior version always appears as suffix after service name", + "when model and behavior change together, behavior version is absent from file name (V0 implied)" + ] + }, + "additive_versioning_validator": { + "rule": "version-014", + "description": "Validates earlier versions still exist (no destructive changes)", + "severity": "critical", + "automation": "pre-commit hook", + "checks": [ + "V0 files still exist when V1 added", + "V1 files still exist when V2 added", + "no deletions unless explicit deprecation" + ] + } + }, + + "api_route_validators": { + "V0_route_validator": { + "rule": "version-020", + "description": "Validates V0 routes follow api/{Resource} pattern", + "severity": "critical", + "automation": "API documentation generation", + "pattern": "^api/[A-Z][a-zA-Z]+$" + }, + "model_version_route_validator": { + "rule": "version-021", + "description": "Validates model version routes follow api/V{n}/{Resource} pattern", + "severity": "critical", + "automation": "API documentation generation", + "pattern": "^api/V\\d+/[A-Z][a-zA-Z]+$" + }, + "behavior_version_route_validator": { + "rule": "version-022", + "description": "Validates behavior version routes follow api/V{n}.{m}/{Resource} pattern", + "severity": "critical", + "automation": "API documentation generation", + "pattern": "^api/V\\d+\\.\\d+/[A-Z][a-zA-Z]+$" + }, + "route_alignment_validator": { + "rules": ["version-023", "version-024"], + "description": "Validates API route aligns with underlying model/service version", + "severity": "critical", + "automation": "integration test suite", + "checks": [ + "api/V1/Students maps to StudentV1 model and StudentV1Service", + "api/V1.1/Students maps to StudentV1 model with behavior version 1", + "route version segments match file version segments" + ] + } + }, + + "deprecation_validators": { + "deprecation_metadata_validator": { + "rules": ["version-030", "version-033", "version-034"], + "description": "Validates deprecated APIs have required metadata", + "severity": "critical", + "automation": "API documentation generation", + "checks": [ + "DeprecatedApi attribute present", + "Sunset date specified", + "Warning message specified", + "Link to migration guidance specified" + ] + }, + "code_deprecation_validator": { + "rule": "version-031", + "description": "Validates deprecated code marked with [Obsolete]", + "severity": "warning", + "automation": "static code analysis", + "checks": [ + "Obsolete attribute present on deprecated classes", + "Obsolete message explains deprecation reason" + ] + }, + "sunset_notice_validator": { + "rule": "version-032", + "description": "Validates consumers have adequate notice before sunset", + "severity": "critical", + "automation": "release pipeline validation", + "checks": [ + "sunset date is at least 90 days from deprecation announcement", + "deprecation notice has been published" + ] + } + }, + + "consistency_validators": { + "version_segment_integrity_validator": { + "description": "Validates version segments represent correct change types", + "severity": "critical", + "automation": "commit message parsing + CI validation", + "checks": [ + "model changes do not hide in service segment", + "service changes do not hide in bug/config segment", + "build increment only used for build changes" + ] + }, + "discoverable_versions_validator": { + "description": "Validates all versions remain discoverable", + "severity": "warning", + "automation": "documentation generation", + "checks": [ + "all version folders are documented", + "all API versions are documented", + "capabilities endpoint lists all supported versions" + ] + } + }, + + "automation_hooks": { + "pre_commit": [ + "file_structure_validators.V0_convention_validator", + "file_structure_validators.versioned_file_location_validator", + "file_structure_validators.file_naming_validator", + "file_structure_validators.additive_versioning_validator" + ], + "pre_push": [ + "consistency_validators.version_segment_integrity_validator" + ], + "ci_pipeline": [ + "release_version_validators.format_validator", + "release_version_validators.segment_increment_validator", + "api_route_validators", + "deprecation_validators.deprecation_metadata_validator" + ], + "release_pipeline": [ + "release_version_validators", + "deprecation_validators.sunset_notice_validator" + ], + "documentation_generation": [ + "api_route_validators", + "consistency_validators.discoverable_versions_validator" + ] + }, + + "reporting": { + "violation_severity_levels": { + "critical": "blocks commit/build/release", + "warning": "requires review but does not block", + "info": "informational only" + }, + "violation_report_format": { + "rule_id": "version-xxx", + "severity": "critical|warning|info", + "violation_description": "human-readable explanation", + "file_path": "path to violating file", + "suggested_fix": "actionable remediation guidance" + } + } +} diff --git a/.agents/skills/the-standard-versioning/manifest.json b/.agents/skills/the-standard-versioning/manifest.json new file mode 100644 index 0000000..d5b8d1e --- /dev/null +++ b/.agents/skills/the-standard-versioning/manifest.json @@ -0,0 +1,50 @@ +{ + "name": "the-standard-versioning", + "version": "1.2.0", + "description": "Governs release versioning (v1.2.3.4), file versioning (Vn folders), API versioning, and deprecation strategies for Standard-compliant systems.", + "the_standard_version": "v2.13.0", + "skill_version": "v1.2.0", + + "inputs": [ + "A release that needs version assignment", + "Files or folders that need versioning", + "API endpoints or contracts that need versioning", + "Code or APIs that need deprecation" + ], + + "outputs": [ + "Correct version numbers following v1.2.3.4 format", + "Properly structured Vn folder hierarchies", + "Version-aware API routes and contracts", + "Deprecation metadata and obsolete markers" + ], + + "dependencies": [ + "the-standard-core", + "the-standard-architecture" + ], + + "activation": { + "trigger": "versioning releases, creating file versions, versioning APIs, deprecating features", + "note": "Always activate the-standard-core and the-standard-architecture first." + }, + + "validation": { + "required": true, + "files": { + "checklist": "validations/checklist.md" + } + }, + + "files": { + "rules": { + "human": "rules/rules.md", + "machine": "rules/rules.json" + }, + "enforcement": "enforcement/enforcement.json", + "contracts": "contracts/contracts.json", + "validations": { + "checklist": "validations/checklist.md" + } + } +} \ No newline at end of file diff --git a/.agents/skills/the-standard-versioning/rules/rules.json b/.agents/skills/the-standard-versioning/rules/rules.json new file mode 100644 index 0000000..a79ba71 --- /dev/null +++ b/.agents/skills/the-standard-versioning/rules/rules.json @@ -0,0 +1,28 @@ +{ + "rules": [ + { "id": "version-001", "category": "release-versioning", "description": "All releases MUST follow the v1.2.3.4 format (major.minor.patch.build).", "severity": "error" }, + { "id": "version-002", "category": "release-versioning", "description": "Model changes MUST increment major version and reset all lower segments to 0.", "severity": "error" }, + { "id": "version-003", "category": "release-versioning", "description": "Service changes MUST increment minor version and reset patch and build to 0.", "severity": "error" }, + { "id": "version-004", "category": "release-versioning", "description": "Bug fixes or configuration changes MUST increment patch version and reset build to 0.", "severity": "error" }, + { "id": "version-005", "category": "release-versioning", "description": "Build/pipeline changes MUST increment build version only.", "severity": "error" }, + { "id": "version-006", "category": "release-versioning", "description": "When multiple changes occur, the highest-order change determines version increment (model > service > patch > build).", "severity": "error" }, + { "id": "version-010", "category": "file-versioning", "description": "Default version is V0 — version is inferred and MUST NOT be included in the filename.", "severity": "error" }, + { "id": "version-011", "category": "file-versioning", "description": "Model changes MUST require creation of a new Vn folder (e.g., V1, V2).", "severity": "error" }, + { "id": "version-012", "category": "file-versioning", "description": "Model-driven service changes MUST create a new service file in the model's Vn folder (e.g., Services/.../V{n}/{Entity}V{n}Service.cs).", "severity": "error" }, + { "id": "version-013", "category": "file-versioning", "description": "Previous version files MUST NOT be overwritten — always create new versioned folders.", "severity": "error" }, + { "id": "version-014", "category": "file-versioning", "description": "File versioning MUST be applied to maintain backward compatibility.", "severity": "error" }, + { "id": "version-015", "category": "file-versioning", "description": "Service behavior-only changes (model still at V0) MUST produce a file named {Entity}ServiceV{n}.cs at the service location root — no version folder is created.", "severity": "error" }, + { "id": "version-016", "category": "file-versioning", "description": "When a versioned model's service behavior changes, the service MUST be named {Entity}V{m}ServiceV{n}.cs inside the model's V{m} folder.", "severity": "error" }, + { "id": "version-017", "category": "file-versioning", "description": "When model and service behavior change together in the same release, the behavior version MUST be absent from the file name — only the model version increments and the behavior version is implied as V0.", "severity": "error" }, + { "id": "version-020", "category": "api-versioning", "description": "V0 APIs have no version prefix in the route.", "severity": "error" }, + { "id": "version-021", "category": "api-versioning", "description": "Model version changes MUST use Vn prefix in routes (e.g., /V1/students).", "severity": "error" }, + { "id": "version-022", "category": "api-versioning", "description": "Service version changes MUST use Vn.m prefix in routes (e.g., /V1.1/students).", "severity": "error" }, + { "id": "version-023", "category": "api-versioning", "description": "API routes are immutable — existing routes MUST NOT be modified or removed.", "severity": "error" }, + { "id": "version-024", "category": "api-versioning", "description": "API versioning MUST reflect the underlying model or service version.", "severity": "error" }, + { "id": "version-030", "category": "deprecation", "description": "Deprecated APIs MUST include sunset metadata indicating when they will be removed.", "severity": "error" }, + { "id": "version-031", "category": "deprecation", "description": "Deprecated code MUST include warning messages to notify consumers.", "severity": "error" }, + { "id": "version-032", "category": "deprecation", "description": "Deprecated code MUST use the [Obsolete] attribute in C# or equivalent in other languages.", "severity": "error" }, + { "id": "version-033", "category": "deprecation", "description": "Deprecation period MUST provide adequate time for consumers to migrate (recommended minimum: 6 months).", "severity": "error" }, + { "id": "version-034", "category": "deprecation", "description": "Deprecation notices MUST be communicated through documentation, API responses, and release notes.", "severity": "error" } + ] +} diff --git a/.agents/skills/the-standard-versioning/rules/rules.md b/.agents/skills/the-standard-versioning/rules/rules.md new file mode 100644 index 0000000..665eaed --- /dev/null +++ b/.agents/skills/the-standard-versioning/rules/rules.md @@ -0,0 +1,37 @@ +# The Standard Versioning — Rules + +## RELEASE VERSIONING (v1.2.3.4 Format) + +**version-001** [ERROR] All releases MUST follow the v1.2.3.4 format (major.minor.patch.build). +**version-002** [ERROR] Model changes MUST increment major version and reset all lower segments to 0. +**version-003** [ERROR] Service changes MUST increment minor version and reset patch and build to 0. +**version-004** [ERROR] Bug fixes or configuration changes MUST increment patch version and reset build to 0. +**version-005** [ERROR] Build/pipeline changes MUST increment build version only. +**version-006** [ERROR] When multiple changes occur, the highest-order change determines version increment (model > service > patch > build). + +## FILE VERSIONING (Vn Folder Structure) + +**version-010** [ERROR] Default version is V0 — version is inferred and MUST NOT be included in the filename. +**version-011** [ERROR] Model changes MUST require creation of a new Vn folder (e.g., V1, V2). +**version-012** [ERROR] Model-driven service changes MUST create a new service file in the model's Vn folder (e.g., `Services/.../V{n}/{Entity}V{n}Service.cs`). +**version-013** [ERROR] Previous version files MUST NOT be overwritten — always create new versioned folders. +**version-014** [ERROR] File versioning MUST be applied to maintain backward compatibility. +**version-015** [ERROR] Service behavior-only changes (model still at V0) MUST produce a file named `{Entity}ServiceV{n}.cs` at the service location root — no version folder is created. +**version-016** [ERROR] When a versioned model's service behavior changes, the service MUST be named `{Entity}V{m}ServiceV{n}.cs` inside the model's `V{m}` folder. +**version-017** [ERROR] When model and service behavior change together in the same release, the behavior version MUST be absent from the file name — only the model version increments and the behavior version is implied as V0. + +## API VERSIONING (Route Versioning) + +**version-020** [ERROR] V0 APIs have no version prefix in the route. +**version-021** [ERROR] Model version changes MUST use Vn prefix in routes (e.g., /V1/students). +**version-022** [ERROR] Service version changes MUST use Vn.m prefix in routes (e.g., /V1.1/students). +**version-023** [ERROR] API routes are immutable — existing routes MUST NOT be modified or removed. +**version-024** [ERROR] API versioning MUST reflect the underlying model or service version. + +## DEPRECATION + +**version-030** [ERROR] Deprecated APIs MUST include sunset metadata indicating when they will be removed. +**version-031** [ERROR] Deprecated code MUST include warning messages to notify consumers. +**version-032** [ERROR] Deprecated code MUST use the `[Obsolete]` attribute in C# or equivalent in other languages. +**version-033** [ERROR] Deprecation period MUST provide adequate time for consumers to migrate (recommended minimum: 6 months). +**version-034** [ERROR] Deprecation notices MUST be communicated through documentation, API responses, and release notes. diff --git a/.agents/skills/the-standard-versioning/validations/checklist.md b/.agents/skills/the-standard-versioning/validations/checklist.md new file mode 100644 index 0000000..d752b05 --- /dev/null +++ b/.agents/skills/the-standard-versioning/validations/checklist.md @@ -0,0 +1,95 @@ +# The Standard Versioning — Validation Checklist + +Run this checklist before releasing a version, creating versioned files, or approving a versioning-related PR. +Each item is binary: PASS or FAIL. + +--- + +## RELEASE VERSIONING + +- [ ] **version-001** Release version follows exact format `v1.2.3.4`. +- [ ] **version-002** Model change increments segment 1 and resets segments 2, 3, 4 to zero. +- [ ] **version-003** Service/routine change increments segment 2 and resets segments 3, 4 to zero. +- [ ] **version-004** Bug fix/config change increments segment 3 and resets segment 4 to zero. +- [ ] **version-005** Automated build increments segment 4 only. +- [ ] **version-006** When multiple changes occur, highest-order change wins and lower segments reset. + +--- + +## FILE VERSIONING + +- [ ] **version-010** Initial files (V0) exist without version folder. +- [ ] **version-011** New model version creates `Models/.../V{n}/{Entity}V{n}.cs`. +- [ ] **version-012** Model-driven service change creates `Services/.../V{n}/{Entity}V{n}Service.cs` (service file in model's Vn folder). +- [ ] **version-013** Versioned exceptions exist at `Models/.../V{n}/Exceptions/`. +- [ ] **version-014** Earlier-version files remain available (additive, not destructive). +- [ ] **version-015** Service behavior-only change creates `Services/.../{Entity}ServiceV{n}.cs` at service location root (no version folder). +- [ ] **version-016** Model-versioned service with behavior change creates `Services/.../V{m}/{Entity}V{m}ServiceV{n}.cs`. +- [ ] **version-017** When model and behavior change together, behavior version is absent from file name (V0 implied) — only model version increments. + +--- + +## FILE NAMING + +- [ ] **File naming** Model files follow pattern `{Entity}V{n}.cs`. +- [ ] **File naming** Service with model version only follows pattern `{Entity}V{n}Service.cs`. +- [ ] **version-015** Service with behavior version only follows pattern `{Entity}ServiceV{n}.cs` (no version folder). +- [ ] **version-016** Service with model and behavior versions follows pattern `{Entity}V{m}ServiceV{n}.cs`. +- [ ] **version-017** When model and behavior change together, behavior version is absent from file name (V0 implied). +- [ ] **File location** Vn folders are subfolders of original V0 location. +- [ ] **File location** Behavior-only service versioning does NOT create a new version folder. + +--- + +## API VERSIONING + +- [ ] **version-020** V0 API route is `api/{Resource}`. +- [ ] **version-021** Model version route is `api/V{n}/{Resource}`. +- [ ] **version-022** Model + behavior version route is `api/V{n}.{m}/{Resource}`. +- [ ] **version-023** Route version communicates intended model and behavior pairing. +- [ ] **version-024** API route aligns with underlying model/service pair versioning. + +--- + +## DEPRECATION + +- [ ] **version-030** Deprecated APIs expose deprecation metadata (sunset date, warning, migration link). +- [ ] **version-031** Deprecated code marked with `[Obsolete("message")]`. +- [ ] **version-032** Consumers given adequate notice before sunset. +- [ ] **version-033** Deprecation metadata includes sunset date. +- [ ] **version-034** Deprecation metadata includes migration guidance link. + +--- + +## CAPABILITIES + +- [ ] **Capabilities** Capabilities endpoint or metadata exists for version discovery. +- [ ] **Capabilities** Supported model versions are discoverable. +- [ ] **Capabilities** Supported behavior versions are discoverable. +- [ ] **Capabilities** Unsupported features are explicitly indicated. + +--- + +## CONSISTENCY + +- [ ] **Consistency** No model changes hidden in service segment. +- [ ] **Consistency** No service changes hidden in bug/config segment. +- [ ] **Consistency** Build increment never used to represent code changes. +- [ ] **Consistency** No overwriting of earlier versions. +- [ ] **Consistency** All version information remains discoverable. + +--- + +## RESULT + +| Category | PASS / FAIL | +|---|---| +| Release Versioning | | +| File Versioning | | +| File Naming | | +| API Versioning | | +| Deprecation | | +| Capabilities | | +| Consistency | | + +**Overall: PASS only when every row is PASS.** \ No newline at end of file diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..f6ee9c9 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,47 @@ +{ + "version": 1, + "skills": { + "The Standard Architecture": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-architecture/SKILL.md", + "computedHash": "fca9d586669c197776f33b8bcc433dd42bf8ae4fffab7fbd7e24d30667df1350" + }, + "The Standard Code CSharp": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/coding/csharp/SKILL.md", + "computedHash": "ba3d10bc6b4e31a795ca20b99073a80270a951f809e8affb7eb97c5948cd83d8" + }, + "The Standard Core": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-core/SKILL.md", + "computedHash": "373f6f9d59b21aa869718ccad8851bb374573ad8aca5be49768c0ceab979c200" + }, + "The Standard Events": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-events/SKILL.md", + "computedHash": "dd808910906f670ec5b1926e75729109ce096820986447335efca4c1031cde68" + }, + "The Standard Practices": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-practices/SKILL.md", + "computedHash": "8d1f3ec7904fcd78b42cd4b67c959c923fc4a8b13710526d2f09350a2f98363e" + }, + "The Standard Testing": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-testing/SKILL.md", + "computedHash": "7cb68e34f383e190cc9373fe13a28f8ca3104dde438ccd12cb632f6e6145a7a5" + }, + "The Standard Versioning": { + "source": "hassanhabib/the-standard-skills", + "sourceType": "github", + "skillPath": ".skills/the-standard-versioning/SKILL.md", + "computedHash": "34bc9c18e711b5b74998b57bec251493a0bcb2aa7e81c6dce2d6b329db368743" + } + } +}