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/cli.py b/expert_build/cli.py
index ebc3244..42beb32 100644
--- a/expert_build/cli.py
+++ b/expert_build/cli.py
@@ -62,6 +62,8 @@ def main():
     sum_p.add_argument("--input-dir", default="sources", help="Source directory (default: sources)")
     sum_p.add_argument("--recursive", "-r", action="store_true",
                        help="Recursively search subdirectories")
+    sum_p.add_argument("--parallel", type=int, default=1,
+                       help="Number of parallel LLM calls (default: 1)")
     sum_p.add_argument("--limit", type=int, help="Max files to process")
     sum_p.add_argument("--model", default="claude", help="Model to use (default: claude)")
 
diff --git a/expert_build/summarize.py b/expert_build/summarize.py
index 78440eb..1cf6a70 100644
--- a/expert_build/summarize.py
+++ b/expert_build/summarize.py
@@ -1,13 +1,102 @@
 """Summarize source documents into entries using an LLM."""
 
+import asyncio
 import sys
 from datetime import date
 from pathlib import Path
 
-from .llm import check_model_available, invoke_sync
+from .llm import check_model_available, invoke
 from .prompts import SUMMARIZE, SUMMARIZE_CODE
 
 
+def _prepare_source(source_path):
+    """Read source file, strip frontmatter, truncate if needed.
+
+    Returns (source_url, source_id, prompt) or None if skipped.
+    """
+    content = source_path.read_text()
+
+    source_url = None
+    source_id = None
+    if content.startswith("---"):
+        end = content.find("---", 3)
+        if end != -1:
+            frontmatter = content[3:end]
+            for line in frontmatter.splitlines():
+                if line.startswith("source_url:"):
+                    source_url = line.split(":", 1)[1].strip()
+                elif line.startswith("source:"):
+                    val = line.split(":", 1)[1].strip()
+                    if not source_url and val.startswith(("http://", "https://")):
+                        source_url = val
+                elif line.startswith("source_id:"):
+                    source_id = line.split(":", 1)[1].strip()
+            content = content[end + 3:].strip()
+
+    if not content.strip():
+        return None
+
+    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"Consider: expert-build chunk-docs")
+
+    template = SUMMARIZE_CODE if source_path.suffix == ".py" else SUMMARIZE
+    prompt = template.format(content=content)
+
+    return source_url, source_id, prompt
+
+
+def _write_entry(source_path, summary, source_url, source_id):
+    """Write entry file with provenance frontmatter."""
+    topic = source_path.stem
+    today = date.today()
+    entry_dir = Path("entries") / str(today.year) / f"{today.month:02d}" / f"{today.day:02d}"
+    entry_dir.mkdir(parents=True, exist_ok=True)
+    entry_path = entry_dir / f"{topic}.md"
+
+    fm_lines = [f"source: {source_path}"]
+    if source_url:
+        fm_lines.append(f"source_url: {source_url}")
+    if source_id:
+        fm_lines.append(f"source_id: {source_id}")
+    frontmatter = "---\n" + "\n".join(fm_lines) + "\n---\n\n"
+
+    entry_path.write_text(frontmatter + summary + "\n")
+    return entry_path
+
+
+async def _summarize_one(source_path, model, semaphore, manifest, done):
+    """Summarize a single source file under concurrency limit."""
+    async with semaphore:
+        prepared = _prepare_source(source_path)
+        if prepared is None:
+            print(f"  SKIP (empty): {source_path.name}")
+            return False
+
+        source_url, source_id, prompt = prepared
+        print(f"Summarizing: {source_path.name}")
+
+        try:
+            summary = await invoke(prompt, model=model)
+        except Exception as e:
+            print(f"  ERROR ({source_path.name}): {e}")
+            return False
+
+        entry_path = _write_entry(source_path, summary, source_url, source_id)
+        print(f"  -> Created {entry_path}")
+
+        with manifest.open("a") as f:
+            f.write(f"{source_path}\n")
+        done.add(str(source_path))
+        return True
+
+
 def cmd_summarize(args):
     """Generate entries from source documents."""
     from .caffeinate import hold as _caffeinate
