Add the LLM prose-composition path for the parcel-forecast chat, layered
over the deterministic Step-1 fallback which stays the safety net.
- chat/tools.py: 5 read-only section tools (exec_summary, product_recommendation,
forecast, risks, scenarios) — pure slices of the loaded report dict, no DB/
recompute, graceful on missing sections. market_now (raw analyze blob) and meta
are deliberately NOT exposed -> highest-PII data cannot reach the LLM.
- chat/safe_payload.py: the §19 gate — single place that builds the outbound
SafePayload from a section-aggregate allowlist; honors is_confidential hard-block.
- chat/orchestrator.py: manual tool-call loop with call-cap/termination, real
grounded_in provenance; any LLMResult.ok=False (disabled/timeout/rate_limited/
redaction_refused/call_cap/provider_error/empty) degrades to the deterministic answer.
- llm/prompts.py: versioned chat_system@v1 — answer only from sections, never
fabricate numbers, advisory tone, decline out-of-scope.
- api/v1/chat.py: branch on settings.llm_enabled; sync complete bridged via
run_in_threadpool. Default-off -> deterministic path, no provider built.
- Tests: fake provider only (no network), planted-secret redaction-boundary +
per-reason fallback + call-cap + numbers-from-report coverage.
Refs #957