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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ dotnet test Cluckwork.sln # 2887 tests as of 2026-09-21; integr

- **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 and Egg Operations are contracted today. The check covers adapters, not peer modules, because the edge walk records namespaces rather than types. → [`849-module-contract.md`](docs/decisions/849-module-contract.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. Nothing checks peer modules: a peer that injects `IFlockRepository` again stays green. → [`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. `CurrencyBoundRowProbe` stays a compatibility exception until Commerce's contract (#854). → [`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`. Commerce's handlers still reach lots through `IEggLotRepository` until #854 builds the stock seam. → [`853-egg-operations-contract.md`](docs/decisions/853-egg-operations-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 and Commerce are contracted today. The check covers adapters, not peer modules, because the edge walk records namespaces rather than types. → [`849-module-contract.md`](docs/decisions/849-module-contract.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. Nothing checks peer modules: a peer that injects `IFlockRepository` again stays green. → [`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
6 changes: 4 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ complete set of inventory writers:
|---|---|---|
| `DailyEntry.Submit` | `SubmitDailyEntryHandler` | creates lots |
| `DailyEntry` adjust / void | `AdjustDailyEntryHandler`, `VoidDailyEntryHandler` | `EggLot.AdjustProduction` reconciles the lot down to what the corrected day says, with the already-sold amount as the floor |
| `SalesOrder.Confirm` / `Void` | `ConfirmSaleHandler`, `VoidSaleHandler` | `Allocate` / `Restore` |
| `SalesOrder.Confirm` / `Void` | `ConfirmSaleHandler`, `VoidSaleHandler`, through Egg Operations' `IEggStock` | `Allocate` / `Restore` |
| **Manual stock movement** | `RecordEggLotMovementHandler` (`/stock`) | `EggLot.AdjustAvailable` for a `Discard`, `InternalUse` or `Reconciliation` movement — no daily entry and no sale involved |

Anything touching lot concurrency or the movement ledger has to account for all
Expand Down Expand Up @@ -122,7 +122,9 @@ allocation before the state changes — insufficient stock on any line aborts th
transaction, so a half-allocated confirmed order cannot exist. Lots are drawn
**FIFO by `ProductionDate`, then `Id`** as tiebreaker
(`Infrastructure/Repositories/EggLotRepository.cs`), locked `FOR UPDATE`, and
each draw writes a `SalesOrderAllocation` row. `Void` re-locks those same lots
each draw writes a `SalesOrderAllocation` row. Commerce reaches the lots only
through Egg Operations' `IEggStock` port, which locks, plans and draws inside
the confirm's transaction and never saves (#854). `Void` re-locks those same lots
**in the same order** — that shared ordering is what stops confirm and void
deadlocking against each other — restores each quantity, and marks the
allocation rows released rather than deleting them.
Expand Down
Loading
Loading