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
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.
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
Fallback city-card reads now distinguish three cases that previously collapsed into the generic fast-evidence copy: observed highs above the model path, observed highs still lagging after the peak window, and low observations before the peak window that should wait for confirmation rather than down-revise immediately. The same pass removes three unused private helpers from the scan terminal service.
Constraint: Fallback output must be useful before the full AI airport-bulletin read returns
Rejected: Treat any low latest METAR as a down-revision | early-day observations can be below the forecast before the peak window
Rejected: Keep unused helper wrappers | they were unreferenced and added noise to an already large module
Confidence: high
Scope-risk: moderate
Tested: pytest tests/test_web_observability.py -q
Tested: npm run build
Fast evidence mode should not say DEB and models support the center when the latest METAR has already exceeded that center or the model upper edge. The fallback now treats the live observation as a lower bound for the daily high and explains the upward revision pressure.
Constraint: Fallback output must remain useful before the full AI bulletin read returns
Rejected: Keep the original DEB center until AI completes | it can be lower than an already-observed temperature
Confidence: high
Scope-risk: narrow
Tested: pytest tests/test_web_observability.py::test_city_ai_fallback_revises_up_when_latest_metar_breaks_above_models tests/test_web_observability.py::test_city_ai_fallback_reasoning_identifies_fast_evidence_mode tests/test_web_observability.py::test_city_ai_stream_request_only_asks_provider_for_observation_read -q
Tested: npm run build