PowerFill Module — Phase 2 Complete
Date: 2026-04-16
Agent: Systems Architect
Scope: Data access layer over the 17 Phase 1 pfill_* entities, PowerFill-owned projections over 9 upstream tables, a PowerFillPreflightService with 9 composable checks wired behind POST /api/powerfill/preflight, and both legacy views (pfillv_existng_pool_disposition + pfillv_pf_forensics_tradeside) shipped as SQL DDL. Includes a SharedDomain project extraction (Choice A3) that eliminates potential duplicate entity paths between BestEx and PowerFill.
Why
Phase 1 gave PowerFill tables and schema. Phase 2 makes those tables queryable through a SaaS-native read surface and gives tenants a single concrete question they can ask of the module: "Is my tenant ready to run PowerFill right now?"
The preflight service is also the first PowerFill module endpoint — it proves out the module → service → endpoint → DI wiring and the TenantDbContext + multi-tenant plumbing in a read-only context. Everything later phases add (pre-processing in Phase 3, constraint CRUD in Phase 4, the allocation engine in Phase 6) extends this surface rather than replacing it.
What Was Done
Phase 0/1 corrections (landed in this commit per same-commit discipline)
Three Phase-0 Truth Rot findings from the plan's Primary-Source Verification step:
- Two views, not one. The NVO creates
pfillv_pf_forensics_tradeside(NVO line 6706) alongsidepfillv_existng_pool_disposition(line 6730). Phase 0 spec + deep dive only mentioned one. The deep dive now documents both. pscat_securities_sec_rulesis fictional. The NVO usespscat_securitization_rules. Corrected in the deep dive (5 occurrences), the spec (BR-1), the ADR-023 context, and Assumption A4.pssaas-dbwas missing 8 tables PowerFill Phase 2 reads. Extendedinfra/sql/init/seed-schema.sqlwith minimal column sets (only the columns Phase 2 actually touches — full PS_DemoData replication is a separate infra concern).
Shared domain extraction (Choice A3)
New project PowerSeller.SaaS.SharedDomain holding Loan, Instrument, ProfileInstrumentMapping, TodaysPrice plus the two composite-key configurations. Both BestEx and PowerFill reference it. Registration is idempotent (TenantDbContext.RegisterEntityAssembly dedups), so both modules calling RegisterEntityAssembly(typeof(Loan).Assembly) is safe. Zero BestEx test regressions — all 32 still pass with no source edits needed beyond namespace re-import.
PowerFill Phase 2 code
- 9 upstream read-only entities in PowerFill/Domain/Upstream —
Trade,Pool,TradePoolRelation,LoanStage,PairOff,LoanShipped,RmcatLoan,SecuritizationRule,TradeAnalysisHistory. Each is a narrow column projection (not full-table mirror); column sets determined from NVO references + view requirements. - 2 keyless read-models in PowerFill/Domain/ReadModels —
PoolDispositionReadModel,TradeForensicsReadModel. Configured viabuilder.HasNoKey().ToView("..."). - Both legacy views in Sql/002_CreatePowerFillViews.sql — transcribed verbatim from NVO functions
of_get_pfillv_existng_pool_disposition(line 12485) andof_get_pfillv_pf_forensics_tradeside(line 11187).CREATE OR ALTER VIEWfor idempotent deployment. docker-compose.ymlmounts the PowerFillSql/directory into the db container;setup.shloops module subdirectories and applies numeric-prefixed scripts. Phase 1'sValidatePS_DemoDataSchema.sqlrenamed (no prefix) so it's not auto-deployed.PowerFillPreflightService+IPowerFillPreflightCheckinterface + 9 concrete checks:
| Code | Severity | Triggers when |
|---|---|---|
NO_CONSTRAINTS | Error | pfill_constraints is empty |
NO_CARRY_COSTS | Error | pfill_carry_cost is empty |
BESTEX_SETUP_EMPTY | Error | A constraint's investor_instrument_name has no row in rmcat_bx_setup_instr_inv |
PRICES_STALE | Warning | rmcat_todays_prices latest market_date older than max_prices_age_days |
SEC_RULE_MISSING | Error | A pfill_constraint_sec_rule_rel.rule_name doesn't exist in pscat_securitization_rules |
PIPELINE_EMPTY | Error | No loans at or above min_status in pipeline |
NO_OPEN_TRADES | Warning | No open non-phantom pscat_trades with settlement_date within max_trade_settle_days |
LOCKDOWN_ORPHAN_POOL | Warning | A pfill_lockdown_guide.pool_name doesn't exist in pscat_pools |
CARRY_COST_COVERAGE_GAP | Warning | A constraint's investor_instrument_name has no pfill_carry_cost curve |
POST /api/powerfill/preflightendpoint returning the aggregated report.200 OKwithis_ready: falsewhen errors present;400only for malformed requests./powerfill/statusupdated fromphase-1-schema-only→phase-2-preflight-ready.
Tests
- 16 new preflight unit tests covering happy path, sad path, and orchestration for each check. Uses EF Core InMemory.
ViewsIntegrationTests.Views_DeployCleanly_AndAreQueryableFromEfReadModels— transient SQL DB, deploys seed + 001 + 002, re-runs 002 (idempotency), queries both views through EF keyless read-models.- Full suite: 60 passing tests, 0 failures (32 BestEx + 27 PowerFill + 1 Api).
Key Decisions
- Choice A3 (shared domain extraction). Eliminates the "silent parallel code paths" antipattern — four entities shared by both modules now live in one place. Costs one project + a module-registration line per consumer. Pays back as more modules come online.
- Choice C1 (SQL views). Shipped both legacy views as SQL DDL rather than LINQ projections. The disposition view's 6-level CTE +
ROW_NUMBER() OVER PARTITION BY+UNION ALLsemantics are not something I want to re-derive in LINQ, for the same reason ADR-022 rejects rewriting the allocation engine. - Preflight is standalone (B1), not part of a future
RunService. One service per concern matches the BestEx style; Phase 6 can add a thin readiness facade if needed. - Hardcoded preflight defaults for Phase 2.
min_status = "Closed",max_prices_age_days = 2,max_trade_settle_days = 60. Phase 4 moves these to tenant-configurable values. Open Q #4 of the plan flagged these for Kevin/Jay/Greg to calibrate.
Files Produced / Modified
New
- src/backend/PowerSeller.SaaS.SharedDomain/ — project + 4 entities + 1 config file
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Domain/Upstream/ — 9 entities + 1 config file
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Domain/ReadModels/ — 2 read-models + 1 config file
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Contracts/ —
PreflightRequest,PreflightReport,PreflightCheckResult,PreflightSeverity - src/backend/PowerSeller.SaaS.Modules.PowerFill/Services/ —
IPowerFillPreflightCheck,PowerFillPreflightService - src/backend/PowerSeller.SaaS.Modules.PowerFill/Services/Checks/ — 9 check classes
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Endpoints/PreflightEndpoints.cs
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Sql/002_CreatePowerFillViews.sql
- src/backend/tests/PowerSeller.SaaS.Modules.PowerFill.Tests/Preflight/PowerFillPreflightServiceTests.cs — 16 unit tests
- src/backend/tests/PowerSeller.SaaS.Modules.PowerFill.Tests/Sql/ViewsIntegrationTests.cs — view round-trip integration test
Modified
- src/backend/PowerSeller.SaaS.sln — added SharedDomain project
- src/backend/PowerSeller.SaaS.Modules.BestEx — removed 4 entity files (moved to SharedDomain), removed their configurations from
BestExEntityConfiguration.cs, added SharedDomain project reference, addedusing PowerSeller.SaaS.SharedDomain;to 5 files that reference the moved types - src/backend/PowerSeller.SaaS.Modules.PowerFill/PowerFillModule.cs — registered preflight service + 9 checks, registered 11 new entities, registered SharedDomain assembly, mapped preflight endpoint
- src/backend/PowerSeller.SaaS.Modules.PowerFill/Sql/ValidatePS_DemoDataSchema.sql — renamed from
002_*.sql(no numeric prefix so setup.sh skips it) - src/backend/PowerSeller.SaaS.Modules.PowerFill/Sql/README.md — updated references
- src/backend/Dockerfile.dev — COPY SharedDomain csproj
- docker-compose.yml — mount PowerFill
Sql/into db container - infra/sql/init/seed-schema.sql — added
mkt_purchase_date/mkt_shipped_datetoloan; added 8 upstream tables - infra/sql/init/setup.sh — loops module Sql subdirs applying numeric-prefixed scripts
- Phase 0/1 doc corrections: deep dive, spec, ADR-023, assumptions log
- docs-site/docs/handoffs/pssaas-session-handoff.md — Phase 2 summary, backlog updates
What's Next
Phase 3 per the spec: the pre-processing stored procedures, an orchestration service that calls them in the right order, and POST /api/powerfill/preprocess. The procedure set will be finalized via Primary-Source Verification Gate at Phase 3 planning time (procedure names and count will be confirmed against n_cst_powerfill.sru directly, not against this devlog).
Risks Captured
- Minimal seed schema means some Phase 3 work may reveal additional upstream columns needed. Mitigation: extend the seed incrementally; full PS_DemoData fidelity still tracked as a future infra task.
pscat_securitiesandpscat_pools_sec_rule_relare cited in theentity-relationship.mdlegacy docs but not in the Phase 2 scope. Phase 3 or Phase 6 will need them when sec-rule evaluation is implemented.- Hardcoded preflight thresholds are Architect guesses. Kevin/Jay/Greg may have calibrated values. Phase 4 makes them configurable.
- Recurring Phase-0 Truth Rot. Three more findings in Phase 2 planning (after Phase 1's three). The gate is working; the pattern is that Phase 0 has unknown-unknowns we discover only by executing against primary source. Phase 3 planning will continue budget time for verification + corrections.