Skip to main content

PowerFill Phase 7 — Completion Report

Author: PSSaaS Systems Architect Date: 2026-04-19 Status: Code complete; deployed to local pssaas-db AND PS_DemoData; sentinel phase-7-reports-ready ✓; 233/233 tests pass (32 BestEx + 200 PowerFill + 1 Api + 6 skipped); 8 GET endpoints exercised end-to-end against PS_DemoData; pending Collaborator review and PO push. Sentinel: phase-7-reports-readyCompanion docs:


TL;DR

Phase 7 (Reports / Recap Query APIs) ships 8 new GET endpoints under /api/powerfill/runs/{run_id}/<report-name> (where <report-name> is one of the 8 routes below) for the 7 canonical PowerFill reports (Guide, Recap, Switching, Pool Candidates, Existing Disposition, Pooling Guide, Cash Trade Slotting) plus the Kickouts surface (per F-7-2 — spec line 363 enumerates this 8th endpoint).

The architectural centerpiece is a single PowerFillReportService umbrella service (per ADR-025 §A7.2 Option A) with latest-Complete-wins semantics for the {run_id} URL parameter (per ADR-025 §A7.1 Option B, A60). Cursor pagination on the 4 unbounded reports uses keyset on natural composite PK (per ADR-025 §A7.3 Option A).

No new SQL artifacts — Phase 7 is read-only over the existing 23-table schema + Phase 2's existing pfillv_existng_pool_disposition view + one new PSSaaS-side upstream EF entity (PscatTradeCashGrid, per A61, type-safe over the legacy pscat_trade_cash_grid table).

The Phase 7 PoC against PS_DemoData demonstrates all 8 endpoints + the 4 freshness verdicts:

  • 404 Not Found on bogus run_id ✓
  • 410 Gone on superseded historical runs (with latest run id in body Note for client redirection) ✓
  • 200 + TerminalEmpty Note on the latest-but-Failed run (BR-9 cleared the user-facing tables; failure_step + failure_message in Note) ✓
  • F-7-8 NEW: Cash Trade Slotting returns 688 real rows on PS_DemoData even on the Failed run because pfill_cash_market_map is NOT BR-9-cleared per A58 — empirical confirmation that A58's preservation scope is observable from the Phase 7 read APIs ✓

Per A54+A56 (Phase 9 carry-over) end-to-end Complete-run real-data exercise on the BR-9-cleared reports remains deferred — Phase 7's contract is the deliverable; the empty-with-Note responses on PS_DemoData are documented expected behavior.

4 new assumption-log entries (A59-A62):

  • A59 — Phase 7 kickoff cited NVO 6730-6753 as view definition source; that's the deploy block, not the body (NVO 12485 is the body). First net-new finding against the kickoff in 3 sub-phases — qualifies A57's pattern observation but does not reverse it.
  • A60 — Per-run vs current-state semantics decided as latest-Complete-wins (ADR-025).
  • A61PscatTradeCashGrid PSSaaS-side EF entity added.
  • A62 — PS_DemoData has the legacy WITH ENCRYPTION version of pfillv_existng_pool_disposition view (pre-dates the note_rate column); Phase 7 service catches SqlException 207/208 and degrades gracefully.

Sub-phase calendar time: ~1 Architect-session. Consistent with 6a-6e velocity. Banking the pattern: subagent dispatch for DTOs + tests, self-implement the architectural decisions + service, ~1 session per major phase even at Phase 7's 8-endpoint scale.


What was produced

New files

  • src/backend/PowerSeller.SaaS.Modules.PowerFill/Contracts/ReportContracts.cs (456 lines, 19 types) — 8 row records + 3 cursor records + 8 response wrappers; every JSON property snake_case via explicit [JsonPropertyName]. Reuses the existing RunStatus enum from RunContracts.cs. Generated by Subagent 1 (clean first-attempt; 0 build warnings).
  • src/backend/PowerSeller.SaaS.Modules.PowerFill/Services/PowerFillReportService.cs (~600 lines) — the umbrella service per ADR-025 §A7.2 Option A. 8 public methods (one per report) + private ResolveRunContextAsync helper that drives the freshness verdict for every endpoint. Self-implemented (architecturally novel; first PSSaaS service to encode A60 / ADR-025 semantics).
  • src/backend/PowerSeller.SaaS.Modules.PowerFill/Domain/Upstream/PscatTradeCashGrid.cs (~45 lines) — PowerFill-owned upstream read projection over pscat_trade_cash_grid per A61. Composite PK (trade_id, rate, syntax_name). Type-safe over the legacy table that 003+006+008+011 procedures reference. Not actually JOINed by the Phase 7 service yet (per A61 + A12: PS_DemoData runs skip Step 1 psp_pfill_bx_cash_grids because bx_price_floor is null; pscat_trade_cash_grid stays empty there).
  • src/backend/tests/PowerSeller.SaaS.Modules.PowerFill.Tests/Services/PowerFillReportServiceTests.cs (~700 lines, 27 tests) — 7 freshness/run-id-resolution tests + 10 per-report happy-path tests + 5 pagination tests + 3 edge-case tests + 2 tenant-scoping tests. Generated by Subagent 2 (one iteration: cursor-emission contract clarification on Recap pagination test). All 27 pass against InMemory.
  • docs-site/docs/adr/adr-025-powerfill-report-api-pattern.md — full ADR documenting A7.1 / A7.2 / A7.3 decisions; precedent-setting for Phase 8+ report endpoints. ADR index updated.
  • docs-site/docs/handoffs/powerfill-phase-7-completion.md — this completion report.
  • docs-site/docs/devlog/2026-04-19d-powerfill-phase-7.md — devlog entry.

