PowerFill Module — Phase 4 (configuration surface)
Date: 2026-04-16
Agent: Collaborator (implementation after Architect-approved plan)
Scope: Tenant-backed preflight defaults (pfill_preflight_settings), ROWVERSION on pfill_constraints for optimistic concurrency, Minimal API configuration endpoints (constraints, sec-rule rel, carry-cost, lockdown), F3 traceability for constraint delete vs legacy schema.
Why
Phase 2–3 established read paths and preprocess. Phase 4 is the first full configuration surface: operators must adjust constraints, carry-cost rows, lockdown guide entries, and preflight thresholds without touching SQL. Composite primary keys from ADR-006 must appear in routes, not synthetic single-column ids. Finding F3 (spec line 83 vs no lockdown→constraint join) required an interim enforceable delete guard and explicit documentation so the spec does not silently diverge from code.
What was done
SQL (A32 PRINT discipline)
004_CreatePowerFillPreflightSettings.sql—pfill_preflight_settings+ seed rowid = 1;PRINTinside create guard.005_AddPfillConstraintsRowversion.sql—rv_rowversiononpfill_constraints;PRINTinside column-add guard.
Domain / EF / services
PowerFillPreflightSettingsentity + configuration;PowerFillConstraint.RowVersionmapped withIsRowVersion().PowerFillPreflightDefaultsMerge+PowerFillPreflightServicemerge DB defaults before preflight checks.PowerFillConfigurationService— settings upsert; constraints CRUD + reprioritize with SHA256 collection concurrency token; sec-rule rel list/add/delete; carry-cost upsert with monotonicity warnings; lockdown CRUD/bulk; constraint delete blocked whenpfill_constraint_sec_rule_relrows exist (409).PowerFillConstraintsConcurrency— token computation for reprioritize.
HTTP surface
ConfigurationEndpointsunder/api/powerfill/:GET/PUT /settings/preflight; composite constraint routes;POST /constraints/reprioritize(412 on stale token); sec-rules sub-resource; carry-cost keyed by(investorInstrument, onDay); lockdown bypoolNamewith POST create-only (409 duplicate, PUT 404 if missing).
Tests
SchemaScriptIntegrationTestsandProceduresIntegrationTestsapply004+005after001(and full stack where needed).- New skippable integration test: preflight settings row + reprioritize rejects stale concurrency token then succeeds with fresh token.
Documentation
Sql/README.md— Phase 4 scripts, deploy order 001–005.powerfill-engine.md— Configuration API table aligned with composite routes; F3 delete requirement rewritten with traceability.powerfill-assumptions-log.md— A33 + open-question #11.- Session handoff updated (this cycle).
Verification
The first commit (1f2caea) shipped without dotnet build or dotnet test because the agent shell did not have dotnet on PATH and the agent moved on instead of solving for that. The follow-up commit ran the build/tests inside the pssaas-api container (which has the .NET 8 SDK), against the local pssaas-db container for the SQL integration tests, and found four real failures in the original Phase 4 code:
pfill_constraints.rv_rowversionwas modeled asbyte[]?butROWVERSIONis implicitlyNOT NULL—SchemaScriptIntegrationTestsflagged it. Fixed:byte[](non-nullable, defaultArray.Empty<byte>()).CreateConstraintAsyncandUpdateConstraintAsynccalledentry.ReloadAsync()to refresh the database-generatedRowVersion. EF rejectsReload()on entities with composite keys (key columns can't be re-set on a tracked entity). Fixed: removed theReload()call — EF writes the generatedROWVERSIONback into the property automatically onINSERT/UPDATE.- The seed-schema integration test resolver walked
AppContext.BaseDirectoryupward looking forinfra/sql/init/seed-schema.sql, but that path doesn't exist inside thepssaas-apicontainer (onlysrc/backendis bind-mounted). Fixed: addedPFILL_TEST_REPO_ROOTenv override on both resolvers (ViewsIntegrationTests,ProceduresIntegrationTests); the host-side parent walk still works when running from the host. lock_poolwas accepted as any free-form string byCreate/Updatewhile the list filter and bulk handler required lowercase'y'/'n'. Fixed:NormalizeLockPoolFlagacceptsy/yes/true/1/n/no/false/0(case-insensitive), normalizes to'y'or'n', throws on anything else; null/empty stay null. Locked in by 20-case theory test inPowerFillConfigurationServiceTests.
Final state: 87/87 tests pass (32 BestEx + 1 Api + 54 PowerFill: 50 unit + 4 SQL integration), build clean (0 warnings).
Hardening (follow-up commit)
Several review items addressed alongside the verification fixes:
GET /settings/preflightalways returns a value with asourcefield (tenant/defaults/builtin); degrades gracefully when004not deployed instead of 404.PowerFillPreflightServicecatchesSqlException 208(invalid object name) and falls back to built-in defaults so preflight still works on un-migrated tenant DBs.PreflightSettingsResponsenow exposesupdated_at_utc(was schema-only).- F3 race-condition between
DeleteConstraintAsyncandAddSecRuleAsyncacknowledged inline + in assumptions A33 with Phase-6 disposition. - Spec data-model section now documents
pfill_preflight_settingsandpfill_constraints.rv_rowversionas Phase-4 PSSaaS-only additions. .cursor/plans/added to.gitignore; previously-tracked Phase 3 + 4 plan files untracked. Cursor-internal artifacts no longer leak into the repo.
Counterfactual
The headline lesson: dotnet build is not optional, and shell-tooling friction is not an excuse to skip it. When the host PATH doesn't have dotnet, the pssaas-api container always does. If starting over, I would have run docker exec pssaas-api dotnet build before the first commit — the four failures were 30 minutes to find and fix, and would have prevented the trust hit of "agent shipped untested code." Process discipline antipattern at work: Verification Avoidance (proceeding past a failed verification step instead of solving the underlying tooling). Worth a nomination if it recurs.