You are a software architect helping improve a deep_dive analysis module. Based on the code analysis below, design a detailed implementation plan for enhancing the module's depth and quality.

## Current Implementation Summary

### Pipeline: fetch → analyze → research → evaluate → report

**models.py** contains:
- `ArticleContent`: title, url, raw_text
- `SearchQueries`: supporting/opposing/expert/fact_checks lists
- `StructuralAnalysis`: main_thesis, sub_claims, logical_structure, key_entities, verifiable_facts, search_queries
- `WebSource`: title, url, snippet (300 chars max)
- `FactCheck`: claim, status, evidence, source_url
- `ResearchResults`: supporting/opposing/expert/fact_checks lists
- `MultiPerspectiveEvaluation`: pro_arguments, con_arguments, neutral_context, bias_assessment (string), credibility_score (float), overall_verdict, further_exploration
- `DeepDiveReport`: article, structure, research, evaluation, generated_at, report_path

**analyzer.py**: Claude LLM step that extracts main_thesis, sub_claims, logical_structure, key_entities, verifiable_facts, and generates search_queries. Chinese output.

**researcher.py**: Exa.ai search step. Searches 4 categories (supporting/opposing/expert/fact_checks). 3 results per query, 2 queries max per category. **CRITICAL BUG**: FactCheck objects are created with `status="unverifiable"` hardcoded — the status is NEVER actually evaluated; the researcher just finds a URL and marks it unverifiable.

**evaluator.py**: Claude LLM step. Takes StructuralAnalysis + ResearchResults. Generates pro/con/neutral arguments and credibility score. Prompt asks for: pro_arguments, con_arguments, neutral_context, bias_assessment (free text), credibility_score (0-1), overall_verdict, further_exploration. Input is truncated to 6000 chars with source snippets at 150 chars each.

**reporter.py**: Renders Markdown. Sections: 核心主张, 论证结构, 互联网多维检索, 事实核查 (table), 多角度评价, 综合评估, 延伸探索建议. Arguments are bare bullet points with NO citations linking to sources.

## Identified Weaknesses

1. **Fact-check status bug**: Status is always "unverifiable" — the evaluator never assesses whether claims are verified/disputed/false
2. **Arguments lack citations**: pro_arguments and con_arguments are bare text with no links to specific sources that support them
3. **No logical quality assessment**: The `logical_structure` field names the type (e.g., "因果论证") but doesn't assess whether the reasoning is valid or contains fallacies
4. **Bias assessment is vague**: Just a free-text string, not structured by bias type
5. **Credibility score lacks explanation**: The score (0.72) appears but there's no breakdown of WHY
6. **No synthesis/insight section**: The report presents information but doesn't synthesize "what does this all mean?"
7. **No key disputes section**: What are the core disagreements? What's actually being contested?
8. **Source authority not assessed**: A blog post and a Nature paper get equal treatment

## Design Requirements

The user wants improvements to make reports:
1. More rigorously evidence-based (verify evidence quality, not just presence)
2. Provide deeper argument explanation and critical thinking
3. More insightful and profound analysis (深刻)

## Constraints
- All Chinese output except search queries
- Test-driven development: tests must be written before/alongside implementation
- Uses Pydantic models for all data structures
- Claude LLM calls have `run()` and `_call_llm()` methods; tests mock `_call_llm`
- parse_llm_json() strips markdown fences before JSON parse
- Avoid adding extra LLM calls if possible (cost/time) — prefer extending existing prompts
- File paths: src/ai_usecases_explorer/deep_dive/{models,analyzer,researcher,evaluator,reporter}.py

## Design Task

Please design a detailed implementation plan covering:

1. **What to change in models.py**: Which new fields to add to existing models (prefer extending over new models)
2. **What to change in evaluator.py**: How to fix fact-check status assessment + add logical quality + structured bias + argument citations + synthesis section. Should this be one enhanced LLM call or two?
3. **What to change in reporter.py**: How to render the new fields in the Markdown report
4. **Test strategy**: What new tests are needed for each change

Focus on changes that give maximum analytical value with minimum complexity. Prioritize:
- Fix the fact-check bug (fix status assessment)
- Add evidence citations to arguments (link args to sources)
- Add logical quality/fallacy detection
- Add core disputes identification
- Better credibility score explanation

Provide specific field names, JSON schema for LLM prompts, and Markdown rendering examples.


I'll explore the codebase thoroughly before designing the implementation plan.


Bash

find /root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive -type f | sort

List all files in the deep_dive module


find /root/projects/ai_usecases_explorer/tests -type f | sort