Modified files

  • src/backend/PowerSeller.SaaS.Modules.PowerFill/Endpoints/RunEndpoints.cs — added 8 new GET endpoints (one per report) inside MapPowerFillRunEndpoints; added private static BuildReportResult<T>(Guid runId, T? body) helper that maps service result to HTTP status (404 on null; 410 Gone when Note carries Stale prefix; 200 OK otherwise). Class-level XML doc updated to enumerate Phase 7 endpoints.
  • src/backend/PowerSeller.SaaS.Modules.PowerFill/Domain/Upstream/UpstreamEntityConfiguration.cs — added PscatTradeCashGridConfiguration (composite PK).
  • src/backend/PowerSeller.SaaS.Modules.PowerFill/PowerFillModule.cs — registered PowerFillReportService (scoped); registered PscatTradeCashGrid entity in RegisterEntities; bumped sentinel from phase-6e-async-runs-ready to phase-7-reports-ready.
  • docs-site/docs/specs/powerfill-engine.md — major amendment to §Output APIs (promoted from placeholder rows to canonical contract specs with response shapes + freshness-verdict table + per-endpoint PoC behavior table); two § Post-Allocation (UE Pass) checklist items marked Phase 7 done; Phased Implementation table marks Phase 7 DONE with calendar-time observation.
  • docs-site/docs/specs/powerfill-assumptions-log.md — A59 (kickoff specificity mis-cite per F-7-1), A60 (latest-Complete-wins semantics per A7.1), A61 (PscatTradeCashGrid PSSaaS EF entity per F-7-5), A62 (PS_DemoData view drift per F-7-7) all added in canonical house style.
  • docs-site/docs/arc42/09-architecture-decisions.md — ADR-025 row added.

Out of scope (deliberately not produced)

  • No SQL deploys — Phase 7 is read-only over the existing 23-table schema + Phase 2's existing view. The new PscatTradeCashGrid entity overlays an existing legacy table; no DDL needed.
  • No per-run snapshot tables — Q3 Option C (pfill_run_history_loans / pfill_run_history_trades) explicitly Phase 7+ per spec line 263.
  • No A54 or A62 fixes — both deferred per ADR-021 verbatim discipline + Backlog #24 disposition.
  • No new ADRs beyond ADR-025.
  • No React UI (Phase 8) — Phase 7 ships JSON; Phase 8 ships dashboards.
  • No Superset SQL (Phase 8) — Phase 8 builds the Superset queries that consume Phase 7 endpoints.

Decisions made

#DecisionRationaleWhere
D1Single PowerFillReportService umbrella service (ADR-025 §A7.2 Option A)Matches existing PSSaaS service pattern (PowerFillRunHistoryService, PowerFillConfigurationService, PowerFillPreflightService); centralizes A7.1 freshness logic; single DI registration; consistent telemetry.ADR-025; PowerFillReportService.cs
D2Latest-Complete-wins semantics for {run_id} URL parameter (ADR-025 §A7.1 Option B / A60)Honest about how the data is stored at v1 (single-run tables; BR-9/BR-10 lifecycle). 410 Gone with latest run id in body Note enables Phase 8 "you're viewing stale data" UX. Future Phase 7+ snapshot replay (Q3 Option C) doesn't break the contract.ADR-025; A60; service.ResolveRunContextAsync
D38th endpoint added: Kickouts (per F-7-2 disposition)Spec line 363 enumerates /runs/{run_id}/kickouts as a real endpoint; kickout report is small + obvious + has its entity (PowerFillKickoutGuide01). Marginal cost = one method + one DTO.F-7-2; service.GetKickoutsAsync
D4Cursor pagination via keyset on natural composite PK (ADR-025 §A7.3 Option A)Stable across re-queries; matches Phase 6e's existing ?before= pattern. Per-column query params for multi-column PKs are explicit + type-safe. Per-report cursor record types (RecapCursor, PoolCandidateCursor, KickoutCursor) carry the PK shape clearly.ADR-025; ReportContracts.cs cursor records
D5Pagination on 4 reports only (Guide, Recap, Pool Candidates, Kickouts); 4 small-volume reports return all rowsSwitching, Existing Disposition, Pooling Guide, Cash Trade Slotting typically have small row counts. Premature optimization to add cursor params to all 8. Phase 7+ adds pagination if a customer DB shows large volumes.Endpoints; service contract
D6Note field convention: dual-source pipe-delimited explanationsBoth freshness verdict (Stale, TerminalEmpty) AND per-report semantic clarifications can compose in one field (e.g. Cash Trade Slotting on Failed run: TerminalEmpty Note + per-report Note joined by &#124;). Avoids expanding the wire shape with multiple Note fields.service per-report Note logic
D7SqlException 207/208 catch-and-degrade in GetExistingDispositionAsync (per F-7-7 / A62)PS_DemoData has the legacy WITH ENCRYPTION version of the view (pre-dates note_rate column). Catch-and-degrade returns empty payload + explanatory Note rather than 500. Keeps Phase 7 unblocked without forcing the deferred Backlog #24 deploy decision.service.GetExistingDispositionAsync; A62
D8Cancel-vs-Stale prefix detection in BuildReportResult helperThe endpoint helper inspects the response Note for the Stale-format prefix to emit 410 Gone vs 200 OK. Avoids reflecting on the response object or threading a separate freshness enum through every endpoint signature. The Note format is owned by PowerFillReportService.StaleNoteFormat.RunEndpoints.cs.BuildReportResult
D9PscatTradeCashGrid lives in Domain.Upstream namespace (per A61)Mirrors existing pattern for PowerFill-owned upstream projections (Trade, Pool, TradePoolRelation, etc.). EntityConfigurationTests scopes to Domain namespace and correctly does NOT pick up upstream entities — no test extension needed.A61; UpstreamEntityConfiguration.cs
D10No mid-phase Reviewable Chunks checkpointMirrors 6c/6d/6e's pattern (Architect ships full deliverable + completion report as consolidated review surface). A7.1/A7.2/A7.3 decisions stated upfront in the inline plan; once approved, the implementation was mechanical. Phase 6 retro confirms this pattern works.Plan §5; matches 6e D6 precedent

Migrations enumerated

This phase ships 0 new schema migrations. Phase 7 is read-only over:

  • The existing 23-table PowerFill schema (last extended by Phase 6e's 012_CreatePfillRunHistoryTable.sql)
  • Phase 2's existing pfillv_existng_pool_disposition + pfillv_pf_forensics_tradeside views (002_CreatePowerFillViews.sql)
  • The legacy pscat_trade_cash_grid upstream table (DDL exists in infra/sql/init/seed-schema.sql lines 449-456 for local pssaas-db; exists on customer DBs as part of the legacy schema)

PowerFill-owned table count: 23 (unchanged from Phase 6e).


Gate findings

Three-layer Primary-Source Verification Gate

IDLayerFindingDisposition
F-7-1NVO-vs-doc (kickoff specificity)Kickoff prompt cites "NVO 6730-6753" as pfillv_existng_pool_disposition view definition. Verified: NVO 6730-6753 is the deploy/registration block; the actual view BODY is at NVO 12485.(b) Scope-changed — Phase 7 reads the existing view via PoolDispositionReadModel; no new SQL artifact. First net-new mis-cite against the kickoff in 3 consecutive sub-phases — qualifies A57's pattern observation but does not reverse it. Documented as A59.
F-7-2Spec-vs-implementationSpec §Output APIs (line 363) lists /runs/{run_id}/kickouts as 8th endpoint. Kickoff prompt enumerated only 7 reports.(b) Scope-changed — added Kickouts as 8th endpoint. Marginal cost (one method + one DTO + one cursor record + one test).
F-7-3NVO-vs-implementationSpec line 226 says "Build pfillv_existng_pool_disposition view or equivalent query — Phase 7." Phase 2 already shipped the view. The Phase 7 task reduces to: surface it via an HTTP endpoint that scopes/filters appropriately.(a) Re-verified — view exists; Phase 7 only needs the read endpoint. Spec amended to mark this checklist item Phase 7 done.
F-7-4Spec-vs-doc (BR-9 + A58)Phase 7 endpoints reading the 7 BR-9-cleared user-facing tables MUST return empty on Failed/Cancelled runs. Endpoints reading PRESERVED tables (4 syn-trades + log per A58, cash_market_map, view) MAY return data on Failed runs.(a) Documented per endpoint in spec amendment + service per-report Note logic. PoC F-7-8 empirically validates: Cash Trade Slotting returns 688 real rows on a Failed run.
F-7-5NVO-vs-tenant-DBpscat_trade_cash_grid is referenced by 003+006+008+011 procedures but no PSSaaS EF entity exists for it.(b) Scope-changed — added as new PowerFill-owned Upstream entity (per A61). Composite PK (trade_id, rate, syntax_name) from local seed-schema. Not actually JOINed by Phase 7 service yet (PS_DemoData runs skip Step 1 per A12).
F-7-6Implementation-vs-runtimeSpec is silent on per-run vs current-state semantics.(b) Scope-changed — A7.1 Option B chosen; documented as A60 + ADR-025.
F-7-7NVO-vs-tenant-DB (NEW from PoC)GET /runs/{run_id}/existing-disposition against PS_DemoData throws SqlException 207: Invalid column name 'note_rate'. PS_DemoData has the legacy WITH ENCRYPTION version of the view that pre-dates the note_rate addition; PSSaaS-side 002_CreatePowerFillViews.sql includes it.(a) Corrected in place — service catches SqlException 207/208 and degrades to empty payload + explanatory Note. Documented as A62. Backlog #24 (deferred deploy of 002 to PS_DemoData) closes this in Phase 9.
F-7-8Implementation-vs-runtime (NEW from PoC)GET /runs/{run_id}/cash-trade-slotting against the latest Failed run on PS_DemoData returns 688 real rows. pfill_cash_market_map is NOT in the BR-9-cleared list per A58 — its content survives Failed runs. Empirical validation that A58's preservation scope is observable from the Phase 7 read APIs.(a) Confirmed expected — A58's design intent (forensic preservation of non-user-facing tables) materializes correctly in the Phase 7 read surface. Per-report Note clarifies: "Cash market map is populated (survives BR-9 cleanup per A58)..."

Pattern observation: A57 corroboration breaks at 3 sessions. A57's "kickoff specificity reduces Truth Rot" had 2-session corroboration (6d + 6e = 0 net-new findings against the kickoff). A59 (= F-7-1) is the first net-new finding against the kickoff in the Phase 6e → Phase 7 arc — one mis-cite out of ~12 NVO/spec-line citations in the Phase 7 prompt. The pattern observation persists with the qualifier: specificity at the line-number level remains a Gate-quality signal but does not eliminate Truth Rot probability; the Gate's NVO-vs-doc layer remains a load-bearing check even with high-specificity kickoffs.

Alternatives-First Gate

Three structural decisions documented in inline plan §3 + ADR-025:

  • A7.1 — {run_id} semantic: chose B (latest-Complete-wins) over A (strict per-run / 410 for everything) and C (snapshot replay — Phase 7+ deferred). Rationale + full Pros/Cons in ADR-025.
  • A7.2 — Repository pattern: chose A (single PowerFillReportService umbrella) over B (per-report repositories) and C (hybrid resolver + 8 query services). Rationale: matches existing PSSaaS service pattern; centralizes freshness logic.
  • A7.3 — Pagination cursor format: chose A (keyset on natural composite PK) over B (offset) and C (opaque token). Rationale: matches Phase 6e's ?before= pattern; type-safe per-report cursor records.

Required Delegation Categories

Delegated:

Subagent 1 — DTO contracts (ReportContracts.cs)
Category match: Templated entity scaffolding > 3 entities
Rationale: 19 types (8 row records + 3 cursor records + 8 response
wrappers); clear pattern (RunContracts.cs lines 410-521); mechanical
work; saves Architect context for the design decisions. Subagent
succeeded clean first-attempt; 0 build warnings.

Subagent 2 — PowerFillReportServiceTests.cs
Category match: Boilerplate tests > 5
Rationale: ~27 net-new tests organized into 5 categories (freshness,
per-report happy paths, pagination, edge cases, tenant scoping).
Pattern is PowerFillRunHistoryServiceTests.cs (InMemory provider,
fixed run_id setup, AAA assertions). Subagent succeeded with one
small iteration on cursor-emission contract; all 27 tests pass.

Self-implemented with Deliberate Non-Delegation:

Deliberate Non-Delegation: not a category match
Task: PowerFillReportService.cs (~600 LOC; 8 service methods + 1
private freshness-resolver helper)
Reason for self-implementation: Architecturally novel work — first PSSaaS
service to encode the latest-Complete-wins semantics + the freshness-
verdict pattern. The A7.1 logic is the load-bearing seam; if delegated,
the subagent would need the entire ADR-025 design context inline.
Per architect-context.md: "Cross-module, cross-domain, or novel
implementation → Self." The PoC iterations (D6 Note dual-source format;
D7 SqlException catch-and-degrade for F-7-7) were live observations
that a delegated subagent without PoC access couldn't have made.

Deliberate Non-Delegation: Templated entity scaffolding ≤ 3 (does NOT
match — only 1 entity)
Task: PscatTradeCashGrid upstream entity + composite-key configuration
Reason: Single entity below the threshold; tied to F-7-5 decision context
(A61 disposition).

Deliberate Non-Delegation: not a category match
Task: RunEndpoints.cs extensions (8 new GET endpoints + BuildReportResult
helper)
Reason: Originally planned as Subagent 2 dispatch but the BuildReportResult
helper logic (D8 Stale-prefix detection for 410 routing) emerged as
load-bearing during service implementation. Self-implementing kept the
routing decision in Architect context; the 8 endpoint registrations
themselves are mechanical but the helper is architectural. Net result:
~200 LOC self-implemented; cleaner than splitting between Architect
(helper) + subagent (registrations).

Deliberate Non-Delegation: not a category match
Task: ADR-025, spec amendments, A59-A62 assumption log entries,
completion report, devlog
Reason: Documentation work — Architect-owned per architect-context.md.

Two delegations + rest self-implemented. Subagent batches both delivered clean (one minor iteration on Subagent 2's pagination test).

Reviewable Chunks at sub-phase scope

Considered the kickoff's hint "Consider checkpointing after the first 1-2 endpoints land to validate the pattern before mass-producing the rest." Decision: NO checkpoint (mirrors 6c/6d/6e's pattern). Rationale documented in plan §5: A7.1/A7.2/A7.3 stated upfront; once approved (by Architect's decision authority on each), implementation is mechanical; ADR-025 + this completion report serve as consolidated post-implementation review surface. Phase 6 retro confirms this pattern works at sub-phase scale.

Deploy Verification Gate

ArmDescriptionEvidence
(a) Sentinel signal/api/powerfill/status returns phase-7-reports-readycurl -s http://localhost:8080/api/powerfill/status{"module":"PowerFill","status":"phase-7-reports-ready"} ✓ (post-restart)
(b) Live API endpoints — TerminalEmpty pathAll 8 endpoints respond against the latest Failed run on PS_DemoData (run 769245cf-d317-411f-87aa-1d4345fb8489); 7 return empty + explanatory Note; 1 (Cash Trade Slotting) returns 688 real rows + composite Note per A58/A12See §"PoC verification commands and outputs" below — full curl outputs per endpoint
(b) Live API endpoints — Stale pathOlder runs (e.g. 909d7f16-...) return 410 Gone + Note containing latest run id for client redirectionVerified — see §PoC for the Guide endpoint (Stale curl; HTTP 410)
(b) Live API endpoints — RunNotFound pathBogus run_id (00000000-...) returns 404 Not FoundVerified — see §PoC
(b) Live API tests27 net-new unit tests cover freshness verdicts + happy paths + pagination + tenant scoping; pre-existing 173 PowerFill tests still passdotnet test: 200 passed, 6 skipped, 0 failed in PowerFill.Tests; 32 BestEx + 1 Api also pass; 233 total passing
(c) Live DB probeNo new SQL deploys this phase (Phase 7 is read-only over existing 23-table schema + existing views + new upstream EF entity over existing legacy table).N/A — confirmed Phase 7 produces no *.sql deliverables

Counterfactual Retro

Knowing what I know now, what would I do differently?

  1. F-7-7 (PS_DemoData view drift) was discoverable at planning time. Backlog #24 has been open since 2026-04-16 explicitly noting "Both expected views exist in PS_DemoData with WITH ENCRYPTION — definition unreadable via sp_helptext. Overwriting blind via CREATE OR ALTER VIEW is technically possible now but risky." Had I read Backlog #24 carefully during planning, I would have predicted F-7-7 BEFORE the live PoC surfaced the SqlException. Lesson: the session-handoff Backlog table is part of the Three-layer Primary-Source Verification Gate's Implementation-vs-runtime layer — known runtime issues should be Gate inputs. Banking for Phase 8/9 kickoff drafting: Backlog table deserves an explicit re-read pass.

  2. The Note field's dual-source format (D6) emerged at PoC time, not design time. Initial design had each report producing one Note (either freshness verdict OR per-report semantic). The PoC revealed that Cash Trade Slotting + Existing Disposition need BOTH the freshness Note AND a per-report semantic Note. The semicolon-delimited concatenation works but is a runtime composition, not a wire-shape design. Lesson: when a single field can carry multiple semantic concerns, design the composition convention upfront (e.g. structured notes: string[] instead of note: string with delimiter conventions). Phase 7 ships with the string-with-delimiter pattern; future Phase 7+ may revisit.

  3. The Cash Trade Slotting empirical finding (F-7-8 — 688 real rows from a Failed run) was a planned outcome. A58's design intent + the kickoff's per-endpoint scope made it predictable that Cash Trade Slotting would return data even on Failed runs. Documenting this explicitly in the spec amendment + ADR-025 + completion report makes A58's preservation scope visible from the read API surface — that's worth the explicit table cell in the spec's §Output APIs.

  4. Subagent dispatch + self-implementation balance was right at this scale. 19 DTO types + 27 tests = 46 mechanical artifacts delegated cleanly. Service + endpoints + ADR + spec + assumptions + completion report = ~5 architecturally-load-bearing artifacts self-implemented. Mirrors 6e's split (greenfield + tests delegable; design + service + ADR self-implement). Banking for Phase 8: report endpoints continue to follow this pattern; React UI scaffolding (Phase 8) will look different (templated component scaffolding — heavy delegation candidate).

  5. The 8-endpoint Phase 7 fits in 1 Architect-session — same as the smaller 6e (4 endpoints + audit table + worker). The compression came from: (a) 6e's PowerFillRunHistoryService already provided the audit-row probe machinery (just had to call GetByIdAsync); (b) 19 DTOs delegated cleanly in one batch; (c) the umbrella service is one file (no cross-file coordination); (d) the 8 endpoints follow identical patterns (one helper + one method + one DTO each). Banking for Phase 8 estimate: the React UI is a different shape (cross-file coordination) but the data contract is fully stable; Architect should expect 1-2 sessions for the API contract layer + 2-3 sessions for the React scaffolding (heavily subagent-delegable).

  6. A57's pattern observation breaks at the 3-session corroboration mark — but qualifies, not reverses. A59 documents the F-7-1 mis-cite; the pattern observation now reads "kickoff specificity reduces but does not eliminate Truth Rot probability." The v3.1 nomination drafting can proceed with this qualifier baked in.

  7. The empirical PoC outcome was richer than expected. I anticipated 7 empty endpoints + 1 weird endpoint (Existing Disposition with the SqlException). Actual: 6 empty + 1 graceful-degradation + 1 with 688 real rows (Cash Trade Slotting per A58). The Cash Trade Slotting result is the cleanest demonstration that A58's preservation scope is testable from the Phase 7 read APIs — worth surfacing in the Phase 9 parallel-validation harness design as the canonical "non-BR-9-cleared sources are validation paths even on Failed runs" pattern.


PoC verification commands and outputs

Status sentinel (Deploy Verification Gate arm a)

$ docker exec pssaas-api curl -s http://localhost:8080/api/powerfill/status
{"module":"PowerFill","status":"phase-7-reports-ready"}

Live API runs (Deploy Verification Gate arm b)

Submit a fresh run on PS_DemoData (per the documented A56 outcome):

$ curl -s -X POST -H "X-Tenant-Id: ps-demodata" -H "Content-Type: application/json" \
"http://localhost:8080/api/powerfill/run" -d '{}'
{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","tenant_id":"ps-demodata",
"status":"Pending","started_at":"2026-04-19T02:49:57.2936753Z",...}

After ~25s natural completion the run finalises Failed at Step 5 (pool_guide) with the IDENTICAL A54 outcome (loan_id=3385000026, trade_id=36177868) — Phase 6e baseline preserved (Steps 1-4 produce 515 allocations; Step 5 fails per A54+A56).

8 Phase 7 endpoints against the latest Failed run

(Run id 769245cf-d317-411f-87aa-1d4345fb8489. The latest run is Failed → TerminalEmpty freshness for the 7 BR-9-cleared reports; the 8th — Cash Trade Slotting — returns real data per A58.)

1/8 GET /runs/{run_id}/guide

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../guide?limit=3"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489",
"run_status":"Failed",
"generated_at_utc":"2026-04-19T02:51:05.8232347Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared the
user-facing output tables. Failure context: SqlException 2627:
Violation of PRIMARY KEY constraint 'PK__##cte_po__F0E022A227C84D71'.
Cannot insert duplicate key in object 'dbo.##cte_posting_set_1300'.
The duplicate key value is (36177868, 3385000026).
Warning: Null value is eliminated by an aggregate or other SET
operation.\n[...]",
"guides":[],
"next_cursor":null}

2/8 GET /runs/{run_id}/recap

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../recap?limit=2"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:53:15.4541407Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared..."
" [same TerminalEmpty Note as Guide]",
"recap":[],
"next_cursor":null}

3/8 GET /runs/{run_id}/switching

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../switching"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:53:15.7640563Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared..."
" [same TerminalEmpty Note]",
"switches":[]}

4/8 GET /runs/{run_id}/pool-candidates

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../pool-candidates?limit=2"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:53:16.0285739Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared..."
" [same TerminalEmpty Note]",
"pool_candidates":[],
"next_cursor":null}

5/8 GET /runs/{run_id}/existing-disposition (with F-7-7 catch-and-degrade)

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../existing-disposition"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:56:03.5290207Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared the
user-facing output tables. Failure context: SqlException 2627: ...
| Schema drift on tenant DB: pfillv_existng_pool_disposition view
is the legacy WITH ENCRYPTION version (pre-dates the note_rate
column in Phase 2's 002_CreatePowerFillViews.sql). Per Backlog #24
the view deploy is deferred pending behavior diff. Phase 9 closes.",
"existing_dispositions":[]}

F-7-7 / A62 dual-Note demonstration — the response composes the freshness Note AND the schema-drift Note via the semicolon delimiter (per D6 convention).

6/8 GET /runs/{run_id}/pooling-guide

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../pooling-guide"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:56:11.6513442Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared..."
" [same TerminalEmpty Note]",
"pooling_guide":[]}

7/8 GET /runs/{run_id}/cash-trade-slotting (F-7-8 — REAL DATA per A58)

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../cash-trade-slotting"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:56:11.9359537Z",
"total_returned":688,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared the
user-facing output tables. Failure context: SqlException 2627: ...
| Cash market map is populated (survives BR-9 cleanup per A58
because it is not in the 7 user-facing tables list). Per-trade
slot fields (trade_id / sequence_number / price / trade_amount)
require pscat_trade_cash_grid which is populated by Step 1
(psp_pfill_bx_cash_grids); skipped on this run per A12 because
bx_price_floor is null.",
"cash_trade_slots":[
{"investor_instrument_name":"10 fhlmc cash","loan_instrument":"CF10",
"cash_grid_type":"note","trade_id":null,"sequence_number":null,
"price":null,"trade_amount":null},
{"investor_instrument_name":"10 fhlmc cash","loan_instrument":"CF10TXA6",
"cash_grid_type":"note","trade_id":null,"sequence_number":null,
"price":null,"trade_amount":null},
{"investor_instrument_name":"10 fnma cash","loan_instrument":"CF10",
"cash_grid_type":"pass","trade_id":null,"sequence_number":null,
"price":null,"trade_amount":null},
...
[688 rows total]
]}

