You are a senior code reviewer. Review the following code changes.

## Specification

No specification provided. Focus on correctness, tests, and integration.





## Code Changes

```diff
diff --git a/expert_build/prompts.py b/expert_build/prompts.py
index 2526135..9110d81 100644
--- a/expert_build/prompts.py
+++ b/expert_build/prompts.py
@@ -6,8 +6,10 @@
 Given the following documentation page, create a concise summary suitable for \
 building domain expertise. Structure your output as:
 
-## Overview
-One paragraph summarizing what this page covers.
+## <Descriptive Title>
+Start with a short, specific title that names the topic (e.g., \
+"IAM Role Configuration", "Network Policy Rules", "Cluster Autoscaling"). \
+Then one paragraph summarizing what this page covers.
 
 ## Key Concepts
 Bulleted list of the most important facts, definitions, and concepts.
@@ -28,6 +30,39 @@
 {content}
 """
 
+SUMMARIZE_CODE = """\
+You are an expert technical writer creating structured notes from source code.
+
+Given the following source code file, create a concise summary focused on how \
+this code is used in practice. Structure your output as:
+
+## <Descriptive Title>
+Start with a short, specific title that names the module or component (e.g., \
+"CLI Entry Point", "PDF Chunker", "LLM Invocation Layer"). Then one paragraph \
+summarizing what this code does and its role in the project.
+
+## Usage Patterns
+How this code is meant to be called or used — entry points, key functions, \
+typical invocations. Include code snippets where helpful.
+
+## API and Configuration
+Key parameters, options, environment variables, config files, or arguments \
+this code accepts.
+
+## Key Behaviors
+Important behaviors, error handling, edge cases, or gotchas a user should know about.
+
+## Relationships
+How this code connects to other components — what it imports, what calls it, \
+what services or systems it interacts with.
+
+---
+
+SOURCE CODE:
+
+{content}
+"""
+
 PROPOSE_BELIEFS = """\
 You are extracting factual claims from study notes to build a belief registry.
 
diff --git a/expert_build/summarize.py b/expert_build/summarize.py
index 5fb718c..e0d6e0b 100644
--- a/expert_build/summarize.py
+++ b/expert_build/summarize.py
@@ -6,7 +6,7 @@
 from pathlib import Path
 
 from .llm import check_model_available, invoke_sync
-from .prompts import SUMMARIZE
+from .prompts import SUMMARIZE, SUMMARIZE_CODE
 
 
 def cmd_summarize(args):
@@ -24,9 +24,12 @@ def cmd_summarize(args):
         print("Install claude CLI or specify --model")
         sys.exit(1)
 
-    sources = sorted(input_dir.glob("*.md"))
+    sources = sorted(
+        [*input_dir.glob("*.md"), *input_dir.glob("*.py")],
+        key=lambda p: p.name,
+    )
     if not sources:
-        print(f"No .md files in {input_dir}")
+        print(f"No .md or .py files in {input_dir}")
         return
 
     if args.limit:
@@ -70,9 +73,17 @@ def cmd_summarize(args):
 
         # Truncate very long documents
         if len(content) > 30000:
+            original_len = len(content)
             content = content[:30000] + "\n\n[Truncated — original was longer]"
+            if source_path.suffix == ".pdf":
+                print(f"  WARN: truncated from {original_len} to 30000 chars. "
+                      f"Consider: expert-build chunk-pdf {source_path}")
+            else:
+                print(f"  WARN: truncated from {original_len} to 30000 chars. "
+                      f"Large documents may lose tail content.")
 
-        prompt = SUMMARIZE.format(content=content)
+        template = SUMMARIZE_CODE if source_path.suffix == ".py" else SUMMARIZE
+        prompt = template.format(content=content)
 
         try:
             summary = invoke_sync(prompt, model=args.model)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_summarize.py b/tests/test_summarize.py
new file mode 100644
index 0000000..5d4d0c1
--- /dev/null
+++ b/tests/test_summarize.py
@@ -0,0 +1,243 @@
+"""Tests for expert_build.summarize."""
+
+import types
+from pathlib import Path
+from unittest.mock import patch, MagicMock
+
+import pytest
+
+from expert_build.summarize import cmd_summarize
+from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE
+
+
+# --- Fixtures ---
+
+@pytest.fixture
+def source_dir(tmp_path):
+    """Create a temp directory with sample source files."""
+    src = tmp_path / "sources"
+    src.mkdir()
+    return src
+
+
+@pytest.fixture
+def work_dir(tmp_path, monkeypatch):
+    """Set working directory to tmp_path so .summarized manifest is isolated."""
+    wd = tmp_path / "work"
+    wd.mkdir()
+    monkeypatch.chdir(wd)
+    return wd
+
+
+def make_args(input_dir, model="test-model", limit=None):
+    return types.SimpleNamespace(input_dir=str(input_dir), model=model, limit=limit)
+
+
+# --- File discovery tests ---
+
+def test_discovers_md_files(source_dir, work_dir):
+    (source_dir / "doc.md").write_text("# Hello\nSome content")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Topic Title\nSummary"), \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+        cmd_summarize(args)
+
+    assert mock_run.called
+
+
+def test_discovers_py_files(source_dir, work_dir):
+    (source_dir / "module.py").write_text("def hello(): pass")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/module.md", stderr="")
+        cmd_summarize(args)
+
+    assert mock_llm.called
+
+
+def test_discovers_both_md_and_py(source_dir, work_dir):
+    (source_dir / "alpha.md").write_text("# Alpha\nContent")
+    (source_dir / "beta.py").write_text("x = 1")
+    args = make_args(source_dir)
+
+    calls = []
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/x.md", stderr="")
+        cmd_summarize(args)
+
+    assert mock_llm.call_count == 2
+
+
+def test_ignores_other_extensions(source_dir, work_dir):
+    (source_dir / "data.json").write_text("{}")
+    (source_dir / "notes.txt").write_text("hello")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync") as mock_llm:
+        cmd_summarize(args)
+
+    assert not mock_llm.called
+
+
+# --- Template selection tests ---
+
+def test_uses_summarize_code_for_py(source_dir, work_dir):
+    (source_dir / "module.py").write_text("def hello(): pass")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Module\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/module.md", stderr="")
+        cmd_summarize(args)
+
+    prompt = mock_llm.call_args[0][0]
+    assert "source code" in prompt.lower()
+
+
+def test_uses_summarize_for_md(source_dir, work_dir):
+    (source_dir / "doc.md").write_text("# Hello\nSome content")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Doc Title\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+        cmd_summarize(args)
+
+    prompt = mock_llm.call_args[0][0]
+    assert "documentation page" in prompt.lower()
+
+
+# --- Truncation tests ---
+
+def test_truncation_warning_for_large_file(source_dir, work_dir, capsys):
+    (source_dir / "big.md").write_text("x" * 50000)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Big Doc\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/big.md", stderr="")
+        cmd_summarize(args)
+
+    captured = capsys.readouterr()
+    assert "WARN: truncated from 50000 to 30000 chars" in captured.out
+    assert "Large documents may lose tail content" in captured.out
+
+
+def test_truncation_content_is_capped(source_dir, work_dir):
+    (source_dir / "big.md").write_text("x" * 50000)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Big\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/big.md", stderr="")
+        cmd_summarize(args)
+
+    prompt = mock_llm.call_args[0][0]
+    assert "[Truncated" in prompt
+    assert len(prompt) < 50000
+
+
+def test_no_truncation_warning_for_small_file(source_dir, work_dir, capsys):
+    (source_dir / "small.md").write_text("Short content")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Small\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/small.md", stderr="")
+        cmd_summarize(args)
+
+    captured = capsys.readouterr()
+    assert "WARN" not in captured.out
+
+
+# --- Manifest / idempotency tests ---
+
+def test_skips_already_summarized(source_dir, work_dir):
+    (source_dir / "doc.md").write_text("# Hello\nContent")
+    manifest = work_dir / ".summarized"
+    manifest.write_text(f"{source_dir / 'doc.md'}\n")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync") as mock_llm:
+        cmd_summarize(args)
+
+    assert not mock_llm.called
+
+
+def test_manifest_records_processed_file(source_dir, work_dir):
+    (source_dir / "doc.md").write_text("# Hello\nContent")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary"), \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+        cmd_summarize(args)
+
+    manifest = work_dir / ".summarized"
+    assert manifest.exists()
+    assert str(source_dir / "doc.md") in manifest.read_text()
+
+
+# --- Frontmatter stripping tests ---
+
+def test_strips_frontmatter_before_summarizing(source_dir, work_dir):
+    content = "---\nsource_url: https://example.com\n---\n\nActual content here"
+    (source_dir / "doc.md").write_text(content)
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync", return_value="## Title\nSummary") as mock_llm, \
+         patch("subprocess.run") as mock_run:
+        mock_run.return_value = MagicMock(returncode=0, stdout="Created entries/doc.md", stderr="")
+        cmd_summarize(args)
+
+    prompt = mock_llm.call_args[0][0]
+    assert "source_url" not in prompt
+    assert "Actual content here" in prompt
+
+
+def test_skips_empty_content_after_frontmatter(source_dir, work_dir, capsys):
+    (source_dir / "empty.md").write_text("---\nsource_url: https://example.com\n---\n\n")
+    args = make_args(source_dir)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke_sync") as mock_llm:
+        cmd_summarize(args)
+
+    assert not mock_llm.called
+    captured = capsys.readouterr()
+    assert "SKIP" in captured.out
+
+
+# --- Prompt template tests ---
+
+def test_summarize_template_requests_descriptive_title():
+    assert "<Descriptive Title>" in SUMMARIZE
+
+
+def test_summarize_code_template_requests_descriptive_title():
+    assert "<Descriptive Title>" in SUMMARIZE_CODE
+
+
+def test_summarize_template_has_content_placeholder():
+    assert "{content}" in SUMMARIZE
+
+
+def test_summarize_code_template_has_content_placeholder():
+    assert "{content}" in SUMMARIZE_CODE

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "cmd_summarize_full": {
    "function": "cmd_summarize",
    "file": "expert_build/summarize.py",
    "start_line": 12,
    "end_line": 142,
    "source": "def cmd_summarize(args):\n    \"\"\"Generate entries from source documents.\"\"\"\n    from .caffeinate import hold as _caffeinate\n    _caffeinate()\n    input_dir = Path(args.input_dir)\n    if not input_dir.exists():\n        print(f\"Source directory not found: {input_dir}\")\n        print(\"Run: expert-build fetch-docs <url>\")\n        sys.exit(1)\n\n    if not check_model_available(args.model):\n        print(f\"Model not available: {args.model}\")\n        print(\"Install claude CLI or specify --model\")\n        sys.exit(1)\n\n    sources = sorted(\n        [*input_dir.glob(\"*.md\"), *input_dir.glob(\"*.py\")],\n        key=lambda p: p.name,\n    )\n    if not sources:\n        print(f\"No .md or .py files in {input_dir}\")\n        return\n\n    if args.limit:\n        sources = sources[:args.limit]\n\n    # Track what's been summarized\n    manifest = Path(\".summarized\")\n    done = set()\n    if manifest.exists():\n        done = set(manifest.read_text().strip().split(\"\\n\"))\n\n    processed = 0\n    skipped = 0\n\n    for source_path in sources:\n        if str(source_path) in done:\n            skipped += 1\n            continue\n\n        print(f\"Summarizing: {source_path.name}\")\n\n        content = source_path.read_text()\n\n        # Extract and strip frontmatter\n        source_url = None\n        source_id = None\n        if content.startswith(\"---\"):\n            end = content.find(\"---\", 3)\n            if end != -1:\n                frontmatter = content[3:end]\n                for line in frontmatter.splitlines():\n                    if line.startswith(\"source_url:\"):\n                        source_url = line.split(\":\", 1)[1].strip()\n                    elif line.startswith(\"source_id:\"):\n                        source_id = line.split(\":\", 1)[1].strip()\n                content = content[end + 3:].strip()\n\n        if not content.strip():\n            print(f\"  SKIP (empty)\")\n            continue\n\n        # Truncate very long documents\n        if len(content) > 30000:\n            original_len = len(content)\n            content = content[:30000] + \"\\n\\n[Truncated \u2014 original was longer]\"\n            if source_path.suffix == \".pdf\":\n                print(f\"  WARN: truncated from {original_len} to 30000 chars. \"\n                      f\"Consider: expert-build chunk-pdf {source_path}\")\n            else:\n                print(f\"  WARN: truncated from {original_len} to 30000 chars. \"\n                      f\"Large documents may lose tail content.\")\n\n        template = SUMMARIZE_CODE if source_path.suffix == \".py\" else SUMMARIZE\n        prompt = template.format(content=content)\n\n        try:\n            summary = invoke_sync(prompt, model=args.model)\n        except Exception as e:\n            print(f\"  ERROR: {e}\")\n            continue\n\n        # Extract a title from the summary or source filename\n        title_match = re.search(r\"^#+ (.+)$\", summary, re.MULTILINE)\n        title = title_match.group(1) if title_match else source_path.stem.replace(\"-\", \" \").title()\n        topic = source_path.stem\n\n        # Create entry via entry CLI\n        entry_path = None\n        try:\n            result = subprocess.run(\n                [\"entry\", \"create\", topic, title, \"--content\", summary],\n                capture_output=True, text=True,\n            )\n            if result.returncode == 0:\n                entry_path = result.stdout.strip().replace(\"Created \", \"\")\n                print(f\"  -> {result.stdout.strip()}\")\n            else:\n                # Try alternative invocation\n                result = subprocess.run(\n                    [\"entry\", \"create\", topic, title],\n                    input=summary,\n                    capture_output=True, text=True,\n                )\n                if result.returncode == 0:\n                    entry_path = result.stdout.strip().replace(\"Created \", \"\")\n                    print(f\"  -> {result.stdout.strip()}\")\n                else:\n                    print(f\"  WARN: entry create failed: {result.stderr.strip()}\")\n        except FileNotFoundError:\n            print(\"  ERROR: entry CLI not found. Install with: uv tool install entry\")\n            sys.exit(1)\n\n        # Prepend source provenance frontmatter to the entry file\n        if entry_path and source_url:\n            ep = Path(entry_path)\n            if ep.exists():\n                fm = f\"---\\nsource_url: {source_url}\\n\"\n                if source_id:\n                    fm += f\"source_id: {source_id}\\n\"\n                fm += \"---\\n\\n\"\n                ep.write_text(fm + ep.read_text())\n\n        # Record as done\n        with manifest.open(\"a\") as f:\n            f.write(f\"{source_path}\\n\")\n        done.add(str(source_path))\n\n        processed += 1\n\n    print(f\"\\nSummarized {processed} sources ({skipped} already done)\")"
  },
  "invoke_sync_body": {
    "function": "invoke_sync",
    "file": "expert_build/llm.py",
    "start_line": 56,
    "end_line": 58,
    "source": "def invoke_sync(prompt: str, model: str = \"claude\", timeout: int = DEFAULT_TIMEOUT) -> str:\n    \"\"\"Synchronous wrapper for invoke.\"\"\"\n    return asyncio.run(invoke(prompt, model, timeout))"
  },
  "invoke_sync_raises": {
    "function": "invoke_sync",
    "file": "expert_build/llm.py",
    "explicit_raises": [],
    "calls": [
      "invoke",
      "run"
    ]
  },
  "summarize_usages": {
    "symbol": "SUMMARIZE",
    "usages": [
      {
        "file": "tests/test_summarize.py",
        "line": 10,
        "text": "from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 231,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 235,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 239,
        "text": "assert \"{content}\" in SUMMARIZE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 243,
        "text": "assert \"{content}\" in SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 9,
        "text": "from .prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 85,
        "text": "template = SUMMARIZE_CODE if source_path.suffix == \".py\" else SUMMARIZE"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 3,
        "text": "SUMMARIZE = \"\"\"\\"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 33,
        "text": "SUMMARIZE_CODE = \"\"\"\\"
      }
    ],
    "production_usages": [
      {
        "file": "expert_build/summarize.py",
        "line": 9,
        "text": "from .prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 85,
        "text": "template = SUMMARIZE_CODE if source_path.suffix == \".py\" else SUMMARIZE"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 3,
        "text": "SUMMARIZE = \"\"\"\\"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 33,
        "text": "SUMMARIZE_CODE = \"\"\"\\"
      }
    ],
    "test_usages": [
      {
        "file": "tests/test_summarize.py",
        "line": 10,
        "text": "from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 231,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 235,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 239,
        "text": "assert \"{content}\" in SUMMARIZE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 243,
        "text": "assert \"{content}\" in SUMMARIZE_CODE"
      }
    ],
    "production_count": 4,
    "test_count": 5,
    "total_count": 9
  },
  "summarize_code_usages": {
    "symbol": "SUMMARIZE_CODE",
    "usages": [
      {
        "file": "tests/test_summarize.py",
        "line": 10,
        "text": "from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 235,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 243,
        "text": "assert \"{content}\" in SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 9,
        "text": "from .prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 85,
        "text": "template = SUMMARIZE_CODE if source_path.suffix == \".py\" else SUMMARIZE"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 33,
        "text": "SUMMARIZE_CODE = \"\"\"\\"
      }
    ],
    "production_usages": [
      {
        "file": "expert_build/summarize.py",
        "line": 9,
        "text": "from .prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 85,
        "text": "template = SUMMARIZE_CODE if source_path.suffix == \".py\" else SUMMARIZE"
      },
      {
        "file": "expert_build/prompts.py",
        "line": 33,
        "text": "SUMMARIZE_CODE = \"\"\"\\"
      }
    ],
    "test_usages": [
      {
        "file": "tests/test_summarize.py",
        "line": 10,
        "text": "from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 235,
        "text": "assert \"<Descriptive Title>\" in SUMMARIZE_CODE"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 243,
        "text": "assert \"{content}\" in SUMMARIZE_CODE"
      }
    ],
    "production_count": 3,
    "test_count": 3,
    "total_count": 6
  },
  "cmd_summarize_callers": {
    "symbol": "cmd_summarize",
    "production_callers": [
      {
        "file": "expert_build/summarize.py",
        "line": 12,
        "text": "def cmd_summarize(args):",
        "context_function": null,
        "context_snippet": "   9: from .prompts import SUMMARIZE, SUMMARIZE_CODE\n   10: \n   11: \n>> 12: def cmd_summarize(args):\n   13:     \"\"\"Generate entries from source documents.\"\"\"\n   14:     from .caffeinate import hold as _caffeinate\n   15:     _caffeinate()"
      },
      {
        "file": "expert_build/cli.py",
        "line": 110,
        "text": "\"summarize\": lambda a: _lazy(\"summarize\", \"cmd_summarize\")(a),",
        "context_function": "main",
        "context_snippet": "   107:         \"init\": lambda a: _lazy(\"init_cmd\", \"cmd_init\")(a),\n   108:         \"chunk-pdf\": lambda a: _lazy(\"chunk_pdf\", \"cmd_chunk_pdf\")(a),\n   109:         \"fetch-docs\": lambda a: _lazy(\"fetch\", \"cmd_fetch_docs\")(a),\n>> 110:         \"summarize\": lambda a: _lazy(\"summarize\", \"cmd_summarize\")(a),\n   111:         \"propose-beliefs\": lambda a: _lazy(\"propose\", \"cmd_propose_beliefs\")(a),\n   112:         \"accept-beliefs\": lambda a: _lazy(\"propose\", \"cmd_accept_beliefs\")(a),\n   113:         \"cert-coverage\": lambda a: _lazy(\"coverage\", \"cmd_cert_coverage\")(a),"
      }
    ],
    "test_callers": [
      {
        "file": "tests/test_summarize.py",
        "line": 9,
        "text": "from expert_build.summarize import cmd_summarize",
        "context_function": null,
        "context_snippet": "   6: \n   7: import pytest\n   8: \n>> 9: from expert_build.summarize import cmd_summarize\n   10: from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE\n   11: \n   12: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 46,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_md_files",
        "context_snippet": "   43:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Topic Title\\nSummary\"), \\\n   44:          patch(\"subprocess.run\") as mock_run:\n   45:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/doc.md\", stderr=\"\")\n>> 46:         cmd_summarize(args)\n   47: \n   48:     assert mock_run.called\n   49: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 59,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_py_files",
        "context_snippet": "   56:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Module\\nSummary\") as mock_llm, \\\n   57:          patch(\"subprocess.run\") as mock_run:\n   58:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/module.md\", stderr=\"\")\n>> 59:         cmd_summarize(args)\n   60: \n   61:     assert mock_llm.called\n   62: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 74,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_both_md_and_py",
        "context_snippet": "   71:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Title\\nSummary\") as mock_llm, \\\n   72:          patch(\"subprocess.run\") as mock_run:\n   73:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/x.md\", stderr=\"\")\n>> 74:         cmd_summarize(args)\n   75: \n   76:     assert mock_llm.call_count == 2\n   77: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 86,
        "text": "cmd_summarize(args)",
        "context_function": "test_ignores_other_extensions",
        "context_snippet": "   83: \n   84:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   85:          patch(\"expert_build.summarize.invoke_sync\") as mock_llm:\n>> 86:         cmd_summarize(args)\n   87: \n   88:     assert not mock_llm.called\n   89: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 101,
        "text": "cmd_summarize(args)",
        "context_function": "test_uses_summarize_code_for_py",
        "context_snippet": "   98:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Module\\nSummary\") as mock_llm, \\\n   99:          patch(\"subprocess.run\") as mock_run:\n   100:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/module.md\", stderr=\"\")\n>> 101:         cmd_summarize(args)\n   102: \n   103:     prompt = mock_llm.call_args[0][0]\n   104:     assert \"source code\" in prompt.lower()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 115,
        "text": "cmd_summarize(args)",
        "context_function": "test_uses_summarize_for_md",
        "context_snippet": "   112:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Doc Title\\nSummary\") as mock_llm, \\\n   113:          patch(\"subprocess.run\") as mock_run:\n   114:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/doc.md\", stderr=\"\")\n>> 115:         cmd_summarize(args)\n   116: \n   117:     prompt = mock_llm.call_args[0][0]\n   118:     assert \"documentation page\" in prompt.lower()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 131,
        "text": "cmd_summarize(args)",
        "context_function": "test_truncation_warning_for_large_file",
        "context_snippet": "   128:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Big Doc\\nSummary\") as mock_llm, \\\n   129:          patch(\"subprocess.run\") as mock_run:\n   130:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/big.md\", stderr=\"\")\n>> 131:         cmd_summarize(args)\n   132: \n   133:     captured = capsys.readouterr()\n   134:     assert \"WARN: truncated from 50000 to 30000 chars\" in captured.out"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 146,
        "text": "cmd_summarize(args)",
        "context_function": "test_truncation_content_is_capped",
        "context_snippet": "   143:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Big\\nSummary\") as mock_llm, \\\n   144:          patch(\"subprocess.run\") as mock_run:\n   145:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/big.md\", stderr=\"\")\n>> 146:         cmd_summarize(args)\n   147: \n   148:     prompt = mock_llm.call_args[0][0]\n   149:     assert \"[Truncated\" in prompt"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 161,
        "text": "cmd_summarize(args)",
        "context_function": "test_no_truncation_warning_for_small_file",
        "context_snippet": "   158:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Small\\nSummary\") as mock_llm, \\\n   159:          patch(\"subprocess.run\") as mock_run:\n   160:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/small.md\", stderr=\"\")\n>> 161:         cmd_summarize(args)\n   162: \n   163:     captured = capsys.readouterr()\n   164:     assert \"WARN\" not in captured.out"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 177,
        "text": "cmd_summarize(args)",
        "context_function": "test_skips_already_summarized",
        "context_snippet": "   174: \n   175:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   176:          patch(\"expert_build.summarize.invoke_sync\") as mock_llm:\n>> 177:         cmd_summarize(args)\n   178: \n   179:     assert not mock_llm.called\n   180: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 190,
        "text": "cmd_summarize(args)",
        "context_function": "test_manifest_records_processed_file",
        "context_snippet": "   187:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Title\\nSummary\"), \\\n   188:          patch(\"subprocess.run\") as mock_run:\n   189:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/doc.md\", stderr=\"\")\n>> 190:         cmd_summarize(args)\n   191: \n   192:     manifest = work_dir / \".summarized\"\n   193:     assert manifest.exists()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 208,
        "text": "cmd_summarize(args)",
        "context_function": "test_strips_frontmatter_before_summarizing",
        "context_snippet": "   205:          patch(\"expert_build.summarize.invoke_sync\", return_value=\"## Title\\nSummary\") as mock_llm, \\\n   206:          patch(\"subprocess.run\") as mock_run:\n   207:         mock_run.return_value = MagicMock(returncode=0, stdout=\"Created entries/doc.md\", stderr=\"\")\n>> 208:         cmd_summarize(args)\n   209: \n   210:     prompt = mock_llm.call_args[0][0]\n   211:     assert \"source_url\" not in prompt"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 221,
        "text": "cmd_summarize(args)",
        "context_function": "test_skips_empty_content_after_frontmatter",
        "context_snippet": "   218: \n   219:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   220:          patch(\"expert_build.summarize.invoke_sync\") as mock_llm:\n>> 221:         cmd_summarize(args)\n   222: \n   223:     assert not mock_llm.called\n   224:     captured = capsys.readouterr()"
      }
    ],
    "production_count": 2,
    "test_count": 14,
    "total_count": 16
  },
  "summarize_imports": {
    "file": "expert_build/summarize.py",
    "imports": [
      "re",
      "subprocess",
      "sys"
    ],
    "from_imports": [
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke_sync"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "SUMMARIZE",
          "SUMMARIZE_CODE"
        ]
      }
    ],
    "import_section": "\"\"\"Summarize source documents into entries using an LLM.\"\"\"\n\nimport re\nimport subprocess\nimport sys\nfrom pathlib import Path\n\nfrom .llm import check_model_available, invoke_sync\nfrom .prompts import SUMMARIZE, SUMMARIZE_CODE\n\n\ndef cmd_summarize(args):\n    \"\"\"Generate entries from source documents.\"\"\"\n    from .caffeinate import hold as _caffeinate"
  },
  "existing_tests": {
    "source_file": "expert_build/summarize.py",
    "test_files": [
      {
        "path": "tests/test_summarize.py",
        "exists": true,
        "line_count": 243
      },
      {
        "path": "tests/test_summarizepy",
        "exists": false
      }
    ],
    "test_count": 2
  }
}
```

