"""Render a verified brief to Markdown / HTML. Claim references are substituted here and nowhere else, so the value a reader sees is the value the verifier checked. A reference with no matching claim renders as an explicit ``[unverified]`` marker rather than silently vanishing. """ from __future__ import annotations import html import re from collections.abc import Mapping from app.models.brief import CLAIM_REF_PATTERN, Brief, NumericClaim UNVERIFIED_MARKER = "[unverified]" def format_claim(claim: NumericClaim) -> str: """Human-facing rendering of one verified figure.""" value = claim.value if claim.unit == "usd": return f"${value:,.2f}" if claim.unit == "percent": return f"{value:+.2f}%" if claim.unit == "ratio": return f"{value:,.2f}" if claim.unit == "score": return f"{value:+.2f}" return f"{value}" def substitute(text: str, claims: Mapping[str, NumericClaim]) -> str: """Replace every ``{{cN}}`` with its verified, formatted value.""" def _replace(match: re.Match[str]) -> str: claim = claims.get(match.group(1)) return format_claim(claim) if claim else UNVERIFIED_MARKER return CLAIM_REF_PATTERN.sub(_replace, text or "") def _cell(claim_id: str | None, claims: Mapping[str, NumericClaim]) -> str: if not claim_id: return "—" claim = claims.get(claim_id) return format_claim(claim) if claim else UNVERIFIED_MARKER def render_markdown(brief: Brief) -> str: """The brief as Markdown — used for the email body and the archive.""" claims = brief.claims_by_id() out: list[str] = [] out.append(f"# AlphaBrief — {brief.generated_for}") out.append("") out.append(f"**{substitute(brief.headline, claims)}**") out.append("") if brief.partial: out.append("> ⚠️ **Partial coverage** — at least one ticker failed to return data.") out.append("") out.append(substitute(brief.executive_summary, claims)) out.append("") if brief.snapshot: out.append("## Snapshot") out.append("") out.append( "| Ticker | Company | Close | 1D | 30D | Vol (ann.) | Max DD | P/E | Sentiment |" ) out.append("| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |") for row in brief.snapshot: out.append( "| " + " | ".join( [ row.ticker, row.company or "—", _cell(row.last_close, claims), _cell(row.change_1d, claims), _cell(row.return_30d, claims), _cell(row.volatility, claims), _cell(row.max_drawdown, claims), _cell(row.pe_ratio, claims), _cell(row.sentiment, claims), ] ) + " |" ) out.append("") if brief.key_moves: out.append("## Key moves") out.append("") for move in brief.key_moves: out.append(f"- **{move.ticker}** — {substitute(move.narrative, claims)}") out.append("") if brief.news_and_sentiment: out.append("## News & sentiment") out.append("") for entry in brief.news_and_sentiment: out.append(f"- **{entry.ticker}** — {substitute(entry.summary, claims)}") if entry.top_headline: source = f" _({entry.headline_source})_" if entry.headline_source else "" out.append(f" - Top headline: “{entry.top_headline}”{source}") out.append("") if brief.risk_flags: out.append("## Risk flags") out.append("") for flag in brief.risk_flags: out.append( f"- **{flag.ticker}** · _{flag.category}_ — {substitute(flag.assessment, claims)}" ) out.append(f" - Evidence: “{flag.evidence}”") out.append("") if brief.watch_items: out.append("## Watch items") out.append("") for item in brief.watch_items: prefix = f"**{item.ticker}** — " if item.ticker else "" out.append(f"- {prefix}{substitute(item.item, claims)}") out.append("") if brief.data_gaps: out.append("## Data gaps") out.append("") for gap in brief.data_gaps: out.append(f"- {gap}") out.append("") out.append("---") out.append( "_Every figure above was computed by the MCP tool layer and independently " "recomputed by a deterministic verifier before this brief was released. " "Delivery required human approval._" ) return "\n".join(out) def render_html(brief: Brief) -> str: """A self-contained dark-theme HTML email body.""" claims = brief.claims_by_id() esc = html.escape rows = "".join( "" + "".join( f"{esc(value)}" for value in [ row.ticker, row.company or "—", _cell(row.last_close, claims), _cell(row.change_1d, claims), _cell(row.return_30d, claims), _cell(row.volatility, claims), _cell(row.max_drawdown, claims), _cell(row.pe_ratio, claims), _cell(row.sentiment, claims), ] ) + "" for row in brief.snapshot ) moves = "".join( f"
  • {esc(m.ticker)} — {esc(substitute(m.narrative, claims))}
  • " for m in brief.key_moves ) news = "".join( f"
  • {esc(n.ticker)} — {esc(substitute(n.summary, claims))}" + (f"
    “{esc(n.top_headline)}”" if n.top_headline else "") + "
  • " for n in brief.news_and_sentiment ) risks = "".join( f"
  • {esc(r.ticker)} · {esc(r.category)} — " f"{esc(substitute(r.assessment, claims))}
    “{esc(r.evidence)}”
  • " for r in brief.risk_flags ) watch = "".join( f"
  • {esc((w.ticker + ' — ') if w.ticker else '')}{esc(substitute(w.item, claims))}
  • " for w in brief.watch_items ) gaps = "".join(f"
  • {esc(g)}
  • " for g in brief.data_gaps) partial_banner = ( '

    ⚠️ Partial coverage — at least one ticker ' "failed to return data.

    " if brief.partial else "" ) return f"""

    ALPHABRIEF

    {esc(substitute(brief.headline, claims))}

    {esc(brief.generated_for)}

    {partial_banner}

    {esc(substitute(brief.executive_summary, claims))}

    Snapshot

    {rows}
    TickerCompanyClose1D30D VolMax DDP/ESent.
    {f'

    Key moves

    ' if moves else ""} {f'

    News & sentiment

    ' if news else ""} {f'

    Risk flags

    ' if risks else ""} {f'

    Watch items

    ' if watch else ""} {f'

    Data gaps

    ' if gaps else ""}

    Every figure above was computed by the MCP tool layer and independently recomputed by a deterministic verifier before release. Delivery required human approval.

    """