This is the cleanest demonstration that A58's preservation scope is observable from the Phase 7 read APIs. 688 cash_market_map rows survive BR-9 cleanup on a Failed run. Per-trade slot fields surface as null per A12 (Step 1 skipped because bx_price_floor is null); the per-report Note tells the client both about the freshness verdict AND about why the slot fields are null.

8/8 GET /runs/{run_id}/kickouts

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/769245cf-.../kickouts?limit=5"

{"run_id":"769245cf-d317-411f-87aa-1d4345fb8489","run_status":"Failed",
"generated_at_utc":"2026-04-19T02:56:12.2784026Z",
"total_returned":0,
"note":"Run terminated as Failed at step 'pool_guide'; BR-9 cleared..."
" [same TerminalEmpty Note]",
"kickouts":[],
"next_cursor":null}

Stale freshness path (HTTP 410 Gone)

$ curl -s -o /dev/null -w "HTTP: %{http_code}\n" -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/909d7f16-.../guide?limit=3"
HTTP: 410

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/909d7f16-.../guide?limit=3"
{"run_id":"909d7f16-ab40-4b30-944c-ca2f8b3bfa7b",
"run_status":"Failed",
"generated_at_utc":"2026-04-19T02:51:15.6740614Z",
"total_returned":0,
"note":"Run 909d7f16-ab40-4b30-944c-ca2f8b3bfa7b is no longer the source
of the pfill_* output tables. The latest run is
769245cf-d317-411f-87aa-1d4345fb8489; fetch
GET /api/powerfill/runs/769245cf-d317-411f-87aa-1d4345fb8489
to confirm its status before reading reports.",
"guides":[],
"next_cursor":null}