@@ -35,89 +124,29 @@ def cmd_summarize(args):
     if args.limit:
         sources = sources[:args.limit]
 
-    # Track what's been summarized
     manifest = Path(".summarized")
     done = set()
     if manifest.exists():
         done = set(manifest.read_text().strip().split("\n"))
 
-    processed = 0
-    skipped = 0
-
-    for source_path in sources:
-        if str(source_path) in done:
-            skipped += 1
-            continue
+    to_process = [s for s in sources if str(s) not in done]
+    skipped = len(sources) - len(to_process)
 
-        print(f"Summarizing: {source_path.name}")
-
-        content = source_path.read_text()
-
-        # Extract and strip frontmatter
-        source_url = None
-        source_id = None
-        if content.startswith("---"):
-            end = content.find("---", 3)
-            if end != -1:
-                frontmatter = content[3:end]
-                for line in frontmatter.splitlines():
-                    if line.startswith("source_url:"):
-                        source_url = line.split(":", 1)[1].strip()
-                    elif line.startswith("source:"):
-                        val = line.split(":", 1)[1].strip()
-                        if not source_url and val.startswith(("http://", "https://")):
-                            source_url = val
-                    elif line.startswith("source_id:"):
-                        source_id = line.split(":", 1)[1].strip()
-                content = content[end + 3:].strip()
-
-        if not content.strip():
-            print(f"  SKIP (empty)")
-            continue
-
-        # 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"Consider: expert-build chunk-docs")
-
-        template = SUMMARIZE_CODE if source_path.suffix == ".py" else SUMMARIZE
-        prompt = template.format(content=content)
-
-        try:
-            summary = invoke_sync(prompt, model=args.model)
-        except Exception as e:
-            print(f"  ERROR: {e}")
-            continue
-
-        topic = source_path.stem
-
-        # Write entry directly with provenance frontmatter
-        today = date.today()
-        entry_dir = Path("entries") / str(today.year) / f"{today.month:02d}" / f"{today.day:02d}"
-        entry_dir.mkdir(parents=True, exist_ok=True)
-        entry_path = entry_dir / f"{topic}.md"
-
-        fm_lines = [f"source: {source_path}"]
-        if source_url:
-            fm_lines.append(f"source_url: {source_url}")
-        if source_id:
-            fm_lines.append(f"source_id: {source_id}")
-        frontmatter = "---\n" + "\n".join(fm_lines) + "\n---\n\n"
+    if not to_process:
+        print(f"\nSummarized 0 sources ({skipped} already done)")
+        return
 
-        entry_path.write_text(frontmatter + summary + "\n")
-        print(f"  -> Created {entry_path}")
+    parallel = max(1, getattr(args, "parallel", 1))
+    semaphore = asyncio.Semaphore(parallel)
 
-        # Record as done
-        with manifest.open("a") as f:
-            f.write(f"{source_path}\n")
-        done.add(str(source_path))
+    async def run_all():
+        tasks = [
+            _summarize_one(s, args.model, semaphore, manifest, done)
+            for s in to_process
+        ]
+        return await asyncio.gather(*tasks)
 
-        processed += 1
+    results = asyncio.run(run_all())
+    processed = sum(1 for r in results if r)
 
     print(f"\nSummarized {processed} sources ({skipped} already done)")
diff --git a/tests/test_summarize.py b/tests/test_summarize.py
index 5b95108..1389343 100644
--- a/tests/test_summarize.py
+++ b/tests/test_summarize.py
@@ -1,8 +1,9 @@
 """Tests for expert_build.summarize."""
 
+import asyncio
 import types
 from pathlib import Path
-from unittest.mock import patch
+from unittest.mock import patch, AsyncMock
 
 import pytest
 
@@ -29,8 +30,8 @@ def work_dir(tmp_path, monkeypatch):
     return wd
 
 
-def make_args(input_dir, model="test-model", limit=None, recursive=False):
-    return types.SimpleNamespace(input_dir=str(input_dir), model=model, limit=limit, recursive=recursive)
+def make_args(input_dir, model="test-model", limit=None, recursive=False, parallel=1):
+    return types.SimpleNamespace(input_dir=str(input_dir), model=model, limit=limit, recursive=recursive, parallel=parallel)
 
 
 def _find_entry(work_dir):
@@ -47,7 +48,7 @@ def test_discovers_md_files(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Topic Title\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -59,7 +60,7 @@ def test_discovers_py_files(source_dir, work_dir):
     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"):
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Module\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -72,7 +73,7 @@ def test_discovers_both_md_and_py(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     assert mock_llm.call_count == 2
@@ -88,7 +89,7 @@ def test_recursive_discovers_nested_files(source_dir, work_dir):
     args = make_args(source_dir, recursive=True)
 
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     assert mock_llm.call_count == 2
@@ -104,7 +105,7 @@ def test_non_recursive_skips_nested_files(source_dir, work_dir):
     args = make_args(source_dir, recursive=False)
 
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     assert mock_llm.call_count == 1
@@ -116,7 +117,7 @@ def test_ignores_other_extensions(source_dir, work_dir):
     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:
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock) as mock_llm:
         cmd_summarize(args)
 
     assert not mock_llm.called
@@ -129,7 +130,7 @@ def test_uses_summarize_code_for_py(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Module\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -141,7 +142,7 @@ def test_uses_summarize_for_md(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Doc Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -155,7 +156,7 @@ def test_truncation_warning_for_large_file(source_dir, work_dir, capsys):
     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"):
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Big Doc\nSummary"):
         cmd_summarize(args)
 
     captured = capsys.readouterr()
@@ -168,7 +169,7 @@ def test_truncation_content_is_capped(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Big\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -181,7 +182,7 @@ def test_no_truncation_warning_for_small_file(source_dir, work_dir, capsys):
     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"):
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Small\nSummary"):
         cmd_summarize(args)
 
     captured = capsys.readouterr()
@@ -197,7 +198,7 @@ def test_skips_already_summarized(source_dir, work_dir):
     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:
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock) as mock_llm:
         cmd_summarize(args)
 
     assert not mock_llm.called
@@ -208,7 +209,7 @@ def test_manifest_records_processed_file(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
         cmd_summarize(args)
 
     manifest = work_dir / ".summarized"
@@ -224,7 +225,7 @@ def test_strips_frontmatter_before_summarizing(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -238,7 +239,7 @@ def test_strips_source_url_frontmatter(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary") as mock_llm:
         cmd_summarize(args)
 
     prompt = mock_llm.call_args[0][0]
@@ -251,7 +252,7 @@ def test_skips_empty_content_after_frontmatter(source_dir, work_dir, capsys):
     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:
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock) as mock_llm:
         cmd_summarize(args)
 
     assert not mock_llm.called
@@ -267,7 +268,7 @@ def test_entry_has_source_frontmatter(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -283,7 +284,7 @@ def test_entry_has_source_url_from_fetch_frontmatter(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## Page\nSummary"):
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Page\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -298,7 +299,7 @@ def test_entry_has_source_id_when_present(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -313,7 +314,7 @@ def test_entry_contains_llm_summary(source_dir, work_dir):
     args = make_args(source_dir)
 
     with patch("expert_build.summarize.check_model_available", return_value=True), \
-         patch("expert_build.summarize.invoke_sync", return_value="## My Title\nDetailed summary here"):
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## My Title\nDetailed summary here"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -327,7 +328,7 @@ def test_entry_directory_structure(source_dir, work_dir):
     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("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
         cmd_summarize(args)
 
     entry = _find_entry(work_dir)
@@ -352,3 +353,31 @@ def test_summarize_template_has_content_placeholder():
 
 def test_summarize_code_template_has_content_placeholder():
     assert "{content}" in SUMMARIZE_CODE
+
+
+# --- Parallel tests ---
+
+def test_parallel_summarizes_multiple_files(source_dir, work_dir):
+    for i in range(4):
+        (source_dir / f"doc{i}.md").write_text(f"# Doc {i}\nContent {i}")
+    args = make_args(source_dir, parallel=2)
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
+        cmd_summarize(args)
+
+    entries = list((work_dir / "entries").rglob("*.md"))
+    assert len(entries) == 4
+
+
+def test_parallel_default_is_sequential(source_dir, work_dir):
+    (source_dir / "doc.md").write_text("# Hello\nContent")
+    args = make_args(source_dir)
+    assert args.parallel == 1
+
+    with patch("expert_build.summarize.check_model_available", return_value=True), \
+         patch("expert_build.summarize.invoke", new_callable=AsyncMock, return_value="## Title\nSummary"):
+        cmd_summarize(args)
+
+    entry = _find_entry(work_dir)
+    assert "Summary" in entry.read_text()

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "invoke_function_body": {
    "function": "invoke",
    "file": "expert_build/llm.py",
    "start_line": 117,
    "end_line": 151,
    "source": "async def invoke(prompt: str, model: str = \"claude\", timeout: int = DEFAULT_TIMEOUT) -> str:\n    \"\"\"Invoke model via CLI, piping prompt through stdin.\n\n    Uses --output-format json to capture token/cost data.\n    Accumulated stats available via get_cost_summary().\n    \"\"\"\n    if model not in MODEL_COMMANDS:\n        raise ValueError(f\"Unknown model: {model}. Available: {list(MODEL_COMMANDS.keys())}\")\n\n    cmd = MODEL_COMMANDS[model]\n\n    # Remove CLAUDECODE env var to allow nested claude invocation\n    env = {k: v for k, v in os.environ.items() if k != \"CLAUDECODE\"}\n\n    proc = await asyncio.create_subprocess_exec(\n        *cmd,\n        stdin=asyncio.subprocess.PIPE,\n        stdout=asyncio.subprocess.PIPE,\n        stderr=asyncio.subprocess.PIPE,\n        env=env,\n    )\n\n    try:\n        stdout, stderr = await asyncio.wait_for(\n            proc.communicate(prompt.encode()),\n            timeout=timeout,\n        )\n    except TimeoutError:\n        proc.kill()\n        raise TimeoutError(f\"Model {model} timed out after {timeout}s\") from None\n\n    if proc.returncode != 0:\n        raise RuntimeError(f\"Model {model} failed: {stderr.decode()}\")\n\n    return _parse_cli_json(stdout.decode(), model)"
  },
  "invoke_sync_migration": {
    "old_name": "invoke_sync",
    "old_name_usages": [
      {
        "file": "tests/test_exam.py",
        "line": 80,
        "text": "with patch(\"expert_build.exam.invoke_sync\","
      },
      {
        "file": "tests/test_exam.py",
        "line": 91,
        "text": "with patch(\"expert_build.exam.invoke_sync\", return_value=\"Still no format\"):"
      },
      {
        "file": "tests/test_exam.py",
        "line": 110,
        "text": "with patch(\"expert_build.exam.invoke_sync\","
      },
      {
        "file": "tests/test_exam.py",
        "line": 119,
        "text": "with patch(\"expert_build.exam.invoke_sync\","
      },
      {
        "file": "tests/test_exam.py",
        "line": 136,
        "text": "with patch(\"expert_build.exam.invoke_sync\", side_effect=side_effect):"
      },
      {
        "file": "tests/test_exam.py",
        "line": 144,
        "text": "with patch(\"expert_build.exam.invoke_sync\", return_value=\"No JSON at all\"):"
      },
      {
        "file": "tests/test_exam.py",
        "line": 152,
        "text": "with patch(\"expert_build.exam.invoke_sync\","
      },
      {
        "file": "tests/test_exam.py",
        "line": 169,
        "text": "with patch(\"expert_build.exam.invoke_sync\", side_effect=side_effect):"
      },
      {
        "file": "tests/test_exam.py",
        "line": 178,
        "text": "with patch(\"expert_build.exam.invoke_sync\","
      },
      {
        "file": "tests/test_coverage.py",
        "line": 55,
        "text": "patch(\"expert_build.coverage.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_coverage.py",
        "line": 72,
        "text": "patch(\"expert_build.coverage.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_coverage.py",
        "line": 93,
        "text": "patch(\"expert_build.coverage.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_coverage.py",
        "line": 109,
        "text": "patch(\"expert_build.coverage.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_coverage.py",
        "line": 125,
        "text": "patch(\"expert_build.coverage.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 64,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 88,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 113,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 140,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 171,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 192,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 216,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 239,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 262,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_propose.py",
        "line": 282,
        "text": "patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\"
      },
      {
        "file": "tests/test_pipeline.py",
        "line": 186,
        "text": "patch(\"expert_build.llm.invoke_sync\", return_value=\"No proposals\"), \\"
      },
      {
        "file": "tests/test_pipeline.py",
        "line": 202,
        "text": "patch(\"expert_build.llm.invoke_sync\", return_value=\"proposal text\"), \\"
      },
      {
        "file": "expert_build/exam.py",
        "line": 9,
        "text": "from .llm import check_model_available, extract_json, invoke_sync, RETRY_JSON"
      },
      {
        "file": "expert_build/exam.py",
        "line": 114,
        "text": "retry_response = invoke_sync("
      },
      {
        "file": "expert_build/exam.py",
        "line": 132,
        "text": "response = invoke_sync(prompt, model=model, timeout=60)"
      },
      {
        "file": "expert_build/exam.py",
        "line": 143,
        "text": "retry_response = invoke_sync("
      }
    ],
    "old_name_count": 40,
    "stale_references": [
      {
        "file": "expert_build/exam.py",
        "line": 9,
        "text": "from .llm import check_model_available, extract_json, invoke_sync, RETRY_JSON"
      },
      {
        "file": "expert_build/exam.py",
        "line": 114,
        "text": "retry_response = invoke_sync("
      },
      {
        "file": "expert_build/exam.py",
        "line": 132,
        "text": "response = invoke_sync(prompt, model=model, timeout=60)"
      },
      {
        "file": "expert_build/exam.py",
        "line": 143,
        "text": "retry_response = invoke_sync("
      },
      {
        "file": "expert_build/exam.py",
        "line": 202,
        "text": "response = invoke_sync(prompt, model=args.model, timeout=120)"
      },
      {
        "file": "expert_build/propose.py",
        "line": 12,
        "text": "from .llm import check_model_available, extract_json, invoke_sync, RETRY_JSON"
      },
      {
        "file": "expert_build/propose.py",
        "line": 389,
        "text": "result = invoke_sync(prompt, model=args.model, timeout=600)"
      },
      {
        "file": "expert_build/propose.py",
        "line": 399,
        "text": "retry_response = invoke_sync("
      },
      {
        "file": "expert_build/llm.py",
        "line": 154,
        "text": "def invoke_sync(prompt: str, model: str = \"claude\", timeout: int = DEFAULT_TIMEOUT) -> str:"
      },
      {
        "file": "expert_build/coverage.py",
        "line": 9,
        "text": "from .llm import check_model_available, extract_json, invoke_sync, RETRY_JSON"
      },
      {
        "file": "expert_build/coverage.py",
        "line": 115,
        "text": "result = invoke_sync(prompt, model=args.model, timeout=120)"
      },
      {
        "file": "expert_build/coverage.py",
        "line": 118,
        "text": "retry_response = invoke_sync("
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 10,
        "text": "from .llm import check_model_available, invoke_sync"
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 193,
        "text": "response = invoke_sync(prompt, model=args.model, timeout=args.timeout)"
      }
    ],
    "stale_count": 14,
    "migration_complete": false,
    "new_name": "invoke",
    "new_name_usages": [
      {
        "file": "tests/test_summarize.py",
        "line": 51,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Topic Title\\nSummary"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 63,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Module\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 76,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 92,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 108,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 120,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 133,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Module\\nSummary\") as"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 145,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Doc Title\\nSummary\")"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 159,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Big Doc\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 172,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Big\\nSummary\") as mo"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 185,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Small\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 201,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 212,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 228,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 242,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 255,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 271,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 287,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Page\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 302,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 317,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## My Title\\nDetailed s"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 331,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 366,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 379,
        "text": "patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):"
      },
      {
        "file": ".venv/lib/python3.14/site-packages/bs4/filter.py",
        "line": 295,
        "text": "# No need to invoke the test function."
      },
      {
        "file": "expert_build/llm.py",
        "line": 117,
        "text": "async def invoke(prompt: str, model: str = \"claude\", timeout: int = DEFAULT_TIMEOUT) -> str:"
      },
      {
        "file": "expert_build/llm.py",
        "line": 155,
        "text": "\"\"\"Synchronous wrapper for invoke.\"\"\""
      },
      {
        "file": "expert_build/llm.py",
        "line": 156,
        "text": "return asyncio.run(invoke(prompt, model, timeout))"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 8,
        "text": "from .llm import check_model_available, invoke"
      },
      {
        "file": "expert_build/summarize.py",
        "line": 86,
        "text": "summary = await invoke(prompt, model=model)"
      }
    ],
    "new_name_count": 29
  },
  "invoke_raises": {
    "function": "invoke",
    "file": "expert_build/llm.py",
    "explicit_raises": [
      "ValueError",
      "TimeoutError",
      "RuntimeError"
    ],
    "calls": [
      "communicate",
      "kill",
      "_parse_cli_json",
      "keys",
      "items",
      "list",
      "RuntimeError",
      "wait_for",
      "ValueError",
      "decode",
      "create_subprocess_exec",
      "encode",
      "TimeoutError"
    ]
  },
  "cmd_summarize_callers": {
    "symbol": "cmd_summarize",
    "production_callers": [
      {
        "file": "expert_build/summarize.py",
        "line": 100,
        "text": "def cmd_summarize(args):"
      },
      {
        "file": "expert_build/cli.py",
        "line": 158,
        "text": "\"summarize\": lambda a: _lazy(\"summarize\", \"cmd_summarize\")(a),"
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 124,
        "text": "from .summarize import cmd_summarize"
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 130,
        "text": "cmd_summarize(sum_args)"
      }
    ],
    "test_callers": [
      {
        "file": "tests/test_summarize.py",
        "line": 10,
        "text": "from expert_build.summarize import cmd_summarize",
        "context_function": null,
        "context_snippet": "   7: \n   8: import pytest\n   9: \n>> 10: from expert_build.summarize import cmd_summarize\n   11: from expert_build.prompts import SUMMARIZE, SUMMARIZE_CODE\n   12: \n   13: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 52,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_md_files",
        "context_snippet": "   49: \n   50:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   51:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Topic Title\\nSummary\"):\n>> 52:         cmd_summarize(args)\n   53: \n   54:     entry = _find_entry(work_dir)\n   55:     assert \"Summary\" in entry.read_text()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 64,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_py_files",
        "context_snippet": "   61: \n   62:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   63:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Module\\nSummary\"):\n>> 64:         cmd_summarize(args)\n   65: \n   66:     entry = _find_entry(work_dir)\n   67:     assert \"Summary\" in entry.read_text()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 77,
        "text": "cmd_summarize(args)",
        "context_function": "test_discovers_both_md_and_py",
        "context_snippet": "   74: \n   75:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   76:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as mock_llm:\n>> 77:         cmd_summarize(args)\n   78: \n   79:     assert mock_llm.call_count == 2\n   80:     entries = list((work_dir / \"entries\").rglob(\"*.md\"))"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 93,
        "text": "cmd_summarize(args)",
        "context_function": "test_recursive_discovers_nested_files",
        "context_snippet": "   90: \n   91:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   92:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as mock_llm:\n>> 93:         cmd_summarize(args)\n   94: \n   95:     assert mock_llm.call_count == 2\n   96:     entries = list((work_dir / \"entries\").rglob(\"*.md\"))"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 109,
        "text": "cmd_summarize(args)",
        "context_function": "test_non_recursive_skips_nested_files",
        "context_snippet": "   106: \n   107:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   108:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as mock_llm:\n>> 109:         cmd_summarize(args)\n   110: \n   111:     assert mock_llm.call_count == 1\n   112: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 121,
        "text": "cmd_summarize(args)",
        "context_function": "test_ignores_other_extensions",
        "context_snippet": "   118: \n   119:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   120:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:\n>> 121:         cmd_summarize(args)\n   122: \n   123:     assert not mock_llm.called\n   124: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 134,
        "text": "cmd_summarize(args)",
        "context_function": "test_uses_summarize_code_for_py",
        "context_snippet": "   131: \n   132:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   133:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Module\\nSummary\") as mock_llm:\n>> 134:         cmd_summarize(args)\n   135: \n   136:     prompt = mock_llm.call_args[0][0]\n   137:     assert \"source code\" in prompt.lower()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 146,
        "text": "cmd_summarize(args)",
        "context_function": "test_uses_summarize_for_md",
        "context_snippet": "   143: \n   144:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   145:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Doc Title\\nSummary\") as mock_llm:\n>> 146:         cmd_summarize(args)\n   147: \n   148:     prompt = mock_llm.call_args[0][0]\n   149:     assert \"documentation page\" in prompt.lower()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 160,
        "text": "cmd_summarize(args)",
        "context_function": "test_truncation_warning_for_large_file",
        "context_snippet": "   157: \n   158:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   159:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Big Doc\\nSummary\"):\n>> 160:         cmd_summarize(args)\n   161: \n   162:     captured = capsys.readouterr()\n   163:     assert \"WARN: truncated from 50000 to 30000 chars\" in captured.out"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 173,
        "text": "cmd_summarize(args)",
        "context_function": "test_truncation_content_is_capped",
        "context_snippet": "   170: \n   171:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   172:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Big\\nSummary\") as mock_llm:\n>> 173:         cmd_summarize(args)\n   174: \n   175:     prompt = mock_llm.call_args[0][0]\n   176:     assert \"[Truncated\" in prompt"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 186,
        "text": "cmd_summarize(args)",
        "context_function": "test_no_truncation_warning_for_small_file",
        "context_snippet": "   183: \n   184:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   185:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Small\\nSummary\"):\n>> 186:         cmd_summarize(args)\n   187: \n   188:     captured = capsys.readouterr()\n   189:     assert \"WARN\" not in captured.out"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 202,
        "text": "cmd_summarize(args)",
        "context_function": "test_skips_already_summarized",
        "context_snippet": "   199: \n   200:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   201:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:\n>> 202:         cmd_summarize(args)\n   203: \n   204:     assert not mock_llm.called\n   205: "
      },
      {
        "file": "tests/test_summarize.py",
        "line": 213,
        "text": "cmd_summarize(args)",
        "context_function": "test_manifest_records_processed_file",
        "context_snippet": "   210: \n   211:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   212:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):\n>> 213:         cmd_summarize(args)\n   214: \n   215:     manifest = work_dir / \".summarized\"\n   216:     assert manifest.exists()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 229,
        "text": "cmd_summarize(args)",
        "context_function": "test_strips_frontmatter_before_summarizing",
        "context_snippet": "   226: \n   227:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   228:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as mock_llm:\n>> 229:         cmd_summarize(args)\n   230: \n   231:     prompt = mock_llm.call_args[0][0]\n   232:     assert \"source:\" not in prompt"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 243,
        "text": "cmd_summarize(args)",
        "context_function": "test_strips_source_url_frontmatter",
        "context_snippet": "   240: \n   241:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   242:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\") as mock_llm:\n>> 243:         cmd_summarize(args)\n   244: \n   245:     prompt = mock_llm.call_args[0][0]\n   246:     assert \"source_url\" not in prompt"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 256,
        "text": "cmd_summarize(args)",
        "context_function": "test_skips_empty_content_after_frontmatter",
        "context_snippet": "   253: \n   254:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   255:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock) as mock_llm:\n>> 256:         cmd_summarize(args)\n   257: \n   258:     assert not mock_llm.called\n   259:     captured = capsys.readouterr()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 272,
        "text": "cmd_summarize(args)",
        "context_function": "test_entry_has_source_frontmatter",
        "context_snippet": "   269: \n   270:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   271:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):\n>> 272:         cmd_summarize(args)\n   273: \n   274:     entry = _find_entry(work_dir)\n   275:     content = entry.read_text()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 288,
        "text": "cmd_summarize(args)",
        "context_function": "test_entry_has_source_url_from_fetch_frontmatter",
        "context_snippet": "   285: \n   286:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   287:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Page\\nSummary\"):\n>> 288:         cmd_summarize(args)\n   289: \n   290:     entry = _find_entry(work_dir)\n   291:     content = entry.read_text()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 303,
        "text": "cmd_summarize(args)",
        "context_function": "test_entry_has_source_id_when_present",
        "context_snippet": "   300: \n   301:     with patch(\"expert_build.summarize.check_model_available\", return_value=True), \\\n   302:          patch(\"expert_build.summarize.invoke\", new_callable=AsyncMock, return_value=\"## Title\\nSummary\"):\n>> 303:         cmd_summarize(args)\n   304: \n   305:     entry = _find_entry(work_dir)\n   306:     content = entry.read_text()"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 318,
        "text": "cmd_summarize(args)"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 332,
        "text": "cmd_summarize(args)"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 367,
        "text": "cmd_summarize(args)"
      },
      {
        "file": "tests/test_summarize.py",
        "line": 380,
        "text": "cmd_summarize(args)"
      }
    ],
    "production_count": 4,
    "test_count": 24,
    "total_count": 28
  },
  "caffeinate_hold_body": {
    "function": "hold",
    "file": "expert_build/caffeinate.py",
    "start_line": 10,
    "end_line": 25,
    "source": "def hold():\n    \"\"\"Start caffeinate to prevent idle sleep. No-op on non-macOS.\"\"\"\n    global _process\n    if _process is not None:\n        return\n    if platform.system() != \"Darwin\":\n        return\n    try:\n        _process = subprocess.Popen(\n            [\"caffeinate\", \"-i\"],\n            stdout=subprocess.DEVNULL,\n            stderr=subprocess.DEVNULL,\n        )\n        atexit.register(release)\n    except FileNotFoundError:\n        pass"
  },
  "summarize_imports": {
    "file": "expert_build/summarize.py",
    "imports": [
      "asyncio",
      "sys"
    ],
    "from_imports": [
      {
        "module": "datetime",
        "names": [
          "date"
        ]
      },
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "SUMMARIZE",
          "SUMMARIZE_CODE"
        ]
      }
    ],
    "import_section": "\"\"\"Summarize source documents into entries using an LLM.\"\"\"\n\nimport asyncio\nimport sys\nfrom datetime import date\nfrom pathlib import Path\n\nfrom .llm import check_model_available, invoke\nfrom .prompts import SUMMARIZE, SUMMARIZE_CODE\n\n\ndef _prepare_source(source_path):\n    \"\"\"Read source file, strip frontmatter, truncate if needed.\n"
  },
  "summarize_test_coverage": {
    "source_file": "expert_build/summarize.py",
    "test_files": [
      {
        "path": "tests/test_summarize.py",
        "exists": true,
        "line_count": 383
      },
      {
        "path": "tests/test_summarizepy",
        "exists": false
      }
    ],
    "test_count": 2
  },
  "prepare_source_generator": {
    "function": "_prepare_source",
    "file": "expert_build/summarize.py",
    "is_generator": false,
    "is_async_generator": false,
    "yield_count": 0,
    "has_return_value": true,
    "return_annotation": null
  }
}
```

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.
