Feat/compile#362
Conversation
vouch compile hands the live approved claims to a deployment-configured llm command (compile.llm_cmd in config.yaml), parses the drafted topic pages, and files the survivors as pending page proposals by the wiki-compiler actor. never calls approve() — the review gate is the ingest review. every citation is verified mechanically before filing: listed claim ids must exist and be live, inline [claim: …] markers must be backed by a listed claim, wikilinks must resolve against existing pages plus the surviving batch (drops cascade to a fixpoint so a dangling link never ships), and a title that collides with an existing page or a pending draft is dropped — approve() routes colliding page ids through update_page, so an unchecked collision would silently overwrite on approval. re-running compile is idempotent against its own pending drafts. the review-ui queue gets a compile wiki button (shown once llm_cmd is configured): posts land the drafts in the same queue, one compile runs at a time per kb, failures surface as a notice, and each run appends a compile.run audit event attributed to whoever triggered it. subprocess i/o is explicit utf-8 (the default follows the locale and mojibakes or crashes on latin-1 hosts), the llm runs in a throwaway cwd so a cli that discovers per-project hooks cannot fire this project's capture pipeline mid-compile, and a malformed compile: stanza degrades to defaults instead of 500ing the queue that reads it per render.
local clients (vouch-ui) run against vouch serve and can only reach features advertised on the kb.* wire, so compile joins the method surface: kb_compile mcp tool, kb.compile jsonl handler, and the capabilities METHODS entry — test_capabilities enforces the parity. the cli command already existed, completing the four registration sites. semantics are unchanged from the compile module: the call blocks while the deployment-configured llm drafts (same shape as a summarize call), files survivors as pending page proposals by the wiki-compiler actor, attributes the run to the calling agent in the compile.run audit event, and never approves. compileerror surfaces as a clean caller-visible error envelope rather than internal_error.
110-second walkthrough recorded live against a throwaway demo kb: auto capture, one-click llm summary, the review gate, vouch compile turning approved claims into cited topic pages, and the real vouch recall digest closing the loop. 720p/3.4mb in docs/, poster frame links to it from the readme. demo content is generic placeholder data only.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughIntroduces a ChangesCompile Pipeline and Interfaces
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Browser
participant WebServer as compile_wiki (POST /compile)
participant CompileLock
participant compile_kb
participant KBStore
participant WebSocket as WS clients
Browser->>WebServer: POST /compile
WebServer->>CompileLock: acquire per-KB lock
alt lock busy
WebServer-->>Browser: redirect compile_error=already running
else lock free
WebServer->>compile_kb: compile_kb(store, actor, max_pages, dry_run)
compile_kb->>KBStore: read approved claims and pages
compile_kb->>compile_kb: build_prompt, run_llm, parse_drafts
compile_kb->>compile_kb: validate citations and wikilinks
compile_kb->>KBStore: propose_page for surviving drafts
compile_kb->>KBStore: log_event(compile.run)
compile_kb-->>WebServer: CompileReport
WebServer->>WebSocket: notify refresh
WebServer-->>Browser: redirect /?compiled=&dropped=
end
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/vouch/web/server.py (1)
531-572: 🩺 Stability & Availability | 🔵 Trivial
compile_lockis process-local — won't hold across multiple workers.The "one compile at a time per KB" guarantee documented in the comment only holds within a single process. If this app is ever run with multiple worker processes (e.g.,
uvicorn --workers N), each worker gets its ownasyncio.Lock(), so two workers could each accept a/compilePOST concurrently, defeating the stated purpose (avoiding exhausted threadpool/LLM spend from concurrent runs).Given the localhost-first, single-KB-root design elsewhere in this file, this is likely fine as-is, but worth confirming the intended deployment topology.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/vouch/web/server.py` around lines 531 - 572, The compile guard in compile_wiki uses a process-local asyncio.Lock, so the “one compile at a time per KB” guarantee only works within a single worker. If this endpoint may run under multiple processes, replace the local compile_lock with a cross-process coordination mechanism in compile_wiki/app.state (or explicitly enforce single-worker deployment); otherwise, if localhost/single-worker is the intended model, keep the current lock but document that assumption alongside compile_lock.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/compile.md`:
- Around line 9-13: The ASCII diagram fence in the compile documentation is
unlabeled and triggers markdownlint MD040; update the fenced block in the
diagram section to use a text language label so the diagram remains valid
markdown without lint warnings. Locate the fenced diagram near the compile flow
illustration and change the opening fence to a text-labeled fence while keeping
the diagram content unchanged.
In `@src/vouch/compile.py`:
- Around line 316-336: Guard against non-positive max_pages in the compile flow,
because compile() currently accepts cap values of 0 or less and then drops every
draft. Update the cap calculation in src/vouch/compile.py (around compile(),
build_prompt(), and the phase 1 survivor loop) to validate or clamp max_pages to
a sane minimum before it is used, so CLI/config values like 0 or negative cannot
silently produce an empty pass.
---
Nitpick comments:
In `@src/vouch/web/server.py`:
- Around line 531-572: The compile guard in compile_wiki uses a process-local
asyncio.Lock, so the “one compile at a time per KB” guarantee only works within
a single worker. If this endpoint may run under multiple processes, replace the
local compile_lock with a cross-process coordination mechanism in
compile_wiki/app.state (or explicitly enforce single-worker deployment);
otherwise, if localhost/single-worker is the intended model, keep the current
lock but document that assumption alongside compile_lock.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e44bc931-0d9f-4ef4-bef4-182790f7da64
⛔ Files ignored due to path filters (2)
docs/img/how-it-works-poster.jpgis excluded by!**/*.jpgdocs/vouch-how-it-works.mp4is excluded by!**/*.mp4
📒 Files selected for processing (13)
CHANGELOG.mdREADME.mddocs/compile.mdsrc/vouch/capabilities.pysrc/vouch/cli.pysrc/vouch/compile.pysrc/vouch/jsonl_server.pysrc/vouch/server.pysrc/vouch/web/server.pysrc/vouch/web/static/app.csssrc/vouch/web/templates/queue.htmltests/test_compile.pytests/test_web.py
| ``` | ||
| sessions / sources claims topic pages | ||
| ───────────────────▶ gate ───────────▶ compile ───────────▶ gate ──▶ wiki | ||
| (capture) (approve) (LLM drafts) (approve) | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Label the ASCII diagram fence as text.
The unlabeled fence triggers markdownlint MD040. Mark it as text so the diagram stays valid markdown without lint noise.
💡 Proposed fix
-```
+```text
sessions / sources claims topic pages
───────────────────▶ gate ───────────▶ compile ───────────▶ gate ──▶ wiki
(capture) (approve) (LLM drafts) (approve)
-```
+```📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| sessions / sources claims topic pages | |
| ───────────────────▶ gate ───────────▶ compile ───────────▶ gate ──▶ wiki | |
| (capture) (approve) (LLM drafts) (approve) | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 9-9: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/compile.md` around lines 9 - 13, The ASCII diagram fence in the compile
documentation is unlabeled and triggers markdownlint MD040; update the fenced
block in the diagram section to use a text language label so the diagram remains
valid markdown without lint warnings. Locate the fenced diagram near the compile
flow illustration and change the opening fence to a text-labeled fence while
keeping the diagram content unchanged.
Source: Linters/SAST tools
| cap = max_pages if max_pages is not None else cfg.max_pages | ||
|
|
||
| prompt = build_prompt(store, max_pages=cap) | ||
| drafts = parse_drafts(run_llm(cmd, prompt, timeout_seconds=cfg.timeout_seconds)) | ||
|
|
||
| report = CompileReport(drafts=drafts, dry_run=dry_run) | ||
|
|
||
| existing = store.list_pages() | ||
| taken_names = {p.title.strip().lower() for p in existing} | ||
| taken_names |= {p.id.strip().lower() for p in existing} | ||
| taken_names |= _pending_page_names(store) | ||
|
|
||
| # phase 1: per-draft validation + the cap. cap first-come: a draft past | ||
| # the cap is dropped even if an earlier one falls later, so the outcome | ||
| # doesn't depend on drop order. | ||
| survivors: list[tuple[dict[str, Any], str]] = [] | ||
| for i, draft in enumerate(drafts): | ||
| title = str(draft.get("title") or f"draft {i}").strip() | ||
| if len(survivors) >= cap: | ||
| report.dropped.append({"title": title, "reason": f"over max_pages={cap}"}) | ||
| continue |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Guard against a non-positive cap.
A config max_pages: 0/negative (or CLI --max-pages 0) makes cap <= 0, so every draft is dropped with over max_pages=0 and the pass silently produces nothing. _coerce only rejects non-numeric values, not out-of-range ones. Consider clamping to a sane minimum.
🛡️ Proposed guard
cap = max_pages if max_pages is not None else cfg.max_pages
+ if cap < 1:
+ raise CompileError(f"max_pages must be >= 1, got {cap}")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cap = max_pages if max_pages is not None else cfg.max_pages | |
| prompt = build_prompt(store, max_pages=cap) | |
| drafts = parse_drafts(run_llm(cmd, prompt, timeout_seconds=cfg.timeout_seconds)) | |
| report = CompileReport(drafts=drafts, dry_run=dry_run) | |
| existing = store.list_pages() | |
| taken_names = {p.title.strip().lower() for p in existing} | |
| taken_names |= {p.id.strip().lower() for p in existing} | |
| taken_names |= _pending_page_names(store) | |
| # phase 1: per-draft validation + the cap. cap first-come: a draft past | |
| # the cap is dropped even if an earlier one falls later, so the outcome | |
| # doesn't depend on drop order. | |
| survivors: list[tuple[dict[str, Any], str]] = [] | |
| for i, draft in enumerate(drafts): | |
| title = str(draft.get("title") or f"draft {i}").strip() | |
| if len(survivors) >= cap: | |
| report.dropped.append({"title": title, "reason": f"over max_pages={cap}"}) | |
| continue | |
| cap = max_pages if max_pages is not None else cfg.max_pages | |
| if cap < 1: | |
| raise CompileError(f"max_pages must be >= 1, got {cap}") | |
| prompt = build_prompt(store, max_pages=cap) | |
| drafts = parse_drafts(run_llm(cmd, prompt, timeout_seconds=cfg.timeout_seconds)) | |
| report = CompileReport(drafts=drafts, dry_run=dry_run) | |
| existing = store.list_pages() | |
| taken_names = {p.title.strip().lower() for p in existing} | |
| taken_names |= {p.id.strip().lower() for p in existing} | |
| taken_names |= _pending_page_names(store) | |
| # phase 1: per-draft validation + the cap. cap first-come: a draft past | |
| # the cap is dropped even if an earlier one falls later, so the outcome | |
| # doesn't depend on drop order. | |
| survivors: list[tuple[dict[str, Any], str]] = [] | |
| for i, draft in enumerate(drafts): | |
| title = str(draft.get("title") or f"draft {i}").strip() | |
| if len(survivors) >= cap: | |
| report.dropped.append({"title": title, "reason": f"over max_pages={cap}"}) | |
| continue |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/vouch/compile.py` around lines 316 - 336, Guard against non-positive
max_pages in the compile flow, because compile() currently accepts cap values of
0 or less and then drops every draft. Update the cap calculation in
src/vouch/compile.py (around compile(), build_prompt(), and the phase 1 survivor
loop) to validate or clamp max_pages to a sane minimum before it is used, so
CLI/config values like 0 or negative cannot silently produce an empty pass.
approve and reject stamp decided_at with wall-clock time while the digest window tests anchor to a fixed NOW; once real time passed NOW + 1 day, test_build_window_excludes_old_decisions started failing because fresh decisions now land inside the "future" window. pin datetime.now in vouch.proposals to NOW for the fixture so decisions are deterministic relative to the window under test.
There was a problem hiding this comment.
Pull request overview
Adds a new “compile” surface to vouch (CLI / MCP / JSONL / review-ui) that runs a deployment-configured LLM command to draft wiki topic pages from approved claims, validates citations/links mechanically, and files surviving drafts as PENDING page proposals (keeping the review gate intact).
Changes:
- Introduces
vouch compile+kb.compile(MCP/JSONL) and a review-ui compile wiki button that runs the same ingest pass. - Adds the compiler implementation (
src/vouch/compile.py) with JSON draft parsing, citation + wikilink validation, caps, and audit attribution. - Adds comprehensive test coverage for compile behavior (CLI/web/JSONL paths) and hardens time-dependent digest tests.
Reviewed changes
Copilot reviewed 14 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_web.py | Adds review-ui compile button/route tests (visibility, redirects, queue effects, busy lock, audit attribution). |
| tests/test_digest.py | Freezes proposal decision timestamps to keep digest window tests stable over time. |
| tests/test_compile.py | New end-to-end tests for compile validation, dropping rules, JSONL wiring, and audit logging. |
| src/vouch/web/templates/queue.html | Adds “compile wiki” form + success/error notices on the queue page. |
| src/vouch/web/static/app.css | Styles compile button and queue notices. |
| src/vouch/web/server.py | Adds /compile POST route, compile availability on GET /, and a per-app compile lock. |
| src/vouch/server.py | Registers new MCP tool kb_compile. |
| src/vouch/jsonl_server.py | Registers JSONL handler for kb.compile. |
| src/vouch/compile.py | Implements compile pipeline: prompt build, LLM invocation, draft parsing, validation, proposal filing, audit event. |
| src/vouch/cli.py | Adds vouch compile command with --dry-run/--max-pages/--llm-cmd/--json. |
| src/vouch/capabilities.py | Adds kb.compile to the advertised method surface. |
| README.md | Adds demo video section referencing compile in the full workflow. |
| docs/compile.md | New user documentation for setup, behavior, validator rules, and limits. |
| CHANGELOG.md | Adds release notes for compile + review-ui button. |
| problem = _draft_problem(store, draft, taken_names=taken_names) | ||
| if problem: | ||
| report.dropped.append({"title": title, "reason": problem}) | ||
| continue | ||
| survivors.append((draft, title)) |
| if compile_lock.locked(): | ||
| reason = quote("a compile is already running — refresh in a moment") | ||
| return RedirectResponse(url=f"/?compile_error={reason}", status_code=303) | ||
| async with compile_lock: | ||
| try: | ||
| report = await run_in_threadpool( | ||
| compile_mod.compile_kb, store, | ||
| config=cfg, triggered_by=reviewer(), | ||
| ) | ||
| except compile_mod.CompileError as e: | ||
| reason = quote(str(e)[:200]) | ||
| return RedirectResponse( | ||
| url=f"/?compile_error={reason}", status_code=303, | ||
| ) | ||
| await _notify("queue", action="compile", proposed=len(report.proposed)) |
| {% if compiled is not none %} | ||
| <p class="notice notice-ok">compiled {{ compiled }} page draft(s) into the queue{% if compile_dropped %} · {{ compile_dropped }} dropped by citation checks{% endif %}.</p> |
| try: | ||
| report = compile_mod.compile_kb( | ||
| _store(), triggered_by=_agent(), | ||
| max_pages=p.get("max_pages"), | ||
| dry_run=bool(p.get("dry_run", False)), | ||
| session_id=p.get("session_id"), | ||
| ) |
a config max_pages: 0 (or --max-pages 0) made cap <= 0, so every draft was dropped with "over max_pages=0" after the LLM run was already spent. raise CompileError up front instead of silently producing nothing.
the unlabeled fence trips markdownlint MD040.
a config max_pages: 0 (or --max-pages 0) made cap <= 0, so every draft was dropped with "over max_pages=0" after the LLM run was already spent. raise CompileError up front instead of silently producing nothing.
…iew) the unlabeled fence trips markdownlint MD040.
… review) markdownlint md040 wants a language on the fence; matches how the compile flow diagram was labelled after the vouchdev#362 review.
What changed
Why
What might break
VEP
Tests
make checkpasses locally (lint + mypy + pytest)CHANGELOG.mdupdated under## [Unreleased]Summary by CodeRabbit
New Features
Bug Fixes