Repository navigation
Description
Converted from PR #423 (closed unmerged) so the design work isn't lost. Original PR description:
Motivation
- Capture a proposed hybrid modular-monolith architecture that enforces module contracts, a small shared kernel, and a single
AppDbContextwhile preserving current runtime/DB topology. - Provide a practical, incremental delivery path and safety-first rollout rules to avoid behavioral or schema regressions during refactor.
- Define concrete enforcement requirements (guards/tests) to prevent accidental compile- or runtime coupling across modules.
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/CurrencyBoundRowProbe as 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 main 2026-09-26; see the audit index. Every slice carries its own dated audit comment.
Track A — declare the map
- [A] #514 slice 1: module ledger and cross-owner edge ratchet #842 slice 1: module ledger and cross-owner edge ratchet
- [A] #514 slice 2: adapter-tier rows for the MCP and OpenIddict surfaces #843 slice 2: adapter-tier rows for the MCP and OpenIddict surfaces
Track B — enforce it
- [B] #514 slice 3: table-owner completeness, walked from the EF model #845 slice 3: table-owner completeness, walked from the EF model
- [B] #514 slice 4: adapter-privilege ratchet over endpoints, CLI, jobs and seeders #846 slice 4: adapter-privilege ratchet over endpoints, CLI, jobs and seeders
- [B] #514 slice 5: seam-surface guard — no persistence type crosses a seam #847 slice 5: seam-surface guard, no persistence type crosses a seam
- [B] #514 slice 6: regenerate the coupling matrix from the ledger, and delete the hand-written one #848 slice 6: regenerate the coupling matrix from the ledger
Track C — move production code. Gated on evidence; see #859 for the measurement.
- [C] #514: per-module chronological census, splitting BusinessRecordModel's hand-maintained arrays #969 per-module chronological census — merged in PR refactor(persistence): split business record census by module #970
- [C] #514 slice 7: Finance pilot — first module behind a contract #849 slice 7: Finance pilot, first module behind a contract — merged in PR refactor(finance): put Finance behind an IFinanceModule contract #1010
- [C] #514 slice 14: Insights read facade for reports, exports and audit reads #856 slice 14: Insights read facade — moved early, five later slices depend on it — merged in PR refactor(insights): read reports, exports and audit provenance through an Insights facade #1012
- [C] #514 slice 8: close or date every compatibility exception #850 slice 8: close or date every compatibility exception — merged in PR test(arch): register and date every module compatibility exception #1013
- [C] #514 slice 9: Farm contract, with tenant identity staying in Platform #851 slice 9: Farm contract, tenant identity staying in Platform — merged in PR refactor(farm): put farm settings behind an IFarmModule contract #1015
- [C] #514 slice 10: Flock Management contract, eligibility and mortality ports #852 slice 10: Flock Management contract, eligibility and mortality ports — merged in PR refactor(flocks): put flock lifecycle behind an IFlockModule contract #1021
- [C] #514 slice 11: Egg Operations contract and the opaque stock seam #853 slice 11: Egg Operations contract and the opaque stock seam — merged in PR refactor(eggs): put egg operations behind an IEggOperationsModule contract #1028 (stock seam moved to [C] #514 slice 12: Commerce contract, consuming the stock seam #854)
- [C] #514 slice 12: Commerce contract, consuming the stock seam #854 slice 12: Commerce contract, consuming the stock seam — merged in PR refactor(sales): put Commerce behind a contract and reach egg stock through IEggStock #1030
- [C] #514 slice 13: General Inventory contract and the flock-eligibility read #855 slice 13: General Inventory contract and the flock-eligibility read — merged in PR refactor(inventory): put general inventory behind an IInventoryModule contract #1027
- [C] #514 slice 15: Access contract — security hold point, runs last #857 slice 15: Access contract, security hold point, runs last
- [C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858 slice 16: Platform composition, jobs, CLI and seeder conversion
- test(arch): guard module-to-module calls against non-contract types #1023 guard module-to-module calls against non-contract types (deferred from [C] #514 slice 10: Flock Management contract, eligibility and mortality ports #852)
- [C] #514 slice 17: assembly split — conditional on Track A/B evidence #859 slice 17: rule files → typed C# registries (scope changed 2026-10-04; assembly split dropped, entry condition unmet)
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.
Activity
mforce commentedon 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, branch
work.graphify-out/graph.jsonexists, but thegraphifyexecutable was unavailable in the analysis environment. The inventory wastherefore 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.
compile against; its implementation is internal and cannot be referenced by peers.
an adapter at a seam, not a place for business orchestration.
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
Current-state coupling hot spots:
2.2 Proposed state — modular monolith with explicit seams
The 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
codex 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);SubmitDailyEntryHandlerandConfirmSaleHandlerinject noIAuditWriterand write no audit row. Since this plan's deliveryrule (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,and
VoidSaleHandleronly checksIPaymentRepository.AnyNonVoidedByOrderAsync— a guard thatrejects 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 alreadylisted 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 allthree. 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 arelegitimate 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.
repositories, Identity, jobs, seeders and cross-module read queries.
3.2 Capability inventory
UserRoleAssignment; account rolesIIdentityProvider, Identity users/roles/refresh tokens, credential middleware, auth/users/me endpoints, bootstrap/recovery/purgeAccount,FarmLogo, currency/unit/brand settingsFlock,BirdMovementDailyEntry/lines,EggGrade,EggLot, egg movementsProduct, conversions/mappings,Customer,SalesOrder, items, allocations,PaymentReportQueries,ExportQueries, audit queryAppDbContext, tenant interceptor, unit of work, idempotency, durable jobs, health, migrations, seeders, CLIThere are 49 application handlers. A source-namespace scan found these direct feature
reference counts: Production-shaped
DailyEntriespoints 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
AspNetUsers,AspNetRoles, Identity join/claim/token tables,refresh_tokens,UserRoleAssignmentsAccounts,FarmLogosFlocks,BirdMovementsDailyEntries,DailyEntryGrades,EggGrades,EggLots,EggInventoryMovementsProducts,ProductEggGradeMappings,EggUnitConversions,Customers,SalesOrders,SalesOrderItems,SalesOrderAllocations,PaymentsInventoryItems,InventoryLots,InventoryMovements,FeedUsages,WaterUsagesExpenseCategories,ExpensesAuditEventsand owner tablesAuditEvents,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 callcolumns in the target design.
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 andcentral 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
DailyEntry.Submit, creates one lot and opening egg movement per quantity, optionallycreates 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. Publish
DailyEntrySubmittedonly aftercommit for Insights; an event must never create authoritative stock asynchronously.
synchronous Egg Operations stock command (
ReserveFifo/RestoreReservation) in onetransaction. 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 on
IPaymentRepository.AnyNonVoidedByOrderAsync, rejecting the void if apayment exists; recording/voiding a payment is its own separate command. The seam
returns opaque DTOs, not lots.
creation. It locks the item first, then — still enlisted inside that same inventory
transaction — calls a narrow Flock Management query
GetFlockProductionEligibility(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 evaluates
CanRecordProductionOnafterthe 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.
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.
documented,
AsNoTrackingcross-owner SQL through a restricted internal read session,including the current repeatable-read export snapshot. It cannot expose
IQueryable,entities or the context, and cannot call
SaveChanges. Events may later populate readmodels, but are not required to manufacture module purity.
Tenant identity resolution stays claim-only in Platform:
TenantResolutionMiddlewarekeeps populating
TenantContextdirectly from the JWTaccount_idclaim, before anymodule contract runs. It must not route through Access/Farm contracts to establish tenant
identity — those calls would either execute tenant-filtered reads before
TenantContextexists or require bypassing filters mid-request, weakening the fail-closed tenant boundary.
Farm contracts are called only after
TenantContextis already resolved (e.g. to read farmsettings), 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
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
Contracts may depend only on
SharedKernel. Implementations depend on their own contract,SharedKernel and narrowly approved platform contracts.
Workflowsdepends only on modulecontracts 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):
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 thesetables would duplicate tenant filters/configuration, complicate the execution strategy and
make atomic workflows fragile without solving a demonstrated runtime problem.
by Platform. Keep
AppDbContextinternal to Platform/implementation persistence adapters.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), whilethat module's repository needs Platform's
AppDbContext, an unavoidablePlatform.Persistence↔Finance.Implementationcycle. A cross-owner foreign key is neverexpressed through EF's generic fluent relationship API (
HasOne<Flock>()) at all, by eitherside — that generic type parameter is exactly what forces the cycle.
raw migration operation (
migrationBuilder.AddForeignKey(...))" — does not hold, becauseFK_Expenses_Flocks_FlockIdandIX_Expenses_FlockIdare already created by the frozenInitialCreate(feat(eggs): make cracked and dirty eggs sellable stock via condition grades (#396) #407). A Phase 2.4 migration re-emittingAddForeignKeyfor asame-named constraint that already exists fails on every already-migrated database; and
simply not emitting one, after
Expense's own configuration stops declaring therelationship 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 afully string-based, non-generic relationship —
builder.HasOne("Cluckwork.Domain.Flocks.Flock").WithMany().HasForeignKey("FlockId") .HasConstraintName("FK_Expenses_Flocks_FlockId").OnDelete(DeleteBehavior.Restrict)— using thefully-qualified type name as a string, not
typeof(Flock)or a generic parameter.(
Cluckwork.Domain.Flock, which doesn't exist — the actual entity isCluckwork.Domain.Flocks.Flock) and omittedExpenseConfiguration's existing.OnDelete(DeleteBehavior.Restrict)(EF's convention default for an optional FK isClientSetNull, a different runtime model). Both are corrected above; a string-typo or adropped 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 to
Flock's CLR type (inFlock Management's assembly) from Finance's configuration, so no
Platform.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.
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.
table-ownership.ymlwith owner, referencedowner, purpose and allowed read/write adapter. Cross-owner navigations are removed from
module-facing models only when a behavior-neutral change proves safe.
TenantContext, global filters,TenantStampInterceptor,Npgsql retry configuration and ambient transaction coordinator.
IgnoreQueryFiltersremains limited to reviewed startup/operational paths.
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 second guard confirms every
AccountIdentity is filtered and stampable.8. Enforcement and adversarial proof requirements
Place boring tests in
tests/Cluckwork.Architecture.Tests:shared kernel; implementations cannot reference peer implementations. Mutation: add
a peer implementation
ProjectReference; run the focused test and record its failure.Mutation: add a reverse contracts reference and observe the cycle test fail.
interfaces and Platform persistence adapters. Mutation: inject
IEggLotRepositoryinto Commerce and observe failure.contract or approved workflow, not handlers/repositories/context. Mutation: restore
one direct handler parameter and observe failure.
EF,
DbContext,DbSet,IQueryable, entity/aggregate bases, repository implementationsor module implementation assemblies. Mutation: return
IQueryable<EggLot>andobserve failure.
docs/architecture/table-ownership.yml;require exactly one owner and validate documented cross-owner FKs. Mutation: remove
Paymentsownership and observe failure.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
transactions already proven atomic.
worsens atomic ledger workflows and violates the single-deploy goal.
consistency and changes HTTP failure semantics.
AppDbContext: preserves the current backdoor.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
temporarily in one implementation assembly while contracts stabilize.
store; the proposed plan keeps writes in Platform and reads in Insights.
has a measurable budget.
Top five risks
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.
mforce commentedon Aug 12, 2026
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
/api/v1DTOs/status codes and SPA callers remain the compatibility oracle.InitialCreate.reverted before commit and red output is attached to review evidence.
a write contract changes even if tests are green; this plan intends no contract changes.
Phase 0 — baseline and characterization (2–4 days)
docs/architecture/table-ownership.yml,module-dependencies.yml, generated inventory script undertools/architecture/; no production filesdotnet build Cluckwork.sln;npm run typecheck --prefix webdotnet test Cluckwork.sln; record Docker limitation if anyDependencies: 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)
tests/Cluckwork.Architecture.Tests/*.csproj, solution entry, graph/contract test helpers; lock filedotnet restore, locked restore, focused teststable-ownership.yml, enumerate EF relational model and cross-owner FKsPaymentsowner → focused test red; restore → greenIQueryable<EggLot>return; every mutation must redsrc/Cluckwork.SharedKernel; initially link/move only stableResult,Error,Money, clocks/paging DTO primitives using compatibility type-forwarding where neededDependencies: 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)
src/Cluckwork.Modules.Finance.Contractswith use-case commands/results andIFinanceModule; add contract testssrc/Cluckwork.Modules.Finance.Implementation; internal registration extension; wrap existing handlers behind contract without moving behaviorLegacyFinanceModuleAdapter; handler unit tests plus module-interface testsFOR SHAREcurrency lock through a synchronous lock-aware Farm portExpense→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 databaseExpenseEndpointsto injectIFinanceModule; central DI delegates toAddFinanceModule; remove legacy handler registrations/adapterscodex review, PR #423 round 8:
SimulationDataSeeder(SimulationDataSeeder.cs:112-113,:1034,:1049,:1283-1284) DI-injectsCreateExpenseCategoryHandler/CreateExpenseHandlerdirectlyand queries
db.ExpenseCategories/db.Expensesfor its idempotent-seeding existence checks andpost-seed exact-count verification — a real production caller of the exact types 2.3/2.4 make
internal to
Finance.Implementation, and Phase 10 item 3 ("Convert demo/simulation seeders tomodule 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:
SimulationDataSeederis 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 theexpense report and export, and
CurrencyBoundRowProbe(CurrencyBoundRowProbe.cs:21) queriesdb.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 SQLsemantics") 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 ofthose 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'sown "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)
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:
ICredentialEpochVerifier; prove fresh DB read per request.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)
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)
Platform composition; preserve middleware ordering.
a time; preserve durable anchor, exact counts and fail-closed validation.
rgproves no caller.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:
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:
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.
mforce commentedon Sep 13, 2026
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."
mforce commentedon 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.
mforce commentedon Sep 14, 2026
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 followonly 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
Mcp/and OpenIddictA 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 with
git 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:
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".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) — exactlywhere #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) andTenantBypass/GuardScanner.cs, a 1,424-line Roslyn walker that parses every.csundersrc/—including
Cluckwork.Api, which that project does not reference — with a parse-error gate and a400-file floor (
GuardScanner.cs:74,:118,:136). An architecture guard is a source guard. A newtest project would cost a CI matrix leg (#775), a
SolutionTestProjectSplitTestsreconcile, atools/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-typeChronologicalListTypeslist —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.cslives in
src/Cluckwork.Domain/Sales/. Both ends are Commerce.Accounts → Media(2) —Domain.MediaholdsImageKind,ImageSanitizer,SanitizedImage. Noentity, 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:UpdateFarmSettingsHandler.cs:12injectsIEggUnitConversionRepository;:149andUpdateFarmSettingsValidator.cs:98callDiscountCeiling.TryParsePercent(#727)SetStepperUnitHandler.cs:13injectsIEggUnitConversionRepositoryConfirmSaleHandler.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 add
IStepUpGrantService,SystemActors,SecurityEvents,AuditActions; §8's preambleshould place guards in
Application.Tests/Architecture/rather than a new project; §8 rule 4 needsthe 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; Phase10.1 is deleted as inherited from #804.
ReportQueries,ExportQueriesandCurrencyBoundRowProbelive in
Infrastructure/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: a
tsconfig.jsonmodulekey, a web testconstant, an unrelated integration test, an AGENTS.md section, and one useful hit (
Customeratsrc/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 committeddocs/architecture/module-ledger.jsonnaming the nine owners, their namespaces and every cross-owneredge with a stated reason, plus a Roslyn walk riding
GuardScanner's existing root/parse/floorhelpers. No
src/file, no.csproj, no CI change, no package. Red on four mutations: an undeclarededge; 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/aDirectRepositorytier with a reason and a review issue, and gives the four OpenIddict tables anAccess owner. Keyed on
EndpointDataSourcerather than theMcp/folder, so it does not fail closedbefore
/mcpexists.Open questions
(C, +57–95 days)? A is correct either way, which is why it is first.
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.
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 anexception. 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 for
ICreatedRecord/IMutableRecord— and does that belong in Track C or its ownissue?
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.
size:Sissues for ~3 days of work may bedisproportionate ceremony.
Dependency notes have been added to #789, #788, #806, #807, #809 and #795. Nothing was closed or
reassigned.
mforce commentedon Sep 14, 2026
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.
mforce commentedon Sep 14, 2026
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
#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
#847 is worth noting: there are zero
IQueryablereturns in Application today, so it starts withno 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.
Three of these are worth singling out:
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.
2608-line seeder,
BusinessRecordModel's cross-module type list. If only part of Track C is everrun, this is the part with a payoff that is not about cleaning up the past.
BusinessRecordModelitem is independent of all of Track C and can be done standaloneat 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.
mforce commentedon Sep 14, 2026
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:IDailyEntryRepositoryfor provenance) and Access → EggOperations (AccountProvisionerseedsEggGrade.Defaults). All five are ledgered with reasons.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.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
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.mforce commentedon Sep 14, 2026
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):owners,edgestables,foreignKeys,tableOwnerOverridesadapterRoots,adapters(150 rows over 397 adapters)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:
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.mforce commentedon Sep 14, 2026
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).
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.
test(arch): module ledger and cross-owner edge ratchet (#842) (#872)
test(arch): seam-surface guard, no persistence type crosses a seam (#847
test(arch): seam-surface guard, no persistence type crosses a seam (#847
test(arch): table-owner completeness walked from the EF model (#845) (#…
test(arch): adapter-privilege ratchet over endpoints, CLI, jobs and s…
test(arch): generate the coupling matrix from the ledger and the thre…
mforce commentedon 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.
test(arch): adapter tiers, dated privilege exemptions declared before…
test: fix two test-infrastructure false positives (hook repo root, im…
mforce commentedon Sep 26, 2026
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
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
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.
mforce commentedon Oct 1, 2026
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.mforce commentedon Oct 5, 2026
Closing: every tracked slice is done, and Track C finished on 2026-10-05.
Track C shipped
IAccessSeedLookup, and Access claimsIIdentityProviderandIStepUpGrantService(E2, which also closed test(arch): guard module-to-module calls against non-contract types #1023).IFarmDirectoryadded (P1–P3).Add<Module>Moduleregistration step per owner, with identical DI dumps (P7).docs/decisions/858-platform-composition.md(P8).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.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
Deliberately not done
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.