List all test files


     1→"""Pydantic data models for the Deep Dive skill."""
     2→from __future__ import annotations
     3→
     4→from datetime import datetime
     5→from pathlib import Path
     6→
     7→from pydantic import BaseModel
     8→
     9→
    10→class ArticleContent(BaseModel):
    11→    """Represents the raw content of an article to be analysed."""
    12→
    13→    title: str
    14→    url: str | None
    15→    raw_text: str
    16→
    17→
    18→class SearchQueries(BaseModel):
    19→    """Search queries generated by the structural analyser for web research."""
    20→
    21→    supporting: list[str]
    22→    opposing: list[str]
    23→    expert: list[str]
    24→    fact_checks: list[str]
    25→
    26→
    27→class StructuralAnalysis(BaseModel):
    28→    """Structural breakdown of an article's argument and logic."""
    29→
    30→    main_thesis: str
    31→    sub_claims: list[str]
    32→    logical_structure: str
    33→    key_entities: list[str]
    34→    verifiable_facts: list[str]
    35→    search_queries: SearchQueries
    36→
    37→
    38→class WebSource(BaseModel):
    39→    """A single web search result used as evidence."""
    40→
    41→    title: str
    42→    url: str
    43→    snippet: str
    44→
    45→
    46→class FactCheck(BaseModel):
    47→    """Result of fact-checking a single verifiable claim."""
    48→
    49→    claim: str
    50→    status: str  # "verified" | "disputed" | "unverifiable" | "false"
    51→    evidence: str
    52→    source_url: str | None
    53→
    54→
    55→class ResearchResults(BaseModel):
    56→    """Aggregated web research from all query categories."""
    57→
    58→    supporting: list[WebSource]
    59→    opposing: list[WebSource]
    60→    expert: list[WebSource]
    61→    fact_checks: list[FactCheck]
    62→
    63→
    64→class MultiPerspectiveEvaluation(BaseModel):
    65→    """Multi-perspective evaluation of an article's claims."""
    66→
    67→    pro_arguments: list[str]
    68→    con_arguments: list[str]
    69→    neutral_context: list[str]
    70→    bias_assessment: str
    71→    credibility_score: float  # 0.0–1.0
    72→    overall_verdict: str
    73→    further_exploration: list[str]
    74→
    75→
    76→class DeepDiveReport(BaseModel):
    77→    """Complete deep-dive analysis report."""
    78→
    79→    article: ArticleContent
    80→    structure: StructuralAnalysis
    81→    research: ResearchResults
    82→    evaluation: MultiPerspectiveEvaluation
    83→    generated_at: datetime
    84→    report_path: Path | None = None
    85→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Step 3 — Multi-perspective evaluator: synthesises research into a verdict."""
     2→from __future__ import annotations
     3→
     4→import anthropic
     5→
     6→from ai_usecases_explorer.deep_dive.models import (
     7→    MultiPerspectiveEvaluation,
     8→    ResearchResults,
     9→    StructuralAnalysis,
    10→)
    11→from ai_usecases_explorer.processors.utils import parse_llm_json
    12→
    13→_SYSTEM = """\
    14→你是一位多角度批判性评估专家。请基于提供的文章结构分析和网络研究结果，从正方、反方和中性角度综合评价该文章。
    15→
    16→【语言要求】所有输出字段必须使用中文，无论输入材料是何种语言。
    17→
    18→请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
    19→{
    20→  "pro_arguments": ["支持该文章核心主张的中文论据1", "中文论据2"],
    21→  "con_arguments": ["质疑或反驳该文章核心主张的中文论据1", "中文论据2"],
    22→  "neutral_context": ["重要的中文背景信息或上下文1", "中文背景2"],
    23→  "bias_assessment": "对文章潜在偏见的中文识别与评估",
    24→  "credibility_score": 0.0到1.0之间的浮点数（基于证据质量、来源多样性、论证严密性）,
    25→  "overall_verdict": "中文综合评估结论（2-3句话）",
    26→  "further_exploration": ["建议进一步探索的中文方向或问题1", "中文方向2"]
    27→}
    28→
    29→评分标准：
    30→- 0.8-1.0：高度可信，证据充分，论证严密
    31→- 0.6-0.8：基本可信，有一定支撑，但存在一些不足
    32→- 0.4-0.6：部分可信，混合证据，需要更多核实
    33→- 0.2-0.4：低可信度，证据薄弱或存在明显偏见
    34→- 0.0-0.2：基本不可信，缺乏证据或含有错误信息
    35→"""
    36→
    37→
    38→class MultiPerspectiveEvaluator:
    39→    """Uses Claude to evaluate an article from multiple perspectives."""
    40→
    41→    def __init__(self, client: anthropic.Anthropic, model: str) -> None:
    42→        self._client = client
    43→        self._model = model
    44→
    45→    def run(
    46→        self, structure: StructuralAnalysis, research: ResearchResults
    47→    ) -> MultiPerspectiveEvaluation:
    48→        """Generate a multi-perspective evaluation.
    49→
    50→        Args:
    51→            structure: The structural analysis of the article.
    52→            research: Web research results from all categories.
    53→
    54→        Returns:
    55→            A MultiPerspectiveEvaluation with arguments, score, and verdict.
    56→        """
    57→        raw = self._call_llm(structure, research)
    58→        return MultiPerspectiveEvaluation(
    59→            pro_arguments=raw["pro_arguments"],
    60→            con_arguments=raw["con_arguments"],
    61→            neutral_context=raw["neutral_context"],
    62→            bias_assessment=raw["bias_assessment"],
    63→            credibility_score=float(raw["credibility_score"]),
    64→            overall_verdict=raw["overall_verdict"],
    65→            further_exploration=raw["further_exploration"],
    66→        )
    67→
    68→    def _call_llm(self, structure: StructuralAnalysis, research: ResearchResults) -> dict:
    69→        structure_summary = (
    70→            f"核心主张：{structure.main_thesis}\n"
    71→            f"主要论点：{'; '.join(structure.sub_claims)}\n"
    72→            f"论证方式：{structure.logical_structure}\n"
    73→            f"关键实体：{', '.join(structure.key_entities)}\n"
    74→            f"可核实事实：{'; '.join(structure.verifiable_facts)}"
    75→        )
    76→
    77→        def _fmt_sources(sources: list) -> str:
    78→            return "\n".join(f"- {s.title}: {s.snippet[:150]}" for s in sources[:4])
    79→
    80→        research_summary = (
    81→            f"支持性来源：\n{_fmt_sources(research.supporting)}\n\n"
    82→            f"反对性来源：\n{_fmt_sources(research.opposing)}\n\n"
    83→            f"权威来源：\n{_fmt_sources(research.expert)}"
    84→        )
    85→
    86→        content = (
    87→            f"=== 文章结构分析 ===\n{structure_summary}\n\n"
    88→            f"=== 网络研究结果 ===\n{research_summary}"
    89→        )[:6000]
    90→
    91→        message = self._client.messages.create(
    92→            model=self._model,
    93→            max_tokens=2048,
    94→            system=_SYSTEM,
    95→            messages=[
    96→                {"role": "user", "content": content},
    97→                {"role": "assistant", "content": "{"},
    98→            ],
    99→        )
   100→        return parse_llm_json("{" + message.content[0].text)
   101→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Step 2 — Web researcher: multi-perspective Exa.ai search."""
     2→from __future__ import annotations
     3→
     4→import logging
     5→
     6→from exa_py import Exa  # type: ignore[import-untyped]
     7→
     8→from ai_usecases_explorer.deep_dive.models import (
     9→    FactCheck,
    10→    ResearchResults,
    11→    SearchQueries,
    12→    WebSource,
    13→)
    14→
    15→logger = logging.getLogger(__name__)
    16→
    17→_RESULTS_PER_QUERY = 3
    18→_MAX_QUERIES_PER_CATEGORY = 2
    19→_FACT_CHECK_RESULTS = 2
    20→
    21→
    22→class WebResearcher:
    23→    """Performs multi-perspective web research using Exa.ai."""
    24→
    25→    def __init__(self, exa_client: Exa) -> None:
    26→        self._exa = exa_client
    27→
    28→    def run(self, queries: SearchQueries) -> ResearchResults:
    29→        """Execute web searches for all query categories.
    30→
    31→        Args:
    32→            queries: SearchQueries with supporting, opposing, expert, and fact-check terms.
    33→
    34→        Returns:
    35→            ResearchResults with deduplicated sources for each category.
    36→        """
    37→        seen_urls: set[str] = set()
    38→
    39→        supporting = self._search_category(
    40→            queries.supporting[:_MAX_QUERIES_PER_CATEGORY],
    41→            seen_urls,
    42→            num_results=_RESULTS_PER_QUERY,
    43→        )
    44→        opposing = self._search_category(
    45→            queries.opposing[:_MAX_QUERIES_PER_CATEGORY],
    46→            seen_urls,
    47→            num_results=_RESULTS_PER_QUERY,
    48→        )
    49→        expert = self._search_category(
    50→            queries.expert[:_MAX_QUERIES_PER_CATEGORY],
    51→            seen_urls,
    52→            num_results=_RESULTS_PER_QUERY,
    53→            category="research paper",
    54→        )
    55→        fact_checks = self._search_fact_checks(
    56→            queries.fact_checks[:_MAX_QUERIES_PER_CATEGORY],
    57→        )
    58→
    59→        return ResearchResults(
    60→            supporting=supporting,
    61→            opposing=opposing,
    62→            expert=expert,
    63→            fact_checks=fact_checks,
    64→        )
    65→
    66→    def _search_category(
    67→        self,
    68→        queries: list[str],
    69→        seen_urls: set[str],
    70→        num_results: int,
    71→        category: str | None = None,
    72→    ) -> list[WebSource]:
    73→        """Search Exa for a list of queries, deduplicating by URL.
    74→
    75→        Args:
    76→            queries: Search terms to query.
    77→            seen_urls: Mutable set of already-seen URLs for deduplication.
    78→            num_results: Number of results to request per query.
    79→            category: Optional Exa result category filter.
    80→
    81→        Returns:
    82→            List of WebSource objects with unique URLs.
    83→        """
    84→        sources: list[WebSource] = []
    85→        for query in queries:
    86→            try:
    87→                kwargs: dict = {
    88→                    "text": True,
    89→                    "num_results": num_results,
    90→                }
    91→                if category:
    92→                    kwargs["category"] = category
    93→                response = self._exa.search_and_contents(query, **kwargs)
    94→            except Exception as exc:
    95→                logger.warning("Exa search failed for query '%s': %s", query, exc)
    96→                continue
    97→
    98→            for result in response.results:
    99→                url = result.url or ""
   100→                if not url or url in seen_urls:
   101→                    continue
   102→                seen_urls.add(url)
   103→                sources.append(
   104→                    WebSource(
   105→                        title=result.title or url,
   106→                        url=url,
   107→                        snippet=(result.text or "")[:300],
   108→                    )
   109→                )
   110→        return sources
   111→
   112→    def _search_fact_checks(self, queries: list[str]) -> list[FactCheck]:
   113→        """Search Exa for fact-checking evidence on specific claims.
   114→
   115→        Args:
   116→            queries: Search terms targeting specific verifiable facts.
   117→
   118→        Returns:
   119→            List of FactCheck objects (status defaults to "unverifiable" — Claude
   120→            will assess them in the evaluation step).
   121→        """
   122→        fact_checks: list[FactCheck] = []
   123→        seen_claims: set[str] = set()
   124→
   125→        for query in queries:
   126→            if query in seen_claims:
   127→                continue
   128→            seen_claims.add(query)
   129→            try:
   130→                response = self._exa.search_and_contents(
   131→                    query,
   132→                    text=True,
   133→                    num_results=_FACT_CHECK_RESULTS,
   134→                )
   135→            except Exception as exc:
   136→                logger.warning("Exa fact-check search failed for '%s': %s", query, exc)
   137→                continue
   138→
   139→            for result in response.results:
   140→                url = result.url or ""
   141→                snippet = (result.text or "")[:300]
   142→                fact_checks.append(
   143→                    FactCheck(
   144→                        claim=query,
   145→                        status="unverifiable",
   146→                        evidence=snippet,
   147→                        source_url=url or None,
   148→                    )
   149→                )
   150→                break  # One best result per claim
   151→
   152→        return fact_checks
   153→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Step 4 — Report generator: writes Deep Dive analysis to Obsidian Markdown."""
     2→from __future__ import annotations
     3→
     4→import re
     5→from pathlib import Path
     6→
     7→from ai_usecases_explorer.deep_dive.models import DeepDiveReport
     8→
     9→_STATUS_EMOJI = {
    10→    "verified": "✅",
    11→    "disputed": "⚠",
    12→    "false": "❌",
    13→    "unverifiable": "❓",
    14→}
    15→
    16→
    17→def _slug(text: str, max_len: int = 40) -> str:
    18→    """Convert text to a URL-safe slug."""
    19→    text = text.lower()
    20→    text = re.sub(r"[^\w\s-]", "", text)
    21→    text = re.sub(r"[\s_]+", "-", text.strip())
    22→    return text[:max_len].rstrip("-")
    23→
    24→
    25→class DeepDiveReporter:
    26→    """Writes a Deep Dive analysis report to an Obsidian vault directory."""
    27→
    28→    def __init__(self, report_dir: Path) -> None:
    29→        self._report_dir = report_dir
    30→
    31→    def write(self, report: DeepDiveReport) -> Path:
    32→        """Render the report and write it to disk.
    33→
    34→        Args:
    35→            report: The completed DeepDiveReport.
    36→
    37→        Returns:
    38→            The Path of the written Markdown file.
    39→        """
    40→        self._report_dir.mkdir(parents=True, exist_ok=True)
    41→        timestamp = report.generated_at.strftime("%Y-%m-%d-%H%M")
    42→        slug = _slug(report.article.title)
    43→        filename = f"{timestamp}-{slug}.md"
    44→        path = self._report_dir / filename
    45→        path.write_text(self._render_markdown(report), encoding="utf-8")
    46→        return path
    47→
    48→    def _render_markdown(self, report: DeepDiveReport) -> str:
    49→        """Render the full Markdown report string.
    50→
    51→        Args:
    52→            report: The completed DeepDiveReport.
    53→
    54→        Returns:
    55→            A Markdown-formatted string ready to be written to disk.
    56→        """
    57→        r = report
    58→        st = r.structure
    59→        ev = r.evaluation
    60→        rs = r.research
    61→
    62→        source_label = r.article.url or "手动输入"
    63→        timestamp_str = r.generated_at.strftime("%Y-%m-%d %H:%M")
    64→
    65→        lines: list[str] = []
    66→
    67→        # Header
    68→        lines += [
    69→            f"# 深度探索：{r.article.title}",
    70→            "",
    71→            f"> 分析时间：{timestamp_str} ｜ 来源：{source_label}",
    72→            "",
    73→            "## 核心主张",
    74→            st.main_thesis,
    75→            "",
    76→            "## 论证结构",
    77→            f"**论证方式**：{st.logical_structure}",
    78→            "",
    79→            "### 主要论点",
    80→        ]
    81→        for i, claim in enumerate(st.sub_claims, 1):
    82→            lines.append(f"{i}. {claim}")
    83→        lines += [
    84→            "",
    85→            "### 关键实体",
    86→            "，".join(st.key_entities),
    87→            "",
    88→            "## 互联网多维检索",
    89→            "",
    90→            "### 📗 支持性证据",
    91→        ]
    92→        if rs.supporting:
    93→            for src in rs.supporting:
    94→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
    95→        else:
    96→            lines.append("*未找到相关支持性来源*")
    97→
    98→        lines += ["", "### 📕 反对与质疑"]
    99→        if rs.opposing:
   100→            for src in rs.opposing:
   101→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
   102→        else:
   103→            lines.append("*未找到相关反对性来源*")
   104→
   105→        lines += ["", "### 🎓 权威观点"]
   106→        if rs.expert:
   107→            for src in rs.expert:
   108→                lines.append(f"- **{src.title}**：{src.snippet} — {src.url}")
   109→        else:
   110→            lines.append("*未找到相关权威来源*")
   111→
   112→        # Fact checks table
   113→        lines += ["", "## 事实核查", ""]
   114→        if rs.fact_checks:
   115→            lines += ["| 声明 | 状态 | 依据 | 来源 |", "|------|------|------|------|"]
   116→            for fc in rs.fact_checks:
   117→                emoji = _STATUS_EMOJI.get(fc.status, "❓")
   118→                url_cell = fc.source_url or "-"
   119→                evidence_short = fc.evidence[:80].replace("|", "｜")
   120→                lines.append(
   121→                    f"| {fc.claim} | {emoji} {fc.status} | {evidence_short} | {url_cell} |"
   122→                )
   123→        else:
   124→            lines.append("*未执行事实核查*")
   125→
   126→        # Evaluation
   127→        lines += [
   128→            "",
   129→            "## 多角度评价",
   130→            "",
   131→            "### 正方：支持论点",
   132→        ]
   133→        for arg in ev.pro_arguments:
   134→            lines.append(f"- {arg}")
   135→
   136→        lines += ["", "### 反方：质疑与挑战"]
   137→        for arg in ev.con_arguments:
   138→            lines.append(f"- {arg}")
   139→
   140→        lines += ["", "### 中性背景"]
   141→        for ctx in ev.neutral_context:
   142→            lines.append(f"- {ctx}")
   143→
   144→        # Summary table
   145→        lines += [
   146→            "",
   147→            "## 综合评估",
   148→            "",
   149→            "| 维度 | 评分/结论 |",
   150→            "|------|----------|",
   151→            f"| 可信度 | {ev.credibility_score:.2f} / 1.0 |",
   152→            f"| 偏见识别 | {ev.bias_assessment} |",
   153→            f"| 总体结论 | {ev.overall_verdict} |",
   154→            "",
   155→            "### 延伸探索建议",
   156→        ]
   157→        for item in ev.further_exploration:
   158→            lines.append(f"- {item}")
   159→
   160→        lines += [
   161→            "",
   162→            "---",
   163→            f"*Deep Dive Skill · Exa.ai + Claude Sonnet 4.6 · 生成时间：{timestamp_str}*",
   164→        ]
   165→
   166→        return "\n".join(lines)
   167→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Step 1 — Structural analyser: extracts argument structure from an article."""
     2→from __future__ import annotations
     3→
     4→import anthropic
     5→
     6→from ai_usecases_explorer.deep_dive.models import ArticleContent, SearchQueries, StructuralAnalysis
     7→from ai_usecases_explorer.processors.utils import parse_llm_json
     8→
     9→_SYSTEM = """\
    10→你是一位批判性思维分析专家。请分析给定文章，提取其论证结构。
    11→
    12→【语言要求】除 search_queries 字段外，所有文本字段必须使用中文输出，无论原文是何种语言。
    13→
    14→请严格以 JSON 格式返回（无 markdown 代码块），包含以下字段：
    15→{
    16→  "main_thesis": "核心主张（中文一句话总结）",
    17→  "sub_claims": ["中文支撑论点1", "中文支撑论点2"],
    18→  "logical_structure": "论证方式（如：因果论证、比较分析、叙事论证、演绎推理）",
    19→  "key_entities": ["关键人物/机构/事件（中文注释）1", "关键人物/机构/事件（中文注释）2"],
    20→  "verifiable_facts": ["中文可查证的具体事实声明1", "中文可查证的具体事实声明2"],
    21→  "search_queries": {
    22→    "supporting": ["英文搜索词（查找支持论点的互联网资料）"],
    23→    "opposing": ["英文搜索词（查找反对或质疑该论点的资料）"],
    24→    "expert": ["英文搜索词（查找权威/学术来源）"],
    25→    "fact_checks": ["英文搜索词（核查具体事实声明）"]
    26→  }
    27→}
    28→
    29→要求：
    30→- main_thesis、sub_claims、key_entities、verifiable_facts、logical_structure 必须全部用中文
    31→- main_thesis 必须是一句话，清晰表达文章的核心主张
    32→- sub_claims 列举 2-5 个支撑论点
    33→- verifiable_facts 只包含可以通过外部来源核实的具体声明（含数字、日期、事件）
    34→- search_queries 保持英文以获得更好的互联网搜索结果，每类最多 2 个搜索词
    35→"""
    36→
    37→
    38→class StructuralAnalyzer:
    39→    """Uses Claude to extract argument structure from an article."""
    40→
    41→    def __init__(self, client: anthropic.Anthropic, model: str) -> None:
    42→        self._client = client
    43→        self._model = model
    44→
    45→    def run(self, article: ArticleContent) -> StructuralAnalysis:
    46→        """Analyse an article and return its structural breakdown.
    47→
    48→        Args:
    49→            article: The article content to analyse.
    50→
    51→        Returns:
    52→            A StructuralAnalysis with thesis, claims, and search queries.
    53→        """
    54→        raw = self._call_llm(article)
    55→        return StructuralAnalysis(
    56→            main_thesis=raw["main_thesis"],
    57→            sub_claims=raw["sub_claims"],
    58→            logical_structure=raw["logical_structure"],
    59→            key_entities=raw["key_entities"],
    60→            verifiable_facts=raw["verifiable_facts"],
    61→            search_queries=SearchQueries(**raw["search_queries"]),
    62→        )
    63→
    64→    def _call_llm(self, article: ArticleContent) -> dict:
    65→        content = f"标题：{article.title}\n\n正文：\n{article.raw_text[:6000]}"
    66→        message = self._client.messages.create(
    67→            model=self._model,
    68→            max_tokens=1024,
    69→            system=_SYSTEM,
    70→            messages=[
    71→                {"role": "user", "content": content},
    72→                {"role": "assistant", "content": "{"},
    73→            ],
    74→        )
    75→        return parse_llm_json("{" + message.content[0].text)
    76→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