The 410 Gone response carries the latest run id in the body Note for client redirection.

RunNotFound path (HTTP 404)

$ curl -s -o /dev/null -w "HTTP: %{http_code}\n" -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/00000000-0000-0000-0000-000000000000/guide"
HTTP: 404

$ curl -s -H "X-Tenant-Id: ps-demodata" \
"http://localhost:8080/api/powerfill/runs/00000000-0000-0000-0000-000000000000/guide"
{"error":"No PowerFill run found for run_id=00000000-0000-0000-0000-000000000000 in this tenant."}

Tests

$ docker exec pssaas-api dotnet build /app/PowerSeller.SaaS.sln --nologo
Build succeeded. 0 Warning(s). 0 Error(s).

$ docker exec pssaas-api dotnet test /app/PowerSeller.SaaS.sln --no-build --nologo
Passed! - Failed: 0, Passed: 32, Skipped: 0, Total: 32, Duration: 35 ms
- PowerSeller.SaaS.Modules.BestEx.Tests.dll
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: < 1 ms
- PowerSeller.SaaS.Api.Tests.dll
Passed! - Failed: 0, Passed: 200, Skipped: 6, Total: 206, Duration: 2 s
- PowerSeller.SaaS.Modules.PowerFill.Tests.dll

# Grand total: 233 passed, 6 skipped, 0 failed.
# Net new for Phase 7: 200 - 173 = 27 PowerFill tests
# (7 freshness + 10 happy paths + 5 pagination + 3 edge + 2 tenant scoping).

