"""Markdown rendering for Trace Field Notes analysis results.""" from __future__ import annotations from collections import defaultdict from schemas import AnalysisResult, DifficultyEpisode RECOVERY_TONE = { "smooth_recovery": "stable", "reflective_recovery": "stable", "iterative_recovery": "iterative", "detour_recovery": "detour", "partial_recovery": "partial", "failed_recovery": "unresolved", "avoidant_recovery": "unresolved", "overconfident_recovery": "risky", "unknown": "unknown", } def render_report(result: AnalysisResult) -> str: """Render a field-note style Markdown report.""" sections = [ render_header(result), render_executive_summary(result), render_model_memo(result), render_timeline(result.episodes), render_difficulty_map(result.episodes), render_detour_analysis(result.episodes), render_recovery_pattern(result), render_outcome_claim_audit(result.episodes), render_privacy_notes(result), ] return "\n\n".join(section for section in sections if section.strip()).strip() + "\n" def render_header(result: AnalysisResult) -> str: return ( f"# Trace Field Notes\n\n" f"**Trace:** {result.trace_title}\n\n" f"**Agent guess:** `{result.agent_type_guess}`\n\n" f"**Analysis scope:** {result.analysis_scope}\n\n" f"**Engine:** `{result.engine}`" ) def render_executive_summary(result: AnalysisResult) -> str: if not result.episodes: return ( "## Executive Summary\n\n" f"The trace yielded {result.narrative_message_count} visible narrative messages, but no explicit " "difficulty episode was strong enough to classify. That does not prove the session had no problems; " "it only means the uploaded narrative did not contain clear self-reported blockage, detour, or " "recovery language. Review the redacted narrative export if you expected visible difficulties." ) patterns = result.overall_patterns caveat = patterns.get("risk_or_caveat", "No caveat available.") return ( "## Executive Summary\n\n" f"This trace contains {result.narrative_message_count} visible narrative messages and " f"{len(result.episodes)} classified difficulty episode(s). " f"{patterns.get('difficulty_style', '')} " f"{patterns.get('detour_style', '')} " f"{patterns.get('recovery_style', '')} " f"{caveat} " "The report describes what the agent visibly reported and claimed; it does not verify whether the code or " "final artifact is correct." ) def render_model_memo(result: AnalysisResult) -> str: if not result.model_memo and not result.model_notes: return "" lines = ["## Small-Model Memo"] if result.model_memo: lines.append(result.model_memo.get("executive_memo", "")) lines.append(f"**Detours:** {result.model_memo.get('detour_memo', '')}") lines.append(f"**Outcome audit:** {result.model_memo.get('outcome_audit_memo', '')}") caveats = result.model_memo.get("caveats") or [] if caveats: lines.append("**Model caveats:**") lines.extend(f"- {caveat}" for caveat in caveats) if result.model_notes: lines.append("**Model notes:**") lines.extend(f"- {note}" for note in result.model_notes) return "\n\n".join(line for line in lines if line) def render_timeline(episodes: list[DifficultyEpisode]) -> str: if not episodes: return "## Journey Timeline\n\nNo difficulty timeline was detected." blocks = ["## Journey Timeline"] for episode in episodes: tone = RECOVERY_TONE.get(episode.recovery_pattern, "unknown") blocks.append( "\n".join( [ f"### {episode.episode_id} - {episode.title}", f"**Tone:** `{tone}`", f"**Intention:** {episode.initial_intention}", f"**Difficulty:** {episode.reported_difficulty}", f"**Shift:** {episode.strategy_after}", f"**Resolution mode:** `{episode.resolution_mode}`", f"**Outcome claim:** `{episode.outcome_claim}`", f"**Duration:** {episode.message_span.duration_label}", ] ) ) return "\n\n".join(blocks) def render_difficulty_map(episodes: list[DifficultyEpisode]) -> str: if not episodes: return "## Difficulty Map\n\nNo thematic difficulty clusters were detected." clusters: dict[str, list[DifficultyEpisode]] = defaultdict(list) for episode in episodes: clusters[episode.difficulty_type].append(episode) lines = ["## Difficulty Map", "Main difficulties observed:"] for difficulty_type, grouped in sorted(clusters.items()): ids = ", ".join(episode.episode_id for episode in grouped) quote = first_quote(grouped) lines.append(f"- **{difficulty_type.replace('_', ' ').title()}**: {ids}. {quote}") return "\n".join(lines) def render_detour_analysis(episodes: list[DifficultyEpisode]) -> str: if not episodes: return "## Detour Analysis\n\nNo visible detours were detected." productive = [episode.episode_id for episode in episodes if episode.productive_detour == "yes"] mixed = [episode.episode_id for episode in episodes if episode.productive_detour == "mixed"] unproductive = [episode.episode_id for episode in episodes if episode.productive_detour == "no"] lines = ["## Detour Analysis"] lines.append(detour_line("Productive detours", productive)) lines.append(detour_line("Mixed detours", mixed)) lines.append(detour_line("Unproductive or risky detours", unproductive)) for episode in episodes: if episode.detour_type == "direct_continuation": continue lines.append( f"- {episode.episode_id}: `{episode.detour_type}`. {episode.analyst_memo}" ) return "\n".join(lines) def detour_line(label: str, episode_ids: list[str]) -> str: value = ", ".join(episode_ids) if episode_ids else "none detected" return f"- **{label}:** {value}" def render_recovery_pattern(result: AnalysisResult) -> str: patterns = result.overall_patterns return ( "## Recovery Pattern\n\n" f"{patterns.get('recovery_style', 'No recovery pattern was classified.')} " f"{patterns.get('difficulty_style', '')} " f"{patterns.get('detour_style', '')}" ) def render_outcome_claim_audit(episodes: list[DifficultyEpisode]) -> str: if not episodes: return ( "## Outcome Claim Audit\n\n" "No explicit outcome claims were attached to difficulty episodes." ) lines = ["## Outcome Claim Audit"] for episode in episodes: evidence = "; ".join(f'"{quote}"' for quote in episode.evidence_quotes[:2]) lines.append( f"- **{episode.episode_id}:** `{episode.outcome_claim}`. " f"Recovery: `{episode.recovery_pattern}`. Evidence: {evidence or 'no short quote available'}." ) return "\n".join(lines) def render_privacy_notes(result: AnalysisResult) -> str: lines = [ "## Privacy Notes", f"Redaction count: {result.redaction_count}", ] lines.extend(f"- {note}" for note in result.privacy_notes) return "\n".join(lines) def first_quote(episodes: list[DifficultyEpisode]) -> str: for episode in episodes: if episode.evidence_quotes: return f'Example: "{episode.evidence_quotes[0]}"' return "No short evidence quote was available."