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 docs/decisions/1023-peer-contract-guard.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ inside a body, but nothing else in the body.
them today. #1013 hit the same arity loss in the compatibility-exception
scanner.
- Its own module, Platform code, and modules with no contract are not checked.
Today those are Access, until #857 declares its contract, and Insights.
Today that is Access, until #857 declares its contract.

Break it and a peer that injects another module's repository passes CI, and the
contract stops meaning anything between modules.
Expand Down
2 changes: 1 addition & 1 deletion src/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ record you must read before changing the rule. Persistence edits also read

- **The coupling matrix is generated from the ledger and its three walks (#848).** `CouplingMatrixRealTreeTests` validates the module-edge, EF-model and adapter reports before rendering `tests/Cluckwork.Application.Tests/Architecture/Data/coupling-matrix.md`, then compares normalized line endings with the committed file. Regenerate with `CLUCKWORK_REGENERATE_MATRIX=1 dotnet test tests/Cluckwork.Application.Tests --filter FullyQualifiedName~CouplingMatrixRealTreeTests`; do not hand-edit the Markdown. Its observable-differences table has seven `W`/`R`/dash mismatches with §3.4. A separate table records its unobservable `E`, `Q` and Platform cells. `P` is the free Platform hub, and `A` counts adapter reaches. → [`848-generated-coupling-matrix.md`](../docs/decisions/848-generated-coupling-matrix.md)

- **Adapters reach a contracted module only through the types its ledger row lists (#514/#849).** A module contract is one `I<Module>Module` interface in the module's own Application namespace, implemented by a `<Module>Module` that delegates to the existing handlers and repositories and is registered beside them. `owners.<Module>.contract` in `module-ledger.json` lists every contract type: the interface, its commands and its `*Details` result records. `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 and Commerce are contracted today. → [`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 `owners.<Module>.contract` nor `owners.<Module>.seam`. Adapters never get the seam. Farm's seam is `IAccountRepository`, `Account` and `UserRoleAssignment`; add to it only on purpose. `owners.<Module>.types` claims a type that sits in a Platform namespace, so the check stops treating it as free hub. Every `contract`, `seam` and `types` entry names a top-level, non-generic type, and a claimed type declares no nested types, because the walk drops generic arity. 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).** Sign-in resolves its farm code through `IIdentityProvider.ResolveFarmCodeAsync`, never `IFarmModule`, because that lookup establishes identity before `TenantContext` exists. `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)
- **Adapters reach a contracted module only through the types its ledger row lists (#514/#849).** A module contract is one `I<Module>Module` interface in the module's own Application namespace, implemented by a `<Module>Module` that delegates to the existing handlers and repositories and is registered beside them. `owners.<Module>.contract` in `module-ledger.json` lists every contract type: the interface, its commands and its `*Details` result records. `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. → [`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 `owners.<Module>.contract` nor `owners.<Module>.seam`. Adapters never get the seam. Farm's seam is `IAccountRepository`, `Account` and `UserRoleAssignment`; add to it only on purpose. `owners.<Module>.types` claims a type that sits in a Platform namespace, so the check stops treating it as free hub. Every `contract`, `seam` and `types` entry names a top-level, non-generic type, and a claimed type declares no nested types, because the walk drops generic arity. 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).** Sign-in resolves its farm code through `IIdentityProvider.ResolveFarmCodeAsync`, never `IFarmModule`, because that lookup establishes identity before `TenantContext` exists. `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)
- **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 ledger's `tables`, never the entity's namespace. A read of a contracted module's table is allowed for the module's own types, a type its ledger edge's `symbols` names, a type listed under `owners.<Module>.implementations`, and a `DbSet` property whose whole body is `Set<T>()`. Every other read needs a `compatibilityExceptions` row in `module-ledger.json`, 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. The next contract slice lists its implementations and registers its module's other readers. → [`850-compatibility-exceptions.md`](../docs/decisions/850-compatibility-exceptions.md)

### Data and correctness
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,20 @@
"Cluckwork.Infrastructure.Repositories.ExpenseCategoryRepository",
"Cluckwork.Infrastructure.Repositories.ExpenseRepository"
] },
"Insights": { "kind": "module", "namespaces": ["Cluckwork.Application.Features.Audit", "Cluckwork.Application.Features.Reports", "Cluckwork.Application.Features.Export", "Cluckwork.Application.Features.Insights", "Cluckwork.Infrastructure.Insights"] },
"Insights": { "kind": "module", "namespaces": ["Cluckwork.Application.Features.Audit", "Cluckwork.Application.Features.Reports", "Cluckwork.Application.Features.Export", "Cluckwork.Application.Features.Insights", "Cluckwork.Infrastructure.Insights"],
"contract": [
"Cluckwork.Application.Features.Insights.IInsightsModule",
"Cluckwork.Application.Features.Insights.AuditEventRead",
"Cluckwork.Application.Features.Audit.EntityProvenance",
"Cluckwork.Application.Features.Export.ExportDataset",
"Cluckwork.Application.Features.Reports.ProductionReport",
"Cluckwork.Application.Features.Reports.ProductionDay",
"Cluckwork.Application.Features.Reports.GradeTotal",
"Cluckwork.Application.Features.Reports.SalesSummary",
"Cluckwork.Application.Features.Reports.ExpenseSummary",
"Cluckwork.Application.Features.Reports.ExpenseCategoryTotal",
"Cluckwork.Application.Features.Reports.ProfitReport"
] },
"Platform": { "kind": "platform", "namespaces": ["Cluckwork.Domain.Common", "Cluckwork.Domain.Auditing", "Cluckwork.Application.Common", "Cluckwork.Infrastructure", "Cluckwork.Api", "Cluckwork.AppHost"], "exactNamespaces": ["Cluckwork.Domain", "Cluckwork.Application"] }
},
"edges": [
Expand Down
Loading