Skip to main content

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.sqlpfill_preflight_settings + seed row id = 1; PRINT inside create guard.
  • 005_AddPfillConstraintsRowversion.sqlrv_rowversion on pfill_constraints; PRINT inside column-add guard.

Domain / EF / services

  • PowerFillPreflightSettings entity + configuration; PowerFillConstraint.RowVersion mapped with IsRowVersion().
  • PowerFillPreflightDefaultsMerge + PowerFillPreflightService merge 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 when pfill_constraint_sec_rule_rel rows exist (409).
  • PowerFillConstraintsConcurrency — token computation for reprioritize.

HTTP surface

  • ConfigurationEndpoints under /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 by poolName with POST create-only (409 duplicate, PUT 404 if missing).

Tests

  • SchemaScriptIntegrationTests and ProceduresIntegrationTests apply 004+005 after 001 (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.mdA33 + 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:

  1. pfill_constraints.rv_rowversion was modeled as byte[]? but ROWVERSION is implicitly NOT NULLSchemaScriptIntegrationTests flagged it. Fixed: byte[] (non-nullable, default Array.Empty<byte>()).
  2. CreateConstraintAsync and UpdateConstraintAsync called entry.ReloadAsync() to refresh the database-generated RowVersion. EF rejects Reload() on entities with composite keys (key columns can't be re-set on a tracked entity). Fixed: removed the Reload() call — EF writes the generated ROWVERSION back into the property automatically on INSERT/UPDATE.
  3. The seed-schema integration test resolver walked AppContext.BaseDirectory upward looking for infra/sql/init/seed-schema.sql, but that path doesn't exist inside the pssaas-api container (only src/backend is bind-mounted). Fixed: added PFILL_TEST_REPO_ROOT env override on both resolvers (ViewsIntegrationTests, ProceduresIntegrationTests); the host-side parent walk still works when running from the host.
  4. lock_pool was accepted as any free-form string by Create/Update while the list filter and bulk handler required lowercase 'y'/'n'. Fixed: NormalizeLockPoolFlag accepts y/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 in PowerFillConfigurationServiceTests.

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/preflight always returns a value with a source field (tenant/defaults/builtin); degrades gracefully when 004 not deployed instead of 404.
  • PowerFillPreflightService catches SqlException 208 (invalid object name) and falls back to built-in defaults so preflight still works on un-migrated tenant DBs.
  • PreflightSettingsResponse now exposes updated_at_utc (was schema-only).
  • F3 race-condition between DeleteConstraintAsync and AddSecRuleAsync acknowledged inline + in assumptions A33 with Phase-6 disposition.
  • Spec data-model section now documents pfill_preflight_settings and pfill_constraints.rv_rowversion as 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.