| """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 |
|
|
| |
| |
| _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"] |
|
|