Use these results to inform your review. Do not request the same observations again.


## Instructions

For each significant change (new file, modified function, etc.), provide a structured verdict.

Use this exact format for each change:

### <file_path or file_path:function_name>
VERDICT: PASS | CONCERN | BLOCK
CORRECTNESS: VALID | QUESTIONABLE | BROKEN
SPEC_COMPLIANCE: MEETS | PARTIAL | VIOLATES | N/A
ISSUE_COMPLIANCE: ADDRESSES | PARTIAL | UNRELATED | N/A
BELIEF_COMPLIANCE: CONSISTENT | VIOLATES | N/A
TEST_COVERAGE: COVERED | PARTIAL | UNTESTED
INTEGRATION: WIRED | PARTIAL | MISSING
REASONING: <brief explanation of your assessment>
---

## Review Criteria

1. **CORRECTNESS**: Does the code do what it claims? Is the logic sound?
   - VALID: Logic is correct, no bugs apparent
   - QUESTIONABLE: Logic may have edge cases or unclear behavior
   - BROKEN: Clear bugs or incorrect behavior

2. **SPEC_COMPLIANCE**: Does it meet MUST requirements from the spec?
   - MEETS: All relevant spec requirements satisfied
   - PARTIAL: Some requirements met, others missing or incomplete
   - VIOLATES: Contradicts spec requirements
   - N/A: No spec provided or not applicable

