diff --git a/ouroboros/context.py b/ouroboros/context.py index fa0ecd97a..fbdf10d39 100644 --- a/ouroboros/context.py +++ b/ouroboros/context.py @@ -610,21 +610,25 @@ def build_knowledge_sections( # One mind keeps its authored common orientation across rooms. The generated # inventory is navigation, not a substitute for that understanding; a # project's shelf adds focus without hiding the common corpus. - from ouroboros.knowledge import INDEX_FILE, OVERVIEW_TOPIC, read_knowledge_note, resolve_knowledge_address + from ouroboros.knowledge import (INDEX_FILE, OVERVIEW_TOPIC, inventory_knowledge, + read_knowledge_note, render_knowledge_index, resolve_knowledge_address) pid = str(project_id or "").strip() - global_address = resolve_knowledge_address(env.drive_root, OVERVIEW_TOPIC, "global") + global_address = resolve_knowledge_address(env.drive_path("memory").parent, OVERVIEW_TOPIC, "global") + authored_overview = False try: overview = read_knowledge_note(global_address) overview_text = overview.source.text_at(overview.source.body_span) if overview.source else overview.text if overview_text.strip(): - sections.append("## Shared understanding\n\n" + overview_text) + authored_overview = overview.source is not None + sections.append(f"## Shared understanding\n\nSource: knowledge_read(topic='{OVERVIEW_TOPIC}', scope='global').\n\n" + overview_text) except FileNotFoundError: pass # The generated index retains prior orientation until one is authored. except (OSError, UnicodeDecodeError) as exc: sections.append(f"Shared understanding source unavailable: knowledge_read(topic='{OVERVIEW_TOPIC}', scope='global'). {type(exc).__name__}.") knowledge_indexes = [(global_address.shelf / INDEX_FILE, - "## Knowledge base", "knowledge index")] + "## Knowledge base\n\nGlobal navigation: knowledge_list(scope='global'); read linked topics with knowledge_read(topic=..., scope='global').", + "knowledge index")] if pid: from ouroboros.project_facts import project_knowledge_dir @@ -633,7 +637,8 @@ def build_knowledge_sections( if include_pattern_body: knowledge_indexes.append((env.drive_path("memory/knowledge/patterns.md"), pattern_header, "patterns register")) for path, header, label in knowledge_indexes: - text = safe_read(path) + text = (render_knowledge_index(inventory_knowledge(global_address), include_summaries=False) + if authored_overview and path == global_address.shelf / INDEX_FILE else safe_read(path)) if not text.strip(): continue if warn_large and len(text) > _LARGE_CONTEXT_SECTION_CHARS: diff --git a/ouroboros/deep_self_review.py b/ouroboros/deep_self_review.py index b23a37b02..aaf4cb57e 100644 --- a/ouroboros/deep_self_review.py +++ b/ouroboros/deep_self_review.py @@ -27,6 +27,7 @@ receipts, coverage and completeness so consecutive reports stay comparable. from __future__ import annotations import logging +import json import pathlib import posixpath import time @@ -53,6 +54,7 @@ from ouroboros.utils import atomic_write_json, estimate_tokens, utc_now_iso # n from ouroboros.config import get_context_mode # noqa: E402 from ouroboros.provider_models import provider_for_model, provider_has_credentials # noqa: E402 from ouroboros.context_layout import generate_doc_nav_map # noqa: E402 +from ouroboros.reference_books import BOOK_ENTRYPOINTS, compose_book, load_reference_book, overview_book # noqa: E402 from ouroboros.reviewer_slot_config import ( # noqa: E402 ROUTE_KIND_API, ROUTE_KIND_SESSION, @@ -138,8 +140,10 @@ How to work: you are reading the repository yourself with read-only tools. Read `BIBLE.md` IN FULL first (about {bible_chars:,} chars — in bounded chunks): every finding is checked against it, and a report that did not read it is not a deep self-review. The memory files below are inlined byte-exact; `docs/ARCHITECTURE.md`, -`docs/DEVELOPMENT.md` and `docs/CHECKLISTS.md` are given as navigation maps — read the -sections you need on demand. Then inspect the code (search_code, query_code, +`docs/DEVELOPMENT.md` are book entrypoints; their overviews identify the actual +physical chapter sources. `docs/CHECKLISTS.md` has its own navigation map. Read +the needed sources on demand, using lines local to the physical file you open, +never treating composed-book line numbers as an entrypoint address. Then inspect the code (search_code, query_code, read_file), cross-reference interactions between modules and follow call chains out of the files you open. Prioritize: CRITICAL > IMPORTANT > ADVISORY. @@ -335,27 +339,37 @@ def build_review_pack( memory = _append_memory_whitelist(memory_parts, skipped, drive_root=drive_root) memory_text = "\n".join(memory_parts) - # Low context mode: render ARCHITECTURE.md as a navigation map (full sections - # read on demand) and exclude it from the atlas full-file selection instead of - # inlining ~32K tokens. Reuses the atlas ``already_included`` mechanism so the - # shared commit-gate atlas (scope / plan review) is unaffected. + # The atlas still owns repository selection. Composed books have one + # physical closure, included once here and excluded from duplicate atlas + # content. Legacy Max monoliths retain their original atlas delivery. nav_parts: list[str] = [] - already_included: frozenset[str] = frozenset() - if get_context_mode() == "low": + book_parts: list[str] = [] + book_views: list[dict] = [] + already_included: set[str] = set() + low = get_context_mode() == "low" + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + if entrypoint not in tracked: + continue try: - arch_text = (repo_dir / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") - except Exception: - arch_text = "" - if arch_text.strip(): - nav_parts.append( - generate_doc_nav_map( - arch_text, title="ARCHITECTURE.md", rel_path="docs/ARCHITECTURE.md" - ) + book = load_reference_book(repo_dir, book_id) + except (OSError, ValueError) as exc: + return "", {"file_count": 0, "total_chars": 0, "skipped": [f"FATAL: {entrypoint} book unavailable: {exc}"]} + if book.legacy and not (low and book_id == "architecture"): + continue + sources = (book.entrypoint, *book.chapters) + partial = low and book_id == "architecture" + already_included.update(s.source_path for s in sources) + book_views.append({"book_id": book_id, "delivery": "overview" if partial else "full", + "sources": [{"path": s.source_path, "sha256": s.sha256, "size": len(s.raw)} for s in sources]}) + if partial: + navigation = (generate_doc_nav_map(book.entrypoint.text, title="ARCHITECTURE.md", rel_path=entrypoint) + if book.legacy else overview_book(book).text) + nav_parts.append(navigation + "\n\nNote for this deep self-review call: this surface has no tool loop, " "so the navigation map is an index of omitted sections, not an actionable " - "read_file instruction. Flag any needed full ARCHITECTURE.md section explicitly." - ) - already_included = frozenset({"docs/ARCHITECTURE.md"}) + "read_file instruction. Flag any needed full Architecture chapter explicitly.") + else: + book_parts.append(f"## Reference book: {entrypoint}\n\n" + compose_book(book)) # Reserve the (bounded) omission section inside the atlas's fixed budget — # it is appended to the pack after the atlas fills, so an unreserved section @@ -363,6 +377,7 @@ def build_review_pack( atlas_fixed_tokens = ( int(fixed_prompt_tokens) + estimate_tokens(memory_text) + + estimate_tokens("\n".join(book_parts)) + estimate_tokens("\n".join(nav_parts)) + _OMISSION_SECTION_RESERVE_TOKENS ) @@ -375,7 +390,7 @@ def build_review_pack( ReviewContextAtlasRequest( repo_dir=repo_dir, tracked_paths=tuple(tracked), - already_included=already_included, + already_included=frozenset(already_included), fixed_prompt_tokens=atlas_fixed_tokens, target_total_tokens=min(850_000, hard_budget), hard_total_tokens=hard_budget, @@ -408,10 +423,17 @@ def build_review_pack( for record in atlas.omitted if record.disposition not in {"already_included", "manifest_only"} ) - parts = [atlas.text] + if book_views: + atlas.manifest["reference_book_views"] = book_views + for row in atlas.manifest.get("coverage", []): + view = next((v for v in book_views if any(s["path"] == row.get("path") for s in v["sources"])), None) + if view: + row["reason"] = ("physical book source included in full composition" if view["delivery"] == "full" + else "book overview only; full chapter text omitted from this one-packet delivery") + parts = [*book_parts, atlas.text] parts.extend(nav_parts) parts.extend(memory_parts) - file_count = len(atlas.selected) + memory["inlined"] + file_count = len(atlas.selected) + memory["inlined"] + sum(len(v["sources"]) for v in book_views if v["delivery"] == "full") _append_omission_section(parts, skipped) pack_text = "\n".join(parts) @@ -635,7 +657,10 @@ def _repo_relative(path: Any, repo_dir: pathlib.Path) -> str: def _native_read_coverage(usage: Dict[str, Any], repo_dir: pathlib.Path) -> Dict[str, Dict[str, Any]]: - """R8: how much of each mandatory read the host OBSERVED, from the episode's + """Prefer the native operation's exact manifest-bound source coverage. + + Historical receipts retain only their explicitly labelled line evidence. + R8: how much of each mandatory read the host OBSERVED, from the episode's receipts — the merged line intervals of every executed repository-root ``read_file`` receipt for the path (a single result is capped, so a full read of BIBLE.md is multi-chunk by construction). @@ -656,6 +681,27 @@ def _native_read_coverage(usage: Dict[str, Any], repo_dir: pathlib.Path) -> Dict traversal shapes before dispatch — see ``_repo_relative``). Disclosure, never a refusal: the report is delivered with the flag in its header. """ + exact = usage.get("native_read_coverage") + sources = exact.get("sources") if isinstance(exact, dict) else None + if isinstance(sources, list) and sources: + out = {} + for index, source in enumerate(sources): + source = source if isinstance(source, dict) else {} + root, path = str(source.get("root") or ""), str(source.get("path") or f"source[{index}]") + address = path if root in _REPO_ROOTS else f"{root}:{path}" + if address in out: + address = f"{root}:{path}@{source.get('source_revision', index)}" + total, covered = source.get("complete_chars"), source.get("covered_chars") + known = (type(total) is int and type(covered) is int and 0 <= covered <= total + and isinstance(source.get("source_revision"), str) and len(source["source_revision"]) == 64) + state = ("read" if source.get("status") == "complete" and known else + "partial" if source.get("status") == "incomplete" and known and covered else + "missing" if source.get("status") == "incomplete" and known else "unobserved") + out[address] = {**source, "state": state, "covered_chars": covered if known else 0, + "complete_chars": total if known else 0, + "fraction": round(covered / total, 3) if known and total else 1.0 if state == "read" else 0.0, + "evidence_basis": "source_ranges"} + return out receipts = [r for r in (usage.get("native_tool_receipts") or []) if isinstance(r, dict)] capped = int(usage.get("native_tool_calls") or 0) > len(receipts) out: Dict[str, Dict[str, Any]] = {} @@ -666,6 +712,7 @@ def _native_read_coverage(usage: Dict[str, Any], repo_dir: pathlib.Path) -> Dict named = r.get("opened_path") if isinstance(r.get("opened_path"), str) and r.get("opened_path") else r.get("path") root = r.get("opened_root") if isinstance(r.get("opened_root"), str) and r.get("opened_root") else str(r.get("root") or "") if (r.get("tool") != "read_file" or r.get("outcome") != "executed" + or r.get("delivered") is False or root not in _REPO_ROOTS or _repo_relative(named, repo_dir) != rel): continue if not all(isinstance(r.get(k), int) for k in ("start_line", "end_line", "total_lines")): @@ -691,7 +738,8 @@ def _native_read_coverage(usage: Dict[str, Any], repo_dir: pathlib.Path) -> Dict else: state = "partial" if covered else "missing" out[rel] = {"state": state, "covered_lines": covered, "total_lines": total, - "fraction": round(covered / total, 3) if total else 0.0} + "fraction": round(covered / total, 3) if total else 0.0, + "evidence_basis": "legacy_lines"} return out @@ -723,7 +771,11 @@ def _delivery_incomplete(delivery: str, usage: Dict[str, Any], message: Optional sets no usage finish reason at all) — meaning the report hit the output reserve; a session's completeness is not host-observable.""" if delivery == "native_tool_rounds": - return str(usage.get("native_incomplete") or "") or "none" + reported = str(usage.get("native_incomplete") or "") + coverage = usage.get("native_read_coverage") or {} + if not reported and coverage.get("sources") and coverage.get("status") != "complete": + reported = "required_source_coverage_incomplete" + return reported or "none" if delivery == "api_packet": msg = message if isinstance(message, dict) else {} cut = (str(usage.get("response_finish_reason") or "") == "length" @@ -761,6 +813,7 @@ def _provenance_header(delivery: str, model: str, usage: Dict[str, Any], memory: "end_reason": usage.get("native_end_reason", ""), "transcript": f"{usage.get('native_transcript_chars', 0)}/{usage.get('native_transcript_bound', 0)}", "landing": f"{usage.get('native_landing_notified', False)}/{usage.get('native_landing_sent', False)}", + "coverage_basis": usage.get("deep_review_coverage_basis", "legacy_lines"), }) elif delivery == "api_packet": facts["attestation"] = "packed" @@ -798,7 +851,8 @@ def _failed(text: str, *, reason_code: str, usage: Optional[Dict[str, Any]] = No return text, out -def _retrieving_task(repo_dir: pathlib.Path, drive_root: pathlib.Path) -> Tuple[str, Dict[str, Any]]: +def _retrieving_task(repo_dir: pathlib.Path, drive_root: pathlib.Path, *, + required_sources_ref: Optional[dict] = None) -> Tuple[str, Dict[str, Any]]: """The route-owned task text for a retrieving row: role + method, the memory whitelist inline (byte-exact, as the packed pack carries it), and the governance navigation maps. BIBLE.md is a mandatory READ, never @@ -811,8 +865,18 @@ def _retrieving_task(repo_dir: pathlib.Path, drive_root: pathlib.Path) -> Tuple[ memory_parts: list[str] = [] skipped: list[str] = [] memory = _append_memory_whitelist(memory_parts, skipped, drive_root=drive_root) + navigation = [] + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + if not (repo_dir / entrypoint).is_file(): + navigation.append(f"[Missing reference book: {entrypoint}]") + continue + book = load_reference_book(repo_dir, book_id) + navigation.append(generate_doc_nav_map(book.entrypoint.text, title=entrypoint, rel_path=entrypoint) + if book.legacy else overview_book(book).text) + navigation.append(governance_nav_maps(repo_dir, tuple(p for p in _NAV_MAP_DOCS if p not in BOOK_ENTRYPOINTS.values()))) parts = [ _ROLE_PROMPT + _RETRIEVING_METHOD.format(bible_chars=len(bible)), + *navigation, "## Memory (runtime data root, inlined byte-exact)", *memory_parts, # EVERY whitelisted entry gets its disposition here — the omission @@ -821,7 +885,9 @@ def _retrieving_task(repo_dir: pathlib.Path, drive_root: pathlib.Path) -> Tuple[ f"Memory dispositions ({memory['total']} whitelisted): " + "; ".join(f"{rel} {d}" for rel, d in memory["dispositions"].items()), ] - parts.append(governance_nav_maps(repo_dir, _NAV_MAP_DOCS)) + if required_sources_ref: + parts.append("Read the complete required source manifest through its exact source handle and cover every listed source across your working views: " + + json.dumps(required_sources_ref, ensure_ascii=False)) return "\n\n".join(parts), {"memory": memory, "bible_chars": len(bible)} @@ -834,6 +900,8 @@ def _run_retrieving_review( *, task_id: str, deadline_at: str, + required_sources: Optional[list] = None, + required_sources_ref: Optional[dict] = None, ) -> Tuple[str, Dict[str, Any]]: """A retrieving row (native episode or delegated session) through the shared executor seam, exactly like the advisory: hand-built request, slot @@ -847,7 +915,10 @@ def _run_retrieving_review( from ouroboros.review_substrate import ReviewRequest from ouroboros.usage_accounting import UsageScope, current_usage_scope, usage_scope - task_text, task_facts = _retrieving_task(repo_dir, drive_root) + task_text, task_facts = _retrieving_task(repo_dir, drive_root, required_sources_ref=required_sources_ref) + policy = {"output_contract": _REPORT_CONTRACT, "native_data_root": str(drive_root)} + if required_sources is not None: + policy.update(native_required_sources=required_sources, native_required_sources_ref=required_sources_ref or {}) request = ReviewRequest( surface="deep_self_review", goal="Deep self-review of the whole Ouroboros system against BIBLE.md.", @@ -859,7 +930,7 @@ def _run_retrieving_review( # REAL runtime root (R5), readable by the reviewer's own tools; memory # coverage itself is the inline whitelist in the task (byte-exact, # disposition-disclosed) — never receipts. - policy={"output_contract": _REPORT_CONTRACT, "native_data_root": str(drive_root)}, + policy=policy, deadline_at=deadline_at, ) # The logical window: the task's absolute ceiling narrowed by the owner @@ -930,17 +1001,21 @@ def _run_retrieving_review( reason_code="deep_self_review_error", usage=usage) if delivery == "native_tool_rounds": detail = _native_read_coverage(usage, repo_dir) + usage["deep_review_coverage_basis"] = "source_ranges" if any(c["evidence_basis"] == "source_ranges" for c in detail.values()) else "legacy_lines" coverage = {rel: (f"partial({c['fraction']:.2f})" if c["state"] == "partial" else c["state"]) for rel, c in detail.items()} for rel, c in detail.items(): if c["state"] != "read": + covered, total, unit = (c["covered_chars"], c["complete_chars"], "characters") if c["evidence_basis"] == "source_ranges" else (c["covered_lines"], c["total_lines"], "lines") + missing = f"no delivered range matching the required source revision of {rel}" if c["evidence_basis"] == "source_ranges" else f"no executed repository-root read_file receipt for {rel}" + unobserved = f"the exact required source extent of {rel} is unobserved" if c["evidence_basis"] == "source_ranges" else f"the {rel} read extent is unobserved (receipts capped or extent not recorded)" usage["capability_delta"].append({ "kind": "capability_delta", "requested": f"mandatory full read of {rel}", "effective": { - "partial": f"{c['covered_lines']} of {c['total_lines']} lines of {rel} delivered (merged receipts)", - "missing": f"no executed repository-root read_file receipt for {rel}", - }.get(c["state"], f"the {rel} read extent is unobserved (receipts capped or extent not recorded)"), + "partial": f"{covered} of {total} {unit} of {rel} delivered (merged receipts)", + "missing": missing, + }.get(c["state"], unobserved), "reason": f"deep_review_mandatory_read_{c['state']}", }) else: @@ -969,10 +1044,12 @@ def _run_retrieving_review( completeness = "complete" if incomplete == "none" else ( "completeness not host-observed" if incomplete == "unobserved" else f"INCOMPLETE ({shown_reason})") if delivery == "native_tool_rounds": - reads = "; ".join( - f"{rel} " + (f"{c['fraction']:.0%} read ({c['covered_lines']}/{c['total_lines']} lines)" if c["state"] == "partial" - else {"read": "read in full", "missing": "NOT read"}.get(c["state"], "read extent unobserved")) - for rel, c in detail.items()) + reads = [] + for rel, c in detail.items(): + covered, total, unit = (c["covered_chars"], c["complete_chars"], "characters") if c["evidence_basis"] == "source_ranges" else (c["covered_lines"], c["total_lines"], "lines") + reads.append(f"{rel} " + (f"{c['fraction']:.0%} read ({covered}/{total} {unit})" if c["state"] == "partial" + else {"read": "read in full", "missing": "NOT read"}.get(c["state"], "read extent unobserved"))) + reads = "; ".join(reads) human = ( f"Deep self-review: native inspection episode on {shown_model} — {int(usage.get('native_rounds') or 0)} rounds, " f"{int(usage.get('native_tool_calls') or 0)} tool calls ({len(usage.get('native_tool_receipts') or [])} host-observed receipts); " @@ -1283,6 +1360,8 @@ def run_deep_self_review( task_id: str = "", deadline_at: str = "", slot: Optional[ConfiguredReviewerSlot] = None, + required_sources: Optional[list] = None, + required_sources_ref: Optional[dict] = None, ) -> Tuple[str, Dict[str, Any]]: """Execute the deep self-review on the configured row. @@ -1297,6 +1376,9 @@ def run_deep_self_review( refusal is budget vocabulary for the agent's budget-pause rail, not a review error. ``slot`` overrides the configured row (tests, callers that already resolved it). + ``required_sources`` and its exact source handle come from the caller's + existing immutable review assembler; this consumer never infers the + complete required surface from a working-tree scan. """ try: try: @@ -1313,6 +1395,7 @@ def run_deep_self_review( if row.retrieves: return _run_retrieving_review( repo_dir, drive_root, llm, emit_progress, row, task_id=task_id, deadline_at=deadline_at, + required_sources=required_sources, required_sources_ref=required_sources_ref, ) return _run_packed_review(repo_dir, drive_root, llm, emit_progress, row, str(model or "")) except BudgetExceeded: diff --git a/ouroboros/knowledge.py b/ouroboros/knowledge.py index 3c2d639de..2de78e7bc 100644 --- a/ouroboros/knowledge.py +++ b/ouroboros/knowledge.py @@ -180,14 +180,15 @@ def _label(value: str) -> str: return value.replace("\\", "\\\\").replace("[", "\\[").replace("]", "\\]").replace("*", "\\*") -def render_knowledge_index(rows: tuple[dict[str, Any], ...], legacy_context: str = "") -> str: +def render_knowledge_index(rows: tuple[dict[str, Any], ...], legacy_context: str = "", + *, include_summaries: bool = True) -> str: """Generated navigation is separate from authored understanding.""" entries = [] for row in rows: topic, title = str(row["topic"]), str(row.get("title") or row["topic"]) line = f"- **{_label(topic)}**: [{_label(title)}](<{quote(topic + '.md', safe='/')}>)" summary = row.get("summary") - if isinstance(summary, str) and summary: + if include_summaries and isinstance(summary, str) and summary: line += "\n" + "\n".join(" " + part for part in summary.splitlines()) if row.get("read_error") or row.get("parse_error"): line += "\n (source metadata unavailable; read the original note)" diff --git a/ouroboros/skill_review_passes.py b/ouroboros/skill_review_passes.py index 478c07c8e..fbb036546 100644 --- a/ouroboros/skill_review_passes.py +++ b/ouroboros/skill_review_passes.py @@ -101,7 +101,13 @@ _SINGLE_CONTENT = ( _SESSION_RETRIEVAL = ( "Use native read/search tools inside the source-repository session root. Read " - "`BIBLE.md`, `docs/ARCHITECTURE.md`, and `docs/DEVELOPMENT.md` in full; read " + "`BIBLE.md` in full. Read the `docs/ARCHITECTURE.md` and `docs/DEVELOPMENT.md` " + "book entrypoints in full, then every physical chapter in each entrypoint's " + "`Chapters` membership list in full, in the order you choose. Resolve each chapter link " + "relative to its entrypoint; a legacy monolith without `Chapters` is already " + "the complete book. Cite physical file paths and file-local lines, never " + "line numbers in a composed book. Reading an entrypoint alone does not " + "cover its chapters; report any missing or unread chapter explicitly. Read " "the `Skill Review Checklist` section of `docs/CHECKLISTS.md`; then read " "`docs/CREATING_SKILLS.md`, `ouroboros/contracts/plugin_api.py`, and " "`ouroboros/extension_ui_validation.py` in full. Treat those source reads as " diff --git a/ouroboros/skill_review_prompt.py b/ouroboros/skill_review_prompt.py index 1d5dc35d5..2cd37c933 100644 --- a/ouroboros/skill_review_prompt.py +++ b/ouroboros/skill_review_prompt.py @@ -16,6 +16,7 @@ import logging import pathlib from typing import Any, Dict, List +from ouroboros.reference_books import BOOK_ENTRYPOINTS, compose_book, load_reference_book from ouroboros.skill_review_history import count_attempts as _count_attempts_for_content from ouroboros.skill_review_status import CRITICAL_ITEMS from ouroboros.tools.review_helpers import ( @@ -67,6 +68,12 @@ def _load_governance_artifact( """Load governance context with an explicit omission marker on failure.""" from ouroboros.tools.review_helpers import load_governance_doc + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + if relpath == entrypoint: + try: + return compose_book(load_reference_book(repo_root, book_id)) + except (OSError, ValueError) as exc: + return f"[⚠️ OMISSION: {relpath} book could not be loaded: {exc}]" return load_governance_doc(repo_root, relpath, on_missing="explicit") @@ -126,11 +133,11 @@ review enforcement mode. ## Governance context — docs/ARCHITECTURE.md -Use Section 10 (Key Invariants), Section 12 (Host Service / Companion / -Chat IDs), and Section 13 (External Skills Layer) -as the binding description of what the skill is allowed to touch. In -particular invariant 11 is the authoritative rule: skills must not write -to the self-modifying repo, and reviewed execution is the primary gate. +Use the named sections "Key Invariants", "Host Service, Companion Processes, +and Chat IDs", and "External Skills Layer" as the binding description of what +the skill is allowed to touch. The "Skill gates do not collapse" criterion +keeps executable review, owner grants, dependencies, enablement, and execution +distinct; apply the Skill Review Checklist's `no_repo_mutation` item. {architecture_text} diff --git a/tests/test_book_context_capture.py b/tests/test_book_context_capture.py index 58e58ceb1..c3c260e45 100644 --- a/tests/test_book_context_capture.py +++ b/tests/test_book_context_capture.py @@ -106,3 +106,18 @@ def test_large_nano_task_retains_exact_owner_source_without_changing_max(tmp_pat assert plan.messages_for("max")[-1]["content"] == task_text assert "FINAL OWNER CRITERION" in json.loads(core.user_content_json) assert plan.nano_projection.estimated_tokens < 81920 + + +def test_authored_common_understanding_is_resident_without_every_note_summary(tmp_path): + from ouroboros.knowledge import resolve_knowledge_address, write_knowledge_note + + env, _core, task = _capture(tmp_path) + write_knowledge_note(resolve_knowledge_address(env.drive_root, "overview"), + "Our shared understanding includes context-sensitive preferences.") + write_knowledge_note(resolve_knowledge_address(env.drive_root, "people/alex"), + "---\ntype: relationship\ntitle: Alex\nsummary: Details belong to their source.\n---\nDetailed source.") + sections = "\n".join(context.build_knowledge_sections(env)) + assert "context-sensitive preferences" in sections + assert "people/alex" in sections and "Alex" in sections + assert "Details belong to their source" not in sections + assert "scope='global'" in sections diff --git a/tests/test_deep_review_book_sources.py b/tests/test_deep_review_book_sources.py new file mode 100644 index 000000000..f94bc341f --- /dev/null +++ b/tests/test_deep_review_book_sources.py @@ -0,0 +1,142 @@ +"""Deep review composes complete books and consumes revision-bound coverage.""" + +import hashlib +import json + +import pytest + +from ouroboros import deep_self_review as deep +from ouroboros.artifacts import read_actor_source_bytes +from ouroboros.reference_books import compose_book, load_reference_book +from tests.test_deep_review_slot import _native_row, _ScriptedLLM, _tool_call + + +def _corpus(root): + files = { + "BIBLE.md": "# Constitution\n\nThe constitutional source.\n", + "docs/CHECKLISTS.md": "# Checklists\n\n## Review\n\nCheck the actual contract.\n", + "worker.py": "def work(): return True\n", + } + for book_id, entry in (("architecture", "docs/ARCHITECTURE.md"), ("development", "docs/DEVELOPMENT.md")): + files[entry] = f"# {book_id.title()}\n\nPurpose of the {book_id} book.\n\n## Chapters\n\n- [Flow]({book_id}/flow.md)\n- [State]({book_id}/state.md)\n" + for name in ("flow", "state"): + files[f"docs/{book_id}/{name}.md"] = ( + f"# {name.title()}\n\nIntroduction to {book_id} {name}.\n\n## Contract\n\nExact {book_id} {name} contract body.\n") + for rel, text in files.items(): + target = root / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text) + return files + + +def _required(root, rel): + raw = (root / rel).read_bytes() + normalized = raw.decode().replace("\r\n", "\n").replace("\r", "\n") + return {"root": "system_repo", "path": rel, "source_revision": hashlib.sha256(raw).hexdigest(), + "complete_sha256": hashlib.sha256(normalized.encode()).hexdigest(), "complete_chars": len(normalized), + "range_basis": "unicode_text_universal_newlines"} + + +def test_packed_chaptered_books_are_complete_once_and_stable_before_atlas(tmp_path, monkeypatch): + monkeypatch.setattr(deep, "get_context_mode", lambda: "max") + monkeypatch.setattr(deep, "_compute_graph_centrality", lambda *a: {}) + prefixes = [] + for name in ("first", "second"): + repo, data = tmp_path / name, tmp_path / f"{name}-data" + files = _corpus(repo) + monkeypatch.setattr(deep, "_dulwich_tracked_paths", lambda *a: (list(files), [])) + monkeypatch.chdir(tmp_path) + pack, stats = deep.build_review_pack(repo, data) + for rel, text in files.items(): + if rel.startswith(("docs/architecture/", "docs/development/")): + assert pack.count(text) == 1 + prefix = "\n".join(f"## Reference book: docs/{book_id.upper()}.md\n\n" + compose_book(load_reference_book(repo, book_id)) + for book_id in ("architecture", "development")) + assert pack.startswith(prefix) + assert str(repo) not in prefix + assert all(view["delivery"] == "full" for view in stats["context_manifest"]["reference_book_views"]) + prefixes.append(prefix) + assert prefixes[0] == prefixes[1] + + +def test_low_packed_architecture_overview_does_not_reinline_omitted_chapters(tmp_path, monkeypatch): + repo, data = tmp_path / "repo", tmp_path / "data" + files = _corpus(repo) + monkeypatch.setattr(deep, "get_context_mode", lambda: "low") + monkeypatch.setattr(deep, "_compute_graph_centrality", lambda *a: {}) + monkeypatch.setattr(deep, "_dulwich_tracked_paths", lambda *a: (list(files), [])) + pack, stats = deep.build_review_pack(repo, data) + assert "Introduction to architecture flow." in pack + assert "Exact architecture flow contract body." not in pack + assert "Exact development flow contract body." in pack + assert "docs/architecture/flow.md" in pack + row = next(r for r in stats["context_manifest"]["coverage"] if r["path"] == "docs/architecture/flow.md") + assert "overview only" in row["reason"] + + +def test_missing_declared_chapter_never_becomes_a_successful_partial_packed_book(tmp_path, monkeypatch): + repo = tmp_path / "repo" + files = _corpus(repo) + (repo / "docs/architecture/state.md").unlink() + monkeypatch.setattr(deep, "_dulwich_tracked_paths", lambda *a: (list(files), [])) + pack, stats = deep.build_review_pack(repo, tmp_path / "data") + assert pack == "" and "book unavailable" in stats["skipped"][0] + assert "state.md" in stats["skipped"][0] + + +def test_retrieving_book_navigation_uses_only_physical_chapter_addresses(tmp_path, monkeypatch): + repo, data = tmp_path / "repo", tmp_path / "data" + _corpus(repo) + # This legacy mapper must not be fed a composed book with invented entrypoint lines. + monkeypatch.setattr(deep, "generate_doc_nav_map", lambda *a, **k: pytest.fail("chaptered book mapped as monolith")) + task, _facts = deep._retrieving_task(repo, data) + assert "Source: `docs/architecture/flow.md`" in task + assert "Source: `docs/development/state.md`" in task + assert "Introduction to architecture state." in task + assert "Exact architecture state contract body." not in task + assert "file you open" in task + assert task.index("Introduction to architecture flow.") < task.index("## Memory") + + +def test_exact_coverage_overrides_complete_legacy_lines_without_rebinding_current_file(tmp_path): + files = _corpus(tmp_path) + row = {**_required(tmp_path, "BIBLE.md"), "status": "incomplete", "covered_chars": 7, + "missing_ranges": [[7, len(files["BIBLE.md"])]]} + usage = {"native_read_coverage": {"status": "incomplete", "sources": [row]}, + "native_tool_receipts": [{"tool": "read_file", "outcome": "executed", "path": "BIBLE.md", + "root": "system_repo", "start_line": 1, "end_line": 99, "total_lines": 99}]} + (tmp_path / "BIBLE.md").write_text("Changed after the observed review source.\n") + detail = deep._native_read_coverage(usage, tmp_path)["BIBLE.md"] + assert detail["state"] == "partial" and detail["covered_chars"] == 7 + assert detail["source_revision"] == row["source_revision"] + assert detail["evidence_basis"] == "source_ranges" and "covered_lines" not in detail + assert deep._delivery_incomplete("native_tool_rounds", usage) == "required_source_coverage_incomplete" + + +def test_legacy_line_evidence_stays_explicit_and_unsent_reads_never_complete_it(tmp_path): + receipt = {"tool": "read_file", "outcome": "executed", "path": "BIBLE.md", "root": "system_repo", + "start_line": 1, "end_line": 5, "total_lines": 5} + usage = {"native_tool_receipts": [receipt]} + detail = deep._native_read_coverage(usage, tmp_path)["BIBLE.md"] + assert detail["state"] == "read" and detail["evidence_basis"] == "legacy_lines" + assert "source_revision" not in detail + receipt["delivered"] = False + assert deep._native_read_coverage(usage, tmp_path)["BIBLE.md"]["state"] == "missing" + + +def test_native_deep_review_reports_exact_chapter_gap_without_changing_the_finding(tmp_path, monkeypatch): + repo, data = tmp_path / "repo", tmp_path / "data" + _corpus(repo) + monkeypatch.setenv("OPENROUTER_API_KEY", "fixture-only") + report = "Read: Constitution.\n\nCRITICAL: a real consumer violates the contract." + llm = _ScriptedLLM([ + {"tool_calls": [_tool_call("read_file", {"path": "BIBLE.md"})]}, {"content": report}]) + required = [_required(repo, "BIBLE.md"), _required(repo, "docs/architecture/flow.md")] + text, usage = deep.run_deep_self_review(repo, data, llm, lambda m: None, + slot=_native_row(), task_id="book-review", required_sources=required) + assert text.endswith(report) + assert usage["deep_review_coverage_basis"] == "source_ranges" + assert usage["native_incomplete"] == "required_source_coverage_incomplete" + assert "docs/architecture/flow.md NOT read" in text + history = json.loads(read_actor_source_bytes(data, "book-review", usage["native_history_source"])) + assert history["required_sources"] == required diff --git a/tests/test_skill_review_packs.py b/tests/test_skill_review_packs.py index ffaf33345..156154f51 100644 --- a/tests/test_skill_review_packs.py +++ b/tests/test_skill_review_packs.py @@ -558,3 +558,73 @@ def test_review_skill_persist_false_does_not_write(tmp_path, monkeypatch): # Default state: nothing written. assert persisted.status == "pending" assert persisted.content_hash == "" + + +@pytest.mark.parametrize("chaptered", [False, True], ids=["legacy", "chapters"]) +def test_skill_governance_is_complete_and_stable_across_checkouts(tmp_path, monkeypatch, chaptered): + import ouroboros.skill_review_prompt as prompt_owner + + corpus = {"BIBLE.md": "# Constitution\n\nConstitutional body.\n"} + for book_id, entrypoint in ( + ("architecture", "docs/ARCHITECTURE.md"), + ("development", "docs/DEVELOPMENT.md"), + ): + corpus[entrypoint] = f"# {book_id.title()}\n\nThe {book_id} introduction.\n" + if chaptered: + corpus[entrypoint] += ( + f"\n## Chapters\n\n- [First]({book_id}/first.md)\n" + f"- [Second][second]\n\n[second]: {book_id}/second.md\n" + ) + for name in ("first", "second"): + corpus[f"docs/{book_id}/{name}.md"] = ( + f"# {name.title()}\n\nThe {book_id} {name} introduction.\n" + f"\n## Details\n\nThe complete {book_id} {name} body.\n" + ) + else: + corpus[entrypoint] += f"\n## Details\n\nThe complete {book_id} monolith.\n" + + monkeypatch.setattr(prompt_owner, "load_checklist_section", lambda _: "Checklist body.") + monkeypatch.setattr(prompt_owner, "build_skill_host_context", lambda _: "Host contract body.") + monkeypatch.chdir(tmp_path) + stable_prefixes = [] + for name in ("checkout-one", "checkout-two"): + root = tmp_path / name + for path, text in corpus.items(): + target = root / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + monkeypatch.setattr(prompt_owner, "_REPO_ROOT", root) + prompt, boundary = prompt_owner._build_review_prompt( + name, root / "external-skill", "{}", name, f"PAYLOAD-{name}", + ) + stable = prompt[:boundary] + stable_prefixes.append(stable) + for text in corpus.values(): + assert stable.count(text) == 1 + assert str(root) not in stable + assert "PAYLOAD-" not in stable + assert "Skill gates do not collapse" in stable + assert "invariant 11" not in stable + assert prompt[boundary:].startswith("## Skill identity") + assert f"PAYLOAD-{name}" in prompt[boundary:] + assert stable_prefixes[0] == stable_prefixes[1] + + +@pytest.mark.parametrize("chapter_body", [None, "# Chapter\n\n## Missing introduction\n\nBody.\n"]) +def test_skill_governance_discloses_unavailable_book_without_partial_body(tmp_path, chapter_body): + from ouroboros.skill_review_prompt import _load_governance_artifact + + docs = tmp_path / "docs" + docs.mkdir() + (docs / "ARCHITECTURE.md").write_text( + "# Architecture\n\nEntrypoint body must not stand in for the whole book.\n" + "\n## Chapters\n\n- [Runtime](architecture/runtime.md)\n", + encoding="utf-8", + ) + if chapter_body is not None: + (docs / "architecture").mkdir() + (docs / "architecture" / "runtime.md").write_text(chapter_body, encoding="utf-8") + text = _load_governance_artifact(tmp_path, "docs/ARCHITECTURE.md") + assert "OMISSION" in text and "docs/ARCHITECTURE.md" in text + assert "runtime.md" in text + assert "Entrypoint body must not stand in" not in text diff --git a/tests/test_skill_review_passes.py b/tests/test_skill_review_passes.py index 3615391fe..082847527 100644 --- a/tests/test_skill_review_passes.py +++ b/tests/test_skill_review_passes.py @@ -121,6 +121,10 @@ def test_chunked_passes_keep_the_full_row_plan_and_exact_session_evidence(): assert "STABLE::" not in call["session_task"] assert all(path in call["session_task"] for path in ( "BIBLE.md", "docs/CHECKLISTS.md", "ouroboros/contracts/plugin_api.py")) + assert "every physical chapter" in call["session_task"] + assert "`Chapters` membership list in full, in the order you choose" in call["session_task"] + assert "legacy monolith without `Chapters`" in call["session_task"] + assert "physical file paths and file-local lines" in call["session_task"] assert f"PART {idx} of 2" in call["session_task"]