1→"""TDD tests for the deep_dive module (all written BEFORE implementation)."""
     2→from __future__ import annotations
     3→
     4→from datetime import datetime
     5→from pathlib import Path
     6→from unittest.mock import MagicMock, patch
     7→
     8→import pytest
     9→from pytest_httpx import HTTPXMock
    10→
    11→from ai_usecases_explorer.deep_dive.models import (
    12→    ArticleContent,
    13→    DeepDiveReport,
    14→    FactCheck,
    15→    MultiPerspectiveEvaluation,
    16→    ResearchResults,
    17→    SearchQueries,
    18→    StructuralAnalysis,
    19→    WebSource,
    20→)
    21→
    22→# ---------------------------------------------------------------------------
    23→# Fixtures / helpers
    24→# ---------------------------------------------------------------------------
    25→
    26→
    27→def _article() -> ArticleContent:
    28→    return ArticleContent(
    29→        title="The Rise of AI Agents",
    30→        url="https://example.com/ai-agents",
    31→        raw_text="AI agents are transforming software development. They can write code, "
    32→        "browse the web, and execute tasks autonomously. Critics argue this creates risks.",
    33→    )
    34→
    35→
    36→def _search_queries() -> SearchQueries:
    37→    return SearchQueries(
    38→        supporting=["AI agents benefits productivity"],
    39→        opposing=["AI agents risks criticism"],
    40→        expert=["AI agents research paper 2025"],
    41→        fact_checks=["AI agents market adoption statistics"],
    42→    )
    43→
    44→
    45→def _structural_analysis() -> StructuralAnalysis:
    46→    return StructuralAnalysis(
    47→        main_thesis="AI agents will replace traditional software engineering workflows.",
    48→        sub_claims=[
    49→            "Agents can autonomously execute multi-step tasks",
    50→            "The technology is production-ready in 2025",
    51→        ],
    52→        logical_structure="因果论证",
    53→        key_entities=["Anthropic", "OpenAI", "GitHub Copilot"],
    54→        verifiable_facts=[
    55→            "GitHub Copilot has 1 million users",
    56→            "Claude 3.5 Sonnet released in 2024",
    57→        ],
    58→        search_queries=_search_queries(),
    59→    )
    60→
    61→
    62→def _web_source(url: str = "https://example.com/source") -> WebSource:
    63→    return WebSource(
    64→        title="Example Source",
    65→        url=url,
    66→        snippet="This source supports the claim about AI agents.",
    67→    )
    68→
    69→
    70→def _fact_check() -> FactCheck:
    71→    return FactCheck(
    72→        claim="GitHub Copilot has 1 million users",
    73→        status="verified",
    74→        evidence="GitHub official blog confirms 1M+ users as of 2024",
    75→        source_url="https://github.blog/copilot-1m-users",
    76→    )
    77→
    78→
    79→def _research_results() -> ResearchResults:
    80→    return ResearchResults(
    81→        supporting=[_web_source("https://example.com/pro")],
    82→        opposing=[_web_source("https://example.com/con")],
    83→        expert=[_web_source("https://example.com/expert")],
    84→        fact_checks=[_fact_check()],
    85→    )
    86→
    87→
    88→def _evaluation() -> MultiPerspectiveEvaluation:
    89→    return MultiPerspectiveEvaluation(
    90→        pro_arguments=["Increases developer productivity by 40%", "Reduces repetitive tasks"],
    91→        con_arguments=["May introduce security vulnerabilities", "Reduces junior developer learning"],
    92→        neutral_context=["Technology is still evolving", "Adoption varies by team"],
    93→        bias_assessment="Author works for an AI company, potential conflict of interest",
    94→        credibility_score=0.72,
    95→        overall_verdict="Partially credible with some unsupported claims",
    96→        further_exploration=["Search for peer-reviewed studies on AI coding tools"],
    97→    )
    98→
    99→
   100→def _report(tmp_path: Path) -> DeepDiveReport:
   101→    return DeepDiveReport(
   102→        article=_article(),
   103→        structure=_structural_analysis(),
   104→        research=_research_results(),
   105→        evaluation=_evaluation(),
   106→        generated_at=datetime(2026, 2, 28, 14, 30, 0),
   107→        report_path=tmp_path / "test-report.md",
   108→    )
   109→
   110→
   111→# ---------------------------------------------------------------------------
   112→# Pydantic model validation tests
   113→# ---------------------------------------------------------------------------
   114→
   115→
   116→class TestArticleContent:
   117→    def test_valid_model_with_url(self) -> None:
   118→        article = ArticleContent(
   119→            title="Test",
   120→            url="https://example.com",
   121→            raw_text="Some text content",
   122→        )
   123→        assert article.title == "Test"
   124→        assert article.url == "https://example.com"
   125→        assert article.raw_text == "Some text content"
   126→
   127→    def test_valid_model_without_url(self) -> None:
   128→        article = ArticleContent(title="Test", url=None, raw_text="Direct text input")
   129→        assert article.url is None
   130→
   131→    def test_requires_title(self) -> None:
   132→        with pytest.raises(Exception):
   133→            ArticleContent(url="https://example.com", raw_text="text")  # type: ignore[call-arg]
   134→
   135→    def test_requires_raw_text(self) -> None:
   136→        with pytest.raises(Exception):
   137→            ArticleContent(title="Test", url=None)  # type: ignore[call-arg]
   138→
   139→
   140→class TestSearchQueries:
   141→    def test_valid_model(self) -> None:
   142→        q = _search_queries()
   143→        assert len(q.supporting) == 1
   144→        assert len(q.opposing) == 1
   145→        assert len(q.expert) == 1
   146→        assert len(q.fact_checks) == 1
   147→
   148→    def test_empty_lists_allowed(self) -> None:
   149→        q = SearchQueries(supporting=[], opposing=[], expert=[], fact_checks=[])
   150→        assert q.supporting == []
   151→
   152→
   153→class TestStructuralAnalysis:
   154→    def test_valid_model(self) -> None:
   155→        sa = _structural_analysis()
   156→        assert sa.main_thesis.startswith("AI agents")
   157→        assert len(sa.sub_claims) == 2
   158→        assert sa.logical_structure == "因果论证"
   159→        assert len(sa.key_entities) == 3
   160→        assert len(sa.verifiable_facts) == 2
   161→        assert isinstance(sa.search_queries, SearchQueries)
   162→
   163→    def test_requires_all_fields(self) -> None:
   164→        with pytest.raises(Exception):
   165→            StructuralAnalysis(main_thesis="only this")  # type: ignore[call-arg]
   166→
   167→
   168→class TestWebSource:
   169→    def test_valid_model(self) -> None:
   170→        ws = _web_source()
   171→        assert ws.title == "Example Source"
   172→        assert ws.url == "https://example.com/source"
   173→        assert "AI agents" in ws.snippet
   174→
   175→
   176→class TestFactCheck:
   177→    def test_valid_model(self) -> None:
   178→        fc = _fact_check()
   179→        assert fc.status == "verified"
   180→        assert fc.source_url is not None
   181→
   182→    def test_status_can_be_any_string(self) -> None:
   183→        fc = FactCheck(
   184→            claim="Some claim",
   185→            status="disputed",
   186→            evidence="Evidence here",
   187→            source_url=None,
   188→        )
   189→        assert fc.status == "disputed"
   190→        assert fc.source_url is None
   191→
   192→
   193→class TestResearchResults:
   194→    def test_valid_model(self) -> None:
   195→        rr = _research_results()
   196→        assert len(rr.supporting) == 1
   197→        assert len(rr.opposing) == 1
   198→        assert len(rr.expert) == 1
   199→        assert len(rr.fact_checks) == 1
   200→
   201→
   202→class TestMultiPerspectiveEvaluation:
   203→    def test_valid_model(self) -> None:
   204→        ev = _evaluation()
   205→        assert ev.credibility_score == 0.72
   206→        assert len(ev.pro_arguments) == 2
   207→        assert len(ev.con_arguments) == 2
   208→
   209→    def test_credibility_score_is_float(self) -> None:
   210→        ev = _evaluation()
   211→        assert isinstance(ev.credibility_score, float)
   212→
   213→
   214→class TestDeepDiveReport:
   215→    def test_valid_model(self, tmp_path: Path) -> None:
   216→        r = _report(tmp_path)
   217→        assert r.article.title == "The Rise of AI Agents"
   218→        assert isinstance(r.generated_at, datetime)
   219→        assert r.report_path is not None
   220→
   221→    def test_report_path_optional(self) -> None:
   222→        r = DeepDiveReport(
   223→            article=_article(),
   224→            structure=_structural_analysis(),
   225→            research=_research_results(),
   226→            evaluation=_evaluation(),
   227→            generated_at=datetime(2026, 2, 28),
   228→        )
   229→        assert r.report_path is None
   230→
   231→
   232→# ---------------------------------------------------------------------------
   233→# ContentFetcher tests
   234→# ---------------------------------------------------------------------------
   235→
   236→
   237→class TestContentFetcher:
   238→    def test_from_text_returns_article(self) -> None:
   239→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   240→
   241→        fetcher = ContentFetcher()
   242→        article = fetcher.from_text("Some article text about AI", title="My Article")
   243→        assert article.title == "My Article"
   244→        assert article.raw_text == "Some article text about AI"
   245→        assert article.url is None
   246→
   247→    def test_from_text_default_title(self) -> None:
   248→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   249→
   250→        fetcher = ContentFetcher()
   251→        article = fetcher.from_text("Some content")
   252→        assert article.title != ""
   253→
   254→    def test_fetch_url_returns_article(self, httpx_mock: HTTPXMock) -> None:
   255→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   256→
   257→        html = "<title>Test Page</title>
