From f4d467824b5009c6c93298e140a9962dae93f942 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 16:53:11 +0000 Subject: [PATCH] refactor(web): extract run-mode helpers into digest.web.run_mode Continue breaking up src/digest/web/app.py by moving the run-mode resolution helpers and the RUN_MODE_OPTIONS table into a focused digest.web.run_mode module (following the feedback/schedule/run_progress/sources/security pattern). - New digest.web.run_mode holds RUN_MODE_OPTIONS, DEFAULT_WEB_RUN_MODE, _resolve_run_mode, _resolve_profile_run_mode, _resolve_run_mode_for_request, _run_mode_options, and _web_live_run_options - app.py re-imports the four route-facing names; DEFAULT_WEB_RUN_MODE, _resolve_run_mode, and _run_mode_options stay internal app.py drops from 1751 to 1688 lines; no behavior change. ruff/bandit/ detect-secrets clean and all 254 backend tests pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01GRvYFEnkge3dkDJRMmBTyh --- .../proposal.md | 30 +++++++ .../web-run-mode-helper-extraction/tasks.md | 17 ++++ src/digest/web/app.py | 75 ++---------------- src/digest/web/run_mode.py | 78 +++++++++++++++++++ 4 files changed, 131 insertions(+), 69 deletions(-) create mode 100644 .docs/doing/web-run-mode-helper-extraction/proposal.md create mode 100644 .docs/doing/web-run-mode-helper-extraction/tasks.md create mode 100644 src/digest/web/run_mode.py diff --git a/.docs/doing/web-run-mode-helper-extraction/proposal.md b/.docs/doing/web-run-mode-helper-extraction/proposal.md new file mode 100644 index 0000000..1a1e4bb --- /dev/null +++ b/.docs/doing/web-run-mode-helper-extraction/proposal.md @@ -0,0 +1,30 @@ +# Web Run-Mode Helper Extraction Proposal + +## Why + +`src/digest/web/app.py` still holds the run-mode resolution helpers and their +`RUN_MODE_OPTIONS` table as a top-of-file block. They are pure functions with no +route or `create_app` state, so they can move into a focused module — continuing +the helper-extraction pattern already applied for `feedback`, `schedule`, +`run_progress`, `sources`, and `security`. + +## Scope + +- Move the run-mode helpers and their config into a new `digest.web.run_mode` + module: `RUN_MODE_OPTIONS`, `DEFAULT_WEB_RUN_MODE`, `_resolve_run_mode`, + `_resolve_profile_run_mode`, `_resolve_run_mode_for_request`, + `_run_mode_options`, and `_web_live_run_options`. +- Re-import the four route-facing names (`RUN_MODE_OPTIONS`, + `_resolve_profile_run_mode`, `_resolve_run_mode_for_request`, + `_web_live_run_options`) into `digest.web.app` so existing call sites and + `from digest.web.app import ...` (used by `test_web_live_run_options`) keep + working. `DEFAULT_WEB_RUN_MODE`, `_resolve_run_mode`, and `_run_mode_options` + stay internal to the new module. +- Verify the live-run-options tests and the full backend/security checks. + +## Non-goals + +- No run-mode resolution behavior changes. +- No API shape changes. +- No route or scheduler-loop changes. +- No frontend changes. diff --git a/.docs/doing/web-run-mode-helper-extraction/tasks.md b/.docs/doing/web-run-mode-helper-extraction/tasks.md new file mode 100644 index 0000000..3a8a2b4 --- /dev/null +++ b/.docs/doing/web-run-mode-helper-extraction/tasks.md @@ -0,0 +1,17 @@ +# Tasks + +- [x] Create `digest.web.run_mode` with `RUN_MODE_OPTIONS`, + `DEFAULT_WEB_RUN_MODE`, and the five run-mode helper functions. +- [x] Remove the moved definitions from `digest.web.app` and re-import the four + route-facing names. +- [x] Confirm no circular import (`import digest.web.app` succeeds). +- [x] `ruff check src tests` is clean. +- [x] Full backend test suite passes (focus: `test_web_live_run_options`). + +## Result + +- `digest/web/app.py` shrank from 1751 to 1688 lines; new + `digest/web/run_mode.py` holds the 78-line module. +- Cumulative across the session's five `app.py` extractions (schedule, + run_progress, sources, security, run_mode), `app.py` dropped from 2589 to + 1688 lines (-901, -35%). diff --git a/src/digest/web/app.py b/src/digest/web/app.py index b435207..c887a08 100644 --- a/src/digest/web/app.py +++ b/src/digest/web/app.py @@ -48,6 +48,12 @@ _feedback_features_for_source_feedback, _feedback_rating_for_label, ) +from digest.web.run_mode import ( + RUN_MODE_OPTIONS, + _resolve_profile_run_mode, + _resolve_run_mode_for_request, + _web_live_run_options, +) from digest.web.run_progress import ( FETCH_STAGES, _as_int, @@ -82,75 +88,6 @@ ) -RUN_MODE_OPTIONS: dict[str, dict[str, bool]] = { - "fresh_only": { - "use_last_completed_window": True, - "only_new": True, - "allow_seen_fallback": False, - }, - "balanced": { - "use_last_completed_window": True, - "only_new": True, - "allow_seen_fallback": True, - }, - "replay_recent": { - "use_last_completed_window": True, - "only_new": False, - "allow_seen_fallback": True, - }, - "backfill": { - "use_last_completed_window": False, - "only_new": False, - "allow_seen_fallback": True, - }, -} -DEFAULT_WEB_RUN_MODE = "fresh_only" - - -def _resolve_run_mode(mode: str, *, fallback: str = DEFAULT_WEB_RUN_MODE) -> str: - candidate = (mode or "").strip().lower() - if candidate in RUN_MODE_OPTIONS: - return candidate - return fallback - - -def _resolve_profile_run_mode(profile_cfg: Any) -> str: - run_policy = getattr(profile_cfg, "run_policy", None) - default_mode = getattr(run_policy, "default_mode", "") - return _resolve_run_mode(str(default_mode or ""), fallback=DEFAULT_WEB_RUN_MODE) - - -def _resolve_run_mode_for_request( - *, - profile_cfg: Any, - requested_mode: str, -) -> str: - default_mode = _resolve_profile_run_mode(profile_cfg) - run_policy = getattr(profile_cfg, "run_policy", None) - allow_override = bool(getattr(run_policy, "allow_run_override", True)) - if not allow_override: - return default_mode - resolved_requested = _resolve_run_mode(requested_mode, fallback="") - if resolved_requested: - return resolved_requested - return default_mode - - -def _run_mode_options(mode: str) -> dict[str, bool]: - resolved_mode = _resolve_run_mode(mode, fallback=DEFAULT_WEB_RUN_MODE) - return dict(RUN_MODE_OPTIONS[resolved_mode]) - - -def _web_live_run_options(*, trigger: str, mode: str = "") -> dict[str, bool]: - if trigger in {"web", "schedule"}: - return _run_mode_options(_resolve_run_mode(mode, fallback=DEFAULT_WEB_RUN_MODE)) - return { - "use_last_completed_window": False, - "only_new": False, - "allow_seen_fallback": True, - } - - @dataclass(slots=True) class WebSettings: sources_path: str diff --git a/src/digest/web/run_mode.py b/src/digest/web/run_mode.py new file mode 100644 index 0000000..f276847 --- /dev/null +++ b/src/digest/web/run_mode.py @@ -0,0 +1,78 @@ +"""Run-mode resolution helpers for the web control plane. + +These are pure functions and configuration extracted from ``digest.web.app``; +they map run-mode names to ingestion/selection options and hold no route or +application state. +""" + +from __future__ import annotations + +from typing import Any + +RUN_MODE_OPTIONS: dict[str, dict[str, bool]] = { + "fresh_only": { + "use_last_completed_window": True, + "only_new": True, + "allow_seen_fallback": False, + }, + "balanced": { + "use_last_completed_window": True, + "only_new": True, + "allow_seen_fallback": True, + }, + "replay_recent": { + "use_last_completed_window": True, + "only_new": False, + "allow_seen_fallback": True, + }, + "backfill": { + "use_last_completed_window": False, + "only_new": False, + "allow_seen_fallback": True, + }, +} +DEFAULT_WEB_RUN_MODE = "fresh_only" + + +def _resolve_run_mode(mode: str, *, fallback: str = DEFAULT_WEB_RUN_MODE) -> str: + candidate = (mode or "").strip().lower() + if candidate in RUN_MODE_OPTIONS: + return candidate + return fallback + + +def _resolve_profile_run_mode(profile_cfg: Any) -> str: + run_policy = getattr(profile_cfg, "run_policy", None) + default_mode = getattr(run_policy, "default_mode", "") + return _resolve_run_mode(str(default_mode or ""), fallback=DEFAULT_WEB_RUN_MODE) + + +def _resolve_run_mode_for_request( + *, + profile_cfg: Any, + requested_mode: str, +) -> str: + default_mode = _resolve_profile_run_mode(profile_cfg) + run_policy = getattr(profile_cfg, "run_policy", None) + allow_override = bool(getattr(run_policy, "allow_run_override", True)) + if not allow_override: + return default_mode + resolved_requested = _resolve_run_mode(requested_mode, fallback="") + if resolved_requested: + return resolved_requested + return default_mode + + +def _run_mode_options(mode: str) -> dict[str, bool]: + resolved_mode = _resolve_run_mode(mode, fallback=DEFAULT_WEB_RUN_MODE) + return dict(RUN_MODE_OPTIONS[resolved_mode]) + + +def _web_live_run_options(*, trigger: str, mode: str = "") -> dict[str, bool]: + if trigger in {"web", "schedule"}: + return _run_mode_options(_resolve_run_mode(mode, fallback=DEFAULT_WEB_RUN_MODE)) + return { + "use_last_completed_window": False, + "only_new": False, + "allow_seen_fallback": True, + }