The scan terminal service had accumulated cache, payload, filtering, AI prompt, AI merge, METAR gate, ranking, and city-row construction details in one file. This splits those stable responsibilities into focused modules while preserving the endpoint payload shape and existing behavior.
Constraint: User-visible behavior and release version must remain unchanged for this internal refactor
Rejected: Rewrite the terminal scan flow around a new abstraction | too risky while production behavior is being stabilized
Confidence: high
Scope-risk: moderate
Directive: Keep scan_terminal_service.py as orchestration; add detailed rule changes to the focused modules instead of re-growing the service file
Tested: py_compile for extracted modules; ruff check .; pytest tests/test_scan_terminal_modules.py tests/test_web_observability.py; full pytest; npm run test:business; npm run build; git diff --cached --check
The scan terminal had grown into overlapping CSS, request-state, AI-provider, and city-card data responsibilities. This refactor separates those boundaries without changing product behavior: CSS modules are split by surface, city AI prompt/provider/fallback logic is isolated, and scan terminal request state now has reusable RemoteData adapters plus business-state tests.
Constraint: Preserve existing global scan-terminal class names and API responses during the refactor
Constraint: No new dependencies; keep this as a file-boundary cleanup
Rejected: Introduce React Query now | higher migration risk than the requested lightweight query-client path
Rejected: Rewrite AI stream behavior | progressive/fallback states are product-sensitive and were only adapter-split
Confidence: high
Scope-risk: moderate
Reversibility: clean
Directive: Keep AI stream state changes covered by business snapshots before changing fallback/cache wording
Tested: npm run test:business; npx tsc --noEmit; npm run build; python pytest -q; ruff check; py_compile targeted city AI modules
Not-tested: Live DeepSeek provider network replay and browser visual QA
The AI helper extraction left a stale private helper import in the scan terminal service. Removing it keeps CI aligned with the current call graph without changing runtime behavior.
Constraint: CI runs ruff F401 as a blocking check.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: .\.codex-tmp\pydeps\bin\ruff.exe check .
Not-tested: Full backend test suite; change is import-only.
Qingdao needs the same airport-settlement path as the other Wunderground-backed APAC cities, so the registry, aliases, timezone, prewarm, official links, market focus, and tests now point to ZSQD / Qingdao Jiaodong International Airport.
Constraint: User supplied Wunderground Qingdao/ZSQD settlement URL.
Rejected: Add a partial registry-only entry | it would show in APIs without frontend links, prewarm coverage, or alias support.
Confidence: high
Scope-risk: narrow
Reversibility: clean
Tested: pytest tests/test_country_networks.py tests/test_web_observability.py::test_cities_endpoint_includes_new_wunderground_cities -q
Tested: npm run build
Not-tested: Live Wunderground fetch for ZSQD in production.
The scan terminal service had grown into a 3.5k-line file that mixed endpoint orchestration, AI provider calls, fallback copy, JSON repair, and deterministic evidence guards. This extracts the city-AI helper layer into a focused module while preserving the old private names through imports for existing tests and callers.
Constraint: Keep behavior unchanged after the previous evidence-guard fixes and avoid a broad service rewrite.
Rejected: Split every scan-terminal concern at once | too much regression risk for this maintenance pass.
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
DeepSeek can return a polished city forecast that conflicts with already-computed stale-observation, observed-break, or peak-window evidence. The completion path now carries the deterministic fallback guard state and overwrites only the critical fields when provider text or numbers contradict those local facts.
Constraint: Airport-read latency optimization keeps provider output narrow, so backend completion remains the authority for final highs and evidence conflicts.
Rejected: Trust provider final wording when present | it can reintroduce stale METAR anchors or miss observed high breaks.
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
City-card fallback reads now stop using stale METAR or official observations as strong live anchors. A stale observation no longer forces high/low revisions, and both backend and browser AI cache keys include the observation fingerprint so updated report times, receipt times, temperatures, or stale status invalidate old AI text.
Constraint: Cached city AI reads must not survive a material observation update
Rejected: Let stale METAR trigger observed-break revisions | stale reports can be older than the active temperature path
Confidence: high
Scope-risk: moderate
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build