Main content here.
"
   258→        httpx_mock.add_response(text=html, headers={"content-type": "text/html"})
   259→
   260→        fetcher = ContentFetcher()
   261→        article = fetcher.fetch_url("https://example.com/article")
   262→        assert article.url == "https://example.com/article"
   263→        assert "Main content here" in article.raw_text
   264→
   265→    def test_fetch_url_truncates_at_8000_chars(self, httpx_mock: HTTPXMock) -> None:
   266→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   267→
   268→        long_text = "word " * 10000  # ~50000 chars
   269→        html = f"
{long_text}
"
   270→        httpx_mock.add_response(text=html, headers={"content-type": "text/html"})
   271→
   272→        fetcher = ContentFetcher()
   273→        article = fetcher.fetch_url("https://example.com/long")
   274→        assert len(article.raw_text) <= 8000
   275→
   276→    def test_fetch_url_sets_title_from_html(self, httpx_mock: HTTPXMock) -> None:
   277→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   278→
   279→        html = "<title>Article Title</title>
Content.
"
   280→        httpx_mock.add_response(text=html, headers={"content-type": "text/html"})
   281→
   282→        fetcher = ContentFetcher()
   283→        article = fetcher.fetch_url("https://example.com/article")
   284→        assert article.title == "Article Title"
   285→
   286→    def test_fetch_url_raises_on_http_error(self, httpx_mock: HTTPXMock) -> None:
   287→        from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
   288→
   289→        httpx_mock.add_response(status_code=404)
   290→
   291→        fetcher = ContentFetcher()
   292→        with pytest.raises(Exception):
   293→            fetcher.fetch_url("https://example.com/notfound")
   294→
   295→
   296→# ---------------------------------------------------------------------------
   297→# StructuralAnalyzer tests
   298→# ---------------------------------------------------------------------------
   299→
   300→
   301→class TestStructuralAnalyzer:
   302→    def _fake_llm_response(self) -> dict:
   303→        return {
   304→            "main_thesis": "AI will transform software development",
   305→            "sub_claims": ["Claim 1", "Claim 2"],
   306→            "logical_structure": "因果论证",
   307→            "key_entities": ["Anthropic", "GitHub"],
   308→            "verifiable_facts": ["GitHub has 100M users"],
   309→            "search_queries": {
   310→                "supporting": ["AI benefits productivity"],
   311→                "opposing": ["AI risks criticism"],
   312→                "expert": ["AI research paper"],
   313→                "fact_checks": ["GitHub users count"],
   314→            },
   315→        }
   316→
   317→    def test_run_returns_structural_analysis(self) -> None:
   318→        from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
   319→
   320→        with patch(
   321→            "ai_usecases_explorer.deep_dive.analyzer.StructuralAnalyzer._call_llm",
   322→            return_value=self._fake_llm_response(),
   323→        ):
   324→            analyzer = StructuralAnalyzer(client=MagicMock(), model="claude-sonnet-4-6")
   325→            result = analyzer.run(_article())
   326→
   327→        assert isinstance(result, StructuralAnalysis)
   328→        assert result.main_thesis == "AI will transform software development"
   329→
   330→    def test_run_parses_sub_claims(self) -> None:
   331→        from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
   332→
   333→        with patch(
   334→            "ai_usecases_explorer.deep_dive.analyzer.StructuralAnalyzer._call_llm",
   335→            return_value=self._fake_llm_response(),
   336→        ):
   337→            analyzer = StructuralAnalyzer(client=MagicMock(), model="claude-sonnet-4-6")
   338→            result = analyzer.run(_article())
   339→
   340→        assert len(result.sub_claims) == 2
   341→        assert "Claim 1" in result.sub_claims
   342→
   343→    def test_run_parses_search_queries(self) -> None:
   344→        from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
   345→
   346→        with patch(
   347→            "ai_usecases_explorer.deep_dive.analyzer.StructuralAnalyzer._call_llm",
   348→            return_value=self._fake_llm_response(),
   349→        ):
   350→            analyzer = StructuralAnalyzer(client=MagicMock(), model="claude-sonnet-4-6")
   351→            result = analyzer.run(_article())
   352→
   353→        assert isinstance(result.search_queries, SearchQueries)
   354→        assert len(result.search_queries.supporting) == 1
   355→
   356→    def test_run_calls_llm_with_article_content(self) -> None:
   357→        from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
   358→
   359→        with patch(
   360→            "ai_usecases_explorer.deep_dive.analyzer.StructuralAnalyzer._call_llm",
   361→            return_value=self._fake_llm_response(),
   362→        ) as mock_llm:
   363→            analyzer = StructuralAnalyzer(client=MagicMock(), model="claude-sonnet-4-6")
   364→            analyzer.run(_article())
   365→
   366→        mock_llm.assert_called_once()
   367→        call_arg = mock_llm.call_args[0][0]
   368→        assert isinstance(call_arg, ArticleContent)
   369→
   370→
   371→# ---------------------------------------------------------------------------
   372→# WebResearcher tests
   373→# ---------------------------------------------------------------------------
   374→
   375→
   376→def _make_exa_result(
   377→    url: str = "https://example.com/result",
   378→    title: str = "Research Finding",
   379→    text: str = "This is the content of the search result.",
   380→) -> MagicMock:
   381→    result = MagicMock()
   382→    result.url = url
   383→    result.title = title
   384→    result.text = text
   385→    return result
   386→
   387→
   388→def _make_exa_response(results: list[MagicMock]) -> MagicMock:
   389→    response = MagicMock()
   390→    response.results = results
   391→    return response
   392→
   393→
   394→class TestWebResearcher:
   395→    def test_run_returns_research_results(self) -> None:
   396→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   397→
   398→        mock_exa = MagicMock()
   399→        mock_exa.search_and_contents.return_value = _make_exa_response(
   400→            [_make_exa_result()]
   401→        )
   402→        researcher = WebResearcher(exa_client=mock_exa)
   403→        result = researcher.run(_search_queries())
   404→
   405→        assert isinstance(result, ResearchResults)
   406→
   407→    def test_run_returns_supporting_sources(self) -> None:
   408→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   409→
   410→        mock_exa = MagicMock()
   411→        mock_exa.search_and_contents.return_value = _make_exa_response(
   412→            [_make_exa_result(url="https://pro.example.com")]
   413→        )
   414→        researcher = WebResearcher(exa_client=mock_exa)
   415→        result = researcher.run(_search_queries())
   416→
   417→        assert isinstance(result.supporting, list)
   418→        assert isinstance(result.opposing, list)
   419→        assert isinstance(result.expert, list)
   420→        assert isinstance(result.fact_checks, list)
   421→
   422→    def test_run_calls_exa_for_each_query_type(self) -> None:
   423→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   424→
   425→        mock_exa = MagicMock()
   426→        mock_exa.search_and_contents.return_value = _make_exa_response([])
   427→        researcher = WebResearcher(exa_client=mock_exa)
   428→        researcher.run(_search_queries())
   429→
   430→        # Should call Exa multiple times (one per query category + individual queries)
   431→        assert mock_exa.search_and_contents.call_count >= 4
   432→
   433→    def test_run_deduplicates_results_by_url(self) -> None:
   434→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   435→
   436→        same_result = _make_exa_result(url="https://example.com/duplicate")
   437→        mock_exa = MagicMock()
   438→        mock_exa.search_and_contents.return_value = _make_exa_response(
   439→            [same_result, same_result]
   440→        )
   441→        researcher = WebResearcher(exa_client=mock_exa)
   442→        result = researcher.run(_search_queries())
   443→
   444→        all_urls = [s.url for s in result.supporting + result.opposing + result.expert]
   445→        assert len(all_urls) == len(set(all_urls))
   446→
   447→    def test_run_gracefully_handles_exa_error(self) -> None:
   448→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   449→
   450→        mock_exa = MagicMock()
   451→        mock_exa.search_and_contents.side_effect = RuntimeError("API error")
   452→        researcher = WebResearcher(exa_client=mock_exa)
   453→        result = researcher.run(_search_queries())
   454→
   455→        # Should return empty results, not raise
   456→        assert isinstance(result, ResearchResults)
   457→        assert result.supporting == []
   458→
   459→    def test_web_source_has_required_fields(self) -> None:
   460→        from ai_usecases_explorer.deep_dive.researcher import WebResearcher
   461→
   462→        mock_exa = MagicMock()
   463→        mock_exa.search_and_contents.return_value = _make_exa_response(
   464→            [_make_exa_result(url="https://example.com/r1", title="Title 1", text="Snippet 1")]
   465→        )
   466→        researcher = WebResearcher(exa_client=mock_exa)
   467→        result = researcher.run(_search_queries())
   468→
   469→        if result.supporting:
   470→            source = result.supporting[0]
   471→            assert source.title != ""
   472→            assert source.url != ""
   473→            assert source.snippet != ""
   474→
   475→
   476→# ---------------------------------------------------------------------------
   477→# MultiPerspectiveEvaluator tests
   478→# ---------------------------------------------------------------------------
   479→
   480→
   481→class TestMultiPerspectiveEvaluator:
   482→    def _fake_llm_response(self) -> dict:
   483→        return {
   484→            "pro_arguments": ["Argument for 1", "Argument for 2"],
   485→            "con_arguments": ["Argument against 1"],
   486→            "neutral_context": ["Background context 1"],
   487→            "bias_assessment": "Slight pro-AI bias detected",
   488→            "credibility_score": 0.65,
   489→            "overall_verdict": "Moderately credible with caveats",
   490→            "further_exploration": ["Look for peer-reviewed studies"],
   491→        }
   492→
   493→    def test_run_returns_evaluation(self) -> None:
   494→        from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
   495→
   496→        with patch(
   497→            "ai_usecases_explorer.deep_dive.evaluator.MultiPerspectiveEvaluator._call_llm",
   498→            return_value=self._fake_llm_response(),
   499→        ):
   500→            evaluator = MultiPerspectiveEvaluator(client=MagicMock(), model="claude-sonnet-4-6")
   501→            result = evaluator.run(_structural_analysis(), _research_results())
   502→
   503→        assert isinstance(result, MultiPerspectiveEvaluation)
   504→        assert result.credibility_score == 0.65
   505→
   506→    def test_run_parses_pro_arguments(self) -> None:
   507→        from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
   508→
   509→        with patch(
   510→            "ai_usecases_explorer.deep_dive.evaluator.MultiPerspectiveEvaluator._call_llm",
   511→            return_value=self._fake_llm_response(),
   512→        ):
   513→            evaluator = MultiPerspectiveEvaluator(client=MagicMock(), model="claude-sonnet-4-6")
   514→            result = evaluator.run(_structural_analysis(), _research_results())
   515→
   516→        assert len(result.pro_arguments) == 2
   517→        assert "Argument for 1" in result.pro_arguments
   518→
   519→    def test_run_parses_con_arguments(self) -> None:
   520→        from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
   521→
   522→        with patch(
   523→            "ai_usecases_explorer.deep_dive.evaluator.MultiPerspectiveEvaluator._call_llm",
   524→            return_value=self._fake_llm_response(),
   525→        ):
   526→            evaluator = MultiPerspectiveEvaluator(client=MagicMock(), model="claude-sonnet-4-6")
   527→            result = evaluator.run(_structural_analysis(), _research_results())
   528→
   529→        assert len(result.con_arguments) == 1
   530→
   531→    def test_run_calls_llm_with_both_inputs(self) -> None:
   532→        from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
   533→
   534→        with patch(
   535→            "ai_usecases_explorer.deep_dive.evaluator.MultiPerspectiveEvaluator._call_llm",
   536→            return_value=self._fake_llm_response(),
   537→        ) as mock_llm:
   538→            evaluator = MultiPerspectiveEvaluator(client=MagicMock(), model="claude-sonnet-4-6")
   539→            evaluator.run(_structural_analysis(), _research_results())
   540→
   541→        mock_llm.assert_called_once()
   542→        args = mock_llm.call_args[0]
   543→        assert isinstance(args[0], StructuralAnalysis)
   544→        assert isinstance(args[1], ResearchResults)
   545→
   546→    def test_overall_verdict_is_string(self) -> None:
   547→        from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
   548→
   549→        with patch(
   550→            "ai_usecases_explorer.deep_dive.evaluator.MultiPerspectiveEvaluator._call_llm",
   551→            return_value=self._fake_llm_response(),
   552→        ):
   553→            evaluator = MultiPerspectiveEvaluator(client=MagicMock(), model="claude-sonnet-4-6")
   554→            result = evaluator.run(_structural_analysis(), _research_results())
   555→
   556→        assert isinstance(result.overall_verdict, str)
   557→        assert len(result.overall_verdict) > 0
   558→
   559→
   560→# ---------------------------------------------------------------------------
   561→# DeepDiveReporter tests
   562→# ---------------------------------------------------------------------------
   563→
   564→
   565→class TestDeepDiveReporter:
   566→    def test_write_creates_file(self, tmp_path: Path) -> None:
   567→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   568→
   569→        reporter = DeepDiveReporter(report_dir=tmp_path)
   570→        report = _report(tmp_path)
   571→        output_path = reporter.write(report)
   572→
   573→        assert output_path.exists()
   574→
   575→    def test_write_returns_path(self, tmp_path: Path) -> None:
   576→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   577→
   578→        reporter = DeepDiveReporter(report_dir=tmp_path)
   579→        report = _report(tmp_path)
   580→        output_path = reporter.write(report)
   581→
   582→        assert isinstance(output_path, Path)
   583→        assert output_path.suffix == ".md"
   584→
   585→    def test_write_creates_parent_dirs(self, tmp_path: Path) -> None:
   586→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   587→
   588→        deep_dir = tmp_path / "a" / "b" / "c"
   589→        reporter = DeepDiveReporter(report_dir=deep_dir)
   590→        report = _report(tmp_path)
   591→        output_path = reporter.write(report)
   592→
   593→        assert output_path.exists()
   594→
   595→    def test_render_markdown_contains_title(self, tmp_path: Path) -> None:
   596→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   597→
   598→        reporter = DeepDiveReporter(report_dir=tmp_path)
   599→        report = _report(tmp_path)
   600→        md = reporter._render_markdown(report)
   601→
   602→        assert "The Rise of AI Agents" in md
   603→
   604→    def test_render_markdown_contains_main_thesis(self, tmp_path: Path) -> None:
   605→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   606→
   607→        reporter = DeepDiveReporter(report_dir=tmp_path)
   608→        report = _report(tmp_path)
   609→        md = reporter._render_markdown(report)
   610→
   611→        assert "AI agents will replace traditional software engineering" in md
   612→
   613→    def test_render_markdown_contains_credibility_score(self, tmp_path: Path) -> None:
   614→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   615→
   616→        reporter = DeepDiveReporter(report_dir=tmp_path)
   617→        report = _report(tmp_path)
   618→        md = reporter._render_markdown(report)
   619→
   620→        assert "0.72" in md
   621→
   622→    def test_render_markdown_contains_pro_arguments(self, tmp_path: Path) -> None:
   623→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   624→
   625→        reporter = DeepDiveReporter(report_dir=tmp_path)
   626→        report = _report(tmp_path)
   627→        md = reporter._render_markdown(report)
   628→
   629→        assert "Increases developer productivity" in md
   630→
   631→    def test_render_markdown_contains_fact_check_table(self, tmp_path: Path) -> None:
   632→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   633→
   634→        reporter = DeepDiveReporter(report_dir=tmp_path)
   635→        report = _report(tmp_path)
   636→        md = reporter._render_markdown(report)
   637→
   638→        assert "GitHub Copilot has 1 million users" in md
   639→
   640→    def test_render_markdown_contains_supporting_sources(self, tmp_path: Path) -> None:
   641→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   642→
   643→        reporter = DeepDiveReporter(report_dir=tmp_path)
   644→        report = _report(tmp_path)
   645→        md = reporter._render_markdown(report)
   646→
   647→        assert "https://example.com/pro" in md
   648→
   649→    def test_filename_contains_date_and_slug(self, tmp_path: Path) -> None:
   650→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   651→
   652→        reporter = DeepDiveReporter(report_dir=tmp_path)
   653→        report = _report(tmp_path)
   654→        output_path = reporter.write(report)
   655→
   656→        filename = output_path.name
   657→        assert "2026-02-28" in filename
   658→
   659→    def test_file_written_with_utf8(self, tmp_path: Path) -> None:
   660→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   661→
   662→        reporter = DeepDiveReporter(report_dir=tmp_path)
   663→        report = _report(tmp_path)
   664→        output_path = reporter.write(report)
   665→
   666→        content = output_path.read_text(encoding="utf-8")
   667→        assert "深度探索" in content
   668→
   669→
   670→# ---------------------------------------------------------------------------
   671→# DeepDiveOrchestrator integration tests (all steps mocked)
   672→# ---------------------------------------------------------------------------
   673→
   674→
   675→class TestDeepDiveOrchestrator:
   676→    def _setup_mocks(self) -> tuple[MagicMock, MagicMock, MagicMock, MagicMock, MagicMock]:
   677→        mock_fetcher = MagicMock()
   678→        mock_fetcher.fetch_url.return_value = _article()
   679→        mock_fetcher.from_text.return_value = _article()
   680→
   681→        mock_analyzer = MagicMock()
   682→        mock_analyzer.run.return_value = _structural_analysis()
   683→
   684→        mock_researcher = MagicMock()
   685→        mock_researcher.run.return_value = _research_results()
   686→
   687→        mock_evaluator = MagicMock()
   688→        mock_evaluator.run.return_value = _evaluation()
   689→
   690→        mock_reporter = MagicMock()
   691→        mock_reporter.write.return_value = Path("/tmp/2026-02-28-1430-test.md")
   692→
   693→        return mock_fetcher, mock_analyzer, mock_researcher, mock_evaluator, mock_reporter
   694→
   695→    def test_run_with_url_returns_report(self, tmp_path: Path) -> None:
   696→        from ai_usecases_explorer.deep_dive.cli import DeepDiveOrchestrator
   697→
   698→        fetcher, analyzer, researcher, evaluator, reporter = self._setup_mocks()
   699→        orchestrator = DeepDiveOrchestrator(
   700→            fetcher=fetcher,
   701→            analyzer=analyzer,
   702→            researcher=researcher,
   703→            evaluator=evaluator,
   704→            reporter=reporter,
   705→        )
   706→        result = orchestrator.run(url="https://example.com/article")
   707→
   708→        assert isinstance(result, DeepDiveReport)
   709→        fetcher.fetch_url.assert_called_once_with("https://example.com/article")
   710→
   711→    def test_run_with_text_returns_report(self, tmp_path: Path) -> None:
   712→        from ai_usecases_explorer.deep_dive.cli import DeepDiveOrchestrator
   713→
   714→        fetcher, analyzer, researcher, evaluator, reporter = self._setup_mocks()
   715→        orchestrator = DeepDiveOrchestrator(
   716→            fetcher=fetcher,
   717→            analyzer=analyzer,
   718→            researcher=researcher,
   719→            evaluator=evaluator,
   720→            reporter=reporter,
   721→        )
   722→        result = orchestrator.run(text="Some article text")
   723→
   724→        assert isinstance(result, DeepDiveReport)
   725→        fetcher.from_text.assert_called_once()
   726→
   727→    def test_run_calls_pipeline_in_order(self) -> None:
   728→        from ai_usecases_explorer.deep_dive.cli import DeepDiveOrchestrator
   729→
   730→        call_order: list[str] = []
   731→        fetcher, analyzer, researcher, evaluator, reporter = self._setup_mocks()
   732→
   733→        fetcher.fetch_url.side_effect = lambda *a, **kw: (call_order.append("fetch"), _article())[1]
   734→        analyzer.run.side_effect = lambda *a, **kw: (call_order.append("analyze"), _structural_analysis())[1]
   735→        researcher.run.side_effect = lambda *a, **kw: (call_order.append("research"), _research_results())[1]
   736→        evaluator.run.side_effect = lambda *a, **kw: (call_order.append("evaluate"), _evaluation())[1]
   737→        reporter.write.side_effect = lambda *a, **kw: (call_order.append("report"), Path("/tmp/r.md"))[1]
   738→
   739→        orchestrator = DeepDiveOrchestrator(
   740→            fetcher=fetcher,
   741→            analyzer=analyzer,
   742→            researcher=researcher,
   743→            evaluator=evaluator,
   744→            reporter=reporter,
   745→        )
   746→        orchestrator.run(url="https://example.com")
   747→
   748→        assert call_order == ["fetch", "analyze", "research", "evaluate", "report"]
   749→
   750→    def test_run_dry_run_skips_reporter(self) -> None:
   751→        from ai_usecases_explorer.deep_dive.cli import DeepDiveOrchestrator
   752→
   753→        fetcher, analyzer, researcher, evaluator, reporter = self._setup_mocks()
   754→        orchestrator = DeepDiveOrchestrator(
   755→            fetcher=fetcher,
   756→            analyzer=analyzer,
   757→            researcher=researcher,
   758→            evaluator=evaluator,
   759→            reporter=reporter,
   760→        )
   761→        result = orchestrator.run(url="https://example.com", dry_run=True)
   762→
   763→        reporter.write.assert_not_called()
   764→        assert isinstance(result, DeepDiveReport)
   765→        assert result.report_path is None
   766→
   767→    def test_run_raises_when_no_input(self) -> None:
   768→        from ai_usecases_explorer.deep_dive.cli import DeepDiveOrchestrator
   769→
   770→        fetcher, analyzer, researcher, evaluator, reporter = self._setup_mocks()
   771→        orchestrator = DeepDiveOrchestrator(
   772→            fetcher=fetcher,
   773→            analyzer=analyzer,
   774→            researcher=researcher,
   775→            evaluator=evaluator,
   776→            reporter=reporter,
   777→        )
   778→        with pytest.raises(ValueError):
   779→            orchestrator.run()
   780→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""Shared utilities for LLM processors."""
     2→import json
     3→import re
     4→
     5→
     6→def parse_llm_json(text: str) -> dict:
     7→    """Parse JSON from LLM output, stripping markdown code fences if present.
     8→
     9→    Falls back to extracting the first {...} block if direct parse fails,
    10→    which handles cases where the LLM prepends/appends explanatory text.
    11→
    12→    Also iteratively repairs unescaped ASCII double-quotes inside JSON string
    13→    values (e.g. when the model writes "vibe coding" without escaping the inner
    14→    quotes), which would otherwise cause JSONDecodeError.
    15→    """
    16→    text = text.strip()
    17→    if not text:
    18→        raise ValueError("LLM returned empty response")
    19→    # Strip ```json ... ``` or ``` ... ``` fences
    20→    text = re.sub(r"^```(?:json)?\s*", "", text)
    21→    text = re.sub(r"\s*```$", "", text)
    22→    text = text.strip()
    23→
    24→    # Extract outermost {...} block (handles leading/trailing prose)
    25→    match = re.search(r"\{.*\}", text, re.DOTALL)
    26→    if match:
    27→        text = match.group()
    28→
    29→    return _repair_and_parse(text)
    30→
    31→
    32→def _repair_and_parse(text: str) -> dict:
    33→    """Iteratively escape unescaped quotes inside JSON string values and parse."""
    34→    for _ in range(30):
    35→        try:
    36→            return json.loads(text)
    37→        except json.JSONDecodeError as exc:
    38→            pos = exc.pos  # 0-indexed position of the problematic character
    39→            # Walk backwards from error to find the unescaped " that closed the string early
    40→            i = pos - 1
    41→            while i >= 0:
    42→                if text[i] == '"' and (i == 0 or text[i - 1] != "\\"):
    43→                    text = text[:i] + '\\"' + text[i + 1 :]
    44→                    break
    45→                i -= 1
    46→            else:
    47→                break  # no fixable quote found
    48→    return json.loads(text)  # final attempt — raises if still broken
    49→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


     1→"""CLI entry point and orchestrator for the Deep Dive skill."""
     2→from __future__ import annotations
     3→
     4→import argparse
     5→from datetime import UTC, datetime
     6→from pathlib import Path
     7→
     8→import anthropic
     9→from exa_py import Exa  # type: ignore[import-untyped]
    10→
    11→from ai_usecases_explorer.deep_dive.analyzer import StructuralAnalyzer
    12→from ai_usecases_explorer.deep_dive.evaluator import MultiPerspectiveEvaluator
    13→from ai_usecases_explorer.deep_dive.fetcher import ContentFetcher
    14→from ai_usecases_explorer.deep_dive.models import DeepDiveReport
    15→from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
    16→from ai_usecases_explorer.deep_dive.researcher import WebResearcher
    17→from ai_usecases_explorer.settings import Settings
    18→
    19→_DEEP_DIVE_MODEL = "claude-sonnet-4-6"
    20→
    21→
    22→class DeepDiveOrchestrator:
    23→    """Orchestrates the four-step deep-dive analysis pipeline."""
    24→
    25→    def __init__(
    26→        self,
    27→        fetcher: ContentFetcher,
    28→        analyzer: StructuralAnalyzer,
    29→        researcher: WebResearcher,
    30→        evaluator: MultiPerspectiveEvaluator,
    31→        reporter: DeepDiveReporter,
    32→    ) -> None:
    33→        self._fetcher = fetcher
    34→        self._analyzer = analyzer
    35→        self._researcher = researcher
    36→        self._evaluator = evaluator
    37→        self._reporter = reporter
    38→
    39→    def run(
    40→        self,
    41→        url: str | None = None,
    42→        text: str | None = None,
    43→        file: Path | None = None,
    44→        dry_run: bool = False,
    45→    ) -> DeepDiveReport:
    46→        """Run the full deep-dive pipeline.
    47→
    48→        Args:
    49→            url: URL of the article to analyse.
    50→            text: Raw article text (alternative to url).
    51→            file: Path to a text file (alternative to url/text).
    52→            dry_run: If True, skip writing the report to disk.
    53→
    54→        Returns:
    55→            A complete DeepDiveReport.
    56→
    57→        Raises:
    58→            ValueError: If none of url, text, or file is provided.
    59→        """
    60→        # Step 0: Acquire content
    61→        if url:
    62→            article = self._fetcher.fetch_url(url)
    63→        elif text:
    64→            article = self._fetcher.from_text(text)
    65→        elif file:
    66→            article = self._fetcher.from_text(file.read_text(encoding="utf-8"), title=file.name)
    67→        else:
    68→            raise ValueError("At least one of --url, --text, or --file must be provided.")
    69→
    70→        # Step 1: Structural analysis
    71→        structure = self._analyzer.run(article)
    72→
    73→        # Step 2: Web research
    74→        research = self._researcher.run(structure.search_queries)
    75→
    76→        # Step 3: Multi-perspective evaluation
    77→        evaluation = self._evaluator.run(structure, research)
    78→
    79→        report = DeepDiveReport(
    80→            article=article,
    81→            structure=structure,
    82→            research=research,
    83→            evaluation=evaluation,
    84→            generated_at=datetime.now(tz=UTC),
    85→        )
    86→
    87→        # Step 4: Write report (unless dry-run)
    88→        if not dry_run:
    89→            report_path = self._reporter.write(report)
    90→            report = report.model_copy(update={"report_path": report_path})
    91→
    92→        return report
    93→
    94→
    95→def _build_orchestrator(settings: Settings) -> DeepDiveOrchestrator:
    96→    """Instantiate all pipeline components from settings."""
    97→    anthropic_kwargs: dict = {"api_key": settings.anthropic_api_key}
    98→    if settings.anthropic_base_url:
    99→        anthropic_kwargs["base_url"] = settings.anthropic_base_url
   100→    client = anthropic.Anthropic(**anthropic_kwargs)
   101→
   102→    exa = Exa(api_key=settings.exa_api_key)
   103→
   104→    return DeepDiveOrchestrator(
   105→        fetcher=ContentFetcher(exa_client=exa),
   106→        analyzer=StructuralAnalyzer(client=client, model=settings.deep_dive_model),
   107→        researcher=WebResearcher(exa_client=exa),
   108→        evaluator=MultiPerspectiveEvaluator(client=client, model=settings.deep_dive_model),
   109→        reporter=DeepDiveReporter(report_dir=settings.deep_dive_report_dir),
   110→    )
   111→
   112→
   113→def main() -> None:
   114→    """Parse arguments and run the deep-dive pipeline."""
   115→    parser = argparse.ArgumentParser(
   116→        description="Deep Dive: multi-perspective article analysis via Exa.ai + Claude."
   117→    )
   118→    group = parser.add_mutually_exclusive_group()
   119→    group.add_argument("--url", help="URL of the article to analyse")
   120→    group.add_argument("--text", help="Raw article text (quoted string)")
   121→    group.add_argument("--file", type=Path, help="Path to a plain-text article file")
   122→    parser.add_argument(
   123→        "--dry-run",
   124→        action="store_true",
   125→        help="Print the report without writing to disk",
   126→    )
   127→    args = parser.parse_args()
   128→
   129→    settings = Settings()
   130→    orchestrator = _build_orchestrator(settings)
   131→
   132→    report = orchestrator.run(
   133→        url=args.url,
   134→        text=args.text,
   135→        file=args.file,
   136→        dry_run=args.dry_run,
   137→    )
   138→
   139→    if args.dry_run:
   140→        from ai_usecases_explorer.deep_dive.reporter import DeepDiveReporter
   141→
   142→        reporter = DeepDiveReporter(report_dir=settings.deep_dive_report_dir)
   143→        print(reporter._render_markdown(report))
   144→    else:
   145→        print(f"REPORT_PATH:{report.report_path}")
   146→        print(f"✅ 报告已保存至 Obsidian：{report.report_path}")
   147→        print(f"   可信度评分：{report.evaluation.credibility_score:.2f} / 1.0")
   148→        print(f"   总体结论：{report.evaluation.overall_verdict}")
   149→
   150→
   151→if __name__ == "__main__":
   152→    main()
   153→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now I have a thorough understanding of every file. Let me design the full implementation plan.

---

## Implementation Plan: Deep Dive Module Enhancement

This plan covers five prioritized improvements delivered with minimum extra LLM calls. The principle is: extend existing prompts rather than add new pipeline steps, and keep all new fields backward-compatible with Pydantic defaults.

---

## Part 1: What to Change in `models.py`

### 1.1 Extend `StructuralAnalysis` — add `logical_quality`

The analyzer already extracts `logical_structure` (names the type, e.g. "因果论证"). We need to extend the LLM call in `analyzer.py` to also assess reasoning quality and flag fallacies. The cleanest model design is to keep `logical_structure` as-is (backward-compatible) and add one new field alongside it.

```python
class LogicalQuality(BaseModel):
    """Assessment of the reasoning validity and potential fallacies."""
    validity_assessment: str       # "论证结构是否严密，推理是否有效" (Chinese)
    identified_fallacies: list[str]  # 0–N named fallacies with brief explanation (Chinese)
    strength: str                  # "强" | "中" | "弱"
```

`StructuralAnalysis` gets one new optional field:
```python
logical_quality: LogicalQuality | None = None
```

Using `None` default means existing test fixtures for `_structural_analysis()` continue to pass without modification.

### 1.2 Extend `FactCheck` — add `source_authority`

```python
class FactCheck(BaseModel):
    claim: str
    status: str           # existing — will now be set by evaluator, not hardcoded
    evidence: str
    source_url: str | None
    source_authority: str | None = None  # NEW: "学术论文" | "新闻媒体" | "官方机构" | "博客/个人" | "未知"
```

`source_authority` is also set by the evaluator in the fact-check assessment pass.

### 1.3 Extend `MultiPerspectiveEvaluation` — structured bias, credibility breakdown, citations, disputes, synthesis

Replace the current flat fields with richer structured types. All new fields use `| None = None` or `list[...] = []` defaults so all existing tests that construct `_evaluation()` with the old positional arguments continue passing.

```python
class CitedArgument(BaseModel):
    """An argument with optional citations linking to research sources."""
    text: str                    # The argument text (Chinese)
    source_urls: list[str] = []  # URLs from ResearchResults that ground this argument

class BiasIndicator(BaseModel):
    """One identified bias dimension."""
    bias_type: str    # e.g., "确认偏误" | "信息选择性" | "来源偏向" (Chinese)
    description: str  # What evidence of this bias exists (Chinese)
    severity: str     # "高" | "中" | "低"

class CredibilityBreakdown(BaseModel):
    """Explains the numeric credibility score dimension by dimension."""
    evidence_quality: float        # 0-1, weight 0.35
    source_diversity: float        # 0-1, weight 0.25
    logical_rigor: float           # 0-1, weight 0.25
    author_transparency: float     # 0-1, weight 0.15
    explanation: str               # Chinese narrative tying them together

class MultiPerspectiveEvaluation(BaseModel):
    # EXISTING fields preserved verbatim (no breakage):
    pro_arguments: list[str]
    con_arguments: list[str]
    neutral_context: list[str]
    bias_assessment: str
    credibility_score: float
    overall_verdict: str
    further_exploration: list[str]

    # NEW fields (all default to None/[] so old construction still works):
    cited_pro_arguments: list[CitedArgument] = []
    cited_con_arguments: list[CitedArgument] = []
    bias_indicators: list[BiasIndicator] = []
    credibility_breakdown: CredibilityBreakdown | None = None
    core_disputes: list[str] = []     # What is actually being contested (Chinese)
    synthesis: str = ""               # Insight paragraph (Chinese, 3-4 sentences)
    assessed_fact_checks: list[FactCheck] = []  # fact checks WITH evaluated status (not "unverifiable")
```

The critical design choice here is that `pro_arguments` / `con_arguments` remain as `list[str]` for backward compatibility, and the new `cited_pro_arguments` / `cited_con_arguments` are parallel lists with citation URLs. The reporter renders whichever version is populated. This keeps all 139 existing tests green because none of the test fixtures need to supply the new fields.

---

## Part 2: What to Change in `evaluator.py`

### 2.1 One enhanced LLM call vs. two calls

The recommendation is **one enhanced call**. The evaluator already receives both `StructuralAnalysis` and `ResearchResults`. Adding fact-check status assessment, logical quality, and structured bias to the same prompt avoids a second latency-adding API call and keeps the architecture unchanged. The trade-off is a larger prompt and output — but the existing `max_tokens=2048` needs to increase to `4096`.

### 2.2 New system prompt schema

The new `_SYSTEM` prompt asks for this JSON shape:

```json
{
  "cited_pro_arguments": [
    {"text": "支持论据（中文）", "source_urls": ["https://..."]}
  ],
  "cited_con_arguments": [
    {"text": "质疑论据（中文）", "source_urls": ["https://..."]}
  ],
  "neutral_context": ["背景信息（中文）"],
  "bias_indicators": [
    {"bias_type": "确认偏误", "description": "描述（中文）", "severity": "高|中|低"}
  ],
  "bias_assessment": "自由文本总结（保留供兼容）",
  "credibility_breakdown": {
    "evidence_quality": 0.7,
    "source_diversity": 0.6,
    "logical_rigor": 0.8,
    "author_transparency": 0.5,
    "explanation": "中文说明"
  },
  "credibility_score": 0.68,
  "core_disputes": ["争议点1（中文）", "争议点2（中文）"],
  "overall_verdict": "综合结论（2-3句话，中文）",
  "synthesis": "深度洞察（3-4句话，揭示更深层含义，中文）",
  "further_exploration": ["探索方向（中文）"],
  "assessed_fact_checks": [
    {
      "claim": "原始声明",
      "status": "verified|disputed|false|unverifiable",
      "evidence": "基于提供的事实核查来源的判断依据（中文）",
      "source_url": "https://...",
      "source_authority": "学术论文|新闻媒体|官方机构|博客/个人|未知"
    }
  ]
}
```

Key instructions added to the system prompt (in Chinese):
- `cited_pro_arguments`: for each argument, list the `source_urls` from the provided research sources that directly ground it. Leave `source_urls: []` if no specific source applies.
- `bias_indicators`: identify 1–4 distinct named bias types, not just free text.
- `credibility_breakdown`: score each dimension independently. `credibility_score` must equal a weighted sum: `evidence_quality*0.35 + source_diversity*0.25 + logical_rigor*0.25 + author_transparency*0.15`.
- `core_disputes`: 2–4 concrete disagreements exposed by the opposing and expert sources.
- `synthesis`: a paragraph that goes beyond description to offer interpretive insight.
- `assessed_fact_checks`: re-evaluate each fact check from the research using the evidence provided. Do NOT default to "unverifiable" — use the snippet content to reason toward a status.

### 2.3 Changes to `_call_llm` in `evaluator.py`

The input to the LLM needs to be enriched:

1. Include source URLs in the research summary (currently only title + snippet, no URL). The evaluator needs the URLs available so it can cite them in `source_urls` lists:
   ```python
   def _fmt_sources(sources: list) -> str:
       return "\n".join(
           f"- [{s.url}] {s.title}: {s.snippet[:150]}"
           for s in sources[:4]
       )
   ```

2. Include the fact_checks from the researcher (currently they are omitted from the evaluator input entirely). This is the root cause of the bug — the evaluator cannot assess fact-check status if it never sees the fact check evidence:
   ```python
   fact_checks_summary = "\n".join(
       f"- 声明：{fc.claim}\n  证据片段：{fc.evidence[:200]}\n  来源：{fc.source_url or '无'}"
       for fc in research.fact_checks
   )
   ```

3. Increase content character limit from `[:6000]` to `[:8000]` and `max_tokens` from `2048` to `4096`.

4. The `run()` method in `evaluator.py` needs to populate both old and new fields from the raw dict. New fields use `.get(key, default)` so missing keys degrade gracefully:
   ```python
   # Backward-compat: derive plain list from cited list if LLM provided it
   cited_pro = [CitedArgument(**a) for a in raw.get("cited_pro_arguments", [])]
   pro_args = raw.get("pro_arguments") or [a["text"] for a in raw.get("cited_pro_arguments", [])]
   ```

---

## Part 3: What to Change in `reporter.py`

### 3.1 New rendering approach

The reporter adds new sections after the existing structure. No existing sections are removed — the new sections are appended or replace existing rendering only when the new fields are populated. This makes the reporter resilient to old `DeepDiveReport` objects that lack the new fields.

### 3.2 Updated `_render_markdown` sections

**Updated: Arguments with citations**

Instead of bare bullets:
```markdown
- Increases developer productivity by 40%
```

Render as citation-linked bullets when `cited_pro_arguments` is non-empty:
```markdown
- 开发者生产力提升40%。[[来源1](https://nature.com/paper)][[来源2](https://github.blog)]
```

Implementation logic:
```python
# In reporter._render_markdown:
args_to_render = ev.cited_pro_arguments or [CitedArgument(text=a) for a in ev.pro_arguments]
for arg in args_to_render:
    citations = "".join(f"[[来源]({u})]" for u in arg.source_urls)
    lines.append(f"- {arg.text}{' ' + citations if citations else ''}")
```

**Updated: Fact-check table uses `assessed_fact_checks` when available**

```python
fact_checks_to_render = ev.assessed_fact_checks or rs.fact_checks
```

This is the key fix for the "always unverifiable" bug — the reporter now prefers the evaluator-assessed list which has reasoned statuses over the researcher's hardcoded unverifiable list.

**New section: Bias analysis (structured)**

After `## 多角度评价`, before `## 综合评估`:
```markdown
## 偏见分析

| 偏见类型 | 说明 | 严重程度 |
|----------|------|---------|
| 确认偏误 | 文章仅引用支持性研究 | 高 |
| 来源偏向 | 主要引用商业报告 | 中 |
```

Falls back to the old `bias_assessment` string if `bias_indicators` is empty.

**New section: Credibility score breakdown**

Replace the single-row credibility table entry:
```markdown
| 可信度 | 0.72 / 1.0 |
```

With a full breakdown block:
```markdown
## 可信度评分分析

| 维度 | 得分 | 权重 |
|------|------|------|
| 证据质量 | 0.75 | 35% |
| 来源多样性 | 0.60 | 25% |
| 论证严密性 | 0.80 | 25% |
| 作者透明度 | 0.55 | 15% |
| **综合评分** | **0.72** | |

> 0.72（基本可信）— 有一定支撑，但主要来源为商业机构……
```

Falls back to the old `| 可信度 | X.XX / 1.0 |` row if `credibility_breakdown` is None.

**New section: Core disputes**

Insert between `## 综合评估` and `### 延伸探索建议`:
```markdown
### 核心争议点

1. AI代理是否已达到生产级可靠性——支持方与反对方对此持截然不同的立场
2. 开发者技能退化风险——尚无充分纵向数据支撑
```

Only rendered when `ev.core_disputes` is non-empty.

**New section: Synthesis insight**

At the end of `## 综合评估`, just before the fact table or `---` footer:
```markdown
### 深度洞察

这篇文章呈现了AI代理领域的典型技术乐观主义叙事……（3–4句中文段落）
```

Only rendered when `ev.synthesis` is non-empty.

**Updated: Logical quality in `## 论证结构`**

The `StructuralAnalysis.logical_quality` field, if present, adds a sub-block after `**论证方式**`:
```markdown
**论证方式**：因果论证

**论证质量**：中

> 论证基本成立，但存在以下问题：
> - 相关性谬误：将相关性混淆为因果
> - 以偏概全：以少数案例推导普遍结论
```

---

## Part 4: What to Change in `analyzer.py`

The `logical_quality` field is populated by the **analyzer**, not the evaluator — this keeps the pipeline clean: the analyzer extracts structure and quality of the source argument; the evaluator assesses external evidence quality.

Extend the analyzer's `_SYSTEM` prompt JSON schema to include:
```json
"logical_quality": {
  "validity_assessment": "论证整体是否严密的中文评价",
  "identified_fallacies": ["谬误名称：具体说明（中文）"],
  "strength": "强|中|弱"
}
```

The `run()` method in `analyzer.py` constructs `LogicalQuality` from `raw.get("logical_quality")` with a `None` fallback when the key is absent (for backward-compat with any cached/mocked responses).

This change does NOT require a second LLM call — it extends the existing analyzer call's output by approximately 200 tokens.

---

## Part 5: Test Strategy

All new tests follow the existing pattern: mock `_call_llm` at the class level, test `run()` behavior.

### 5.1 New tests in `TestMultiPerspectiveEvaluation` (in `test_deep_dive.py`)

**Test: cited arguments are parsed correctly**
```python
def _fake_llm_response_v2(self) -> dict:
    return {
        "cited_pro_arguments": [
            {"text": "证据充分论点", "source_urls": ["https://nature.com/1"]}
        ],
        "cited_con_arguments": [
            {"text": "质疑论点", "source_urls": []}
        ],
        "neutral_context": [...],
        "bias_indicators": [
            {"bias_type": "确认偏误", "description": "说明", "severity": "中"}
        ],
        "bias_assessment": "slight bias",
        "credibility_breakdown": {
            "evidence_quality": 0.7, "source_diversity": 0.6,
            "logical_rigor": 0.8, "author_transparency": 0.5,
            "explanation": "解释"
        },
        "credibility_score": 0.65,
        "core_disputes": ["争议1"],
        "overall_verdict": "总体结论",
        "synthesis": "深度洞察",
        "further_exploration": ["探索方向"],
        "assessed_fact_checks": [
            {
                "claim": "GitHub has 1M users",
                "status": "verified",
                "evidence": "官方博客确认",
                "source_url": "https://github.blog",
                "source_authority": "官方机构"
            }
        ]
    }

def test_run_parses_cited_pro_arguments(self) -> None:
    # ... mock _call_llm with _fake_llm_response_v2
    assert len(result.cited_pro_arguments) == 1
    assert result.cited_pro_arguments[0].source_urls == ["https://nature.com/1"]

def test_run_parses_bias_indicators(self) -> None:
    # ... assert result.bias_indicators[0].bias_type == "确认偏误"
    # ... assert result.bias_indicators[0].severity == "中"

def test_run_parses_credibility_breakdown(self) -> None:
    # ... assert result.credibility_breakdown is not None
    # ... assert result.credibility_breakdown.evidence_quality == 0.7

def test_run_parses_core_disputes(self) -> None:
    # ... assert "争议1" in result.core_disputes

def test_run_parses_synthesis(self) -> None:
    # ... assert result.synthesis == "深度洞察"

def test_run_parses_assessed_fact_checks_with_status(self) -> None:
    # ... assert result.assessed_fact_checks[0].status == "verified"
    # ... assert result.assessed_fact_checks[0].status != "unverifiable"
    # ... assert result.assessed_fact_checks[0].source_authority == "官方机构"

def test_run_backward_compat_missing_new_fields(self) -> None:
    # Use _fake_llm_response() (OLD format without new keys)
    # Assert result.cited_pro_arguments == []
    # Assert result.credibility_breakdown is None
    # Assert result.core_disputes == []
```

### 5.2 New tests in `TestStructuralAnalyzer`

```python
def _fake_llm_response_v2(self) -> dict:
    base = self._fake_llm_response()
    base["logical_quality"] = {
        "validity_assessment": "论证基本成立",
        "identified_fallacies": ["以偏概全：以少数案例推断普遍规律"],
        "strength": "中"
    }
    return base

def test_run_parses_logical_quality(self) -> None:
    # ... assert result.logical_quality is not None
    # ... assert result.logical_quality.strength == "中"
    # ... assert len(result.logical_quality.identified_fallacies) == 1

def test_run_handles_missing_logical_quality(self) -> None:
    # Use _fake_llm_response() (no logical_quality key)
    # ... assert result.logical_quality is None
```

### 5.3 New tests in `TestDeepDiveReporter`

```python
def test_render_markdown_uses_cited_pro_arguments_with_links(self, tmp_path):
    # Build report with cited_pro_arguments having source_urls
    # Assert "[来源]" in md and the URL appears

def test_render_markdown_shows_credibility_breakdown_table(self, tmp_path):
    # Build report with credibility_breakdown set
    # Assert "证据质量" in md and "35%" in md

def test_render_markdown_shows_core_disputes(self, tmp_path):
    # Build report with core_disputes = ["争议1"]
    # Assert "核心争议点" in md
    # Assert "争议1" in md

def test_render_markdown_shows_synthesis(self, tmp_path):
    # Build report with synthesis = "深度洞察段落"
    # Assert "深度洞察" in md (section header)
    # Assert "深度洞察段落" in md

def test_render_markdown_uses_assessed_fact_checks_status(self, tmp_path):
    # Build evaluation with assessed_fact_checks having status="verified"
    # Assert "✅" in md  (not "❓")

def test_render_markdown_shows_bias_indicators_table(self, tmp_path):
    # Build evaluation with bias_indicators set
    # Assert "偏见分析" in md
    # Assert "确认偏误" in md

def test_render_markdown_shows_logical_quality_in_structure(self, tmp_path):
    # Build structure with logical_quality having identified_fallacies
    # Assert "论证质量" in md
    # Assert the fallacy text in md

def test_render_markdown_fallback_when_new_fields_empty(self, tmp_path):
    # Use old _evaluation() and _structural_analysis() with no new fields
    # Assert "0.72" in md  (old credibility score still renders)
    # Assert "偏见识别" in md  (old bias_assessment still renders)
```

### 5.4 New unit test for `_search_fact_checks` bug documentation

```python
def test_fact_check_status_is_unverifiable_from_researcher(self) -> None:
    """Documents the known limitation: researcher always returns 'unverifiable'.
    Status is assigned by the evaluator via assessed_fact_checks instead."""
    from ai_usecases_explorer.deep_dive.researcher import WebResearcher
    mock_exa = MagicMock()
    mock_exa.search_and_contents.return_value = _make_exa_response([
        _make_exa_result(url="https://example.com/fc", text="Evidence text")
    ])
    researcher = WebResearcher(exa_client=mock_exa)
    result = researcher.run(_search_queries())
    # Researcher ALWAYS sets "unverifiable" — this is intentional (evaluator assesses)
    if result.fact_checks:
        assert result.fact_checks[0].status == "unverifiable"
```

This test documents rather than fights the researcher behavior. The fix is in the evaluator's `assessed_fact_checks` output, not by changing the researcher.

---

## Part 6: Implementation Sequencing

The changes should be implemented in this order to maintain a passing test suite at each step:

1. **models.py first** — add all new Pydantic models and extend existing ones with `= None`/`= []` defaults. Run `pytest`: all 139 tests should still pass because all new fields are optional.

2. **test_deep_dive.py — new model tests** — add `TestLogicalQuality`, `TestCitedArgument`, `TestBiasIndicator`, `TestCredibilityBreakdown` model validation tests. These test only model construction and will pass immediately after step 1.

3. **analyzer.py** — extend `_SYSTEM` prompt to include `logical_quality`. Extend `run()` to parse it with a `.get()` fallback. Add new analyzer tests. Run `pytest`: all pass.

4. **evaluator.py** — extend `_SYSTEM` prompt to include all new fields. Update `_call_llm` to include source URLs and fact_check evidence in the input. Update `run()` to parse new fields. Increase `max_tokens` to `4096` and content limit to `[:8000]`. Add new evaluator tests. Run `pytest`: all pass.

5. **reporter.py** — add rendering logic for all new sections with fallback when fields are empty. Add new reporter tests. Run `pytest`: all pass.

6. **Final full run**: `pytest tests/ -v` should show all previously-passing tests still green plus all new tests green. Then `ruff check .` to verify style.

---

## Trade-off Notes

**One LLM call vs. two**: A second LLM call (a dedicated "fact-check verifier" call) would produce more focused fact-check assessments but would double the latency of the evaluation step. The chosen design folds fact-check assessment into the same evaluator prompt. Given that the evaluator already receives the fact-check snippets, the LLM has all context it needs.

**LogicalQuality in analyzer vs. evaluator**: Logical quality assessment belongs in the analyzer because it concerns the internal structure of the article's own argument, which is independent of external evidence. The evaluator is concerned with external validity (do outside sources confirm or contradict). This separation gives each LLM call a single clear responsibility.

**`assessed_fact_checks` as new field vs. mutating `ResearchResults.fact_checks`**: Adding `assessed_fact_checks` to `MultiPerspectiveEvaluation` keeps the data flow immutable — the researcher's raw output is not overwritten. The reporter selects the evaluated version when available.

**`pro_arguments`/`con_arguments` as plain strings kept in parallel with `cited_pro_arguments`/`cited_con_arguments`**: This avoids breaking the 12 existing tests that check `result.pro_arguments[0]`. The LLM is prompted to fill the cited lists; the run() method derives the plain lists from them as a fallback.

---

### Critical Files for Implementation

- `/root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive/models.py` — All new Pydantic models (`LogicalQuality`, `CitedArgument`, `BiasIndicator`, `CredibilityBreakdown`) and extensions to `StructuralAnalysis`, `FactCheck`, `MultiPerspectiveEvaluation` go here first
- `/root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive/evaluator.py` — Core logic change: enriched system prompt, fact-check evidence added to LLM input, new fields parsed in `run()`, `max_tokens` increase; this is where the fact-check status bug is fixed
- `/root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive/reporter.py` — All new Markdown section rendering (citations, breakdown table, disputes, synthesis, bias table, logical quality block) and the critical fix that prefers `assessed_fact_checks` over `fact_checks`
- `/root/projects/ai_usecases_explorer/src/ai_usecases_explorer/deep_dive/analyzer.py` — Extended system prompt to elicit `logical_quality` JSON block; `run()` extended to parse `LogicalQuality` with `None` fallback
- `/root/projects/ai_usecases_explorer/tests/test_deep_dive.py` — All new TDD tests written before/alongside each implementation step; existing fixture functions (`_structural_analysis`, `_evaluation`) must not be changed to preserve existing test coverage