.vbw-planning/milestones/03-coverage-analysis-quick-wins-critical-path-test-co/phases/03-large-file-refactoring/PLAN-01.md
Extract lib/source_monitor/fetching/feed_fetcher.rb (627 lines) into focused sub-modules following the existing extraction pattern used by item_scraper/ (which already has adapter_resolver.rb and persistence.rb sub-modules) and completion/ (which has event_publisher.rb, follow_up_handler.rb, retention_handler.rb). The public API (FeedFetcher.new(source:).call returning a Result struct) must remain unchanged. All 1219 lines of existing tests in feed_fetcher_test.rb must continue to pass without modification.
Decomposition rationale: FeedFetcher has three clearly separable responsibility clusters: (1) source state updates after fetch (update_source_for_success, update_source_for_not_modified, update_source_for_failure, reset_retry_state!, apply_retry_strategy!, create_fetch_log -- ~120 lines), (2) adaptive interval computation (apply_adaptive_interval!, compute_next_interval_seconds, adjusted_interval_with_jitter, jitter_offset, and all interval config helpers -- ~110 lines), (3) entry processing (process_feed_entries, normalize_item_error, safe_entry_guid, safe_entry_title -- ~80 lines). The remaining orchestration (call, perform_fetch, handle_response, handle_success, handle_not_modified, handle_failure, HTTP helpers) stays in the main file.
Trade-offs considered:
What constrains the structure:
Execution note: Task 2 (AdaptiveInterval) should be completed before Task 1 (SourceUpdater) because SourceUpdater depends on AdaptiveInterval for the apply_adaptive_interval! method. Task 3 (EntryProcessor) is independent of the other two. Task 4 is the final wiring pass.
name: extract-source-updater
files:
lib/source_monitor/fetching/feed_fetcher/source_updater.rb (new)lib/source_monitor/fetching/feed_fetcher.rbaction: Create lib/source_monitor/fetching/feed_fetcher/source_updater.rb containing a SourceMonitor::Fetching::FeedFetcher::SourceUpdater class. Move these methods from feed_fetcher.rb into the new class:
update_source_for_success (lines 192-216)update_source_for_not_modified (lines 218-241)update_source_for_failure (lines 243-259)reset_retry_state! (lines 261-265)apply_retry_strategy! (lines 267-299)create_fetch_log (lines 301-320)feed_metadata (lines 328-335)normalized_headers (lines 337-341)error_backtrace (lines 343-347)derive_feed_format (lines 322-326)feed_signature_changed? (lines 419-423)updated_metadata (lines 490-495)parse_http_time (lines 349-355)elapsed_ms (lines 357-359)The SourceUpdater constructor takes source: and adaptive_interval: (the AdaptiveInterval instance from Task 2). Add require "source_monitor/fetching/feed_fetcher/source_updater" at the top of feed_fetcher.rb. In FeedFetcher, create a source_updater method that lazily instantiates the SourceUpdater passing source and adaptive_interval. Replace all calls to the moved methods with delegation to source_updater.method_name. The SourceUpdater must be a private implementation detail -- not exposed in the public API.
verify: ruby -c lib/source_monitor/fetching/feed_fetcher/source_updater.rb exits 0 AND bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb exits 0 with zero failures
done: SourceUpdater extracted. All calls delegated. Tests pass unchanged.
name: extract-adaptive-interval
files:
lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb (new)lib/source_monitor/fetching/feed_fetcher.rbaction: Create lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb containing a SourceMonitor::Fetching::FeedFetcher::AdaptiveInterval class. Move these methods:
apply_adaptive_interval! (lines 425-438)compute_next_interval_seconds (lines 441-455)current_interval_seconds (lines 457-459)interval_minutes_for (lines 461-464)min_fetch_interval_seconds (lines 466-468)max_fetch_interval_seconds (lines 470-472)increase_factor_value (lines 474-476)decrease_factor_value (lines 478-480)failure_increase_factor_value (lines 482-484)jitter_percent_value (lines 486-488)adjusted_interval_with_jitter (lines 497-502)jitter_offset (lines 504-512)configured_seconds (lines 569-574)configured_positive (lines 576-580)configured_non_negative (lines 583-588)extract_numeric (lines 590-597)fetching_config (lines 599-601)Also move the constants: MIN_FETCH_INTERVAL, MAX_FETCH_INTERVAL, INCREASE_FACTOR, DECREASE_FACTOR, FAILURE_INCREASE_FACTOR, JITTER_PERCENT.
The constructor takes source: and jitter_proc:. Add require "source_monitor/fetching/feed_fetcher/adaptive_interval" at the top of feed_fetcher.rb. In FeedFetcher, create an adaptive_interval method that lazily instantiates AdaptiveInterval. Replace all calls to moved methods with delegation. Keep the constant references working by aliasing from the main class or referencing the sub-module.
verify: ruby -c lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb exits 0 AND bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb exits 0
done: AdaptiveInterval extracted. Constants accessible. Tests pass unchanged.
name: extract-entry-processor
files:
lib/source_monitor/fetching/feed_fetcher/entry_processor.rb (new)lib/source_monitor/fetching/feed_fetcher.rbaction: Create lib/source_monitor/fetching/feed_fetcher/entry_processor.rb containing a SourceMonitor::Fetching::FeedFetcher::EntryProcessor class. Move these methods:
process_feed_entries (lines 520-567)normalize_item_error (lines 603-612)safe_entry_guid (lines 614-620)safe_entry_title (lines 622-624)The constructor takes source:. It returns EntryProcessingResult structs (which stay defined in the main FeedFetcher class and are referenced as FeedFetcher::EntryProcessingResult). Add require "source_monitor/fetching/feed_fetcher/entry_processor" at the top of feed_fetcher.rb. In FeedFetcher, create an entry_processor method and delegate process_feed_entries to it. The other methods are only called from within entry_processor so they move wholesale.
verify: ruby -c lib/source_monitor/fetching/feed_fetcher/entry_processor.rb exits 0 AND bin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb exits 0
done: EntryProcessor extracted. Tests pass unchanged.
lib/source_monitor/fetching/feed_fetcher.rbcall, perform_fetch, handle_response, handle_success, handle_not_modified, handle_failure, perform_request, connection, request_headers, build_http_error_from_faraday, body_digest, and the lazy accessor methods for source_updater, adaptive_interval, and entry_processor. Clean up any dead code, unused private methods, or orphaned requires. Ensure no method is duplicated between the main file and sub-modules. Verify the main file is under 300 lines. Run the full test suite to confirm no regressions.wc -l lib/source_monitor/fetching/feed_fetcher.rb shows fewer than 300 lines AND bin/rails test exits 0 with 760+ runs and 0 failures AND bin/rubocop lib/source_monitor/fetching/feed_fetcher.rb lib/source_monitor/fetching/feed_fetcher/ exits 0wc -l lib/source_monitor/fetching/feed_fetcher.rb shows fewer than 300 lineswc -l lib/source_monitor/fetching/feed_fetcher/source_updater.rb lib/source_monitor/fetching/feed_fetcher/adaptive_interval.rb lib/source_monitor/fetching/feed_fetcher/entry_processor.rb shows all existbin/rails test test/lib/source_monitor/fetching/feed_fetcher_test.rb exits 0 with zero failuresbin/rails test exits 0 (no regressions)bin/rubocop lib/source_monitor/fetching/ exits 0