Skip to content

feat(compile): structured pages, wiki index/moc, two-phase drafting#544

Merged
plind-junior merged 1 commit into
testfrom
feat/compile-structured-pages
Jul 22, 2026
Merged

feat(compile): structured pages, wiki index/moc, two-phase drafting#544
plind-junior merged 1 commit into
testfrom
feat/compile-structured-pages

Conversation

@plind-junior

@plind-junior plind-junior commented Jul 22, 2026

Copy link
Copy Markdown
Member

what

vouch compile now drafts structured, fully-cited wiki pages and renders a
browsable 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:

  • phase 1 — typed section scaffolding in the draft prompt (what/why/how,
    context/options/decision), summary/tags/aliases frontmatter (stored in
    page metadata, no model change), and a citation-density guardrail that drops
    drafts whose substantive sentences are mostly uncited.
  • phase 2src/vouch/wiki_render.py renders a derived index.md
    (grouped by type, with summaries) and MOC.md (backlink graph) over approved
    pages — a regenerable view, never a gated write — via vouch render-wiki.
    the wikilink resolver now matches title, slug, or alias.
  • phase 3 — opt-in two-phase compile (compile.two_phase) that plans
    durable 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):

metric llm-wiki-compiler vouch (this pr)
structured sections yes yes
index + moc + backlinks yes yes
summary / tags / aliases yes yes
dangling wikilinks 55 of 57 (96% broken) 0 (dropped by construction)
uncited / embellished claims at least 2 (a fabricated design decision) 0 (100% coverage, receipt-verified)
review gate conditional / off by default mandatory-pending

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

  • every compiled page is still a pending proposal — proposals.approve is untouched.
  • every inline [claim: id] is verified against the store; uncited prose is
    dropped, not repaired.
  • index and moc are derived renders, not proposals — no new ungated write path.

tests

15 new tests, all test-first (red then green): structured frontmatter, the
citation-density guardrail, alias resolution, wiki_render index/moc/backlinks,
the render-wiki cli command, and two-phase drafting. make check is green
(pytest, mypy, ruff).

follow-ups (not in this pr)

  • wire the index into vault_sync so the obsidian vault gets the front door too.
  • optionally raise default granularity via the two-phase planner.

Summary by CodeRabbit

  • New Features

    • Added vouch render-wiki to generate a searchable wiki index and map of content from approved pages.
    • Display the generated index in the terminal or save index.md and MOC.md to a chosen directory.
    • Wiki links now resolve using page titles, IDs, and aliases, with backlinks and inbound-link rankings.
    • Compilation now supports optional topic planning, page summaries, tags, aliases, and citation coverage validation.
  • Bug Fixes

    • Improved handling of aliases and unsupported low-citation drafts.

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.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The 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 vouch render-wiki.

Changes

Compilation and wiki rendering

Layer / File(s) Summary
Draft schema and citation validation
src/vouch/compile.py, tests/test_compile.py
Compile prompts require summaries, tags, aliases, sectioning, and inline citations; low-coverage drafts are dropped, while accepted metadata and tags are persisted.
Two-phase planning and alias-aware compilation
src/vouch/compile.py, tests/test_compile.py
Optional topic planning drives a second drafting prompt, and aliases participate in wikilink resolution and proposal metadata.
Derived wiki views and CLI output
src/vouch/wiki_render.py, src/vouch/cli.py, tests/test_wiki_render.py, tests/test_cli.py
Approved pages produce deterministic indexes, backlinks, and inbound-reference-ranked MOCs, which the CLI prints or writes to disk.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • vouchdev/vouch#362: Earlier compile-pipeline work involving draft validation, citations, wikilinks, and prompt flow.

Suggested reviewers: tet-9

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: structured pages, wiki render outputs, and two-phase drafting.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/compile-structured-pages

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added cli command line interface tests tests and fixtures size: L 500-999 changed non-doc lines ci: passing ci is green labels Jul 22, 2026
@plind-junior
plind-junior merged commit 285bf79 into test Jul 22, 2026
14 of 18 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (1)
src/vouch/compile.py (1)

53-57: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

use 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

📥 Commits

Reviewing files that changed from the base of the PR and between 1c9ca7c and 0020ea8.

📒 Files selected for processing (6)
  • src/vouch/cli.py
  • src/vouch/compile.py
  • src/vouch/wiki_render.py
  • tests/test_cli.py
  • tests/test_compile.py
  • tests/test_wiki_render.py

Comment thread src/vouch/cli.py
Comment on lines +3107 to +3109
store = _load_store()
pages = store.list_pages()
index = wiki_render_mod.render_index(pages)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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.py

Repository: 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/*.py

Repository: 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.py

Repository: 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

Comment thread src/vouch/compile.py
Comment on lines +82 to +88
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Comment thread src/vouch/compile.py
raw.get("timeout_seconds", DEFAULT_TIMEOUT_SECONDS),
DEFAULT_TIMEOUT_SECONDS, float,
),
two_phase=bool(raw.get("two_phase", False)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 calling bool() 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.

Comment thread src/vouch/compile.py
Comment on lines +461 to +469
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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 pass None to 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.

Comment thread src/vouch/wiki_render.py
Comment on lines +29 to +32
for name in (page.title, page.id, *(page.metadata.get("aliases") or [])):
key = str(name).strip().lower()
if key:
index.setdefault(key, page)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci: passing ci is green cli command line interface size: L 500-999 changed non-doc lines tests tests and fixtures

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant