Skip to content

Modular-monolith architecture design + implementation plan (from PR #423) #514

Description

@mforce

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 AppDbContext while 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

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.

Activity

  1. mforce commented on Aug 12, 2026

    @mforce
    OwnerAuthor

    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.json exists, but the
    graphify executable 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 needed
    

    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

    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 rows
    

    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 injects IAuditWriter); SubmitDailyEntryHandler and
    ConfirmSaleHandler inject no IAuditWriter and 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: ConfirmSaleHandler never references payments at all,
    and VoidSaleHandler only checks IPaymentRepository.AnyNonVoidedByOrderAsync — a guard that
    rejects the void if a payment exists — it never mutates one. Payments are recorded/voided through
    their own separate commands (VoidPaymentHandler is 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

    Application references Domain; Infrastructure references both; Api references all
    three. The API composition root registers feature repositories and handlers one by one.
    AppDbContext inherits 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/Error and 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 roles user handlers, IIdentityProvider, Identity users/roles/refresh tokens, credential middleware, auth/users/me endpoints, bootstrap/recovery/purge login, set-password, users; auth/session contexts
    Farm Account, FarmLogo, currency/unit/brand settings account/logo handlers and repos, tenant/farm clocks, account endpoints settings/account, farm context
    Flock Management Flock, BirdMovement flock handlers/repos and lifecycle/scope checks flocks
    Egg Operations DailyEntry/lines, EggGrade, EggLot, egg movements daily-entry, grade and stock handlers/repos; lock-sweep daily entry, history, grades, stock
    Commerce Product, conversions/mappings, Customer, SalesOrder, items, allocations, Payment catalog/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 query dashboard, reports, export, audit
    Platform audit event plus operational records AppDbContext, tenant interceptor, unit of work, idempotency, durable jobs, health, migrations, seeders, CLI client error/idempotency transport only

    There are 49 application handlers. A source-namespace scan found these direct feature
    reference counts: Production-shaped DailyEntries points 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, UserRoleAssignments
    Farm Accounts, FarmLogos
    Flock Management Flocks, BirdMovements
    Egg Operations DailyEntries, DailyEntryGrades, EggGrades, EggLots, EggInventoryMovements
    Commerce Products, ProductEggGradeMappings, EggUnitConversions, Customers, SalesOrders, SalesOrderItems, SalesOrderAllocations, Payments
    General Inventory InventoryItems, InventoryLots, InventoryMovements, FeedUsages, WaterUsages
    Finance ExpenseCategories, Expenses
    Insights no source-of-truth tables initially; reads AuditEvents and owner tables
    Platform AuditEvents, idempotency_records, durable_jobs, simulation_seed_state, EF history

    Important 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 W seams 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

    1. 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. Publish DailyEntrySubmitted only after
      commit for Insights; an event must never create authoritative stock asynchronously.
    2. 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 on IPaymentRepository.AnyNonVoidedByOrderAsync, rejecting the void if a
      payment exists; recording/voiding a payment is its own separate command. The seam
      returns opaque DTOs, not lots.
    3. 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 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 CanRecordProductionOn after
      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.
    4. 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.
    5. Reports/exports. Insights owns query DTOs and read-only adapters. It may execute
      documented, AsNoTracking cross-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 read
      models, but are not required to manufacture module purity.
    6. Identity/platform paths. Access owns token/security state and credential epoch.
      Tenant identity resolution stays claim-only in Platform: TenantResolutionMiddleware
      keeps populating TenantContext directly from the JWT account_id claim, 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 before TenantContext
      exists or require bypassing filters mid-request, weakening the fail-closed tenant boundary.
      Farm contracts are called only after TenantContext is 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 session
    

    Contracts may depend only on SharedKernel. Implementations depend on their own contract,
    SharedKernel and narrowly approved platform contracts. Workflows depends 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; small IFarmContextReader returning immutable FarmContextSnapshot | FarmSettingsChanged after commit; one aggregate transaction | no Identity or feature repo; tenant filter/stamp/timezone fail-closed tests |
    | Flock Management | flock lifecycle and bird ledger | IFlockModule plus 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, deep ReserveFifo/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 | IInsightsModule returns 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 or IQueryable; 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 AppDbContext and 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. Keep AppDbContext internal 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's AppDbContext, an unavoidable
      Platform.Persistence ↔ Finance.Implementation cycle. 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_FlockId and IX_Expenses_FlockId are 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-emitting AddForeignKey for a
      same-named constraint that already exists fails on every already-migrated database; and
      simply not emitting one, after Expense'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, not typeof(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 omitted ExpenseConfiguration'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 to Flock's CLR type (in
      Flock Management's assembly) from Finance's configuration, so no Platform.Persistence ↔
      Finance.Implementation cycle and no Finance → Flock Management compile 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.yml with 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 every AccountId entity is filtered and stampable.

    8. Enforcement and adversarial proof requirements

    Place boring tests in tests/Cluckwork.Architecture.Tests:

    1. Parse project references and public type signatures: contracts depend only on the
      shared kernel; implementations cannot reference peer implementations. Mutation: add
      a peer implementation ProjectReference; run the focused test and record its failure.
    2. 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.
    3. Inspect implementation IL/source namespaces: a module may use only its own repository
      interfaces and Platform persistence adapters. Mutation: inject
      IEggLotRepository into Commerce and observe failure.
    4. 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.
    5. 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: return IQueryable<EggLot> and
      observe failure.
    6. Compare relational model table names with docs/architecture/table-ownership.yml;
      require exactly one owner and validate documented cross-owner FKs. Mutation: remove
      Payments ownership and observe failure.
    7. 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

    1. Confirm Egg Operations as the owner of DailyEntry/grades/lots/egg ledger and Commerce as the caller of its deep stock seam.
    2. Approve Insights' controlled cross-table read privilege versus building projections now.
    3. Choose whether module implementations are one assembly per module initially or grouped
      temporarily in one implementation assembly while contracts stabilize.
    4. 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.
    5. Confirm acceptable delivery horizon and whether CI build-time growth from new projects
      has a measurable budget.

    Top five risks

    1. Breaking an atomic ledger/cache transaction while replacing repository calls with seams.
    2. Changing FIFO lock acquisition order and introducing deadlocks or allocation races.
    3. Leaking EF/entity types through a convenient contract, recreating coupling under new names.
    4. Weakening tenant or credential fail-closed behavior through duplicated contexts/adapters.
    5. 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/v1 contracts, 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.

  2. mforce commented on Aug 12, 2026

    @mforce
    OwnerAuthor

    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/v1 DTOs/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 under tools/architecture/; no production files script output matches EF model, 49 handlers and endpoint inventory; dotnet build Cluckwork.sln; npm run typecheck --prefix web delete 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 any tests 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 file dotnet restore, locked restore, focused tests remove project/solution entry
    1.2 Ownership guard consume table-ownership.yml, enumerate EF relational model and cross-owner FKs remove Payments owner → focused test red; restore → green guard/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 red revert 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 stable Result, Error, Money, clocks/paging DTO primitives using compatibility type-forwarding where needed build 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.Contracts with use-case commands/results and IFinanceModule; add contract tests existing 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 behavior temporary LegacyFinanceModuleAdapter; handler unit tests plus module-interface tests DI 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 SHARE currency lock through a synchronous lock-aware Farm port type 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→Flock relationship via the string-based non-generic HasOne("Cluckwork.Domain.Flocks.Flock")...HasForeignKey("FlockId").HasConstraintName("FK_Expenses_Flocks_FlockId").OnDelete(DeleteBehavior.Restrict) (no HasOne<Flock>() generic and no typeof(Flock) — either would force a Finance→Flock Management/Platform.Persistence↔Finance.Implementation reference, design §7); this reproduces the FK/index/delete-behavior InitialCreate and the current ExpenseConfiguration already create byte-for-byte (codex review round 6: the fully-qualified name and the Restrict delete behavior are both load-bearing — a typo'd namespace or a dropped OnDelete each independently fail the model-equivalence check), so it needs no new migration — a fresh AddForeignKey for the same constraint name would fail against an already-migrated database EF 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 ExpenseEndpoints to inject IFinanceModule; central DI delegates to AddFinanceModule; remove legacy handler registrations/adapters endpoint 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-injects CreateExpenseCategoryHandler/CreateExpenseHandler directly
    and queries db.ExpenseCategories/db.Expenses for 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 to Finance.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) and ExportQueries
    (ExportQueries.cs:235, :240) query db.Expenses/db.ExpenseCategories directly for the
    expense report and export, and CurrencyBoundRowProbe (CurrencyBoundRowProbe.cs:21) queries
    db.Expenses as 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, TenantContext adapter 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)

    1. 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:

    1. Add Access contracts around existing use cases without rewriting Identity.
    2. Switch users/me endpoints and flock assignment through contracts.
    3. Switch auth endpoints while preserving cookies, JWT claims and error/timing behavior.
    4. Switch credential middleware to ICredentialEpochVerifier; prove fresh DB read per request.
    5. Move bootstrap/recover services and refresh purge behind Access; CLI/job adapters call it.
    6. Internalize Identity implementation and remove legacy IIdentityProvider exposure.

    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)

    1. Add Insights contract/materialized DTOs and restricted read-session abstraction.
    2. Move report queries without changing SQL semantics.
    3. Move repeatable-read export snapshot/streaming.
    4. Expose audit reads; leave append sink in Platform and document ownership.
    5. 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)

    1. Move idempotency, transaction, tenant, persistence and health registration into explicit
      Platform composition; preserve middleware ordering.
    2. Convert daily lock/token purge jobs to contract callers; retain durable worker behavior.
    3. Convert demo/simulation seeders to module bootstrap/orchestration contracts one dataset at
      a time; preserve durable anchor, exact counts and fail-closed validation.
    4. Convert CLI verbs to contracts; migrate remains direct Platform migration operation.
    5. Delete generic repository/UoW and legacy feature registrations once rg proves no caller.
    6. 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.

  3. mforce commented on Sep 13, 2026

    @mforce
    OwnerAuthor

    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 the graphify executable 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.json exists, so graphify explain can do it properly.

    The design's own status line still governs: "proposed architecture; no production refactor is authorized by this document."

  4. added this to the Modular monolith milestone on Sep 14, 2026
  5. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    Promoted to an epic on 2026-09-14 (owner), with a Modular monolith milestone and an epic-514 label 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 the graphify executable 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.json now exists, so graphify explain can 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 tier4 deliberately. 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.

  6. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    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 @
    6231b31 and 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 OpenIddict 2 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 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:

    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 McpCallContext is a stronger implementation of §3.5.6 than this design wrote down.

    #674 does not interact: nothing here reads web/, nothing there touches src/.

    Everything past Track A waits for a mechanical reason rather than a cautious one. B and C land in
    Program.cs (577 lines) and CluckworkFeatureServiceCollectionExtensions.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.Tests already carries Microsoft.CodeAnalysis.CSharp (.csproj:4) and
    TenantBypass/GuardScanner.cs, a 1,424-line Roslyn walker that parses every .cs under src/ —
    including Cluckwork.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), a SolutionTestProjectSplitTests reconcile, a
    tools/coverage/collect.sh entry 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-34 holds an 11-type
    ChronologicalListTypes list — 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.Implementation cycle. 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:line cites 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 reference Cluckwork.Domain.Sales, and Customer.cs
      lives in src/Cluckwork.Domain/Sales/. Both ends are Commerce.
    • Accounts → Media (2) — Domain.Media holds ImageKind, ImageSanitizer, SanitizedImage. No
      entity, no table; FarmLogo is in Domain/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 read R:

    Cell Evidence
    Farm → Commerce UpdateFarmSettingsHandler.cs:12 injects IEggUnitConversionRepository; :149 and UpdateFarmSettingsValidator.cs:98 call DiscountCeiling.TryParsePercent (#727)
    Access → Commerce SetStepperUnitHandler.cs:13 injects IEggUnitConversionRepository
    Commerce → Access ConfirmSaleHandler.cs:23 injects IUserRoleAssignmentRepository; :171 calls GetEffectiveRoleAsync inside the transaction (#727)

    Remaining corrections: 49 handlers → 57; §3.2's reference-count paragraph counted only
    Application.Features.* while the 2026-09 measurement mixes Domain.*, 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 preamble
    should place guards in Application.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, Error and Money already sit in Domain/Common/, which every layer references; Phase
    10.1 is deleted as inherited from #804. ReportQueries, ExportQueries and CurrencyBoundRowProbe
    live in Infrastructure/Repositories/, not Persistence/.

    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.json module key, a web test
    constant, an unrelated integration test, an AGENTS.md section, and one useful hit (Customer at
    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.json naming the nine owners, their namespaces and every cross-owner
    edge with a stated reason, plus a Roslyn walk riding GuardScanner's existing root/parse/floor
    helpers. No src/ 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). Declares Mcp/ a
    DirectRepository tier with a reason and a review issue, and gives the four OpenIddict tables an
    Access owner. Keyed on EndpointDataSource rather than the Mcp/ folder, so it does not fail closed
    before /mcp exists.

    Open questions

    1. 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.
    2. Does priority:tier4 gate 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.
    3. Is an Mcp DirectRepository tier 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.
    4. BusinessRecordModel.ChronologicalListTypes is 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 for ICreatedRecord/IMutableRecord — and does that belong in Track C or its own
      issue?
    5. 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.
    6. Two slice issues or one? MCP filed 8, OAuth 6. Two size:S issues 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.

  7. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    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, both priority:tier4, both on the Modular monolith milestone.

    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-514 label picks both up.

    Tracks B and C remain unfiled, pending the authorisation question in the re-plan comment above.

  8. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    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 IQueryable returns 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:

    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.

  9. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    Two slices in review: #872 (Track A slice 1) and #875 (Track B slice 5)

    #842 is implemented as #872: 16 cells, 66 symbols, five mutations recorded red, four review rounds (two CodeRabbit, two Codex) closed. #847 is 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 in Program.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 IDailyEntryRepository for provenance) and Access → EggOperations (AccountProvisioner seeds EggGrade.Defaults). All five are ledgered with reasons.
    • Finance → Farm is R, not W. The code reads the account row under FOR SHARE and mutates nothing, the same shape §3.4 classes R on Commerce → Farm and Inventory → Farm. The ledger records R with 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.Domain and Cluckwork.Application exactly, never as subtrees. The two GlobalUsings.cs files force someone to own the project roots, and a subtree claim would absorb a new Cluckwork.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

  10. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    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 main as 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-checked

    The 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)

    E and Q are not syntactically observable, so the generated file reports W/R letters with live symbol counts, fk:<n> from the table census, P for the Platform column and A (n) adapter-reach counts for the Platform row. Full file: tests/Cluckwork.Application.Tests/Architecture/Data/coupling-matrix.md on #879.

    Track B's "waits for #788/#789" reason was the rebase war in Program.cs. None of these five PRs touches src/, so they went ahead; #843 still waits for MCP or OAuth to unpark, and Track C still needs the authorisation decision.

  11. mforce commented on Sep 14, 2026

    @mforce
    OwnerAuthor

    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.

  12. mforce commented on Sep 15, 2026

    @mforce
    OwnerAuthor

    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 adapterTiers ledger section holding one dormant row, Cluckwork.Api.Mcp → DirectRepository, surfaced by MapMcp, 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 need adapters rows and AppDbContext stays 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.

  13. mforce commented on Sep 26, 2026

    @mforce
    OwnerAuthor

    Track C audit, 2026-09-26, against main at e47288c

    All 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 IAuditEventRepository directly 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.

  14. mforce commented on Oct 1, 2026

    @mforce
    OwnerAuthor

    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 FlockScope shadowed the production FlockScope class and broke six files. Module moves in Track C create new namespaces, so this is the trap to watch. Details in docs/decisions/985-csharp-style-gate.md and the AGENTS.md paragraph.

  15. mforce commented on Oct 5, 2026

    @mforce
    OwnerAuthor

    Closing: every tracked slice is done, and Track C finished on 2026-10-05.

    Track C shipped

    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

    • 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationepicPhase-level tracking issueepic-514Modular monolith architecture (#514)priority:tier4Deferred or speculative

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions