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/propose.py b/expert_build/propose.py
index 66e6ad5..6d25458 100644
--- a/expert_build/propose.py
+++ b/expert_build/propose.py
@@ -349,7 +349,29 @@ def cmd_propose_beliefs(args):
 
     print(f"Processing {len(batches)} batches (batch size: {args.batch_size})...")
 
-    all_proposals = []
+    source_desc = (", ".join(str(e) for e in entries)
+                   if has_entry_flag
+                   else f"{len(entries)} entries from {input_dir}/")
+    output = Path(args.output)
+
+    # Write header before first batch if starting a new file
+    if not (output.exists() and output.stat().st_size > 0):
+        with output.open("w") as f:
+            f.write("# Proposed Beliefs\n\n")
+            f.write("Edit each entry: change `[ACCEPT/REJECT]` to `[ACCEPT]` or `[REJECT]`.\n")
+            f.write("Then run: `expert-build accept-beliefs`\n\n")
+            f.write("---\n\n")
+            f.write(f"**Generated:** {date.today().isoformat()}\n")
+            f.write(f"**Source:** {source_desc}\n")
+            f.write(f"**Model:** {args.model}\n\n")
+    else:
+        with output.open("a") as f:
+            f.write(f"\n---\n\n")
+            f.write(f"**Generated:** {date.today().isoformat()}\n")
+            f.write(f"**Source:** {source_desc}\n")
+            f.write(f"**Model:** {args.model}\n\n")
+
+    total_skipped = 0
     for i, batch_text in enumerate(batches):
         print(f"  Batch {i + 1}/{len(batches)}...")
         existing_context = _build_dedup_context(
@@ -359,18 +381,15 @@ def cmd_propose_beliefs(args):
         prompt = PROPOSE_BELIEFS.format(entries=batch_text) + existing_context
         try:
             result = invoke_sync(prompt, model=args.model, timeout=600)
-            all_proposals.append(result)
         except Exception as e:
             print(f"  ERROR: {e}")
             continue
 
-    # Filter out proposals whose IDs already exist
-    filtered_proposals = []
-    skipped = 0
-    for proposal in all_proposals:
-        lines = proposal.split("\n")
+        # Filter out proposals whose IDs already exist
+        lines = result.split("\n")
         filtered_lines = []
         skip_until_next = False
+        skipped = 0
         for line in lines:
             m = re.match(r"^### \[?(?:ACCEPT|REJECT)\]? (\S+)", line)
             if m:
@@ -387,42 +406,20 @@ def cmd_propose_beliefs(args):
                     filtered_lines.append(line)
                 continue
             filtered_lines.append(line)
-        filtered_proposals.append("\n".join(filtered_lines))
+        total_skipped += skipped
 
-    if skipped:
-        print(f"  Filtered {skipped} already-accepted beliefs")
+        # Write this batch's proposals immediately
+        with output.open("a") as f:
+            f.write("\n".join(filtered_lines))
+            f.write("\n\n")
+
+    if total_skipped:
+        print(f"  Filtered {total_skipped} already-accepted beliefs")
 
     # Record processed entries
     _save_processed(processed_path, entries, processed)
 
-    # Write proposals file (append if it already exists)
-    source_desc = (", ".join(str(e) for e in entries)
-                   if has_entry_flag
-                   else f"{len(entries)} entries from {input_dir}/")
-    output = Path(args.output)
-    if output.exists() and output.stat().st_size > 0:
-        with output.open("a") as f:
-            f.write(f"\n---\n\n")
-            f.write(f"**Generated:** {date.today().isoformat()}\n")
-            f.write(f"**Source:** {source_desc}\n")
-            f.write(f"**Model:** {args.model}\n\n")
-            for proposal in filtered_proposals:
-                f.write(proposal)
-                f.write("\n\n")
-        print(f"\nAppended to {output}")
-    else:
-        with output.open("w") as f:
-            f.write("# Proposed Beliefs\n\n")
-            f.write("Edit each entry: change `[ACCEPT/REJECT]` to `[ACCEPT]` or `[REJECT]`.\n")
-            f.write("Then run: `expert-build accept-beliefs`\n\n")
-            f.write("---\n\n")
-            f.write(f"**Generated:** {date.today().isoformat()}\n")
-            f.write(f"**Source:** {source_desc}\n")
-            f.write(f"**Model:** {args.model}\n\n")
-            for proposal in filtered_proposals:
-                f.write(proposal)
-                f.write("\n\n")
-        print(f"\nWrote {output}")
+    print(f"\nWrote {output}")
 
     print("Review the file, mark entries as [ACCEPT] or [REJECT], then run:")
     print("  expert-build accept-beliefs")
diff --git a/tests/test_propose.py b/tests/test_propose.py
new file mode 100644
index 0000000..c21cb1d
--- /dev/null
+++ b/tests/test_propose.py
@@ -0,0 +1,111 @@
+"""Tests for expert_build.propose — incremental batch writing."""
+
+import types
+from pathlib import Path
+from unittest.mock import patch, MagicMock
+
+import pytest
+
+from expert_build.propose import cmd_propose_beliefs
+
+
+@pytest.fixture
+def entries_dir(tmp_path):
+    d = tmp_path / "entries"
+    d.mkdir()
+    return d
+
+
+@pytest.fixture
+def work_dir(tmp_path, monkeypatch):
+    wd = tmp_path / "work"
+    wd.mkdir()
+    (wd / ".expert-build").mkdir()
+    monkeypatch.chdir(wd)
+    return wd
+
+
+def make_args(input_dir, output="proposed-beliefs.md", batch_size=2, model="test"):
+    return types.SimpleNamespace(
+        input_dir=str(input_dir),
+        output=output,
+        batch_size=batch_size,
+        model=model,
+        all=False,
+    )
+
+
+def test_proposals_written_after_each_batch(entries_dir, work_dir):
+    """Proposals from completed batches survive a crash in a later batch."""
+    for i in range(4):
+        (entries_dir / f"entry{i}.md").write_text(f"# Entry {i}\nContent {i}")
+
+    output = work_dir / "proposed-beliefs.md"
+    args = make_args(entries_dir, output=str(output), batch_size=2)
+
+    call_count = 0
+    def invoke_side_effect(prompt, model=None, timeout=None):
+        nonlocal call_count
+        call_count += 1
+        if call_count == 2:
+            raise RuntimeError("simulated crash")
+        return f"### [ACCEPT/REJECT] belief-from-batch-{call_count}\nA belief.\n"
+
+    with patch("expert_build.propose.check_model_available", return_value=True), \
+         patch("expert_build.propose.invoke_sync", side_effect=invoke_side_effect), \
+         patch("expert_build.propose._load_existing_beliefs", return_value=[]), \
+         patch("expert_build.propose._has_embeddings", return_value=False):
+        cmd_propose_beliefs(args)
+
+    content = output.read_text()
+    assert "belief-from-batch-1" in content
+    assert "belief-from-batch-2" not in content
+
+
+def test_all_batches_written_on_success(entries_dir, work_dir):
+    for i in range(4):
+        (entries_dir / f"entry{i}.md").write_text(f"# Entry {i}\nContent {i}")
+
+    output = work_dir / "proposed-beliefs.md"
+    args = make_args(entries_dir, output=str(output), batch_size=2)
+
+    call_count = 0
+    def invoke_side_effect(prompt, model=None, timeout=None):
+        nonlocal call_count
+        call_count += 1
+        return f"### [ACCEPT/REJECT] belief-{call_count}\nA belief.\n"
+
+    with patch("expert_build.propose.check_model_available", return_value=True), \
+         patch("expert_build.propose.invoke_sync", side_effect=invoke_side_effect), \
+         patch("expert_build.propose._load_existing_beliefs", return_value=[]), \
+         patch("expert_build.propose._has_embeddings", return_value=False):
+        cmd_propose_beliefs(args)
+
+    content = output.read_text()
+    assert "belief-1" in content
+    assert "belief-2" in content
+
+
+def test_existing_beliefs_filtered_per_batch(entries_dir, work_dir):
+    (entries_dir / "entry0.md").write_text("# Entry\nContent")
+
+    output = work_dir / "proposed-beliefs.md"
+    args = make_args(entries_dir, output=str(output), batch_size=5)
+
+    existing = [{"id": "already-exists", "text": "old belief", "source": ""}]
+
+    def invoke_side_effect(prompt, model=None, timeout=None):
+        return (
+            "### [ACCEPT] already-exists\nDuplicate.\n\n"
+            "### [ACCEPT] new-belief\nFresh.\n"
+        )
+
+    with patch("expert_build.propose.check_model_available", return_value=True), \
+         patch("expert_build.propose.invoke_sync", side_effect=invoke_side_effect), \
+         patch("expert_build.propose._load_existing_beliefs", return_value=existing), \
+         patch("expert_build.propose._has_embeddings", return_value=False):
+        cmd_propose_beliefs(args)
+
+    content = output.read_text()
+    assert "new-belief" in content
+    assert "already-exists" not in content

```

## Observation Results

You previously requested observations. Here are the results:

```json
{
  "full_cmd_propose_beliefs": {
    "function": "cmd_propose_beliefs",
    "file": "expert_build/propose.py",
    "start_line": 261,
    "end_line": 425,
    "source": "def cmd_propose_beliefs(args):\n    \"\"\"Extract candidate beliefs from entries for human review.\"\"\"\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\"Entries directory not found: {input_dir}\")\n        sys.exit(1)\n\n    if not check_model_available(args.model):\n        print(f\"Model not available: {args.model}\")\n        sys.exit(1)\n\n    # Collect entries\n    if hasattr(args, 'entry') and args.entry:\n        entries = [Path(p) for p in args.entry]\n    else:\n        entries = sorted(input_dir.rglob(\"*.md\"))\n\n    if not entries:\n        print(f\"No .md files found.\")\n        return\n\n    # Filter out already-processed entries (unless --all or --entry)\n    processed_path = Path(PROJECT_DIR) / \"proposed-entries.json\"\n    processed = _load_processed(processed_path)\n    process_all = getattr(args, 'all', False)\n    has_entry_flag = hasattr(args, 'entry') and args.entry\n\n    if not process_all and not has_entry_flag:\n        total = len(entries)\n        entries = _filter_unprocessed(entries, processed)\n        skipped = total - len(entries)\n        if skipped:\n            print(f\"Skipping {skipped} already-processed entries (use --all to reprocess)\")\n        if not entries:\n            print(\"No new entries to process.\")\n            return\n\n    # Load existing beliefs for dedup context\n    existing_beliefs = _load_existing_beliefs()\n    existing_ids = {b[\"id\"] for b in existing_beliefs}\n\n    if existing_ids:\n        print(f\"Found {len(existing_ids)} existing beliefs (will skip duplicates)\")\n\n    # Compute belief embeddings once (if fastembed available)\n    belief_vectors = None\n    if existing_beliefs and _has_embeddings():\n        print(\"Computing belief embeddings for semantic dedup...\")\n        cache_path = Path(PROJECT_DIR) / \"belief-vectors.json\"\n        belief_vectors = _get_belief_embeddings(existing_beliefs, cache_path)\n        print(f\"  {len(belief_vectors)} belief vectors ready\")\n    elif existing_beliefs:\n        print(\"(install fastembed for semantic dedup: uv pip install 'expert-agent-builder[embeddings]')\")\n\n    print(f\"Reading {len(entries)} entries...\")\n\n    # Batch entries \u2014 track paths per batch for relevance scoring\n    batches = []\n    batch_paths = []\n    current_batch = []\n    current_paths = []\n    for entry_path in entries:\n        content = entry_path.read_text()\n        if len(content) > 10000:\n            content = content[:10000] + \"\\n[Truncated]\"\n        source_url = \"\"\n        if content.startswith(\"---\"):\n            end = content.find(\"---\", 3)\n            if end != -1:\n                for line in content[3:end].splitlines():\n                    if line.startswith(\"source_url:\"):\n                        source_url = line.split(\":\", 1)[1].strip()\n        header = f\"--- FILE: {entry_path}\"\n        if source_url:\n            header += f\" | SOURCE_URL: {source_url}\"\n        header += \" ---\"\n        current_batch.append(f\"{header}\\n{content}\")\n        current_paths.append(str(entry_path))\n        if len(current_batch) >= args.batch_size:\n            batches.append(\"\\n\\n\".join(current_batch))\n            batch_paths.append(current_paths)\n            current_batch = []\n            current_paths = []\n    if current_batch:\n        batches.append(\"\\n\\n\".join(current_batch))\n        batch_paths.append(current_paths)\n\n    print(f\"Processing {len(batches)} batches (batch size: {args.batch_size})...\")\n\n    source_desc = (\", \".join(str(e) for e in entries)\n                   if has_entry_flag\n                   else f\"{len(entries)} entries from {input_dir}/\")\n    output = Path(args.output)\n\n    # Write header before first batch if starting a new file\n    if not (output.exists() and output.stat().st_size > 0):\n        with output.open(\"w\") as f:\n            f.write(\"# Proposed Beliefs\\n\\n\")\n            f.write(\"Edit each entry: change `[ACCEPT/REJECT]` to `[ACCEPT]` or `[REJECT]`.\\n\")\n            f.write(\"Then run: `expert-build accept-beliefs`\\n\\n\")\n            f.write(\"---\\n\\n\")\n            f.write(f\"**Generated:** {date.today().isoformat()}\\n\")\n            f.write(f\"**Source:** {source_desc}\\n\")\n            f.write(f\"**Model:** {args.model}\\n\\n\")\n    else:\n        with output.open(\"a\") as f:\n            f.write(f\"\\n---\\n\\n\")\n            f.write(f\"**Generated:** {date.today().isoformat()}\\n\")\n            f.write(f\"**Source:** {source_desc}\\n\")\n            f.write(f\"**Model:** {args.model}\\n\\n\")\n\n    total_skipped = 0\n    for i, batch_text in enumerate(batches):\n        print(f\"  Batch {i + 1}/{len(batches)}...\")\n        existing_context = _build_dedup_context(\n            existing_beliefs, batch_paths[i], batch_text,\n            belief_vectors=belief_vectors,\n        )\n        prompt = PROPOSE_BELIEFS.format(entries=batch_text) + existing_context\n        try:\n            result = invoke_sync(prompt, model=args.model, timeout=600)\n        except Exception as e:\n            print(f\"  ERROR: {e}\")\n            continue\n\n        # Filter out proposals whose IDs already exist\n        lines = result.split(\"\\n\")\n        filtered_lines = []\n        skip_until_next = False\n        skipped = 0\n        for line in lines:\n            m = re.match(r\"^### \\[?(?:ACCEPT|REJECT)\\]? (\\S+)\", line)\n            if m:\n                belief_id = m.group(1)\n                if belief_id in existing_ids:\n                    skip_until_next = True\n                    skipped += 1\n                    continue\n                else:\n                    skip_until_next = False\n            if skip_until_next:\n                if line.startswith(\"### \"):\n                    skip_until_next = False\n                    filtered_lines.append(line)\n                continue\n            filtered_lines.append(line)\n        total_skipped += skipped\n\n        # Write this batch's proposals immediately\n        with output.open(\"a\") as f:\n            f.write(\"\\n\".join(filtered_lines))\n            f.write(\"\\n\\n\")\n\n    if total_skipped:\n        print(f\"  Filtered {total_skipped} already-accepted beliefs\")\n\n    # Record processed entries\n    _save_processed(processed_path, entries, processed)\n\n    print(f\"\\nWrote {output}\")\n\n    print(\"Review the file, mark entries as [ACCEPT] or [REJECT], then run:\")\n    print(\"  expert-build accept-beliefs\")"
  },
  "build_dedup_context": {
    "function": "_build_dedup_context",
    "file": "expert_build/propose.py",
    "start_line": 208,
    "end_line": 247,
    "source": "def _build_dedup_context(\n    existing_beliefs: list[dict],\n    batch_entry_paths: list[str],\n    batch_text: str,\n    max_detailed: int = 50,\n    max_compact: int = 200,\n    belief_vectors: dict[str, list[float]] | None = None,\n) -> str:\n    \"\"\"Build per-batch dedup context: relevant beliefs with text, rest as compact IDs.\"\"\"\n    if not existing_beliefs:\n        return \"\"\n\n    if belief_vectors:\n        scored = _score_by_embedding(\n            existing_beliefs, belief_vectors, batch_text, batch_entry_paths,\n        )\n    else:\n        scored = _score_by_keywords(\n            existing_beliefs, batch_text, batch_entry_paths,\n        )\n\n    detailed = scored[:max_detailed]\n    compact = scored[max_detailed:max_detailed + max_compact]\n\n    parts = [\n        \"\\n\\n## Already Accepted Beliefs\\n\\n\"\n        \"The following beliefs already exist. Do NOT propose beliefs with these IDs \"\n        \"or that duplicate their meaning under different names.\\n\"\n    ]\n\n    if detailed:\n        parts.append(\"\\nRelevant existing beliefs:\")\n        for _, belief in detailed:\n            parts.append(f\"- `{belief['id']}`: {belief['text']}\")\n\n    if compact:\n        compact_ids = \", \".join(b[\"id\"] for _, b in compact)\n        parts.append(f\"\\nOther existing IDs: {compact_ids}\")\n\n    return \"\\n\".join(parts) + \"\\n\""
  },
  "save_processed": {
    "function": "_save_processed",
    "file": "expert_build/propose.py",
    "start_line": 70,
    "end_line": 78,
    "source": "def _save_processed(path: Path, entries: list[Path], existing: dict[str, str]):\n    \"\"\"Record entries as processed by content hash.\"\"\"\n    updated = dict(existing)\n    for entry_path in entries:\n        content = entry_path.read_text()\n        content_hash = hashlib.sha256(content.encode()).hexdigest()[:16]\n        updated[str(entry_path)] = content_hash\n    path.parent.mkdir(parents=True, exist_ok=True)\n    path.write_text(json.dumps(updated, indent=2) + \"\\n\")"
  },
  "invoke_sync_raises": {
    "function": "invoke_sync",
    "file": "expert_build/propose.py",
    "explicit_raises": [],
    "calls": [],
    "error": "Function 'invoke_sync' not found"
  },
  "cmd_propose_beliefs_callers": {
    "symbol": "cmd_propose_beliefs",
    "production_callers": [
      {
        "file": "expert_build/propose.py",
        "line": 261,
        "text": "def cmd_propose_beliefs(args):",
        "context_function": "auto_accept_proposals",
        "context_snippet": "   258:     path.write_text(text)\n   259: \n   260: \n>> 261: def cmd_propose_beliefs(args):\n   262:     \"\"\"Extract candidate beliefs from entries for human review.\"\"\"\n   263:     from .caffeinate import hold as _caffeinate\n   264:     _caffeinate()"
      },
      {
        "file": "expert_build/cli.py",
        "line": 131,
        "text": "\"propose-beliefs\": lambda a: _lazy(\"propose\", \"cmd_propose_beliefs\")(a),",
        "context_function": "main",
        "context_snippet": "   128:         \"chunk-pdf\": lambda a: _lazy(\"chunk_pdf\", \"cmd_chunk_pdf\")(a),\n   129:         \"fetch-docs\": lambda a: _lazy(\"fetch\", \"cmd_fetch_docs\")(a),\n   130:         \"summarize\": lambda a: _lazy(\"summarize\", \"cmd_summarize\")(a),\n>> 131:         \"propose-beliefs\": lambda a: _lazy(\"propose\", \"cmd_propose_beliefs\")(a),\n   132:         \"accept-beliefs\": lambda a: _lazy(\"propose\", \"cmd_accept_beliefs\")(a),\n   133:         \"cert-coverage\": lambda a: _lazy(\"coverage\", \"cmd_cert_coverage\")(a),\n   134:         \"exam\": lambda a: _lazy(\"exam\", \"cmd_exam\")(a),"
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 61,
        "text": "from .propose import cmd_propose_beliefs, cmd_accept_beliefs",
        "context_function": "_stage_extract",
        "context_snippet": "   58: \n   59: def _stage_extract(args):\n   60:     \"\"\"Stage 3: Extract beliefs from entries and optionally auto-accept.\"\"\"\n>> 61:     from .propose import cmd_propose_beliefs, cmd_accept_beliefs\n   62: \n   63:     prop_args = SimpleNamespace(\n   64:         input_dir=\"entries\","
      },
      {
        "file": "expert_build/pipeline.py",
        "line": 72,
        "text": "cmd_propose_beliefs(prop_args)",
        "context_function": "_stage_extract",
        "context_snippet": "   69:     )\n   70:     setattr(prop_args, \"all\", False)\n   71: \n>> 72:     cmd_propose_beliefs(prop_args)\n   73: \n   74:     if args.no_auto_accept:\n   75:         print(\"\\nStopping after propose-beliefs (--no-auto-accept)\", file=sys.stderr)"
      }
    ],
    "test_callers": [
      {
        "file": "tests/test_propose.py",
        "line": 9,
        "text": "from expert_build.propose import cmd_propose_beliefs",
        "context_function": null,
        "context_snippet": "   6: \n   7: import pytest\n   8: \n>> 9: from expert_build.propose import cmd_propose_beliefs\n   10: \n   11: \n   12: @pytest.fixture"
      },
      {
        "file": "tests/test_propose.py",
        "line": 58,
        "text": "cmd_propose_beliefs(args)",
        "context_function": "invoke_side_effect",
        "context_snippet": "   55:          patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\\n   56:          patch(\"expert_build.propose._load_existing_beliefs\", return_value=[]), \\\n   57:          patch(\"expert_build.propose._has_embeddings\", return_value=False):\n>> 58:         cmd_propose_beliefs(args)\n   59: \n   60:     content = output.read_text()\n   61:     assert \"belief-from-batch-1\" in content"
      },
      {
        "file": "tests/test_propose.py",
        "line": 82,
        "text": "cmd_propose_beliefs(args)",
        "context_function": "invoke_side_effect",
        "context_snippet": "   79:          patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\\n   80:          patch(\"expert_build.propose._load_existing_beliefs\", return_value=[]), \\\n   81:          patch(\"expert_build.propose._has_embeddings\", return_value=False):\n>> 82:         cmd_propose_beliefs(args)\n   83: \n   84:     content = output.read_text()\n   85:     assert \"belief-1\" in content"
      },
      {
        "file": "tests/test_propose.py",
        "line": 107,
        "text": "cmd_propose_beliefs(args)",
        "context_function": "invoke_side_effect",
        "context_snippet": "   104:          patch(\"expert_build.propose.invoke_sync\", side_effect=invoke_side_effect), \\\n   105:          patch(\"expert_build.propose._load_existing_beliefs\", return_value=existing), \\\n   106:          patch(\"expert_build.propose._has_embeddings\", return_value=False):\n>> 107:         cmd_propose_beliefs(args)\n   108: \n   109:     content = output.read_text()\n   110:     assert \"new-belief\" in content"
      },
      {
        "file": "tests/test_pipeline.py",
        "line": 132,
        "text": "with patch(\"expert_build.propose.cmd_propose_beliefs\"):",
        "context_function": "test_stops_on_no_auto_accept",
        "context_snippet": "   129: class TestStageExtract:\n   130:     def test_stops_on_no_auto_accept(self, work_dir, capsys):\n   131:         args = make_pipeline_args(no_auto_accept=True)\n>> 132:         with patch(\"expert_build.propose.cmd_propose_beliefs\"):\n   133:             result = _stage_extract(args)\n   134:         assert result is False\n   135:         captured = capsys.readouterr()"
      },
      {
        "file": "tests/test_pipeline.py",
        "line": 143,
        "text": "with patch(\"expert_build.propose.cmd_propose_beliefs\"), \\",
        "context_function": "test_auto_accepts_and_imports",
        "context_snippet": "   140:         proposals.write_text(\"### [ACCEPT/REJECT] test-belief\\nText\\n- Source: test\\n\")\n   141:         args = make_pipeline_args()\n   142: \n>> 143:         with patch(\"expert_build.propose.cmd_propose_beliefs\"), \\\n   144:              patch(\"expert_build.propose.cmd_accept_beliefs\") as mock_accept:\n   145:             result = _stage_extract(args)\n   146: "
      }
    ],
    "production_count": 4,
    "test_count": 6,
    "total_count": 10
  },
  "load_existing_beliefs": {
    "function": "_load_existing_beliefs",
    "file": "expert_build/propose.py",
    "start_line": 41,
    "end_line": 57,
    "source": "def _load_existing_beliefs(db_path: str = REASONS_DB) -> list[dict]:\n    \"\"\"Load existing beliefs from the reasons database.\"\"\"\n    if not Path(db_path).exists():\n        return []\n    try:\n        from reasons_lib.api import export_network\n        network = export_network(db_path=db_path)\n        beliefs = []\n        for nid, ndata in network.get(\"nodes\", {}).items():\n            beliefs.append({\n                \"id\": nid,\n                \"text\": ndata.get(\"text\", \"\"),\n                \"source\": ndata.get(\"source\", \"\"),\n            })\n        return beliefs\n    except Exception:\n        return []"
  },
  "propose_imports": {
    "file": "expert_build/propose.py",
    "imports": [
      "hashlib",
      "json",
      "re",
      "sys"
    ],
    "from_imports": [
      {
        "module": "datetime",
        "names": [
          "date"
        ]
      },
      {
        "module": "pathlib",
        "names": [
          "Path"
        ]
      },
      {
        "module": "reasons_lib.api",
        "names": [
          "add_node",
          "list_nodes"
        ]
      },
      {
        "module": "llm",
        "names": [
          "check_model_available",
          "invoke_sync"
        ]
      },
      {
        "module": "prompts",
        "names": [
          "PROPOSE_BELIEFS"
        ]
      }
    ],
    "import_section": "\"\"\"Propose and accept beliefs from entries.\"\"\"\n\nimport hashlib\nimport json\nimport re\nimport sys\nfrom datetime import date\nfrom pathlib import Path\n\nfrom reasons_lib.api import add_node, list_nodes\n\nfrom .llm import check_model_available, invoke_sync\nfrom .prompts import PROPOSE_BELIEFS\n\nPROJECT_DIR = \".expert-build\"\nREASONS_DB = \"reasons.db\"\n\n\ndef _has_embeddings() -> bool:\n    \"\"\"Check if fastembed is available.\"\"\""
  }
}
```

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.
