Adds a canonical machine-readable probability signal endpoint that distils
a completed simulation report into a structured prediction thesis.
New: backend/app/services/signal_extractor.py
- SignalExtractor.extract() calls chat_json() (3 attempts, temp 0.1, step 0.05)
against the report markdown and returns a validated MiroSignal dataclass
- Validates and normalises all fields: p_yes clamped to [0.01, 0.99],
confidence/action enums enforced, action recomputed from p_yes when invalid
- _trim_report() keeps the tail (conclusions) for long reports to stay within
token limits
- _salvage() fallback_parser recovers a minimal signal from partial LLM output
using regex probability extraction
New endpoint: POST /api/report/<report_id>/signal
- 404 if report not found
- 400 if report not yet completed or content is empty
- 422 if LLM fails after all retry attempts
- Returns canonical signal with thesis.{p_yes, confidence, action, regime,
summary, drivers, invalidators} — schema_version 1.1
New: backend/tests/services/test_signal_extractor.py
- 27 tests covering happy path, field validation/normalisation, report
trimming, _salvage fallback, and LLM failure propagation
- No real API calls — LLMClient fully mocked
Closes#277
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>