"""Render a schema-valid case+note dict into classroom-ready Markdown. The hero of Case Forge is that the output is *usable in class*, not a wall of JSON. This turns the contract (`data/schema.py`) into a well-structured case and teaching note, and surfaces the genre's quality checks as visible badges. """ from __future__ import annotations # Section labels per language (UI chrome lives in shared/i18n; these are the # document headings, kept next to the renderer that emits them). _L = { "case": { "en": { "decision": "The decision", "protagonist": "Protagonist", "context": "Context", "data": "Data & evidence", "exhibits": "Exhibits", "alternatives": "Paths to weigh", "closing": "Where it stands", "references": "References", }, "pt": { "decision": "A decisão", "protagonist": "Protagonista", "context": "Contexto", "data": "Dados e evidências", "exhibits": "Anexos", "alternatives": "Caminhos a ponderar", "closing": "Onde isso para", "references": "Referências", }, }, "note": { "en": { "title": "Teaching note", "summary": "Summary", "audience": "Audience", "relevance": "Managerial relevance", "objectives": "Learning objectives", "sources": "Data sources", "anchor": "Theoretical anchor", "plan": "Discussion plan", "questions": "Discussion questions", "analysis": "Analysis & expected answers", "closure": "Closure", "epilogue": "Epilogue", "biblio": "Bibliography", "min": "min", }, "pt": { "title": "Nota de ensino", "summary": "Resumo", "audience": "Público-alvo", "relevance": "Relevância gerencial", "objectives": "Objetivos de aprendizagem", "sources": "Fontes dos dados", "anchor": "Ancoragem teórica", "plan": "Plano de discussão", "questions": "Questões de discussão", "analysis": "Análise e respostas esperadas", "closure": "Fechamento", "epilogue": "Epílogo", "biblio": "Bibliografia", "min": "min", }, }, } def _lang(obj: dict) -> str: return obj.get("language") if obj.get("language") in ("pt", "en") else "pt" def render_case(obj: dict) -> str: """The case itself, as Markdown — stops at the decision point.""" if not obj: return "" lang = _lang(obj) L = _L["case"][lang] c = obj.get("case") or {} out: list[str] = [] out.append(f"# {obj.get('title', '').strip()}") domain = obj.get("domain", "").strip() if domain: out.append(f"*{domain}*") out.append("") if c.get("hook"): out.append(c["hook"].strip()) out.append("") if c.get("protagonist"): out.append(f"**{L['protagonist']}:** {c['protagonist'].strip()}") out.append("") if c.get("decision_point"): out.append(f"> **{L['decision']}:** {c['decision_point'].strip()}") out.append("") if c.get("context"): out.append(f"## {L['context']}") out.append(c["context"].strip()) out.append("") if c.get("data"): out.append(f"## {L['data']}") out += [f"- {d.strip()}" for d in c["data"]] out.append("") if c.get("exhibits"): out.append(f"## {L['exhibits']}") for ex in c["exhibits"]: out.append(f"**{ex.get('title', '').strip()}**") out.append("") out.append(str(ex.get("content", "")).strip()) out.append("") if c.get("alternatives"): out.append(f"## {L['alternatives']}") out += [f"- {a.strip()}" for a in c["alternatives"]] out.append("") if c.get("closing"): out.append(f"## {L['closing']}") out.append(c["closing"].strip()) out.append("") if c.get("references"): out.append(f"## {L['references']}") out += [f"- {r.strip()}" for r in c["references"]] out.append("") return "\n".join(out).strip() def render_note(obj: dict) -> str: """The teaching note, as Markdown — instructor-facing (the epilogue lives here).""" if not obj: return "" lang = _lang(obj) L = _L["note"][lang] n = obj.get("teaching_note") or {} out: list[str] = [f"# {L['title']}", ""] def block(label_key: str, text): if text: out.append(f"## {L[label_key]}") out.append(str(text).strip()) out.append("") def bullets(label_key: str, items, ordered=False): if items: out.append(f"## {L[label_key]}") for i, it in enumerate(items, 1): out.append(f"{i}. {str(it).strip()}" if ordered else f"- {str(it).strip()}") out.append("") block("summary", n.get("summary")) block("audience", n.get("audience")) block("relevance", n.get("managerial_relevance")) bullets("objectives", n.get("learning_objectives"), ordered=True) block("sources", n.get("data_sources")) bullets("anchor", n.get("theoretical_anchor")) plan = n.get("discussion_plan") or [] if plan: out.append(f"## {L['plan']}") for b in plan: mins = b.get("minutes") head = f"**{b.get('block', '').strip()}**" if mins: head += f" — {mins} {L['min']}" out.append(head) if b.get("activity"): out.append(f" {b['activity'].strip()}") out.append("") bullets("questions", n.get("discussion_questions"), ordered=True) block("analysis", n.get("analysis")) block("closure", n.get("closure")) block("epilogue", n.get("epilogue")) bullets("biblio", n.get("bibliography")) return "\n".join(out).strip() def quality_flags(obj: dict, errors: list[str], warnings: list[str]) -> dict[str, bool]: """Map the validator result to the four classroom quality checks the UI shows.""" note = (obj or {}).get("teaching_note") or {} objs = note.get("learning_objectives") or [] leak = any("revelar a decisão" in w or "reveal" in w.lower() for w in warnings) no_source = any("citar fonte" in w or "sourced" in w.lower() for w in warnings) return { "schema": not errors, "noleak": not leak, "objectives": 1 <= len(objs) <= 4, "sourced": not no_source, } __all__ = ["render_case", "render_note", "quality_flags"]