3. **ISSUE_COMPLIANCE** (only when an issue is provided): Do the changes address the problem or feature described in the issue?
   - ADDRESSES: Changes directly solve the issue's stated problem or implement the requested feature
   - PARTIAL: Changes partially address the issue but leave some aspects unresolved
   - UNRELATED: Changes do not appear related to the issue
   - N/A: No issue provided

4. **TEST_COVERAGE**: Are there tests for the new/changed code?
   - COVERED: Tests exist and cover the changes
   - PARTIAL: Some tests exist but coverage is incomplete
   - UNTESTED: No tests for the changes

5. **INTEGRATION**: Are callers updated? Is the feature usable end-to-end?
   - WIRED: Feature is fully integrated and usable
   - PARTIAL: Interface exists but callers not updated, or integration incomplete
   - MISSING: No integration with existing code

6. **BELIEF_COMPLIANCE** (only when beliefs are provided): Do the changes respect known architectural invariants, contracts, and rules?
   - CONSISTENT: Changes align with or reinforce known beliefs
   - VIOLATES: Changes contradict a specific belief — cite the belief ID
   - N/A: No beliefs provided or no relevant beliefs apply

## Verdict Guidelines

- **BLOCK**: Security issues, broken functionality, spec violations, or missing critical integration
- **CONCERN**: Missing tests, partial integration, questionable patterns, or unclear logic
- **PASS**: Correct, tested, well-integrated code

## Important

- Full function bodies for modified functions may be available in the observations section — use them to verify the complete logic, not just the diff hunks
- Related test files (prefixed with ``related_test:``) may be included in observations — check whether existing test assertions still match modified return types, signatures, or behavior. Flag any test that would break due to the changes
- If duplicate test coverage is detected (multiple test files covering the same source), note it in your review
- Focus on actual issues, not style preferences
- If a method signature is added but callers aren't updated, that's PARTIAL integration
- Be specific in reasoning - reference line numbers or function names
- When in doubt, use CONCERN rather than PASS

## Self-Review

After completing your review, add a brief self-assessment:

### SELF_REVIEW
LIMITATIONS: <what context were you missing that affected review quality?>
---

Examples of limitations:
- "Could not see full class to verify no other methods access the modified field"
- "Test file not included in diff - cannot verify coverage claims"
- "Spec file referenced but not provided"


## Feature Requests

If this review tool could be improved to help you do a better job, suggest features:

### FEATURE_REQUESTS
- <suggestion 1>
- <suggestion 2>
---

Examples:
- "Include full file context for modified functions, not just diff hunks"
- "Show callers of modified methods to verify integration"
- "Include test file alongside implementation changes"

Only include this section if you have specific suggestions. Skip if none.