The 6 skipped: 4 pre-existing PFILL_TEST_SQLSERVER-env-gated SQL integration tests (unchanged); 2 InMemory-blocked Phase 6a placeholders (unchanged).


Open questions and blockers

Carry-over to Phase 8 / Phase 9 kickoff

A54 (legacy proc PK bug on PS_DemoData) — STILL DEFERRED Phase 9. Per ADR-021 verbatim discipline + Option C disposition (PO-confirmed in 6c/6d/6e). Phase 7 ships with this carry-over; the read APIs that depend on Step 5/6 output return empty + Note explaining the block. Phase 9 closes when A54 closes.

A56 (Step 5 fail-fast cascade) — STILL OBSERVATION, doubly-blocked with A54. Phase 7's 7 BR-9-cleared reports demonstrate the empty-with-Note path on PS_DemoData; the 1 non-BR-9-cleared report (Cash Trade Slotting) returns 688 real rows from cash_market_map per A58. Phase 9 parallel-validation harness should bank A58's preservation as the canonical validation path on Failed runs.

A57 (kickoff specificity reduces Truth Rot) — 3-session-corroboration breaks at A59. A59 documents the F-7-1 mis-cite; the pattern observation now reads "kickoff specificity reduces but does not eliminate Truth Rot probability." The v3.1 nomination drafting can still proceed with this qualifier baked in.

A62 (PS_DemoData view drift) — DEFERRED per Backlog #24. The Phase 7 service catches SqlException 207/208 and degrades to empty payload + explanatory Note. Phase 9 closes either by deploying 002_CreatePowerFillViews.sql to PS_DemoData (now possible per A30 RESOLVED) OR renaming PSSaaS view to pfillv2_*.

Architect recommendation for the Phase 8 kickoff drafting

