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..063852a 100644
--- a/expert_build/prompts.py
+++ b/expert_build/prompts.py
@@ -28,6 +28,37 @@
 {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:
+
+## Overview
+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..3d4da6d 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,16 @@ 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]"
-
-        prompt = SUMMARIZE.format(content=content)
+            print(f"  WARN: truncated from {original_len} to 30000 chars. "
+                  f"Consider: code-expert chunk-pdf {source_path}"
+                  if source_path.suffix == ".pdf"
+                  else f"  WARN: truncated from {original_len} to 30000 chars. "
+                  f"Large documents may lose tail content.")
+
+        template = SUMMARIZE_CODE if source_path.suffix == ".py" else SUMMARIZE
+        prompt = template.format(content=content)
 
         try:
             summary = invoke_sync(prompt, model=args.model)

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "full_cmd_summarize": {
    "function": "cmd_summarize",
    "file": "expert_build/summarize.py",
    "start_line": 12,
    "end_line": 141,
    "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            print(f\"  WARN: truncated from {original_len} to 30000 chars. \"\n                  f\"Consider: code-expert chunk-pdf {source_path}\"\n                  if source_path.suffix == \".pdf\"\n                  else 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_raises": {
    "function": "invoke_sync",
    "file": "expert_build/llm.py",
    "explicit_raises": [],
    "calls": [
      "run",
      "invoke"
    ]
  },
  "summarize_tests": {
    "source_file": "expert_build/summarize.py",
    "test_files": [],
    "test_count": 0
  },
  "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": [],
    "production_count": 2,
    "test_count": 0,
    "total_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.
