## Code Review: `--parallel` flag for `propose-beliefs`

### expert_build/cli.py
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Clean argument addition. `type=int, default=1` is correct. No validation needed here since `propose.py` clamps with `max(1, ...)`.
---

### expert_build/propose.py:cmd_propose_beliefs
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: The async conversion is well-structured. Key correctness properties hold:

1. **No shared mutable state** — `_process_batch` only reads from `existing_beliefs`, `existing_ids`, `batch_paths`, `belief_vectors`. All mutation (filtering, counting) is local to each batch and returned as a tuple.
2. **File I/O is sequential** — `asyncio.gather` collects all results first, then the synchronous loop writes to the output file. No file write races.
3. **`_save_processed` is sequential** — called in the result loop, not inside async tasks.
4. **Order preserved** — `asyncio.gather` returns results in input order, so output file ordering matches the sequential version.
5. **No nested `asyncio.run()`** — the old code used `invoke_sync` (which wraps `asyncio.run(invoke(...))`). Calling that inside another `asyncio.run()` would crash. The switch to `await invoke(...)` is not just an optimization, it's *required* for correctness in the async context.
6. **`parallel=0` handled** — `max(1, ...)` clamps to 1, preventing a deadlocked semaphore.
7. **Closure captures are safe** — `i` is a parameter (not a loop variable captured by reference), so `batch_paths[i]` resolves correctly per-task.
---

### expert_build/propose.py (import change)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Import changed from `invoke_sync` to `invoke`. `invoke_sync` is still defined in `llm.py` and still used by `exam.py`, `coverage.py`, and `pipeline.py` (11 stale references per observation). Those modules are out-of-scope for this change and unaffected — they don't run inside an event loop. No breakage.
---

### tests/test_propose.py (existing test updates)
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: All 9 existing tests correctly updated: `invoke_sync` → `invoke` with `new_callable=AsyncMock`. `make_args` updated to include `parallel=1` default, matching the real argparse default. Since `AsyncMock` side-effects that are regular functions work identically to `MagicMock` side-effects (the function is called synchronously, result wrapped in a coroutine), all existing test semantics are preserved.
---

### tests/test_propose.py:test_parallel_processes_all_batches
VERDICT: PASS
CORRECTNESS: VALID
SPEC_COMPLIANCE: N/A
ISSUE_COMPLIANCE: N/A
BELIEF_COMPLIANCE: N/A
TEST_COVERAGE: COVERED
INTEGRATION: WIRED
REASONING: Tests `parallel=2` with 6 entries / batch_size=2 (3 batches). Verifies all 3 batches processed and all beliefs appear in output. The `call_count` nonlocal is safe — asyncio is single-threaded, and the `invoke_side_effect` function executes synchronously within the event loop (no yield between read and write of `call_count`). The test doesn't verify actual concurrent execution (which would require timing or ordering assertions), but that's appropriate for a unit test — the semaphore/gather mechanics are stdlib and don't need re-testing.
---

### SELF_REVIEW
LIMITATIONS: Could not run the test suite to confirm all tests pass with the async changes. Could not verify whether `_caffeinate()` at the top of `cmd_propose_beliefs` interacts poorly with the event loop (e.g., if it spawns threads or signals). Did not see the full test file to check if there are any tests that were missed in the mock update.
---

### FEATURE_REQUESTS
- Show the full test file in observations when test changes span more than half the file, to verify no tests were missed in a bulk mock rename
- Flag when a function uses `asyncio.run()` and the caller chain should be checked for existing event loops (e.g., if this could ever be called from an async framework)
---
