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
2 changes: 1 addition & 1 deletion src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ record you must read before changing the rule. Persistence edits also read
- **Adapters reach a contracted module only through the types its ledger row lists (#514/#849).** A module contract is one `I<Module>Module` interface, implemented by a `<Module>Module` that delegates to the existing handlers and repositories and is registered beside them. Every contract type sits top-level in its own file in the module's `Modules/<Owner>/Contracts/` folder and namespace (#1087): the interface, its commands and its `*Details` result records, and in Domain the enums entities use. `RealModuleLedger` collects them into the owner's `Contract` through `ModuleContracts.OwnerOf`, the rule CW1004 applies too. A type nested in a contract type through public types only, such as `FlockNameResolution.Found`, is contract as well, and the purity guard walks it; a nested type behind a private or internal level is not (#1116). A `Contracts` namespace naming no owner fails. `NamespaceFolderAgreementTests` keeps every file's namespace equal to its folder, so a file moved out of `Contracts/` cannot keep its contract namespace. `AdapterReachRealTreeTests` fails on an adapter parameter or service resolution that reaches the owner through any other type. It also fails on a listed type that is not declared or not owned by that module. `ModuleContractRealAssemblyTests` rejects any persistence type, entity or aggregate inside a contract type at any depth. That rule is stricter than #847, which allows concrete aggregates on other seams. Endpoints still call `IValidator<TCommand>` because commands are contract types. Finance, Farm, Flock Management, General Inventory, Egg Operations, Commerce and Insights are contracted today. `IInsightsModule` inherits `IReportQueries` and `IExportQueries`; never move either into `Contracts/`, because doing so lets any adapter or peer inject it past the ledger guards and skip the facade. → [`849-module-contract.md`](../docs/decisions/849-module-contract.md) **Peer modules reach a contracted module only through its contract or `seam` (#1023).** `PeerContractRealTreeTests` runs the adapter walk over every module's own namespaces and fails when a member takes another contracted module's type listed in neither its `Contract` nor its `Seam`. Adapters never get the seam. Farm's seam is `IAccountRepository`, `Account`, `UserRoleAssignment` and `ICurrencyBoundRowSource`, the currency probe port peer repositories implement (#1116); add to it only on purpose. Every `contract` and `seam` entry names a non-generic type, because the walk drops generic arity, and a `seam` entry a top-level one; a nested contract entry's reflection `+` is compared as the source's `.`. Like the adapter check, it reads parameter types and service resolutions, never return types or method bodies. → [`1023-peer-contract-guard.md`](../docs/decisions/1023-peer-contract-guard.md) **Farm's contract stops at settings, logo and banner (#851), plus the cross-farm `IFarmDirectory` (#858).** `IFarmDirectory` ignores the tenant filter so the operator verbs and the lock sweep can list farms and resolve a farm code; `FarmDirectoryCallerTests` fails when a file outside `Api/Cli`, `Infrastructure/Jobs` and the directory's declaration, implementation and registration names it or `AccountRepository`, because a request path that reached it could show one farm another's. Sign-in resolves its farm code through `IIdentityProvider.ResolveFarmCodeAsync`, never `IFarmModule`, because that lookup establishes identity before `TenantContext` exists. That method is the only caller of the seam's cross-farm `IAccountRepository.FindBySlugAsync`; `FindBySlugCallerTests` fails when any other member references it (#1053). `FarmClock` stays on `IAccountRepository` because it sits in the Platform hub. Peer modules keep calling `IAccountRepository` and `Domain.Accounts` directly, the #162 locked currency snapshot included; do not fold that seam into the contract. → [`851-farm-contract.md`](../docs/decisions/851-farm-contract.md) **Flock Management has three contract interfaces (#852).** Adapters call `IFlockModule`; peer modules call the `IFlockLookup` read port and the `IMortalityLedger` write port, so a peer request does not construct six lifecycle handlers it never uses. `IMortalityLedger.AppendAsync` adds the movement to the caller's unit of work and never saves; keep it that way, because `MortalityLedgerAtomicityTests` is all that holds Submit's mortality in the entry's commit. `IFlockLookup.ResolveByNameAsync` returns `Ambiguous` with every candidate on a duplicate name, never a first match. A peer that injects `IFlockRepository` again fails `PeerContractRealTreeTests` (#1023). → [`852-flock-contract.md`](../docs/decisions/852-flock-contract.md) **General Inventory keeps `RecordFeedUsage`'s lock order (#855).** The handler locks the item, reads flock eligibility through `IFlockLookup`, then locks the FIFO lots, in one transaction; `FeedUsageLockOrderTests` fails if either read moves. Keep `IFlockLookup`'s reads untracked (#1022): a tracked read hands the in-transaction check the flock loaded before the transaction, stale. → [`855-inventory-contract.md`](../docs/decisions/855-inventory-contract.md) **Egg lots lock in `(ProductionDate, Id)` order on every path (#853).** Confirm, sale void and daily-entry adjust and void each take `FOR UPDATE` on `EggLots` in that order, so they queue instead of deadlocking; before #853 nothing failed when it changed. `EggLotLockOrderTests` fails when a request locks the later of two lots first, on different dates and on one date. `EggLotLockSqlTests` fails when any `FOR UPDATE` over `EggLots` stops ending `ORDER BY "ProductionDate", "Id"`; it covers the one change the behaviour test cannot see, a dropped `Id` tie-breaker on the sale-void read, whose rows the confirm has already rewritten in canonical order. Egg Operations splits like #852: adapters call `IEggOperationsModule`, and peers call `IEggGradeLookup` and `IDailyEntryLookup`; `SaleEndpoints`, an adapter that needs only grade names, also takes `IEggGradeLookup`. → [`853-egg-operations-contract.md`](../docs/decisions/853-egg-operations-contract.md) **Commerce reaches egg stock only through `IEggStock` (#854).** Confirm and void call Egg Operations' `IEggStock`: one farm-wide `FOR UPDATE` per confirm, a planner over the locked lots that changes nothing, so the #612 assigned-flock retry takes no second lock, and draws and restores that join the caller's transaction and never save. The port refuses to run without a transaction, and a reservation refuses to plan or draw once the transaction that locked it is no longer current (`EggStockTransactionTests`). Commerce never sees `EggLot`: `PeerContractRealTreeTests` fails when a Commerce type takes an Egg Operations type outside its contract, though it reads only parameter types and service resolutions, not return types or method bodies. `ConfirmSaleAtomicityTests` fails when the confirm runs outside its transaction or saves a draw before the end, each on its own. `EggLotLockOrderTests` has a two-grade confirm because a single grade cannot show a lock per grade. Farm's currency probe asks every `ICurrencyBoundRowSource`, which the repository owning each probed table implements; a new table that stores a farm-currency amount registers one. Adapters call `ICommerceModule`; peers read packed-unit conversions through `IEggUnitConversionLookup`. → [`854-commerce-contract.md`](../docs/decisions/854-commerce-contract.md)
- **Access has an aggregate-free contract and owns its Common identity ports (#857/#1023).** Its 29 ledger entries separate request/profile operations, authorization lookup, epoch verification, purge, operations, lifecycle and fixture reads. `IIdentityProvider` and `IStepUpGrantService` sit in Access's own namespace (`Application/Modules/Access/Users`), outside the contract; a peer or adapter taking either fails the ownership walk. Fixture reads use `IAccessSeedLookup`, registered outside Production, so demo does not construct IdentityProvider or Redis. `ICurrentUser` and `CurrentUserContext` remain one scoped instance; the context and `SimulationOptions` belong to Persistence. Assignment reads return null only for zero rows or any null-flock row; failures propagate, and otherwise every assigned flock remains in the set. Lifecycle verbs must resolve without UserManager, Identity or Redis. Simulation's flock assignment goes through `IAccessFixture`, also registered outside Production: it skips only the interactive handler's step-up and Worker-role checks, and refuses another farm's user, flock or tenant (#858). Both seeders read the Owner-role prerequisite through `IAccessSeedLookup` (#858); `AccessModule` names a user's flock assignments through `IFlockLookup.GetDisplayNamesAsync` (#859). Session reshaping needs a separate design. → [`857-access-contract.md`](../docs/decisions/857-access-contract.md)
- **Register every read of a contracted module's tables from outside it (#514/#850).** `CompatibilityExceptionRealTreeTests` compiles `src/Cluckwork.Infrastructure` from source, and the projects referencing it with errors tolerated. It finds every member that obtains a `DbSet<T>`, maps `T` through the EF model to the tables a query touches (its own, owned values mapped apart and derived types), and takes each table's owner from the table-owner walk, so a `TableOwnerOverrides` row outranks the entity's namespace. A read of a contracted module's table is allowed for the module's own types (its repositories included, since #1087 S9), a type its ledger edge's `Symbols` names, and a `DbSet` property whose whole body is `Set<T>()`. Every other read needs a `RealModuleLedger.CompatibilityExceptions` row, keyed by namespace, type and member, with `Reaches`, the `Tables` read, a ledger `Owner`, a `Reason` and `DeleteWhen`, the slice issue that deletes it. `DeleteWhen` is `#<issue>`, never a date. A new read, a table the row does not name, a `DbSet` of a type parameter and an unbound candidate name fail; a row or table no longer read fails as stale; an Infrastructure compile error fails closed. SQL text is not read. → [`850-compatibility-exceptions.md`](../docs/decisions/850-compatibility-exceptions.md)
- **Seeders and operator verbs compose modules through contracts and fixture ports (#514/#858).** Seeder-only reads and writes go through `I<Module>Fixture` (Access: `IAccessSeedLookup`) in the module's `Contracts/` folder, and registered only inside `AddCluckworkPersistence`'s `!IsProduction()` block; members return ids, counts, records or a `Result`, never entities or queries, and purges join the caller's transaction and never commit. `FixturePortRegistrationTests` runs the real API per process role and environment and fails if Production resolves any `I*Fixture`; name a new port that way or add it beside `IAccessSeedLookup`. Cross-farm operator reads use Farm's `IFarmDirectory`, named only where `FarmDirectoryCallerTests` allows and never behind a forwarder. Register services in the owner's file under `Cluckwork.Api/Modules/<Owner>/` and list a new module once in `AddCluckworkModules`. `RealModuleLedger.CompatibilityExceptions` is empty since #859. Order the guards cannot see is kept, not enforced: the first two `ICurrencyBoundRowSource` registrations and demo cleanup's flocks-last delete. → [`858-platform-composition.md`](../docs/decisions/858-platform-composition.md)
- **Seeders and operator verbs compose modules through contracts and fixture ports (#514/#858).** Seeder-only reads and writes go through `I<Module>Fixture` (Access: `IAccessSeedLookup`) in the module's `Contracts/` folder, and registered only inside `AddCluckworkPersistence`'s `!IsProduction()` block; members return ids, counts, records or a `Result`, never entities or queries, and purges join the caller's transaction and never commit. `FixturePortRegistrationTests` runs the real API per process role and environment and fails if Production resolves any `I*Fixture`; name a new port that way or add it beside `IAccessSeedLookup`. Cross-farm operator reads use Farm's `IFarmDirectory`, named only where `FarmDirectoryCallerTests` allows and never behind a forwarder. Register services in the owner's file under `Cluckwork.Api/Modules/<Owner>/` and list a new module once in `AddCluckworkModules`. Farm provisioning is the one Production writer into peers: `AccountProvisioner` stages a new farm's default grades and unit conversions through Egg Operations' `IEggGradeProvisioning` and Commerce's `IEggUnitConversionProvisioning`, which stage for the resolved tenant into the caller's context, refuse to run outside its transaction, and never save (#1116, `ProvisioningPortTests`). `RealModuleLedger.CompatibilityExceptions` is empty since #859. Order the guards cannot see is kept, not enforced: the first two `ICurrencyBoundRowSource` registrations and demo cleanup's flocks-last delete. → [`858-platform-composition.md`](../docs/decisions/858-platform-composition.md)

### Data and correctness

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public static IServiceCollection AddCommerceModule(this IServiceCollection servi
services.AddScoped<
Cluckwork.Application.Modules.Commerce.Contracts.IEggUnitConversionLookup,
Cluckwork.Application.Modules.Commerce.Catalog.EggUnitConversionLookup>();
services.AddScoped<IEggUnitConversionProvisioning, EggUnitConversionProvisioning>();
services.AddScoped<VoidPaymentHandler>();
services.AddScoped<
Cluckwork.Application.Modules.Commerce.Catalog.CreateProduct.CreateProductHandler>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public static IServiceCollection AddEggOperationsModule(this IServiceCollection
Cluckwork.Application.Modules.EggOperations.Eggs.EggOperationsModule>();
services.AddScoped<IEggGradeLookup, EggGradeLookup>();
services.AddScoped<IEggStock, EggStock>();
services.AddScoped<IEggGradeProvisioning, EggGradeProvisioning>();
services.AddScoped<IDailyEntryLookup, DailyEntryLookup>();

return services;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Cluckwork.Application.Modules.Commerce.Contracts;

// #1116: Access provisioning's one write into Commerce. Stages a new farm's default unit conversions for the
// resolved tenant into the caller's unit of work, inside the caller's transaction, and never saves; the caller commits.
public interface IEggUnitConversionProvisioning
{
void StageDefaults();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Cluckwork.Application.Modules.EggOperations.Contracts;

// #1116: Access provisioning's one write into Egg Operations. Stages a new farm's default grades for the resolved
// tenant into the caller's unit of work, inside the caller's transaction, and never saves; the caller commits.
public interface IEggGradeProvisioning
{
void StageDefaults();
}
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 inserts EggUnitConversion.Defaults for a new farm inside its provisioning transaction; 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 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).",
"Cluckwork.Application.Modules.Access.Contracts.UserProfile",
"Cluckwork.Application.Modules.Access.Users.IIdentityProvider",
"Cluckwork.Application.Modules.Access.Users.SetStepperUnit.SetStepperUnitHandler",
Expand All @@ -18,7 +18,7 @@ namespace Cluckwork.Domain.Common.Architecture;
"Cluckwork.Infrastructure.Modules.Access.Configurations.ApplicationUserConfiguration")]
[ModuleEdge(
"Access", "EggOperations", "W",
"W because AccountProvisioner inserts a new farm's default grades, Domain.Eggs.EggGrade.Defaults(accountId, SeedDefaults.FarmId), inside its provisioning transaction, so farm provisioning writes Egg Operations' table and depends on its grade shape. Design 3.4 shows Access -> Egg Ops as none; this is live coupling the target design has still to remove.",
"W because AccountProvisioner stages a new farm's default grades through Egg Operations' IEggGradeProvisioning port inside its provisioning transaction (#1116), so farm provisioning writes Egg Operations' table while Egg Operations builds the rows and owns their shape. Design 3.4 shows Access -> Egg Ops as none; this is live coupling the target design has still to remove.",
"Cluckwork.Infrastructure.Modules.Access.Identity.AccountProvisioner")]
[ModuleEdge(
"Access", "Farm", "W",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ 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).",
"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).",
"Cluckwork.Application.Modules.EggOperations.EggGrades.CreateEggGrade.CreateEggGradeHandler",
"Cluckwork.Application.Modules.EggOperations.EggGrades.EggGradeFloorPolicy",
"Cluckwork.Infrastructure.Modules.EggOperations.Repositories.EggGradeProvisioning",
"Cluckwork.Infrastructure.Modules.EggOperations.Repositories.EggOperationsFixture")]
[ModuleEdge(
"EggOperations", "FlockManagement", "W",
Expand Down
Loading
Loading