Repository navigation
Modular-monolith architecture design + implementation plan (from PR #423) #514
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentation
on Aug 12, 2026 docs/architecture/modular-monolith-design.md
Modular-monolith design
Status: proposed architecture; no production refactor is authorized by this document.
Evidence date: 2026-08-04, branchwork.graphify-out/graph.jsonexists, but the
graphifyexecutable was unavailable in the analysis environment. The inventory was
therefore produced from project references, namespace references, constructors, EF
configuration, endpoints, tests, seeders and SPA calls. Folder names were not treated as
boundary evidence.1. Decision and vocabulary
Adopt a hybrid modular monolith: business-module interface and implementation
assemblies, a small shared kernel, a platform/persistence assembly, and the existing API
as composition root. It remains one process, one deployment and one PostgreSQL database.- A module owns a cohesive business responsibility, its rules and its data.
- A module's public interface is the narrow contract other modules and host adapters
compile against; its implementation is internal and cannot be referenced by peers. - A seam is a deliberate call/event boundary. An HTTP endpoint, CLI command or job is
an adapter at a seam, not a place for business orchestration. - Depth means substantial policy behind a small interface. Leverage is how many
callers gain that policy without learning internals. Locality means a change to a
business rule normally stays inside its owning module.
The recommended business modules are Access, Farm, Flock Management, Egg
Operations, Commerce, General Inventory, Finance, and Insights. Platform
is a host capability. Egg Operations deliberately merges daily capture, egg grades, egg lots
and the egg ledger because submission/correction prove that they share invariants. Commerce
owns the customer-to-payment lifecycle and calls one deep stock seam.2. At-a-glance diagrams
These diagrams are intentionally higher-level than the inventory tables below. They show
what changes for a reader who wants the shape of the architecture before the details.2.1 Current state — layered monolith with feature folders but shared internals
Browser SPA | v Cluckwork.Api minimal endpoints, middleware, jobs, CLI verbs, DI registration | | direct handler calls v Cluckwork.Application feature folders: Accounts, DailyEntries, Flocks, Inventory, Sales, Users, ... handlers can reference common ports and feature repositories across folders | | repository interfaces / unit of work / identity ports v Cluckwork.Infrastructure EF repositories, Identity provider, seeders, read queries, background jobs | | one AppDbContext exposes all sets v PostgreSQL database Accounts, Flocks, DailyEntries, EggLots, EggInventoryMovements, SalesOrders, SalesOrderAllocations, Payments, InventoryLots, Expenses, ... Cluckwork.Domain sits underneath the application layer and contains aggregates/value objects for all capabilities. Because the monolith is layered by technical concern, feature folders improve navigation but do not enforce module ownership.Current-state coupling hot spots:
DailyEntries ---> EggGrades / EggLots / EggInventoryMovements / Flocks Sales ---> Catalog / Customers / EggGrades / EggLots / EggInventoryMovements Inventory ---> Accounts / Flocks Expenses ---> Accounts / Flocks Users ---> Flocks Meaning: these are not necessarily wrong business dependencies, but today they are implemented through broad project/folder visibility rather than deliberate module seams.2.2 Proposed state — modular monolith with explicit seams
Browser SPA | v Cluckwork.Api HTTP / CLI / job adapters only | | calls contracts, not repositories or implementations v Module contracts Access.Contracts Farm.Contracts FlockManagement.Contracts EggOperations.Contracts Commerce.Contracts Inventory.Contracts Finance.Contracts Insights.Contracts | | DI wires each contract to one hidden implementation v Module implementations Access.Implementation Farm.Implementation FlockManagement.Implementation EggOperations.Implementation Commerce.Implementation Inventory.Implementation Finance.Implementation Insights.Implementation | | approved persistence/session abstractions only v Platform.Persistence single AppDbContext, migrations, tenant filter/stamping, transactions, idempotency, audit append, execution strategy | v Same PostgreSQL database SharedKernel tiny primitives only: Result/Error, Money, clocks/paging DTOs as neededThe important change is not process count. The important change is that module
implementations become private and callers cross a deliberate interface seam.2.3 Cross-module write seams that stay synchronous
Egg Operations.SubmitDailyEntry | same ambient transaction +--> Flock Management.AppendMortalityMovement +--> Egg Operations creates lots + egg-ledger opening movements +--> commits once, then publishes read events (SubmitDailyEntryHandler injects no IAuditWriter today — no audit row) Commerce.ConfirmSale | same ambient transaction +--> Egg Operations.ReserveFifo +--> Commerce updates order and allocation state (no payment access — ConfirmSaleHandler does not touch payments) +--> commits once, then publishes read events (ConfirmSaleHandler injects no IAuditWriter today — no audit row) Commerce.VoidSale | same ambient transaction +--> Egg Operations.RestoreReservation +--> Commerce updates order/allocation state and GATES on payments (rejects if any non-voided payment exists via IPaymentRepository.AnyNonVoidedByOrderAsync — does not mutate them; payments are voided separately through VoidPaymentHandler) +--> Platform appends the audit row in the SAME transaction, commits once, then publishes read events (VoidSaleHandler already injects IAuditWriter today) Inventory.RecordFeedUsage | inventory transaction, item lock held first +--> Flock Management.GetFlockProductionEligibility (enlisted, after the item lock) +--> Inventory consumes FIFO lots and writes movement/usage rowscodex review, PR #423 round 6: the previous version of this diagram showed every one of these
commands appending an audit row inside its transaction. That's true today only for the
adjust/void paths (AdjustDailyEntryHandler,VoidDailyEntryHandler,VoidSaleHandler,
VoidPaymentHandler— each already injectsIAuditWriter);SubmitDailyEntryHandlerand
ConfirmSaleHandlerinject noIAuditWriterand write no audit row. Since this plan's delivery
rule (implementation-plan.md) is "no schema/behavior changes are expected," a diagram implying an
audited-write panel exists for every shown command would have an implementer add audit rows these
commands don't emit today — a real product change, not the refactor this plan authorizes. Split
above to say only what's true now.codex review, PR #423 round 7: the previous version also folded payment mutation into
confirm/void. It doesn't happen there:ConfirmSaleHandlernever references payments at all,
andVoidSaleHandleronly checksIPaymentRepository.AnyNonVoidedByOrderAsync— a guard that
rejects the void if a payment exists — it never mutates one. Payments are recorded/voided through
their own separate commands (VoidPaymentHandleris one of the adjust/void handlers already
listed above as the ones that do audit). Corrected above to describe VoidSale's payment
interaction as a gate, not a mutation, for the same no-behavior-change reason as the audit fix.These seams stay in-process because Cluckwork must preserve atomic ledger updates,
FIFO lock ordering, optimistic concurrency and existing HTTP/idempotency contracts.
The audit row is one of those atomic writes, not a decoupled event: it's appended
in the SAME transaction as the mutation it records (§3.5 item 4, §6's Platform row)
and would be rolled back with it on failure. Only the READ-model publish happens
after commit — a crash between commit and that publish loses a cache refresh, never
an audit trail entry.3. Current-state evidence
3.1 Assembly and composition shape
ApplicationreferencesDomain;Infrastructurereferences both;Apireferences all
three. The API composition root registers feature repositories and handlers one by one.
AppDbContextinherits the Identity context and exposes every business and platform set.
This is useful runtime unity but accidental compile-time coupling: any infrastructure
class can access any table and any application handler can inject any feature repository.Shared abstractions are:
Domain.Common:Entity,AggregateRoot,ValueObject,Money,Result,Error,
and domain-event marker.Result/Errorand the smallest identity-free primitives are
legitimate shared-kernel candidates; aggregate bases remain implementation details.Application.Common: clock/farm clock, current user, audit writer, identity provider,
flock-scope guard, generic repository, unit of work, paging and result logging. The
clocks/paging/result DTOs are shared contracts. Repositories,IUnitOfWork, audit,
identity, and flock scope are currently high-leverage but boundary-erasing ports and
must move behind owners or Platform contracts.- Infrastructure: one context, tenant context/stamping, transaction/execution strategy,
repositories, Identity, jobs, seeders and cross-module read queries.
3.2 Capability inventory
Capability Domain model Application/infra/API SPA alignment Access UserRoleAssignment; account rolesuser handlers, IIdentityProvider, Identity users/roles/refresh tokens, credential middleware, auth/users/me endpoints, bootstrap/recovery/purgelogin, set-password, users; auth/session contexts Farm Account,FarmLogo, currency/unit/brand settingsaccount/logo handlers and repos, tenant/farm clocks, account endpoints settings/account, farm context Flock Management Flock,BirdMovementflock handlers/repos and lifecycle/scope checks flocks Egg Operations DailyEntry/lines,EggGrade,EggLot, egg movementsdaily-entry, grade and stock handlers/repos; lock-sweep daily entry, history, grades, stock Commerce Product, conversions/mappings,Customer,SalesOrder, items, allocations,Paymentcatalog/customer/sales handlers/repos products, customers, sales General inventory item, lot, movement, feed usage, water usage inventory/water handlers and repos inventory, water Finance expense/category expense handlers/repos expenses Insights no write aggregates; report/export DTOs ReportQueries,ExportQueries, audit querydashboard, reports, export, audit Platform audit event plus operational records AppDbContext, tenant interceptor, unit of work, idempotency, durable jobs, health, migrations, seeders, CLIclient error/idempotency transport only There are 49 application handlers. A source-namespace scan found these direct feature
reference counts: Production-shapedDailyEntriespoints to EggGrades 5, EggLots 3,
Eggs 3 and Flocks 4; Sales points to Accounts 1, Catalog 1, Customers 1, EggGrades 2,
EggLots 2 and Eggs 2; Inventory points to Accounts 3 and Flocks 3; Catalog points to
Accounts 2 and EggGrades 2; Expenses points to Accounts 1 and Flocks 2; Users points to
Flocks 1. These counts measure source references, not runtime weight.3.3 Tables, owners and cross-owner foreign keys
Owner Tables Access AspNetUsers,AspNetRoles, Identity join/claim/token tables,refresh_tokens,UserRoleAssignmentsFarm Accounts,FarmLogosFlock Management Flocks,BirdMovementsEgg Operations DailyEntries,DailyEntryGrades,EggGrades,EggLots,EggInventoryMovementsCommerce Products,ProductEggGradeMappings,EggUnitConversions,Customers,SalesOrders,SalesOrderItems,SalesOrderAllocations,PaymentsGeneral Inventory InventoryItems,InventoryLots,InventoryMovements,FeedUsages,WaterUsagesFinance ExpenseCategories,ExpensesInsights no source-of-truth tables initially; reads AuditEventsand owner tablesPlatform AuditEvents,idempotency_records,durable_jobs,simulation_seed_state, EF historyImportant cross-owner FKs are retained as database integrity constraints: Egg Operations
daily-entry grades to Egg Operations grades; Egg Operations lots to Flock Management flocks;
General Inventory usages/movements to Flock Management flocks and daily-entry provenance;
Finance expenses to Flock Management flocks; Access flock assignments to Flock Management
flocks. Their existence
does not grant an owner permission to mutate the referenced table. References in module
models are IDs plus contract snapshots, not navigation entities.3.4 Coupling matrix
W= necessary synchronous write coupling,R= read/validation coupling,E=
after-commit event,Q= read-model query,P= platform service,—= none. Rows call
columns in the target design.from / to Access Farm Flock Egg Ops Commerce Inventory Finance Insights Platform Access — R R — — — — E P Farm — — — — — — — E P Flock — R — — — — — E P Egg Ops — R W — — — — E P Commerce — R — W — — — E P Inventory — R R — — — — E P Finance — W R — — — — E P Insights — — — — — — — — Q/P Platform/adapters W W W W W W W R — The
Wseams are synchronous and transaction-aware where invariants require it: Egg Operations appends mortality through Flock Management, Commerce reserves/restores stock through Egg Operations, and Finance obtains a lock-aware currency snapshot from Farm. The current namespace/project coupling, universal repository visibility and
central DI list are accidental. Lifecycle validation, FIFO stock allocation, currency
settings and cross-domain atomicity are necessary business coupling.3.5 Critical workflow traces and decisions
- Submit daily entry. The handler loads entry/flock/scope/active grades, calls
DailyEntry.Submit, creates one lot and opening egg movement per quantity, optionally
creates mortality movement, then saves once. Keep entry, grades, lots and egg ledger in
Egg Operations. Its use case calls Flock Management synchronously to validate and append
mortality on the same ambient context/transaction. Any failure rolls everything back;
concurrency remains 409 and
a retry sees the existing 422 state contract. PublishDailyEntrySubmittedonly after
commit for Insights; an event must never create authoritative stock asynchronously. - Confirm/void sale. Commerce orchestrates order/allocation mutation and a deep
synchronous Egg Operations stock command (ReserveFifo/RestoreReservation) in one
transaction. Stock owns lot mutation, ledger rows and canonical(ProductionDate, Id)
locks; Commerce owns order locks and provenance. Payments are not mutated here — void
only gates onIPaymentRepository.AnyNonVoidedByOrderAsync, rejecting the void if a
payment exists; recording/voiding a payment is its own separate command. The seam
returns opaque DTOs, not lots. - Record feed usage. General Inventory owns FIFO lot consumption and usage/movement
creation. It locks the item first, then — still enlisted inside that same inventory
transaction — calls a narrow Flock Management queryGetFlockProductionEligibility(flockId,date)
before reading lots. The check must stay inside the transaction, not run as a precondition
before it opens: today's handler reads the flock and evaluatesCanRecordProductionOnafter
the item lock is already held, which shrinks a concurrent archive/deplete race to the commit
itself rather than closing it — the flock row is deliberately left unlocked (a same-day
deplete racing a same-day feed is business-valid either way; the birds ate before they left).
Moving the check outside the transaction would widen that window instead and change today's
behavior. No event is suitable: rejection must be immediate. All inventory writes are atomic. - Manager adjust/void. Keep entry/lot/egg-ledger correction in Egg Operations and call
Flock Management synchronously for the compensating mortality row in the same ambient
transaction. Acquire lots in canonical order and preserve
the client aggregate version and append compensating rows; never replace this with an
eventually consistent event. Audit participates in the ambient transaction. - Reports/exports. Insights owns query DTOs and read-only adapters. It may execute
documented,AsNoTrackingcross-owner SQL through a restricted internal read session,
including the current repeatable-read export snapshot. It cannot exposeIQueryable,
entities or the context, and cannot callSaveChanges. Events may later populate read
models, but are not required to manufacture module purity. - Identity/platform paths. Access owns token/security state and credential epoch.
Tenant identity resolution stays claim-only in Platform:TenantResolutionMiddleware
keeps populatingTenantContextdirectly from the JWTaccount_idclaim, before any
module contract runs. It must not route through Access/Farm contracts to establish tenant
identity — those calls would either execute tenant-filtered reads beforeTenantContext
exists or require bypassing filters mid-request, weakening the fail-closed tenant boundary.
Farm contracts are called only afterTenantContextis already resolved (e.g. to read farm
settings), never to resolve it. Middleware order and fresh credential-epoch read stay
unchanged. Idempotency wraps HTTP writes outside
module calls. Jobs call module interfaces (LockDueDailyEntries, token purge), not EF.
Seeders and simulation become explicit orchestration adapters calling module bootstrap
interfaces; migration/health remain Platform. Bootstrap and break-glass remain Access
commands with their current transaction, stdout-secret and fail-closed semantics.
4. Three credible structures
Criterion Capability folders in four assemblies Assembly per module, full slices Hybrid module interfaces/implementations + host/platform Compile-time enforcement weak; namespaces are convention strongest but many projects strong at public seams; practical exceptions isolated Interface depth/callers easy to keep shallow repositories can become excessive module RPC deep use-case interfaces; host orchestration only where atomic evidence demands EF/migrations simplest multiple contexts/migration streams are risky one context/migration stream, configurations assigned to owners Transactions current UoW easy cross-context enlistment complexity one scoped session/ambient transaction Tests/build least disruption most projects and test reshaping moderate, module contract tests plus existing integration suite Incremental cost low moves, low protection high/flag-day pressure adapters allow one seam at a time Circular risk hidden runtime circles compile errors but pressure for shared dumping ground DAG tests plus explicit workflow assembly Extractability low high but over-optimizes for services adequate; extraction is not the goal Folders-only loses because it cannot stop the exact direct repository/context access this
design is meant to prevent. Full vertical assemblies lose because one context, one frozen
migration history and demonstrated cross-module transactions would force infrastructure
leakage or elaborate cross-context coordination. The hybrid supplies enforcement where it
has leverage while preserving EF and transaction locality.5. Target structure and dependency diagram
Cluckwork.Api (HTTP/CLI/job adapters and composition root) | Cluckwork.Workflows (only evidenced cross-module commands) +----------------------+----------------------+ v v Modules.{Access,Farm,FlockManagement,EggOperations,Commerce,Inventory,Finance}.Contracts ^ | (DI only; peer implementations are invisible) Modules.*.Implementation -----+ \ / Cluckwork.Platform.Persistence (single AppDbContext/migrations) | Cluckwork.SharedKernel Modules.Insights.Contracts <- Modules.Insights.Implementation -> read-only reporting sessionContracts may depend only on
SharedKernel. Implementations depend on their own contract,
SharedKernel and narrowly approved platform contracts.Workflowsdepends only on module
contracts and transaction coordinator. The API must not reference implementation types in
endpoint source; registration extension methods are the single composition exception.6. Module contracts
| Module | Responsibility / owned model | External interface and dependencies | Events / transaction | Forbidden references and fitness tests |
|---|---|---|---|
| Access | authentication, users, roles, assignments and refresh credentials; Access tables above |IAccessModule.Login/Refresh/Logout/ChangePassword/ManageUser,ICredentialEpochVerifier, assignment commands. Incoming API/CLI/jobs; outgoing Farm account existence and Flock lookup only | user/role/security events after commit; each issuance/reset/recovery is one existing transaction | no peer implementation/EF entity; security characterization for epoch 0/missing claim, reset, bootstrap, recovery; fresh-read and middleware-order guards |
| Farm | tenant account, settings, logo, currency/unit/timezone |IFarmModule.Get/UpdateSettings/Logo; smallIFarmContextReaderreturning immutableFarmContextSnapshot|FarmSettingsChangedafter commit; one aggregate transaction | no Identity or feature repo; tenant filter/stamp/timezone fail-closed tests |
| Flock Management | flock lifecycle and bird ledger |IFlockModuleplus eligibility/name and mortality-append port | events after commit; mortality may enlist in Egg Operations transaction | no Egg Ops entities/repos; lifecycle/scope/Version/race tests |
| Egg Operations | daily capture/locking, grades, egg lots and ledger |IEggOperationsModule, deepReserveFifo/RestoreReservation; calls Flock port | entry/grade/stock events after commit; submit/adjust/void include enlisted mortality | no Commerce entities/repos; Version/FIFO/balance/concurrency tests |
| Commerce | products, customers, orders/allocations/payments |ICommerceModule; calls Farm currency and opaque Egg Operations stock seam | sale/payment events after commit; confirm/void use one ambient transaction | no Egg Ops entities/repos; provenance/payment/parallel tests |
| General Inventory | items/lots/movements, feed and water usage |IInventoryModule; calls Flock eligibility and Farm currency snapshot | inventory events after commit; purchase/consume/adjust each one transaction | no Flock/Account entities or repos; FIFO, rollback and lifecycle-version race tests |
| Finance | expense categories/expenses |IFinanceModule; calls Farm currency and Flock reference validation | expense events after commit; one expense transaction | no Farm/Flock entities/repos; money/version/tenant tests |
| Insights | reporting, export and audit views |IInsightsModulereturns materialized/streamed DTOs; incoming API only; read adapter depends on restricted reporting session | consumes events only for optional projections; repeatable-read export, otherwise read-only | no command/repository interfaces,SaveChanges, entities orIQueryable; SQL tenant and snapshot-consistency tests |
| Platform | persistence transaction, tenant stamping, idempotency, audit sink, jobs, health, migrations, seeding adapters |IModuleTransaction, clocks, audit append, registration only; adapters call contracts | audit append can enlist; operational events are not business integration events | no business policy in middleware/jobs/seeders; frozen migration, tenant, retry and idempotency guards |Illustrative contract shapes (not implementation code):
public interface IEggOperationsModule { Task<Outcome<SubmissionResult>> Submit(SubmitDailyEntry request, CancellationToken ct); Task<Outcome<Reservation>> ReserveFifo(StockRequest request, CancellationToken ct); } public interface IFlockActivityPort { Task<Outcome<FlockEligibility>> GetFlockEligibility(Guid flockId, DateOnly on, CancellationToken ct); Task<Outcome> AppendMortality(MortalityChange change, CancellationToken ct); } public interface IModuleTransaction { Task<Outcome<T>> Execute<T>(Func<CancellationToken, Task<Outcome<T>>> work, CancellationToken ct); }
These DTOs contain IDs, values and versions only. They never contain an aggregate, EF
entity, repository, context, change tracker or deferred query.7. Data and EF Core strategy
Retain one
AppDbContextand one migration assembly. Multiple contexts sharing these
tables would duplicate tenant filters/configuration, complicate the execution strategy and
make atomic workflows fragile without solving a demonstrated runtime problem.- Move configurations into owner-named folders/assemblies over time, discovered explicitly
by Platform. KeepAppDbContextinternal to Platform/implementation persistence adapters. - codex review, PR Add modular-monolith architecture design and incremental implementation plan #423 round 3: putting a cross-owner relationship in "a Platform-owned
composition file" (this bullet's previous text) does not compile — Platform would need to
reference the owning module's Implementation assembly for the entity type (Expense), while
that module's repository needs Platform'sAppDbContext, an unavoidable
Platform.Persistence↔Finance.Implementationcycle. A cross-owner foreign key is never
expressed through EF's generic fluent relationship API (HasOne<Flock>()) at all, by either
side — that generic type parameter is exactly what forces the cycle. - codex review, PR Add modular-monolith architecture design and incremental implementation plan #423 round 5: the round-3 fix's second half — "the FK constraint itself is a
raw migration operation (migrationBuilder.AddForeignKey(...))" — does not hold, because
FK_Expenses_Flocks_FlockIdandIX_Expenses_FlockIdare already created by the frozen
InitialCreate(feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407). A Phase 2.4 migration re-emittingAddForeignKeyfor a
same-named constraint that already exists fails on every already-migrated database; and
simply not emitting one, afterExpense's own configuration stops declaring the
relationship at all, drops the FK/index from EF's runtime model — which is itself a model
change the before/after digest guard (§8) is built to catch. The corrected mechanism needs no
migration at all, because it changes nothing about the schema, only how the relationship is
declared in C#:Expense's own configuration (owner-authored, in Finance) keeps a
fully string-based, non-generic relationship —
builder.HasOne("Cluckwork.Domain.Flocks.Flock").WithMany().HasForeignKey("FlockId") .HasConstraintName("FK_Expenses_Flocks_FlockId").OnDelete(DeleteBehavior.Restrict)— using the
fully-qualified type name as a string, nottypeof(Flock)or a generic parameter. - codex review, PR Add modular-monolith architecture design and incremental implementation plan #423 round 6: the round-5 example named the wrong fully-qualified type
(Cluckwork.Domain.Flock, which doesn't exist — the actual entity is
Cluckwork.Domain.Flocks.Flock) and omittedExpenseConfiguration's existing
.OnDelete(DeleteBehavior.Restrict)(EF's convention default for an optional FK is
ClientSetNull, a different runtime model). Both are corrected above; a string-typo or a
dropped delete-behavior would each independently fail the promised model-equivalence check.
This reproduces the exact FK column, constraint name, index, AND delete behavior EF already
generates today, so the model snapshot is byte-identical and the digest guard passes with
zero new migration — while requiring no compile-time C# reference toFlock's CLR type (in
Flock Management's assembly) from Finance's configuration, so noPlatform.Persistence↔
Finance.Implementationcycle and noFinance→Flock Managementcompile dependency either.
Same posture Base provisioning: static ref-data into schema + secure first-run admin (generated secret + MustChangePassword) — retire Seed:* config #283's base-reference-data migrations already take (favor a mechanism with no C#
type reference over one requiring it) and consistent with §3.3's existing
statement that cross-owner FKs are retained as database integrity constraints, not typed EF
navigations. Authoring location: each owner's own configuration, for its own entity, using the
string-based API for the far side. No Platform-owned composition file, no cycle, no migration,
no digest change. - Never edit/regenerate
20260801190854_InitialCreate. Keep one ordered migration stream;
every schema change is a new migration. Structural extraction should require no schema
migration. Preserve the model snapshot and the migration digest guards. - Keep cross-owner FKs and document them in
table-ownership.ymlwith owner, referenced
owner, purpose and allowed read/write adapter. Cross-owner navigations are removed from
module-facing models only when a behavior-neutral change proves safe. - Use the same scoped context,
TenantContext, global filters,TenantStampInterceptor,
Npgsql retry configuration and ambient transaction coordinator.IgnoreQueryFilters
remains limited to reviewed startup/operational paths. - Give Insights an internal read-only facade which materializes DTOs/streams and checks
tenant resolution. It may query cross-owner tables; this is a read privilege, never a
dependency from a writer to another writer's repository. - A guard enumerates EF model table mappings and fails when a table lacks exactly one owner;
a second guard confirms everyAccountIdentity is filtered and stampable.
8. Enforcement and adversarial proof requirements
Place boring tests in
tests/Cluckwork.Architecture.Tests:- Parse project references and public type signatures: contracts depend only on the
shared kernel; implementations cannot reference peer implementations. Mutation: add
a peer implementationProjectReference; run the focused test and record its failure. - Build a directed graph from project references plus an allow-list and assert acyclic.
Mutation: add a reverse contracts reference and observe the cycle test fail. - Inspect implementation IL/source namespaces: a module may use only its own repository
interfaces and Platform persistence adapters. Mutation: inject
IEggLotRepositoryinto Commerce and observe failure. - Inspect endpoint constructor/parameter types and source: endpoints may call a module
contract or approved workflow, not handlers/repositories/context. Mutation: restore
one direct handler parameter and observe failure. - Reflect every public contract signature recursively and reject namespaces/types matching
EF,DbContext,DbSet,IQueryable, entity/aggregate bases, repository implementations
or module implementation assemblies. Mutation: returnIQueryable<EggLot>and
observe failure. - Compare relational model table names with
docs/architecture/table-ownership.yml;
require exactly one owner and validate documented cross-owner FKs. Mutation: remove
Paymentsownership and observe failure. - Preserve existing guards for Version increments, concurrency races, tenant filters,
idempotency/statuses, FIFO lock ordering, credential epoch, migration freeze and retry
boundaries. Each new guard's PR includes the red mutation output; revert the mutation
before commit. A claim without recorded mutation evidence is incomplete.
Do not use a clever custom Roslyn analyzer initially. Project-reference tests, reflection
and a small YAML ownership manifest are portable, understandable, and cheap to repair.9. Rejected alternatives
- One module per aggregate: rejects depth, creates synchronous chatter and fractures
transactions already proven atomic. - Separate schemas/databases or microservices: no demonstrated deployment/data problem;
worsens atomic ledger workflows and violates the single-deploy goal. - An event bus for stock creation/adjustment: turns authoritative invariants into eventual
consistency and changes HTTP failure semantics. - A global public repository/UoW or public
AppDbContext: preserves the current backdoor. - Reporting calling every module repeatedly: shallow interfaces, poor query plans and
inconsistent snapshots. A controlled read-side adapter is the honest seam.
10. Independent-review disposition
Three independent reviews were requested before finalizing implementation: domain
boundaries, EF/data ownership, and incremental delivery. Their Important findings and the
document amendments are recorded in the companion plan's review log. No implementation
phase may start while an Important finding remains open.11. Owner decisions, risks and finish line
Decisions requiring the owner
- Confirm Egg Operations as the owner of DailyEntry/grades/lots/egg ledger and Commerce as the caller of its deep stock seam.
- Approve Insights' controlled cross-table read privilege versus building projections now.
- Choose whether module implementations are one assembly per module initially or grouped
temporarily in one implementation assembly while contracts stabilize. - Decide whether audit remains Platform-owned or becomes an Insights-owned append-only
store; the proposed plan keeps writes in Platform and reads in Insights. - Confirm acceptable delivery horizon and whether CI build-time growth from new projects
has a measurable budget.
Top five risks
- Breaking an atomic ledger/cache transaction while replacing repository calls with seams.
- Changing FIFO lock acquisition order and introducing deadlocks or allocation races.
- Leaking EF/entity types through a convenient contract, recreating coupling under new names.
- Weakening tenant or credential fail-closed behavior through duplicated contexts/adapters.
- Allowing compatibility adapters to become permanent, leaving two callable architectures.
Recommended first slice
Pilot Finance after characterization/guards. Its 13 application files have only three
measured outgoing feature references (Farm currency and Flock validation), no
incoming write dependency, no FIFO locks, and a small two-table model. That makes its seam
real—not isolated by convenience—while still testing cross-module reference validation,
tenant ownership, audit, money and optimistic concurrency. Access is lower business
coupling but unacceptable as a first learning experiment because its security blast radius
is high.Objectively testable definition of “modular monolith achieved”
All business endpoints/jobs/CLI/seed adapters call module contracts or an approved workflow;
all module implementations are inaccessible to peers; the dependency graph is acyclic;
every relational table has exactly one documented owner; no contract exposes persistence;
cross-module writes are limited to the enumerated transaction workflows; architecture
mutations fail; all existing build/unit/integration/frontend/simulation contract checks pass;
and compatibility adapters/repository registrations outside module composition are zero.This re-architecture will not
Change
/api/v1contracts, user-visible behavior, database schema, migration baseline,
hosting provider assumptions, deployment/process/database count, business rules, FIFO or
credential semantics. It will not introduce network messaging, promise microservice
extraction, redesign the SPA, or combine structural moves with product features.docs/architecture/modular-monolith-implementation-plan.md
Modular-monolith incremental implementation plan
This plan implements the companion design without a flag day. Every commit builds and is
behaviorally compatible; production refactoring begins only after owner approval. Estimates
are focused engineering days including tests, not calendar commitments.Delivery rules
- One structural intent per commit; never mix a move with changed behavior.
- Existing
/api/v1DTOs/status codes and SPA callers remain the compatibility oracle. - No schema changes are expected. If one becomes necessary, add a new migration; never edit
InitialCreate. - A compatibility adapter has an owner, deletion phase and guard preventing new callers.
- Each architecture guard is mutation-tested red before its claim is accepted; mutation is
reverted before commit and red output is attached to review evidence. - Full integration tests require Docker. Simulation/k6/Playwright callers are reviewed when
a write contract changes even if tests are green; this plan intends no contract changes.
Phase 0 — baseline and characterization (2–4 days)
Commit Exact likely changes Verification and completion evidence Rollback / risk 0.1 Record architecture baseline add docs/architecture/table-ownership.yml,module-dependencies.yml, generated inventory script undertools/architecture/; no production filesscript output matches EF model, 49 handlers and endpoint inventory; dotnet build Cluckwork.sln;npm run typecheck --prefix webdelete docs/script; risk is a hand-maintained false inventory, mitigated by model enumeration 0.2 Characterize seams add integration tests for current HTTP status/idempotency behavior of submit/adjust/void/confirm/void/feed; add/locate parallel-race and FIFO assertions focused test filters plus full dotnet test Cluckwork.sln; record Docker limitation if anytests only; risk is asserting implementation rather than observable contracts 0.3 Characterize security/platform add focused tests only where missing for middleware order, fresh credential epoch, tenant filtering/stamping, job and CLI outcomes security and migration guard filters; existing simulation verifier revert tests; never alter security production code in this phase Dependencies: none. Completion means baseline commands are green and every invariant in the
design maps to an existing or newly added characterization test.Phase 1 — shared kernel and architecture guards (3–5 days)
Commit Exact likely changes Verification / required mutation Rollback / risk 1.1 Add test project add tests/Cluckwork.Architecture.Tests/*.csproj, solution entry, graph/contract test helpers; lock filedotnet restore, locked restore, focused testsremove project/solution entry 1.2 Ownership guard consume table-ownership.yml, enumerate EF relational model and cross-owner FKsremove Paymentsowner → focused test red; restore → greenguard/docs only; risk: design-time tenant dependency, use existing factory fixture 1.3 Dependency/signature guards add project DAG, forbidden implementation reference, endpoint bypass and public-signature tests individually mutate peer reference, reverse edge, direct handler endpoint and IQueryable<EggLot>return; every mutation must redrevert guards if they block unchanged baseline; do not weaken allow-list silently 1.4 Create shared kernel add src/Cluckwork.SharedKernel; initially link/move only stableResult,Error,Money, clocks/paging DTO primitives using compatibility type-forwarding where neededbuild and all tests after each type; API surface snapshot unchanged revert project refs/type forwards; risk is broad namespace churn—keep types in original namespace initially Dependencies: Phase 0. Completion means all six architecture mutations described in the
design have recorded red evidence and clean-tree tests are green.Phase 2 — Finance pilot (5–8 days)
Commit Exact likely changes Adapter and verification Rollback / risk 2.1 Contract project add src/Cluckwork.Modules.Finance.Contractswith use-case commands/results andIFinanceModule; add contract testsexisting endpoint DTO maps 1:1 through an API adapter; signature guard project is additive 2.2 Implementation shell add src/Cluckwork.Modules.Finance.Implementation; internal registration extension; wrap existing handlers behind contract without moving behaviortemporary LegacyFinanceModuleAdapter; handler unit tests plus module-interface testsDI switches back to legacy handlers 2.3 Move domain/application move Expense/category model, handlers and validators mechanically; preserve namespaces first, then separate namespace-only commit; preserve the account FOR SHAREcurrency lock through a synchronous lock-aware Farm porttype forwards or narrow legacy repository adapters; build/test after each move revert individual move; no schema/model configuration edit 2.4 Move persistence adapter move Expense's own configuration/repository behind internal Finance implementation; keep the Expense→Flockrelationship via the string-based non-genericHasOne("Cluckwork.Domain.Flocks.Flock")...HasForeignKey("FlockId").HasConstraintName("FK_Expenses_Flocks_FlockId").OnDelete(DeleteBehavior.Restrict)(noHasOne<Flock>()generic and notypeof(Flock)— either would force aFinance→Flock Management/Platform.Persistence↔Finance.Implementationreference, design §7); this reproduces the FK/index/delete-behaviorInitialCreateand the currentExpenseConfigurationalready create byte-for-byte (codex review round 6: the fully-qualified name and theRestrictdelete behavior are both load-bearing — a typo'd namespace or a droppedOnDeleteeach independently fail the model-equivalence check), so it needs no new migration — a freshAddForeignKeyfor the same constraint name would fail against an already-migrated databaseEF model digest before/after identical (zero pending migration); integration CRUD/tenant/version/audit tests restore old registrations/files 2.5 Switch endpoints and remove adapter change ExpenseEndpointsto injectIFinanceModule; central DI delegates toAddFinanceModule; remove legacy handler registrations/adaptersendpoint contract and SPA tests; guard reports zero endpoint bypasses one-commit DI rollback codex review, PR #423 round 8:
SimulationDataSeeder(SimulationDataSeeder.cs:112-113,:1034,
:1049,:1283-1284) DI-injectsCreateExpenseCategoryHandler/CreateExpenseHandlerdirectly
and queriesdb.ExpenseCategories/db.Expensesfor its idempotent-seeding existence checks and
post-seed exact-count verification — a real production caller of the exact types 2.3/2.4 make
internal toFinance.Implementation, and Phase 10 item 3 ("Convert demo/simulation seeders to
module bootstrap/orchestration contracts one dataset at a time") is where this plan already
schedules converting it, not Phase 2. Pulling that single caller's conversion forward into the
Finance pilot would duplicate Phase 10's planned work; the plan instead makes the gap explicit
and owned rather than silently contradicting the completion criterion below:SimulationDataSeeder
is a named, tracked compatibility exception to Phase 2's "peers cannot reference
implementation" rule (per the Delivery rules' compatibility-adapter policy — owner: this seeder;
deletion phase: 10.3).A second codex pass on the same commit found two more real callers that same exception did not
cover:ReportQueries(ReportQueries.cs:225,:230,:252) andExportQueries
(ExportQueries.cs:235,:240) querydb.Expenses/db.ExpenseCategoriesdirectly for the
expense report and export, andCurrencyBoundRowProbe(CurrencyBoundRowProbe.cs:21) queries
db.Expensesas one of seven rows-carrying-an-amount checks that lock a farm's currency (design
§4.6). Each becomes its own named, tracked compatibility exception, same policy, with its own
resolution point rather than one shared deadline:ReportQueries/ExportQueries— Phase 9 item 2 ("move report queries without changing SQL
semantics") already schedules moving these; that move is this exception's deletion point, not
separate follow-up work.CurrencyBoundRowProbe— spans three future module boundaries, not one:Expenses(Finance,
this phase),SalesOrders/Payments/Products(Commerce, Phase 6),InventoryLots/
FeedUsages/InventoryItems(Inventory, Phase 7). It cannot fully resolve until the LAST of
those lands (Phase 7) — until then it stays allow-listed, and each of Phases 2/6/7 should
confirm the rows it now owns still compile through the probe rather than silently widening the
exception further. Owner: Farm module (the probe backs its currency-change guard).
The dependency/signature guard (1.3) must allow-list all four names above (
SimulationDataSeeder,
ReportQueries,ExportQueries,CurrencyBoundRowProbe) rather than fail on them, so the guard's
own "zero bypasses" claim stays true about every other caller while these four are visible and
dated, not accidentally exempted.Principal risks: currency snapshot and optional flock validation may be accidentally copied
instead of called through Farm/Flock seams; EF configuration discovery may change the
model. Completion: Finance can be tested solely through its contract, peers cannot reference
implementation except the four named, dated exceptions above, EF digest and HTTP behavior are
unchanged, and compatibility adapters are zero apart from those same four exceptions. Hold a
retrospective before Phase 3 and adjust templates/guards.Phase 3 — Farm module (4–7 days)
Commits: (3.1) Farm contracts (
IFarmModule, immutable currency/timezone/context snapshot);
(3.2) wrapper and API adapter; (3.3) mechanical Account/FarmLogo/settings moves; (3.4)
persistence configurations/repositories; (3.5) switch account/logo endpoints and clocks;
(3.6) remove adapters. Likely touched files are current Accounts domain/features,
AccountRepository,FarmLogoRepository, their configurations, account endpoints,
FarmClock,TenantContextadapter and DI extensions.Tests: settings/logo HTTP contracts, invalid timezone fail-closed, currency-bound-row probe,
tenant query/stamp tests, EF model digest, SPA Account/Settings/FarmContext tests. Roll back
at each DI switch. Risk: tenant resolution must not depend cyclically on a tenant-filtered
Farm query; keep tenant identity in Platform and farm settings behind the resolved seam.
Effort 4–7 days. Depends on the Finance pilot lessons.Phase 4 — Flock Management (5–9 days)
Commits: add contracts for lifecycle commands, eligibility/scope/name lookups and mortality append; wrap existing handlers; switch flock endpoints; mechanically move
Flock/BirdMovement, repositories and configuration; convert Access, Inventory and Finance callers to lookup contracts; remove adapters. Run lifecycle, scope, Version/race, tenant and API tests plus the EF model digest. Roll back each endpoint registration independently. Main risk: breaking scope checks or mortality enlistment. Depends on Farm.Phase 5 — Egg Operations (10–16 days)
Commits: add contracts/registration; move EggGrade behavior; move DailyEntry/lines and lock-sweep behavior; move EggLot/egg movement and stock reads; move submit/adjust/void whole using the Flock mortality transaction port; switch daily-entry/grade/stock endpoints and lock-sweep; remove adapters. Likely files are Domain DailyEntry/EggGrade/EggLot/EggInventoryMovement, Application DailyEntries/EggGrades/EggLots/Eggs, matching persistence/endpoints and the lock sweep. Test parallel submit/adjust/void, sold floors, canonical FIFO locks, cached balance=ledger, rollback after each write, tenant, HTTP and SPA contracts; EF model digest must match. Roll back per endpoint/job registration. Risks: lock drift and partial mortality/stock commit. Depends on Flock and Farm clock.
Phase 6 — Commerce and deep stock seam (10–18 days)
Commits: add Commerce contracts; mechanically move catalog/customer/order draft/payment behavior; add opaque Egg Operations
ReserveFifo/RestoreReservation; move confirm in one ambient transaction; mutation-fail after lot allocation, egg movement, allocation and order confirmation to prove rollback; move void with order-first then canonical lot locks, payment gate, provenance release, compensating ledger and audit; switch endpoints; remove adapters. Likely files are Domain Catalog/Sales, related Application, persistence/endpoints, stock port and transaction coordinator. Run product snapshots, order Version, parallel confirm/void, FIFO, insufficient-stock rollback, payments, provenance, tenant, retry and SPA contracts. No event performs authoritative writes. Roll back through the prior endpoint adapter. Risks: partial commit, lock inversion and replay. Depends on Egg Operations and Farm currency.Phase 7 — General Inventory (6–10 days)
- Contract/wrapper; 2. item/purchase/adjust FIFO moves; 3. water moves; 4. feed usage with
Flock eligibility seam; 5. persistence/endpoints switch; 6. adapters removed.
Likely files are Domain/Application Inventory, inventory configuration/repositories,
Inventory/Water endpoints and DI. Tests include backdated FIFO, insufficient rollback,
currency, concurrent lot consumption, flock eligibility behavior, tenant/version, API and SPA.
Retain one inventory transaction. Roll back per endpoint registration. Risk: importing Flock entities/repositories instead of the read-only eligibility decision. Effort 6–10 days; depends on Farm and Flock contracts.
Phase 8 — Access (10–18 days, security hold point)
After all lower-risk module mechanics are proven:
- Add Access contracts around existing use cases without rewriting Identity.
- Switch users/me endpoints and flock assignment through contracts.
- Switch auth endpoints while preserving cookies, JWT claims and error/timing behavior.
- Switch credential middleware to
ICredentialEpochVerifier; prove fresh DB read per request. - Move bootstrap/recover services and refresh purge behind Access; CLI/job adapters call it.
- Internalize Identity implementation and remove legacy
IIdentityProviderexposure.
Likely touched: Domain Accounts role assignment, Application Users/common identity ports,
Infrastructure Identity, user repository/config, Auth/Users/Me endpoints, credential and
must-change middleware, bootstrap/recovery CLI and DI. Run every security integration test,
retry-boundary test, timing/lockout tests, bootstrap/break-glass drills and tenant tests.
Require a dedicated security review before switching DI. Roll back at adapter switches;
never dual-issue tokens. Principal risk is credential revocation regression. Effort 10–18
days; depends on stable Production assignment and Farm account contracts.Phase 9 — Insights and audit (6–10 days)
- Add Insights contract/materialized DTOs and restricted read-session abstraction.
- Move report queries without changing SQL semantics.
- Move repeatable-read export snapshot/streaming.
- Expose audit reads; leave append sink in Platform and document ownership.
- Switch report/export/audit/dashboard endpoints and remove old query registrations.
Likely files: Application Reports/Export/Audit query contracts, Infrastructure ReportQueries,
ExportQueries/Audit repository, corresponding endpoints and SPA API wrappers only if imports
move (URLs/types do not). Tests: report totals, tenant isolation, export exact datasets and
repeatable snapshot, cancellation/stream failure, query guard forbidding writes/IQueryable.
Rollback via DI. Risk: attempting purity through N+1 module calls; retain controlled SQL.
Effort 6–10 days; depends on ownership manifest, not all write moves.Phase 10 — Platform adapters, seeding and cleanup (7–12 days)
- Move idempotency, transaction, tenant, persistence and health registration into explicit
Platform composition; preserve middleware ordering. - Convert daily lock/token purge jobs to contract callers; retain durable worker behavior.
- Convert demo/simulation seeders to module bootstrap/orchestration contracts one dataset at
a time; preserve durable anchor, exact counts and fail-closed validation. - Convert CLI verbs to contracts; migrate remains direct Platform migration operation.
- Delete generic repository/UoW and legacy feature registrations once
rgproves no caller. - Tighten architecture allow-list and document final graph/table ownership.
Run full solution tests, locked restore/build, frontend typecheck/tests/build, simulation
verification and (when available) k6/Playwright. Because production boot guards/config are
not changed, simulation manifests should not need configuration changes; any discovered
change updates all harness files in the same commit. Rollback each adapter separately.
Risks are seed drift and compatibility adapters surviving. Effort 7–12 days.Verification command set
Run as applicable after every commit; full set at phase boundaries:
dotnet restore Cluckwork.sln dotnet restore Cluckwork.sln --locked-mode dotnet build Cluckwork.sln --no-restore dotnet test tests/Cluckwork.Architecture.Tests/Cluckwork.Architecture.Tests.csproj dotnet test tests/Cluckwork.Domain.Tests/Cluckwork.Domain.Tests.csproj dotnet test tests/Cluckwork.Application.Tests/Cluckwork.Application.Tests.csproj dotnet test tests/Cluckwork.Api.IntegrationTests/Cluckwork.Api.IntegrationTests.csproj npm --prefix web run typecheck npm --prefix web test -- --run npm --prefix web run build bash tools/simulation/verify-harness.sh git diff --exit-code -- src/Cluckwork.Infrastructure/Persistence/Migrations/20260801190854_InitialCreate.cs
For every endpoint write, also search non-CI callers before changing a command:
rg -n 'daily-entries|sales|inventory|expenses|payments' web tools src tests.Independent review log and amendment gate
Before implementation, obtain three reviews against both documents:
Review Required questions Important-finding disposition Domain boundaries Independent review found DailyEntry/grades/lots/egg ledger too coupled to split and Sales needs a deep stock seam. Resolved: Egg Operations now owns that invariant cluster; Flock is a synchronous participant and Commerce uses opaque reserve/restore contracts. EF/data ownership Review required one scoped context/migration stream, centralized tenant/retry policy, physical FKs/locks, and privileged EF composition. Resolved: one context, ownership manifest, ambient transactions and read-only Insights are explicit; CLR visibility remains an owner decision. Incremental delivery Review required Expenses HTTP characterization/facade first, lock-aware currency, adapter deletion and no broad test friend access. Resolved: Phases 0–2 and delivery rules now require these constraints. All Important findings from the 2026-08-04 independent reviews are resolved in these documents. New Important findings re-block implementation; record evidence, amendment and resolution, then repeat review. This is a hard gate, not a ceremonial checklist.
Rollout/rollback policy
This is an in-process structural rollout: deploy after any green phase if desired. There is
no data backfill or dual-write. The rollback unit is the last endpoint/job/CLI DI switch;
retain the old adapter for exactly one subsequent commit, then delete it after production
confidence. If EF model digest, SQL, route snapshot or response characterization changes,
stop and split the behavioral difference into a separately authorized change.Completion evidence
The re-architecture is complete only when the objective finish line in the design passes,
the independent review log has no Important item, full verification is green, source search
finds no endpoint/job/CLI direct handler/repository/context use, the allow-list contains only
documented workflow/read exceptions, every table has one owner, and no temporary adapter
remains.It does not create services, databases/schemas, new endpoints, migrations, product
behavior, provider configuration or asynchronous authoritative writes. The recommended
first authorized implementation slice is Phases 0–2 (characterization, mutation-proven
guards and Finance pilot), with an owner review before proceeding to Flock Management or Egg
Operations.Reviewed in the 2026-09-13 issue cleanup. Kept at
priority:tier4— no change.One caveat worth recording for whoever picks this up: the inventory behind the design is dated. It was taken 2026-08-04 on branch
work, and the document says thegraphifyexecutable was unavailable, so the module map was assembled by hand from project references, namespaces, constructors, EF configuration, endpoints, tests, seeders and SPA calls.Since then the codebase has moved under it — Central Package Management (#684), the Aspire AppHost (#565), multi-farm tenancy (#530) and the #543 shared-state ports have all landed. Re-derive the inventory before trusting the seams, and this time
graphify-out/graph.jsonexists, sographify explaincan do it properly.The design's own status line still governs: "proposed architecture; no production refactor is authorized by this document."
- addedepic-514Modular monolith architecture (#514)Modular monolith architecture (#514)
on Sep 14, 2026 Promoted to an epic on 2026-09-14 (owner), with a
Modular monolithmilestone and anepic-514label for its slices.This changes what the issue is. It was a design document parked at tier4 and reviewed as such yesterday. It is now the tracking issue for a workstream, in the same shape as #674: the body and comments hold the design, the label collects the slices, and the milestone counts them.
Three things have to happen before a slice is filed, and none of them are the refactor.
1. Re-derive the inventory. The module map in the comments was taken 2026-08-04 from branch
work, by hand, because thegraphifyexecutable was unavailable in that environment. The codebase has moved under it since: Central Package Management (#684), the Aspire AppHost (#565), multi-farm tenancy (#530) and the #543 shared-state ports all landed afterwards.graphify-out/graph.jsonnow exists, sographify explaincan produce the seams properly rather than from project references and namespaces read by eye.2. Answer the authorisation question the document leaves open. Its own status line still governs: "proposed architecture; no production refactor is authorized by this document." A milestone does not overturn that. Either the owner authorises the refactor and the line is amended, or the epic tracks enforcement work (guards, contract tests) without the moves.
3. Decide what the first slice proves. The plan promises every commit builds and stays behaviourally compatible, with one structural intent per commit and no mixing of a move with a change in behaviour. That discipline is only real if the first slice is small enough to verify. A guard that fails when a module contract is violated is a better first slice than any file move, because it makes every later move checkable.
Priority stays
tier4deliberately. The milestone tracks the work; it does not schedule it. Nothing here is ahead of the four workstreams that have running slices.Recorded so the promotion is a decision rather than a drift. The tier4 review from 2026-09-13 stands on the facts; only the tracking shape has changed.
Re-planned 2026-09-14 — three tracks, and only the first is proposed for now
The design and plan in the comments above were written 2026-08-04. Re-derived against
main@
6231b31and re-planned. Nothing is filed yet — this comment is the record; slice issues follow
only on an explicit go-ahead.Method: three independent design candidates produced in parallel, an independent cross-judge, then
synthesis. Every claim below was verified against source during that work.The epic splits into three tracks, and they get different answers
Track What it is Slices Sizes Days A Module ledger + cross-owner edge ratchet; adapter-tier rows for Mcp/and OpenIddict2 2S 2–4 B Table-owner completeness; adapter-privilege classification; seam-surface guard; §3.4 census re-derived 4 1S 3M 10–17 C The refactor — eight modules behind contracts, then a conditional assembly split 11 3M 8L 57–95 17 3S 6M 8L 69–116 A is proposed now. B waits for #789 and #788 to close. C needs an authorisation decision that has
not been made — this design's own status line still governs.The total barely moves from the 2026-08 estimate of 68–117 days: putting eight modules behind
contracts did not get cheaper. What changed is the committed portion. The old plan's first
authorised increment was Phases 0–2 — 10–17 days ending in a moved Finance module. Track A's first
slice is 1–2 days and ends in a file that reverts withgit rm.Why A runs now rather than with the rest
Four seams connect this epic to MCP and OAuth, and they do not all point the same way:
Seam Order Why Tool→repository injection (#807–#809) #514 first Seven tool classes inject repositories by design ( 770/01-design.md:112); §8 rule 4 forbids that at an adapter. Merge them first and the guard is red on merged code the day it is written — and the cheap fix is to weaken it, which is #407's "a wrong guard reads as safety".OpenIddict tables (#795) #514 first, barely Today's 38 tables map onto §3.3 with no gap. #795 makes it 42 unowned. With the ledger it is four lines inside #795's own PR. Idempotency (#804) MCP first #804 does Phase 10.1's work, better specified, with guard 24 pinning the #307 suites. Phase 10.1 is deleted, not rescheduled. Identity bridge (#805) MCP first McpCallContextis a stronger implementation of §3.5.6 than this design wrote down.#674 does not interact: nothing here reads
web/, nothing there touchessrc/.Everything past Track A waits for a mechanical reason rather than a cautious one. B and C land in
Program.cs(577 lines) andCluckworkFeatureServiceCollectionExtensions.cs(352 lines) — exactly
where #795, #796, #798, #806 and #810 all land. A tier4 epic loses that rebase war.What is stale in the 2026-08 documents
Three corrections change decisions.
1. Phase 1.1's new test project should be deleted — it costs zero days.
tests/Cluckwork.Application.Testsalready carriesMicrosoft.CodeAnalysis.CSharp(.csproj:4) and
TenantBypass/GuardScanner.cs, a 1,424-line Roslyn walker that parses every.csundersrc/—
includingCluckwork.Api, which that project does not reference — with a parse-error gate and a
400-file floor (GuardScanner.cs:74,:118,:136). An architecture guard is a source guard. A new
test project would cost a CI matrix leg (#775), aSolutionTestProjectSplitTestsreconcile, a
tools/coverage/collect.shentry and a lock file, and buy nothing.2. There is a fifth compatibility exception, worse-shaped than the four Phase 2 names.
src/Cluckwork.Infrastructure/Persistence/BusinessRecordModel.cs:21-34holds an 11-type
ChronologicalListTypeslist —SalesOrder,Expense,DailyEntry,EggLot,BirdMovement,
Payment,InventoryLot,FeedUsage,WaterUsage,InventoryMovement,EggInventoryMovement—
spanning six future modules, in a Platform assembly, by CLR type. That is the "Platform-owned
composition file" codex review round 3 rejected as forcing a
Platform.Persistence ↔ Module.Implementationcycle. It arrived with #819, the current HEAD, and
#819's own rule says the list grows with every new time-paged table.The plan's
file:linecites rotted in the same five weeks: it names the seeder's expense injection at
:112-113/:1034/:1049/:1283-1284; today they are:124-125,:1859,:1875,:2237-2238.
A list maintained by recall goes stale; a walk does not. This is why Track A's ledger keys on owner
and enclosing symbol, never on position (#632).3. Two of the five apparent new coupling edges are measurement artifacts.
Customers → Sales(5 refs) — all five referenceCluckwork.Domain.Sales, andCustomer.cs
lives insrc/Cluckwork.Domain/Sales/. Both ends are Commerce.Accounts → Media(2) —Domain.MediaholdsImageKind,ImageSanitizer,SanitizedImage. No
entity, no table;FarmLogois inDomain/Accounts/.
A namespace-keyed ratchet would demand a justification for these forever, and the owner would learn
to write "not really an edge" — which is how a registry stops being read. An owner-keyed one never
raises them. That distinction is why the chosen shape keys on owner.Three edges are real, each a
—cell in §3.4 that should readR:Cell Evidence Farm → Commerce UpdateFarmSettingsHandler.cs:12injectsIEggUnitConversionRepository;:149andUpdateFarmSettingsValidator.cs:98callDiscountCeiling.TryParsePercent(#727)Access → Commerce SetStepperUnitHandler.cs:13injectsIEggUnitConversionRepositoryCommerce → Access ConfirmSaleHandler.cs:23injectsIUserRoleAssignmentRepository;:171callsGetEffectiveRoleAsyncinside the transaction (#727)Remaining corrections: 49 handlers → 57; §3.2's reference-count paragraph counted only
Application.Features.*while the 2026-09 measurement mixesDomain.*, so the basis must be stated;
§3.3's table list is still exactly right at 38 (note that #795 makes it 42); §3.1's shared-port
list should addIStepUpGrantService,SystemActors,SecurityEvents,AuditActions; §8's preamble
should place guards inApplication.Tests/Architecture/rather than a new project; §8 rule 4 needs
the adapter-tier concept or #770's seven tool classes violate it on arrival; §11's "pilot Finance
first" becomes "ledger first, Finance contingent"; Phase 1.4 (shared kernel) is deleted —
Result,ErrorandMoneyalready sit inDomain/Common/, which every layer references; Phase
10.1 is deleted as inherited from #804.ReportQueries,ExportQueriesandCurrencyBoundRowProbe
live inInfrastructure/Repositories/, notPersistence/.Correction to this issue's own decision (a)
graphify cannot re-derive the module inventory, and the decision should stop asking it to.
Run against the question that decides the artifact issue above — "which module owns Customer, and is
Customers → Sales cross-module" — it returned 9 nodes: atsconfig.jsonmodulekey, a web test
constant, an unrelated integration test, an AGENTS.md section, and one useful hit (Customerat
src/Cluckwork.Domain/Sales/Customer.cs:8).It locates a symbol well and infers ownership not at all. The design's evidence note should be
replaced not with "graph-derived" but with "namespace-keyed source walk, because the graph does not
model ownership". Making ownership answerable is precisely what the ledger is for, which is the
argument for building it rather than a reason to defer.Track A, as proposed
A1 — module ledger + cross-owner edge ratchet (
size:S). A committed
docs/architecture/module-ledger.jsonnaming the nine owners, their namespaces and every cross-owner
edge with a stated reason, plus a Roslyn walk ridingGuardScanner's existing root/parse/floor
helpers. Nosrc/file, no.csproj, no CI change, no package. Red on four mutations: an undeclared
edge; a stale row — the leg that stops a ratchet rotting into a list of edges that were true once;
an unowned namespace; the file-count floor.A2 — adapter-tier rows for
Mcp/and the OpenIddict tables (size:S). DeclaresMcp/a
DirectRepositorytier with a reason and a review issue, and gives the four OpenIddict tables an
Access owner. Keyed onEndpointDataSourcerather than theMcp/folder, so it does not fail closed
before/mcpexists.Open questions
- Authorisation. Enforcement (A+B, 12–21 days, a defensible finish line) or the moves as well
(C, +57–95 days)? A is correct either way, which is why it is first. - Does
priority:tier4gate even A1? This issue's decision (c) reads as "when you file slices,
make the first one a guard" — not necessarily "file now". A1 is 1–2 days and wants to precede
MCP slice 3: map /mcp, wire RBAC and tools/list filtering #806 and OAuth slice 1: stand up OpenIddict — tables, migration, token issuance #795. - Is an
McpDirectRepositorytier acceptable, or should MCP slice 4: read tools — flocks, stock, egg lots, daily entries #807–MCP slice 6: write tool — record daily entry #809 wait for contracts?
Waiting blocks a tier3 epic on a tier4 one. Not waiting means §8 rule 4 acquires a seven-caller
exemption on day one. BusinessRecordModel.ChronologicalListTypesis a cycle under any assembly split, not an
exception. Is the answer a marker interface each module applies to its own type — the shape Standardize business-record timestamps and chronological list ordering #819
already uses forICreatedRecord/IMutableRecord— and does that belong in Track C or its own
issue?- Who owns
Domain.Media? No entity, used by both logo and banner. Platform utility or Farm?
The ledger has to answer; all three design candidates waved it through as Farm without noticing. - Two slice issues or one? MCP filed 8, OAuth 6. Two
size:Sissues for ~3 days of work may be
disproportionate ceremony.
Dependency notes have been added to #789, #788, #806, #807, #809 and #795. Nothing was closed or
reassigned.Track A filed as #842 (module ledger + cross-owner edge ratchet) and #843 (adapter-tier rows for the MCP and OpenIddict surfaces), both
size:S, bothpriority:tier4, both on theModular monolithmilestone.No body checklist was added here deliberately — the 2026-09-14 promotion comment set the tracking shape as "the label collects the slices, and the milestone counts them", and the
epic-514label picks both up.Tracks B and C remain unfiled, pending the authorisation question in the re-plan comment above.
All 17 slices filed — index and build order
Supersedes the "Tracks B and C remain unfiled" line in the previous comment. Sizes match the re-plan
exactly: 3S / 6M / 8L, 69–116 focused engineering days.Filing is not scheduling. Track C in particular is filed so the analysis is tracked work rather
than a comment; each of those bodies carries a header saying so.Track A — ~2–4 days, no production code
Slice Size Needs #842 module ledger + cross-owner edge ratchet S — #843 adapter-tier rows for the MCP and OpenIddict surfaces S #842 #843's only deadline was preceding #806 and #795. If MCP and OAuth are parked it has none, and it is
reasonably folded into #842 as a second commit.Track B — ~10–17 days, no production code
Slice Size Needs #845 table-owner completeness, walked from the EF model M #842 #846 adapter-privilege ratchet over endpoints, CLI, jobs, seeders M #842 #847 seam-surface guard — no persistence type crosses a seam S #842 #848 regenerate the coupling matrix, delete the hand-written one M #842, #845, #846 #847 is worth noting: there are zero
IQueryablereturns in Application today, so it starts with
no exemptions. That is the cheapest it will ever be.Track C — ~57–95 days, moves production code, unscheduled
Strictly ordered except where noted. Each slice is independently revertible at its DI switch.
Slice Size Needs #849 Finance pilot — first module behind a contract L Track B green #850 close or date every compatibility exception M #849 #851 Farm contract, tenant identity stays in Platform L #849 #852 Flock Management contract, eligibility + mortality ports L #851 #853 Egg Operations contract and the opaque stock seam L #852 #854 Commerce contract, consuming the stock seam L #853 #855 General Inventory contract, flock-eligibility read M #851, #852 #856 Insights read facade M #845 — not the write moves #857 Access contract — security hold point L #851, runs last #858 Platform composition, jobs, CLI, seeder conversion L #849–#857 #859 assembly split — conditional L see its entry condition Three of these are worth singling out:
- [C] #514 slice 17: assembly split — conditional on Track A/B evidence #859 has an entry condition, not just dependencies. Run it only if Tracks A and B produced
evidence that a boundary was crossed by accident. If the ratchet stayed quiet, a source walk was
sufficient and this buys a compiler error for a problem that did not occur. It is the most
expensive slice and the one most likely not to earn its cost. - [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858 is the one that makes adding a new module cheap — the 352-line registration file, the
2608-line seeder,BusinessRecordModel's cross-module type list. If only part of Track C is ever
run, this is the part with a payoff that is not about cleaning up the past. - [C] #514 slice 8: close or date every compatibility exception #850's
BusinessRecordModelitem is independent of all of Track C and can be done standalone
at any time. It is the thing that degrades with every new time-paged table (Standardize business-record timestamps and chronological list ordering #819).
Recommendation on the record
The re-plan recommends #842, then gating #845–#848 on whether the ratchet actually fires, and gating
Track C on what those produce. With no MCP/OAuth urgency and no planned new modules, the argument
for Track C rests on evidence that does not exist yet — and #842 costs 2–4 days to start producing
it. The slices are filed either way so the decision has something concrete to point at.- [C] #514 slice 17: assembly split — conditional on Track A/B evidence #859 has an entry condition, not just dependencies. Run it only if Tracks A and B produced
Two slices in review: #872 (Track A slice 1) and #875 (Track B slice 5)
#842is implemented as #872: 16 cells, 66 symbols, five mutations recorded red, four review rounds (two CodeRabbit, two Codex) closed.#847is implemented as #875: 35 public interfaces inspected, zero violations, an assembly-reference pin, three review rounds closed. Track B's "waits for #788/#789" reason is the rebase war inProgram.cs, which a test-only guard never touches, so #847 went ahead; #845, #846 and #848 still wait. Three things the walk settled that the 2026-09-14 re-plan left open, so the next reader does not re-derive them:- §3.4 is wrong in five cells, not three. Besides Farm → Commerce, Access → Commerce and Commerce → Access (Sales: per-farm discount ceiling, with Owner/Manager approval above it #727), the walk found GeneralInventory → EggOperations (feed and water usage inject
IDailyEntryRepositoryfor provenance) and Access → EggOperations (AccountProvisionerseedsEggGrade.Defaults). All five are ledgered with reasons. - Finance → Farm is
R, notW. The code reads the account row underFOR SHAREand mutates nothing, the same shape §3.4 classesRon Commerce → Farm and Inventory → Farm. The ledger recordsRwith the disagreement in the reason; [B] #514 slice 6: regenerate the coupling matrix from the ledger, and delete the hand-written one #848 regenerates the matrix from that. - Open question 5 (
Domain.Media) is Farm, on evidence: every caller is the logo or banner path. If a second module ever uses it, that shows up as an edge and forces the decision then.
Platform claims
Cluckwork.DomainandCluckwork.Applicationexactly, never as subtrees. The twoGlobalUsings.csfiles force someone to own the project roots, and a subtree claim would absorb a newCluckwork.Domain.<Module>silently, which is the unowned-namespace leg dying exactly where a new module first appears. Mutation 5 on #872 is the proof.What is blocked, and on what
- [A] #514 slice 2: adapter-tier rows for the MCP and OpenIddict surfaces #843 wants to precede MCP slice 3: map /mcp, wire RBAC and tools/list filtering #806 and OAuth slice 1: stand up OpenIddict — tables, migration, token issuance #795, and neither has started (
src/Cluckwork.Api/Mcp/does not exist, no OpenIddict package). Its "undeclared adapter privilege" mutation needs every adapter that injects a repository classified first, which is [B] #514 slice 4: adapter-privilege ratchet over endpoints, CLI, jobs and seeders #846's ratchet. Filing it as a second commit on [A] #514 slice 1: module ledger and cross-owner edge ratchet #842 would pull Track B forward. Left unscheduled until MCP or OAuth is unparked. - Track B waits on Build an OAuth 2.1 authorization server (OpenIddict) so MCP clients can authenticate #788 and EPIC: MCP server support #789 per the re-plan; both are open.
- Track C still needs the authorisation decision. The design's status line governs.
- §3.4 is wrong in five cells, not three. Besides Farm → Commerce, Access → Commerce and Commerce → Access (Sales: per-farm discount ceiling, with Owner/Manager approval above it #727), the walk found GeneralInventory → EggOperations (feed and water usage inject
Track B is fully in review: #877, #878, #879 join #872 and #875
All five test-only slices now exist as PRs, stacked in dependency order (each retargets to
mainas its base merges):PR Slice Stacks on Ledger sections #872 #842 module ledger + edge ratchet main owners,edges#875 #847 seam-surface guard main (reflection, no ledger) #877 #845 table-owner completeness #872 tables,foreignKeys,tableOwnerOverrides#878 #846 adapter-privilege ratchet #877 adapterRoots,adapters(150 rows over 397 adapters)#879 #848 generated coupling matrix #878 coupling-matrix.md, regenerated and byte-checkedThe generated matrix supersedes §3.4 in the design comment above. Seven cells differ from the hand-written version, each established by reading the code during the review rounds:
from to hand-written generated Access Farm R W (19) fk:1 Access EggOperations — W (2) Access Commerce — W (6) Farm Commerce — R (3) Commerce Access — R (1) GeneralInventory EggOperations — R (2) fk:2 Finance Farm W R (2) EandQare not syntactically observable, so the generated file reportsW/Rletters with live symbol counts,fk:<n>from the table census,Pfor the Platform column andA (n)adapter-reach counts for the Platform row. Full file:tests/Cluckwork.Application.Tests/Architecture/Data/coupling-matrix.mdon #879.Track B's "waits for #788/#789" reason was the rebase war in
Program.cs. None of these five PRs touchessrc/, so they went ahead; #843 still waits for MCP or OAuth to unpark, and Track C still needs the authorisation decision.Tracks A and B: five PRs, all approved by the reviewer of record
Per the owner's decisions on 2026-09-14: enforcement only (Track C stays unscheduled; the design's status line still governs), and Codex gpt-5.6-sol reviews in lieu of the CodeRabbit bot (the free tier serialised five PRs to one round per ~50 minutes).
PR Slice Reviewer verdict Merge order #872 #842 module ledger + edge ratchet approve, CI green on f975dfb 1 #875 #847 seam-surface guard approve, CI green on c2ae9e7 independent #877 #845 table-owner completeness approve 2, retarget to main after #872 #878 #846 adapter-privilege ratchet approve 3, retarget after #877 #879 #848 generated coupling matrix approve 4, rebase onto #878's head a67a461, retarget after #878 Review yield across the five, for the record: 46 findings raised over 21 rounds (CodeRabbit 5, Codex 41), 44 confirmed and fixed with a regression test each, 2 dismissed with a stated reason. Every fix round that touched a guard added its own mutation or fixture proof before the claim.
Left for later, deliberately: #843 (adapter-tier rows for MCP and OpenIddict) until #806 or #795 unparks; Track C (#849 to #859) until authorised.
- added 6 commits that reference this issue
on Sep 15, 2026 Track A is complete. #843 ships as #880, approved by the Codex sol reviewer of record after three rounds (9 findings, 9 fixed, 0 dismissed), on
main, CI running.What landed: an
adapterTiersledger section holding one dormant row,Cluckwork.Api.Mcp→DirectRepository, surfaced byMapMcp, reviewed by #806. The scanner is green today with no/mcp, and goes red the day #806 maps it without the row or puts a[McpServerToolType]class outside the tier. Tier namespaces are also adapter roots, so tool classes' repository parameters needadaptersrows andAppDbContextstays forbidden. The issue's OpenIddict half is enforced by #877's table-owner guard, which fails #795's PR until its four tables have owners.Milestone state after #880 merges: Tracks A and B done (#842, #843, #845, #846, #847, #848). Track C (#849 to #859) remains unscheduled by decision.
Track C audit, 2026-09-26, against
mainat e47288cAll eleven Track C slices were written on 2026-09-14 and have now been re-checked against the code, twelve days and 62 merges later. Each issue carries its own audit comment; this is the index and the two structural changes.
Verdict per slice
Slice Verdict Why #849 Finance pilot amend its endpoint reaches four owners, so its own acceptance criterion cannot be met by a Finance contract alone #850 exceptions no change all five exist at the same lines, none new #851 Farm amend under-scoped for its six consumers, and missing one load-bearing sentence #852 Flock Management amend prose no egg dependency is true in code, false in the schema #853 Egg Operations amend misses a fourth egg-lot writer and the new grade-floor policy #854 Commerce amend add the grade-name read from #912 #855 General Inventory no change the lock-then-eligibility-then-lots ordering is intact #856 Insights amend and reschedule creates six edges rather than removing any, and blocks five later slices #857 Access no change the fresh per-request credential read is still uncached #858 Platform amend refreshed numbers, and one item carved out #859 assembly split blocked its entry condition is measurably unmet #969 chronological census new carved out of #858, no prerequisites, shippable now Change 1: Insights moves near the front
Six endpoint files inject
IAuditEventRepositorydirectly for response provenance, and one of them is the pilot's. Scheduled at slice 14, five later slices each inherit an injection they were supposed to remove. Recommended order is the pilot, then Insights, then the rest as designed.Change 2: #859 is blocked, not merely unscheduled
Its entry condition was evidence that a boundary was crossed by accident. Over 62 merges the ledger fired three times, lit zero new module cells, and caught zero accidental crossings. Every crossing was deliberate and declared correctly first time. The issue records the numbers and a re-check trigger.
On ordering generally
An earlier hypothesis that slices should be ordered by consumer count was tested and rejected. Each slice converts a module's own internals rather than its callers, so cost tracks edge weight, not incoming count. Access carries the only genuinely heavy edge at 19 symbols and correctly runs last. The designed order stands apart from the Insights move.
Current census
Module Files Tables Consumers Access 59 9 1 Commerce 53 8 2 General Inventory 32 5 0 Egg Operations 30 5 3 Farm 23 2 6 Flock Management 18 2 4 Finance 15 2 0 Insights 4 0 0 The gate on Track C as a whole is unchanged and still the owner's call: the ratchet has been firing at roughly one pull request in twenty, always on a deliberate crossing.
Track C prerequisite landed: #985. Every slice now builds under two C# style gates: usings go outside the namespace and namespaces are file-scoped. A violation is a build error, not a review comment, so slice briefs don't need to restate the rule. Fix a violation with
dotnet format style <project> --diagnostics IDE0065 IDE0161 --severity warn --include <file>.One thing for slice authors, from the sweep: never name a namespace segment after an existing type. A test namespace called
FlockScopeshadowed the productionFlockScopeclass and broke six files. Module moves in Track C create new namespaces, so this is the trap to watch. Details indocs/decisions/985-csharp-style-gate.mdand theAGENTS.mdparagraph.Closing: every tracked slice is done, and Track C finished on 2026-10-05.
Track C shipped
- Access contract ([C] #514 slice 15: Access contract — security hold point, runs last #857). Live reads are centralised and account lifecycle is isolated (E1). The contract is declared, the seeders use
IAccessSeedLookup, and Access claimsIIdentityProviderandIStepUpGrantService(E2, which also closed test(arch): guard module-to-module calls against non-contract types #1023). - Platform composition ([C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858):
- Insights contracted, the generic repository removed and
IFarmDirectoryadded (P1–P3). - Seeder fixture ports for every module, registered only outside Production (P4–P6).
- One
Add<Module>Moduleregistration step per owner, with identical DI dumps (P7). - The ledger end state and
docs/decisions/858-platform-composition.md(P8).
- Insights contracted, the generic repository removed and
- Rule files to typed C# ([C] #514 slice 17: assembly split — conditional on Track A/B evidence #859, scope changed).
module-ledger.json,tenant-bypass-allowlist.jsonandfilter-free-set-sites.tsvare now typed C# registries in the test project, with behaviour unchanged (refactor(test): give the architecture and tenant guards a loader seam #1067–refactor(test): move the module ledger rows into typed C# #1069). The last compatibility exception, the Flocks LEFT JOIN, is gone (refactor(access): name flock assignments through IFlockLookup, removing the last compatibility exception #1070), soCompatibilityExceptionsis empty. Seedocs/decisions/859-typed-rule-registries.md. - Guard hardening found along the way:
- source guards parse with the build's real preprocessor symbols (test(arch): match build symbols and reject inactive source #1056, test(tenancy): allow only login to call the cross-farm FindBySlugAsync #1061);
- only login may call the cross-farm
FindBySlugAsync(test(tenancy): allow only login to call the cross-farm FindBySlugAsync #1061, closing arch: IAccountRepository.FindBySlugAsync is a cross-farm read on Farm's seam with no caller guard #1053).
Evidence on boundary guards. All 64 failed PR runs since #842 were checked. The guards caught zero accidental cross-module crossings: every failure was a deliberate declaration, bookkeeping or a guard defect. That is why #859 dropped the assembly split.
Follow-ups filed in Platform hardening
- test(arch): compute module edges from bound symbols, not import scope #1071: compute edges from bound symbols. This removes 7 false rows and declares 2 real references.
- test(tenancy): fingerprint the 40 allow-listed tenant bypasses so edits force re-review #1072: fingerprint the 40 allow-listed tenant bypasses.
- test(arch): fail on unused adapter reach instead of reporting it #1073: fail on unused adapter reach.
- test(arch): derive table owners from namespaces and delete the hand-written tables rows #1074: derive table owners from namespaces.
Deliberately not done
- A Roslyn analyzer for build-time enforcement. It would need the rules moved into
src/as attributes, and it is estimated at 23–30 days. Revisit if a guard ever catches a real accidental crossing, or if feedback speed starts to matter. - The per-module assembly split. Its entry condition is unmet.
- Access contract ([C] #514 slice 15: Access contract — security hold point, runs last #857). Live reads are centralised and account lifecycle is isolated (E1). The contract is declared, the seeders use
Converted from PR #423 (closed unmerged) so the design work isn't lost. Original PR description:
Motivation
AppDbContextwhile preserving current runtime/DB topology.Description
docs/architecture/modular-monolith-design.md— recommended design, vocabulary, module responsibilities, cross-module seams, EF/Core strategy, enforcement requirements, and an objective finish line.docs/architecture/modular-monolith-implementation-plan.md— phased, incremental implementation plan with per-phase commits, verification commands, rollback rules, and a recommended Finance pilot.docs/architecture/modular-monolith-diagrams.html— rendered diagrams accompanying the design doc.No production code, migrations, or API surface changes — documentation/planning only. The docs went through 8 rounds of Codex review on the PR (feed-usage transaction race wording, tenant-resolution ordering, cross-owner EF FK strategy, audit-transaction boundary, Flock FQN fix, payment-mutation claims, and naming
SimulationDataSeeder/ReportQueries/ExportQueries/CurrencyBoundRowProbeas tracked Phase 2 compatibility exceptions) — see PR #423 commit history for the full back-and-forth.Permalinks to the final reviewed content (commit
e96c264):Full text of the two markdown docs is pasted in the comments below (design doc in the next comment, implementation plan in the one after) since GitHub's issue API has no arbitrary-file-attachment endpoint.
Next step
Decide whether to resume this as a real PR (docs land first, then Phase 1 implementation per the plan) or park it. Not scheduled against a phase epic yet.
Slice tracker
Audited against
main2026-09-26; see the audit index. Every slice carries its own dated audit comment.Track A — declare the map
Track B — enforce it
Track C — move production code. Gated on evidence; see #859 for the measurement.
Track C is listed in recommended order, which is the designed order with Insights pulled forward. The milestone's own progress bar counts merged pull requests as well as issues, so this list is the authority on slice progress.