Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/vouch/compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
from . import llm_draft
from .config_coerce import coerce_bool
from .context import _RETRACTED_CLAIM_STATUSES
from .models import ProposalStatus
from .models import Page, PageStatus, ProposalStatus
from .proposals import ProposalError, _slugify, propose_page
from .storage import ArtifactNotFoundError, KBStore

Expand Down Expand Up @@ -190,6 +190,19 @@ def to_dict(self) -> dict[str, Any]:
}


def _live_pages(store: KBStore) -> list[Page]:
"""Pages the wiki still has, i.e. everything but the archived ones.

Archiving is how an operator retires a bad compile page. Counting an
archived page as taken makes that retirement one-way: the LLM is told not
to redraft a topic the wiki no longer carries, and a draft that reuses the
title is dropped as a duplicate of a page nobody can read. Claims already
get this treatment via `_RETRACTED_CLAIM_STATUSES`; this is the same live
set recall, digest, search and the wiki view use.
"""
return [p for p in store.list_pages() if p.status is not PageStatus.ARCHIVED]


def _pending_page_names(store: KBStore) -> set[str]:
"""Lowercased titles + ids of page proposals already awaiting review."""
names: set[str] = set()
Expand Down Expand Up @@ -285,7 +298,7 @@ def build_prompt(
]
if not claims:
raise CompileError("nothing to compile: the KB has no live approved claims")
pages = store.list_pages()
pages = _live_pages(store)
pending = _pending_page_names(store)

lines = [
Expand Down Expand Up @@ -472,7 +485,7 @@ def compile_kb(

report = CompileReport(drafts=drafts, dry_run=dry_run)

existing = store.list_pages()
existing = _live_pages(store)
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)
Expand Down
70 changes: 70 additions & 0 deletions tests/test_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -689,3 +689,73 @@ def test_two_phase_compile_drafts_planned_pages(
cmd = _phased_stub(tmp_path, topics, drafts)
report = compile_kb(store, config=_cfg(cmd, two_phase=True))
assert [r["title"] for r in report.proposed] == ["Retry Policy"]


# --- archived pages are retired, not permanently taken (#700) ---------------


def _archived_page(store: KBStore, page_id: str, title: str) -> None:
from vouch.models import Page, PageStatus

store.put_page(
Page(id=page_id, title=title, body="old", status=PageStatus.ARCHIVED)
)


def test_archived_pages_are_not_listed_as_taken_topics(store: KBStore) -> None:
"""Regression for #700: an archived topic stayed in TAKEN TOPICS forever,
so the LLM was told never to redraft a page the wiki no longer carries."""
_approved_claim(store, "the retry limit is three attempts")
_archived_page(store, "retry-policy", "Retry Policy")

prompt = compile_mod.build_prompt(store, max_pages=3)
assert "retry-policy: Retry Policy" not in prompt
assert "- (none)" in prompt


def test_live_pages_are_still_listed_as_taken_topics(store: KBStore) -> None:
from vouch.models import Page

_approved_claim(store, "the retry limit is three attempts")
store.put_page(Page(id="live-policy", title="Live Policy", body="current"))

prompt = compile_mod.build_prompt(store, max_pages=3)
assert "live-policy: Live Policy" in prompt


def test_a_draft_may_reuse_an_archived_title(
store: KBStore, tmp_path: Path
) -> None:
"""The other half of #700: the collision gate dropped a draft that reused
an archived title, so archiving a bad page made that topic unwritable."""
c1 = _approved_claim(store, "the retry limit is three attempts before failing")
_archived_page(store, "retry-policy", "Retry Policy")

cmd = _stub_llm(tmp_path, [{
"title": "Retry Policy",
"type": "decision",
"body": f"Rewritten from scratch [claim: {c1}]",
"claims": [c1],
}])
report = compile_kb(store, config=_cfg(cmd))
assert [r["title"] for r in report.proposed] == ["Retry Policy"]
assert report.dropped == []


def test_a_draft_reusing_a_live_title_is_still_dropped(
store: KBStore, tmp_path: Path
) -> None:
from vouch.models import Page

c1 = _approved_claim(store, "the retry limit is three attempts before failing")
store.put_page(Page(id="retry-policy", title="Retry Policy", body="current"))

cmd = _stub_llm(tmp_path, [{
"title": "Retry Policy",
"type": "decision",
"body": f"Would collide [claim: {c1}]",
"claims": [c1],
}])
report = compile_kb(store, config=_cfg(cmd))
assert report.proposed == []
assert len(report.dropped) == 1
Loading