Repository navigation
[C] #514 slice 16: Platform composition, jobs, CLI and seeder conversion #858
Description
Activity
- addedsliceThin vertical work itemThin vertical work itemarea:apiAPI/endpoint layerAPI/endpoint layerpriority:tier4Deferred or speculativeDeferred or speculativesize:LSeveral days; wide blast radius or unresolved scopeSeveral days; wide blast radius or unresolved scopeepic-514Modular monolith architecture (#514)Modular monolith architecture (#514)track:C#514 Track C — moves production code; UNSCHEDULED, needs authorisation#514 Track C — moves production code; UNSCHEDULED, needs authorisation
on Sep 14, 2026 - 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 Audit 2026-09-26, against
mainat e47288cBody left as written.
Refreshed numbers.
File In the issue Today Hosting/CluckworkFeatureServiceCollectionExtensions.cs352 352 Program.cs577 607 SimulationDataSeeder.cs2608 2643 Correction: that growth is not evidence for this slice.
Program.csmoved 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
BusinessRecordModelitem 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 splittingChronologicalListTypesandMappedExclusionsinto per-module contributions merged beforeValidateCensusruns, keeping the walk and the validation centralized. Remove it from this slice's scope and point at the new issue.Amendment from #850 (PR #1013)
What shipped.
module-ledger.jsonnow has acompatibilityExceptionssection.CompatibilityExceptionRealTreeTestsfails 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 ownerPlatform:Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EnsureExpenseCategoryAsynclooks updb.ExpenseCategoriesby name;Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.EnsureExpenseAsyncchecksdb.Expensesfor the fixture description;Cluckwork.Infrastructure.Persistence.SimulationDataSeeder.ComputeCountsAsynccounts 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.
IFinanceModuledeliberately has no count or existence read yet; #850 did not widen it. The rule is indocs/decisions/850-compatibility-exceptions.md.- added a commit that references this issue
on Oct 2, 2026 Amendment from #852 (PR #1021). That PR ships part of this issue's scope. The body above is left as written.
Shipped:
DemoDataSeederandSimulationDataSeederno longer inject the flock lifecycle handlers orIFlockRepository. They callIFlockModuleandIFlockLookup.SimulationDataSeeder.EnsureFlockAsyncresolves 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 areDemoDataSeeder.CleanupPartialSeedAsync,.SeedAsyncand.SeedDemoAsync, andSimulationDataSeeder.ComputeCountsAsyncand.SeedExplicitBirdMovementsAsync.Amendment from #855 (PR #1027)
What changes for this slice once #1027 merges. General Inventory is contracted (
IInventoryModule), so seven moreSimulationDataSeederreads are registered compatibility exceptions withdeleteWhen#858, owner Platform,reaches: GeneralInventory:ComputeCountsAsync(manifest counts of all five Inventory tables)EnsureAdjustmentAsync,SeedFeedAdjustmentsAsync(InventoryMovementsexistence and count)EnsureInventoryItemAsync(InventoryItemsby name)EnsureOpeningPurchaseAsync(InventoryLotsexistence and id)SeedFeedUsageAsync(FeedUsagesexistence)SeedWaterUsageAsync(WaterUsagesexistence)
IInventoryModulehas 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 throughIInventoryModule. Its one formerIInventoryItemRepository.HasLotsAsynccall is now the identicaldb.InventoryLots.AnyAsyncinsideEnsureOpeningPurchaseAsync, covered by that method's row.- added a commit that references this issue
on Oct 2, 2026 Amendment from #853 (PR #1028). The body above is left as written.
Shipped:
DailyEntryLockSweeplocks each account's due entries throughIEggOperationsModule.LockSubmittedEntriesAsync. The lock loop moved, unchanged, intoLockDueDailyEntriesHandler. Scope item 2's lock-sweep half is done, apart fromDailyEntryLockSweep.RunAsync's cross-farmdb.Accountsread, which stays an existing [C] #514 slice 8: close or date every compatibility exception #850 row.DemoDataSeederandSimulationDataSeedercallIEggOperationsModuleinstead of the daily-entry handlers and the grade and daily-entry repositories.
Remains here. Five new #850
compatibilityExceptionsrows, all owner Platform withdeleteWhen#858:DemoDataSeeder.CleanupPartialSeedAsync, which deletes a partial demo seed'sDailyEntries,DailyEntryGrades,EggInventoryMovementsandEggLots;DemoDataSeeder.MissingBaseDataAsync, which readsEggGrades;SimulationDataSeeder.ComputeCountsAsync, which countsDailyEntriesandEggLots;SimulationDataSeeder.MissingBaseDataAsync, which readsEggGrades;SimulationDataSeeder.SeedFlockHistoryAsync, the natural-key existence check. It becamedb.DailyEntries.AnyAsyncwith the same predicate, so the contract does not grow a read only the seeder needs.
Amendment from #854 (PR #1030). The body above is left as written.
Shipped:
DemoDataSeederandSimulationDataSeedercallICommerceModuleinstead of the six Commerce handlers (create product, customer and order, add a line, confirm, record a payment).Remains here. Eight new #850
compatibilityExceptionsrows, all owner Platform withdeleteWhen#858:DemoDataSeeder.CleanupPartialSeedAsync, which deletes a partial demo seed'sSalesOrderItems,SalesOrdersandCustomers;SimulationDataSeeder.ComputeCountsAsync, which countsCustomers,SalesOrdersby status andPayments;SimulationDataSeeder.EnsureConfirmedOrderAsync, which reads the order's status before confirming;SimulationDataSeeder.EnsureCustomerAsyncandEnsureProductAsync, the lookups by name;SimulationDataSeeder.EnsureExtraLineAsync, which checksSalesOrderItemsfor the product's line;SimulationDataSeeder.EnsurePartialPaymentAsync, which checksPaymentsand 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.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."
IUnitOfWorkstays. 49 handlers andExportEndpointscall 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 itIModuleTransaction; 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 keepsAddCluckworkIdentity. The sevenICurrencyBoundRowSourceregistrations 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.
CreateUserAsyncstays with E'sIAccessOperations.
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; keepIFlockRepository.Updateand the live assignment and logoRemoveafter P1 P3 Scoped IFarmDirectoryfor the cross-farmAccountsreads inlist-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, deliberateLoosenablepruning, decision record,src/AGENTS.md. Also delete theAccountSlugLookup.ResolveAsyncforwarder, inliningGetRequiredService<IFarmDirectory>().FindIdBySlugAsyncinto the four verbs (#1043 review, Opus F1), once #857 D has moved themlast; 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.ResolveAsyncforwarder 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
FarmDirectorytype; - the unguarded
IAccountRepository.FindBySlugAsynccross-farm seam read; - seven other guards that parse
srcwithout preprocessor symbols (listed on refactor(accounts): move the cross-farm account reads behind IFarmDirectory #1043).
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 throughIAccessOperations.CreateUserAsync.CurrentUserContextandSimulationOptionsmove from Identity to Platform's Persistence namespace with their bodies unchanged; the scopedICurrentUseralias 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:AspNetRolesprerequisite (P6). - Simulation
MissingBaseDataAsync:AspNetRolesprerequisite (P5). - Simulation
FindOwnerAsync:AspNetRolesprerequisite (P5). - Simulation
ComputeCountsAsync:AspNetUserstotal count (P5). - Simulation
RestrictOneWorkerAsync:UserRoleAssignmentsread/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.
- Demo
- added 2 commits that reference this issue
on Oct 4, 2026
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:
CluckworkFeatureServiceCollectionExtensions.csProgram.csSimulationDataSeeder.csBusinessRecordModel.cs:21-34If a new domain is ever added — raising, processing, anything — this is the work that decides whether
that is pleasant or painful. The
BusinessRecordModelitem in particular is independent of therest of Track C and could be done standalone at any time.
Scope
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).
unchanged (Background worker has no single-runner guarantee — double-runs if scaled >1 instance #271).
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).
migratestays a direct Platform migration operation (Deploy: separate the migration/owner DB role from the runtime app role (no DDL at request-time) #263).no caller remains.
Done when
Full solution green; frontend typecheck, tests and build green;
tools/simulation/verify-harness.shpasses; 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.