Is your feature request related to a problem? Please describe.
ScenarioRunService currently owns both scenario lifecycle orchestration and a substantial progress-reporting subsystem. In particular:
_get_progress_snapshot manages the per-run LRU cache, keyset paging, cursor advancement, stale identifier-enrichment detection, legacy-plan synthesis, persistence-to-API mapping, and summary invalidation;
_build_progress_summary derives overall, technique, display-group, seed-group, and atomic-group rollups, including statuses, retry/error counts, scorer metadata, and unattributed attempts;
- the snapshot boundary is an unnamed tuple of deltas, mapped results, summary, and effective plan, while cache invalidation state is also represented with positional tuples.
This makes scenario lifecycle code harder to reason about and couples independently testable read-model behavior to the service's run creation, execution, cancellation, and history responsibilities. The concern is responsibility concentration and implicit state invariants, not method size alone.
Describe the solution you'd like
Extract an internal, typed scenario-progress read-model component. It should own:
- incremental hydration of lightweight persisted attack-result deltas;
- keyset cursor advancement and bounded per-run caching;
- cache invalidation when plan, terminal/live state, or persisted identifier enrichment changes;
- legacy effective-plan synthesis and plan lookup used for result mapping;
- mapping persisted deltas into
ScenarioProgressResult values;
- canonical construction of
ScenarioProgressSummary.
Replace positional snapshot/state tuples with explicit internal dataclasses or equivalent typed models. ScenarioRunService should retain lifecycle orchestration: capture active-run state, load and validate the run header and persisted plan, request the progress snapshot, apply client-facing pagination, and construct the existing ScenarioRunProgress response.
The extraction must preserve the current REST schema, storage schema, cursor format, sorting, status calculation, retry/error accounting, scorer projection, legacy-plan behavior, and public APIs.
Describe alternatives you've considered, if relevant
Splitting _get_progress_snapshot and _build_progress_summary into more private methods on ScenarioRunService would reduce local method size but leave lifecycle and read-model responsibilities coupled.
A broader simultaneous split of MemoryInterface history/aggregation SQL was also considered. That has higher query and migration risk and should be handled as a separate follow-up after this boundary is established, rather than expanding this issue into a backend redesign.
Additional context
This was identified independently in the September 10 and September 11, 2026 repository complexity audits. No exact active issue, pull request, or audit session was found.
Suggested regression coverage should preserve and extend the existing scenario progress tests for:
- incremental polling where only newly persisted rows are mapped;
- multi-page storage hydration and deterministic keyset ordering;
- a persisted row whose attack-technique identifier is enriched after its initial insertion;
- plan changes and invalid or unavailable persisted plans;
- live, completed, failed, and cancelled status transitions;
- retry, error, success, and unattributed-attempt rollups;
- technique, display-group, seed-group, and atomic-group projections;
- objective scorer identity and official metric projection;
- client
since cursors, page limits, has_more, and first-page-only plan delivery;
- cache eviction and concurrent reads of different runs.
One important edge case is an existing cached first page containing an unenriched result, followed by in-place enrichment of that result and the append of a later page. A subsequent poll should refresh the enriched row, include the appended row exactly once, preserve cursor order, and avoid summary double-counting.
Definition of done:
- progress hydration/projection has a clear typed boundary outside
ScenarioRunService;
- duplicated or superseded progress branches are removed rather than wrapped in pass-through helpers;
- current API and persistence behavior remain unchanged;
- focused backend service and route tests, Ruff, typing, and
git diff --check pass.
Is your feature request related to a problem? Please describe.
ScenarioRunServicecurrently owns both scenario lifecycle orchestration and a substantial progress-reporting subsystem. In particular:_get_progress_snapshotmanages the per-run LRU cache, keyset paging, cursor advancement, stale identifier-enrichment detection, legacy-plan synthesis, persistence-to-API mapping, and summary invalidation;_build_progress_summaryderives overall, technique, display-group, seed-group, and atomic-group rollups, including statuses, retry/error counts, scorer metadata, and unattributed attempts;This makes scenario lifecycle code harder to reason about and couples independently testable read-model behavior to the service's run creation, execution, cancellation, and history responsibilities. The concern is responsibility concentration and implicit state invariants, not method size alone.
Describe the solution you'd like
Extract an internal, typed scenario-progress read-model component. It should own:
ScenarioProgressResultvalues;ScenarioProgressSummary.Replace positional snapshot/state tuples with explicit internal dataclasses or equivalent typed models.
ScenarioRunServiceshould retain lifecycle orchestration: capture active-run state, load and validate the run header and persisted plan, request the progress snapshot, apply client-facing pagination, and construct the existingScenarioRunProgressresponse.The extraction must preserve the current REST schema, storage schema, cursor format, sorting, status calculation, retry/error accounting, scorer projection, legacy-plan behavior, and public APIs.
Describe alternatives you've considered, if relevant
Splitting
_get_progress_snapshotand_build_progress_summaryinto more private methods onScenarioRunServicewould reduce local method size but leave lifecycle and read-model responsibilities coupled.A broader simultaneous split of
MemoryInterfacehistory/aggregation SQL was also considered. That has higher query and migration risk and should be handled as a separate follow-up after this boundary is established, rather than expanding this issue into a backend redesign.Additional context
This was identified independently in the September 10 and September 11, 2026 repository complexity audits. No exact active issue, pull request, or audit session was found.
Suggested regression coverage should preserve and extend the existing scenario progress tests for:
sincecursors, page limits,has_more, and first-page-only plan delivery;One important edge case is an existing cached first page containing an unenriched result, followed by in-place enrichment of that result and the append of a later page. A subsequent poll should refresh the enriched row, include the appended row exactly once, preserve cursor order, and avoid summary double-counting.
Definition of done:
ScenarioRunService;git diff --checkpass.