Skip to content

[C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858

Description

@mforce

Amended 2026-10-04 (#857 E2, PR #1060): both seeders use the non-Production Access read port; simulation creation uses Access operations. At E2 head, 37 rows remain for #858 (29 simulation, 8 demo), including five new Access rows for P5/P6. See this comment.

Amended 2026-10-03 (approved plan): eight PRs, P1 to P8; scope item 5 drops IUnitOfWork and now covers the generic repository and legacy registrations; seeders move onto per-module, non-Production fixture ports. See this comment.

Amended 2026-10-03 (#854, PR #1030): both seeders call ICommerceModule instead of the Commerce handlers; eight Commerce seeder reads remain as #850 rows. See this comment.

Amended 2026-10-02 (#853, PR #1028): the lock sweep and both seeders call IEggOperationsModule; five Egg Operations seeder reads remain as #850 rows. See this comment.

Amended 2026-10-02 (#852, PR #1021): both seeders call IFlockModule/IFlockLookup instead of the flock handlers; five Flock Management reads remain as #850 rows. See this comment.

Important

Track C is unscheduled. These slices move production code, and the 2026-08 design's own status
line still governs: "proposed architecture; no production refactor is authorized by this
document."
Filed so the analysis is tracked work rather than a comment, not as a commitment to
run it.

The 2026-09-14 re-plan
recommends gating Track C on evidence from Tracks A and B: if the #842 ratchet fires repeatedly in
real pull requests, these become worth their cost; if it stays quiet, they do not. Track C is
57–95 focused engineering days and produces no user-visible change.

Read the re-plan before picking any of these up. Several assumptions in the 2026-08 plan are
corrected there.

Platform composition, jobs, CLI verbs and seeders. The slice that actually makes adding a new module
cheap
— worth knowing if only part of Track C is ever run.

Why this one is different from the eight before it

Slices 7–15 clean up existing modules. This one fixes the files that get worse with every module
added, whether or not the existing ones are ever extracted:

File Size today Why it degrades per module
CluckworkFeatureServiceCollectionExtensions.cs 352 lines every handler, validator and repository registered by hand
Program.cs 577 lines composition root
SimulationDataSeeder.cs 2608 lines must know every module's datasets
BusinessRecordModel.cs:21-34 11 types, 6 modules #819 says it grows with every time-paged table

If a new domain is ever added — raising, processing, anything — this is the work that decides whether
that is pleasant or painful. The BusinessRecordModel item in particular is independent of the
rest of Track C
and could be done standalone at any time.

Scope

  1. Move idempotency, transaction, tenant, persistence and health registration into explicit Platform
    composition. Preserve middleware ordering exactly (refactor(api): make process role explicit for boot guards #347, Credential epoch: per-request revocation check, deployed inert ahead of the user-admin mutations #364).
  2. Daily lock-sweep and token-purge jobs call contracts; durable worker and single-leader behaviour
    unchanged (Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271).
  3. Convert demo and simulation seeders to module bootstrap contracts, one dataset at a time.
    Preserve the durable anchor, the exact counts and the fail-closed validation (Extract DB seeding (Demo/Simulation) out of API startup into an explicit command entry point #280, fix(seed): seeded audit events carry "(unresolved)" as the actor, now visible on record History columns #500).
  4. CLI verbs call contracts. migrate stays a direct Platform migration operation (Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263).
  5. Delete the generic repository, unit of work and legacy feature registrations once a search proves
    no caller remains.
  6. Tighten the ledger's declared surface and record the final graph.

Done when

Full solution green; frontend typecheck, tests and build green; tools/simulation/verify-harness.sh
passes; Playwright quick suite green. Simulation manifests should need no configuration change,
because no boot guard or config key changes — if one does, every harness file updates in the same
commit (#370), and the AppHost too (#565).

Explicitly not in this slice

The idempotency protocol itself. If MCP's #804 is ever built it extracts that, and this slice
consumes the result rather than writing a second copy — writing a second copy is how #307 gets
reopened.

Activity

  1. added this to the Modular monolith milestone on Sep 14, 2026
  2. added
    sliceThin vertical work item
    area:apiAPI/endpoint layer
    size:LSeveral days; wide blast radius or unresolved scope
    epic-514Modular monolith architecture (#514)
    track:C#514 Track C — moves production code; UNSCHEDULED, needs authorisation
    on Sep 14, 2026
  3. changed the title [-]#514 slice 16: Platform composition, jobs, CLI and seeder conversion[/-] [+][C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion[/+] on Sep 14, 2026
  4. mforce commented on Sep 26, 2026

    @mforce
    OwnerAuthor

    Audit 2026-09-26, against main at e47288c

    Body left as written.

    Refreshed numbers.

    File In the issue Today
    Hosting/CluckworkFeatureServiceCollectionExtensions.cs 352 352
    Program.cs 577 607
    SimulationDataSeeder.cs 2608 2643

    Correction: that growth is not evidence for this slice. Program.cs moved for CSP nonce plumbing (#874) and a rate-limit test window (#895). The seeder moved for a fixture hen-day fix (#959). None of it is composition-root creep or new module registration. The argument for this slice is what a new domain would cost, not observed drift over the last twelve days, and the issue should say so rather than lean on line counts that grew for unrelated reasons.

    The BusinessRecordModel item is carved out. It is genuinely independent, needs no contracts, no dependency-injection changes and no seam work, so it is now its own issue and can ship at any time. The file is 153 lines; the work is splitting ChronologicalListTypes and MappedExclusions into per-module contributions merged before ValidateCensus runs, keeping the walk and the validation centralized. Remove it from this slice's scope and point at the new issue.

  5. mforce commented on Oct 2, 2026

    @mforce
    OwnerAuthor

    Amendment from #850 (PR #1013)

    What shipped. module-ledger.json now has a compatibilityExceptions section. CompatibilityExceptionRealTreeTests fails CI on any read of a contracted module's tables that is neither allowed by structure nor registered. Three rows name this issue as their deletion trigger, all with owner Platform:

    • Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EnsureExpenseCategoryAsync looks up db.ExpenseCategories by name;
    • Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EnsureExpenseAsync checks db.Expenses for the fixture description;
    • Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsync counts both tables for the manifest check.

    What this issue now owns. Converting those three reads deletes the rows. The guard reports a row as stale once its read is gone, so the slice that converts the seeder must delete the rows in the same PR. IFinanceModule deliberately has no count or existence read yet; #850 did not widen it. The rule is in docs/decisions/850-compatibility-exceptions.md.

  6. mforce commented on Oct 2, 2026

    @mforce
    OwnerAuthor

    Amendment from #852 (PR #1021). That PR ships part of this issue's scope. The body above is left as written.

    Shipped:

    • DemoDataSeeder and SimulationDataSeeder no longer inject the flock lifecycle handlers or IFlockRepository. They call IFlockModule and IFlockLookup.
    • SimulationDataSeeder.EnsureFlockAsync resolves by name and fails on a duplicate name instead of taking the first match.

    Remains: five Flock Management reads are registered as #850 compatibility exceptions with deleteWhen: #858. They are DemoDataSeeder.CleanupPartialSeedAsync, .SeedAsync and .SeedDemoAsync, and SimulationDataSeeder.ComputeCountsAsync and .SeedExplicitBirdMovementsAsync.

  7. mforce commented on Oct 2, 2026

    @mforce
    OwnerAuthor

    Amendment from #855 (PR #1027)

    What changes for this slice once #1027 merges. General Inventory is contracted (IInventoryModule), so seven more SimulationDataSeeder reads are registered compatibility exceptions with deleteWhen #858, owner Platform, reaches: GeneralInventory:

    • ComputeCountsAsync (manifest counts of all five Inventory tables)
    • EnsureAdjustmentAsync, SeedFeedAdjustmentsAsync (InventoryMovements existence and count)
    • EnsureInventoryItemAsync (InventoryItems by name)
    • EnsureOpeningPurchaseAsync (InventoryLots existence and id)
    • SeedFeedUsageAsync (FeedUsages existence)
    • SeedWaterUsageAsync (WaterUsages existence)

    IInventoryModule has no count, existence or by-name reads, following #850's choice for Finance: the contract does not grow reads only the seeder needs. The seeder's writes already go through IInventoryModule. Its one former IInventoryItemRepository.HasLotsAsync call is now the identical db.InventoryLots.AnyAsync inside EnsureOpeningPurchaseAsync, covered by that method's row.

  8. mforce commented on Oct 2, 2026

    @mforce
    OwnerAuthor

    Amendment from #853 (PR #1028). The body above is left as written.

    Shipped:

    • DailyEntryLockSweep locks each account's due entries through IEggOperationsModule.LockSubmittedEntriesAsync. The lock loop moved, unchanged, into LockDueDailyEntriesHandler. Scope item 2's lock-sweep half is done, apart from DailyEntryLockSweep.RunAsync's cross-farm db.Accounts read, which stays an existing [C] #514 slice 8: close or date every compatibility exception #850 row.
    • DemoDataSeeder and SimulationDataSeeder call IEggOperationsModule instead of the daily-entry handlers and the grade and daily-entry repositories.

    Remains here. Five new #850 compatibilityExceptions rows, all owner Platform with deleteWhen #858:

    • DemoDataSeeder.CleanupPartialSeedAsync, which deletes a partial demo seed's DailyEntries, DailyEntryGrades, EggInventoryMovements and EggLots;
    • DemoDataSeeder.MissingBaseDataAsync, which reads EggGrades;
    • SimulationDataSeeder.ComputeCountsAsync, which counts DailyEntries and EggLots;
    • SimulationDataSeeder.MissingBaseDataAsync, which reads EggGrades;
    • SimulationDataSeeder.SeedFlockHistoryAsync, the natural-key existence check. It became db.DailyEntries.AnyAsync with the same predicate, so the contract does not grow a read only the seeder needs.
  9. mforce commented on Oct 3, 2026

    @mforce
    OwnerAuthor

    Amendment from #854 (PR #1030). The body above is left as written.

    Shipped: DemoDataSeeder and SimulationDataSeeder call ICommerceModule instead of the six Commerce handlers (create product, customer and order, add a line, confirm, record a payment).

    Remains here. Eight new #850 compatibilityExceptions rows, all owner Platform with deleteWhen #858:

    • DemoDataSeeder.CleanupPartialSeedAsync, which deletes a partial demo seed's SalesOrderItems, SalesOrders and Customers;
    • SimulationDataSeeder.ComputeCountsAsync, which counts Customers, SalesOrders by status and Payments;
    • SimulationDataSeeder.EnsureConfirmedOrderAsync, which reads the order's status before confirming;
    • SimulationDataSeeder.EnsureCustomerAsync and EnsureProductAsync, the lookups by name;
    • SimulationDataSeeder.EnsureExtraLineAsync, which checks SalesOrderItems for the product's line;
    • SimulationDataSeeder.EnsurePartialPaymentAsync, which checks Payments and reads the order total;
    • SimulationDataSeeder.FindOrderAsync, the (customer, order date) natural key.

    The contract did not grow reads only a seeder needs (#850, #853, #855).

    Rationale: docs/decisions/854-commerce-contract.md, which lands with PR #1030.

  10. mforce commented on Oct 3, 2026

    @mforce
    OwnerAuthor

    Approved plan, 2026-10-03

    The owner approved the #858 design checkpoint together with every change from the independent review. The body above is left as written.

    Scope amendment

    Scope item 5 now reads "Delete the generic repository and legacy registrations once a search proves no caller remains." IUnitOfWork stays. 49 handlers and ExportEndpoints call it, and it is already the Platform transaction port that joins the request's ambient transaction without replaying the caller's work. The 2026-08 design called it IModuleTransaction; renaming it adds no protection.

    Decisions

    • Seeders move onto per-module fixture ports. Each port lives in its module, is listed in that module's ledger contract, and is registered only outside Production. There is no blanket read allowance for the seeder types.
    • Registration is one small file per module under Hosting/Modules/, plus a Platform file. Access keeps AddCluckworkIdentity. The seven ICurrencyBoundRowSource registrations stay one ordered block.
    • No permanent DI golden test. P7 compares the real service descriptors before and after, as scratch evidence, for Development, Testing and Production and for the Serving and OneShot process roles.
    • If [C] #514 slice 15: Access contract — security hold point, runs last #857 E leaves the seeders' flock-assignment write here, it becomes a non-Production Access fixture operation, security-reviewed in P5. CreateUserAsync stays with E's IAccessOperations.

    PR list

    PR Change Starts
    P1 Declare Insights' ledger contract (IInsightsModule, EntityProvenance, ExportDataset) now
    P2 Delete IRepository<T,TId> and the repository members nothing calls; keep IFlockRepository.Update and the live assignment and logo Remove after P1
    P3 Scoped IFarmDirectory for the cross-farm Accounts reads in list-accounts, the farm-code lookup and the lock sweep (3 rows) after P2; rebases over #857 D
    P4 Finance and General Inventory fixture ports (10 rows) after #857 E
    P5 Commerce, Egg Operations, Flock Management and Farm fixture ports in the simulation seeder (15 rows), plus E's simulation Access rows after P4
    P6 Demo seeder fixture ports (7 rows), plus E's demo Access rows, with three new cleanup tests after P5
    P7 Per-module registration files and the Platform registration file after P6
    P8 Ledger end state: empty compatibilityExceptions, deliberate Loosenable pruning, decision record, src/AGENTS.md. Also delete the AccountSlugLookup.ResolveAsync forwarder, inlining GetRequiredService<IFarmDirectory>().FindIdBySlugAsync into the four verbs (#1043 review, Opus F1), once #857 D has moved them last; closes this issue

    The 35 rows naming this issue split 25 simulation seeder, 7 demo seeder, 2 CLI and 1 lock sweep. Recount after #857 E; the total is not a finish-line test.

    Every PR carries its own change map, mutation table and deslop record, and this issue's full "Done when" gates apply.

    Edited 2026-10-03: P8 gains the AccountSlugLookup.ResolveAsync forwarder deletion from the #1043 review.

    Handover for P4 onward (2026-10-03)

    P1 (#1040) and P2 (#1042) are merged; P3 (#1043) is in review. After P3, 32 rows name this issue: 25 in the simulation seeder and 7 in the demo seeder. Recount at #857 E's head.

    Notes for the next worker:

    • Any type on a module's contract can be injected by any adapter. A fixture port that must stay out of request code needs a caller guard. For fixture ports, that is the Production-absence test.
    • Extend FarmDirectoryCallerTests' allowlist in P5 (seeders) and P7 (Hosting/Modules/).
    • Parse source guards with ModuleLedgerScanner.ParseOptions.
    • Moved queries must pass test: fail the integration suite on EF Core query-shape warnings #1039's query-shape rule.

    The full notes are in the worker's design file. Open follow-ups from the reviews, not scheduled:

    • the tracked-read guard's blind spots (EggUnitConversionRepository, ProductEggGradeMapping);
    • a namespace-keyed farm-directory allowance, one slug-normalization contract, and a separate FarmDirectory type;
    • the unguarded IAccountRepository.FindBySlugAsync cross-farm seam read;
    • seven other guards that parse src without preprocessor symbols (listed on refactor(accounts): move the cross-farm account reads behind IFarmDirectory #1043).
  11. mforce commented on Oct 4, 2026

    @mforce
    OwnerAuthor

    Amendment from #857 E2 (PR #1060)

    PR #1060 implements the Access precondition for the approved P5/P6 plan. Both seeders read real actor DTOs through IAccessSeedLookup, registered only outside Production. Simulation alone creates cast users through IAccessOperations.CreateUserAsync. CurrentUserContext and SimulationOptions move from Identity to Platform's Persistence namespace with their bodies unchanged; the scoped ICurrentUser alias remains the same actor instance. No fixture configuration key changes.

    Remaining here: at E2 head 3731894a, 37 compatibility rows name #858: 29 simulation and 8 demo. The five new Access rows are:

    • Demo MissingBaseDataAsync: AspNetRoles prerequisite (P6).
    • Simulation MissingBaseDataAsync: AspNetRoles prerequisite (P5).
    • Simulation FindOwnerAsync: AspNetRoles prerequisite (P5).
    • Simulation ComputeCountsAsync: AspNetUsers total count (P5).
    • Simulation RestrictOneWorkerAsync: UserRoleAssignments read/write (P5).

    The last write deliberately stays in composition for P5's non-Production Access fixture operation and security review. It retains the original ordered, untracked, tenant-filtered read; entity factory; actual Owner actor and flock-name audit; and one save after that audit. An injected audit failure proves that no assignment or completion marker commits separately. The repository's filtered Flocks LEFT JOIN instead expires at #859.

    Parity evidence: 54/54 cases pass on baseline and E2 across every requested seeder sibling class. Demo's exact 977 action/actor rows and simulation's exact cast, six products, 453 action/actor rows, worker rotation, manifest, anchor, completion and fingerprint match. Full SQL differs only by one explicit account-scoped Owner reread in each profile. E2 does not implement the rest of P4-P8 or claim #858's full harness/Playwright acceptance gates. Those remain with this issue. PR #1060 is open for CI and independent Astra plus Opus security review; this amendment records its implemented scope without claiming it has merged.

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

    area:apiAPI/endpoint layerepic-514Modular monolith architecture (#514)priority:tier4Deferred or speculativesize:LSeveral days; wide blast radius or unresolved scopesliceThin vertical work itemtrack:C#514 Track C — moves production code; UNSCHEDULED, needs authorisation

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions