feat(compile): structured pages, wiki index/moc, two-phase drafting#544
Conversation
compile now drafts browsable, fully-grounded wiki pages that match the llm-wiki compiler's presentation without inheriting its failure modes. phase 1 — the page-draft prompt asks for typed markdown sections (what/why/ how, context/options/decision) plus a one-line summary, tags, and aliases, carried in page metadata. a citation-density guardrail drops any draft whose substantive sentences are mostly uncited, so richer prose cannot smuggle in assertions that trace to no claim. phase 2 — a new wiki_render module renders a derived index.md and a backlink map-of-content over approved pages (a regenerable view, never a gated write), exposed as `vouch render-wiki`. the wikilink resolver now matches title, slug, or alias, so a link to an existing page's alias no longer false-drops. phase 3 — optional two-phase compile (compile.two_phase) plans durable topics first, then drafts one focused page each. off by default; it doubles the llm calls in exchange for finer-grained pages. every write still lands as a pending proposal and every inline [claim: id] is verified against the store — the review gate and receipt fidelity are untouched; this only improves what the compiler drafts behind them.
WalkthroughThe compiler now validates citation coverage, preserves draft metadata, resolves aliases, and optionally plans topics before drafting. A new renderer computes indexes, backlinks, and maps of content for approved pages, exposed through ChangesCompilation and wiki rendering
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 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: 5
🧹 Nitpick comments (1)
src/vouch/compile.py (1)
53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueuse lowercase prose in the new comments and docstrings.
As per path instructions, “Use lowercase prose in comments and review notes.”
Also applies to: 105-110, 136-138, 205-210, 240-244
🤖 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 53 - 57, Update the newly added comments and docstrings in compile.py, including the guardrail comments near the cited sections, so their prose uses lowercase wording throughout. Preserve code, identifiers, and formatting while lowercasing only explanatory comment and docstring text.Source: Path instructions
🤖 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 `@src/vouch/cli.py`:
- Around line 3107-3109: Filter the result of store.list_pages() in the
render-wiki flow before passing it to wiki_render_mod.render_index, retaining
only approved/non-draft pages. Keep the approved-page boundary explicit and add
coverage verifying draft pages are absent from both index.md and MOC.md outputs.
In `@src/vouch/compile.py`:
- Around line 82-88: The citation coverage logic around has_marker in
src/vouch/compile.py lines 82-88 must count a sentence as cited only when its
valid claim marker terminates the substantive sentence, allowing only trailing
punctuation or whitespace; update the marker validation accordingly while
preserving the existing substantive-word filtering. Add a regression case in
tests/test_compile.py lines 536-568 with a mid-sentence marker followed by an
uncited assertion, and assert that the sentence is dropped.
- Line 172: The two_phase configuration currently treats quoted YAML "false" as
true; update the two_phase parsing in src/vouch/compile.py:172-172 to explicitly
validate or parse boolean values instead of applying bool() to arbitrary input,
while preserving native boolean behavior. Add a regression test in
tests/test_compile.py:632-639 proving quoted false does not enable two-phase
compilation.
- Around line 461-469: Preserve the distinction between disabled two-phase mode
and an empty phase-A plan: in src/vouch/compile.py lines 461-469, when
cfg.two_phase is enabled and parse_topics yields no titles, return no drafts or
raise the established planning error instead of passing None to build_prompt;
retain None only when two-phase mode is disabled. In tests/test_compile.py lines
663-679, add an integration case where phase A returns [] and verify that no
unconstrained page is proposed.
In `@src/vouch/wiki_render.py`:
- Around line 29-32: Update the alias expansion in the page indexing loop to
accept aliases only when metadata["aliases"] is a list of strings; treat
missing, non-list, or otherwise invalid aliases as empty. Preserve normalization
and indexing of page.title, page.id, and valid aliases through the existing key
logic.
---
Nitpick comments:
In `@src/vouch/compile.py`:
- Around line 53-57: Update the newly added comments and docstrings in
compile.py, including the guardrail comments near the cited sections, so their
prose uses lowercase wording throughout. Preserve code, identifiers, and
formatting while lowercasing only explanatory comment and docstring text.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 16fbf50b-3492-479d-a3c3-9cc239ca2bf8
📒 Files selected for processing (6)
src/vouch/cli.pysrc/vouch/compile.pysrc/vouch/wiki_render.pytests/test_cli.pytests/test_compile.pytests/test_wiki_render.py
| store = _load_store() | ||
| pages = store.list_pages() | ||
| index = wiki_render_mod.render_index(pages) |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/vouch/storage.py --match KBStore --view expanded
rg -n -A45 -B4 '^\s*def list_pages\(' src/vouch/storage.py
rg -n -A12 -B4 '\.list_pages\s*\(' src/vouch/cli.pyRepository: vouchdev/vouch
Length of output: 8240
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# inspect page model, storage layout, and any status filtering around pages
ast-grep outline src/vouch/models.py --view expanded
ast-grep outline src/vouch/storage.py --match _load_page_or_skip --view expanded
rg -n -A25 -B8 'class Page|PageStatus|status:|def put_page\(|def update_page\(|def _page_path\(' src/vouch/*.pyRepository: vouchdev/vouch
Length of output: 50370
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# inspect wiki rendering inputs to understand whether it expects approved-only pages
ast-grep outline src/vouch/wiki_render.py --view expanded
rg -n -A25 -B8 'render_index\(|render_moc\(|approved|page' src/vouch/wiki_render.py src/vouch/cli.pyRepository: vouchdev/vouch
Length of output: 50371
filter out draft pages before rendering the wiki front door. store.list_pages() returns every page, so render-wiki can write unapproved drafts into index.md and MOC.md; keep the approved-page boundary explicit here and add a test that a draft page stays out of both outputs.
🤖 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/cli.py` around lines 3107 - 3109, Filter the result of
store.list_pages() in the render-wiki flow before passing it to
wiki_render_mod.render_index, retaining only approved/non-draft pages. Keep the
approved-page boundary explicit and add coverage verifying draft pages are
absent from both index.md and MOC.md outputs.
Source: Path instructions
| has_marker = "[claim:" in s | ||
| stripped = _WIKILINK_RE.sub("", _CLAIM_MARKER_RE.sub("", s)) | ||
| if len(_WORD_RE.findall(stripped)) < _MIN_SUBSTANTIVE_WORDS: | ||
| continue | ||
| total += 1 | ||
| if has_marker: | ||
| cited += 1 |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
citation coverage accepts markers that do not cite the full sentence. Require the marker to terminate the substantive sentence, then lock that contract down with a regression test.
src/vouch/compile.py#L82-L88: count a sentence as cited only when its valid claim marker appears at the end, aside from trailing punctuation or whitespace.tests/test_compile.py#L536-L568: add a sentence with a mid-sentence marker followed by an uncited assertion and assert it is dropped.
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 83-83: XPath query is request-/variable-derived; use parameterized XPath to prevent injection.
Context: _WORD_RE.findall(stripped)
Note: [CWE-643] Improper Neutralization of Data within XPath Expressions ('XPath Injection').
(xpath-injection-python)
📍 Affects 2 files
src/vouch/compile.py#L82-L88(this comment)tests/test_compile.py#L536-L568
🤖 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 82 - 88, The citation coverage logic
around has_marker in src/vouch/compile.py lines 82-88 must count a sentence as
cited only when its valid claim marker terminates the substantive sentence,
allowing only trailing punctuation or whitespace; update the marker validation
accordingly while preserving the existing substantive-word filtering. Add a
regression case in tests/test_compile.py lines 536-568 with a mid-sentence
marker followed by an uncited assertion, and assert that the sentence is
dropped.
| raw.get("timeout_seconds", DEFAULT_TIMEOUT_SECONDS), | ||
| DEFAULT_TIMEOUT_SECONDS, float, | ||
| ), | ||
| two_phase=bool(raw.get("two_phase", False)), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
quoted yaml false values enable two-phase mode. Python truthiness makes the non-empty string "false" true.
src/vouch/compile.py#L172-L172: validate or explicitly parse the configured boolean instead of callingbool()on arbitrary YAML values.tests/test_compile.py#L632-L639: add coverage proving that quoted false does not enable two-phase compilation.
📍 Affects 2 files
src/vouch/compile.py#L172-L172(this comment)tests/test_compile.py#L632-L639
🤖 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` at line 172, The two_phase configuration currently
treats quoted YAML "false" as true; update the two_phase parsing in
src/vouch/compile.py:172-172 to explicitly validate or parse boolean values
instead of applying bool() to arbitrary input, while preserving native boolean
behavior. Add a regression test in tests/test_compile.py:632-639 proving quoted
false does not enable two-phase compilation.
| planned: list[str] | None = None | ||
| if cfg.two_phase: | ||
| # phase A: plan the durable topics, then draft one focused page each. | ||
| topics_raw = run_llm( | ||
| cmd, build_topic_prompt(store, max_pages=cap), | ||
| timeout_seconds=cfg.timeout_seconds, | ||
| ) | ||
| planned = parse_topics(topics_raw)[:cap] or None | ||
| prompt = build_prompt(store, max_pages=cap, planned_topics=planned) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
an empty plan disables the plan constraint and allows arbitrary drafts. Preserve the distinction between no two-phase mode and a valid empty phase-A result.
src/vouch/compile.py#L461-L469: return no drafts or raise a planning error when phase A yields no titles; do not passNoneto phase B.tests/test_compile.py#L663-L679: add an integration case where phase A returns[]and verify no unconstrained page is proposed.
📍 Affects 2 files
src/vouch/compile.py#L461-L469(this comment)tests/test_compile.py#L663-L679
🤖 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 461 - 469, Preserve the distinction
between disabled two-phase mode and an empty phase-A plan: in
src/vouch/compile.py lines 461-469, when cfg.two_phase is enabled and
parse_topics yields no titles, return no drafts or raise the established
planning error instead of passing None to build_prompt; retain None only when
two-phase mode is disabled. In tests/test_compile.py lines 663-679, add an
integration case where phase A returns [] and verify that no unconstrained page
is proposed.
| for name in (page.title, page.id, *(page.metadata.get("aliases") or [])): | ||
| key = str(name).strip().lower() | ||
| if key: | ||
| index.setdefault(key, page) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
normalize aliases before expanding them.
Page.metadata permits arbitrary values. A hand-edited aliases: beta string is expanded into b, e, t, a, creating false resolutions and backlinks. Accept only a list of strings (or validate this at the page metadata boundary).
proposed fix
def _link_index(pages: list[Page]) -> dict[str, Page]:
index: dict[str, Page] = {}
for page in pages:
- for name in (page.title, page.id, *(page.metadata.get("aliases") or [])):
+ aliases = page.metadata.get("aliases")
+ if not isinstance(aliases, list):
+ aliases = []
+ for name in (page.title, page.id, *aliases):
key = str(name).strip().lower()📝 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.
| for name in (page.title, page.id, *(page.metadata.get("aliases") or [])): | |
| key = str(name).strip().lower() | |
| if key: | |
| index.setdefault(key, page) | |
| for page in pages: | |
| aliases = page.metadata.get("aliases") | |
| if not isinstance(aliases, list): | |
| aliases = [] | |
| for name in (page.title, page.id, *aliases): | |
| key = str(name).strip().lower() | |
| if key: | |
| index.setdefault(key, page) |
🤖 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/wiki_render.py` around lines 29 - 32, Update the alias expansion in
the page indexing loop to accept aliases only when metadata["aliases"] is a list
of strings; treat missing, non-list, or otherwise invalid aliases as empty.
Preserve normalization and indexing of page.title, page.id, and valid aliases
through the existing key logic.
what
vouch compilenow drafts structured, fully-cited wiki pages and renders abrowsable index + backlink map — matching the llm-wiki compiler's presentation
while keeping vouch's fidelity and mandatory review gate. three phases, each
written test-first:
context/options/decision),
summary/tags/aliasesfrontmatter (stored inpage metadata, no model change), and a citation-density guardrail that drops
drafts whose substantive sentences are mostly uncited.
src/vouch/wiki_render.pyrenders a derivedindex.md(grouped by type, with summaries) and
MOC.md(backlink graph) over approvedpages — a regenerable view, never a gated write — via
vouch render-wiki.the wikilink resolver now matches title, slug, or alias.
compile.two_phase) that plansdurable topics before drafting one focused page each.
why — measured against llm-wiki-compiler
the same real claude-code session was compiled through both tools with the same
model (claude sonnet):
the llm-wiki compiler's richer-looking wiki was 96% broken links and contained
fabrications; vouch's is fully interlinked and every sentence traces to a
verified claim. this pr closes the presentation gap without touching the
extract/receipt path or the review gate.
invariants held
proposals.approveis untouched.[claim: id]is verified against the store; uncited prose isdropped, not repaired.
tests
15 new tests, all test-first (red then green): structured frontmatter, the
citation-density guardrail, alias resolution,
wiki_renderindex/moc/backlinks,the
render-wikicli command, and two-phase drafting.make checkis green(pytest, mypy, ruff).
follow-ups (not in this pr)
vault_syncso the obsidian vault gets the front door too.Summary by CodeRabbit
New Features
vouch render-wikito generate a searchable wiki index and map of content from approved pages.index.mdandMOC.mdto a chosen directory.Bug Fixes