Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,10 @@ The table explains five of those positions:
| `IdempotencyMiddleware` **after** `UseAuthorization` | A replay returns a cached response *without invoking the endpoint* | A role-denied caller replaying someone else's key gets the cached response instead of a 403 |
| `SpaShell` **before** the static-file middleware | It templates `/` and `/index.html` with this response's CSP nonce (#873), and the static middleware would otherwise serve the untemplated file from `wwwroot` first | The browser refuses MUI's Emotion styles and nothing reports it. Clients that installed the service worker are hit hardest, because it precaches `/index.html` and answers every navigation from it |

The epoch check reads the database on **every authenticated request**. That
read is what makes a revoked credential fail closed, so do not cache it.
The epoch check reads the database on **every authenticated request** except
logout and the `/error` re-execution. That read is what makes a revoked
credential fail closed, so do not cache it. If a token's user or account claim
does not parse, the middleware rejects it without reading the database.

## The egg loop

Expand Down
2 changes: 1 addition & 1 deletion docs/decisions/1023-peer-contract-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,4 @@ previous measurement and deferral above describe the pre-contract baseline.

## Amendment, 2026-10-06: no type claims (#1087)

Access's two identity ports moved into Access's own namespace (`Application/Modules/Access/Users`, #1087 S8), so no owner claims a type in a Platform namespace any more. The `types` list, its registry checks and the analyzer's reading of it are deleted (S10); the claim-related failures listed above no longer exist. The seam (`Seam`) and the peer walk are unchanged.
Access's two identity ports moved into Access's own namespace (`Application/Modules/Access/Users`, #1095), so no owner claims a type in a Platform namespace any more. The `types` list, its registry checks and the analyzer's reading of it are deleted (#1099); the claim-related failures listed above no longer exist. The seam (`Seam`) and the peer walk are unchanged.
2 changes: 1 addition & 1 deletion docs/decisions/514-module-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,4 @@ owner claims); their output is attached to the PR that landed this record.

## Amendment, 2026-10-06: module folders replace the namespace lists (#1087)

Each module's code now lives under `Modules/<Owner>/` in Domain, Application and Infrastructure, and its `[ModuleOwner]` row claims those fixed roots, `Cluckwork.{Domain,Application,Infrastructure}.Modules.<Owner>`, instead of a list of feature namespaces. Adding a feature folder needs no row. Any namespace outside the roots and the Platform list is unowned: a leftover `Cluckwork.Application.Features.*` fails `ModuleLedgerRealTreeTests` and CW1003. Edge cells, reasons and the free Platform hub are unchanged. The `types` and `implementations` lists are gone (#1087 S10): no owner used either after the move.
Each module's code now lives under `Modules/<Owner>/` in Domain, Application and Infrastructure, and its `[ModuleOwner]` row claims those fixed roots, `Cluckwork.{Domain,Application,Infrastructure}.Modules.<Owner>`, instead of a list of feature namespaces. Adding a feature folder needs no row. Any namespace outside the roots and the Platform list is unowned: a leftover `Cluckwork.Application.Features.*` fails `ModuleLedgerRealTreeTests` and CW1003. Edge cells, reasons and the free Platform hub are unchanged. The `types` and `implementations` lists are gone (#1099): no owner used either after the move.
2 changes: 1 addition & 1 deletion docs/decisions/849-module-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,4 @@ DI. Moving validation behind the contract changes both.

## Amendment, 2026-10-06: the Contracts folder is the contract (#1087)

`[ModuleContract("<Owner>")]` is deleted. A module's contract is every top-level type in its `Modules/<Owner>/Contracts/` folder, in Application and, for enums entities use, in Domain; nested types stay outside it. `RealModuleLedger` reads the contract from the namespace, and `NamespaceFolderAgreementTests` keeps every file's namespace equal to its folder, so a file moved out of `Contracts/` cannot keep its contract namespace. `IReportQueries` and `IExportQueries` stay outside `Contracts/`, so adapters and peers still reach Insights only through `IInsightsModule`. The guards this record describes are unchanged.
`[ModuleContract("<Owner>")]` is deleted. A module's contract is every top-level type in its `Modules/<Owner>/Contracts/` folder, in Application and, for enums entities use, in Domain; nested types stay outside it. `RealModuleLedger` reads the contract from the namespace, and `NamespaceFolderAgreementTests` keeps every file's namespace equal to its folder, so a file moved out of `Contracts/` cannot keep its contract namespace. `IReportQueries` and `IExportQueries` stay outside `Contracts/`, so adapters and peers still reach Insights only through `IInsightsModule`. The guards this record describes are unchanged. #1116 later widened the folder's contents: Domain `Contracts/` folders also hold value types and shared rules, and a type nested in a contract type through public types only is contract too. See the CW1004 amendment in [859](859-typed-rule-registries.md).
2 changes: 1 addition & 1 deletion docs/decisions/850-compatibility-exceptions.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,4 @@ audit kept the contract narrow, and the seeder's conversion belongs to #858.

## Amendment, 2026-10-06: no `Implementations` allowance (#1087)

Repositories moved into their modules (`Infrastructure/Modules/<Owner>/Repositories`, #1087 S9), so they read their tables as the module's own code. The `Implementations` list and the "declared implementation" allowance are deleted (S10). A read is allowed for the module's own types, a type its edge's `Symbols` names, and a `DbSet` property whose whole body is `Set<T>()`; every other read still needs a compatibility-exception row.
Repositories moved into their modules (`Infrastructure/Modules/<Owner>/Repositories`, #1097), so they read their tables as the module's own code. The `Implementations` list and the "declared implementation" allowance are deleted (#1099). A read is allowed for the module's own types, a type its edge's `Symbols` names, and a `DbSet` property whose whole body is `Set<T>()`; every other read still needs a compatibility-exception row.
6 changes: 3 additions & 3 deletions src/AGENTS.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions src/Cluckwork.Analyzers/ModuleEdgeAnalyzer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -321,8 +321,8 @@ private static bool InServiceRegistration(SemanticModel model, SyntaxNode node)
node.Ancestors().OfType<MethodDeclarationSyntax>().Any(m => model.GetDeclaredSymbol(m) is { IsExtensionMethod: true } method
&& method.Parameters[0].Type.ToDisplayString() == "Microsoft.Extensions.DependencyInjection.IServiceCollection");

// ModuleLedgerScanner.ScanFile's attribution: each top-level type is charged to its namespace owner, or to the
// owner claiming it (#1023); a file without types is charged as a whole.
// ModuleLedgerScanner.ScanFile's attribution: each top-level type is charged to its namespace owner; a file without
// types is charged as a whole.
private static List<Attribution> Attribute(
SemanticModelAnalysisContext context, CompilationUnitSyntax root, string path, string assembly, ModuleMap map)
{
Expand All @@ -337,7 +337,7 @@ private static List<Attribution> Attribute(
}

var symbol = IsFileLocal(type) ? $"{declared}.{Identifier(type)}@{Relative(path)}" : $"{declared}.{Identifier(type)}";
attributions.Add(new Attribution(type, symbol, map.Claimant(symbol) ?? owner, declared));
attributions.Add(new Attribution(type, symbol, owner, declared));
}

if (attributions.Count == 0)
Expand Down
2 changes: 0 additions & 2 deletions src/Cluckwork.Analyzers/ModuleMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,6 @@ internal bool IsAdapter(string declaredNamespace, string type) =>

internal bool IsDeclared(string from, string to, string symbol) => _declared.Contains((from, to, symbol));

internal string? Claimant(string symbol) => _claims.TryGetValue(symbol, out var claim) ? claim.Owner : null;

// ModuleLedgerScanner.Resolve: the longest claimed prefix. Resolving a declared namespace, an exact claim covers
// only its own name.
internal (string Owner, string Namespace)? Resolve(string dotted, bool declared)
Expand Down
4 changes: 2 additions & 2 deletions src/Cluckwork.Domain/Common/Architecture/Modules/Access.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ namespace Cluckwork.Domain.Common.Architecture;
])]
[ModuleEdge(
"Access", "Commerce", "W",
"W because AccountProvisioner stages a new farm's default unit conversions through Commerce's IEggUnitConversionProvisioning port inside its provisioning transaction (#1116); the rest of the cell reads. Identity carries the per-user stepper-unit preference: ApplicationUser stores Domain.Catalog.EggUnit, IIdentityProvider, the Identity port Access claims, takes it in SetStepperUnitAsync, IdentityProvider reads and writes it, SetStepperUnitHandler resolves it through Commerce's IEggUnitConversionLookup port (#854) and rejects inactive conversions, and SetStepperUnitValidator validates only the enum name. Nothing prevents a stored preference from pointing at a conversion later deactivated through UpdateEggUnitConversionHandler. Design 3.4 shows Access -> Commerce as none; this is live coupling the target design has still to remove. ApplicationUserConfiguration maps the stepper-unit preference column as the Domain.Catalog.EggUnit enum name (#1087 S9). The Access contract's UserProfile returns that preference to the /me adapter typed as Commerce's published Cluckwork.Domain.Modules.Commerce.Contracts.EggUnit, as Farm's FarmSettingsDetails returns the farm default (#1103).",
"W because AccountProvisioner stages a new farm's default unit conversions through Commerce's IEggUnitConversionProvisioning port inside its provisioning transaction (#1116); the rest of the cell reads. Identity carries the per-user stepper-unit preference: ApplicationUser stores Commerce's EggUnit, IIdentityProvider, the Identity port Access claims, takes it in SetStepperUnitAsync, IdentityProvider reads and writes it, SetStepperUnitHandler resolves it through Commerce's IEggUnitConversionLookup port (#854) and rejects inactive conversions, and SetStepperUnitValidator validates only the enum name. Nothing prevents a stored preference from pointing at a conversion later deactivated through UpdateEggUnitConversionHandler. Design 3.4 shows Access -> Commerce as none; this is live coupling the target design has still to remove. ApplicationUserConfiguration maps the stepper-unit preference column as Commerce's EggUnit enum name (#1097). The Access contract's UserProfile returns that preference to the /me adapter typed as Commerce's published Cluckwork.Domain.Modules.Commerce.Contracts.EggUnit, as Farm's FarmSettingsDetails returns the farm default (#1103).",
"Cluckwork.Application.Modules.Access.Contracts.UserProfile",
"Cluckwork.Application.Modules.Access.Users.IIdentityProvider",
"Cluckwork.Application.Modules.Access.Users.SetStepperUnit.SetStepperUnitHandler",
Expand All @@ -22,7 +22,7 @@ namespace Cluckwork.Domain.Common.Architecture;
"Cluckwork.Infrastructure.Modules.Access.Identity.AccountProvisioner")]
[ModuleEdge(
"Access", "Farm", "W",
"W because AccountProvisioner inserts the new Account inside its provisioning transaction and AccountRenameService calls Account.Rename and saves; the rest of the cell reads. Identity is where a farm is created, renamed, suspended and recovered. AccountProvisioner, AccountRenameService, AccountSuspensionService, AdminRecoveryService, FirstRunAdminService and FirstRunStatusService load Domain.Accounts.Account or inject Farm's IAccountRepository, and IdentityProvider takes that repository by fully qualified name for the account-scoped login (#532). CredentialEpochVerifier reads Account.IsActive in the same fresh per-request query as the user's credential epoch, so a suspended farm's credentials stop working at once (#364, #579, #857). The user validators read Domain.Accounts.Roles for the assignable role set, and IUserRoleAssignmentRepository returns Domain.Accounts.UserRoleAssignment rows, which AccessModule reads to list a user's flock assignments. IAccessLookup returns Domain.Accounts.EffectiveAccountRole, and AccessLookup resolves it with Roles.ResolveEffective in its private effective-role routine (#612, #857). Design 3.4 row Access -> Farm = R. AccessFixture (#858) creates the simulation fixture's UserRoleAssignment through the scoped context, and AccessSeedLookup checks the fixture's Owner role by Roles.Owner. Access owns the UserRoleAssignments table (TableOwnerOverrides), so its mapping and store read and write Farm's UserRoleAssignment entity: UserRoleAssignmentConfiguration configures it and UserRoleAssignmentRepository lists, adds and removes it. FlockScopeGuard, declared beside the repository, decides whether a Worker is narrowed to assigned flocks through Roles.ResolveEffective and EffectiveAccountRole (#1087 S9).",
"W because AccountProvisioner inserts the new Account inside its provisioning transaction and AccountRenameService calls Account.Rename and saves; the rest of the cell reads. Identity is where a farm is created, renamed, suspended and recovered. AccountProvisioner, AccountRenameService, AccountSuspensionService, AdminRecoveryService, FirstRunAdminService and FirstRunStatusService load Farm's Account or inject Farm's IAccountRepository, and IdentityProvider takes that repository by fully qualified name for the account-scoped login (#532). CredentialEpochVerifier reads Account.IsActive in the same fresh per-request query as the user's credential epoch, so a suspended farm's credentials stop working at once (#364, #579, #857). The user validators read Farm's Roles for the assignable role set, and IUserRoleAssignmentRepository returns Farm's UserRoleAssignment rows, which AccessModule reads to list a user's flock assignments. IAccessLookup returns Farm's EffectiveAccountRole, and AccessLookup resolves it with Roles.ResolveEffective in its private effective-role routine (#612, #857). Design 3.4 row Access -> Farm = R. AccessFixture (#858) creates the simulation fixture's UserRoleAssignment through the scoped context, and AccessSeedLookup checks the fixture's Owner role by Roles.Owner. Access owns the UserRoleAssignments table (TableOwnerOverrides), so its mapping and store read and write Farm's UserRoleAssignment entity: UserRoleAssignmentConfiguration configures it and UserRoleAssignmentRepository lists, adds and removes it. FlockScopeGuard, declared beside the repository, decides whether a Worker is narrowed to assigned flocks through Roles.ResolveEffective and EffectiveAccountRole (#1097).",
"Cluckwork.Application.Modules.Access.Users.AssignFlock.AssignFlockHandler",
"Cluckwork.Application.Modules.Access.Users.AssignFlock.UnassignFlockHandler",
"Cluckwork.Application.Modules.Access.Users.ChangeUserRole.ChangeUserRoleValidator",
Expand Down
4 changes: 2 additions & 2 deletions src/Cluckwork.Domain/Common/Architecture/Modules/Commerce.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Cluckwork.Domain.Common.Architecture;
"Cluckwork.Application.Modules.Commerce.Sales.ConfirmSale.ConfirmSaleHandler")]
[ModuleEdge(
"Commerce", "EggOperations", "W",
"FIFO egg stock. ConfirmSaleHandler and VoidSaleHandler lock, draw from and return egg lots through Egg Operations' IEggStock port, which adds the lot changes and Sale or Void movements to the confirm and void transactions and never saves (#854); Commerce sees plans and lot ids, never Domain.Eggs.EggLot. Products, order lines and confirm refusals read grades through IEggGradeLookup in CreateProductHandler, UpdateProductHandler, AddOrderItemHandler and ConfirmSaleHandler. Design 3.4 row Commerce -> Egg Ops = W. Commerce's EF configurations declare its foreign keys into Egg Operations' tables: ProductEggGradeMappingConfiguration and SalesOrderItemConfiguration to EggGrade, SalesOrderAllocationConfiguration to EggLot (#1087 S9).",
"FIFO egg stock. ConfirmSaleHandler and VoidSaleHandler lock, draw from and return egg lots through Egg Operations' IEggStock port, which adds the lot changes and Sale or Void movements to the confirm and void transactions and never saves (#854); Commerce sees plans and lot ids, never EggLot. Products, order lines and confirm refusals read grades through IEggGradeLookup in CreateProductHandler, UpdateProductHandler, AddOrderItemHandler and ConfirmSaleHandler. Design 3.4 row Commerce -> Egg Ops = W. Commerce's EF configurations declare its foreign keys into Egg Operations' tables: ProductEggGradeMappingConfiguration and SalesOrderItemConfiguration to EggGrade, SalesOrderAllocationConfiguration to EggLot (#1097).",
"Cluckwork.Application.Modules.Commerce.Catalog.CreateProduct.CreateProductHandler",
"Cluckwork.Application.Modules.Commerce.Catalog.UpdateProduct.UpdateProductHandler",
"Cluckwork.Application.Modules.Commerce.Sales.AddOrderItem.AddOrderItemHandler",
Expand All @@ -23,7 +23,7 @@ namespace Cluckwork.Domain.Common.Architecture;
"Cluckwork.Infrastructure.Modules.Commerce.Configurations.SalesOrderItemConfiguration")]
[ModuleEdge(
"Commerce", "Farm", "R",
"Orders and products snapshot the farm's currency through Farm's IAccountRepository (CreateSalesOrderHandler, CreateProductHandler, UpdateProductHandler), with the sales-order path holding the FOR SHARE lock on the account row (#162) so an order cannot land in a denomination the farm is mid-flight out of; ConfirmSaleHandler also reads the farm's discount ceiling and worker sale-allocation policy off Account. Design 3.4 row Commerce -> Farm = R. PaymentRepository, ProductRepository and SalesOrderRepository implement Farm's ICurrencyBoundRowSource, so Farm's currency probe can ask whether their tables hold farm-currency amounts (#1087 S9).",
"Orders and products snapshot the farm's currency through Farm's IAccountRepository (CreateSalesOrderHandler, CreateProductHandler, UpdateProductHandler), with the sales-order path holding the FOR SHARE lock on the account row (#162) so an order cannot land in a denomination the farm is mid-flight out of; ConfirmSaleHandler also reads the farm's discount ceiling and worker sale-allocation policy off Account. Design 3.4 row Commerce -> Farm = R. PaymentRepository, ProductRepository and SalesOrderRepository implement Farm's ICurrencyBoundRowSource, so Farm's currency probe can ask whether their tables hold farm-currency amounts (#1097).",
"Cluckwork.Application.Modules.Commerce.Catalog.CreateProduct.CreateProductHandler",
"Cluckwork.Application.Modules.Commerce.Catalog.UpdateProduct.UpdateProductHandler",
"Cluckwork.Application.Modules.Commerce.Sales.ConfirmSale.ConfirmSaleHandler",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace Cluckwork.Domain.Common.Architecture;
])]
[ModuleEdge(
"EggOperations", "Farm", "R",
"CreateEggGradeHandler attaches a new grade to Domain.Accounts.SeedDefaults.FarmId, the single-farm stand-in for the farm the grade belongs to; EggGradeFloorPolicy resolves the caller's effective role through Domain.Accounts.Roles to decide whether a grade's low-stock floor may move (#911, Owner-only per #729). Design 3.4 row Egg Ops -> Farm = R. EggOperationsFixture counts the simulation fixture's daily entries at SeedDefaults.FarmId and SeedDefaults.HouseId (#858, #1087 S9), and EggGradeProvisioning places a new farm's default grades on SeedDefaults.FarmId (#1116).",
"CreateEggGradeHandler attaches a new grade to Farm's SeedDefaults.FarmId, the single-farm stand-in for the farm the grade belongs to; EggGradeFloorPolicy resolves the caller's effective role through Farm's Roles to decide whether a grade's low-stock floor may move (#911, Owner-only per #729). Design 3.4 row Egg Ops -> Farm = R. EggOperationsFixture counts the simulation fixture's daily entries at SeedDefaults.FarmId and SeedDefaults.HouseId (#858, #1097), and EggGradeProvisioning places a new farm's default grades on SeedDefaults.FarmId (#1116).",
"Cluckwork.Application.Modules.EggOperations.EggGrades.CreateEggGrade.CreateEggGradeHandler",
"Cluckwork.Application.Modules.EggOperations.EggGrades.EggGradeFloorPolicy",
"Cluckwork.Infrastructure.Modules.EggOperations.Repositories.EggGradeProvisioning",
Expand Down
Loading
Loading