The Phase 7 Architect recommends:

  1. Phase 8 kickoff drafting follows the 6d/6e/7 specificity pattern — every spec section number cited, every prior completion-report Architect-recommendation cross-referenced, every NVO line citation explicit at the line-number level (Phase 8 has no NVO scope; the spec/ADR sections are the equivalent). Document A59 as the corrective: also re-read the Backlog table during planning to surface known runtime issues (F-7-7 was Backlog-discoverable).

  2. Phase 8 should explicitly scope around A54 + A56 + A62 carry-over — the React UI / Superset dashboards consume Phase 7 endpoints; the dashboards on PS_DemoData will surface the empty-with-Note responses + the 688-row Cash Trade Slotting result. Phase 8 mockups should include the Note-as-banner UX for the 4 freshness verdicts.

  3. Phase 8 (React UI + Superset) breakdowns can begin in parallel — the Phase 7 contract surface is now stable + ADR-025-documented; Phase 8 mockups + Superset SQL queries can both proceed without additional Phase 7 changes.

  4. Phase 9 (Parallel Validation) can bank Phase 7's empirical findings — F-7-8 (Cash Trade Slotting returns real data on Failed runs per A58) is the canonical "non-BR-9-cleared sources are validation paths" pattern. Phase 9 harness should structure validation around this: BR-9-cleared reports validate post-Phase-9-A54-fix; non-BR-9-cleared reports validate now.

  5. Consider revisiting the Phase 7 test harness in Phase 9 — the InMemory caveat continues (the 27 new tests cover the InMemory-supported subset; some BR-9 cleanup paths and SqlException-catching paths are not exercised against the SQL Server provider). Phase 9 is the natural place to add SQL-Server-backed integration tests for the Phase 7 service paths that the InMemory provider can't cover.


  1. Collaborator review of:

    • Contracts/ReportContracts.cs (456 lines — review focus: snake_case JSON contract; cursor record types per report)
    • Services/PowerFillReportService.cs (~600 LOC — review focus: A60 freshness logic in ResolveRunContextAsync; per-report Note conventions; F-7-7 catch-and-degrade in GetExistingDispositionAsync)
    • Endpoints/RunEndpoints.cs extensions (8 new GET endpoints + BuildReportResult helper — review focus: D8 Stale-prefix detection for 410 routing)
    • Domain/Upstream/PscatTradeCashGrid.cs + composite-key configuration (small; per A61)
    • PowerFillModule.cs (service + entity registration; sentinel bump)
    • PowerFillReportServiceTests.cs (~700 LOC, 27 tests — review focus: freshness verdict coverage; pagination patterns; tenant-scoping edge cases)
    • Spec amendment to powerfill-engine.md (§Output APIs canonical-contract promotion + Phased Implementation table update)
    • ADR-025 (Phase 7 Report API Pattern — full A7.1/A7.2/A7.3 provenance)
    • Assumptions log A59-A62
    • This completion report
    • Devlog entry Estimated 2-3 hours (Phase 7 is comparable scale to 6e but with more new C# surface — the umbrella service is ~600 LOC of architecturally-novel code, and ADR-025 is a precedent-setting document).
  2. PO sign-off on:

    • Phase 7 (Reports / Recap Query APIs) COMPLETE declaration (sentinel phase-7-reports-ready)
    • ADR-025 (Phase 7 Report API Pattern; precedent-setting for Phase 8+ report endpoints)
    • A59-A62 dispositions
    • Architect recommendation that Phase 8 + Phase 9 breakdowns can begin in parallel
  3. PO push of the atomic commits (Architect commits; PO controls git push).

  4. Pre-push docs-build check per the Phase 6e MDX-trap lesson: docker build -f docs-site/Dockerfile.prod docs-site before pushing.

  5. Phase 8 kickoff drafting — prerequisites: Phase 7 contract surface stable + ADR-025 accepted; A54 + A56 + A62 carry-over acknowledged. Phase 8 ships React UI + Superset dashboards over the Phase 7 endpoints.

  6. Optional follow-up (deferred to Phase 8+ or Phase 9):

    • SQL-Server-backed integration tests for Phase 7 service paths the InMemory provider can't cover (BR-9 cleanup observability; SqlException-catching paths)
    • A62 closure: deploy 002_CreatePowerFillViews.sql to PS_DemoData (now possible per A30 RESOLVED) OR rename PSSaaS view to pfillv2_*
    • A54 fix per Phase 9 plan (legacy proc PK extension; requires ADR-021 amendment)
    • Snapshot replay tables (Q3 Option C) per ADR-025 Future Considerations
    • Per-tenant rate limiting on the unbounded report endpoints (Phase 7+ if needed)

Notes on this session's process

  • Three-layer Primary-Source Verification Gate exercised; produced 8 findings (F-7-1 through F-7-8). 1 net-new mis-cite against the kickoff (F-7-1) — first in 3 consecutive sub-phases; A57's pattern observation gets a qualifier (specificity reduces but does not eliminate Truth Rot); the v3.1 nomination drafting can proceed with the qualifier baked in.
  • Reviewable Chunks at sub-phase scope — considered the kickoff's hint to checkpoint after first 1-2 endpoints; declined per 6c/6d/6e precedent. Documented in plan §5.
  • Required Delegation Categories classification: 19 DTOs delegated (Subagent 1; clean first-attempt); 27 tests delegated (Subagent 2; one minor iteration); umbrella service + endpoints + ADR + spec + assumptions + completion report self-implemented per Deliberate Non-Delegation.
  • Andon-cord readiness — used twice during the session: (a) F-7-7 SqlException surfaced during PoC; service updated with catch-and-degrade pattern; documented as A62; (b) the StaleNoteFormat said "latest Complete run" but the latest run can be Failed/Cancelled too; updated to "latest run" + "fetch GET /runs/{run_id} to confirm status." Both surfaced in the live PoC and were fixed in-session before the completion report was drafted.
  • Counterfactual Retro filled with 7 named observations — most important: (1) F-7-7 was Backlog-discoverable at planning time (Backlog table = primary-source check input); (2) Note dual-source format emerged at PoC time (could've been designed upfront with structured notes[]); (3) F-7-8 Cash Trade Slotting real-data result is the canonical A58-preservation validation pattern for Phase 9.
  • Deploy Verification Gate all 3 arms exercised: sentinel green; live API exercised through all 8 endpoints + Stale + RunNotFound paths; no SQL deploys this phase (Phase 7 is read-only over existing schema).
  • Sub-phase calendar time: ~1 Architect-session — consistent with 6a-6e velocity. Banking for Phase 8 estimate: API contract layer ~1-2 sessions; React scaffolding ~2-3 sessions (heavier subagent delegation candidate).

Phase 7 is code complete; Phase 6 (Core Allocation Engine) → Phase 7 (Reports / Recap Query APIs) arc ships; the Phase 8 Architect can dispatch their work with full visibility into the Phase 7 contract surface (ADR-025) + A54/A56/A62 carry-over (Phase 9 remediation gates) + the empirical PoC findings (F-7-7 graceful degradation; F-7-8 A58 preservation observable from read APIs).


End of Phase 7 completion report. Phase 7 (Reports / Recap Query APIs) ships.